Skip to content

Technical design — MOD-104 AWS shared infrastructure bootstrap

Module: MOD-104 — AWS shared infrastructure bootstrap System: SD07 — Data Platform & Governance Infrastructure Repo: bank-platform FR scope: FR-417, FR-418, FR-419, FR-420 Policies satisfied: GOV-005 (GATE), GOV-006 (LOG), DT-002 (GATE) Author: AI agent (Claude Opus 4.7) Date: 2026-04-21 Stage covered: dev (account 647751526084, region ap-southeast-2, org o-1gqwjyxxhw) SST permalink: https://sst.dev/u/73920289


Objective

MOD-104 provisions the foundational AWS infrastructure every other module across the 8 system domains depends on. It is the bottom of the dependency tree. The module is deployed once per environment via sst deploy and must be fully deployed before any other module.

This document describes the as-deployed state in dev. The implementation conforms to the wiki spec and all 98 integration tests pass. build_status: Built in the wiki.


Execution model

Aspect Decision
IaC tool SST v3 Ion + raw @pulumi/aws resources (ADR-025)
State management SST Ion home: "aws" — auto-bootstrap via SSM /sst/bootstrap
Environments local | dev | uat | prod (environments-and-deployment)
Tenant model Single-tenant. Each customer gets a complete new stack. tenant_id=totara-bank for this deployment.
Region Primary ap-southeast-2, secondary ap-southeast-4 (ADR-023)
Tagging Provider defaultTags applies tenant_id, module_id, environment, plus system_id, cost_center, managed_by
Deployment identity GitHub OIDC CI/CD role; dev work via AWS_PROFILE=bank-dev

Stack layout

MOD-104-bootstrap/
├── sst.config.ts                 — app() config + async run() orchestration
├── src/
│   ├── config/
│   │   ├── stages.ts             — Environment = local | dev | uat | prod
│   │   ├── tags.ts               — baseTags(moduleId, env)
│   │   ├── regions.ts            — primary/secondary + AZs
│   │   └── naming.ts             — SystemDomain, DataClassification, name helpers
│   ├── stacks/
│   │   ├── networking.ts         — VPC, 6 subnets, IGW, RTs, S3/DDB gateway endpoints
│   │   ├── kms.ts                — 3 classification CMKs + aliases + policies
│   │   ├── storage.ts            — 6 S3 buckets with SSE-KMS, SSL-only, lifecycle
│   │   ├── eventbridge.ts        — 8 domain buses + cross-acct policy + archive + DLQ
│   │   ├── cognito.ts            — 2 user pools + 4 app clients + custom attrs
│   │   ├── firehose.ts           — 2 delivery streams (CDC + usage) to S3 iceberg
│   │   ├── cloudtrail.ts         — trail + log-bucket immutability + CW Logs
│   │   ├── lambda-roles.ts       — 8 system-domain Lambda execution roles
│   │   ├── iam.ts                — CI/CD role (OIDC), break-glass role (MFA)
│   │   ├── secrets.ts            — bank-secrets-read attachable policy
│   │   ├── observability.ts      — shared log group, alerts SNS, X-Ray sampling
│   │   └── tag-enforcement.ts    — SCP via assume-role to org management account
│   └── outputs.ts                — ~45 SSM parameters under /bank/{env}/...
└── __tests__/integration/        — 7 FR+policy test files, Jest + AWS SDK v3

AWS resources provisioned (dev stage)

Networking — VPC and endpoints

Resource ID / ARN
VPC (10.40.0.0/16) vpc-090237e1cdec5e3bc
Internet Gateway igw-068ad8d85d8e23fdb
Public subnets ×3 subnet-08907ebcf008a1702, -08523bc96f49b7ac7, -090d7605be0708298
Private subnets ×3 subnet-04eeb916c183c86c0, -0e4afe5a479b12005, -08e0e30275673f360
S3 gateway endpoint vpce-01031da9d5ae9f32b
DynamoDB gateway endpoint vpce-01d1491aae464eb7a

No NAT gateway. Private subnets reach AWS-native storage via gateway endpoints (free); when a Lambda needs other AWS APIs from a VPC attachment, that module adds interface endpoints.

KMS — three classification-aligned CMKs (FR-419)

