AutoDeploy

Terraform Demos

Copy and modify these pre-built Terraform configurations for your infrastructure.

AWS

AWS S3 Bucket with Versioning

AWSUploaded

Create an S3 bucket with versioning, encryption, and lifecycle policy.

By System · Storage

main.tf · variables.tf · outputs.tf

Download
terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}

provider "aws" {
  region = var.aws_region
}

resource "aws_s3_bucket" "main" {
  bucket = var.bucket_name

  tags = {
    Name        = var.bucket_name
    Environment = var.environment
  }
}

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

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"
    }
  }
}

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
}

variable "bucket_name" {
  description = "S3 bucket name"
  type        = string
}

variable "aws_region" {
  description = "AWS region"
  type        = string
  default     = "us-east-1"
}

variable "environment" {
  description = "Environment"
  type        = string
  default     = "dev"
}

Troubleshooting & Solutions

Common Issues

  • Bucket name already exists
  • Cannot access bucket
  • Versioning not enabled

Solutions

  • Use a globally unique bucket name
  • Check IAM permissions
  • Verify versioning configuration

Troubleshooting Steps

  1. Verify bucket name is globally unique
  2. Check IAM permissions for S3
  3. Review bucket policy
  4. Ensure region is correct

Version compatibility: Terraform >= 1.0, AWS Provider >= 4.0

AWS

AWS VPC with Public and Private Subnets

AWSUploaded

A complete VPC setup with public and private subnets, an internet gateway, and a NAT gateway.

By System · Networking

main.tf · variables.tf · outputs.tf

Download
terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}

provider "aws" {
  region = var.aws_region
}

resource "aws_vpc" "main" {
  cidr_block           = var.vpc_cidr
  enable_dns_support   = true
  enable_dns_hostnames = true
  tags                 = { Name = "${var.environment}-vpc" }
}

resource "aws_internet_gateway" "main" {
  vpc_id = aws_vpc.main.id
  tags   = { Name = "${var.environment}-igw" }
}

resource "aws_subnet" "public" {
  count                   = length(var.public_subnet_cidrs)
  vpc_id                  = aws_vpc.main.id
  cidr_block              = var.public_subnet_cidrs[count.index]
  availability_zone       = var.availability_zones[count.index]
  map_public_ip_on_launch = true
  tags                    = { Name = "${var.environment}-public-${count.index}" }
}

resource "aws_subnet" "private" {
  count             = length(var.private_subnet_cidrs)
  vpc_id            = aws_vpc.main.id
  cidr_block        = var.private_subnet_cidrs[count.index]
  availability_zone = var.availability_zones[count.index]
  tags              = { Name = "${var.environment}-private-${count.index}" }
}

resource "aws_eip" "nat" {
  domain = "vpc"
}

resource "aws_nat_gateway" "main" {
  allocation_id = aws_eip.nat.id
  subnet_id     = aws_subnet.public[0].id
  tags          = { Name = "${var.environment}-nat" }
}

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" "private" {
  vpc_id = aws_vpc.main.id
  route {
    cidr_block     = "0.0.0.0/0"
    nat_gateway_id = aws_nat_gateway.main.id
  }
}

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

resource "aws_route_table_association" "private" {
  count          = length(aws_subnet.private)
  subnet_id      = aws_subnet.private[count.index].id
  route_table_id = aws_route_table.private.id
}

variable "aws_region" {
  type    = string
  default = "us-east-1"
}

variable "environment" {
  type    = string
  default = "dev"
}

variable "vpc_cidr" {
  type    = string
  default = "10.0.0.0/16"
}

variable "public_subnet_cidrs" {
  type    = list(string)
  default = ["10.0.1.0/24", "10.0.2.0/24"]
}

variable "private_subnet_cidrs" {
  type    = list(string)
  default = ["10.0.101.0/24", "10.0.102.0/24"]
}

variable "availability_zones" {
  type    = list(string)
  default = ["us-east-1a", "us-east-1b"]
}

Troubleshooting & Solutions

Common Issues

  • CIDR block overlap with an existing VPC
  • NAT gateway costs more than expected
  • Private subnet has no internet access

Solutions

  • Pick a non-overlapping CIDR range
  • Use one NAT gateway per environment, not per AZ, for cost-sensitive setups
  • Confirm the private route table points at the NAT gateway

