Managing AWS CloudFormation with Terraform

A comprehensive guide to managing AWS CloudFormation stacks and resources using Terraform Infrastructure as Code

Managing AWS CloudFormation with Terraform

AWS CloudFormation allows you to create and manage AWS infrastructure deployments predictably and repeatedly. This guide shows how to manage CloudFormation using Terraform.

Prerequisites

  • AWS CLI configured
  • Terraform installed
  • Understanding of CloudFormation templates
  • Basic knowledge of AWS services

Project Structure

aws-cloudformation-terraform/
├── main.tf
├── variables.tf
├── outputs.tf
└── terraform.tfvars

Basic CloudFormation Configuration

# main.tf
provider "aws" {
  region = var.aws_region
}

# S3 Bucket for Templates
resource "aws_s3_bucket" "templates" {
  bucket = "${var.project_name}-cfn-templates"

  tags = {
    Environment = var.environment
  }
}

# S3 Bucket Policy
resource "aws_s3_bucket_policy" "templates" {
  bucket = aws_s3_bucket.templates.id

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Sid    = "AllowCloudFormationAccess"
        Effect = "Allow"
        Principal = {
          Service = "cloudformation.amazonaws.com"
        }
        Action = [
          "s3:GetObject",
          "s3:ListBucket"
        ]
        Resource = [
          aws_s3_bucket.templates.arn,
          "${aws_s3_bucket.templates.arn}/*"
        ]
      }
    ]
  })
}

# IAM Role for CloudFormation
resource "aws_iam_role" "cloudformation" {
  name = "${var.project_name}-cfn-role"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Action = "sts:AssumeRole"
        Effect = "Allow"
        Principal = {
          Service = "cloudformation.amazonaws.com"
        }
      }
    ]
  })
}

# IAM Policy for CloudFormation
resource "aws_iam_role_policy" "cloudformation" {
  name = "${var.project_name}-cfn-policy"
  role = aws_iam_role.cloudformation.id

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect = "Allow"
        Action = [
          "s3:*",
          "ec2:*",
          "rds:*",
          "elasticloadbalancing:*",
          "autoscaling:*",
          "cloudwatch:*",
          "sns:*"
        ]
        Resource = "*"
      }
    ]
  })
}

# CloudFormation Stack
resource "aws_cloudformation_stack" "network" {
  name = "${var.project_name}-network"

  template_body = jsonencode({
    AWSTemplateFormatVersion = "2010-09-09"
    Description = "Network Infrastructure"
    
    Parameters = {
      VpcCidr = {
        Type = "String"
        Default = "10.0.0.0/16"
        Description = "CIDR block for VPC"
      }
    }
    
    Resources = {
      VPC = {
        Type = "AWS::EC2::VPC"
        Properties = {
          CidrBlock = { Ref = "VpcCidr" }
          EnableDnsHostnames = true
          EnableDnsSupport = true
          Tags = [
            {
              Key = "Name"
              Value = "${var.project_name}-vpc"
            }
          ]
        }
      }
      
      InternetGateway = {
        Type = "AWS::EC2::InternetGateway"
        Properties = {
          Tags = [
            {
              Key = "Name"
              Value = "${var.project_name}-igw"
            }
          ]
        }
      }
      
      AttachGateway = {
        Type = "AWS::EC2::VPCGatewayAttachment"
        Properties = {
          VpcId = { Ref = "VPC" }
          InternetGatewayId = { Ref = "InternetGateway" }
        }
      }
    }
    
    Outputs = {
      VpcId = {
        Description = "VPC ID"
        Value = { Ref = "VPC" }
      }
    }
  })

  parameters = {
    VpcCidr = var.vpc_cidr
  }

  capabilities = ["CAPABILITY_IAM", "CAPABILITY_NAMED_IAM"]
  
  timeout_in_minutes = 30

  tags = {
    Environment = var.environment
  }
}

# CloudFormation Stack Set
resource "aws_cloudformation_stack_set" "baseline" {
  name = "${var.project_name}-baseline"
  
  permission_model = "SERVICE_MANAGED"
  
  template_body = jsonencode({
    AWSTemplateFormatVersion = "2010-09-09"
    Description = "Baseline Security Configuration"
    
    Resources = {
      SecurityGroup = {
        Type = "AWS::EC2::SecurityGroup"
        Properties = {
          GroupDescription = "Baseline security group"
          SecurityGroupIngress = [
            {
              IpProtocol = "tcp"
              FromPort = 443
              ToPort = 443
              CidrIp = "0.0.0.0/0"
            }
          ]
          Tags = [
            {
              Key = "Name"
              Value = "baseline-sg"
            }
          ]
        }
      }
      
      CloudWatchLogGroup = {
        Type = "AWS::Logs::LogGroup"
        Properties = {
          LogGroupName = "/aws/baseline"
          RetentionInDays = 30
        }
      }
    }
  })

  auto_deployment {
    enabled = true
    retain_stacks_on_account_removal = false
  }

  capabilities = ["CAPABILITY_IAM", "CAPABILITY_NAMED_IAM"]

  administration_role_arn = aws_iam_role.stackset_admin.arn
  execution_role_name    = "AWSCloudFormationStackSetExecutionRole"
}

