Skip to content

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.


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.

InfrastructureAccountRegionPartitions
Alpha001009765408297us-east-1demo, prod
Alpha002139852620346us-east-1dev, stage
SandboxKyle002575126609604us-east-1kyle

Source: infrastructure/src/main/cdk/platforms.ts, infrastructure/src/main/cdk/platform/aws-configuration.ts.

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.

PlantUML diagram

Root Account resources (deployed once, globally):

ResourceStackAccountPurpose
Route53 io.arda.cards zoneRootConfigurationStackRoot (841876193886)Parent zone for API/IO subdomains. All infrastructure accounts delegate from here.
Route53 app.arda.cards zoneRootConfigurationStackRoot (841876193886)Parent zone for application subdomains.
Route53 auth.arda.cards zoneRootConfigurationStackRoot (841876193886)Parent zone for authentication subdomains.
Route53 assets.arda.cards zone (NEW)RootConfigurationStackRoot (841876193886)Parent zone for static asset CDN subdomains (PD-02).
IAM AllowCreatingNSRecords roleRootConfigurationStackRoot (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.

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.

PlantUML diagram

Resources created (per infrastructure account):

ResourceStackPurpose
VPC (10.X.0.0/16)NetworkingInfrastructureStackNetwork isolation. Public + private subnets per AZ. NAT gateways for egress.
S3 VPC Endpoint (Gateway)NetworkingInfrastructureStackPrivate S3 access from within the VPC without traversing the internet.
Secrets Manager VPC Endpoint (Interface)NetworkingInfrastructureStackPrivate Secrets Manager access with private DNS.
EKS ClusterInfrastructureEksStackContainer orchestration. Fargate profiles, OIDC provider, GitHub access entries.
Route53 Subdomain Zones (io, app, auth)InfrastructureIngressSubdomain zones for this infrastructure. NS records delegated to root account zones via cross-account role.
Route53 Subdomain Zone (assets) (NEW)InfrastructureIngressalpha001.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)InfrastructureIngressTLS for *.alpha001.io.arda.cards, *.alpha001.app.arda.cards, *.alpha001.auth.arda.cards. DNS-validated against the subdomain zones.
ACM Wildcard Certificate (assets) (NEW)InfrastructureIngressTLS 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.

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.

PlantUML diagram

Resources created (per partition):

ResourceStackPurpose
S3 Upload Bucket (1-day TTL)BulkStoresStackEphemeral storage for CSV uploads. Presigned PUT. CORS for browser uploads.
S3 Logging Bucket (90-day)BulkStoresStackServer access logs for the upload bucket.
Upload Presigning RoleBulkStoresStackIAM role assumed by EKS pod to generate presigned URLs. s3:PutObject, s3:GetObject with HTTPS + SSE-S3 conditions.
Aurora PostgreSQL 16.6PurposeAuroraClusterStackBitemporal application database. Multi-AZ, encrypted, auto-backup.
Cognito User PoolPartitionAuthnUser authentication. OAuth2 clients (M2M, web), resource servers, custom scopes.
NLB (Network Load Balancer)PurposeIngressInternal load balancer routing traffic to EKS pods. Ports 80, 443.
API Gateway V2 (HTTP API)PurposeIngressREST API entry point. VPC Link to NLB. JWT authorizer (Cognito). Custom domain.
CloudFront (API Distribution)PurposeIngressEdge distribution for the API Gateway. HTTPS redirect, no caching.
Route53 A RecordsPurposeDnsStackDNS records pointing to the API Gateway custom domain.
EKS Namespace + PodPurposeComputeStackApplication workload (operations service).
Amplify AppCloudFormation (amplify.cfn.yaml)Next.js SSR frontend. Only deployed for selected partitions.
Partition SecretsCloudFormation (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.

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.

PlantUML diagram

ResourceTypeKey ConfigurationSource Requirement
Image Asset BucketS3Persistent (no TTL), versioning, SSE-S3, RETAIN removal policy, CORS for browser POST, abort incomplete multipart after 1 dayAWS-FR-001, AWS-FR-005, AWS-FR-006
Image Upload Presigning RoleIAM Roles3:PutObject + s3:GetObject with HTTPS + SSE-S3 conditions. Assumable by EKS pod role.AWS-FR-003
Image Asset CDNCloudFrontOAC origin to S3 bucket, trusted key groups for signed cookies, custom domain <partition>.<infra>.assets.arda.cards, HTTPS-only, GET/HEAD only, CachingOptimized, PriceClass_100AWS-FR-004, AWS-NFR-001
Signing Key GroupCloudFront + Secrets ManagerRSA 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 RecordRoute53CNAME or Alias record <partition>.<infra>.assets.arda.cards pointing to CloudFront distribution.AWS-FR-004
Existing ResourceInteractionDirection
EKS Pod RoleAssumes the new Image Upload Presigning Role via sts:AssumeRolePod → Presigning Role
S3 VPC EndpointUsed for presigned POST form generation (backend-side, within VPC)Internal
Secrets Manager VPC EndpointUsed by BFF to retrieve the CloudFront signing private keyInternal
assets.arda.cards Hosted Zone (NEW — root)Parent zone for <infra>.assets.arda.cards subdomain delegationDNS
<infra>.assets.arda.cards Zone (NEW — infra)Subdomain zone hosting CDN A records per partitionDNS
ACM *.<infra>.assets.arda.cards Cert (NEW — infra)TLS certificate for CloudFront distributionsTLS

Source: AWS Specification, CDN Access Control.


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.

amm.sh executes the following steps for each invocation:

  1. Setup — Parse arguments, derive infrastructure and partition list, load secrets from 1Password (local) or GitHub Secrets (CI).
  2. Infrastructure — Create CloudWatch log group (CloudFormation), bootstrap CDK, deploy infrastructure CDK app (infra.ts), configure EKS (kubeconfig, FluentBit, Load Balancer Controller, External Secrets).
  3. 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).