Troubleshooting Steps

  1. Check CIDR blocks don't overlap with peered VPCs
  2. Verify route table associations are correct per subnet
  3. Confirm NAT gateway is in a public subnet
  4. Check availability zone names match the target region

Version compatibility: Terraform >= 1.0, AWS Provider >= 5.0

AWS

AWS EC2 Instance with Security Group

AWSUploaded

Launch an EC2 instance with a properly scoped security group and user data.

By System · Compute

main.tf · variables.tf · outputs.tf

Download
terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}

provider "aws" {
  region = var.aws_region
}

data "aws_ami" "ubuntu" {
  most_recent = true
  owners      = ["099720109477"]
  filter {
    name   = "name"
    values = ["ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-*"]
  }
}

resource "aws_security_group" "app" {
  name        = "${var.app_name}-sg"
  description = "Allow SSH and app traffic"
  vpc_id      = var.vpc_id

  ingress {
    from_port   = 22
    to_port     = 22
    protocol    = "tcp"
    cidr_blocks = [var.ssh_cidr]
  }

  ingress {
    from_port   = var.app_port
    to_port     = var.app_port
    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"]
  }
}

resource "aws_instance" "app" {
  ami                    = data.aws_ami.ubuntu.id
  instance_type          = var.instance_type
  subnet_id              = var.subnet_id
  vpc_security_group_ids = [aws_security_group.app.id]

  user_data = <<-EOF
    #!/bin/bash
    apt-get update -y
    apt-get install -y docker.io
    systemctl enable docker
    systemctl start docker
  EOF

  tags = { Name = var.app_name }
}

variable "aws_region" {
  type    = string
  default = "us-east-1"
}

variable "app_name" {
  type = string
}

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

variable "app_port" {
  type    = number
  default = 3000
}

variable "ssh_cidr" {
  type    = string
  default = "0.0.0.0/0"
}

variable "vpc_id" {
  type = string
}

variable "subnet_id" {
  type = string
}

Troubleshooting & Solutions

Common Issues

  • Instance unreachable via SSH
  • AMI not found in region
  • Security group too permissive

Solutions

  • Scope ssh_cidr to your own IP, not 0.0.0.0/0, in real deployments
  • AMI IDs are region-specific — use the data source, not a hardcoded ID
  • Restrict ingress rules to only the ports actually needed

Troubleshooting Steps

  1. Confirm the subnet has a route to an internet gateway if public access is needed
  2. Check the security group allows your actual source IP
  3. Verify the instance has a public IP if you're connecting externally
  4. Check user_data logs at /var/log/cloud-init-output.log on the instance

Version compatibility: Terraform >= 1.0, AWS Provider >= 5.0

AWS

AWS Application Load Balancer

AWSUploaded

Create an ALB with a target group and listener for HTTP traffic.

By System · Networking

main.tf · variables.tf · outputs.tf

Download
resource "aws_lb" "main" {
  name               = "${var.app_name}-alb"
  internal           = false
  load_balancer_type = "application"
  security_groups    = [aws_security_group.alb.id]
  subnets            = var.public_subnet_ids
}

resource "aws_security_group" "alb" {
  name   = "${var.app_name}-alb-sg"
  vpc_id = var.vpc_id

  ingress {
    from_port   = 80
    to_port     = 80
    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"]
  }
}

resource "aws_lb_target_group" "app" {
  name     = "${var.app_name}-tg"
  port     = var.app_port
  protocol = "HTTP"
  vpc_id   = var.vpc_id

  health_check {
    path                = "/health"
    healthy_threshold   = 2
    unhealthy_threshold = 3
    interval            = 15
  }
}

resource "aws_lb_listener" "http" {
  load_balancer_arn = aws_lb.main.arn
  port              = 80
  protocol          = "HTTP"

  default_action {
    type             = "forward"
    target_group_arn = aws_lb_target_group.app.arn
  }
}

variable "app_name" {
  type = string
}

variable "app_port" {
  type    = number
  default = 3000
}

variable "vpc_id" {
  type = string
}

variable "public_subnet_ids" {
  type = list(string)
}

Troubleshooting & Solutions