Variables Configuration

# variables.tf
variable "aws_region" {
  description = "AWS region"
  type        = string
  default     = "us-west-2"
}

variable "project_name" {
  description = "Project name"
  type        = string
}

variable "environment" {
  description = "Environment name"
  type        = string
  default     = "dev"
}

variable "vpc_cidr" {
  description = "CIDR block for VPC"
  type        = string
  default     = "10.0.0.0/16"
}

Best Practices

  1. Stack Management

    • Use nested stacks
    • Implement change sets
    • Configure proper rollbacks
    • Regular stack reviews
  2. Security

    • Use IAM roles
    • Implement stack policies
    • Configure access controls
    • Regular security reviews
  3. Cost Optimization

    • Monitor resource usage
    • Clean up unused stacks
    • Use cost allocation tags
    • Regular cost reviews
  4. Performance

    • Optimize dependencies
    • Use proper timeouts
    • Monitor stack operations
    • Regular performance reviews

Nested Stacks

# Parent Stack
resource "aws_cloudformation_stack" "parent" {
  name = "${var.project_name}-parent"

  template_body = jsonencode({
    AWSTemplateFormatVersion = "2010-09-09"
    Description = "Parent Stack"
    
    Resources = {
      NetworkStack = {
        Type = "AWS::CloudFormation::Stack"
        Properties = {
          TemplateURL = "https://${aws_s3_bucket.templates.bucket_regional_domain_name}/network.yaml"
          Parameters = {
            VpcCidr = var.vpc_cidr
          }
        }
      }
      
      SecurityStack = {
        Type = "AWS::CloudFormation::Stack"
        Properties = {
          TemplateURL = "https://${aws_s3_bucket.templates.bucket_regional_domain_name}/security.yaml"
          Parameters = {
            VpcId = { "Fn::GetAtt" = ["NetworkStack", "Outputs.VpcId"] }
          }
        }
      }
    }
  })

  capabilities = ["CAPABILITY_IAM", "CAPABILITY_NAMED_IAM"]
  
  depends_on = [
    aws_s3_bucket.templates,
    aws_s3_bucket_policy.templates
  ]
}

Change Set Management

# Change Set
resource "aws_cloudformation_stack" "with_changeset" {
  name = "${var.project_name}-with-changeset"

  template_body = jsonencode({
    AWSTemplateFormatVersion = "2010-09-09"
    Description = "Stack with Change Set"
    
    Resources = {
      Bucket = {
        Type = "AWS::S3::Bucket"
        Properties = {
          BucketName = "${var.project_name}-bucket"
          VersioningConfiguration = {
            Status = "Enabled"
          }
        }
      }
    }
  })

  capabilities = ["CAPABILITY_IAM"]

  lifecycle {
    create_before_destroy = true
  }
}

# Stack Policy
resource "aws_cloudformation_stack_policy" "protect_resources" {
  stack_name = aws_cloudformation_stack.with_changeset.name

  policy_body = jsonencode({
    Statement = [
      {
        Effect = "Allow"
        Action = "Update:*"
        Principal = "*"
        Resource = "*"
      },
      {
        Effect = "Deny"
        Action = "Update:Delete"
        Principal = "*"
        Resource = "*"
        Condition = {
          StringEquals = {
            "ResourceType" = [
              "AWS::S3::Bucket"
            ]
          }
        }
      }
    ]
  })
}

Monitoring Configuration

# CloudWatch Dashboard
resource "aws_cloudwatch_dashboard" "cloudformation" {
  dashboard_name = "${var.project_name}-cloudformation-dashboard"

  dashboard_body = jsonencode({
    widgets = [
      {
        type   = "metric"
        x      = 0
        y      = 0
        width  = 12
        height = 6

        properties = {
          metrics = [
            ["AWS/CloudFormation", "StackCreationTime", "StackName", aws_cloudformation_stack.network.name],
            ["AWS/CloudFormation", "StackDeleteTime", "StackName", aws_cloudformation_stack.network.name]
          ]
          period = 300
          stat   = "Average"
          region = var.aws_region
          title  = "Stack Operation Times"
        }
      }
    ]
  })
}

