Skip to content

Infrastructure as Code

Verified

Define and manage cloud infrastructure with code. Use when writing Terraform, CloudFormation, or Pulumi configs, managing state, planning deployments, setting up networking/compute/storage resources, or debugging infrastructure drift.

1,271

Install

Claude Code

Add to .claude/skills/

About This Skill

# Infrastructure as Code

Define, deploy, and manage cloud infrastructure using declarative configuration. Covers Terraform (multi-cloud), AWS CloudFormation, and Pulumi (code-first), with patterns for compute, networking, storage, databases, and state management.

When to Use

  • Setting up cloud infrastructure (VPCs, EC2, Lambda, S3, RDS, etc.)
  • Writing or modifying Terraform configurations
  • Managing Terraform state (remote backends, workspaces, imports)
  • Creating CloudFormation templates
  • Using Pulumi for infrastructure in TypeScript/Python/Go
  • Planning and previewing infrastructure changes safely
  • Debugging drift between declared state and actual resources
  • Setting up multi-environment deployments (dev/staging/prod)

Terraform

Quick Start

```bash # Install: https://developer.hashicorp.com/terraform/install

# Initialize a project mkdir infra && cd infra terraform init

# Core workflow terraform plan # Preview changes (safe, read-only) terraform apply # Apply changes (creates/modifies resources) terraform destroy # Tear down all resources

# Format and validate terraform fmt -recursive # Auto-format all .tf files terraform validate # Check syntax and config validity ```

Project Structure

``` infra/ main.tf # Primary resources variables.tf # Input variable declarations outputs.tf # Output values providers.tf # Provider configuration terraform.tfvars # Variable values (don't commit secrets) backend.tf # Remote state configuration modules/ vpc/ main.tf variables.tf outputs.tf compute/ main.tf variables.tf outputs.tf ```

Provider Configuration

```hcl # providers.tf terraform { required_version = ">= 1.5" required_providers { aws = { source = "hashicorp/aws" version = "~> 5.0" } } }

provider "aws" { region = var.aws_region

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

Variables and Outputs

```hcl # variables.tf variable "aws_region" { type = string default = "us-east-1" description = "AWS region for all resources" }

variable "environment" { type = string description = "Deployment environment" validation { condition = contains(["dev", "staging", "prod"], var.environment) error_message = "Environment must be dev, staging, or prod." } }

variable "instance_type" { type = string default = "t3.micro" }

variable "db_password" { type = string sensitive = true description = "Database password (pass via TF_VAR_db_password env var)" }

# outputs.tf output "vpc_id" { value = aws_vpc.main.id description = "VPC ID" }

output "api_endpoint" { value = aws_lb.api.dns_name } ```

VPC + Networking

```hcl # Networking module resource "aws_vpc" "main" { cidr_block = "10.0.0.0/16" enable_dns_support = true enable_dns_hostnames = true

tags = { Name = "${var.project_name}-vpc" } }

resource "aws_subnet" "public" { count = 2 vpc_id = aws_vpc.main.id cidr_block = "10.0.${count.index + 1}.0/24" availability_zone = data.aws_availability_zones.available.names[count.index]

map_public_ip_on_launch = true tags = { Name = "${var.project_name}-public-${count.index + 1}" } }

resource "aws_subnet" "private" { count = 2 vpc_id = aws_vpc.main.id cidr_block = "10.0.${count.index + 10}.0/24" availability_zone = data.aws_availability_zones.available.names[count.index]

tags = { Name = "${var.project_name}-private-${count.index + 1}" } }

resource "aws_internet_gateway" "main" { vpc_id = aws_vpc.main.id }

resource "aws_route_table" "public" { vpc_id = aws_vpc.main.id

route { cidr_block = "0.0.0.0/0" gateway_id = aws_internet_gateway.main.id } }

resource "aws_route_table_association" "public" { count = 2 subnet_id = aws_subnet.public[count.index].id route_table_id = aws_route_table.public.id }

resource "aws_security_group" "web" { name_prefix = "${var.project_name}-web-" vpc_id = aws_vpc.main.id

ingress { from_port = 80 to_port = 80 protocol = "tcp" cidr_blocks = ["0.0.0.0/0"] }

ingress { from_port = 443 to_port = 443 protocol = "tcp" cidr_blocks = ["0.0.0.0/0"] }

egress { from_port = 0 to_port = 0 protocol = "-1" cidr_blocks = ["0.0.0.0/0"] } }

