---
name: migrate-lambda-ansible-to-terraform
description: Migrates an AWS Lambda from the legacy Humand Ansible deployment (`platform-ecs-deployment` playbooks, `cd.yml` workflow, `config/*.yml`) to Terraform with the `DataDog/lambda-datadog/aws` module, S3 backend state, per-env GitHub Actions workflows, and `tf-checks` PR validation. Includes optional Node runtime upgrade and a Lambda Layer subworkflow for functions that bundle native binaries. Use when a Lambda repo has `deploy-lambda-playbook.yaml` or `config/{dev,stg,prd}.yml` files, or when the user mentions migrating a Lambda from Ansible to Terraform. Does NOT cover TypeScript migration.
---

# /migrate-lambda-ansible-to-terraform — Migrate Lambda from Ansible to Terraform

Migrates a Humand Lambda repo away from the deprecated Ansible-based deployment (`platform-ecs-deployment` playbooks) to Terraform-managed infrastructure, following the conventions in `aws-lambda-insights-event-handler` (greenfield Terraform reference) and `thumbnail-generator` (migration reference).

For full file templates see [reference.md](reference.md).

## When to use

- Repo has `.github/workflows/cd.yml` calling `platform-ecs-deployment` playbooks
- Repo has `config/{dev,stg,prd,...}.yml` with `function.runtime`, `function.handler`, etc.
- Lambda is currently deployed by Ansible and you want to move it to Terraform

## When NOT to use

- Lambda is already Terraform-managed
- Migration also includes JavaScript → TypeScript (do this skill first; TS migration is separate)
- The Lambda is going to be deleted

## Phase 0: Locate repos

Ask the user:

1. **Source repo path** — absolute local path to the Lambda repo being migrated (e.g. `../aws-lambda-html-to-pdf`). Verify it contains a `.github/workflows/cd.yml` or `config/*.yml`.
2. **Reference repo path** — absolute local path to a Terraform-managed Lambda of the same shape (e.g. `../aws-lambda-insights-event-handler` or `../thumbnail-generator`). This is used to extract per-env infrastructure metadata without hardcoding defaults.
3. **Target environments** — default `["dev", "stg", "prd", "testslot1", "testslot2"]`; confirm or adjust.

## Phase 1: Analysis (subagent)

1. Read the full content of `agents/lambda-ansible-config-analyzer.md` from the plugin directory.
2. Launch a subagent with `subagent_type="explore"`, `model: fast`.
3. Pass:
   - `sourceRepoPath` — from Phase 0
   - `referenceRepoPath` — from Phase 0
   - `targetEnvironments` — from Phase 0
4. Wait for the structured report. Validate it covers all 8 sections. If any section is `[NOT FOUND]` for critical data (IAM, env vars, account IDs), ask the user to supply the missing values before proceeding.

## Phase 2: Confirm parameters

Present a summary to the user for confirmation:

- Lambda function name (from the report)
- Target Node.js runtime version
- Native binary detected? (yes → Phase 6 applies)
- `context.done`/`context.fail` usage detected? (yes → handler refactor needed in Phase 4)
- Per-env account IDs and VPC IDs (extracted from reference repo, not hardcoded)
- List of environments to scaffold

Ask: "Does this look correct? Any adjustments before I generate the Terraform?"

## Phase 3: Terraform + workflows scaffolding (subagent)

1. Read the full content of `agents/lambda-terraform-scaffolder.md` from the plugin directory.
2. Launch a subagent with `subagent_type="generalPurpose"`, `model: fast`.
3. Pass:
   - `targetRepoPath` — source repo path from Phase 0
   - `analyzerReport` — full report from Phase 1
   - `templatesPath` — path to this skill's `reference.md`
   - `targetEnvironments` — confirmed in Phase 2
   - `nativeBinarySubworkflow` — boolean from Phase 2 confirmation
4. Wait for the subagent to complete. Review its "Files created" report. If any required file is missing, create it manually using the templates in [reference.md](reference.md).

Also delete the old Ansible-specific files if the subagent didn't:
- `.github/workflows/cd.yml`
- `config/*.yml`

## Phase 4: Update function code and dependencies (main thread)

Run these changes in the main agent thread so the user can see them.

### `package.json`

