Skip to content

Instantly share code, notes, and snippets.

@Armadillidiid
Last active June 7, 2026 21:30
Show Gist options
  • Select an option

  • Save Armadillidiid/2d3cd57db2c291fec27c0a5ec14f9e45 to your computer and use it in GitHub Desktop.

Select an option

Save Armadillidiid/2d3cd57db2c291fec27c0a5ec14f9e45 to your computer and use it in GitHub Desktop.
CDK Amplify construct for monorepo Next.js apps
import * as amplify from "@aws-cdk/aws-amplify-alpha";
import * as cdk from "aws-cdk-lib";
import * as codebuild from "aws-cdk-lib/aws-codebuild";
import * as iam from "aws-cdk-lib/aws-iam";
import * as route53 from "aws-cdk-lib/aws-route53";
import { Construct } from "constructs";
import { envConfig } from "../config.ts";
import { ENVIRONMENT } from "../constants.ts";
export interface AmplifyAppProps {
readonly hostedZone: route53.IHostedZone;
readonly repositoryOwner: string;
readonly repositoryName: string;
readonly tokenSecretName: string;
readonly appRoot: string;
readonly packageName: string;
readonly domainPrefix: string;
}
export class AmplifyApp extends Construct {
public readonly app: amplify.App;
public readonly domain: amplify.Domain;
public readonly stagingBranch: amplify.Branch;
public readonly prodBranch: amplify.Branch;
public readonly serviceRole: iam.Role;
public readonly appIdOutput: cdk.CfnOutput;
public readonly stagingUrlOutput: cdk.CfnOutput;
public readonly prodUrlOutput: cdk.CfnOutput;
constructor(scope: Construct, id: string, props: AmplifyAppProps) {
super(scope, id);
const amplifyAppServiceRole = new iam.Role(this, "AmplifyAppServiceRole", {
assumedBy: new iam.ServicePrincipal("amplify.amazonaws.com"),
});
this.serviceRole = amplifyAppServiceRole;
const amplifySecretsPathArn = cdk.Stack.of(this).formatArn({
service: "ssm",
resource: "parameter",
resourceName: "amplify/*",
arnFormat: cdk.ArnFormat.SLASH_RESOURCE_NAME,
});
amplifyAppServiceRole.addToPolicy(
new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
actions: [
"ssm:GetParameter",
"ssm:GetParameters",
"ssm:GetParametersByPath",
],
resources: [amplifySecretsPathArn],
}),
);
amplifyAppServiceRole.addToPolicy(
new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
actions: ["kms:Decrypt"],
resources: ["*"],
}),
);
amplifyAppServiceRole.addToPolicy(
new iam.PolicyStatement({
actions: [
"logs:CreateLogStream",
"logs:CreateLogGroup",
"logs:DescribeLogGroups",
"logs:PutLogEvents",
],
resources: ["*"],
}),
);
amplifyAppServiceRole.addToPolicy(
new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
actions: [
"acm:DescribeCertificate",
"acm:ListCertificates",
"acm:RequestCertificate",
],
resources: ["*"],
}),
);
this.app = new amplify.App(this, "AmplifyApp", {
platform: amplify.Platform.WEB_COMPUTE,
sourceCodeProvider: new amplify.GitHubSourceCodeProvider({
owner: props.repositoryOwner,
repository: props.repositoryName,
oauthToken: cdk.SecretValue.secretsManager(props.tokenSecretName, {
jsonField: "github_pat",
}),
}),
role: amplifyAppServiceRole,
buildSpec: codebuild.BuildSpec.fromObjectToYaml({
version: 1,
applications: [
{
appRoot: props.appRoot,
frontend: {
buildPath: "/",
phases: {
preBuild: {
commands: [
"nvm use 24",
"corepack enable",
"pnpm config --location=project set nodeLinker hoisted",
`node -e "Object.entries(JSON.parse(process.env.secrets||'{}')).forEach(([k,v])=>console.log(k+'='+v))" > ${props.appRoot}/.env.production`,
"pnpm install --frozen-lockfile",
"node --experimental-strip-types scripts/resolve-catalogs.ts",
"pnpm install --lockfile-only",
],
},
build: {
commands: [
`pnpm turbo run build --filter=${props.packageName}`,
],
},
},
artifacts: {
baseDirectory: `${props.appRoot}/.next`,
files: ["**/*"],
},
cache: {
paths: ["node_modules/**/*", ".turbo/**/*"],
},
},
},
],
}),
});
const hasPrefix = props.domainPrefix !== "";
const stagingUrl = hasPrefix
? `https://${ENVIRONMENT.STAGING}-${props.domainPrefix}.${props.hostedZone.zoneName}`
: `https://${ENVIRONMENT.STAGING}.${props.hostedZone.zoneName}`;
const prodUrl = hasPrefix
? `https://${props.domainPrefix}.${props.hostedZone.zoneName}`
: `https://${props.hostedZone.zoneName}`;
this.stagingBranch = this.app.addBranch("StagingBranch", {
branchName: envConfig[ENVIRONMENT.STAGING].branch,
pullRequestPreview: false,
autoBuild: false,
environmentVariables: {
PRODUCTION_URL: stagingUrl,
AMPLIFY_MONOREPO_APP_ROOT: props.appRoot,
NODE_OPTIONS: "--max-old-space-size=4096",
},
// NOTE: Basic auth moved to Next.js proxy
// Amplify BasicAuth uses AWS-managed realm that differs per CloudFront
// behavior, causing browser to re-prompt for credentials
//
// basicAuth: amplify.BasicAuth.fromCredentials(
// "admin",
// cdk.SecretValue.secretsManager(props.tokenSecretName, {
// jsonField: "staging_auth_password",
// }),
// ),
});
this.prodBranch = this.app.addBranch("ProdBranch", {
branchName: envConfig[ENVIRONMENT.PROD].branch,
pullRequestPreview: false,
autoBuild: false,
environmentVariables: {
PRODUCTION_URL: prodUrl,
AMPLIFY_MONOREPO_APP_ROOT: props.appRoot,
},
});
const stagingPrefix = hasPrefix
? `${ENVIRONMENT.STAGING}-${props.domainPrefix}`
: ENVIRONMENT.STAGING;
const prodPrefix = props.domainPrefix;
this.domain = this.app.addDomain(props.hostedZone.zoneName, {
subDomains: [
{ branch: this.stagingBranch, prefix: stagingPrefix },
{ branch: this.prodBranch, prefix: prodPrefix },
],
});
this.appIdOutput = new cdk.CfnOutput(this, "AmplifyAppId", {
value: this.app.appId,
description: "Amplify App ID for frontend app",
});
this.stagingUrlOutput = new cdk.CfnOutput(this, "StagingBranchUrl", {
value: stagingUrl,
description: "Staging branch URL",
});
this.prodUrlOutput = new cdk.CfnOutput(this, "ProdBranchUrl", {
value: prodUrl,
description: "Production branch URL",
});
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment