darellchua2

aws-iac-safety-skill

AWS IaC safety patterns — resource count gating, SSM CI passthrough, ECR lifecycle ownership, SSM_SECRETS_REFERENCE, Lambda Web Adapter dual-target.

darellchua2 7 5 Updated 3w ago
GitHub

Install

npx skillscat add darellchua2/civiltekk-skills/aws-iac-safety-skill

Install via the SkillsCat registry.

SKILL.md

What I do

I provide safety patterns for AWS Infrastructure-as-Code (Terraform/OpenTofu) and GitHub Actions CI/CD, extracted from production incidents. Each pattern caused a real outage or security issue.

When to use me

Use this skill when:

  • Writing or reviewing Terraform/OpenTofu modules managing GitHub or AWS resources
  • Setting up multi-environment CI/CD with SSM parameters
  • Managing ECR repositories across multiple modules
  • Deploying the same Docker image to both EC2 and Lambda
  • Auditing IaC for cross-module ownership violations

Related Skills

  • civiltekk-opentofu-skill (route explore, AWS section): AWS resource discovery (Lambda Function URL CNAME pattern).
  • civiltekk-opentofu-skill (route ecr-provision): ECR provisioning (lowercase-safe naming pattern).
  • security-audit-skill: Security auditing (Lambda public-without-auth, local Terraform state patterns).
  • docker-containerization-skill: Docker patterns (no-rollback-on-deploy pattern).

A. GitHub Resource Management

A1. Repo-Level GitHub Resources Without Count Gating

GitHub repo-level variables/secrets are singular — one per name, shared across environments. Without count gating, non-dev environments attempt to create resources that already exist, triggering HTTP 422 conflicts.

# BAD — creates in ALL environments, 422 on non-dev
resource "github_actions_variable" "registry_token" {
  repository = var.repo_name
  variable_name  = "REGISTRY_TOKEN"
  variable_value = var.registry_token
}

# GOOD — only dev creates repo-level resources
resource "github_actions_variable" "registry_token" {
  count          = var.environment == "dev" ? 1 : 0
  repository     = var.repo_name
  variable_name  = "REGISTRY_TOKEN"
  variable_value = var.registry_token
}

Gotcha: The CI cleanup-resources mechanism uses tofu state rm (not import). After state rm, OpenTofu sees no state for the resource and attempts CREATE on the next plan — triggering the 422 again. The fix is count-gating, not state manipulation.


B. SSM Parameter Management

B1. Hardcoded Dev SSM Path via CI Variable

Passing the full SSM path as a TF_VAR_* via GitHub Actions CI hardcodes the dev path. All environments receive the dev token, causing cross-env auth failures (HTTP 401 in UAT/prod).

# BAD — CI passes TF_VAR_registry_ssm_parameter="/canvastekk/workflow-engine/dev/..."
variable "registry_ssm_parameter" {
  type = string
}
data "aws_ssm_parameter" "registry_token" {
  name = var.registry_ssm_parameter
}

# GOOD — derive path locally from var.environment; remove the variable + CI TF_VAR line
locals {
  ssm_path = "/canvastekk/workflow-engine/${var.environment}/registry-service-token"
}
data "aws_ssm_parameter" "registry_token" {
  name = local.ssm_path
}

Detection: Search for modules with both variable "*_ssm_parameter" AND TF_VAR_*_ssm_parameter in CI workflows.

B2. Update SSM_SECRETS_REFERENCE.md on New Parameters

Every new aws_ssm_parameter resource must be documented in docs/SSM_SECRETS_REFERENCE.md under the appropriate service section. SSM is the single source of truth for secrets; the reference doc is the human-readable index.

<!-- docs/SSM_SECRETS_REFERENCE.md -->

## Workflow Engine

| Parameter | Environment | Description |
|-----------|-------------|-------------|
| `/canvastekk/workflow-engine/dev/registry-service-token` | dev | ECR auth token |
| `/canvastekk/workflow-engine/uat/registry-service-token` | uat | ECR auth token |

C. ECR Lifecycle Management

C1. Cross-Module ECR Lifecycle Policy Ownership

AWS allows only one lifecycle policy per ECR repo. Creating aws_ecr_lifecycle_policy in a consumer module on a repo owned by another module causes silent overwrites and state divergence across environments.

# BAD — consumer module manages lifecycle on a repo it doesn't own
data "aws_ecr_repository" "shared" {
  name = var.shared_repo_name
}

resource "aws_ecr_lifecycle_policy" "shared" {
  repository = data.aws_ecr_repository.shared.name
  policy     = jsonencode({ ... })
}

# GOOD — manage lifecycle where the repo is created (owning module)
# Consumer modules use data sources for READ-ONLY references only

D. Lambda Deployment

D1. Lambda Web Adapter for Dual-Target Deployment

Use AWS Lambda Web Adapter (Rust-based extension) over Mangum (Python ASGI wrapper) for zero-code-change deployment of the same FastAPI Docker image to both EC2 (docker-compose) and Lambda. The adapter is inert outside the Lambda runtime environment.

# Dockerfile — same image works for EC2 and Lambda
FROM python:3.12-slim

# Lambda Web Adapter (harmless on EC2, activated on Lambda)
COPY --from=public.ecr.aws/awsguru/aws-lambda-adapter:0.8.4 \
     /lambda-adapter /opt/extensions/lambda-adapter

# Standard FastAPI setup
COPY . /app
WORKDIR /app
RUN pip install fastapi uvicorn
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8080"]

Operational prerequisites for Lambda:

  • /tmp storage: minimum 5 GB
  • Timeout: minimum 60s (recommended 120s)
  • Memory: minimum 2 GB (recommended 4 GB)
  • Provisioned concurrency for production traffic

Categories