Common Issues

  • Target group shows unhealthy targets
  • ALB times out
  • 502 from the ALB

Solutions

  • Confirm the health check path actually exists and returns 2xx
  • Check the target's security group allows traffic from the ALB's security group
  • Verify the target's app is listening on the port the target group expects

Troubleshooting Steps

  1. Check target group health in the console/CLI
  2. Confirm the ALB's subnets are public with a route to an IGW
  3. Verify listener rules point at the right target group
  4. Check the target instance's own security group, not just the ALB's

Version compatibility: Terraform >= 1.0, AWS Provider >= 5.0

AWS

AWS Auto Scaling Group with Launch Template

AWSUploaded

Create an ASG with a launch template and target-tracking scaling policy.

By System · Compute

main.tf · variables.tf · outputs.tf

Download
resource "aws_launch_template" "app" {
  name_prefix   = "${var.app_name}-"
  image_id      = var.ami_id
  instance_type = var.instance_type

  network_interfaces {
    security_groups = [var.security_group_id]
  }

  tag_specifications {
    resource_type = "instance"
    tags          = { Name = var.app_name }
  }
}

resource "aws_autoscaling_group" "app" {
  name                = "${var.app_name}-asg"
  desired_capacity    = var.desired_capacity
  min_size            = var.min_size
  max_size            = var.max_size
  vpc_zone_identifier = var.subnet_ids
  target_group_arns   = var.target_group_arns

  launch_template {
    id      = aws_launch_template.app.id
    version = "$Latest"
  }
}

resource "aws_autoscaling_policy" "cpu" {
  name                   = "${var.app_name}-cpu-scaling"
  autoscaling_group_name = aws_autoscaling_group.app.name
  policy_type            = "TargetTrackingScaling"

  target_tracking_configuration {
    predefined_metric_specification {
      predefined_metric_type = "ASGAverageCPUUtilization"
    }
    target_value = 60
  }
}

variable "app_name" {
  type = string
}

variable "ami_id" {
  type = string
}

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

variable "security_group_id" {
  type = string
}

variable "subnet_ids" {
  type = list(string)
}

variable "target_group_arns" {
  type    = list(string)
  default = []
}

variable "desired_capacity" {
  type    = number
  default = 2
}

variable "min_size" {
  type    = number
  default = 1
}

variable "max_size" {
  type    = number
  default = 4
}

Troubleshooting & Solutions

Common Issues

  • Instances launch then immediately terminate
  • Scaling policy never triggers
  • New launch template version not picked up

Solutions

  • Check the launch template's AMI/instance type is valid in the target AZ
  • Confirm CloudWatch metrics are actually being published for the ASG
  • Reference "$Latest" (not a pinned version) if you want new instances to pick up template changes automatically

Troubleshooting Steps

  1. Check ASG activity history for the termination reason
  2. Verify the launch template's security group and subnet are compatible
  3. Confirm target tracking has enough data points before expecting it to scale
  4. Check IAM permissions for the ASG service-linked role

Version compatibility: Terraform >= 1.0, AWS Provider >= 5.0

AWS

AWS RDS PostgreSQL Database

AWSUploaded

Create a secure RDS PostgreSQL instance with automated backups.

By System · Storage

main.tf · variables.tf · outputs.tf

Download
resource "aws_db_subnet_group" "main" {
  name       = "${var.app_name}-db-subnets"
  subnet_ids = var.private_subnet_ids
}

resource "aws_security_group" "db" {
  name   = "${var.app_name}-db-sg"
  vpc_id = var.vpc_id

  ingress {
    from_port       = 5432
    to_port         = 5432
    protocol        = "tcp"
    security_groups = [var.app_security_group_id]
  }
}

resource "aws_db_instance" "main" {
  identifier              = "${var.app_name}-db"
  engine                  = "postgres"
  engine_version          = "16"
  instance_class          = var.instance_class
  allocated_storage       = 20
  storage_encrypted       = true
  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     = false
  final_snapshot_identifier = "${var.app_name}-final-snapshot"
}

variable "app_name" {
  type = string
}

variable "vpc_id" {
  type = string
}

variable "private_subnet_ids" {
  type = list(string)
}

variable "app_security_group_id" {
  type = string
}

