diff --git a/adapters/terraform/adapter.py b/adapters/terraform/adapter.py index d7ea1d1..bb7f81c 100644 --- a/adapters/terraform/adapter.py +++ b/adapters/terraform/adapter.py @@ -132,7 +132,8 @@ def adapt(stack_instance, out_dir): parts = [_emit_module_block(r, terraform_dirs, repo_root) for r in resources] for out_name, out_spec in stack_outputs.items(): if isinstance(out_spec, dict) and "from" in out_spec: - rid, mod_out = out_spec["from"].split(".", 1) + rid = out_spec["from"] + mod_out = out_spec.get("output", out_name) parts.append(_emit_root_output(out_name, rid, mod_out)) main_tf = "\n\n".join(parts) + "\n" diff --git a/modules/l1/alb/terraform/locals.tf b/modules/l1/alb/terraform/locals.tf new file mode 100644 index 0000000..20501fd --- /dev/null +++ b/modules/l1/alb/terraform/locals.tf @@ -0,0 +1,4 @@ +locals { + subnet_list = split(",", var.subnets) + security_groups = var.security_group != null ? [var.security_group] : [] +} diff --git a/modules/l1/alb/terraform/main.tf b/modules/l1/alb/terraform/main.tf new file mode 100644 index 0000000..f17728b --- /dev/null +++ b/modules/l1/alb/terraform/main.tf @@ -0,0 +1,25 @@ +resource "aws_lb" "this" { + name = var.name + load_balancer_type = var.load_balancer_type + subnets = local.subnet_list + security_groups = local.security_groups +} + +resource "aws_lb_target_group" "this" { + name = var.name + port = var.port + protocol = var.protocol + vpc_id = var.vpc_id + target_type = var.target_type +} + +resource "aws_lb_listener" "this" { + load_balancer_arn = aws_lb.this.id + port = var.port + protocol = var.protocol + + default_action { + type = "forward" + target_group_arn = aws_lb_target_group.this.arn + } +} diff --git a/modules/l1/alb/terraform/outputs.tf b/modules/l1/alb/terraform/outputs.tf new file mode 100644 index 0000000..f44ace4 --- /dev/null +++ b/modules/l1/alb/terraform/outputs.tf @@ -0,0 +1,14 @@ +output "lb_arn" { + value = aws_lb.this.id + description = "The load balancer ARN." +} + +output "listener_arn" { + value = aws_lb_listener.this.arn + description = "The listener ARN." +} + +output "target_group_arn" { + value = aws_lb_target_group.this.arn + description = "The target group ARN." +} diff --git a/modules/l1/alb/terraform/variables.tf b/modules/l1/alb/terraform/variables.tf new file mode 100644 index 0000000..8ca89bb --- /dev/null +++ b/modules/l1/alb/terraform/variables.tf @@ -0,0 +1,52 @@ +variable "name" { + type = string + description = "Name tag for the load balancer and child resources." + default = "app" +} + +variable "subnets" { + type = string + description = "Comma-separated subnet ids (ref to vpc)." +} + +variable "security_group" { + type = string + description = "Security group id for the load balancer." + default = null +} + +variable "port" { + type = number + description = "Listener port (default 80)." + default = 80 +} + +variable "protocol" { + type = string + description = "Listener protocol (default HTTP)." + default = "HTTP" +} + +variable "region" { + type = string + description = "AWS region (provider-level; not a resource arg)." + default = null +} + +variable "load_balancer_type" { + type = string + description = "Load balancer type (application or network)." + default = "application" +} + +variable "target_type" { + type = string + description = "Target group target type (ip or instance)." + default = "ip" +} + +variable "vpc_id" { + type = string + description = "VPC ID for the target group (ref to vpc or platform VPC)." + default = null +} diff --git a/modules/l1/alb/terraform/versions.tf b/modules/l1/alb/terraform/versions.tf new file mode 100644 index 0000000..6995a77 --- /dev/null +++ b/modules/l1/alb/terraform/versions.tf @@ -0,0 +1,9 @@ +terraform { + required_version = ">= 1.9, < 1.10" + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 5.0" + } + } +} diff --git a/modules/l1/cloudfront/terraform/locals.tf b/modules/l1/cloudfront/terraform/locals.tf new file mode 100644 index 0000000..e3b8a41 --- /dev/null +++ b/modules/l1/cloudfront/terraform/locals.tf @@ -0,0 +1,7 @@ +locals { + # OAC defaults (adapter previously hardcoded these). + oac_name = "acdl-oac" + oac_origin_type = "s3" + oac_signing_behavior = "always" + oac_signing_protocol = "sigv4" +} diff --git a/modules/l1/cloudfront/terraform/main.tf b/modules/l1/cloudfront/terraform/main.tf new file mode 100644 index 0000000..7ffc8cc --- /dev/null +++ b/modules/l1/cloudfront/terraform/main.tf @@ -0,0 +1,47 @@ +resource "aws_cloudfront_origin_access_control" "this" { + name = local.oac_name + origin_access_control_origin_type = local.oac_origin_type + signing_behavior = local.oac_signing_behavior + signing_protocol = local.oac_signing_protocol +} + +resource "aws_cloudfront_distribution" "this" { + origin { + origin_id = "s3-origin" + domain_name = var.bucket_regional_domain_name + origin_access_control_id = aws_cloudfront_origin_access_control.this.id + s3_origin_config { + origin_access_identity = "" + } + } + + enabled = true + price_class = var.price_class + default_cache_behavior { + viewer_protocol_policy = var.viewer_protocol_policy + target_origin_id = "s3-origin" + min_ttl = 0 + default_ttl = var.default_ttl + max_ttl = var.max_ttl + allowed_methods = ["GET", "HEAD"] + cached_methods = ["GET", "HEAD"] + forwarded_values { + query_string = false + cookies { + forward = "none" + } + } + } + + restrictions { + geo_restriction { + restriction_type = "none" + } + } + + viewer_certificate { + cloudfront_default_certificate = true + } + + web_acl_id = var.waf_web_acl_arn +} diff --git a/modules/l1/cloudfront/terraform/outputs.tf b/modules/l1/cloudfront/terraform/outputs.tf new file mode 100644 index 0000000..949afff --- /dev/null +++ b/modules/l1/cloudfront/terraform/outputs.tf @@ -0,0 +1,14 @@ +output "distribution_arn" { + value = aws_cloudfront_distribution.this.arn + description = "The CloudFront distribution ARN." +} + +output "distribution_domain_name" { + value = aws_cloudfront_distribution.this.domain_name + description = "The CloudFront distribution domain name." +} + +output "oac_id" { + value = aws_cloudfront_origin_access_control.this.id + description = "The Origin Access Control ID." +} diff --git a/modules/l1/cloudfront/terraform/variables.tf b/modules/l1/cloudfront/terraform/variables.tf new file mode 100644 index 0000000..4819355 --- /dev/null +++ b/modules/l1/cloudfront/terraform/variables.tf @@ -0,0 +1,40 @@ +variable "bucket_regional_domain_name" { + type = string + description = "The S3 bucket regional domain name (ref to s3 origin)." +} + +variable "price_class" { + type = string + description = "CloudFront price class (default PriceClass_100)." + default = "PriceClass_100" +} + +variable "viewer_protocol_policy" { + type = string + description = "Viewer protocol policy (default redirect-to-https)." + default = "redirect-to-https" +} + +variable "default_ttl" { + type = number + description = "Default TTL in seconds (default 3600)." + default = 3600 +} + +variable "max_ttl" { + type = number + description = "Max TTL in seconds (default 86400)." + default = 86400 +} + +variable "waf_web_acl_arn" { + type = string + description = "WAF web ACL ARN to associate (optional, ref to waf)." + default = null +} + +variable "region" { + type = string + description = "AWS region (CloudFront is global but the provider region is used for the OAC)." + default = null +} diff --git a/modules/l1/cloudfront/terraform/versions.tf b/modules/l1/cloudfront/terraform/versions.tf new file mode 100644 index 0000000..6995a77 --- /dev/null +++ b/modules/l1/cloudfront/terraform/versions.tf @@ -0,0 +1,9 @@ +terraform { + required_version = ">= 1.9, < 1.10" + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 5.0" + } + } +} diff --git a/modules/l1/ecr/terraform/main.tf b/modules/l1/ecr/terraform/main.tf new file mode 100644 index 0000000..7b8b8c0 --- /dev/null +++ b/modules/l1/ecr/terraform/main.tf @@ -0,0 +1,21 @@ +locals { + encryption_config = var.kms_key_arn != null ? { + encryption_type = "KMS" + kms_key = var.kms_key_arn + } : null +} + +resource "aws_ecr_repository" "this" { + name = var.name + image_tag_mutability = "MUTABLE" + image_scanning_configuration { + scan_on_push = true + } + dynamic "encryption_configuration" { + for_each = local.encryption_config != null ? [local.encryption_config] : [] + content { + encryption_type = encryption_configuration.value.encryption_type + kms_key = encryption_configuration.value.kms_key + } + } +} \ No newline at end of file diff --git a/modules/l1/ecr/terraform/outputs.tf b/modules/l1/ecr/terraform/outputs.tf new file mode 100644 index 0000000..a686adf --- /dev/null +++ b/modules/l1/ecr/terraform/outputs.tf @@ -0,0 +1,9 @@ +output "repository_url" { + value = aws_ecr_repository.this.repository_url + description = "The ECR repository URL." +} + +output "repository_arn" { + value = aws_ecr_repository.this.arn + description = "The ECR repository ARN." +} \ No newline at end of file diff --git a/modules/l1/ecr/terraform/variables.tf b/modules/l1/ecr/terraform/variables.tf new file mode 100644 index 0000000..7e86c44 --- /dev/null +++ b/modules/l1/ecr/terraform/variables.tf @@ -0,0 +1,16 @@ +variable "name" { + type = string + description = "ECR repository name." +} + +variable "region" { + type = string + description = "AWS region (provider-level; not a resource arg)." + default = null +} + +variable "kms_key_arn" { + type = string + description = "ARN of the CMK for ECR encryption; if absent, uses managed key." + default = null +} \ No newline at end of file diff --git a/modules/l1/ecr/terraform/versions.tf b/modules/l1/ecr/terraform/versions.tf new file mode 100644 index 0000000..6995a77 --- /dev/null +++ b/modules/l1/ecr/terraform/versions.tf @@ -0,0 +1,9 @@ +terraform { + required_version = ">= 1.9, < 1.10" + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 5.0" + } + } +} diff --git a/modules/l1/ecs-cluster/terraform/main.tf b/modules/l1/ecs-cluster/terraform/main.tf new file mode 100644 index 0000000..96c5264 --- /dev/null +++ b/modules/l1/ecs-cluster/terraform/main.tf @@ -0,0 +1,3 @@ +resource "aws_ecs_cluster" "this" { + name = var.name +} diff --git a/modules/l1/ecs-cluster/terraform/outputs.tf b/modules/l1/ecs-cluster/terraform/outputs.tf new file mode 100644 index 0000000..7fc3625 --- /dev/null +++ b/modules/l1/ecs-cluster/terraform/outputs.tf @@ -0,0 +1,9 @@ +output "cluster_arn" { + value = aws_ecs_cluster.this.arn + description = "The ECS cluster ARN." +} + +output "cluster_id" { + value = aws_ecs_cluster.this.id + description = "The ECS cluster ID." +} diff --git a/modules/l1/ecs-cluster/terraform/variables.tf b/modules/l1/ecs-cluster/terraform/variables.tf new file mode 100644 index 0000000..2d4e8f7 --- /dev/null +++ b/modules/l1/ecs-cluster/terraform/variables.tf @@ -0,0 +1,17 @@ +variable "name" { + type = string + description = "ECS cluster name." + default = "acdl-cluster" +} + +variable "region" { + type = string + description = "AWS region (provider-level; not a resource arg)." + default = null +} + +variable "kms_key_arn" { + type = string + description = "ARN of the CMK for CloudWatch log group encryption; if absent, uses managed key." + default = null +} diff --git a/modules/l1/ecs-cluster/terraform/versions.tf b/modules/l1/ecs-cluster/terraform/versions.tf new file mode 100644 index 0000000..6995a77 --- /dev/null +++ b/modules/l1/ecs-cluster/terraform/versions.tf @@ -0,0 +1,9 @@ +terraform { + required_version = ">= 1.9, < 1.10" + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 5.0" + } + } +} diff --git a/modules/l1/ecs-service/terraform/locals.tf b/modules/l1/ecs-service/terraform/locals.tf new file mode 100644 index 0000000..23983b3 --- /dev/null +++ b/modules/l1/ecs-service/terraform/locals.tf @@ -0,0 +1,23 @@ +locals { + # Fargate compat defaults (adapter previously hardcoded these). + requires_compatibilities = var.launch_type == "FARGATE" ? ["FARGATE"] : ["EC2"] + network_mode = var.launch_type == "FARGATE" ? "awsvpc" : "bridge" + + # Container definitions from image/port/env (adapter previously hardcoded this). + container_definitions = jsonencode([{ + name = "app" + image = var.image + essential = true + portMappings = [{ + containerPort = var.port + hostPort = var.port + protocol = "tcp" + }] + }]) + + # Subnet list from comma-separated string. + subnet_list = split(",", var.subnets) + + # Security groups list. + security_groups = var.security_group != null ? [var.security_group] : [] +} diff --git a/modules/l1/ecs-service/terraform/main.tf b/modules/l1/ecs-service/terraform/main.tf new file mode 100644 index 0000000..5da3443 --- /dev/null +++ b/modules/l1/ecs-service/terraform/main.tf @@ -0,0 +1,31 @@ +resource "aws_ecs_task_definition" "this" { + family = var.family + cpu = tostring(var.cpu) + memory = tostring(var.memory) + requires_compatibilities = local.requires_compatibilities + network_mode = local.network_mode + container_definitions = local.container_definitions +} + +resource "aws_ecs_service" "this" { + name = "acdl-microservice" + cluster = var.cluster_arn + task_definition = aws_ecs_task_definition.this.arn + desired_count = var.desired_count + launch_type = var.launch_type + + network_configuration { + subnets = local.subnet_list + security_groups = local.security_groups + assign_public_ip = var.launch_type == "FARGATE" + } + + dynamic "load_balancer" { + for_each = var.lb_target_group_arn != null ? [1] : [] + content { + target_group_arn = var.lb_target_group_arn + container_name = "app" + container_port = var.port + } + } +} diff --git a/modules/l1/ecs-service/terraform/outputs.tf b/modules/l1/ecs-service/terraform/outputs.tf new file mode 100644 index 0000000..15d3b8a --- /dev/null +++ b/modules/l1/ecs-service/terraform/outputs.tf @@ -0,0 +1,9 @@ +output "service_arn" { + value = aws_ecs_service.this.id + description = "The ECS service ARN." +} + +output "task_def_arn" { + value = aws_ecs_task_definition.this.arn + description = "The ECS task definition ARN." +} diff --git a/modules/l1/ecs-service/terraform/variables.tf b/modules/l1/ecs-service/terraform/variables.tf new file mode 100644 index 0000000..b0ce66f --- /dev/null +++ b/modules/l1/ecs-service/terraform/variables.tf @@ -0,0 +1,80 @@ +variable "image" { + type = string + description = "ECR image URL for the task container." +} + +variable "port" { + type = number + description = "Container port the service listens on." + default = 80 +} + +variable "cpu" { + type = number + description = "Task CPU units (Fargate)." + default = 256 +} + +variable "memory" { + type = number + description = "Task memory (MiB, Fargate)." + default = 512 +} + +variable "env" { + type = string + description = "Environment variables as a JSON map string (optional)." + default = null +} + +variable "cluster_arn" { + type = string + description = "ECS cluster ARN (ref to ecs-cluster)." +} + +variable "subnets" { + type = string + description = "Comma-separated subnet ids (ref to vpc)." +} + +variable "security_group" { + type = string + description = "Security group id for the service ENIs." + default = null +} + +variable "lb_target_group_arn" { + type = string + description = "Optional ALB target group ARN (ref to alb)." + default = null +} + +variable "region" { + type = string + description = "AWS region (provider-level; not a resource arg)." + default = null +} + +variable "kms_key_arn" { + type = string + description = "ARN of the CMK for CloudWatch log group encryption; if absent, uses managed key." + default = null +} + +variable "desired_count" { + type = number + description = "Desired number of ECS task replicas (Fargate)." + default = 1 +} + +variable "launch_type" { + type = string + description = "ECS launch type (FARGATE or EC2)." + default = "FARGATE" +} + +variable "family" { + type = string + description = "ECS task definition family name." + default = "app" +} diff --git a/modules/l1/ecs-service/terraform/versions.tf b/modules/l1/ecs-service/terraform/versions.tf new file mode 100644 index 0000000..6995a77 --- /dev/null +++ b/modules/l1/ecs-service/terraform/versions.tf @@ -0,0 +1,9 @@ +terraform { + required_version = ">= 1.9, < 1.10" + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 5.0" + } + } +} diff --git a/modules/l1/iam-role/terraform/locals.tf b/modules/l1/iam-role/terraform/locals.tf new file mode 100644 index 0000000..ae78972 --- /dev/null +++ b/modules/l1/iam-role/terraform/locals.tf @@ -0,0 +1,42 @@ +locals { + # Default ECS task trust policy when none is supplied (adapter previously hardcoded this). + assume_role_policy = var.assume_role_policy != null ? var.assume_role_policy : jsonencode({ + Version = "2012-10-17" + Statement = [{ + Effect = "Allow" + Principal = { Service = "ecs-tasks.amazonaws.com" } + Action = "sts:AssumeRole" + }] + }) + + # Default inline ECR+logs policy when no managed_policies supplied (adapter previously hardcoded this). + managed_policy_arns = var.managed_policies != null ? split(",", var.managed_policies) : [] + + inline_policy = var.managed_policies == null ? { + name = "ecr-logs" + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Effect = "Allow" + Action = [ + "ecr:GetDownloadUrlForLayer", + "ecr:BatchGetImage", + "ecr:GetAuthorizationToken", + "ecr:BatchCheckLayerAvailability" + ] + Resource = "*" + }, + { + Effect = "Allow" + Action = [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents" + ] + Resource = "*" + } + ] + }) + } : null +} diff --git a/modules/l1/iam-role/terraform/main.tf b/modules/l1/iam-role/terraform/main.tf new file mode 100644 index 0000000..de4daf7 --- /dev/null +++ b/modules/l1/iam-role/terraform/main.tf @@ -0,0 +1,11 @@ +resource "aws_iam_role" "this" { + name = var.role_name + assume_role_policy = local.assume_role_policy +} + +resource "aws_iam_role_policy" "ecr_logs" { + count = local.inline_policy != null ? 1 : 0 + name = local.inline_policy.name + role = aws_iam_role.this.id + policy = local.inline_policy.policy +} diff --git a/modules/l1/iam-role/terraform/outputs.tf b/modules/l1/iam-role/terraform/outputs.tf new file mode 100644 index 0000000..371acfa --- /dev/null +++ b/modules/l1/iam-role/terraform/outputs.tf @@ -0,0 +1,9 @@ +output "role_arn" { + value = aws_iam_role.this.arn + description = "The IAM role ARN." +} + +output "role_id" { + value = aws_iam_role.this.id + description = "The IAM role ID." +} diff --git a/modules/l1/iam-role/terraform/variables.tf b/modules/l1/iam-role/terraform/variables.tf new file mode 100644 index 0000000..65df80f --- /dev/null +++ b/modules/l1/iam-role/terraform/variables.tf @@ -0,0 +1,23 @@ +variable "role_name" { + type = string + description = "The IAM role name." + default = "acdl-microservice-role" +} + +variable "assume_role_policy" { + type = string + description = "Assume-role policy document (JSON string)." + default = null +} + +variable "managed_policies" { + type = string + description = "Comma-separated list of managed policy ARNs to attach." + default = null +} + +variable "region" { + type = string + description = "AWS region (provider-level; not a resource arg)." + default = null +} diff --git a/modules/l1/iam-role/terraform/versions.tf b/modules/l1/iam-role/terraform/versions.tf new file mode 100644 index 0000000..6995a77 --- /dev/null +++ b/modules/l1/iam-role/terraform/versions.tf @@ -0,0 +1,9 @@ +terraform { + required_version = ">= 1.9, < 1.10" + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 5.0" + } + } +} diff --git a/modules/l1/kms-key/terraform/main.tf b/modules/l1/kms-key/terraform/main.tf new file mode 100644 index 0000000..58ffb20 --- /dev/null +++ b/modules/l1/kms-key/terraform/main.tf @@ -0,0 +1,10 @@ +resource "aws_kms_key" "this" { + description = var.description + enable_key_rotation = true + deletion_window_in_days = var.deletion_window_days +} + +resource "aws_kms_alias" "this" { + name = "alias/acdl-${var.description != null ? var.description : "default"}" + target_key_id = aws_kms_key.this.key_id +} \ No newline at end of file diff --git a/modules/l1/kms-key/terraform/outputs.tf b/modules/l1/kms-key/terraform/outputs.tf new file mode 100644 index 0000000..8828df8 --- /dev/null +++ b/modules/l1/kms-key/terraform/outputs.tf @@ -0,0 +1,9 @@ +output "kms_key_arn" { + value = aws_kms_key.this.arn + description = "The KMS key ARN." +} + +output "kms_key_id" { + value = aws_kms_key.this.key_id + description = "The KMS key ID." +} \ No newline at end of file diff --git a/modules/l1/kms-key/terraform/variables.tf b/modules/l1/kms-key/terraform/variables.tf new file mode 100644 index 0000000..33ebfd6 --- /dev/null +++ b/modules/l1/kms-key/terraform/variables.tf @@ -0,0 +1,17 @@ +variable "description" { + type = string + description = "Description of the KMS key." + default = "ACDL per-stack CMK" +} + +variable "region" { + type = string + description = "AWS region (provider-level; not a resource arg)." + default = null +} + +variable "deletion_window_days" { + type = number + description = "Deletion window in days (7-30)." + default = 30 +} \ No newline at end of file diff --git a/modules/l1/kms-key/terraform/versions.tf b/modules/l1/kms-key/terraform/versions.tf new file mode 100644 index 0000000..0bf712c --- /dev/null +++ b/modules/l1/kms-key/terraform/versions.tf @@ -0,0 +1,9 @@ +terraform { + required_version = ">= 1.9, < 1.10" + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 5.0" + } + } +} \ No newline at end of file diff --git a/modules/l1/rds/terraform/main.tf b/modules/l1/rds/terraform/main.tf new file mode 100644 index 0000000..768f5c1 --- /dev/null +++ b/modules/l1/rds/terraform/main.tf @@ -0,0 +1,13 @@ +resource "aws_db_instance" "this" { + engine = var.engine + engine_version = var.engine_version + instance_class = var.instance_class + allocated_storage = var.allocated_storage + db_name = var.db_name + username = var.username + multi_az = var.multi_az + storage_encrypted = var.storage_encrypted + kms_key_id = var.kms_key_arn + skip_final_snapshot = true + publicly_accessible = false +} diff --git a/modules/l1/rds/terraform/outputs.tf b/modules/l1/rds/terraform/outputs.tf new file mode 100644 index 0000000..cc0696b --- /dev/null +++ b/modules/l1/rds/terraform/outputs.tf @@ -0,0 +1,9 @@ +output "db_endpoint" { + value = aws_db_instance.this.endpoint + description = "The RDS instance endpoint." +} + +output "db_arn" { + value = aws_db_instance.this.arn + description = "The RDS instance ARN." +} diff --git a/modules/l1/rds/terraform/variables.tf b/modules/l1/rds/terraform/variables.tf new file mode 100644 index 0000000..6ca976e --- /dev/null +++ b/modules/l1/rds/terraform/variables.tf @@ -0,0 +1,54 @@ +variable "engine" { + type = string + description = "Database engine (postgres, mysql, etc.)." +} + +variable "engine_version" { + type = string + description = "Database engine version." +} + +variable "instance_class" { + type = string + description = "RDS instance class (e.g. db.t3.micro)." +} + +variable "allocated_storage" { + type = number + description = "Allocated storage in GB." + default = 20 +} + +variable "db_name" { + type = string + description = "Database name." +} + +variable "username" { + type = string + description = "Database admin username." +} + +variable "multi_az" { + type = bool + description = "Enable Multi-AZ." + default = false +} + +variable "storage_encrypted" { + type = bool + description = "Enable storage encryption." + default = true +} + +variable "region" { + type = string + description = "AWS region (provider-level; not a resource arg)." + default = null +} + +variable "kms_key_arn" { + type = string + description = "ARN of the CMK for storage encryption; if absent, uses managed key." + default = null +} diff --git a/modules/l1/rds/terraform/versions.tf b/modules/l1/rds/terraform/versions.tf new file mode 100644 index 0000000..6995a77 --- /dev/null +++ b/modules/l1/rds/terraform/versions.tf @@ -0,0 +1,9 @@ +terraform { + required_version = ">= 1.9, < 1.10" + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 5.0" + } + } +} diff --git a/modules/l1/uptime/terraform/locals.tf b/modules/l1/uptime/terraform/locals.tf new file mode 100644 index 0000000..1bb6c51 --- /dev/null +++ b/modules/l1/uptime/terraform/locals.tf @@ -0,0 +1,12 @@ +locals { + container_definitions = jsonencode([{ + name = "uptime-kuma" + image = var.container_image + essential = true + portMappings = [{ + containerPort = 3001 + hostPort = 3001 + protocol = "tcp" + }] + }]) +} diff --git a/modules/l1/uptime/terraform/main.tf b/modules/l1/uptime/terraform/main.tf new file mode 100644 index 0000000..63eb787 --- /dev/null +++ b/modules/l1/uptime/terraform/main.tf @@ -0,0 +1,16 @@ +resource "aws_ecs_task_definition" "uptime" { + family = "acdl-uptime" + cpu = tostring(var.cpu) + memory = tostring(var.memory) + requires_compatibilities = ["FARGATE"] + network_mode = "awsvpc" + container_definitions = local.container_definitions +} + +resource "aws_ecs_service" "uptime" { + name = "acdl-uptime" + cluster = "default" + task_definition = aws_ecs_task_definition.uptime.arn + desired_count = var.feature_flag_enabled ? 1 : 0 + launch_type = "FARGATE" +} diff --git a/modules/l1/uptime/terraform/outputs.tf b/modules/l1/uptime/terraform/outputs.tf new file mode 100644 index 0000000..53f424f --- /dev/null +++ b/modules/l1/uptime/terraform/outputs.tf @@ -0,0 +1,14 @@ +output "uptime_url" { + value = var.uptime_url != null ? var.uptime_url : "http://localhost:3001" + description = "The uptime-kuma URL." +} + +output "service_arn" { + value = aws_ecs_service.uptime.id + description = "The uptime ECS service ARN." +} + +output "task_definition_arn" { + value = aws_ecs_task_definition.uptime.arn + description = "The uptime task definition ARN." +} diff --git a/modules/l1/uptime/terraform/variables.tf b/modules/l1/uptime/terraform/variables.tf new file mode 100644 index 0000000..07d55bf --- /dev/null +++ b/modules/l1/uptime/terraform/variables.tf @@ -0,0 +1,52 @@ +variable "container_image" { + type = string + description = "Container image for the uptime-kuma service." +} + +variable "region" { + type = string + description = "AWS region (provider-level; not a resource arg)." + default = null +} + +variable "uptime_url" { + type = string + description = "The uptime-kuma URL (output)." + default = null +} + +variable "monitored_endpoints" { + type = any + description = "List of monitored endpoint objects." + default = [] +} + +variable "static_checks" { + type = any + description = "Static check configuration." + default = null +} + +variable "alert_channels" { + type = any + description = "Alert channel configuration." + default = {} +} + +variable "feature_flag_enabled" { + type = bool + description = "Feature flag to enable/disable the uptime service." + default = true +} + +variable "cpu" { + type = number + description = "Task CPU units (Fargate)." + default = 256 +} + +variable "memory" { + type = number + description = "Task memory (MiB, Fargate)." + default = 512 +} diff --git a/modules/l1/uptime/terraform/versions.tf b/modules/l1/uptime/terraform/versions.tf new file mode 100644 index 0000000..6995a77 --- /dev/null +++ b/modules/l1/uptime/terraform/versions.tf @@ -0,0 +1,9 @@ +terraform { + required_version = ">= 1.9, < 1.10" + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 5.0" + } + } +} diff --git a/modules/l1/vpc/terraform/locals.tf b/modules/l1/vpc/terraform/locals.tf new file mode 100644 index 0000000..9389d4a --- /dev/null +++ b/modules/l1/vpc/terraform/locals.tf @@ -0,0 +1,10 @@ +locals { + cidr_block = var.cidr != null ? var.cidr : "10.0.0.0/16" + az_list = split(",", var.azs) + name_tag = var.name != null ? var.name : "acdl-vpc" + + # Derive subnet CIDRs from the VPC CIDR + subnet_cidrs = [ + for i in range(length(local.az_list)) : cidrsubnet(local.cidr_block, 8, i + 1) + ] +} diff --git a/modules/l1/vpc/terraform/main.tf b/modules/l1/vpc/terraform/main.tf new file mode 100644 index 0000000..3b23246 --- /dev/null +++ b/modules/l1/vpc/terraform/main.tf @@ -0,0 +1,40 @@ +resource "aws_vpc" "this" { + cidr_block = local.cidr_block + tags = { + Name = local.name_tag + } +} + +resource "aws_subnet" "this" { + count = length(local.az_list) + vpc_id = aws_vpc.this.id + cidr_block = local.subnet_cidrs[count.index] + availability_zone = local.az_list[count.index] + tags = { + Name = "${local.name_tag}-subnet-${count.index}" + } +} + +resource "aws_internet_gateway" "this" { + vpc_id = aws_vpc.this.id + tags = { + Name = "${local.name_tag}-igw" + } +} + +resource "aws_route_table" "this" { + vpc_id = aws_vpc.this.id + route { + cidr_block = "0.0.0.0/0" + gateway_id = aws_internet_gateway.this.id + } + tags = { + Name = "${local.name_tag}-rt" + } +} + +resource "aws_route_table_association" "this" { + count = length(local.az_list) + subnet_id = aws_subnet.this[count.index].id + route_table_id = aws_route_table.this.id +} diff --git a/modules/l1/vpc/terraform/outputs.tf b/modules/l1/vpc/terraform/outputs.tf new file mode 100644 index 0000000..afabd44 --- /dev/null +++ b/modules/l1/vpc/terraform/outputs.tf @@ -0,0 +1,9 @@ +output "vpc_id" { + value = aws_vpc.this.id + description = "The VPC id." +} + +output "subnet_ids" { + value = join(",", aws_subnet.this[*].id) + description = "Comma-separated subnet ids." +} diff --git a/modules/l1/vpc/terraform/variables.tf b/modules/l1/vpc/terraform/variables.tf new file mode 100644 index 0000000..8ffd52f --- /dev/null +++ b/modules/l1/vpc/terraform/variables.tf @@ -0,0 +1,23 @@ +variable "cidr" { + type = string + description = "VPC CIDR block, e.g. 10.0.0.0/16." + default = null +} + +variable "azs" { + type = string + description = "Comma-separated availability zones, e.g. us-east-1a,us-east-1b." + default = "us-east-1a" +} + +variable "name" { + type = string + description = "Name tag for the VPC and child resources." + default = "acdl-vpc" +} + +variable "region" { + type = string + description = "AWS region (provider-level; not a resource arg)." + default = null +} diff --git a/modules/l1/vpc/terraform/versions.tf b/modules/l1/vpc/terraform/versions.tf new file mode 100644 index 0000000..6995a77 --- /dev/null +++ b/modules/l1/vpc/terraform/versions.tf @@ -0,0 +1,9 @@ +terraform { + required_version = ">= 1.9, < 1.10" + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 5.0" + } + } +} diff --git a/modules/l1/waf/terraform/locals.tf b/modules/l1/waf/terraform/locals.tf new file mode 100644 index 0000000..c2ae966 --- /dev/null +++ b/modules/l1/waf/terraform/locals.tf @@ -0,0 +1,3 @@ +locals { + action_type = var.default_action == "block" ? "block" : "allow" +} diff --git a/modules/l1/waf/terraform/main.tf b/modules/l1/waf/terraform/main.tf new file mode 100644 index 0000000..942eef4 --- /dev/null +++ b/modules/l1/waf/terraform/main.tf @@ -0,0 +1,52 @@ +resource "aws_wafv2_web_acl" "this" { + name = var.name + scope = var.scope + + default_action { + dynamic "allow" { + for_each = local.action_type == "allow" ? [1] : [] + content {} + } + dynamic "block" { + for_each = local.action_type == "block" ? [1] : [] + content {} + } + } + + visibility_config { + cloudwatch_metrics_enabled = true + metric_name = "acdl-waf-metrics" + sampled_requests_enabled = true + } + + dynamic "rule" { + for_each = var.rules + content { + name = lookup(rule.value, "name", "custom-rule-${rule.key}") + priority = lookup(rule.value, "priority", rule.key) + override_action { + none {} + } + statement { + byte_match_statement { + search_string = lookup(rule.value, "search_string", "/") + positional_constraint = "CONTAINS" + field_to_match { + single_header { + name = "user-agent" + } + } + text_transformation { + priority = 0 + type = "NONE" + } + } + } + visibility_config { + cloudwatch_metrics_enabled = true + metric_name = "${lookup(rule.value, "name", "custom-rule-${rule.key}")}-metrics" + sampled_requests_enabled = true + } + } + } +} \ No newline at end of file diff --git a/modules/l1/waf/terraform/outputs.tf b/modules/l1/waf/terraform/outputs.tf new file mode 100644 index 0000000..b8552ca --- /dev/null +++ b/modules/l1/waf/terraform/outputs.tf @@ -0,0 +1,4 @@ +output "web_acl_arn" { + value = aws_wafv2_web_acl.this.arn + description = "The WAF Web ACL ARN." +} diff --git a/modules/l1/waf/terraform/variables.tf b/modules/l1/waf/terraform/variables.tf new file mode 100644 index 0000000..4760b7e --- /dev/null +++ b/modules/l1/waf/terraform/variables.tf @@ -0,0 +1,29 @@ +variable "name" { + type = string + description = "WAF Web ACL name." + default = "acdl-waf" +} + +variable "scope" { + type = string + description = "WAF scope (CLOUDFRONT or REGIONAL)." + default = "CLOUDFRONT" +} + +variable "default_action" { + type = string + description = "Default action (allow or block)." + default = "allow" +} + +variable "rules" { + type = any + description = "Custom WAF rules (list of rule objects)." + default = [] +} + +variable "region" { + type = string + description = "AWS region (provider-level; not a resource arg)." + default = null +} diff --git a/modules/l1/waf/terraform/versions.tf b/modules/l1/waf/terraform/versions.tf new file mode 100644 index 0000000..6995a77 --- /dev/null +++ b/modules/l1/waf/terraform/versions.tf @@ -0,0 +1,9 @@ +terraform { + required_version = ">= 1.9, < 1.10" + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 5.0" + } + } +} diff --git a/modules/registry.json b/modules/registry.json index 59d32ea..bc78ccc 100644 --- a/modules/registry.json +++ b/modules/registry.json @@ -11,77 +11,88 @@ "1.0.0": { "interface": "modules/l1/vpc/interface.json", "published_at": "2026-07-21T21:30:00Z", - "deprecated": false + "deprecated": false, + "terraform_dir": "modules/l1/vpc/terraform" } }, "ecs-cluster": { "1.0.0": { "interface": "modules/l1/ecs-cluster/interface.json", "published_at": "2026-07-21T21:30:00Z", - "deprecated": false + "deprecated": false, + "terraform_dir": "modules/l1/ecs-cluster/terraform" } }, "ecs-service": { "1.0.0": { "interface": "modules/l1/ecs-service/interface.json", "published_at": "2026-07-21T21:30:00Z", - "deprecated": false + "deprecated": false, + "terraform_dir": "modules/l1/ecs-service/terraform" } }, "iam-role": { "1.0.0": { "interface": "modules/l1/iam-role/interface.json", "published_at": "2026-07-21T21:30:00Z", - "deprecated": false + "deprecated": false, + "terraform_dir": "modules/l1/iam-role/terraform" } }, "alb": { "1.0.0": { "interface": "modules/l1/alb/interface.json", "published_at": "2026-07-21T21:30:00Z", - "deprecated": false + "deprecated": false, + "terraform_dir": "modules/l1/alb/terraform" } }, "ecr": { "1.0.0": { "interface": "modules/l1/ecr/interface.json", "published_at": "2026-07-21T21:30:00Z", - "deprecated": false + "deprecated": false, + "terraform_dir": "modules/l1/ecr/terraform" } }, "cloudfront": { "1.0.0": { "interface": "modules/l1/cloudfront/interface.json", "published_at": "2026-07-22T19:00:00Z", - "deprecated": false + "deprecated": false, + "terraform_dir": "modules/l1/cloudfront/terraform" } }, "waf": { "1.0.0": { "interface": "modules/l1/waf/interface.json", "published_at": "2026-07-22T19:00:00Z", - "deprecated": false + "deprecated": false, + "terraform_dir": "modules/l1/waf/terraform" } }, "rds": { "1.0.0": { "interface": "modules/l1/rds/interface.json", "published_at": "2026-07-22T20:00:00Z", - "deprecated": false + "deprecated": false, + "terraform_dir": "modules/l1/rds/terraform" } }, "kms-key": { "1.0.0": { "interface": "modules/l1/kms-key/interface.json", "published_at": "2026-07-22T20:00", - "deprecated": false + "deprecated": false, + "terraform_dir": "modules/l1/kms-key/terraform" } }, "uptime": { "1.0.0": { "interface": "modules/l1/uptime/interface.json", "published_at": "2026-07-22T21:00", - "deprecated": false + "deprecated": false, + "terraform_dir": "modules/l1/uptime/terraform" } }, "static-assets": { @@ -98,4 +109,4 @@ "deprecated": false } } -} \ No newline at end of file +} diff --git a/tests/test_environment_check.py b/tests/test_environment_check.py index dc8f163..d8f6488 100644 --- a/tests/test_environment_check.py +++ b/tests/test_environment_check.py @@ -87,7 +87,6 @@ class TestRunPlatformWireIn: assert "environment_check.py" in content assert "Step 0: environment onboarding check" in content - @pytest.mark.skip(reason="run_platform.sh --check-only defaults to static-assets.yml which needs cloudfront/waf terraform dirs (P56b)") def test_check_only_passes_with_dev_environment(self): import subprocess result = subprocess.run( diff --git a/tests/test_pipeline.py b/tests/test_pipeline.py index bbe3512..fa80f84 100644 --- a/tests/test_pipeline.py +++ b/tests/test_pipeline.py @@ -44,7 +44,6 @@ class TestPipelineIntegration: assert sig.band == "pass" assert sig.score >= 0.50 - @pytest.mark.skip(reason="run_platform.sh --check-only defaults to static-assets.yml which needs cloudfront/waf terraform dirs (P56b)") def test_run_platform_check_only(self): result = subprocess.run( ["bash", str(ROOT / "scripts/run_platform.sh"), "--check-only"], @@ -54,7 +53,6 @@ class TestPipelineIntegration: assert result.returncode == 0, f"stdout: {result.stdout}\nstderr: {result.stderr}" assert "PLATFORM CHECK OK" in result.stdout - @pytest.mark.skip(reason="run_platform.sh --check-only defaults to static-assets.yml which needs cloudfront/waf terraform dirs (P56b)") def test_run_platform_check_only_no_aws_creds(self): env = os.environ.copy() env.pop("AWS_ACCESS_KEY_ID", None) diff --git a/tests/test_pipeline_contract.py b/tests/test_pipeline_contract.py index b6b578a..1b51207 100644 --- a/tests/test_pipeline_contract.py +++ b/tests/test_pipeline_contract.py @@ -193,7 +193,6 @@ class TestRunCiScript: content = open(ROOT / "scripts/run_ci.sh").read() assert "CI PIPELINE OK" in content - @pytest.mark.skip(reason="run_platform.sh --check-only defaults to static-assets.yml which needs cloudfront/waf terraform dirs (P56b)") def test_run_ci_lint_and_check_only_pass(self): result = subprocess.run( ["bash", "-c", @@ -218,7 +217,6 @@ class TestRunCiScript: class TestRunPlatformStreaming: - @pytest.mark.skip(reason="run_platform.sh --check-only defaults to static-assets.yml which needs cloudfront/waf terraform dirs (P56b)") def test_check_only_streams_emitted_terraform(self): result = subprocess.run( ["bash", str(ROOT / "scripts/run_platform.sh"), "--check-only"], @@ -229,9 +227,8 @@ class TestRunPlatformStreaming: assert "PLATFORM CHECK OK" in result.stdout assert "--- emitted" in result.stdout assert "main.tf" in result.stdout - assert "aws_s3_bucket" in result.stdout + assert "module" in result.stdout - @pytest.mark.skip(reason="run_platform.sh --check-only defaults to static-assets.yml which needs cloudfront/waf terraform dirs (P56b)") def test_check_only_quiet_suppresses_terraform(self): result = subprocess.run( ["bash", str(ROOT / "scripts/run_platform.sh"), "--check-only", "--quiet"],