terraform fmt -check
passStarted 2026-10-03T06:25:29Z · 0.181s
Terraform / IaC
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.
Counted with python-hcl2 from the .tf files. A module file counts once, not once per caller.
79 resource blocks, 7 module blocks.
| Resource type | Count |
|---|---|
aws_appautoscaling_policy | 1 |
aws_appautoscaling_target | 1 |
aws_cloudwatch_log_group | 3 |
aws_db_instance | 1 |
aws_db_subnet_group | 1 |
aws_default_security_group | 1 |
aws_dlm_lifecycle_policy | 1 |
aws_dynamodb_table | 1 |
aws_ecr_lifecycle_policy | 1 |
aws_ecr_repository | 1 |
aws_ecs_cluster | 1 |
aws_ecs_service | 1 |
aws_ecs_task_definition | 1 |
aws_eip | 2 |
aws_flow_log | 1 |
aws_iam_instance_profile | 1 |
aws_iam_role | 6 |
aws_iam_role_policy | 6 |
aws_iam_role_policy_attachment | 1 |
aws_instance | 1 |
aws_internet_gateway | 1 |
aws_kms_alias | 3 |
aws_kms_key | 3 |
aws_lb | 1 |
aws_lb_listener | 2 |
aws_lb_target_group | 1 |
aws_nat_gateway | 1 |
aws_route | 2 |
aws_route_table | 2 |
aws_route_table_association | 2 |
aws_s3_bucket | 2 |
aws_s3_bucket_lifecycle_configuration | 2 |
aws_s3_bucket_ownership_controls | 2 |
aws_s3_bucket_policy | 2 |
aws_s3_bucket_public_access_block | 2 |
aws_s3_bucket_server_side_encryption_configuration | 2 |
aws_s3_bucket_versioning | 2 |
aws_security_group | 5 |
aws_security_group_rule | 1 |
aws_ssm_parameter | 3 |
aws_subnet | 2 |
aws_vpc | 1 |
aws_vpc_endpoint | 2 |
| Module | Source | File |
|---|---|---|
state | ../modules/state_backend | bootstrap/main.tf |
network | ../modules/network | ecs-fargate/main.tf |
ecr | ../modules/ecr | ecs-fargate/main.tf |
alb | ../modules/alb | ecs-fargate/main.tf |
service | ../modules/ecs_service | ecs-fargate/main.tf |
network | ../modules/network | single-ec2/main.tf |
host | ../modules/ec2_docker_host | single-ec2/main.tf |
构建时记录. 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.
Started 2026-10-03T06:25:29Z · 0.181s
single_ec2 exit 0 · 2.04secs_fargate exit 0 · 2.431sbootstrap exit 0 · 2.007sStarted 2026-10-03T06:25:35Z · 1.058s
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.
CKV_AWS_260 LOW · module.alb.aws_security_group.alb · Ensure no security groups allow ingress from 0.0.0.0:0 to port 80CKV_AWS_150 LOW · module.alb.aws_lb.this · Ensure that Load Balancer has deletion protection enabledCKV_AWS_260 LOW · module.host.aws_security_group.host · Ensure no security groups allow ingress from 0.0.0.0:0 to port 80CKV2_AWS_62 LOW · module.alb.aws_s3_bucket.logs · Ensure S3 buckets should have event notifications enabledCKV2_AWS_62 LOW · module.state.aws_s3_bucket.state · Ensure S3 buckets should have event notifications enabledCKV2_AWS_19 LOW · module.network.aws_eip.nat[0] · Ensure that all EIP addresses allocated to a VPC are attached to EC2 instancesCKV_AWS_144 LOW · module.alb.aws_s3_bucket.logs · Ensure that S3 bucket has cross-region replication enabledCKV_AWS_144 LOW · module.state.aws_s3_bucket.state · Ensure that S3 bucket has cross-region replication enabledCKV2_AWS_5 LOW · module.network.aws_security_group.endpoints[0] · Ensure that Security Groups are attached to another resourceCKV2_AWS_34 LOW · module.host.aws_ssm_parameter.cwagent · AWS SSM Parameter should be EncryptedCKV2_AWS_30 LOW · aws_db_instance.app · Ensure Postgres RDS as aws_db_instance has Query Logging enabledCKV_AWS_18 LOW · module.alb.aws_s3_bucket.logs · Ensure the S3 bucket has access logging enabledCKV_AWS_18 LOW · module.state.aws_s3_bucket.state · Ensure the S3 bucket has access logging enabledCKV2_AWS_28 LOW · module.alb.aws_lb.this · Ensure public facing ALB are protected by WAFCKV_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 ARNStarted 2026-10-03T06:25:41Z · 5.071s. passed 6, failed 0, errored 0, skipped 0.
ec2_dev_sg_and_tags passec2_prod_size passecs_dev_alb_role_and_tags passecs_prod_size passecs_without_nat passecs_with_rds passNo prices. Sizes are read from envs/dev.tfvars and envs/prod.tfvars.
| Single EC2 | ECS Fargate | |
|---|---|---|
| Shape | One host, Compose, user_data | VPC, ALB, ECR, Fargate service |
| Availability | One machine in one zone | Two zones, load balancer, rolling deploy |
| Operations | Patch the OS. SSM instead of SSH. Daily EBS snapshots. | No host OS. Ship a new task definition. |
| Change in size | Replace the instance | CPU target tracking between min and max |
| Dev in the example | t3.small | 256 CPU / 512 MiB |
| Prod in the example | t3.medium | 512 CPU / 1024 MiB |
| Fits when | The service can live on one machine | A task or a zone failing should not take the service down |
modules/ec2_docker_host/main.tfdata "aws_ami" "al2023" {
most_recent = true
owners = ["amazon"]
filter {
name = "name"
values = ["al2023-ami-2023.*-x86_64"]
}
filter {
name = "architecture"
values = ["x86_64"]
}
filter {
name = "virtualization-type"
values = ["hvm"]
}
}
data "aws_caller_identity" "current" {}
locals {
tags = {
Project = var.project
Env = var.env
Owner = var.owner
}
account_id = data.aws_caller_identity.current.account_id
}
resource "aws_security_group" "host" {
name = "${var.name}-host"
description = "HTTP and HTTPS only. Administration uses Session Manager."
vpc_id = var.vpc_id
tags = merge(local.tags, { Name = "${var.name}-host" })
ingress {
description = "http"
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
description = "https"
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
egress {
description = "https for packages images and aws apis"
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
egress {
description = "dns"
from_port = 53
to_port = 53
protocol = "udp"
cidr_blocks = ["0.0.0.0/0"]
}
}
resource "aws_cloudwatch_log_group" "agent" {
name = "/${var.project}/${var.env}/ec2"
retention_in_days = 365
kms_key_id = var.kms_key_arn
tags = local.tags
}
resource "aws_ssm_parameter" "app_secret" {
name = "/${var.project}/${var.env}/checkout-api/app_secret"
type = "SecureString"
key_id = var.kms_key_arn
value = var.app_secret
tags = local.tags
}
resource "aws_ssm_parameter" "cwagent" {
name = "/${var.project}/${var.env}/cloudwatch-agent"
type = "String"
tags = local.tags
value = jsonencode({
logs = {
logs_collected = {
files = {
collect_list = [{
file_path = "/var/log/messages"
log_group_name = aws_cloudwatch_log_group.agent.name
log_stream_name = "{instance_id}"
}]
}
}
}
})
}
resource "aws_iam_role" "instance" {
name = "${var.name}-instance"
tags = local.tags
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Principal = { Service = "ec2.amazonaws.com" }
Action = "sts:AssumeRole"
}]
})
}
resource "aws_iam_role_policy" "instance" {
name = "${var.name}-instance"
role = aws_iam_role.instance.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Sid = "ReadParameters"
Effect = "Allow"
Action = ["ssm:GetParameter"]
Resource = [
aws_ssm_parameter.cwagent.arn,
aws_ssm_parameter.app_secret.arn,
]
},
{
Sid = "DecryptParameters"
Effect = "Allow"
Action = ["kms:Decrypt"]
Resource = var.kms_key_arn
},
{
Sid = "WriteAgentLogs"
Effect = "Allow"
Action = [
"logs:CreateLogStream",
"logs:PutLogEvents",
"logs:DescribeLogStreams",
]
Resource = "${aws_cloudwatch_log_group.agent.arn}:*"
},
]
})
}
# Session Manager's channel and message APIs do not support resource-level
# permissions. AWS documents Resource "*" for them. Checkov still treats the
# write actions as unconstrained, so the exception is recorded in checks.json.
resource "aws_iam_role_policy" "session" {
#checkov:skip=CKV_AWS_355:ssmmessages, ec2messages, and ssm:UpdateInstanceInformation do not support resource-level permissions; AWS requires Resource *
#checkov:skip=CKV_AWS_290:those Session Manager write actions cannot be constrained to a resource ARN
name = "${var.name}-session"
role = aws_iam_role.instance.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Sid = "SessionManager"
Effect = "Allow"
Action = [
"ssmmessages:CreateControlChannel",
"ssmmessages:CreateDataChannel",
"ssmmessages:OpenControlChannel",
"ssmmessages:OpenDataChannel",
"ssm:UpdateInstanceInformation",
"ec2messages:GetMessages",
"ec2messages:AcknowledgeMessage",
"ec2messages:SendReply",
]
Resource = "*"
}]
})
}
resource "aws_iam_instance_profile" "instance" {
name = "${var.name}-instance"
role = aws_iam_role.instance.name
tags = local.tags
}
resource "aws_instance" "host" {
ami = data.aws_ami.al2023.id
instance_type = var.instance_type
subnet_id = var.subnet_id
vpc_security_group_ids = [aws_security_group.host.id]
iam_instance_profile = aws_iam_instance_profile.instance.name
associate_public_ip_address = false
monitoring = true
ebs_optimized = true
user_data = templatefile("${path.module}/user_data.sh.tpl", {
app_image = var.app_image
aws_region = var.region
ssm_param = aws_ssm_parameter.cwagent.name
secret_param = aws_ssm_parameter.app_secret.name
})
tags = merge(local.tags, { Name = var.name })
metadata_options {
http_endpoint = "enabled"
http_tokens = "required"
http_put_response_hop_limit = 1
}
root_block_device {
volume_type = "gp3"
volume_size = var.volume_size
encrypted = true
kms_key_id = var.kms_key_arn
tags = merge(local.tags, { Snapshot = "daily" })
}
}
resource "aws_eip" "host" {
domain = "vpc"
instance = aws_instance.host.id
tags = merge(local.tags, { Name = "${var.name}-eip" })
}
resource "aws_iam_role" "dlm" {
name = "${var.name}-dlm"
tags = local.tags
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Principal = { Service = "dlm.amazonaws.com" }
Action = "sts:AssumeRole"
}]
})
}
resource "aws_iam_role_policy" "dlm" {
name = "${var.name}-dlm"
role = aws_iam_role.dlm.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Sid = "ManageSnapshots"
Effect = "Allow"
Action = [
"ec2:CreateSnapshot",
"ec2:CreateTags",
"ec2:DeleteSnapshot",
]
Resource = [
"arn:aws:ec2:${var.region}:${local.account_id}:volume/*",
"arn:aws:ec2:${var.region}:${local.account_id}:snapshot/*",
]
},
{
Sid = "DescribeForDlm"
Effect = "Allow"
Action = [
"ec2:DescribeInstances",
"ec2:DescribeVolumes",
"ec2:DescribeSnapshots",
]
# Describe actions for DLM do not support resource-level permissions.
Resource = "*"
},
]
})
}
resource "aws_dlm_lifecycle_policy" "daily" {
description = "Daily snapshots of ${var.name} volumes"
execution_role_arn = aws_iam_role.dlm.arn
state = "ENABLED"
tags = local.tags
policy_details {
resource_types = ["VOLUME"]
target_tags = {
Snapshot = "daily"
}
schedule {
name = "daily"
create_rule {
interval = 24
interval_unit = "HOURS"
times = ["03:00"]
}
retain_rule {
count = var.snapshot_retain
}
copy_tags = true
}
}
}