variable "instance_class" {
  type    = string
  default = "db.t3.micro"
}

variable "db_name" {
  type = string
}

variable "db_username" {
  type      = string
  sensitive = true
}

variable "db_password" {
  type      = string
  sensitive = true
}

Troubleshooting & Solutions

Common Issues

  • Connection timeout from the app
  • Password shows up in state in plain text
  • Instance stuck in "creating" for a long time

Solutions

  • Confirm the app's security group is allowed in the DB security group's ingress rule
  • Use a secrets manager (AWS Secrets Manager, SSM Parameter Store) instead of a plain .tfvars value for db_password in real deployments
  • Large allocated_storage or Multi-AZ can add 10-20 minutes to creation — this is normal

Troubleshooting Steps

  1. Verify the DB subnet group spans private subnets in at least 2 AZs
  2. Check the app and DB are in the same VPC or have VPC peering
  3. Confirm storage_encrypted is compatible with your KMS key permissions
  4. Review RDS event logs in the console for the specific failure

Version compatibility: Terraform >= 1.0, AWS Provider >= 5.0

AWS

AWS Lambda Function with API Gateway

AWSUploaded

Create a Lambda function with an HTTP API Gateway integration.

By System · Other

main.tf · variables.tf · outputs.tf

Download
resource "aws_iam_role" "lambda" {
  name = "${var.function_name}-role"
  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Effect    = "Allow"
      Principal = { Service = "lambda.amazonaws.com" }
      Action    = "sts:AssumeRole"
    }]
  })
}

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

resource "aws_lambda_function" "app" {
  function_name = var.function_name
  role          = aws_iam_role.lambda.arn
  handler       = var.handler
  runtime       = var.runtime
  filename      = var.zip_path
  timeout       = 10
}

resource "aws_apigatewayv2_api" "http" {
  name          = "${var.function_name}-api"
  protocol_type = "HTTP"
}

resource "aws_apigatewayv2_integration" "lambda" {
  api_id                 = aws_apigatewayv2_api.http.id
  integration_type       = "AWS_PROXY"
  integration_uri        = aws_lambda_function.app.invoke_arn
  payload_format_version = "2.0"
}

resource "aws_apigatewayv2_route" "default" {
  api_id    = aws_apigatewayv2_api.http.id
  route_key = "$default"
  target    = "integrations/${aws_apigatewayv2_integration.lambda.id}"
}

resource "aws_apigatewayv2_stage" "default" {
  api_id      = aws_apigatewayv2_api.http.id
  name        = "$default"
  auto_deploy = true
}

resource "aws_lambda_permission" "apigw" {
  statement_id  = "AllowAPIGatewayInvoke"
  action        = "lambda:InvokeFunction"
  function_name = aws_lambda_function.app.function_name
  principal     = "apigateway.amazonaws.com"
  source_arn    = "${aws_apigatewayv2_api.http.execution_arn}/*/*"
}

variable "function_name" {
  type = string
}

variable "handler" {
  type    = string
  default = "index.handler"
}

variable "runtime" {
  type    = string
  default = "nodejs20.x"
}

variable "zip_path" {
  type = string
}

Troubleshooting & Solutions

Common Issues

  • 403 Forbidden calling the API
  • Lambda times out on cold start
  • Deployment package too large

Solutions

  • Confirm the aws_lambda_permission resource exists and its source_arn matches the actual API
  • Increase the timeout and memory for functions with heavy cold-start init work
  • Use Lambda layers or container images for large dependencies instead of one big zip

Troubleshooting Steps

  1. Check CloudWatch Logs for the specific Lambda invocation error
  2. Verify the IAM role has the AWSLambdaBasicExecutionRole policy attached
  3. Confirm payload_format_version matches what your handler code expects (1.0 vs 2.0 event shape differs)
  4. Test the function directly with `aws lambda invoke` before blaming API Gateway

Version compatibility: Terraform >= 1.0, AWS Provider >= 5.0

AWS

AWS CloudWatch Alarms and Dashboard

AWSUploaded

Create CloudWatch alarms and a dashboard for monitoring.

By System · Security

main.tf · variables.tf · outputs.tf