Classification Alias Rotation Key policy
PII alias/bank/pii annual root account
Financial alias/bank/financial annual root account
Operational alias/bank/operational annual root account + CloudTrail + CloudWatch Logs + SNS service principals

ARNs are published to SSM at /bank/{env}/kms/{classification}/arn.

S3 — six buckets (FR-418)

Bucket Purpose CMK Lifecycle
bank-iceberg-dev Iceberg data lake / CDC output financial Glacier @ 90d · expire @ 7y
bank-firehose-landing-dev Firehose landing zone operational expire @ 1d (ephemeral)
bank-documents-dev Customer documents (KYC scans, statements) pii Glacier @ 1y · expire @ 7y
bank-artefacts-dev Build/deploy artefacts operational expire @ 90d
bank-reports-dev Regulatory report output financial Glacier @ 1y · expire @ 7y
bank-cloudtrail-dev CloudTrail logs operational Glacier @ 90d · expire @ 7y · Object Lock (GOVERNANCE, 7y)
bank-pulumi-state-dev Pulumi state backend (non-SST modules — bank-risk-platform's bare-Pulumi runner per ADR-025) operational Versioning ON · no expiration (state must persist for stack lifetime)

All buckets: SSE-KMS, versioning, public-access-block (all 4 flags), SSL-only Deny policy, DenyUnencryptedPut policy, DenyIncorrectEncryption policy.

EventBridge — eight system-domain buses (FR-417)

Bus names (no env suffix, per spec): bank-core, bank-kyc, bank-aml, bank-payments, bank-credit, bank-risk-platform, bank-platform, bank-app. Each bus has: - Cross-account resource policy (root account + org aws:PrincipalOrgID=o-1gqwjyxxhw) - 30-day event archive (EventArchive) - Dead-letter SQS queue ({domain}-events-dlq-{env}, KMS-encrypted, 14-day retention)

Cognito — two user pools (FR-420)

Pool MFA Custom attrs App clients
bank-customers-dev TOTP required (biometric via app layer) custom:user_id, custom:party_id, custom:jurisdiction mobile (PKCE, public), web (PKCE, public)
bank-staff-dev TOTP required custom:staff_id back-office (confidential), api-authoriser (confidential)

Password policy: min length 12 (customers) / 14 (staff), all character classes required. Deletion protection: INACTIVE in dev, ACTIVE in prod.

Kinesis Firehose — two delivery streams (FR-420)

Stream Destination Partitioning Buffer
bank-cdc-stream bank-iceberg-dev under cdc/source=neon/year=.../month=.../day=.../ time-based 128 MB / 300 s
bank-usage-events bank-iceberg-dev under usage/year=.../month=.../day=.../ time-based 128 MB / 300 s

Both: SSE with alias/bank/financial, GZIP compression, CloudWatch error logging, Firehose DLQ.

Lambda execution IAM roles — eight system-domain roles (FR-420)

Roles BankCoreRole, BankKycRole, BankAmlRole, BankPaymentsRole, BankCreditRole, BankRiskRole, BankPlatformRole, BankAppRole — each with Lambda trust (scoped to this account), AWSLambdaBasicExecutionRole + AWSLambdaVPCAccessExecutionRole managed-policy attachments, and a domain-permissions inline policy covering: - CloudWatch Logs on /aws/lambda/{domain}-* - Secrets Manager read on {domain}/* (and bank-neon/*/{domain_db}/* where neonDbName is set) - SSM read (GetParameter/GetParameters/GetParametersByPath) on /bank/* — every domain Lambda resolves shared infra discovery (Neon pooler-host, S3 artefacts bucket, EventBridge bus ARNs, Snowflake DB names) at cold start. Added 2026-05-04 after bank-aml's cold-start crash on /bank/dev/neon/pooler-host. Asserted by fr-420-… test. - EventBridge PutEvents on the domain's bus - SNS Publish on bank-* topics — universal alerting pattern (ADR-031). MOD-076 publishes the alarm-intake topic; per-domain modules also stand up service-level alert topics. Added 2026-05-04 after the broader IAM audit found BankKycRole (5 modules: MOD-010/011/013/014/015) and BankRiskRole (MOD-085) missing this grant. - KMS Decrypt on the classification CMK(s) appropriate to the domain (e.g. BankKycRole → pii+financial) - X-Ray PutTraceSegments

BankAmlRole has additional appconfigdata:GetLatestConfiguration + appconfig:StartConfigurationSession for ADR-033 live-rule-parameter loading (MOD-016 typology engine, MOD-018 SAR threshold loader). Its cross-bus EventBridge grant (events:PutRule/PutTargets/DescribeRule/ListTargetsByRule on bank-core's bus, scoped to bank-aml-mod-* rule names) is intentionally narrowed — DeleteRule / RemoveTargets were removed 2026-05-04 because handler code only provisions rules, never tears them down at runtime; teardown happens via SST/Pulumi at deploy time using the CICD role.

BankPaymentsRole grants lambda:InvokeFunction on arn:aws:lambda:*:{accountId}:function:bank-core-mod-004-*FxConversionHandler* (FR-142 — MOD-025 ConfirmConversionHandler synchronously invokes MOD-004 to confirm an FX rate before persisting the payment leg). The wildcards on either side of FxConversionHandler accommodate SST's CDK-style suffixed function names.

Trust has aws:SourceAccount condition and aws:RequestTag/module_id condition (modules attaching Lambdas to these roles must tag with module_id).

CloudTrail — multi-region trail (FR-420, GOV-006)

Property Value
Trail name bank-platform
Log bucket bank-cloudtrail-dev (Object Lock GOVERNANCE, 7y default retention)
Multi-region true
Include global service events true
Log-file validation enabled
KMS alias/bank/operational
CloudWatch Logs integration /aws/cloudtrail/bank-platform-dev
Management events read + write
Data events all S3 objects

Log bucket policy includes explicit Deny on DeleteObject/DeleteObjectVersion (belt + braces alongside Object Lock GOVERNANCE).

Observability

Resource Value
CloudWatch log group /aws/bank-platform/dev
Alerts SNS topic bank-platform-alerts-dev
X-Ray sampling rule bank-platform-default-dev (5% fixed, reservoir 1)

Platform IAM (CI/CD + break-glass)

Role Trust Permissions
bank-platform-cicd GitHub OIDC (repo:totara-bank/bank-platform:*) AdministratorAccess
bank-platform-break-glass root account + MFA AdministratorAccess, 1h max session

Secrets — attachable namespace read policy

Policy ARN pattern Grants
bank-secrets-read arn:aws:iam::647751526084:policy/bank-secrets-read GetSecretValue + DescribeSecret on bank-*/* secrets; Decrypt on all three classification CMKs

Tag enforcement — SCP (GOV-005 GATE)

Provisioned via a second Pulumi provider configured with the bank-org management-account profile (via BANK_ORG_PROFILE=bank-org env var). SCP bank-platform-required-tags (policy id p-pyh0z523) attached to the organization root r-2qho. Policy denies Create* on 7 taggable services (S3/EC2/KMS/Events/IAM/Lambda/CloudTrail) unless tenant_id, module_id, and environment request tags are present.

Deploy command: AWS_PROFILE=bank-dev BANK_ORG_PROFILE=bank-org pnpm sst deploy --stage <env> One-time org precondition: aws organizations enable-policy-type --root-id r-2qho --policy-type SERVICE_CONTROL_POLICY (idempotent after first run).

The GOV-005 negative test now passes — a direct-SDK CreateBucket without the required tags returns AccessDenied before the request reaches S3.


SSM outputs table (consumer contract)

All under arn:aws:ssm:ap-southeast-2:647751526084:parameter. Path convention per spec: /bank/{env}/{service}/{parameter}. Downstream modules resolve these at deploy time.

SSM path Value Consumed by
/bank/{env}/network/vpc-id VPC ID MOD-103 (Neon), MOD-075 (API gateway), any VPC-attached Lambda
/bank/{env}/network/private-subnet-ids CSV of 3 subnet IDs MOD-103, MOD-075, MOD-076, MOD-097
/bank/{env}/network/public-subnet-ids CSV of 3 subnet IDs MOD-075 (ALB), MOD-100 (external connector)
/bank/{env}/kms/pii/arn KMS ARN bank-kyc (MOD-009), bank-app (MOD-044 JWT), MOD-103 PII secrets
/bank/{env}/kms/financial/arn KMS ARN bank-core (MOD-001 ledger), MOD-042 CDC pipeline, Snowflake ingest
/bank/{env}/kms/operational/arn KMS ARN CloudTrail, CloudWatch Logs, Firehose landing, misc ops
/bank/{env}/eventbridge/bank-core/arn Bus ARN bank-core producers/consumers
/bank/{env}/eventbridge/bank-kyc/arn Bus ARN bank-kyc producers/consumers
/bank/{env}/eventbridge/bank-aml/arn Bus ARN bank-aml producers/consumers
/bank/{env}/eventbridge/bank-payments/arn Bus ARN bank-payments producers/consumers
/bank/{env}/eventbridge/bank-credit/arn Bus ARN bank-credit producers/consumers
/bank/{env}/eventbridge/bank-risk-platform/arn Bus ARN bank-risk-platform producers/consumers
/bank/{env}/eventbridge/bank-platform/arn Bus ARN CDC (MOD-042), usage (MOD-097), notifications (MOD-063)
/bank/{env}/eventbridge/bank-app/arn Bus ARN bank-app producers/consumers
/bank/{env}/eventbridge/{domain}/dlq-arn SQS ARN Rule DeadLetterConfig target for each domain
/bank/{env}/eventbridge/{domain}/archive-arn Archive ARN Replay operations for each domain
/bank/{env}/s3/iceberg/name Bucket name MOD-042 (CDC output), MOD-102 (Snowflake ingest), Firehose
/bank/{env}/s3/iceberg/arn Bucket ARN Bucket-ARN-scoped IAM policies in downstream modules
/bank/{env}/s3/firehose-landing/name Bucket name Firehose error output
/bank/{env}/s3/documents/name Bucket name bank-kyc (MOD-009 KYC scans), bank-app (statements)
/bank/{env}/s3/artefacts/name Bucket name All 8 repos' CI/CD deploy intermediates
/bank/{env}/s3/reports/name Bucket name MOD-099 usage/billing reports, regulatory reporting
/bank/{env}/s3/cloudtrail/name Bucket name Ops read (immutable logs)
/bank/{env}/s3/pulumi-state/name Bucket name bank-risk-platform pulumi login s3://...; any non-SST Pulumi state backend
/bank/{env}/s3/pulumi-state/arn Bucket ARN IAM policies scoping deploy roles to the state bucket
/bank/{env}/iam/lambda/{domain}/arn Role ARN (×8) Each domain's Lambda handlers attach to their domain role
/bank/{env}/iam/cicd/arn Role ARN All 8 repos' GitHub Actions
/bank/{env}/iam/break-glass/arn Role ARN Ops incident response only
/bank/{env}/iam/secrets-read/arn Policy ARN Attach to any service role needing bank-*/* secrets
/bank/{env}/cognito/customers/pool-id Pool ID bank-app (MOD-044 JWT issuer), MOD-022 onboarding
/bank/{env}/cognito/customers/arn Pool ARN bank-app IAM policies
/bank/{env}/cognito/customers/client/{client-name} Client ID Mobile/web frontends (client-name ∈ {mobile, web})
/bank/{env}/cognito/staff/pool-id Pool ID Back-office auth
/bank/{env}/cognito/staff/arn Pool ARN Back-office IAM policies
/bank/{env}/cognito/staff/client/{client-name} Client ID Back-office web, API Gateway authoriser (client-name ∈ {back-office, api-authoriser})
/bank/{env}/firehose/cdc/arn Stream ARN MOD-042 CDC producer
/bank/{env}/firehose/usage/arn Stream ARN MOD-097 usage producer
/bank/{env}/cloudtrail/arn Trail ARN Ops monitoring
/bank/{env}/cloudtrail/log-group-arn Log group ARN CloudWatch Logs integration
/bank/{env}/logs/bank-platform/arn Log group ARN Cross-module infra log events
/bank/{env}/sns/alerts/arn Topic ARN MOD-076 alarm targets
/bank/{env}/xray/sampling/arn Sampling rule ARN All bank-platform Lambdas default sampling

Resolution example

// In downstream module (Pulumi)
const piiCmkArn = aws.ssm.getParameterOutput({
  name: `/bank/${stage}/kms/pii/arn`,
}).value;

// In Lambda runtime (AWS SDK v3)
const ssm = new SSMClient({ region });
const { Parameter } = await ssm.send(
  new GetParameterCommand({ Name: `/bank/${stage}/s3/documents/name` }),
);
const bucketName = Parameter?.Value;

Acceptance criteria status (dev stage, 2026-04-21)

Run: AWS_PROFILE=bank-dev STAGE=dev pnpm test

FR / Policy Mode Tests Pass Fail Status
FR-417 — 8 EventBridge buses + cross-acct policy + 30d archive + DLQ 32 32 0 PASS
FR-418 — 5 named S3 buckets + KMS by classification + SSL-only + lifecycle 36 36 0 PASS
FR-419 — 3 classification CMKs + rotation + SSM export 9 9 0 PASS
FR-420 — Cognito + Firehose + 8 IAM roles + CloudTrail 14 14 0 PASS
GOV-005 — untagged resources blocked GATE 4 4 0 PASS
GOV-006 — CloudTrail enabled + logs immutable LOG 4 4 0 PASS
DT-002 — CMKs per classification + DenyUnencryptedPut GATE 4 4 0 PASS
Total 98 98 0 100%

All quality gates for IaC modules met: one infrastructure integration test per FR, one policy satisfaction test per row, GATE modes with negative tests, LOG mode with immutability test, SSM outputs table present and accurate.


Test approach

Jest + AWS SDK v3 integration tests in MOD-104-bootstrap/__tests__/integration/. No mocks; every assertion queries live AWS in the dev stage.

File Coverage
fr-417-eventbridge-buses.test.ts Bus + cross-account policy + archive + DLQ per domain
fr-418-s3-buckets.test.ts Existence, KMS-SSE, versioning, PAB, SSL-only, lifecycle per bucket
fr-419-kms-classifications.test.ts Alias presence, rotation, SSM export per classification
fr-420-cognito-firehose-iam-cloudtrail.test.ts Cognito pools + attrs, Firehose streams, Lambda roles, CloudTrail
gov-005-tag-enforcement.test.ts Positive: spec tag keys present. Negative: untagged CreateBucket rejected
gov-006-cloudtrail-immutability.test.ts Trail + logging + log-bucket immutability + DeleteObject probe
dt-002-encryption-at-rest.test.ts Classification CMKs exist + unencrypted PUT rejected

Run: AWS_PROFILE=bank-dev STAGE=dev pnpm test from MOD-104-bootstrap/.


Operational notes

  • Deploy (full, including SCP): AWS_PROFILE=bank-dev BANK_ORG_PROFILE=bank-org pnpm sst deploy --stage <env>
  • Deploy (skip SCP, e.g. from CI without mgmt-account profile): AWS_PROFILE=bank-dev pnpm sst deploy --stage <env> — all member-account resources deploy; SCP stack is skipped
  • Remove: AWS_PROFILE=bank-dev pnpm sst remove --stage <env> — KMS keys enter 30-day pending deletion, S3 buckets with versioning need manual version purge, CloudTrail log bucket has Object Lock (use --bypass-governance-retention to purge)
  • One-time org precondition (idempotent): AWS_PROFILE=bank-org aws organizations enable-policy-type --root-id r-2qho --policy-type SERVICE_CONTROL_POLICY
  • SST permalink (latest dev deploy): https://sst.dev/u/73920289

  • Wiki spec: bank-wiki/source/entities/modules/MOD-104.{yaml,md}
  • Wiki outputs contract: bank-wiki/source/entities/modules/MOD-104.yamloutputs: section
  • Handoff: docs/handoffs/MOD-104-complete.handoff.md
  • Bootstrap README: MOD-104-bootstrap/README.md
  • Methodology: https://bank-wiki.pages.dev/delivery/methodology/
  • Environments & deployment: https://bank-wiki.pages.dev/architecture/environments-and-deployment/
  • ADRs in effect: ADR-023, ADR-025, ADR-026, ADR-027, ADR-029, ADR-030, ADR-031, ADR-035, ADR-040, ADR-042