Skip to content
ExponaBLOG
AWS FOR DEVELOPERS · DAY 064 / 100

Infrastructure as Code on AWS: best practices explained

CDK, Terraform or CloudFormation?

Expona Admin8 min read

Infrastructure as Code on AWS: best practices explained

You've moved beyond manual console clicks and spreadsheets. Your team now manages AWS infrastructure through code, versioning every resource like it's application logic. But choosing the right tool—CDK, Terraform, or CloudFormation—and then sticking to it determines whether you end up with a reliable, auditable foundation or a tangled mess of drift and undocumented changes.

Why infrastructure as code matters in production

Infrastructure as code (IaC) shifts AWS resource management from point-and-click operations to declarative, version-controlled definitions. This approach brings several tangible benefits:

  • Reproducibility: spin up identical environments for staging, testing, and production
  • Auditability: every infrastructure change lives in git history with commit messages and reviewer approvals
  • Collaboration: teams can peer-review infrastructure changes the same way they review code
  • Recovery: reverting a bad infrastructure change is as simple as reverting a commit

The trade-off is that you need discipline. IaC only works if everyone on the team commits to it. One person making manual changes in the console undermines the entire system.

Options: CDK, Terraform, or CloudFormation

CDK (AWS Cloud Development Kit)

CDK lets you write infrastructure in real programming languages: TypeScript, Python, Java, Go, or C#. You define constructs—reusable, composable building blocks—that compile down to CloudFormation templates.

Strengths:

  • High-level abstractions. A single CDK construct can define a complete VPC, security groups, NAT gateways, and subnets without writing 200 lines of JSON
  • Leverage your existing language skills. Teams already writing Node.js or Python can stay in that ecosystem
  • Conditional logic and loops. You can iterate, branch, and compute values—impossible in raw CloudFormation
  • AWS-native and rapidly updated. New services and features land in CDK quickly

Weaknesses:

  • AWS-only. If your team manages infrastructure across AWS, Azure, and GCP, CDK won't help
  • Compilation step. You must run cdk synth to generate CloudFormation before deploying
  • Smaller community than Terraform. Fewer third-party modules and integrations

Example CDK stack in TypeScript:

bash
import * as cdk from 'aws-cdk-lib';
import * as s3 from 'aws-cdk-lib/aws-s3';
import * as ec2 from 'aws-cdk-lib/aws-ec2';

export class MyStack extends cdk.Stack {
  constructor(scope: cdk.App, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    const vpc = new ec2.Vpc(this, 'MyVpc', {
      cidr: '10.0.0.0/16',
      maxAzs: 3,
    });

    const bucket = new s3.Bucket(this, 'MyBucket', {
      versioned: true,
      encryption: s3.BucketEncryption.KMS,
      blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
    });
  }
}

Notice how a few lines define a multi-AZ VPC with encryption and access controls. The same infrastructure in raw CloudFormation would span 100+ lines of YAML or JSON.

Terraform

Terraform is HashiCorp's declarative language for infrastructure. You write .tf files in HCL, and Terraform translates them into API calls to hundreds of providers: AWS, Azure, GCP, Kubernetes, GitHub, Datadog, and more.

Strengths:

  • Multi-cloud. Write one infrastructure definition language and deploy to any cloud provider
  • Massive ecosystem. The Terraform Registry hosts thousands of community and official modules
  • Language-agnostic. HCL is simple and readable; no programming knowledge required
  • Mature state management. Terraform's state file tracks infrastructure and detects drift
  • Large community. You'll find solutions, examples, and modules for nearly any use case

Weaknesses:

  • Not AWS-native. New AWS features sometimes lag behind in Terraform provider updates
  • State management complexity. Terraform state files must be stored securely (usually in S3 with locking via DynamoDB)
  • Learning curve for teams new to declarative infrastructure

Example Terraform configuration:

bash
resource "aws_vpc" "main" {
  cidr_block           = "10.0.0.0/16"
  enable_dns_hostnames = true

  tags = {
    Name = "main-vpc"
  }
}

resource "aws_s3_bucket" "main" {
  bucket = "my-app-bucket-${data.aws_caller_identity.current.account_id}"

  tags = {
    Name = "main-bucket"
  }
}

resource "aws_s3_bucket_versioning" "main" {
  bucket = aws_s3_bucket.main.id

  versioning_configuration {
    status = "Enabled"
  }
}

CloudFormation

CloudFormation is AWS's native IaC service. You write templates in JSON or YAML and deploy them as stacks. No compilation step, no external dependencies—just AWS.

Strengths:

  • Native to AWS. Deeply integrated with all AWS services, often with feature parity within hours of launch
  • No extra tooling. CloudFormation lives in the AWS console and CLI
  • Tight integration with AWS services. You can trigger lambda functions, SNS topics, and other resources in response to stack events
  • Built-in intrinsic functions for computing values and referencing resources

Weaknesses:

  • Verbose templates. Even simple stacks balloon to hundreds of lines
  • Limited reusability. Code duplication is common; there's no native construct library like CDK
  • Steep learning curve. The syntax is dense and error messages can be cryptic
  • Drift detection is manual. You must actively check for configuration drift; it doesn't happen automatically

Raw CloudFormation for a simple VPC becomes a wall of YAML:

bash
AWSTemplateFormatVersion: '2010-09-09'
Resources:
  MyVPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: 10.0.0.0/16
      EnableDnsHostnames: true
  PublicSubnet1:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref MyVPC
      CidrBlock: 10.0.1.0/24
      AvailabilityZone: us-east-1a
  PublicSubnet2:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref MyVPC
      CidrBlock: 10.0.2.0/24
      AvailabilityZone: us-east-1b
  # ... many more lines ...