Download
resource "aws_cloudwatch_metric_alarm" "cpu_high" {
  alarm_name          = "${var.app_name}-cpu-high"
  comparison_operator = "GreaterThanThreshold"
  evaluation_periods   = 2
  metric_name          = "CPUUtilization"
  namespace            = "AWS/EC2"
  period               = 300
  statistic            = "Average"
  threshold            = 80
  alarm_actions        = [var.sns_topic_arn]
  dimensions = {
    InstanceId = var.instance_id
  }
}

resource "aws_cloudwatch_dashboard" "main" {
  dashboard_name = "${var.app_name}-dashboard"
  dashboard_body = jsonencode({
    widgets = [
      {
        type   = "metric"
        x      = 0
        y      = 0
        width  = 12
        height = 6
        properties = {
          metrics = [["AWS/EC2", "CPUUtilization", "InstanceId", var.instance_id]]
          period  = 300
          stat    = "Average"
          region  = var.aws_region
          title   = "CPU Utilization"
        }
      }
    ]
  })
}

variable "app_name" {
  type = string
}

variable "aws_region" {
  type    = string
  default = "us-east-1"
}

variable "instance_id" {
  type = string
}

variable "sns_topic_arn" {
  type = string
}

Troubleshooting & Solutions

Common Issues

  • Alarm never triggers
  • Too many false-positive alerts
  • Dashboard shows no data

Solutions

  • Confirm the dimensions block exactly matches the resource's actual metric dimensions
  • Tune evaluation_periods/threshold based on real baseline traffic, not guesses
  • Check the metric namespace and name are spelled exactly as AWS publishes them

Troubleshooting Steps

  1. Check alarm state history in the CloudWatch console
  2. Verify the SNS topic has a confirmed subscription
  3. Confirm the monitored resource is actually publishing that metric
  4. Check the region matches where the resource actually lives

Version compatibility: Terraform >= 1.0, AWS Provider >= 5.0

AWS

AWS Route53 Hosted Zone and Records

AWSUploaded

Create a Route53 hosted zone with DNS records.

By System · Networking

main.tf · variables.tf · outputs.tf

Download
resource "aws_route53_zone" "main" {
  name = var.domain_name
}

resource "aws_route53_record" "root" {
  zone_id = aws_route53_zone.main.zone_id
  name    = var.domain_name
  type    = "A"

  alias {
    name                   = var.alb_dns_name
    zone_id                = var.alb_zone_id
    evaluate_target_health = true
  }
}

resource "aws_route53_record" "www" {
  zone_id = aws_route53_zone.main.zone_id
  name    = "www.${var.domain_name}"
  type    = "CNAME"
  ttl     = 300
  records = [var.domain_name]
}

variable "domain_name" {
  type = string
}

variable "alb_dns_name" {
  type = string
}

variable "alb_zone_id" {
  type = string
}

Troubleshooting & Solutions

Common Issues

  • DNS not resolving after apply
  • Domain registered elsewhere doesn't pick up records
  • Certificate validation stuck pending

Solutions

  • DNS propagation can take up to 48 hours even after records are correct — check with `dig`, not just the browser
  • Update the domain's nameservers at the registrar to Route53's assigned NS records
  • Add the ACM validation CNAME record this hosted zone needs for certificate issuance

Troubleshooting Steps

  1. Run `dig NS <domain>` to confirm the registrar points at Route53's nameservers
  2. Run `dig <domain>` to confirm the A/CNAME record resolves
  3. Check the alias target's zone_id matches the actual ALB/CloudFront zone, not a guessed value
  4. Verify there's no conflicting record (e.g. both A and CNAME) at the same name

Version compatibility: Terraform >= 1.0, AWS Provider >= 5.0

AWS

EKS Cluster with Deployment

AWSUploaded

Create an EKS cluster with a sample deployment and service.

By System · Containers

main.tf · variables.tf · outputs.tf · deployment.yaml

Download
resource "aws_iam_role" "eks_cluster" {
  name = "${var.cluster_name}-role"
  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Effect    = "Allow"
      Principal = { Service = "eks.amazonaws.com" }
      Action    = "sts:AssumeRole"
    }]
  })
}

resource "aws_iam_role_policy_attachment" "eks_cluster_policy" {
  role       = aws_iam_role.eks_cluster.name
  policy_arn = "arn:aws:iam::aws:policy/AmazonEKSClusterPolicy"
}

