Repository: aws-samples/serverless-full-stack-webapp-starter-kit Branch: main Commit: e4b703eba018 Files: 81 Total size: 373.6 KB Directory structure: gitextract_lfz83ak7/ ├── .github/ │ └── workflows/ │ ├── build.yml │ ├── commitlint.yml │ ├── needs-triage.yml │ ├── release-please.yml │ ├── stale.yml │ └── update_snapshot.yml ├── .prettierrc ├── .release-please-manifest.json ├── .serverless-full-stack-webapp-starter-kit/ │ └── design/ │ └── DESIGN_PRINCIPLES.md ├── AGENTS.md ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── cdk/ │ ├── .gitignore │ ├── README.md │ ├── bin/ │ │ └── cdk.ts │ ├── cdk.json │ ├── jest.config.js │ ├── lib/ │ │ ├── constructs/ │ │ │ ├── async-job.ts │ │ │ ├── auth/ │ │ │ │ ├── .gitignore │ │ │ │ ├── index.ts │ │ │ │ └── prefix-generator.js │ │ │ ├── cf-lambda-furl-service/ │ │ │ │ ├── edge-function.ts │ │ │ │ ├── lambda/ │ │ │ │ │ └── sign-payload.ts │ │ │ │ └── service.ts │ │ │ ├── database.ts │ │ │ ├── event-bus/ │ │ │ │ ├── handler.mjs │ │ │ │ └── index.ts │ │ │ └── webapp.ts │ │ ├── main-stack.ts │ │ └── us-east-1-stack.ts │ ├── package.json │ ├── test/ │ │ ├── __snapshots__/ │ │ │ ├── serverless-fullstack-webapp-starter-kit-without-domain.test.ts.snap │ │ │ └── serverless-fullstack-webapp-starter-kit.test.ts.snap │ │ ├── serverless-fullstack-webapp-starter-kit-without-domain.test.ts │ │ ├── serverless-fullstack-webapp-starter-kit.test.ts │ │ └── snapshot-plugin.ts │ └── tsconfig.json ├── compose.yaml ├── release-please-config.json └── webapp/ ├── .dockerignore ├── .env.local.example ├── .gitignore ├── Dockerfile ├── README.md ├── components.json ├── eslint.config.mjs ├── job.Dockerfile ├── next.config.ts ├── package.json ├── postcss.config.mjs ├── prisma/ │ └── schema.prisma ├── run.sh ├── src/ │ ├── app/ │ │ ├── (root)/ │ │ │ ├── actions.ts │ │ │ ├── components/ │ │ │ │ ├── CreateTodoForm.tsx │ │ │ │ └── TodoItem.tsx │ │ │ ├── page.tsx │ │ │ └── schemas.ts │ │ ├── api/ │ │ │ ├── auth/ │ │ │ │ └── [slug]/ │ │ │ │ └── route.ts │ │ │ └── cognito-token/ │ │ │ └── route.ts │ │ ├── auth-callback/ │ │ │ └── page.tsx │ │ ├── globals.css │ │ ├── layout.tsx │ │ └── sign-in/ │ │ └── page.tsx │ ├── components/ │ │ ├── Header.tsx │ │ └── ui/ │ │ └── sonner.tsx │ ├── hooks/ │ │ └── use-event-bus.ts │ ├── jobs/ │ │ ├── async-job/ │ │ │ └── translate.ts │ │ ├── async-job-runner.ts │ │ └── migration-runner.ts │ ├── lib/ │ │ ├── amplifyServerUtils.ts │ │ ├── auth.ts │ │ ├── events.ts │ │ ├── jobs.ts │ │ ├── prisma.ts │ │ ├── safe-action.ts │ │ └── utils.ts │ └── proxy.ts └── tsconfig.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/workflows/build.yml ================================================ permissions: contents: read name: Build on: push: branches: - main workflow_dispatch: pull_request: jobs: Build-and-Test-CDK: runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - name: Use Node.js uses: actions/setup-node@v6 with: node-version: '22.x' - run: | npm ci npm run format:check working-directory: ./cdk name: Install dependencies and run static analysis - run: | npm run build npm run test working-directory: ./cdk name: build and test Build-and-Test-Webapp: runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - name: Use Node.js uses: actions/setup-node@v6 with: node-version: '22.x' - run: | npm ci npm run format:check working-directory: ./webapp name: Install dependencies and run static analysis - run: | cp .env.local.example .env.local npm run build working-directory: ./webapp name: build ================================================ FILE: .github/workflows/commitlint.yml ================================================ name: PR Title on: pull_request_target: types: [opened, edited, synchronize] permissions: pull-requests: read jobs: lint: runs-on: ubuntu-latest steps: - uses: amannn/action-semantic-pull-request@v6 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} ================================================ FILE: .github/workflows/needs-triage.yml ================================================ name: Add needs-triage label on: issues: types: [opened] permissions: issues: write jobs: add-label: if: ${{ !endsWith(github.event.issue.user.login, '[bot]') }} runs-on: ubuntu-latest steps: - uses: actions/github-script@v7 with: script: | await github.rest.issues.addLabels({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number, labels: ['needs-triage'] }); ================================================ FILE: .github/workflows/release-please.yml ================================================ on: push: branches: - main permissions: contents: write pull-requests: write name: release-please jobs: release-please: runs-on: ubuntu-latest steps: - uses: googleapis/release-please-action@v4 ================================================ FILE: .github/workflows/stale.yml ================================================ name: Close stale issues and PRs on: schedule: - cron: '0 0 * * *' permissions: issues: write pull-requests: write jobs: stale: runs-on: ubuntu-latest steps: - uses: actions/stale@v9 with: days-before-stale: 60 days-before-close: 14 stale-issue-label: stale stale-pr-label: stale stale-issue-message: > This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs within 14 days. stale-pr-message: > This PR has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs within 14 days. exempt-issue-labels: 'priority: high' exempt-pr-labels: 'priority: high' ================================================ FILE: .github/workflows/update_snapshot.yml ================================================ name: Update snapshot on: workflow_dispatch: push: branches: - 'dependabot/**' issue_comment: types: [created] permissions: contents: write jobs: update: if: > github.event_name == 'push' || github.event_name == 'workflow_dispatch' || (github.event.issue.pull_request && contains(github.event.comment.body, '/update-snapshot')) runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 with: ref: ${{ github.event_name == 'issue_comment' && format('refs/pull/{0}/head', github.event.issue.number) || '' }} - name: Use Node.js uses: actions/setup-node@v6 with: node-version: "22.x" - run: | npm ci npm run test -- -u working-directory: ./cdk - name: Add & Commit uses: EndBug/add-and-commit@v7.2.0 with: add: "cdk/test/__snapshots__/." message: "update snapshot" ================================================ FILE: .prettierrc ================================================ { "trailingComma": "all", "tabWidth": 2, "semi": true, "singleQuote": true, "printWidth": 120 } ================================================ FILE: .release-please-manifest.json ================================================ { ".": "2.1.0" } ================================================ FILE: .serverless-full-stack-webapp-starter-kit/design/DESIGN_PRINCIPLES.md ================================================ # Design Principles This document is for kit maintainers. If you copied this kit to build your own app, you can safely delete the `.serverless-full-stack-webapp-starter-kit/` directory. ## What this kit is A template you **copy** (not fork) and grow into your own app. It is not a framework — users are expected to read, understand, and modify every file. ## Quality standards As an aws-samples project: - Correctness is the top priority — users learn patterns from this code. - Reproducibility — following the README must produce a working deployment. - Readability — code should be understandable by developers new to serverless. - One-command deploy — `npx cdk deploy --all` must be the only deployment step. The litmus test for any PR: "After this merges, will a developer who copies the kit build their app on a correct understanding?" ## Design decisions ### Template, not framework - Breaking changes have low impact — users copy and diverge. Major versions can be bumped without a lengthy deprecation cycle. - The sample app exists solely to prove all kit components work together. Users will delete it. Do not expand sample app features beyond what is needed for this proof. - Avoid over-abstraction. Readability and modifiability matter more than DRY. ### What to include - Patterns that every serverless full-stack webapp needs (auth, DB, async jobs, real-time). - Operational essentials (migration, logging, cost-optimized defaults). - Only what cannot be trivially added later. ### What to exclude - App-specific business logic. - Dependencies on specific AI models or services. - Patterns needed by fewer than half of expected users. ### Technology choices | Choice | Rationale | |--------|-----------| | `prisma db push` over `prisma migrate` | Simpler default for starter-kit scope. Users can switch to `prisma migrate` when they need migration history. | | NAT Instance over NAT Gateway | ~$30/month savings. Acceptable trade-off for a starter kit. | | Single Lambda for all async jobs | Reduces cold starts and simplifies deployment. `cmd` parameter selects the entry point. | | `proxy.ts` over Next.js middleware | Runs inside Lambda handler, avoiding cold-start CPU starvation from JWKS fetch in middleware. | | `output: 'standalone'` | Required for Lambda deployment via Docker image. | | Lambda Web Adapter | Enables response streaming with CloudFront + Lambda Function URL. | ### Architecture Decision Records (ADR) ADRs are not used yet. Introduce `design/adr/` when a major technology decision is made (e.g., ORM migration, database engine change) that requires recording the context, alternatives considered, and rationale. ### Migration guides When a breaking change is introduced, write a migration guide alongside the ADR. Place it in `docs/migration/` (e.g., `docs/migration/v3-migration-prompt.md`). A migration guide is not a complete step-by-step procedure for users. It is an input for an AI coding agent — a meta-prompt that the agent reads, compares against the user's codebase, and uses to build a project-specific migration plan. Write it with that consumer in mind: describe what changed, why, and what patterns in user code are affected, rather than prescribing exact commands. To surface the guide in release notes, include a link in the `BREAKING CHANGE:` commit footer: ``` feat!: replace ORM from Prisma to Drizzle BREAKING CHANGE: ORM has been replaced. See [migration guide](docs/migration/v3-migration-prompt.md) for details. ``` release-please will carry this into the Breaking Changes section of the GitHub Release. ================================================ FILE: AGENTS.md ================================================ # AGENTS.md ## Commands ```bash # webapp cd webapp && npm ci cd webapp && npm run dev # starts on port 3010 cd webapp && npm run build cd webapp && npm run lint cd webapp && npm run format # cdk cd cdk && npm ci cd cdk && npm run build cd cdk && npm test cd cdk && npm run format cd cdk && npx cdk deploy --all cd cdk && npx cdk diff # local development (requires Docker) docker compose up -d # PostgreSQL on port 5432 cd webapp && npx prisma db push # sync schema to local DB cd webapp && npm run dev ``` ## Development guide ### Authentication All server-side mutations must go through `authActionClient` (defined in `lib/safe-action.ts`). It validates the Cognito session via Amplify server-side auth and injects `ctx.userId`. Never call Prisma directly from a Server Action without this middleware. `proxy.ts` handles route protection (redirect to `/sign-in` for unauthenticated users). It is NOT a Next.js middleware file — it runs inside the Lambda handler. There is no `middleware.ts` in this project. ### Async jobs The dispatch flow is: Server Action → `runJob()` (Lambda async invoke) → `async-job-runner.ts` (discriminated union dispatch) → job handler → `sendEvent()` (AppSync Events) → client `useEventBus` hook. To add a new job: 1. Add a Zod schema with a `type` literal to the discriminated union in `async-job-runner.ts` 2. Implement the handler in `src/jobs/async-job/` 3. Add the case to the switch statement All job types share a single Lambda function via `job.Dockerfile`. The CDK `cmd` parameter selects the entry point. ### Database migration `prisma db push` is used for schema sync by default. The migration runner Lambda is invoked automatically during `cdk deploy` via CDK Trigger. For manual invocation, use the `MigrationCommand` from CDK outputs. Schema changes: edit `prisma/schema.prisma` → run `npx prisma db push` locally → commit. The `zod-prisma-types` generator auto-generates Zod schemas from the Prisma schema. If you switch to `prisma migrate`, update the migration runner accordingly. ### Lambda environment The webapp runs on Lambda behind CloudFront via Lambda Web Adapter (response streaming). `next.config.ts` uses `output: 'standalone'`. Build-time env vars (prefixed `NEXT_PUBLIC_`) are injected via CDK `ContainerImageBuild` build args — they cannot be changed at runtime. ### Real-time notifications Server → client push uses AppSync Events. Server-side: `sendEvent(channelName, payload)` with IAM SigV4 signing. Client-side: `useEventBus` hook with Cognito user pool auth. The channel namespace is `event-bus/`. ## Documentation policy - Do not document what can be derived from code. An agent can read the codebase. - Enforce verifiable constraints with tests and linters, not prose. - Code comments explain "why not" only — the non-obvious reason something was done a certain way. - Before adding a line to this file, ask: "If I remove this line, will an agent make a mistake?" If no, don't add it. If a root cause is fixed, remove the corresponding line. ## Conventions - PR titles and code comments in English. - Issues and discussions in English or Japanese. - PR titles follow [Conventional Commits](https://www.conventionalcommits.org/) (`feat:`, `fix:`, `chore:`, etc.). - UI components: use [shadcn/ui](https://ui.shadcn.com/). Do not introduce alternative component libraries. - Logs: use JSON structured output. - Dependencies: esbuild and Next.js bundle everything, so only packages with native binaries needed at Lambda runtime belong in `dependencies`. Everything else goes in `devDependencies`. ## Do not - Do not bypass `authActionClient` for any mutation. No raw Prisma calls from Server Actions. - Do not add `middleware.ts`. Route protection is handled by `proxy.ts` inside the Lambda runtime. - Do not use `prisma migrate` commands unless you have explicitly switched from `prisma db push`. The default setup uses `prisma db push`. - Do not hardcode AWS region or account IDs. Use CDK context or environment variables. - Do not add `NEXT_PUBLIC_` env vars to `.env.local` for deployed builds — they must be set as CDK build args in `webapp.ts`. ================================================ FILE: CHANGELOG.md ================================================ # Changelog ## [2.1.0](https://github.com/aws-samples/serverless-full-stack-webapp-starter-kit/compare/v2.0.0...v2.1.0) (2026-03-22) ### Features * add /update-snapshot comment trigger to update_snapshot workflow ([764a4fa](https://github.com/aws-samples/serverless-full-stack-webapp-starter-kit/commit/764a4fa0808b7fb11307f393208449588daa8b3c)) * add CloudWatch LogGroup with retention policy to Lambda functions ([#117](https://github.com/aws-samples/serverless-full-stack-webapp-starter-kit/issues/117)) ([53877bb](https://github.com/aws-samples/serverless-full-stack-webapp-starter-kit/commit/53877bb31b5af7cfbb5e80903be076e8ce1c38d6)), closes [#103](https://github.com/aws-samples/serverless-full-stack-webapp-starter-kit/issues/103) * **database:** enable Data API and connection logging ([#123](https://github.com/aws-samples/serverless-full-stack-webapp-starter-kit/issues/123)) ([e32dc7a](https://github.com/aws-samples/serverless-full-stack-webapp-starter-kit/commit/e32dc7ad5ceb0c36fd287c18e64177e92f0c5ff0)) * increase webapp Lambda memory from 512MB to 1024MB ([#116](https://github.com/aws-samples/serverless-full-stack-webapp-starter-kit/issues/116)) ([03c5a00](https://github.com/aws-samples/serverless-full-stack-webapp-starter-kit/commit/03c5a007e141c25b9631a3b38680f62dfe22320f)), closes [#101](https://github.com/aws-samples/serverless-full-stack-webapp-starter-kit/issues/101) ### Bug Fixes * add lambda:InvokeFunction permission for CloudFront OAC ([#83](https://github.com/aws-samples/serverless-full-stack-webapp-starter-kit/issues/83)) ([3cc66bf](https://github.com/aws-samples/serverless-full-stack-webapp-starter-kit/commit/3cc66bfb775f3a086f231783955b258136ddd266)) * **auth:** improve auth error handling and fix Link CORS issue ([#120](https://github.com/aws-samples/serverless-full-stack-webapp-starter-kit/issues/120)) ([84be605](https://github.com/aws-samples/serverless-full-stack-webapp-starter-kit/commit/84be6057d7416c7dd34a7eec3422144bce2f964c)) * disable Cognito self sign-up by default ([#115](https://github.com/aws-samples/serverless-full-stack-webapp-starter-kit/issues/115)) ([9396e6f](https://github.com/aws-samples/serverless-full-stack-webapp-starter-kit/commit/9396e6fbefe2feadb5e2eea4ccc03aa2a1c0888e)), closes [#106](https://github.com/aws-samples/serverless-full-stack-webapp-starter-kit/issues/106) * prevent CloudFront cache poisoning for Next.js RSC responses ([#119](https://github.com/aws-samples/serverless-full-stack-webapp-starter-kit/issues/119)) ([70cddda](https://github.com/aws-samples/serverless-full-stack-webapp-starter-kit/commit/70cddda904ee816a763c53a7a36ce1ea183ff941)) * **prisma:** add retry for Aurora Serverless v2 connection errors ([#121](https://github.com/aws-samples/serverless-full-stack-webapp-starter-kit/issues/121)) ([7c05dfb](https://github.com/aws-samples/serverless-full-stack-webapp-starter-kit/commit/7c05dfb61949caf8b8f79a56ec7c2c1e88a04839)) * support Amazon Linux 2023 for NAT instance ([#81](https://github.com/aws-samples/serverless-full-stack-webapp-starter-kit/issues/81)) ([0c41aa8](https://github.com/aws-samples/serverless-full-stack-webapp-starter-kit/commit/0c41aa8ec1946883f397126c8e6ae91c0e96b1b0)) ## 2.0.0 (2026-03-18) ### Features * invalidate cloudfront caches when lambda configuration changes ([#63](https://github.com/aws-samples/serverless-full-stack-webapp-starter-kit/issues/63)) ([b76d122](https://github.com/aws-samples/serverless-full-stack-webapp-starter-kit/commit/b76d1223d1e00a6b77ee5f89b4d7f9678b01232a)) ### Bug Fixes * pass useNatInstance prop to MainStack ([#71](https://github.com/aws-samples/serverless-full-stack-webapp-starter-kit/issues/71)) ([23c9e31](https://github.com/aws-samples/serverless-full-stack-webapp-starter-kit/commit/23c9e31a73d44254cf45d84015bc6cd9c045880b)) * Workflow does not contain permissions ([#59](https://github.com/aws-samples/serverless-full-stack-webapp-starter-kit/issues/59)) ([34be1de](https://github.com/aws-samples/serverless-full-stack-webapp-starter-kit/commit/34be1deaf7e02320e097861b989e1e046bfa8488)) * Workflow does not contain permissions ([#60](https://github.com/aws-samples/serverless-full-stack-webapp-starter-kit/issues/60)) ([6fb8901](https://github.com/aws-samples/serverless-full-stack-webapp-starter-kit/commit/6fb89018d26428c069679a1443e2010cb4bb4fc5)) ================================================ FILE: CODE_OF_CONDUCT.md ================================================ ## Code of Conduct This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact opensource-codeofconduct@amazon.com with any additional questions or comments. ================================================ FILE: CONTRIBUTING.md ================================================ # Contributing Guidelines Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional documentation, we greatly value feedback and contributions from our community. Please read through this document before submitting any issues or pull requests to ensure we have all the necessary information to effectively respond to your bug report or contribution. ## Language policy - PR titles and code comments: English - Issues and discussions: English or Japanese ## Commit and PR conventions PR titles follow [Conventional Commits](https://www.conventionalcommits.org/) (Angular style). Enforced by CI. Releases are automated via [Release Please](https://github.com/googleapis/release-please). ## Reporting Bugs/Feature Requests We welcome you to use the GitHub issue tracker to report bugs or suggest features. When filing an issue, please check existing open, or recently closed, issues to make sure somebody else hasn't already reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: * A reproducible test case or series of steps * The version of our code being used * Any modifications you've made relevant to the bug * Anything unusual about your environment or deployment ## Contributing via Pull Requests Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: 1. You are working against the latest source on the *main* branch. 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. 3. You open an issue to discuss any significant work - we would hate for your time to be wasted. To send us a pull request, please: 1. Fork the repository. 2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. 3. Ensure local tests pass. 4. Commit to your fork using clear commit messages. 5. Send us a pull request, answering any default questions in the pull request interface. 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). ## Finding contributions to work on Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any 'help wanted' issues is a great place to start. ## Code of Conduct This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact opensource-codeofconduct@amazon.com with any additional questions or comments. ## Security issue notifications If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. ## Licensing See the [LICENSE](LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. ================================================ FILE: LICENSE ================================================ Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # Serverless Full Stack WebApp Starter Kit [![Build](https://github.com/aws-samples/serverless-full-stack-webapp-starter-kit/actions/workflows/build.yml/badge.svg)](https://github.com/aws-samples/serverless-full-stack-webapp-starter-kit/actions/workflows/build.yml) [![Release](https://img.shields.io/github/v/release/aws-samples/serverless-full-stack-webapp-starter-kit)](https://github.com/aws-samples/serverless-full-stack-webapp-starter-kit/releases) A serverless full-stack web app template you **copy and grow into your own app**. Not a framework — you own every file. Copy, deploy with a single command, then replace the sample todo app with your own features. ## What you get 1. **Working sample app** — A todo app with authentication, DB CRUD, async jobs, and real-time notifications wired end-to-end. Designed as a readable reference for AI coding agents and humans alike. 2. **End-to-end type safety** — Types flow from Prisma ORM through Zod schemas and Server Actions to React components in a single chain. 3. **Serverless from day one** — Fully serverless architecture starting under $10/month that scales without operational overhead. 4. **Integrated DB migration** — Schema migration is integrated into the CDK deploy process via CDK Trigger, providing a development-to-production path out of the box. You can refer to [the blog article](https://tmokmss.github.io/blog/posts/serverless-fullstack-webapp-architecture-2025/) for more details (also [Japanese version](https://tmokmss.hatenablog.com/entry/serverless-fullstack-webapp-architecture-2025)). ## Sample app The kit includes a simple todo app to demonstrate how all components work together. Sign in/up page redirects to Cognito Managed Login.
  After login, you can add, delete, and manage your todo items. The translate button triggers an async job and pushes a real-time notification to refresh the page.
## Architecture ![architecture](./.serverless-full-stack-webapp-starter-kit/docs/imgs/architecture.png) | Service | Role | |---------|------| | [Aurora PostgreSQL Serverless v2](https://aws.amazon.com/rds/aurora/serverless/) | Relational database with Prisma ORM | | [Next.js App Router](https://nextjs.org/docs/app) on [Lambda](https://aws.amazon.com/lambda/) | Unified frontend and backend | | [CloudFront](https://aws.amazon.com/cloudfront/) + Lambda Function URL | Content delivery with response streaming | | [Cognito](https://aws.amazon.com/cognito/) | Authentication (email by default, OIDC federation supported) | | [AppSync Events](https://docs.aws.amazon.com/appsync/latest/eventapi/event-api-welcome.html) + Lambda | Async jobs and real-time notifications | | [EventBridge](https://aws.amazon.com/eventbridge/) | Scheduled jobs | | [CloudWatch](https://aws.amazon.com/cloudwatch/) + S3 | Access logging | | [CDK](https://aws.amazon.com/cdk/) | Infrastructure as Code | Fully serverless — high cost efficiency, scalability, and minimal operational overhead. ## Getting started Prerequisites: * [Node.js](https://nodejs.org/) (>= v20) * [Docker](https://docs.docker.com/get-docker/) * [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html) with a configured IAM profile ### 1. Copy the kit Use the GitHub template ("Use this template" button) or clone and copy: ```sh git clone https://github.com/aws-samples/serverless-full-stack-webapp-starter-kit.git my-app cd my-app rm -rf .git && git init # Record the kit version in your initial commit for future reference git add -A && git commit -m "Initial commit from serverless-full-stack-webapp-starter-kit vX.Y.Z" ``` ### 2. Customize (optional) - Update the application name (stack name, tags) in [`cdk/bin/cdk.ts`](cdk/bin/cdk.ts) - Set a custom domain in `cdk/bin/cdk.ts` - Remove `cdk.context.json` from `cdk/.gitignore` and commit it (recommended for your own project) - Switch from `prisma db push` to `prisma migrate` if you need migration history ### 3. Deploy ```sh cd cdk npm ci npx cdk bootstrap npx cdk deploy --all ``` Initial deployment takes about 20 minutes. After success, you'll see: ``` ✅ ServerlessWebappStarterKitStack Outputs: ServerlessWebappStarterKitStack.FrontendDomainName = https://web.example.com ServerlessWebappStarterKitStack.DatabaseSecretsCommand = aws secretsmanager get-secret-value ... ServerlessWebappStarterKitStack.DatabasePortForwardCommand = aws ssm start-session ... ``` Open the `FrontendDomainName` URL to try the sample app. > **Note:** `DatabasePortForwardCommand` establishes a local connection to your RDS database, and `DatabaseSecretsCommand` retrieves database credentials from Secrets Manager. ### 4. Add your own features See [`AGENTS.md`](./AGENTS.md) for development guide — local development setup, authentication patterns, async job setup, DB migration, and coding conventions. To add social sign-in (Google, Facebook, etc.), see [Add social sign-in to a user pool](https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pools-configuring-federation-with-social-idp.html). ## Maintenance policy This kit follows [Semantic Versioning](https://semver.org/). Since users copy (not fork) this kit, breaking changes are introduced as new major versions without a lengthy deprecation cycle. ## Cost Sample cost breakdown for us-east-1, one month, with cost-optimized configuration: | Service | Usage Details | Monthly Cost [USD] | |---------|--------------|-------------------| | Aurora Serverless v2 | 0.5 ACU × 2 hour/day, 1GB storage | 3.6 | | Cognito | 100 MAU | 1.5 | | AppSync Events | 100 events/month, 10 hours connection/user/month | 0.02 | | Lambda | 1024MB × 200ms/request | 0.15 | | Lambda@Edge | 128MB × 50ms/request | 0.09 | | VPC | NAT Instance (t4g.nano) × 1 | 3.02 | | EventBridge | Scheduler 100 jobs/month | 0.0001 | | CloudFront | Data transfer 1kB/request | 0.01 | | **Total** | | **8.49** | Assumes 100 users/month, 1000 requests/user. Costs could be further reduced with [Free Tier](https://aws.amazon.com/free/). ## Clean up ```sh cd cdk npx cdk destroy --force ``` ## Maintainers * [Kenji Kono (konokenj)](https://github.com/konokenj) ### Core contributors * [Masashi Tomooka (tmokmss)](https://github.com/tmokmss) — original author * [Kazuho Cryer-Shinozuka (badmintoncryer)](https://github.com/badmintoncryer) ## Contributing See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines. Contributors (human and AI) **must** read [`.serverless-full-stack-webapp-starter-kit/design/DESIGN_PRINCIPLES.md`](./.serverless-full-stack-webapp-starter-kit/design/DESIGN_PRINCIPLES.md) before making changes. It defines the design decisions and constraints that govern this kit. ## Security See [CONTRIBUTING](CONTRIBUTING.md#security-issue-notifications) for more information. ## License This library is licensed under the MIT-0 License. See the LICENSE file. ================================================ FILE: cdk/.gitignore ================================================ *.js !jest.config.js !lib/constructs/cf-lambda-furl-service/cf-function/*.js *.d.ts node_modules # CDK asset staging directory .cdk.staging cdk.out # Ignoring this file only because it is an OSS sample solution. # It is generally recommended to store the cdk.context.json in git. cdk.context.json ================================================ FILE: cdk/README.md ================================================ # Infrastructure as Code (AWS CDK) This is the IaC project written in AWS Cloud Development Kit (CDK). ## Useful commands * `npx cdk deploy`: deploy the infrastructure * `npx cdk deploy --require-approval never`: deploy without confirmation (useful for automation) * `npx cdk deploy --hotswap`: deploy with hotswap feature enabled (useful for development) * `npx cdk watch`: watch your code and deploy every time changes are detected * `npx cdk destroy`: delete the infrastructure ================================================ FILE: cdk/bin/cdk.ts ================================================ #!/usr/bin/env node import 'source-map-support/register'; import * as cdk from 'aws-cdk-lib'; import { MainStack } from '../lib/main-stack'; import { UsEast1Stack } from '../lib/us-east-1-stack'; const app = new cdk.App(); interface EnvironmentProps { account: string; /** * Custom domain name for the webapp and Cognito. * You need to have a public Route53 hosted zone for the domain name in your AWS account. * * @default No custom domain name. When not specified, the stack automatically generates * a random prefix for the Cognito domain (e.g., webapp-abc123def4.auth.us-west-2.amazoncognito.com) * and uses the CloudFront default domain (e.g., d1234567890.cloudfront.net) for the webapp. */ domainName?: string; /** * Use a NAT instance instead of NAT Gateways. * @default true */ useNatInstance?: boolean; } const props: EnvironmentProps = { account: process.env.CDK_DEFAULT_ACCOUNT!, // domainName: 'FIXME.example.com', useNatInstance: true, }; const virginia = new UsEast1Stack(app, 'ServerlessWebappStarterKitUsEast1Stack', { env: { account: props.account, region: 'us-east-1', }, crossRegionReferences: true, domainName: props.domainName, }); new MainStack(app, 'ServerlessWebappStarterKitStack', { env: { account: props.account, region: process.env.CDK_DEFAULT_REGION, }, crossRegionReferences: true, sharedCertificate: virginia.certificate, domainName: props.domainName, useNatInstance: props.useNatInstance, signPayloadHandler: virginia.signPayloadHandler, }); cdk.Tags.of(app).add('Application', 'ServerlessFullStackWebappStarterKit'); // import { Aspects } from 'aws-cdk-lib'; // import { AwsSolutionsChecks } from 'cdk-nag'; // Aspects.of(app).add(new AwsSolutionsChecks()); ================================================ FILE: cdk/cdk.json ================================================ { "app": "npx ts-node --prefer-ts-exts bin/cdk.ts", "watch": { "include": [ "**" ], "exclude": [ "README.md", "cdk*.json", "**/*.d.ts", "**/*.js", "tsconfig.json", "package*.json", "yarn.lock", "node_modules", "test" ] }, "context": { "@aws-cdk/aws-lambda:recognizeLayerVersion": true, "@aws-cdk/core:checkSecretUsage": true, "@aws-cdk/core:target-partitions": [ "aws", "aws-cn" ], "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true, "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true, "@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true, "@aws-cdk/aws-iam:minimizePolicies": true, "@aws-cdk/core:validateSnapshotRemovalPolicy": true, "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true, "@aws-cdk/aws-s3:createDefaultLoggingPolicy": true, "@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true, "@aws-cdk/aws-apigateway:disableCloudWatchRole": true, "@aws-cdk/core:enablePartitionLiterals": true, "@aws-cdk/aws-events:eventsTargetQueueSameAccount": true, "@aws-cdk/aws-iam:standardizedServicePrincipals": true, "@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true, "@aws-cdk/aws-iam:importedRoleStackSafeDefaultPolicyName": true, "@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true, "@aws-cdk/aws-route53-patters:useCertificate": true, "@aws-cdk/customresources:installLatestAwsSdkDefault": false, "@aws-cdk/aws-rds:databaseProxyUniqueResourceName": true, "@aws-cdk/aws-codedeploy:removeAlarmsFromDeploymentGroup": true, "@aws-cdk/aws-apigateway:authorizerChangeDeploymentLogicalId": true, "@aws-cdk/aws-ec2:launchTemplateDefaultUserData": true, "@aws-cdk/aws-secretsmanager:useAttachedSecretResourcePolicyForSecretTargetAttachments": true, "@aws-cdk/aws-redshift:columnId": true, "@aws-cdk/aws-stepfunctions-tasks:enableEmrServicePolicyV2": true } } ================================================ FILE: cdk/jest.config.js ================================================ module.exports = { testEnvironment: 'node', roots: ['/test'], testMatch: ['**/*.test.ts'], transform: { '^.+\\.tsx?$': 'ts-jest' }, snapshotSerializers: ['/test/snapshot-plugin.ts'] }; ================================================ FILE: cdk/lib/constructs/async-job.ts ================================================ import { Construct } from 'constructs'; import { CfnOutput, Duration, RemovalPolicy, TimeZone } from 'aws-cdk-lib'; import { LogGroup, RetentionDays } from 'aws-cdk-lib/aws-logs'; import { Architecture, DockerImageCode, DockerImageFunction, IFunction } from 'aws-cdk-lib/aws-lambda'; import { Platform } from 'aws-cdk-lib/aws-ecr-assets'; import { Database } from './database'; import { EventBus } from './event-bus'; import { PolicyStatement } from 'aws-cdk-lib/aws-iam'; import { join } from 'path'; import { Schedule, ScheduleExpression, ScheduleTargetInput } from 'aws-cdk-lib/aws-scheduler'; import { LambdaInvoke } from 'aws-cdk-lib/aws-scheduler-targets'; export interface AsyncJobProps { readonly database: Database; readonly eventBus: EventBus; } export class AsyncJob extends Construct { readonly handler: IFunction; constructor(scope: Construct, id: string, props: AsyncJobProps) { super(scope, id); const { database, eventBus } = props; const handler = new DockerImageFunction(this, 'Handler', { code: DockerImageCode.fromImageAsset(join('..', 'webapp'), { cmd: ['async-job-runner.handler'], platform: Platform.LINUX_ARM64, file: 'job.Dockerfile', }), memorySize: 256, timeout: Duration.minutes(10), architecture: Architecture.ARM_64, environment: { ...database.getLambdaEnvironment('main'), EVENT_HTTP_ENDPOINT: eventBus.httpEndpoint, }, vpc: database.cluster.vpc, // limit concurrency to mitigate any possible EDoS attacks reservedConcurrentExecutions: 1, logGroup: new LogGroup(this, 'HandlerLogs', { retention: RetentionDays.ONE_WEEK, removalPolicy: RemovalPolicy.DESTROY, }), }); handler.connections.allowToDefaultPort(database); eventBus.api.grantPublish(handler); handler.addToRolePolicy( new PolicyStatement({ actions: ['translate:TranslateText', 'comprehend:DetectDominantLanguage'], resources: ['*'], }), ); new CfnOutput(this, 'HandlerArn', { value: handler.functionArn }); this.handler = handler; // you can add scheduled jobs here. this.addSchedule( 'SampleJob', ScheduleExpression.cron({ minute: '0', hour: '0', day: '1', timeZone: TimeZone.ETC_UTC }), ); } public addSchedule(jobType: string, schedule: ScheduleExpression, payload?: any) { return new Schedule(this, jobType, { schedule, target: new LambdaInvoke(this.handler, { input: ScheduleTargetInput.fromObject({ jobType, payload }), retryAttempts: 5, }), }); } } ================================================ FILE: cdk/lib/constructs/auth/.gitignore ================================================ !prefix-generator.js ================================================ FILE: cdk/lib/constructs/auth/index.ts ================================================ import { UpdateUserPoolClientCommandInput } from '@aws-sdk/client-cognito-identity-provider'; import { CfnOutput, CfnResource, CustomResource, Duration, RemovalPolicy, Stack } from 'aws-cdk-lib'; import { ICertificate } from 'aws-cdk-lib/aws-certificatemanager'; import { CfnManagedLoginBranding, ManagedLoginVersion, UserPool, UserPoolClient } from 'aws-cdk-lib/aws-cognito'; import { Code, Runtime, SingletonFunction } from 'aws-cdk-lib/aws-lambda'; import { CnameRecord, IHostedZone } from 'aws-cdk-lib/aws-route53'; import { AwsCustomResource, AwsCustomResourcePolicy, PhysicalResourceId } from 'aws-cdk-lib/custom-resources'; import { Construct } from 'constructs'; import { readFileSync } from 'fs'; import { join } from 'path'; export interface AuthProps { /** * Route 53 hosted zone for custom domain. * * @default No custom domain. A random prefix will be automatically generated for the Cognito domain. */ readonly hostedZone?: IHostedZone; /** * ACM certificate for custom domain (must be in us-east-1 for Cognito). * * @default No custom domain. */ readonly sharedCertificate?: ICertificate; } export class Auth extends Construct { readonly userPool: UserPool; readonly client: UserPoolClient; readonly domainName: string; private callbackUrlCount = 0; constructor(scope: Construct, id: string, props: AuthProps) { super(scope, id); const { hostedZone } = props; const subDomain = 'auth'; let domainPrefix = ''; if (!hostedZone) { // When we do not use a custom domain, we must make domainPrefix unique in the AWS region. // To avoid a collision, we generate a random string with CFn custom resource. // This allows the stack to work without requiring a custom domain setup. const generator = new SingletonFunction(this, 'RandomStringGenerator', { runtime: Runtime.NODEJS_22_X, handler: 'index.handler', timeout: Duration.seconds(5), lambdaPurpose: 'RandomStringGenerator', uuid: '11e9c903-f11a-4989-833c-985dddef5eb2', code: Code.fromInline(readFileSync(join(__dirname, 'prefix-generator.js')).toString()), }); const domainPrefixResource = new CustomResource(this, 'DomainPrefix', { serviceToken: generator.functionArn, resourceType: 'Custom::RandomString', properties: { prefix: 'webapp-', length: 10 }, serviceTimeout: Duration.seconds(10), }); domainPrefix = domainPrefixResource.getAttString('generated'); } this.domainName = hostedZone ? `${subDomain}.${hostedZone.zoneName}` : `${domainPrefix}.auth.${Stack.of(this).region}.amazoncognito.com`; const userPool = new UserPool(this, 'UserPool', { passwordPolicy: { requireUppercase: true, requireSymbols: true, requireDigits: true, minLength: 8, }, // Set to true to allow self sign-up. // When false, administrators must create users via the Cognito console or API. selfSignUpEnabled: false, signInAliases: { username: false, email: true, }, removalPolicy: RemovalPolicy.DESTROY, }); const client = userPool.addClient(`Client`, { idTokenValidity: Duration.days(1), authFlows: { userPassword: true, userSrp: true, }, oAuth: { flows: { authorizationCodeGrant: true, }, callbackUrls: ['http://localhost/dummy'], logoutUrls: ['http://localhost/dummy'], }, }); this.client = client; this.userPool = userPool; const domain = userPool.addDomain('CognitoDomain', { ...(hostedZone && props.sharedCertificate ? { customDomain: { domainName: this.domainName, certificate: props.sharedCertificate, }, } : { cognitoDomain: { domainPrefix, }, }), managedLoginVersion: ManagedLoginVersion.NEWER_MANAGED_LOGIN, }); if (hostedZone) { new CnameRecord(this, 'CognitoDomainRecord', { zone: hostedZone, recordName: subDomain, domainName: domain.cloudFrontEndpoint, }); } new CfnManagedLoginBranding(this, 'Branding', { userPoolId: this.userPool.userPoolId, clientId: client.userPoolClientId, useCognitoProvidedValues: true, }); new CfnOutput(this, 'UserPoolId', { value: userPool.userPoolId }); new CfnOutput(this, 'UserPoolClientId', { value: client.userPoolClientId }); new CfnOutput(this, 'UserPoolDomainName', { value: this.domainName }); } public addAllowedCallbackUrls(callbackUrl: string, logoutUrl: string) { const resource = this.client.node.defaultChild; if (!CfnResource.isCfnResource(resource)) { throw new Error('Expected CfnResource'); } resource.addPropertyOverride(`CallbackURLs.${this.callbackUrlCount}`, callbackUrl); resource.addPropertyOverride(`LogoutURLs.${this.callbackUrlCount}`, logoutUrl); this.callbackUrlCount += 1; } public updateAllowedCallbackUrls(callbackUrls: string[], logoutUrls: string[]) { // Lambda depends on userPoolClientId but userPoolClient depends on the CloudFront domain name (callback URL) which depends on Lambda (fURL). // To avoid the circular dependency, we update the callback URL after a userPoolClientId is created. // We only use this when custom domain is not used. new AwsCustomResource(this, 'UpdateCallbackUrls', { onUpdate: { service: '@aws-sdk/client-cognito-identity-provider', action: 'updateUserPoolClient', parameters: { ClientId: this.client.userPoolClientId, UserPoolId: this.userPool.userPoolId, AllowedOAuthFlows: ['code'], AllowedOAuthFlowsUserPoolClient: true, AllowedOAuthScopes: ['profile', 'phone', 'email', 'openid', 'aws.cognito.signin.user.admin'], ExplicitAuthFlows: ['ALLOW_USER_PASSWORD_AUTH', 'ALLOW_USER_SRP_AUTH', 'ALLOW_REFRESH_TOKEN_AUTH'], CallbackURLs: callbackUrls, LogoutURLs: logoutUrls, SupportedIdentityProviders: ['COGNITO'], TokenValidityUnits: { IdToken: 'minutes', }, IdTokenValidity: 1440, } satisfies UpdateUserPoolClientCommandInput, physicalResourceId: PhysicalResourceId.of(this.userPool.userPoolId), }, policy: AwsCustomResourcePolicy.fromSdkCalls({ resources: [this.userPool.userPoolArn], }), }); } } ================================================ FILE: cdk/lib/constructs/auth/prefix-generator.js ================================================ const response = require('cfn-response'); const crypto = require('crypto'); exports.handler = async function (event, context) { try { console.log(event); if (event.RequestType == 'Delete') { return await response.send(event, context, response.SUCCESS); } const prefix = event.ResourceProperties.prefix ?? ''; const length = event.ResourceProperties.length ?? '8'; const generate = () => { const random = crypto.randomBytes(parseInt(length)).toString('hex'); return `${prefix}${random.slice(0, length)}`; }; if (event.RequestType == 'Create') { const generated = generate(); return await response.send(event, context, response.SUCCESS, { generated }, generated); } if (event.RequestType == 'Update') { const current = event.PhysicalResourceId; if (current.startsWith(prefix)) { return await response.send(event, context, response.SUCCESS, { generated: current }, current); } const generated = generate(); return await response.send(event, context, response.SUCCESS, { generated }, generated); } } catch (e) { console.log(e); await response.send(event, context, response.FAILED); } }; ================================================ FILE: cdk/lib/constructs/cf-lambda-furl-service/edge-function.ts ================================================ import { Stack } from 'aws-cdk-lib'; import { PolicyStatement, ServicePrincipal, Role } from 'aws-cdk-lib/aws-iam'; import { Runtime, IVersion, Version, Architecture } from 'aws-cdk-lib/aws-lambda'; import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs'; import { StringParameter } from 'aws-cdk-lib/aws-ssm'; import { AwsCustomResource, PhysicalResourceId, AwsCustomResourcePolicy } from 'aws-cdk-lib/custom-resources'; import { Construct } from 'constructs'; // L@E can only be deployed to us-east-1 region. const StackRegion = 'us-east-1'; export interface EdgeFunctionProps { entryPath: string; } export class EdgeFunction extends Construct { private readonly functionVersionParameter: StringParameter; constructor(scope: Construct, id: string, props: EdgeFunctionProps) { super(scope, id); if (Stack.of(this).region !== StackRegion) { throw new Error('EdgeFunction can only be deployed to us-east-1 region.'); } const handler = new NodejsFunction(this, 'Handler', { runtime: Runtime.NODEJS_22_X, entry: props.entryPath, }); handler.currentVersion; this.functionVersionParameter = new StringParameter(this, 'FunctionVersion', { stringValue: handler.currentVersion.edgeArn, }); const statement = new PolicyStatement(); const edgeLambdaServicePrincipal = new ServicePrincipal('edgelambda.amazonaws.com'); statement.addPrincipals(edgeLambdaServicePrincipal); statement.addActions(edgeLambdaServicePrincipal.assumeRoleAction); (handler.role as Role).assumeRolePolicy!.addStatements(statement); } public versionArn(scope: Construct) { const id = `VersionArn${this.functionVersionParameter.node.addr}`; const existing = Stack.of(scope).node.tryFindChild(id) as IVersion; if (existing) { return existing; } const lookup = new AwsCustomResource(Stack.of(scope), `Lookup${id}`, { onUpdate: { // will also be called for a CREATE event service: 'SSM', action: 'getParameter', parameters: { Name: this.functionVersionParameter.parameterName, }, // it is impossible to know when the parameter is updated. // so we need to get the value on every deployment. physicalResourceId: PhysicalResourceId.of(`${Date.now()}`), region: StackRegion, }, policy: AwsCustomResourcePolicy.fromSdkCalls({ resources: [this.functionVersionParameter.parameterArn], }), }); return Version.fromVersionArn(Stack.of(scope), id, lookup.getResponseField('Parameter.Value')); } } ================================================ FILE: cdk/lib/constructs/cf-lambda-furl-service/lambda/sign-payload.ts ================================================ import type { CloudFrontRequestHandler } from 'aws-lambda'; import { createHash } from 'crypto'; const hashPayload = (payload: Buffer) => { return createHash('sha256').update(payload).digest('hex'); }; export const handler: CloudFrontRequestHandler = async (event) => { const request = event.Records[0].cf.request; const body = request.body?.data ?? ''; const hashedBody = hashPayload(Buffer.from(body, 'base64')); request.headers['x-amz-content-sha256'] = [{ key: 'x-amz-content-sha256', value: hashedBody }]; // LWA replaces authorization2 to authorization again // if (request.headers['authorization'] != null) { // request.headers['authorization2'] = [{ key: 'authorization2', value: request.headers['authorization'][0].value }]; // delete request.headers['authorization']; // } return request; }; ================================================ FILE: cdk/lib/constructs/cf-lambda-furl-service/service.ts ================================================ import { Construct } from 'constructs'; import { Aws, Duration } from 'aws-cdk-lib'; import { FunctionUrlAuthType, Function, InvokeMode, CfnPermission } from 'aws-cdk-lib/aws-lambda'; import { AllowedMethods, CacheCookieBehavior, CacheHeaderBehavior, CachePolicy, CacheQueryStringBehavior, Distribution, Function as CfFunction, FunctionCode as CfFunctionCode, FunctionEventType, FunctionRuntime, LambdaEdgeEventType, OriginRequestPolicy, SecurityPolicyProtocol, } from 'aws-cdk-lib/aws-cloudfront'; import { FunctionUrlOrigin } from 'aws-cdk-lib/aws-cloudfront-origins'; import * as path from 'path'; import { StringParameter } from 'aws-cdk-lib/aws-ssm'; import { ARecord, IHostedZone, RecordTarget } from 'aws-cdk-lib/aws-route53'; import { CloudFrontTarget } from 'aws-cdk-lib/aws-route53-targets'; import { ICertificate } from 'aws-cdk-lib/aws-certificatemanager'; import { Bucket } from 'aws-cdk-lib/aws-s3'; import { EdgeFunction } from './edge-function'; import { AwsCustomResource, PhysicalResourceId, AwsCustomResourcePolicy } from 'aws-cdk-lib/custom-resources'; export interface CloudFrontLambdaFunctionUrlServiceProps { /** * Subdomain name for the service. If not specified, the root domain will be used. * * @default use root domain */ subDomain?: string; handler: Function; /** * This should be unique across the app */ serviceName: string; /** * @default basic auth is disabled */ basicAuthUsername?: string; basicAuthPassword?: string; /** * Route 53 hosted zone for custom domain. * * @default No custom domain. CloudFront's default domain will be used. */ hostedZone?: IHostedZone; /** * ACM certificate for custom domain (must be in us-east-1 for CloudFront). * * @default No custom domain. */ certificate?: ICertificate; signPayloadHandler: EdgeFunction; accessLogBucket: Bucket; } export class CloudFrontLambdaFunctionUrlService extends Construct { public readonly urlParameter: StringParameter; public readonly url: string; public readonly domainName: string; constructor(scope: Construct, id: string, props: CloudFrontLambdaFunctionUrlServiceProps) { super(scope, id); const { handler, serviceName, subDomain, hostedZone, certificate, accessLogBucket, signPayloadHandler } = props; let domainName = ''; if (hostedZone) { domainName = subDomain ? `${subDomain}.${hostedZone.zoneName}` : hostedZone.zoneName; } const furl = handler.addFunctionUrl({ authType: FunctionUrlAuthType.AWS_IAM, invokeMode: InvokeMode.RESPONSE_STREAM, }); const origin = FunctionUrlOrigin.withOriginAccessControl(furl, { connectionTimeout: Duration.seconds(6), readTimeout: Duration.seconds(60), }); const cachePolicy = new CachePolicy(this, 'SharedCachePolicy', { queryStringBehavior: CacheQueryStringBehavior.all(), headerBehavior: CacheHeaderBehavior.allowList( // CachePolicy.USE_ORIGIN_CACHE_CONTROL_HEADERS_QUERY_STRINGS contains Host header here, // making it impossible to use with API Gateway 'authorization', 'Origin', 'X-HTTP-Method-Override', 'X-HTTP-Method', 'X-Method-Override', // Hashed Next.js RSC headers set by the CloudFront Function below. // See cf-function/cache-key.js for details. 'x-nextjs-cache-key', ), defaultTtl: Duration.seconds(0), cookieBehavior: CacheCookieBehavior.all(), enableAcceptEncodingBrotli: true, enableAcceptEncodingGzip: true, }); // CloudFront Function to hash Next.js RSC headers into a single cache key header. // This prevents cache poisoning between HTML and RSC flight responses while // staying within CloudFront's 10-header limit on Cache Policies. const cacheKeyFunction = new CfFunction(this, 'CacheKeyFunction', { runtime: FunctionRuntime.JS_2_0, code: CfFunctionCode.fromFile({ filePath: path.join(__dirname, 'cf-function', 'cache-key.js') }), }); const distribution = new Distribution(this, 'Resource', { comment: `CloudFront for ${serviceName}`, defaultBehavior: { origin, cachePolicy, allowedMethods: AllowedMethods.ALLOW_ALL, originRequestPolicy: OriginRequestPolicy.ALL_VIEWER_EXCEPT_HOST_HEADER, functionAssociations: [ { function: cacheKeyFunction, eventType: FunctionEventType.VIEWER_REQUEST, }, ], edgeLambdas: [ { functionVersion: signPayloadHandler.versionArn(this), eventType: LambdaEdgeEventType.ORIGIN_REQUEST, includeBody: true, }, ], }, // errorResponses: [{ httpStatus: 404, responsePagePath: '/', responseHttpStatus: 200 }], logBucket: accessLogBucket, logFilePrefix: `${serviceName}/`, ...(hostedZone ? { certificate: certificate, domainNames: [domainName] } : {}), minimumProtocolVersion: SecurityPolicyProtocol.TLS_V1_2_2021, }); // Starting October 2025, new function URLs require both lambda:InvokeFunctionUrl // and lambda:InvokeFunction permissions for CloudFront OAC. // CDK's FunctionUrlOrigin.withOriginAccessControl only adds lambda:InvokeFunctionUrl, // so we explicitly add lambda:InvokeFunction here. // See: https://docs.aws.amazon.com/lambda/latest/dg/urls-auth.html new CfnPermission(this, 'InvokeFunctionPermission', { action: 'lambda:InvokeFunction', functionName: handler.functionArn, principal: 'cloudfront.amazonaws.com', sourceArn: `arn:${Aws.PARTITION}:cloudfront::${Aws.ACCOUNT_ID}:distribution/${distribution.distributionId}`, }); if (hostedZone) { new ARecord(this, 'Record', { zone: hostedZone, target: RecordTarget.fromAlias(new CloudFrontTarget(distribution)), recordName: subDomain, }); } else { domainName = distribution.domainName; } // Invalidate CloudFront when Lambda function version changes new AwsCustomResource(this, 'CloudFrontInvalidation', { onUpdate: { service: 'cloudfront', action: 'createInvalidation', parameters: { DistributionId: distribution.distributionId, InvalidationBatch: { CallerReference: `${handler.currentVersion.version}`, Paths: { Quantity: 1, Items: ['/*'], }, }, }, physicalResourceId: PhysicalResourceId.of('invalidation'), }, policy: AwsCustomResourcePolicy.fromSdkCalls({ resources: [distribution.distributionArn], }), }); this.url = `https://${domainName}`; this.domainName = domainName; } } ================================================ FILE: cdk/lib/constructs/database.ts ================================================ import { CfnOutput, Stack, Token } from 'aws-cdk-lib'; import * as ec2 from 'aws-cdk-lib/aws-ec2'; import * as logs from 'aws-cdk-lib/aws-logs'; import * as rds from 'aws-cdk-lib/aws-rds'; import * as secretsmanager from 'aws-cdk-lib/aws-secretsmanager'; import { Construct } from 'constructs'; interface DatabaseProps { vpc: ec2.IVpc; } export class Database extends Construct implements ec2.IConnectable { readonly cluster: rds.DatabaseCluster; readonly secret: secretsmanager.ISecret; readonly connections: ec2.Connections; constructor(scope: Construct, id: string, props: DatabaseProps) { super(scope, id); const vpc = props.vpc; const engine = rds.DatabaseClusterEngine.auroraPostgres({ version: rds.AuroraPostgresEngineVersion.VER_16_6 }); const cluster = new rds.DatabaseCluster(this, 'Cluster', { engine, writer: rds.ClusterInstance.serverlessV2('Writer', { enablePerformanceInsights: true, autoMinorVersionUpgrade: true, }), serverlessV2MinCapacity: 0, vpc, vpcSubnets: vpc.selectSubnets({ subnets: vpc.isolatedSubnets.concat(vpc.privateSubnets) }), storageEncrypted: true, // Exclude some more special characters from password string to avoid from URI encoding issue // see: https://www.prisma.io/docs/orm/reference/connection-urls#special-characters credentials: rds.Credentials.fromUsername(engine.defaultUsername ?? 'admin', { excludeCharacters: ' %+~`#$&*()|[]{}:;<>?!\'/@"\\,=^', }), enableDataApi: true, cloudwatchLogsExports: ['postgresql'], cloudwatchLogsRetention: logs.RetentionDays.ONE_WEEK, parameterGroup: new rds.ParameterGroup(this, 'ParameterGroup', { engine, parameters: { // Close idle connection after 60 seconds for Aurora auto-pause idle_session_timeout: '60000', log_connections: '1', log_disconnections: '1', }, }), }); this.cluster = cluster; this.secret = cluster.secret!; this.connections = this.cluster.connections; const host = new ec2.BastionHostLinux(this, 'BastionHost', { vpc, machineImage: ec2.MachineImage.latestAmazonLinux2023({ cpuType: ec2.AmazonLinuxCpuType.ARM_64 }), instanceType: ec2.InstanceType.of(ec2.InstanceClass.T4G, ec2.InstanceSize.NANO), blockDevices: [ { deviceName: '/dev/sdf', volume: ec2.BlockDeviceVolume.ebs(8, { encrypted: true, }), }, ], }); this.connections.allowDefaultPortFrom(host); new CfnOutput(this, 'PortForwardCommand', { value: `aws ssm start-session --region ${Stack.of(this).region} --target ${ host.instanceId } --document-name AWS-StartPortForwardingSessionToRemoteHost --parameters '{"portNumber":["${ cluster.clusterEndpoint.port }"], "localPortNumber":["5433"], "host": ["${cluster.clusterEndpoint.hostname}"]}'`, }); new CfnOutput(this, 'DatabaseSecretsCommand', { value: `aws secretsmanager get-secret-value --secret-id ${cluster.secret!.secretName} --region ${ Stack.of(this).region }`, }); } public getConnectionInfo() { return { // We use direct reference for host and port because using only secret here results in failure of refreshing values. // Also refer to: https://github.com/aws-cloudformation/cloudformation-coverage-roadmap/issues/369 host: this.cluster.clusterEndpoint.hostname, port: Token.asString(this.cluster.clusterEndpoint.port), engine: this.secret.secretValueFromJson('engine').unsafeUnwrap(), username: this.secret.secretValueFromJson('username').unsafeUnwrap(), password: this.secret.secretValueFromJson('password').unsafeUnwrap(), }; } public getLambdaEnvironment(databaseName: string) { const conn = this.getConnectionInfo(); // connection_limit=1: Each Lambda instance handles one request at a time // connect_timeout=30: Aurora Serverless v2 auto-pause resume takes ~15s (longer after 24h+ pause) // https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/aurora-serverless-v2-auto-pause.html const option = '?connection_limit=1&connect_timeout=30'; return { DATABASE_HOST: conn.host, DATABASE_NAME: databaseName, DATABASE_USER: conn.username, DATABASE_PASSWORD: conn.password, DATABASE_ENGINE: conn.engine, DATABASE_PORT: conn.port, DATABASE_OPTION: option, DATABASE_URL: `${conn.engine}://${conn.username}:${conn.password}@${conn.host}:${conn.port}/${databaseName}${option}`, }; } } ================================================ FILE: cdk/lib/constructs/event-bus/handler.mjs ================================================ import { util } from '@aws-appsync/utils'; /** * Allow subscription only for the channels that * 1. begin with /public * 2. begin with /user/ * https://docs.aws.amazon.com/appsync/latest/eventapi/channel-namespace-handlers.html */ export function onSubscribe(ctx) { if (ctx.info.channel.path.startsWith(`/event-bus/public`)) { return; } if (ctx.info.channel.path.startsWith(`/event-bus/user/${ctx.identity.username}`)) { return; } console.log(`user ${ctx.identity.username} tried connecting to wrong channel: ${ctx.channel}`); util.unauthorized(); } ================================================ FILE: cdk/lib/constructs/event-bus/index.ts ================================================ import { Construct } from 'constructs'; import * as appsync from 'aws-cdk-lib/aws-appsync'; import { CfnOutput, CfnResource, Names, Stack } from 'aws-cdk-lib'; import { IUserPool } from 'aws-cdk-lib/aws-cognito'; import { join } from 'path'; export interface EventBusProps {} export class EventBus extends Construct { public readonly httpEndpoint: string; public readonly api: appsync.EventApi; private userPoolCount = 0; constructor(scope: Construct, id: string, props: EventBusProps) { super(scope, id); const api = new appsync.EventApi(this, 'Api', { apiName: Names.uniqueResourceName(this, { maxLength: 30 }), authorizationConfig: { authProviders: [ { authorizationType: appsync.AppSyncAuthorizationType.IAM, }, ], connectionAuthModeTypes: [appsync.AppSyncAuthorizationType.IAM], defaultPublishAuthModeTypes: [appsync.AppSyncAuthorizationType.IAM], defaultSubscribeAuthModeTypes: [appsync.AppSyncAuthorizationType.IAM], }, }); new appsync.ChannelNamespace(this, 'Namespace', { api, channelNamespaceName: 'event-bus', code: appsync.Code.fromAsset(join(__dirname, 'handler.mjs')), }); this.httpEndpoint = `https://${api.httpDns}`; this.api = api; new CfnOutput(this, 'HttpEndpoint', { value: this.httpEndpoint }); } public addUserPoolProvider(userPool: IUserPool) { if (this.userPoolCount == 0) { (this.api.node.defaultChild as CfnResource).addPropertyOverride('EventConfig.ConnectionAuthModes.1', { AuthType: 'AMAZON_COGNITO_USER_POOLS', }); (this.api.node.defaultChild as CfnResource).addPropertyOverride('EventConfig.DefaultSubscribeAuthModes.1', { AuthType: 'AMAZON_COGNITO_USER_POOLS', }); } this.userPoolCount += 1; (this.api.node.defaultChild as CfnResource).addPropertyOverride(`EventConfig.AuthProviders.${this.userPoolCount}`, { AuthType: 'AMAZON_COGNITO_USER_POOLS', CognitoConfig: { AwsRegion: Stack.of(this).region, UserPoolId: userPool.userPoolId, }, }); } } ================================================ FILE: cdk/lib/constructs/webapp.ts ================================================ import { IgnoreMode, Duration, CfnOutput, Stack, RemovalPolicy } from 'aws-cdk-lib'; import { LogGroup, RetentionDays } from 'aws-cdk-lib/aws-logs'; import { Platform } from 'aws-cdk-lib/aws-ecr-assets'; import { DockerImageFunction, DockerImageCode, Architecture } from 'aws-cdk-lib/aws-lambda'; import { Construct } from 'constructs'; import { readFileSync } from 'fs'; import { CloudFrontLambdaFunctionUrlService } from './cf-lambda-furl-service/service'; import { IHostedZone } from 'aws-cdk-lib/aws-route53'; import { Bucket } from 'aws-cdk-lib/aws-s3'; import { Database } from './database'; import { EdgeFunction } from './cf-lambda-furl-service/edge-function'; import { ICertificate } from 'aws-cdk-lib/aws-certificatemanager'; import { Auth } from './auth/'; import { ContainerImageBuild } from 'deploy-time-build'; import { join } from 'path'; import { EventBus } from './event-bus/'; import { AsyncJob } from './async-job'; import { Trigger } from 'aws-cdk-lib/triggers'; import { StringParameter } from 'aws-cdk-lib/aws-ssm'; import { AwsCustomResource, AwsCustomResourcePolicy, PhysicalResourceId } from 'aws-cdk-lib/custom-resources'; export interface WebappProps { database: Database; signPayloadHandler: EdgeFunction; accessLogBucket: Bucket; auth: Auth; eventBus: EventBus; asyncJob: AsyncJob; /** * Route 53 hosted zone for custom domain. * * @default No custom domain. The webapp will use CloudFront's default domain (e.g., d1234567890.cloudfront.net). */ hostedZone?: IHostedZone; /** * ACM certificate for custom domain (must be in us-east-1 for CloudFront). * * @default No custom domain. */ certificate?: ICertificate; /** * Subdomain name for the webapp. If not specified, the root domain will be used. * * @default Use root domain */ subDomain?: string; } export class Webapp extends Construct { public readonly baseUrl: string; constructor(scope: Construct, id: string, props: WebappProps) { super(scope, id); const { database, hostedZone, auth, subDomain, eventBus, asyncJob } = props; // Use ContainerImageBuild to inject deploy-time values in the build environment const image = new ContainerImageBuild(this, 'Build', { directory: join('..', 'webapp'), platform: Platform.LINUX_ARM64, ignoreMode: IgnoreMode.DOCKER, exclude: readFileSync(join('..', 'webapp', '.dockerignore')) .toString() .split('\n'), tagPrefix: 'webapp-starter-', buildArgs: { ALLOWED_ORIGIN_HOST: hostedZone ? `*.${hostedZone.zoneName}` : '*.cloudfront.net', SKIP_TS_BUILD: 'true', NEXT_PUBLIC_EVENT_HTTP_ENDPOINT: eventBus.httpEndpoint, NEXT_PUBLIC_AWS_REGION: Stack.of(this).region, }, }); const handler = new DockerImageFunction(this, 'Handler', { code: image.toLambdaDockerImageCode(), timeout: Duration.minutes(3), environment: { ...database.getLambdaEnvironment('main'), COGNITO_DOMAIN: auth.domainName, USER_POOL_ID: auth.userPool.userPoolId, USER_POOL_CLIENT_ID: auth.client.userPoolClientId, ASYNC_JOB_HANDLER_ARN: asyncJob.handler.functionArn, }, vpc: database.cluster.vpc, memorySize: 1024, architecture: Architecture.ARM_64, logGroup: new LogGroup(this, 'HandlerLogs', { retention: RetentionDays.ONE_WEEK, removalPolicy: RemovalPolicy.DESTROY, }), }); handler.connections.allowToDefaultPort(database); asyncJob.handler.grantInvoke(handler); const service = new CloudFrontLambdaFunctionUrlService(this, 'Resource', { subDomain, handler, serviceName: 'Webapp', hostedZone, certificate: props.certificate, accessLogBucket: props.accessLogBucket, signPayloadHandler: props.signPayloadHandler, }); this.baseUrl = service.url; if (hostedZone) { auth.addAllowedCallbackUrls( `http://localhost:3010/api/auth/sign-in-callback`, `http://localhost:3010/api/auth/sign-out-callback`, ); auth.addAllowedCallbackUrls( `${this.baseUrl}/api/auth/sign-in-callback`, `${this.baseUrl}/api/auth/sign-out-callback`, ); handler.addEnvironment('AMPLIFY_APP_ORIGIN', service.url); } else { auth.updateAllowedCallbackUrls( [`${this.baseUrl}/api/auth/sign-in-callback`, `http://localhost:3010/api/auth/sign-in-callback`], [`${this.baseUrl}/api/auth/sign-out-callback`, `http://localhost:3010/api/auth/sign-out-callback`], ); const originSourceParameter = new StringParameter(this, 'OriginSourceParameter', { stringValue: 'dummy', }); originSourceParameter.grantRead(handler); handler.addEnvironment('AMPLIFY_APP_ORIGIN_SOURCE_PARAMETER', originSourceParameter.parameterName); // We need to pass AMPLIFY_APP_ORIGIN environment variable for callback URL, // but we cannot know CloudFront domain before deploying Lambda function. // To avoid the circular dependency, we fetch the domain name on runtime. new AwsCustomResource(this, 'UpdateAmplifyOriginSourceParameter', { onUpdate: { service: 'ssm', action: 'putParameter', parameters: { Name: originSourceParameter.parameterName, Value: service.url, Overwrite: true, }, physicalResourceId: PhysicalResourceId.of(originSourceParameter.parameterName), }, policy: AwsCustomResourcePolicy.fromSdkCalls({ resources: [originSourceParameter.parameterArn], }), }); } const migrationRunner = new DockerImageFunction(this, 'MigrationRunner', { code: DockerImageCode.fromImageAsset(join('..', 'webapp'), { platform: Platform.LINUX_ARM64, cmd: ['migration-runner.handler'], file: 'job.Dockerfile', }), architecture: Architecture.ARM_64, timeout: Duration.minutes(5), environment: { ...database.getLambdaEnvironment('main'), }, vpc: database.cluster.vpc, memorySize: 256, logGroup: new LogGroup(this, 'MigrationRunnerLogs', { retention: RetentionDays.ONE_WEEK, removalPolicy: RemovalPolicy.DESTROY, }), }); migrationRunner.connections.allowToDefaultPort(database); // Run database migration during CDK deployment // The Trigger construct automatically invokes the migration runner with default payload (command: 'deploy') // To manually run migrations with different commands (e.g., 'force'), use the AWS CLI command shown in the CDK output below const trigger = new Trigger(this, 'MigrationTrigger', { handler: migrationRunner, }); // make sure migration is executed after the database cluster is available. trigger.node.addDependency(database.cluster); // Output migration-related information for manual invocation // Available commands: "deploy" (default), "force" (with --accept-data-loss) // Example: aws lambda invoke --function-name --payload '{"command":"force"}' --cli-binary-format raw-in-base64-out /dev/stdout new CfnOutput(Stack.of(this), 'MigrationFunctionName', { value: migrationRunner.functionName }); new CfnOutput(Stack.of(this), 'MigrationCommand', { value: `aws lambda invoke --function-name ${migrationRunner.functionName} --payload '{"command":"deploy"}' --cli-binary-format raw-in-base64-out /dev/stdout`, }); } } ================================================ FILE: cdk/lib/main-stack.ts ================================================ import { CfnOutput, RemovalPolicy, Stack, StackProps } from 'aws-cdk-lib'; import { BlockPublicAccess, Bucket, BucketEncryption, ObjectOwnership } from 'aws-cdk-lib/aws-s3'; import { Construct } from 'constructs'; import { AsyncJob } from './constructs/async-job'; import { Auth } from './constructs/auth/'; import { Database } from './constructs/database'; import { InstanceClass, InstanceSize, InstanceType, NatProvider, UserData, Vpc } from 'aws-cdk-lib/aws-ec2'; import { HostedZone } from 'aws-cdk-lib/aws-route53'; import { ICertificate } from 'aws-cdk-lib/aws-certificatemanager'; import { Webapp } from './constructs/webapp'; import { EdgeFunction } from './constructs/cf-lambda-furl-service/edge-function'; import { EventBus } from './constructs/event-bus/'; interface MainStackProps extends StackProps { readonly signPayloadHandler: EdgeFunction; /** * Custom domain name for the webapp and Cognito. * * @default No custom domain. CloudFront and Cognito will use their default domains. */ readonly domainName?: string; /** * ACM certificate for custom domain (must be in us-east-1). * * @default No custom domain. */ readonly sharedCertificate?: ICertificate; /** * Use a NAT instance instead of NAT Gateways for cost optimization. * * @default true */ readonly useNatInstance?: boolean; } export class MainStack extends Stack { constructor(scope: Construct, id: string, props: MainStackProps) { super(scope, id, { description: 'Serverless fullstack webapp stack (uksb-1tupboc47)', ...props }); const { useNatInstance = true } = props; const hostedZone = props.domainName ? HostedZone.fromLookup(this, 'HostedZone', { domainName: props.domainName, }) : undefined; const accessLogBucket = new Bucket(this, 'AccessLogBucket', { encryption: BucketEncryption.S3_MANAGED, blockPublicAccess: BlockPublicAccess.BLOCK_ALL, enforceSSL: true, removalPolicy: RemovalPolicy.DESTROY, objectOwnership: ObjectOwnership.OBJECT_WRITER, autoDeleteObjects: true, }); // Custom user data for NAT instance to support Amazon Linux 2023. // CDK's default user data uses `route` command which requires net-tools package, // but AL2023 doesn't have net-tools pre-installed. We use `ip route` instead. // Retry yum install to handle RPM lock conflicts during boot. const natUserData = UserData.forLinux(); natUserData.addCommands( // Retry yum install up to 5 times with 10 second intervals 'for i in {1..5}; do yum install iptables-services -y && break || sleep 10; done', 'systemctl enable iptables', 'systemctl start iptables', 'echo "net.ipv4.ip_forward=1" > /etc/sysctl.d/custom-ip-forwarding.conf', 'sysctl -p /etc/sysctl.d/custom-ip-forwarding.conf', "IFACE=$(ip route show default | awk '{print $5}')", '/sbin/iptables -t nat -A POSTROUTING -o $IFACE -j MASQUERADE', '/sbin/iptables -F FORWARD', 'service iptables save', ); const vpc = new Vpc(this, `Vpc`, { ...(useNatInstance ? { natGatewayProvider: NatProvider.instanceV2({ instanceType: InstanceType.of(InstanceClass.T4G, InstanceSize.NANO), associatePublicIpAddress: true, userData: natUserData, }), natGateways: 1, } : {}), }); const database = new Database(this, 'Database', { vpc }); const auth = new Auth(this, 'Auth', { hostedZone, sharedCertificate: props.sharedCertificate, }); const eventBus = new EventBus(this, 'EventBus', {}); eventBus.addUserPoolProvider(auth.userPool); const asyncJob = new AsyncJob(this, 'AsyncJob', { database: database, eventBus }); const webapp = new Webapp(this, 'Webapp', { database, hostedZone, certificate: props.sharedCertificate, signPayloadHandler: props.signPayloadHandler, accessLogBucket, auth, eventBus, asyncJob, subDomain: 'web', }); new CfnOutput(this, 'FrontendDomainName', { value: webapp.baseUrl, }); } } ================================================ FILE: cdk/lib/us-east-1-stack.ts ================================================ import * as cdk from 'aws-cdk-lib'; import { Certificate, CertificateValidation, ICertificate } from 'aws-cdk-lib/aws-certificatemanager'; import { ARecord, HostedZone, RecordTarget } from 'aws-cdk-lib/aws-route53'; import { Construct } from 'constructs'; import { EdgeFunction } from './constructs/cf-lambda-furl-service/edge-function'; import { join } from 'path'; interface UsEast1StackProps extends cdk.StackProps { /** * Custom domain name for the webapp and Cognito. * * @default No custom domain. CloudFront and Cognito will use their default domains. */ domainName?: string; } export class UsEast1Stack extends cdk.Stack { /** * the ACM certificate for CloudFront (it must be deployed in us-east-1). * undefined if domainName is not set. */ public readonly certificate: ICertificate | undefined = undefined; /** * the signer L@E function (it must be deployed in us-east-1). */ public readonly signPayloadHandler: EdgeFunction; constructor(scope: Construct, id: string, props: UsEast1StackProps) { super(scope, id, props); if (props.domainName) { const hostedZone = HostedZone.fromLookup(this, 'HostedZone', { domainName: props.domainName, }); // cognito requires A record for Hosted UI custom domain // https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pools-add-custom-domain.html#cognito-user-pools-add-custom-domain-adding // > Its parent domain must have a valid DNS A record. You can assign any value to this record. new ARecord(this, 'Record', { zone: hostedZone, target: RecordTarget.fromIpAddresses('8.8.8.8'), }); const cert = new Certificate(this, 'CertificateV2', { domainName: `*.${hostedZone.zoneName}`, validation: CertificateValidation.fromDns(hostedZone), subjectAlternativeNames: [hostedZone.zoneName], }); this.certificate = cert; } const signPayloadHandler = new EdgeFunction(this, 'SignPayloadHandler', { entryPath: join(__dirname, 'constructs', 'cf-lambda-furl-service', 'lambda', 'sign-payload.ts'), }); this.signPayloadHandler = signPayloadHandler; } } ================================================ FILE: cdk/package.json ================================================ { "name": "@aws-samples/serverless-fullstack-webapp-starter-kit", "version": "0.1.0", "private": true, "scripts": { "build": "tsc", "watch": "tsc -w", "test": "jest", "format": "prettier --write './**/*.{ts,tsx,mjs,mts}'", "format:check": "prettier --check './**/*.{ts,tsx,mjs,mts}'", "cdk": "cdk" }, "devDependencies": { "@types/jest": "^27.5.0", "@types/node": "^22.14.1", "@types/prettier": "2.6.0", "aws-cdk": "^2.1007.0", "esbuild": "^0.25.4", "jest": "^30.1.3", "prettier": "^3.5.3", "ts-jest": "^29.4.4", "ts-node": "^10.7.0", "typescript": "^5.9.2" }, "dependencies": { "@aws-appsync/utils": "^1.12.0", "@aws-sdk/client-cognito-identity-provider": "^3.987.0", "@types/aws-lambda": "^8.10.149", "aws-cdk-lib": "^2.189.1", "cdk-nag": "^2.14.29", "constructs": "^10.0.0", "deploy-time-build": "^0.3.32", "source-map-support": "^0.5.21" } } ================================================ FILE: cdk/test/__snapshots__/serverless-fullstack-webapp-starter-kit-without-domain.test.ts.snap ================================================ // Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing exports[`Snapshot test 1`] = ` { "Parameters": { "BootstrapVersion": { "Default": "/cdk-bootstrap/hnb659fds/version", "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]", "Type": "AWS::SSM::Parameter::Value", }, }, "Resources": { "CustomCrossRegionExportWriterCustomResourceProviderHandlerD8786E8A": { "DependsOn": [ "CustomCrossRegionExportWriterCustomResourceProviderRoleC951B1E1", ], "Properties": { "Code": { "S3Bucket": "cdk-hnb659fds-assets-123456789012-us-east-1", "S3Key": "REDACTED", }, "Handler": "__entrypoint__.handler", "MemorySize": 128, "Role": { "Fn::GetAtt": [ "CustomCrossRegionExportWriterCustomResourceProviderRoleC951B1E1", "Arn", ], }, "Runtime": "nodejs22.x", "Timeout": 900, }, "Type": "AWS::Lambda::Function", }, "CustomCrossRegionExportWriterCustomResourceProviderRoleC951B1E1": { "Properties": { "AssumeRolePolicyDocument": { "Statement": [ { "Action": "sts:AssumeRole", "Effect": "Allow", "Principal": { "Service": "lambda.amazonaws.com", }, }, ], "Version": "2012-10-17", }, "ManagedPolicyArns": [ { "Fn::Sub": "arn:\${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole", }, ], "Policies": [ { "PolicyDocument": { "Statement": [ { "Action": [ "ssm:DeleteParameters", "ssm:ListTagsForResource", "ssm:GetParameters", "ssm:PutParameter", ], "Effect": "Allow", "Resource": [ { "Fn::Join": [ "", [ "arn:", { "Ref": "AWS::Partition", }, ":ssm:us-west-2:123456789012:parameter/cdk/exports/*", ], ], }, ], }, ], "Version": "2012-10-17", }, "PolicyName": "Inline", }, ], }, "Type": "AWS::IAM::Role", }, "ExportsWriteruswest209BD44F0A7CF058B": { "DeletionPolicy": "Delete", "Properties": { "ServiceToken": { "Fn::GetAtt": [ "CustomCrossRegionExportWriterCustomResourceProviderHandlerD8786E8A", "Arn", ], }, "WriterProps": { "exports": { "/cdk/exports/ServerlessWebappStarterKitStack/ServerlessWebappStarterKitUsEast1Stackuseast1RefSignPayloadHandlerFunctionVersionF9FE430A3006B9FA": { "Ref": "SignPayloadHandlerFunctionVersionF9FE430A", }, }, "region": "us-west-2", }, }, "Type": "Custom::CrossRegionExportWriter", "UpdateReplacePolicy": "Delete", }, "SignPayloadHandlerCFEAA14C": { "DependsOn": [ "SignPayloadHandlerServiceRole29261232", ], "Properties": { "Code": { "S3Bucket": "cdk-hnb659fds-assets-123456789012-us-east-1", "S3Key": "REDACTED", }, "Handler": "index.handler", "Role": { "Fn::GetAtt": [ "SignPayloadHandlerServiceRole29261232", "Arn", ], }, "Runtime": "nodejs22.x", }, "Type": "AWS::Lambda::Function", }, "SignPayloadHandlerCurrentVersionREDACTED": { "Properties": { "FunctionName": { "Ref": "SignPayloadHandlerCFEAA14C", }, }, "Type": "AWS::Lambda::Version", }, "SignPayloadHandlerFunctionVersionF9FE430A": { "Properties": { "Type": "String", "Value": { "Ref": "SignPayloadHandlerCurrentVersionREDACTED", }, }, "Type": "AWS::SSM::Parameter", }, "SignPayloadHandlerServiceRole29261232": { "Properties": { "AssumeRolePolicyDocument": { "Statement": [ { "Action": "sts:AssumeRole", "Effect": "Allow", "Principal": { "Service": "lambda.amazonaws.com", }, }, { "Action": "sts:AssumeRole", "Effect": "Allow", "Principal": { "Service": "edgelambda.amazonaws.com", }, }, ], "Version": "2012-10-17", }, "ManagedPolicyArns": [ { "Fn::Join": [ "", [ "arn:", { "Ref": "AWS::Partition", }, ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole", ], ], }, ], }, "Type": "AWS::IAM::Role", }, }, "Rules": { "CheckBootstrapVersion": { "Assertions": [ { "Assert": { "Fn::Not": [ { "Fn::Contains": [ [ "1", "2", "3", "4", "5", ], { "Ref": "BootstrapVersion", }, ], }, ], }, "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI.", }, ], }, }, } `; exports[`Snapshot test 2`] = ` { "Description": "Serverless fullstack webapp stack (uksb-1tupboc47)", "Outputs": { "AsyncJobHandlerArnCA46B385": { "Value": { "Fn::GetAtt": [ "AsyncJobHandler438266BD", "Arn", ], }, }, "AuthUserPoolClientId8216BF9A": { "Value": { "Ref": "AuthUserPoolClientC635291F", }, }, "AuthUserPoolDomainName8D4A2606": { "Value": { "Fn::Join": [ "", [ { "Fn::GetAtt": [ "AuthDomainPrefixE1742B23", "generated", ], }, ".auth.us-west-2.amazoncognito.com", ], ], }, }, "AuthUserPoolIdC0605E59": { "Value": { "Ref": "AuthUserPool8115E87F", }, }, "DatabaseBastionHostBastionHostId1600F37C": { "Description": "Instance ID of the bastion host. Use this to connect via SSM Session Manager", "Value": { "Ref": "DatabaseBastionHost4C4FAD9C", }, }, "DatabaseDatabaseSecretsCommandF4A622EB": { "Value": { "Fn::Join": [ "", [ "aws secretsmanager get-secret-value --secret-id ", { "Fn::Join": [ "-", [ { "Fn::Select": [ 0, { "Fn::Split": [ "-", { "Fn::Select": [ 6, { "Fn::Split": [ ":", { "Ref": "DatabaseClusterSecretD1FB634F", }, ], }, ], }, ], }, ], }, { "Fn::Select": [ 1, { "Fn::Split": [ "-", { "Fn::Select": [ 6, { "Fn::Split": [ ":", { "Ref": "DatabaseClusterSecretD1FB634F", }, ], }, ], }, ], }, ], }, ], ], }, " --region us-west-2", ], ], }, }, "DatabasePortForwardCommandC3718B89": { "Value": { "Fn::Join": [ "", [ "aws ssm start-session --region us-west-2 --target ", { "Ref": "DatabaseBastionHost4C4FAD9C", }, " --document-name AWS-StartPortForwardingSessionToRemoteHost --parameters '{"portNumber":["", { "Fn::GetAtt": [ "DatabaseCluster5B53A178", "Endpoint.Port", ], }, ""], "localPortNumber":["5433"], "host": ["", { "Fn::GetAtt": [ "DatabaseCluster5B53A178", "Endpoint.Address", ], }, ""]}'", ], ], }, }, "EventBusHttpEndpoint1C68A807": { "Value": { "Fn::Join": [ "", [ "https://", { "Fn::GetAtt": [ "EventBusApi6E8C7C94", "Dns.Http", ], }, ], ], }, }, "FrontendDomainName": { "Value": { "Fn::Join": [ "", [ "https://", { "Fn::GetAtt": [ "Webapp107041BD", "DomainName", ], }, ], ], }, }, "MigrationCommand": { "Value": { "Fn::Join": [ "", [ "aws lambda invoke --function-name ", { "Ref": "WebappMigrationRunnerAC67C012", }, " --payload '{"command":"deploy"}' --cli-binary-format raw-in-base64-out /dev/stdout", ], ], }, }, "MigrationFunctionName": { "Value": { "Ref": "WebappMigrationRunnerAC67C012", }, }, }, "Parameters": { "BootstrapVersion": { "Default": "/cdk-bootstrap/hnb659fds/version", "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]", "Type": "AWS::SSM::Parameter::Value", }, "SsmParameterValueawsserviceamiamazonlinuxlatestal2023amikernel61arm64C96584B6F00A464EAD1953AFF4B05118Parameter": { "Default": "/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-6.1-arm64", "Type": "AWS::SSM::Parameter::Value", }, }, "Resources": { "AWS679f53fac002430cb0da5b7982bd22872D164C4C": { "DependsOn": [ "AWS679f53fac002430cb0da5b7982bd2287ServiceRoleC1EA0FF2", ], "Properties": { "Code": { "S3Bucket": "cdk-hnb659fds-assets-123456789012-us-west-2", "S3Key": "REDACTED", }, "Handler": "index.handler", "Role": { "Fn::GetAtt": [ "AWS679f53fac002430cb0da5b7982bd2287ServiceRoleC1EA0FF2", "Arn", ], }, "Runtime": "nodejs22.x", "Timeout": 120, }, "Type": "AWS::Lambda::Function", }, "AWS679f53fac002430cb0da5b7982bd2287ServiceRoleC1EA0FF2": { "Properties": { "AssumeRolePolicyDocument": { "Statement": [ { "Action": "sts:AssumeRole", "Effect": "Allow", "Principal": { "Service": "lambda.amazonaws.com", }, }, ], "Version": "2012-10-17", }, "ManagedPolicyArns": [ { "Fn::Join": [ "", [ "arn:", { "Ref": "AWS::Partition", }, ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole", ], ], }, ], }, "Type": "AWS::IAM::Role", }, "AWSCDKTriggerCustomResourceProviderCustomResourceProviderHandler97BECD91": { "DependsOn": [ "AWSCDKTriggerCustomResourceProviderCustomResourceProviderRoleE18FAF0A", ], "Properties": { "Code": { "S3Bucket": "cdk-hnb659fds-assets-123456789012-us-west-2", "S3Key": "REDACTED", }, "Handler": "__entrypoint__.handler", "MemorySize": 128, "Role": { "Fn::GetAtt": [ "AWSCDKTriggerCustomResourceProviderCustomResourceProviderRoleE18FAF0A", "Arn", ], }, "Runtime": "nodejs22.x", "Timeout": 900, }, "Type": "AWS::Lambda::Function", }, "AWSCDKTriggerCustomResourceProviderCustomResourceProviderRoleE18FAF0A": { "Properties": { "AssumeRolePolicyDocument": { "Statement": [ { "Action": "sts:AssumeRole", "Effect": "Allow", "Principal": { "Service": "lambda.amazonaws.com", }, }, ], "Version": "2012-10-17", }, "ManagedPolicyArns": [ { "Fn::Sub": "arn:\${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole", }, ], "Policies": [ { "PolicyDocument": { "Statement": [ { "Action": [ "lambda:InvokeFunction", ], "Effect": "Allow", "Resource": [ { "Fn::Join": [ "", [ { "Fn::GetAtt": [ "WebappMigrationRunnerAC67C012", "Arn", ], }, ":*", ], ], }, ], }, ], "Version": "2012-10-17", }, "PolicyName": "Inline", }, ], }, "Type": "AWS::IAM::Role", }, "AccessLogBucketAutoDeleteObjectsCustomResource01AB31E8": { "DeletionPolicy": "Delete", "DependsOn": [ "AccessLogBucketPolicyF52D2D01", ], "Properties": { "BucketName": { "Ref": "AccessLogBucketDA470295", }, "ServiceToken": { "Fn::GetAtt": [ "CustomS3AutoDeleteObjectsCustomResourceProviderHandler9D90184F", "Arn", ], }, }, "Type": "Custom::S3AutoDeleteObjects", "UpdateReplacePolicy": "Delete", }, "AccessLogBucketDA470295": { "DeletionPolicy": "Delete", "Properties": { "BucketEncryption": { "ServerSideEncryptionConfiguration": [ { "ServerSideEncryptionByDefault": { "SSEAlgorithm": "AES256", }, }, ], }, "OwnershipControls": { "Rules": [ { "ObjectOwnership": "ObjectWriter", }, ], }, "PublicAccessBlockConfiguration": { "BlockPublicAcls": true, "BlockPublicPolicy": true, "IgnorePublicAcls": true, "RestrictPublicBuckets": true, }, "Tags": [ { "Key": "aws-cdk:auto-delete-objects", "Value": "true", }, ], }, "Type": "AWS::S3::Bucket", "UpdateReplacePolicy": "Delete", }, "AccessLogBucketPolicyF52D2D01": { "Properties": { "Bucket": { "Ref": "AccessLogBucketDA470295", }, "PolicyDocument": { "Statement": [ { "Action": "s3:*", "Condition": { "Bool": { "aws:SecureTransport": "false", }, }, "Effect": "Deny", "Principal": { "AWS": "*", }, "Resource": [ { "Fn::GetAtt": [ "AccessLogBucketDA470295", "Arn", ], }, { "Fn::Join": [ "", [ { "Fn::GetAtt": [ "AccessLogBucketDA470295", "Arn", ], }, "/*", ], ], }, ], }, { "Action": [ "s3:PutBucketPolicy", "s3:GetBucket*", "s3:List*", "s3:DeleteObject*", ], "Effect": "Allow", "Principal": { "AWS": { "Fn::GetAtt": [ "CustomS3AutoDeleteObjectsCustomResourceProviderRole3B1BD092", "Arn", ], }, }, "Resource": [ { "Fn::GetAtt": [ "AccessLogBucketDA470295", "Arn", ], }, { "Fn::Join": [ "", [ { "Fn::GetAtt": [ "AccessLogBucketDA470295", "Arn", ], }, "/*", ], ], }, ], }, ], "Version": "2012-10-17", }, }, "Type": "AWS::S3::BucketPolicy", }, "AsyncJobHandler438266BD": { "DependsOn": [ "AsyncJobHandlerServiceRoleDefaultPolicy0B2DEDB5", "AsyncJobHandlerServiceRoleFE9F530F", "VpcPrivateSubnet1DefaultRouteBE02A9ED", "VpcPrivateSubnet1RouteTableAssociation70C59FA6", "VpcPrivateSubnet2DefaultRoute060D2087", "VpcPrivateSubnet2RouteTableAssociationA89CAD56", "VpcPrivateSubnet3DefaultRoute94B74F0D", "VpcPrivateSubnet3RouteTableAssociation16BDDC43", ], "Properties": { "Architectures": [ "arm64", ], "Code": { "ImageUri": { "Fn::Sub": "REDACTED", }, }, "Environment": { "Variables": { "DATABASE_ENGINE": { "Fn::Join": [ "", [ "{{resolve:secretsmanager:", { "Ref": "DatabaseClusterSecretAttachmentDC8466C0", }, ":SecretString:engine::}}", ], ], }, "DATABASE_HOST": { "Fn::GetAtt": [ "DatabaseCluster5B53A178", "Endpoint.Address", ], }, "DATABASE_NAME": "main", "DATABASE_OPTION": "?connection_limit=1&connect_timeout=30", "DATABASE_PASSWORD": { "Fn::Join": [ "", [ "{{resolve:secretsmanager:", { "Ref": "DatabaseClusterSecretAttachmentDC8466C0", }, ":SecretString:password::}}", ], ], }, "DATABASE_PORT": { "Fn::GetAtt": [ "DatabaseCluster5B53A178", "Endpoint.Port", ], }, "DATABASE_URL": { "Fn::Join": [ "", [ "{{resolve:secretsmanager:", { "Ref": "DatabaseClusterSecretAttachmentDC8466C0", }, ":SecretString:engine::}}://{{resolve:secretsmanager:", { "Ref": "DatabaseClusterSecretAttachmentDC8466C0", }, ":SecretString:username::}}:{{resolve:secretsmanager:", { "Ref": "DatabaseClusterSecretAttachmentDC8466C0", }, ":SecretString:password::}}@", { "Fn::GetAtt": [ "DatabaseCluster5B53A178", "Endpoint.Address", ], }, ":", { "Fn::GetAtt": [ "DatabaseCluster5B53A178", "Endpoint.Port", ], }, "/main?connection_limit=1&connect_timeout=30", ], ], }, "DATABASE_USER": { "Fn::Join": [ "", [ "{{resolve:secretsmanager:", { "Ref": "DatabaseClusterSecretAttachmentDC8466C0", }, ":SecretString:username::}}", ], ], }, "EVENT_HTTP_ENDPOINT": { "Fn::Join": [ "", [ "https://", { "Fn::GetAtt": [ "EventBusApi6E8C7C94", "Dns.Http", ], }, ], ], }, }, }, "ImageConfig": { "Command": [ "async-job-runner.handler", ], }, "LoggingConfig": { "LogGroup": { "Ref": "AsyncJobHandlerLogs20DFEE3E", }, }, "MemorySize": 256, "PackageType": "Image", "ReservedConcurrentExecutions": 1, "Role": { "Fn::GetAtt": [ "AsyncJobHandlerServiceRoleFE9F530F", "Arn", ], }, "Timeout": 600, "VpcConfig": { "SecurityGroupIds": [ { "Fn::GetAtt": [ "AsyncJobHandlerSecurityGroupF59812E6", "GroupId", ], }, ], "SubnetIds": [ { "Ref": "VpcPrivateSubnet1Subnet536B997A", }, { "Ref": "VpcPrivateSubnet2Subnet3788AAA1", }, { "Ref": "VpcPrivateSubnet3SubnetF258B56E", }, ], }, }, "Type": "AWS::Lambda::Function", }, "AsyncJobHandlerLogs20DFEE3E": { "DeletionPolicy": "Delete", "Properties": { "RetentionInDays": 7, }, "Type": "AWS::Logs::LogGroup", "UpdateReplacePolicy": "Delete", }, "AsyncJobHandlerSecurityGroupF59812E6": { "DependsOn": [ "VpcPrivateSubnet1DefaultRouteBE02A9ED", "VpcPrivateSubnet1RouteTableAssociation70C59FA6", "VpcPrivateSubnet2DefaultRoute060D2087", "VpcPrivateSubnet2RouteTableAssociationA89CAD56", "VpcPrivateSubnet3DefaultRoute94B74F0D", "VpcPrivateSubnet3RouteTableAssociation16BDDC43", ], "Properties": { "GroupDescription": "Automatic security group for Lambda Function ServerlessWebappStarterKitStackAsyncJobHandlerF20B94EA", "SecurityGroupEgress": [ { "CidrIp": "0.0.0.0/0", "Description": "Allow all outbound traffic by default", "IpProtocol": "-1", }, ], "VpcId": { "Ref": "Vpc8378EB38", }, }, "Type": "AWS::EC2::SecurityGroup", }, "AsyncJobHandlerServiceRoleDefaultPolicy0B2DEDB5": { "DependsOn": [ "VpcPrivateSubnet1DefaultRouteBE02A9ED", "VpcPrivateSubnet1RouteTableAssociation70C59FA6", "VpcPrivateSubnet2DefaultRoute060D2087", "VpcPrivateSubnet2RouteTableAssociationA89CAD56", "VpcPrivateSubnet3DefaultRoute94B74F0D", "VpcPrivateSubnet3RouteTableAssociation16BDDC43", ], "Properties": { "PolicyDocument": { "Statement": [ { "Action": "appsync:EventPublish", "Effect": "Allow", "Resource": { "Fn::Join": [ "", [ "arn:", { "Ref": "AWS::Partition", }, ":appsync:us-west-2:123456789012:apis/", { "Fn::GetAtt": [ "EventBusApi6E8C7C94", "ApiId", ], }, "/channelNamespace/*", ], ], }, }, { "Action": [ "translate:TranslateText", "comprehend:DetectDominantLanguage", ], "Effect": "Allow", "Resource": "*", }, ], "Version": "2012-10-17", }, "PolicyName": "AsyncJobHandlerServiceRoleDefaultPolicy0B2DEDB5", "Roles": [ { "Ref": "AsyncJobHandlerServiceRoleFE9F530F", }, ], }, "Type": "AWS::IAM::Policy", }, "AsyncJobHandlerServiceRoleFE9F530F": { "DependsOn": [ "VpcPrivateSubnet1DefaultRouteBE02A9ED", "VpcPrivateSubnet1RouteTableAssociation70C59FA6", "VpcPrivateSubnet2DefaultRoute060D2087", "VpcPrivateSubnet2RouteTableAssociationA89CAD56", "VpcPrivateSubnet3DefaultRoute94B74F0D", "VpcPrivateSubnet3RouteTableAssociation16BDDC43", ], "Properties": { "AssumeRolePolicyDocument": { "Statement": [ { "Action": "sts:AssumeRole", "Effect": "Allow", "Principal": { "Service": "lambda.amazonaws.com", }, }, ], "Version": "2012-10-17", }, "ManagedPolicyArns": [ { "Fn::Join": [ "", [ "arn:", { "Ref": "AWS::Partition", }, ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole", ], ], }, { "Fn::Join": [ "", [ "arn:", { "Ref": "AWS::Partition", }, ":iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole", ], ], }, ], }, "Type": "AWS::IAM::Role", }, "AsyncJobSampleJob3C1EBA2C": { "Properties": { "FlexibleTimeWindow": { "Mode": "OFF", }, "ScheduleExpression": "cron(0 0 1 * ? *)", "ScheduleExpressionTimezone": "Etc/UTC", "State": "ENABLED", "Target": { "Arn": { "Fn::GetAtt": [ "AsyncJobHandler438266BD", "Arn", ], }, "Input": "{"jobType":"SampleJob"}", "RetryPolicy": { "MaximumEventAgeInSeconds": 86400, "MaximumRetryAttempts": 5, }, "RoleArn": { "Fn::GetAtt": [ "SchedulerRoleForTarget44ece2CFC6840F", "Arn", ], }, }, }, "Type": "AWS::Scheduler::Schedule", }, "AuthBranding34BB87FD": { "Properties": { "ClientId": { "Ref": "AuthUserPoolClientC635291F", }, "UseCognitoProvidedValues": true, "UserPoolId": { "Ref": "AuthUserPool8115E87F", }, }, "Type": "AWS::Cognito::ManagedLoginBranding", }, "AuthDomainPrefixE1742B23": { "DeletionPolicy": "Delete", "Properties": { "ServiceTimeout": "10", "ServiceToken": { "Fn::GetAtt": [ "RandomStringGenerator11e9c903f11a4989833c985dddef5eb28C5103D0", "Arn", ], }, "length": 10, "prefix": "webapp-", }, "Type": "Custom::RandomString", "UpdateReplacePolicy": "Delete", }, "AuthUpdateCallbackUrlsA55622E9": { "DeletionPolicy": "Delete", "DependsOn": [ "AuthUpdateCallbackUrlsCustomResourcePolicy14EEB23D", ], "Properties": { "Create": { "Fn::Join": [ "", [ "{"service":"@aws-sdk/client-cognito-identity-provider","action":"updateUserPoolClient","parameters":{"ClientId":"", { "Ref": "AuthUserPoolClientC635291F", }, "","UserPoolId":"", { "Ref": "AuthUserPool8115E87F", }, "","AllowedOAuthFlows":["code"],"AllowedOAuthFlowsUserPoolClient":true,"AllowedOAuthScopes":["profile","phone","email","openid","aws.cognito.signin.user.admin"],"ExplicitAuthFlows":["ALLOW_USER_PASSWORD_AUTH","ALLOW_USER_SRP_AUTH","ALLOW_REFRESH_TOKEN_AUTH"],"CallbackURLs":["https://", { "Fn::GetAtt": [ "Webapp107041BD", "DomainName", ], }, "/api/auth/sign-in-callback","http://localhost:3010/api/auth/sign-in-callback"],"LogoutURLs":["https://", { "Fn::GetAtt": [ "Webapp107041BD", "DomainName", ], }, "/api/auth/sign-out-callback","http://localhost:3010/api/auth/sign-out-callback"],"SupportedIdentityProviders":["COGNITO"],"TokenValidityUnits":{"IdToken":"minutes"},"IdTokenValidity":1440},"physicalResourceId":{"id":"", { "Ref": "AuthUserPool8115E87F", }, ""}}", ], ], }, "InstallLatestAwsSdk": true, "ServiceToken": { "Fn::GetAtt": [ "AWS679f53fac002430cb0da5b7982bd22872D164C4C", "Arn", ], }, "Update": { "Fn::Join": [ "", [ "{"service":"@aws-sdk/client-cognito-identity-provider","action":"updateUserPoolClient","parameters":{"ClientId":"", { "Ref": "AuthUserPoolClientC635291F", }, "","UserPoolId":"", { "Ref": "AuthUserPool8115E87F", }, "","AllowedOAuthFlows":["code"],"AllowedOAuthFlowsUserPoolClient":true,"AllowedOAuthScopes":["profile","phone","email","openid","aws.cognito.signin.user.admin"],"ExplicitAuthFlows":["ALLOW_USER_PASSWORD_AUTH","ALLOW_USER_SRP_AUTH","ALLOW_REFRESH_TOKEN_AUTH"],"CallbackURLs":["https://", { "Fn::GetAtt": [ "Webapp107041BD", "DomainName", ], }, "/api/auth/sign-in-callback","http://localhost:3010/api/auth/sign-in-callback"],"LogoutURLs":["https://", { "Fn::GetAtt": [ "Webapp107041BD", "DomainName", ], }, "/api/auth/sign-out-callback","http://localhost:3010/api/auth/sign-out-callback"],"SupportedIdentityProviders":["COGNITO"],"TokenValidityUnits":{"IdToken":"minutes"},"IdTokenValidity":1440},"physicalResourceId":{"id":"", { "Ref": "AuthUserPool8115E87F", }, ""}}", ], ], }, }, "Type": "Custom::AWS", "UpdateReplacePolicy": "Delete", }, "AuthUpdateCallbackUrlsCustomResourcePolicy14EEB23D": { "Properties": { "PolicyDocument": { "Statement": [ { "Action": "cognito-idp:UpdateUserPoolClient", "Effect": "Allow", "Resource": { "Fn::GetAtt": [ "AuthUserPool8115E87F", "Arn", ], }, }, ], "Version": "2012-10-17", }, "PolicyName": "AuthUpdateCallbackUrlsCustomResourcePolicy14EEB23D", "Roles": [ { "Ref": "AWS679f53fac002430cb0da5b7982bd2287ServiceRoleC1EA0FF2", }, ], }, "Type": "AWS::IAM::Policy", }, "AuthUserPool8115E87F": { "DeletionPolicy": "Delete", "Properties": { "AccountRecoverySetting": { "RecoveryMechanisms": [ { "Name": "verified_phone_number", "Priority": 1, }, { "Name": "verified_email", "Priority": 2, }, ], }, "AdminCreateUserConfig": { "AllowAdminCreateUserOnly": true, }, "AutoVerifiedAttributes": [ "email", ], "EmailVerificationMessage": "The verification code to your new account is {####}", "EmailVerificationSubject": "Verify your new account", "Policies": { "PasswordPolicy": { "MinimumLength": 8, "RequireNumbers": true, "RequireSymbols": true, "RequireUppercase": true, }, }, "SmsVerificationMessage": "The verification code to your new account is {####}", "UsernameAttributes": [ "email", ], "VerificationMessageTemplate": { "DefaultEmailOption": "CONFIRM_WITH_CODE", "EmailMessage": "The verification code to your new account is {####}", "EmailSubject": "Verify your new account", "SmsMessage": "The verification code to your new account is {####}", }, }, "Type": "AWS::Cognito::UserPool", "UpdateReplacePolicy": "Delete", }, "AuthUserPoolClientC635291F": { "Properties": { "AllowedOAuthFlows": [ "code", ], "AllowedOAuthFlowsUserPoolClient": true, "AllowedOAuthScopes": [ "profile", "phone", "email", "openid", "aws.cognito.signin.user.admin", ], "CallbackURLs": [ "http://localhost/dummy", ], "ExplicitAuthFlows": [ "ALLOW_USER_PASSWORD_AUTH", "ALLOW_USER_SRP_AUTH", "ALLOW_REFRESH_TOKEN_AUTH", ], "IdTokenValidity": 1440, "LogoutURLs": [ "http://localhost/dummy", ], "SupportedIdentityProviders": [ "COGNITO", ], "TokenValidityUnits": { "IdToken": "minutes", }, "UserPoolId": { "Ref": "AuthUserPool8115E87F", }, }, "Type": "AWS::Cognito::UserPoolClient", }, "AuthUserPoolCognitoDomainAD9D79E1": { "Properties": { "Domain": { "Fn::GetAtt": [ "AuthDomainPrefixE1742B23", "generated", ], }, "ManagedLoginVersion": 2, "UserPoolId": { "Ref": "AuthUserPool8115E87F", }, }, "Type": "AWS::Cognito::UserPoolDomain", }, "ContainerImageBuildArm64e83729feb1564e709bec452b15847a30amd64C13E3549": { "Properties": { "Artifacts": { "Type": "NO_ARTIFACTS", }, "Cache": { "Type": "NO_CACHE", }, "EncryptionKey": "alias/aws/s3", "Environment": { "ComputeType": "BUILD_GENERAL1_SMALL", "Image": "aws/codebuild/amazonlinux2-aarch64-standard:3.0", "ImagePullCredentialsType": "CODEBUILD", "PrivilegedMode": true, "Type": "ARM_CONTAINER", }, "ServiceRole": { "Fn::GetAtt": [ "ContainerImageBuildArm64e83729feb1564e709bec452b15847a30amd64RoleC5F7BBFE", "Arn", ], }, "Source": { "BuildSpec": "{ "version": "0.2", "phases": { "build": { "commands": [ "current_dir=$(pwd)", "echo \\"$input\\"", "mkdir workdir", "cd workdir", "aws s3 cp \\"$sourceS3Url\\" temp.zip", "unzip temp.zip", "ls -la", "aws ecr get-login-password | docker login --username AWS --password-stdin $repositoryAuthUri", "aws ecr-public get-login-password --region us-east-1 | docker login --username AWS --password-stdin public.ecr.aws", "docker buildx create --use", "docker buildx ls", "eval \\"$buildCommand\\"" ] }, "post_build": { "commands": [ "echo Build completed on \`date\`", "\\nSTATUS='SUCCESS'\\nif [ $CODEBUILD_BUILD_SUCCEEDING -ne 1 ] # Test if the build is failing\\nthen\\nSTATUS='FAILED'\\nREASON=\\"ContainerImageBuild failed. See CloudWatch Log stream for the detailed reason: \\nhttps://$AWS_REGION.console.aws.amazon.com/cloudwatch/home?region=$AWS_REGION#logsV2:log-groups/log-group/\\\\$252Faws\\\\$252Fcodebuild\\\\$252F$projectName/log-events/$CODEBUILD_LOG_PATH\\"\\nfi\\ncat < payload.json\\n{\\n \\"StackId\\": \\"$stackId\\",\\n \\"RequestId\\": \\"$requestId\\",\\n \\"LogicalResourceId\\":\\"$logicalResourceId\\",\\n \\"PhysicalResourceId\\": \\"$imageTag\\",\\n \\"Status\\": \\"$STATUS\\",\\n \\"Reason\\": \\"$REASON\\",\\n \\"Data\\": {\\n \\"ImageTag\\": \\"$imageTag\\"\\n }\\n}\\nEOF\\ncurl -v -i -X PUT -H 'Content-Type:' -d \\"@payload.json\\" \\"$responseURL\\"\\n " ] } } }", "Type": "NO_SOURCE", }, }, "Type": "AWS::CodeBuild::Project", }, "ContainerImageBuildArm64e83729feb1564e709bec452b15847a30amd64RoleC5F7BBFE": { "Properties": { "AssumeRolePolicyDocument": { "Statement": [ { "Action": "sts:AssumeRole", "Effect": "Allow", "Principal": { "Service": "codebuild.amazonaws.com", }, }, ], "Version": "2012-10-17", }, "ManagedPolicyArns": [ { "Fn::Join": [ "", [ "arn:", { "Ref": "AWS::Partition", }, ":iam::aws:policy/AmazonElasticContainerRegistryPublicReadOnly", ], ], }, ], }, "Type": "AWS::IAM::Role", }, "ContainerImageBuildArm64e83729feb1564e709bec452b15847a30amd64RoleDefaultPolicy2316728F": { "Properties": { "PolicyDocument": { "Statement": [ { "Action": [ "logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents", ], "Effect": "Allow", "Resource": [ { "Fn::Join": [ "", [ "arn:", { "Ref": "AWS::Partition", }, ":logs:us-west-2:123456789012:log-group:/aws/codebuild/", { "Ref": "ContainerImageBuildArm64e83729feb1564e709bec452b15847a30amd64C13E3549", }, ], ], }, { "Fn::Join": [ "", [ "arn:", { "Ref": "AWS::Partition", }, ":logs:us-west-2:123456789012:log-group:/aws/codebuild/", { "Ref": "ContainerImageBuildArm64e83729feb1564e709bec452b15847a30amd64C13E3549", }, ":*", ], ], }, ], }, { "Action": [ "codebuild:CreateReportGroup", "codebuild:CreateReport", "codebuild:UpdateReport", "codebuild:BatchPutTestCases", "codebuild:BatchPutCodeCoverages", ], "Effect": "Allow", "Resource": { "Fn::Join": [ "", [ "arn:", { "Ref": "AWS::Partition", }, ":codebuild:us-west-2:123456789012:report-group/", { "Ref": "ContainerImageBuildArm64e83729feb1564e709bec452b15847a30amd64C13E3549", }, "-*", ], ], }, }, { "Action": [ "ecr:BatchCheckLayerAvailability", "ecr:GetDownloadUrlForLayer", "ecr:BatchGetImage", "ecr:CompleteLayerUpload", "ecr:UploadLayerPart", "ecr:InitiateLayerUpload", "ecr:PutImage", ], "Effect": "Allow", "Resource": { "Fn::GetAtt": [ "WebappBuildRepository4C93D48D", "Arn", ], }, }, { "Action": "ecr:GetAuthorizationToken", "Effect": "Allow", "Resource": "*", }, { "Action": "ecr:DescribeImages", "Effect": "Allow", "Resource": { "Fn::GetAtt": [ "WebappBuildRepository4C93D48D", "Arn", ], }, }, { "Action": [ "s3:GetObject*", "s3:GetBucket*", "s3:List*", ], "Effect": "Allow", "Resource": [ { "Fn::Join": [ "", [ "arn:", { "Ref": "AWS::Partition", }, ":s3:::cdk-hnb659fds-assets-123456789012-us-west-2", ], ], }, { "Fn::Join": [ "", [ "arn:", { "Ref": "AWS::Partition", }, ":s3:::cdk-hnb659fds-assets-123456789012-us-west-2/*", ], ], }, ], }, ], "Version": "2012-10-17", }, "PolicyName": "ContainerImageBuildArm64e83729feb1564e709bec452b15847a30amd64RoleDefaultPolicy2316728F", "Roles": [ { "Ref": "ContainerImageBuildArm64e83729feb1564e709bec452b15847a30amd64RoleC5F7BBFE", }, ], }, "Type": "AWS::IAM::Policy", }, "CustomCrossRegionExportReaderCustomResourceProviderHandler46647B68": { "DependsOn": [ "CustomCrossRegionExportReaderCustomResourceProviderRole10531BBD", ], "Properties": { "Code": { "S3Bucket": "cdk-hnb659fds-assets-123456789012-us-west-2", "S3Key": "REDACTED", }, "Handler": "__entrypoint__.handler", "MemorySize": 128, "Role": { "Fn::GetAtt": [ "CustomCrossRegionExportReaderCustomResourceProviderRole10531BBD", "Arn", ], }, "Runtime": "nodejs22.x", "Timeout": 900, }, "Type": "AWS::Lambda::Function", }, "CustomCrossRegionExportReaderCustomResourceProviderRole10531BBD": { "Properties": { "AssumeRolePolicyDocument": { "Statement": [ { "Action": "sts:AssumeRole", "Effect": "Allow", "Principal": { "Service": "lambda.amazonaws.com", }, }, ], "Version": "2012-10-17", }, "ManagedPolicyArns": [ { "Fn::Sub": "arn:\${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole", }, ], "Policies": [ { "PolicyDocument": { "Statement": [ { "Action": [ "ssm:AddTagsToResource", "ssm:RemoveTagsFromResource", "ssm:GetParameters", ], "Effect": "Allow", "Resource": { "Fn::Join": [ "", [ "arn:", { "Ref": "AWS::Partition", }, ":ssm:us-west-2:123456789012:parameter/cdk/exports/ServerlessWebappStarterKitStack/*", ], ], }, }, ], "Version": "2012-10-17", }, "PolicyName": "Inline", }, ], }, "Type": "AWS::IAM::Role", }, "CustomS3AutoDeleteObjectsCustomResourceProviderHandler9D90184F": { "DependsOn": [ "CustomS3AutoDeleteObjectsCustomResourceProviderRole3B1BD092", ], "Properties": { "Code": { "S3Bucket": "cdk-hnb659fds-assets-123456789012-us-west-2", "S3Key": "REDACTED", }, "Description": { "Fn::Join": [ "", [ "Lambda function for auto-deleting objects in ", { "Ref": "AccessLogBucketDA470295", }, " S3 bucket.", ], ], }, "Handler": "index.handler", "MemorySize": 128, "Role": { "Fn::GetAtt": [ "CustomS3AutoDeleteObjectsCustomResourceProviderRole3B1BD092", "Arn", ], }, "Runtime": "nodejs22.x", "Timeout": 900, }, "Type": "AWS::Lambda::Function", }, "CustomS3AutoDeleteObjectsCustomResourceProviderRole3B1BD092": { "Properties": { "AssumeRolePolicyDocument": { "Statement": [ { "Action": "sts:AssumeRole", "Effect": "Allow", "Principal": { "Service": "lambda.amazonaws.com", }, }, ], "Version": "2012-10-17", }, "ManagedPolicyArns": [ { "Fn::Sub": "arn:\${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole", }, ], }, "Type": "AWS::IAM::Role", }, "DatabaseBastionHost4C4FAD9C": { "DependsOn": [ "DatabaseBastionHostInstanceRoleDefaultPolicy15D8D0EA", "DatabaseBastionHostInstanceRole87A429B0", ], "Properties": { "AvailabilityZone": "dummy1a", "BlockDeviceMappings": [ { "DeviceName": "/dev/sdf", "Ebs": { "Encrypted": true, "VolumeSize": 8, }, }, ], "IamInstanceProfile": { "Ref": "DatabaseBastionHostInstanceProfile0F4F3411", }, "ImageId": { "Ref": "SsmParameterValueawsserviceamiamazonlinuxlatestal2023amikernel61arm64C96584B6F00A464EAD1953AFF4B05118Parameter", }, "InstanceType": "t4g.nano", "SecurityGroupIds": [ { "Fn::GetAtt": [ "DatabaseBastionHostInstanceSecurityGroup39D7809A", "GroupId", ], }, ], "SubnetId": { "Ref": "VpcPrivateSubnet1Subnet536B997A", }, "Tags": [ { "Key": "Name", "Value": "BastionHost", }, ], "UserData": { "Fn::Base64": "#!/bin/bash", }, }, "Type": "AWS::EC2::Instance", }, "DatabaseBastionHostInstanceProfile0F4F3411": { "Properties": { "Roles": [ { "Ref": "DatabaseBastionHostInstanceRole87A429B0", }, ], }, "Type": "AWS::IAM::InstanceProfile", }, "DatabaseBastionHostInstanceRole87A429B0": { "Properties": { "AssumeRolePolicyDocument": { "Statement": [ { "Action": "sts:AssumeRole", "Effect": "Allow", "Principal": { "Service": "ec2.amazonaws.com", }, }, ], "Version": "2012-10-17", }, "Tags": [ { "Key": "Name", "Value": "BastionHost", }, ], }, "Type": "AWS::IAM::Role", }, "DatabaseBastionHostInstanceRoleDefaultPolicy15D8D0EA": { "Properties": { "PolicyDocument": { "Statement": [ { "Action": [ "ssmmessages:*", "ssm:UpdateInstanceInformation", "ec2messages:*", ], "Effect": "Allow", "Resource": "*", }, ], "Version": "2012-10-17", }, "PolicyName": "DatabaseBastionHostInstanceRoleDefaultPolicy15D8D0EA", "Roles": [ { "Ref": "DatabaseBastionHostInstanceRole87A429B0", }, ], }, "Type": "AWS::IAM::Policy", }, "DatabaseBastionHostInstanceSecurityGroup39D7809A": { "Properties": { "GroupDescription": "ServerlessWebappStarterKitStack/Database/BastionHost/Resource/InstanceSecurityGroup", "SecurityGroupEgress": [ { "CidrIp": "0.0.0.0/0", "Description": "Allow all outbound traffic by default", "IpProtocol": "-1", }, ], "Tags": [ { "Key": "Name", "Value": "BastionHost", }, ], "VpcId": { "Ref": "Vpc8378EB38", }, }, "Type": "AWS::EC2::SecurityGroup", }, "DatabaseCluster5B53A178": { "DeletionPolicy": "Snapshot", "Properties": { "CopyTagsToSnapshot": true, "DBClusterParameterGroupName": { "Ref": "DatabaseParameterGroup2A921026", }, "DBSubnetGroupName": { "Ref": "DatabaseClusterSubnets5540150D", }, "EnableCloudwatchLogsExports": [ "postgresql", ], "EnableHttpEndpoint": true, "Engine": "aurora-postgresql", "EngineVersion": "16.6", "MasterUserPassword": { "Fn::Join": [ "", [ "{{resolve:secretsmanager:", { "Ref": "DatabaseClusterSecretD1FB634F", }, ":SecretString:password::}}", ], ], }, "MasterUsername": { "Fn::Join": [ "", [ "{{resolve:secretsmanager:", { "Ref": "DatabaseClusterSecretD1FB634F", }, ":SecretString:username::}}", ], ], }, "Port": 5432, "ServerlessV2ScalingConfiguration": { "MaxCapacity": 2, "MinCapacity": 0, }, "StorageEncrypted": true, "VpcSecurityGroupIds": [ { "Fn::GetAtt": [ "DatabaseClusterSecurityGroupFEF1426A", "GroupId", ], }, ], }, "Type": "AWS::RDS::DBCluster", "UpdateReplacePolicy": "Snapshot", }, "DatabaseClusterLogRetentionpostgresql025D39CE": { "Properties": { "LogGroupName": { "Fn::Join": [ "", [ "/aws/rds/cluster/", { "Ref": "DatabaseCluster5B53A178", }, "/postgresql", ], ], }, "RetentionInDays": 7, "ServiceToken": { "Fn::GetAtt": [ "LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aFD4BFC8A", "Arn", ], }, }, "Type": "Custom::LogRetention", }, "DatabaseClusterSecretAttachmentDC8466C0": { "Properties": { "SecretId": { "Ref": "DatabaseClusterSecretD1FB634F", }, "TargetId": { "Ref": "DatabaseCluster5B53A178", }, "TargetType": "AWS::RDS::DBCluster", }, "Type": "AWS::SecretsManager::SecretTargetAttachment", }, "DatabaseClusterSecretD1FB634F": { "DeletionPolicy": "Delete", "Properties": { "Description": { "Fn::Join": [ "", [ "Generated by the CDK for stack: ", { "Ref": "AWS::StackName", }, ], ], }, "GenerateSecretString": { "ExcludeCharacters": " %+~\`#$&*()|[]{}:;<>?!'/@"\\,=^", "GenerateStringKey": "password", "PasswordLength": 30, "SecretStringTemplate": "{"username":"postgres"}", }, }, "Type": "AWS::SecretsManager::Secret", "UpdateReplacePolicy": "Delete", }, "DatabaseClusterSecurityGroupFEF1426A": { "Properties": { "GroupDescription": "RDS security group", "SecurityGroupEgress": [ { "CidrIp": "0.0.0.0/0", "Description": "Allow all outbound traffic by default", "IpProtocol": "-1", }, ], "VpcId": { "Ref": "Vpc8378EB38", }, }, "Type": "AWS::EC2::SecurityGroup", }, "DatabaseClusterSecurityGroupfromServerlessWebappStarterKitStackAsyncJobHandlerSecurityGroup5220DFB3IndirectPort9323962E": { "Properties": { "Description": "from ServerlessWebappStarterKitStackAsyncJobHandlerSecurityGroup5220DFB3:{IndirectPort}", "FromPort": { "Fn::GetAtt": [ "DatabaseCluster5B53A178", "Endpoint.Port", ], }, "GroupId": { "Fn::GetAtt": [ "DatabaseClusterSecurityGroupFEF1426A", "GroupId", ], }, "IpProtocol": "tcp", "SourceSecurityGroupId": { "Fn::GetAtt": [ "AsyncJobHandlerSecurityGroupF59812E6", "GroupId", ], }, "ToPort": { "Fn::GetAtt": [ "DatabaseCluster5B53A178", "Endpoint.Port", ], }, }, "Type": "AWS::EC2::SecurityGroupIngress", }, "DatabaseClusterSecurityGroupfromServerlessWebappStarterKitStackDatabaseBastionHostInstanceSecurityGroup4F0DD25BIndirectPort8AFD9922": { "Properties": { "Description": "from ServerlessWebappStarterKitStackDatabaseBastionHostInstanceSecurityGroup4F0DD25B:{IndirectPort}", "FromPort": { "Fn::GetAtt": [ "DatabaseCluster5B53A178", "Endpoint.Port", ], }, "GroupId": { "Fn::GetAtt": [ "DatabaseClusterSecurityGroupFEF1426A", "GroupId", ], }, "IpProtocol": "tcp", "SourceSecurityGroupId": { "Fn::GetAtt": [ "DatabaseBastionHostInstanceSecurityGroup39D7809A", "GroupId", ], }, "ToPort": { "Fn::GetAtt": [ "DatabaseCluster5B53A178", "Endpoint.Port", ], }, }, "Type": "AWS::EC2::SecurityGroupIngress", }, "DatabaseClusterSecurityGroupfromServerlessWebappStarterKitStackWebappHandlerSecurityGroupA009CF4AIndirectPort7311063E": { "Properties": { "Description": "from ServerlessWebappStarterKitStackWebappHandlerSecurityGroupA009CF4A:{IndirectPort}", "FromPort": { "Fn::GetAtt": [ "DatabaseCluster5B53A178", "Endpoint.Port", ], }, "GroupId": { "Fn::GetAtt": [ "DatabaseClusterSecurityGroupFEF1426A", "GroupId", ], }, "IpProtocol": "tcp", "SourceSecurityGroupId": { "Fn::GetAtt": [ "WebappHandlerSecurityGroup5451B519", "GroupId", ], }, "ToPort": { "Fn::GetAtt": [ "DatabaseCluster5B53A178", "Endpoint.Port", ], }, }, "Type": "AWS::EC2::SecurityGroupIngress", }, "DatabaseClusterSecurityGroupfromServerlessWebappStarterKitStackWebappMigrationRunnerSecurityGroupC0959349IndirectPortDCF4A356": { "Properties": { "Description": "from ServerlessWebappStarterKitStackWebappMigrationRunnerSecurityGroupC0959349:{IndirectPort}", "FromPort": { "Fn::GetAtt": [ "DatabaseCluster5B53A178", "Endpoint.Port", ], }, "GroupId": { "Fn::GetAtt": [ "DatabaseClusterSecurityGroupFEF1426A", "GroupId", ], }, "IpProtocol": "tcp", "SourceSecurityGroupId": { "Fn::GetAtt": [ "WebappMigrationRunnerSecurityGroup7F0DF264", "GroupId", ], }, "ToPort": { "Fn::GetAtt": [ "DatabaseCluster5B53A178", "Endpoint.Port", ], }, }, "Type": "AWS::EC2::SecurityGroupIngress", }, "DatabaseClusterSubnets5540150D": { "Properties": { "DBSubnetGroupDescription": "Subnets for Cluster database", "SubnetIds": [ { "Ref": "VpcPrivateSubnet1Subnet536B997A", }, { "Ref": "VpcPrivateSubnet2Subnet3788AAA1", }, { "Ref": "VpcPrivateSubnet3SubnetF258B56E", }, ], }, "Type": "AWS::RDS::DBSubnetGroup", }, "DatabaseClusterWriterD43085C6": { "DeletionPolicy": "Delete", "DependsOn": [ "VpcPrivateSubnet1DefaultRouteBE02A9ED", "VpcPrivateSubnet1RouteTableAssociation70C59FA6", "VpcPrivateSubnet2DefaultRoute060D2087", "VpcPrivateSubnet2RouteTableAssociationA89CAD56", "VpcPrivateSubnet3DefaultRoute94B74F0D", "VpcPrivateSubnet3RouteTableAssociation16BDDC43", ], "Properties": { "AutoMinorVersionUpgrade": true, "DBClusterIdentifier": { "Ref": "DatabaseCluster5B53A178", }, "DBInstanceClass": "db.serverless", "EnablePerformanceInsights": true, "Engine": "aurora-postgresql", "PerformanceInsightsRetentionPeriod": 7, "PromotionTier": 0, "PubliclyAccessible": false, }, "Type": "AWS::RDS::DBInstance", "UpdateReplacePolicy": "Delete", }, "DatabaseParameterGroup2A921026": { "Properties": { "Description": "Cluster parameter group for aurora-postgresql16", "Family": "aurora-postgresql16", "Parameters": { "idle_session_timeout": "60000", "log_connections": "1", "log_disconnections": "1", }, }, "Type": "AWS::RDS::DBClusterParameterGroup", }, "DeployTimeBuildCustomResourceHandlerdb740fd554364a848a09e6dfcd01f4f306AEFF37": { "DependsOn": [ "DeployTimeBuildCustomResourceHandlerdb740fd554364a848a09e6dfcd01f4f3ServiceRoleDefaultPolicyFECC51DC", "DeployTimeBuildCustomResourceHandlerdb740fd554364a848a09e6dfcd01f4f3ServiceRoleB008BAA4", ], "Properties": { "Code": { "S3Bucket": "cdk-hnb659fds-assets-123456789012-us-west-2", "S3Key": "REDACTED", }, "Handler": "index.handler", "Role": { "Fn::GetAtt": [ "DeployTimeBuildCustomResourceHandlerdb740fd554364a848a09e6dfcd01f4f3ServiceRoleB008BAA4", "Arn", ], }, "Runtime": "nodejs20.x", "Timeout": 300, }, "Type": "AWS::Lambda::Function", }, "DeployTimeBuildCustomResourceHandlerdb740fd554364a848a09e6dfcd01f4f3ServiceRoleB008BAA4": { "Properties": { "AssumeRolePolicyDocument": { "Statement": [ { "Action": "sts:AssumeRole", "Effect": "Allow", "Principal": { "Service": "lambda.amazonaws.com", }, }, ], "Version": "2012-10-17", }, "ManagedPolicyArns": [ { "Fn::Join": [ "", [ "arn:", { "Ref": "AWS::Partition", }, ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole", ], ], }, ], }, "Type": "AWS::IAM::Role", }, "DeployTimeBuildCustomResourceHandlerdb740fd554364a848a09e6dfcd01f4f3ServiceRoleDefaultPolicyFECC51DC": { "Properties": { "PolicyDocument": { "Statement": [ { "Action": "codebuild:StartBuild", "Effect": "Allow", "Resource": { "Fn::GetAtt": [ "ContainerImageBuildArm64e83729feb1564e709bec452b15847a30amd64C13E3549", "Arn", ], }, }, ], "Version": "2012-10-17", }, "PolicyName": "DeployTimeBuildCustomResourceHandlerdb740fd554364a848a09e6dfcd01f4f3ServiceRoleDefaultPolicyFECC51DC", "Roles": [ { "Ref": "DeployTimeBuildCustomResourceHandlerdb740fd554364a848a09e6dfcd01f4f3ServiceRoleB008BAA4", }, ], }, "Type": "AWS::IAM::Policy", }, "EventBusApi6E8C7C94": { "Properties": { "EventConfig": { "AuthProviders": [ { "AuthType": "AWS_IAM", }, { "AuthType": "AMAZON_COGNITO_USER_POOLS", "CognitoConfig": { "AwsRegion": "us-west-2", "UserPoolId": { "Ref": "AuthUserPool8115E87F", }, }, }, ], "ConnectionAuthModes": [ { "AuthType": "AWS_IAM", }, { "AuthType": "AMAZON_COGNITO_USER_POOLS", }, ], "DefaultPublishAuthModes": [ { "AuthType": "AWS_IAM", }, ], "DefaultSubscribeAuthModes": [ { "AuthType": "AWS_IAM", }, { "AuthType": "AMAZON_COGNITO_USER_POOLS", }, ], }, "Name": "ServerlessWackEventBus8815362F", }, "Type": "AWS::AppSync::Api", }, "EventBusNamespaceA69F015E": { "Properties": { "ApiId": { "Fn::GetAtt": [ "EventBusApi6E8C7C94", "ApiId", ], }, "CodeS3Location": "s3://cdk-hnb659fds-assets-123456789012-us-west-2/REDACTED", "HandlerConfigs": {}, "Name": "event-bus", }, "Type": "AWS::AppSync::ChannelNamespace", }, "ExportsReader8B249524": { "DeletionPolicy": "Delete", "Properties": { "ReaderProps": { "imports": { "/cdk/exports/ServerlessWebappStarterKitStack/ServerlessWebappStarterKitUsEast1Stackuseast1RefSignPayloadHandlerFunctionVersionF9FE430A3006B9FA": "{{resolve:ssm:/cdk/exports/ServerlessWebappStarterKitStack/ServerlessWebappStarterKitUsEast1Stackuseast1RefSignPayloadHandlerFunctionVersionF9FE430A3006B9FA}}", }, "prefix": "ServerlessWebappStarterKitStack", "region": "us-west-2", }, "ServiceToken": { "Fn::GetAtt": [ "CustomCrossRegionExportReaderCustomResourceProviderHandler46647B68", "Arn", ], }, }, "Type": "Custom::CrossRegionExportReader", "UpdateReplacePolicy": "Delete", }, "LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aFD4BFC8A": { "DependsOn": [ "LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aServiceRoleDefaultPolicyADDA7DEB", "LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aServiceRole9741ECFB", ], "Properties": { "Code": { "S3Bucket": "cdk-hnb659fds-assets-123456789012-us-west-2", "S3Key": "REDACTED", }, "Handler": "index.handler", "Role": { "Fn::GetAtt": [ "LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aServiceRole9741ECFB", "Arn", ], }, "Runtime": "nodejs22.x", "Timeout": 900, }, "Type": "AWS::Lambda::Function", }, "LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aServiceRole9741ECFB": { "Properties": { "AssumeRolePolicyDocument": { "Statement": [ { "Action": "sts:AssumeRole", "Effect": "Allow", "Principal": { "Service": "lambda.amazonaws.com", }, }, ], "Version": "2012-10-17", }, "ManagedPolicyArns": [ { "Fn::Join": [ "", [ "arn:", { "Ref": "AWS::Partition", }, ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole", ], ], }, ], }, "Type": "AWS::IAM::Role", }, "LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aServiceRoleDefaultPolicyADDA7DEB": { "Properties": { "PolicyDocument": { "Statement": [ { "Action": [ "logs:PutRetentionPolicy", "logs:DeleteRetentionPolicy", ], "Effect": "Allow", "Resource": "*", }, ], "Version": "2012-10-17", }, "PolicyName": "LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aServiceRoleDefaultPolicyADDA7DEB", "Roles": [ { "Ref": "LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aServiceRole9741ECFB", }, ], }, "Type": "AWS::IAM::Policy", }, "LookupVersionArnc8730278af02f875114ca902814c77b68f19b0087110E04D0A": { "DeletionPolicy": "Delete", "DependsOn": [ "LookupVersionArnc8730278af02f875114ca902814c77b68f19b00871CustomResourcePolicy7CD24C5F", ], "Properties": { "Create": { "Fn::Join": [ "", [ "{"service":"SSM","action":"getParameter","parameters":{"Name":"", { "Fn::GetAtt": [ "ExportsReader8B249524", "/cdk/exports/ServerlessWebappStarterKitStack/ServerlessWebappStarterKitUsEast1Stackuseast1RefSignPayloadHandlerFunctionVersionF9FE430A3006B9FA", ], }, ""},"physicalResourceId":{"id":"1577836800000"},"region":"us-east-1"}", ], ], }, "InstallLatestAwsSdk": true, "ServiceToken": { "Fn::GetAtt": [ "AWS679f53fac002430cb0da5b7982bd22872D164C4C", "Arn", ], }, "Update": { "Fn::Join": [ "", [ "{"service":"SSM","action":"getParameter","parameters":{"Name":"", { "Fn::GetAtt": [ "ExportsReader8B249524", "/cdk/exports/ServerlessWebappStarterKitStack/ServerlessWebappStarterKitUsEast1Stackuseast1RefSignPayloadHandlerFunctionVersionF9FE430A3006B9FA", ], }, ""},"physicalResourceId":{"id":"1577836800000"},"region":"us-east-1"}", ], ], }, }, "Type": "Custom::AWS", "UpdateReplacePolicy": "Delete", }, "LookupVersionArnc8730278af02f875114ca902814c77b68f19b00871CustomResourcePolicy7CD24C5F": { "Properties": { "PolicyDocument": { "Statement": [ { "Action": "ssm:GetParameter", "Effect": "Allow", "Resource": { "Fn::Join": [ "", [ "arn:", { "Ref": "AWS::Partition", }, ":ssm:us-east-1:123456789012:parameter/", { "Fn::GetAtt": [ "ExportsReader8B249524", "/cdk/exports/ServerlessWebappStarterKitStack/ServerlessWebappStarterKitUsEast1Stackuseast1RefSignPayloadHandlerFunctionVersionF9FE430A3006B9FA", ], }, ], ], }, }, ], "Version": "2012-10-17", }, "PolicyName": "LookupVersionArnc8730278af02f875114ca902814c77b68f19b00871CustomResourcePolicy7CD24C5F", "Roles": [ { "Ref": "AWS679f53fac002430cb0da5b7982bd2287ServiceRoleC1EA0FF2", }, ], }, "Type": "AWS::IAM::Policy", }, "RandomStringGenerator11e9c903f11a4989833c985dddef5eb28C5103D0": { "DependsOn": [ "RandomStringGenerator11e9c903f11a4989833c985dddef5eb2ServiceRoleAB6B57A9", ], "Properties": { "Code": { "ZipFile": "const response = require('cfn-response'); const crypto = require('crypto'); exports.handler = async function (event, context) { try { console.log(event); if (event.RequestType == 'Delete') { return await response.send(event, context, response.SUCCESS); } const prefix = event.ResourceProperties.prefix ?? ''; const length = event.ResourceProperties.length ?? '8'; const generate = () => { const random = crypto.randomBytes(parseInt(length)).toString('hex'); return \`\${prefix}\${random.slice(0, length)}\`; }; if (event.RequestType == 'Create') { const generated = generate(); return await response.send(event, context, response.SUCCESS, { generated }, generated); } if (event.RequestType == 'Update') { const current = event.PhysicalResourceId; if (current.startsWith(prefix)) { return await response.send(event, context, response.SUCCESS, { generated: current }, current); } const generated = generate(); return await response.send(event, context, response.SUCCESS, { generated }, generated); } } catch (e) { console.log(e); await response.send(event, context, response.FAILED); } }; ", }, "Handler": "index.handler", "Role": { "Fn::GetAtt": [ "RandomStringGenerator11e9c903f11a4989833c985dddef5eb2ServiceRoleAB6B57A9", "Arn", ], }, "Runtime": "nodejs22.x", "Timeout": 5, }, "Type": "AWS::Lambda::Function", }, "RandomStringGenerator11e9c903f11a4989833c985dddef5eb2ServiceRoleAB6B57A9": { "Properties": { "AssumeRolePolicyDocument": { "Statement": [ { "Action": "sts:AssumeRole", "Effect": "Allow", "Principal": { "Service": "lambda.amazonaws.com", }, }, ], "Version": "2012-10-17", }, "ManagedPolicyArns": [ { "Fn::Join": [ "", [ "arn:", { "Ref": "AWS::Partition", }, ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole", ], ], }, ], }, "Type": "AWS::IAM::Role", }, "SchedulerRoleForTarget44ece2CFC6840F": { "Properties": { "AssumeRolePolicyDocument": { "Statement": [ { "Action": "sts:AssumeRole", "Condition": { "StringEquals": { "aws:SourceAccount": "123456789012", "aws:SourceArn": { "Fn::Join": [ "", [ "arn:", { "Ref": "AWS::Partition", }, ":scheduler:us-west-2:123456789012:schedule-group/default", ], ], }, }, }, "Effect": "Allow", "Principal": { "Service": "scheduler.amazonaws.com", }, }, ], "Version": "2012-10-17", }, }, "Type": "AWS::IAM::Role", }, "SchedulerRoleForTarget44ece2DefaultPolicyFDF3E159": { "Properties": { "PolicyDocument": { "Statement": [ { "Action": "lambda:InvokeFunction", "Effect": "Allow", "Resource": [ { "Fn::GetAtt": [ "AsyncJobHandler438266BD", "Arn", ], }, { "Fn::Join": [ "", [ { "Fn::GetAtt": [ "AsyncJobHandler438266BD", "Arn", ], }, ":*", ], ], }, ], }, ], "Version": "2012-10-17", }, "PolicyName": "SchedulerRoleForTarget44ece2DefaultPolicyFDF3E159", "Roles": [ { "Ref": "SchedulerRoleForTarget44ece2CFC6840F", }, ], }, "Type": "AWS::IAM::Policy", }, "Vpc8378EB38": { "Properties": { "CidrBlock": "10.0.0.0/16", "EnableDnsHostnames": true, "EnableDnsSupport": true, "InstanceTenancy": "default", "Tags": [ { "Key": "Name", "Value": "ServerlessWebappStarterKitStack/Vpc", }, ], }, "Type": "AWS::EC2::VPC", }, "VpcIGWD7BA715C": { "Properties": { "Tags": [ { "Key": "Name", "Value": "ServerlessWebappStarterKitStack/Vpc", }, ], }, "Type": "AWS::EC2::InternetGateway", }, "VpcNatSecurityGroup8DA26EDC": { "Properties": { "GroupDescription": "Security Group for NAT instances", "SecurityGroupEgress": [ { "CidrIp": "0.0.0.0/0", "Description": "Allow all outbound traffic by default", "IpProtocol": "-1", }, ], "SecurityGroupIngress": [ { "CidrIp": "0.0.0.0/0", "Description": "from 0.0.0.0/0:ALL TRAFFIC", "IpProtocol": "-1", }, ], "Tags": [ { "Key": "Name", "Value": "ServerlessWebappStarterKitStack/Vpc", }, ], "VpcId": { "Ref": "Vpc8378EB38", }, }, "Type": "AWS::EC2::SecurityGroup", }, "VpcPrivateSubnet1DefaultRouteBE02A9ED": { "Properties": { "DestinationCidrBlock": "0.0.0.0/0", "InstanceId": { "Ref": "VpcPublicSubnet1NatInstance57B636B8", }, "RouteTableId": { "Ref": "VpcPrivateSubnet1RouteTableB2C5B500", }, }, "Type": "AWS::EC2::Route", }, "VpcPrivateSubnet1RouteTableAssociation70C59FA6": { "Properties": { "RouteTableId": { "Ref": "VpcPrivateSubnet1RouteTableB2C5B500", }, "SubnetId": { "Ref": "VpcPrivateSubnet1Subnet536B997A", }, }, "Type": "AWS::EC2::SubnetRouteTableAssociation", }, "VpcPrivateSubnet1RouteTableB2C5B500": { "Properties": { "Tags": [ { "Key": "Name", "Value": "ServerlessWebappStarterKitStack/Vpc/PrivateSubnet1", }, ], "VpcId": { "Ref": "Vpc8378EB38", }, }, "Type": "AWS::EC2::RouteTable", }, "VpcPrivateSubnet1Subnet536B997A": { "Properties": { "AvailabilityZone": "dummy1a", "CidrBlock": "10.0.96.0/19", "MapPublicIpOnLaunch": false, "Tags": [ { "Key": "aws-cdk:subnet-name", "Value": "Private", }, { "Key": "aws-cdk:subnet-type", "Value": "Private", }, { "Key": "Name", "Value": "ServerlessWebappStarterKitStack/Vpc/PrivateSubnet1", }, ], "VpcId": { "Ref": "Vpc8378EB38", }, }, "Type": "AWS::EC2::Subnet", }, "VpcPrivateSubnet2DefaultRoute060D2087": { "Properties": { "DestinationCidrBlock": "0.0.0.0/0", "InstanceId": { "Ref": "VpcPublicSubnet1NatInstance57B636B8", }, "RouteTableId": { "Ref": "VpcPrivateSubnet2RouteTableA678073B", }, }, "Type": "AWS::EC2::Route", }, "VpcPrivateSubnet2RouteTableA678073B": { "Properties": { "Tags": [ { "Key": "Name", "Value": "ServerlessWebappStarterKitStack/Vpc/PrivateSubnet2", }, ], "VpcId": { "Ref": "Vpc8378EB38", }, }, "Type": "AWS::EC2::RouteTable", }, "VpcPrivateSubnet2RouteTableAssociationA89CAD56": { "Properties": { "RouteTableId": { "Ref": "VpcPrivateSubnet2RouteTableA678073B", }, "SubnetId": { "Ref": "VpcPrivateSubnet2Subnet3788AAA1", }, }, "Type": "AWS::EC2::SubnetRouteTableAssociation", }, "VpcPrivateSubnet2Subnet3788AAA1": { "Properties": { "AvailabilityZone": "dummy1b", "CidrBlock": "10.0.128.0/19", "MapPublicIpOnLaunch": false, "Tags": [ { "Key": "aws-cdk:subnet-name", "Value": "Private", }, { "Key": "aws-cdk:subnet-type", "Value": "Private", }, { "Key": "Name", "Value": "ServerlessWebappStarterKitStack/Vpc/PrivateSubnet2", }, ], "VpcId": { "Ref": "Vpc8378EB38", }, }, "Type": "AWS::EC2::Subnet", }, "VpcPrivateSubnet3DefaultRoute94B74F0D": { "Properties": { "DestinationCidrBlock": "0.0.0.0/0", "InstanceId": { "Ref": "VpcPublicSubnet1NatInstance57B636B8", }, "RouteTableId": { "Ref": "VpcPrivateSubnet3RouteTableD98824C7", }, }, "Type": "AWS::EC2::Route", }, "VpcPrivateSubnet3RouteTableAssociation16BDDC43": { "Properties": { "RouteTableId": { "Ref": "VpcPrivateSubnet3RouteTableD98824C7", }, "SubnetId": { "Ref": "VpcPrivateSubnet3SubnetF258B56E", }, }, "Type": "AWS::EC2::SubnetRouteTableAssociation", }, "VpcPrivateSubnet3RouteTableD98824C7": { "Properties": { "Tags": [ { "Key": "Name", "Value": "ServerlessWebappStarterKitStack/Vpc/PrivateSubnet3", }, ], "VpcId": { "Ref": "Vpc8378EB38", }, }, "Type": "AWS::EC2::RouteTable", }, "VpcPrivateSubnet3SubnetF258B56E": { "Properties": { "AvailabilityZone": "dummy1c", "CidrBlock": "10.0.160.0/19", "MapPublicIpOnLaunch": false, "Tags": [ { "Key": "aws-cdk:subnet-name", "Value": "Private", }, { "Key": "aws-cdk:subnet-type", "Value": "Private", }, { "Key": "Name", "Value": "ServerlessWebappStarterKitStack/Vpc/PrivateSubnet3", }, ], "VpcId": { "Ref": "Vpc8378EB38", }, }, "Type": "AWS::EC2::Subnet", }, "VpcPublicSubnet1DefaultRoute3DA9E72A": { "DependsOn": [ "VpcVPCGWBF912B6E", ], "Properties": { "DestinationCidrBlock": "0.0.0.0/0", "GatewayId": { "Ref": "VpcIGWD7BA715C", }, "RouteTableId": { "Ref": "VpcPublicSubnet1RouteTable6C95E38E", }, }, "Type": "AWS::EC2::Route", }, "VpcPublicSubnet1NatInstance57B636B8": { "DependsOn": [ "VpcPublicSubnet1DefaultRoute3DA9E72A", "VpcPublicSubnet1NatInstanceInstanceRole9D835E32", "VpcPublicSubnet1RouteTableAssociation97140677", ], "Properties": { "AvailabilityZone": "dummy1a", "IamInstanceProfile": { "Ref": "VpcPublicSubnet1NatInstanceInstanceProfileEE10C485", }, "ImageId": { "Ref": "SsmParameterValueawsserviceamiamazonlinuxlatestal2023amikernel61arm64C96584B6F00A464EAD1953AFF4B05118Parameter", }, "InstanceType": "t4g.nano", "NetworkInterfaces": [ { "AssociatePublicIpAddress": true, "DeviceIndex": "0", "GroupSet": [ { "Fn::GetAtt": [ "VpcNatSecurityGroup8DA26EDC", "GroupId", ], }, ], "SubnetId": { "Ref": "VpcPublicSubnet1Subnet5C2D37C4", }, }, ], "SourceDestCheck": false, "Tags": [ { "Key": "Name", "Value": "ServerlessWebappStarterKitStack/Vpc/PublicSubnet1/NatInstance", }, ], "UserData": { "Fn::Base64": "#!/bin/bash for i in {1..5}; do yum install iptables-services -y && break || sleep 10; done systemctl enable iptables systemctl start iptables echo "net.ipv4.ip_forward=1" > /etc/sysctl.d/custom-ip-forwarding.conf sysctl -p /etc/sysctl.d/custom-ip-forwarding.conf IFACE=$(ip route show default | awk '{print $5}') /sbin/iptables -t nat -A POSTROUTING -o $IFACE -j MASQUERADE /sbin/iptables -F FORWARD service iptables save", }, }, "Type": "AWS::EC2::Instance", }, "VpcPublicSubnet1NatInstanceInstanceProfileEE10C485": { "Properties": { "Roles": [ { "Ref": "VpcPublicSubnet1NatInstanceInstanceRole9D835E32", }, ], }, "Type": "AWS::IAM::InstanceProfile", }, "VpcPublicSubnet1NatInstanceInstanceRole9D835E32": { "Properties": { "AssumeRolePolicyDocument": { "Statement": [ { "Action": "sts:AssumeRole", "Effect": "Allow", "Principal": { "Service": "ec2.amazonaws.com", }, }, ], "Version": "2012-10-17", }, "Tags": [ { "Key": "Name", "Value": "ServerlessWebappStarterKitStack/Vpc/PublicSubnet1/NatInstance", }, ], }, "Type": "AWS::IAM::Role", }, "VpcPublicSubnet1RouteTable6C95E38E": { "Properties": { "Tags": [ { "Key": "Name", "Value": "ServerlessWebappStarterKitStack/Vpc/PublicSubnet1", }, ], "VpcId": { "Ref": "Vpc8378EB38", }, }, "Type": "AWS::EC2::RouteTable", }, "VpcPublicSubnet1RouteTableAssociation97140677": { "Properties": { "RouteTableId": { "Ref": "VpcPublicSubnet1RouteTable6C95E38E", }, "SubnetId": { "Ref": "VpcPublicSubnet1Subnet5C2D37C4", }, }, "Type": "AWS::EC2::SubnetRouteTableAssociation", }, "VpcPublicSubnet1Subnet5C2D37C4": { "Properties": { "AvailabilityZone": "dummy1a", "CidrBlock": "10.0.0.0/19", "MapPublicIpOnLaunch": true, "Tags": [ { "Key": "aws-cdk:subnet-name", "Value": "Public", }, { "Key": "aws-cdk:subnet-type", "Value": "Public", }, { "Key": "Name", "Value": "ServerlessWebappStarterKitStack/Vpc/PublicSubnet1", }, ], "VpcId": { "Ref": "Vpc8378EB38", }, }, "Type": "AWS::EC2::Subnet", }, "VpcPublicSubnet2DefaultRoute97F91067": { "DependsOn": [ "VpcVPCGWBF912B6E", ], "Properties": { "DestinationCidrBlock": "0.0.0.0/0", "GatewayId": { "Ref": "VpcIGWD7BA715C", }, "RouteTableId": { "Ref": "VpcPublicSubnet2RouteTable94F7E489", }, }, "Type": "AWS::EC2::Route", }, "VpcPublicSubnet2RouteTable94F7E489": { "Properties": { "Tags": [ { "Key": "Name", "Value": "ServerlessWebappStarterKitStack/Vpc/PublicSubnet2", }, ], "VpcId": { "Ref": "Vpc8378EB38", }, }, "Type": "AWS::EC2::RouteTable", }, "VpcPublicSubnet2RouteTableAssociationDD5762D8": { "Properties": { "RouteTableId": { "Ref": "VpcPublicSubnet2RouteTable94F7E489", }, "SubnetId": { "Ref": "VpcPublicSubnet2Subnet691E08A3", }, }, "Type": "AWS::EC2::SubnetRouteTableAssociation", }, "VpcPublicSubnet2Subnet691E08A3": { "Properties": { "AvailabilityZone": "dummy1b", "CidrBlock": "10.0.32.0/19", "MapPublicIpOnLaunch": true, "Tags": [ { "Key": "aws-cdk:subnet-name", "Value": "Public", }, { "Key": "aws-cdk:subnet-type", "Value": "Public", }, { "Key": "Name", "Value": "ServerlessWebappStarterKitStack/Vpc/PublicSubnet2", }, ], "VpcId": { "Ref": "Vpc8378EB38", }, }, "Type": "AWS::EC2::Subnet", }, "VpcPublicSubnet3DefaultRoute4697774F": { "DependsOn": [ "VpcVPCGWBF912B6E", ], "Properties": { "DestinationCidrBlock": "0.0.0.0/0", "GatewayId": { "Ref": "VpcIGWD7BA715C", }, "RouteTableId": { "Ref": "VpcPublicSubnet3RouteTable93458DBB", }, }, "Type": "AWS::EC2::Route", }, "VpcPublicSubnet3RouteTable93458DBB": { "Properties": { "Tags": [ { "Key": "Name", "Value": "ServerlessWebappStarterKitStack/Vpc/PublicSubnet3", }, ], "VpcId": { "Ref": "Vpc8378EB38", }, }, "Type": "AWS::EC2::RouteTable", }, "VpcPublicSubnet3RouteTableAssociation1F1EDF02": { "Properties": { "RouteTableId": { "Ref": "VpcPublicSubnet3RouteTable93458DBB", }, "SubnetId": { "Ref": "VpcPublicSubnet3SubnetBE12F0B6", }, }, "Type": "AWS::EC2::SubnetRouteTableAssociation", }, "VpcPublicSubnet3SubnetBE12F0B6": { "Properties": { "AvailabilityZone": "dummy1c", "CidrBlock": "10.0.64.0/19", "MapPublicIpOnLaunch": true, "Tags": [ { "Key": "aws-cdk:subnet-name", "Value": "Public", }, { "Key": "aws-cdk:subnet-type", "Value": "Public", }, { "Key": "Name", "Value": "ServerlessWebappStarterKitStack/Vpc/PublicSubnet3", }, ], "VpcId": { "Ref": "Vpc8378EB38", }, }, "Type": "AWS::EC2::Subnet", }, "VpcVPCGWBF912B6E": { "Properties": { "InternetGatewayId": { "Ref": "VpcIGWD7BA715C", }, "VpcId": { "Ref": "Vpc8378EB38", }, }, "Type": "AWS::EC2::VPCGatewayAttachment", }, "Webapp107041BD": { "Properties": { "DistributionConfig": { "Comment": "CloudFront for Webapp", "DefaultCacheBehavior": { "AllowedMethods": [ "GET", "HEAD", "OPTIONS", "PUT", "PATCH", "POST", "DELETE", ], "CachePolicyId": { "Ref": "WebappSharedCachePolicy14FEE4A0", }, "Compress": true, "FunctionAssociations": [ { "EventType": "viewer-request", "FunctionARN": { "Fn::GetAtt": [ "WebappCacheKeyFunction6C227CE2", "FunctionARN", ], }, }, ], "LambdaFunctionAssociations": [ { "EventType": "origin-request", "IncludeBody": true, "LambdaFunctionARN": { "Fn::GetAtt": [ "LookupVersionArnc8730278af02f875114ca902814c77b68f19b0087110E04D0A", "Parameter.Value", ], }, }, ], "OriginRequestPolicyId": "b689b0a8-53d0-40ab-baf2-68738e2966ac", "TargetOriginId": "ServerlessWebappStarterKitStackWebappOrigin1D7B867FF", "ViewerProtocolPolicy": "allow-all", }, "Enabled": true, "HttpVersion": "http2", "IPV6Enabled": true, "Logging": { "Bucket": { "Fn::GetAtt": [ "AccessLogBucketDA470295", "RegionalDomainName", ], }, "Prefix": "Webapp/", }, "Origins": [ { "ConnectionTimeout": 6, "CustomOriginConfig": { "OriginProtocolPolicy": "https-only", "OriginReadTimeout": 60, "OriginSSLProtocols": [ "TLSv1.2", ], }, "DomainName": { "Fn::Select": [ 2, { "Fn::Split": [ "/", { "Fn::GetAtt": [ "WebappHandlerFunctionUrl7AEF8DEE", "FunctionUrl", ], }, ], }, ], }, "Id": "ServerlessWebappStarterKitStackWebappOrigin1D7B867FF", "OriginAccessControlId": { "Fn::GetAtt": [ "WebappOrigin1FunctionUrlOriginAccessControlEA98B600", "Id", ], }, }, ], }, }, "Type": "AWS::CloudFront::Distribution", }, "WebappBuild348806E3": { "DeletionPolicy": "Delete", "DependsOn": [ "ContainerImageBuildArm64e83729feb1564e709bec452b15847a30amd64C13E3549", "ContainerImageBuildArm64e83729feb1564e709bec452b15847a30amd64RoleDefaultPolicy2316728F", "ContainerImageBuildArm64e83729feb1564e709bec452b15847a30amd64RoleC5F7BBFE", ], "Properties": { "ServiceToken": { "Fn::GetAtt": [ "DeployTimeBuildCustomResourceHandlerdb740fd554364a848a09e6dfcd01f4f306AEFF37", "Arn", ], }, "buildCommand": { "Fn::Join": [ "", [ "docker buildx build --build-arg ALLOWED_ORIGIN_HOST=*.cloudfront.net --build-arg SKIP_TS_BUILD=true --build-arg NEXT_PUBLIC_EVENT_HTTP_ENDPOINT=https://", { "Fn::GetAtt": [ "EventBusApi6E8C7C94", "Dns.Http", ], }, " --build-arg NEXT_PUBLIC_AWS_REGION=us-west-2 --platform linux/arm64 --output type=image,name=", { "Fn::Select": [ 4, { "Fn::Split": [ ":", { "Fn::GetAtt": [ "WebappBuildRepository4C93D48D", "Arn", ], }, ], }, ], }, ".dkr.ecr.", { "Fn::Select": [ 3, { "Fn::Split": [ ":", { "Fn::GetAtt": [ "WebappBuildRepository4C93D48D", "Arn", ], }, ], }, ], }, ".", { "Ref": "AWS::URLSuffix", }, "/", { "Ref": "WebappBuildRepository4C93D48D", }, ":,push=true --provenance=false .", ], ], }, "codeBuildProjectName": { "Ref": "ContainerImageBuildArm64e83729feb1564e709bec452b15847a30amd64C13E3549", }, "repositoryUri": { "Fn::Join": [ "", [ { "Fn::Select": [ 4, { "Fn::Split": [ ":", { "Fn::GetAtt": [ "WebappBuildRepository4C93D48D", "Arn", ], }, ], }, ], }, ".dkr.ecr.", { "Fn::Select": [ 3, { "Fn::Split": [ ":", { "Fn::GetAtt": [ "WebappBuildRepository4C93D48D", "Arn", ], }, ], }, ], }, ".", { "Ref": "AWS::URLSuffix", }, "/", { "Ref": "WebappBuildRepository4C93D48D", }, ], ], }, "sourceS3Url": "s3://cdk-hnb659fds-assets-123456789012-us-west-2/REDACTED", "tagPrefix": "REDACTED", "type": "ContainerImageBuild", }, "Type": "Custom::CDKContainerImageBuild", "UpdateReplacePolicy": "Delete", }, "WebappBuildRepository4C93D48D": { "DeletionPolicy": "Delete", "Properties": { "EmptyOnDelete": true, "RepositoryPolicyText": { "Statement": [ { "Action": [ "ecr:BatchCheckLayerAvailability", "ecr:GetDownloadUrlForLayer", "ecr:BatchGetImage", ], "Effect": "Allow", "Principal": { "Service": "lambda.amazonaws.com", }, }, ], "Version": "2012-10-17", }, }, "Type": "AWS::ECR::Repository", "UpdateReplacePolicy": "Delete", }, "WebappCacheKeyFunction6C227CE2": { "Properties": { "AutoPublish": true, "FunctionCode": "// CloudFront Functions JS 2.0 // Combines Next.js RSC-related headers into a single hashed cache key header // to prevent cache poisoning between HTML and RSC flight responses. // // Next.js App Router sets Vary: rsc, next-router-state-tree, next-router-prefetch, // next-router-segment-prefetch (and next-url for interception routes). // CloudFront ignores Vary and requires explicit cache key configuration, // but its Cache Policy has a 10-header limit. This function hashes all // RSC headers into one header to stay within the limit. async function handler(event) { var h = event.request.headers; var parts = [ 'rsc', 'next-router-prefetch', 'next-router-state-tree', 'next-router-segment-prefetch', 'next-url', ]; var key = ''; for (var i = 0; i < parts.length; i++) { if (h[parts[i]]) { key += parts[i] + '=' + h[parts[i]].value + ';'; } } if (key) { // FNV-1a hash (32-bit). Cryptographic strength is unnecessary; // we only need distinct cache keys for distinct header combinations. // See: https://en.wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function var FNV_OFFSET_BASIS = 2166136261; var FNV_PRIME = 16777619; var hash = FNV_OFFSET_BASIS; for (var j = 0; j < key.length; j++) { hash ^= key.charCodeAt(j); hash = (hash * FNV_PRIME) | 0; } event.request.headers['x-nextjs-cache-key'] = { value: String(hash >>> 0) }; } return event.request; } ", "FunctionConfig": { "Comment": "us-west-2ServerlessWebappCacheKeyFunction86D1ABE9", "Runtime": "cloudfront-js-2.0", }, "Name": "us-west-2ServerlessWebappCacheKeyFunction86D1ABE9", }, "Type": "AWS::CloudFront::Function", }, "WebappCloudFrontInvalidation588CF152": { "DeletionPolicy": "Delete", "DependsOn": [ "WebappCloudFrontInvalidationCustomResourcePolicy18C215D6", ], "Properties": { "Create": { "Fn::Join": [ "", [ "{"service":"cloudfront","action":"createInvalidation","parameters":{"DistributionId":"", { "Ref": "Webapp107041BD", }, "","InvalidationBatch":{"CallerReference":"", { "Fn::GetAtt": [ "WebappHandlerCurrentVersionREDACTED", "Version", ], }, "","Paths":{"Quantity":1,"Items":["/*"]}}},"physicalResourceId":{"id":"invalidation"}}", ], ], }, "InstallLatestAwsSdk": true, "ServiceToken": { "Fn::GetAtt": [ "AWS679f53fac002430cb0da5b7982bd22872D164C4C", "Arn", ], }, "Update": { "Fn::Join": [ "", [ "{"service":"cloudfront","action":"createInvalidation","parameters":{"DistributionId":"", { "Ref": "Webapp107041BD", }, "","InvalidationBatch":{"CallerReference":"", { "Fn::GetAtt": [ "WebappHandlerCurrentVersionREDACTED", "Version", ], }, "","Paths":{"Quantity":1,"Items":["/*"]}}},"physicalResourceId":{"id":"invalidation"}}", ], ], }, }, "Type": "Custom::AWS", "UpdateReplacePolicy": "Delete", }, "WebappCloudFrontInvalidationCustomResourcePolicy18C215D6": { "Properties": { "PolicyDocument": { "Statement": [ { "Action": "cloudfront:CreateInvalidation", "Effect": "Allow", "Resource": { "Fn::Join": [ "", [ "arn:", { "Ref": "AWS::Partition", }, ":cloudfront::123456789012:distribution/", { "Ref": "Webapp107041BD", }, ], ], }, }, ], "Version": "2012-10-17", }, "PolicyName": "WebappCloudFrontInvalidationCustomResourcePolicy18C215D6", "Roles": [ { "Ref": "AWS679f53fac002430cb0da5b7982bd2287ServiceRoleC1EA0FF2", }, ], }, "Type": "AWS::IAM::Policy", }, "WebappHandler8DD158A3": { "DependsOn": [ "VpcPrivateSubnet1DefaultRouteBE02A9ED", "VpcPrivateSubnet1RouteTableAssociation70C59FA6", "VpcPrivateSubnet2DefaultRoute060D2087", "VpcPrivateSubnet2RouteTableAssociationA89CAD56", "VpcPrivateSubnet3DefaultRoute94B74F0D", "VpcPrivateSubnet3RouteTableAssociation16BDDC43", "WebappHandlerServiceRoleDefaultPolicy7D06F4EA", "WebappHandlerServiceRole4F4D1ACD", ], "Properties": { "Architectures": [ "arm64", ], "Code": { "ImageUri": { "Fn::Join": [ "", [ { "Fn::Select": [ 4, { "Fn::Split": [ ":", { "Fn::GetAtt": [ "WebappBuildRepository4C93D48D", "Arn", ], }, ], }, ], }, ".dkr.ecr.", { "Fn::Select": [ 3, { "Fn::Split": [ ":", { "Fn::GetAtt": [ "WebappBuildRepository4C93D48D", "Arn", ], }, ], }, ], }, ".", { "Ref": "AWS::URLSuffix", }, "/", { "Ref": "WebappBuildRepository4C93D48D", }, ":", { "Fn::GetAtt": [ "WebappBuild348806E3", "ImageTag", ], }, ], ], }, }, "Environment": { "Variables": { "AMPLIFY_APP_ORIGIN_SOURCE_PARAMETER": { "Ref": "WebappOriginSourceParameterD87E143B", }, "ASYNC_JOB_HANDLER_ARN": { "Fn::GetAtt": [ "AsyncJobHandler438266BD", "Arn", ], }, "COGNITO_DOMAIN": { "Fn::Join": [ "", [ { "Fn::GetAtt": [ "AuthDomainPrefixE1742B23", "generated", ], }, ".auth.us-west-2.amazoncognito.com", ], ], }, "DATABASE_ENGINE": { "Fn::Join": [ "", [ "{{resolve:secretsmanager:", { "Ref": "DatabaseClusterSecretAttachmentDC8466C0", }, ":SecretString:engine::}}", ], ], }, "DATABASE_HOST": { "Fn::GetAtt": [ "DatabaseCluster5B53A178", "Endpoint.Address", ], }, "DATABASE_NAME": "main", "DATABASE_OPTION": "?connection_limit=1&connect_timeout=30", "DATABASE_PASSWORD": { "Fn::Join": [ "", [ "{{resolve:secretsmanager:", { "Ref": "DatabaseClusterSecretAttachmentDC8466C0", }, ":SecretString:password::}}", ], ], }, "DATABASE_PORT": { "Fn::GetAtt": [ "DatabaseCluster5B53A178", "Endpoint.Port", ], }, "DATABASE_URL": { "Fn::Join": [ "", [ "{{resolve:secretsmanager:", { "Ref": "DatabaseClusterSecretAttachmentDC8466C0", }, ":SecretString:engine::}}://{{resolve:secretsmanager:", { "Ref": "DatabaseClusterSecretAttachmentDC8466C0", }, ":SecretString:username::}}:{{resolve:secretsmanager:", { "Ref": "DatabaseClusterSecretAttachmentDC8466C0", }, ":SecretString:password::}}@", { "Fn::GetAtt": [ "DatabaseCluster5B53A178", "Endpoint.Address", ], }, ":", { "Fn::GetAtt": [ "DatabaseCluster5B53A178", "Endpoint.Port", ], }, "/main?connection_limit=1&connect_timeout=30", ], ], }, "DATABASE_USER": { "Fn::Join": [ "", [ "{{resolve:secretsmanager:", { "Ref": "DatabaseClusterSecretAttachmentDC8466C0", }, ":SecretString:username::}}", ], ], }, "USER_POOL_CLIENT_ID": { "Ref": "AuthUserPoolClientC635291F", }, "USER_POOL_ID": { "Ref": "AuthUserPool8115E87F", }, }, }, "LoggingConfig": { "LogGroup": { "Ref": "WebappHandlerLogs87A6D2D7", }, }, "MemorySize": 1024, "PackageType": "Image", "Role": { "Fn::GetAtt": [ "WebappHandlerServiceRole4F4D1ACD", "Arn", ], }, "Timeout": 180, "VpcConfig": { "SecurityGroupIds": [ { "Fn::GetAtt": [ "WebappHandlerSecurityGroup5451B519", "GroupId", ], }, ], "SubnetIds": [ { "Ref": "VpcPrivateSubnet1Subnet536B997A", }, { "Ref": "VpcPrivateSubnet2Subnet3788AAA1", }, { "Ref": "VpcPrivateSubnet3SubnetF258B56E", }, ], }, }, "Type": "AWS::Lambda::Function", }, "WebappHandlerCurrentVersionREDACTED": { "DependsOn": [ "VpcPrivateSubnet1DefaultRouteBE02A9ED", "VpcPrivateSubnet1RouteTableAssociation70C59FA6", "VpcPrivateSubnet2DefaultRoute060D2087", "VpcPrivateSubnet2RouteTableAssociationA89CAD56", "VpcPrivateSubnet3DefaultRoute94B74F0D", "VpcPrivateSubnet3RouteTableAssociation16BDDC43", ], "Properties": { "FunctionName": { "Ref": "WebappHandler8DD158A3", }, }, "Type": "AWS::Lambda::Version", }, "WebappHandlerFunctionUrl7AEF8DEE": { "DependsOn": [ "VpcPrivateSubnet1DefaultRouteBE02A9ED", "VpcPrivateSubnet1RouteTableAssociation70C59FA6", "VpcPrivateSubnet2DefaultRoute060D2087", "VpcPrivateSubnet2RouteTableAssociationA89CAD56", "VpcPrivateSubnet3DefaultRoute94B74F0D", "VpcPrivateSubnet3RouteTableAssociation16BDDC43", ], "Properties": { "AuthType": "AWS_IAM", "InvokeMode": "RESPONSE_STREAM", "TargetFunctionArn": { "Fn::GetAtt": [ "WebappHandler8DD158A3", "Arn", ], }, }, "Type": "AWS::Lambda::Url", }, "WebappHandlerLogs87A6D2D7": { "DeletionPolicy": "Delete", "Properties": { "RetentionInDays": 7, }, "Type": "AWS::Logs::LogGroup", "UpdateReplacePolicy": "Delete", }, "WebappHandlerSecurityGroup5451B519": { "DependsOn": [ "VpcPrivateSubnet1DefaultRouteBE02A9ED", "VpcPrivateSubnet1RouteTableAssociation70C59FA6", "VpcPrivateSubnet2DefaultRoute060D2087", "VpcPrivateSubnet2RouteTableAssociationA89CAD56", "VpcPrivateSubnet3DefaultRoute94B74F0D", "VpcPrivateSubnet3RouteTableAssociation16BDDC43", ], "Properties": { "GroupDescription": "Automatic security group for Lambda Function ServerlessWebappStarterKitStackWebappHandlerF1A4ACC9", "SecurityGroupEgress": [ { "CidrIp": "0.0.0.0/0", "Description": "Allow all outbound traffic by default", "IpProtocol": "-1", }, ], "VpcId": { "Ref": "Vpc8378EB38", }, }, "Type": "AWS::EC2::SecurityGroup", }, "WebappHandlerServiceRole4F4D1ACD": { "DependsOn": [ "VpcPrivateSubnet1DefaultRouteBE02A9ED", "VpcPrivateSubnet1RouteTableAssociation70C59FA6", "VpcPrivateSubnet2DefaultRoute060D2087", "VpcPrivateSubnet2RouteTableAssociationA89CAD56", "VpcPrivateSubnet3DefaultRoute94B74F0D", "VpcPrivateSubnet3RouteTableAssociation16BDDC43", ], "Properties": { "AssumeRolePolicyDocument": { "Statement": [ { "Action": "sts:AssumeRole", "Effect": "Allow", "Principal": { "Service": "lambda.amazonaws.com", }, }, ], "Version": "2012-10-17", }, "ManagedPolicyArns": [ { "Fn::Join": [ "", [ "arn:", { "Ref": "AWS::Partition", }, ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole", ], ], }, { "Fn::Join": [ "", [ "arn:", { "Ref": "AWS::Partition", }, ":iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole", ], ], }, ], }, "Type": "AWS::IAM::Role", }, "WebappHandlerServiceRoleDefaultPolicy7D06F4EA": { "DependsOn": [ "VpcPrivateSubnet1DefaultRouteBE02A9ED", "VpcPrivateSubnet1RouteTableAssociation70C59FA6", "VpcPrivateSubnet2DefaultRoute060D2087", "VpcPrivateSubnet2RouteTableAssociationA89CAD56", "VpcPrivateSubnet3DefaultRoute94B74F0D", "VpcPrivateSubnet3RouteTableAssociation16BDDC43", ], "Properties": { "PolicyDocument": { "Statement": [ { "Action": "lambda:InvokeFunction", "Effect": "Allow", "Resource": [ { "Fn::GetAtt": [ "AsyncJobHandler438266BD", "Arn", ], }, { "Fn::Join": [ "", [ { "Fn::GetAtt": [ "AsyncJobHandler438266BD", "Arn", ], }, ":*", ], ], }, ], }, { "Action": [ "ssm:DescribeParameters", "ssm:GetParameters", "ssm:GetParameter", "ssm:GetParameterHistory", ], "Effect": "Allow", "Resource": { "Fn::Join": [ "", [ "arn:", { "Ref": "AWS::Partition", }, ":ssm:us-west-2:123456789012:parameter/", { "Ref": "WebappOriginSourceParameterD87E143B", }, ], ], }, }, ], "Version": "2012-10-17", }, "PolicyName": "WebappHandlerServiceRoleDefaultPolicy7D06F4EA", "Roles": [ { "Ref": "WebappHandlerServiceRole4F4D1ACD", }, ], }, "Type": "AWS::IAM::Policy", }, "WebappInvokeFunctionPermission8F3F2610": { "Properties": { "Action": "lambda:InvokeFunction", "FunctionName": { "Fn::GetAtt": [ "WebappHandler8DD158A3", "Arn", ], }, "Principal": "cloudfront.amazonaws.com", "SourceArn": { "Fn::Join": [ "", [ "arn:", { "Ref": "AWS::Partition", }, ":cloudfront::", { "Ref": "AWS::AccountId", }, ":distribution/", { "Ref": "Webapp107041BD", }, ], ], }, }, "Type": "AWS::Lambda::Permission", }, "WebappMigrationRunnerAC67C012": { "DependsOn": [ "VpcPrivateSubnet1DefaultRouteBE02A9ED", "VpcPrivateSubnet1RouteTableAssociation70C59FA6", "VpcPrivateSubnet2DefaultRoute060D2087", "VpcPrivateSubnet2RouteTableAssociationA89CAD56", "VpcPrivateSubnet3DefaultRoute94B74F0D", "VpcPrivateSubnet3RouteTableAssociation16BDDC43", "WebappMigrationRunnerServiceRoleE27E1F7A", ], "Properties": { "Architectures": [ "arm64", ], "Code": { "ImageUri": { "Fn::Sub": "REDACTED", }, }, "Environment": { "Variables": { "DATABASE_ENGINE": { "Fn::Join": [ "", [ "{{resolve:secretsmanager:", { "Ref": "DatabaseClusterSecretAttachmentDC8466C0", }, ":SecretString:engine::}}", ], ], }, "DATABASE_HOST": { "Fn::GetAtt": [ "DatabaseCluster5B53A178", "Endpoint.Address", ], }, "DATABASE_NAME": "main", "DATABASE_OPTION": "?connection_limit=1&connect_timeout=30", "DATABASE_PASSWORD": { "Fn::Join": [ "", [ "{{resolve:secretsmanager:", { "Ref": "DatabaseClusterSecretAttachmentDC8466C0", }, ":SecretString:password::}}", ], ], }, "DATABASE_PORT": { "Fn::GetAtt": [ "DatabaseCluster5B53A178", "Endpoint.Port", ], }, "DATABASE_URL": { "Fn::Join": [ "", [ "{{resolve:secretsmanager:", { "Ref": "DatabaseClusterSecretAttachmentDC8466C0", }, ":SecretString:engine::}}://{{resolve:secretsmanager:", { "Ref": "DatabaseClusterSecretAttachmentDC8466C0", }, ":SecretString:username::}}:{{resolve:secretsmanager:", { "Ref": "DatabaseClusterSecretAttachmentDC8466C0", }, ":SecretString:password::}}@", { "Fn::GetAtt": [ "DatabaseCluster5B53A178", "Endpoint.Address", ], }, ":", { "Fn::GetAtt": [ "DatabaseCluster5B53A178", "Endpoint.Port", ], }, "/main?connection_limit=1&connect_timeout=30", ], ], }, "DATABASE_USER": { "Fn::Join": [ "", [ "{{resolve:secretsmanager:", { "Ref": "DatabaseClusterSecretAttachmentDC8466C0", }, ":SecretString:username::}}", ], ], }, }, }, "ImageConfig": { "Command": [ "migration-runner.handler", ], }, "LoggingConfig": { "LogGroup": { "Ref": "WebappMigrationRunnerLogsD9A84B90", }, }, "MemorySize": 256, "PackageType": "Image", "Role": { "Fn::GetAtt": [ "WebappMigrationRunnerServiceRoleE27E1F7A", "Arn", ], }, "Timeout": 300, "VpcConfig": { "SecurityGroupIds": [ { "Fn::GetAtt": [ "WebappMigrationRunnerSecurityGroup7F0DF264", "GroupId", ], }, ], "SubnetIds": [ { "Ref": "VpcPrivateSubnet1Subnet536B997A", }, { "Ref": "VpcPrivateSubnet2Subnet3788AAA1", }, { "Ref": "VpcPrivateSubnet3SubnetF258B56E", }, ], }, }, "Type": "AWS::Lambda::Function", }, "WebappMigrationRunnerCurrentVersionREDACTED": { "DependsOn": [ "VpcPrivateSubnet1DefaultRouteBE02A9ED", "VpcPrivateSubnet1RouteTableAssociation70C59FA6", "VpcPrivateSubnet2DefaultRoute060D2087", "VpcPrivateSubnet2RouteTableAssociationA89CAD56", "VpcPrivateSubnet3DefaultRoute94B74F0D", "VpcPrivateSubnet3RouteTableAssociation16BDDC43", ], "Properties": { "FunctionName": { "Ref": "WebappMigrationRunnerAC67C012", }, }, "Type": "AWS::Lambda::Version", }, "WebappMigrationRunnerLogsD9A84B90": { "DeletionPolicy": "Delete", "Properties": { "RetentionInDays": 7, }, "Type": "AWS::Logs::LogGroup", "UpdateReplacePolicy": "Delete", }, "WebappMigrationRunnerSecurityGroup7F0DF264": { "DependsOn": [ "VpcPrivateSubnet1DefaultRouteBE02A9ED", "VpcPrivateSubnet1RouteTableAssociation70C59FA6", "VpcPrivateSubnet2DefaultRoute060D2087", "VpcPrivateSubnet2RouteTableAssociationA89CAD56", "VpcPrivateSubnet3DefaultRoute94B74F0D", "VpcPrivateSubnet3RouteTableAssociation16BDDC43", ], "Properties": { "GroupDescription": "Automatic security group for Lambda Function ServerlessWebappStarterKitStackWebappMigrationRunner45EAC73E", "SecurityGroupEgress": [ { "CidrIp": "0.0.0.0/0", "Description": "Allow all outbound traffic by default", "IpProtocol": "-1", }, ], "VpcId": { "Ref": "Vpc8378EB38", }, }, "Type": "AWS::EC2::SecurityGroup", }, "WebappMigrationRunnerServiceRoleE27E1F7A": { "DependsOn": [ "VpcPrivateSubnet1DefaultRouteBE02A9ED", "VpcPrivateSubnet1RouteTableAssociation70C59FA6", "VpcPrivateSubnet2DefaultRoute060D2087", "VpcPrivateSubnet2RouteTableAssociationA89CAD56", "VpcPrivateSubnet3DefaultRoute94B74F0D", "VpcPrivateSubnet3RouteTableAssociation16BDDC43", ], "Properties": { "AssumeRolePolicyDocument": { "Statement": [ { "Action": "sts:AssumeRole", "Effect": "Allow", "Principal": { "Service": "lambda.amazonaws.com", }, }, ], "Version": "2012-10-17", }, "ManagedPolicyArns": [ { "Fn::Join": [ "", [ "arn:", { "Ref": "AWS::Partition", }, ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole", ], ], }, { "Fn::Join": [ "", [ "arn:", { "Ref": "AWS::Partition", }, ":iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole", ], ], }, ], }, "Type": "AWS::IAM::Role", }, "WebappMigrationTrigger42AFC1D9": { "DeletionPolicy": "Delete", "DependsOn": [ "DatabaseClusterLogRetentionpostgresql025D39CE", "DatabaseCluster5B53A178", "DatabaseClusterSecretAttachmentDC8466C0", "DatabaseClusterSecretD1FB634F", "DatabaseClusterSecurityGroupfromServerlessWebappStarterKitStackAsyncJobHandlerSecurityGroup5220DFB3IndirectPort9323962E", "DatabaseClusterSecurityGroupfromServerlessWebappStarterKitStackDatabaseBastionHostInstanceSecurityGroup4F0DD25BIndirectPort8AFD9922", "DatabaseClusterSecurityGroupfromServerlessWebappStarterKitStackWebappHandlerSecurityGroupA009CF4AIndirectPort7311063E", "DatabaseClusterSecurityGroupfromServerlessWebappStarterKitStackWebappMigrationRunnerSecurityGroupC0959349IndirectPortDCF4A356", "DatabaseClusterSecurityGroupFEF1426A", "DatabaseClusterSubnets5540150D", "DatabaseClusterWriterD43085C6", ], "Properties": { "ExecuteOnHandlerChange": true, "HandlerArn": { "Ref": "WebappMigrationRunnerCurrentVersionREDACTED", }, "InvocationType": "RequestResponse", "ServiceToken": { "Fn::GetAtt": [ "AWSCDKTriggerCustomResourceProviderCustomResourceProviderHandler97BECD91", "Arn", ], }, "Timeout": "120000", }, "Type": "Custom::Trigger", "UpdateReplacePolicy": "Delete", }, "WebappOrigin1FunctionUrlOriginAccessControlEA98B600": { "Properties": { "OriginAccessControlConfig": { "Name": "ServerlessWebappStarterKitStnctionUrlOriginAccessControl17EB4E66", "OriginAccessControlOriginType": "lambda", "SigningBehavior": "always", "SigningProtocol": "sigv4", }, }, "Type": "AWS::CloudFront::OriginAccessControl", }, "WebappOrigin1InvokeFromApiForServerlessWebappStarterKitStackWebappOrigin1D7B867FF58DBB477": { "Properties": { "Action": "lambda:InvokeFunctionUrl", "FunctionName": { "Fn::GetAtt": [ "WebappHandlerFunctionUrl7AEF8DEE", "FunctionArn", ], }, "Principal": "cloudfront.amazonaws.com", "SourceArn": { "Fn::Join": [ "", [ "arn:", { "Ref": "AWS::Partition", }, ":cloudfront::", { "Ref": "AWS::AccountId", }, ":distribution/", { "Ref": "Webapp107041BD", }, ], ], }, }, "Type": "AWS::Lambda::Permission", }, "WebappOriginSourceParameterD87E143B": { "Properties": { "Type": "String", "Value": "dummy", }, "Type": "AWS::SSM::Parameter", }, "WebappSharedCachePolicy14FEE4A0": { "Properties": { "CachePolicyConfig": { "DefaultTTL": 0, "MaxTTL": 31536000, "MinTTL": 0, "Name": "ServerlessWebappStarterKitStackWebappSharedCachePolicy211E133B-us-west-2", "ParametersInCacheKeyAndForwardedToOrigin": { "CookiesConfig": { "CookieBehavior": "all", }, "EnableAcceptEncodingBrotli": true, "EnableAcceptEncodingGzip": true, "HeadersConfig": { "HeaderBehavior": "whitelist", "Headers": [ "authorization", "Origin", "X-HTTP-Method-Override", "X-HTTP-Method", "X-Method-Override", "x-nextjs-cache-key", ], }, "QueryStringsConfig": { "QueryStringBehavior": "all", }, }, }, }, "Type": "AWS::CloudFront::CachePolicy", }, "WebappUpdateAmplifyOriginSourceParameter3F609A08": { "DeletionPolicy": "Delete", "DependsOn": [ "WebappUpdateAmplifyOriginSourceParameterCustomResourcePolicy5D80E122", ], "Properties": { "Create": { "Fn::Join": [ "", [ "{"service":"ssm","action":"putParameter","parameters":{"Name":"", { "Ref": "WebappOriginSourceParameterD87E143B", }, "","Value":"https://", { "Fn::GetAtt": [ "Webapp107041BD", "DomainName", ], }, "","Overwrite":true},"physicalResourceId":{"id":"", { "Ref": "WebappOriginSourceParameterD87E143B", }, ""}}", ], ], }, "InstallLatestAwsSdk": true, "ServiceToken": { "Fn::GetAtt": [ "AWS679f53fac002430cb0da5b7982bd22872D164C4C", "Arn", ], }, "Update": { "Fn::Join": [ "", [ "{"service":"ssm","action":"putParameter","parameters":{"Name":"", { "Ref": "WebappOriginSourceParameterD87E143B", }, "","Value":"https://", { "Fn::GetAtt": [ "Webapp107041BD", "DomainName", ], }, "","Overwrite":true},"physicalResourceId":{"id":"", { "Ref": "WebappOriginSourceParameterD87E143B", }, ""}}", ], ], }, }, "Type": "Custom::AWS", "UpdateReplacePolicy": "Delete", }, "WebappUpdateAmplifyOriginSourceParameterCustomResourcePolicy5D80E122": { "Properties": { "PolicyDocument": { "Statement": [ { "Action": "ssm:PutParameter", "Effect": "Allow", "Resource": { "Fn::Join": [ "", [ "arn:", { "Ref": "AWS::Partition", }, ":ssm:us-west-2:123456789012:parameter/", { "Ref": "WebappOriginSourceParameterD87E143B", }, ], ], }, }, ], "Version": "2012-10-17", }, "PolicyName": "WebappUpdateAmplifyOriginSourceParameterCustomResourcePolicy5D80E122", "Roles": [ { "Ref": "AWS679f53fac002430cb0da5b7982bd2287ServiceRoleC1EA0FF2", }, ], }, "Type": "AWS::IAM::Policy", }, }, "Rules": { "CheckBootstrapVersion": { "Assertions": [ { "Assert": { "Fn::Not": [ { "Fn::Contains": [ [ "1", "2", "3", "4", "5", ], { "Ref": "BootstrapVersion", }, ], }, ], }, "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI.", }, ], }, }, } `; ================================================ FILE: cdk/test/__snapshots__/serverless-fullstack-webapp-starter-kit.test.ts.snap ================================================ // Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing exports[`Snapshot test 1`] = ` { "Parameters": { "BootstrapVersion": { "Default": "/cdk-bootstrap/hnb659fds/version", "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]", "Type": "AWS::SSM::Parameter::Value", }, }, "Resources": { "CertificateV29EE77EAF": { "Properties": { "DomainName": "*.example.com", "DomainValidationOptions": [ { "DomainName": "example.com", "HostedZoneId": "DUMMY", }, ], "SubjectAlternativeNames": [ "example.com", ], "Tags": [ { "Key": "Name", "Value": "ServerlessWebappStarterKitUsEast1Stack/CertificateV2", }, ], "ValidationMethod": "DNS", }, "Type": "AWS::CertificateManager::Certificate", }, "CustomCrossRegionExportWriterCustomResourceProviderHandlerD8786E8A": { "DependsOn": [ "CustomCrossRegionExportWriterCustomResourceProviderRoleC951B1E1", ], "Properties": { "Code": { "S3Bucket": "cdk-hnb659fds-assets-123456789012-us-east-1", "S3Key": "REDACTED", }, "Handler": "__entrypoint__.handler", "MemorySize": 128, "Role": { "Fn::GetAtt": [ "CustomCrossRegionExportWriterCustomResourceProviderRoleC951B1E1", "Arn", ], }, "Runtime": "nodejs22.x", "Timeout": 900, }, "Type": "AWS::Lambda::Function", }, "CustomCrossRegionExportWriterCustomResourceProviderRoleC951B1E1": { "Properties": { "AssumeRolePolicyDocument": { "Statement": [ { "Action": "sts:AssumeRole", "Effect": "Allow", "Principal": { "Service": "lambda.amazonaws.com", }, }, ], "Version": "2012-10-17", }, "ManagedPolicyArns": [ { "Fn::Sub": "arn:\${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole", }, ], "Policies": [ { "PolicyDocument": { "Statement": [ { "Action": [ "ssm:DeleteParameters", "ssm:ListTagsForResource", "ssm:GetParameters", "ssm:PutParameter", ], "Effect": "Allow", "Resource": [ { "Fn::Join": [ "", [ "arn:", { "Ref": "AWS::Partition", }, ":ssm:us-west-2:123456789012:parameter/cdk/exports/*", ], ], }, ], }, ], "Version": "2012-10-17", }, "PolicyName": "Inline", }, ], }, "Type": "AWS::IAM::Role", }, "ExportsWriteruswest209BD44F0A7CF058B": { "DeletionPolicy": "Delete", "Properties": { "ServiceToken": { "Fn::GetAtt": [ "CustomCrossRegionExportWriterCustomResourceProviderHandlerD8786E8A", "Arn", ], }, "WriterProps": { "exports": { "/cdk/exports/ServerlessWebappStarterKitStack/ServerlessWebappStarterKitUsEast1Stackuseast1RefCertificateV29EE77EAF5C85697F": { "Ref": "CertificateV29EE77EAF", }, "/cdk/exports/ServerlessWebappStarterKitStack/ServerlessWebappStarterKitUsEast1Stackuseast1RefSignPayloadHandlerFunctionVersionF9FE430A3006B9FA": { "Ref": "SignPayloadHandlerFunctionVersionF9FE430A", }, }, "region": "us-west-2", }, }, "Type": "Custom::CrossRegionExportWriter", "UpdateReplacePolicy": "Delete", }, "Record83264F3E": { "Properties": { "HostedZoneId": "DUMMY", "Name": "example.com.", "ResourceRecords": [ "8.8.8.8", ], "TTL": "1800", "Type": "A", }, "Type": "AWS::Route53::RecordSet", }, "SignPayloadHandlerCFEAA14C": { "DependsOn": [ "SignPayloadHandlerServiceRole29261232", ], "Properties": { "Code": { "S3Bucket": "cdk-hnb659fds-assets-123456789012-us-east-1", "S3Key": "REDACTED", }, "Handler": "index.handler", "Role": { "Fn::GetAtt": [ "SignPayloadHandlerServiceRole29261232", "Arn", ], }, "Runtime": "nodejs22.x", }, "Type": "AWS::Lambda::Function", }, "SignPayloadHandlerCurrentVersionREDACTED": { "Properties": { "FunctionName": { "Ref": "SignPayloadHandlerCFEAA14C", }, }, "Type": "AWS::Lambda::Version", }, "SignPayloadHandlerFunctionVersionF9FE430A": { "Properties": { "Type": "String", "Value": { "Ref": "SignPayloadHandlerCurrentVersionREDACTED", }, }, "Type": "AWS::SSM::Parameter", }, "SignPayloadHandlerServiceRole29261232": { "Properties": { "AssumeRolePolicyDocument": { "Statement": [ { "Action": "sts:AssumeRole", "Effect": "Allow", "Principal": { "Service": "lambda.amazonaws.com", }, }, { "Action": "sts:AssumeRole", "Effect": "Allow", "Principal": { "Service": "edgelambda.amazonaws.com", }, }, ], "Version": "2012-10-17", }, "ManagedPolicyArns": [ { "Fn::Join": [ "", [ "arn:", { "Ref": "AWS::Partition", }, ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole", ], ], }, ], }, "Type": "AWS::IAM::Role", }, }, "Rules": { "CheckBootstrapVersion": { "Assertions": [ { "Assert": { "Fn::Not": [ { "Fn::Contains": [ [ "1", "2", "3", "4", "5", ], { "Ref": "BootstrapVersion", }, ], }, ], }, "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI.", }, ], }, }, } `; exports[`Snapshot test 2`] = ` { "Description": "Serverless fullstack webapp stack (uksb-1tupboc47)", "Mappings": { "AWSCloudFrontPartitionHostedZoneIdMap": { "aws": { "zoneId": "Z2FDTNDATAQYW2", }, "aws-cn": { "zoneId": "Z3RFFRIM2A3IF5", }, }, }, "Outputs": { "AsyncJobHandlerArnCA46B385": { "Value": { "Fn::GetAtt": [ "AsyncJobHandler438266BD", "Arn", ], }, }, "AuthUserPoolClientId8216BF9A": { "Value": { "Ref": "AuthUserPoolClientC635291F", }, }, "AuthUserPoolDomainName8D4A2606": { "Value": "auth.example.com", }, "AuthUserPoolIdC0605E59": { "Value": { "Ref": "AuthUserPool8115E87F", }, }, "DatabaseBastionHostBastionHostId1600F37C": { "Description": "Instance ID of the bastion host. Use this to connect via SSM Session Manager", "Value": { "Ref": "DatabaseBastionHost4C4FAD9C", }, }, "DatabaseDatabaseSecretsCommandF4A622EB": { "Value": { "Fn::Join": [ "", [ "aws secretsmanager get-secret-value --secret-id ", { "Fn::Join": [ "-", [ { "Fn::Select": [ 0, { "Fn::Split": [ "-", { "Fn::Select": [ 6, { "Fn::Split": [ ":", { "Ref": "DatabaseClusterSecretD1FB634F", }, ], }, ], }, ], }, ], }, { "Fn::Select": [ 1, { "Fn::Split": [ "-", { "Fn::Select": [ 6, { "Fn::Split": [ ":", { "Ref": "DatabaseClusterSecretD1FB634F", }, ], }, ], }, ], }, ], }, ], ], }, " --region us-west-2", ], ], }, }, "DatabasePortForwardCommandC3718B89": { "Value": { "Fn::Join": [ "", [ "aws ssm start-session --region us-west-2 --target ", { "Ref": "DatabaseBastionHost4C4FAD9C", }, " --document-name AWS-StartPortForwardingSessionToRemoteHost --parameters '{"portNumber":["", { "Fn::GetAtt": [ "DatabaseCluster5B53A178", "Endpoint.Port", ], }, ""], "localPortNumber":["5433"], "host": ["", { "Fn::GetAtt": [ "DatabaseCluster5B53A178", "Endpoint.Address", ], }, ""]}'", ], ], }, }, "EventBusHttpEndpoint1C68A807": { "Value": { "Fn::Join": [ "", [ "https://", { "Fn::GetAtt": [ "EventBusApi6E8C7C94", "Dns.Http", ], }, ], ], }, }, "FrontendDomainName": { "Value": "https://web.example.com", }, "MigrationCommand": { "Value": { "Fn::Join": [ "", [ "aws lambda invoke --function-name ", { "Ref": "WebappMigrationRunnerAC67C012", }, " --payload '{"command":"deploy"}' --cli-binary-format raw-in-base64-out /dev/stdout", ], ], }, }, "MigrationFunctionName": { "Value": { "Ref": "WebappMigrationRunnerAC67C012", }, }, }, "Parameters": { "BootstrapVersion": { "Default": "/cdk-bootstrap/hnb659fds/version", "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]", "Type": "AWS::SSM::Parameter::Value", }, "SsmParameterValueawsserviceamiamazonlinuxlatestal2023amikernel61arm64C96584B6F00A464EAD1953AFF4B05118Parameter": { "Default": "/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-6.1-arm64", "Type": "AWS::SSM::Parameter::Value", }, }, "Resources": { "AWS679f53fac002430cb0da5b7982bd22872D164C4C": { "DependsOn": [ "AWS679f53fac002430cb0da5b7982bd2287ServiceRoleC1EA0FF2", ], "Properties": { "Code": { "S3Bucket": "cdk-hnb659fds-assets-123456789012-us-west-2", "S3Key": "REDACTED", }, "Handler": "index.handler", "Role": { "Fn::GetAtt": [ "AWS679f53fac002430cb0da5b7982bd2287ServiceRoleC1EA0FF2", "Arn", ], }, "Runtime": "nodejs22.x", "Timeout": 120, }, "Type": "AWS::Lambda::Function", }, "AWS679f53fac002430cb0da5b7982bd2287ServiceRoleC1EA0FF2": { "Properties": { "AssumeRolePolicyDocument": { "Statement": [ { "Action": "sts:AssumeRole", "Effect": "Allow", "Principal": { "Service": "lambda.amazonaws.com", }, }, ], "Version": "2012-10-17", }, "ManagedPolicyArns": [ { "Fn::Join": [ "", [ "arn:", { "Ref": "AWS::Partition", }, ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole", ], ], }, ], }, "Type": "AWS::IAM::Role", }, "AWSCDKTriggerCustomResourceProviderCustomResourceProviderHandler97BECD91": { "DependsOn": [ "AWSCDKTriggerCustomResourceProviderCustomResourceProviderRoleE18FAF0A", ], "Properties": { "Code": { "S3Bucket": "cdk-hnb659fds-assets-123456789012-us-west-2", "S3Key": "REDACTED", }, "Handler": "__entrypoint__.handler", "MemorySize": 128, "Role": { "Fn::GetAtt": [ "AWSCDKTriggerCustomResourceProviderCustomResourceProviderRoleE18FAF0A", "Arn", ], }, "Runtime": "nodejs22.x", "Timeout": 900, }, "Type": "AWS::Lambda::Function", }, "AWSCDKTriggerCustomResourceProviderCustomResourceProviderRoleE18FAF0A": { "Properties": { "AssumeRolePolicyDocument": { "Statement": [ { "Action": "sts:AssumeRole", "Effect": "Allow", "Principal": { "Service": "lambda.amazonaws.com", }, }, ], "Version": "2012-10-17", }, "ManagedPolicyArns": [ { "Fn::Sub": "arn:\${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole", }, ], "Policies": [ { "PolicyDocument": { "Statement": [ { "Action": [ "lambda:InvokeFunction", ], "Effect": "Allow", "Resource": [ { "Fn::Join": [ "", [ { "Fn::GetAtt": [ "WebappMigrationRunnerAC67C012", "Arn", ], }, ":*", ], ], }, ], }, ], "Version": "2012-10-17", }, "PolicyName": "Inline", }, ], }, "Type": "AWS::IAM::Role", }, "AccessLogBucketAutoDeleteObjectsCustomResource01AB31E8": { "DeletionPolicy": "Delete", "DependsOn": [ "AccessLogBucketPolicyF52D2D01", ], "Properties": { "BucketName": { "Ref": "AccessLogBucketDA470295", }, "ServiceToken": { "Fn::GetAtt": [ "CustomS3AutoDeleteObjectsCustomResourceProviderHandler9D90184F", "Arn", ], }, }, "Type": "Custom::S3AutoDeleteObjects", "UpdateReplacePolicy": "Delete", }, "AccessLogBucketDA470295": { "DeletionPolicy": "Delete", "Properties": { "BucketEncryption": { "ServerSideEncryptionConfiguration": [ { "ServerSideEncryptionByDefault": { "SSEAlgorithm": "AES256", }, }, ], }, "OwnershipControls": { "Rules": [ { "ObjectOwnership": "ObjectWriter", }, ], }, "PublicAccessBlockConfiguration": { "BlockPublicAcls": true, "BlockPublicPolicy": true, "IgnorePublicAcls": true, "RestrictPublicBuckets": true, }, "Tags": [ { "Key": "aws-cdk:auto-delete-objects", "Value": "true", }, ], }, "Type": "AWS::S3::Bucket", "UpdateReplacePolicy": "Delete", }, "AccessLogBucketPolicyF52D2D01": { "Properties": { "Bucket": { "Ref": "AccessLogBucketDA470295", }, "PolicyDocument": { "Statement": [ { "Action": "s3:*", "Condition": { "Bool": { "aws:SecureTransport": "false", }, }, "Effect": "Deny", "Principal": { "AWS": "*", }, "Resource": [ { "Fn::GetAtt": [ "AccessLogBucketDA470295", "Arn", ], }, { "Fn::Join": [ "", [ { "Fn::GetAtt": [ "AccessLogBucketDA470295", "Arn", ], }, "/*", ], ], }, ], }, { "Action": [ "s3:PutBucketPolicy", "s3:GetBucket*", "s3:List*", "s3:DeleteObject*", ], "Effect": "Allow", "Principal": { "AWS": { "Fn::GetAtt": [ "CustomS3AutoDeleteObjectsCustomResourceProviderRole3B1BD092", "Arn", ], }, }, "Resource": [ { "Fn::GetAtt": [ "AccessLogBucketDA470295", "Arn", ], }, { "Fn::Join": [ "", [ { "Fn::GetAtt": [ "AccessLogBucketDA470295", "Arn", ], }, "/*", ], ], }, ], }, ], "Version": "2012-10-17", }, }, "Type": "AWS::S3::BucketPolicy", }, "AsyncJobHandler438266BD": { "DependsOn": [ "AsyncJobHandlerServiceRoleDefaultPolicy0B2DEDB5", "AsyncJobHandlerServiceRoleFE9F530F", "VpcPrivateSubnet1DefaultRouteBE02A9ED", "VpcPrivateSubnet1RouteTableAssociation70C59FA6", "VpcPrivateSubnet2DefaultRoute060D2087", "VpcPrivateSubnet2RouteTableAssociationA89CAD56", "VpcPrivateSubnet3DefaultRoute94B74F0D", "VpcPrivateSubnet3RouteTableAssociation16BDDC43", ], "Properties": { "Architectures": [ "arm64", ], "Code": { "ImageUri": { "Fn::Sub": "REDACTED", }, }, "Environment": { "Variables": { "DATABASE_ENGINE": { "Fn::Join": [ "", [ "{{resolve:secretsmanager:", { "Ref": "DatabaseClusterSecretAttachmentDC8466C0", }, ":SecretString:engine::}}", ], ], }, "DATABASE_HOST": { "Fn::GetAtt": [ "DatabaseCluster5B53A178", "Endpoint.Address", ], }, "DATABASE_NAME": "main", "DATABASE_OPTION": "?connection_limit=1&connect_timeout=30", "DATABASE_PASSWORD": { "Fn::Join": [ "", [ "{{resolve:secretsmanager:", { "Ref": "DatabaseClusterSecretAttachmentDC8466C0", }, ":SecretString:password::}}", ], ], }, "DATABASE_PORT": { "Fn::GetAtt": [ "DatabaseCluster5B53A178", "Endpoint.Port", ], }, "DATABASE_URL": { "Fn::Join": [ "", [ "{{resolve:secretsmanager:", { "Ref": "DatabaseClusterSecretAttachmentDC8466C0", }, ":SecretString:engine::}}://{{resolve:secretsmanager:", { "Ref": "DatabaseClusterSecretAttachmentDC8466C0", }, ":SecretString:username::}}:{{resolve:secretsmanager:", { "Ref": "DatabaseClusterSecretAttachmentDC8466C0", }, ":SecretString:password::}}@", { "Fn::GetAtt": [ "DatabaseCluster5B53A178", "Endpoint.Address", ], }, ":", { "Fn::GetAtt": [ "DatabaseCluster5B53A178", "Endpoint.Port", ], }, "/main?connection_limit=1&connect_timeout=30", ], ], }, "DATABASE_USER": { "Fn::Join": [ "", [ "{{resolve:secretsmanager:", { "Ref": "DatabaseClusterSecretAttachmentDC8466C0", }, ":SecretString:username::}}", ], ], }, "EVENT_HTTP_ENDPOINT": { "Fn::Join": [ "", [ "https://", { "Fn::GetAtt": [ "EventBusApi6E8C7C94", "Dns.Http", ], }, ], ], }, }, }, "ImageConfig": { "Command": [ "async-job-runner.handler", ], }, "LoggingConfig": { "LogGroup": { "Ref": "AsyncJobHandlerLogs20DFEE3E", }, }, "MemorySize": 256, "PackageType": "Image", "ReservedConcurrentExecutions": 1, "Role": { "Fn::GetAtt": [ "AsyncJobHandlerServiceRoleFE9F530F", "Arn", ], }, "Timeout": 600, "VpcConfig": { "SecurityGroupIds": [ { "Fn::GetAtt": [ "AsyncJobHandlerSecurityGroupF59812E6", "GroupId", ], }, ], "SubnetIds": [ { "Ref": "VpcPrivateSubnet1Subnet536B997A", }, { "Ref": "VpcPrivateSubnet2Subnet3788AAA1", }, { "Ref": "VpcPrivateSubnet3SubnetF258B56E", }, ], }, }, "Type": "AWS::Lambda::Function", }, "AsyncJobHandlerLogs20DFEE3E": { "DeletionPolicy": "Delete", "Properties": { "RetentionInDays": 7, }, "Type": "AWS::Logs::LogGroup", "UpdateReplacePolicy": "Delete", }, "AsyncJobHandlerSecurityGroupF59812E6": { "DependsOn": [ "VpcPrivateSubnet1DefaultRouteBE02A9ED", "VpcPrivateSubnet1RouteTableAssociation70C59FA6", "VpcPrivateSubnet2DefaultRoute060D2087", "VpcPrivateSubnet2RouteTableAssociationA89CAD56", "VpcPrivateSubnet3DefaultRoute94B74F0D", "VpcPrivateSubnet3RouteTableAssociation16BDDC43", ], "Properties": { "GroupDescription": "Automatic security group for Lambda Function ServerlessWebappStarterKitStackAsyncJobHandlerF20B94EA", "SecurityGroupEgress": [ { "CidrIp": "0.0.0.0/0", "Description": "Allow all outbound traffic by default", "IpProtocol": "-1", }, ], "VpcId": { "Ref": "Vpc8378EB38", }, }, "Type": "AWS::EC2::SecurityGroup", }, "AsyncJobHandlerServiceRoleDefaultPolicy0B2DEDB5": { "DependsOn": [ "VpcPrivateSubnet1DefaultRouteBE02A9ED", "VpcPrivateSubnet1RouteTableAssociation70C59FA6", "VpcPrivateSubnet2DefaultRoute060D2087", "VpcPrivateSubnet2RouteTableAssociationA89CAD56", "VpcPrivateSubnet3DefaultRoute94B74F0D", "VpcPrivateSubnet3RouteTableAssociation16BDDC43", ], "Properties": { "PolicyDocument": { "Statement": [ { "Action": "appsync:EventPublish", "Effect": "Allow", "Resource": { "Fn::Join": [ "", [ "arn:", { "Ref": "AWS::Partition", }, ":appsync:us-west-2:123456789012:apis/", { "Fn::GetAtt": [ "EventBusApi6E8C7C94", "ApiId", ], }, "/channelNamespace/*", ], ], }, }, { "Action": [ "translate:TranslateText", "comprehend:DetectDominantLanguage", ], "Effect": "Allow", "Resource": "*", }, ], "Version": "2012-10-17", }, "PolicyName": "AsyncJobHandlerServiceRoleDefaultPolicy0B2DEDB5", "Roles": [ { "Ref": "AsyncJobHandlerServiceRoleFE9F530F", }, ], }, "Type": "AWS::IAM::Policy", }, "AsyncJobHandlerServiceRoleFE9F530F": { "DependsOn": [ "VpcPrivateSubnet1DefaultRouteBE02A9ED", "VpcPrivateSubnet1RouteTableAssociation70C59FA6", "VpcPrivateSubnet2DefaultRoute060D2087", "VpcPrivateSubnet2RouteTableAssociationA89CAD56", "VpcPrivateSubnet3DefaultRoute94B74F0D", "VpcPrivateSubnet3RouteTableAssociation16BDDC43", ], "Properties": { "AssumeRolePolicyDocument": { "Statement": [ { "Action": "sts:AssumeRole", "Effect": "Allow", "Principal": { "Service": "lambda.amazonaws.com", }, }, ], "Version": "2012-10-17", }, "ManagedPolicyArns": [ { "Fn::Join": [ "", [ "arn:", { "Ref": "AWS::Partition", }, ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole", ], ], }, { "Fn::Join": [ "", [ "arn:", { "Ref": "AWS::Partition", }, ":iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole", ], ], }, ], }, "Type": "AWS::IAM::Role", }, "AsyncJobSampleJob3C1EBA2C": { "Properties": { "FlexibleTimeWindow": { "Mode": "OFF", }, "ScheduleExpression": "cron(0 0 1 * ? *)", "ScheduleExpressionTimezone": "Etc/UTC", "State": "ENABLED", "Target": { "Arn": { "Fn::GetAtt": [ "AsyncJobHandler438266BD", "Arn", ], }, "Input": "{"jobType":"SampleJob"}", "RetryPolicy": { "MaximumEventAgeInSeconds": 86400, "MaximumRetryAttempts": 5, }, "RoleArn": { "Fn::GetAtt": [ "SchedulerRoleForTarget44ece2CFC6840F", "Arn", ], }, }, }, "Type": "AWS::Scheduler::Schedule", }, "AuthBranding34BB87FD": { "Properties": { "ClientId": { "Ref": "AuthUserPoolClientC635291F", }, "UseCognitoProvidedValues": true, "UserPoolId": { "Ref": "AuthUserPool8115E87F", }, }, "Type": "AWS::Cognito::ManagedLoginBranding", }, "AuthCognitoDomainRecordBA8AA168": { "Properties": { "HostedZoneId": "DUMMY", "Name": "auth.example.com.", "ResourceRecords": [ { "Fn::GetAtt": [ "AuthUserPoolCognitoDomainAD9D79E1", "CloudFrontDistribution", ], }, ], "TTL": "1800", "Type": "CNAME", }, "Type": "AWS::Route53::RecordSet", }, "AuthUserPool8115E87F": { "DeletionPolicy": "Delete", "Properties": { "AccountRecoverySetting": { "RecoveryMechanisms": [ { "Name": "verified_phone_number", "Priority": 1, }, { "Name": "verified_email", "Priority": 2, }, ], }, "AdminCreateUserConfig": { "AllowAdminCreateUserOnly": true, }, "AutoVerifiedAttributes": [ "email", ], "EmailVerificationMessage": "The verification code to your new account is {####}", "EmailVerificationSubject": "Verify your new account", "Policies": { "PasswordPolicy": { "MinimumLength": 8, "RequireNumbers": true, "RequireSymbols": true, "RequireUppercase": true, }, }, "SmsVerificationMessage": "The verification code to your new account is {####}", "UsernameAttributes": [ "email", ], "VerificationMessageTemplate": { "DefaultEmailOption": "CONFIRM_WITH_CODE", "EmailMessage": "The verification code to your new account is {####}", "EmailSubject": "Verify your new account", "SmsMessage": "The verification code to your new account is {####}", }, }, "Type": "AWS::Cognito::UserPool", "UpdateReplacePolicy": "Delete", }, "AuthUserPoolClientC635291F": { "Properties": { "AllowedOAuthFlows": [ "code", ], "AllowedOAuthFlowsUserPoolClient": true, "AllowedOAuthScopes": [ "profile", "phone", "email", "openid", "aws.cognito.signin.user.admin", ], "CallbackURLs": [ "http://localhost:3010/api/auth/sign-in-callback", "https://web.example.com/api/auth/sign-in-callback", ], "ExplicitAuthFlows": [ "ALLOW_USER_PASSWORD_AUTH", "ALLOW_USER_SRP_AUTH", "ALLOW_REFRESH_TOKEN_AUTH", ], "IdTokenValidity": 1440, "LogoutURLs": [ "http://localhost:3010/api/auth/sign-out-callback", "https://web.example.com/api/auth/sign-out-callback", ], "SupportedIdentityProviders": [ "COGNITO", ], "TokenValidityUnits": { "IdToken": "minutes", }, "UserPoolId": { "Ref": "AuthUserPool8115E87F", }, }, "Type": "AWS::Cognito::UserPoolClient", }, "AuthUserPoolCognitoDomainAD9D79E1": { "Properties": { "CustomDomainConfig": { "CertificateArn": { "Fn::GetAtt": [ "ExportsReader8B249524", "/cdk/exports/ServerlessWebappStarterKitStack/ServerlessWebappStarterKitUsEast1Stackuseast1RefCertificateV29EE77EAF5C85697F", ], }, }, "Domain": "auth.example.com", "ManagedLoginVersion": 2, "UserPoolId": { "Ref": "AuthUserPool8115E87F", }, }, "Type": "AWS::Cognito::UserPoolDomain", }, "ContainerImageBuildArm64e83729feb1564e709bec452b15847a30amd64C13E3549": { "Properties": { "Artifacts": { "Type": "NO_ARTIFACTS", }, "Cache": { "Type": "NO_CACHE", }, "EncryptionKey": "alias/aws/s3", "Environment": { "ComputeType": "BUILD_GENERAL1_SMALL", "Image": "aws/codebuild/amazonlinux2-aarch64-standard:3.0", "ImagePullCredentialsType": "CODEBUILD", "PrivilegedMode": true, "Type": "ARM_CONTAINER", }, "ServiceRole": { "Fn::GetAtt": [ "ContainerImageBuildArm64e83729feb1564e709bec452b15847a30amd64RoleC5F7BBFE", "Arn", ], }, "Source": { "BuildSpec": "{ "version": "0.2", "phases": { "build": { "commands": [ "current_dir=$(pwd)", "echo \\"$input\\"", "mkdir workdir", "cd workdir", "aws s3 cp \\"$sourceS3Url\\" temp.zip", "unzip temp.zip", "ls -la", "aws ecr get-login-password | docker login --username AWS --password-stdin $repositoryAuthUri", "aws ecr-public get-login-password --region us-east-1 | docker login --username AWS --password-stdin public.ecr.aws", "docker buildx create --use", "docker buildx ls", "eval \\"$buildCommand\\"" ] }, "post_build": { "commands": [ "echo Build completed on \`date\`", "\\nSTATUS='SUCCESS'\\nif [ $CODEBUILD_BUILD_SUCCEEDING -ne 1 ] # Test if the build is failing\\nthen\\nSTATUS='FAILED'\\nREASON=\\"ContainerImageBuild failed. See CloudWatch Log stream for the detailed reason: \\nhttps://$AWS_REGION.console.aws.amazon.com/cloudwatch/home?region=$AWS_REGION#logsV2:log-groups/log-group/\\\\$252Faws\\\\$252Fcodebuild\\\\$252F$projectName/log-events/$CODEBUILD_LOG_PATH\\"\\nfi\\ncat < payload.json\\n{\\n \\"StackId\\": \\"$stackId\\",\\n \\"RequestId\\": \\"$requestId\\",\\n \\"LogicalResourceId\\":\\"$logicalResourceId\\",\\n \\"PhysicalResourceId\\": \\"$imageTag\\",\\n \\"Status\\": \\"$STATUS\\",\\n \\"Reason\\": \\"$REASON\\",\\n \\"Data\\": {\\n \\"ImageTag\\": \\"$imageTag\\"\\n }\\n}\\nEOF\\ncurl -v -i -X PUT -H 'Content-Type:' -d \\"@payload.json\\" \\"$responseURL\\"\\n " ] } } }", "Type": "NO_SOURCE", }, }, "Type": "AWS::CodeBuild::Project", }, "ContainerImageBuildArm64e83729feb1564e709bec452b15847a30amd64RoleC5F7BBFE": { "Properties": { "AssumeRolePolicyDocument": { "Statement": [ { "Action": "sts:AssumeRole", "Effect": "Allow", "Principal": { "Service": "codebuild.amazonaws.com", }, }, ], "Version": "2012-10-17", }, "ManagedPolicyArns": [ { "Fn::Join": [ "", [ "arn:", { "Ref": "AWS::Partition", }, ":iam::aws:policy/AmazonElasticContainerRegistryPublicReadOnly", ], ], }, ], }, "Type": "AWS::IAM::Role", }, "ContainerImageBuildArm64e83729feb1564e709bec452b15847a30amd64RoleDefaultPolicy2316728F": { "Properties": { "PolicyDocument": { "Statement": [ { "Action": [ "logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents", ], "Effect": "Allow", "Resource": [ { "Fn::Join": [ "", [ "arn:", { "Ref": "AWS::Partition", }, ":logs:us-west-2:123456789012:log-group:/aws/codebuild/", { "Ref": "ContainerImageBuildArm64e83729feb1564e709bec452b15847a30amd64C13E3549", }, ], ], }, { "Fn::Join": [ "", [ "arn:", { "Ref": "AWS::Partition", }, ":logs:us-west-2:123456789012:log-group:/aws/codebuild/", { "Ref": "ContainerImageBuildArm64e83729feb1564e709bec452b15847a30amd64C13E3549", }, ":*", ], ], }, ], }, { "Action": [ "codebuild:CreateReportGroup", "codebuild:CreateReport", "codebuild:UpdateReport", "codebuild:BatchPutTestCases", "codebuild:BatchPutCodeCoverages", ], "Effect": "Allow", "Resource": { "Fn::Join": [ "", [ "arn:", { "Ref": "AWS::Partition", }, ":codebuild:us-west-2:123456789012:report-group/", { "Ref": "ContainerImageBuildArm64e83729feb1564e709bec452b15847a30amd64C13E3549", }, "-*", ], ], }, }, { "Action": [ "ecr:BatchCheckLayerAvailability", "ecr:GetDownloadUrlForLayer", "ecr:BatchGetImage", "ecr:CompleteLayerUpload", "ecr:UploadLayerPart", "ecr:InitiateLayerUpload", "ecr:PutImage", ], "Effect": "Allow", "Resource": { "Fn::GetAtt": [ "WebappBuildRepository4C93D48D", "Arn", ], }, }, { "Action": "ecr:GetAuthorizationToken", "Effect": "Allow", "Resource": "*", }, { "Action": "ecr:DescribeImages", "Effect": "Allow", "Resource": { "Fn::GetAtt": [ "WebappBuildRepository4C93D48D", "Arn", ], }, }, { "Action": [ "s3:GetObject*", "s3:GetBucket*", "s3:List*", ], "Effect": "Allow", "Resource": [ { "Fn::Join": [ "", [ "arn:", { "Ref": "AWS::Partition", }, ":s3:::cdk-hnb659fds-assets-123456789012-us-west-2", ], ], }, { "Fn::Join": [ "", [ "arn:", { "Ref": "AWS::Partition", }, ":s3:::cdk-hnb659fds-assets-123456789012-us-west-2/*", ], ], }, ], }, ], "Version": "2012-10-17", }, "PolicyName": "ContainerImageBuildArm64e83729feb1564e709bec452b15847a30amd64RoleDefaultPolicy2316728F", "Roles": [ { "Ref": "ContainerImageBuildArm64e83729feb1564e709bec452b15847a30amd64RoleC5F7BBFE", }, ], }, "Type": "AWS::IAM::Policy", }, "CustomCrossRegionExportReaderCustomResourceProviderHandler46647B68": { "DependsOn": [ "CustomCrossRegionExportReaderCustomResourceProviderRole10531BBD", ], "Properties": { "Code": { "S3Bucket": "cdk-hnb659fds-assets-123456789012-us-west-2", "S3Key": "REDACTED", }, "Handler": "__entrypoint__.handler", "MemorySize": 128, "Role": { "Fn::GetAtt": [ "CustomCrossRegionExportReaderCustomResourceProviderRole10531BBD", "Arn", ], }, "Runtime": "nodejs22.x", "Timeout": 900, }, "Type": "AWS::Lambda::Function", }, "CustomCrossRegionExportReaderCustomResourceProviderRole10531BBD": { "Properties": { "AssumeRolePolicyDocument": { "Statement": [ { "Action": "sts:AssumeRole", "Effect": "Allow", "Principal": { "Service": "lambda.amazonaws.com", }, }, ], "Version": "2012-10-17", }, "ManagedPolicyArns": [ { "Fn::Sub": "arn:\${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole", }, ], "Policies": [ { "PolicyDocument": { "Statement": [ { "Action": [ "ssm:AddTagsToResource", "ssm:RemoveTagsFromResource", "ssm:GetParameters", ], "Effect": "Allow", "Resource": { "Fn::Join": [ "", [ "arn:", { "Ref": "AWS::Partition", }, ":ssm:us-west-2:123456789012:parameter/cdk/exports/ServerlessWebappStarterKitStack/*", ], ], }, }, ], "Version": "2012-10-17", }, "PolicyName": "Inline", }, ], }, "Type": "AWS::IAM::Role", }, "CustomS3AutoDeleteObjectsCustomResourceProviderHandler9D90184F": { "DependsOn": [ "CustomS3AutoDeleteObjectsCustomResourceProviderRole3B1BD092", ], "Properties": { "Code": { "S3Bucket": "cdk-hnb659fds-assets-123456789012-us-west-2", "S3Key": "REDACTED", }, "Description": { "Fn::Join": [ "", [ "Lambda function for auto-deleting objects in ", { "Ref": "AccessLogBucketDA470295", }, " S3 bucket.", ], ], }, "Handler": "index.handler", "MemorySize": 128, "Role": { "Fn::GetAtt": [ "CustomS3AutoDeleteObjectsCustomResourceProviderRole3B1BD092", "Arn", ], }, "Runtime": "nodejs22.x", "Timeout": 900, }, "Type": "AWS::Lambda::Function", }, "CustomS3AutoDeleteObjectsCustomResourceProviderRole3B1BD092": { "Properties": { "AssumeRolePolicyDocument": { "Statement": [ { "Action": "sts:AssumeRole", "Effect": "Allow", "Principal": { "Service": "lambda.amazonaws.com", }, }, ], "Version": "2012-10-17", }, "ManagedPolicyArns": [ { "Fn::Sub": "arn:\${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole", }, ], }, "Type": "AWS::IAM::Role", }, "DatabaseBastionHost4C4FAD9C": { "DependsOn": [ "DatabaseBastionHostInstanceRoleDefaultPolicy15D8D0EA", "DatabaseBastionHostInstanceRole87A429B0", ], "Properties": { "AvailabilityZone": "dummy1a", "BlockDeviceMappings": [ { "DeviceName": "/dev/sdf", "Ebs": { "Encrypted": true, "VolumeSize": 8, }, }, ], "IamInstanceProfile": { "Ref": "DatabaseBastionHostInstanceProfile0F4F3411", }, "ImageId": { "Ref": "SsmParameterValueawsserviceamiamazonlinuxlatestal2023amikernel61arm64C96584B6F00A464EAD1953AFF4B05118Parameter", }, "InstanceType": "t4g.nano", "SecurityGroupIds": [ { "Fn::GetAtt": [ "DatabaseBastionHostInstanceSecurityGroup39D7809A", "GroupId", ], }, ], "SubnetId": { "Ref": "VpcPrivateSubnet1Subnet536B997A", }, "Tags": [ { "Key": "Name", "Value": "BastionHost", }, ], "UserData": { "Fn::Base64": "#!/bin/bash", }, }, "Type": "AWS::EC2::Instance", }, "DatabaseBastionHostInstanceProfile0F4F3411": { "Properties": { "Roles": [ { "Ref": "DatabaseBastionHostInstanceRole87A429B0", }, ], }, "Type": "AWS::IAM::InstanceProfile", }, "DatabaseBastionHostInstanceRole87A429B0": { "Properties": { "AssumeRolePolicyDocument": { "Statement": [ { "Action": "sts:AssumeRole", "Effect": "Allow", "Principal": { "Service": "ec2.amazonaws.com", }, }, ], "Version": "2012-10-17", }, "Tags": [ { "Key": "Name", "Value": "BastionHost", }, ], }, "Type": "AWS::IAM::Role", }, "DatabaseBastionHostInstanceRoleDefaultPolicy15D8D0EA": { "Properties": { "PolicyDocument": { "Statement": [ { "Action": [ "ssmmessages:*", "ssm:UpdateInstanceInformation", "ec2messages:*", ], "Effect": "Allow", "Resource": "*", }, ], "Version": "2012-10-17", }, "PolicyName": "DatabaseBastionHostInstanceRoleDefaultPolicy15D8D0EA", "Roles": [ { "Ref": "DatabaseBastionHostInstanceRole87A429B0", }, ], }, "Type": "AWS::IAM::Policy", }, "DatabaseBastionHostInstanceSecurityGroup39D7809A": { "Properties": { "GroupDescription": "ServerlessWebappStarterKitStack/Database/BastionHost/Resource/InstanceSecurityGroup", "SecurityGroupEgress": [ { "CidrIp": "0.0.0.0/0", "Description": "Allow all outbound traffic by default", "IpProtocol": "-1", }, ], "Tags": [ { "Key": "Name", "Value": "BastionHost", }, ], "VpcId": { "Ref": "Vpc8378EB38", }, }, "Type": "AWS::EC2::SecurityGroup", }, "DatabaseCluster5B53A178": { "DeletionPolicy": "Snapshot", "Properties": { "CopyTagsToSnapshot": true, "DBClusterParameterGroupName": { "Ref": "DatabaseParameterGroup2A921026", }, "DBSubnetGroupName": { "Ref": "DatabaseClusterSubnets5540150D", }, "EnableCloudwatchLogsExports": [ "postgresql", ], "EnableHttpEndpoint": true, "Engine": "aurora-postgresql", "EngineVersion": "16.6", "MasterUserPassword": { "Fn::Join": [ "", [ "{{resolve:secretsmanager:", { "Ref": "DatabaseClusterSecretD1FB634F", }, ":SecretString:password::}}", ], ], }, "MasterUsername": { "Fn::Join": [ "", [ "{{resolve:secretsmanager:", { "Ref": "DatabaseClusterSecretD1FB634F", }, ":SecretString:username::}}", ], ], }, "Port": 5432, "ServerlessV2ScalingConfiguration": { "MaxCapacity": 2, "MinCapacity": 0, }, "StorageEncrypted": true, "VpcSecurityGroupIds": [ { "Fn::GetAtt": [ "DatabaseClusterSecurityGroupFEF1426A", "GroupId", ], }, ], }, "Type": "AWS::RDS::DBCluster", "UpdateReplacePolicy": "Snapshot", }, "DatabaseClusterLogRetentionpostgresql025D39CE": { "Properties": { "LogGroupName": { "Fn::Join": [ "", [ "/aws/rds/cluster/", { "Ref": "DatabaseCluster5B53A178", }, "/postgresql", ], ], }, "RetentionInDays": 7, "ServiceToken": { "Fn::GetAtt": [ "LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aFD4BFC8A", "Arn", ], }, }, "Type": "Custom::LogRetention", }, "DatabaseClusterSecretAttachmentDC8466C0": { "Properties": { "SecretId": { "Ref": "DatabaseClusterSecretD1FB634F", }, "TargetId": { "Ref": "DatabaseCluster5B53A178", }, "TargetType": "AWS::RDS::DBCluster", }, "Type": "AWS::SecretsManager::SecretTargetAttachment", }, "DatabaseClusterSecretD1FB634F": { "DeletionPolicy": "Delete", "Properties": { "Description": { "Fn::Join": [ "", [ "Generated by the CDK for stack: ", { "Ref": "AWS::StackName", }, ], ], }, "GenerateSecretString": { "ExcludeCharacters": " %+~\`#$&*()|[]{}:;<>?!'/@"\\,=^", "GenerateStringKey": "password", "PasswordLength": 30, "SecretStringTemplate": "{"username":"postgres"}", }, }, "Type": "AWS::SecretsManager::Secret", "UpdateReplacePolicy": "Delete", }, "DatabaseClusterSecurityGroupFEF1426A": { "Properties": { "GroupDescription": "RDS security group", "SecurityGroupEgress": [ { "CidrIp": "0.0.0.0/0", "Description": "Allow all outbound traffic by default", "IpProtocol": "-1", }, ], "VpcId": { "Ref": "Vpc8378EB38", }, }, "Type": "AWS::EC2::SecurityGroup", }, "DatabaseClusterSecurityGroupfromServerlessWebappStarterKitStackAsyncJobHandlerSecurityGroup5220DFB3IndirectPort9323962E": { "Properties": { "Description": "from ServerlessWebappStarterKitStackAsyncJobHandlerSecurityGroup5220DFB3:{IndirectPort}", "FromPort": { "Fn::GetAtt": [ "DatabaseCluster5B53A178", "Endpoint.Port", ], }, "GroupId": { "Fn::GetAtt": [ "DatabaseClusterSecurityGroupFEF1426A", "GroupId", ], }, "IpProtocol": "tcp", "SourceSecurityGroupId": { "Fn::GetAtt": [ "AsyncJobHandlerSecurityGroupF59812E6", "GroupId", ], }, "ToPort": { "Fn::GetAtt": [ "DatabaseCluster5B53A178", "Endpoint.Port", ], }, }, "Type": "AWS::EC2::SecurityGroupIngress", }, "DatabaseClusterSecurityGroupfromServerlessWebappStarterKitStackDatabaseBastionHostInstanceSecurityGroup4F0DD25BIndirectPort8AFD9922": { "Properties": { "Description": "from ServerlessWebappStarterKitStackDatabaseBastionHostInstanceSecurityGroup4F0DD25B:{IndirectPort}", "FromPort": { "Fn::GetAtt": [ "DatabaseCluster5B53A178", "Endpoint.Port", ], }, "GroupId": { "Fn::GetAtt": [ "DatabaseClusterSecurityGroupFEF1426A", "GroupId", ], }, "IpProtocol": "tcp", "SourceSecurityGroupId": { "Fn::GetAtt": [ "DatabaseBastionHostInstanceSecurityGroup39D7809A", "GroupId", ], }, "ToPort": { "Fn::GetAtt": [ "DatabaseCluster5B53A178", "Endpoint.Port", ], }, }, "Type": "AWS::EC2::SecurityGroupIngress", }, "DatabaseClusterSecurityGroupfromServerlessWebappStarterKitStackWebappHandlerSecurityGroupA009CF4AIndirectPort7311063E": { "Properties": { "Description": "from ServerlessWebappStarterKitStackWebappHandlerSecurityGroupA009CF4A:{IndirectPort}", "FromPort": { "Fn::GetAtt": [ "DatabaseCluster5B53A178", "Endpoint.Port", ], }, "GroupId": { "Fn::GetAtt": [ "DatabaseClusterSecurityGroupFEF1426A", "GroupId", ], }, "IpProtocol": "tcp", "SourceSecurityGroupId": { "Fn::GetAtt": [ "WebappHandlerSecurityGroup5451B519", "GroupId", ], }, "ToPort": { "Fn::GetAtt": [ "DatabaseCluster5B53A178", "Endpoint.Port", ], }, }, "Type": "AWS::EC2::SecurityGroupIngress", }, "DatabaseClusterSecurityGroupfromServerlessWebappStarterKitStackWebappMigrationRunnerSecurityGroupC0959349IndirectPortDCF4A356": { "Properties": { "Description": "from ServerlessWebappStarterKitStackWebappMigrationRunnerSecurityGroupC0959349:{IndirectPort}", "FromPort": { "Fn::GetAtt": [ "DatabaseCluster5B53A178", "Endpoint.Port", ], }, "GroupId": { "Fn::GetAtt": [ "DatabaseClusterSecurityGroupFEF1426A", "GroupId", ], }, "IpProtocol": "tcp", "SourceSecurityGroupId": { "Fn::GetAtt": [ "WebappMigrationRunnerSecurityGroup7F0DF264", "GroupId", ], }, "ToPort": { "Fn::GetAtt": [ "DatabaseCluster5B53A178", "Endpoint.Port", ], }, }, "Type": "AWS::EC2::SecurityGroupIngress", }, "DatabaseClusterSubnets5540150D": { "Properties": { "DBSubnetGroupDescription": "Subnets for Cluster database", "SubnetIds": [ { "Ref": "VpcPrivateSubnet1Subnet536B997A", }, { "Ref": "VpcPrivateSubnet2Subnet3788AAA1", }, { "Ref": "VpcPrivateSubnet3SubnetF258B56E", }, ], }, "Type": "AWS::RDS::DBSubnetGroup", }, "DatabaseClusterWriterD43085C6": { "DeletionPolicy": "Delete", "DependsOn": [ "VpcPrivateSubnet1DefaultRouteBE02A9ED", "VpcPrivateSubnet1RouteTableAssociation70C59FA6", "VpcPrivateSubnet2DefaultRoute060D2087", "VpcPrivateSubnet2RouteTableAssociationA89CAD56", "VpcPrivateSubnet3DefaultRoute94B74F0D", "VpcPrivateSubnet3RouteTableAssociation16BDDC43", ], "Properties": { "AutoMinorVersionUpgrade": true, "DBClusterIdentifier": { "Ref": "DatabaseCluster5B53A178", }, "DBInstanceClass": "db.serverless", "EnablePerformanceInsights": true, "Engine": "aurora-postgresql", "PerformanceInsightsRetentionPeriod": 7, "PromotionTier": 0, "PubliclyAccessible": false, }, "Type": "AWS::RDS::DBInstance", "UpdateReplacePolicy": "Delete", }, "DatabaseParameterGroup2A921026": { "Properties": { "Description": "Cluster parameter group for aurora-postgresql16", "Family": "aurora-postgresql16", "Parameters": { "idle_session_timeout": "60000", "log_connections": "1", "log_disconnections": "1", }, }, "Type": "AWS::RDS::DBClusterParameterGroup", }, "DeployTimeBuildCustomResourceHandlerdb740fd554364a848a09e6dfcd01f4f306AEFF37": { "DependsOn": [ "DeployTimeBuildCustomResourceHandlerdb740fd554364a848a09e6dfcd01f4f3ServiceRoleDefaultPolicyFECC51DC", "DeployTimeBuildCustomResourceHandlerdb740fd554364a848a09e6dfcd01f4f3ServiceRoleB008BAA4", ], "Properties": { "Code": { "S3Bucket": "cdk-hnb659fds-assets-123456789012-us-west-2", "S3Key": "REDACTED", }, "Handler": "index.handler", "Role": { "Fn::GetAtt": [ "DeployTimeBuildCustomResourceHandlerdb740fd554364a848a09e6dfcd01f4f3ServiceRoleB008BAA4", "Arn", ], }, "Runtime": "nodejs20.x", "Timeout": 300, }, "Type": "AWS::Lambda::Function", }, "DeployTimeBuildCustomResourceHandlerdb740fd554364a848a09e6dfcd01f4f3ServiceRoleB008BAA4": { "Properties": { "AssumeRolePolicyDocument": { "Statement": [ { "Action": "sts:AssumeRole", "Effect": "Allow", "Principal": { "Service": "lambda.amazonaws.com", }, }, ], "Version": "2012-10-17", }, "ManagedPolicyArns": [ { "Fn::Join": [ "", [ "arn:", { "Ref": "AWS::Partition", }, ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole", ], ], }, ], }, "Type": "AWS::IAM::Role", }, "DeployTimeBuildCustomResourceHandlerdb740fd554364a848a09e6dfcd01f4f3ServiceRoleDefaultPolicyFECC51DC": { "Properties": { "PolicyDocument": { "Statement": [ { "Action": "codebuild:StartBuild", "Effect": "Allow", "Resource": { "Fn::GetAtt": [ "ContainerImageBuildArm64e83729feb1564e709bec452b15847a30amd64C13E3549", "Arn", ], }, }, ], "Version": "2012-10-17", }, "PolicyName": "DeployTimeBuildCustomResourceHandlerdb740fd554364a848a09e6dfcd01f4f3ServiceRoleDefaultPolicyFECC51DC", "Roles": [ { "Ref": "DeployTimeBuildCustomResourceHandlerdb740fd554364a848a09e6dfcd01f4f3ServiceRoleB008BAA4", }, ], }, "Type": "AWS::IAM::Policy", }, "EventBusApi6E8C7C94": { "Properties": { "EventConfig": { "AuthProviders": [ { "AuthType": "AWS_IAM", }, { "AuthType": "AMAZON_COGNITO_USER_POOLS", "CognitoConfig": { "AwsRegion": "us-west-2", "UserPoolId": { "Ref": "AuthUserPool8115E87F", }, }, }, ], "ConnectionAuthModes": [ { "AuthType": "AWS_IAM", }, { "AuthType": "AMAZON_COGNITO_USER_POOLS", }, ], "DefaultPublishAuthModes": [ { "AuthType": "AWS_IAM", }, ], "DefaultSubscribeAuthModes": [ { "AuthType": "AWS_IAM", }, { "AuthType": "AMAZON_COGNITO_USER_POOLS", }, ], }, "Name": "ServerlessWackEventBus8815362F", }, "Type": "AWS::AppSync::Api", }, "EventBusNamespaceA69F015E": { "Properties": { "ApiId": { "Fn::GetAtt": [ "EventBusApi6E8C7C94", "ApiId", ], }, "CodeS3Location": "s3://cdk-hnb659fds-assets-123456789012-us-west-2/REDACTED", "HandlerConfigs": {}, "Name": "event-bus", }, "Type": "AWS::AppSync::ChannelNamespace", }, "ExportsReader8B249524": { "DeletionPolicy": "Delete", "Properties": { "ReaderProps": { "imports": { "/cdk/exports/ServerlessWebappStarterKitStack/ServerlessWebappStarterKitUsEast1Stackuseast1RefCertificateV29EE77EAF5C85697F": "{{resolve:ssm:/cdk/exports/ServerlessWebappStarterKitStack/ServerlessWebappStarterKitUsEast1Stackuseast1RefCertificateV29EE77EAF5C85697F}}", "/cdk/exports/ServerlessWebappStarterKitStack/ServerlessWebappStarterKitUsEast1Stackuseast1RefSignPayloadHandlerFunctionVersionF9FE430A3006B9FA": "{{resolve:ssm:/cdk/exports/ServerlessWebappStarterKitStack/ServerlessWebappStarterKitUsEast1Stackuseast1RefSignPayloadHandlerFunctionVersionF9FE430A3006B9FA}}", }, "prefix": "ServerlessWebappStarterKitStack", "region": "us-west-2", }, "ServiceToken": { "Fn::GetAtt": [ "CustomCrossRegionExportReaderCustomResourceProviderHandler46647B68", "Arn", ], }, }, "Type": "Custom::CrossRegionExportReader", "UpdateReplacePolicy": "Delete", }, "LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aFD4BFC8A": { "DependsOn": [ "LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aServiceRoleDefaultPolicyADDA7DEB", "LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aServiceRole9741ECFB", ], "Properties": { "Code": { "S3Bucket": "cdk-hnb659fds-assets-123456789012-us-west-2", "S3Key": "REDACTED", }, "Handler": "index.handler", "Role": { "Fn::GetAtt": [ "LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aServiceRole9741ECFB", "Arn", ], }, "Runtime": "nodejs22.x", "Timeout": 900, }, "Type": "AWS::Lambda::Function", }, "LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aServiceRole9741ECFB": { "Properties": { "AssumeRolePolicyDocument": { "Statement": [ { "Action": "sts:AssumeRole", "Effect": "Allow", "Principal": { "Service": "lambda.amazonaws.com", }, }, ], "Version": "2012-10-17", }, "ManagedPolicyArns": [ { "Fn::Join": [ "", [ "arn:", { "Ref": "AWS::Partition", }, ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole", ], ], }, ], }, "Type": "AWS::IAM::Role", }, "LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aServiceRoleDefaultPolicyADDA7DEB": { "Properties": { "PolicyDocument": { "Statement": [ { "Action": [ "logs:PutRetentionPolicy", "logs:DeleteRetentionPolicy", ], "Effect": "Allow", "Resource": "*", }, ], "Version": "2012-10-17", }, "PolicyName": "LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aServiceRoleDefaultPolicyADDA7DEB", "Roles": [ { "Ref": "LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aServiceRole9741ECFB", }, ], }, "Type": "AWS::IAM::Policy", }, "LookupVersionArnc8730278af02f875114ca902814c77b68f19b0087110E04D0A": { "DeletionPolicy": "Delete", "DependsOn": [ "LookupVersionArnc8730278af02f875114ca902814c77b68f19b00871CustomResourcePolicy7CD24C5F", ], "Properties": { "Create": { "Fn::Join": [ "", [ "{"service":"SSM","action":"getParameter","parameters":{"Name":"", { "Fn::GetAtt": [ "ExportsReader8B249524", "/cdk/exports/ServerlessWebappStarterKitStack/ServerlessWebappStarterKitUsEast1Stackuseast1RefSignPayloadHandlerFunctionVersionF9FE430A3006B9FA", ], }, ""},"physicalResourceId":{"id":"1577836800000"},"region":"us-east-1"}", ], ], }, "InstallLatestAwsSdk": true, "ServiceToken": { "Fn::GetAtt": [ "AWS679f53fac002430cb0da5b7982bd22872D164C4C", "Arn", ], }, "Update": { "Fn::Join": [ "", [ "{"service":"SSM","action":"getParameter","parameters":{"Name":"", { "Fn::GetAtt": [ "ExportsReader8B249524", "/cdk/exports/ServerlessWebappStarterKitStack/ServerlessWebappStarterKitUsEast1Stackuseast1RefSignPayloadHandlerFunctionVersionF9FE430A3006B9FA", ], }, ""},"physicalResourceId":{"id":"1577836800000"},"region":"us-east-1"}", ], ], }, }, "Type": "Custom::AWS", "UpdateReplacePolicy": "Delete", }, "LookupVersionArnc8730278af02f875114ca902814c77b68f19b00871CustomResourcePolicy7CD24C5F": { "Properties": { "PolicyDocument": { "Statement": [ { "Action": "ssm:GetParameter", "Effect": "Allow", "Resource": { "Fn::Join": [ "", [ "arn:", { "Ref": "AWS::Partition", }, ":ssm:us-east-1:123456789012:parameter/", { "Fn::GetAtt": [ "ExportsReader8B249524", "/cdk/exports/ServerlessWebappStarterKitStack/ServerlessWebappStarterKitUsEast1Stackuseast1RefSignPayloadHandlerFunctionVersionF9FE430A3006B9FA", ], }, ], ], }, }, ], "Version": "2012-10-17", }, "PolicyName": "LookupVersionArnc8730278af02f875114ca902814c77b68f19b00871CustomResourcePolicy7CD24C5F", "Roles": [ { "Ref": "AWS679f53fac002430cb0da5b7982bd2287ServiceRoleC1EA0FF2", }, ], }, "Type": "AWS::IAM::Policy", }, "SchedulerRoleForTarget44ece2CFC6840F": { "Properties": { "AssumeRolePolicyDocument": { "Statement": [ { "Action": "sts:AssumeRole", "Condition": { "StringEquals": { "aws:SourceAccount": "123456789012", "aws:SourceArn": { "Fn::Join": [ "", [ "arn:", { "Ref": "AWS::Partition", }, ":scheduler:us-west-2:123456789012:schedule-group/default", ], ], }, }, }, "Effect": "Allow", "Principal": { "Service": "scheduler.amazonaws.com", }, }, ], "Version": "2012-10-17", }, }, "Type": "AWS::IAM::Role", }, "SchedulerRoleForTarget44ece2DefaultPolicyFDF3E159": { "Properties": { "PolicyDocument": { "Statement": [ { "Action": "lambda:InvokeFunction", "Effect": "Allow", "Resource": [ { "Fn::GetAtt": [ "AsyncJobHandler438266BD", "Arn", ], }, { "Fn::Join": [ "", [ { "Fn::GetAtt": [ "AsyncJobHandler438266BD", "Arn", ], }, ":*", ], ], }, ], }, ], "Version": "2012-10-17", }, "PolicyName": "SchedulerRoleForTarget44ece2DefaultPolicyFDF3E159", "Roles": [ { "Ref": "SchedulerRoleForTarget44ece2CFC6840F", }, ], }, "Type": "AWS::IAM::Policy", }, "Vpc8378EB38": { "Properties": { "CidrBlock": "10.0.0.0/16", "EnableDnsHostnames": true, "EnableDnsSupport": true, "InstanceTenancy": "default", "Tags": [ { "Key": "Name", "Value": "ServerlessWebappStarterKitStack/Vpc", }, ], }, "Type": "AWS::EC2::VPC", }, "VpcIGWD7BA715C": { "Properties": { "Tags": [ { "Key": "Name", "Value": "ServerlessWebappStarterKitStack/Vpc", }, ], }, "Type": "AWS::EC2::InternetGateway", }, "VpcNatSecurityGroup8DA26EDC": { "Properties": { "GroupDescription": "Security Group for NAT instances", "SecurityGroupEgress": [ { "CidrIp": "0.0.0.0/0", "Description": "Allow all outbound traffic by default", "IpProtocol": "-1", }, ], "SecurityGroupIngress": [ { "CidrIp": "0.0.0.0/0", "Description": "from 0.0.0.0/0:ALL TRAFFIC", "IpProtocol": "-1", }, ], "Tags": [ { "Key": "Name", "Value": "ServerlessWebappStarterKitStack/Vpc", }, ], "VpcId": { "Ref": "Vpc8378EB38", }, }, "Type": "AWS::EC2::SecurityGroup", }, "VpcPrivateSubnet1DefaultRouteBE02A9ED": { "Properties": { "DestinationCidrBlock": "0.0.0.0/0", "InstanceId": { "Ref": "VpcPublicSubnet1NatInstance57B636B8", }, "RouteTableId": { "Ref": "VpcPrivateSubnet1RouteTableB2C5B500", }, }, "Type": "AWS::EC2::Route", }, "VpcPrivateSubnet1RouteTableAssociation70C59FA6": { "Properties": { "RouteTableId": { "Ref": "VpcPrivateSubnet1RouteTableB2C5B500", }, "SubnetId": { "Ref": "VpcPrivateSubnet1Subnet536B997A", }, }, "Type": "AWS::EC2::SubnetRouteTableAssociation", }, "VpcPrivateSubnet1RouteTableB2C5B500": { "Properties": { "Tags": [ { "Key": "Name", "Value": "ServerlessWebappStarterKitStack/Vpc/PrivateSubnet1", }, ], "VpcId": { "Ref": "Vpc8378EB38", }, }, "Type": "AWS::EC2::RouteTable", }, "VpcPrivateSubnet1Subnet536B997A": { "Properties": { "AvailabilityZone": "dummy1a", "CidrBlock": "10.0.96.0/19", "MapPublicIpOnLaunch": false, "Tags": [ { "Key": "aws-cdk:subnet-name", "Value": "Private", }, { "Key": "aws-cdk:subnet-type", "Value": "Private", }, { "Key": "Name", "Value": "ServerlessWebappStarterKitStack/Vpc/PrivateSubnet1", }, ], "VpcId": { "Ref": "Vpc8378EB38", }, }, "Type": "AWS::EC2::Subnet", }, "VpcPrivateSubnet2DefaultRoute060D2087": { "Properties": { "DestinationCidrBlock": "0.0.0.0/0", "InstanceId": { "Ref": "VpcPublicSubnet1NatInstance57B636B8", }, "RouteTableId": { "Ref": "VpcPrivateSubnet2RouteTableA678073B", }, }, "Type": "AWS::EC2::Route", }, "VpcPrivateSubnet2RouteTableA678073B": { "Properties": { "Tags": [ { "Key": "Name", "Value": "ServerlessWebappStarterKitStack/Vpc/PrivateSubnet2", }, ], "VpcId": { "Ref": "Vpc8378EB38", }, }, "Type": "AWS::EC2::RouteTable", }, "VpcPrivateSubnet2RouteTableAssociationA89CAD56": { "Properties": { "RouteTableId": { "Ref": "VpcPrivateSubnet2RouteTableA678073B", }, "SubnetId": { "Ref": "VpcPrivateSubnet2Subnet3788AAA1", }, }, "Type": "AWS::EC2::SubnetRouteTableAssociation", }, "VpcPrivateSubnet2Subnet3788AAA1": { "Properties": { "AvailabilityZone": "dummy1b", "CidrBlock": "10.0.128.0/19", "MapPublicIpOnLaunch": false, "Tags": [ { "Key": "aws-cdk:subnet-name", "Value": "Private", }, { "Key": "aws-cdk:subnet-type", "Value": "Private", }, { "Key": "Name", "Value": "ServerlessWebappStarterKitStack/Vpc/PrivateSubnet2", }, ], "VpcId": { "Ref": "Vpc8378EB38", }, }, "Type": "AWS::EC2::Subnet", }, "VpcPrivateSubnet3DefaultRoute94B74F0D": { "Properties": { "DestinationCidrBlock": "0.0.0.0/0", "InstanceId": { "Ref": "VpcPublicSubnet1NatInstance57B636B8", }, "RouteTableId": { "Ref": "VpcPrivateSubnet3RouteTableD98824C7", }, }, "Type": "AWS::EC2::Route", }, "VpcPrivateSubnet3RouteTableAssociation16BDDC43": { "Properties": { "RouteTableId": { "Ref": "VpcPrivateSubnet3RouteTableD98824C7", }, "SubnetId": { "Ref": "VpcPrivateSubnet3SubnetF258B56E", }, }, "Type": "AWS::EC2::SubnetRouteTableAssociation", }, "VpcPrivateSubnet3RouteTableD98824C7": { "Properties": { "Tags": [ { "Key": "Name", "Value": "ServerlessWebappStarterKitStack/Vpc/PrivateSubnet3", }, ], "VpcId": { "Ref": "Vpc8378EB38", }, }, "Type": "AWS::EC2::RouteTable", }, "VpcPrivateSubnet3SubnetF258B56E": { "Properties": { "AvailabilityZone": "dummy1c", "CidrBlock": "10.0.160.0/19", "MapPublicIpOnLaunch": false, "Tags": [ { "Key": "aws-cdk:subnet-name", "Value": "Private", }, { "Key": "aws-cdk:subnet-type", "Value": "Private", }, { "Key": "Name", "Value": "ServerlessWebappStarterKitStack/Vpc/PrivateSubnet3", }, ], "VpcId": { "Ref": "Vpc8378EB38", }, }, "Type": "AWS::EC2::Subnet", }, "VpcPublicSubnet1DefaultRoute3DA9E72A": { "DependsOn": [ "VpcVPCGWBF912B6E", ], "Properties": { "DestinationCidrBlock": "0.0.0.0/0", "GatewayId": { "Ref": "VpcIGWD7BA715C", }, "RouteTableId": { "Ref": "VpcPublicSubnet1RouteTable6C95E38E", }, }, "Type": "AWS::EC2::Route", }, "VpcPublicSubnet1NatInstance57B636B8": { "DependsOn": [ "VpcPublicSubnet1DefaultRoute3DA9E72A", "VpcPublicSubnet1NatInstanceInstanceRole9D835E32", "VpcPublicSubnet1RouteTableAssociation97140677", ], "Properties": { "AvailabilityZone": "dummy1a", "IamInstanceProfile": { "Ref": "VpcPublicSubnet1NatInstanceInstanceProfileEE10C485", }, "ImageId": { "Ref": "SsmParameterValueawsserviceamiamazonlinuxlatestal2023amikernel61arm64C96584B6F00A464EAD1953AFF4B05118Parameter", }, "InstanceType": "t4g.nano", "NetworkInterfaces": [ { "AssociatePublicIpAddress": true, "DeviceIndex": "0", "GroupSet": [ { "Fn::GetAtt": [ "VpcNatSecurityGroup8DA26EDC", "GroupId", ], }, ], "SubnetId": { "Ref": "VpcPublicSubnet1Subnet5C2D37C4", }, }, ], "SourceDestCheck": false, "Tags": [ { "Key": "Name", "Value": "ServerlessWebappStarterKitStack/Vpc/PublicSubnet1/NatInstance", }, ], "UserData": { "Fn::Base64": "#!/bin/bash for i in {1..5}; do yum install iptables-services -y && break || sleep 10; done systemctl enable iptables systemctl start iptables echo "net.ipv4.ip_forward=1" > /etc/sysctl.d/custom-ip-forwarding.conf sysctl -p /etc/sysctl.d/custom-ip-forwarding.conf IFACE=$(ip route show default | awk '{print $5}') /sbin/iptables -t nat -A POSTROUTING -o $IFACE -j MASQUERADE /sbin/iptables -F FORWARD service iptables save", }, }, "Type": "AWS::EC2::Instance", }, "VpcPublicSubnet1NatInstanceInstanceProfileEE10C485": { "Properties": { "Roles": [ { "Ref": "VpcPublicSubnet1NatInstanceInstanceRole9D835E32", }, ], }, "Type": "AWS::IAM::InstanceProfile", }, "VpcPublicSubnet1NatInstanceInstanceRole9D835E32": { "Properties": { "AssumeRolePolicyDocument": { "Statement": [ { "Action": "sts:AssumeRole", "Effect": "Allow", "Principal": { "Service": "ec2.amazonaws.com", }, }, ], "Version": "2012-10-17", }, "Tags": [ { "Key": "Name", "Value": "ServerlessWebappStarterKitStack/Vpc/PublicSubnet1/NatInstance", }, ], }, "Type": "AWS::IAM::Role", }, "VpcPublicSubnet1RouteTable6C95E38E": { "Properties": { "Tags": [ { "Key": "Name", "Value": "ServerlessWebappStarterKitStack/Vpc/PublicSubnet1", }, ], "VpcId": { "Ref": "Vpc8378EB38", }, }, "Type": "AWS::EC2::RouteTable", }, "VpcPublicSubnet1RouteTableAssociation97140677": { "Properties": { "RouteTableId": { "Ref": "VpcPublicSubnet1RouteTable6C95E38E", }, "SubnetId": { "Ref": "VpcPublicSubnet1Subnet5C2D37C4", }, }, "Type": "AWS::EC2::SubnetRouteTableAssociation", }, "VpcPublicSubnet1Subnet5C2D37C4": { "Properties": { "AvailabilityZone": "dummy1a", "CidrBlock": "10.0.0.0/19", "MapPublicIpOnLaunch": true, "Tags": [ { "Key": "aws-cdk:subnet-name", "Value": "Public", }, { "Key": "aws-cdk:subnet-type", "Value": "Public", }, { "Key": "Name", "Value": "ServerlessWebappStarterKitStack/Vpc/PublicSubnet1", }, ], "VpcId": { "Ref": "Vpc8378EB38", }, }, "Type": "AWS::EC2::Subnet", }, "VpcPublicSubnet2DefaultRoute97F91067": { "DependsOn": [ "VpcVPCGWBF912B6E", ], "Properties": { "DestinationCidrBlock": "0.0.0.0/0", "GatewayId": { "Ref": "VpcIGWD7BA715C", }, "RouteTableId": { "Ref": "VpcPublicSubnet2RouteTable94F7E489", }, }, "Type": "AWS::EC2::Route", }, "VpcPublicSubnet2RouteTable94F7E489": { "Properties": { "Tags": [ { "Key": "Name", "Value": "ServerlessWebappStarterKitStack/Vpc/PublicSubnet2", }, ], "VpcId": { "Ref": "Vpc8378EB38", }, }, "Type": "AWS::EC2::RouteTable", }, "VpcPublicSubnet2RouteTableAssociationDD5762D8": { "Properties": { "RouteTableId": { "Ref": "VpcPublicSubnet2RouteTable94F7E489", }, "SubnetId": { "Ref": "VpcPublicSubnet2Subnet691E08A3", }, }, "Type": "AWS::EC2::SubnetRouteTableAssociation", }, "VpcPublicSubnet2Subnet691E08A3": { "Properties": { "AvailabilityZone": "dummy1b", "CidrBlock": "10.0.32.0/19", "MapPublicIpOnLaunch": true, "Tags": [ { "Key": "aws-cdk:subnet-name", "Value": "Public", }, { "Key": "aws-cdk:subnet-type", "Value": "Public", }, { "Key": "Name", "Value": "ServerlessWebappStarterKitStack/Vpc/PublicSubnet2", }, ], "VpcId": { "Ref": "Vpc8378EB38", }, }, "Type": "AWS::EC2::Subnet", }, "VpcPublicSubnet3DefaultRoute4697774F": { "DependsOn": [ "VpcVPCGWBF912B6E", ], "Properties": { "DestinationCidrBlock": "0.0.0.0/0", "GatewayId": { "Ref": "VpcIGWD7BA715C", }, "RouteTableId": { "Ref": "VpcPublicSubnet3RouteTable93458DBB", }, }, "Type": "AWS::EC2::Route", }, "VpcPublicSubnet3RouteTable93458DBB": { "Properties": { "Tags": [ { "Key": "Name", "Value": "ServerlessWebappStarterKitStack/Vpc/PublicSubnet3", }, ], "VpcId": { "Ref": "Vpc8378EB38", }, }, "Type": "AWS::EC2::RouteTable", }, "VpcPublicSubnet3RouteTableAssociation1F1EDF02": { "Properties": { "RouteTableId": { "Ref": "VpcPublicSubnet3RouteTable93458DBB", }, "SubnetId": { "Ref": "VpcPublicSubnet3SubnetBE12F0B6", }, }, "Type": "AWS::EC2::SubnetRouteTableAssociation", }, "VpcPublicSubnet3SubnetBE12F0B6": { "Properties": { "AvailabilityZone": "dummy1c", "CidrBlock": "10.0.64.0/19", "MapPublicIpOnLaunch": true, "Tags": [ { "Key": "aws-cdk:subnet-name", "Value": "Public", }, { "Key": "aws-cdk:subnet-type", "Value": "Public", }, { "Key": "Name", "Value": "ServerlessWebappStarterKitStack/Vpc/PublicSubnet3", }, ], "VpcId": { "Ref": "Vpc8378EB38", }, }, "Type": "AWS::EC2::Subnet", }, "VpcVPCGWBF912B6E": { "Properties": { "InternetGatewayId": { "Ref": "VpcIGWD7BA715C", }, "VpcId": { "Ref": "Vpc8378EB38", }, }, "Type": "AWS::EC2::VPCGatewayAttachment", }, "Webapp107041BD": { "Properties": { "DistributionConfig": { "Aliases": [ "web.example.com", ], "Comment": "CloudFront for Webapp", "DefaultCacheBehavior": { "AllowedMethods": [ "GET", "HEAD", "OPTIONS", "PUT", "PATCH", "POST", "DELETE", ], "CachePolicyId": { "Ref": "WebappSharedCachePolicy14FEE4A0", }, "Compress": true, "FunctionAssociations": [ { "EventType": "viewer-request", "FunctionARN": { "Fn::GetAtt": [ "WebappCacheKeyFunction6C227CE2", "FunctionARN", ], }, }, ], "LambdaFunctionAssociations": [ { "EventType": "origin-request", "IncludeBody": true, "LambdaFunctionARN": { "Fn::GetAtt": [ "LookupVersionArnc8730278af02f875114ca902814c77b68f19b0087110E04D0A", "Parameter.Value", ], }, }, ], "OriginRequestPolicyId": "b689b0a8-53d0-40ab-baf2-68738e2966ac", "TargetOriginId": "ServerlessWebappStarterKitStackWebappOrigin1D7B867FF", "ViewerProtocolPolicy": "allow-all", }, "Enabled": true, "HttpVersion": "http2", "IPV6Enabled": true, "Logging": { "Bucket": { "Fn::GetAtt": [ "AccessLogBucketDA470295", "RegionalDomainName", ], }, "Prefix": "Webapp/", }, "Origins": [ { "ConnectionTimeout": 6, "CustomOriginConfig": { "OriginProtocolPolicy": "https-only", "OriginReadTimeout": 60, "OriginSSLProtocols": [ "TLSv1.2", ], }, "DomainName": { "Fn::Select": [ 2, { "Fn::Split": [ "/", { "Fn::GetAtt": [ "WebappHandlerFunctionUrl7AEF8DEE", "FunctionUrl", ], }, ], }, ], }, "Id": "ServerlessWebappStarterKitStackWebappOrigin1D7B867FF", "OriginAccessControlId": { "Fn::GetAtt": [ "WebappOrigin1FunctionUrlOriginAccessControlEA98B600", "Id", ], }, }, ], "ViewerCertificate": { "AcmCertificateArn": { "Fn::GetAtt": [ "ExportsReader8B249524", "/cdk/exports/ServerlessWebappStarterKitStack/ServerlessWebappStarterKitUsEast1Stackuseast1RefCertificateV29EE77EAF5C85697F", ], }, "MinimumProtocolVersion": "TLSv1.2_2021", "SslSupportMethod": "sni-only", }, }, }, "Type": "AWS::CloudFront::Distribution", }, "WebappBuild348806E3": { "DeletionPolicy": "Delete", "DependsOn": [ "ContainerImageBuildArm64e83729feb1564e709bec452b15847a30amd64C13E3549", "ContainerImageBuildArm64e83729feb1564e709bec452b15847a30amd64RoleDefaultPolicy2316728F", "ContainerImageBuildArm64e83729feb1564e709bec452b15847a30amd64RoleC5F7BBFE", ], "Properties": { "ServiceToken": { "Fn::GetAtt": [ "DeployTimeBuildCustomResourceHandlerdb740fd554364a848a09e6dfcd01f4f306AEFF37", "Arn", ], }, "buildCommand": { "Fn::Join": [ "", [ "docker buildx build --build-arg ALLOWED_ORIGIN_HOST=*.example.com --build-arg SKIP_TS_BUILD=true --build-arg NEXT_PUBLIC_EVENT_HTTP_ENDPOINT=https://", { "Fn::GetAtt": [ "EventBusApi6E8C7C94", "Dns.Http", ], }, " --build-arg NEXT_PUBLIC_AWS_REGION=us-west-2 --platform linux/arm64 --output type=image,name=", { "Fn::Select": [ 4, { "Fn::Split": [ ":", { "Fn::GetAtt": [ "WebappBuildRepository4C93D48D", "Arn", ], }, ], }, ], }, ".dkr.ecr.", { "Fn::Select": [ 3, { "Fn::Split": [ ":", { "Fn::GetAtt": [ "WebappBuildRepository4C93D48D", "Arn", ], }, ], }, ], }, ".", { "Ref": "AWS::URLSuffix", }, "/", { "Ref": "WebappBuildRepository4C93D48D", }, ":,push=true --provenance=false .", ], ], }, "codeBuildProjectName": { "Ref": "ContainerImageBuildArm64e83729feb1564e709bec452b15847a30amd64C13E3549", }, "repositoryUri": { "Fn::Join": [ "", [ { "Fn::Select": [ 4, { "Fn::Split": [ ":", { "Fn::GetAtt": [ "WebappBuildRepository4C93D48D", "Arn", ], }, ], }, ], }, ".dkr.ecr.", { "Fn::Select": [ 3, { "Fn::Split": [ ":", { "Fn::GetAtt": [ "WebappBuildRepository4C93D48D", "Arn", ], }, ], }, ], }, ".", { "Ref": "AWS::URLSuffix", }, "/", { "Ref": "WebappBuildRepository4C93D48D", }, ], ], }, "sourceS3Url": "s3://cdk-hnb659fds-assets-123456789012-us-west-2/REDACTED", "tagPrefix": "REDACTED", "type": "ContainerImageBuild", }, "Type": "Custom::CDKContainerImageBuild", "UpdateReplacePolicy": "Delete", }, "WebappBuildRepository4C93D48D": { "DeletionPolicy": "Delete", "Properties": { "EmptyOnDelete": true, "RepositoryPolicyText": { "Statement": [ { "Action": [ "ecr:BatchCheckLayerAvailability", "ecr:GetDownloadUrlForLayer", "ecr:BatchGetImage", ], "Effect": "Allow", "Principal": { "Service": "lambda.amazonaws.com", }, }, ], "Version": "2012-10-17", }, }, "Type": "AWS::ECR::Repository", "UpdateReplacePolicy": "Delete", }, "WebappCacheKeyFunction6C227CE2": { "Properties": { "AutoPublish": true, "FunctionCode": "// CloudFront Functions JS 2.0 // Combines Next.js RSC-related headers into a single hashed cache key header // to prevent cache poisoning between HTML and RSC flight responses. // // Next.js App Router sets Vary: rsc, next-router-state-tree, next-router-prefetch, // next-router-segment-prefetch (and next-url for interception routes). // CloudFront ignores Vary and requires explicit cache key configuration, // but its Cache Policy has a 10-header limit. This function hashes all // RSC headers into one header to stay within the limit. async function handler(event) { var h = event.request.headers; var parts = [ 'rsc', 'next-router-prefetch', 'next-router-state-tree', 'next-router-segment-prefetch', 'next-url', ]; var key = ''; for (var i = 0; i < parts.length; i++) { if (h[parts[i]]) { key += parts[i] + '=' + h[parts[i]].value + ';'; } } if (key) { // FNV-1a hash (32-bit). Cryptographic strength is unnecessary; // we only need distinct cache keys for distinct header combinations. // See: https://en.wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function var FNV_OFFSET_BASIS = 2166136261; var FNV_PRIME = 16777619; var hash = FNV_OFFSET_BASIS; for (var j = 0; j < key.length; j++) { hash ^= key.charCodeAt(j); hash = (hash * FNV_PRIME) | 0; } event.request.headers['x-nextjs-cache-key'] = { value: String(hash >>> 0) }; } return event.request; } ", "FunctionConfig": { "Comment": "us-west-2ServerlessWebappCacheKeyFunction86D1ABE9", "Runtime": "cloudfront-js-2.0", }, "Name": "us-west-2ServerlessWebappCacheKeyFunction86D1ABE9", }, "Type": "AWS::CloudFront::Function", }, "WebappCloudFrontInvalidation588CF152": { "DeletionPolicy": "Delete", "DependsOn": [ "WebappCloudFrontInvalidationCustomResourcePolicy18C215D6", ], "Properties": { "Create": { "Fn::Join": [ "", [ "{"service":"cloudfront","action":"createInvalidation","parameters":{"DistributionId":"", { "Ref": "Webapp107041BD", }, "","InvalidationBatch":{"CallerReference":"", { "Fn::GetAtt": [ "WebappHandlerCurrentVersionREDACTED", "Version", ], }, "","Paths":{"Quantity":1,"Items":["/*"]}}},"physicalResourceId":{"id":"invalidation"}}", ], ], }, "InstallLatestAwsSdk": true, "ServiceToken": { "Fn::GetAtt": [ "AWS679f53fac002430cb0da5b7982bd22872D164C4C", "Arn", ], }, "Update": { "Fn::Join": [ "", [ "{"service":"cloudfront","action":"createInvalidation","parameters":{"DistributionId":"", { "Ref": "Webapp107041BD", }, "","InvalidationBatch":{"CallerReference":"", { "Fn::GetAtt": [ "WebappHandlerCurrentVersionREDACTED", "Version", ], }, "","Paths":{"Quantity":1,"Items":["/*"]}}},"physicalResourceId":{"id":"invalidation"}}", ], ], }, }, "Type": "Custom::AWS", "UpdateReplacePolicy": "Delete", }, "WebappCloudFrontInvalidationCustomResourcePolicy18C215D6": { "Properties": { "PolicyDocument": { "Statement": [ { "Action": "cloudfront:CreateInvalidation", "Effect": "Allow", "Resource": { "Fn::Join": [ "", [ "arn:", { "Ref": "AWS::Partition", }, ":cloudfront::123456789012:distribution/", { "Ref": "Webapp107041BD", }, ], ], }, }, ], "Version": "2012-10-17", }, "PolicyName": "WebappCloudFrontInvalidationCustomResourcePolicy18C215D6", "Roles": [ { "Ref": "AWS679f53fac002430cb0da5b7982bd2287ServiceRoleC1EA0FF2", }, ], }, "Type": "AWS::IAM::Policy", }, "WebappHandler8DD158A3": { "DependsOn": [ "VpcPrivateSubnet1DefaultRouteBE02A9ED", "VpcPrivateSubnet1RouteTableAssociation70C59FA6", "VpcPrivateSubnet2DefaultRoute060D2087", "VpcPrivateSubnet2RouteTableAssociationA89CAD56", "VpcPrivateSubnet3DefaultRoute94B74F0D", "VpcPrivateSubnet3RouteTableAssociation16BDDC43", "WebappHandlerServiceRoleDefaultPolicy7D06F4EA", "WebappHandlerServiceRole4F4D1ACD", ], "Properties": { "Architectures": [ "arm64", ], "Code": { "ImageUri": { "Fn::Join": [ "", [ { "Fn::Select": [ 4, { "Fn::Split": [ ":", { "Fn::GetAtt": [ "WebappBuildRepository4C93D48D", "Arn", ], }, ], }, ], }, ".dkr.ecr.", { "Fn::Select": [ 3, { "Fn::Split": [ ":", { "Fn::GetAtt": [ "WebappBuildRepository4C93D48D", "Arn", ], }, ], }, ], }, ".", { "Ref": "AWS::URLSuffix", }, "/", { "Ref": "WebappBuildRepository4C93D48D", }, ":", { "Fn::GetAtt": [ "WebappBuild348806E3", "ImageTag", ], }, ], ], }, }, "Environment": { "Variables": { "AMPLIFY_APP_ORIGIN": "https://web.example.com", "ASYNC_JOB_HANDLER_ARN": { "Fn::GetAtt": [ "AsyncJobHandler438266BD", "Arn", ], }, "COGNITO_DOMAIN": "auth.example.com", "DATABASE_ENGINE": { "Fn::Join": [ "", [ "{{resolve:secretsmanager:", { "Ref": "DatabaseClusterSecretAttachmentDC8466C0", }, ":SecretString:engine::}}", ], ], }, "DATABASE_HOST": { "Fn::GetAtt": [ "DatabaseCluster5B53A178", "Endpoint.Address", ], }, "DATABASE_NAME": "main", "DATABASE_OPTION": "?connection_limit=1&connect_timeout=30", "DATABASE_PASSWORD": { "Fn::Join": [ "", [ "{{resolve:secretsmanager:", { "Ref": "DatabaseClusterSecretAttachmentDC8466C0", }, ":SecretString:password::}}", ], ], }, "DATABASE_PORT": { "Fn::GetAtt": [ "DatabaseCluster5B53A178", "Endpoint.Port", ], }, "DATABASE_URL": { "Fn::Join": [ "", [ "{{resolve:secretsmanager:", { "Ref": "DatabaseClusterSecretAttachmentDC8466C0", }, ":SecretString:engine::}}://{{resolve:secretsmanager:", { "Ref": "DatabaseClusterSecretAttachmentDC8466C0", }, ":SecretString:username::}}:{{resolve:secretsmanager:", { "Ref": "DatabaseClusterSecretAttachmentDC8466C0", }, ":SecretString:password::}}@", { "Fn::GetAtt": [ "DatabaseCluster5B53A178", "Endpoint.Address", ], }, ":", { "Fn::GetAtt": [ "DatabaseCluster5B53A178", "Endpoint.Port", ], }, "/main?connection_limit=1&connect_timeout=30", ], ], }, "DATABASE_USER": { "Fn::Join": [ "", [ "{{resolve:secretsmanager:", { "Ref": "DatabaseClusterSecretAttachmentDC8466C0", }, ":SecretString:username::}}", ], ], }, "USER_POOL_CLIENT_ID": { "Ref": "AuthUserPoolClientC635291F", }, "USER_POOL_ID": { "Ref": "AuthUserPool8115E87F", }, }, }, "LoggingConfig": { "LogGroup": { "Ref": "WebappHandlerLogs87A6D2D7", }, }, "MemorySize": 1024, "PackageType": "Image", "Role": { "Fn::GetAtt": [ "WebappHandlerServiceRole4F4D1ACD", "Arn", ], }, "Timeout": 180, "VpcConfig": { "SecurityGroupIds": [ { "Fn::GetAtt": [ "WebappHandlerSecurityGroup5451B519", "GroupId", ], }, ], "SubnetIds": [ { "Ref": "VpcPrivateSubnet1Subnet536B997A", }, { "Ref": "VpcPrivateSubnet2Subnet3788AAA1", }, { "Ref": "VpcPrivateSubnet3SubnetF258B56E", }, ], }, }, "Type": "AWS::Lambda::Function", }, "WebappHandlerCurrentVersionREDACTED": { "DependsOn": [ "VpcPrivateSubnet1DefaultRouteBE02A9ED", "VpcPrivateSubnet1RouteTableAssociation70C59FA6", "VpcPrivateSubnet2DefaultRoute060D2087", "VpcPrivateSubnet2RouteTableAssociationA89CAD56", "VpcPrivateSubnet3DefaultRoute94B74F0D", "VpcPrivateSubnet3RouteTableAssociation16BDDC43", ], "Properties": { "FunctionName": { "Ref": "WebappHandler8DD158A3", }, }, "Type": "AWS::Lambda::Version", }, "WebappHandlerFunctionUrl7AEF8DEE": { "DependsOn": [ "VpcPrivateSubnet1DefaultRouteBE02A9ED", "VpcPrivateSubnet1RouteTableAssociation70C59FA6", "VpcPrivateSubnet2DefaultRoute060D2087", "VpcPrivateSubnet2RouteTableAssociationA89CAD56", "VpcPrivateSubnet3DefaultRoute94B74F0D", "VpcPrivateSubnet3RouteTableAssociation16BDDC43", ], "Properties": { "AuthType": "AWS_IAM", "InvokeMode": "RESPONSE_STREAM", "TargetFunctionArn": { "Fn::GetAtt": [ "WebappHandler8DD158A3", "Arn", ], }, }, "Type": "AWS::Lambda::Url", }, "WebappHandlerLogs87A6D2D7": { "DeletionPolicy": "Delete", "Properties": { "RetentionInDays": 7, }, "Type": "AWS::Logs::LogGroup", "UpdateReplacePolicy": "Delete", }, "WebappHandlerSecurityGroup5451B519": { "DependsOn": [ "VpcPrivateSubnet1DefaultRouteBE02A9ED", "VpcPrivateSubnet1RouteTableAssociation70C59FA6", "VpcPrivateSubnet2DefaultRoute060D2087", "VpcPrivateSubnet2RouteTableAssociationA89CAD56", "VpcPrivateSubnet3DefaultRoute94B74F0D", "VpcPrivateSubnet3RouteTableAssociation16BDDC43", ], "Properties": { "GroupDescription": "Automatic security group for Lambda Function ServerlessWebappStarterKitStackWebappHandlerF1A4ACC9", "SecurityGroupEgress": [ { "CidrIp": "0.0.0.0/0", "Description": "Allow all outbound traffic by default", "IpProtocol": "-1", }, ], "VpcId": { "Ref": "Vpc8378EB38", }, }, "Type": "AWS::EC2::SecurityGroup", }, "WebappHandlerServiceRole4F4D1ACD": { "DependsOn": [ "VpcPrivateSubnet1DefaultRouteBE02A9ED", "VpcPrivateSubnet1RouteTableAssociation70C59FA6", "VpcPrivateSubnet2DefaultRoute060D2087", "VpcPrivateSubnet2RouteTableAssociationA89CAD56", "VpcPrivateSubnet3DefaultRoute94B74F0D", "VpcPrivateSubnet3RouteTableAssociation16BDDC43", ], "Properties": { "AssumeRolePolicyDocument": { "Statement": [ { "Action": "sts:AssumeRole", "Effect": "Allow", "Principal": { "Service": "lambda.amazonaws.com", }, }, ], "Version": "2012-10-17", }, "ManagedPolicyArns": [ { "Fn::Join": [ "", [ "arn:", { "Ref": "AWS::Partition", }, ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole", ], ], }, { "Fn::Join": [ "", [ "arn:", { "Ref": "AWS::Partition", }, ":iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole", ], ], }, ], }, "Type": "AWS::IAM::Role", }, "WebappHandlerServiceRoleDefaultPolicy7D06F4EA": { "DependsOn": [ "VpcPrivateSubnet1DefaultRouteBE02A9ED", "VpcPrivateSubnet1RouteTableAssociation70C59FA6", "VpcPrivateSubnet2DefaultRoute060D2087", "VpcPrivateSubnet2RouteTableAssociationA89CAD56", "VpcPrivateSubnet3DefaultRoute94B74F0D", "VpcPrivateSubnet3RouteTableAssociation16BDDC43", ], "Properties": { "PolicyDocument": { "Statement": [ { "Action": "lambda:InvokeFunction", "Effect": "Allow", "Resource": [ { "Fn::GetAtt": [ "AsyncJobHandler438266BD", "Arn", ], }, { "Fn::Join": [ "", [ { "Fn::GetAtt": [ "AsyncJobHandler438266BD", "Arn", ], }, ":*", ], ], }, ], }, ], "Version": "2012-10-17", }, "PolicyName": "WebappHandlerServiceRoleDefaultPolicy7D06F4EA", "Roles": [ { "Ref": "WebappHandlerServiceRole4F4D1ACD", }, ], }, "Type": "AWS::IAM::Policy", }, "WebappInvokeFunctionPermission8F3F2610": { "Properties": { "Action": "lambda:InvokeFunction", "FunctionName": { "Fn::GetAtt": [ "WebappHandler8DD158A3", "Arn", ], }, "Principal": "cloudfront.amazonaws.com", "SourceArn": { "Fn::Join": [ "", [ "arn:", { "Ref": "AWS::Partition", }, ":cloudfront::", { "Ref": "AWS::AccountId", }, ":distribution/", { "Ref": "Webapp107041BD", }, ], ], }, }, "Type": "AWS::Lambda::Permission", }, "WebappMigrationRunnerAC67C012": { "DependsOn": [ "VpcPrivateSubnet1DefaultRouteBE02A9ED", "VpcPrivateSubnet1RouteTableAssociation70C59FA6", "VpcPrivateSubnet2DefaultRoute060D2087", "VpcPrivateSubnet2RouteTableAssociationA89CAD56", "VpcPrivateSubnet3DefaultRoute94B74F0D", "VpcPrivateSubnet3RouteTableAssociation16BDDC43", "WebappMigrationRunnerServiceRoleE27E1F7A", ], "Properties": { "Architectures": [ "arm64", ], "Code": { "ImageUri": { "Fn::Sub": "REDACTED", }, }, "Environment": { "Variables": { "DATABASE_ENGINE": { "Fn::Join": [ "", [ "{{resolve:secretsmanager:", { "Ref": "DatabaseClusterSecretAttachmentDC8466C0", }, ":SecretString:engine::}}", ], ], }, "DATABASE_HOST": { "Fn::GetAtt": [ "DatabaseCluster5B53A178", "Endpoint.Address", ], }, "DATABASE_NAME": "main", "DATABASE_OPTION": "?connection_limit=1&connect_timeout=30", "DATABASE_PASSWORD": { "Fn::Join": [ "", [ "{{resolve:secretsmanager:", { "Ref": "DatabaseClusterSecretAttachmentDC8466C0", }, ":SecretString:password::}}", ], ], }, "DATABASE_PORT": { "Fn::GetAtt": [ "DatabaseCluster5B53A178", "Endpoint.Port", ], }, "DATABASE_URL": { "Fn::Join": [ "", [ "{{resolve:secretsmanager:", { "Ref": "DatabaseClusterSecretAttachmentDC8466C0", }, ":SecretString:engine::}}://{{resolve:secretsmanager:", { "Ref": "DatabaseClusterSecretAttachmentDC8466C0", }, ":SecretString:username::}}:{{resolve:secretsmanager:", { "Ref": "DatabaseClusterSecretAttachmentDC8466C0", }, ":SecretString:password::}}@", { "Fn::GetAtt": [ "DatabaseCluster5B53A178", "Endpoint.Address", ], }, ":", { "Fn::GetAtt": [ "DatabaseCluster5B53A178", "Endpoint.Port", ], }, "/main?connection_limit=1&connect_timeout=30", ], ], }, "DATABASE_USER": { "Fn::Join": [ "", [ "{{resolve:secretsmanager:", { "Ref": "DatabaseClusterSecretAttachmentDC8466C0", }, ":SecretString:username::}}", ], ], }, }, }, "ImageConfig": { "Command": [ "migration-runner.handler", ], }, "LoggingConfig": { "LogGroup": { "Ref": "WebappMigrationRunnerLogsD9A84B90", }, }, "MemorySize": 256, "PackageType": "Image", "Role": { "Fn::GetAtt": [ "WebappMigrationRunnerServiceRoleE27E1F7A", "Arn", ], }, "Timeout": 300, "VpcConfig": { "SecurityGroupIds": [ { "Fn::GetAtt": [ "WebappMigrationRunnerSecurityGroup7F0DF264", "GroupId", ], }, ], "SubnetIds": [ { "Ref": "VpcPrivateSubnet1Subnet536B997A", }, { "Ref": "VpcPrivateSubnet2Subnet3788AAA1", }, { "Ref": "VpcPrivateSubnet3SubnetF258B56E", }, ], }, }, "Type": "AWS::Lambda::Function", }, "WebappMigrationRunnerCurrentVersionREDACTED": { "DependsOn": [ "VpcPrivateSubnet1DefaultRouteBE02A9ED", "VpcPrivateSubnet1RouteTableAssociation70C59FA6", "VpcPrivateSubnet2DefaultRoute060D2087", "VpcPrivateSubnet2RouteTableAssociationA89CAD56", "VpcPrivateSubnet3DefaultRoute94B74F0D", "VpcPrivateSubnet3RouteTableAssociation16BDDC43", ], "Properties": { "FunctionName": { "Ref": "WebappMigrationRunnerAC67C012", }, }, "Type": "AWS::Lambda::Version", }, "WebappMigrationRunnerLogsD9A84B90": { "DeletionPolicy": "Delete", "Properties": { "RetentionInDays": 7, }, "Type": "AWS::Logs::LogGroup", "UpdateReplacePolicy": "Delete", }, "WebappMigrationRunnerSecurityGroup7F0DF264": { "DependsOn": [ "VpcPrivateSubnet1DefaultRouteBE02A9ED", "VpcPrivateSubnet1RouteTableAssociation70C59FA6", "VpcPrivateSubnet2DefaultRoute060D2087", "VpcPrivateSubnet2RouteTableAssociationA89CAD56", "VpcPrivateSubnet3DefaultRoute94B74F0D", "VpcPrivateSubnet3RouteTableAssociation16BDDC43", ], "Properties": { "GroupDescription": "Automatic security group for Lambda Function ServerlessWebappStarterKitStackWebappMigrationRunner45EAC73E", "SecurityGroupEgress": [ { "CidrIp": "0.0.0.0/0", "Description": "Allow all outbound traffic by default", "IpProtocol": "-1", }, ], "VpcId": { "Ref": "Vpc8378EB38", }, }, "Type": "AWS::EC2::SecurityGroup", }, "WebappMigrationRunnerServiceRoleE27E1F7A": { "DependsOn": [ "VpcPrivateSubnet1DefaultRouteBE02A9ED", "VpcPrivateSubnet1RouteTableAssociation70C59FA6", "VpcPrivateSubnet2DefaultRoute060D2087", "VpcPrivateSubnet2RouteTableAssociationA89CAD56", "VpcPrivateSubnet3DefaultRoute94B74F0D", "VpcPrivateSubnet3RouteTableAssociation16BDDC43", ], "Properties": { "AssumeRolePolicyDocument": { "Statement": [ { "Action": "sts:AssumeRole", "Effect": "Allow", "Principal": { "Service": "lambda.amazonaws.com", }, }, ], "Version": "2012-10-17", }, "ManagedPolicyArns": [ { "Fn::Join": [ "", [ "arn:", { "Ref": "AWS::Partition", }, ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole", ], ], }, { "Fn::Join": [ "", [ "arn:", { "Ref": "AWS::Partition", }, ":iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole", ], ], }, ], }, "Type": "AWS::IAM::Role", }, "WebappMigrationTrigger42AFC1D9": { "DeletionPolicy": "Delete", "DependsOn": [ "DatabaseClusterLogRetentionpostgresql025D39CE", "DatabaseCluster5B53A178", "DatabaseClusterSecretAttachmentDC8466C0", "DatabaseClusterSecretD1FB634F", "DatabaseClusterSecurityGroupfromServerlessWebappStarterKitStackAsyncJobHandlerSecurityGroup5220DFB3IndirectPort9323962E", "DatabaseClusterSecurityGroupfromServerlessWebappStarterKitStackDatabaseBastionHostInstanceSecurityGroup4F0DD25BIndirectPort8AFD9922", "DatabaseClusterSecurityGroupfromServerlessWebappStarterKitStackWebappHandlerSecurityGroupA009CF4AIndirectPort7311063E", "DatabaseClusterSecurityGroupfromServerlessWebappStarterKitStackWebappMigrationRunnerSecurityGroupC0959349IndirectPortDCF4A356", "DatabaseClusterSecurityGroupFEF1426A", "DatabaseClusterSubnets5540150D", "DatabaseClusterWriterD43085C6", ], "Properties": { "ExecuteOnHandlerChange": true, "HandlerArn": { "Ref": "WebappMigrationRunnerCurrentVersionREDACTED", }, "InvocationType": "RequestResponse", "ServiceToken": { "Fn::GetAtt": [ "AWSCDKTriggerCustomResourceProviderCustomResourceProviderHandler97BECD91", "Arn", ], }, "Timeout": "120000", }, "Type": "Custom::Trigger", "UpdateReplacePolicy": "Delete", }, "WebappOrigin1FunctionUrlOriginAccessControlEA98B600": { "Properties": { "OriginAccessControlConfig": { "Name": "ServerlessWebappStarterKitStnctionUrlOriginAccessControl17EB4E66", "OriginAccessControlOriginType": "lambda", "SigningBehavior": "always", "SigningProtocol": "sigv4", }, }, "Type": "AWS::CloudFront::OriginAccessControl", }, "WebappOrigin1InvokeFromApiForServerlessWebappStarterKitStackWebappOrigin1D7B867FF58DBB477": { "Properties": { "Action": "lambda:InvokeFunctionUrl", "FunctionName": { "Fn::GetAtt": [ "WebappHandlerFunctionUrl7AEF8DEE", "FunctionArn", ], }, "Principal": "cloudfront.amazonaws.com", "SourceArn": { "Fn::Join": [ "", [ "arn:", { "Ref": "AWS::Partition", }, ":cloudfront::", { "Ref": "AWS::AccountId", }, ":distribution/", { "Ref": "Webapp107041BD", }, ], ], }, }, "Type": "AWS::Lambda::Permission", }, "WebappRecord02DDD651": { "Properties": { "AliasTarget": { "DNSName": { "Fn::GetAtt": [ "Webapp107041BD", "DomainName", ], }, "HostedZoneId": { "Fn::FindInMap": [ "AWSCloudFrontPartitionHostedZoneIdMap", { "Ref": "AWS::Partition", }, "zoneId", ], }, }, "HostedZoneId": "DUMMY", "Name": "web.example.com.", "Type": "A", }, "Type": "AWS::Route53::RecordSet", }, "WebappSharedCachePolicy14FEE4A0": { "Properties": { "CachePolicyConfig": { "DefaultTTL": 0, "MaxTTL": 31536000, "MinTTL": 0, "Name": "ServerlessWebappStarterKitStackWebappSharedCachePolicy211E133B-us-west-2", "ParametersInCacheKeyAndForwardedToOrigin": { "CookiesConfig": { "CookieBehavior": "all", }, "EnableAcceptEncodingBrotli": true, "EnableAcceptEncodingGzip": true, "HeadersConfig": { "HeaderBehavior": "whitelist", "Headers": [ "authorization", "Origin", "X-HTTP-Method-Override", "X-HTTP-Method", "X-Method-Override", "x-nextjs-cache-key", ], }, "QueryStringsConfig": { "QueryStringBehavior": "all", }, }, }, }, "Type": "AWS::CloudFront::CachePolicy", }, }, "Rules": { "CheckBootstrapVersion": { "Assertions": [ { "Assert": { "Fn::Not": [ { "Fn::Contains": [ [ "1", "2", "3", "4", "5", ], { "Ref": "BootstrapVersion", }, ], }, ], }, "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI.", }, ], }, }, } `; ================================================ FILE: cdk/test/serverless-fullstack-webapp-starter-kit-without-domain.test.ts ================================================ import * as cdk from 'aws-cdk-lib'; import { Template } from 'aws-cdk-lib/assertions'; import { MainStack } from '../lib/main-stack'; import { UsEast1Stack } from '../lib/us-east-1-stack'; test('Snapshot test', () => { jest.useFakeTimers().setSystemTime(new Date('2020-01-01')); const app = new cdk.App(); const props = { account: '123456789012', }; const virginia = new UsEast1Stack(app, 'ServerlessWebappStarterKitUsEast1Stack', { env: { account: props.account, region: 'us-east-1', }, crossRegionReferences: true, }); const mainStack = new MainStack(app, 'ServerlessWebappStarterKitStack', { env: { account: props.account, region: 'us-west-2', }, crossRegionReferences: true, signPayloadHandler: virginia.signPayloadHandler, }); const virginiaTemplate = Template.fromStack(virginia); const mainTemplate = Template.fromStack(mainStack); expect(virginiaTemplate).toMatchSnapshot(); expect(mainTemplate).toMatchSnapshot(); }); ================================================ FILE: cdk/test/serverless-fullstack-webapp-starter-kit.test.ts ================================================ import * as cdk from 'aws-cdk-lib'; import { Template } from 'aws-cdk-lib/assertions'; import { MainStack } from '../lib/main-stack'; import { UsEast1Stack } from '../lib/us-east-1-stack'; test('Snapshot test', () => { jest.useFakeTimers().setSystemTime(new Date('2020-01-01')); const app = new cdk.App(); const props = { account: '123456789012', domainName: 'example.com', }; const virginia = new UsEast1Stack(app, 'ServerlessWebappStarterKitUsEast1Stack', { env: { account: props.account, region: 'us-east-1', }, crossRegionReferences: true, domainName: props.domainName, }); const mainStack = new MainStack(app, 'ServerlessWebappStarterKitStack', { env: { account: props.account, region: 'us-west-2', }, crossRegionReferences: true, sharedCertificate: virginia.certificate, domainName: props.domainName, signPayloadHandler: virginia.signPayloadHandler, }); const virginiaTemplate = Template.fromStack(virginia); const mainTemplate = Template.fromStack(mainStack); expect(virginiaTemplate).toMatchSnapshot(); expect(mainTemplate).toMatchSnapshot(); }); ================================================ FILE: cdk/test/snapshot-plugin.ts ================================================ module.exports = { test: (val: any) => typeof val === 'string', serialize: (val: string) => { return `"${val // .replace(/([A-Fa-f0-9]{64}.zip)/, 'REDACTED') .replace(/([A-Fa-f0-9]{64}.mjs)/, 'REDACTED') .replace(/.*cdk-hnb659fds-container-assets-.*/, 'REDACTED') .replace(/webapp-starter-[0-9a-z]*/, 'REDACTED') .replace(/(.*CurrentVersion).*/, '$1REDACTED')}"`; }, }; ================================================ FILE: cdk/tsconfig.json ================================================ { "compilerOptions": { "target": "ES2018", "module": "commonjs", "lib": [ "es2018" ], "declaration": true, "strict": true, "noImplicitAny": true, "strictNullChecks": true, "noImplicitThis": true, "alwaysStrict": true, "noUnusedLocals": false, "noUnusedParameters": false, "noImplicitReturns": true, "noFallthroughCasesInSwitch": false, "inlineSourceMap": true, "inlineSources": true, "experimentalDecorators": true, "noEmit": true, "strictPropertyInitialization": false, "typeRoots": [ "./node_modules/@types" ] }, "exclude": [ "node_modules", "cdk.out" ] } ================================================ FILE: compose.yaml ================================================ services: postgres: image: "public.ecr.aws/docker/library/postgres:16" ports: - "5432:5432" volumes: - "postgres_data:/var/lib/postgresql/data" restart: always environment: POSTGRES_PASSWORD: password POSTGRES_USER: root volumes: mysql_data: postgres_data: ================================================ FILE: release-please-config.json ================================================ { "packages": { ".": { "release-type": "simple", "initial-version": "2.0.0" } } } ================================================ FILE: webapp/.dockerignore ================================================ node_modules dist .next .env.local ================================================ FILE: webapp/.env.local.example ================================================ COGNITO_DOMAIN=auth.example.com AMPLIFY_APP_ORIGIN=http://localhost:3010 USER_POOL_CLIENT_ID=dummy USER_POOL_ID=us-west-2_dummy NEXT_PUBLIC_EVENT_HTTP_ENDPOINT="" NEXT_PUBLIC_AWS_REGION="us-west-2" ASYNC_JOB_HANDLER_ARN="" ================================================ FILE: webapp/.gitignore ================================================ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. # dependencies /node_modules /.pnp .pnp.* .yarn/* !.yarn/patches !.yarn/plugins !.yarn/releases !.yarn/versions # testing /coverage # next.js /.next/ /out/ # production /build # misc .DS_Store *.pem # debug npm-debug.log* yarn-debug.log* yarn-error.log* .pnpm-debug.log* # env files (can opt-in for committing if needed) .env* # vercel .vercel # typescript *.tsbuildinfo next-env.d.ts dist !prisma/.env !.env.local.example src/lib/generated ================================================ FILE: webapp/Dockerfile ================================================ FROM public.ecr.aws/lambda/nodejs:22 AS builder WORKDIR /build COPY package*.json ./ RUN --mount=type=cache,target=/root/.npm npm ci COPY ./ ./ RUN npx prisma generate COPY prisma ./ ARG SKIP_TS_BUILD="" ARG ALLOWED_ORIGIN_HOST="" ARG NEXT_PUBLIC_EVENT_HTTP_ENDPOINT="" ARG NEXT_PUBLIC_AWS_REGION="" ENV USER_POOL_CLIENT_ID="dummy" ENV USER_POOL_ID="dummy" ENV AMPLIFY_APP_ORIGIN="https://dummy.example.com" ENV COGNITO_DOMAIN="dummy.example.com" RUN --mount=type=cache,target=/build/.next/cache npm run build FROM public.ecr.aws/lambda/nodejs:22 AS runner COPY --from=public.ecr.aws/awsguru/aws-lambda-adapter:0.9.0 /lambda-adapter /opt/extensions/lambda-adapter ENV AWS_LWA_PORT=3000 ENV AWS_LWA_READINESS_CHECK_PATH="/api/health" ENV AWS_LWA_INVOKE_MODE="response_stream" COPY --from=builder /build/.next/static ./.next/static COPY --from=builder /build/.next/standalone ./ COPY --from=builder /build/run.sh ./run.sh RUN ln -s /tmp/cache ./.next/cache ENTRYPOINT ["sh"] CMD ["run.sh"] ================================================ FILE: webapp/README.md ================================================ ## Run locally ```bash # Run this command in the repository root docker compose up -d # Run these commands in the webapp directory cd webapp npm ci npx prisma db push cp .env.local.example .env.local # Edit .env.local with values from CDK deploy outputs npm run dev ``` Open [http://localhost:3010](http://localhost:3010) with your browser to see the result. ## Environment variables - Runtime env vars (e.g. `USER_POOL_ID`, `COGNITO_DOMAIN`) are set in `.env.local` for local development and injected via CDK `environment` for deployed builds. - Build-time env vars prefixed with `NEXT_PUBLIC_` must be set as CDK build args in `webapp.ts` — they are baked into the Docker image at build time and cannot be changed at runtime. See `.env.local.example` for the full list. ## Development guide See [`AGENTS.md`](../AGENTS.md) in the repository root for authentication patterns, async job setup, DB migration, coding conventions, and constraints. ================================================ FILE: webapp/components.json ================================================ { "$schema": "https://ui.shadcn.com/schema.json", "style": "new-york", "rsc": true, "tsx": true, "tailwind": { "config": "", "css": "src/app/globals.css", "baseColor": "neutral", "cssVariables": true, "prefix": "" }, "aliases": { "components": "@/components", "utils": "@/lib/utils", "ui": "@/components/ui", "lib": "@/lib", "hooks": "@/hooks" }, "iconLibrary": "lucide" } ================================================ FILE: webapp/eslint.config.mjs ================================================ import { dirname } from 'path'; import { fileURLToPath } from 'url'; import { FlatCompat } from '@eslint/eslintrc'; import { defineConfig } from 'eslint/config'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); const compat = new FlatCompat({ baseDirectory: __dirname, }); export default defineConfig([ ...compat.extends('next/core-web-vitals', 'next/typescript'), { rules: { '@typescript-eslint/no-unused-vars': 'off', }, }, ]); ================================================ FILE: webapp/job.Dockerfile ================================================ FROM public.ecr.aws/lambda/nodejs:22 AS builder WORKDIR /build COPY package*.json ./ RUN --mount=type=cache,target=/root/.npm npm ci COPY ./ ./ RUN npx prisma generate RUN npx esbuild src/jobs/*.ts --bundle --outdir=dist --platform=node --charset=utf8 --external:@prisma/client FROM public.ecr.aws/lambda/nodejs:22 AS runner COPY package*.json ./ COPY prisma ./prisma RUN --mount=type=cache,target=/root/.npm npm ci --omit=dev RUN npx prisma generate --generator client COPY --from=builder /build/dist/. ./ CMD ["migration-runner.handler"] ================================================ FILE: webapp/next.config.ts ================================================ import type { NextConfig } from 'next'; const allowedOrigins = ['localhost:3000']; if (process.env.ALLOWED_ORIGIN_HOST) { allowedOrigins.push(process.env.ALLOWED_ORIGIN_HOST); } const nextConfig: NextConfig = { output: 'standalone', experimental: { webpackBuildWorker: true, parallelServerBuildTraces: true, parallelServerCompiles: true, serverActions: { allowedOrigins, }, }, typescript: { ignoreBuildErrors: process.env.SKIP_TS_BUILD == 'true', }, }; export default nextConfig; ================================================ FILE: webapp/package.json ================================================ { "name": "webapp", "version": "0.1.0", "private": true, "scripts": { "dev": "next dev --turbopack -p 3010", "build": "next build", "format": "prettier --write './**/*.{ts,tsx,mjs,mts}' && npx prisma format", "format:check": "prettier --check './**/*.{ts,tsx,mjs,mts}' && npx prisma format --check", "start": "next start", "lint": "next lint" }, "dependencies": { "@aws-amplify/adapter-nextjs": "^1.7.1", "@aws-sdk/client-lambda": "^3.995.0", "@aws-sdk/client-ssm": "^3.995.0", "@aws-sdk/client-translate": "^3.995.0", "@next-safe-action/adapter-react-hook-form": "^1.0.14", "@prisma/client": "^6.6.0", "aws-amplify": "^6.16.2", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "lucide-react": "^0.488.0", "next": "^16.1.6", "next-safe-action": "^7.10.5", "next-themes": "^0.4.6", "prisma": "^6.6.0", "react": "^19.0.0", "react-dom": "^19.0.0", "react-hook-form": "^7.55.0", "sonner": "^2.0.3", "tailwind-merge": "^3.2.0", "tw-animate-css": "^1.2.5", "zod": "^3.24.2" }, "devDependencies": { "@eslint/eslintrc": "^3", "@tailwindcss/postcss": "^4", "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", "eslint": "^9", "eslint-config-next": "15.3.0", "prettier": "^3.5.3", "tailwindcss": "^4", "typescript": "^5", "zod-prisma-types": "^3.2.4" } } ================================================ FILE: webapp/postcss.config.mjs ================================================ const config = { plugins: ['@tailwindcss/postcss'], }; export default config; ================================================ FILE: webapp/prisma/schema.prisma ================================================ generator client { provider = "prisma-client-js" output = "../node_modules/.prisma/client" } generator zod { provider = "zod-prisma-types" output = "../src/lib/generated/prisma/zod" useMultipleFiles = true writeBarrelFiles = false } datasource db { provider = "postgresql" url = env("DATABASE_URL") } model User { id String @id TodoItem TodoItem[] } enum TodoItemStatus { PENDING COMPLETED } model TodoItem { id String @id @default(uuid()) title String description String @db.Text() userId String status TodoItemStatus createdAt DateTime @default(now()) updatedAt DateTime @updatedAt user User @relation(fields: [userId], references: [id]) @@index([userId, createdAt]) } ================================================ FILE: webapp/run.sh ================================================ #!/bin/bash -x [ ! -d '/tmp/cache' ] && mkdir -p /tmp/cache exec node server.js ================================================ FILE: webapp/src/app/(root)/actions.ts ================================================ 'use server'; import { authActionClient } from '@/lib/safe-action'; import { createTodoSchema, deleteTodoSchema, runTranslateJobSchema, updateTodoSchema, updateTodoStatusSchema, } from './schemas'; import { prisma } from '@/lib/prisma'; import { revalidatePath } from 'next/cache'; import { TodoItemStatus } from '@prisma/client'; import { runJob } from '@/lib/jobs'; export const createTodo = authActionClient.schema(createTodoSchema).action(async ({ parsedInput, ctx }) => { const { title, description } = parsedInput; const { userId } = ctx; const todo = await prisma.todoItem.create({ data: { title, description, userId, status: TodoItemStatus.PENDING, }, }); revalidatePath('/'); return { todo }; }); export const updateTodo = authActionClient.schema(updateTodoSchema).action(async ({ parsedInput, ctx }) => { const { id, title, description, status } = parsedInput; const { userId } = ctx; const todo = await prisma.todoItem.update({ where: { id, userId, }, data: { title, description, status, }, }); revalidatePath('/'); return { todo }; }); export const deleteTodo = authActionClient.schema(deleteTodoSchema).action(async ({ parsedInput, ctx }) => { const { id } = parsedInput; const { userId } = ctx; await prisma.todoItem.delete({ where: { id, userId, }, }); revalidatePath('/'); return { success: true }; }); export const updateTodoStatus = authActionClient.schema(updateTodoStatusSchema).action(async ({ parsedInput, ctx }) => { const { id, status } = parsedInput; const { userId } = ctx; const todo = await prisma.todoItem.update({ where: { id, userId, }, data: { status, }, }); revalidatePath('/'); return { todo }; }); export const runTranslateJob = authActionClient.schema(runTranslateJobSchema).action(async ({ parsedInput, ctx }) => { const { id } = parsedInput; const { userId } = ctx; await runJob({ type: 'translate', todoItemId: id, userId: userId, }); }); ================================================ FILE: webapp/src/app/(root)/components/CreateTodoForm.tsx ================================================ 'use client'; import { useState } from 'react'; import { zodResolver } from '@hookform/resolvers/zod'; import { useHookFormAction } from '@next-safe-action/adapter-react-hook-form/hooks'; import { createTodo } from '../actions'; import { createTodoSchema } from '../schemas'; import { toast } from 'sonner'; import { useEventBus } from '@/hooks/use-event-bus'; import { useRouter } from 'next/navigation'; export default function CreateTodoForm(props: { userId: string }) { const [isFormOpen, setIsFormOpen] = useState(false); const router = useRouter(); useEventBus({ channelName: `user/${props.userId}/jobs`, onReceived: (data) => { console.log('received', data); router.refresh(); }, }); const { form: { register, formState, reset }, action, handleSubmitWithAction, } = useHookFormAction(createTodo, zodResolver(createTodoSchema), { actionProps: { onSuccess: () => { toast.success('Todo created successfully'); reset(); setIsFormOpen(false); }, onError: ({ error }) => { toast.error(typeof error === 'string' ? error : 'Failed to create todo'); }, }, formProps: { defaultValues: { title: '', description: '', }, }, }); if (!isFormOpen) { return (
); } return (

Create New Todo

{formState.errors.title &&

{formState.errors.title.message}

}