Design: AWS Infrastructure for Item Image Upload
Design document for the AWS Infrastructure project (Phase 1 of Item Image Upload). This document covers three concerns: the runtime AWS resources (what exists in the AWS account), the deployment machinery (CDK constructs, stacks, and scripts that create those resources), and the implementation patterns to follow.
For diagram conventions used in this document, see aws-resources-design.md.
1. AWS Resources
Section titled “1. AWS Resources”1.1 Current Infrastructure Context
Section titled “1.1 Current Infrastructure Context”The Arda platform is deployed across multiple AWS accounts, each hosting
one Infrastructure (shared networking, compute, and DNS) and one or
more Partitions (isolated application environments sharing the
infrastructure). The deployment is orchestrated by amm.sh which invokes
CDK and CloudFormation to provision all resources.
Source: infrastructure/src/main/cdk/platforms.ts,
infrastructure/src/main/cdk/apps/Al1x/infra.ts,
infrastructure/src/main/cdk/apps/Al1x/partition.ts.
Active Environments
Section titled “Active Environments”| Infrastructure | Account | Region | Partitions |
|---|---|---|---|
| Alpha001 | 009765408297 | us-east-1 | demo, prod |
| Alpha002 | 139852620346 | us-east-1 | dev, stage |
| SandboxKyle002 | 575126609604 | us-east-1 | kyle |
Source: infrastructure/src/main/cdk/platforms.ts,
infrastructure/src/main/cdk/platform/aws-configuration.ts.
Root Account — Global DNS
Section titled “Root Account — Global DNS”The Arda platform uses a dedicated Root Account (841876193886) to
host the parent Route53 hosted zones for the arda.cards domain. These
zones are shared across all infrastructure accounts. Each infrastructure
account creates subdomain zones and writes NS delegation records back to
the root zones via a cross-account IAM role.
Root Account resources (deployed once, globally):
| Resource | Stack | Account | Purpose |
|---|---|---|---|
Route53 io.arda.cards zone | RootConfigurationStack | Root (841876193886) | Parent zone for API/IO subdomains. All infrastructure accounts delegate from here. |
Route53 app.arda.cards zone | RootConfigurationStack | Root (841876193886) | Parent zone for application subdomains. |
Route53 auth.arda.cards zone | RootConfigurationStack | Root (841876193886) | Parent zone for authentication subdomains. |
Route53 assets.arda.cards zone (NEW) | RootConfigurationStack | Root (841876193886) | Parent zone for static asset CDN subdomains (PD-02). |
IAM AllowCreatingNSRecords role | RootConfigurationStack | Root (841876193886) | Cross-account role assumed by infrastructure accounts to write NS delegation records into root zones. Trust policy scoped to the AWS Organization (o-zcyoxikfq5). |
Source: infrastructure/src/main/cdk/stacks/root/root-configuration-stack.ts,
infrastructure/src/main/cdk/apps/rootConfiguration/r53-zones.ts,
infrastructure/src/main/cdk/constructs/oam/allow-creating-ns-records-role.ts.
Infrastructure-Level Resources
Section titled “Infrastructure-Level Resources”These resources are shared across all partitions within an AWS account.
They are created by the buildInfra() function
(apps/Al1x/infra.ts) which instantiates three stacks:
NetworkingInfrastructureStack, InfrastructureEksStack, and
InfrastructureIngress.
Resources created (per infrastructure account):
| Resource | Stack | Purpose |
|---|---|---|
VPC (10.X.0.0/16) | NetworkingInfrastructureStack | Network isolation. Public + private subnets per AZ. NAT gateways for egress. |
| S3 VPC Endpoint (Gateway) | NetworkingInfrastructureStack | Private S3 access from within the VPC without traversing the internet. |
| Secrets Manager VPC Endpoint (Interface) | NetworkingInfrastructureStack | Private Secrets Manager access with private DNS. |
| EKS Cluster | InfrastructureEksStack | Container orchestration. Fargate profiles, OIDC provider, GitHub access entries. |
| Route53 Subdomain Zones (io, app, auth) | InfrastructureIngress | Subdomain zones for this infrastructure. NS records delegated to root account zones via cross-account role. |
| Route53 Subdomain Zone (assets) (NEW) | InfrastructureIngress | alpha001.assets.arda.cards — subdomain zone for static asset CDN (PD-02). NS delegation to root assets.arda.cards zone. |
| ACM Wildcard Certificates (io, app, auth) | InfrastructureIngress | TLS for *.alpha001.io.arda.cards, *.alpha001.app.arda.cards, *.alpha001.auth.arda.cards. DNS-validated against the subdomain zones. |
| ACM Wildcard Certificate (assets) (NEW) | InfrastructureIngress | TLS for *.alpha001.assets.arda.cards. Used by the image CDN CloudFront distribution. |
Source: infrastructure/src/main/cdk/stacks/infrastructure/networking-stack.ts,
infrastructure/src/main/cdk/stacks/infrastructure/eks-stack.ts,
infrastructure/src/main/cdk/stacks/infrastructure/ingress-stack.ts,
infrastructure/src/main/cdk/constructs/xgress/write-ns-records-to-upstream-dns.ts.
Partition-Level Resources
Section titled “Partition-Level Resources”Each partition is an isolated application environment. Partitions share
the infrastructure VPC, EKS cluster, and DNS zones but have their own
storage, database, authentication, API gateway, and compute resources.
Created by buildPartition() in apps/Al1x/partition.ts.
Resources created (per partition):
| Resource | Stack | Purpose |
|---|---|---|
| S3 Upload Bucket (1-day TTL) | BulkStoresStack | Ephemeral storage for CSV uploads. Presigned PUT. CORS for browser uploads. |
| S3 Logging Bucket (90-day) | BulkStoresStack | Server access logs for the upload bucket. |
| Upload Presigning Role | BulkStoresStack | IAM role assumed by EKS pod to generate presigned URLs. s3:PutObject, s3:GetObject with HTTPS + SSE-S3 conditions. |
| Aurora PostgreSQL 16.6 | PurposeAuroraClusterStack | Bitemporal application database. Multi-AZ, encrypted, auto-backup. |
| Cognito User Pool | PartitionAuthn | User authentication. OAuth2 clients (M2M, web), resource servers, custom scopes. |
| NLB (Network Load Balancer) | PurposeIngress | Internal load balancer routing traffic to EKS pods. Ports 80, 443. |
| API Gateway V2 (HTTP API) | PurposeIngress | REST API entry point. VPC Link to NLB. JWT authorizer (Cognito). Custom domain. |
| CloudFront (API Distribution) | PurposeIngress | Edge distribution for the API Gateway. HTTPS redirect, no caching. |
| Route53 A Records | PurposeDnsStack | DNS records pointing to the API Gateway custom domain. |
| EKS Namespace + Pod | PurposeComputeStack | Application workload (operations service). |
| Amplify App | CloudFormation (amplify.cfn.yaml) | Next.js SSR frontend. Only deployed for selected partitions. |
| Partition Secrets | CloudFormation (partitionSecrets.cfn.yaml) | API keys, HubSpot credentials, Pylon widget keys in Secrets Manager. |
Source:
infrastructure/src/main/cdk/stacks/purpose/partition-bulk-stores.ts,infrastructure/src/main/cdk/stacks/purpose/purpose-storage.ts,infrastructure/src/main/cdk/stacks/purpose/partition-authn.ts,infrastructure/src/main/cdk/stacks/purpose/purpose-ingress.ts,infrastructure/src/main/cdk/stacks/purpose/purpose-dns.ts,infrastructure/src/main/cdk/stacks/purpose/purpose-compute.ts,infrastructure/src/main/cfn/amplify.cfn.yaml,infrastructure/src/main/cfn/partitionSecrets.cfn.yaml.
1.2 Resource Updates
Section titled “1.2 Resource Updates”This project adds four new resource groups to each partition: a persistent S3 bucket for image assets, a CloudFront CDN distribution for image delivery, an IAM presigning role for upload credential generation, and a CloudFront signing key group for tenant-scoped access control.
New Partition Resources
Section titled “New Partition Resources”Resource Details
Section titled “Resource Details”| Resource | Type | Key Configuration | Source Requirement |
|---|---|---|---|
| Image Asset Bucket | S3 | Persistent (no TTL), versioning, SSE-S3, RETAIN removal policy, CORS for browser POST, abort incomplete multipart after 1 day | AWS-FR-001, AWS-FR-005, AWS-FR-006 |
| Image Upload Presigning Role | IAM Role | s3:PutObject + s3:GetObject with HTTPS + SSE-S3 conditions. Assumable by EKS pod role. | AWS-FR-003 |
| Image Asset CDN | CloudFront | OAC origin to S3 bucket, trusted key groups for signed cookies, custom domain <partition>.<infra>.assets.arda.cards, HTTPS-only, GET/HEAD only, CachingOptimized, PriceClass_100 | AWS-FR-004, AWS-NFR-001 |
| Signing Key Group | CloudFront + Secrets Manager | RSA key pair. Public key in CloudFront trusted key group. Private key in Secrets Manager for BFF consumption. Multiple active keys for rotation. | AWS-FR-004, AWS-NFR-004 |
| DNS Record | Route53 | CNAME or Alias record <partition>.<infra>.assets.arda.cards pointing to CloudFront distribution. | AWS-FR-004 |
Interaction with Existing Resources
Section titled “Interaction with Existing Resources”| Existing Resource | Interaction | Direction |
|---|---|---|
| EKS Pod Role | Assumes the new Image Upload Presigning Role via sts:AssumeRole | Pod → Presigning Role |
| S3 VPC Endpoint | Used for presigned POST form generation (backend-side, within VPC) | Internal |
| Secrets Manager VPC Endpoint | Used by BFF to retrieve the CloudFront signing private key | Internal |
| assets.arda.cards Hosted Zone (NEW — root) | Parent zone for <infra>.assets.arda.cards subdomain delegation | DNS |
<infra>.assets.arda.cards Zone (NEW — infra) | Subdomain zone hosting CDN A records per partition | DNS |
ACM *.<infra>.assets.arda.cards Cert (NEW — infra) | TLS certificate for CloudFront distributions | TLS |
Source: AWS Specification, CDN Access Control.
2. Deployment Design
Section titled “2. Deployment Design”2.1 Current Deployment System
Section titled “2.1 Current Deployment System”The deployment system is orchestrated by amm.sh, which calls CDK to
synthesize and deploy stacks, CloudFormation directly for resources CDK
does not support (log streams, Amplify, secrets), and Helm for Kubernetes
components.
Source: infrastructure/amm.sh, infrastructure/.github/workflows/amm.yml.
Deployment Flow
Section titled “Deployment Flow”amm.sh executes the following steps for each invocation:
- Setup — Parse arguments, derive infrastructure and partition list, load secrets from 1Password (local) or GitHub Secrets (CI).
- Infrastructure — Create CloudWatch log group (CloudFormation),
bootstrap CDK, deploy infrastructure CDK app (
infra.ts), configure EKS (kubeconfig, FluentBit, Load Balancer Controller, External Secrets). - Per Partition — Deploy partition CDK app (
<partition>.ts), install NGINX Ingress Controller (Helm), register NLB target groups, deploy partition secrets (CloudFormation), optionally deploy Amplify (CloudFormation).
CDK Application Architecture
Section titled “CDK Application Architecture”CloudFormation Templates (Non-CDK)
Section titled “CloudFormation Templates (Non-CDK)”amm.sh deploys these templates directly via aws cloudformation deploy:
| Template | Stack Name Pattern | Purpose |
|---|---|---|
cloudWatch.cfn.yaml | {infra}-CloudWatchLog | Deployment log group and stream |
partitionSecrets.cfn.yaml | {infra}-{partition}-Secrets | API keys, HubSpot, Pylon secrets |
amplify.cfn.yaml | {infra}-{partition}-Amplify | Amplify app, compute role, service role |
amplifyBranch.cfn.yaml | {infra}-{partition}-AmplifyBranch | Branch resource, domain binding, PR preview |
Cross-Stack Export Pattern
Section titled “Cross-Stack Export Pattern”All stacks use the publish() / readImports() pattern from
stacks/types.ts:
- Each stack defines
ExportKeys(union of string literal types) andExportDefinition(maps keys to{exportName, description}). publish()createsCfnOutputresources. Keys containing-API-are exported publicly; keys containing-I-are exported as protected/internal.ImportingStack(inapps/Al1x/util.ts) reconstructs infrastructure resources from these exports usingFn.importValue/Fn.importListValue.
Source: infrastructure/src/main/cdk/stacks/types.ts,
infrastructure/src/main/cdk/apps/Al1x/util.ts.
2.2 Deployment Updates
Section titled “2.2 Deployment Updates”This project adds four new CDK constructs in a new ImageStorageStack
and wires it into the partition deployment. BulkStoresStack is
unchanged (PD-04). No changes are needed to
amm.sh itself — the new resources deploy automatically when the
partition CDK app runs.
New Constructs
Section titled “New Constructs”Modified Stacks
Section titled “Modified Stacks”New Cross-Stack Exports
Section titled “New Cross-Stack Exports”| Export Key | Value | Consumer |
|---|---|---|
${fqn}-API-ImageAssetBucketArn | Bucket ARN | Operations CloudFormation |
${fqn}-API-ImageAssetBucketName | Bucket name | Operations CloudFormation |
${fqn}-API-ImagePresignRoleArn | Presigning role ARN | Operations CloudFormation |
${fqn}-API-ImageCdnDomain | CloudFront domain name | Operations (CDN URL construction) |
${fqn}-API-ImageCdnSigningKeyId | CloudFront key pair ID | BFF (cookie signing) |
${fqn}-API-ImageCdnSigningKeySecretArn | Secrets Manager ARN | BFF (cookie signing) |
Verification Script
Section titled “Verification Script”A new infrastructure/tools/verify-image-cdn.ts script validates the
deployed infrastructure:
- Assumes the presigning role via STS.
- Uploads a test JPEG to S3 via presigned POST.
- Verifies CloudFront returns 403 without signed cookies.
- Retrieves the signing private key from Secrets Manager.
- Generates tenant-scoped signed cookies (RSA-SHA1 custom policy).
- Verifies CloudFront returns 200 with valid cookies.
- Verifies tenant isolation (wrong-tenant cookies get 403).
- Cleans up the test object.
Source: Phasing — Phase 1 Verification.
3. Design and Implementation Patterns
Section titled “3. Design and Implementation Patterns”Patterns extracted from the infrastructure repository that must be
followed when implementing the new constructs and stack modifications.
3.1 Construct Pattern
Section titled “3.1 Construct Pattern”Every CDK construct follows the Configuration → Props → Built pattern:
// 1. Configuration: what the consumer decides at design timeexport interface Configuration { readonly locator: purpose.Locator; readonly name: string; // ... design-time parameters}
// 2. Props: Configuration + runtime dependencies injected by the stackexport interface Props extends Configuration { readonly bucketClientRoleArn: string; readonly loggingBucket: s3.Bucket; // ... dependencies from other constructs/stacks}
// 3. Built: what the construct exposes after constructionexport interface Built { readonly bucket: s3.Bucket; readonly preSigningRole: iam.Role; // ... created resources}The construct class extends Construct (or ArdaConstruct<P> for
automatic validation and tagging) and populates this.built in the
constructor.
Source: infrastructure/src/main/cdk/constructs/storage/public-upload-bucket.ts,
infrastructure/src/main/cdk/utils/arda-construct.ts.
3.2 Stack Pattern
Section titled “3.2 Stack Pattern”Stacks follow a parallel pattern to constructs:
Configurationinterface — design-time parameters.Propsinterface extendingConfiguration— adds dependencies.Builtinterface — constructed sub-resources.ExportKeystype — string union of export key names.ExportDefinitionextendingExportDefinitions<ExportKeys>— maps keys to{exportName, description}.ExportValues— maps keys toStackIOValue(definition + value).validateProps()static method — throwsMultiErrorif invalid.publish()method — callsstackTypes.publish(this, this.outputs).
Key conventions:
- Export names with
-API-are public (readable by non-CDK consumers like CloudFormation templates). - Export names with
-I-are internal/protected. - The
publishingPrefixis alwayspurpose.fqn(locator)(kebab-case).
Source: infrastructure/src/main/cdk/stacks/purpose/partition-bulk-stores.ts,
infrastructure/src/main/cdk/stacks/types.ts.
3.3 Naming Conventions
Section titled “3.3 Naming Conventions”| Element | Pattern | Example |
|---|---|---|
| Bucket name | ${lcFqn}-<purpose>-bucket | alpha001-demo-image-assets-bucket |
| IAM role name | ${fqn}-<Purpose>Role | Alpha001-demo-ImageUploadPreSigningRole |
| CloudFront comment | ${fqn}-<id> - Distribution for <purpose> | Alpha001-demo-ImageCDN - Distribution for image assets |
| Export name (public) | ${fqn}-API-<Key> | Alpha001-demo-API-ImageAssetBucketArn |
| Export name (internal) | ${fqn}-I-<Key> | Alpha001-demo-I-ImageSigningKeyArn |
| Secret name | ${fqn}-<SecretName> | Alpha001-demo-ImageCdnSigningKey |
| CDK construct ID | PascalCase, descriptive | ImageAssetBucket, ImagePresignRole |
Source: infrastructure/src/main/cdk/utils/purpose.ts,
infrastructure/src/main/cdk/utils/fqn.ts,
infrastructure/src/main/cdk/constructs/storage/public-upload-bucket.ts.
3.4 S3 Bucket Pattern
Section titled “3.4 S3 Bucket Pattern”Derived from UploadBucket in public-upload-bucket.ts:
- Block all public access:
BlockPublicAccess.BLOCK_ALL. - Enforce SSL:
enforceSSL: true. - SSE-S3 encryption:
BucketEncryption.S3_MANAGED. - Object ownership:
BUCKET_OWNER_ENFORCED(ACLs disabled). - Logging: Separate logging bucket with S3 service principal write permission and 90-day lifecycle.
- Bucket resource policy: Restrict
PutObjectto presigning role with HTTPS + SSE-S3 conditions. RestrictGetObjectto presigning role (or OAC for CDN access). - CORS: Only if
appUrlsare defined (browser-direct uploads). - Lifecycle rules: At minimum, abort incomplete multipart uploads.
Differences for ImageAssetBucket:
versioned: true(notfalse).removalPolicy: RETAIN(notDESTROY).- No expiration lifecycle rule (persistent storage).
- OAC policy statement granting CloudFront
s3:GetObject.
Source: infrastructure/src/main/cdk/constructs/storage/public-upload-bucket.ts.
3.5 IAM Role Pattern
Section titled “3.5 IAM Role Pattern”Derived from the presigning role in UploadBucket:
- Explicit role name: Constructed from
fqn+ purpose suffix. assumedBy:ArnPrincipal(clientRoleArn)— the EKS pod role.- Policy statements with SIDs: Each statement has a descriptive
sid(e.g.,"PutAndGetObjects","MultipartUploads"). - Conditions:
aws:SecureTransport: trueands3:x-amz-server-side-encryption: AES256on all S3 operations. - Least privilege: Separate statements for different operation groups (list, put/get, multipart).
Source: infrastructure/src/main/cdk/constructs/storage/public-upload-bucket.ts.
3.6 CloudFront Pattern
Section titled “3.6 CloudFront Pattern”Derived from ApiCloudFront in api-cloudfront.ts:
- Certificate in us-east-1: CloudFront requires ACM certificates in
us-east-1regardless of the stack’s region. - Domain names: Set via
domainNamesarray on the distribution. - Validation: Static
validateProps()method checks required fields. - Tagging: Apply tags from props to the distribution.
- Built interface: Expose
distributionanddistributionDomainName.
Differences for ImageAssetCdn:
- Origin: S3 bucket with OAC (not API Gateway HTTP origin).
- Viewer access: Trusted key groups (signed cookies), not open.
- Cache policy:
CachingOptimized(notCACHING_DISABLED). - Allowed methods: GET, HEAD only (not ALL).
- Custom domain on
<partition>.<infra>.assets.arda.cards(notio.arda.cards).
Source: infrastructure/src/main/cdk/constructs/xgress/api-cloudfront.ts.
3.7 Secrets Manager Pattern
Section titled “3.7 Secrets Manager Pattern”Derived from PredefinedSecret and GeneratedSecret:
- Secret name:
${prefix}-${secretName}— prefix is the FQN. - RemovalPolicy: Match the construct’s lifecycle (RETAIN for the signing key since it is operationally critical).
- Access via VPC endpoint: Secrets Manager traffic stays within the VPC through the existing Secrets Manager VPC endpoint.
For the CloudFront signing key, the private key must be stored as a plain string (PEM format) retrievable by the BFF at runtime.
Source: infrastructure/src/main/cdk/constructs/oam/predefined-secret.ts,
infrastructure/src/main/cdk/constructs/oam/generated-secret.ts.
3.8 Partition Integration Pattern
Section titled “3.8 Partition Integration Pattern”Derived from buildPartition() in apps/Al1x/partition.ts:
- All partition resources are instantiated in
buildPartition(). New stacks or constructs must be added here. - Stack dependencies: Use
stack.addDependency(other)when one stack depends on another’s outputs. publish()must be called after stack construction to create CloudFormation exports.- ImportingStack provides infrastructure-level dependencies (VPC, EKS, hosted zones, certificates) reconstructed from imports.
- Props are assembled in
buildPartition()by combiningPartitionmetadata,ImportingStackimports, andwebConfigurationparameters.
Source: infrastructure/src/main/cdk/apps/Al1x/partition.ts,
infrastructure/src/main/cdk/apps/Al1x/util.ts.
3.9 Validation Pattern
Section titled “3.9 Validation Pattern”All stacks and the ArdaConstruct base class use the same validation
approach:
- Static
validateProps()method returnsError[]. - Constructor checks before calling
super(). MultiErroraggregates multiple validation failures into a single throw.- Regex-based validation for AWS resource names, ARNs, and
identifiers (from
utils/aws.ts).
Source: infrastructure/src/main/cdk/utils/misc.ts (MultiError),
infrastructure/src/main/cdk/utils/aws.ts (regex patterns),
infrastructure/src/main/cdk/utils/arda-construct.ts.
Copyright: (c) Arda Systems 2025-2026, All rights reserved
Copyright: © Arda Systems 2025-2026, All rights reserved