We assume you have followed Stacks' installation instructions.
On your working directory, create the following base directory structure:
|-- environments/
`-- stacks/
An environment is a unique context where your Terraform stacks will be deployed.
Environments are represented by a subdirectory of environments/
and their name must conform to this regular expression: ^[a-zA-Z0-9-]{,254}$
.
Let's create your first environment:
|-- environments/
| `-- development/
| `-- env.tfvars # must be named "env.tfvars"
`-- stacks/
This creates a development
environment.
The env.tfvars
file should contain any environment-specific variables to be shared accross stacks deployed in this environment.
Typically these are provider settings like IAM roles to assume, or the bucket where you want this environment's state to be stored.
A stack is the collection of a base and its layers. A stack base is input code that translates to a Terraform root module. A stack layer is an instance of its stack's base, on the environment it maps to.
Stacks are represented by a subdirectory of stacks/
and their name must conform to this regular expression: ^[a-zA-Z0-9-]{,254}$
.
A stack's base is stored under its base/
subdirectory.
A stack's layers are stored under its layers/
subdirectory.
Let's create your first stack:
|-- environments/
| `-- development/
| `-- env.tfvars
`-- stacks/
`-- vpc/
|-- base/
| |-- backend.tf
| `-- main.tf # you can do any code structure you want here
|-- layers/
| `-- development/ # must be named after an existing environment
| `-- layer.tfvars # can be named <anything>.tfvars
`-- stack.tfvars # can be named <anything>.tfvars
This creates a vpc
stack, with its base and one layer on the development
environment we created earlier.
The variables in stack.tfvars
will be injected in all layers of the stack.
You can override them on a per-layer basis through the layer.tfvars
file.
You can have multiple *.tfvars
files.
Now let's deploy your newly created layer:
cd stacks/vpc/layers/development
stacks terraform apply
You'll notice a stacks.out
directory was created.
This is where the output code is stored for Terraform to consume, feel free to inspect it.
Make sure to exclude stacks.out
from version control (e.g. in Git add it to .gitignore
).
Now that we've validated our base in our development
environment, let's deploy to production now.
To do that, we'll need a new production
environment, and a new production
layer of our vpc
stack:
|-- environments/
| |-- development/
| | `-- env.tfvars
| `-- production/
| `-- env.tfvars
`-- stacks/
`-- vpc/
|-- base/
| |-- backend.tf
| `-- main.tf
|-- layers/
| |-- development/
| | `-- layer.tfvars
| `-- production/
| `-- layer.tfvars
`-- stack.tfvars
Then repeat the deployment process for our production
layer:
cd stacks/vpc/layers/production
stacks terraform apply
Let's now deploy something on our VPC:
|-- environments/
| |-- development/
| | `-- env.tfvars
| `-- production/
| `-- env.tfvars
`-- stacks/
|-- ec2/
| |-- base/
| | |-- backend.tf
| | `-- main.tf
| |-- layers/
| | |-- development/
| | | `-- layer.tfvars
| | `-- production/
| | `-- layer.tfvars
| `-- stack.tfvars
`-- vpc/
|-- base/
| |-- backend.tf
| `-- main.tf
|-- layers/
| |-- development/
| | `-- layer.tfvars
| `-- production/
| `-- layer.tfvars
`-- stack.tfvars
And stacks terraform apply
accordingly.
You've surely realized by now that there are a number of things we're duplicating accross environments, stacks and layers.
For starters, you can remove the need to define backend.tf
on every stack by moving it to stacks/
:
|-- environments/
`-- stacks/
|-- backend.tf # (Step 3) Make it a Jinja template and put it here for all stacks to share.
|-- ec2/
| |-- base/ # (Step 1) Remove backend.tf here.
| | `-- main.tf
| |-- layers/
| `-- stack.tfvars
`-- vpc/
|-- base/ # (Step 2) ...and here.
| `-- main.tf
|-- layers/
`-- stack.tfvars
If you're doing things the Terraform way, I'm sure you're also using a remote state data source in the ec2
stack to pull the vpc_id
output you exported in the vpc
stack. And I'm confident that has at least a couple hard-coded settings as to where the state is stored and how to access it.
Why do that when you could simply use {{ output("vpc_id", stack="vpc") }}
wherever you need the VPC ID in your ec2
stack? The output
Jinja filter will figure out the value and inject it for you.
Better yet, if you don't even want to define an output
in your vpc
stack you can use {{ resource("aws_vpc.main", stack="vpc")["id"] }}
instead, which will look up the resource by address in the vpc
stack's state and inject it just like before.
There's surely some other things you're duplicating either between env.tfvars
or stack.tfvars
files.
For those, you can create common *.tfvars
files in stacks/
too, much like with global *.tf
Terraform code:
|-- environments/
`-- stacks/
|-- backend.tf
|-- globals.tfvars # put common variables here
|-- ec2/
`-- vpc/
Hopefully you're starting to see how Stacks can drastically reduce the amount of code you need to write for Terraform to work.
From templating common code and boilerplate away both globally and per-stack, to cascading variable scopes that allow defining values once without losing the ability to override per-stack or even per-layer, plus some extra goodies like the variable
, output
and resource
Jinja filters that completely remove the need for hard-coded remote state data sources to communicate states together.
Check out the Reference section of these docs for an exhaustive list of Stacks built-in features along with usage examples, plus other features not native to Stacks but that you can build on top of it.