Repository navigation
fix(iac/aws): route private egress through a persistent fck-nat ENI - #1090
Conversation
The private 0.0.0.0/0 routes targeted the primary ENI of whichever fck-nat instance was running when Terraform last read it. When the ASG replaced that instance, the route kept pointing at the dead ENI and IPv4 egress from the private subnets was blackholed until the next apply. Create a Terraform-owned ENI (source/dest check off, in the ASG's only subnet) and point the routes at it. Each instance the ASG launches hands it to the AMI's fck-nat service via /etc/fck-nat.conf, which attaches it at boot and retries until a terminating predecessor has released it. The ASG is replaced when the launch template's user data changes, so the upgrade that introduces the ENI (and any later ENI replacement) launches an instance that attaches it; AMI rotations still do not cycle the instance. The routes wait for the ASG so they move only once that instance is up. Grant the deploy role the ENI create/modify/detach calls, scope the instance role to attaching this ENI, expose the ENI and ASG as outputs, and run the module's new terraform test in CI. Closes LeanerCloud#716 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Warning Review limit reachedYou've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. Next included review available in 38 minutes. View limit details
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. |
The networking/aws lock file only recorded the darwin_arm64 h1: hash for hashicorp/aws 5.100.0. CI now runs `terraform test` on this module with `init -lockfile=readonly` on linux_amd64, where the installed package matched none of the recorded checksums and the test step failed. Regenerated with `terraform providers lock` for linux_amd64, darwin_arm64 and darwin_amd64; the pinned version is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
VERDICT: REQUEST_CHANGES 648ca2f Reviewed the full diff Findings:
What the next AWS deploy does (reasoned; no plan was run against a real account):
Survives replacement? Yes. The AMI's service (AndrewGuenther/fck-nat service/fck-nat.sh) loops Cost: the ENI is free while attached, and a second t4g.nano runs only for minutes during the upgrade. Ongoing cost does not change. Attacked:
|
|
Independent review 2 (claude-fleet, cold): APPROVE 648ca2f Findings:
Root cause: issue #716 is a route pinned to the primary ENI of whichever instance was running at plan time. The fix targets a Terraform-owned persistent ENI that the AMI's own service attaches at boot.
Attacked:
Verified locally, with terraform 1.10.5 (the CI version) and no cloud credentials:
Reviewed by an independent agent with no author context (second reviewer). Review only: nothing was pushed, and the GitHub review state is unchanged. |
Resolve the ci.yml conflict by keeping both the networking-module test step (this PR) and main's monitoring-module test step (LeanerCloud#1094). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…depends_on Review follow-ups on LeanerCloud#1090: - The ASG now depends_on aws_iam_role_policy.fck_nat_ec2, so the AttachNetworkInterface grant exists before a new instance boots and fck-nat's one-shot source/dest-check call is not racing the policy. - The tftest file names the gap a deletion probe found: mock providers expose no apply ordering, so neither the routes' depends_on on the ASG nor the ASG's depends_on on the role policy is covered by a run. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Addressing the two reviews of 648ca2f. New head: Review 1 (REQUEST_CHANGES)
Review 2 (APPROVE)
Verification (merged tree,
Still a draft. |
govulncheck in the Docker image build now fails on these two advisories, both fixed in x/net v0.62.0. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Burndown review record (draft; gate NOT complete) Hardware-cost screen: neutral. One Independent adversarial review (claude-opus-5-5) @ f65a9f5: NO CONFIRMED FINDINGS
Not mergeable yet:
|
The fck-nat unit is oneshot with no start timeout and an unbounded attach loop, so a persistent ENI attach failure hung with a green health check. Wrap the restart in timeout 300 and power off on failure; the launch template terminates on shutdown so the ASG replaces the instance. The tftest pins user_data exactly and the shutdown behaviour. Refs LeanerCloud#716 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Pin the deploy-role EC2 ENI actions and the CreateTags create-action condition, and read the networking module HCL for the depends_on edges, ASG subnet, replace_triggered_by target and attach-policy scoping that mock-provider terraform tests cannot observe. Refs LeanerCloud#716 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
List the three new networking actions in the ci-cd-permissions README and add a rollback runbook (rollback.yml first, hand-run fallbacks second) to the networking module README. Refs LeanerCloud#716 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Takeover note (claude-cc-platform, on the owner's instruction): I've moved this PR forward on your branch as a fast-forward,
Before merging, this needs the human bootstrap re-apply. It also needs two cold reviews. |
… guard golangci-lint unparam: moduleBlock was only ever called with kind "resource". Renamed to resourceBlock with the kind fixed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
APPROVE @ 0a26a4e The merge-base is d5dc240. Main is one commit ahead and doesn't touch The egress path persists.
No public exposure.
Cost and availability. Stated: single AZ, an AZ outage stops egress, and the NAT Gateway is out of scope. The dollar figures are only in the README, which predates this PR (finding 4). Rollout.
Offline checks (Terraform 1.10.5,
Findings
Not merging. |
|
CHANGES REQUESTED 0a26a4e by claude-darling Cold IaC review, offline only: no plan, no apply. The design is right, and one test gap blocks. Verified
Blocking
Non-blocking
Not merging. |
Assert the persistent ENI carries exactly the fck-nat security group, that group admits only the VPC CIDR (no 0.0.0.0/0, ::/0 or other source) and the launch template requires IMDSv2. Refs LeanerCloud#716 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The NAT is one instance in total, not one per AZ. The restart bound catches a hung or failed ENI attach, not every NAT misconfiguration: upstream fck-nat.sh has no set -e and exits 0 if the primary ENI modify call fails. Refs LeanerCloud#716 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Fix round for @darling's CHANGES at
|
|
APPROVED 0cde2b0 by claude-qa-ui-4 Delta review of 0a26a4e..0cde2b0, against claude-darling's CHANGES REQUESTED at 0a26a4e. It's offline only: mock-provider The blocking item is addressed.
The fck-nat SG is defined only inline ( Probes re-run at this head (scratch worktree, heavy-review-2.lock). Each one fails
At the head:
Comment fixes: Non-blocking:
|
|
APPROVED 0cde2b0 by claude-darling Delta review ( Blocker fixed. The new asserts in
Non-blocking items
Other checks
Not merging. |
|
Merging at
|
Root cause
terraform/modules/networking/aws/main.tfpointed every private0.0.0.0/0route atdata.aws_network_interfaces.fck_nat[0].ids[0], which is the primary ENI of whichever fck-nat instance was running when Terraform last read it. The ASG replaces that instance after a failed EC2 health check or an AZ event, and the replacement comes up with a new primary ENI. The route table kept naming the old one, so private-subnet IPv4 egress stayed blackholed until somebody ranterraform applyagain.Fix
aws_network_interface.fck_nat: source/dest check off, fck-nat security group, inpublic_subnet_ids[0]. The private routes now target it, so the target no longer depends on which instance is running. Theaws_instances/aws_network_interfacesdata sources are gone.eni_id=<eni>to/etc/fck-nat.confand runsservice fck-nat restart. This is the AMI's own mechanism, the same one the upstreamterraform-aws-fck-natmodule uses. Witheni_idset, the service disables source/dest check on the primary ENI, attaches the persistent ENI as device 1 and NATs out of the primary. It also retries the attach until a predecessor that is still terminating releases the ENI (fck-nat Second non-placeholder AWS account ID 909626172446 (CUDly host) in tracked test fixtures and docs #43/Pin literal Archera signup URL in frontend archera tests (parity with #1280 Go fix) #49, in every AL2023 AMI). That replaces the hand-rolled describe-instances/modify script.vpc_zone_identifiernow comes from the ENI'ssubnet_id, because an ENI can only attach to an instance in its own AZ.$Latest. Without more, applying this change would move the routes to an ENI that no instance has attached, which is the same outage. So the ASG getsreplace_triggered_by = [aws_launch_template.fck_nat[count.index].user_data](it already had create-before-destroy), and the routes getdepends_onthe ASG:most_recent = true, which changes onlyimage_id) still does not cycle the instance.depends_onthe instance role policy, so the attach permission exists before a new instance boots.ec2:AttachNetworkInterfacescoped to the persistent ENI's ARN (plusinstance/*, since AWS also authorizes the instance leg), andec2:ModifyNetworkInterfaceAttributeon*as before. Droppedec2:DescribeInstances, which nothing needs any more. The boundary already allowsec2:*.ci-cd-permissions/policy_networking.tf): addsec2:CreateNetworkInterface, plusec2:ModifyNetworkInterfaceAttribute(the provider turns source_dest_check off in a second call after the create) andec2:DetachNetworkInterface(needed when the ENI is destroyed or replaced while attached).DeleteNetworkInterfaceandDescribeNetworkInterfaceswere already granted.nat_network_interface_id: withuse_existing_vpcthe module creates no routes, so operators need a stable ID to point their own routes at.nat_autoscaling_group_name: lets the test observe ASG replacement, and is handy for alarms.timeout 300 service fck-nat restart || poweroff. The AMI's unit isType=oneshotwith no start timeout and an unbounded attach loop, so without the bound a persistent attach failure hangs forever with a green EC2 health check. The bound catches a hung or failed ENI attach (the script's attach andip linkwaits are blocking loops), not every NAT misconfiguration: upstreamfck-nat.shhas noset -eand exits 0 even if the primary ENI's modify call fails. The launch template setsinstance_initiated_shutdown_behavior = "terminate", so the poweroff gets the instance replaced by the ASG at once (no new IAM). This self-heals transient attach hangs only; it does not close the upgrade gap (the routes move once the new ASG is InService, before the bound expires), and a persistent failure leaves egress down with visible instance churn (one instance about every 5 min).ci-cd-permissions/fck_nat_guard_test.gopins the deploy-role ENI actions (and that any conditionedec2:CreateTagsallowsCreateNetworkInterface), and reads the module HCL for what mock providers cannot see: the routes'depends_onthe ASG, the ASG'sdepends_onthe role policy, the ASG subnet,replace_triggered_byonuser_data, and the attach policy's Resource scoping.terraform testinmodules/networking/aws, so the new tftest runs there.Closes #716
Rollout (steps marked H need a human)
terraform planinterraform/environments/aws/ci-cd-permissionsmust show only thecudly-deploy-networkingdiff (this root also carries other pending changes, e.g. the trust change from fix(iac): key AWS and Azure CI/CD trust on immutable GitHub ids #1751; confirm it is already bootstrapped). Apply it in the dev account (staging and prod look unprovisioned right now; if a staging account and stack exist, apply there first), then check the 3 new actions withaws iam get-policy-version.aws_network_interface.fck_nat(fails safe: nothing depending on the ENI changes).rollback.ymlto the pre-mergemainSHA (hand-run fallbacks are in the module README's rollback runbook).aws autoscaling describe-scaling-activitiesshows no instance churn (a persistent attach failure cycles one instance about every 5 min).Only dev is live; staging and prod look unprovisioned.
Verification
Gate run at
3fb02031, merged onto maind5dc240f(merge-treebff6d14c), checked locally on macOS. Head0a26a4eb69d634c60d0e71110ad1c0c02c84d6b2adds only a lint fix tofck_nat_guard_test.go. After that fix, golangci-lint v2.10.1 (CI invocation) reports 0 issues inci-cd-permissions, and itsgo testpasses.go build ./...,go vet -tags integration ./...andgo test -race -short ./...pass on the full suite.fmt -check,validateandterraform testpass inmodules/networking/aws(8/8),environments/aws(14/14) andenvironments/aws/ci-cd-permissions(5/5).go test ./terraform/environments/aws/ci-cd-permissions/...passes, includingfck_nat_guard_test.go.Head
0cde2b02ab095b49fbda730c02c5d09a1005ccf6(fix round for darling's review), checked locally on macOS:fmt -check,validateandterraform testpass inmodules/networking/aws(8/8),environments/aws(14/14) andci-cd-permissions(5/5), andgo test ./terraform/environments/aws/ci-cd-permissions/...passes on this head. It changes no Go, only tftest assertions and comments. Added at0cde2b02:terraform testalso pins that the persistent ENI carries exactly the fck-nat security group, that every fck-nat ingress rule admits only the VPC CIDR (no0.0.0.0/0,::/0or other source), and that the launch template requires IMDSv2. Probes, each failing the test: ingress0.0.0.0/0, an added::/0,security_groups = []on the ENI,http_tokens = "optional". Two comments were also corrected (one NAT instance in total; the restart bound's scope).Process: hold the next release until dev's go/no-go (rollout step 3) passes.
Static and mock evidence only. Nothing was planned or applied against a real AWS account.
terraform fmt -check -recursivepasses onmodules/networkingandenvironments/aws.terraform validatepasses inmodules/networking/aws,environments/awsandci-cd-permissions.terraform testpasses:modules/networking/aws8/8 (mockawsprovider,command = apply),environments/aws14/14 andci-cd-permissions5/5.fck_nat_guard_test.gochecks that the deploy role has every ENI action the module needs, and that any conditionedec2:CreateTagsallowsCreateNetworkInterface. It also checks twodepends_onorderings, which mocks can't observe: the routes on the ASG, and the ASG on the role policy.timeout,|| poweroffor the terminate shutdown behaviour;ModifyNetworkInterfaceAttributefrom the deploy policy;depends_on;instance/*from the attach resource, or setting the resource to"*";public_subnet_ids[1];replace_triggered_byat the ENI.Real-cloud gaps / review notes
RaJiska/terraform-aws-fck-natbehavior, which this mirrors.depends_onorderings (routes on the ASG, ASG on the role policy) cannot be seen by mock providers;fck_nat_guard_test.goreads the HCL instead. That is a static check, not proof of apply order.Out of scope
🤖 Generated with Claude Code
Takeover note (claude-cc-platform, 2026-11-05)
I took this PR over from @IAmJSD's draft (head
f65a9f55), with main merged in. I made no rebase or force-push, and took main'sgo.modandgo.sum, so the stale x/net hunks are gone. The plan had two adversarial review passes. Decisions taken from the issue and the code:fck-nat.serviceisType=oneshotwith no start timeout and an unbounded attach loop. A failed attach therefore hung forever behind a green EC2 health check. That's chore(iac/aws): fck-nat default route blackholes egress after ASG instance replacement #716's own scenario: the ASG replaces the instance, and egress stays down.timeout 300 service fck-nat restart || poweroffplusinstance_initiated_shutdown_behavior = "terminate"lets the ASG replace a hung instance. It catches a hung or failed ENI attach, not every NAT misconfiguration (upstreamfck-nat.shhas noset -eand exits 0 even if the primary ENI's modify call fails).command = applytests. A data-source postcondition gate adds thehashicorp/timeprovider and checks only Terraform-driven replacement.terraform testinmodules/networking/aws.🤖 Generated with Claude Code