resource "aws_eks_cluster" "main" {
  name     = var.cluster_name
  role_arn = aws_iam_role.eks_cluster.arn
  version  = var.kubernetes_version

  vpc_config {
    subnet_ids = var.subnet_ids
  }

  depends_on = [aws_iam_role_policy_attachment.eks_cluster_policy]
}

resource "aws_iam_role" "eks_nodes" {
  name = "${var.cluster_name}-node-role"
  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Effect    = "Allow"
      Principal = { Service = "ec2.amazonaws.com" }
      Action    = "sts:AssumeRole"
    }]
  })
}

resource "aws_iam_role_policy_attachment" "eks_worker_policy" {
  role       = aws_iam_role.eks_nodes.name
  policy_arn = "arn:aws:iam::aws:policy/AmazonEKSWorkerNodePolicy"
}

resource "aws_iam_role_policy_attachment" "eks_cni_policy" {
  role       = aws_iam_role.eks_nodes.name
  policy_arn = "arn:aws:iam::aws:policy/AmazonEKS_CNI_Policy"
}

resource "aws_eks_node_group" "main" {
  cluster_name    = aws_eks_cluster.main.name
  node_group_name = "${var.cluster_name}-nodes"
  node_role_arn   = aws_iam_role.eks_nodes.arn
  subnet_ids      = var.subnet_ids

  scaling_config {
    desired_size = var.desired_nodes
    min_size     = 1
    max_size     = var.max_nodes
  }
}

variable "cluster_name" {
  type = string
}

variable "kubernetes_version" {
  type    = string
  default = "1.30"
}

variable "subnet_ids" {
  type = list(string)
}

variable "desired_nodes" {
  type    = number
  default = 2
}

variable "max_nodes" {
  type    = number
  default = 4
}

Troubleshooting & Solutions

Common Issues

  • Nodes never join the cluster
  • kubectl can't authenticate
  • Cluster creation takes 15+ minutes

Solutions

  • Confirm subnets are tagged correctly for EKS auto-discovery and span at least 2 AZs
  • Run `aws eks update-kubeconfig --name <cluster>` to refresh local kubectl auth
  • 10-15 minute creation time is normal for EKS — this isn't a hang

Troubleshooting Steps

  1. Check node group status and any scaling activity errors
  2. Verify the node IAM role has all three required policies attached
  3. Confirm security groups allow node-to-control-plane communication
  4. Check `kubectl get nodes` after update-kubeconfig to confirm nodes actually joined

Version compatibility: Terraform >= 1.0, AWS Provider >= 5.0, Kubernetes 1.28+

Azure

Azure Virtual Network with Subnets

AzureUploaded

Create an Azure VNet with public and private subnets, an NSG, and a route table.

By System · Networking

main.tf · variables.tf · outputs.tf

Download
terraform {
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~> 3.0"
    }
  }
}

provider "azurerm" {
  features {}
}

resource "azurerm_resource_group" "main" {
  name     = "${var.app_name}-rg"
  location = var.location
}

resource "azurerm_virtual_network" "main" {
  name                = "${var.app_name}-vnet"
  address_space       = [var.vnet_cidr]
  location            = azurerm_resource_group.main.location
  resource_group_name = azurerm_resource_group.main.name
}

resource "azurerm_subnet" "public" {
  name                 = "public"
  resource_group_name  = azurerm_resource_group.main.name
  virtual_network_name = azurerm_virtual_network.main.name
  address_prefixes     = [var.public_subnet_cidr]
}

resource "azurerm_subnet" "private" {
  name                 = "private"
  resource_group_name  = azurerm_resource_group.main.name
  virtual_network_name = azurerm_virtual_network.main.name
  address_prefixes     = [var.private_subnet_cidr]
}

resource "azurerm_network_security_group" "main" {
  name                = "${var.app_name}-nsg"
  location            = azurerm_resource_group.main.location
  resource_group_name = azurerm_resource_group.main.name

  security_rule {
    name                       = "AllowSSH"
    priority                   = 100
    direction                  = "Inbound"
    access                     = "Allow"
    protocol                   = "Tcp"
    source_port_range          = "*"
    destination_port_range     = "22"
    source_address_prefix      = "*"
    destination_address_prefix = "*"
  }
}

