Introduction
Managing infrastructure resources with code is a common goal.
When it comes time to use Terraform, you might find yourself wondering about the best directory structure and how to organize your workspaces.
In this article, I’ll share my personal best practices for a simple and efficient Terraform setup for small organizations.
Premises
This article is targeted at individuals or teams who fit the following criteria:
- Want to manage AWS resources using Terraform
- Have a small team (1-2 people) that can handle all infrastructure changes, where minor conflicts aren’t a major concern
- Perfer to keep costs low, so instead of using Terraform Cloud, plan to manage the Terraform state using S3 on AWS.
On the other hand, this setup may not be suitable for larger organizations considering Terraform adoption.
Here are the reasons why:
- In larger teams, frequent Terraform Apply operations can lead to conflicts aand unintended resource changes.
- Currently, Terraform Apply can lock the state file on S3, but…
- When CI/CD is set up, Terraform Apply is usually executed based on the state of the main branch.
- If multiple member merge into the main branch, the risk of unintended changes increases.
- In such cases, it’s better to move to Terraform Cloud for managing the state.
Overview
In conclusion, I recommend the following structure.
link: https://github.com/gonkunkun/terraform-template
Key points of this structure are as follows:
- The terraform state file is not managed by Terraform itself, but by CloudFormation
- This is a given.
It is unacceptale to delete the Terraform state file from Terraformm itself.
- This is a given.
- The environment consists of two envs: staging and production.
- Each environment’s state file is managed separately.
Do not separate if files for each env.
- This is likely the most debatable point.
- By not separating tf file for each environment, the following benefits arise:
- Minimize the differences between stg and prd env.
- If there are any environment-specific differences, they will be visible at a glance in the code.
- Prevents double maintenance of code.
- However, there are some drawbacks as well:
- If there are changes that should only be applied to stg or prd, it requires extra effort.
- Specifically, you will need to use conditional branching or
count
to apply changes only to one environment at a time.
Details
Directory Structure
The directory structure is as follows.
~/D/g/terraform-template ❯❯❯ tree . main
.
├── README.md
├── cloudformation
│ └── S3.yaml
└── workloads
├── data.tf
├── envs
│ ├── prd
│ │ ├── backend.hcl
│ │ └── terraform.tfvars
│ └── stg
│ ├── backend.hcl
│ └── terraform.tfvars
├── locals.tf
├── main.tf
├── providers.tf
├── terraform.tfvars
├── variables.tf
├── versions.tf
└── vpc.tf
Let’s now look at some key points.
Location of State Files.
Ideally, I would like to manage this setting via CI/CD, but this hasn’t been considered at this stage.
After configuring the bucket name and a user with Terraform permissions, we will create the S3 bucket using CloufFormation.
Resources:
StateBucketStg:
Type: "AWS::S3::Bucket"
Properties:
BucketName: "xxxxx-terraform-state-stg"
VersioningConfiguration:
Status: Enabled
PublicAccessBlockConfiguration:
BlockPublicAcls: true
BlockPublicPolicy: true
IgnorePublicAcls: true
RestrictPublicBuckets: true
BucketEncryption:
ServerSideEncryptionConfiguration:
- ServerSideEncryptionByDefault:
SSEAlgorithm: AES256
StateFileBucketPolicyForStg:
Type: "AWS::S3::BucketPolicy"
Properties:
Bucket: !Ref StateBucketStg
PolicyDocument:
Version: "2012-10-17"
Statement:
- Sid: "AllowSpecificIAMUser"
Effect: "Allow"
Principal:
AWS: "arn:aws:iam::xxxxxxxxx:user/terraform"
Action:
- "s3:GetObject"
- "s3:PutObject"
- "s3:ListBucket"
- "s3:DeleteObject"
Resource:
- !Sub "arn:aws:s3:::${StateBucketStg}/*"
- !Sub "arn:aws:s3:::${StateBucketStg}"
TerraformPlanOnlyPolicyForStg:
Type: "AWS::IAM::Policy"
Properties:
PolicyName: "TerraformPlanOnlyPolicy"
Roles:
- !Ref TerraformUserRole
PolicyDocument:
Version: "2012-10-17"
Statement:
- Sid: "AllowPlanActions"
Effect: "Allow"
Action:
- "s3:GetObject"
- "s3:ListBucket"
Resource:
- !Sub "arn:aws:s3:::${StateBucket}"
- !Sub "arn:aws:s3:::${StateBucket}/*"
- Sid: "DenyApplyAndDestroy"
Effect: "Deny"
Action:
- "s3:PutObject"
- "s3:DeleteObject"
Resource:
- !Sub "arn:aws:s3:::${StateBucket}"
- !Sub "arn:aws:s3:::${StateBucket}/*"
StateBucketPrd:
Type: "AWS::S3::Bucket"
Properties:
BucketName: "xxxxx-terraform-state-prd"
VersioningConfiguration:
Status: Enabled
PublicAccessBlockConfiguration:
BlockPublicAcls: true
BlockPublicPolicy: true
IgnorePublicAcls: true
RestrictPublicBuckets: true
BucketEncryption:
ServerSideEncryptionConfiguration:
- ServerSideEncryptionByDefault:
SSEAlgorithm: AES256
StateFileBucketPolicyForPrd:
Type: "AWS::S3::BucketPolicy"
Properties:
Bucket: !Ref StateBucketPrd
PolicyDocument:
Version: "2012-10-17"
Statement:
- Sid: "AllowSpecificIAMUser"
Effect: "Allow"
Principal:
AWS: "arn:aws:iam::xxxxxxxxx:user/terraform"
Action:
- "s3:GetObject"
- "s3:PutObject"
- "s3:ListBucket"
- "s3:DeleteObject"
Resource:
- !Sub "arn:aws:s3:::${StateBucketPrd}/*"
- !Sub "arn:aws:s3:::${StateBucketPrd}"
ref: https://github.com/gonkunkun/terraform-template/blob/main/cloudformation/S3.yaml
Backend Configuration (State File Storage Location)
The backend configuration is specified for each environment.
bucket = "xxxxx-terraform-state-stg"
region = "ap-northeast-1"
key = "stg/terraform.tfstate"
use_lockfile = true
encrypt = true
stg: https://github.com/gonkunkun/terraform-template/blob/main/workloads/envs/stg/backend.hcl
bucket = "xxxxx-terraform-state-prd"
region = "ap-northeast-1"
key = "prd/terraform.tfstate"
use_lockfile = true
encrypt = true
prd: https://github.com/gonkunkun/terraform-template/blob/main/workloads/envs/prd/backend.hcl
It’s great that the need for state locking with DynamoDB has been removed.
ref: https://github.com/hashicorp/terraform/blob/5279e432611752a9bcba27baaf657a3305c226f8/website/docs/language/backend/s3.mdx#state-locking
By the way, when running plan or apply, the command will be as follows.
# In the case of stg
## Specify the backend
terraform init -backend-config="envs/stg/backend.hcl"
## Plan
terraform plan -var="env=stg"
# In the case of prd
## Specify the backend
terraform init -backend-config="envs/prd/backend.hcl"
## Plan
terraform plan -var="env=prd"
Defining Resources for stg and prd Env
Define variables for each environment and pass the to the Terraform resources.
locals {
cidr_block = {
stg : "10.10.0.0/16",
prd : "10.20.0.0/16",
}[var.env]
}
#trivy:ignore:AVD-AWS-0178
resource "aws_vpc" "sample" {
cidr_block = local.cidr_block
tags = {
"Name" = "sample-${var.env}-vpc"
}
tags_all = {
"Name" = "sample-${var.env}-vpc"
}
lifecycle {
prevent_destroy = true
}
}
ref: https://github.com/gonkunkun/terraform-template/blob/main/workloads/vpc.tf
Setup (Up to Terraform Plan)
Now, let’s go over the setup process to be able to run plan from your local environment.
Prerequisites
Plese complete the following tasks in advance:
- Create the target AWS account
- Create the S3 bucket for state management
- Create an AIM user or role for Terraform exectuon/ Configure the profile on your local environment
Configurations
Let’s clone the repository.
# Install Terraform (using brew)
brew install terraform
# Clone the repository
git clone https://github.com/gonkunkun/terraform-template
Set the correct backend S3 bucket name.
stg: https://github.com/gonkunkun/terraform-template/blob/main/workloads/envs/stg/backend.hcl
prd: https://github.com/gonkunkun/terraform-template/blob/main/workloads/envs/prd/backend.hcl
Set the region according to your environment.
https://github.com/gonkunkun/terraform-template/blob/main/workloads/providers.tf
Select the backend and initialize.
(This should generate the state file on S3 at this point.)
# Initialize the provider (for the staging environment)
cd ./terraform-template/workloads
terraform init -backend-config="envs/stg/backend.hcl"
Then, confirm that you can run plan.
terraform plan -var="env=stg"
Conclusion
In this article, we covered the directory structure and initial setup for Terraform.
I’m sure you might also be interested in CI/CD setup, so I will cover that in a separate article.
Thank you for reading this far.