data "aws_availability_zones" "available" { state = "available" } ```

Compute (EC2)

```hcl resource "aws_instance" "app" { ami = data.aws_ami.ubuntu.id instance_type = var.instance_type subnet_id = aws_subnet.public[0].id vpc_security_group_ids = [aws_security_group.web.id] key_name = var.key_pair_name

user_data = <<-EOF #!/bin/bash apt-get update apt-get install -y docker.io systemctl start docker docker run -d -p 80:8080 ${var.docker_image} EOF

tags = { Name = "${var.project_name}-app" } }

data "aws_ami" "ubuntu" { most_recent = true owners = ["099720109477"] # Canonical

filter { name = "name" values = ["ubuntu/images/hvm-ssd/ubuntu-*-24.04-amd64-server-*"] } } ```

S3 + Static Website

```hcl resource "aws_s3_bucket" "website" { bucket = "${var.project_name}-website" }

resource "aws_s3_bucket_website_configuration" "website" { bucket = aws_s3_bucket.website.id

index_document { suffix = "index.html" } error_document { key = "error.html" } }

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

block_public_acls = false block_public_policy = false ignore_public_acls = false restrict_public_buckets = false }

resource "aws_s3_bucket_policy" "website" { bucket = aws_s3_bucket.website.id

policy = jsonencode({ Version = "2012-10-17" Statement = [{ Sid = "PublicRead" Effect = "Allow" Principal = "*" Action = "s3:GetObject" Resource = "${aws_s3_bucket.website.arn}/*" }] })

depends_on = [aws_s3_bucket_public_access_block.website] } ```

RDS Database

```hcl resource "aws_db_subnet_group" "main" { name = "${var.project_name}-db" subnet_ids = aws_subnet.private[*].id }

resource "aws_security_group" "db" { name_prefix = "${var.project_name}-db-" vpc_id = aws_vpc.main.id

ingress { from_port = 5432 to_port = 5432 protocol = "tcp" security_groups = [aws_security_group.web.id] } }

resource "aws_db_instance" "main" { identifier = "${var.project_name}-db" engine = "postgres" engine_version = "16.1" instance_class = "db.t3.micro" allocated_storage = 20

db_name = var.db_name username = var.db_username password = var.db_password

db_subnet_group_name = aws_db_subnet_group.main.name vpc_security_group_ids = [aws_security_group.db.id]

backup_retention_period = 7 skip_final_snapshot = var.environment != "prod" deletion_protection = var.environment == "prod" } ```

Lambda Function

```hcl data "archive_file" "lambda_zip" { type = "zip" source_dir = "${path.module}/lambda" output_path = "${path.module}/lambda.zip" }

resource "aws_lambda_function" "api" { function_name = "${var.project_name}-api" filename = data.archive_file.lambda_zip.output_path source_code_hash = data.archive_file.lambda_zip.output_base64sha256 handler = "index.handler" runtime = "nodejs20.x" timeout = 30

role = aws_iam_role.lambda_exec.arn

environment { variables = { DB_HOST = aws_db_instance.main.endpoint ENVIRONMENT = var.environment } } }

resource "aws_iam_role" "lambda_exec" { name = "${var.project_name}-lambda-exec"

assume_role_policy = jsonencode({ Version = "2012-10-17" Statement = [{ Action = "sts:AssumeRole" Effect = "Allow" Principal = { Service = "lambda.amazonaws.com" } }] }) }

