DEV Community

Said Olano
Said Olano

Posted on

Infrastructure as Code on AWS: Terraform vs CloudFormation in Practice

Introduction

Clicking through a cloud console works for a first experiment. It stops working the moment you need the same environment in staging and production, a repeatable disaster recovery region, or an audit trail of who changed a security group and when. Infrastructure as Code (IaC) solves this by describing infrastructure in version-controlled text files that can be reviewed, tested, and applied the same way every time.

On AWS, the two dominant options are Terraform (HashiCorp, cloud-agnostic) and AWS CloudFormation (AWS-native). Both are declarative: you describe the desired end state, and the tool works out what to create, update, or delete. The differences show up in how they manage state, how they handle multiple clouds, and how they fit into your existing tooling.

This article walks through the core concepts, builds the same small network and storage stack in both tools, and ends with guidance on choosing between them.

Core Concepts Shared by Both Tools

Regardless of the tool, a solid IaC workflow rests on a few ideas:

  • Declarative desired state. You describe what should exist, not the sequence of API calls to create it.
  • Plan before apply. Both tools can show a diff of proposed changes before touching real resources. Treat that step as a code review for infrastructure.
  • State tracking. The tool must know which real resources map to which definitions. Terraform keeps this in a state file; CloudFormation keeps it inside the stack itself.
  • Idempotency. Applying the same definition twice should produce no changes the second time.
  • Drift. Resources changed outside the tool (for example, through the console) create a mismatch between code and reality. Detecting and resolving drift is an ongoing operational concern.

Terraform: Multi-Cloud and Provider-Based

Terraform uses HashiCorp Configuration Language (HCL) and a plugin model called providers. The AWS provider maps HCL resources to AWS APIs, and other providers cover Azure, GCP, Kubernetes, and many SaaS products, which makes Terraform a good fit when your estate spans more than one platform.

Strengths

  • Cloud-agnostic syntax. Once your team knows HCL, the same workflow applies across providers.
  • Modules. Reusable building blocks, published in a registry or kept in your own repositories, encapsulate standards such as tagging, encryption defaults, and logging.
  • Rich ecosystem. Linters (tflint), security scanners (checkov, tfsec), and testing frameworks (terraform test) are mature.
  • Explicit state control. Remote backends such as S3 with DynamoDB locking, or Terraform Cloud, give you fine-grained control over who can apply what.

Costs

  • You own the state. A misconfigured backend, a lost state file, or concurrent applies can corrupt your view of reality. Remote state with locking is not optional in a team setting.
  • Provider versions drift. Pin provider versions in required_providers and upgrade deliberately.

Example: VPC and an encrypted S3 bucket in Terraform

terraform {
  required_version = ">= 1.6.0"

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }

  backend "s3" {
    bucket         = "acme-terraform-state"
    key            = "network/prod/terraform.tfstate"
    region         = "us-east-1"
    dynamodb_table = "acme-terraform-locks"
    encrypt        = true
  }
}

provider "aws" {
  region = var.region

  default_tags {
    tags = {
      Environment = var.environment
      ManagedBy   = "terraform"
    }
  }
}

variable "region" {
  type    = string
  default = "us-east-1"
}

variable "environment" {
  type    = string
  default = "prod"
}

variable "vpc_cidr" {
  type    = string
  default = "10.0.0.0/16"
}

resource "aws_vpc" "main" {
  cidr_block           = var.vpc_cidr
  enable_dns_support   = true
  enable_dns_hostnames = true

  tags = {
    Name = "${var.environment}-main-vpc"
  }
}

data "aws_caller_identity" "current" {}

resource "aws_s3_bucket" "artifacts" {
  bucket = "${var.environment}-acme-artifacts-${data.aws_caller_identity.current.account_id}"
}

resource "aws_s3_bucket_server_side_encryption_configuration" "artifacts" {
  bucket = aws_s3_bucket.artifacts.id

  rule {
    apply_server_side_encryption_by_default {
      sse_algorithm = "AES256"
    }
  }
}

resource "aws_s3_bucket_public_access_block" "artifacts" {
  bucket = aws_s3_bucket.artifacts.id

  block_public_acls       = true
  block_public_policy     = true
  ignore_public_acls      = true
  restrict_public_buckets = true
}

output "vpc_id" {
  value = aws_vpc.main.id
}

output "artifacts_bucket" {
  value = aws_s3_bucket.artifacts.bucket
}
Enter fullscreen mode Exit fullscreen mode

Typical workflow:

terraform init
terraform plan -out=tfplan
terraform apply tfplan
Enter fullscreen mode Exit fullscreen mode

Saving the plan to a file and applying that exact file guarantees that what was reviewed is what gets executed.

CloudFormation: AWS-Native and Managed

CloudFormation describes resources in JSON or YAML templates and deploys them as stacks. AWS holds the state for you, rolls back failed stacks automatically by default, and integrates directly with IAM, Service Catalog, StackSets (for multi-account and multi-region rollouts), and AWS Control Tower.

