Infrastructure as Code: Best Practices with Terraform and CloudFormation
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

  1. Version Control
  2. Reproducibility
  3. Consistency
  4. Automation
  5. 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:

  1. Proper planning and architecture
  2. Version control integration
  3. Automated testing and deployment
  4. Security best practices
  5. Cost optimization

Remember to:

  • Keep code DRY and modular
  • Use version control
  • Implement proper testing
  • Monitor costs
  • Document everything

Additional Resources

  1. Terraform Documentation
  2. AWS CloudFormation User Guide
  3. Infrastructure as Code Best Practices
  4. Terraform Best Practices
IaC
Terraform
CloudFormation
AWS