Subchapter 27.44
references/phases/generate/generate-artifacts-infra.mdMarkdown59 KBView on GitHub
Loaded by generate.md when generation-infra.json and aws-design.json exist.
Execute ALL steps in order. Do not skip or optimize.
Transform the design (aws-design.json) and migration plan (generation-infra.json) into deployable Terraform configurations. Migration scripts are generated separately by generate-artifacts-scripts.md.
Read from $MIGRATION_DIR/:
aws-design.json (REQUIRED) — AWS architecture design with cluster-level resource mappingsgeneration-infra.json (REQUIRED) — Migration plan with timeline and service assignmentspreferences.json (REQUIRED) — User preferences including target region, sizing, compliancegcp-resource-clusters.json (REQUIRED) — Cluster dependency graph for orderingReference files (read as needed): references/design-refs/index.md and domain-specific files (compute.md, database.md, storage.md, networking.md, messaging.md, security.md, ai.md).
If any REQUIRED file is missing: STOP. Output: “Missing required artifact: [filename]. Complete the prior phase that produces it.”
Generate $MIGRATION_DIR/terraform/ with only the files needed for domains that have resources in aws-design.json:
| File | Domain | Contains |
|---|---|---|
main.tf | core | Provider config, backend, data sources |
variables.tf | core | All input variables with types and defaults |
outputs.tf | core | Resource outputs and migration summary |
baseline.tf | security baseline | Account-wide security baseline: alternate contacts, password policy, S3 PAB, EBS encryption, Access Analyzer, IMDSv2 default, CloudTrail + S3 log bucket, AWS Budget, GuardDuty. Plus a compliance-conditional section (Config + Security Hub + standards) when preferences.json.compliance contains soc2/pci/hipaa/fedramp. Always emitted; users who want to skip it can delete this file before terraform apply. |
vpc.tf | networking | VPC, subnets, NAT, security groups, route tables |
security.tf | security | IAM roles, policies, KMS keys, Secrets Manager |
storage.tf | storage | S3 buckets, EFS, backup vaults |
database.tf | database | RDS/Aurora instances, parameter groups |
compute.tf | compute | Fargate/ECS, Lambda, EC2, Elastic Beanstalk |
monitoring.tf | monitoring | CloudWatch dashboards, alarms, log groups |
README.md | core | Cost tiers vs this Terraform (one stack; Balanced-aligned) |
Build a generation manifest: read all resources from aws-design.json clusters, assign each to its target .tf file by aws_service:
| AWS Service | Target File |
|---|---|
| Account Alternate Contacts, IAM Account Password Policy, S3 Account Public Access Block, EBS Default Encryption, IAM Access Analyzer (ACCOUNT), EC2 Instance Metadata Defaults, CloudTrail, AWS Budgets, GuardDuty Detector, S3 buckets for CloudTrail and Config logs, AWS Config recorder/delivery/role, AWS Security Hub + standards | baseline.tf |
| VPC, Subnet, NAT Gateway, Security Group, Route Table | vpc.tf |
| IAM Role, IAM Policy, KMS Key, Secrets Manager | security.tf |
| S3, EFS, Backup Vault | storage.tf |
| RDS, Aurora, DynamoDB, ElastiCache | database.tf |
| Fargate, ECS, Lambda, EC2, Elastic Beanstalk | compute.tf |
| CloudWatch, SNS (for alarms) | monitoring.tf |
baseline.tfis always emitted. It is NOT driven byaws-design.jsonclusters — the resources are workload-independent account controls. The compliance-conditional subset (Config + Security Hub) is emitted within the same file whenpreferences.json.compliancecontains soc2/pci/hipaa/fedramp. Theaws_budgets_budgetresource readsestimation-infra.jsonto set itslimit_amount. See Step 1.5 below. Users who want to skip the baseline can deleteterraform/baseline.tfbeforeterraform apply.
BigQuery / specialist-deferred: If aws_service is Deferred — specialist engagement, do not generate Terraform for that resource (no Glue, Athena, Redshift, or EMR modules from the plugin). Optionally add terraform/README-BIGQUERY-DEFERRED.md with a short checklist: engage AWS account team and/or data analytics migration partner before implementing analytics infrastructure.
Requirements:
main.tf, before terraform {): Explain that (1) this directory implements the single architecture in aws-design.json; (2) the migration report’s Premium / Balanced / Optimized figures are three pricing scenarios from estimation-infra.json for that same map — not three separate generated stacks; (3) this Terraform is aligned with the Balanced cost scenario (default sizing/HA posture used for the middle estimate); (4) Premium = higher HA / higher $ model; Optimized = cost-optimization assumptions — users must edit IaC or add modules to realize those postures. Point readers to terraform/README.md and the migration_summary output.terraform block: required_version >= 1.5.0, hashicorp/aws ~> 5.80, active S3 backend (see Step 1a below — do NOT comment it out)provider "aws" block: region = var.aws_region, default_tags with Project, Environment, ManagedBy, MigrationIdaws_caller_identity, aws_region, aws_availability_zonesAlways emit an active (not commented-out) S3 backend block in main.tf. Local state is not safe for production — terraform.tfstate stores resource metadata and sensitive values in plaintext on the local filesystem.
Emit the following backend block inside the terraform {} block in main.tf:
backend "s3" {
# Bootstrap: these resources are created by baseline.tf.
# First run: terraform init -backend=false && terraform apply \
# -target=aws_s3_bucket.tfstate \
# -target=aws_s3_bucket_versioning.tfstate \
# -target=aws_s3_bucket_server_side_encryption_configuration.tfstate \
# -target=aws_s3_bucket_public_access_block.tfstate \
# -target=aws_dynamodb_table.tfstate_lock
# Then re-run: terraform init (migrates local state to S3)
bucket = "<project_name>-<environment>-tfstate-<account_id>" # TODO: substitute values
key = "migration/terraform.tfstate"
region = "<aws_region>" # TODO: substitute target region
dynamodb_table = "<project_name>-<environment>-tfstate-lock" # TODO: substitute values
encrypt = true
}Also emit the following resources in baseline.tf (append after the always-on resources):
# Remote state backend infrastructure
resource "aws_s3_bucket" "tfstate" {
bucket = "${var.project_name}-${var.environment}-tfstate-${data.aws_caller_identity.current.account_id}"
tags = merge(local.baseline_tags, { Component = "terraform-state" })
}
resource "aws_s3_bucket_versioning" "tfstate" {
bucket = aws_s3_bucket.tfstate.id
versioning_configuration { status = "Enabled" }
}
resource "aws_s3_bucket_server_side_encryption_configuration" "tfstate" {
bucket = aws_s3_bucket.tfstate.id
rule {
apply_server_side_encryption_by_default {
sse_algorithm = "aws:kms"
}
}
}
resource "aws_s3_bucket_public_access_block" "tfstate" {
bucket = aws_s3_bucket.tfstate.id
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}
resource "aws_dynamodb_table" "tfstate_lock" {
name = "${var.project_name}-${var.environment}-tfstate-lock"
billing_mode = "PAY_PER_REQUEST"
hash_key = "LockID"
attribute {
name = "LockID"
type = "S"
}
tags = merge(local.baseline_tags, { Component = "terraform-state" })
}Add a Bootstrap section to terraform/README.md explaining the two-step init process.
Always create $MIGRATION_DIR/terraform/README.md when generating Terraform (same pass as Step 1).
Required sections:
aws-design.json (and generation-infra.json / preferences.json as applicable).estimation-infra.json for the same service mapping; order is high → mid → low estimate.estimation-infra.json, migration-report.html / MIGRATION_GUIDE.md for full tier tables.Keep it under one screen of text.
Always emitted. The baseline applies account-wide security controls that should be in place on any new AWS account. Users who do not want the baseline can delete terraform/baseline.tf before terraform apply.
Compute retention. Read preferences.json.compliance (array of strings; may be absent or empty). Compute cloudtrail_retention_days using this mapping, taking max() across all declared values (use 90 if the array is empty or absent):
[] → 90soc2 → 365pci → 365hipaa → 2190fedramp → 1095gdpr → 365Compute budget limit. Read estimation-infra.json.projected_costs.breakdown.total.mid (or the canonical equivalent). Compute budget_limit = max(50, ceil(total_mid * 1.2)). If estimation-infra.json is missing or unreadable, use 50 and emit an inline comment noting that the projection was unavailable.
Choose file-header variant. If compliance contains any of soc2, pci, hipaa, fedramp, emit the compliance-expansion header. Otherwise emit the base header. Both variants include a two-sentence provenance note stating per-unit rates were verified against the AWS Pricing API for us-east-1 on 2026-05-04. Substitute the resolved cloudtrail_retention_days value into the header.
Emit baseline.tf starting with the file-header comment block and a locals block containing the resolved cloudtrail_retention_days integer:
locals {
cloudtrail_retention_days = <N>
}Append the always-on resources, in this order. Each resource carries the plugin’s standard four default tags plus Component = "security-baseline":
aws_account_alternate_contact.operations (ACCT.01, email_address = var.operations_email — fill-once variable, see Step 2)aws_account_alternate_contact.billing (ACCT.01, email_address = var.billing_email)aws_account_alternate_contact.security (ACCT.01, email_address = var.security_email)aws_iam_account_password_policy.baseline (ACCT.06; minimum_password_length = 14, password_reuse_prevention = 24, max_password_age = 90, all four character-class requirements true, hard_expiry = false)aws_s3_account_public_access_block.baseline (ACCT.08; all four flags true)aws_ebs_encryption_by_default.baseline (defense-in-depth; enabled = true)aws_accessanalyzer_analyzer.baseline (ACCT.11; type = "ACCOUNT")aws_ec2_instance_metadata_defaults.baseline (defense-in-depth; http_tokens = "required", http_put_response_hop_limit = 2)aws_cloudtrail.baseline (ACCT.07; multi-region, management events only, enable_log_file_validation = true)aws_s3_bucket.cloudtrail_logs plus aws_s3_bucket_public_access_block, aws_s3_bucket_server_side_encryption_configuration, aws_s3_bucket_versioning, aws_s3_bucket_lifecycle_configuration (transitions driven by local.cloudtrail_retention_days per item 7), and aws_s3_bucket_policy restricting the CloudTrail service principal by aws:SourceArnaws_budgets_budget.monthly_spend (ACCT.10; limit_amount = "<budget_limit>" from item 2; three notification blocks at 50/80/100% ACTUAL; subscriber_email_addresses = [var.billing_email] — same fill-once variable as the alternate contact, entered exactly once in tfvars)aws_guardduty_detector.baseline (defense-in-depth; enable = true, finding_publishing_frequency = "FIFTEEN_MINUTES")If compliance contains any of soc2, pci, hipaa, fedramp, append the compliance-conditional section, wrapped in ########## Compliance-Conditional ########## / ########## End Compliance-Conditional ########## dividers:
aws_iam_role.config + aws_iam_role_policy_attachment for the managed policy arn:aws:iam::aws:policy/service-role/AWS_ConfigRole (note the underscore — AWSConfigRole without it is a deprecated policy name and fails apply)aws_config_configuration_recorder.baseline with recording_group { all_supported = true, include_global_resource_types = true }aws_config_delivery_channel.baseline pointing at the Config S3 bucketaws_config_configuration_recorder_status.baseline with is_enabled = trueaws_s3_bucket.config_logs plus PAB, SSE, versioning, lifecycle (same local.cloudtrail_retention_days), and a bucket policy allowing the config.amazonaws.com service principalaws_securityhub_account.baselineaws_securityhub_standards_subscription.fsbp (always emitted in this section)aws_securityhub_standards_subscription.pci_dss (only if compliance contains pci)Do NOT emit an NIST 800-53 standards subscription, even if compliance contains hipaa or fedramp. Security Hub does not provide a HIPAA-specific standard; FedRAMP attestation is out-of-band.
Lifecycle rule adjustment. Omit the STANDARD_IA transition block when the resolved retention is less than 90 days. Omit the GLACIER transition block when retention is less than 365 days. Both rules apply to both the CloudTrail log bucket and (when emitted) the Config log bucket.
Attach inline HCL comments:
aws_account_alternate_contact.*: a comment pointing at the tfvars fill-once variable (# set var.operations_email in terraform.tfvars — plan fails until you do).aws_cloudtrail.baseline: a collision warning for users who already have a trail in the region.aws_budgets_budget.monthly_spend: the limit-rationale comment (max(50, ceil(total_mid * 1.2)); $50 floor prevents alert noise; users may edit limit_amount directly post-apply).aws_guardduty_detector.baseline: a cost disclosure noting the 30-day free trial and ~$2–25/mo post-trial.aws_config_configuration_recorder.baseline: a cost disclosure ($0.003/CI continuous; $0.012/daily-CI as an opt-in for cost-sensitive users).aws_securityhub_account.baseline: a cost disclosure noting the 30-day free trial and ~$1–15/mo post-trial.defense-in-depth in the inline comment.compute.tf modification (runs during Step 3 compute domain, not here): every aws_launch_template emitted for ECS-EC2, EKS node groups, or bare EC2 receives IMDSv2 enforcement unconditionally — the security baseline is always applied:
metadata_options {
http_tokens = "required"
http_put_response_hop_limit = 1
http_endpoint = "enabled"
instance_metadata_tags = "enabled"
}Fargate, Lambda, and App Runner do not emit launch templates and are unaffected (no synthetic launch template is created). Hop limit 1 here is intentionally different from the account-level default 2 in aws_ec2_instance_metadata_defaults.baseline — strict on templates the plugin owns, permissive at the account default.
Emission conditions:
baseline.tf even when aws-design.json contains only AI or billing-only resources (no infrastructure clusters). The baseline is workload-independent.Global variables (always include): aws_region (from preferences.json target_region), project_name, environment (from preferences.json), migration_id, and the fill-once contact variables operations_email, billing_email, security_email (type string, no default — plan fails until set).
Placeholder guards (REQUIRED): terraform validate passes on syntactically valid placeholders — a user can apply with TODO-ops@example.com as their account security contact and never know. Every variable whose value cannot be inferred (the contact emails, ECR image URIs, and any other user-supplied value that ships with a placeholder in terraform.tfvars.example) MUST carry a validation block that rejects placeholder tokens, so the failure happens loudly at terraform plan with an actionable message:
variable "billing_email" {
description = "Billing contact + budget alert recipient (fill-in checklist #2)"
type = string
validation {
condition = !strcontains(var.billing_email, "TODO") && !strcontains(var.billing_email, "example.com") && strcontains(var.billing_email, "@")
error_message = "Set billing_email in terraform.tfvars to a real inbox (see MIGRATION_GUIDE.md fill-in checklist)."
}
}For non-email placeholders, reject the specific placeholder tokens the example ships with (TODO, ACCOUNT_ID, <). Error messages MUST name the tfvars key and point at the fill-in checklist. The backend block cannot use variables (Terraform limitation) — its ACCOUNT_ID placeholder is covered by the fill-in checklist row instead.
Per-cluster variables: Extract configurable values from aws_config in aws-design.json. Infer types (string, number, bool, list(string), map(string)). Use aws_config values as defaults. Deduplicate shared variables. Add GCP source as comment (e.g., # GCP source: db-custom-2-7680).
Always emit $MIGRATION_DIR/terraform/terraform.tfvars.example alongside variables.tf. Populate it with actual values from aws-design.json, preferences.json, and estimation-infra.json where available. Use descriptive placeholder strings (not empty values) for anything that cannot be inferred. Format:
# Copy this file to terraform.tfvars and fill in the values before running terraform plan.
# Do NOT commit terraform.tfvars to source control — it may contain sensitive values.
aws_region = "<target_region>" # from preferences.json target_region
project_name = "<your-project>" # TODO: set your project name
environment = "production" # TODO: dev | staging | production
migration_id = "<MMDD-HHMM>" # from migration run ID
# One entry per variable in variables.tf, with source annotation as commentAlso emit $MIGRATION_DIR/terraform/.gitignore with:
# Never commit actual variable values — may contain sensitive data
terraform.tfvars
*.tfvars
!terraform.tfvars.example
.terraform/
*.tfstate
*.tfstate.backupBefore generating any .tf, invoke the tf-best-practices skill for its authoring posture —
it is the single source of truth for “what good AWS Terraform looks like.” Treat it as a black
box: pass the context below and follow whatever posture it returns. Do not reach into its
files or assume how it is organized internally — it evolves independently.
Invoke the
tf-best-practicesskill, telling it you are about to authorterraform/(the authoring/pre-generation context), and emit Terraform that satisfies every rule it returns.
Pass the caller context the skill needs (these are gcp-to-aws’s to supply; the skill reads none of our artifacts itself):
compliance — the value of preferences.json → design_constraints.compliance (array;
may be empty/absent). Empty ⇒ the skill emits no compliance-conditional hardening, keeping the
stack minimal and immediately applyable.aws_config values — instance classes, CPU/memory, storage sizes, engine versions from
each resource’s aws_config in aws-design.json. Populate resource attributes from these;
the skill’s posture constrains the shape, not the numbers.Do not re-specify the skill’s posture here — the skill owns it. Step 3.1 covers only the source-glue and resource wiring the skill does not (and should not) know about.
For each domain with resources in the generation manifest:
General rules:
references/design-refs/*.md for AWS configuration best practicesgcp_config / aws_config values from aws-design.json to populate resource attributesconfidence: "inferred" resources, add comment: # Tailored to your setup — verify configuration (JSON confidence: inferred)confidence: "deterministic" resources, optional comment: # Standard pairing (fixed mapping list)secondary_resources from the cluster (IAM roles, security groups)GCP-source-specific rules (these stay here — the skill is source-agnostic and cannot own them):
authorized_networks → warning: if a source google_sql_database_instance has
authorized_networks containing 0.0.0.0/0, emit a warnings[] entry in aws-design.json:
“Cloud SQL authorized_networks includes 0.0.0.0/0 — mapped to private RDS with no public
access.” (The skill already mandates a private, non-0.0.0.0/0 RDS; this warning just records
that the source was public and was intentionally not carried over.)generation-infra.json success_metrics (a gcp-to-aws
artifact); wire them into the CloudWatch dashboard/alarms the skill’s monitoring baseline calls
for.aws_config values.graviton.target_architecture from aws-design.json (see references/shared/graviton.md).
When arm64: emit aws_ecs_task_definition with
runtime_platform { cpu_architecture = "ARM64" operating_system_family = "LINUX" },
aws_lambda_function with architectures = ["arm64"], the Graviton instance type from
aws_config (e.g., m7g.xlarge) on EC2/EKS launch templates, and for Elastic Beanstalk the
t4g.* InstanceType setting on aws_elastic_beanstalk_environment — add inline comment
# Graviton (ARM64) — ~15-20% cheaper than x86; build images with --platform linux/arm64.
When x86_64: emit x86 types with a comment citing the blocker from graviton.caveats.
EKS arm64 node groups are single-arch on dev tier.Domain-specific wiring (posture is owned by the tf-best-practices skill — these rows list
only the gcp-to-aws resource wiring / value population per domain; for every security rule,
follow the posture the skill returned in Step 3.0):
| Domain | gcp-to-aws wiring (skill owns the posture) |
|---|---|
| Networking | Emit the VPC/subnets/NAT/SGs and (for compliance) flow logs per the skill’s posture; populate CIDRs/AZ count from design. Wire an internet-facing ALB only if the design has one. |
| Security | Emit per-service Fargate/Lambda IAM roles and Secrets Manager resources per the skill’s posture (least-privilege, no plaintext master password, compliance-conditional rotation/KMS). Populate ARNs/role names from the cluster’s secondary_resources. |
| Storage | Emit S3 buckets per the skill’s posture (versioning, SSE, block-public-access, CloudFront/OAC for public, compliance-conditional access logging). Populate bucket names/lifecycle from design. |
| Database | Emit RDS/Aurora per the skill’s posture (private, encrypted, deletion_protection, master-password-via-Secrets-Manager, DB-port SG scoping). Populate engine/version/instance class from aws_config; add the Cloud SQL authorized_networks warning (Step 3.1) when applicable. |
| Compute | Emit Fargate/EKS/ECR per the skill’s posture (private subnets, EKS private endpoint, ECR scan-on-push). Populate task CPU/memory and autoscaling from aws_config. Apply Graviton/ARM64 wiring from the CPU architecture rule above. Elastic Beanstalk (App Engine → EB): emit one aws_elastic_beanstalk_application for the app and one aws_elastic_beanstalk_environment per source_service (from aws_config). Resolve solution_stack_name with a Terraform data source (self-updating, needs no generate-time credentials, cannot go stale in the committed file) — do not paste aws_config.platform verbatim (it is a human-readable label like "Python 3.12 running on 64bit Amazon Linux 2023"): emit data "aws_elastic_beanstalk_solution_stack" "<svc>" { most_recent = true, name_regex = "64bit Amazon Linux 2023 (.*) running Python 3.12" } (build name_regex from the language + version in platform) and reference .name. Fallback only if a data source can’t be used: aws elasticbeanstalk list-available-solution-stacks at generate time. Also emit the EB instance profile this environment references — aws_iam_role (EC2 trust) + aws_iam_instance_profile + the managed-policy attachment (AWSElasticBeanstalkWebTier, plus AWSElasticBeanstalkWorkerTier/AWSElasticBeanstalkMulticontainerDocker as the platform needs) — since EB does not auto-create it in Terraform; do not reference an instance profile that no resource emits. Emit setting blocks for: aws:autoscaling:launchconfiguration/IamInstanceProfile (the instance profile just emitted); aws:autoscaling:launchconfiguration/SecurityGroups (an SG for the instances, required when they sit in private subnets); aws:autoscaling:launchconfiguration/InstanceType (from instance_type); aws:elasticbeanstalk:environment/EnvironmentType (from environment_type); when environment_type == "LoadBalanced", also aws:elasticbeanstalk:environment/LoadBalancerType = application (ALB, not the Classic default) plus aws:autoscaling:asg/MinSize (from min_instances) and MaxSize (from max_instances); aws:ec2:vpc/VPCId, Subnets, and ELBSubnets — for LoadBalanced, instances in private subnets + ALB in public (ELBScheme public); for SingleInstance (no ALB) put the instance in a public subnet with AssociatePublicIpAddress=true so it is reachable; and aws:elasticbeanstalk:application:environment for env vars. ARM64: pick the t4g.* instance type from aws_config per the Graviton rule. See design-refs/elastic-beanstalk.md. |
| Monitoring | Emit CloudWatch log groups/dashboard/alarms per the skill’s monitoring baseline; source alarm thresholds from generation-infra.json success_metrics. |
Output identifiers for key resources (VPC ID, database endpoint, ECS cluster name, etc.) plus a migration_summary output (object) including at minimum:
| Key | Type / example | Purpose |
|---|---|---|
aws_region | string | From var.aws_region |
environment | string | From var.environment |
migration_id | string | From var.migration_id |
service_count | number | Count of primary logical services / resources represented |
aligned_with_estimate_tier | string | Always "balanced" for this advisor — generated IaC matches the Balanced scenario in estimation-infra.json |
cost_scenarios_modeled_in_terraform | string | e.g. "design_baseline_only" — only one stack generated; Premium/Optimized exist as pricing scenarios in estimates, not as additional Terraform trees |
Add VPC ID or other IDs when known from resources. Descriptions on every output.
Example shape:
output "migration_summary" {
description = "Migration run metadata and cost-tier alignment (Balanced baseline)"
value = {
aws_region = var.aws_region
environment = var.environment
migration_id = var.migration_id
service_count = <number>
aligned_with_estimate_tier = "balanced"
cost_scenarios_modeled_in_terraform = "design_baseline_only"
}
}Verify these quality rules before reporting completion:
Security posture (owned by the tf-best-practices skill; the Step 6 policy gate re-verifies the statically-checkable subset):
tf-best-practices skill returned at Step 3.0, for the compliance context passed. (The skill defines the rules; do not re-enumerate them here.)gcp-to-aws generation self-check (caller-specific — not owned by the skill):
Not done: No default VPC references — all resources use the created VPC
Not done: No hardcoded credentials in any .tf file
Not done: Tags on every resource (Project, Environment, ManagedBy, MigrationId)
Not done: Every variable has type and description
Not done: Every output has description
Not done: Region from var.aws_region, never hardcoded
Not done: terraform/README.md exists with cost-tier vs Terraform explanation
Not done: main.tf begins with the required cost-tier / Balanced alignment comment block
Not done: migration_summary output includes aligned_with_estimate_tier and cost_scenarios_modeled_in_terraform
Not done: baseline.tf exists.
Not done: baseline.tf contains aws_account_alternate_contact for each of OPERATIONS, BILLING, SECURITY, plus aws_iam_account_password_policy, aws_s3_account_public_access_block, aws_ebs_encryption_by_default, aws_cloudtrail, aws_guardduty_detector, aws_accessanalyzer_analyzer, aws_ec2_instance_metadata_defaults, and aws_budgets_budget.
Not done: baseline.tf contains a locals block with cloudtrail_retention_days set to a positive integer, and the lifecycle expiration.days on aws_s3_bucket_lifecycle_configuration.cloudtrail_logs equals local.cloudtrail_retention_days.
Not done: aws_budgets_budget.monthly_spend.limit_amount equals max(50, ceil(estimation-infra.json.projected_costs.breakdown.total.mid * 1.2)) as a string.
Not done: If compute.tf contains any aws_launch_template, every such launch template has metadata_options { http_tokens = "required", http_put_response_hop_limit = 1 } — this applies unconditionally (the security baseline is always emitted).
Not done: Every aws_ecr_repository in compute.tf has image_scanning_configuration { scan_on_push = true }.
Not done: If compliance contains soc2/pci/hipaa/fedramp, baseline.tf contains aws_config_configuration_recorder, aws_config_delivery_channel, aws_config_configuration_recorder_status, aws_securityhub_account, aws_securityhub_standards_subscription for FSBP.
Not done: If compliance contains pci, an additional aws_securityhub_standards_subscription for PCI DSS exists.
Not done: baseline.tf does NOT contain any aws_securityhub_standards_subscription whose standards_arn references nist-800-53, regardless of compliance values.
Not done: If compliance is empty, absent, or contains only gdpr, baseline.tf does NOT contain any aws_config_* or aws_securityhub_* resources.
Not done: baseline.tf does NOT contain any invented SSB control IDs. Search for ACCT.IAM, ACCT.S3, ACCT.EBS, ACCT.CT, ACCT.GD, ACCT.CFG, ACCT.SH, WKLD.EC2.01 — all MUST have zero matches. Only bare ACCT.01 through ACCT.13 identifiers are permitted.
Not done: baseline.tf does NOT mention “Trusted Advisor” anywhere (Trusted Advisor is docs-action only and out of scope).
Not done: Security Hub subscribes to FSBP (always when the compliance-conditional section is emitted) and PCI DSS (only when compliance contains pci). No other standards subscriptions.
After the Step 5 self-check, validate $MIGRATION_DIR/terraform and write
$MIGRATION_DIR/validation-report.json.
Invoke the tf-best-practices skill for the post-writing validation context — tell it the
terraform/ directory has been written and pass $MIGRATION_DIR/terraform as the target dir.
Treat it as a black box: follow the validation protocol and policy verdict it returns (exit
codes, violations[] shape). Do not reach into its internal files — it evolves
independently.
The authoring posture was already applied at Step 3.0, so the generated Terraform should pass by construction; this step re-verifies the statically checkable subset and produces the machine-readable verdict.
This phase is the caller in that protocol. tf-best-practices is a read-only verdict
producer — it reports whether the Terraform passes. This phase owns everything the skill does
NOT: pass $MIGRATION_DIR/terraform as the target dir, run the fmt/init/validate stages, apply
the fix-and-retry edits to the reported violations[] sites (budget 3), run the retry/skip/abort
prompt, and write validation-report.json.
Caller responsibilities specific to this phase (not owned by the skill):
validation-report.json as policy_status
(+ policy_violations on failure). The verdict is recorded independently of the fmt/init/
validate outcome, so a policy failure is never masked by passed_degraded_offline.POLICY_FAIL that the user does not skip/abort sets top-level
status: "policy_failed" and blocks Phase Completion (see below).Report generated files to the parent orchestrator. Do NOT update .phase-status.json — the parent generate.md handles phase completion.
Before reporting completion, enforce artifact output gate:
terraform/ directory exists.terraform/main.tf, terraform/variables.tf, and terraform/outputs.tf exist.vpc.tf, security.tf, storage.tf, database.tf, compute.tf, monitoring.tf.terraform/baseline.tf MUST exist (baseline is always emitted).validation-report.json MUST exist with policy_status: "POLICY_OK" (per Step 6). A
POLICY_FAIL that was not resolved blocks completion unless the user explicitly chose
skip/abort in the Step 6 retry loop.If this gate fails: STOP and output: “generate-artifacts-infra did not produce required Terraform artifacts; do not complete Generate Stage 2.”
Generated terraform artifacts:
- terraform/README.md
- terraform/main.tf
- terraform/variables.tf
- terraform/outputs.tf
- terraform/[domain].tf (for each domain with resources)
- validation-report.json (status: <validation_status>)
Total: [N] Terraform files
Validation: <validation_status> (attempts=<N>, errors_fixed=<N>)
TODO markers: [N] items requiring manual configuration