resource "azurerm_subnet_network_security_group_association" "public" {
  subnet_id                 = azurerm_subnet.public.id
  network_security_group_id = azurerm_network_security_group.main.id
}

variable "app_name" {
  type = string
}

variable "location" {
  type    = string
  default = "East US"
}

variable "vnet_cidr" {
  type    = string
  default = "10.1.0.0/16"
}

variable "public_subnet_cidr" {
  type    = string
  default = "10.1.1.0/24"
}

variable "private_subnet_cidr" {
  type    = string
  default = "10.1.2.0/24"
}

Troubleshooting & Solutions

Common Issues

  • Subnet delegation conflicts
  • NSG rule priority collision
  • Resource group location mismatch

Solutions

  • Keep subnet address prefixes within the VNet's address_space and non-overlapping
  • Give every NSG rule a unique priority number — Azure rejects duplicates
  • Make sure every resource in the group uses the same location as the resource group unless intentionally distributed

Troubleshooting Steps

  1. Run `az network vnet subnet list` to confirm actual subnet ranges
  2. Check NSG associations with `az network nsg show`
  3. Verify service principal / CLI auth has Network Contributor role
  4. Confirm the subscription has quota for the resource types being created

Version compatibility: Terraform >= 1.0, AzureRM Provider >= 3.0

Azure

Azure Virtual Machine with Extensions

AzureUploaded

Create an Azure VM with a custom script extension and a managed disk.

By System · Compute

main.tf · variables.tf · outputs.tf

Download
resource "azurerm_network_interface" "main" {
  name                = "${var.app_name}-nic"
  location            = var.location
  resource_group_name = var.resource_group_name

  ip_configuration {
    name                          = "internal"
    subnet_id                     = var.subnet_id
    private_ip_address_allocation = "Dynamic"
  }
}

resource "azurerm_linux_virtual_machine" "main" {
  name                = var.app_name
  resource_group_name = var.resource_group_name
  location            = var.location
  size                = var.vm_size
  admin_username      = var.admin_username

  network_interface_ids = [azurerm_network_interface.main.id]

  admin_ssh_key {
    username   = var.admin_username
    public_key = var.ssh_public_key
  }

  os_disk {
    caching              = "ReadWrite"
    storage_account_type = "Standard_LRS"
  }

  source_image_reference {
    publisher = "Canonical"
    offer     = "0001-com-ubuntu-server-jammy"
    sku       = "22_04-lts-gen2"
    version   = "latest"
  }
}

resource "azurerm_virtual_machine_extension" "docker" {
  name                 = "install-docker"
  virtual_machine_id   = azurerm_linux_virtual_machine.main.id
  publisher            = "Microsoft.Azure.Extensions"
  type                 = "CustomScript"
  type_handler_version = "2.1"

  settings = jsonencode({
    commandToExecute = "apt-get update -y && apt-get install -y docker.io"
  })
}

variable "app_name" {
  type = string
}

variable "location" {
  type    = string
  default = "East US"
}

variable "resource_group_name" {
  type = string
}

variable "subnet_id" {
  type = string
}

variable "vm_size" {
  type    = string
  default = "Standard_B1s"
}

variable "admin_username" {
  type    = string
  default = "azureuser"
}

variable "ssh_public_key" {
  type = string
}

Troubleshooting & Solutions

Common Issues

  • SSH key format rejected
  • Extension fails silently
  • VM size unavailable in the region

Solutions

  • admin_ssh_key expects the exact OpenSSH public key format (ssh-rsa/ssh-ed25519 ...)
  • Check extension status with `az vm extension list` — failures log to /var/log/azure on the VM
  • Pick a VM size actually available in your target region/zone via `az vm list-sizes`

Troubleshooting Steps

  1. Verify the SSH public key has no trailing newline issues
  2. Check the custom script extension's exit code and stdout/stderr logs
  3. Confirm the subnet has enough free IP addresses
  4. Check the subscription's quota for the requested VM size family

Version compatibility: Terraform >= 1.0, AzureRM Provider >= 3.0

GCP

GCP VPC with Firewall Rules

GCPUploaded

