mirror of
https://github.com/Sea-Haven-Industries/open-swe.git
synced 2026-09-30 10:23:14 +00:00
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.
This commit is contained in:
parent
5a5818f4c2
commit
92fd886076
16 changed files with 5386 additions and 0 deletions
19
infra/.gitignore
vendored
Normal file
19
infra/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
# CDK / build output
|
||||
cdk.out/
|
||||
*.js
|
||||
*.d.ts
|
||||
*.js.map
|
||||
|
||||
# deps
|
||||
node_modules/
|
||||
|
||||
# env
|
||||
.env
|
||||
|
||||
# coverage
|
||||
coverage/
|
||||
|
||||
# NOTE: cdk.context.json IS committed on purpose (pins the AMI / lookups so
|
||||
# deploys are reproducible and don't implicitly pick up a newer AMI — see
|
||||
# lib/constructs/ami-cache.ts and feedback_inline_ebs_volumes).
|
||||
!cdk.context.json
|
||||
132
infra/README.md
Normal file
132
infra/README.md
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
# 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:
|
||||
|
||||
```bash
|
||||
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
|
||||
|
||||
```bash
|
||||
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`.
|
||||
35
infra/bin/app.ts
Normal file
35
infra/bin/app.ts
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
#!/usr/bin/env node
|
||||
import "source-map-support/register";
|
||||
import * as cdk from "aws-cdk-lib";
|
||||
import { ACCOUNT, REGION } from "../lib/config";
|
||||
import { OpenSweIamStack } from "../lib/open-swe-iam-stack";
|
||||
import { OpenSweStack } from "../lib/open-swe-stack";
|
||||
import { KebabNamingAspect } from "../lib/aspects/kebab-naming-aspect";
|
||||
|
||||
const app = new cdk.App();
|
||||
const env = { account: ACCOUNT, region: REGION };
|
||||
|
||||
// Account-level shared OIDC deploy roles (singletons). Deployed FIRST.
|
||||
new OpenSweIamStack(app, "OpenSweIamStack", {
|
||||
stackName: "open-swe-iam",
|
||||
env,
|
||||
});
|
||||
|
||||
// The two env stacks — explicit kebab-case stackName (never let CDK default to
|
||||
// PascalCase), env-parameterised so resources are `open-swe-<env>-*`.
|
||||
new OpenSweStack(app, "OpenSweDevStack", {
|
||||
stackName: "open-swe-dev",
|
||||
env,
|
||||
envName: "dev",
|
||||
});
|
||||
|
||||
new OpenSweStack(app, "OpenSweProdStack", {
|
||||
stackName: "open-swe-prod",
|
||||
env,
|
||||
envName: "prod",
|
||||
});
|
||||
|
||||
// Fail synth on any non-kebab-case explicit resource/stack name.
|
||||
cdk.Aspects.of(app).add(new KebabNamingAspect());
|
||||
|
||||
app.synth();
|
||||
3
infra/cdk.context.json
Normal file
3
infra/cdk.context.json
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"ssm:account=328440206208:parameterName=/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-6.1-arm64:region=us-east-1": "ami-00000000000000000"
|
||||
}
|
||||
21
infra/cdk.json
Normal file
21
infra/cdk.json
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"app": "npx ts-node --prefer-ts-exts bin/app.ts",
|
||||
"watch": {
|
||||
"include": ["**"],
|
||||
"exclude": [
|
||||
"README.md",
|
||||
"cdk*.json",
|
||||
"**/*.d.ts",
|
||||
"**/*.js",
|
||||
"tsconfig.json",
|
||||
"package*.json",
|
||||
"node_modules",
|
||||
"cdk.out"
|
||||
]
|
||||
},
|
||||
"context": {
|
||||
"@aws-cdk/aws-lambda:recognizeLayerVersion": true,
|
||||
"@aws-cdk/core:checkSecretUsage": true,
|
||||
"@aws-cdk/core:target-partitions": ["aws"]
|
||||
}
|
||||
}
|
||||
98
infra/lib/aspects/kebab-naming-aspect.ts
Normal file
98
infra/lib/aspects/kebab-naming-aspect.ts
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
import { Annotations, CfnResource, IAspect, Stack, Token } from "aws-cdk-lib";
|
||||
import { IConstruct } from "constructs";
|
||||
|
||||
/**
|
||||
* One "/"-delimited segment must be lower kebab-case: `a-b-c`, digits allowed.
|
||||
*/
|
||||
const KEBAB_SEGMENT = /^[a-z0-9]+(-[a-z0-9]+)*$/;
|
||||
|
||||
/**
|
||||
* CloudFormation property keys that carry an *explicit physical name*. The
|
||||
* codegen'd L1 stores these either camelCased (`roleName`) or CFN-cased
|
||||
* (`RoleName`) depending on the construct, so the aspect matches keys
|
||||
* case-insensitively.
|
||||
*
|
||||
* We deliberately validate physical NAMES + the stack name only — not CDK
|
||||
* logical construct ids (those are conventionally PascalCase, e.g.
|
||||
* `InfraDeployRole`, and validating them would be wrong).
|
||||
*/
|
||||
const NAME_PROPERTY_KEYS = [
|
||||
"RoleName",
|
||||
"BucketName",
|
||||
"FunctionName",
|
||||
"TableName",
|
||||
"LogGroupName",
|
||||
"QueueName",
|
||||
"TopicName",
|
||||
"SecretName",
|
||||
"StreamName",
|
||||
"RepositoryName",
|
||||
"DBInstanceIdentifier",
|
||||
"DBClusterIdentifier",
|
||||
"StateMachineName",
|
||||
"RuleName",
|
||||
"UserPoolName",
|
||||
];
|
||||
// NOTE: `PolicyName` is intentionally NOT checked — CDK auto-generates inline
|
||||
// `DefaultPolicy` names (e.g. "InstanceRoleDefaultPolicyF15F...") from the
|
||||
// logical id; those are not explicit, user-controlled physical names and are
|
||||
// outside the naming convention's scope.
|
||||
|
||||
const NAME_KEYS_LC = new Set(NAME_PROPERTY_KEYS.map((k) => k.toLowerCase()));
|
||||
|
||||
/**
|
||||
* `true` when every non-empty "/"-delimited segment is kebab-case.
|
||||
*
|
||||
* Path-style names are tolerated so the same check works for Secrets Manager
|
||||
* (`open-swe-dev/foo`), SSM params (`/open-swe-dev/foo`) and log groups
|
||||
* (`/open-swe/dev/agent`): each segment is validated independently, and a
|
||||
* leading slash (empty first segment) is ignored.
|
||||
*/
|
||||
export function isKebabCase(value: string): boolean {
|
||||
return value
|
||||
.split("/")
|
||||
.filter((seg) => seg.length > 0)
|
||||
.every((seg) => KEBAB_SEGMENT.test(seg));
|
||||
}
|
||||
|
||||
/**
|
||||
* Aspect that FAILS synth (`Annotations.addError`) when an explicitly-named
|
||||
* resource — or a stack name — is not kebab-case. Enforces the org naming
|
||||
* convention (naming-conventions.md) deterministically at synth time so a
|
||||
* non-conforming name can never reach a deploy. Wired in bin/app.ts via
|
||||
* `Aspects.of(app).add(new KebabNamingAspect())`.
|
||||
*/
|
||||
export class KebabNamingAspect implements IAspect {
|
||||
public visit(node: IConstruct): void {
|
||||
if (node instanceof Stack) {
|
||||
const name = node.stackName;
|
||||
if (!Token.isUnresolved(name) && !isKebabCase(name)) {
|
||||
Annotations.of(node).addError(
|
||||
`Stack name "${name}" is not kebab-case (open-swe naming convention).`,
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (node instanceof CfnResource) {
|
||||
// `_cfnProperties` is the props as set on the L1; resolve to collapse any
|
||||
// intrinsic tokens (refs/getatt) so only literal strings are checked.
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const raw = (node as any)._cfnProperties ?? {};
|
||||
const resolved = Stack.of(node).resolve(raw) ?? {};
|
||||
for (const [key, value] of Object.entries(resolved)) {
|
||||
if (
|
||||
NAME_KEYS_LC.has(key.toLowerCase()) &&
|
||||
typeof value === "string" &&
|
||||
!Token.isUnresolved(value) &&
|
||||
!isKebabCase(value)
|
||||
) {
|
||||
Annotations.of(node).addError(
|
||||
`Resource "${node.node.path}" property ${key}="${value}" is not kebab-case ` +
|
||||
`(open-swe naming convention).`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
47
infra/lib/config.ts
Normal file
47
infra/lib/config.ts
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
/**
|
||||
* Shared, non-sensitive constants for the open-swe infra app.
|
||||
* Account / region are locked per the migration spec (TODO.md "Architecture (locked)").
|
||||
*/
|
||||
|
||||
export const ACCOUNT = "328440206208";
|
||||
export const REGION = "us-east-1";
|
||||
|
||||
export const GITHUB_ORG = "Sea-Haven-Industries";
|
||||
export const GITHUB_REPO = "open-swe";
|
||||
|
||||
export type EnvName = "dev" | "prod";
|
||||
|
||||
/** `open-swe-dev` / `open-swe-prod` — kebab-case stack + resource prefix. */
|
||||
export const prefix = (env: EnvName): string => `open-swe-${env}`;
|
||||
|
||||
/**
|
||||
* Per-ENV GitHub OIDC trust subject for the deploy roles (T5 OSWE-IAC-01/02 fix:
|
||||
* the dev/prod boundary is enforced in the IAM trust, not by convention).
|
||||
*
|
||||
* - `dev` → the `dev` integration branch ref (auto-deploy on push to dev).
|
||||
* - `prod` → the **GitHub `prod` Environment** subject. A workflow can only mint
|
||||
* a token with sub `…:environment:prod` by declaring `environment: prod`,
|
||||
* which triggers the Environment's manual-approval gate (Adam, T18). So the
|
||||
* prod approval is now expressed at the IAM layer: a dev-branch token can
|
||||
* never assume a prod deploy role.
|
||||
*
|
||||
* Each env gets its OWN infra + app role (githubdeploy-open-swe-{infra,app}-<env>)
|
||||
* so a dev token cannot reach prod. Exact subject → StringEquals (no `*`).
|
||||
*
|
||||
* Residual (documented): CDK's single account-wide `cfn-exec-role` means the dev
|
||||
* INFRA role can still technically `cdk deploy open-swe-prod`; the workflow only
|
||||
* ever targets its own env stack, and prod's environment-gated role is the
|
||||
* approved path. Per-env bootstrap qualifiers would close this fully (future).
|
||||
*/
|
||||
export const oidcSubject = (env: EnvName): string =>
|
||||
env === "prod"
|
||||
? `repo:${GITHUB_ORG}/${GITHUB_REPO}:environment:prod`
|
||||
: `repo:${GITHUB_ORG}/${GITHUB_REPO}:ref:refs/heads/dev`;
|
||||
|
||||
/**
|
||||
* The GitHub Actions OIDC provider already exists account-wide (created for
|
||||
* seahaven-site; see .github/oidc-deploy-roles.yaml `CreateOIDCProvider=false`).
|
||||
* Reference it by ARN — never create a duplicate `AWS::IAM::OIDCProvider`
|
||||
* (CloudFormation rejects a second provider for the same URL).
|
||||
*/
|
||||
export const GITHUB_OIDC_PROVIDER_ARN = `arn:aws:iam::${ACCOUNT}:oidc-provider/token.actions.githubusercontent.com`;
|
||||
62
infra/lib/constructs/ami-cache.ts
Normal file
62
infra/lib/constructs/ami-cache.ts
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
import * as ec2 from "aws-cdk-lib/aws-ec2";
|
||||
|
||||
/**
|
||||
* cdk.context.json key for the cached AL2023 ARM64 AMI. `latestAmazonLinux2023`
|
||||
* + ARM_64 in the pinned aws-cdk-lib resolves the public SSM parameter below
|
||||
* (the default-kernel alias for this CDK version is the `kernel-6.1` line); with
|
||||
* `cachedInContext: true` CDK stores the resolved id under this exact key.
|
||||
* Exported so the env stack can check "is the AMI already pinned?" and skip the
|
||||
* resolve at synth when it is not — guaranteeing T3 synth makes no live AWS call.
|
||||
*
|
||||
* If an aws-cdk-lib bump changes the default kernel alias, `cdk synth` will write
|
||||
* a new key into cdk.context.json — update this constant + the committed pin to
|
||||
* match (the AMI cache naturally tracks the CDK version).
|
||||
*/
|
||||
export const AL2023_ARM64_SSM_CONTEXT_KEY =
|
||||
"ssm:account=328440206208:parameterName=/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-6.1-arm64:region=us-east-1";
|
||||
|
||||
/**
|
||||
* Cached ARM64 Amazon Linux 2023 machine image.
|
||||
*
|
||||
* ── EBS / AMI cache discipline (see memory feedback_inline_ebs_volumes) ──
|
||||
*
|
||||
* `cachedInContext: true` PINS the resolved AMI id into the committed
|
||||
* cdk.context.json. Without it, `latestAmazonLinux2023()` resolves the NEWEST
|
||||
* AL2023 release on every synth/deploy, so a routine deploy can swap the AMI →
|
||||
* EC2 instance REPLACEMENT whenever AWS ships a release. That was the root cause
|
||||
* of the file-share data-loss incidents (5/15, 5/27, 6/5). Refresh the pin
|
||||
* DELIBERATELY:
|
||||
*
|
||||
* cdk context --reset '<AL2023_ARM64_SSM_CONTEXT_KEY>' && cdk synth
|
||||
*
|
||||
* then review the `cdk diff` (it WILL report "requires replacement") before
|
||||
* deploying.
|
||||
*
|
||||
* ── userDataCausesReplacement intent (consumed at T12) ──
|
||||
*
|
||||
* open-swe user-data is provisioning-only: install the langgraph runtime, pull
|
||||
* config from Secrets Manager / SSM, pull the build artifact from S3, start the
|
||||
* service. It holds NO durable state. T12 sets `userDataCausesReplacement: true`
|
||||
* DELIBERATELY so a config/bootstrap change rolls a fresh, known-good box.
|
||||
*
|
||||
* ── "No durable state on box → no RETAIN volume" assertion ──
|
||||
*
|
||||
* The langgraph store is in-memory and is reconstructed on every boot from S3
|
||||
* (artifact) + Secrets Manager / SSM (config). Nothing of record lives only on
|
||||
* the instance's disk. Therefore there is intentionally NO standalone
|
||||
* `ec2.Volume` + `removalPolicy.RETAIN` here: the design goal is replacement-
|
||||
* TOLERANCE, not replacement-avoidance.
|
||||
*
|
||||
* ── Operational guard still applies at T12 (feedback_inline_ebs_volumes) ──
|
||||
*
|
||||
* Before ANY replacing deploy (AMI / userData / instance-type change): snapshot
|
||||
* the root volume AND wait for `state=completed`, re-verify "no local-only
|
||||
* durable state" first, and ensure cdk-diff-on-PR surfaces the replacement at
|
||||
* review time.
|
||||
*/
|
||||
export function cachedArm64AmazonLinux2023(): ec2.IMachineImage {
|
||||
return ec2.MachineImage.latestAmazonLinux2023({
|
||||
cpuType: ec2.AmazonLinuxCpuType.ARM_64,
|
||||
cachedInContext: true,
|
||||
});
|
||||
}
|
||||
154
infra/lib/constructs/github-deploy-roles.ts
Normal file
154
infra/lib/constructs/github-deploy-roles.ts
Normal file
|
|
@ -0,0 +1,154 @@
|
|||
import * as iam from "aws-cdk-lib/aws-iam";
|
||||
import { Construct } from "constructs";
|
||||
import {
|
||||
ACCOUNT,
|
||||
EnvName,
|
||||
GITHUB_OIDC_PROVIDER_ARN,
|
||||
REGION,
|
||||
oidcSubject,
|
||||
} from "../config";
|
||||
|
||||
/**
|
||||
* Per-ENV GitHub Actions OIDC deploy roles. Created ONCE per env in the
|
||||
* dedicated `open-swe-iam` stack. Two roles per env, per the locked architecture's
|
||||
* "dual OIDC roles":
|
||||
*
|
||||
* - githubdeploy-open-swe-infra-<env> → CFN/IAM (CDK) deploys of that env's stack
|
||||
* - githubdeploy-open-swe-app-<env> → app deploys (env-tag-scoped SSM + S3 read)
|
||||
*
|
||||
* T5 OSWE-IAC-01/02 fix: roles are split per env and the trust subject is
|
||||
* env-scoped (dev = dev branch ref; prod = the GitHub `prod` Environment subject,
|
||||
* so the manual-approval gate is IAM-enforced). A dev-branch token therefore
|
||||
* cannot SendCommand to the prod box nor assume a prod deploy role.
|
||||
*
|
||||
* Reviewed at T4 (GPT-4.1 IAM cross-review) + T5 (/sh-security-review) and
|
||||
* deployed FIRST (BLOCK#3 "OIDC-role-first" ordering) before any other infra or
|
||||
* secrets CI step.
|
||||
*/
|
||||
export class GithubDeployRoles extends Construct {
|
||||
public readonly infraRole: iam.Role;
|
||||
public readonly appRole: iam.Role;
|
||||
|
||||
constructor(scope: Construct, id: string, envName: EnvName) {
|
||||
super(scope, id);
|
||||
|
||||
// The provider already exists account-wide — reference, never re-create.
|
||||
const provider = iam.OpenIdConnectProvider.fromOpenIdConnectProviderArn(
|
||||
this,
|
||||
"GithubOidcProvider",
|
||||
GITHUB_OIDC_PROVIDER_ARN,
|
||||
);
|
||||
|
||||
// T4 BLOCK#2 + T5 IAC-01/02: exact env-scoped subject via StringEquals (no
|
||||
// StringLike, no `*`). prod = environment:prod (manual-approval gate),
|
||||
// dev = the dev branch ref.
|
||||
const trust = new iam.WebIdentityPrincipal(provider.openIdConnectProviderArn, {
|
||||
StringEquals: {
|
||||
"token.actions.githubusercontent.com:aud": "sts.amazonaws.com",
|
||||
"token.actions.githubusercontent.com:sub": oidcSubject(envName),
|
||||
},
|
||||
});
|
||||
|
||||
// ---- githubdeploy-open-swe-infra-<env> --------------------------------
|
||||
this.infraRole = new iam.Role(this, "InfraDeployRole", {
|
||||
roleName: `githubdeploy-open-swe-infra-${envName}`,
|
||||
assumedBy: trust,
|
||||
description: `GitHub OIDC role for CDK deploys of the open-swe-${envName} infra stack (assumes CDK bootstrap roles).`,
|
||||
});
|
||||
|
||||
// Org-standard CDK deploy pattern (mirrors githubdeploy-seahaven-account-
|
||||
// baseline / -forgejo / -apm-wo-analysis): the deploy role only needs to
|
||||
// assume the CDK bootstrap roles. The actual CloudFormation + IAM + resource
|
||||
// permissions are exercised by the bootstrap `cfn-exec-role`, whose scope is
|
||||
// owned by the CDKToolkit stack — NOT granted directly here.
|
||||
//
|
||||
// T4 BLOCK#1: GPT-4.1 flagged the `cdk-hnb659fds-*` wildcard and recommended
|
||||
// enumerating the four exact ARNs. ACCEPTED EXCEPTION (Adam, 2026-06-26): kept
|
||||
// as the verified org-wide convention (githubdeploy-seahaven-account-baseline
|
||||
// uses the identical wildcard). Only `cdk bootstrap` creates roles with this
|
||||
// prefix, so practical escalation risk is low.
|
||||
// T5 residual (OSWE-IAC-02): the single account-wide cfn-exec-role means the
|
||||
// dev infra role can technically deploy any stack; per-env trust gates WHO can
|
||||
// assume, and the prod role requires the environment:prod approval. Per-env
|
||||
// bootstrap qualifiers would close the residual fully (future hardening).
|
||||
this.infraRole.addToPolicy(
|
||||
new iam.PolicyStatement({
|
||||
sid: "AssumeCdkBootstrapRoles",
|
||||
actions: ["sts:AssumeRole"],
|
||||
resources: [`arn:aws:iam::${ACCOUNT}:role/cdk-hnb659fds-*`],
|
||||
}),
|
||||
);
|
||||
|
||||
// ---- githubdeploy-open-swe-app-<env> ----------------------------------
|
||||
this.appRole = new iam.Role(this, "AppDeployRole", {
|
||||
roleName: `githubdeploy-open-swe-app-${envName}`,
|
||||
assumedBy: trust,
|
||||
description: `GitHub OIDC role for open-swe-${envName} app deploys: env-tag-scoped ssm:SendCommand + read of the ${envName} S3 artifact bucket.`,
|
||||
});
|
||||
|
||||
// T5 OSWE-IAC-01 fix: SendCommand only to instances tagged project=open-swe
|
||||
// AND env=<this env> (a SINGLE value, not {dev,prod}). The dev app role can
|
||||
// never command the prod box and vice versa — env isolation in IAM.
|
||||
this.appRole.addToPolicy(
|
||||
new iam.PolicyStatement({
|
||||
sid: "SsmSendCommandTagScoped",
|
||||
actions: ["ssm:SendCommand"],
|
||||
resources: [`arn:aws:ec2:${REGION}:${ACCOUNT}:instance/*`],
|
||||
conditions: {
|
||||
StringEquals: {
|
||||
"ssm:resourceTag/project": "open-swe",
|
||||
"ssm:resourceTag/env": envName,
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
// SendCommand also has to reference the command document. Scope to this env's
|
||||
// open-swe deploy document.
|
||||
// T4 BLOCK#3: GPT-4.1 flagged AWS-RunShellScript as an arbitrary-shell escalation
|
||||
// path. TIMEBOXED (accepted until T19): the current SSM deploy runs deploy.sh via
|
||||
// AWS-RunShellScript; it is already tag-scoped to env=<env> (statement above).
|
||||
// TODO(T19): drop AWS-RunShellScript once `open-swe-<env>-deploy` is the only path.
|
||||
this.appRole.addToPolicy(
|
||||
new iam.PolicyStatement({
|
||||
sid: "SsmSendCommandDocuments",
|
||||
actions: ["ssm:SendCommand"],
|
||||
resources: [
|
||||
`arn:aws:ssm:${REGION}:${ACCOUNT}:document/open-swe-${envName}-deploy`,
|
||||
`arn:aws:ssm:${REGION}::document/AWS-RunShellScript`,
|
||||
],
|
||||
}),
|
||||
);
|
||||
|
||||
// Poll command results. These read actions do not support resource-level
|
||||
// scoping, so `*` is required by the API (T4 FIX: API limitation, documented).
|
||||
this.appRole.addToPolicy(
|
||||
new iam.PolicyStatement({
|
||||
sid: "SsmReadCommandStatus",
|
||||
actions: [
|
||||
"ssm:GetCommandInvocation",
|
||||
"ssm:ListCommands",
|
||||
"ssm:ListCommandInvocations",
|
||||
],
|
||||
resources: ["*"],
|
||||
}),
|
||||
);
|
||||
|
||||
// Read-only access to THIS env's artifact bucket only (CI uploads; the box
|
||||
// pulls via its instance role — the app deploy role only reads to verify).
|
||||
this.appRole.addToPolicy(
|
||||
new iam.PolicyStatement({
|
||||
sid: "ReadArtifactBucket",
|
||||
actions: ["s3:GetObject"],
|
||||
resources: [`arn:aws:s3:::open-swe-${envName}-assets/*`],
|
||||
}),
|
||||
);
|
||||
this.appRole.addToPolicy(
|
||||
new iam.PolicyStatement({
|
||||
sid: "ListArtifactBucket",
|
||||
actions: ["s3:ListBucket", "s3:GetBucketLocation"],
|
||||
resources: [`arn:aws:s3:::open-swe-${envName}-assets`],
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
93
infra/lib/constructs/instance-role.ts
Normal file
93
infra/lib/constructs/instance-role.ts
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
import * as iam from "aws-cdk-lib/aws-iam";
|
||||
import { Construct } from "constructs";
|
||||
import { ACCOUNT, EnvName, REGION, prefix } from "../config";
|
||||
|
||||
/**
|
||||
* Least-privilege EC2 instance role for the open-swe box (one per env).
|
||||
*
|
||||
* Grants exactly what the boot/runtime flow needs and NOTHING ELSE — no admin,
|
||||
* no `*` resources except where the AWS action genuinely has no resource-level
|
||||
* scoping. Per-env so the dev box can never read prod secrets/config and vice
|
||||
* versa. Reviewed at T4 (GPT-4.1 IAM cross-review) / T5 (/sh-security-review)
|
||||
* before it is ever deployed (T6).
|
||||
*/
|
||||
export class InstanceRole extends Construct {
|
||||
public readonly role: iam.Role;
|
||||
|
||||
constructor(scope: Construct, id: string, env: EnvName) {
|
||||
super(scope, id);
|
||||
const p = prefix(env);
|
||||
|
||||
this.role = new iam.Role(this, "Role", {
|
||||
roleName: `${p}-instance-role`,
|
||||
assumedBy: new iam.ServicePrincipal("ec2.amazonaws.com"),
|
||||
description: `EC2 instance role for the ${p} open-swe box (least-privilege).`,
|
||||
});
|
||||
|
||||
// AWS-managed: lets the SSM agent register the instance and RECEIVE the
|
||||
// app-deploy `ssm:SendCommand` from githubdeploy-open-swe-app. This is the
|
||||
// standard Session-Manager / RunCommand grant and is the only managed
|
||||
// policy on the role. DELIBERATE — flag for T4 confirmation.
|
||||
this.role.addManagedPolicy(
|
||||
iam.ManagedPolicy.fromAwsManagedPolicyName("AmazonSSMManagedInstanceCore"),
|
||||
);
|
||||
|
||||
// Read the build artifact from the env's S3 asset bucket (deploy = pull).
|
||||
this.role.addToPolicy(
|
||||
new iam.PolicyStatement({
|
||||
sid: "ReadArtifactObjects",
|
||||
actions: ["s3:GetObject"],
|
||||
resources: [`arn:aws:s3:::${p}-assets/*`],
|
||||
}),
|
||||
);
|
||||
this.role.addToPolicy(
|
||||
new iam.PolicyStatement({
|
||||
sid: "ListArtifactBucket",
|
||||
actions: ["s3:ListBucket", "s3:GetBucketLocation"],
|
||||
resources: [`arn:aws:s3:::${p}-assets`],
|
||||
}),
|
||||
);
|
||||
|
||||
// Read non-sensitive config from SSM Parameter Store under /open-swe-<env>/*.
|
||||
this.role.addToPolicy(
|
||||
new iam.PolicyStatement({
|
||||
sid: "ReadSsmConfig",
|
||||
actions: ["ssm:GetParameter", "ssm:GetParameters", "ssm:GetParametersByPath"],
|
||||
resources: [`arn:aws:ssm:${REGION}:${ACCOUNT}:parameter/${p}/*`],
|
||||
}),
|
||||
);
|
||||
|
||||
// Read secrets from Secrets Manager under open-swe-<env>/*. Secret ARNs carry
|
||||
// a random 6-char suffix, hence the trailing `*`.
|
||||
this.role.addToPolicy(
|
||||
new iam.PolicyStatement({
|
||||
sid: "ReadSecrets",
|
||||
actions: ["secretsmanager:GetSecretValue", "secretsmanager:DescribeSecret"],
|
||||
resources: [`arn:aws:secretsmanager:${REGION}:${ACCOUNT}:secret:${p}/*`],
|
||||
}),
|
||||
);
|
||||
|
||||
// NOTE (T11): SSM SecureString + Secrets Manager here are assumed to use the
|
||||
// AWS-managed keys (alias/aws/ssm, alias/aws/secretsmanager) for which the
|
||||
// service grants Decrypt implicitly — so NO kms:Decrypt is granted. If T11
|
||||
// moves these to a customer CMK, add a scoped `kms:Decrypt` on that key ARN
|
||||
// ONLY (not `*`).
|
||||
|
||||
// Ship application logs to CloudWatch Logs under /open-swe/<env>/*.
|
||||
this.role.addToPolicy(
|
||||
new iam.PolicyStatement({
|
||||
sid: "PutAppLogs",
|
||||
actions: [
|
||||
"logs:CreateLogGroup",
|
||||
"logs:CreateLogStream",
|
||||
"logs:PutLogEvents",
|
||||
"logs:DescribeLogStreams",
|
||||
],
|
||||
resources: [
|
||||
`arn:aws:logs:${REGION}:${ACCOUNT}:log-group:/open-swe/${env}/*`,
|
||||
`arn:aws:logs:${REGION}:${ACCOUNT}:log-group:/open-swe/${env}/*:*`,
|
||||
],
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
38
infra/lib/open-swe-iam-stack.ts
Normal file
38
infra/lib/open-swe-iam-stack.ts
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
import * as cdk from "aws-cdk-lib";
|
||||
import { Construct } from "constructs";
|
||||
import { GithubDeployRoles } from "./constructs/github-deploy-roles";
|
||||
|
||||
/**
|
||||
* Account-level IAM stack: the per-ENV GitHub OIDC deploy roles
|
||||
* (githubdeploy-open-swe-{infra,app}-{dev,prod} — four roles).
|
||||
*
|
||||
* T5 OSWE-IAC-01/02 fix: roles are split per env with env-scoped OIDC trust, so
|
||||
* a dev-branch token cannot reach prod (prod roles require the GitHub
|
||||
* `prod` Environment manual-approval gate). They live in this dedicated stack
|
||||
* rather than the env stacks because IAM roles are global and this stack ships
|
||||
* FIRST (TODO.md BLOCK#3): the infra OIDC roles + the repo deploy-role-ARN
|
||||
* secrets must exist before any infra/secrets CI step. Synth-only until the
|
||||
* Phase-1 security gate (T4 + T5) clears (T6).
|
||||
*/
|
||||
export class OpenSweIamStack extends cdk.Stack {
|
||||
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
|
||||
super(scope, id, props);
|
||||
|
||||
const dev = new GithubDeployRoles(this, "DeployRolesDev", "dev");
|
||||
const prod = new GithubDeployRoles(this, "DeployRolesProd", "prod");
|
||||
|
||||
cdk.Tags.of(this).add("project", "open-swe");
|
||||
cdk.Tags.of(this).add("ManagedBy", "cdk");
|
||||
|
||||
const out = (id: string, role: { roleName?: string }, env: string, kind: string) =>
|
||||
new cdk.CfnOutput(this, id, {
|
||||
value: `arn:aws:iam::${this.account}:role/${role.roleName}`,
|
||||
description: `OIDC role ARN for ${env} ${kind} deploys — set as the ${env} deploy-role secret.`,
|
||||
});
|
||||
|
||||
out("InfraDeployRoleDevArn", dev.infraRole, "dev", "infra (CDK)");
|
||||
out("AppDeployRoleDevArn", dev.appRole, "dev", "app (tag-scoped SSM + S3)");
|
||||
out("InfraDeployRoleProdArn", prod.infraRole, "prod", "infra (CDK)");
|
||||
out("AppDeployRoleProdArn", prod.appRole, "prod", "app (tag-scoped SSM + S3)");
|
||||
}
|
||||
}
|
||||
58
infra/lib/open-swe-stack.ts
Normal file
58
infra/lib/open-swe-stack.ts
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
import * as cdk from "aws-cdk-lib";
|
||||
import { Construct } from "constructs";
|
||||
import { EnvName, prefix } from "./config";
|
||||
import { InstanceRole } from "./constructs/instance-role";
|
||||
import {
|
||||
AL2023_ARM64_SSM_CONTEXT_KEY,
|
||||
cachedArm64AmazonLinux2023,
|
||||
} from "./constructs/ami-cache";
|
||||
|
||||
export interface OpenSweStackProps extends cdk.StackProps {
|
||||
/** open-swe environment — drives the `open-swe-<env>-*` resource naming. */
|
||||
readonly envName: EnvName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Per-env open-swe stack (`open-swe-dev` / `open-swe-prod`). Resource names are
|
||||
* prefixed `open-swe-<env>-*`.
|
||||
*
|
||||
* T3 scope: the per-env EC2 instance role + the wired-but-not-yet-instantiated
|
||||
* AMI cache helper. The EC2 instance, ALB target groups, listener rules, SG,
|
||||
* Route53 and NAT come at T12 — this stack is the synth-able shell they plug
|
||||
* into.
|
||||
*/
|
||||
export class OpenSweStack extends cdk.Stack {
|
||||
public readonly instanceRole: InstanceRole;
|
||||
|
||||
constructor(scope: Construct, id: string, props: OpenSweStackProps) {
|
||||
super(scope, id, props);
|
||||
|
||||
const envName = props.envName;
|
||||
const p = prefix(envName);
|
||||
|
||||
cdk.Tags.of(this).add("project", "open-swe");
|
||||
cdk.Tags.of(this).add("env", envName);
|
||||
cdk.Tags.of(this).add("ManagedBy", "cdk");
|
||||
|
||||
// Per-env least-privilege EC2 instance role (open-swe-<env>-instance-role).
|
||||
this.instanceRole = new InstanceRole(this, "Instance", envName);
|
||||
|
||||
// AMI cache discipline (see lib/constructs/ami-cache.ts). T3 is synth-only:
|
||||
// resolve + surface the pinned AMI id ONLY when it is already cached in
|
||||
// cdk.context.json, so synth never makes a live SSM call. T12 consumes
|
||||
// `cachedArm64AmazonLinux2023()` for the actual ec2.Instance.
|
||||
if (this.node.tryGetContext(AL2023_ARM64_SSM_CONTEXT_KEY) !== undefined) {
|
||||
const amiId = cachedArm64AmazonLinux2023().getImage(this).imageId;
|
||||
new cdk.CfnOutput(this, "PinnedAmiId", {
|
||||
value: amiId,
|
||||
description:
|
||||
"Cached AL2023 ARM64 AMI id (pinned in cdk.context.json; consumed by the T12 EC2 instance).",
|
||||
});
|
||||
}
|
||||
|
||||
new cdk.CfnOutput(this, "InstanceRoleArn", {
|
||||
value: this.instanceRole.role.roleArn,
|
||||
description: `${p} EC2 instance role ARN.`,
|
||||
});
|
||||
}
|
||||
}
|
||||
4487
infra/package-lock.json
generated
Normal file
4487
infra/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
31
infra/package.json
Normal file
31
infra/package.json
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
{
|
||||
"name": "open-swe-infra",
|
||||
"version": "1.0.0",
|
||||
"description": "Open SWE AWS infrastructure (CDK TypeScript) — open-swe-dev / open-swe-prod stacks + shared OIDC deploy roles.",
|
||||
"private": true,
|
||||
"bin": {
|
||||
"open-swe-infra": "bin/app.js"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"cdk": "cdk",
|
||||
"synth": "cdk synth",
|
||||
"diff": "cdk diff",
|
||||
"test": "jest"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jest": "^29.5.14",
|
||||
"@types/node": "^24.0.0",
|
||||
"@types/source-map-support": "^0.5.10",
|
||||
"aws-cdk": "^2.1029.0",
|
||||
"jest": "^29.7.0",
|
||||
"source-map-support": "^0.5.21",
|
||||
"ts-jest": "^29.2.5",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "~5.6.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"aws-cdk-lib": "2.260.0",
|
||||
"constructs": "^10.0.0"
|
||||
}
|
||||
}
|
||||
84
infra/test/kebab-naming-aspect.test.ts
Normal file
84
infra/test/kebab-naming-aspect.test.ts
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
import * as cdk from "aws-cdk-lib";
|
||||
import { Annotations, Match } from "aws-cdk-lib/assertions";
|
||||
import * as iam from "aws-cdk-lib/aws-iam";
|
||||
import { KebabNamingAspect, isKebabCase } from "../lib/aspects/kebab-naming-aspect";
|
||||
import { OpenSweIamStack } from "../lib/open-swe-iam-stack";
|
||||
import { OpenSweStack } from "../lib/open-swe-stack";
|
||||
|
||||
const ENV = { account: "328440206208", region: "us-east-1" };
|
||||
|
||||
describe("isKebabCase", () => {
|
||||
it.each([
|
||||
"open-swe-dev",
|
||||
"open-swe-prod-instance-role",
|
||||
"githubdeploy-open-swe-infra",
|
||||
"open-swe-dev/slack-signing", // Secrets Manager path
|
||||
"/open-swe-dev/feature-flag", // SSM param path
|
||||
"/open-swe/dev/agent", // log group path
|
||||
"abc123",
|
||||
])("accepts conforming name %s", (name) => {
|
||||
expect(isKebabCase(name)).toBe(true);
|
||||
});
|
||||
|
||||
it.each([
|
||||
"OpenSweDev",
|
||||
"open_swe_dev",
|
||||
"openSweDev",
|
||||
"Open-Swe-Dev",
|
||||
"open-swe-dev/SlackSigning",
|
||||
])("rejects non-conforming name %s", (name) => {
|
||||
expect(isKebabCase(name)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("KebabNamingAspect", () => {
|
||||
it("passes the real app stacks (no errors)", () => {
|
||||
const app = new cdk.App();
|
||||
cdk.Aspects.of(app).add(new KebabNamingAspect());
|
||||
|
||||
new OpenSweIamStack(app, "OpenSweIamStack", { stackName: "open-swe-iam", env: ENV });
|
||||
const dev = new OpenSweStack(app, "OpenSweDevStack", {
|
||||
stackName: "open-swe-dev",
|
||||
env: ENV,
|
||||
envName: "dev",
|
||||
});
|
||||
const prod = new OpenSweStack(app, "OpenSweProdStack", {
|
||||
stackName: "open-swe-prod",
|
||||
env: ENV,
|
||||
envName: "prod",
|
||||
});
|
||||
|
||||
for (const s of [dev, prod]) {
|
||||
Annotations.fromStack(s).hasNoError("*", Match.anyValue());
|
||||
}
|
||||
});
|
||||
|
||||
it("flags a deliberately non-kebab-case resource name", () => {
|
||||
const app = new cdk.App();
|
||||
const stack = new cdk.Stack(app, "ConformingStackId", { stackName: "open-swe-test", env: ENV });
|
||||
cdk.Aspects.of(stack).add(new KebabNamingAspect());
|
||||
|
||||
// Deliberately bad physical name — must be flagged.
|
||||
new iam.Role(stack, "BadlyNamedRole", {
|
||||
roleName: "OpenSweBadRole",
|
||||
assumedBy: new iam.ServicePrincipal("ec2.amazonaws.com"),
|
||||
});
|
||||
|
||||
Annotations.fromStack(stack).hasError(
|
||||
"*",
|
||||
Match.stringLikeRegexp("not kebab-case"),
|
||||
);
|
||||
});
|
||||
|
||||
it("flags a deliberately non-kebab-case stack name", () => {
|
||||
const app = new cdk.App();
|
||||
// PascalCase stackName — the convention CDK defaults to and that we forbid.
|
||||
const stack = new cdk.Stack(app, "BadStack", { stackName: "OpenSweBadStack", env: ENV });
|
||||
cdk.Aspects.of(stack).add(new KebabNamingAspect());
|
||||
|
||||
Annotations.fromStack(stack).hasError(
|
||||
"*",
|
||||
Match.stringLikeRegexp("Stack name .* is not kebab-case"),
|
||||
);
|
||||
});
|
||||
});
|
||||
24
infra/tsconfig.json
Normal file
24
infra/tsconfig.json
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "commonjs",
|
||||
"lib": ["ES2022"],
|
||||
"types": ["node", "jest"],
|
||||
"declaration": true,
|
||||
"strict": true,
|
||||
"noImplicitAny": true,
|
||||
"strictNullChecks": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"inlineSourceMap": true,
|
||||
"inlineSources": true,
|
||||
"strictPropertyInitialization": false,
|
||||
"outDir": "./cdk.out",
|
||||
"rootDir": ".",
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"esModuleInterop": true
|
||||
},
|
||||
"exclude": ["node_modules", "cdk.out"]
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue