This hands-on lab teaches Infrastructure as Code (IaC) fundamentals using AWS CloudFormation and Terraform. Students will learn to define, deploy, and manage cloud infrastructure using declarative configuration files. This lab demonstrates how IaC enables version control, repeatability, and automation of infrastructure provisioning.
- Understand Infrastructure as Code principles and benefits
- Write CloudFormation templates in YAML
- Deploy and manage CloudFormation stacks
- Create Terraform configurations in HCL
- Use Terraform CLI for infrastructure lifecycle management
- Compare CloudFormation vs Terraform approaches
- Implement version control for infrastructure
- Apply IaC best practices
- Completion of Lab 01: AWS API Interaction
- AWS Academy Learner Lab [155046] access
- Basic understanding of YAML and JSON
- Text editor or IDE (VS Code recommended)
- Terminal/command line familiarity
- AWS CLI configured (from Lab 01)
Infrastructure as Code (IaC) is the practice of managing infrastructure through code rather than manual processes:
- Define infrastructure in configuration files
- Version control your infrastructure
- Automate provisioning and updates
- Ensure consistency across environments
- Enable collaboration and code review
- Reduce human error and configuration drift
CloudFormation is AWS's native IaC service that uses templates to provision and manage AWS resources.
Key Concepts:
- Template: JSON or YAML file defining resources
- Stack: Collection of AWS resources managed as a single unit
- Resource: AWS service component (e.g., S3 bucket, EC2 instance)
- Parameters: Input values for templates
- Outputs: Values returned after stack creation
Create a directory for your CloudFormation files:
mkdir cloudformation-lab
cd cloudformation-labCreate a file named s3-bucket-template.yaml:
AWSTemplateFormatVersion: '2010-09-09'
Description: 'CloudFormation template to create an S3 bucket with versioning and encryption'
Parameters:
BucketNameSuffix:
Type: String
Description: Suffix for the bucket name (e.g., your initials)
Default: 'demo'
AllowedPattern: '[a-z0-9-]+'
ConstraintDescription: Must contain only lowercase letters, numbers, and hyphens
Resources:
MyS3Bucket:
Type: 'AWS::S3::Bucket'
Properties:
BucketName: !Sub 'an-2026-cfn-${BucketNameSuffix}'
AccessControl: Private
VersioningConfiguration:
Status: Enabled
BucketEncryption:
ServerSideEncryptionConfiguration:
- ServerSideEncryptionByDefault:
SSEAlgorithm: AES256
PublicAccessBlockConfiguration:
BlockPublicAcls: true
BlockPublicPolicy: true
IgnorePublicAcls: true
RestrictPublicBuckets: true
Tags:
- Key: Environment
Value: Lab
- Key: ManagedBy
Value: CloudFormation
Outputs:
BucketName:
Description: 'Name of the S3 bucket'
Value: !Ref MyS3Bucket
Export:
Name: !Sub '${AWS::StackName}-BucketName'
BucketARN:
Description: 'ARN of the S3 bucket'
Value: !GetAtt MyS3Bucket.Arn
Export:
Name: !Sub '${AWS::StackName}-BucketARN'
BucketDomainName:
Description: 'Domain name of the S3 bucket'
Value: !GetAtt MyS3Bucket.DomainNameBefore deploying, validate the template syntax:
aws cloudformation validate-template \
--template-body file://s3-bucket-template.yamlExpected output shows template parameters and description.
Deploy using AWS CLI:
aws cloudformation create-stack \
--stack-name s3-bucket-stack \
--template-body file://s3-bucket-template.yaml \
--parameters ParameterKey=BucketNameSuffix,ParameterValue=[your-initials]Replace [your-initials] with your actual initials (e.g., jag).
Check stack status:
aws cloudformation describe-stacks \
--stack-name s3-bucket-stack \
--query 'Stacks[0].StackStatus'Watch stack events in real-time:
aws cloudformation describe-stack-events \
--stack-name s3-bucket-stack \
--max-items 10Wait for status: CREATE_COMPLETE
aws cloudformation describe-stacks \
--stack-name s3-bucket-stack \
--query 'Stacks[0].Outputs'- Go to AWS Console → CloudFormation
- Find your stack
s3-bucket-stack - Click on the stack name
- Explore tabs:
- Stack info: Overview and status
- Events: Creation timeline
- Resources: Created AWS resources
- Outputs: Exported values
- Parameters: Input values
- Template: View the YAML template
Let's add a lifecycle policy. Update s3-bucket-template.yaml:
AWSTemplateFormatVersion: '2010-09-09'
Description: 'CloudFormation template to create an S3 bucket with versioning and encryption'
Parameters:
BucketNameSuffix:
Type: String
Description: Suffix for the bucket name (e.g., your initials)
Default: 'demo'
AllowedPattern: '[a-z0-9-]+'
ConstraintDescription: Must contain only lowercase letters, numbers, and hyphens
Resources:
MyS3Bucket:
Type: 'AWS::S3::Bucket'
Properties:
BucketName: !Sub 'an-2026-cfn-${BucketNameSuffix}'
AccessControl: Private
VersioningConfiguration:
Status: Enabled
BucketEncryption:
ServerSideEncryptionConfiguration:
- ServerSideEncryptionByDefault:
SSEAlgorithm: AES256
PublicAccessBlockConfiguration:
BlockPublicAcls: true
BlockPublicPolicy: true
IgnorePublicAcls: true
RestrictPublicBuckets: true
LifecycleConfiguration:
Rules:
- Id: DeleteOldVersions
Status: Enabled
NoncurrentVersionExpirationInDays: 30
Tags:
- Key: Environment
Value: Lab
- Key: ManagedBy
Value: CloudFormation
Outputs:
BucketName:
Description: 'Name of the S3 bucket'
Value: !Ref MyS3Bucket
Export:
Name: !Sub '${AWS::StackName}-BucketName'
BucketARN:
Description: 'ARN of the S3 bucket'
Value: !GetAtt MyS3Bucket.Arn
Export:
Name: !Sub '${AWS::StackName}-BucketARN'
BucketDomainName:
Description: 'Domain name of the S3 bucket'
Value: !GetAtt MyS3Bucket.DomainNameUpdate the stack:
aws cloudformation update-stack \
--stack-name s3-bucket-stack \
--template-body file://s3-bucket-template.yaml \
--parameters ParameterKey=BucketNameSuffix,ParameterValue=[your-initials]Monitor the update:
aws cloudformation describe-stacks \
--stack-name s3-bucket-stack \
--query 'Stacks[0].StackStatus'When done, delete all resources:
# First, empty the bucket (CloudFormation can't delete non-empty buckets)
aws s3 rm s3://an-2026-cfn-[your-initials] --recursive
# Then delete the stack
aws cloudformation delete-stack --stack-name s3-bucket-stackVerify deletion:
aws cloudformation describe-stacks --stack-name s3-bucket-stackMethod: AWS CloudFormation
Key Features:
- Native AWS service (no additional tools)
- YAML or JSON templates
- Automatic rollback on errors
- Change sets for preview
- Stack policies for protection
- Drift detection
Advantages:
- Deep AWS integration
- No cost (only for resources)
- Automatic dependency resolution
- Built-in rollback
- AWS support
Disadvantages:
- AWS-only (vendor lock-in)
- Verbose syntax
- Limited to AWS services
- Slower than Terraform
Terraform is a cloud-agnostic IaC tool that supports multiple cloud providers using HashiCorp Configuration Language (HCL).
macOS:
brew tap hashicorp/tap
brew install hashicorp/tap/terraformWindows: Download from: https://www.terraform.io/downloads
Linux:
wget -O- https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] \
https://apt.releases.hashicorp.com $(lsb_release -cs) main" | \
sudo tee /etc/apt/sources.list.d/hashicorp.list
sudo apt update && sudo apt install terraformVerify installation:
terraform versionKey Concepts:
- Configuration: HCL files defining infrastructure
- Provider: Plugin for cloud platform (AWS, Azure, GCP)
- Resource: Infrastructure component
- State: Current infrastructure state tracking
- Plan: Preview of changes before applying
- Apply: Execute the planned changes
Create a directory for Terraform files:
mkdir terraform-lab
cd terraform-labCreate main.tf:
# Configure Terraform
terraform {
required_version = ">= 1.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
# Configure AWS Provider
provider "aws" {
region = var.aws_region
}
# Create S3 Bucket
resource "aws_s3_bucket" "main" {
bucket = "an-2026-tf-${var.bucket_suffix}"
tags = {
Name = "an-2026-tf-${var.bucket_suffix}"
Environment = "Lab"
ManagedBy = "Terraform"
}
}
# Enable Versioning
resource "aws_s3_bucket_versioning" "main" {
bucket = aws_s3_bucket.main.id
versioning_configuration {
status = "Enabled"
}
}
# Enable Encryption
resource "aws_s3_bucket_server_side_encryption_configuration" "main" {
bucket = aws_s3_bucket.main.id
rule {
apply_server_side_encryption_by_default {
sse_algorithm = "AES256"
}
}
}
# Block Public Access
resource "aws_s3_bucket_public_access_block" "main" {
bucket = aws_s3_bucket.main.id
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}
# Lifecycle Policy
resource "aws_s3_bucket_lifecycle_configuration" "main" {
bucket = aws_s3_bucket.main.id
rule {
id = "delete-old-versions"
status = "Enabled"
noncurrent_version_expiration {
noncurrent_days = 30
}
}
}Create variables.tf:
variable "aws_region" {
description = "AWS region for resources"
type = string
default = "us-east-1"
}
variable "bucket_suffix" {
description = "Suffix for bucket name (e.g., your initials)"
type = string
validation {
condition = can(regex("^[a-z0-9-]+$", var.bucket_suffix))
error_message = "Bucket suffix must contain only lowercase letters, numbers, and hyphens."
}
}Create outputs.tf:
output "bucket_name" {
description = "Name of the S3 bucket"
value = aws_s3_bucket.main.id
}
output "bucket_arn" {
description = "ARN of the S3 bucket"
value = aws_s3_bucket.main.arn
}
output "bucket_domain_name" {
description = "Domain name of the S3 bucket"
value = aws_s3_bucket.main.bucket_domain_name
}
output "bucket_region" {
description = "Region of the S3 bucket"
value = aws_s3_bucket.main.region
}Create terraform.tfvars:
bucket_suffix = "jag" # Change to your initials
aws_region = "us-east-1"Initialize the working directory and download providers:
terraform initThis creates:
.terraform/directory with provider plugins.terraform.lock.hclfile with dependency versions
Format code to canonical style:
terraform fmtValidate configuration:
terraform validatePreview what Terraform will create:
terraform planReview the output:
- Resources Terraform will create (green
+) - Resources Terraform will modify (yellow
~) - Resources Terraform will destroy (red
-)
Create the infrastructure:
terraform applyType yes when prompted to confirm.
Terraform will:
- Create the S3 bucket
- Enable versioning
- Configure encryption
- Block public access
- Set lifecycle policy
- Display outputs
Terraform tracks infrastructure in a state file:
# List resources in state
terraform state list
# Show details of a resource
terraform state show aws_s3_bucket.mainView outputs:
terraform outputGet specific output:
terraform output bucket_name- Go to AWS Console → S3
- Find your bucket
an-2026-tf-[your-initials] - Verify all configurations match your Terraform code
Let's add a tag. Update main.tf:
resource "aws_s3_bucket" "main" {
bucket = "an-2026-tf-${var.bucket_suffix}"
tags = {
Name = "an-2026-tf-${var.bucket_suffix}"
Environment = "Lab"
ManagedBy = "Terraform"
Course = "Cloud Architecture" # New tag
}
}Plan the change:
terraform planApply the change:
terraform applyWhen done, destroy all resources:
terraform destroyType yes to confirm.
Terraform will:
- Remove lifecycle policy
- Remove public access block
- Remove encryption configuration
- Remove versioning
- Delete the bucket
Method: Terraform
Key Features:
- Cloud-agnostic (AWS, Azure, GCP, etc.)
- HCL (HashiCorp Configuration Language)
- State management
- Plan before apply
- Module system for reusability
- Large provider ecosystem
Advantages:
- Multi-cloud support
- Cleaner, more readable syntax
- Strong community and modules
- Faster execution
- Better state management
Disadvantages:
- Requires separate tool installation
- State file management complexity
- Learning curve for HCL
- No automatic rollback
| Aspect | CloudFormation | Terraform |
|---|---|---|
| Provider | AWS | HashiCorp |
| Cloud Support | AWS only | Multi-cloud |
| Language | YAML/JSON | HCL |
| State Management | AWS-managed | Local/Remote |
| Cost | Free | Free (Enterprise paid) |
| Rollback | Automatic | Manual |
| Preview Changes | Change Sets | Plan |
| Learning Curve | Medium | Medium-High |
| Community | AWS-focused | Large, multi-cloud |
| Best For | AWS-only projects | Multi-cloud, flexibility |
- Version Control: Store all IaC code in Git
- Modularization: Break large configurations into reusable modules
- Documentation: Comment complex logic and decisions
- Naming Conventions: Use consistent, descriptive names
- Secrets Management: Never hardcode credentials
- Testing: Validate before applying to production
- State Management: Secure and backup state files
- Code Review: Peer review infrastructure changes
- Use parameters for reusability
- Leverage intrinsic functions (!Ref, !Sub, !GetAtt)
- Export outputs for cross-stack references
- Use nested stacks for complex architectures
- Enable termination protection for production
- Use change sets to preview updates
- Use remote state (S3 + DynamoDB)
- Organize code with modules
- Use workspaces for environments
- Lock state during operations
- Use variables and locals effectively
- Implement consistent tagging strategy
- Use data sources for existing resources
Create a more complex infrastructure with both tools:
Requirements:
- S3 bucket for static website hosting
- CloudFront distribution
- Route53 DNS record
- ACM certificate
Try implementing this with both CloudFormation and Terraform to compare the experience.
Delete all resources you created:
aws cloudformation delete-stack --stack-name s3-bucket-stackcd terraform-lab
terraform destroyOpen the AWS Console and verify that no lab resources remain.
- IaC enables infrastructure automation - Define infrastructure as code
- Version control your infrastructure - Track changes like application code
- CloudFormation is AWS-native - Deep integration but AWS-only
- Terraform is cloud-agnostic - Flexibility across multiple providers
- Both tools have strengths - Choose based on requirements
- State management is critical - Understand how each tool tracks resources
- Plan before apply - Always preview changes before execution
- IaC is essential for DevOps - Foundation for CI/CD and automation
Issue: Stack creation fails Solution: Check Events tab for error details. Common issues:
- Bucket name already exists (must be globally unique)
- Insufficient permissions
- Invalid template syntax
Issue: Stack stuck in DELETE_FAILED Solution: Manually delete resources blocking deletion (e.g., non-empty S3 bucket), then retry.
Issue: "Error: configuring Terraform AWS Provider"
Solution: Verify that your AWS credentials are correct. Check ~/.aws/credentials.
Issue: "Error acquiring the state lock" Solution: Another Terraform process is running. Wait or force-unlock (use carefully):
terraform force-unlock <lock-id>Issue: State file conflicts Solution: Use remote state with locking (S3 + DynamoDB) for team environments.
After completing this lab, you should:
- Understand Infrastructure as Code principles
- Be able to write CloudFormation templates
- Be comfortable with Terraform configurations
- Know when to use each tool
- Apply IaC best practices
You're now ready to build more complex cloud architectures using IaC!