# Reference: file templates All templates below are based on the conventions in `aws-lambda-insights-event-handler`, `thumbnail-generator`, and `humand-rocket-service`. Adapt to the Lambda being migrated; do not copy verbatim without checking. ## `infrastructure/env//versions.tf` ```hcl terraform { required_version = ">= 1.12.2, < 2.0.0" required_providers { aws = { source = "hashicorp/aws" version = "~> 6.0" } } backend "s3" { bucket = "humand-terraform-state-" key = "service//terraform.tfstate" region = "us-east-1" dynamodb_table = "humand-terraform-locks-" encrypt = true } } ``` ## `infrastructure/env//providers.tf` ```hcl provider "aws" { region = "us-east-1" default_tags { tags = { Environment = "" Terraform = "true" Repository = "" } } } ``` ## `infrastructure/env//main.tf` ```hcl locals { account_id = "" vpc_id = "" } data "aws_subnets" "private" { filter { name = "vpc-id" values = [local.vpc_id] } filter { name = "map-public-ip-on-launch" values = [false] } } # Only if Phase 6 applies: module "layer" { source = "../../modules/layer" zip_path = "../../../layer/dist/-layer.zip" } module "service" { source = "../../modules/service" vpc_id = local.vpc_id account_id = local.account_id env = "" private_subnet_ids = data.aws_subnets.private.ids multimedia_bucket = "" # Only if Phase 6: # _layer_arn = module.layer.arn } ``` ## `infrastructure/modules/service/versions.tf` ```hcl terraform { required_version = ">= 1.12.2, < 2.0.0" required_providers { aws = { source = "hashicorp/aws" version = "~> 6.0" } } } ``` ## `infrastructure/modules/service/variables.tf` ```hcl variable "vpc_id" { description = "The VPC ID — used for the security group" type = string } variable "account_id" { description = "The AWS account ID" type = string } variable "env" { description = "The environment name" type = string } variable "private_subnet_ids" { description = "List of private subnet IDs for VPC configuration" type = list(string) } ``` ## `infrastructure/modules/service/main.tf` (skeleton) The IAM policy bodies below are illustrative — read the deployed function's actual policies and bring them across faithfully. ```hcl locals { lambda_name = "" description = "" datadog_extension_layer_version = 94 datadog_node_layer_version = 137 env_without_underscore = replace(var.env, "_", "") } data "aws_region" "current" {} data "aws_secretsmanager_secret" "dd_api_key" { name = "/common/datadog-api-key" } resource "aws_iam_role" "lambda_role" { name = "${local.lambda_name}-lambda" assume_role_policy = jsonencode({ Version = "2012-10-17" Statement = [{ Action = "sts:AssumeRole" Effect = "Allow" Principal = { Service = ["lambda.amazonaws.com"] } }] }) tags = { Name = "${local.lambda_name}-lambda" Environment = var.env } } resource "aws_iam_role_policy" "lambda_inline" { name = "${local.lambda_name}-lambda-policy" role = aws_iam_role.lambda_role.id policy = jsonencode({ Version = "2012-10-17" Statement = [ { Sid = "LambdaSelfPermissions" Effect = "Allow" Action = ["lambda:GetFunction", "lambda:UpdateFunctionCode", "lambda:UpdateFunctionConfiguration"] Resource = ["arn:aws:lambda:${data.aws_region.current.name}:${var.account_id}:function:${local.lambda_name}"] }, { Sid = "CloudWatchLogs" Effect = "Allow" Action = ["logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents"] Resource = "arn:aws:logs:${data.aws_region.current.name}:${var.account_id}:*" }, { Sid = "EC2NetworkInterface" Effect = "Allow" Action = [ "ec2:DescribeNetworkInterfaces", "ec2:CreateNetworkInterface", "ec2:DeleteNetworkInterface", "ec2:DescribeInstances", "ec2:AttachNetworkInterface" ] Resource = "*" } # ... bring across the rest from the existing inline policy ... ] }) } resource "aws_security_group" "lambda_sg" { name = "${local.lambda_name}-${local.env_without_underscore}-lambda" description = "${local.lambda_name}-${local.env_without_underscore}-lambda Security Group" vpc_id = var.vpc_id egress { protocol = "-1" from_port = 0 to_port = 0 cidr_blocks = ["0.0.0.0/0"] description = "Allow request to Internet" } tags = { Name = "${local.lambda_name}-${var.env}-lambda" Environment = var.env } } module "lambda-datadog" { source = "DataDog/lambda-datadog/aws" version = "4.5.0" environment_variables = { DD_API_KEY_SECRET_ARN = data.aws_secretsmanager_secret.dd_api_key.arn DD_ENV = var.env DD_SERVICE = local.lambda_name DD_SITE = "us5.datadoghq.com" DD_LOGS_INJECTION = true DD_TRACE_ENABLED = true DD_PROFILING_ENABLED = true DD_CAPTURE_LAMBDA_PAYLOAD = true DD_MERGE_XRAY_TRACES = false DD_SERVERLESS_APPSEC_ENABLED = false ENV = var.env env = var.env environment = var.env LAMBDA_AWS_REGION = data.aws_region.current.name # ... function-specific env vars ... } datadog_extension_layer_version = local.datadog_extension_layer_version datadog_node_layer_version = local.datadog_node_layer_version role = aws_iam_role.lambda_role.arn filename = "../../../dist/lambda.zip" source_code_hash = filebase64sha256("../../../dist/lambda.zip") function_name = local.lambda_name description = local.description handler = "index.handler" runtime = "nodejs24.x" architectures = ["x86_64"] timeout = 300 publish = true vpc_config_security_group_ids = [aws_security_group.lambda_sg.id] vpc_config_subnet_ids = var.private_subnet_ids # Only if Phase 6: # layers = [var._layer_arn] ephemeral_storage_size = 512 # adjust to existing memory_size = 1024 # adjust to existing } ``` ## `infrastructure/modules/layer/main.tf` (Phase 6 only) ```hcl resource "aws_lambda_layer_version" "" { filename = var.zip_path layer_name = "" source_code_hash = filebase64sha256(var.zip_path) compatible_runtimes = ["nodejs24.x"] description = " + AL2023-compatible shared libs" } ``` ## `infrastructure/modules/layer/variables.tf` ```hcl variable "zip_path" { description = "Path to the layer zip file" type = string } ``` ## `infrastructure/modules/layer/outputs.tf` ```hcl output "arn" { description = "ARN of the published layer version" value = aws_lambda_layer_version..arn } ``` ## `infrastructure/tflint.hcl` ```hcl plugin "aws" { enabled = true version = "0.34.0" source = "github.com/terraform-linters/tflint-ruleset-aws" } plugin "terraform" { enabled = true preset = "recommended" } rule "aws_instance_invalid_type" { enabled = true } rule "terraform_required_providers" { enabled = true } ``` ## `infrastructure/generate-terraform-docs.sh` ```bash #!/bin/bash set -euo pipefail has_terraform_files() { local dir="$1" ls "$dir"/*.tf >/dev/null 2>&1 } modules_dir="./modules" if [ -d "$modules_dir" ]; then find "$modules_dir" -type d -not -path "*/.*" | while read -r path; do if has_terraform_files "$path"; then echo "Generating Terraform docs in $path" terraform-docs markdown table --output-file "$(realpath "$path")/README.md" --output-mode inject "$path" fi done fi ``` Mark executable: `chmod +x infrastructure/generate-terraform-docs.sh` ## `.github/workflows/deployment.yml` (reusable) ```yaml name: General deployment workflow on: workflow_call: inputs: environment: { required: true, type: string } account: { required: true, type: string } ref: { required: true, type: string } env: AWS_REGION: "us-east-1" permissions: id-token: write contents: read jobs: build-and-deploy-code: runs-on: ubuntu-latest environment: ${{ inputs.environment }} steps: - uses: actions/checkout@v6 with: { ref: ${{ inputs.ref }} } - uses: hashicorp/setup-terraform@v4 - uses: aws-actions/configure-aws-credentials@v6 with: role-to-assume: ${{ format('arn:aws:iam::{0}:role/github-oidc-{1}', inputs.account, inputs.environment) }} role-session-name: github-oidc-session aws-region: ${{ env.AWS_REGION }} - uses: actions/setup-node@v6 with: node-version-file: 'package.json' cache: 'npm' # Only if Phase 6: # - name: Build layer # run: ./layer/build.sh - run: npm install - run: npm run build - run: npm run dist - name: Initialize Terraform env: TERRAFORM_ROOT: ${{ format('infrastructure/env/{0}', inputs.environment) }} run: terraform -chdir=$TERRAFORM_ROOT init - name: Apply env: TERRAFORM_ROOT: ${{ format('infrastructure/env/{0}', inputs.environment) }} run: terraform -chdir=$TERRAFORM_ROOT apply -auto-approve ``` ## `.github/workflows/.yml` ```yaml name: Deploy to on: workflow_dispatch: inputs: ref: description: "Git ref to deploy" required: true default: "master" # For stg only: # push: # branches: [master] jobs: deploy: uses: ./.github/workflows/deployment.yml with: environment: account: "" ref: ${{ github.event.inputs.ref || github.sha }} secrets: inherit ``` ## `.github/workflows/ci-infra.yml` ```yaml name: Infrastructure CI on: pull_request: branches: [master, main] paths: - 'infrastructure/**' jobs: terraform-checks: uses: HumandDev/github-actions/.github/workflows/tf-checks.yml@main with: tf_dir: './infrastructure' ``` ## `package.json` (scripts only) ```json { "engines": { "node": "24.x" }, "scripts": { "build": "rm -rf build && mkdir build && cp -r index.js node_modules build/", "dist": "rm -rf dist && mkdir dist && cd build && zip -r lambda.zip . && mv lambda.zip ../dist/" } } ``` If the Lambda has additional asset folders (`headers/`, `footers/`, `templates/`, etc.), add them to the `cp -r` list in `build`. ## `.gitignore` (additions) ``` ### Terraform ### **/.terraform/* *.tfstate *.tfstate.* ### Build artifacts ### build dist node_modules ``` If using Phase 6, also add `layer/build` and `layer/dist`. ## `infrastructure/README.md` Use the same structure as `humand-rocket-service/infrastructure/README.md`: 1. **Installing Dependencies** — Terraform >= 1.12.2, TFLint, Terraform Docs, Node, (Docker if Phase 6) 2. **Best Practices** — `tflint --force --recursive --minimum-failure-severity=warning`, `terraform fmt -recursive -check`, `./generate-terraform-docs.sh` 3. **Deploy** — manual deploy steps: SSO login → build artifacts → `terraform -chdir=$TERRAFORM_ROOT init` → `terraform -chdir=$TERRAFORM_ROOT apply` ## Lambda Layer Dockerfile (Phase 6) ```dockerfile FROM debian:12-slim AS builder RUN apt-get update && apt-get install -y --no-install-recommends \ wget ca-certificates \ \ && rm -rf /var/lib/apt/lists/* # Install / extract the binary into /layer/bin/ # Copy required .so files into /layer/lib/ # Copy any required config / fonts into /layer/etc/ or /layer/fonts/ FROM scratch COPY --from=builder /layer / ``` ## Lambda Layer build.sh (Phase 6) ```bash #!/usr/bin/env bash set -euo pipefail REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" docker build --platform linux/amd64 -t -layer-builder "${REPO_ROOT}/layer" mkdir -p "${REPO_ROOT}/layer/build" "${REPO_ROOT}/layer/dist" # /bin/sh is a placeholder; FROM scratch images need a non-empty CMD for docker create # even though the container is never started. CONTAINER=$(docker create --platform linux/amd64 -layer-builder /bin/sh) docker cp "${CONTAINER}:/" - | tar xf - -C "${REPO_ROOT}/layer/build" docker rm "${CONTAINER}" (cd "${REPO_ROOT}/layer/build" && zip -r ../dist/-layer.zip .) ``` ## Module README starter (with terraform-docs markers) ```markdown # Module: ``` Run `./infrastructure/generate-terraform-docs.sh` to populate the section.