resource "aws_iam_role_policy_attachment" "lambda_basic" { role = aws_iam_role.lambda_exec.name policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" } ```

State Management

```hcl # backend.tf - Remote state in S3 terraform { backend "s3" { bucket = "my-terraform-state" key = "project/terraform.tfstate" region = "us-east-1" dynamodb_table = "terraform-locks" encrypt = true } } ```

```bash # State operations terraform state list # List all resources in state terraform state show aws_instance.app # Show resource details terraform state mv aws_instance.app aws_instance.web # Rename resource terraform state rm aws_instance.old # Remove from state (doesn't destroy)

# Import existing resource into Terraform terraform import aws_instance.app i-1234567890abcdef0

# Workspaces (multiple environments, same config) terraform workspace new dev terraform workspace new staging terraform workspace new prod terraform workspace select dev terraform workspace list ```

Multi-Environment Pattern

```hcl # Use workspaces + tfvars files # terraform.tfvars (default) # env/dev.tfvars # env/staging.tfvars # env/prod.tfvars

# Apply for specific environment # terraform apply -var-file=env/prod.tfvars ```

```bash # Environment-specific apply ENV=${1:-dev} terraform workspace select "$ENV" || terraform workspace new "$ENV" terraform apply -var-file="env/$ENV.tfvars" ```

AWS CloudFormation

Template Structure

```yaml # cloudformation.yaml AWSTemplateFormatVersion: '2010-09-09' Description: My application stack

Parameters: Environment: Type: String AllowedValues: [dev, staging, prod] Default: dev InstanceType: Type: String Default: t3.micro

Conditions: IsProd: !Equals [!Ref Environment, prod]

Resources: VPC: Type: AWS::EC2::VPC Properties: CidrBlock: 10.0.0.0/16 EnableDnsSupport: true EnableDnsHostnames: true Tags: - Key: Name Value: !Sub "${AWS::StackName}-vpc"

PublicSubnet: Type: AWS::EC2::Subnet Properties: VpcId: !Ref VPC CidrBlock: 10.0.1.0/24 MapPublicIpOnLaunch: true Tags: - Key: Name Value: !Sub "${AWS::StackName}-public"

AppInstance: Type: AWS::EC2::Instance Properties: InstanceType: !Ref InstanceType SubnetId: !Ref PublicSubnet ImageId: !FindInMap [RegionAMI, !Ref "AWS::Region", ubuntu]

Database: Type: AWS::RDS::DBInstance Condition: IsProd DeletionPolicy: Snapshot Properties: Engine: postgres DBInstanceClass: db.t3.micro AllocatedStorage: 20 MasterUsername: admin MasterUserPassword: !Ref DBPassword

Outputs: VpcId: Value: !Ref VPC Export: Name: !Sub "${AWS::StackName}-VpcId" InstanceIP: Value: !GetAtt AppInstance.PublicIp ```

CloudFormation CLI

```bash # Validate template aws cloudformation validate-template --template-body file://cloudformation.yaml

# Create stack aws cloudformation create-stack \ --stack-name myapp-dev \ --template-body file://cloudformation.yaml \ --parameters ParameterKey=Environment,ParameterValue=dev \ --capabilities CAPABILITY_IAM

# Update stack aws cloudformation update-stack \ --stack-name myapp-dev \ --template-body file://cloudformation.yaml \ --parameters ParameterKey=Environment,ParameterValue=dev

# Preview changes (changeset) aws cloudformation create-change-set \ --stack-name myapp-dev \ --change-set-name update-1 \ --template-body file://cloudformation.yaml

aws cloudformation describe-change-set \ --stack-name myapp-dev \ --change-set-name update-1

# Delete stack aws cloudformation delete-stack --stack-name myapp-dev

# List stacks aws cloudformation list-stacks --stack-status-filter CREATE_COMPLETE UPDATE_COMPLETE

# Stack events (debugging) aws cloudformation describe-stack-events --stack-name myapp-dev | head -50 ```

Pulumi (Code-First IaC)

Quick Start (TypeScript)

```bash # Install: https://www.pulumi.com/docs/install/ pulumi new aws-typescript

# Core workflow pulumi preview # Preview changes pulumi up # Apply changes pulumi destroy # Tear down pulumi stack ls # List stacks ```

TypeScript Example

```typescript // index.ts import * as pulumi from "@pulumi/pulumi"; import * as aws from "@pulumi/aws";

const config = new pulumi.Config(); const environment = config.require("environment");

// VPC const vpc = new aws.ec2.Vpc("main", { cidrBlock: "10.0.0.0/16", enableDnsSupport: true, enableDnsHostnames: true, tags: { Name: `myapp-${environment}-vpc` }, });

// Public subnet const publicSubnet = new aws.ec2.Subnet("public", { vpcId: vpc.id, cidrBlock: "10.0.1.0/24", mapPublicIpOnLaunch: true, tags: { Name: `myapp-${environment}-public` }, });

// S3 bucket const bucket = new aws.s3.Bucket("data", { bucket: `myapp-${environment}-data`, versioning: { enabled: true }, });

// Lambda function const lambdaRole = new aws.iam.Role("lambda-role", { assumeRolePolicy: JSON.stringify({ Version: "2012-10-17", Statement: [{ Action: "sts:AssumeRole", Effect: "Allow", Principal: { Service: "lambda.amazonaws.com" }, }], }), });

const lambdaFunc = new aws.lambda.Function("api", { runtime: "nodejs20.x", handler: "index.handler", role: lambdaRole.arn, code: new pulumi.asset.FileArchive("./lambda"), environment: { variables: { BUCKET_NAME: bucket.id, ENVIRONMENT: environment, }, }, });

// Outputs export const vpcId = vpc.id; export const bucketName = bucket.id; export const lambdaArn = lambdaFunc.arn; ```

Python Example

```python # __main__.py import pulumi import pulumi_aws as aws

config = pulumi.Config() environment = config.require("environment")

vpc = aws.ec2.Vpc("main", cidr_block="10.0.0.0/16", enable_dns_support=True, enable_dns_hostnames=True, tags={"Name": f"myapp-{environment}-vpc"})

bucket = aws.s3.Bucket("data", bucket=f"myapp-{environment}-data", versioning=aws.s3.BucketVersioningArgs(enabled=True))

pulumi.export("vpc_id", vpc.id) pulumi.export("bucket_name", bucket.id) ```

Pulumi State and Stacks

```bash # Create per-environment stacks pulumi stack init dev pulumi stack init staging pulumi stack init prod

# Switch stack pulumi stack select dev

# Set config per stack pulumi config set environment dev pulumi config set aws:region us-east-1 pulumi config set --secret dbPassword 'my-secret-pass'

# Stack references (cross-stack) # In consuming stack: const infra = new pulumi.StackReference("org/infra/prod"); const vpcId = infra.getOutput("vpcId"); ```

Debugging Infrastructure

Terraform plan issues

```bash # Detailed plan output terraform plan -out=plan.tfplan terraform show plan.tfplan terraform show -json plan.tfplan | jq '.resource_changes[] | {address, change: .change.actions}'

# Debug mode TF_LOG=DEBUG terraform plan 2> debug.log

# Check for drift terraform plan -refresh-only

# Force refresh state terraform apply -refresh-only ```

Common issues

```bash # Resource stuck in "tainted" state terraform untaint aws_instance.app

# State locked (another apply running or crashed) terraform force-unlock LOCK_ID

# Provider version conflict terraform providers lock # Generate lock file terraform init -upgrade # Upgrade providers

# Circular dependency # Error: "Cycle" in terraform plan # Fix: use depends_on explicitly, or break the cycle with data sources ```

Cost estimation

```bash # Infracost (estimates monthly cost from Terraform plans) # Install: https://www.infracost.io/docs/ infracost breakdown --path . infracost diff --path . --compare-to infracost-base.json ```

Tips

  • Always run `terraform plan` before `apply`. Read the plan output carefully — especially lines showing `destroy` or `replace`.
  • Use remote state from day one. Local state files get lost, can't be shared, and have no locking.
  • Tag everything. At minimum: Project, Environment, ManagedBy. Tags make cost tracking and cleanup possible.
  • Never store secrets in `.tf` files or `terraform.tfvars`. Use environment variables (`TF_VAR_name`), secrets managers, or Vault.
  • Use `prevent_destroy` lifecycle rules on stateful resources (databases, S3 buckets with data) to prevent accidental deletion.
  • Pin provider versions (`~> 5.0` not `>= 5.0`) to avoid surprise breaking changes.
  • For multi-environment setups, prefer workspaces + var files over duplicated configurations.
  • CloudFormation change sets are the equivalent of `terraform plan` — always create one before updating a stack.
  • Pulumi's advantage is using real programming languages (loops, conditionals, type checking). Use it when Terraform's HCL feels limiting.

Use Cases

  • Write Terraform, CloudFormation, or Pulumi configurations for cloud infrastructure
  • Manage infrastructure state and plan deployments across environments
  • Set up networking, compute, and storage resources declaratively
  • Automate cloud infrastructure provisioning with best-practice patterns
  • Review and optimize Infrastructure as Code templates for security and cost

Pros & Cons

Pros

  • + Compatible with multiple platforms including claude-code, openclaw
  • + Well-documented with detailed usage instructions and examples
  • + Strong community adoption with a large number of downloads
  • + Open source with permissive licensing

Cons

  • - No built-in analytics or usage metrics dashboard
  • - Configuration may require familiarity with ai & machine learning concepts

Frequently Asked Questions

What does Infrastructure as Code do?

Define and manage cloud infrastructure with code. Use when writing Terraform, CloudFormation, or Pulumi configs, managing state, planning deployments, setting up networking/compute/storage resources, or debugging infrastructure drift.

What platforms support Infrastructure as Code?

Infrastructure as Code is available on Claude Code, OpenClaw.

What are the use cases for Infrastructure as Code?

Write Terraform, CloudFormation, or Pulumi configurations for cloud infrastructure. Manage infrastructure state and plan deployments across environments. Set up networking, compute, and storage resources declaratively.

Stay Updated on Agent Skills

Get weekly curated skills + safety alerts