PlantUML diagram

amm.sh deploys these templates directly via aws cloudformation deploy:

TemplateStack Name PatternPurpose
cloudWatch.cfn.yaml{infra}-CloudWatchLogDeployment log group and stream
partitionSecrets.cfn.yaml{infra}-{partition}-SecretsAPI keys, HubSpot, Pylon secrets
amplify.cfn.yaml{infra}-{partition}-AmplifyAmplify app, compute role, service role
amplifyBranch.cfn.yaml{infra}-{partition}-AmplifyBranchBranch resource, domain binding, PR preview

All stacks use the publish() / readImports() pattern from stacks/types.ts:

  1. Each stack defines ExportKeys (union of string literal types) and ExportDefinition (maps keys to {exportName, description}).
  2. publish() creates CfnOutput resources. Keys containing -API- are exported publicly; keys containing -I- are exported as protected/internal.
  3. ImportingStack (in apps/Al1x/util.ts) reconstructs infrastructure resources from these exports using Fn.importValue / Fn.importListValue.

Source: infrastructure/src/main/cdk/stacks/types.ts, infrastructure/src/main/cdk/apps/Al1x/util.ts.

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.

PlantUML diagram

PlantUML diagram

Export KeyValueConsumer
${fqn}-API-ImageAssetBucketArnBucket ARNOperations CloudFormation
${fqn}-API-ImageAssetBucketNameBucket nameOperations CloudFormation
${fqn}-API-ImagePresignRoleArnPresigning role ARNOperations CloudFormation
${fqn}-API-ImageCdnDomainCloudFront domain nameOperations (CDN URL construction)
${fqn}-API-ImageCdnSigningKeyIdCloudFront key pair IDBFF (cookie signing)
${fqn}-API-ImageCdnSigningKeySecretArnSecrets Manager ARNBFF (cookie signing)

A new infrastructure/tools/verify-image-cdn.ts script validates the deployed infrastructure:

  1. Assumes the presigning role via STS.
  2. Uploads a test JPEG to S3 via presigned POST.
  3. Verifies CloudFront returns 403 without signed cookies.
  4. Retrieves the signing private key from Secrets Manager.
  5. Generates tenant-scoped signed cookies (RSA-SHA1 custom policy).
  6. Verifies CloudFront returns 200 with valid cookies.
  7. Verifies tenant isolation (wrong-tenant cookies get 403).
  8. Cleans up the test object.

Source: Phasing — Phase 1 Verification.


Patterns extracted from the infrastructure repository that must be followed when implementing the new constructs and stack modifications.

Every CDK construct follows the Configuration → Props → Built pattern:

// 1. Configuration: what the consumer decides at design time
export interface Configuration {
readonly locator: purpose.Locator;
readonly name: string;
// ... design-time parameters
}
// 2. Props: Configuration + runtime dependencies injected by the stack
export interface Props extends Configuration {
readonly bucketClientRoleArn: string;
readonly loggingBucket: s3.Bucket;
// ... dependencies from other constructs/stacks
}
// 3. Built: what the construct exposes after construction
export 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.