Strengths

  • No state file to manage. AWS tracks the stack state, so there is no backend to configure or lock.
  • Automatic rollback. A failed update returns the stack to its last stable state.
  • Deep AWS integration. New AWS services and features usually appear in CloudFormation resource types quickly, and it is the foundation for AWS Serverless Application Model (SAM) and CDK.
  • Change sets. They provide a plan-like preview before any change is applied.
  • Native governance. Drift detection, stack policies, and termination protection are built in.

Costs

  • AWS-only. Multi-cloud estates need a second tool.
  • Verbose templates. YAML or JSON can become long, though nested stacks and modules help.
  • Slower feedback on some resources. Stack operations can take a long time, and rollbacks on large stacks can be painful to debug.

Example: the same VPC and encrypted S3 bucket in CloudFormation

AWSTemplateFormatVersion: "2010-09-09"
Description: Network and artifact storage for the production environment

Parameters:
  Environment:
    Type: String
    Default: prod
    AllowedValues: [dev, staging, prod]
  VpcCidr:
    Type: String
    Default: 10.0.0.0/16

Resources:
  MainVpc:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: !Ref VpcCidr
      EnableDnsSupport: true
      EnableDnsHostnames: true
      Tags:
        - Key: Name
          Value: !Sub "${Environment}-main-vpc"
        - Key: Environment
          Value: !Ref Environment
        - Key: ManagedBy
          Value: cloudformation

  ArtifactsBucket:
    Type: AWS::S3::Bucket
    DeletionPolicy: Retain
    UpdateReplacePolicy: Retain
    Properties:
      BucketName: !Sub "${Environment}-acme-artifacts-${AWS::AccountId}"
      BucketEncryption:
        ServerSideEncryptionConfiguration:
          - ServerSideEncryptionByDefault:
              SSEAlgorithm: AES256
      PublicAccessBlockConfiguration:
        BlockPublicAcls: true
        BlockPublicPolicy: true
        IgnorePublicAcls: true
        RestrictPublicBuckets: true
      Tags:
        - Key: Environment
          Value: !Ref Environment

Outputs:
  VpcId:
    Value: !Ref MainVpc
  ArtifactsBucket:
    Value: !Ref ArtifactsBucket
Enter fullscreen mode Exit fullscreen mode

Deploy it with a change set so you can review the plan first:

aws cloudformation deploy \
  --template-file network.yaml \
  --stack-name prod-network \
  --parameter-overrides Environment=prod \
  --no-fail-on-empty-changeset \
  --capabilities CAPABILITY_IAM
Enter fullscreen mode Exit fullscreen mode

Note the DeletionPolicy: Retain on the bucket. Without it, deleting the stack would delete the bucket and its contents. This is one of the most important safety settings in CloudFormation and is easy to forget.

Side-by-Side Comparison

Aspect Terraform CloudFormation
Scope Multi-cloud, many providers AWS only
Language HCL YAML or JSON
State You manage it (usually S3 + DynamoDB) AWS manages it
Preview terraform plan Change sets
Failure handling Partial state possible; you fix and re-apply Automatic rollback by default
Reuse Modules Nested stacks, modules, CDK constructs
Drift detection terraform plan shows drift Built-in drift detection
Multi-account rollout Requires pipeline or workspaces StackSets

Practical Guidance: Choosing Between Them

Choose Terraform when:

  • Your infrastructure spans AWS and at least one other platform.
  • Your team already uses HCL or wants a single tool across the organization.
  • You want a rich module ecosystem and strong community tooling.

Choose CloudFormation when:

  • You are all-in on AWS and want zero state management overhead.
  • You need native multi-account and multi-region rollouts through StackSets or Control Tower.
  • You want automatic rollback and AWS-supported governance features.

Many organizations use both: CloudFormation or Control Tower for the account baseline, and Terraform for application infrastructure. That split can work well as long as ownership boundaries are clear, so that no resource is managed by two tools at once.

Best Practices That Apply to Either Tool

  1. Keep everything in version control and require pull requests for infrastructure changes.
  2. Run plans in CI and attach the output to the pull request so reviewers see the exact impact.
  3. Protect stateful resources with lifecycle rules in Terraform (prevent_destroy) or deletion policies in CloudFormation (Retain).
  4. Use consistent tagging through default tags or shared modules, so cost allocation and ownership are traceable.
  5. Scan templates before deployment with checkov, cfn-lint, or tfsec.
  6. Separate environments into different accounts or state files, and never share state between dev and production.
  7. Apply with least-privilege roles dedicated to the deployment pipeline, not with administrator credentials on a laptop.
  8. Detect and reconcile drift on a schedule, and treat manual console changes as exceptions that must be codified afterward.

Conclusion and Key Takeaways

  • Infrastructure as Code turns infrastructure into reviewable, repeatable, versioned artifacts.
  • Terraform offers multi-cloud reach and a rich module ecosystem, at the cost of managing state yourself.
  • CloudFormation offers AWS-native integration, managed state, and automatic rollback, but it is limited to AWS.
  • The same VPC and encrypted bucket can be expressed in both; the mental model is the same even though the syntax differs.
  • Whichever you choose, plan before apply, protect stateful resources, and keep infrastructure changes in code review.

If you have questions or have run either tool at scale, I would like to hear what worked and what did not.

Top comments (0)