Pick one tool per project

This is the golden rule: do not mix CDK, Terraform, and CloudFormation in the same project. Mixing tools creates infrastructure drift and makes debugging a nightmare.

Real-world example: Your team starts with CloudFormation for core networking. Six months later, a new hire brings a Terraform module from a previous job and deploys a database stack in Terraform. Now you have two systems managing overlapping resources. Someone modifies a security group in Terraform. The CloudFormation stack still expects the old rules. A developer sees the old rules in the console, assumes that's the source of truth, and manually edits the security group. Now neither Terraform nor CloudFormation reflects reality. Your infrastructure is unmaintainable.

The decision should come down to your context:

  • Choose CDK if: You're AWS-only, your team is comfortable with programming, and you want the highest-level abstractions
  • Choose Terraform if: You need multi-cloud support, want the largest ecosystem of modules, or prefer declarative over imperative
  • Choose CloudFormation if: You need the tightest AWS integration, have no dependencies, or your organization mandates AWS-native tooling

Document this decision in your README or architecture decision record (ADR). Make it clear to new team members which tool owns which parts of the infrastructure.

Review plans before every apply

Whether you're using CDK, Terraform, or CloudFormation, always read the diff before deploying.

With Terraform, this is terraform plan:

bash
terraform plan -out=tfplan

Read the output carefully. Look for:

  • Resource deletions (marked with a minus). Deleting a database or S3 bucket is rarely reversible
  • Resource replacements (marked with -/+). Some changes force a resource to be destroyed and recreated
  • Updates to security groups, IAM policies, and network routes. Changes here can cause outages

With CDK, generate the template and review it:

bash
cdk diff

This shows you what will change compared to the deployed stack.

With CloudFormation, use the console or AWS CLI:

bash
aws cloudformation create-change-set \
  --stack-name my-stack \
  --change-set-name my-change \
  --template-body file://template.yaml

Then review the change set before executing it.

Real scenario: A junior engineer runs terraform apply without reviewing the plan. Terraform was configured to recreate an RDS database when the backup retention period changes from 7 to 14 days. The database is deleted and recreated, wiping all data. This is now a post-mortem conversation with the CEO. Always review the plan.

No click-ops in production

Console changes are undocumented, unreviewed, and unmaintainable. Every change made via the AWS console is a divergence from your infrastructure code.

Common scenarios where this happens:

  • Debugging a performance issue. An engineer tweaks a database parameter via the console to fix it temporarily
  • Emergency hotfix. A late-night incident requires adding an ingress rule to a security group. It's faster to click than to commit and deploy
  • Testing a change. Someone wants to see if a different instance type will work better. They launch it in the console instead of updating the code

Each of these creates drift. The infrastructure code no longer matches reality. Next week, someone redeploys from code and overwrites the production fix. Or the next infrastructure change conflicts with undocumented modifications.

The rule: If it's not in code, it didn't happen. Every production change—every security group rule, every parameter, every tag—must go through your infrastructure code, be reviewed, and be committed to git.

To enforce this:

  • Remove console modification permissions for production resources in IAM policies
  • Use read-only roles for troubleshooting. Developers can view but not modify
  • Enable CloudTrail and regularly audit it for manual changes. Alert when production resources are modified outside of your IaC process
  • Run terraform plan or cdk diff in CI/CD before every deployment. If drift is detected, the pipeline should fail

Example IAM policy that allows viewing but not modifying:

bash
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "ec2:Describe*",
        "rds:Describe*",
        "s3:Get*",
        "s3:List*"
      ],
      "Resource": "*"
    }
  ]
}

This allows describing and reading resources but nothing that modifies them.

Common mistakes

Hardcoding values instead of using parameters. Don't hardcode account IDs, regions, or environment names. Use variables or parameters so the same code deploys to dev, staging, and production with different values.

Storing secrets in code. Never commit database passwords, API keys, or SSH keys to your repository. Use AWS Secrets Manager, Systems Manager Parameter Store, or environment variables. Reference them in your IaC, never define them.

Skipping the plan review step. It takes 30 seconds to read terraform plan. Skipping it has cost teams entire databases. Read the plan every time.

Creating stacks manually and managing them with code. If a resource exists outside your IaC—created manually years ago—and now you want to manage it with Terraform or CDK, you must either import it or destroy and recreate it. Mixed management breeds disaster.

Not using version control for infrastructure. Your infrastructure code should live in git, in the same repository as your application code or in a dedicated infrastructure repo. Track all changes, enforce code review, and keep history.

Key takeaways

  • CDK, Terraform, and CloudFormation each have trade-offs. CDK offers high-level abstractions and programming language support. Terraform provides multi-cloud capability and ecosystem. CloudFormation is native to AWS with no extra tooling. Choose one based on your constraints, not on hype
  • Do not mix infrastructure tools in a single project. Mixing creates drift, confusion, and maintenance headaches. Pick one tool, document the decision, and stick with it
  • Always review the plan before applying. terraform plan, cdk diff, and CloudFormation change sets are your safety net. Read the diff, catch destructive changes, and prevent outages
  • Ban manual console changes in production. Every infrastructure modification must go through your IaC process, be peer-reviewed, and be committed to version control. Drift is a bug, not a feature
  • Infrastructure code is a contract. It documents your system, enables collaboration, and makes recovery from mistakes possible. Treat it with the same rigor as application code
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles