SkyForge

Terraform / IaC

SkyForge

Terraform for AWS: a single-EC2 Docker host, or ECS Fargate, tested offline.

Reference architecture and offline tests for a fictional Initech checkout-api. This is not a client project. No AWS account is used and nothing here is a price.

构建时记录 . Recorded when this demo was built. The page does not re-run Terraform, tflint, or checkov.

Two routes

A. Single EC2
Clients Elastic IP EC2 t3, Amazon Linux 2023 nginx checkout-api Docker Compose from user_data Security group 80 and 443 EBS gp3, encrypted DLM daily snapshot SSM Session Manager no port 22 CloudWatch agent logs
B. ECS Fargate
Clients ALB + ACM 443 forward 80 redirect VPC, two availability zones Public subnet one NAT optional off Private AZ a Fargate task Private AZ b Fargate task ECR SSM parameter Optional RDS Postgres private subnets only CloudWatch Logs CPU autoscaling

Resource inventory

Counted with python-hcl2 from the .tf files. A module file counts once, not once per caller.

79 resource blocks, 7 module blocks.

Resource typeCount
aws_appautoscaling_policy1
aws_appautoscaling_target1
aws_cloudwatch_log_group3
aws_db_instance1
aws_db_subnet_group1
aws_default_security_group1
aws_dlm_lifecycle_policy1
aws_dynamodb_table1
aws_ecr_lifecycle_policy1
aws_ecr_repository1
aws_ecs_cluster1
aws_ecs_service1
aws_ecs_task_definition1
aws_eip2
aws_flow_log1
aws_iam_instance_profile1
aws_iam_role6
aws_iam_role_policy6
aws_iam_role_policy_attachment1
aws_instance1
aws_internet_gateway1
aws_kms_alias3
aws_kms_key3
aws_lb1
aws_lb_listener2
aws_lb_target_group1
aws_nat_gateway1
aws_route2
aws_route_table2
aws_route_table_association2
aws_s3_bucket2
aws_s3_bucket_lifecycle_configuration2
aws_s3_bucket_ownership_controls2
aws_s3_bucket_policy2
aws_s3_bucket_public_access_block2
aws_s3_bucket_server_side_encryption_configuration2
aws_s3_bucket_versioning2
aws_security_group5
aws_security_group_rule1
aws_ssm_parameter3
aws_subnet2
aws_vpc1
aws_vpc_endpoint2

Module calls

ModuleSourceFile
state../modules/state_backendbootstrap/main.tf
network../modules/networkecs-fargate/main.tf
ecr../modules/ecrecs-fargate/main.tf
alb../modules/albecs-fargate/main.tf
service../modules/ecs_serviceecs-fargate/main.tf
network../modules/networksingle-ec2/main.tf
host../modules/ec2_docker_hostsingle-ec2/main.tf

Checks

构建时记录. These cards repeat data/checks.json. Opening the page does not run Terraform.

terraform: Terraform v1.9.8 · tflint: TFLint version 0.55.1 · checkov: 3.2.449

AWS_ACCESS_KEY_ID was empty when these commands ran.

terraform fmt -check

pass

exit 0

Started 2026-10-03T06:25:29Z · 0.181s

terraform validate

pass

3 roots

  • single_ec2 exit 0 · 2.04s
  • ecs_fargate exit 0 · 2.431s
  • bootstrap exit 0 · 2.007s

tflint

pass

exit 0

Started 2026-10-03T06:25:35Z · 1.058s

checkov

fail

14 failed

passed 218, failed 14, skipped 2. HIGH failures: 0.

checkov 3.2.449 leaves severity empty unless a Bridgecrew or Prisma API key is set. Each severity below is the Severity field on that check's public Prisma Cloud policy page, fetched while this file was written.

Failed checks

  • CKV_AWS_260 LOW · module.alb.aws_security_group.alb · Ensure no security groups allow ingress from 0.0.0.0:0 to port 80
  • CKV_AWS_150 LOW · module.alb.aws_lb.this · Ensure that Load Balancer has deletion protection enabled
  • CKV_AWS_260 LOW · module.host.aws_security_group.host · Ensure no security groups allow ingress from 0.0.0.0:0 to port 80
  • CKV2_AWS_62 LOW · module.alb.aws_s3_bucket.logs · Ensure S3 buckets should have event notifications enabled
  • CKV2_AWS_62 LOW · module.state.aws_s3_bucket.state · Ensure S3 buckets should have event notifications enabled
  • CKV2_AWS_19 LOW · module.network.aws_eip.nat[0] · Ensure that all EIP addresses allocated to a VPC are attached to EC2 instances
  • CKV_AWS_144 LOW · module.alb.aws_s3_bucket.logs · Ensure that S3 bucket has cross-region replication enabled
  • CKV_AWS_144 LOW · module.state.aws_s3_bucket.state · Ensure that S3 bucket has cross-region replication enabled
  • CKV2_AWS_5 LOW · module.network.aws_security_group.endpoints[0] · Ensure that Security Groups are attached to another resource
  • CKV2_AWS_34 LOW · module.host.aws_ssm_parameter.cwagent · AWS SSM Parameter should be Encrypted
  • CKV2_AWS_30 LOW · aws_db_instance.app · Ensure Postgres RDS as aws_db_instance has Query Logging enabled
  • CKV_AWS_18 LOW · module.alb.aws_s3_bucket.logs · Ensure the S3 bucket has access logging enabled
  • CKV_AWS_18 LOW · module.state.aws_s3_bucket.state · Ensure the S3 bucket has access logging enabled
  • CKV2_AWS_28 LOW · module.alb.aws_lb.this · Ensure public facing ALB are protected by WAF

HIGH exceptions

  • CKV_AWS_355 HIGH · module.host.aws_iam_role_policy.session — ssmmessages, ec2messages, and ssm:UpdateInstanceInformation do not support resource-level permissions; AWS requires Resource *
  • CKV_AWS_290 HIGH · module.host.aws_iam_role_policy.session — those Session Manager write actions cannot be constrained to a resource ARN

terraform test

pass

6 runs

Started 2026-10-03T06:25:41Z · 5.071s. passed 6, failed 0, errored 0, skipped 0.

  • ec2_dev_sg_and_tags pass
  • ec2_prod_size pass
  • ecs_dev_alb_role_and_tags pass
  • ecs_prod_size pass
  • ecs_without_nat pass
  • ecs_with_rds pass

Which route

No prices. Sizes are read from envs/dev.tfvars and envs/prod.tfvars.

Single EC2ECS Fargate
ShapeOne host, Compose, user_dataVPC, ALB, ECR, Fargate service
AvailabilityOne machine in one zoneTwo zones, load balancer, rolling deploy
OperationsPatch the OS. SSM instead of SSH. Daily EBS snapshots.No host OS. Ship a new task definition.
Change in sizeReplace the instanceCPU target tracking between min and max
Dev in the examplet3.small256 CPU / 512 MiB
Prod in the examplet3.medium512 CPU / 1024 MiB
Fits whenThe service can live on one machineA task or a zone failing should not take the service down

Files

tests/offline.tftest.hcl

mock_provider "aws" {
  mock_data "aws_ami" {
    defaults = {
      id           = "ami-0mockal2023example"
      architecture = "x86_64"
    }
  }

  mock_data "aws_availability_zones" {
    defaults = {
      names    = ["us-east-1a", "us-east-1b", "us-east-1c"]
      zone_ids = ["use1-az1", "use1-az2", "use1-az3"]
    }
  }

  mock_data "aws_caller_identity" {
    defaults = {
      account_id = "000000000000"
      arn        = "arn:aws:iam::000000000000:root"
      user_id    = "AIDMOCK"
    }
  }
}

variables {
  availability_zones = ["us-east-1a", "us-east-1b"]
}

run "ec2_dev_sg_and_tags" {
  command = plan

  module {
    source = "./single-ec2"
  }

  variables {
    env           = "dev"
    instance_type = "t3.small"
  }

  assert {
    condition     = output.instance_type == "t3.small"
    error_message = "dev example should plan a t3.small"
  }

  assert {
    condition = alltrue([
      for rule in output.ingress : !(rule.from_port <= 22 && rule.to_port >= 22)
    ])
    error_message = "host security group must not open port 22"
  }

  assert {
    condition = alltrue([
      for rule in output.ingress : !(
        rule.from_port <= 22 && rule.to_port >= 22 && contains(rule.cidrs, "0.0.0.0/0")
      )
    ])
    error_message = "host security group must not open port 22 to 0.0.0.0/0"
  }

  assert {
    condition = (
      length(output.resource_env_tags) > 0
      && alltrue([for tag in output.resource_env_tags : tag == "dev"])
    )
    error_message = "every taggable resource on the dev EC2 route must carry Env=dev"
  }
}

run "ec2_prod_size" {
  command = plan

  module {
    source = "./single-ec2"
  }

  variables {
    env           = "prod"
    instance_type = "t3.medium"
  }

  assert {
    condition     = output.instance_type == "t3.medium"
    error_message = "prod example should plan a t3.medium"
  }

  assert {
    condition     = output.instance_type != run.ec2_dev_sg_and_tags.instance_type
    error_message = "dev and prod instance types must differ"
  }

  assert {
    condition = (
      length(output.resource_env_tags) > 0
      && alltrue([for tag in output.resource_env_tags : tag == "prod"])
    )
    error_message = "every taggable resource on the prod EC2 route must carry Env=prod"
  }
}

run "ecs_dev_alb_role_and_tags" {
  command = plan

  module {
    source = "./ecs-fargate"
  }

  variables {
    env         = "dev"
    task_cpu    = 256
    task_memory = 512
    enable_nat  = true
    enable_rds  = false
  }

  assert {
    condition     = output.https_port == 443 && output.https_action == "forward"
    error_message = "the load balancer must forward on 443"
  }

  assert {
    condition     = output.http_port == 80 && output.http_action == "redirect"
    error_message = "port 80 must redirect and must not forward"
  }

  assert {
    condition = alltrue([
      for action in output.task_role_actions : action != "*" && !strcontains(action, "*:*")
    ]) && length(output.task_role_actions) > 0
    error_message = "the ECS task role must not allow *:*"
  }

  assert {
    condition = (
      length(output.resource_env_tags) > 0
      && alltrue([for tag in output.resource_env_tags : tag == "dev"])
    )
    error_message = "every taggable resource on the dev Fargate route must carry Env=dev"
  }
}

run "ecs_prod_size" {
  command = plan

  module {
    source = "./ecs-fargate"
  }

  variables {
    env         = "prod"
    task_cpu    = 512
    task_memory = 1024
    enable_nat  = true
    enable_rds  = false
  }

  assert {
    condition     = output.task_cpu == 512 && output.task_cpu != run.ecs_dev_alb_role_and_tags.task_cpu
    error_message = "dev and prod Fargate CPU sizes must differ"
  }

  assert {
    condition = (
      length(output.resource_env_tags) > 0
      && alltrue([for tag in output.resource_env_tags : tag == "prod"])
    )
    error_message = "every taggable resource on the prod Fargate route must carry Env=prod"
  }
}

run "ecs_without_nat" {
  command = plan

  module {
    source = "./ecs-fargate"
  }

  variables {
    env        = "dev"
    enable_nat = false
  }

  assert {
    condition     = output.https_port == 443 && output.http_action == "redirect"
    error_message = "turning NAT off must keep the listener rules"
  }
}

run "ecs_with_rds" {
  command = plan

  module {
    source = "./ecs-fargate"
  }

  variables {
    env        = "dev"
    enable_rds = true
  }

  assert {
    condition     = alltrue([for tag in output.resource_env_tags : tag == "dev"])
    error_message = "RDS resources must carry Env=dev"
  }
}