From 4d3ba81dd12c02b712ad0c35eeddb43143cbf6ba Mon Sep 17 00:00:00 2001 From: Daniel Naab Date: Sun, 3 May 2026 21:33:14 +0000 Subject: [PATCH 1/4] Add Route53 custom domain for App Runner demo deployment Configure 10x-forms.labs.flexion.us DNS for the demo App Runner service, including hosted zone, custom domain association, certificate validation records, and CNAME to the service URL. --- infra/cdktf/src/lib/aws/sandbox-stack.ts | 65 +++++++++++++++++++++++- infra/cdktf/src/spaces/aws/demo.ts | 1 + 2 files changed, 64 insertions(+), 2 deletions(-) diff --git a/infra/cdktf/src/lib/aws/sandbox-stack.ts b/infra/cdktf/src/lib/aws/sandbox-stack.ts index 0427c76..ea42042 100644 --- a/infra/cdktf/src/lib/aws/sandbox-stack.ts +++ b/infra/cdktf/src/lib/aws/sandbox-stack.ts @@ -1,5 +1,5 @@ import { Construct } from 'constructs'; -import { Fn } from 'cdktf'; +import { Fn, TerraformOutput } from 'cdktf'; import { Vpc } from '../../../.gen/providers/aws/vpc'; import { Subnet } from '../../../.gen/providers/aws/subnet'; @@ -19,9 +19,13 @@ import { IamRole } from '../../../.gen/providers/aws/iam-role'; import { IamRolePolicy } from '../../../.gen/providers/aws/iam-role-policy'; import { IamRolePolicyAttachment } from '../../../.gen/providers/aws/iam-role-policy-attachment'; import { DataAwsAvailabilityZones } from '../../../.gen/providers/aws/data-aws-availability-zones'; +import { Route53Zone } from '../../../.gen/providers/aws/route53-zone'; +import { Route53Record } from '../../../.gen/providers/aws/route53-record'; +import { ApprunnerCustomDomainAssociation } from '../../../.gen/providers/aws/apprunner-custom-domain-association'; interface SandboxStackConfig { environment: string; + customDomain?: string; } export class SandboxStack extends Construct { @@ -365,7 +369,7 @@ export class SandboxStack extends Construct { ); // App Runner Service - new ApprunnerService(this, `${id}-apprunner-service`, { + const appRunnerService = new ApprunnerService(this, `${id}-apprunner-service`, { serviceName: `${id}`, sourceConfiguration: { autoDeploymentsEnabled: true, @@ -415,5 +419,62 @@ export class SandboxStack extends Construct { createBeforeDestroy: true, }, }); + + // Custom domain and DNS configuration + if (config.customDomain) { + const domainName = config.customDomain; + + // Route53 hosted zone for the custom domain + const zone = new Route53Zone(this, `${id}-zone`, { + name: domainName, + tags: { + Name: `${id}-zone`, + Environment: environment, + }, + }); + + // Associate custom domain with App Runner service + const customDomainAssociation = new ApprunnerCustomDomainAssociation( + this, + `${id}-custom-domain`, + { + domainName: domainName, + serviceArn: appRunnerService.arn, + } + ); + + // Create DNS validation records for App Runner certificate + // App Runner provides CNAME records for certificate validation + for (let i = 0; i < 3; i++) { + new Route53Record( + this, + `${id}-validation-record-${i}`, + { + zoneId: zone.zoneId, + name: `\${${customDomainAssociation.fqn}.certificate_validation_records[${i}].name}`, + type: `\${${customDomainAssociation.fqn}.certificate_validation_records[${i}].type}`, + records: [ + `\${${customDomainAssociation.fqn}.certificate_validation_records[${i}].value}`, + ], + ttl: 300, + } + ); + } + + // CNAME record pointing the domain to App Runner service URL + new Route53Record(this, `${id}-apprunner-alias`, { + zoneId: zone.zoneId, + name: domainName, + type: 'CNAME', + records: [appRunnerService.serviceUrl], + ttl: 300, + }); + + // Output the name servers for delegation + new TerraformOutput(this, `${id}-nameservers`, { + value: zone.nameServers, + description: `Name servers for ${domainName} - configure these in the parent zone (labs.flexion.us)`, + }); + } } } diff --git a/infra/cdktf/src/spaces/aws/demo.ts b/infra/cdktf/src/spaces/aws/demo.ts index 4a4cdd5..fb5af36 100644 --- a/infra/cdktf/src/spaces/aws/demo.ts +++ b/infra/cdktf/src/spaces/aws/demo.ts @@ -19,6 +19,7 @@ class AwsDemoStack extends TerraformStack { // Create the sandbox infrastructure new SandboxStack(this, stackName, { environment: 'flexion-forms-demo', + customDomain: '10x-forms.labs.flexion.us', }); } } From 95fb60ddfb2db2ab25966379f35db301db0a40f6 Mon Sep 17 00:00:00 2001 From: Daniel Naab Date: Mon, 4 May 2026 02:32:30 +0000 Subject: [PATCH 2/4] Add nix shell and direnv config for dev environment Provides Node.js 22, pnpm, Terraform, and native build tools (gcc, make, python3, pkg-config) needed for cdktf and node-gyp modules. --- .envrc | 1 + shell.nix | 13 +++++++++++++ 2 files changed, 14 insertions(+) create mode 100644 .envrc create mode 100644 shell.nix diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..1d953f4 --- /dev/null +++ b/.envrc @@ -0,0 +1 @@ +use nix diff --git a/shell.nix b/shell.nix new file mode 100644 index 0000000..78ff57c --- /dev/null +++ b/shell.nix @@ -0,0 +1,13 @@ +{ pkgs ? import { config.allowUnfreePredicate = pkg: builtins.elem (pkgs.lib.getName pkg) [ "terraform" ]; } }: + +pkgs.mkShell { + buildInputs = with pkgs; [ + nodejs_22 + corepack_22 + python3 # node-gyp dependency + gnumake + gcc + pkg-config + terraform + ]; +} From 816f93aa8bc764a3490868d7d58aafdc0d091197 Mon Sep 17 00:00:00 2001 From: Daniel Naab Date: Mon, 4 May 2026 02:54:37 +0000 Subject: [PATCH 3/4] Fix custom domain config: remove validation records, add outputs The certificate validation records are only known after the App Runner custom domain association is created, so they cannot be managed as separate Terraform resources in a single apply. Instead, output them for manual DNS configuration after the initial deploy. --- infra/cdktf/src/lib/aws/sandbox-stack.ts | 31 ++++++++++-------------- 1 file changed, 13 insertions(+), 18 deletions(-) diff --git a/infra/cdktf/src/lib/aws/sandbox-stack.ts b/infra/cdktf/src/lib/aws/sandbox-stack.ts index ea42042..91d3262 100644 --- a/infra/cdktf/src/lib/aws/sandbox-stack.ts +++ b/infra/cdktf/src/lib/aws/sandbox-stack.ts @@ -443,24 +443,6 @@ export class SandboxStack extends Construct { } ); - // Create DNS validation records for App Runner certificate - // App Runner provides CNAME records for certificate validation - for (let i = 0; i < 3; i++) { - new Route53Record( - this, - `${id}-validation-record-${i}`, - { - zoneId: zone.zoneId, - name: `\${${customDomainAssociation.fqn}.certificate_validation_records[${i}].name}`, - type: `\${${customDomainAssociation.fqn}.certificate_validation_records[${i}].type}`, - records: [ - `\${${customDomainAssociation.fqn}.certificate_validation_records[${i}].value}`, - ], - ttl: 300, - } - ); - } - // CNAME record pointing the domain to App Runner service URL new Route53Record(this, `${id}-apprunner-alias`, { zoneId: zone.zoneId, @@ -475,6 +457,19 @@ export class SandboxStack extends Construct { value: zone.nameServers, description: `Name servers for ${domainName} - configure these in the parent zone (labs.flexion.us)`, }); + + // Output certificate validation records for manual DNS configuration. + // After the first apply, create these CNAME records in the hosted zone + // to complete App Runner certificate validation. + new TerraformOutput(this, `${id}-cert-validation-records`, { + value: customDomainAssociation.certificateValidationRecords, + description: `Certificate validation CNAME records for ${domainName}`, + }); + + new TerraformOutput(this, `${id}-dns-target`, { + value: customDomainAssociation.dnsTarget, + description: `App Runner DNS target for ${domainName}`, + }); } } } From 5b8b4b17b7a24c46e39ec981e7799bf3232eee78 Mon Sep 17 00:00:00 2001 From: Daniel Naab Date: Mon, 4 May 2026 03:06:13 +0000 Subject: [PATCH 4/4] Fix CNAME at zone apex, add preventDestroy and enableWwwSubdomain Remove the CNAME record that would have been placed at the zone apex (10x-forms.labs.flexion.us), which is invalid per RFC 1034 as it conflicts with SOA/NS records. App Runner routes traffic for custom domains automatically once certificate validation records are in place. Also add preventDestroy lifecycle to the Route53 zone to guard against accidental deletion (which would require re-submitting NS records to the parent zone), and explicitly set enableWwwSubdomain: false on the custom domain association. --- infra/cdktf/src/lib/aws/sandbox-stack.ts | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/infra/cdktf/src/lib/aws/sandbox-stack.ts b/infra/cdktf/src/lib/aws/sandbox-stack.ts index 91d3262..797eb62 100644 --- a/infra/cdktf/src/lib/aws/sandbox-stack.ts +++ b/infra/cdktf/src/lib/aws/sandbox-stack.ts @@ -20,7 +20,6 @@ import { IamRolePolicy } from '../../../.gen/providers/aws/iam-role-policy'; import { IamRolePolicyAttachment } from '../../../.gen/providers/aws/iam-role-policy-attachment'; import { DataAwsAvailabilityZones } from '../../../.gen/providers/aws/data-aws-availability-zones'; import { Route53Zone } from '../../../.gen/providers/aws/route53-zone'; -import { Route53Record } from '../../../.gen/providers/aws/route53-record'; import { ApprunnerCustomDomainAssociation } from '../../../.gen/providers/aws/apprunner-custom-domain-association'; interface SandboxStackConfig { @@ -431,27 +430,25 @@ export class SandboxStack extends Construct { Name: `${id}-zone`, Environment: environment, }, + lifecycle: { + preventDestroy: true, + }, }); - // Associate custom domain with App Runner service + // Associate custom domain with App Runner service. + // App Runner handles traffic routing for the custom domain once the + // certificate validation records are in place — no separate CNAME/ALIAS + // record is needed (and a CNAME at the zone apex would be invalid). const customDomainAssociation = new ApprunnerCustomDomainAssociation( this, `${id}-custom-domain`, { domainName: domainName, serviceArn: appRunnerService.arn, + enableWwwSubdomain: false, } ); - // CNAME record pointing the domain to App Runner service URL - new Route53Record(this, `${id}-apprunner-alias`, { - zoneId: zone.zoneId, - name: domainName, - type: 'CNAME', - records: [appRunnerService.serviceUrl], - ttl: 300, - }); - // Output the name servers for delegation new TerraformOutput(this, `${id}-nameservers`, { value: zone.nameServers,