Last active
June 7, 2026 21:30
-
-
Save Armadillidiid/2d3cd57db2c291fec27c0a5ec14f9e45 to your computer and use it in GitHub Desktop.
CDK Amplify construct for monorepo Next.js apps
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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