Stacks follow a parallel pattern to constructs:

  1. Configuration interface — design-time parameters.
  2. Props interface extending Configuration — adds dependencies.
  3. Built interface — constructed sub-resources.
  4. ExportKeys type — string union of export key names.
  5. ExportDefinition extending ExportDefinitions<ExportKeys> — maps keys to {exportName, description}.
  6. ExportValues — maps keys to StackIOValue (definition + value).
  7. validateProps() static method — throws MultiError if invalid.
  8. publish() method — calls stackTypes.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 publishingPrefix is always purpose.fqn(locator) (kebab-case).

Source: infrastructure/src/main/cdk/stacks/purpose/partition-bulk-stores.ts, infrastructure/src/main/cdk/stacks/types.ts.

ElementPatternExample
Bucket name${lcFqn}-<purpose>-bucketalpha001-demo-image-assets-bucket
IAM role name${fqn}-<Purpose>RoleAlpha001-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 IDPascalCase, descriptiveImageAssetBucket, 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.

Derived from UploadBucket in public-upload-bucket.ts:

  1. Block all public access: BlockPublicAccess.BLOCK_ALL.
  2. Enforce SSL: enforceSSL: true.
  3. SSE-S3 encryption: BucketEncryption.S3_MANAGED.
  4. Object ownership: BUCKET_OWNER_ENFORCED (ACLs disabled).
  5. Logging: Separate logging bucket with S3 service principal write permission and 90-day lifecycle.
  6. Bucket resource policy: Restrict PutObject to presigning role with HTTPS + SSE-S3 conditions. Restrict GetObject to presigning role (or OAC for CDN access).
  7. CORS: Only if appUrls are defined (browser-direct uploads).
  8. Lifecycle rules: At minimum, abort incomplete multipart uploads.

Differences for ImageAssetBucket:

  • versioned: true (not false).
  • removalPolicy: RETAIN (not DESTROY).
  • No expiration lifecycle rule (persistent storage).
  • OAC policy statement granting CloudFront s3:GetObject.

Source: infrastructure/src/main/cdk/constructs/storage/public-upload-bucket.ts.

Derived from the presigning role in UploadBucket:

  1. Explicit role name: Constructed from fqn + purpose suffix.
  2. assumedBy: ArnPrincipal(clientRoleArn) — the EKS pod role.
  3. Policy statements with SIDs: Each statement has a descriptive sid (e.g., "PutAndGetObjects", "MultipartUploads").
  4. Conditions: aws:SecureTransport: true and s3:x-amz-server-side-encryption: AES256 on all S3 operations.
  5. Least privilege: Separate statements for different operation groups (list, put/get, multipart).

Source: infrastructure/src/main/cdk/constructs/storage/public-upload-bucket.ts.

Derived from ApiCloudFront in api-cloudfront.ts:

  1. Certificate in us-east-1: CloudFront requires ACM certificates in us-east-1 regardless of the stack’s region.
  2. Domain names: Set via domainNames array on the distribution.
  3. Validation: Static validateProps() method checks required fields.
  4. Tagging: Apply tags from props to the distribution.
  5. Built interface: Expose distribution and distributionDomainName.

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 (not CACHING_DISABLED).
  • Allowed methods: GET, HEAD only (not ALL).
  • Custom domain on <partition>.<infra>.assets.arda.cards (not io.arda.cards).

Source: infrastructure/src/main/cdk/constructs/xgress/api-cloudfront.ts.

Derived from PredefinedSecret and GeneratedSecret:

  1. Secret name: ${prefix}-${secretName} — prefix is the FQN.
  2. RemovalPolicy: Match the construct’s lifecycle (RETAIN for the signing key since it is operationally critical).
  3. 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.

Derived from buildPartition() in apps/Al1x/partition.ts:

  1. All partition resources are instantiated in buildPartition(). New stacks or constructs must be added here.
  2. Stack dependencies: Use stack.addDependency(other) when one stack depends on another’s outputs.
  3. publish() must be called after stack construction to create CloudFormation exports.
  4. ImportingStack provides infrastructure-level dependencies (VPC, EKS, hosted zones, certificates) reconstructed from imports.
  5. Props are assembled in buildPartition() by combining Partition metadata, ImportingStack imports, and webConfiguration parameters.

Source: infrastructure/src/main/cdk/apps/Al1x/partition.ts, infrastructure/src/main/cdk/apps/Al1x/util.ts.

All stacks and the ArdaConstruct base class use the same validation approach:

  1. Static validateProps() method returns Error[].
  2. Constructor checks before calling super().
  3. MultiError aggregates multiple validation failures into a single throw.
  4. 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