Create a GCP VPC network with custom firewall rules for web and SSH access.

By System · Networking

main.tf · variables.tf · outputs.tf

Download
terraform {
  required_providers {
    google = {
      source  = "hashicorp/google"
      version = "~> 5.0"
    }
  }
}

provider "google" {
  project = var.project_id
  region  = var.region
}

resource "google_compute_network" "main" {
  name                    = "${var.app_name}-vpc"
  auto_create_subnetworks = false
}

resource "google_compute_subnetwork" "main" {
  name          = "${var.app_name}-subnet"
  ip_cidr_range = var.subnet_cidr
  region        = var.region
  network       = google_compute_network.main.id
}

resource "google_compute_firewall" "allow_ssh" {
  name    = "${var.app_name}-allow-ssh"
  network = google_compute_network.main.id

  allow {
    protocol = "tcp"
    ports    = ["22"]
  }

  source_ranges = [var.ssh_source_range]
  target_tags   = ["ssh"]
}

resource "google_compute_firewall" "allow_web" {
  name    = "${var.app_name}-allow-web"
  network = google_compute_network.main.id

  allow {
    protocol = "tcp"
    ports    = ["80", "443"]
  }

  source_ranges = ["0.0.0.0/0"]
  target_tags   = ["web"]
}

variable "project_id" {
  type = string
}

variable "region" {
  type    = string
  default = "us-central1"
}

variable "app_name" {
  type = string
}

variable "subnet_cidr" {
  type    = string
  default = "10.2.0.0/24"
}

variable "ssh_source_range" {
  type    = string
  default = "0.0.0.0/0"
}

Troubleshooting & Solutions

Common Issues

  • Firewall rule has no effect
  • Instance unreachable despite allow rule
  • auto_create_subnetworks conflict

Solutions

  • Firewall rules only apply to instances carrying the matching target_tags — confirm the instance has the tag
  • Check the instance actually has an external IP if connecting from outside GCP
  • Set auto_create_subnetworks = false and define subnets explicitly to avoid the default "legacy" network conflicting

Troubleshooting Steps

  1. Run `gcloud compute firewall-rules list` to confirm the rule is active
  2. Check the target instance's network tags match target_tags exactly
  3. Verify the subnet's region matches the instance's region
  4. Use `gcloud compute networks subnets list` to confirm CIDR ranges don't overlap

Version compatibility: Terraform >= 1.0, Google Provider >= 5.0

GCP

Google Compute Engine Instance

GCPUploaded

Create a GCE instance with a startup script and proper tags.

By System · Compute

main.tf · variables.tf · outputs.tf

Download
resource "google_compute_instance" "app" {
  name         = var.app_name
  machine_type = var.machine_type
  zone         = var.zone
  tags         = ["web", "ssh"]

  boot_disk {
    initialize_params {
      image = "ubuntu-os-cloud/ubuntu-2204-lts"
      size  = 20
    }
  }

  network_interface {
    network    = var.network_id
    subnetwork = var.subnetwork_id
    access_config {}
  }

  metadata_startup_script = <<-EOF
    #!/bin/bash
    apt-get update -y
    apt-get install -y docker.io
    systemctl enable docker
    systemctl start docker
  EOF

  service_account {
    scopes = ["cloud-platform"]
  }
}

variable "app_name" {
  type = string
}

variable "machine_type" {
  type    = string
  default = "e2-small"
}

variable "zone" {
  type    = string
  default = "us-central1-a"
}

variable "network_id" {
  type = string
}

variable "subnetwork_id" {
  type = string
}

Troubleshooting & Solutions

Common Issues

  • Startup script doesn't run
  • No external IP assigned
  • Instance stuck provisioning

Solutions

  • Check startup script logs via `gcloud compute instances get-serial-port-output`
  • An empty `access_config {}` block is what actually grants an ephemeral external IP — remove it for internal-only instances
  • Verify the boot disk image family/project name is spelled exactly right

Troubleshooting Steps

  1. Check serial port output for boot and startup-script errors
  2. Confirm the service account has the scopes/IAM roles the startup script needs
  3. Verify zone matches an actual zone in the target region
  4. Check project quotas for CPUs/instances in that region

Version compatibility: Terraform >= 1.0, Google Provider >= 5.0