# CloudWatch Alarms
resource "aws_cloudwatch_metric_alarm" "stack_operation_failed" {
  alarm_name          = "${var.project_name}-stack-operation-failed"
  comparison_operator = "GreaterThanThreshold"
  evaluation_periods  = "1"
  metric_name         = "StackOperationFailed"
  namespace           = "AWS/CloudFormation"
  period             = "300"
  statistic          = "Sum"
  threshold          = "0"
  alarm_description  = "This metric monitors CloudFormation stack operation failures"
  alarm_actions      = [var.sns_topic_arn]

  dimensions = {
    StackName = aws_cloudformation_stack.network.name
  }
}

Common Use Cases

  1. Application Stack
resource "aws_cloudformation_stack" "application" {
  name = "${var.project_name}-application"

  template_body = jsonencode({
    AWSTemplateFormatVersion = "2010-09-09"
    Description = "Application Stack"
    
    Parameters = {
      InstanceType = {
        Type = "String"
        Default = "t3.micro"
        AllowedValues = ["t3.micro", "t3.small", "t3.medium"]
      }
    }
    
    Resources = {
      LaunchTemplate = {
        Type = "AWS::EC2::LaunchTemplate"
        Properties = {
          LaunchTemplateData = {
            InstanceType = { Ref = "InstanceType" }
            ImageId = "ami-12345678"
            SecurityGroupIds = [{ Ref = "SecurityGroup" }]
            UserData = {
              "Fn::Base64" = <<-EOF
                #!/bin/bash
                yum update -y
                yum install -y httpd
                systemctl start httpd
                systemctl enable httpd
              EOF
            }
          }
        }
      }
      
      AutoScalingGroup = {
        Type = "AWS::AutoScaling::AutoScalingGroup"
        Properties = {
          MinSize = "1"
          MaxSize = "3"
          DesiredCapacity = "2"
          LaunchTemplate = {
            LaunchTemplateId = { Ref = "LaunchTemplate" }
            Version = { "Fn::GetAtt" = ["LaunchTemplate", "LatestVersionNumber"] }
          }
        }
      }
    }
  })

  parameters = {
    InstanceType = var.instance_type
  }

  capabilities = ["CAPABILITY_IAM"]
}
  1. Database Stack
resource "aws_cloudformation_stack" "database" {
  name = "${var.project_name}-database"

  template_body = jsonencode({
    AWSTemplateFormatVersion = "2010-09-09"
    Description = "Database Stack"
    
    Parameters = {
      DBInstanceClass = {
        Type = "String"
        Default = "db.t3.micro"
      }
      DBName = {
        Type = "String"
      }
      DBUsername = {
        Type = "String"
        NoEcho = true
      }
      DBPassword = {
        Type = "String"
        NoEcho = true
      }
    }
    
    Resources = {
      DBInstance = {
        Type = "AWS::RDS::DBInstance"
        Properties = {
          DBInstanceClass = { Ref = "DBInstanceClass" }
          Engine = "postgres"
          MasterUsername = { Ref = "DBUsername" }
          MasterUserPassword = { Ref = "DBPassword" }
          DBName = { Ref = "DBName" }
          AllocatedStorage = "20"
          BackupRetentionPeriod = "7"
          MultiAZ = false
        }
      }
    }
  })

  parameters = {
    DBInstanceClass = var.db_instance_class
    DBName         = var.db_name
    DBUsername     = var.db_username
    DBPassword     = var.db_password
  }
}

Advanced Features

# Custom Resource
resource "aws_cloudformation_stack" "custom_resource" {
  name = "${var.project_name}-custom-resource"

  template_body = jsonencode({
    AWSTemplateFormatVersion = "2010-09-09"
    Description = "Stack with Custom Resource"
    
    Resources = {
      CustomFunction = {
        Type = "AWS::Lambda::Function"
        Properties = {
          Handler = "index.handler"
          Role = aws_iam_role.lambda.arn
          Code = {
            ZipFile = <<-EOF
              exports.handler = async (event, context) => {
                console.log('Event:', JSON.stringify(event));
                return {
                  Status: 'SUCCESS',
                  PhysicalResourceId: context.logStreamName,
                  Data: {
                    Message: 'Custom resource executed successfully'
                  }
                };
              };
            EOF
          }
          Runtime = "nodejs14.x"
        }
      }
      
      CustomResource = {
        Type = "Custom::Resource"
        Properties = {
          ServiceToken = { "Fn::GetAtt" = ["CustomFunction", "Arn"] }
          CustomData = "Example"
        }
      }
    }
  })

  capabilities = ["CAPABILITY_IAM"]
}

Conclusion

This setup provides a comprehensive foundation for managing CloudFormation using Terraform. Remember to:

  • Plan your stack structure carefully
  • Implement proper change management
  • Monitor stack operations and costs
  • Keep your configurations versioned
  • Test thoroughly before production deployment

The complete code can be customized based on your specific requirements and use cases.