|$ curl https://forge-ai.dev/api/markdown?path=docs/ci/iac
$cat docs/ci/cd-—-infrastructure-as-code.md
updated Recently·14 min read·published

CI/CD — Infrastructure as Code

CI/CDAdvanced
Introduction

Infrastructure as Code (IaC) is the practice of managing and provisioning infrastructure through machine-readable configuration files rather than manual processes. IaC enables version-controlled, repeatable, and auditable infrastructure changes.

This section covers Terraform, Pulumi, and AWS CloudFormation — the three dominant IaC tools — along with state management, module design, testing, and integrating IaC into CI/CD pipelines.

What is Infrastructure as Code?

IaC treats infrastructure configuration as software. Instead of clicking through a cloud console or running ad-hoc CLI commands, you define resources in code, commit that code to Git, and apply it through automated pipelines.

ApproachDescription
ImperativeStep-by-step commands to create resources (CLI scripts)
DeclarativeDescribe desired end state; the tool figures out the steps (Terraform, CloudFormation)
ProgrammaticUse a general-purpose language to define infrastructure (Pulumi, CDK)

info

Declarative IaC is strongly preferred for most use cases. It enables plan/diff workflows (see what will change before applying), automatic drift detection, and idempotent operations.
Terraform

Terraform by HashiCorp is the most widely adopted IaC tool. It uses HCL (HashiCorp Configuration Language) to define resources and supports all major cloud providers through a plugin-based provider system.

main.tf
HCL
1# main.tf — Provision an AWS ECS service
2terraform {
3 required_version = ">= 1.6"
4 required_providers {
5 aws = {
6 source = "hashicorp/aws"
7 version = "~> 5.0"
8 }
9 }
10
11 backend "s3" {
12 bucket = "myorg-terraform-state"
13 key = "production/vpc/terraform.tfstate"
14 region = "us-east-1"
15 dynamodb_table = "terraform-locks"
16 encrypt = true
17 }
18}
19
20provider "aws" {
21 region = var.aws_region
22}
23
24variable "aws_region" {
25 type = string
26 default = "us-east-1"
27}
28
29resource "aws_vpc" "main" {
30 cidr_block = "10.0.0.0/16"
31 enable_dns_hostnames = true
32 enable_dns_support = true
33
34 tags = {
35 Name = "production-vpc"
36 Environment = "production"
37 }
38}
39
40resource "aws_subnet" "public" {
41 count = 3
42 vpc_id = aws_vpc.main.id
43 cidr_block = cidrsubnet(aws_vpc.main.cidr_block, 8, count.index)
44 availability_zone = data.aws_availability_zones.available.names[count.index]
45 map_public_ip_on_launch = true
46}
47
48output "vpc_id" {
49 value = aws_vpc.main.id
50}
terraform-commands.sh
Bash
1# Terraform workflow
2terraform init # Download providers and initialize backend
3terraform plan # Preview changes (always run before apply)
4terraform apply # Apply the planned changes
5terraform destroy # Tear down all managed resources
6
7# Import existing infrastructure
8terraform import aws_vpc.main vpc-abc123
9
10# Format and validate
11terraform fmt -recursive
12terraform validate

warning

Always run terraform plan beforeterraform apply. Review the planned changes carefully — a misconfigured apply can destroy production resources in seconds.
Pulumi