- Set `"engines": { "node": "<target>.x" }` matching the runtime from Phase 2
- Replace `aws-sdk` v2 with `@aws-sdk/client-*` v3 modular packages (v2 is ~74 MB; v3 modular packages are ~5 MB — critical for the 250 MB size limit)
- Drop any unused legacy deps (`memorystream`, etc.)
- **Scripts must work with `ignore-scripts=true`** in npm config — use `thumbnail-generator`'s pattern with no `pre*` hooks:
  ```json
  "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 asset folders (`headers/`, `footers/`, `templates/`, etc.), add them to the `cp -r` list.

### Handler

- Remove `context.done(null, value)` → `return value` (Node 18+ dropped these APIs)
- Remove `context.fail(error)` → `throw error`
- Migrate AWS SDK v2 calls to v3: `new S3Client({})`, `s3.send(new GetObjectCommand({...}))`, `Body.transformToString()`, `new PutObjectCommand({...})`

### `.gitignore`

Ensure these are present. Match `humand-rocket-service`:

```
**/.terraform/*
*.tfstate
*.tfstate.*
```

Do NOT ignore `.terraform.lock.hcl` — it should be committed (provider dependency lockfile).

## Phase 5: Documentation (main thread)

Create or update:

- `README.md` — purpose, event shape, build/deploy steps, local testing with Lambda RIE, resource limits, env vars table
- `AGENTS.md` — architecture, conventions for AI agents, out-of-scope items
- `infrastructure/README.md` — tflint + fmt + terraform-docs + manual deploy guide
- `infrastructure/modules/<name>/README.md` — heading + `<!-- BEGIN_TF_DOCS -->` / `<!-- END_TF_DOCS -->` block

## Phase 6: Lambda Layer subworkflow (conditional)

Only if the Lambda bundles a **native binary** (detected in Phase 1). Skip otherwise.

Create:

```
layer/
├── Dockerfile    # debian:12-slim base; ABI-compatible with Amazon Linux 2023
├── build.sh      # Builds Docker image, extracts contents, zips to layer/dist/
└── README.md
```

And `infrastructure/modules/layer/` (see templates in [reference.md](reference.md)).

Key conventions:
- Base image: `debian:12-slim` (glibc-compatible with AL2023)
- Final stage: `FROM scratch` — binary + `.so` files + fonts only
- `docker create` gotcha: `FROM scratch` images need a non-empty command argument even though the container is never started → `docker create --platform linux/amd64 <image> /bin/sh`
- Lambda env vars: `LD_LIBRARY_PATH=/opt/lib`, plus any binary-specific config paths under `/opt/...`
- Handler: point to binary at `/opt/bin/<name>`

## Phase 7: Migration runbook (main thread)

Write a `TERRAFORM_IMPORT.md` in the repo with per-env `terraform import` commands. Typical resources for most Lambdas:

```bash
# For each existing env (dev, stg, prd):
terraform -chdir=infrastructure/env/<env> import \
  module.service.aws_iam_role.lambda_role <function-name>-lambda
terraform -chdir=infrastructure/env/<env> import \
  module.service.aws_iam_role_policy.lambda_inline <role-name>:<policy-name>
terraform -chdir=infrastructure/env/<env> import \
  module.service.aws_security_group.lambda_sg <sg-id>
terraform -chdir=infrastructure/env/<env> import \
  'module.service.module.lambda-datadog.aws_lambda_function.this' <function-name>
```

After import, `terraform plan` should show only intended diffs. Fix any unintended diffs before applying.

### First-apply size limit (Phase 6 migrations only)

If the function currently bundles a native binary inline (Phase 6 case), AWS rejects `UpdateFunctionConfiguration` when `current_deployed_code + new_layers > 262144000 bytes`. Workaround — upload slim code via CLI **before** `terraform apply`:

```bash
npm run build && npm run dist
aws lambda update-function-code \
  --function-name <name> \
  --zip-file fileb://dist/lambda.zip \
  --region us-east-1
terraform -chdir=infrastructure/env/<env> apply
```

Document this workaround in `TERRAFORM_IMPORT.md` for each affected env.

### Runtime + downtime window

Between the CLI upload and `terraform apply`, the function is broken (new code calls `/opt/bin/...` but the layer isn't attached yet). For **dev/test slots** this is fine. For **stg/prd** treat it as a ≈30–90s maintenance window.

Also verify the actual current runtime before proceeding: `aws lambda get-function-configuration --function-name <name> --query 'Runtime'` — the `config/*.yml` files are often stale.

## Verification

- [ ] `terraform fmt -recursive -check` passes from `infrastructure/`
- [ ] `tflint --force --recursive --minimum-failure-severity=warning` passes
- [ ] After import: `terraform plan` for each env shows only intended changes
- [ ] `ls -lh dist/lambda.zip` — usually < 10 MB for a slim Lambda
- [ ] `du -sh node_modules` — should be ~20 MB with SDK v3 modular packages, not 75 MB+
- [ ] Old `cd.yml`, `config/*.yml`, and Ansible-specific files have been deleted
- [ ] `README.md`, `AGENTS.md`, `infrastructure/README.md`, and module READMEs exist

## Out of scope

- **JavaScript → TypeScript** — separate migration; do this skill first
- **AWS SDK v3 deep refactor** — only the surface-level swap to `@aws-sdk/client-*` for size
