DevOps
Infrastructure as Code: Best Practices with Terraform and CloudFormation
Learn best practices for implementing Infrastructure as Code using Terraform and AWS CloudFormation.
January 21, 2024
DevHub Team
5 min read
Infrastructure as Code: Best Practices with Terraform and CloudFormation
Infrastructure as Code (IaC) is a fundamental DevOps practice that enables teams to manage and provision infrastructure through code rather than manual processes.
Understanding Infrastructure as Code
Key Benefits
- Version Control
- Reproducibility
- Consistency
- Automation
- Documentation
Terraform Fundamentals
Basic Structure
# main.tf terraform { required_providers { aws = { source = "hashicorp/aws" version = "~> 4.0" } } } provider "aws" { region = "us-west-2" } resource "aws_instance" "web" { ami = "ami-0c55b159cbfafe1f0" instance_type = "t2.micro" tags = { Name = "WebServer" Environment = "Production" } }
Variables and Outputs
# variables.tf variable "environment" { type = string description = "Environment name" default = "development" } variable "instance_type" { type = string description = "EC2 instance type" default = "t2.micro" } # outputs.tf output "instance_ip" { value = aws_instance.web.public_ip description = "Public IP of the web server" }
AWS CloudFormation
Template Structure
AWSTemplateFormatVersion: '2010-09-09' Description: 'Web Server Infrastructure' Parameters: EnvironmentName: Type: String Default: Development Resources: WebServerInstance: Type: AWS::EC2::Instance Properties: InstanceType: t2.micro ImageId: ami-0c55b159cbfafe1f0 Tags: - Key: Name Value: WebServer - Key: Environment Value: !Ref EnvironmentName Outputs: WebServerIP: Description: Public IP of the web server Value: !GetAtt WebServerInstance.PublicIp
Infrastructure Modules
Terraform Modules
# modules/vpc/main.tf module "vpc" { source = "terraform-aws-modules/vpc/aws" name = var.vpc_name cidr = var.vpc_cidr azs = var.availability_zones private_subnets = var.private_subnet_cidrs public_subnets = var.public_subnet_cidrs enable_nat_gateway = true enable_vpn_gateway = false tags = { Environment = var.environment Terraform = "true" } }
CloudFormation Nested Stacks
Resources: NetworkStack: Type: AWS::CloudFormation::Stack Properties: TemplateURL: https://s3.amazonaws.com/templates/network.yaml Parameters: EnvironmentName: !Ref EnvironmentName ApplicationStack: Type: AWS::CloudFormation::Stack Properties: TemplateURL: https://s3.amazonaws.com/templates/application.yaml Parameters: VpcId: !GetAtt NetworkStack.Outputs.VpcId
State Management
Terraform State
# backend.tf terraform { backend "s3" { bucket = "terraform-state-bucket" key = "prod/terraform.tfstate" region = "us-west-2" dynamodb_table = "terraform-locks" encrypt = true } }
State Locking
resource "aws_dynamodb_table" "terraform_locks" { name = "terraform-locks" billing_mode = "PAY_PER_REQUEST" hash_key = "LockID" attribute { name = "LockID" type = "S" } }
Security Best Practices
1. Secrets Management
# Using AWS Secrets Manager data "aws_secretsmanager_secret_version" "db_password" { secret_id = "prod/db/password" } resource "aws_db_instance" "database" { password = data.aws_secretsmanager_secret_version.db_password.secret_string # Other configuration... }
2. IAM Policies
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "s3:ListBucket", "s3:GetObject", "s3:PutObject" ], "Resource": [ "arn:aws:s3:::terraform-state-bucket", "arn:aws:s3:::terraform-state-bucket/*" ] } ] }
Testing Infrastructure Code
1. Terraform Testing
# test/main_test.go package test import ( "testing" "github.com/gruntwork-io/terratest/modules/terraform" "github.com/stretchr/testify/assert" ) func TestTerraformWebServer(t *testing.T) { terraformOptions := &terraform.Options{ TerraformDir: "../", Vars: map[string]interface{}{ "environment": "test", }, } defer terraform.Destroy(t, terraformOptions) terraform.InitAndApply(t, terraformOptions) instanceID := terraform.Output(t, terraformOptions, "instance_id") assert.NotEmpty(t, instanceID) }
2. CloudFormation Testing
# test-template.yaml AWSTemplateFormatVersion: '2010-09-09' Transform: AWS::Serverless-2016-10-31 Resources: TestFunction: Type: AWS::Serverless::Function Properties: Handler: index.handler Runtime: nodejs14.x CodeUri: ./test Events: TestApi: Type: Api Properties: Path: /test Method: get
Continuous Integration/Deployment
1. GitHub Actions Workflow
name: Terraform CI on: push: branches: [ main ] pull_request: branches: [ main ] jobs: terraform: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Setup Terraform uses: hashicorp/setup-terraform@v1 - name: Terraform Init run: terraform init - name: Terraform Format run: terraform fmt -check - name: Terraform Plan run: terraform plan - name: Terraform Apply if: github.ref == 'refs/heads/main' run: terraform apply -auto-approve
2. Jenkins Pipeline
pipeline { agent any environment { AWS_CREDENTIALS = credentials('aws-credentials') } stages { stage('Terraform Init') { steps { sh 'terraform init' } } stage('Terraform Plan') { steps { sh 'terraform plan -out=tfplan' } } stage('Terraform Apply') { when { branch 'main' } steps { sh 'terraform apply -auto-approve tfplan' } } } }
Cost Management
1. Cost Estimation
# Using Infracost provider "aws" { region = "us-west-2" } resource "aws_instance" "web" { instance_type = "t2.micro" # Other configuration... # Monthly cost: ~$8.50 } resource "aws_s3_bucket" "storage" { bucket = "my-app-storage" # Monthly cost: ~$0.023 per GB }
2. Resource Tagging
locals { common_tags = { Environment = var.environment Project = var.project_name Owner = var.team CostCenter = var.cost_center } } resource "aws_instance" "web" { # ... other configuration ... tags = merge( local.common_tags, { Name = "WebServer" } ) }
Disaster Recovery
1. Backup Configuration
resource "aws_backup_plan" "example" { name = "tf_example_backup_plan" rule { rule_name = "tf_example_backup_rule" target_vault_name = aws_backup_vault.example.name schedule = "cron(0 12 * * ? *)" lifecycle { delete_after = 14 } } } resource "aws_backup_selection" "example" { name = "tf_example_backup_selection" plan_id = aws_backup_plan.example.id iam_role_arn = aws_iam_role.example.arn resources = [ aws_db_instance.example.arn, aws_ebs_volume.example.arn ] }
2. Multi-Region Deployment
provider "aws" { alias = "primary" region = "us-west-2" } provider "aws" { alias = "dr" region = "us-east-1" } module "primary" { source = "./application" providers = { aws = aws.primary } } module "dr" { source = "./application" providers = { aws = aws.dr } }
Conclusion
Implementing Infrastructure as Code requires:
- Proper planning and architecture
- Version control integration
- Automated testing and deployment
- Security best practices
- Cost optimization
Remember to:
- Keep code DRY and modular
- Use version control
- Implement proper testing
- Monitor costs
- Document everything
Additional Resources
IaC
Terraform
CloudFormation
AWS