Pulumi lets you define infrastructure using general-purpose programming languages (TypeScript, Python, Go, C#). You get the full power of loops, conditionals, functions, and type checking — no DSL required.

index.ts
TypeScript
1import * as pulumi from "@pulumi/pulumi";
2import * as aws from "@pulumi/aws";
3
4// Create a VPC with subnets
5const vpc = new aws.ec2.Vpc("main", {
6 cidrBlock: "10.0.0.0/16",
7 enableDnsHostnames: true,
8 tags: { Name: "production-vpc" },
9});
10
11const availabilityZones = aws.getAvailabilityZones();
12
13const publicSubnets = availabilityZones.names.slice(0, 3).map(
14 (az, i) =>
15 new aws.ec2.Subnet(`public-${i}`, {
16 vpcId: vpc.id,
17 cidrBlock: `10.0.${i}.0/24`,
18 availabilityZone: az,
19 mapPublicIpOnLaunch: true,
20 tags: { Name: `public-subnet-${i}` },
21 })
22);
23
24// Export the VPC ID
25export const vpcId = vpc.id;
pulumi-commands.sh
Bash
1# Pulumi workflow
2pulumi login s3://myorg-pulumi-state
3pulumi stack select production
4pulumi install # Install dependencies
5pulumi preview # Preview changes
6pulumi up # Apply changes
7pulumi destroy # Tear down

best practice

Pulumi is ideal when your infrastructure requires complex logic — dynamic resource creation based on input parameters, loops over configuration arrays, or conditional resource creation. Use its testing framework to validate infrastructure code with unit tests.
AWS CloudFormation & CDK

CloudFormation is AWS's native IaC service. It uses YAML/JSON templates and is tightly integrated with AWS services. The AWS CDK (Cloud Development Kit) lets you define CloudFormation templates using TypeScript, Python, or other languages.

template.yaml
YAML
1AWSTemplateFormatVersion: "2010-09-09"
2Description: Production ECS Fargate Service
3
4Parameters:
5 Environment:
6 Type: String
7 Default: production
8 ImageUri:
9 Type: String
10
11Resources:
12 Cluster:
13 Type: AWS::ECS::Cluster
14 Properties:
15 ClusterName: !Sub "${Environment}-cluster"
16
17 Service:
18 Type: AWS::ECS::Service
19 DependsOn: ALBListener
20 Properties:
21 Cluster: !Ref Cluster
22 DesiredCount: 2
23 LaunchType: FARGATE
24 TaskDefinition: !Ref TaskDefinition
25 NetworkConfiguration:
26 AwsvpcConfiguration:
27 SecurityGroups: [!Ref ServiceSecurityGroup]
28 Subnets: !Ref PublicSubnets
29
30Outputs:
31 ClusterArn:
32 Value: !GetAtt Cluster.Arn

info

Use CloudFormation if you are purely on AWS and want native service integration (drift detection, change sets, StackSets). Use CDK to get the benefits of a programming language while still deploying CloudFormation under the hood.
State Management

State management tracks which resources are managed by IaC and their current configuration. Terraform stores state in a state file; losing or corrupting it can orphan resources and cause destructive applies.

Remote State

Store state in S3, GCS, or Azure Blob Storage. Never use local state files in team environments.

State Locking

Use DynamoDB (AWS) or similar to prevent concurrent state modifications that cause corruption.

State Encryption

Enable server-side encryption on the state bucket. State files contain secrets (database passwords, API keys).

remote-state.tf
HCL
1# Remote state with S3 backend
2terraform {
3 backend "s3" {
4 bucket = "myorg-terraform-state"
5 key = "production/vpc/terraform.tfstate"
6 region = "us-east-1"
7 dynamodb_table = "terraform-locks"
8 encrypt = true
9 }
10}
11
12# Reference outputs from another state file
13data "terraform_remote_state" "vpc" {
14 backend = "s3"
15 config = {
16 bucket = "myorg-terraform-state"
17 key = "production/vpc/terraform.tfstate"
18 region = "us-east-1"
19 }
20}
21
22# Use the VPC ID from the remote state
23resource "aws_ecs_service" "app" {
24 network_configuration {
25 subnets = data.terraform_remote_state.vpc.outputs.public_subnet_ids
26 }
27}

warning

Treat your state file as a secret. It contains resource IDs, configurations, and sometimes embedded sensitive values. Encrypt it at rest and in transit, restrict access with IAM, and back it up regularly.
Modules & Reusability

Terraform modules are reusable, composable packages of infrastructure code. A well-designed module encapsulates a set of related resources with a clean input/output interface.

modules-example.tf
HCL
1# modules/ecs-service/main.tf
2variable "service_name" {
3 type = string
4}
5
6variable "image_uri" {
7 type = string
8}
9
10variable "port" {
11 type = number
12 default = 80
13}
14
15variable "desired_count" {
16 type = number
17 default = 2
18}
19
20resource "aws_ecs_task_definition" "this" {
21 family = var.service_name
22 requires_compatibilities = ["FARGATE"]
23 network_mode = "awsvpc"
24 cpu = 256
25 memory = 512
26
27 container_definitions = jsonencode([{
28 name = var.service_name
29 image = var.image_uri
30 portMappings = [{
31 containerPort = var.port
32 protocol = "tcp"
33 }]
34 }])
35}
36
37output "task_definition_arn" {
38 value = aws_ecs_task_definition.this.arn
39}
40
41# Root module usage
42module "api_service" {
43 source = "./modules/ecs-service"
44 service_name = "api"
45 image_uri = "ghcr.io/myorg/api:v1.0.0"
46 port = 3000
47}
48
49module "worker_service" {
50 source = "./modules/ecs-service"
51 service_name = "worker"
52 image_uri = "ghcr.io/myorg/worker:v1.0.0"
53 desired_count = 1
54}

best practice

Publish reusable modules to a private registry (Terraform Cloud, GitHub Packages) and version them semantically. This gives teams a curated set of infrastructure building blocks that enforce standards across the organization.
IaC in CI/CD Pipelines

Integrating IaC into CI/CD ensures infrastructure changes go through the same review, testing, and approval processes as application code.

terraform-ci.yml
YAML
1name: Terraform Plan & Apply
2on:
3 pull_request:
4 paths: ["infrastructure/**"]
5 push:
6 branches: [main]
7 paths: ["infrastructure/**"]
8
9jobs:
10 plan:
11 runs-on: ubuntu-latest
12 steps:
13 - uses: actions/checkout@v4
14 - uses: hashicorp/setup-terraform@v3
15 with:
16 terraform_version: "1.7"
17
18 - name: Terraform Init
19 run: terraform init
20 working-directory: infrastructure
21
22 - name: Terraform Plan
23 run: terraform plan -out=tfplan
24 working-directory: infrastructure
25
26 - name: Comment plan on PR
27 if: github.event_name == 'pull_request'
28 uses: actions/github-script@v7
29 with:
30 script: |
31 const plan = execSync('terraform show -no-color tfplan', {
32 cwd: 'infrastructure'
33 });
34 github.rest.issues.createComment({
35 ...context.repo,
36 issue_number: context.issue.number,
37 body: `## Terraform Plan\n```\n${plan}\n```
38 });
39
40 apply:
41 if: github.ref == 'refs/heads/main'
42 needs: plan
43 runs-on: ubuntu-latest
44 environment: production
45 steps:
46 - uses: actions/checkout@v4
47 - uses: hashicorp/setup-terraform@v3
48 - run: terraform init
49 working-directory: infrastructure
50 - run: terraform apply -auto-approve
51 working-directory: infrastructure

info

Use GitHub Environments with required reviewers for production infrastructure changes. This adds a manual approval gate beforeterraform apply runs, preventing accidental infrastructure destruction.
Security & Secrets in IaC

Secrets in IaC require careful handling. Never hardcode credentials in configuration files. Use external secret managers and the appropriate provider integrations.

AWS: Use SSM Parameter Store or Secrets Manager

Reference secrets via data sources instead of embedding them in state.

Use Environment Variables for Provider Credentials

AWS_ACCESS_KEY_ID andAWS_SECRET_ACCESS_KEY as env vars, not in code. Prefer IAM roles in CI.

Scan for Hardcoded Secrets

Use tfsec, checkov, or trivy to scan IaC code for secrets, misconfigurations, and security violations before merging.

Testing Infrastructure Code

Infrastructure code should be tested like application code. Use static analysis, plan validation, and policy-as-code to catch issues before they reach production.

ToolTypePurpose
tfsec / trivyStatic analysisScan for security misconfigurations
checkovPolicy as codeEnforce organizational policies (e.g., encryption, tagging)
TerratestIntegration testingDeploy real infrastructure and validate with Go tests
conftest / OPAPolicy evaluationWrite Rego policies for plan output validation
Best Practices

✓ Use remote state with locking

Never use local state files in team environments. Store state in S3/GCS/Azure with DynamoDB or equivalent locking.

✓ Modularize infrastructure

Break infrastructure into reusable modules with clear interfaces. Publish modules to a registry for cross-team reuse.

✓ Plan before every apply

Review the plan output in CI before approving. Use policy-as-code to auto-approve safe changes and require manual review for risky ones.

✓ Tag all resources

Consistent tagging enables cost allocation, access control, and resource discovery. Enforce tags with policy-as-code.

$Blueprint — Engineering Documentation·Section ID: CICD-IAC·Revision: 1.0