Goal
The goal is to bootstrap an AWS environment using Terraform.

Setting up an AWS account
I won’t lay out the steps that I took as I deem it out of scope for this lab, but in general when creating an AWS account for a new project:
- Don’t reuse an existing AWS account. Create a fresh one to minimize tha chance of existing configuration interfering with the new resources.
- Once the root user is created, enable MFA and create an IAM admin user. This will be the only time you’d need to log in using the root user.
- Be sure to create budget to prevent surprising bills.
Once the account is set up, I created two sets of IAM credentials: one for my local development and one for Github Actions as secrets.
Bootstrapping the Terraform Resources
Terraform works by tracking your environment’s state in a state file. For any serious collaborative effort, this state should be hosted centrally and have state locking enabled. For AWS, this is done by creating an S3 bucket and a DynamoDB table for state storage and locking, respectively.
This poses a chicken-and-egg problem: how do we manage the S3 bucket and DynamoDB table through Terraform, if Terraform requires these resources to begin with?
There are a handful of options: ClickOps the resources, script it out with the AWS CLI, or use CloudFormation. But I find it intuitive to keep everything in Terraform and use the trussworks/bootstrap/aws
module. In a nutshell, this will create the S3 bucket and DynamoDB table, and the ensuing state file is supposed to be stored in the repository.
The non-bootstrapped Terraform resources will use the bootstraped resources as its backend, so no state file is necessary.
Of note is that the bootstrap Terraform code is a submodule. This is the directory structure:
terraform
|-- bootstrap
| |-- locals.tf
| |-- main.tf
| |-- outputs.tf
| |-- terraform.tfstate
Creating an EC2 instance
With the bootstrap step done, I can now create the actual AWS resources as the top-level module.
terraform
|-- bootstrap
| |-- locals.tf
| |-- main.tf
| |-- outputs.tf
| |-- terraform.tfstate
|-- locals.tf
|-- main.tf
|-- ec2.tf
Given that the purpose of the EC2 instance is to demonstrate that Terraform works, a very simple definition should suffice:
data "aws_ami" "ubuntu" {
most_recent = true
filter {
name = "name"
values = ["ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-*"]
}
filter {
name = "virtualization-type"
values = ["hvm"]
}
owners = ["099720109477"] # Canonical
}
resource "aws_instance" "web_server" {
ami = data.aws_ami.ubuntu.id
instance_type = "t3.micro"
}
Scripting the Terraform commands
Instead of manually typing the terraform
commands in the Makefile
, I created a handful of scripts to simplify the process (and get shell autocomplete). As an example, here’s a script that runs plan and apply:
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR=$(pwd)
TF_ROOT_DIR="${ROOT_DIR}/terraform"
cd "$TF_ROOT_DIR"
terraform apply -auto-approve
The scripts live under the Terraform directory:
terraform
|-- bootstrap
| |-- locals.tf
| |-- main.tf
| |-- outputs.tf
| |-- terraform.tfstate
| `-- terraform.tfstate.backup
|-- ec2.tf
|-- locals.tf
|-- main.tf
|-- outputs.tf
`-- scripts
|-- bootstrap.sh
|-- destroy.sh
|-- format.sh
|-- init.sh
|-- lint.sh
`-- plan-and-apply.sh
Creating a Github Action on merge
With the scripts and the Makefile
targets in place, creating the action is a straightforward procedure:
name: Deploy
on:
push:
branches: [ "master" ]
jobs:
deploy:
name: Run deploy command
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: hashicorp/setup-terraform@v3
with:
terraform_wrapper: false
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: us-east-1
- run: make deploy
We now have a deployment script runnable via local invocation and on merge to master
!
Conclusion
In this lab, we bootstrap supporting resources that Terraform needs to function and create a basic EC2 instance to validate our Terraform usage.
The source is available at the feature/010-bootstrap-terraform
branch.