tf-best-practices
This skill provides authoring rules for generating secure AWS Terraform and runs a read-only policy gate to verify generated code against those rules.
Identity proven by GitHub's verified organisation badge (aws, awslabs)
- Agents
- Claude Code, Codex, Cursor, Gemini CLI, GitHub Copilot, OpenCode
- License
- Apache-2.0 (permissive)
- Pin
- df2ab44pinned Oct 9, 2026
- Context cost
- about 3,000 tokenswhen the skill loads
- GitHub stars
- 2,830on aws/agent-toolkit-for-aws
- Scripts
- 2 scripts
What it does
“Best-practice authoring guidance AND a read-only policy gate for AWS Terraform generated by a migration skill. Load during any phase that writes a terraform/ directory — first as the "what to emit" posture rules + security-baseline spec, then after writing as the deterministic policy verdict. Read-only: it reports whether the generated Terraform passes; it never edits .tf files, never touches .phase-status.json, and never decides phase completion. Complements (does not replace) terraform fmt/init/validate.”
Generated summary from the skill's own text
This skill provides authoring rules for generating secure AWS Terraform and runs a read-only policy gate to verify generated code against those rules. It checks for TLS on internet-facing load balancers, encryption on databases, restricted security group access, and IAM least-privilege patterns. The policy checker runs as a deterministic validator that reports violations by file and line but never modifies Terraform files.
When to use it
- Before writing Terraform to understand required security posture
- After generating Terraform to validate against policy gates
- In migration workflows that auto-generate AWS infrastructure code
- To verify compliance with TLS, encryption, and access control requirements
When not to
- If you need automated fixes to Terraform files instead of just reporting violations
- For checking non-AWS or non-Terraform infrastructure code
- When you need to manage workflow state or phase completion decisions
Install
These commands install from the publisher's repository and need no account.
skills CLI
npx skills add aws/agent-toolkit-for-aws --skill tf-best-practicesInstalls from the repository's default branch, which may have changed since the commit we checked.
GitHub CLI 2.90 or later
gh skill install aws/agent-toolkit-for-aws tf-best-practices@df2ab44e485b28358353d827e5ebafbe267cce9fInstalls the commit we checked (df2ab44).
Copy by hand
Download the folder at df2ab44 and put it in .claude/skills/tf-best-practices for Claude Code and in .agents/skills/tf-best-practices for Codex, Cursor, Gemini CLI, GitHub Copilot and OpenCode.
Claude Code plugin
/plugin install amazon-selling-partner@claude-plugins-officialThe publisher's plugin in Claude's official marketplace. It may hold more than this skill, and we have not pinned it.
Claude Code plugin
/plugin install aws-agents@claude-plugins-officialThe publisher's plugin in Claude's official marketplace. It may hold more than this skill, and we have not pinned it.
Claude Code plugin
/plugin install aws-agents-for-devsecops@claude-plugins-officialThe publisher's plugin in Claude's official marketplace. It may hold more than this skill, and we have not pinned it.
Copy for your agent
A prompt for any coding agent. It installs this skill from GitHub at the pinned commit, copies the files as they are, and runs nothing unless you ask.
Prompt
Install the agent skill "tf-best-practices" by Amazon (AWS). Source, pinned to commit df2ab44e485b28358353d827e5ebafbe267cce9f: https://github.com/aws/agent-toolkit-for-aws/tree/df2ab44e485b28358353d827e5ebafbe267cce9f/plugins/aws-startup-advisor/skills/tf-best-practices 1. Download that folder at exactly this commit. Do not use the default branch. 2. Copy every file unchanged, keeping the folder structure, into .claude/skills/tf-best-practices/ for Claude Code and .agents/skills/tf-best-practices/ for Codex, Cursor, Gemini CLI, GitHub Copilot and OpenCode. Create the folders if they do not exist. 3. Do not edit the files, and do not run any script in the folder unless I ask you to. 4. When you are done, list the files you copied and where you put them. Listing and checks: https://www.agentnative.dev/skills/amazon-aws/tf-best-practices-agent-toolkit-for-aws
SKILL.md at the pin
---
name: tf-best-practices
description: >-
Best-practice authoring guidance AND a read-only policy gate for AWS Terraform generated by a
migration skill. Load during any phase that writes a terraform/ directory — first as the "what to
emit" posture rules + security-baseline spec, then after writing as the deterministic policy verdict.
Read-only: it reports whether the generated Terraform passes; it never edits .tf files, never touches
.phase-status.json, and never decides phase completion. Complements (does not replace) terraform
fmt/init/validate.
---
# tf-best-practices — Generated-IaC posture rules + read-only policy gate
A **shared authoring guide and verdict producer**, not a workflow. It answers two
questions for a phase that generates AWS Terraform:
1. **Before writing** — "what security posture must the generated `terraform/` follow?"
(the posture rules + the `baseline.tf` account-hardening spec)
2. **After writing** — "does the generated `terraform/` pass policy?" (a deterministic,
**read-only** verdict + a machine-readable report)
## Routing — load the part that matches your context
This skill is entered at two touchpoints in the caller's Generate flow, with the caller's
own terraform-authoring work in between. **The caller states which touchpoint it is at when it
loads this skill**, and reads the corresponding part:
| Caller context | Load | Why |
| ----------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- |
| **About to author `terraform/`** (before writing) | Part 1 → [`references/security-posture-rules.md`](references/security-posture-rules.md) | The "what to emit" AWS authoring rules (gate-enforced + authoring-only + compliance-conditional). |
| **`terraform/` written, ready to validate** (after writing) | Part 2 → [`references/terraform-validation.md`](references/terraform-validation.md) + run the gate script | The `fmt → init → validate → policy` protocol and the read-only verdict. |
Everything this skill states is **source-cloud-agnostic** (pure AWS Terraform). Any GCP/Heroku
detection or artifact reading is the caller's job; where a rule needs a caller-known fact (e.g.
declared compliance frameworks), the caller passes it as a **caller-context signal** — see
`references/security-posture-rules.md` § _Caller-context signals_.
## Boundary (read this first)
This unit is a **verdict producer, never a mutator**. Its entire write surface is the
JSON verdict it is asked to emit. Specifically it **MUST NOT**:
- edit, format, or rewrite any `.tf` file (the caller owns remediation),
- read or write `.phase-status.json` or any run-state file (interpreter-owned),
- decide whether a phase may complete, or prompt the user (caller policy).
The caller (a migration skill's Generate phase) owns: the fix-and-retry loop that edits
the `.tf` it generated, `terraform fmt` auto-apply, the retry/skip/abort prompt, the
Phase Completion gate, and every `.phase-status.json` write. See the consuming skill's
generate phase for how the verdict feeds those decisions.
> **Consumers:** `gcp-to-aws` (prose Generate) and `heroku-to-aws` (DSL Generate). The
> contract is source-agnostic; each caller wires the two touchpoints in its own Generate
> idiom — gcp-to-aws as prose steps, heroku-to-aws as a fragment step plus a fail-closed
> `_postconditions` assert enforced by the interpreter.
## Part 1 — Authoring posture (load before writing `terraform/`)
Emit generated Terraform that satisfies the posture in
[`references/security-posture-rules.md`](references/security-posture-rules.md).
These are the "what good AWS Terraform looks like" rules. Following them makes the Part 2 gate
pass by construction. This unit does not read the caller's artifacts — it consumes only
caller-context signals the caller passes in.
> **Scope.** `security-posture-rules.md` covers, in three tiers:
>
> - **Gate-enforced** (Part 2 verifies statically): ALB TLS, no-public-database, RDS +
> ElastiCache encryption-at-rest, no-public-DB-port ingress, no-public admin/datastore-port
> ingress, no-wildcard-IAM.
> - **Authoring-only** (not gate-checkable, still required): `deletion_protection`,
> master-password-via-Secrets-Manager, S3 hardening, Fargate/EKS/ECR settings, private-subnet
> placement, backups, baseline monitoring.
> - **Compliance-conditional** (emitted when the caller declares `soc2`/`pci`/`hipaa`/`fedramp`):
> VPC flow logs, S3 access logging, secret rotation, customer-managed KMS.
>
> Still the **caller's** own generation concern (candidates to migrate here later): the
> account-hardening `baseline.tf` layer (CloudTrail, GuardDuty, Config, Security Hub).
## Part 2 — Policy gate (run after writing `terraform/`)
Run the read-only checker against the generated directory. Resolve the script path relative to
the plugin root (`$PLUGIN_ROOT/skills/tf-best-practices/scripts/...`), the same convention the
plugin uses for its other scripts:
```bash
python3 "$PLUGIN_ROOT/skills/tf-best-practices/scripts/validate-terraform-policy.py" "$TERRAFORM_DIR" --json "$VERDICT_PATH"
```
- `$TERRAFORM_DIR` — **required, caller-supplied**: the generated `terraform/` directory
(e.g. `$MIGRATION_DIR/terraform`). This skill never defaults or discovers it — the caller
always passes the path it wrote Terraform to.
- `--json $VERDICT_PATH` — optional; writes a machine-readable verdict the caller can merge
into its own `validation-report.json`.
The policy check is one stage of a larger validation flow (`fmt → init → validate → policy`).
The full protocol — including offline-fallback behavior and how the policy verdict maps into a
`validation-report.json` — is documented in
[`references/terraform-validation.md`](references/terraform-validation.md). That protocol is
descriptive: the **caller** owns the fmt/init/validate execution, the fix-and-retry loop, and
the report write; this unit contributes only the read-only policy stage + verdict shape.
### Exit codes → caller action
| Exit | stdout | Meaning | Caller does |
| ---- | ------------- | ------------------ | ------------------------------------------------------------------------------- |
| `0` | `POLICY_OK` | posture satisfied | proceed |
| `1` | `POLICY_FAIL` | violations present | read `violations[]`, edit the named `.tf` sites, re-run (caller's retry budget) |
| `2` | (usage error) | bad path / IO | surface to user; do not treat as pass |
### Verdict shape (`--json`)
```json
{
"check": "policy",
"policy_status": "POLICY_OK | POLICY_FAIL",
"violations": [
{
"check": "policy",
"rule": "alb_https_listener | alb_http_redirect | no_tf_files",
"file": "compute.tf",
"line": 7,
"severity": "error",
"summary": "human-readable violation",
"fix_hint": "concrete remediation the caller can apply"
}
]
}
```
Each `violations[]` entry is **actionable evidence** — `file` + `line` + `fix_hint` tell the
caller exactly what to edit. The caller applies the edit; this unit only reports.
## Policy rules enforced today
Every rule is **fail-open on ambiguity** — it fires only on unambiguous, in-block literal
evidence, so a valid stack is never falsely blocked (a `POLICY_FAIL` is a hard completion gate
for the caller, so a false positive would block a real migration).
**Internet-facing ALB TLS posture** (an ALB is internet-facing when `internal` is absent,
`false`, or variable-driven — fail-safe):
- **`alb_https_listener`** — must have an HTTPS listener on `443` with `certificate_arn` and a
`forward` action.
- **`alb_http_redirect`** — an HTTP `:80` listener must `redirect` to HTTPS, never `forward`
to targets. Internal ALBs (`internal = true`) are exempt.
> **Elastic Beanstalk ALBs are invisible to these rules.** The ALB rules inspect standalone
> `aws_lb_listener` blocks. An EB **LoadBalanced** environment provisions its ALB from
> `aws_elastic_beanstalk_environment` `setting` blocks, which the static checker does not read —
> so a pure-EB design passes the ALB rules **vacuously** (no listener to inspect). EB listener/TLS
> posture is therefore authoring-only, not gate-enforced. (Fixtures `good-heroku-eb-only`
> and `good-heroku-eb-singleinstance` document this; `good-heroku-eb-loadbalanced` carries a
> standalone ALB so the listener rules are exercised on real blocks.)
**Managed database exposure & encryption** (`aws_db_instance`, `aws_rds_cluster`):
- **`rds_not_public`** — must not set `publicly_accessible = true` (absent/variable → fail-open).
- **`rds_encryption_at_rest`** — must set `storage_encrypted = true`; missing or literal `false`
fires (RDS defaults to unencrypted), variable-driven fails open. S3 is not checked (default
SSE-S3 since Jan 2023).
**ElastiCache encryption** (`aws_elasticache_replication_group`, Redis `aws_elasticache_cluster`):
- **`elasticache_encryption_at_rest`** — a replication group must set
`at_rest_encryption_enabled = true`; missing or literal `false` fires, variable-driven fails
open.
- **`elasticache_cluster_encryption`** — a Redis-engine `aws_elasticache_cluster` (single-node:
`engine = "redis"`, no `replication_group_id`) must set BOTH `at_rest_encryption_enabled = true`
and `transit_encryption_enabled = true`; missing or literal `false` on either fires,
variable-driven fails open. `engine = "memcached"` clusters (and variable-driven/absent engine)
are exempt — Memcached does not support these attributes.
**Security group ingress:**
- **`db_sg_no_public_ingress`** — an inline `aws_security_group` ingress covering `5432`/`3306`
must not allow `0.0.0.0/0` or `::/0`.
- **`sg_no_public_admin_ingress`** — an inline ingress must not open a curated never-public
admin/datastore port (`22`, `3389`, `6379`, `11211`, `27017`, `9200`/`9300`, `5601`) to
`0.0.0.0/0` or `::/0`. Web (`80`/`443`) and app/game ports are not flagged; DB ports are
handled by the rule above. Both check `cidr_blocks` and `ipv6_cidr_blocks` independently, so a
benign IPv4 list does not mask an open IPv6 one. Both: separate `aws_security_group_rule` /
`aws_vpc_security_group_ingress_rule` resources fail open (not correlated).
**IAM least-privilege** (`aws_iam_policy`, `aws_iam_role_policy`, `aws_iam_group_policy`,
`aws_iam_user_policy`):
- **`no_wildcard_iam`** — an `Allow` statement must not use `Action`/`Resource` `"*"`.
The one narrow exception is an isolated
`elasticbeanstalk:CreateStorageLocation` statement with `Resource = "*"` because
AWS does not support resource-level permissions for that action.
`aws_iam_policy_document` data sources and assume-role trust policies fail open.
> The checker is a zero-dependency static HCL reader (no `terraform init`, no provider
> download) — it runs even when the registry is unreachable. It uses brace-depth matching
> for nested blocks, so a valid HTTPS listener written with a nested `forward { ... }`
> block is **not** a false failure.
## Fixtures (also the checker's regression suite)
`fixtures/terraform-policy/` holds **intentionally-shaped** Terraform used by
`scripts/test_validate_terraform_policy.py`:
- `bad-http-forward/` — internet-facing ALB that forwards plaintext HTTP → MUST `POLICY_FAIL`.
- `internal-alb-only/` — internal ALB on HTTP → MUST `POLICY_OK` (HTTP allowed internally).
- `good-https-redirect/` — the correct pattern → `POLICY_OK`.
These are deliberately non-compliant test data (never deployed). They are excluded from the
repo-wide `checkov` scan via `.checkov.yaml` `skip-path`; do **not** "harden" them — doing so
breaks the tests that assert the failure paths.
## Verification
```bash
# from skills/tf-best-practices/
uv run --python 3.12 --with pytest python -m pytest scripts/test_validate_terraform_policy.py -q
```
From aws/agent-toolkit-for-aws at df2ab44, shown as plain text under its license: Apache-2.0 (permissive). The license text is in LICENSE.
View SKILL.md on GitHub at df2ab44Files
- SKILL.md13 KB · 453d31be9091
- .gitignore51 B · 7297ce5ab823
- fixtures/
- terraform-policy/
- bad-db-sg-public-quoted-port/
- vpc.tf514 B · 426b021b2cb6
- bad-db-sg-public/
- vpc.tf356 B · c50b130a639e
- bad-elasticache-cluster-redis-unencrypted/
- cache.tf389 B · 146a1f5c652f
- bad-elasticache-unencrypted/
- cache.tf422 B · e703be11f292
- bad-heroku-eb-elasticache/
- cache.tf834 B · 4e4582c9fdc2
- bad-http-forward/
- compute.tf556 B · 9682a3a8bb54
- bad-rds-public-brace-in-comment/
- main.tf690 B · e5d3d1b052a5
- bad-rds-public-closing-brace/
- main.tf692 B · bfa162513204
- bad-rds-public-interpolated-brace/
- rds.tf903 B · c0efe2d0fc8a
- bad-rds-public-unencrypted/
- database.tf372 B · a2c7f8d9b96f
- bad-sg-public-ipv6/
- vpc.tf1.1 KB · 81948c912c5d
- bad-sg-public-ssh/
- vpc.tf537 B · 0d7fc552e93d
- bad-wildcard-iam-listform/
- security.tf470 B · a1e9dc72397e
- bad-wildcard-iam/
- security.tf381 B · 7e8382d69968
- good-db-sg-scoped/
- vpc.tf986 B · 35d3b846d657
- good-elasticache-cluster-memcached/
- cache.tf372 B · 12ee5e54e029
- good-elasticache-cluster-redis-encrypted/
- cache.tf601 B · f614674ac58b
- good-elasticache-encrypted/
- cache.tf749 B · b83834a40438
- good-heroku-eb-codepipeline/
- pipeline.tf295 B · 0d61f837c9c1
- good-heroku-eb-loadbalanced/
- compute.tf2.7 KB · 11d270474446
- database.tf839 B · ce7371f4e120
- security.tf899 B · d6b535a119d1
- good-heroku-eb-only/
- compute.tf1.5 KB · 41168b604b26
- good-heroku-eb-singleinstance/
- compute.tf1.9 KB · 9c43195b63f0
- good-https-redirect/
- compute.tf1.0 KB · 8b0d29b57d66
- good-iam-scoped-list/
- security.tf573 B · 4b525d461559
- good-internet-nlb/
- compute.tf621 B · e26810a8f909
- good-quoted-port-https/
- compute.tf1.3 KB · 15b35a006f7d
- good-rds-private-encrypted/
- database.tf607 B · f2534c5ee593
- good-scoped-iam/
- security.tf744 B · a7e4491c1b16
- good-sg-ipv6-scoped/
- vpc.tf1.0 KB · 902c7c9dbebd
- good-sg-public-webapp/
- vpc.tf989 B · d850c8eaa5ee
- internal-alb-only/
- compute.tf555 B · 5fd4c8f4bfc5
- references/
- security-posture-rules.md16 KB · f6cb76bf605b
- terraform-validation.md9.3 KB · b23691bc5c03
- scripts/
- test_validate_terraform_policy.py34 KB · 56ae40cf6947
- validate-terraform-policy.py45 KB · 711bf2aa931d
39 files, 144 KB in total. Each file has a sha256 in the pin's manifest; the first 12 characters are shown.
Skill folder on GitHub at df2ab44Also in
The same SKILL.md content appears in these places too. We list it once and credit the oldest public source.
More from Amazon (AWS)
agent-advisor
Guides startups through a phased decision process to choose an AWS runtime for AI agents (AgentCore, ECS, EKS, Lambda, or Lambda MicroVMs), plan migrations of…
Contains scripts
Claude Code · about 4.6K tokens · pinned 7 days ago
agents-build
This skill extends an AgentCore agent project with capabilities like memory, app integration, VPC networking, multi-agent orchestration, browser automation…
Contains scripts
Claude Code · about 2K tokens · pinned 7 days ago
amazon-bedrock
Builds generative AI applications on Amazon Bedrock, covering model invocation, Knowledge Bases for RAG, Bedrock Agents, Guardrails for content safety, and…
Contains scripts
Claude Code, Codex, Cursor +3 · about 8.3K tokens · pinned 7 days ago
amazon-eventbridge-event-bus
Enables building event-driven applications on AWS EventBridge custom event buses with publish/subscribe routing, event retention, filtering, transformation…
Claude Code, Codex, Cursor +3 · about 8.4K tokens · pinned 7 days ago
amazon-neptune
Provides guidance on Amazon Neptune Database and Neptune Analytics for graph workloads including fraud detection, agentic memory, recommendations, and…
Contains scripts
Claude Code, Codex, Cursor +3 · about 4.5K tokens · pinned 7 days ago
architect-for-startups
Provides stage-appropriate AWS architecture guidance for startups from pre-revenue through Series B+, considering runway, team size, and AWS credits.
Claude Code, Codex, Cursor +4 · about 3.4K tokens · pinned 7 days ago
Related in Cloud and DevOps
azure-to-aws
This skill guides agents through migrating Azure workloads to AWS across six phases: discovering Azure resources from Terraform, live CLI, app code, and…
by Amazon (AWS)Official publisher
Contains scripts
Claude Code, Codex, Cursor +3 · about 4.3K tokens · pinned 7 days ago
gcp-to-aws
Migrates workloads from Google Cloud Platform to AWS, including AI and agentic applications from any provider.
by Amazon (AWS)Official publisher
Contains scripts
Claude Code, Codex, Cursor +3 · about 14.3K tokens · pinned 7 days ago
heroku-to-aws
This skill guides migration of applications and databases from Heroku to AWS services like Elastic Beanstalk, RDS, ElastiCache, and MSK.
by Amazon (AWS)Official publisher
Contains scripts
Claude Code, Codex, Cursor +3 · about 7K tokens · pinned 7 days ago
gcp-to-aws
Migrates Google Cloud Platform workloads to AWS through a structured 6-phase process that discovers GCP resources from Terraform, billing exports, or live…
by Amazon (AWS)Official publisher
Contains scripts
Claude Code, Codex, Cursor +3 · about 13.9K tokens · pinned 7 days ago
gcp-to-aws
Migrates workloads from Google Cloud Platform to AWS through a six-phase process: discover GCP resources from Terraform, code, or billing data; clarify…
by Amazon (AWS)Official publisher
Contains scripts
Claude Code, Codex, Cursor +3 · about 13.2K tokens · pinned 7 days ago
heroku-to-aws
The skill guides users through a 6-phase structured process to migrate Heroku workloads to AWS services.
by Amazon (AWS)Official publisher
Contains scripts
Claude Code, Codex, Cursor +3 · about 6.5K tokens · pinned 7 days ago