CloudFormation and Terraform are both powerful Infrastructure as Code (IaC) tools for managing AWS resources, but they approach the problem from fundamentally different angles, leading to distinct strengths and weaknesses.

Let’s see them in action. Imagine you need to provision a simple S3 bucket and a Lambda function that can write to it.

CloudFormation Example:

Resources:
  MyS3Bucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Sub "my-unique-bucket-${AWS::AccountId}"

  MyLambdaFunction:
    Type: AWS::Lambda::Function
    Properties:
      FunctionName: my-s3-writer-lambda
      Handler: index.handler
      Role: !GetAtt MyLambdaExecutionRole.Arn
      Runtime: python3.9
      Code:
        ZipFile: |
          import boto3
          import os

          s3 = boto3.client('s3')
          bucket_name = os.environ['BUCKET_NAME']

          def handler(event, context):
              try:
                  s3.put_object(Bucket=bucket_name, Key='hello.txt', Body='Hello from Lambda!')
                  return {
                      'statusCode': 200,
                      'body': 'Successfully wrote to S3'
                  }
              except Exception as e:
                  print(f"Error writing to S3: {e}")
                  return {
                      'statusCode': 500,
                      'body': str(e)
                  }

  MyLambdaExecutionRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service: lambda.amazonaws.com
            Action: sts:AssumeRole
      Policies:
        - PolicyName: S3WritePolicy
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action:
                  - s3:PutObject
                Resource: !Sub "arn:aws:s3:::my-unique-bucket-${AWS::AccountId}/*"

To deploy this, you’d use the AWS CLI:

aws cloudformation create-stack --stack-name my-s3-lambda-stack --template-body file://template.yaml

Terraform Example:

provider "aws" {
  region = "us-east-1"
}

resource "aws_s3_bucket" "my_bucket" {
  bucket = "my-unique-bucket-${data.aws_caller_identity.current.account_id}"
}

data "aws_caller_identity" "current" {}

resource "aws_iam_role" "lambda_exec_role" {
  name = "lambda-s3-writer-role"

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

resource "aws_iam_role_policy" "s3_write_policy" {
  name = "s3-write-policy"
  role = aws_iam_role.lambda_exec_role.id

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Action = [
          "s3:PutObject",
        ]
        Effect   = "Allow"
        Resource = "${aws_s3_bucket.my_bucket.arn}/*"
      },
    ]
  })
}

resource "aws_lambda_function" "s3_writer" {
  function_name    = "my-s3-writer-lambda"
  handler          = "index.handler"
  runtime          = "python3.9"
  role             = aws_iam_role.lambda_exec_role.arn
  source_code_hash = filebase64sha256("lambda_function.zip") # Assuming you have lambda_function.zip

  environment {
    variables = {
      BUCKET_NAME = aws_s3_bucket.my_bucket.bucket
    }
  }
}

# You'd need to create lambda_function.zip separately containing index.py
# For example:
# echo 'import boto3; import os; s3 = boto3.client("s3"); bucket_name = os.environ["BUCKET_NAME"]; def handler(event, context): try: s3.put_object(Bucket=bucket_name, Key="hello.txt", Body="Hello from Lambda!"); return {"statusCode": 200, "body": "Successfully wrote to S3"}; except Exception as e: print(f"Error writing to S3: {e}"); return {"statusCode": 500, "body": str(e)}' > index.py
# zip lambda_function.zip index.py

To deploy this, you’d use the Terraform CLI:

terraform init
terraform plan
terraform apply

The core problem CloudFormation and Terraform solve is the declarative management of cloud infrastructure. Instead of writing imperative scripts (e.g., aws s3 mb ... && aws lambda create-function ...), you define the desired end-state of your infrastructure, and the tool figures out how to get there. This leads to more reliable, repeatable, and auditable deployments.

CloudFormation is AWS’s native IaC service. It’s deeply integrated with AWS, meaning it understands AWS resources and their properties intimately. When you define a CloudFormation template, you’re essentially telling AWS what resources you want, and AWS itself orchestrates their creation, update, and deletion. This tight integration means CloudFormation often supports new AWS features on day one.

Terraform, on the other hand, is a cloud-agnostic IaC tool developed by HashiCorp. It uses "providers" to interact with various cloud platforms and services. For AWS, there’s an official AWS provider. Terraform maintains its own state file, which tracks the resources it manages. This state file is crucial for Terraform to understand what exists in your infrastructure and to plan changes.

The biggest difference lies in how they handle state and drift. CloudFormation is stateful within AWS itself. When you create a stack, AWS tracks its resources. If you manually change a resource managed by CloudFormation (e.g., change the public access block on an S3 bucket), CloudFormation will detect this drift during an update and attempt to revert it to the state defined in the template. Terraform also maintains a state file, typically stored remotely (e.g., in an S3 bucket with DynamoDB locking). It uses this state file to determine what needs to be created, updated, or destroyed. Manual changes outside of Terraform will only be detected by Terraform during a terraform plan if they deviate from the state file’s record.

Terraform’s strength lies in its multi-cloud and multi-provider capabilities. You can manage AWS, Azure, Google Cloud, Kubernetes, Datadog, and many other services with a single tool and consistent syntax. CloudFormation is strictly AWS. For organizations heavily invested in AWS, CloudFormation’s native integration can be a significant advantage, especially for adopting new AWS services quickly. For those operating in a multi-cloud environment or using a mix of cloud and SaaS services, Terraform’s universality is compelling.

The actual execution model is also different. CloudFormation executes entirely within AWS. You upload a template, and AWS performs the operations. Terraform, however, typically runs locally or on a CI/CD runner. It calls the AWS API (or other provider APIs) to provision resources. This means Terraform needs its own execution environment and credentials configured.

When it comes to updating resources, CloudFormation often uses a "replace" strategy for certain changes, which can lead to brief downtime if not managed carefully. Terraform, especially with newer versions and specific resource configurations, can sometimes perform in-place updates more gracefully. However, the exact behavior depends heavily on the specific AWS resource and how it’s defined in the IaC.

The one thing most people don’t know about CloudFormation is its intrinsic drift detection and remediation. If you manually modify a resource managed by a CloudFormation stack, the next time you try to update the stack, CloudFormation will detect that the actual state of the resource doesn’t match the template’s desired state. It will then attempt to reconcile this by updating the resource to match the template. This is a powerful safety net that Terraform’s state file also provides, but CloudFormation’s is baked into the AWS service itself.

Ultimately, the choice between CloudFormation and Terraform often comes down to your organization’s existing AWS footprint, multi-cloud strategy, and preference for native tooling versus a universal solution.

The next concept you’ll likely encounter is managing sensitive data like database passwords or API keys within your IaC.

Want structured learning?

Take the full Cloudformation course →