Terraform Version Control
Introduction
When working with Terraform to manage your infrastructure as code, version control becomes a critical component of your workflow. Version control allows you to track changes, collaborate with team members, and maintain a history of your infrastructure evolution. In this guide, we'll explore best practices for implementing version control for your Terraform code, ensuring that your infrastructure deployments remain consistent, traceable, and reliable.
Why Version Control for Terraform?
Infrastructure as Code (IaC) with Terraform means your infrastructure is defined in code files. Like application code, these files benefit from version control for several reasons:
- History tracking: Maintain a complete history of infrastructure changes
- Collaboration: Enable multiple team members to work on infrastructure safely
- Rollback capability: Easily revert to previous working states if issues occur
- Auditing: Track who made what changes and when
- CI/CD integration: Enable automated testing and deployment pipelines
Getting Started with Terraform Version Control
Basic Git Setup for Terraform Projects
The most common version control system for Terraform is Git. Here's how to set up a basic Terraform project with Git:
# Initialize a new directory
mkdir terraform-project
cd terraform-project
# Initialize Git repository
git init
# Create basic Terraform files
touch main.tf variables.tf outputs.tf
# Initialize Terraform
terraform init
# Add files to Git
git add .
git commit -m "Initial Terraform project setup"
Repository Structure
A well-organized repository structure helps maintain clarity as your Terraform codebase grows. Here's a recommended structure:
terraform-project/
├── README.md
├── main.tf
├── variables.tf
├── outputs.tf
├── terraform.tfvars.example
├── .gitignore
├��─ modules/
│ ├── networking/
│ │ ├── main.tf
│ │ ├── variables.tf
│ │ └── outputs.tf
│ └── compute/
│ ├── main.tf
│ ├── variables.tf
│ └── outputs.tf
└── environments/
├── dev/
│ ├── main.tf
│ └── terraform.tfvars
├── staging/
│ ├── main.tf
│ └── terraform.tfvars
└── prod/
├── main.tf
└── terraform.tfvars
Essential .gitignore for Terraform
Properly configuring your .gitignore
file is crucial to avoid committing sensitive or unnecessary files. Here's a starter .gitignore
for Terraform projects:
# Local .terraform directories
**/.terraform/*
# .tfstate files
*.tfstate
*.tfstate.*
# Crash log files
crash.log
# Exclude all .tfvars files, which are likely to contain sensitive data
*.tfvars
!*.tfvars.example
# Ignore override files as they're usually used for local dev
override.tf
override.tf.json
*_override.tf
*_override.tf.json
# Ignore CLI configuration files
.terraformrc
terraform.rc
# Ignore lock files if you're sharing modules
# .terraform.lock.hcl
Version Control Best Practices for Terraform
1. Lock Your Terraform Version
Ensure consistent Terraform behavior by locking the version in your configuration:
terraform {
required_version = "~> 1.5.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 4.16.0"
}
}
}
2. Commit the State Lock File
The .terraform.lock.hcl
file should be committed to your version control system to ensure consistent provider versions:
git add .terraform.lock.hcl
git commit -m "Add provider lock file"
3. Use Branches for Changes
Follow a branching strategy for infrastructure changes:
# Create a feature branch
git checkout -b feature/add-rds-database
# Make your Terraform changes
# ...
# Commit changes
git add .
git commit -m "Add RDS database resources"
# Push branch and create a pull request
git push origin feature/add-rds-database
4. Pull Request Workflow
Use pull requests to review infrastructure changes before they're applied:
- Create a branch for your changes
- Make your Terraform code changes
- Run
terraform fmt
to standardize code formatting - Run
terraform validate
to check for errors - Create a pull request with your changes
- Include a
terraform plan
output in your PR description - Have team members review the changes
- Merge only after approval
Remote State Management
Managing Terraform state files is a crucial aspect of version control. The state file should not be directly stored in your Git repository.
Setting Up Remote State
Configure a remote backend to store your state file:
terraform {
backend "s3" {
bucket = "my-terraform-state"
key = "prod/terraform.tfstate"
region = "us-west-2"
encrypt = true
dynamodb_table = "terraform-locks"
}
}
This example uses AWS S3 with DynamoDB for state locking, but Terraform supports many backend types including:
- Azure Storage
- Google Cloud Storage
- HashiCorp Terraform Cloud
- HashiCorp Consul
State Locking
State locking prevents concurrent modifications, which could corrupt your state file:
Collaboration Workflow
Here's a step-by-step workflow for collaborating on Terraform code:
1. Clone the Repository
git clone https://github.com/organization/terraform-project.git
cd terraform-project
2. Create a New Branch
git checkout -b feature/add-vpc-peering
3. Make Changes and Test Locally
# Edit Terraform files
vim main.tf
# Format and validate
terraform fmt
terraform validate
# See what changes would be made
terraform plan
4. Commit and Push
git add .
git commit -m "Add VPC peering configuration"
git push origin feature/add-vpc-peering
5. Create a Pull Request
Create a pull request in your Git hosting platform (GitHub, GitLab, etc.) that includes:
- Description of the changes
- Output from
terraform plan
- Any related issues or tickets
6. Review and Merge
After peer review and approval, merge the changes to the main branch.
7. Apply Changes
git checkout main
git pull
terraform apply
Advanced Version Control Strategies
Using Git Tags for Releases
Tag significant infrastructure states for easy reference:
# After a successful deployment
git tag -a v1.2.0 -m "Release v1.2.0 - Added RDS instances"
git push origin v1.2.0
Module Versioning
When creating reusable modules, implement semantic versioning:
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "3.14.0"
# Module configuration
}
For internal modules stored in Git:
module "custom_security" {
source = "git::https://github.com/org/terraform-modules.git//security?ref=v1.0.0"
}
Managing Sensitive Data
Use tools like HashiCorp Vault or AWS Secrets Manager for sensitive data rather than storing it in your repository:
data "vault_generic_secret" "db_credentials" {
path = "secret/database/credentials"
}
resource "aws_db_instance" "database" {
username = data.vault_generic_secret.db_credentials.data["username"]
password = data.vault_generic_secret.db_credentials.data["password"]
# Other configuration
}
Integrating with CI/CD
GitHub Actions Example
Here's a basic GitHub Actions workflow for Terraform:
name: Terraform CI
on:
pull_request:
branches: [ main ]
jobs:
terraform:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Terraform
uses: hashicorp/setup-terraform@v2
with:
terraform_version: 1.5.0
- name: Terraform Init
run: terraform init
- name: Terraform Format
run: terraform fmt -check
- name: Terraform Validate
run: terraform validate
- name: Terraform Plan
run: terraform plan -no-color
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
Atlantis for Pull Request Automation
Atlantis is a tool that automates Terraform workflows on pull requests:
# atlantis.yaml
version: 3
projects:
- name: dev
dir: environments/dev
workflow: terraform
autoplan:
when_modified: ["*.tf", "../modules/**/*.tf"]
enabled: true
- name: prod
dir: environments/prod
workflow: terraform
autoplan:
when_modified: ["*.tf", "../modules/**/*.tf"]
enabled: true
Common Challenges and Solutions
Challenge: State Conflicts
Solution: Use remote state with locking and follow a strict workflow where only CI/CD systems apply changes in shared environments.
Challenge: Managing Multiple Environments
Solution: Use separate state files for each environment and consider tools like Terragrunt for DRY configurations:
├── terragrunt.hcl
├── dev/
│ ├── terragrunt.hcl
│ └── database/
│ └── terragrunt.hcl
├── staging/
│ ├── terragrunt.hcl
│ └── database/
│ └── terragrunt.hcl
└── prod/
├── terragrunt.hcl
└── database/
└── terragrunt.hcl
Challenge: Code Reviews for Infrastructure
Solution: Generate and include detailed plans in pull requests and use tools like Infracost to show cost implications of changes.
Summary
Implementing proper version control for your Terraform code is essential for maintaining consistent, reliable infrastructure deployments. By following the best practices outlined in this guide, you can:
- Track the history of your infrastructure changes
- Collaborate effectively with team members
- Rollback to previous states when needed
- Implement automated testing and deployment pipelines
- Maintain consistent environments
Remember that version control is not just about using Git—it's about implementing a complete workflow that includes code organization, state management, collaboration practices, and CI/CD integration.
Additional Resources
- Terraform Documentation on State
- HashiCorp Learn: Terraform Modules
- Terraform Cloud and Enterprise
- Terragrunt Documentation
Practice Exercises
- Set up a basic Terraform project with the recommended repository structure
- Configure remote state using an S3 bucket and DynamoDB table
- Create a GitHub Actions workflow for Terraform validation
- Implement a module versioning strategy for a simple infrastructure component
- Practice the full collaboration workflow with a teammate
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)