EthicsComputerCourses
Join training +91 90532 10052

Infrastructure as Code: CloudFormation, CDK & Terraform

AWS · Lesson 30 of 42 Advanced 7 min read Updated

Clicking through the console to build a VPC, a bucket, a table and three functions takes an afternoon. Doing it again for a test environment takes another afternoon, and the two never end up exactly the same. Infrastructure as code (IaC) means describing your resources in files, keeping those files in Git, and letting a tool create, update and delete the real resources to match.

In this lesson you will learn why IaC matters, write and deploy a CloudFormation template, see the same idea in AWS SAM, the AWS CDK and Terraform, and learn how to choose between them.

Why infrastructure as code?

  • Repeatable: the same file creates identical dev, test and production environments, in Mumbai or Hyderabad.
  • Reviewable: changes go through pull requests, just like application code, and Git history shows who changed what.
  • Previewable: you see what will change before it changes.
  • Clean up: deleting a stack removes everything it created, so no forgotten resources keep billing.
  • Documentation: the code is the up-to-date description of your system.

AWS CloudFormation

CloudFormation is AWS's built-in IaC service. You write a template in YAML or JSON, and CloudFormation creates a stack: the set of real resources described by the template. Its main sections are:

  • AWSTemplateFormatVersion: always "2010-09-09" (like the IAM version, a fixed value).
  • Parameters: inputs, such as an environment name.
  • Resources: the only required section; each resource has a logical name, a Type and Properties.
  • Outputs: values to show or export, such as a bucket name.

Intrinsic functions connect things: !Ref returns a parameter's value or a resource's main ID, !GetAtt returns an attribute (such as an ARN), and !Sub builds strings with ${...} placeholders.

YAML
AWSTemplateFormatVersion: "2010-09-09"
Description: Storage for the tiffin app

Parameters:
  Env:
    Type: String
    AllowedValues: [dev, prod]
    Default: dev

Resources:
  ReceiptsBucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Sub "tiffin-receipts-${Env}-${AWS::AccountId}"
      VersioningConfiguration:
        Status: Enabled
      PublicAccessBlockConfiguration:
        BlockPublicAcls: true
        IgnorePublicAcls: true
        BlockPublicPolicy: true
        RestrictPublicBuckets: true

  OrdersTable:
    Type: AWS::DynamoDB::Table
    Properties:
      BillingMode: PAY_PER_REQUEST
      AttributeDefinitions:
        - AttributeName: customerId
          AttributeType: S
        - AttributeName: orderSk
          AttributeType: S
      KeySchema:
        - AttributeName: customerId
          KeyType: HASH
        - AttributeName: orderSk
          KeyType: RANGE

Outputs:
  BucketName:
    Value: !Ref ReceiptsBucket
  TableArn:
    Value: !GetAtt OrdersTable.Arn

${AWS::AccountId} is a pseudo parameter that CloudFormation fills in, which helps make the bucket name unique. Deploy the template with one command. It creates a change set (a preview of changes) and executes it:

Terminal
aws cloudformation deploy --template-file storage.yaml \
  --stack-name tiffin-storage-dev --parameter-overrides Env=dev

aws cloudformation describe-stacks --stack-name tiffin-storage-dev \
  --query "Stacks[0].Outputs[].[OutputKey, OutputValue]" --output text
Output
Waiting for changeset to be created..
Waiting for stack create/update to complete
Successfully created/updated stack - tiffin-storage-dev
BucketName	tiffin-receipts-dev-111122223333
TableArn	arn:aws:dynamodb:ap-south-1:111122223333:table/tiffin-storage-dev-OrdersTable-1A2B3C4D5E6F

Because the table has no fixed name, CloudFormation generated one from the stack and logical names. Run deploy again after editing the file and only the differences are applied. For templates that create IAM resources, add --capabilities CAPABILITY_IAM to confirm you allow that. aws cloudformation delete-stack --stack-name tiffin-storage-dev removes everything (S3 buckets must be empty first).

Other useful features: drift detection finds resources changed by hand outside CloudFormation, stack policies and termination protection guard production stacks, and the IaC generator can create a template from resources you already built in the console.

AWS SAM for serverless apps

The AWS Serverless Application Model (SAM) is an extension of CloudFormation with short resource types for serverless apps. One AWS::Serverless::Function with an event creates the function, its role, its log permissions and an HTTP API:

YAML
AWSTemplateFormatVersion: "2010-09-09"
Transform: AWS::Serverless-2016-10-31

Resources:
  BillFunction:
    Type: AWS::Serverless::Function
    Properties:
      Runtime: python3.14
      Architectures: [arm64]
      Handler: app.lambda_handler
      CodeUri: src/
      Timeout: 10
      Events:
        Bill:
          Type: HttpApi
          Properties:
            Path: /bill
            Method: post

sam build packages the code and sam deploy --guided asks a few questions the first time and deploys. sam local invoke runs the function in a local container for testing.

AWS CDK: infrastructure in a programming language

The AWS Cloud Development Kit (CDK) lets you write infrastructure in TypeScript, JavaScript, Python, Java, C# or Go. Your code synthesises a CloudFormation template, which is then deployed. Its libraries provide constructs with safe defaults, so a few lines produce many correctly configured resources. Use CDK v2 (the aws-cdk-lib package); v1 reached end of support in June 2023.

