open-swe/infra
Adam Moussa 92fd886076
feat(infra): add /infra CDK scaffold + per-env OIDC/instance IAM role defs (#6)
PR#1 of the AWS migration. CDK TypeScript app under /infra: stacks open-swe-iam
(four per-env GitHub-OIDC deploy roles) + open-swe-dev/-prod (per-env EC2 instance
role + AMI cache). aws-cdk-lib pinned exact 2.260.0; kebab naming Aspect + 15 tests.

IAM is synth-only (NOT deployed). Cleared the Phase-1 security gates:
- T4 GPT-4.1 IAM cross-review (StringEquals trust; cdk-hnb659fds-* wildcard kept as
  org convention; AWS-RunShellScript timeboxed to T19).
- T5 /sh-security-review: deploy roles split PER-ENV with env-scoped OIDC trust
  (dev=branch ref+tag dev, prod=environment:prod+tag prod) so a dev token cannot
  reach prod; re-verified block:false.
2026-06-26 15:06:30 -04:00
..
bin feat(infra): add /infra CDK scaffold + per-env OIDC/instance IAM role defs (#6) 2026-06-26 15:06:30 -04:00
lib feat(infra): add /infra CDK scaffold + per-env OIDC/instance IAM role defs (#6) 2026-06-26 15:06:30 -04:00
test feat(infra): add /infra CDK scaffold + per-env OIDC/instance IAM role defs (#6) 2026-06-26 15:06:30 -04:00
.gitignore feat(infra): add /infra CDK scaffold + per-env OIDC/instance IAM role defs (#6) 2026-06-26 15:06:30 -04:00
cdk.context.json feat(infra): add /infra CDK scaffold + per-env OIDC/instance IAM role defs (#6) 2026-06-26 15:06:30 -04:00
cdk.json feat(infra): add /infra CDK scaffold + per-env OIDC/instance IAM role defs (#6) 2026-06-26 15:06:30 -04:00
package-lock.json feat(infra): add /infra CDK scaffold + per-env OIDC/instance IAM role defs (#6) 2026-06-26 15:06:30 -04:00
package.json feat(infra): add /infra CDK scaffold + per-env OIDC/instance IAM role defs (#6) 2026-06-26 15:06:30 -04:00
README.md feat(infra): add /infra CDK scaffold + per-env OIDC/instance IAM role defs (#6) 2026-06-26 15:06:30 -04:00
tsconfig.json feat(infra): add /infra CDK scaffold + per-env OIDC/instance IAM role defs (#6) 2026-06-26 15:06:30 -04:00

open-swe infra (CDK TypeScript)

AWS infrastructure for the Open SWE → AWS migration. Synth-only at this stage — nothing here is deployed yet. All IAM is applied only after the Phase-1 security gate (T4 GPT-4.1 IAM cross-review + T5 /sh-security-review) clears (T6).

Layout

infra/
├── bin/
│   └── app.ts                       # CDK app entry — instantiates the 3 stacks, applies the naming Aspect
├── lib/
│   ├── config.ts                    # account/region/org constants, env type, OIDC trust subjects
│   ├── open-swe-iam-stack.ts        # account-level: shared OIDC deploy roles
│   ├── open-swe-stack.ts            # per-env stack (instance role + AMI cache wiring)
│   ├── aspects/
│   │   └── kebab-naming-aspect.ts   # fails synth on any non-kebab-case explicit name
│   └── constructs/
│       ├── github-deploy-roles.ts   # githubdeploy-open-swe-infra + githubdeploy-open-swe-app
│       ├── instance-role.ts         # open-swe-<env>-instance-role (least-privilege)
│       └── ami-cache.ts             # cached ARM64 AL2023 helper + EBS/AMI discipline docs
├── test/
│   └── kebab-naming-aspect.test.ts  # jest: Aspect passes conforming names, flags bad ones
├── cdk.json
├── cdk.context.json                 # COMMITTED — pins the AMI (see AMI cache discipline)
├── package.json                     # aws-cdk-lib pinned EXACT (2.260.0)
├── tsconfig.json
├── jest.config.js
└── .gitignore

Stacks

Stack name (kebab) Construct Contents
open-swe-iam OpenSweIamStack Account-level shared GitHub OIDC deploy roles (singletons).
open-swe-dev OpenSweStack (envName: dev) open-swe-dev-instance-role + AMI-cache wiring. EC2/ALB/etc. land at T12.
open-swe-prod OpenSweStack (envName: prod) open-swe-prod-instance-role + AMI-cache wiring.

Account 328440206208, region us-east-1. Stack names are set explicitly so CDK never defaults to PascalCase; resource names follow open-swe-<env>-*.

The two env stacks (open-swe-dev / open-swe-prod) are the required pair. The shared OIDC deploy roles are account-wide singletons (one RoleName each), so they live in their own dedicated open-swe-iam stack rather than being duplicated across the env stacks — and that stack deploys first (see ordering).

IAM roles defined (unapplied)

  • githubdeploy-open-swe-infra — GitHub OIDC role for CDK/CFN infra deploys. Trust scoped to repo:Sea-Haven-Industries/open-swe on the main/dev branches only. Permission is the org-standard CDK pattern: sts:AssumeRole on the CDK bootstrap roles (cdk-hnb659fds-*) — the real CFN/IAM/resource scope lives in the bootstrap cfn-exec-role, not in this role.
  • githubdeploy-open-swe-app — GitHub OIDC role for app deploys. Tag-scoped ssm:SendCommand (instances tagged project=open-swe + env in {dev,prod}) + read-only access to the open-swe-<env>-assets S3 artifact buckets.
  • open-swe-<env>-instance-role — EC2 instance role, least-privilege: read open-swe-<env>-assets (S3), read /open-swe-<env>/* (SSM), read open-swe-<env>/* (Secrets Manager), put /open-swe/<env>/* CloudWatch Logs, plus AmazonSSMManagedInstanceCore for SSM agent registration. No admin.

The GitHub OIDC provider already exists account-wide (created for seahaven-site); it is referenced by ARN, never re-created.

Kebab-case naming Aspect

KebabNamingAspect (applied app-wide in bin/app.ts) fails synth via Annotations.addError when a stack name or an explicit physical resource name (RoleName, BucketName, …) is not kebab-case. Path-style names (Secrets Manager a/b, SSM /a/b, log groups /aws/.../x) are validated per /-segment. CDK logical construct ids are intentionally NOT validated (they are conventionally PascalCase). Covered by test/kebab-naming-aspect.test.ts.

AMI cache discipline (EBS-fix plumbing — stub for T12)

cachedArm64AmazonLinux2023() (in lib/constructs/ami-cache.ts) returns an ARM64 Amazon Linux 2023 image with cachedInContext: true, so the resolved AMI id is pinned in the committed cdk.context.json. Without the pin, every deploy could pick up a newer AL2023 release → AMI change → EC2 instance replacement (the file-share data-loss root cause — memory feedback_inline_ebs_volumes).

Design intent documented in code for T12 to plug into:

  • userDataCausesReplacement: true is the deliberate choice — user-data is provisioning-only and carries no durable state.
  • No durable state on the box → no RETAIN volume. The in-memory langgraph store is rebuilt on every boot from S3 + Secrets Manager / SSM, so there is intentionally no standalone ec2.Volume + removalPolicy.RETAIN. The goal is replacement-tolerance, not avoidance.
  • Snapshot-before-replace still applies operationally at T12: snapshot the root volume and wait state=completed before any replacing deploy, and re-verify "no local-only durable state" first.

Refresh the AMI pin deliberately:

cdk context --reset 'ssm:account=328440206208:parameterName=/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-arm64:region=us-east-1'
cdk synth   # review the diff — it WILL show "requires replacement"

The committed cdk.context.json ships a dummy-but-valid-shaped AMI id (ami-00000000000000000) so cdk synth resolves the cache locally without any live AWS call. Replace it with the real resolved id when T8/T12 build the AMI.

Commands

npm install
npx cdk synth open-swe-iam
npx cdk synth open-swe-dev
npx cdk synth open-swe-prod
npm test            # jest — naming Aspect

Deploy ordering (when the gate clears — NOT yet)

  1. open-swe-iam first — create githubdeploy-open-swe-infra + set the repo AWS_DEPLOY_ROLE_ARN secret before any infra/secrets CI step (BLOCK#3).
  2. Security gate — T4 GPT-4.1 IAM cross-review + T5 /sh-security-review on the synth; resolve every confirmed critical/high.
  3. IAM applied (T6) — only after the gate.
  4. Env stacks (open-swe-dev, then open-swe-prod) build out at T12+, prod gated by a GitHub Environment manual approval.

Version policy

aws-cdk-lib is pinned EXACT (2.260.0) — no ^/~. Dependabot keeps it current; CI (npm ci + cdk synth) + dependency review gate each bump. See aws-infrastructure.md "CDK Version Policy" and memory feedback_cdk_lib_bundled_deps.