Python
from aws_cdk import App, Environment, RemovalPolicy, Stack
from aws_cdk import aws_dynamodb as dynamodb
from aws_cdk import aws_s3 as s3
from constructs import Construct


class TiffinStorage(Stack):
    def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None:
        super().__init__(scope, construct_id, **kwargs)

        s3.Bucket(self, "Receipts",
                  versioned=True,
                  block_public_access=s3.BlockPublicAccess.BLOCK_ALL,
                  enforce_ssl=True,
                  removal_policy=RemovalPolicy.RETAIN)

        dynamodb.TableV2(self, "Orders",
                         partition_key=dynamodb.Attribute(name="customerId", type=dynamodb.AttributeType.STRING),
                         sort_key=dynamodb.Attribute(name="orderSk", type=dynamodb.AttributeType.STRING),
                         billing=dynamodb.Billing.on_demand())


app = App()
TiffinStorage(app, "TiffinStorage", env=Environment(region="ap-south-1"))
app.synth()
Terminal
npm install -g aws-cdk
cdk init app --language python
cdk bootstrap
cdk synth
cdk diff
cdk deploy
cdk destroy

cdk bootstrap runs once per account and Region to create the resources CDK needs for deployments. cdk synth prints the generated template, cdk diff previews changes, and enforce_ssl=True adds the "deny non-HTTPS" bucket policy you wrote by hand in the S3 lesson.

Terraform and OpenTofu

Terraform, from HashiCorp (now part of IBM), is a popular IaC tool that works with AWS and hundreds of other providers using its own language, HCL. It keeps a state file that maps your code to real resources; for teams, store it in an S3 bucket with locking. Terraform changed to the Business Source Licence in 2023, and OpenTofu is an open-source fork managed by the Linux Foundation that uses the same language.

HCL (Terraform)
terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 6.0"
    }
  }
  backend "s3" {
    bucket       = "priya-tf-state-111122223333"
    key          = "tiffin/terraform.tfstate"
    region       = "ap-south-1"
    use_lockfile = true
  }
}

provider "aws" {
  region = "ap-south-1"
}

resource "aws_s3_bucket" "receipts" {
  bucket = "tiffin-receipts-tf-111122223333"
}

resource "aws_s3_bucket_versioning" "receipts" {
  bucket = aws_s3_bucket.receipts.id
  versioning_configuration {
    status = "Enabled"
  }
}

The workflow is terraform init, then terraform plan (preview), terraform apply and, when finished, terraform destroy. use_lockfile = true uses S3's own locking; older setups used a DynamoDB table for locks.

Which tool should you choose?

ToolLanguageBest when
CloudFormationYAML or JSONYou want AWS-native, no extra tools, and simple templates
AWS SAMYAML (CloudFormation extension)You build mainly Lambda and API Gateway apps
AWS CDKTypeScript, Python, Java, C#, GoDevelopers prefer real code, loops and reusable components
Terraform or OpenTofuHCLYou manage several clouds or the team already uses it

All of them are far better than clicking. Pick one per project and do not mix manual console changes into resources it manages.

Common mistakes

  • Changing IaC-managed resources in the console, causing drift and surprises at the next deploy.
  • Hard-coding account IDs, Regions and names instead of parameters and pseudo parameters.
  • Committing secrets into templates or Terraform variables files.
  • Keeping Terraform state on one laptop. Use a remote backend with locking.
  • Deploying without a preview. Always look at the change set, cdk diff or terraform plan first.

Key points

  • IaC makes infrastructure repeatable, reviewable, previewable and easy to delete.
  • CloudFormation templates have Parameters, Resources and Outputs; !Ref, !GetAtt and !Sub connect values; stacks group resources.
  • aws cloudformation deploy creates and executes a change set; deleting the stack removes its resources.
  • AWS SAM shortens serverless templates; the AWS CDK v2 generates CloudFormation from TypeScript, Python and other languages.
  • Terraform and OpenTofu use HCL and a state file, best stored in S3 with locking.

Exercise

Write a CloudFormation template with a parameter ClubName and one S3 bucket whose name is the club name, then -match-photos-, then the account ID, with versioning enabled and an output called PhotosBucket. Then write the command to deploy it as stack cricket-photos with ClubName=rohtak-xi.

Show solution
YAML
AWSTemplateFormatVersion: "2010-09-09"
Parameters:
  ClubName:
    Type: String
Resources:
  PhotosBucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Sub "${ClubName}-match-photos-${AWS::AccountId}"
      VersioningConfiguration:
        Status: Enabled
Outputs:
  PhotosBucket:
    Value: !Ref PhotosBucket
Terminal
aws cloudformation deploy --template-file photos.yaml \
  --stack-name cricket-photos --parameter-overrides ClubName=rohtak-xi

!Sub replaces both the parameter and the pseudo parameter, and !Ref on a bucket returns its name. Bucket names must be lowercase, which is why the club name uses lowercase letters and a hyphen.

Quick Quiz

1. Which CloudFormation template section is required?

2. What does !GetAtt OrdersTable.Arn return?

3. What does cdk synth do?

4. Where should a team keep Terraform state?

Learn AWS with a mentor

Free lessons get you started. Our Full Stack Web Development training adds real projects and code reviews from working developers.

  • Project-based learning
  • Mentor code reviews
  • Free counselling call