├── .eslintrc.json ├── .gitattributes ├── .github ├── pull_request_template.md └── workflows │ ├── build.yml │ ├── pull-request-lint.yml │ ├── release.yml │ └── upgrade-main.yml ├── .gitignore ├── .mergify.yml ├── .npmignore ├── .projen ├── deps.json ├── files.json └── tasks.json ├── .projenrc.ts ├── API.md ├── AmazonQ.md ├── LICENSE ├── README.md ├── example ├── .gitignore ├── README.md ├── cdk.json ├── example-app │ ├── .gitignore │ ├── index.html │ ├── package-lock.json │ ├── package.json │ ├── src │ │ ├── App.css │ │ ├── App.tsx │ │ ├── favicon.svg │ │ ├── index.css │ │ ├── logo.svg │ │ ├── main.tsx │ │ └── vite-env.d.ts │ ├── tsconfig.json │ ├── tsconfig.node.json │ └── vite.config.ts ├── example-image │ └── Dockerfile └── index.ts ├── imgs ├── architecture.png ├── demo.png └── soci-architecture.png ├── lambda └── trigger-codebuild │ ├── .npmignore │ ├── index.ts │ ├── package-lock.json │ └── package.json ├── package-lock.json ├── package.json ├── src ├── container-image-build.ts ├── index.ts ├── nodejs-build.ts ├── singleton-project.ts ├── soci-index-build.ts └── types.ts ├── test ├── container-image-build.integ.snapshot │ ├── ContainerImageBuildIntegTest.template.json │ ├── TestDefaultTestDeployAssert12909640.template.json │ ├── asset.9b19c7d81a94bbc19fe2164453716f4d7f651b21f923c5c264fe80f5a939e1db │ │ └── index.js │ ├── asset.f7fa10e7cd7b9b27f49a4a335f4aa9795fb7e68a665c34ca8bf5711f0aa6aeab │ │ └── Dockerfile │ ├── cdk.out │ ├── integ.json │ ├── manifest.json │ └── tree.json ├── hello.test.ts ├── integ.container-image-build.ts ├── integ.nodejs-build.ts ├── integ.soci-index-build.ts ├── nodejs-build.integ.snapshot │ ├── NodejsBuildIntegTest.template.json │ ├── TestDefaultTestDeployAssert12909640.template.json │ ├── asset.1d3b5490cd99feddeb525a62c046988997469f2a765d0f12b43cff9d87a284fa.zip │ ├── asset.72f05c779a8bc73c6ec86f6eafc720508792e7526696d3ae45a7fddcfc473c9d │ │ ├── .gitignore │ │ ├── index.html │ │ ├── package-lock.json │ │ ├── package.json │ │ ├── src │ │ │ ├── App.css │ │ │ ├── App.tsx │ │ │ ├── favicon.svg │ │ │ ├── index.css │ │ │ ├── logo.svg │ │ │ ├── main.tsx │ │ │ └── vite-env.d.ts │ │ ├── tsconfig.json │ │ ├── tsconfig.node.json │ │ └── vite.config.ts │ ├── asset.9b19c7d81a94bbc19fe2164453716f4d7f651b21f923c5c264fe80f5a939e1db │ │ └── index.js │ ├── asset.f98b78092dcdd31f5e6d47489beb5f804d4835ef86a8085d0a2053cb9ae711da │ │ └── index.py │ ├── cdk.out │ ├── integ.json │ ├── manifest.json │ └── tree.json └── soci-index-build.integ.snapshot │ ├── SociIndexBuildIntegTest.template.json │ ├── TestDefaultTestDeployAssert12909640.template.json │ ├── asset.9b19c7d81a94bbc19fe2164453716f4d7f651b21f923c5c264fe80f5a939e1db │ └── index.js │ ├── asset.a8bbd8136347d097316685416937f7ad1e2612d23493dcc4e0ef33d290c09b05 │ └── Dockerfile │ ├── asset.cf17887cc251b0d2ad6a734dbfd8c28d46168a4e632883267d3a06e454b1c3ad │ └── Dockerfile │ ├── cdk.out │ ├── integ.json │ ├── manifest.json │ └── tree.json └── tsconfig.dev.json /.eslintrc.json: -------------------------------------------------------------------------------- 1 | // ~~ Generated by projen. To modify, edit .projenrc.ts and run "npx projen". 2 | { 3 | "env": { 4 | "jest": true, 5 | "node": true 6 | }, 7 | "root": true, 8 | "plugins": [ 9 | "@typescript-eslint", 10 | "import" 11 | ], 12 | "parser": "@typescript-eslint/parser", 13 | "parserOptions": { 14 | "ecmaVersion": 2018, 15 | "sourceType": "module", 16 | "project": "./tsconfig.dev.json" 17 | }, 18 | "extends": [ 19 | "plugin:import/typescript" 20 | ], 21 | "settings": { 22 | "import/parsers": { 23 | "@typescript-eslint/parser": [ 24 | ".ts", 25 | ".tsx" 26 | ] 27 | }, 28 | "import/resolver": { 29 | "node": {}, 30 | "typescript": { 31 | "project": "./tsconfig.dev.json", 32 | "alwaysTryTypes": true 33 | } 34 | } 35 | }, 36 | "ignorePatterns": [ 37 | "example/**/*", 38 | "lambda/**/*", 39 | "test/assets/**/*", 40 | "test/*.snapshot/**/*", 41 | "*.d.ts", 42 | "!.projenrc.ts", 43 | "!projenrc/**/*.ts" 44 | ], 45 | "rules": { 46 | "indent": [ 47 | "off" 48 | ], 49 | "@typescript-eslint/indent": [ 50 | "error", 51 | 2 52 | ], 53 | "quotes": [ 54 | "error", 55 | "single", 56 | { 57 | "avoidEscape": true 58 | } 59 | ], 60 | "comma-dangle": [ 61 | "error", 62 | "always-multiline" 63 | ], 64 | "comma-spacing": [ 65 | "error", 66 | { 67 | "before": false, 68 | "after": true 69 | } 70 | ], 71 | "no-multi-spaces": [ 72 | "error", 73 | { 74 | "ignoreEOLComments": false 75 | } 76 | ], 77 | "array-bracket-spacing": [ 78 | "error", 79 | "never" 80 | ], 81 | "array-bracket-newline": [ 82 | "error", 83 | "consistent" 84 | ], 85 | "object-curly-spacing": [ 86 | "error", 87 | "always" 88 | ], 89 | "object-curly-newline": [ 90 | "error", 91 | { 92 | "multiline": true, 93 | "consistent": true 94 | } 95 | ], 96 | "object-property-newline": [ 97 | "error", 98 | { 99 | "allowAllPropertiesOnSameLine": true 100 | } 101 | ], 102 | "keyword-spacing": [ 103 | "error" 104 | ], 105 | "brace-style": [ 106 | "error", 107 | "1tbs", 108 | { 109 | "allowSingleLine": true 110 | } 111 | ], 112 | "space-before-blocks": [ 113 | "error" 114 | ], 115 | "curly": [ 116 | "error", 117 | "multi-line", 118 | "consistent" 119 | ], 120 | "@typescript-eslint/member-delimiter-style": [ 121 | "error" 122 | ], 123 | "semi": [ 124 | "error", 125 | "always" 126 | ], 127 | "max-len": [ 128 | "error", 129 | { 130 | "code": 150, 131 | "ignoreUrls": true, 132 | "ignoreStrings": true, 133 | "ignoreTemplateLiterals": true, 134 | "ignoreComments": true, 135 | "ignoreRegExpLiterals": true 136 | } 137 | ], 138 | "quote-props": [ 139 | "error", 140 | "consistent-as-needed" 141 | ], 142 | "@typescript-eslint/no-require-imports": [ 143 | "error" 144 | ], 145 | "import/no-extraneous-dependencies": [ 146 | "error", 147 | { 148 | "devDependencies": [ 149 | "**/test/**", 150 | "**/build-tools/**", 151 | ".projenrc.ts", 152 | "projenrc/**/*.ts" 153 | ], 154 | "optionalDependencies": false, 155 | "peerDependencies": true 156 | } 157 | ], 158 | "import/no-unresolved": [ 159 | "error" 160 | ], 161 | "import/order": [ 162 | "warn", 163 | { 164 | "groups": [ 165 | "builtin", 166 | "external" 167 | ], 168 | "alphabetize": { 169 | "order": "asc", 170 | "caseInsensitive": true 171 | } 172 | } 173 | ], 174 | "import/no-duplicates": [ 175 | "error" 176 | ], 177 | "no-shadow": [ 178 | "off" 179 | ], 180 | "@typescript-eslint/no-shadow": [ 181 | "error" 182 | ], 183 | "key-spacing": [ 184 | "error" 185 | ], 186 | "no-multiple-empty-lines": [ 187 | "error" 188 | ], 189 | "@typescript-eslint/no-floating-promises": [ 190 | "error" 191 | ], 192 | "no-return-await": [ 193 | "off" 194 | ], 195 | "@typescript-eslint/return-await": [ 196 | "error" 197 | ], 198 | "no-trailing-spaces": [ 199 | "error" 200 | ], 201 | "dot-notation": [ 202 | "error" 203 | ], 204 | "no-bitwise": [ 205 | "error" 206 | ], 207 | "@typescript-eslint/member-ordering": [ 208 | "error", 209 | { 210 | "default": [ 211 | "public-static-field", 212 | "public-static-method", 213 | "protected-static-field", 214 | "protected-static-method", 215 | "private-static-field", 216 | "private-static-method", 217 | "field", 218 | "constructor", 219 | "method" 220 | ] 221 | } 222 | ] 223 | }, 224 | "overrides": [ 225 | { 226 | "files": [ 227 | ".projenrc.ts" 228 | ], 229 | "rules": { 230 | "@typescript-eslint/no-require-imports": "off", 231 | "import/no-extraneous-dependencies": "off" 232 | } 233 | } 234 | ] 235 | } 236 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.ts and run "npx projen". 2 | 3 | * text=auto eol=lf 4 | *.snap linguist-generated 5 | /.eslintrc.json linguist-generated 6 | /.gitattributes linguist-generated 7 | /.github/pull_request_template.md linguist-generated 8 | /.github/workflows/build.yml linguist-generated 9 | /.github/workflows/pull-request-lint.yml linguist-generated 10 | /.github/workflows/release.yml linguist-generated 11 | /.github/workflows/upgrade-main.yml linguist-generated 12 | /.gitignore linguist-generated 13 | /.mergify.yml linguist-generated 14 | /.npmignore linguist-generated 15 | /.projen/** linguist-generated 16 | /.projen/deps.json linguist-generated 17 | /.projen/files.json linguist-generated 18 | /.projen/tasks.json linguist-generated 19 | /API.md linguist-generated 20 | /LICENSE linguist-generated 21 | /package-lock.json linguist-generated 22 | /package.json linguist-generated 23 | /tsconfig.dev.json linguist-generated -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | Fixes # -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.ts and run "npx projen". 2 | 3 | name: build 4 | on: 5 | pull_request: {} 6 | workflow_dispatch: {} 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | permissions: 11 | contents: write 12 | outputs: 13 | self_mutation_happened: ${{ steps.self_mutation.outputs.self_mutation_happened }} 14 | env: 15 | CI: "true" 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v4 19 | with: 20 | ref: ${{ github.event.pull_request.head.ref }} 21 | repository: ${{ github.event.pull_request.head.repo.full_name }} 22 | - name: Setup Node.js 23 | uses: actions/setup-node@v4 24 | with: 25 | node-version: lts/* 26 | - name: Install dependencies 27 | run: npm install 28 | - name: build 29 | run: npx projen build 30 | - name: Find mutations 31 | id: self_mutation 32 | run: |- 33 | git add . 34 | git diff --staged --patch --exit-code > repo.patch || echo "self_mutation_happened=true" >> $GITHUB_OUTPUT 35 | working-directory: ./ 36 | - name: Upload patch 37 | if: steps.self_mutation.outputs.self_mutation_happened 38 | uses: actions/upload-artifact@v4.4.0 39 | with: 40 | name: repo.patch 41 | path: repo.patch 42 | overwrite: true 43 | - name: Fail build on mutation 44 | if: steps.self_mutation.outputs.self_mutation_happened 45 | run: |- 46 | echo "::error::Files were changed during build (see build log). If this was triggered from a fork, you will need to update your branch." 47 | cat repo.patch 48 | exit 1 49 | - name: Backup artifact permissions 50 | run: cd dist && getfacl -R . > permissions-backup.acl 51 | continue-on-error: true 52 | - name: Upload artifact 53 | uses: actions/upload-artifact@v4.4.0 54 | with: 55 | name: build-artifact 56 | path: dist 57 | overwrite: true 58 | self-mutation: 59 | needs: build 60 | runs-on: ubuntu-latest 61 | permissions: 62 | contents: write 63 | if: always() && needs.build.outputs.self_mutation_happened && !(github.event.pull_request.head.repo.full_name != github.repository) 64 | steps: 65 | - name: Checkout 66 | uses: actions/checkout@v4 67 | with: 68 | token: ${{ secrets.PROJEN_GITHUB_TOKEN }} 69 | ref: ${{ github.event.pull_request.head.ref }} 70 | repository: ${{ github.event.pull_request.head.repo.full_name }} 71 | - name: Download patch 72 | uses: actions/download-artifact@v4 73 | with: 74 | name: repo.patch 75 | path: ${{ runner.temp }} 76 | - name: Apply patch 77 | run: '[ -s ${{ runner.temp }}/repo.patch ] && git apply ${{ runner.temp }}/repo.patch || echo "Empty patch. Skipping."' 78 | - name: Set git identity 79 | run: |- 80 | git config user.name "github-actions" 81 | git config user.email "github-actions@github.com" 82 | - name: Push changes 83 | env: 84 | PULL_REQUEST_REF: ${{ github.event.pull_request.head.ref }} 85 | run: |- 86 | git add . 87 | git commit -s -m "chore: self mutation" 88 | git push origin HEAD:$PULL_REQUEST_REF 89 | package-js: 90 | needs: build 91 | runs-on: ubuntu-latest 92 | permissions: 93 | contents: read 94 | if: ${{ !needs.build.outputs.self_mutation_happened }} 95 | steps: 96 | - uses: actions/setup-node@v4 97 | with: 98 | node-version: lts/* 99 | - name: Download build artifacts 100 | uses: actions/download-artifact@v4 101 | with: 102 | name: build-artifact 103 | path: dist 104 | - name: Restore build artifact permissions 105 | run: cd dist && setfacl --restore=permissions-backup.acl 106 | continue-on-error: true 107 | - name: Checkout 108 | uses: actions/checkout@v4 109 | with: 110 | ref: ${{ github.event.pull_request.head.ref }} 111 | repository: ${{ github.event.pull_request.head.repo.full_name }} 112 | path: .repo 113 | - name: Install Dependencies 114 | run: cd .repo && npm ci 115 | - name: Extract build artifact 116 | run: tar --strip-components=1 -xzvf dist/js/*.tgz -C .repo 117 | - name: Move build artifact out of the way 118 | run: mv dist dist.old 119 | - name: Create js artifact 120 | run: cd .repo && npx projen package:js 121 | - name: Collect js artifact 122 | run: mv .repo/dist dist 123 | package-python: 124 | needs: build 125 | runs-on: ubuntu-latest 126 | permissions: 127 | contents: read 128 | if: ${{ !needs.build.outputs.self_mutation_happened }} 129 | steps: 130 | - uses: actions/setup-node@v4 131 | with: 132 | node-version: lts/* 133 | - uses: actions/setup-python@v5 134 | with: 135 | python-version: 3.x 136 | - name: Download build artifacts 137 | uses: actions/download-artifact@v4 138 | with: 139 | name: build-artifact 140 | path: dist 141 | - name: Restore build artifact permissions 142 | run: cd dist && setfacl --restore=permissions-backup.acl 143 | continue-on-error: true 144 | - name: Checkout 145 | uses: actions/checkout@v4 146 | with: 147 | ref: ${{ github.event.pull_request.head.ref }} 148 | repository: ${{ github.event.pull_request.head.repo.full_name }} 149 | path: .repo 150 | - name: Install Dependencies 151 | run: cd .repo && npm ci 152 | - name: Extract build artifact 153 | run: tar --strip-components=1 -xzvf dist/js/*.tgz -C .repo 154 | - name: Move build artifact out of the way 155 | run: mv dist dist.old 156 | - name: Create python artifact 157 | run: cd .repo && npx projen package:python 158 | - name: Collect python artifact 159 | run: mv .repo/dist dist 160 | -------------------------------------------------------------------------------- /.github/workflows/pull-request-lint.yml: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.ts and run "npx projen". 2 | 3 | name: pull-request-lint 4 | on: 5 | pull_request_target: 6 | types: 7 | - labeled 8 | - opened 9 | - synchronize 10 | - reopened 11 | - ready_for_review 12 | - edited 13 | merge_group: {} 14 | jobs: 15 | validate: 16 | name: Validate PR title 17 | runs-on: ubuntu-latest 18 | permissions: 19 | pull-requests: write 20 | if: (github.event_name == 'pull_request' || github.event_name == 'pull_request_target') 21 | steps: 22 | - uses: amannn/action-semantic-pull-request@v5.4.0 23 | env: 24 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 25 | with: 26 | types: |- 27 | feat 28 | fix 29 | chore 30 | requireScope: false 31 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.ts and run "npx projen". 2 | 3 | name: release 4 | on: 5 | push: 6 | branches: 7 | - main 8 | workflow_dispatch: {} 9 | concurrency: 10 | group: ${{ github.workflow }} 11 | cancel-in-progress: false 12 | jobs: 13 | release: 14 | runs-on: ubuntu-latest 15 | permissions: 16 | contents: write 17 | outputs: 18 | latest_commit: ${{ steps.git_remote.outputs.latest_commit }} 19 | tag_exists: ${{ steps.check_tag_exists.outputs.exists }} 20 | env: 21 | CI: "true" 22 | steps: 23 | - name: Checkout 24 | uses: actions/checkout@v4 25 | with: 26 | fetch-depth: 0 27 | - name: Set git identity 28 | run: |- 29 | git config user.name "github-actions" 30 | git config user.email "github-actions@github.com" 31 | - name: Setup Node.js 32 | uses: actions/setup-node@v4 33 | with: 34 | node-version: lts/* 35 | - name: Install dependencies 36 | run: npm ci 37 | - name: release 38 | run: npx projen release 39 | - name: Check if version has already been tagged 40 | id: check_tag_exists 41 | run: |- 42 | TAG=$(cat dist/releasetag.txt) 43 | ([ ! -z "$TAG" ] && git ls-remote -q --exit-code --tags origin $TAG && (echo "exists=true" >> $GITHUB_OUTPUT)) || (echo "exists=false" >> $GITHUB_OUTPUT) 44 | cat $GITHUB_OUTPUT 45 | - name: Check for new commits 46 | id: git_remote 47 | run: |- 48 | echo "latest_commit=$(git ls-remote origin -h ${{ github.ref }} | cut -f1)" >> $GITHUB_OUTPUT 49 | cat $GITHUB_OUTPUT 50 | - name: Backup artifact permissions 51 | if: ${{ steps.git_remote.outputs.latest_commit == github.sha }} 52 | run: cd dist && getfacl -R . > permissions-backup.acl 53 | continue-on-error: true 54 | - name: Upload artifact 55 | if: ${{ steps.git_remote.outputs.latest_commit == github.sha }} 56 | uses: actions/upload-artifact@v4.4.0 57 | with: 58 | name: build-artifact 59 | path: dist 60 | overwrite: true 61 | release_github: 62 | name: Publish to GitHub Releases 63 | needs: 64 | - release 65 | - release_npm 66 | - release_pypi 67 | runs-on: ubuntu-latest 68 | permissions: 69 | contents: write 70 | if: needs.release.outputs.tag_exists != 'true' && needs.release.outputs.latest_commit == github.sha 71 | steps: 72 | - uses: actions/setup-node@v4 73 | with: 74 | node-version: lts/* 75 | - name: Download build artifacts 76 | uses: actions/download-artifact@v4 77 | with: 78 | name: build-artifact 79 | path: dist 80 | - name: Restore build artifact permissions 81 | run: cd dist && setfacl --restore=permissions-backup.acl 82 | continue-on-error: true 83 | - name: Release 84 | env: 85 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 86 | GITHUB_REPOSITORY: ${{ github.repository }} 87 | GITHUB_REF: ${{ github.sha }} 88 | run: errout=$(mktemp); gh release create $(cat dist/releasetag.txt) -R $GITHUB_REPOSITORY -F dist/changelog.md -t $(cat dist/releasetag.txt) --target $GITHUB_REF 2> $errout && true; exitcode=$?; if [ $exitcode -ne 0 ] && ! grep -q "Release.tag_name already exists" $errout; then cat $errout; exit $exitcode; fi 89 | release_npm: 90 | name: Publish to npm 91 | needs: release 92 | runs-on: ubuntu-latest 93 | permissions: 94 | id-token: write 95 | contents: read 96 | if: needs.release.outputs.tag_exists != 'true' && needs.release.outputs.latest_commit == github.sha 97 | steps: 98 | - uses: actions/setup-node@v4 99 | with: 100 | node-version: lts/* 101 | - name: Download build artifacts 102 | uses: actions/download-artifact@v4 103 | with: 104 | name: build-artifact 105 | path: dist 106 | - name: Restore build artifact permissions 107 | run: cd dist && setfacl --restore=permissions-backup.acl 108 | continue-on-error: true 109 | - name: Checkout 110 | uses: actions/checkout@v4 111 | with: 112 | path: .repo 113 | - name: Install Dependencies 114 | run: cd .repo && npm ci 115 | - name: Extract build artifact 116 | run: tar --strip-components=1 -xzvf dist/js/*.tgz -C .repo 117 | - name: Move build artifact out of the way 118 | run: mv dist dist.old 119 | - name: Create js artifact 120 | run: cd .repo && npx projen package:js 121 | - name: Collect js artifact 122 | run: mv .repo/dist dist 123 | - name: Release 124 | env: 125 | NPM_DIST_TAG: latest 126 | NPM_REGISTRY: registry.npmjs.org 127 | NPM_CONFIG_PROVENANCE: "true" 128 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 129 | run: npx -p publib@latest publib-npm 130 | release_pypi: 131 | name: Publish to PyPI 132 | needs: release 133 | runs-on: ubuntu-latest 134 | permissions: 135 | contents: read 136 | if: needs.release.outputs.tag_exists != 'true' && needs.release.outputs.latest_commit == github.sha 137 | steps: 138 | - uses: actions/setup-node@v4 139 | with: 140 | node-version: lts/* 141 | - uses: actions/setup-python@v5 142 | with: 143 | python-version: 3.x 144 | - name: Download build artifacts 145 | uses: actions/download-artifact@v4 146 | with: 147 | name: build-artifact 148 | path: dist 149 | - name: Restore build artifact permissions 150 | run: cd dist && setfacl --restore=permissions-backup.acl 151 | continue-on-error: true 152 | - name: Checkout 153 | uses: actions/checkout@v4 154 | with: 155 | path: .repo 156 | - name: Install Dependencies 157 | run: cd .repo && npm ci 158 | - name: Extract build artifact 159 | run: tar --strip-components=1 -xzvf dist/js/*.tgz -C .repo 160 | - name: Move build artifact out of the way 161 | run: mv dist dist.old 162 | - name: Create python artifact 163 | run: cd .repo && npx projen package:python 164 | - name: Collect python artifact 165 | run: mv .repo/dist dist 166 | - name: Release 167 | env: 168 | TWINE_USERNAME: ${{ secrets.TWINE_USERNAME }} 169 | TWINE_PASSWORD: ${{ secrets.TWINE_PASSWORD }} 170 | run: npx -p publib@latest publib-pypi 171 | -------------------------------------------------------------------------------- /.github/workflows/upgrade-main.yml: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.ts and run "npx projen". 2 | 3 | name: upgrade-main 4 | on: 5 | workflow_dispatch: {} 6 | schedule: 7 | - cron: 0 0 * * * 8 | jobs: 9 | upgrade: 10 | name: Upgrade 11 | runs-on: ubuntu-latest 12 | permissions: 13 | contents: read 14 | outputs: 15 | patch_created: ${{ steps.create_patch.outputs.patch_created }} 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v4 19 | with: 20 | ref: main 21 | - name: Setup Node.js 22 | uses: actions/setup-node@v4 23 | with: 24 | node-version: lts/* 25 | - name: Install dependencies 26 | run: npm ci 27 | - name: Upgrade dependencies 28 | run: npx projen upgrade 29 | - name: Find mutations 30 | id: create_patch 31 | run: |- 32 | git add . 33 | git diff --staged --patch --exit-code > repo.patch || echo "patch_created=true" >> $GITHUB_OUTPUT 34 | working-directory: ./ 35 | - name: Upload patch 36 | if: steps.create_patch.outputs.patch_created 37 | uses: actions/upload-artifact@v4.4.0 38 | with: 39 | name: repo.patch 40 | path: repo.patch 41 | overwrite: true 42 | pr: 43 | name: Create Pull Request 44 | needs: upgrade 45 | runs-on: ubuntu-latest 46 | permissions: 47 | contents: read 48 | if: ${{ needs.upgrade.outputs.patch_created }} 49 | steps: 50 | - name: Checkout 51 | uses: actions/checkout@v4 52 | with: 53 | ref: main 54 | - name: Download patch 55 | uses: actions/download-artifact@v4 56 | with: 57 | name: repo.patch 58 | path: ${{ runner.temp }} 59 | - name: Apply patch 60 | run: '[ -s ${{ runner.temp }}/repo.patch ] && git apply ${{ runner.temp }}/repo.patch || echo "Empty patch. Skipping."' 61 | - name: Set git identity 62 | run: |- 63 | git config user.name "github-actions" 64 | git config user.email "github-actions@github.com" 65 | - name: Create Pull Request 66 | id: create-pr 67 | uses: peter-evans/create-pull-request@v6 68 | with: 69 | token: ${{ secrets.PROJEN_GITHUB_TOKEN }} 70 | commit-message: |- 71 | chore(deps): upgrade dependencies 72 | 73 | Upgrades project dependencies. See details in [workflow run]. 74 | 75 | [Workflow Run]: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} 76 | 77 | ------ 78 | 79 | *Automatically created by projen via the "upgrade-main" workflow* 80 | branch: github-actions/upgrade-main 81 | title: "chore(deps): upgrade dependencies" 82 | body: |- 83 | Upgrades project dependencies. See details in [workflow run]. 84 | 85 | [Workflow Run]: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} 86 | 87 | ------ 88 | 89 | *Automatically created by projen via the "upgrade-main" workflow* 90 | author: github-actions 91 | committer: github-actions 92 | signoff: true 93 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.ts and run "npx projen". 2 | !/.gitattributes 3 | !/.projen/tasks.json 4 | !/.projen/deps.json 5 | !/.projen/files.json 6 | !/.github/workflows/pull-request-lint.yml 7 | !/package.json 8 | !/LICENSE 9 | !/.npmignore 10 | logs 11 | *.log 12 | npm-debug.log* 13 | yarn-debug.log* 14 | yarn-error.log* 15 | lerna-debug.log* 16 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 17 | pids 18 | *.pid 19 | *.seed 20 | *.pid.lock 21 | lib-cov 22 | coverage 23 | *.lcov 24 | .nyc_output 25 | build/Release 26 | node_modules/ 27 | jspm_packages/ 28 | *.tsbuildinfo 29 | .eslintcache 30 | *.tgz 31 | .yarn-integrity 32 | .cache 33 | *.js 34 | *.d.ts 35 | !test/*.integ.snapshot/**/* 36 | /test-reports/ 37 | junit.xml 38 | /coverage/ 39 | !/.github/workflows/build.yml 40 | /dist/changelog.md 41 | /dist/version.txt 42 | !/.github/workflows/release.yml 43 | !/.mergify.yml 44 | !/.github/workflows/upgrade-main.yml 45 | !/.github/pull_request_template.md 46 | !/test/ 47 | !/tsconfig.dev.json 48 | !/src/ 49 | /lib 50 | /dist/ 51 | !/.eslintrc.json 52 | .jsii 53 | tsconfig.json 54 | !/API.md 55 | !/.projenrc.ts 56 | -------------------------------------------------------------------------------- /.mergify.yml: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.ts and run "npx projen". 2 | 3 | queue_rules: 4 | - name: default 5 | update_method: merge 6 | conditions: 7 | - "#approved-reviews-by>=1" 8 | - -label~=(do-not-merge) 9 | - status-success=build 10 | - status-success=package-js 11 | - status-success=package-python 12 | merge_method: squash 13 | commit_message_template: |- 14 | {{ title }} (#{{ number }}) 15 | 16 | {{ body }} 17 | pull_request_rules: 18 | - name: Automatic merge on approval and successful build 19 | actions: 20 | delete_head_branch: {} 21 | queue: 22 | name: default 23 | conditions: 24 | - "#approved-reviews-by>=1" 25 | - -label~=(do-not-merge) 26 | - status-success=build 27 | - status-success=package-js 28 | - status-success=package-python 29 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.ts and run "npx projen". 2 | /.projen/ 3 | /test-reports/ 4 | junit.xml 5 | /coverage/ 6 | permissions-backup.acl 7 | /dist/changelog.md 8 | /dist/version.txt 9 | /.mergify.yml 10 | /test/ 11 | /tsconfig.dev.json 12 | /src/ 13 | !/lib/ 14 | !/lib/**/*.js 15 | !/lib/**/*.d.ts 16 | dist 17 | /tsconfig.json 18 | /.github/ 19 | /.vscode/ 20 | /.idea/ 21 | /.projenrc.js 22 | tsconfig.tsbuildinfo 23 | /.eslintrc.json 24 | !.jsii 25 | /.gitattributes 26 | /.projenrc.ts 27 | /projenrc 28 | -------------------------------------------------------------------------------- /.projen/deps.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": [ 3 | { 4 | "name": "@aws-cdk/integ-runner", 5 | "version": "2.38.0", 6 | "type": "build" 7 | }, 8 | { 9 | "name": "@aws-cdk/integ-tests-alpha", 10 | "version": "2.38.0-alpha.0", 11 | "type": "build" 12 | }, 13 | { 14 | "name": "@types/jest", 15 | "type": "build" 16 | }, 17 | { 18 | "name": "@types/node", 19 | "type": "build" 20 | }, 21 | { 22 | "name": "@typescript-eslint/eslint-plugin", 23 | "version": "^7", 24 | "type": "build" 25 | }, 26 | { 27 | "name": "@typescript-eslint/parser", 28 | "version": "^7", 29 | "type": "build" 30 | }, 31 | { 32 | "name": "commit-and-tag-version", 33 | "version": "^12", 34 | "type": "build" 35 | }, 36 | { 37 | "name": "eslint-import-resolver-typescript", 38 | "type": "build" 39 | }, 40 | { 41 | "name": "eslint-plugin-import", 42 | "type": "build" 43 | }, 44 | { 45 | "name": "eslint", 46 | "version": "^8", 47 | "type": "build" 48 | }, 49 | { 50 | "name": "jest", 51 | "type": "build" 52 | }, 53 | { 54 | "name": "jest-junit", 55 | "version": "^15", 56 | "type": "build" 57 | }, 58 | { 59 | "name": "jsii-diff", 60 | "type": "build" 61 | }, 62 | { 63 | "name": "jsii-docgen", 64 | "version": "^10.5.0", 65 | "type": "build" 66 | }, 67 | { 68 | "name": "jsii-pacmak", 69 | "type": "build" 70 | }, 71 | { 72 | "name": "jsii-rosetta", 73 | "version": "~5.8.0", 74 | "type": "build" 75 | }, 76 | { 77 | "name": "jsii", 78 | "version": "~5.8.0", 79 | "type": "build" 80 | }, 81 | { 82 | "name": "projen", 83 | "type": "build" 84 | }, 85 | { 86 | "name": "ts-jest", 87 | "type": "build" 88 | }, 89 | { 90 | "name": "ts-node", 91 | "type": "build" 92 | }, 93 | { 94 | "name": "typescript", 95 | "type": "build" 96 | }, 97 | { 98 | "name": "aws-cdk-lib", 99 | "version": "^2.38.0", 100 | "type": "peer" 101 | }, 102 | { 103 | "name": "constructs", 104 | "version": "^10.0.5", 105 | "type": "peer" 106 | } 107 | ], 108 | "//": "~~ Generated by projen. To modify, edit .projenrc.ts and run \"npx projen\"." 109 | } 110 | -------------------------------------------------------------------------------- /.projen/files.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | ".eslintrc.json", 4 | ".gitattributes", 5 | ".github/pull_request_template.md", 6 | ".github/workflows/build.yml", 7 | ".github/workflows/pull-request-lint.yml", 8 | ".github/workflows/release.yml", 9 | ".github/workflows/upgrade-main.yml", 10 | ".gitignore", 11 | ".mergify.yml", 12 | ".projen/deps.json", 13 | ".projen/files.json", 14 | ".projen/tasks.json", 15 | "LICENSE", 16 | "tsconfig.dev.json" 17 | ], 18 | "//": "~~ Generated by projen. To modify, edit .projenrc.ts and run \"npx projen\"." 19 | } 20 | -------------------------------------------------------------------------------- /.projen/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "tasks": { 3 | "build": { 4 | "name": "build", 5 | "description": "Full release build", 6 | "steps": [ 7 | { 8 | "spawn": "default" 9 | }, 10 | { 11 | "spawn": "pre-compile" 12 | }, 13 | { 14 | "spawn": "compile" 15 | }, 16 | { 17 | "spawn": "post-compile" 18 | }, 19 | { 20 | "spawn": "test" 21 | }, 22 | { 23 | "spawn": "package" 24 | } 25 | ] 26 | }, 27 | "bump": { 28 | "name": "bump", 29 | "description": "Bumps version based on latest git tag and generates a changelog entry", 30 | "env": { 31 | "OUTFILE": "package.json", 32 | "CHANGELOG": "dist/changelog.md", 33 | "BUMPFILE": "dist/version.txt", 34 | "RELEASETAG": "dist/releasetag.txt", 35 | "RELEASE_TAG_PREFIX": "", 36 | "BUMP_PACKAGE": "commit-and-tag-version@^12" 37 | }, 38 | "steps": [ 39 | { 40 | "builtin": "release/bump-version" 41 | } 42 | ], 43 | "condition": "git log --oneline -1 | grep -qv \"chore(release):\"" 44 | }, 45 | "clobber": { 46 | "name": "clobber", 47 | "description": "hard resets to HEAD of origin and cleans the local repo", 48 | "env": { 49 | "BRANCH": "$(git branch --show-current)" 50 | }, 51 | "steps": [ 52 | { 53 | "exec": "git checkout -b scratch", 54 | "name": "save current HEAD in \"scratch\" branch" 55 | }, 56 | { 57 | "exec": "git checkout $BRANCH" 58 | }, 59 | { 60 | "exec": "git fetch origin", 61 | "name": "fetch latest changes from origin" 62 | }, 63 | { 64 | "exec": "git reset --hard origin/$BRANCH", 65 | "name": "hard reset to origin commit" 66 | }, 67 | { 68 | "exec": "git clean -fdx", 69 | "name": "clean all untracked files" 70 | }, 71 | { 72 | "say": "ready to rock! (unpushed commits are under the \"scratch\" branch)" 73 | } 74 | ], 75 | "condition": "git diff --exit-code > /dev/null" 76 | }, 77 | "compat": { 78 | "name": "compat", 79 | "description": "Perform API compatibility check against latest version", 80 | "steps": [ 81 | { 82 | "exec": "jsii-diff npm:$(node -p \"require('./package.json').name\") -k --ignore-file .compatignore || (echo \"\nUNEXPECTED BREAKING CHANGES: add keys such as 'removed:constructs.Node.of' to .compatignore to skip.\n\" && exit 1)" 83 | } 84 | ] 85 | }, 86 | "compile": { 87 | "name": "compile", 88 | "description": "Only compile", 89 | "steps": [ 90 | { 91 | "exec": "npm ci && npm run build", 92 | "cwd": "lambda/trigger-codebuild" 93 | }, 94 | { 95 | "exec": "jsii --silence-warnings=reserved-word" 96 | } 97 | ] 98 | }, 99 | "default": { 100 | "name": "default", 101 | "description": "Synthesize project files", 102 | "steps": [ 103 | { 104 | "exec": "ts-node --project tsconfig.dev.json .projenrc.ts" 105 | } 106 | ] 107 | }, 108 | "docgen": { 109 | "name": "docgen", 110 | "description": "Generate API.md from .jsii manifest", 111 | "steps": [ 112 | { 113 | "exec": "jsii-docgen -o API.md" 114 | } 115 | ] 116 | }, 117 | "eject": { 118 | "name": "eject", 119 | "description": "Remove projen from the project", 120 | "env": { 121 | "PROJEN_EJECTING": "true" 122 | }, 123 | "steps": [ 124 | { 125 | "spawn": "default" 126 | } 127 | ] 128 | }, 129 | "eslint": { 130 | "name": "eslint", 131 | "description": "Runs eslint against the codebase", 132 | "steps": [ 133 | { 134 | "exec": "eslint --ext .ts,.tsx --fix --no-error-on-unmatched-pattern $@ test build-tools projenrc .projenrc.ts", 135 | "receiveArgs": true 136 | } 137 | ] 138 | }, 139 | "install": { 140 | "name": "install", 141 | "description": "Install project dependencies and update lockfile (non-frozen)", 142 | "steps": [ 143 | { 144 | "exec": "npm install" 145 | } 146 | ] 147 | }, 148 | "install:ci": { 149 | "name": "install:ci", 150 | "description": "Install project dependencies using frozen lockfile", 151 | "steps": [ 152 | { 153 | "exec": "npm ci" 154 | } 155 | ] 156 | }, 157 | "package": { 158 | "name": "package", 159 | "description": "Creates the distribution package", 160 | "steps": [ 161 | { 162 | "spawn": "package:js", 163 | "condition": "node -e \"if (!process.env.CI) process.exit(1)\"" 164 | }, 165 | { 166 | "spawn": "package-all", 167 | "condition": "node -e \"if (process.env.CI) process.exit(1)\"" 168 | } 169 | ] 170 | }, 171 | "package-all": { 172 | "name": "package-all", 173 | "description": "Packages artifacts for all target languages", 174 | "steps": [ 175 | { 176 | "spawn": "package:js" 177 | }, 178 | { 179 | "spawn": "package:python" 180 | } 181 | ] 182 | }, 183 | "package:js": { 184 | "name": "package:js", 185 | "description": "Create js language bindings", 186 | "steps": [ 187 | { 188 | "exec": "jsii-pacmak -v --target js" 189 | } 190 | ] 191 | }, 192 | "package:python": { 193 | "name": "package:python", 194 | "description": "Create python language bindings", 195 | "steps": [ 196 | { 197 | "exec": "jsii-pacmak -v --target python" 198 | } 199 | ] 200 | }, 201 | "post-compile": { 202 | "name": "post-compile", 203 | "description": "Runs after successful compilation", 204 | "steps": [ 205 | { 206 | "spawn": "docgen" 207 | } 208 | ] 209 | }, 210 | "post-upgrade": { 211 | "name": "post-upgrade", 212 | "description": "Runs after upgrading dependencies" 213 | }, 214 | "pre-compile": { 215 | "name": "pre-compile", 216 | "description": "Prepare the project for compilation" 217 | }, 218 | "release": { 219 | "name": "release", 220 | "description": "Prepare a release from \"main\" branch", 221 | "env": { 222 | "RELEASE": "true" 223 | }, 224 | "steps": [ 225 | { 226 | "exec": "rm -fr dist" 227 | }, 228 | { 229 | "spawn": "bump" 230 | }, 231 | { 232 | "spawn": "build" 233 | }, 234 | { 235 | "spawn": "unbump" 236 | }, 237 | { 238 | "exec": "git diff --ignore-space-at-eol --exit-code" 239 | } 240 | ] 241 | }, 242 | "test": { 243 | "name": "test", 244 | "description": "Run tests", 245 | "steps": [ 246 | { 247 | "exec": "jest --passWithNoTests --updateSnapshot", 248 | "receiveArgs": true 249 | }, 250 | { 251 | "spawn": "eslint" 252 | }, 253 | { 254 | "exec": "npx tsc -p tsconfig.dev.json && npx integ-runner" 255 | } 256 | ] 257 | }, 258 | "test:watch": { 259 | "name": "test:watch", 260 | "description": "Run jest in watch mode", 261 | "steps": [ 262 | { 263 | "exec": "jest --watch" 264 | } 265 | ] 266 | }, 267 | "unbump": { 268 | "name": "unbump", 269 | "description": "Restores version to 0.0.0", 270 | "env": { 271 | "OUTFILE": "package.json", 272 | "CHANGELOG": "dist/changelog.md", 273 | "BUMPFILE": "dist/version.txt", 274 | "RELEASETAG": "dist/releasetag.txt", 275 | "RELEASE_TAG_PREFIX": "", 276 | "BUMP_PACKAGE": "commit-and-tag-version@^12" 277 | }, 278 | "steps": [ 279 | { 280 | "builtin": "release/reset-version" 281 | } 282 | ] 283 | }, 284 | "upgrade": { 285 | "name": "upgrade", 286 | "description": "upgrade dependencies", 287 | "env": { 288 | "CI": "0" 289 | }, 290 | "steps": [ 291 | { 292 | "exec": "npx npm-check-updates@16 --upgrade --target=minor --peer --dep=dev,peer,prod,optional --filter=@types/jest,@types/node,eslint-import-resolver-typescript,eslint-plugin-import,jest,jsii-diff,jsii-pacmak,projen,ts-jest,ts-node,typescript" 293 | }, 294 | { 295 | "exec": "npm install" 296 | }, 297 | { 298 | "exec": "npm update @aws-cdk/integ-runner @aws-cdk/integ-tests-alpha @types/jest @types/node @typescript-eslint/eslint-plugin @typescript-eslint/parser commit-and-tag-version eslint-import-resolver-typescript eslint-plugin-import eslint jest jest-junit jsii-diff jsii-docgen jsii-pacmak jsii-rosetta jsii projen ts-jest ts-node typescript aws-cdk-lib constructs" 299 | }, 300 | { 301 | "exec": "npx projen" 302 | }, 303 | { 304 | "spawn": "post-upgrade" 305 | } 306 | ] 307 | }, 308 | "watch": { 309 | "name": "watch", 310 | "description": "Watch & compile in the background", 311 | "steps": [ 312 | { 313 | "exec": "jsii -w --silence-warnings=reserved-word" 314 | } 315 | ] 316 | } 317 | }, 318 | "env": { 319 | "PATH": "$(npx -c \"node --print process.env.PATH\")" 320 | }, 321 | "//": "~~ Generated by projen. To modify, edit .projenrc.ts and run \"npx projen\"." 322 | } 323 | -------------------------------------------------------------------------------- /.projenrc.ts: -------------------------------------------------------------------------------- 1 | import { awscdk } from 'projen'; 2 | import { NodePackageManager } from 'projen/lib/javascript'; 3 | 4 | const project = new awscdk.AwsCdkConstructLibrary({ 5 | projenrcTs: true, 6 | author: 'tmokmss', 7 | authorAddress: 'tomookam@live.jp', 8 | cdkVersion: '2.38.0', // For using @aws-cdk/integ-runner 9 | defaultReleaseBranch: 'main', 10 | jsiiVersion: '~5.8.0', 11 | name: 'deploy-time-build', 12 | license: 'MIT', 13 | repositoryUrl: 'https://github.com/tmokmss/deploy-time-build.git', 14 | publishToPypi: { 15 | distName: 'deploy-time-build', 16 | module: 'deploy_time_build', 17 | }, 18 | packageManager: NodePackageManager.NPM, 19 | eslintOptions: { 20 | dirs: [], 21 | ignorePatterns: ['example/**/*', 'lambda/**/*', 'test/assets/**/*', 'test/*.snapshot/**/*', '*.d.ts'], 22 | }, 23 | gitignore: ['*.js', '*.d.ts', '!test/*.integ.snapshot/**/*'], 24 | keywords: ['aws', 'cdk', 'lambda', 'aws-cdk', 'ecr', 'ecs'], 25 | tsconfigDev: { 26 | compilerOptions: {}, 27 | exclude: ['example', 'test/*.integ.snapshot'], 28 | }, 29 | devDeps: ['@aws-cdk/integ-runner@2.38.0', '@aws-cdk/integ-tests-alpha@2.38.0-alpha.0'], 30 | description: 'Build during CDK deployment.', 31 | }); 32 | // Bundle custom resource handler Lambda code 33 | project.projectBuild.compileTask.prependExec('npm ci && npm run build', { 34 | cwd: 'lambda/trigger-codebuild', 35 | }); 36 | // Run integ-test 37 | project.projectBuild.testTask.exec('npx tsc -p tsconfig.dev.json && npx integ-runner'); 38 | project.synth(); 39 | -------------------------------------------------------------------------------- /AmazonQ.md: -------------------------------------------------------------------------------- 1 | # Developer Notes for deploy-time-build 2 | 3 | This document contains helpful information for developers working on the `deploy-time-build` project. 4 | 5 | ## Pull Request Guidelines 6 | 7 | ### PR Title Format 8 | 9 | All text in PR title or description must be written in English. 10 | 11 | Pull request titles must follow the [Conventional Commits](https://www.conventionalcommits.org/) format. Only the following prefixes are allowed: 12 | 13 | - `feat:` - For new features 14 | - `fix:` - For bug fixes 15 | - `chore:` - For maintenance tasks, dependencies updates, etc. 16 | 17 | Examples: 18 | - `feat: add support for arm64 instances` 19 | - `fix: resolve build failure on Windows` 20 | - `chore: update dependency versions` 21 | 22 | PRs with titles not following this format will fail validation checks. 23 | 24 | ## Project Structure 25 | 26 | - `src/` - Main source code 27 | - `container-image-build.ts` - Container image building functionality 28 | - `nodejs-build.ts` - NodeJS build functionality 29 | - `soci-index-build.ts` - SOCI index functionality 30 | - `singleton-project.ts` - Utility for creating singleton CodeBuild projects 31 | - `types.ts` - Type definitions 32 | 33 | ## Build 34 | 35 | You should make sure the build succeeds by the following command before commiting a change: 36 | 37 | ```bash 38 | npm run build 39 | ``` 40 | 41 | After a successful build, several files such as API.md might be updated. Please check git status and commit them if there are any changes. 42 | 43 | ## Testing 44 | 45 | The project uses snapshot tests for integration testing. When making infrastructure changes that affect the generated CloudFormation templates, you'll need to update the snapshot tests. 46 | 47 | ### Updating Snapshot Tests 48 | 49 | When you make changes that intentionally alter the infrastructure (like adding new resources or changing existing ones), the snapshot tests will fail. This is expected behavior. 50 | 51 | After your changes are approved and merged, update the snapshot tests by running: 52 | 53 | ```bash 54 | npx integ-runner --update-on-failed 55 | ``` 56 | 57 | ## Common Issues 58 | 59 | ### Breaking Changes in Infrastructure 60 | 61 | Some changes will create new resources and destroy old ones. This should be clearly noted in PRs. 62 | 63 | ## CI/CD Pipeline 64 | 65 | The CI pipeline runs tests that verify the snapshot templates. Breaking changes will cause these tests to fail. When submitting PRs with intentional breaking changes, clearly document that these failures are expected. 66 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2025 tmokmss 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Deploy-time Build 2 | AWS CDK L3 construct that allows you to run a build job for specific purposes. Currently this library supports the following use cases: 3 | 4 | * Build web frontend static files 5 | * Build a container image 6 | * Build Seekable OCI (SOCI) indices for container images 7 | 8 | ## Usage 9 | Install from npm: 10 | 11 | ```sh 12 | npm i deploy-time-build 13 | ``` 14 | 15 | This library defines several L3 constructs for specific use cases. Here is the usage for each case. 16 | 17 | ### Build Node.js apps 18 | 19 | You can build a Node.js app such as a React frontend app on deploy time by the `NodejsBuild` construct. 20 | 21 | ![architecture](./imgs/architecture.png) 22 | 23 | The following code is an example to use the construct: 24 | 25 | ```ts 26 | import { NodejsBuild } from 'deploy-time-build'; 27 | 28 | declare const api: apigateway.RestApi; 29 | declare const destinationBucket: s3.IBucket; 30 | declare const distribution: cloudfront.IDistribution; 31 | new NodejsBuild(this, 'ExampleBuild', { 32 | assets: [ 33 | { 34 | path: 'example-app', 35 | exclude: ['dist', 'node_modules'], 36 | }, 37 | ], 38 | destinationBucket, 39 | distribution, 40 | outputSourceDirectory: 'dist', 41 | buildCommands: ['npm ci', 'npm run build'], 42 | buildEnvironment: { 43 | VITE_API_ENDPOINT: api.url, 44 | }, 45 | }); 46 | ``` 47 | 48 | Note that it is possible to pass environment variable `VITE_API_ENDPOINT: api.url` to the construct, which is resolved on deploy time, and injected to the build environment (a vite process in this case.) 49 | The resulting build artifacts will be deployed to `destinationBucket` using a [`s3-deployment.BucketDeployment`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_s3_deployment.BucketDeployment.html) construct. 50 | 51 | You can specify multiple input assets by `assets` property. These assets are extracted to respective sub directories. For example, assume you specified assets like the following: 52 | 53 | ```ts 54 | assets: [ 55 | { 56 | // directory containing source code and package.json 57 | path: 'example-app', 58 | exclude: ['dist', 'node_modules'], 59 | commands: ['npm install'], 60 | }, 61 | { 62 | // directory that is also required for the build 63 | path: 'module1', 64 | }, 65 | ], 66 | ``` 67 | 68 | Then, the extracted directories will be located as the following: 69 | 70 | ```sh 71 | . # a temporary directory (automatically created) 72 | ├── example-app # extracted example-app assets 73 | │ ├── src/ # dist or node_modules directories are excluded even if they exist locally. 74 | │ ├── package.json # npm install will be executed since its specified in `commands` property. 75 | │ └── package-lock.json 76 | └── module1 # extracted module1 assets 77 | ``` 78 | 79 | You can also override the path where assets are extracted by `extractPath` property for each asset. 80 | 81 | With `outputEnvFile` property enabled, a `.env` file is automatically generated and uploaded to your S3 bucket. This file can be used running you frontend project locally. You can download the file to your local machine by running the command added in the stack output. 82 | 83 | Please also check [the example directory](./example/) for a complete example. 84 | 85 | #### Allowing access from the build environment to other AWS resources 86 | Since `NodejsBuild` construct implements [`iam.IGrantable`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_iam.IGrantable.html) interface, you can use `grant*` method of other constructs to allow access from the build environment. 87 | 88 | ```ts 89 | declare const someBucket: s3.IBucket; 90 | declare const build: NodejsBuild; 91 | someBucket.grantReadWrite(build); 92 | ``` 93 | 94 | You can also use [`iam.Grant`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_iam.Grant.html) class to allow any actions and resources. 95 | 96 | ```ts 97 | declare const build: NodejsBuild; 98 | iam.Grant.addToPrincipal({ grantee: build, actions: ['s3:ListBucket'], resources:['*'] }) 99 | ``` 100 | 101 | #### Motivation - why do we need the `NodejsBuild` construct? 102 | I talked about why this construct can be useful in some situations at CDK Day 2023. See the recording or slides below: 103 | 104 | [Recording](https://www.youtube.com/live/b-nSH18gFQk?si=ogEZ2x1NixOj6J6j&t=373) | [Slides](https://speakerdeck.com/tmokmss/deploy-web-frontend-apps-with-aws-cdk) 105 | 106 | #### Considerations 107 | Since this construct builds your frontend apps every time you deploy the stack and there is any change in input assets (and currently there's even no build cache in the Lambda function!), the time a deployment takes tends to be longer (e.g. a few minutes even for the simple app in `example` directory.) This might results in worse developer experience if you want to deploy changes frequently (imagine `cdk watch` deployment always re-build your frontend app). 108 | 109 | To mitigate this issue, you can separate the stack for frontend construct from other stacks especially for a dev environment. Another solution would be to set a fixed string as an asset hash, and avoid builds on every deployment. 110 | 111 | ```ts 112 | assets: [ 113 | { 114 | path: '../frontend', 115 | exclude: ['node_modules', 'dist'], 116 | commands: ['npm ci'], 117 | // Set a fixed string as a asset hash to prevent deploying changes. 118 | // This can be useful for an environment you use to develop locally. 119 | assetHash: 'frontend_asset', 120 | }, 121 | ], 122 | ``` 123 | 124 | ### Build a container image 125 | You can build a container image at deploy time by the following code: 126 | 127 | ```ts 128 | import { ContainerImageBuild } from 'deploy-time-build'; 129 | 130 | const image = new ContainerImageBuild(this, 'Build', { 131 | directory: 'example-image', 132 | buildArgs: { DUMMY_FILE_SIZE_MB: '15' }, 133 | tag: 'my-image-tag', 134 | }); 135 | new DockerImageFunction(this, 'Function', { 136 | code: image.toLambdaDockerImageCode(), 137 | }); 138 | const armImage = new ContainerImageBuild(this, 'BuildArm', { 139 | directory: 'example-image', 140 | platform: Platform.LINUX_ARM64, 141 | repository: image.repository, 142 | zstdCompression: true, 143 | }); 144 | new FargateTaskDefinition(this, 'TaskDefinition', { 145 | runtimePlatform: { cpuArchitecture: CpuArchitecture.ARM64 } 146 | }).addContainer('main', { 147 | image: armImage.toEcsDockerImageCode(), 148 | }); 149 | ``` 150 | 151 | The third argument (props) are a superset of DockerImageAsset's properties. You can set a few additional properties such as `tag`, `repository`, and `zstdCompression`. 152 | 153 | ### Build SOCI index for a container image 154 | [Seekable OCI (SOCI)](https://aws.amazon.com/about-aws/whats-new/2022/09/introducing-seekable-oci-lazy-loading-container-images/) is a way to help start tasks faster for Amazon ECS tasks on Fargate 1.4.0. You can build and push a SOCI index using the `SociIndexBuild` construct. 155 | 156 | ![soci-architecture](imgs/soci-architecture.png) 157 | 158 | The following code is an example to use the construct: 159 | 160 | ```ts 161 | import { SociIndexBuild } from 'deploy-time-build'; 162 | 163 | const asset = new DockerImageAsset(this, 'Image', { directory: 'example-image' }); 164 | new SociIndexBuild(this, 'Index', { imageTag: asset.assetHash, repository: asset.repository }); 165 | // or using a utility method 166 | SociIndexBuild.fromDockerImageAsset(this, 'Index2', asset); 167 | 168 | // Use the asset for ECS Fargate tasks 169 | import { AssetImage } from 'aws-cdk-lib/aws-ecs'; 170 | const assetImage = AssetImage.fromDockerImageAsset(asset); 171 | ``` 172 | 173 | We currently use [`soci-wrapper`](https://github.com/tmokmss/soci-wrapper) to build and push SOCI indices. 174 | 175 | #### Motivation - why do we need the `SociIndexBuild` construct? 176 | 177 | Currently there are several other ways to build a SOCI index; 1. use `soci-snapshotter` CLI, or 2. use [cfn-ecr-aws-soci-index-builder](https://github.com/aws-ia/cfn-ecr-aws-soci-index-builder) solution, none of which can be directly used from AWS CDK. If you are familiar with CDK, you should often deploy container images as CDK assets, which is an ideal way to integrate with other L2 constructs such as ECS. To make the developer experience for SOCI as close as the ordinary container images, the `SociIndexBuild` allows you to deploying a SOCI index directly from CDK, without any dependencies outside of CDK context. 178 | 179 | ## Development 180 | Commands for maintainers: 181 | 182 | ```sh 183 | # run test locally 184 | npx tsc -p tsconfig.dev.json 185 | npx integ-runner 186 | npx integ-runner --update-on-failed 187 | ``` 188 | -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | cdk.out -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | ## How to Deploy 2 | ```sh 3 | # Assume current directory is the example directory 4 | cd ../lambda/nodejs-build 5 | npm ci 6 | npm run build 7 | cd - 8 | npx cdk deploy NodejsTestStack 9 | ``` 10 | 11 | ## Description 12 | After a successful deployment, you will get a CloudFront URL from the stack output. 13 | 14 | ![demo](../imgs/demo.png) 15 | 16 | You can see the API endpoint is successfully injected to the sample frontend app. 17 | -------------------------------------------------------------------------------- /example/cdk.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": "npx ts-node --prefer-ts-exts index.ts" 3 | } -------------------------------------------------------------------------------- /example/example-app/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | !tsconfig.json -------------------------------------------------------------------------------- /example/example-app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite App 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /example/example-app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example-app", 3 | "private": true, 4 | "version": "0.0.0", 5 | "scripts": { 6 | "dev": "vite", 7 | "build": "tsc && vite build", 8 | "preview": "vite preview" 9 | }, 10 | "dependencies": { 11 | "react": "^18.0.0", 12 | "react-dom": "^18.0.0" 13 | }, 14 | "devDependencies": { 15 | "@types/react": "^18.0.0", 16 | "@types/react-dom": "^18.0.0", 17 | "@vitejs/plugin-react": "^1.3.0", 18 | "typescript": "^4.6.3", 19 | "vite": "^2.9.9" 20 | } 21 | } -------------------------------------------------------------------------------- /example/example-app/src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | height: 40vmin; 7 | pointer-events: none; 8 | } 9 | 10 | @media (prefers-reduced-motion: no-preference) { 11 | .App-logo { 12 | animation: App-logo-spin infinite 20s linear; 13 | } 14 | } 15 | 16 | .App-header { 17 | background-color: #282c34; 18 | min-height: 100vh; 19 | display: flex; 20 | flex-direction: column; 21 | align-items: center; 22 | justify-content: center; 23 | font-size: calc(10px + 2vmin); 24 | color: white; 25 | } 26 | 27 | .App-link { 28 | color: #61dafb; 29 | } 30 | 31 | @keyframes App-logo-spin { 32 | from { 33 | transform: rotate(0deg); 34 | } 35 | to { 36 | transform: rotate(360deg); 37 | } 38 | } 39 | 40 | button { 41 | font-size: calc(10px + 2vmin); 42 | } 43 | -------------------------------------------------------------------------------- /example/example-app/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react' 2 | import logo from './logo.svg' 3 | import './App.css' 4 | 5 | function App() { 6 | const [count, setCount] = useState(0) 7 | 8 | return ( 9 |
10 |
11 | logo 12 |

Hello Vite + React!

13 |

14 | 17 |

18 |

19 | Your API endpoint: {import.meta.env.VITE_API_ENDPOINT ?? 'undefined'} 20 |

21 |

22 | 28 | Learn React 29 | 30 | {' | '} 31 | 37 | Vite Docs 38 | 39 |

40 |
41 |
42 | ) 43 | } 44 | 45 | export default App 46 | -------------------------------------------------------------------------------- /example/example-app/src/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /example/example-app/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /example/example-app/src/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/example-app/src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom/client' 3 | import App from './App' 4 | import './index.css' 5 | 6 | ReactDOM.createRoot(document.getElementById('root')!).render( 7 | 8 | 9 | 10 | ) 11 | -------------------------------------------------------------------------------- /example/example-app/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /example/example-app/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 6 | "allowJs": false, 7 | "skipLibCheck": true, 8 | "esModuleInterop": false, 9 | "allowSyntheticDefaultImports": true, 10 | "strict": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "module": "ESNext", 13 | "moduleResolution": "Node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "react-jsx" 18 | }, 19 | "include": ["src"], 20 | "references": [{ "path": "./tsconfig.node.json" }] 21 | } 22 | -------------------------------------------------------------------------------- /example/example-app/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "esnext", 5 | "moduleResolution": "node" 6 | }, 7 | "include": ["vite.config.ts"] 8 | } 9 | -------------------------------------------------------------------------------- /example/example-app/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()] 7 | }) 8 | -------------------------------------------------------------------------------- /example/example-image/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM public.ecr.aws/nginx/nginx:1.26 2 | # create dummy file to change the size of a image 3 | ARG DUMMY_FILE_SIZE_MB="10" 4 | # RUN fallocate -l ${DUMMY_FILE_SIZE_MB} dummy.img 5 | RUN dd if=/dev/zero of=dummy.img bs=1M count=${DUMMY_FILE_SIZE_MB} 6 | -------------------------------------------------------------------------------- /example/index.ts: -------------------------------------------------------------------------------- 1 | import { Stack, StackProps, App, CfnOutput } from 'aws-cdk-lib'; 2 | import { Construct } from 'constructs'; 3 | import { MockIntegration, RestApi } from 'aws-cdk-lib/aws-apigateway'; 4 | import { ContainerImageBuild, NodejsBuild, SociIndexBuild } from '../src/'; 5 | import { BlockPublicAccess, Bucket, BucketEncryption } from 'aws-cdk-lib/aws-s3'; 6 | import { DockerImageAsset, Platform } from 'aws-cdk-lib/aws-ecr-assets'; 7 | import { DockerImageFunction } from 'aws-cdk-lib/aws-lambda'; 8 | import { AwsLogDriver, Cluster, CpuArchitecture, FargateTaskDefinition } from 'aws-cdk-lib/aws-ecs'; 9 | import { SubnetType, Vpc } from 'aws-cdk-lib/aws-ec2'; 10 | import { OriginAccessIdentity, CloudFrontWebDistribution } from 'aws-cdk-lib/aws-cloudfront'; 11 | 12 | class NodejsTestStack extends Stack { 13 | constructor(scope: Construct, id: string, props: StackProps = {}) { 14 | super(scope, id, props); 15 | 16 | const api = new RestApi(this, 'ExampleApi'); 17 | api.root.addMethod( 18 | 'ANY', 19 | new MockIntegration({ 20 | integrationResponses: [{ statusCode: '200' }], 21 | requestTemplates: { 22 | 'application/json': '{ "statusCode": 200 }', 23 | }, 24 | }), 25 | { 26 | methodResponses: [{ statusCode: '200' }], 27 | } 28 | ); 29 | 30 | const dstBucket = new Bucket(this, 'DstBucket', { 31 | // autoDeleteObjects: true, 32 | // removalPolicy: RemovalPolicy.DESTROY, 33 | blockPublicAccess: BlockPublicAccess.BLOCK_ALL, 34 | encryption: BucketEncryption.S3_MANAGED, 35 | }); 36 | const dstPath = '/'; 37 | 38 | const originAccessIdentity = new OriginAccessIdentity(this, 'OriginAccessIdentity'); 39 | 40 | const distribution = new CloudFrontWebDistribution(this, 'Distribution', { 41 | originConfigs: [ 42 | { 43 | s3OriginSource: { 44 | s3BucketSource: dstBucket, 45 | originAccessIdentity, 46 | // originPath: dstPath, 47 | }, 48 | behaviors: [ 49 | { 50 | isDefaultBehavior: true, 51 | }, 52 | ], 53 | }, 54 | ], 55 | }); 56 | 57 | new CfnOutput(this, 'DistributionUrl', { 58 | value: `https://${distribution.distributionDomainName}`, 59 | }); 60 | 61 | new NodejsBuild(this, 'ExampleBuild', { 62 | assets: [ 63 | { 64 | path: 'example-app', 65 | exclude: ['dist', 'node_modules'], 66 | }, 67 | ], 68 | destinationBucket: dstBucket, 69 | destinationKeyPrefix: dstPath, 70 | outputSourceDirectory: 'dist', 71 | distribution, 72 | buildCommands: ['npm ci', 'npm run build'], 73 | buildEnvironment: { 74 | VITE_API_ENDPOINT: api.url, 75 | AAA: 'asdf', 76 | BBB: dstBucket.bucketName, 77 | }, 78 | nodejsVersion: 20, 79 | outputEnvFile: true, 80 | }); 81 | 82 | } 83 | } 84 | 85 | class SociIndexTestStack extends Stack { 86 | constructor(scope: Construct, id: string, props: StackProps = {}) { 87 | super(scope, id, props); 88 | 89 | const asset = new DockerImageAsset(this, 'Image', { directory: 'example-image' }); 90 | 91 | new SociIndexBuild(this, 'Index', { imageTag: asset.assetHash, repository: asset.repository }); 92 | } 93 | } 94 | 95 | class ContainerImageTestStack extends Stack { 96 | constructor(scope: Construct, id: string, props: StackProps = {}) { 97 | super(scope, id, props); 98 | 99 | const image = new ContainerImageBuild(this, 'Build', { directory: 'example-image', buildArgs: { DUMMY_FILE_SIZE_MB: '15' } }); 100 | const armImage = new ContainerImageBuild(this, 'BuildArm', { 101 | directory: 'example-image', 102 | platform: Platform.LINUX_ARM64, 103 | repository: image.repository, 104 | zstdCompression: true, 105 | }); 106 | new DockerImageFunction(this, 'Function', { 107 | code: image.toLambdaDockerImageCode(), 108 | }); 109 | new Cluster(this, 'Cluster', { 110 | vpc: new Vpc(this, 'Vpc', { 111 | maxAzs: 1, 112 | subnetConfiguration: [ 113 | { 114 | cidrMask: 24, 115 | name: 'public', 116 | subnetType: SubnetType.PUBLIC, 117 | }, 118 | ], 119 | }), 120 | }); 121 | new FargateTaskDefinition(this, 'TaskDefinition', { runtimePlatform: { cpuArchitecture: CpuArchitecture.ARM64 } }).addContainer('main', { 122 | image: armImage.toEcsDockerImageCode(), 123 | logging: new AwsLogDriver({ streamPrefix: 'main' }), 124 | }); 125 | } 126 | } 127 | 128 | class TestApp extends App { 129 | constructor() { 130 | super(); 131 | 132 | new NodejsTestStack(this, 'NodejsTestStack'); 133 | new SociIndexTestStack(this, 'SociIndexTestStack'); 134 | new ContainerImageTestStack(this, 'ContainerImageTestStack'); 135 | } 136 | } 137 | 138 | new TestApp().synth(); 139 | -------------------------------------------------------------------------------- /imgs/architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tmokmss/deploy-time-build/b9abc255766d58fe2d8dcc98adbd9b09830cc42b/imgs/architecture.png -------------------------------------------------------------------------------- /imgs/demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tmokmss/deploy-time-build/b9abc255766d58fe2d8dcc98adbd9b09830cc42b/imgs/demo.png -------------------------------------------------------------------------------- /imgs/soci-architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tmokmss/deploy-time-build/b9abc255766d58fe2d8dcc98adbd9b09830cc42b/imgs/soci-architecture.png -------------------------------------------------------------------------------- /lambda/trigger-codebuild/.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | !dist/* 3 | -------------------------------------------------------------------------------- /lambda/trigger-codebuild/index.ts: -------------------------------------------------------------------------------- 1 | import { BatchGetBuildsCommand, CodeBuildClient, StartBuildCommand } from '@aws-sdk/client-codebuild'; 2 | import type { ResourceProperties } from '../../src/types'; 3 | import Crypto from 'crypto'; 4 | 5 | const cb = new CodeBuildClient({}); 6 | 7 | type Event = { 8 | RequestType: 'Create' | 'Update' | 'Delete'; 9 | ResponseURL: string; 10 | StackId: string; 11 | RequestId: string; 12 | ResourceType: string; 13 | LogicalResourceId: string; 14 | ResourceProperties: ResourceProperties; 15 | }; 16 | 17 | export const handler = async (event: Event, context: any) => { 18 | console.log(JSON.stringify(event)); 19 | 20 | try { 21 | if (event.RequestType == 'Create' || event.RequestType == 'Update') { 22 | const props = event.ResourceProperties; 23 | const commonEnvironments = [ 24 | { 25 | name: 'responseURL', 26 | value: event.ResponseURL, 27 | }, 28 | { 29 | name: 'stackId', 30 | value: event.StackId, 31 | }, 32 | { 33 | name: 'requestId', 34 | value: event.RequestId, 35 | }, 36 | { 37 | name: 'logicalResourceId', 38 | value: event.LogicalResourceId, 39 | }, 40 | ]; 41 | const newPhysicalId = Crypto.randomBytes(16).toString('hex'); 42 | 43 | let command: StartBuildCommand; 44 | switch (props.type) { 45 | case 'NodejsBuild': 46 | command = new StartBuildCommand({ 47 | projectName: props.codeBuildProjectName, 48 | environmentVariablesOverride: [ 49 | ...commonEnvironments, 50 | { 51 | name: 'input', 52 | value: JSON.stringify( 53 | props.sources.map((source) => ({ 54 | assetUrl: `s3://${source.sourceBucketName}/${source.sourceObjectKey}`, 55 | extractPath: source.extractPath, 56 | commands: (source.commands ?? []).join(' && '), 57 | })) 58 | ), 59 | }, 60 | { 61 | name: 'buildCommands', 62 | value: props.buildCommands.join(' && '), 63 | }, 64 | { 65 | name: 'destinationBucketName', 66 | value: props.destinationBucketName, 67 | }, 68 | { 69 | name: 'destinationObjectKey', 70 | // This should be random to always trigger a BucketDeployment update process 71 | value: `${newPhysicalId}.zip`, 72 | }, 73 | { 74 | name: 'workingDirectory', 75 | value: props.workingDirectory, 76 | }, 77 | { 78 | name: 'outputSourceDirectory', 79 | value: props.outputSourceDirectory, 80 | }, 81 | { 82 | name: 'projectName', 83 | value: props.codeBuildProjectName, 84 | }, 85 | { 86 | name: 'outputEnvFile', 87 | value: props.outputEnvFile.toString(), 88 | }, 89 | { 90 | name: 'envFileKey', 91 | value: `deploy-time-build/${event.StackId.split('/')[1]}/${event.LogicalResourceId}/${newPhysicalId}.env`, 92 | }, 93 | { 94 | name: 'envNames', 95 | value: Object.keys(props.environment ?? {}).join(','), 96 | }, 97 | ...Object.entries(props.environment ?? {}).map(([name, value]) => ({ 98 | name, 99 | value, 100 | })), 101 | ], 102 | }); 103 | break; 104 | case 'SociIndexBuild': 105 | command = new StartBuildCommand({ 106 | projectName: props.codeBuildProjectName, 107 | environmentVariablesOverride: [ 108 | ...commonEnvironments, 109 | { 110 | name: 'repositoryName', 111 | value: props.repositoryName, 112 | }, 113 | { 114 | name: 'imageTag', 115 | value: props.imageTag, 116 | }, 117 | { 118 | name: 'projectName', 119 | value: props.codeBuildProjectName, 120 | }, 121 | ], 122 | }); 123 | break; 124 | case 'ContainerImageBuild': { 125 | const imageTag = props.imageTag ?? `${props.tagPrefix ?? ''}${newPhysicalId}`; 126 | const buildCommand = props.buildCommand.replaceAll('', imageTag); 127 | command = new StartBuildCommand({ 128 | projectName: props.codeBuildProjectName, 129 | environmentVariablesOverride: [ 130 | ...commonEnvironments, 131 | { 132 | name: 'repositoryUri', 133 | value: props.repositoryUri, 134 | }, 135 | { 136 | name: 'repositoryAuthUri', 137 | value: props.repositoryUri.split('/')[0], 138 | }, 139 | { 140 | name: 'buildCommand', 141 | value: buildCommand, 142 | }, 143 | { 144 | name: 'imageTag', 145 | value: imageTag, 146 | }, 147 | { 148 | name: 'projectName', 149 | value: props.codeBuildProjectName, 150 | }, 151 | { 152 | name: 'sourceS3Url', 153 | value: props.sourceS3Url, 154 | }, 155 | ], 156 | }); 157 | break; 158 | } 159 | default: 160 | throw new Error(`invalid event type ${props}}`); 161 | } 162 | // start code build project 163 | const build = await cb.send(command); 164 | 165 | // Sometimes CodeBuild build fails before running buildspec, without calling the CFn callback. 166 | // We can poll the status of a build for a few minutes and sendStatus if such errors are detected. 167 | // if (build.build?.id == null) { 168 | // throw new Error('build id is null'); 169 | // } 170 | 171 | // for (let i=0; i< 20; i++) { 172 | // const res = await cb.send(new BatchGetBuildsCommand({ ids: [build.build.id] })); 173 | // const status = res.builds?.[0].buildStatus; 174 | // if (status == null) { 175 | // throw new Error('build status is null'); 176 | // } 177 | 178 | // await new Promise((resolve) => setTimeout(resolve, 5000)); 179 | // } 180 | } else { 181 | // Do nothing on a DELETE event. 182 | await sendStatus('SUCCESS', event, context); 183 | } 184 | } catch (e) { 185 | console.log(e); 186 | const err = e as Error; 187 | await sendStatus('FAILED', event, context, err.message); 188 | } 189 | }; 190 | 191 | const sendStatus = async (status: 'SUCCESS' | 'FAILED', event: Event, context: any, reason?: string) => { 192 | const responseBody = JSON.stringify({ 193 | Status: status, 194 | Reason: reason ?? 'See the details in CloudWatch Log Stream: ' + context.logStreamName, 195 | PhysicalResourceId: context.logStreamName, 196 | StackId: event.StackId, 197 | RequestId: event.RequestId, 198 | LogicalResourceId: event.LogicalResourceId, 199 | NoEcho: false, 200 | Data: {}, //responseData 201 | }); 202 | 203 | await fetch(event.ResponseURL, { 204 | method: 'PUT', 205 | body: responseBody, 206 | headers: { 207 | 'Content-Type': '', 208 | 'Content-Length': responseBody.length.toString(), 209 | }, 210 | }); 211 | }; 212 | -------------------------------------------------------------------------------- /lambda/trigger-codebuild/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lambda", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "build": "esbuild index.ts --bundle --outdir=./dist --platform=node --external:@aws-sdk/*" 8 | }, 9 | "dependencies": { 10 | "@aws-sdk/client-codebuild": "^3.321.1" 11 | }, 12 | "devDependencies": { 13 | "@types/aws-lambda": "^8.10.95", 14 | "@types/node": "^18.16.3", 15 | "esbuild": "^0.14.39" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "deploy-time-build", 3 | "description": "Build during CDK deployment.", 4 | "repository": { 5 | "type": "git", 6 | "url": "https://github.com/tmokmss/deploy-time-build.git" 7 | }, 8 | "scripts": { 9 | "build": "npx projen build", 10 | "bump": "npx projen bump", 11 | "clobber": "npx projen clobber", 12 | "compat": "npx projen compat", 13 | "compile": "npx projen compile", 14 | "default": "npx projen default", 15 | "docgen": "npx projen docgen", 16 | "eject": "npx projen eject", 17 | "eslint": "npx projen eslint", 18 | "package": "npx projen package", 19 | "package-all": "npx projen package-all", 20 | "package:js": "npx projen package:js", 21 | "package:python": "npx projen package:python", 22 | "post-compile": "npx projen post-compile", 23 | "post-upgrade": "npx projen post-upgrade", 24 | "pre-compile": "npx projen pre-compile", 25 | "release": "npx projen release", 26 | "test": "npx projen test", 27 | "test:watch": "npx projen test:watch", 28 | "unbump": "npx projen unbump", 29 | "upgrade": "npx projen upgrade", 30 | "watch": "npx projen watch", 31 | "projen": "npx projen" 32 | }, 33 | "author": { 34 | "name": "tmokmss", 35 | "email": "tomookam@live.jp", 36 | "organization": false 37 | }, 38 | "devDependencies": { 39 | "@aws-cdk/integ-runner": "2.38.0", 40 | "@aws-cdk/integ-tests-alpha": "2.38.0-alpha.0", 41 | "@types/jest": "^27", 42 | "@types/node": "^18", 43 | "@typescript-eslint/eslint-plugin": "^7", 44 | "@typescript-eslint/parser": "^7", 45 | "aws-cdk-lib": "2.38.0", 46 | "commit-and-tag-version": "^12", 47 | "constructs": "10.0.5", 48 | "eslint": "^8", 49 | "eslint-import-resolver-typescript": "^2.7.1", 50 | "eslint-plugin-import": "^2.31.0", 51 | "jest": "^27", 52 | "jest-junit": "^15", 53 | "jsii": "~5.8.0", 54 | "jsii-diff": "^1.104.0", 55 | "jsii-docgen": "^10.5.0", 56 | "jsii-pacmak": "^1.104.0", 57 | "jsii-rosetta": "~5.8.0", 58 | "projen": "^0.88.9", 59 | "ts-jest": "^27", 60 | "ts-node": "^10.9.2", 61 | "typescript": "^4.9.5" 62 | }, 63 | "peerDependencies": { 64 | "aws-cdk-lib": "^2.38.0", 65 | "constructs": "^10.0.5" 66 | }, 67 | "keywords": [ 68 | "aws", 69 | "aws-cdk", 70 | "cdk", 71 | "ecr", 72 | "ecs", 73 | "lambda" 74 | ], 75 | "main": "lib/index.js", 76 | "license": "MIT", 77 | "publishConfig": { 78 | "access": "public" 79 | }, 80 | "version": "0.0.0", 81 | "jest": { 82 | "coverageProvider": "v8", 83 | "testMatch": [ 84 | "/@(src|test)/**/*(*.)@(spec|test).ts?(x)", 85 | "/@(src|test)/**/__tests__/**/*.ts?(x)", 86 | "/@(projenrc)/**/*(*.)@(spec|test).ts?(x)", 87 | "/@(projenrc)/**/__tests__/**/*.ts?(x)" 88 | ], 89 | "clearMocks": true, 90 | "collectCoverage": true, 91 | "coverageReporters": [ 92 | "json", 93 | "lcov", 94 | "clover", 95 | "cobertura", 96 | "text" 97 | ], 98 | "coverageDirectory": "coverage", 99 | "coveragePathIgnorePatterns": [ 100 | "/node_modules/" 101 | ], 102 | "testPathIgnorePatterns": [ 103 | "/node_modules/" 104 | ], 105 | "watchPathIgnorePatterns": [ 106 | "/node_modules/" 107 | ], 108 | "reporters": [ 109 | "default", 110 | [ 111 | "jest-junit", 112 | { 113 | "outputDirectory": "test-reports" 114 | } 115 | ] 116 | ], 117 | "preset": "ts-jest", 118 | "globals": { 119 | "ts-jest": { 120 | "tsconfig": "tsconfig.dev.json" 121 | } 122 | } 123 | }, 124 | "types": "lib/index.d.ts", 125 | "stability": "stable", 126 | "jsii": { 127 | "outdir": "dist", 128 | "targets": { 129 | "python": { 130 | "distName": "deploy-time-build", 131 | "module": "deploy_time_build" 132 | } 133 | }, 134 | "tsc": { 135 | "outDir": "lib", 136 | "rootDir": "src" 137 | } 138 | }, 139 | "//": "~~ Generated by projen. To modify, edit .projenrc.ts and run \"npx projen\"." 140 | } 141 | -------------------------------------------------------------------------------- /src/container-image-build.ts: -------------------------------------------------------------------------------- 1 | import { existsSync, readFileSync } from 'fs'; 2 | import { join } from 'path'; 3 | import { CfnResource, CustomResource, Duration, RemovalPolicy } from 'aws-cdk-lib'; 4 | import { BuildSpec, ComputeType, LinuxArmBuildImage, LinuxBuildImage } from 'aws-cdk-lib/aws-codebuild'; 5 | import { IVpc } from 'aws-cdk-lib/aws-ec2'; 6 | import { IRepository, Repository } from 'aws-cdk-lib/aws-ecr'; 7 | import { DockerImageAssetProps } from 'aws-cdk-lib/aws-ecr-assets'; 8 | import { ContainerImage } from 'aws-cdk-lib/aws-ecs'; 9 | import { IGrantable, IPrincipal, ManagedPolicy, PolicyStatement } from 'aws-cdk-lib/aws-iam'; 10 | import { Code, DockerImageCode, Runtime, RuntimeFamily, SingletonFunction } from 'aws-cdk-lib/aws-lambda'; 11 | import { Asset } from 'aws-cdk-lib/aws-s3-assets'; 12 | import { Construct } from 'constructs'; 13 | import { SingletonProject } from './singleton-project'; 14 | import { ContainerImageBuildResourceProps } from './types'; 15 | 16 | export interface ContainerImageBuildProps extends DockerImageAssetProps { 17 | /** 18 | * The tag when to push the image 19 | * @default use assetHash as tag 20 | */ 21 | readonly tag?: string; 22 | 23 | /** 24 | * Prefix to add to the image tag 25 | * @default no prefix 26 | */ 27 | readonly tagPrefix?: string; 28 | 29 | /** 30 | * The ECR repository to push the image. 31 | * @default create a new ECR repository 32 | */ 33 | readonly repository?: IRepository; 34 | 35 | /** 36 | * Use zstd for compressing a container image. 37 | * @default false 38 | */ 39 | readonly zstdCompression?: boolean; 40 | 41 | /** 42 | * The VPC where your build job will be deployed. 43 | * This VPC must have private subnets with NAT Gateways. 44 | * 45 | * Use this property when you want to control the outbound IP addresses that base images are pulled from. 46 | * @default No VPC used. 47 | */ 48 | readonly vpc?: IVpc; 49 | } 50 | 51 | /** 52 | * Build a container image and push it to an ECR repository on deploy-time. 53 | */ 54 | export class ContainerImageBuild extends Construct implements IGrantable { 55 | public readonly grantPrincipal: IPrincipal; 56 | public readonly repository: IRepository; 57 | public readonly imageTag: string; 58 | 59 | constructor(scope: Construct, id: string, private readonly props: ContainerImageBuildProps) { 60 | super(scope, id); 61 | 62 | const handler = new SingletonFunction(this, 'CustomResourceHandler', { 63 | // Use raw string to avoid from tightening CDK version requirement 64 | runtime: new Runtime('nodejs20.x', RuntimeFamily.NODEJS), 65 | code: Code.fromAsset(join(__dirname, '../lambda/trigger-codebuild/dist')), 66 | handler: 'index.handler', 67 | uuid: 'db740fd5-5436-4a84-8a09-e6dfcd01f4f3', // generated for this construct 68 | lambdaPurpose: 'DeployTimeBuildCustomResourceHandler', 69 | timeout: Duration.minutes(5), 70 | }); 71 | 72 | // Use buildx for cross-platform image build 73 | const armImage = LinuxArmBuildImage.fromCodeBuildImageId('aws/codebuild/amazonlinux2-aarch64-standard:3.0'); 74 | const x64Image = LinuxBuildImage.fromCodeBuildImageId('aws/codebuild/standard:7.0'); 75 | // Select the build image based on the target platform 76 | const isArm64 = props.platform?.platform === 'linux/arm64'; 77 | const buildImage = isArm64 ? armImage : x64Image; 78 | 79 | let repository = props.repository; 80 | if (repository === undefined) { 81 | repository = new Repository(this, 'Repository', { removalPolicy: RemovalPolicy.DESTROY }); 82 | (repository.node.defaultChild as CfnResource).addPropertyOverride('EmptyOnDelete', true); 83 | } 84 | 85 | const project = new SingletonProject(this, 'Project', { 86 | uuid: 'e83729fe-b156-4e70-9bec-452b15847a30', 87 | projectPurpose: isArm64 ? 'ContainerImageBuildArm64' : 'ContainerImageBuildAmd64', 88 | environment: { 89 | computeType: ComputeType.SMALL, 90 | buildImage: buildImage, 91 | privileged: true, 92 | }, 93 | vpc: props.vpc, 94 | buildSpec: BuildSpec.fromObject({ 95 | version: '0.2', 96 | phases: { 97 | build: { 98 | commands: [ 99 | 'current_dir=$(pwd)', 100 | 'echo "$input"', 101 | 'mkdir workdir', 102 | 'cd workdir', 103 | 'aws s3 cp "$sourceS3Url" temp.zip', 104 | 'unzip temp.zip', 105 | 'ls -la', 106 | 'aws ecr get-login-password | docker login --username AWS --password-stdin $repositoryAuthUri', 107 | 'aws ecr-public get-login-password --region us-east-1 | docker login --username AWS --password-stdin public.ecr.aws', 108 | 'docker buildx create --use', 109 | 'docker buildx ls', 110 | 'eval "$buildCommand"', 111 | ], 112 | }, 113 | post_build: { 114 | commands: [ 115 | 'echo Build completed on `date`', 116 | ` 117 | STATUS='SUCCESS' 118 | if [ $CODEBUILD_BUILD_SUCCEEDING -ne 1 ] # Test if the build is failing 119 | then 120 | STATUS='FAILED' 121 | REASON="ContainerImageBuild failed. See CloudWatch Log stream for the detailed reason: 122 | https://$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" 123 | fi 124 | cat < payload.json 125 | { 126 | "StackId": "$stackId", 127 | "RequestId": "$requestId", 128 | "LogicalResourceId":"$logicalResourceId", 129 | "PhysicalResourceId": "$imageTag", 130 | "Status": "$STATUS", 131 | "Reason": "$REASON", 132 | "Data": { 133 | "ImageTag": "$imageTag" 134 | } 135 | } 136 | EOF 137 | curl -v -i -X PUT -H 'Content-Type:' -d "@payload.json" "$responseURL" 138 | `, 139 | ], 140 | }, 141 | }, 142 | }), 143 | }).project; 144 | 145 | project.role!.addManagedPolicy(ManagedPolicy.fromAwsManagedPolicyName('AmazonElasticContainerRegistryPublicReadOnly')); 146 | repository.grantPullPush(project); 147 | repository.grant(project, 'ecr:DescribeImages'); 148 | 149 | handler.addToRolePolicy( 150 | new PolicyStatement({ 151 | actions: ['codebuild:StartBuild'], 152 | resources: [project.projectArn], 153 | }), 154 | ); 155 | 156 | this.grantPrincipal = project.grantPrincipal; 157 | 158 | let assetExclude = props.exclude; 159 | // automatically read .dockerignore for convenience 160 | if (assetExclude === undefined && existsSync(join(props.directory, '.dockerignore'))) { 161 | assetExclude = readFileSync(join(props.directory, '.dockerignore')).toString().split('\n'); 162 | } 163 | const asset = new Asset(this, 'Source', { 164 | ...props, 165 | exclude: assetExclude, 166 | path: props.directory, 167 | }); 168 | asset.grantRead(project); 169 | 170 | const buildCommandOptions = { ...props, platform: props.platform?.platform } as any; 171 | buildCommandOptions.outputs ??= []; 172 | // will be replaced in the trigger Lambda 173 | // to enable zstd compression, buildx directly pushes the artifact image to a registry 174 | // https://aws.amazon.com/blogs/containers/reducing-aws-fargate-startup-times-with-zstd-compressed-container-images/ 175 | buildCommandOptions.outputs.push('type=image', `name=${repository.repositoryUri}:`, 'push=true'); 176 | if (props.zstdCompression) { 177 | buildCommandOptions.outputs.push('oci-mediatypes=true', 'compression=zstd', 'force-compression=true', 'compression-level=3'); 178 | } 179 | const buildCommand = this.getDockerBuildCommand(buildCommandOptions); 180 | 181 | const properties: ContainerImageBuildResourceProps = { 182 | type: 'ContainerImageBuild', 183 | buildCommand: buildCommand, 184 | repositoryUri: repository.repositoryUri, 185 | imageTag: props.tag, 186 | tagPrefix: props.tagPrefix, 187 | codeBuildProjectName: project.projectName, 188 | sourceS3Url: asset.s3ObjectUrl, 189 | }; 190 | 191 | const custom = new CustomResource(this, 'Resource', { 192 | serviceToken: handler.functionArn, 193 | resourceType: 'Custom::CDKContainerImageBuild', 194 | properties, 195 | }); 196 | custom.node.addDependency(project); 197 | 198 | this.repository = repository; 199 | this.imageTag = custom.getAttString('ImageTag'); 200 | } 201 | 202 | /** 203 | * Get the instance of {@link DockerImageCode} for a Lambda function image. 204 | */ 205 | public toLambdaDockerImageCode() { 206 | if (this.props.zstdCompression) { 207 | throw new Error('You cannot enable zstdCompression for a Lambda image.'); 208 | } 209 | return DockerImageCode.fromEcr(this.repository, { tagOrDigest: this.imageTag }); 210 | } 211 | 212 | /** 213 | * Get the instance of {@link ContainerImage} for an ECS task definition. 214 | */ 215 | public toEcsDockerImageCode() { 216 | return ContainerImage.fromEcrRepository(this.repository, this.imageTag); 217 | } 218 | 219 | private getDockerBuildCommand(options: any) { 220 | // the members of props differs with CDK version. 221 | // By regarding props as any, we can use props that are not available in older cdk versions. 222 | 223 | // logic is copied from packages/cdk-assets/lib/private/docker.ts 224 | const cacheOptionToFlag = (option: any): string => { 225 | let flag = `type=${option.type}`; 226 | if (option.params) { 227 | flag += 228 | ',' + 229 | Object.entries(option.params) 230 | .map(([k, v]) => `${k}=${v}`) 231 | .join(','); 232 | } 233 | return flag; 234 | }; 235 | const flatten = (x: string[][]) => { 236 | return Array.prototype.concat([], ...x); 237 | }; 238 | 239 | const dockerBuildCommand = [ 240 | 'docker buildx build', 241 | ...flatten(Object.entries(options.buildArgs || {}).map(([k, v]) => ['--build-arg', `${k}=${v}`])), 242 | ...flatten(Object.entries(options.buildSecrets || {}).map(([k, v]) => ['--secret', `id=${k},${v}`])), 243 | ...(options.buildSsh ? ['--ssh', options.buildSsh] : []), 244 | ...(options.target ? ['--target', options.target] : []), 245 | ...(options.file ? ['--file', options.file] : []), 246 | ...(options.networkMode ? ['--network', options.networkMode] : []), 247 | ...(options.platform ? ['--platform', options.platform] : []), 248 | ...(options.outputs ? ['--output', options.outputs.join(',')] : []), 249 | ...(options.cacheFrom ? [...options.cacheFrom.map((cacheFrom: any) => ['--cache-from', cacheOptionToFlag(cacheFrom)]).flat()] : []), 250 | ...(options.cacheTo ? ['--cache-to', cacheOptionToFlag(options.cacheTo)] : []), 251 | ...(options.cacheDisabled ? ['--no-cache'] : []), 252 | '--provenance=false', 253 | '.', 254 | ]; 255 | return dockerBuildCommand.join(' '); 256 | } 257 | } 258 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './container-image-build'; 2 | export * from './nodejs-build'; 3 | export * from './soci-index-build'; 4 | -------------------------------------------------------------------------------- /src/nodejs-build.ts: -------------------------------------------------------------------------------- 1 | import { posix, join, basename } from 'path'; 2 | import { Annotations, CfnOutput, CustomResource, Duration } from 'aws-cdk-lib'; 3 | import { IDistribution } from 'aws-cdk-lib/aws-cloudfront'; 4 | import { BuildSpec, LinuxBuildImage, Project } from 'aws-cdk-lib/aws-codebuild'; 5 | import { IGrantable, IPrincipal, PolicyStatement } from 'aws-cdk-lib/aws-iam'; 6 | import { Code, Runtime, RuntimeFamily, SingletonFunction } from 'aws-cdk-lib/aws-lambda'; 7 | import { IBucket } from 'aws-cdk-lib/aws-s3'; 8 | import { Asset, AssetProps } from 'aws-cdk-lib/aws-s3-assets'; 9 | import { BucketDeployment, Source } from 'aws-cdk-lib/aws-s3-deployment'; 10 | import { Construct } from 'constructs'; 11 | import { NodejsBuildResourceProps } from './types'; 12 | 13 | export interface AssetConfig extends AssetProps { 14 | /** 15 | * Shell commands executed right after the asset zip is extracted to the build environment. 16 | * @default No command is executed. 17 | */ 18 | readonly commands?: string[]; 19 | 20 | /** 21 | * Relative path from a build directory to the directory where the asset is extracted. 22 | * @default basename of the asset path. 23 | */ 24 | readonly extractPath?: string; 25 | } 26 | 27 | export interface NodejsBuildProps { 28 | /** 29 | * The AssetProps from which s3-assets are created and copied to the build environment. 30 | */ 31 | readonly assets: AssetConfig[]; 32 | /** 33 | * Environment variables injected to the build environment. 34 | * You can use CDK deploy-time values as well as literals. 35 | * @default {} 36 | */ 37 | readonly buildEnvironment?: { [key: string]: string }; 38 | /** 39 | * S3 Bucket to which your build artifacts are finally deployed. 40 | */ 41 | readonly destinationBucket: IBucket; 42 | /** 43 | * Key prefix to deploy your build artifact. 44 | * @default '/' 45 | */ 46 | readonly destinationKeyPrefix?: string; 47 | /** 48 | * The distribution you are using to publish you build artifact. 49 | * If any specified, the caches are invalidated on new artifact deployments. 50 | * @default No distribution 51 | */ 52 | readonly distribution?: IDistribution; 53 | /** 54 | * Shell commands to build your project. They are executed on the working directory you specified. 55 | * @default ['npm run build'] 56 | */ 57 | readonly buildCommands?: string[]; 58 | /** 59 | * Relative path from the build directory to the directory where build commands run. 60 | * @default assetProps[0].extractPath 61 | */ 62 | readonly workingDirectory?: string; 63 | /** 64 | * Relative path from the working directory to the directory where the build artifacts are output. 65 | */ 66 | readonly outputSourceDirectory: string; 67 | /** 68 | * The version of Node.js to use in a build environment. Available versions: 12, 14, 16, 18, 20. 69 | * @default 18 70 | */ 71 | readonly nodejsVersion?: number; 72 | /** 73 | * If true, a .env file is uploaded to an S3 bucket with values of `buildEnvironment` property. 74 | * You can copy it to your local machine by running the command in the stack output. 75 | * @default false 76 | */ 77 | readonly outputEnvFile?: boolean; 78 | /** 79 | * If true, common unnecessary files/directories such as .DS_Store, .git, node_modules, etc are excluded 80 | * from the assets by default. 81 | * @default true 82 | */ 83 | readonly excludeCommonFiles?: boolean; 84 | } 85 | 86 | /** 87 | * Build Node.js app and optionally publish the artifact to an S3 bucket. 88 | */ 89 | export class NodejsBuild extends Construct implements IGrantable { 90 | public readonly grantPrincipal: IPrincipal; 91 | 92 | constructor(scope: Construct, id: string, props: NodejsBuildProps) { 93 | super(scope, id); 94 | 95 | const handler = new SingletonFunction(this, 'CustomResourceHandler', { 96 | // Use raw string to avoid from tightening CDK version requirement 97 | runtime: new Runtime('nodejs20.x', RuntimeFamily.NODEJS), 98 | code: Code.fromAsset(join(__dirname, '../lambda/trigger-codebuild/dist')), 99 | handler: 'index.handler', 100 | uuid: '25648b21-2c40-4f09-aa65-b6bbb0c44659', // generated for this construct 101 | lambdaPurpose: 'NodejsBuildCustomResourceHandler', 102 | timeout: Duration.minutes(5), 103 | }); 104 | 105 | const nodejsVersion = props.nodejsVersion ?? 18; 106 | let buildImage = 'aws/codebuild/standard:7.0'; 107 | // See: https://docs.aws.amazon.com/codebuild/latest/userguide/available-runtimes.html#linux-runtimes 108 | switch (nodejsVersion) { 109 | case 12: 110 | case 14: 111 | buildImage = 'aws/codebuild/standard:5.0'; 112 | break; 113 | case 16: 114 | buildImage = 'aws/codebuild/standard:6.0'; 115 | break; 116 | case 18: 117 | case 20: 118 | buildImage = 'aws/codebuild/standard:7.0'; 119 | break; 120 | default: 121 | Annotations.of(this).addWarning(`Possibly unsupported Node.js version: ${nodejsVersion}. Currently 12, 14, 16, 18, and 20 are supported.`); 122 | } 123 | 124 | const destinationObjectKeyOutputKey = 'destinationObjectKey'; 125 | const envFileKeyOutputKey = 'envFileKey'; 126 | 127 | const project = new Project(this, 'Project', { 128 | environment: { buildImage: LinuxBuildImage.fromCodeBuildImageId(buildImage) }, 129 | buildSpec: BuildSpec.fromObject({ 130 | version: '0.2', 131 | env: { 132 | shell: 'bash', 133 | }, 134 | phases: { 135 | install: { 136 | 'runtime-versions': { 137 | nodejs: nodejsVersion, 138 | }, 139 | }, 140 | build: { 141 | commands: [ 142 | 'current_dir=$(pwd)', 143 | // Iterate a json array using jq 144 | // https://www.starkandwayne.com/blog/bash-for-loop-over-json-array-using-jq/index.html 145 | ` 146 | echo "$input" 147 | for obj in $(echo "$input" | jq -r '.[] | @base64'); do 148 | decoded=$(echo "$obj" | base64 --decode) 149 | assetUrl=$(echo "$decoded" | jq -r '.assetUrl') 150 | extractPath=$(echo "$decoded" | jq -r '.extractPath') 151 | commands=$(echo "$decoded" | jq -r '.commands') 152 | 153 | # Download the zip file 154 | aws s3 cp "$assetUrl" temp.zip 155 | 156 | # Extract the zip file to the extractPath directory 157 | mkdir -p "$extractPath" 158 | unzip temp.zip -d "$extractPath" 159 | 160 | # Remove the zip file 161 | rm temp.zip 162 | 163 | # Run the specified commands in the extractPath directory 164 | cd "$extractPath" 165 | ls -la 166 | eval "$commands" 167 | cd "$current_dir" 168 | ls -la 169 | done 170 | `, 171 | 'ls -la', 172 | 'cd "$workingDirectory"', 173 | 'eval "$buildCommands"', 174 | 'ls -la', 175 | 'cd "$current_dir"', 176 | 'cd "$outputSourceDirectory"', 177 | 'zip -r output.zip ./', 178 | 'aws s3 cp output.zip "s3://$destinationBucketName/$destinationObjectKey"', 179 | // Upload .env if required 180 | ` 181 | if [[ $outputEnvFile == "true" ]] 182 | then 183 | # Split the comma-separated string into an array 184 | for var_name in \${envNames//,/ } 185 | do 186 | echo "Element: $var_name" 187 | var_value="\${!var_name}" 188 | echo "$var_name=$var_value" >> tmp.env 189 | done 190 | 191 | aws s3 cp tmp.env "s3://$destinationBucketName/$envFileKey" 192 | fi 193 | `, 194 | ], 195 | }, 196 | post_build: { 197 | commands: [ 198 | 'echo Build completed on `date`', 199 | ` 200 | STATUS='SUCCESS' 201 | if [ $CODEBUILD_BUILD_SUCCEEDING -ne 1 ] # Test if the build is failing 202 | then 203 | STATUS='FAILED' 204 | REASON="NodejsBuild failed. See CloudWatch Log stream for the detailed reason: 205 | https://$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" 206 | fi 207 | cat < payload.json 208 | { 209 | "StackId": "$stackId", 210 | "RequestId": "$requestId", 211 | "LogicalResourceId":"$logicalResourceId", 212 | "PhysicalResourceId": "$logicalResourceId", 213 | "Status": "$STATUS", 214 | "Reason": "$REASON", 215 | "Data": { 216 | "${destinationObjectKeyOutputKey}": "$destinationObjectKey", 217 | "${envFileKeyOutputKey}": "$envFileKey" 218 | } 219 | } 220 | EOF 221 | curl -v -i -X PUT -H 'Content-Type:' -d "@payload.json" "$responseURL" 222 | `, 223 | ], 224 | }, 225 | }, 226 | }), 227 | }); 228 | 229 | handler.addToRolePolicy( 230 | new PolicyStatement({ 231 | actions: ['codebuild:StartBuild'], 232 | resources: [project.projectArn], 233 | }), 234 | ); 235 | 236 | this.grantPrincipal = project.grantPrincipal; 237 | 238 | const commonExclude = ['.DS_Store', '.git', 'node_modules']; 239 | const assets = props.assets.map((assetProps) => { 240 | const asset = new Asset(this, `Source-${assetProps.path.replace('/', '')}`, { 241 | ...assetProps, 242 | ...(props.excludeCommonFiles ?? true ? { exclude: [...commonExclude, ...(assetProps.exclude ?? [])] } : {}), 243 | }); 244 | asset.grantRead(project); 245 | return asset; 246 | }); 247 | 248 | // use the asset bucket that are created by CDK bootstrap to store intermediate artifacts 249 | const bucket = assets[0].bucket; 250 | bucket.grantWrite(project); 251 | 252 | const sources: NodejsBuildResourceProps['sources'] = props.assets.map((s, i) => ({ 253 | sourceBucketName: assets[i].s3BucketName, 254 | sourceObjectKey: assets[i].s3ObjectKey, 255 | extractPath: s.extractPath ?? basename(s.path), 256 | commands: s.commands, 257 | })); 258 | 259 | const properties: NodejsBuildResourceProps = { 260 | type: 'NodejsBuild', 261 | sources, 262 | destinationBucketName: bucket.bucketName, 263 | workingDirectory: sources[0].extractPath, 264 | // join paths for CodeBuild (Linux) platform 265 | outputSourceDirectory: posix.join(sources[0].extractPath, props.outputSourceDirectory), 266 | environment: props.buildEnvironment, 267 | buildCommands: props.buildCommands ?? ['npm run build'], 268 | codeBuildProjectName: project.projectName, 269 | outputEnvFile: props.outputEnvFile ?? false, 270 | }; 271 | 272 | const custom = new CustomResource(this, 'Resource', { 273 | serviceToken: handler.functionArn, 274 | resourceType: 'Custom::CDKNodejsBuild', 275 | properties, 276 | }); 277 | 278 | const deploy = new BucketDeployment(this, 'Deploy', { 279 | sources: [Source.bucket(bucket, custom.getAttString(destinationObjectKeyOutputKey))], 280 | destinationBucket: props.destinationBucket, 281 | destinationKeyPrefix: props.destinationKeyPrefix, 282 | distribution: props.distribution, 283 | memoryLimit: 512, // sometimes timeout on default (128MB) memory 284 | }); 285 | 286 | deploy.node.addDependency(custom); 287 | 288 | if (props.outputEnvFile) { 289 | new CfnOutput(this, 'DownloadEnvFile', { value: `aws s3 cp ${bucket.s3UrlForObject(custom.getAttString(envFileKeyOutputKey))} .env.local` }); 290 | } 291 | } 292 | } 293 | -------------------------------------------------------------------------------- /src/singleton-project.ts: -------------------------------------------------------------------------------- 1 | import { Stack } from 'aws-cdk-lib'; 2 | import { Project, ProjectProps } from 'aws-cdk-lib/aws-codebuild'; 3 | import { Construct } from 'constructs'; 4 | 5 | export interface SingletonProjectProps extends ProjectProps { 6 | /** 7 | * A unique identifier to identify this CodeBuild project. 8 | * 9 | * The identifier should be unique across all singleton projects. We recommend generating a UUID per project. 10 | */ 11 | readonly uuid: string; 12 | 13 | /** 14 | * A descriptive name for the purpose of this CodeBuild project. 15 | * 16 | * If the project does not have a physical name, this string will be reflected its generated name. The combination of projectPurpose and uuid must be unique. 17 | * 18 | * @default SingletonProject 19 | */ 20 | readonly projectPurpose?: string; 21 | } 22 | 23 | /** 24 | * A CodeBuild project that is created only once per stack. 25 | */ 26 | export class SingletonProject extends Construct { 27 | public readonly project: Project; 28 | 29 | constructor(scope: Construct, id: string, props: SingletonProjectProps) { 30 | super(scope, id); 31 | this.project = this.ensureProject(props); 32 | } 33 | 34 | private ensureProject(props: SingletonProjectProps): Project { 35 | const constructName = (props.projectPurpose ?? 'SingletonProject') + this.slugify(props.uuid, this.propsToAdditionalString(props)); 36 | const existing = Stack.of(this).node.tryFindChild(constructName); 37 | if (existing) { 38 | return existing as Project; 39 | } 40 | 41 | return new Project(Stack.of(this), constructName, props); 42 | } 43 | 44 | private propsToAdditionalString(props: SingletonProjectProps) { 45 | // This string must be stable to avoid from replacement. 46 | // Things that can be added to the slug later (we have to create a new project per these properties): 47 | // * vpc addr 48 | // * instance type 49 | // * platform (amd64/arm64) 50 | // But actually, replacement will not cause any disruption because of its stateless nature. 51 | let slug = ''; 52 | slug += props.vpc?.node.addr ?? ''; 53 | // Get platform info from environment.buildImage if available 54 | if (props.environment?.buildImage) { 55 | slug += props.environment.buildImage.toString().includes('aarch64') ? 'arm64' : 'amd64'; 56 | } 57 | return slug; 58 | } 59 | 60 | private slugify(x: string, additionalString?: string): string { 61 | return `${x}${additionalString ?? ''}`.replace(/[^a-zA-Z0-9]/g, ''); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/soci-index-build.ts: -------------------------------------------------------------------------------- 1 | import { join } from 'path'; 2 | import { CustomResource, Duration } from 'aws-cdk-lib'; 3 | import { BuildSpec, LinuxBuildImage } from 'aws-cdk-lib/aws-codebuild'; 4 | import { IRepository } from 'aws-cdk-lib/aws-ecr'; 5 | import { DockerImageAsset } from 'aws-cdk-lib/aws-ecr-assets'; 6 | import { PolicyStatement } from 'aws-cdk-lib/aws-iam'; 7 | import { Code, Runtime, RuntimeFamily, SingletonFunction } from 'aws-cdk-lib/aws-lambda'; 8 | import { Construct } from 'constructs'; 9 | import { SingletonProject } from './singleton-project'; 10 | import { SociIndexBuildResourceProps } from './types'; 11 | 12 | export interface SociIndexBuildProps { 13 | /** 14 | * The ECR repository your container image is stored. 15 | * You can only specify a repository in the same environment (account/region). 16 | * The index artifact will be uploaded to this repository. 17 | */ 18 | readonly repository: IRepository; 19 | 20 | /** 21 | * The tag of the container image you want to build index for. 22 | */ 23 | readonly imageTag: string; 24 | } 25 | 26 | /** 27 | * Build and publish a SOCI index for a container image. 28 | * A SOCI index helps start Fargate tasks faster in some cases. 29 | * Please read the following document for more details: https://docs.aws.amazon.com/AmazonECS/latest/userguide/container-considerations.html 30 | */ 31 | export class SociIndexBuild extends Construct { 32 | /** 33 | * A utility method to create a SociIndexBuild construct from a DockerImageAsset instance. 34 | */ 35 | public static fromDockerImageAsset(scope: Construct, id: string, imageAsset: DockerImageAsset) { 36 | return new SociIndexBuild(scope, id, { 37 | repository: imageAsset.repository, 38 | imageTag: imageAsset.assetHash, 39 | }); 40 | } 41 | 42 | constructor(scope: Construct, id: string, props: SociIndexBuildProps) { 43 | super(scope, id); 44 | 45 | const sociWrapperVersion = 'v0.1.2'; 46 | 47 | const binaryUrl = `https://github.com/tmokmss/soci-wrapper/releases/download/${sociWrapperVersion}/soci-wrapper-${sociWrapperVersion}-linux-amd64.tar.gz`; 48 | 49 | const handler = new SingletonFunction(this, 'CustomResourceHandler', { 50 | // Use raw string to avoid from tightening CDK version requirement 51 | runtime: new Runtime('nodejs20.x', RuntimeFamily.NODEJS), 52 | code: Code.fromAsset(join(__dirname, '../lambda/trigger-codebuild/dist')), 53 | handler: 'index.handler', 54 | uuid: 'db740fd5-5436-4a84-8a09-e6dfcd01f4f3', // generated for this construct 55 | lambdaPurpose: 'DeployTimeBuildCustomResourceHandler', 56 | timeout: Duration.minutes(5), 57 | }); 58 | 59 | const project = new SingletonProject(this, 'Project', { 60 | uuid: '024cf76a-1003-4aa4-aa4b-12c32c09ca3c', // generated for this construct 61 | projectPurpose: 'SociIndexBuild', 62 | environment: { buildImage: LinuxBuildImage.fromCodeBuildImageId('aws/codebuild/standard:7.0') }, 63 | buildSpec: BuildSpec.fromObject({ 64 | version: '0.2', 65 | phases: { 66 | build: { 67 | commands: [ 68 | 'current_dir=$(pwd)', 69 | `wget --quiet -O soci-wrapper.tar.gz ${binaryUrl}`, 70 | 'tar -xvzf soci-wrapper.tar.gz', 71 | '', 72 | 'export AWS_ACCOUNT=$(aws sts get-caller-identity --query "Account" --output text)', 73 | 'export REGISTRY_USER=AWS', 74 | 'export REGISTRY_PASSWORD=$(aws ecr get-login-password --region $AWS_REGION)', 75 | 'export REGISTRY=$AWS_ACCOUNT.dkr.ecr.$AWS_REGION.amazonaws.com', 76 | 'aws ecr get-login-password --region $AWS_REGION | docker login --username AWS --password-stdin $REGISTRY', 77 | 'REPO_NAME=$repositoryName', 78 | 'IMAGE_TAG=$imageTag', 79 | 'DIGEST=$(aws ecr describe-images --repository-name $REPO_NAME --image-ids imageTag=$IMAGE_TAG --query imageDetails[0].imageDigest --output text)', 80 | './soci-wrapper $REPO_NAME $DIGEST $AWS_REGION $AWS_ACCOUNT', 81 | ], 82 | }, 83 | post_build: { 84 | commands: [ 85 | 'echo Build completed on `date`', 86 | ` 87 | STATUS='SUCCESS' 88 | if [ $CODEBUILD_BUILD_SUCCEEDING -ne 1 ] # Test if the build is failing 89 | then 90 | STATUS='FAILED' 91 | REASON="deploy-time-build failed. See CloudWatch Log stream for the detailed reason: 92 | https://$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" 93 | fi 94 | cat < payload.json 95 | { 96 | "StackId": "$stackId", 97 | "RequestId": "$requestId", 98 | "LogicalResourceId":"$logicalResourceId", 99 | "PhysicalResourceId": "$imageTag", 100 | "Status": "$STATUS", 101 | "Reason": "$REASON" 102 | } 103 | EOF 104 | curl -vv -i -X PUT -H 'Content-Type:' -d "@payload.json" "$responseURL" 105 | `, 106 | ], 107 | }, 108 | }, 109 | }), 110 | }).project; 111 | 112 | handler.addToRolePolicy( 113 | new PolicyStatement({ 114 | actions: ['codebuild:StartBuild'], 115 | resources: [project.projectArn], 116 | }), 117 | ); 118 | 119 | props.repository.grantPullPush(project); 120 | props.repository.grant(project, 'ecr:DescribeImages'); 121 | 122 | const properties: SociIndexBuildResourceProps = { 123 | type: 'SociIndexBuild', 124 | imageTag: props.imageTag, 125 | repositoryName: props.repository.repositoryName, 126 | codeBuildProjectName: project.projectName, 127 | }; 128 | 129 | new CustomResource(this, 'Resource', { 130 | serviceToken: handler.functionArn, 131 | resourceType: 'Custom::CDKSociIndexBuild', 132 | properties, 133 | }); 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | export type ResourceProperties = NodejsBuildResourceProps | SociIndexBuildResourceProps | ContainerImageBuildResourceProps; 2 | 3 | export type NodejsBuildResourceProps = { 4 | type: 'NodejsBuild'; 5 | sources: { 6 | sourceBucketName: string; 7 | sourceObjectKey: string; 8 | extractPath: string; 9 | commands?: string[]; 10 | }[]; 11 | environment?: { [key: string]: string }; 12 | destinationBucketName: string; 13 | workingDirectory: string; 14 | outputSourceDirectory: string; 15 | buildCommands: string[]; 16 | codeBuildProjectName: string; 17 | outputEnvFile: boolean; 18 | }; 19 | 20 | export type SociIndexBuildResourceProps = { 21 | type: 'SociIndexBuild'; 22 | repositoryName: string; 23 | imageTag: string; 24 | codeBuildProjectName: string; 25 | }; 26 | 27 | export type ContainerImageBuildResourceProps = { 28 | type: 'ContainerImageBuild'; 29 | buildCommand: string; 30 | repositoryUri: string; 31 | imageTag?: string; 32 | tagPrefix?: string; 33 | codeBuildProjectName: string; 34 | sourceS3Url: string; 35 | }; 36 | -------------------------------------------------------------------------------- /test/container-image-build.integ.snapshot/TestDefaultTestDeployAssert12909640.template.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /test/container-image-build.integ.snapshot/asset.9b19c7d81a94bbc19fe2164453716f4d7f651b21f923c5c264fe80f5a939e1db/index.js: -------------------------------------------------------------------------------- 1 | var __create = Object.create; 2 | var __defProp = Object.defineProperty; 3 | var __getOwnPropDesc = Object.getOwnPropertyDescriptor; 4 | var __getOwnPropNames = Object.getOwnPropertyNames; 5 | var __getProtoOf = Object.getPrototypeOf; 6 | var __hasOwnProp = Object.prototype.hasOwnProperty; 7 | var __export = (target, all) => { 8 | for (var name in all) 9 | __defProp(target, name, { get: all[name], enumerable: true }); 10 | }; 11 | var __copyProps = (to, from, except, desc) => { 12 | if (from && typeof from === "object" || typeof from === "function") { 13 | for (let key of __getOwnPropNames(from)) 14 | if (!__hasOwnProp.call(to, key) && key !== except) 15 | __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); 16 | } 17 | return to; 18 | }; 19 | var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod)); 20 | var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); 21 | 22 | // index.ts 23 | var trigger_codebuild_exports = {}; 24 | __export(trigger_codebuild_exports, { 25 | handler: () => handler 26 | }); 27 | module.exports = __toCommonJS(trigger_codebuild_exports); 28 | var import_client_codebuild = require("@aws-sdk/client-codebuild"); 29 | var import_crypto = __toESM(require("crypto")); 30 | var cb = new import_client_codebuild.CodeBuildClient({}); 31 | var handler = async (event, context) => { 32 | console.log(JSON.stringify(event)); 33 | try { 34 | if (event.RequestType == "Create" || event.RequestType == "Update") { 35 | const props = event.ResourceProperties; 36 | const commonEnvironments = [ 37 | { 38 | name: "responseURL", 39 | value: event.ResponseURL 40 | }, 41 | { 42 | name: "stackId", 43 | value: event.StackId 44 | }, 45 | { 46 | name: "requestId", 47 | value: event.RequestId 48 | }, 49 | { 50 | name: "logicalResourceId", 51 | value: event.LogicalResourceId 52 | } 53 | ]; 54 | const newPhysicalId = import_crypto.default.randomBytes(16).toString("hex"); 55 | let command; 56 | switch (props.type) { 57 | case "NodejsBuild": 58 | command = new import_client_codebuild.StartBuildCommand({ 59 | projectName: props.codeBuildProjectName, 60 | environmentVariablesOverride: [ 61 | ...commonEnvironments, 62 | { 63 | name: "input", 64 | value: JSON.stringify(props.sources.map((source) => ({ 65 | assetUrl: `s3://${source.sourceBucketName}/${source.sourceObjectKey}`, 66 | extractPath: source.extractPath, 67 | commands: (source.commands ?? []).join(" && ") 68 | }))) 69 | }, 70 | { 71 | name: "buildCommands", 72 | value: props.buildCommands.join(" && ") 73 | }, 74 | { 75 | name: "destinationBucketName", 76 | value: props.destinationBucketName 77 | }, 78 | { 79 | name: "destinationObjectKey", 80 | value: `${newPhysicalId}.zip` 81 | }, 82 | { 83 | name: "workingDirectory", 84 | value: props.workingDirectory 85 | }, 86 | { 87 | name: "outputSourceDirectory", 88 | value: props.outputSourceDirectory 89 | }, 90 | { 91 | name: "projectName", 92 | value: props.codeBuildProjectName 93 | }, 94 | { 95 | name: "outputEnvFile", 96 | value: props.outputEnvFile.toString() 97 | }, 98 | { 99 | name: "envFileKey", 100 | value: `deploy-time-build/${event.StackId.split("/")[1]}/${event.LogicalResourceId}/${newPhysicalId}.env` 101 | }, 102 | { 103 | name: "envNames", 104 | value: Object.keys(props.environment ?? {}).join(",") 105 | }, 106 | ...Object.entries(props.environment ?? {}).map(([name, value]) => ({ 107 | name, 108 | value 109 | })) 110 | ] 111 | }); 112 | break; 113 | case "SociIndexBuild": 114 | command = new import_client_codebuild.StartBuildCommand({ 115 | projectName: props.codeBuildProjectName, 116 | environmentVariablesOverride: [ 117 | ...commonEnvironments, 118 | { 119 | name: "repositoryName", 120 | value: props.repositoryName 121 | }, 122 | { 123 | name: "imageTag", 124 | value: props.imageTag 125 | }, 126 | { 127 | name: "projectName", 128 | value: props.codeBuildProjectName 129 | } 130 | ] 131 | }); 132 | break; 133 | case "ContainerImageBuild": { 134 | const imageTag = props.imageTag ?? `${props.tagPrefix ?? ""}${newPhysicalId}`; 135 | const buildCommand = props.buildCommand.replaceAll("", imageTag); 136 | command = new import_client_codebuild.StartBuildCommand({ 137 | projectName: props.codeBuildProjectName, 138 | environmentVariablesOverride: [ 139 | ...commonEnvironments, 140 | { 141 | name: "repositoryUri", 142 | value: props.repositoryUri 143 | }, 144 | { 145 | name: "repositoryAuthUri", 146 | value: props.repositoryUri.split("/")[0] 147 | }, 148 | { 149 | name: "buildCommand", 150 | value: buildCommand 151 | }, 152 | { 153 | name: "imageTag", 154 | value: imageTag 155 | }, 156 | { 157 | name: "projectName", 158 | value: props.codeBuildProjectName 159 | }, 160 | { 161 | name: "sourceS3Url", 162 | value: props.sourceS3Url 163 | } 164 | ] 165 | }); 166 | break; 167 | } 168 | default: 169 | throw new Error(`invalid event type ${props}}`); 170 | } 171 | const build = await cb.send(command); 172 | } else { 173 | await sendStatus("SUCCESS", event, context); 174 | } 175 | } catch (e) { 176 | console.log(e); 177 | const err = e; 178 | await sendStatus("FAILED", event, context, err.message); 179 | } 180 | }; 181 | var sendStatus = async (status, event, context, reason) => { 182 | const responseBody = JSON.stringify({ 183 | Status: status, 184 | Reason: reason ?? "See the details in CloudWatch Log Stream: " + context.logStreamName, 185 | PhysicalResourceId: context.logStreamName, 186 | StackId: event.StackId, 187 | RequestId: event.RequestId, 188 | LogicalResourceId: event.LogicalResourceId, 189 | NoEcho: false, 190 | Data: {} 191 | }); 192 | await fetch(event.ResponseURL, { 193 | method: "PUT", 194 | body: responseBody, 195 | headers: { 196 | "Content-Type": "", 197 | "Content-Length": responseBody.length.toString() 198 | } 199 | }); 200 | }; 201 | // Annotate the CommonJS export names for ESM import in node: 202 | 0 && (module.exports = { 203 | handler 204 | }); 205 | -------------------------------------------------------------------------------- /test/container-image-build.integ.snapshot/asset.f7fa10e7cd7b9b27f49a4a335f4aa9795fb7e68a665c34ca8bf5711f0aa6aeab/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM public.ecr.aws/nginx/nginx:1.26 2 | # create dummy file to change the size of a image 3 | ARG DUMMY_FILE_SIZE_MB="10" 4 | # RUN fallocate -l ${DUMMY_FILE_SIZE_MB} dummy.img 5 | RUN dd if=/dev/zero of=dummy.img bs=1M count=${DUMMY_FILE_SIZE_MB} 6 | -------------------------------------------------------------------------------- /test/container-image-build.integ.snapshot/cdk.out: -------------------------------------------------------------------------------- 1 | {"version":"20.0.0"} -------------------------------------------------------------------------------- /test/container-image-build.integ.snapshot/integ.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "20.0.0", 3 | "testCases": { 4 | "Test/DefaultTest": { 5 | "stacks": [ 6 | "ContainerImageBuildIntegTest" 7 | ], 8 | "diffAssets": true, 9 | "assertionStack": "TestDefaultTestDeployAssert12909640" 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /test/container-image-build.integ.snapshot/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "20.0.0", 3 | "artifacts": { 4 | "Tree": { 5 | "type": "cdk:tree", 6 | "properties": { 7 | "file": "tree.json" 8 | } 9 | }, 10 | "ContainerImageBuildIntegTest": { 11 | "type": "aws:cloudformation:stack", 12 | "environment": "aws://unknown-account/unknown-region", 13 | "properties": { 14 | "templateFile": "ContainerImageBuildIntegTest.template.json", 15 | "validateOnSynth": false 16 | }, 17 | "metadata": { 18 | "/ContainerImageBuildIntegTest": [ 19 | { 20 | "type": "aws:cdk:asset", 21 | "data": { 22 | "path": "asset.9b19c7d81a94bbc19fe2164453716f4d7f651b21f923c5c264fe80f5a939e1db", 23 | "id": "9b19c7d81a94bbc19fe2164453716f4d7f651b21f923c5c264fe80f5a939e1db", 24 | "packaging": "zip", 25 | "sourceHash": "9b19c7d81a94bbc19fe2164453716f4d7f651b21f923c5c264fe80f5a939e1db", 26 | "s3BucketParameter": "AssetParameters9b19c7d81a94bbc19fe2164453716f4d7f651b21f923c5c264fe80f5a939e1dbS3Bucket7C24A369", 27 | "s3KeyParameter": "AssetParameters9b19c7d81a94bbc19fe2164453716f4d7f651b21f923c5c264fe80f5a939e1dbS3VersionKeyD5434FC9", 28 | "artifactHashParameter": "AssetParameters9b19c7d81a94bbc19fe2164453716f4d7f651b21f923c5c264fe80f5a939e1dbArtifactHashA955B34A" 29 | } 30 | }, 31 | { 32 | "type": "aws:cdk:asset", 33 | "data": { 34 | "path": "asset.f7fa10e7cd7b9b27f49a4a335f4aa9795fb7e68a665c34ca8bf5711f0aa6aeab", 35 | "id": "f7fa10e7cd7b9b27f49a4a335f4aa9795fb7e68a665c34ca8bf5711f0aa6aeab", 36 | "packaging": "zip", 37 | "sourceHash": "f7fa10e7cd7b9b27f49a4a335f4aa9795fb7e68a665c34ca8bf5711f0aa6aeab", 38 | "s3BucketParameter": "AssetParametersf7fa10e7cd7b9b27f49a4a335f4aa9795fb7e68a665c34ca8bf5711f0aa6aeabS3Bucket1FF22598", 39 | "s3KeyParameter": "AssetParametersf7fa10e7cd7b9b27f49a4a335f4aa9795fb7e68a665c34ca8bf5711f0aa6aeabS3VersionKey681A3270", 40 | "artifactHashParameter": "AssetParametersf7fa10e7cd7b9b27f49a4a335f4aa9795fb7e68a665c34ca8bf5711f0aa6aeabArtifactHashC9CEFA00" 41 | } 42 | } 43 | ], 44 | "/ContainerImageBuildIntegTest/VpcV2/Resource": [ 45 | { 46 | "type": "aws:cdk:logicalId", 47 | "data": "VpcV257066EE3" 48 | } 49 | ], 50 | "/ContainerImageBuildIntegTest/VpcV2/PublicSubnet1/Subnet": [ 51 | { 52 | "type": "aws:cdk:logicalId", 53 | "data": "VpcV2PublicSubnet1SubnetD67FC535" 54 | } 55 | ], 56 | "/ContainerImageBuildIntegTest/VpcV2/PublicSubnet1/RouteTable": [ 57 | { 58 | "type": "aws:cdk:logicalId", 59 | "data": "VpcV2PublicSubnet1RouteTable6094F526" 60 | } 61 | ], 62 | "/ContainerImageBuildIntegTest/VpcV2/PublicSubnet1/RouteTableAssociation": [ 63 | { 64 | "type": "aws:cdk:logicalId", 65 | "data": "VpcV2PublicSubnet1RouteTableAssociation7CEECFFF" 66 | } 67 | ], 68 | "/ContainerImageBuildIntegTest/VpcV2/PublicSubnet1/DefaultRoute": [ 69 | { 70 | "type": "aws:cdk:logicalId", 71 | "data": "VpcV2PublicSubnet1DefaultRoute00753B94" 72 | } 73 | ], 74 | "/ContainerImageBuildIntegTest/VpcV2/PublicSubnet1/EIP": [ 75 | { 76 | "type": "aws:cdk:logicalId", 77 | "data": "VpcV2PublicSubnet1EIPCB084C48" 78 | } 79 | ], 80 | "/ContainerImageBuildIntegTest/VpcV2/PublicSubnet1/NATGateway": [ 81 | { 82 | "type": "aws:cdk:logicalId", 83 | "data": "VpcV2PublicSubnet1NATGatewayD04F745E" 84 | } 85 | ], 86 | "/ContainerImageBuildIntegTest/VpcV2/PublicSubnet2/Subnet": [ 87 | { 88 | "type": "aws:cdk:logicalId", 89 | "data": "VpcV2PublicSubnet2Subnet63F50919" 90 | } 91 | ], 92 | "/ContainerImageBuildIntegTest/VpcV2/PublicSubnet2/RouteTable": [ 93 | { 94 | "type": "aws:cdk:logicalId", 95 | "data": "VpcV2PublicSubnet2RouteTable4FB96B9F" 96 | } 97 | ], 98 | "/ContainerImageBuildIntegTest/VpcV2/PublicSubnet2/RouteTableAssociation": [ 99 | { 100 | "type": "aws:cdk:logicalId", 101 | "data": "VpcV2PublicSubnet2RouteTableAssociationE75A579E" 102 | } 103 | ], 104 | "/ContainerImageBuildIntegTest/VpcV2/PublicSubnet2/DefaultRoute": [ 105 | { 106 | "type": "aws:cdk:logicalId", 107 | "data": "VpcV2PublicSubnet2DefaultRoute9078EB37" 108 | } 109 | ], 110 | "/ContainerImageBuildIntegTest/VpcV2/PrivateSubnet1/Subnet": [ 111 | { 112 | "type": "aws:cdk:logicalId", 113 | "data": "VpcV2PrivateSubnet1Subnet10A485FB" 114 | } 115 | ], 116 | "/ContainerImageBuildIntegTest/VpcV2/PrivateSubnet1/RouteTable": [ 117 | { 118 | "type": "aws:cdk:logicalId", 119 | "data": "VpcV2PrivateSubnet1RouteTable7476B1CF" 120 | } 121 | ], 122 | "/ContainerImageBuildIntegTest/VpcV2/PrivateSubnet1/RouteTableAssociation": [ 123 | { 124 | "type": "aws:cdk:logicalId", 125 | "data": "VpcV2PrivateSubnet1RouteTableAssociation52F79D41" 126 | } 127 | ], 128 | "/ContainerImageBuildIntegTest/VpcV2/PrivateSubnet1/DefaultRoute": [ 129 | { 130 | "type": "aws:cdk:logicalId", 131 | "data": "VpcV2PrivateSubnet1DefaultRoute5D002E26" 132 | } 133 | ], 134 | "/ContainerImageBuildIntegTest/VpcV2/PrivateSubnet2/Subnet": [ 135 | { 136 | "type": "aws:cdk:logicalId", 137 | "data": "VpcV2PrivateSubnet2Subnet038DDA50" 138 | } 139 | ], 140 | "/ContainerImageBuildIntegTest/VpcV2/PrivateSubnet2/RouteTable": [ 141 | { 142 | "type": "aws:cdk:logicalId", 143 | "data": "VpcV2PrivateSubnet2RouteTable7B419F52" 144 | } 145 | ], 146 | "/ContainerImageBuildIntegTest/VpcV2/PrivateSubnet2/RouteTableAssociation": [ 147 | { 148 | "type": "aws:cdk:logicalId", 149 | "data": "VpcV2PrivateSubnet2RouteTableAssociationAA17A176" 150 | } 151 | ], 152 | "/ContainerImageBuildIntegTest/VpcV2/PrivateSubnet2/DefaultRoute": [ 153 | { 154 | "type": "aws:cdk:logicalId", 155 | "data": "VpcV2PrivateSubnet2DefaultRouteA0C8D70C" 156 | } 157 | ], 158 | "/ContainerImageBuildIntegTest/VpcV2/IGW": [ 159 | { 160 | "type": "aws:cdk:logicalId", 161 | "data": "VpcV2IGWD1C41C9C" 162 | } 163 | ], 164 | "/ContainerImageBuildIntegTest/VpcV2/VPCGW": [ 165 | { 166 | "type": "aws:cdk:logicalId", 167 | "data": "VpcV2VPCGW167F37E8" 168 | } 169 | ], 170 | "/ContainerImageBuildIntegTest/Build/Repository/Resource": [ 171 | { 172 | "type": "aws:cdk:logicalId", 173 | "data": "BuildRepository4E6D17C2" 174 | } 175 | ], 176 | "/ContainerImageBuildIntegTest/Build/Resource/Default": [ 177 | { 178 | "type": "aws:cdk:logicalId", 179 | "data": "Build45A36621" 180 | } 181 | ], 182 | "/ContainerImageBuildIntegTest/DeployTimeBuildCustomResourceHandlerdb740fd554364a848a09e6dfcd01f4f3/ServiceRole/Resource": [ 183 | { 184 | "type": "aws:cdk:logicalId", 185 | "data": "DeployTimeBuildCustomResourceHandlerdb740fd554364a848a09e6dfcd01f4f3ServiceRoleB008BAA4" 186 | } 187 | ], 188 | "/ContainerImageBuildIntegTest/DeployTimeBuildCustomResourceHandlerdb740fd554364a848a09e6dfcd01f4f3/ServiceRole/DefaultPolicy/Resource": [ 189 | { 190 | "type": "aws:cdk:logicalId", 191 | "data": "DeployTimeBuildCustomResourceHandlerdb740fd554364a848a09e6dfcd01f4f3ServiceRoleDefaultPolicyFECC51DC" 192 | } 193 | ], 194 | "/ContainerImageBuildIntegTest/DeployTimeBuildCustomResourceHandlerdb740fd554364a848a09e6dfcd01f4f3/Resource": [ 195 | { 196 | "type": "aws:cdk:logicalId", 197 | "data": "DeployTimeBuildCustomResourceHandlerdb740fd554364a848a09e6dfcd01f4f306AEFF37" 198 | } 199 | ], 200 | "/ContainerImageBuildIntegTest/AssetParameters/9b19c7d81a94bbc19fe2164453716f4d7f651b21f923c5c264fe80f5a939e1db/S3Bucket": [ 201 | { 202 | "type": "aws:cdk:logicalId", 203 | "data": "AssetParameters9b19c7d81a94bbc19fe2164453716f4d7f651b21f923c5c264fe80f5a939e1dbS3Bucket7C24A369" 204 | } 205 | ], 206 | "/ContainerImageBuildIntegTest/AssetParameters/9b19c7d81a94bbc19fe2164453716f4d7f651b21f923c5c264fe80f5a939e1db/S3VersionKey": [ 207 | { 208 | "type": "aws:cdk:logicalId", 209 | "data": "AssetParameters9b19c7d81a94bbc19fe2164453716f4d7f651b21f923c5c264fe80f5a939e1dbS3VersionKeyD5434FC9" 210 | } 211 | ], 212 | "/ContainerImageBuildIntegTest/AssetParameters/9b19c7d81a94bbc19fe2164453716f4d7f651b21f923c5c264fe80f5a939e1db/ArtifactHash": [ 213 | { 214 | "type": "aws:cdk:logicalId", 215 | "data": "AssetParameters9b19c7d81a94bbc19fe2164453716f4d7f651b21f923c5c264fe80f5a939e1dbArtifactHashA955B34A" 216 | } 217 | ], 218 | "/ContainerImageBuildIntegTest/AssetParameters/f7fa10e7cd7b9b27f49a4a335f4aa9795fb7e68a665c34ca8bf5711f0aa6aeab/S3Bucket": [ 219 | { 220 | "type": "aws:cdk:logicalId", 221 | "data": "AssetParametersf7fa10e7cd7b9b27f49a4a335f4aa9795fb7e68a665c34ca8bf5711f0aa6aeabS3Bucket1FF22598" 222 | } 223 | ], 224 | "/ContainerImageBuildIntegTest/AssetParameters/f7fa10e7cd7b9b27f49a4a335f4aa9795fb7e68a665c34ca8bf5711f0aa6aeab/S3VersionKey": [ 225 | { 226 | "type": "aws:cdk:logicalId", 227 | "data": "AssetParametersf7fa10e7cd7b9b27f49a4a335f4aa9795fb7e68a665c34ca8bf5711f0aa6aeabS3VersionKey681A3270" 228 | } 229 | ], 230 | "/ContainerImageBuildIntegTest/AssetParameters/f7fa10e7cd7b9b27f49a4a335f4aa9795fb7e68a665c34ca8bf5711f0aa6aeab/ArtifactHash": [ 231 | { 232 | "type": "aws:cdk:logicalId", 233 | "data": "AssetParametersf7fa10e7cd7b9b27f49a4a335f4aa9795fb7e68a665c34ca8bf5711f0aa6aeabArtifactHashC9CEFA00" 234 | } 235 | ], 236 | "/ContainerImageBuildIntegTest/ContainerImageBuildAmd64e83729feb1564e709bec452b15847a30amd64/Role/Resource": [ 237 | { 238 | "type": "aws:cdk:logicalId", 239 | "data": "ContainerImageBuildAmd64e83729feb1564e709bec452b15847a30amd64RoleD95F32B9" 240 | } 241 | ], 242 | "/ContainerImageBuildIntegTest/ContainerImageBuildAmd64e83729feb1564e709bec452b15847a30amd64/Role/DefaultPolicy/Resource": [ 243 | { 244 | "type": "aws:cdk:logicalId", 245 | "data": "ContainerImageBuildAmd64e83729feb1564e709bec452b15847a30amd64RoleDefaultPolicyBD233B55" 246 | } 247 | ], 248 | "/ContainerImageBuildIntegTest/ContainerImageBuildAmd64e83729feb1564e709bec452b15847a30amd64/Resource": [ 249 | { 250 | "type": "aws:cdk:logicalId", 251 | "data": "ContainerImageBuildAmd64e83729feb1564e709bec452b15847a30amd6491AAA9B2" 252 | } 253 | ], 254 | "/ContainerImageBuildIntegTest/BuildArm/Resource/Default": [ 255 | { 256 | "type": "aws:cdk:logicalId", 257 | "data": "BuildArmF8ABF624" 258 | } 259 | ], 260 | "/ContainerImageBuildIntegTest/ContainerImageBuildArm64e83729feb1564e709bec452b15847a30amd64/Role/Resource": [ 261 | { 262 | "type": "aws:cdk:logicalId", 263 | "data": "ContainerImageBuildArm64e83729feb1564e709bec452b15847a30amd64RoleC5F7BBFE" 264 | } 265 | ], 266 | "/ContainerImageBuildIntegTest/ContainerImageBuildArm64e83729feb1564e709bec452b15847a30amd64/Role/DefaultPolicy/Resource": [ 267 | { 268 | "type": "aws:cdk:logicalId", 269 | "data": "ContainerImageBuildArm64e83729feb1564e709bec452b15847a30amd64RoleDefaultPolicy2316728F" 270 | } 271 | ], 272 | "/ContainerImageBuildIntegTest/ContainerImageBuildArm64e83729feb1564e709bec452b15847a30amd64/Resource": [ 273 | { 274 | "type": "aws:cdk:logicalId", 275 | "data": "ContainerImageBuildArm64e83729feb1564e709bec452b15847a30amd64C13E3549" 276 | } 277 | ], 278 | "/ContainerImageBuildIntegTest/Function/ServiceRole/Resource": [ 279 | { 280 | "type": "aws:cdk:logicalId", 281 | "data": "FunctionServiceRole675BB04A" 282 | } 283 | ], 284 | "/ContainerImageBuildIntegTest/Function/Resource": [ 285 | { 286 | "type": "aws:cdk:logicalId", 287 | "data": "Function76856677" 288 | } 289 | ], 290 | "/ContainerImageBuildIntegTest/Cluster/Resource": [ 291 | { 292 | "type": "aws:cdk:logicalId", 293 | "data": "ClusterEB0386A7" 294 | } 295 | ], 296 | "/ContainerImageBuildIntegTest/TaskDefinition/TaskRole/Resource": [ 297 | { 298 | "type": "aws:cdk:logicalId", 299 | "data": "TaskDefinitionTaskRoleFD40A61D" 300 | } 301 | ], 302 | "/ContainerImageBuildIntegTest/TaskDefinition/Resource": [ 303 | { 304 | "type": "aws:cdk:logicalId", 305 | "data": "TaskDefinitionB36D86D9" 306 | } 307 | ], 308 | "/ContainerImageBuildIntegTest/TaskDefinition/main/LogGroup/Resource": [ 309 | { 310 | "type": "aws:cdk:logicalId", 311 | "data": "TaskDefinitionmainLogGroup2F2AC027" 312 | } 313 | ], 314 | "/ContainerImageBuildIntegTest/TaskDefinition/ExecutionRole/Resource": [ 315 | { 316 | "type": "aws:cdk:logicalId", 317 | "data": "TaskDefinitionExecutionRole8D61C2FB" 318 | } 319 | ], 320 | "/ContainerImageBuildIntegTest/TaskDefinition/ExecutionRole/DefaultPolicy/Resource": [ 321 | { 322 | "type": "aws:cdk:logicalId", 323 | "data": "TaskDefinitionExecutionRoleDefaultPolicy1F3406F5" 324 | } 325 | ], 326 | "/ContainerImageBuildIntegTest/BuildVpc/Repository/Resource": [ 327 | { 328 | "type": "aws:cdk:logicalId", 329 | "data": "BuildVpcRepositoryC4EB5D59" 330 | } 331 | ], 332 | "/ContainerImageBuildIntegTest/BuildVpc/Resource/Default": [ 333 | { 334 | "type": "aws:cdk:logicalId", 335 | "data": "BuildVpc1895B133" 336 | } 337 | ], 338 | "/ContainerImageBuildIntegTest/ContainerImageBuildAmd64e83729feb1564e709bec452b15847a30c8a085f75ccbc05f5149859bcf147b49eadd8ac37damd64/Role/Resource": [ 339 | { 340 | "type": "aws:cdk:logicalId", 341 | "data": "ContainerImageBuildAmd64e83729feb1564e709bec452b15847a30c8a085f75ccbc05f5149859bcf147b49eadd8ac37damd64Role7FBDDED1" 342 | } 343 | ], 344 | "/ContainerImageBuildIntegTest/ContainerImageBuildAmd64e83729feb1564e709bec452b15847a30c8a085f75ccbc05f5149859bcf147b49eadd8ac37damd64/Role/DefaultPolicy/Resource": [ 345 | { 346 | "type": "aws:cdk:logicalId", 347 | "data": "ContainerImageBuildAmd64e83729feb1564e709bec452b15847a30c8a085f75ccbc05f5149859bcf147b49eadd8ac37damd64RoleDefaultPolicyF7E07B6C" 348 | } 349 | ], 350 | "/ContainerImageBuildIntegTest/ContainerImageBuildAmd64e83729feb1564e709bec452b15847a30c8a085f75ccbc05f5149859bcf147b49eadd8ac37damd64/SecurityGroup/Resource": [ 351 | { 352 | "type": "aws:cdk:logicalId", 353 | "data": "ContainerImageBuildAmd64e83729feb1564e709bec452b15847a30c8a085f75ccbc05f5149859bcf147b49eadd8ac37damd64SecurityGroup48FE16CC" 354 | } 355 | ], 356 | "/ContainerImageBuildIntegTest/ContainerImageBuildAmd64e83729feb1564e709bec452b15847a30c8a085f75ccbc05f5149859bcf147b49eadd8ac37damd64/Resource": [ 357 | { 358 | "type": "aws:cdk:logicalId", 359 | "data": "ContainerImageBuildAmd64e83729feb1564e709bec452b15847a30c8a085f75ccbc05f5149859bcf147b49eadd8ac37damd64C6D1C187" 360 | } 361 | ], 362 | "/ContainerImageBuildIntegTest/ContainerImageBuildAmd64e83729feb1564e709bec452b15847a30c8a085f75ccbc05f5149859bcf147b49eadd8ac37damd64/PolicyDocument/Resource": [ 363 | { 364 | "type": "aws:cdk:logicalId", 365 | "data": "ContainerImageBuildAmd64e83729feb1564e709bec452b15847a30c8a085f75ccbc05f5149859bcf147b49eadd8ac37damd64PolicyDocument194E3D91" 366 | } 367 | ] 368 | }, 369 | "displayName": "ContainerImageBuildIntegTest" 370 | }, 371 | "TestDefaultTestDeployAssert12909640": { 372 | "type": "aws:cloudformation:stack", 373 | "environment": "aws://unknown-account/unknown-region", 374 | "properties": { 375 | "templateFile": "TestDefaultTestDeployAssert12909640.template.json", 376 | "validateOnSynth": false 377 | }, 378 | "displayName": "Test/DefaultTest/DeployAssert" 379 | } 380 | } 381 | } -------------------------------------------------------------------------------- /test/hello.test.ts: -------------------------------------------------------------------------------- 1 | // import { NodejsBuild } from '../src'; 2 | 3 | test('hello', () => { 4 | }); -------------------------------------------------------------------------------- /test/integ.container-image-build.ts: -------------------------------------------------------------------------------- 1 | import { IntegTest } from '@aws-cdk/integ-tests-alpha'; 2 | import { Stack, StackProps, App } from 'aws-cdk-lib'; 3 | import { Vpc } from 'aws-cdk-lib/aws-ec2'; 4 | import { Platform } from 'aws-cdk-lib/aws-ecr-assets'; 5 | import { Cluster, FargateTaskDefinition, CpuArchitecture, AwsLogDriver } from 'aws-cdk-lib/aws-ecs'; 6 | import { DockerImageFunction } from 'aws-cdk-lib/aws-lambda'; 7 | import { Construct } from 'constructs'; 8 | import { ContainerImageBuild } from '../src'; 9 | 10 | const app = new App(); 11 | 12 | class TestStack extends Stack { 13 | constructor(scope: Construct, id: string, props: StackProps = {}) { 14 | super(scope, id, props); 15 | 16 | const vpc = new Vpc(this, 'VpcV2', { 17 | natGateways: 1, 18 | }); 19 | 20 | const image = new ContainerImageBuild(this, 'Build', { directory: '../example/example-image', buildArgs: { DUMMY_FILE_SIZE_MB: '15' } }); 21 | const armImage = new ContainerImageBuild(this, 'BuildArm', { 22 | directory: '../example/example-image', 23 | platform: Platform.LINUX_ARM64, 24 | repository: image.repository, 25 | zstdCompression: true, 26 | }); 27 | new DockerImageFunction(this, 'Function', { 28 | code: image.toLambdaDockerImageCode(), 29 | }); 30 | new Cluster(this, 'Cluster', { 31 | vpc, 32 | }); 33 | new FargateTaskDefinition(this, 'TaskDefinition', { runtimePlatform: { cpuArchitecture: CpuArchitecture.ARM64 } }).addContainer('main', { 34 | image: armImage.toEcsDockerImageCode(), 35 | logging: new AwsLogDriver({ streamPrefix: 'main' }), 36 | }); 37 | 38 | const build = new ContainerImageBuild(this, 'BuildVpc', { 39 | directory: '../example/example-image', 40 | vpc, 41 | }); 42 | // build must run after NAT gateways are configured 43 | build.node.addDependency(vpc); 44 | } 45 | } 46 | 47 | const stack = new TestStack(app, 'ContainerImageBuildIntegTest'); 48 | 49 | new IntegTest(app, 'Test', { 50 | testCases: [stack], 51 | diffAssets: true, 52 | }); 53 | -------------------------------------------------------------------------------- /test/integ.nodejs-build.ts: -------------------------------------------------------------------------------- 1 | import { IntegTest } from '@aws-cdk/integ-tests-alpha'; 2 | import { Stack, StackProps, App } from 'aws-cdk-lib'; 3 | import { BlockPublicAccess, Bucket, BucketEncryption } from 'aws-cdk-lib/aws-s3'; 4 | import { Construct } from 'constructs'; 5 | import { NodejsBuild } from '../src/nodejs-build'; 6 | 7 | const app = new App(); 8 | 9 | class TestStack extends Stack { 10 | constructor(scope: Construct, id: string, props: StackProps = {}) { 11 | super(scope, id, props); 12 | 13 | const dstBucket = new Bucket(this, 'Destination', { 14 | blockPublicAccess: BlockPublicAccess.BLOCK_ALL, 15 | encryption: BucketEncryption.S3_MANAGED, 16 | }); 17 | const dstPath = '/'; 18 | 19 | new NodejsBuild(this, 'ExampleBuild', { 20 | assets: [ 21 | { 22 | path: '../example/example-app', 23 | exclude: ['dist'], 24 | }, 25 | ], 26 | destinationBucket: dstBucket, 27 | destinationKeyPrefix: dstPath, 28 | outputSourceDirectory: 'dist', 29 | buildCommands: ['npm ci', 'npm run build'], 30 | buildEnvironment: { 31 | VITE_SOME_TOKEN: dstBucket.bucketName, 32 | }, 33 | nodejsVersion: 20, 34 | outputEnvFile: true, 35 | }); 36 | } 37 | } 38 | 39 | const stack = new TestStack(app, 'NodejsBuildIntegTest'); 40 | 41 | new IntegTest(app, 'Test', { 42 | testCases: [stack], 43 | diffAssets: true, 44 | }); 45 | -------------------------------------------------------------------------------- /test/integ.soci-index-build.ts: -------------------------------------------------------------------------------- 1 | import { IntegTest } from '@aws-cdk/integ-tests-alpha'; 2 | import { Stack, StackProps, App } from 'aws-cdk-lib'; 3 | import { DockerImageAsset } from 'aws-cdk-lib/aws-ecr-assets'; 4 | import { Construct } from 'constructs'; 5 | import { SociIndexBuild } from '../src'; 6 | 7 | const app = new App(); 8 | 9 | class TestStack extends Stack { 10 | constructor(scope: Construct, id: string, props: StackProps = {}) { 11 | super(scope, id, props); 12 | 13 | // make sure we can build more than one indices. 14 | { 15 | const parent = new Construct(this, 'Image1'); 16 | const asset = new DockerImageAsset(parent, 'Image', { 17 | directory: '../example/example-image', 18 | buildArgs: { DUMMY_FILE_SIZE_MB: '10' }, 19 | }); 20 | new SociIndexBuild(parent, 'Index', { imageTag: asset.assetHash, repository: asset.repository }); 21 | } 22 | 23 | { 24 | const parent = new Construct(this, 'Image2'); 25 | const asset = new DockerImageAsset(parent, 'Image', { 26 | directory: '../example/example-image', 27 | buildArgs: { DUMMY_FILE_SIZE_MB: '500' }, 28 | }); 29 | SociIndexBuild.fromDockerImageAsset(parent, 'Index', asset); 30 | } 31 | } 32 | } 33 | 34 | const stack = new TestStack(app, 'SociIndexBuildIntegTest'); 35 | 36 | new IntegTest(app, 'Test', { 37 | testCases: [stack], 38 | diffAssets: true, 39 | }); 40 | -------------------------------------------------------------------------------- /test/nodejs-build.integ.snapshot/TestDefaultTestDeployAssert12909640.template.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /test/nodejs-build.integ.snapshot/asset.1d3b5490cd99feddeb525a62c046988997469f2a765d0f12b43cff9d87a284fa.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tmokmss/deploy-time-build/b9abc255766d58fe2d8dcc98adbd9b09830cc42b/test/nodejs-build.integ.snapshot/asset.1d3b5490cd99feddeb525a62c046988997469f2a765d0f12b43cff9d87a284fa.zip -------------------------------------------------------------------------------- /test/nodejs-build.integ.snapshot/asset.72f05c779a8bc73c6ec86f6eafc720508792e7526696d3ae45a7fddcfc473c9d/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | !tsconfig.json -------------------------------------------------------------------------------- /test/nodejs-build.integ.snapshot/asset.72f05c779a8bc73c6ec86f6eafc720508792e7526696d3ae45a7fddcfc473c9d/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite App 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /test/nodejs-build.integ.snapshot/asset.72f05c779a8bc73c6ec86f6eafc720508792e7526696d3ae45a7fddcfc473c9d/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example-app", 3 | "private": true, 4 | "version": "0.0.0", 5 | "scripts": { 6 | "dev": "vite", 7 | "build": "tsc && vite build", 8 | "preview": "vite preview" 9 | }, 10 | "dependencies": { 11 | "react": "^18.0.0", 12 | "react-dom": "^18.0.0" 13 | }, 14 | "devDependencies": { 15 | "@types/react": "^18.0.0", 16 | "@types/react-dom": "^18.0.0", 17 | "@vitejs/plugin-react": "^1.3.0", 18 | "typescript": "^4.6.3", 19 | "vite": "^2.9.9" 20 | } 21 | } -------------------------------------------------------------------------------- /test/nodejs-build.integ.snapshot/asset.72f05c779a8bc73c6ec86f6eafc720508792e7526696d3ae45a7fddcfc473c9d/src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | height: 40vmin; 7 | pointer-events: none; 8 | } 9 | 10 | @media (prefers-reduced-motion: no-preference) { 11 | .App-logo { 12 | animation: App-logo-spin infinite 20s linear; 13 | } 14 | } 15 | 16 | .App-header { 17 | background-color: #282c34; 18 | min-height: 100vh; 19 | display: flex; 20 | flex-direction: column; 21 | align-items: center; 22 | justify-content: center; 23 | font-size: calc(10px + 2vmin); 24 | color: white; 25 | } 26 | 27 | .App-link { 28 | color: #61dafb; 29 | } 30 | 31 | @keyframes App-logo-spin { 32 | from { 33 | transform: rotate(0deg); 34 | } 35 | to { 36 | transform: rotate(360deg); 37 | } 38 | } 39 | 40 | button { 41 | font-size: calc(10px + 2vmin); 42 | } 43 | -------------------------------------------------------------------------------- /test/nodejs-build.integ.snapshot/asset.72f05c779a8bc73c6ec86f6eafc720508792e7526696d3ae45a7fddcfc473c9d/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react' 2 | import logo from './logo.svg' 3 | import './App.css' 4 | 5 | function App() { 6 | const [count, setCount] = useState(0) 7 | 8 | return ( 9 |
10 |
11 | logo 12 |

Hello Vite + React!

13 |

14 | 17 |

18 |

19 | Your API endpoint: {import.meta.env.VITE_API_ENDPOINT ?? 'undefined'} 20 |

21 |

22 | 28 | Learn React 29 | 30 | {' | '} 31 | 37 | Vite Docs 38 | 39 |

40 |
41 |
42 | ) 43 | } 44 | 45 | export default App 46 | -------------------------------------------------------------------------------- /test/nodejs-build.integ.snapshot/asset.72f05c779a8bc73c6ec86f6eafc720508792e7526696d3ae45a7fddcfc473c9d/src/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /test/nodejs-build.integ.snapshot/asset.72f05c779a8bc73c6ec86f6eafc720508792e7526696d3ae45a7fddcfc473c9d/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /test/nodejs-build.integ.snapshot/asset.72f05c779a8bc73c6ec86f6eafc720508792e7526696d3ae45a7fddcfc473c9d/src/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /test/nodejs-build.integ.snapshot/asset.72f05c779a8bc73c6ec86f6eafc720508792e7526696d3ae45a7fddcfc473c9d/src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom/client' 3 | import App from './App' 4 | import './index.css' 5 | 6 | ReactDOM.createRoot(document.getElementById('root')!).render( 7 | 8 | 9 | 10 | ) 11 | -------------------------------------------------------------------------------- /test/nodejs-build.integ.snapshot/asset.72f05c779a8bc73c6ec86f6eafc720508792e7526696d3ae45a7fddcfc473c9d/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /test/nodejs-build.integ.snapshot/asset.72f05c779a8bc73c6ec86f6eafc720508792e7526696d3ae45a7fddcfc473c9d/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 6 | "allowJs": false, 7 | "skipLibCheck": true, 8 | "esModuleInterop": false, 9 | "allowSyntheticDefaultImports": true, 10 | "strict": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "module": "ESNext", 13 | "moduleResolution": "Node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "react-jsx" 18 | }, 19 | "include": ["src"], 20 | "references": [{ "path": "./tsconfig.node.json" }] 21 | } 22 | -------------------------------------------------------------------------------- /test/nodejs-build.integ.snapshot/asset.72f05c779a8bc73c6ec86f6eafc720508792e7526696d3ae45a7fddcfc473c9d/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "esnext", 5 | "moduleResolution": "node" 6 | }, 7 | "include": ["vite.config.ts"] 8 | } 9 | -------------------------------------------------------------------------------- /test/nodejs-build.integ.snapshot/asset.72f05c779a8bc73c6ec86f6eafc720508792e7526696d3ae45a7fddcfc473c9d/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()] 7 | }) 8 | -------------------------------------------------------------------------------- /test/nodejs-build.integ.snapshot/asset.9b19c7d81a94bbc19fe2164453716f4d7f651b21f923c5c264fe80f5a939e1db/index.js: -------------------------------------------------------------------------------- 1 | var __create = Object.create; 2 | var __defProp = Object.defineProperty; 3 | var __getOwnPropDesc = Object.getOwnPropertyDescriptor; 4 | var __getOwnPropNames = Object.getOwnPropertyNames; 5 | var __getProtoOf = Object.getPrototypeOf; 6 | var __hasOwnProp = Object.prototype.hasOwnProperty; 7 | var __export = (target, all) => { 8 | for (var name in all) 9 | __defProp(target, name, { get: all[name], enumerable: true }); 10 | }; 11 | var __copyProps = (to, from, except, desc) => { 12 | if (from && typeof from === "object" || typeof from === "function") { 13 | for (let key of __getOwnPropNames(from)) 14 | if (!__hasOwnProp.call(to, key) && key !== except) 15 | __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); 16 | } 17 | return to; 18 | }; 19 | var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod)); 20 | var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); 21 | 22 | // index.ts 23 | var trigger_codebuild_exports = {}; 24 | __export(trigger_codebuild_exports, { 25 | handler: () => handler 26 | }); 27 | module.exports = __toCommonJS(trigger_codebuild_exports); 28 | var import_client_codebuild = require("@aws-sdk/client-codebuild"); 29 | var import_crypto = __toESM(require("crypto")); 30 | var cb = new import_client_codebuild.CodeBuildClient({}); 31 | var handler = async (event, context) => { 32 | console.log(JSON.stringify(event)); 33 | try { 34 | if (event.RequestType == "Create" || event.RequestType == "Update") { 35 | const props = event.ResourceProperties; 36 | const commonEnvironments = [ 37 | { 38 | name: "responseURL", 39 | value: event.ResponseURL 40 | }, 41 | { 42 | name: "stackId", 43 | value: event.StackId 44 | }, 45 | { 46 | name: "requestId", 47 | value: event.RequestId 48 | }, 49 | { 50 | name: "logicalResourceId", 51 | value: event.LogicalResourceId 52 | } 53 | ]; 54 | const newPhysicalId = import_crypto.default.randomBytes(16).toString("hex"); 55 | let command; 56 | switch (props.type) { 57 | case "NodejsBuild": 58 | command = new import_client_codebuild.StartBuildCommand({ 59 | projectName: props.codeBuildProjectName, 60 | environmentVariablesOverride: [ 61 | ...commonEnvironments, 62 | { 63 | name: "input", 64 | value: JSON.stringify(props.sources.map((source) => ({ 65 | assetUrl: `s3://${source.sourceBucketName}/${source.sourceObjectKey}`, 66 | extractPath: source.extractPath, 67 | commands: (source.commands ?? []).join(" && ") 68 | }))) 69 | }, 70 | { 71 | name: "buildCommands", 72 | value: props.buildCommands.join(" && ") 73 | }, 74 | { 75 | name: "destinationBucketName", 76 | value: props.destinationBucketName 77 | }, 78 | { 79 | name: "destinationObjectKey", 80 | value: `${newPhysicalId}.zip` 81 | }, 82 | { 83 | name: "workingDirectory", 84 | value: props.workingDirectory 85 | }, 86 | { 87 | name: "outputSourceDirectory", 88 | value: props.outputSourceDirectory 89 | }, 90 | { 91 | name: "projectName", 92 | value: props.codeBuildProjectName 93 | }, 94 | { 95 | name: "outputEnvFile", 96 | value: props.outputEnvFile.toString() 97 | }, 98 | { 99 | name: "envFileKey", 100 | value: `deploy-time-build/${event.StackId.split("/")[1]}/${event.LogicalResourceId}/${newPhysicalId}.env` 101 | }, 102 | { 103 | name: "envNames", 104 | value: Object.keys(props.environment ?? {}).join(",") 105 | }, 106 | ...Object.entries(props.environment ?? {}).map(([name, value]) => ({ 107 | name, 108 | value 109 | })) 110 | ] 111 | }); 112 | break; 113 | case "SociIndexBuild": 114 | command = new import_client_codebuild.StartBuildCommand({ 115 | projectName: props.codeBuildProjectName, 116 | environmentVariablesOverride: [ 117 | ...commonEnvironments, 118 | { 119 | name: "repositoryName", 120 | value: props.repositoryName 121 | }, 122 | { 123 | name: "imageTag", 124 | value: props.imageTag 125 | }, 126 | { 127 | name: "projectName", 128 | value: props.codeBuildProjectName 129 | } 130 | ] 131 | }); 132 | break; 133 | case "ContainerImageBuild": { 134 | const imageTag = props.imageTag ?? `${props.tagPrefix ?? ""}${newPhysicalId}`; 135 | const buildCommand = props.buildCommand.replaceAll("", imageTag); 136 | command = new import_client_codebuild.StartBuildCommand({ 137 | projectName: props.codeBuildProjectName, 138 | environmentVariablesOverride: [ 139 | ...commonEnvironments, 140 | { 141 | name: "repositoryUri", 142 | value: props.repositoryUri 143 | }, 144 | { 145 | name: "repositoryAuthUri", 146 | value: props.repositoryUri.split("/")[0] 147 | }, 148 | { 149 | name: "buildCommand", 150 | value: buildCommand 151 | }, 152 | { 153 | name: "imageTag", 154 | value: imageTag 155 | }, 156 | { 157 | name: "projectName", 158 | value: props.codeBuildProjectName 159 | }, 160 | { 161 | name: "sourceS3Url", 162 | value: props.sourceS3Url 163 | } 164 | ] 165 | }); 166 | break; 167 | } 168 | default: 169 | throw new Error(`invalid event type ${props}}`); 170 | } 171 | const build = await cb.send(command); 172 | } else { 173 | await sendStatus("SUCCESS", event, context); 174 | } 175 | } catch (e) { 176 | console.log(e); 177 | const err = e; 178 | await sendStatus("FAILED", event, context, err.message); 179 | } 180 | }; 181 | var sendStatus = async (status, event, context, reason) => { 182 | const responseBody = JSON.stringify({ 183 | Status: status, 184 | Reason: reason ?? "See the details in CloudWatch Log Stream: " + context.logStreamName, 185 | PhysicalResourceId: context.logStreamName, 186 | StackId: event.StackId, 187 | RequestId: event.RequestId, 188 | LogicalResourceId: event.LogicalResourceId, 189 | NoEcho: false, 190 | Data: {} 191 | }); 192 | await fetch(event.ResponseURL, { 193 | method: "PUT", 194 | body: responseBody, 195 | headers: { 196 | "Content-Type": "", 197 | "Content-Length": responseBody.length.toString() 198 | } 199 | }); 200 | }; 201 | // Annotate the CommonJS export names for ESM import in node: 202 | 0 && (module.exports = { 203 | handler 204 | }); 205 | -------------------------------------------------------------------------------- /test/nodejs-build.integ.snapshot/asset.f98b78092dcdd31f5e6d47489beb5f804d4835ef86a8085d0a2053cb9ae711da/index.py: -------------------------------------------------------------------------------- 1 | import contextlib 2 | import json 3 | import logging 4 | import os 5 | import shutil 6 | import subprocess 7 | import tempfile 8 | from urllib.request import Request, urlopen 9 | from uuid import uuid4 10 | from zipfile import ZipFile 11 | 12 | import boto3 13 | 14 | logger = logging.getLogger() 15 | logger.setLevel(logging.INFO) 16 | 17 | cloudfront = boto3.client('cloudfront') 18 | s3 = boto3.client('s3') 19 | 20 | CFN_SUCCESS = "SUCCESS" 21 | CFN_FAILED = "FAILED" 22 | ENV_KEY_MOUNT_PATH = "MOUNT_PATH" 23 | ENV_KEY_SKIP_CLEANUP = "SKIP_CLEANUP" 24 | 25 | CUSTOM_RESOURCE_OWNER_TAG = "aws-cdk:cr-owned" 26 | 27 | def handler(event, context): 28 | 29 | def cfn_error(message=None): 30 | logger.error("| cfn_error: %s" % message) 31 | cfn_send(event, context, CFN_FAILED, reason=message) 32 | 33 | try: 34 | # We are not logging ResponseURL as this is a pre-signed S3 URL, and could be used to tamper 35 | # with the response CloudFormation sees from this Custom Resource execution. 36 | logger.info({ key:value for (key, value) in event.items() if key != 'ResponseURL'}) 37 | 38 | # cloudformation request type (create/update/delete) 39 | request_type = event['RequestType'] 40 | 41 | # extract resource properties 42 | props = event['ResourceProperties'] 43 | old_props = event.get('OldResourceProperties', {}) 44 | physical_id = event.get('PhysicalResourceId', None) 45 | 46 | try: 47 | source_bucket_names = props['SourceBucketNames'] 48 | source_object_keys = props['SourceObjectKeys'] 49 | source_markers = props.get('SourceMarkers', None) 50 | dest_bucket_name = props['DestinationBucketName'] 51 | dest_bucket_prefix = props.get('DestinationBucketKeyPrefix', '') 52 | retain_on_delete = props.get('RetainOnDelete', "true") == "true" 53 | distribution_id = props.get('DistributionId', '') 54 | user_metadata = props.get('UserMetadata', {}) 55 | system_metadata = props.get('SystemMetadata', {}) 56 | prune = props.get('Prune', 'true').lower() == 'true' 57 | exclude = props.get('Exclude', []) 58 | include = props.get('Include', []) 59 | 60 | # backwards compatibility - if "SourceMarkers" is not specified, 61 | # assume all sources have an empty market map 62 | if source_markers is None: 63 | source_markers = [{} for i in range(len(source_bucket_names))] 64 | 65 | default_distribution_path = dest_bucket_prefix 66 | if not default_distribution_path.endswith("/"): 67 | default_distribution_path += "/" 68 | if not default_distribution_path.startswith("/"): 69 | default_distribution_path = "/" + default_distribution_path 70 | default_distribution_path += "*" 71 | 72 | distribution_paths = props.get('DistributionPaths', [default_distribution_path]) 73 | except KeyError as e: 74 | cfn_error("missing request resource property %s. props: %s" % (str(e), props)) 75 | return 76 | 77 | # treat "/" as if no prefix was specified 78 | if dest_bucket_prefix == "/": 79 | dest_bucket_prefix = "" 80 | 81 | s3_source_zips = list(map(lambda name, key: "s3://%s/%s" % (name, key), source_bucket_names, source_object_keys)) 82 | s3_dest = "s3://%s/%s" % (dest_bucket_name, dest_bucket_prefix) 83 | old_s3_dest = "s3://%s/%s" % (old_props.get("DestinationBucketName", ""), old_props.get("DestinationBucketKeyPrefix", "")) 84 | 85 | 86 | # obviously this is not 87 | if old_s3_dest == "s3:///": 88 | old_s3_dest = None 89 | 90 | logger.info("| s3_dest: %s" % s3_dest) 91 | logger.info("| old_s3_dest: %s" % old_s3_dest) 92 | 93 | # if we are creating a new resource, allocate a physical id for it 94 | # otherwise, we expect physical id to be relayed by cloudformation 95 | if request_type == "Create": 96 | physical_id = "aws.cdk.s3deployment.%s" % str(uuid4()) 97 | else: 98 | if not physical_id: 99 | cfn_error("invalid request: request type is '%s' but 'PhysicalResourceId' is not defined" % request_type) 100 | return 101 | 102 | # delete or create/update (only if "retain_on_delete" is false) 103 | if request_type == "Delete" and not retain_on_delete: 104 | if not bucket_owned(dest_bucket_name, dest_bucket_prefix): 105 | aws_command("s3", "rm", s3_dest, "--recursive") 106 | 107 | # if we are updating without retention and the destination changed, delete first 108 | if request_type == "Update" and not retain_on_delete and old_s3_dest != s3_dest: 109 | if not old_s3_dest: 110 | logger.warn("cannot delete old resource without old resource properties") 111 | return 112 | 113 | aws_command("s3", "rm", old_s3_dest, "--recursive") 114 | 115 | if request_type == "Update" or request_type == "Create": 116 | s3_deploy(s3_source_zips, s3_dest, user_metadata, system_metadata, prune, exclude, include, source_markers) 117 | 118 | if distribution_id: 119 | cloudfront_invalidate(distribution_id, distribution_paths) 120 | 121 | cfn_send(event, context, CFN_SUCCESS, physicalResourceId=physical_id, responseData={ 122 | # Passing through the ARN sequences dependencees on the deployment 123 | 'DestinationBucketArn': props.get('DestinationBucketArn') 124 | }) 125 | except KeyError as e: 126 | cfn_error("invalid request. Missing key %s" % str(e)) 127 | except Exception as e: 128 | logger.exception(e) 129 | cfn_error(str(e)) 130 | 131 | #--------------------------------------------------------------------------------------------------- 132 | # populate all files from s3_source_zips to a destination bucket 133 | def s3_deploy(s3_source_zips, s3_dest, user_metadata, system_metadata, prune, exclude, include, source_markers): 134 | # list lengths are equal 135 | if len(s3_source_zips) != len(source_markers): 136 | raise Exception("'source_markers' and 's3_source_zips' must be the same length") 137 | 138 | # create a temporary working directory in /tmp or if enabled an attached efs volume 139 | if ENV_KEY_MOUNT_PATH in os.environ: 140 | workdir = os.getenv(ENV_KEY_MOUNT_PATH) + "/" + str(uuid4()) 141 | os.mkdir(workdir) 142 | else: 143 | workdir = tempfile.mkdtemp() 144 | 145 | logger.info("| workdir: %s" % workdir) 146 | 147 | # create a directory into which we extract the contents of the zip file 148 | contents_dir=os.path.join(workdir, 'contents') 149 | os.mkdir(contents_dir) 150 | 151 | try: 152 | # download the archive from the source and extract to "contents" 153 | for i in range(len(s3_source_zips)): 154 | s3_source_zip = s3_source_zips[i] 155 | markers = source_markers[i] 156 | 157 | archive=os.path.join(workdir, str(uuid4())) 158 | logger.info("archive: %s" % archive) 159 | aws_command("s3", "cp", s3_source_zip, archive) 160 | logger.info("| extracting archive to: %s\n" % contents_dir) 161 | logger.info("| markers: %s" % markers) 162 | extract_and_replace_markers(archive, contents_dir, markers) 163 | 164 | # sync from "contents" to destination 165 | 166 | s3_command = ["s3", "sync"] 167 | 168 | if prune: 169 | s3_command.append("--delete") 170 | 171 | if exclude: 172 | for filter in exclude: 173 | s3_command.extend(["--exclude", filter]) 174 | 175 | if include: 176 | for filter in include: 177 | s3_command.extend(["--include", filter]) 178 | 179 | s3_command.extend([contents_dir, s3_dest]) 180 | s3_command.extend(create_metadata_args(user_metadata, system_metadata)) 181 | aws_command(*s3_command) 182 | finally: 183 | if not os.getenv(ENV_KEY_SKIP_CLEANUP): 184 | shutil.rmtree(workdir) 185 | 186 | #--------------------------------------------------------------------------------------------------- 187 | # invalidate files in the CloudFront distribution edge caches 188 | def cloudfront_invalidate(distribution_id, distribution_paths): 189 | invalidation_resp = cloudfront.create_invalidation( 190 | DistributionId=distribution_id, 191 | InvalidationBatch={ 192 | 'Paths': { 193 | 'Quantity': len(distribution_paths), 194 | 'Items': distribution_paths 195 | }, 196 | 'CallerReference': str(uuid4()), 197 | }) 198 | # by default, will wait up to 10 minutes 199 | cloudfront.get_waiter('invalidation_completed').wait( 200 | DistributionId=distribution_id, 201 | Id=invalidation_resp['Invalidation']['Id']) 202 | 203 | #--------------------------------------------------------------------------------------------------- 204 | # set metadata 205 | def create_metadata_args(raw_user_metadata, raw_system_metadata): 206 | if len(raw_user_metadata) == 0 and len(raw_system_metadata) == 0: 207 | return [] 208 | 209 | format_system_metadata_key = lambda k: k.lower() 210 | format_user_metadata_key = lambda k: k.lower() 211 | 212 | system_metadata = { format_system_metadata_key(k): v for k, v in raw_system_metadata.items() } 213 | user_metadata = { format_user_metadata_key(k): v for k, v in raw_user_metadata.items() } 214 | 215 | flatten = lambda l: [item for sublist in l for item in sublist] 216 | system_args = flatten([[f"--{k}", v] for k, v in system_metadata.items()]) 217 | user_args = ["--metadata", json.dumps(user_metadata, separators=(',', ':'))] if len(user_metadata) > 0 else [] 218 | 219 | return system_args + user_args + ["--metadata-directive", "REPLACE"] 220 | 221 | #--------------------------------------------------------------------------------------------------- 222 | # executes an "aws" cli command 223 | def aws_command(*args): 224 | aws="/opt/awscli/aws" # from AwsCliLayer 225 | logger.info("| aws %s" % ' '.join(args)) 226 | subprocess.check_call([aws] + list(args)) 227 | 228 | #--------------------------------------------------------------------------------------------------- 229 | # sends a response to cloudformation 230 | def cfn_send(event, context, responseStatus, responseData={}, physicalResourceId=None, noEcho=False, reason=None): 231 | 232 | responseUrl = event['ResponseURL'] 233 | logger.info(responseUrl) 234 | 235 | responseBody = {} 236 | responseBody['Status'] = responseStatus 237 | responseBody['Reason'] = reason or ('See the details in CloudWatch Log Stream: ' + context.log_stream_name) 238 | responseBody['PhysicalResourceId'] = physicalResourceId or context.log_stream_name 239 | responseBody['StackId'] = event['StackId'] 240 | responseBody['RequestId'] = event['RequestId'] 241 | responseBody['LogicalResourceId'] = event['LogicalResourceId'] 242 | responseBody['NoEcho'] = noEcho 243 | responseBody['Data'] = responseData 244 | 245 | body = json.dumps(responseBody) 246 | logger.info("| response body:\n" + body) 247 | 248 | headers = { 249 | 'content-type' : '', 250 | 'content-length' : str(len(body)) 251 | } 252 | 253 | try: 254 | request = Request(responseUrl, method='PUT', data=bytes(body.encode('utf-8')), headers=headers) 255 | with contextlib.closing(urlopen(request)) as response: 256 | logger.info("| status code: " + response.reason) 257 | except Exception as e: 258 | logger.error("| unable to send response to CloudFormation") 259 | logger.exception(e) 260 | 261 | 262 | #--------------------------------------------------------------------------------------------------- 263 | # check if bucket is owned by a custom resource 264 | # if it is then we don't want to delete content 265 | def bucket_owned(bucketName, keyPrefix): 266 | tag = CUSTOM_RESOURCE_OWNER_TAG 267 | if keyPrefix != "": 268 | tag = tag + ':' + keyPrefix 269 | try: 270 | request = s3.get_bucket_tagging( 271 | Bucket=bucketName, 272 | ) 273 | return any((x["Key"].startswith(tag)) for x in request["TagSet"]) 274 | except Exception as e: 275 | logger.info("| error getting tags from bucket") 276 | logger.exception(e) 277 | return False 278 | 279 | # extract archive and replace markers in output files 280 | def extract_and_replace_markers(archive, contents_dir, markers): 281 | with ZipFile(archive, "r") as zip: 282 | zip.extractall(contents_dir) 283 | 284 | # replace markers for this source 285 | for file in zip.namelist(): 286 | file_path = os.path.join(contents_dir, file) 287 | if os.path.isdir(file_path): continue 288 | replace_markers(file_path, markers) 289 | 290 | def replace_markers(filename, markers): 291 | # convert the dict of string markers to binary markers 292 | replace_tokens = dict([(k.encode('utf-8'), v.encode('utf-8')) for k, v in markers.items()]) 293 | 294 | outfile = filename + '.new' 295 | with open(filename, 'rb') as fi, open(outfile, 'wb') as fo: 296 | for line in fi: 297 | for token in replace_tokens: 298 | line = line.replace(token, replace_tokens[token]) 299 | fo.write(line) 300 | 301 | # # delete the original file and rename the new one to the original 302 | os.remove(filename) 303 | os.rename(outfile, filename) -------------------------------------------------------------------------------- /test/nodejs-build.integ.snapshot/cdk.out: -------------------------------------------------------------------------------- 1 | {"version":"20.0.0"} -------------------------------------------------------------------------------- /test/nodejs-build.integ.snapshot/integ.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "20.0.0", 3 | "testCases": { 4 | "Test/DefaultTest": { 5 | "stacks": [ 6 | "NodejsBuildIntegTest" 7 | ], 8 | "diffAssets": true, 9 | "assertionStack": "TestDefaultTestDeployAssert12909640" 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /test/nodejs-build.integ.snapshot/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "20.0.0", 3 | "artifacts": { 4 | "Tree": { 5 | "type": "cdk:tree", 6 | "properties": { 7 | "file": "tree.json" 8 | } 9 | }, 10 | "NodejsBuildIntegTest": { 11 | "type": "aws:cloudformation:stack", 12 | "environment": "aws://unknown-account/unknown-region", 13 | "properties": { 14 | "templateFile": "NodejsBuildIntegTest.template.json", 15 | "validateOnSynth": false 16 | }, 17 | "metadata": { 18 | "/NodejsBuildIntegTest": [ 19 | { 20 | "type": "aws:cdk:asset", 21 | "data": { 22 | "path": "asset.9b19c7d81a94bbc19fe2164453716f4d7f651b21f923c5c264fe80f5a939e1db", 23 | "id": "9b19c7d81a94bbc19fe2164453716f4d7f651b21f923c5c264fe80f5a939e1db", 24 | "packaging": "zip", 25 | "sourceHash": "9b19c7d81a94bbc19fe2164453716f4d7f651b21f923c5c264fe80f5a939e1db", 26 | "s3BucketParameter": "AssetParameters9b19c7d81a94bbc19fe2164453716f4d7f651b21f923c5c264fe80f5a939e1dbS3Bucket7C24A369", 27 | "s3KeyParameter": "AssetParameters9b19c7d81a94bbc19fe2164453716f4d7f651b21f923c5c264fe80f5a939e1dbS3VersionKeyD5434FC9", 28 | "artifactHashParameter": "AssetParameters9b19c7d81a94bbc19fe2164453716f4d7f651b21f923c5c264fe80f5a939e1dbArtifactHashA955B34A" 29 | } 30 | }, 31 | { 32 | "type": "aws:cdk:asset", 33 | "data": { 34 | "path": "asset.72f05c779a8bc73c6ec86f6eafc720508792e7526696d3ae45a7fddcfc473c9d", 35 | "id": "72f05c779a8bc73c6ec86f6eafc720508792e7526696d3ae45a7fddcfc473c9d", 36 | "packaging": "zip", 37 | "sourceHash": "72f05c779a8bc73c6ec86f6eafc720508792e7526696d3ae45a7fddcfc473c9d", 38 | "s3BucketParameter": "AssetParameters72f05c779a8bc73c6ec86f6eafc720508792e7526696d3ae45a7fddcfc473c9dS3Bucket7BF37945", 39 | "s3KeyParameter": "AssetParameters72f05c779a8bc73c6ec86f6eafc720508792e7526696d3ae45a7fddcfc473c9dS3VersionKey360A250F", 40 | "artifactHashParameter": "AssetParameters72f05c779a8bc73c6ec86f6eafc720508792e7526696d3ae45a7fddcfc473c9dArtifactHash61592905" 41 | } 42 | }, 43 | { 44 | "type": "aws:cdk:asset", 45 | "data": { 46 | "path": "asset.1d3b5490cd99feddeb525a62c046988997469f2a765d0f12b43cff9d87a284fa.zip", 47 | "id": "1d3b5490cd99feddeb525a62c046988997469f2a765d0f12b43cff9d87a284fa", 48 | "packaging": "file", 49 | "sourceHash": "1d3b5490cd99feddeb525a62c046988997469f2a765d0f12b43cff9d87a284fa", 50 | "s3BucketParameter": "AssetParameters1d3b5490cd99feddeb525a62c046988997469f2a765d0f12b43cff9d87a284faS3Bucket4457F0E8", 51 | "s3KeyParameter": "AssetParameters1d3b5490cd99feddeb525a62c046988997469f2a765d0f12b43cff9d87a284faS3VersionKeyD347D3A4", 52 | "artifactHashParameter": "AssetParameters1d3b5490cd99feddeb525a62c046988997469f2a765d0f12b43cff9d87a284faArtifactHash79E5727E" 53 | } 54 | }, 55 | { 56 | "type": "aws:cdk:asset", 57 | "data": { 58 | "path": "asset.f98b78092dcdd31f5e6d47489beb5f804d4835ef86a8085d0a2053cb9ae711da", 59 | "id": "f98b78092dcdd31f5e6d47489beb5f804d4835ef86a8085d0a2053cb9ae711da", 60 | "packaging": "zip", 61 | "sourceHash": "f98b78092dcdd31f5e6d47489beb5f804d4835ef86a8085d0a2053cb9ae711da", 62 | "s3BucketParameter": "AssetParametersf98b78092dcdd31f5e6d47489beb5f804d4835ef86a8085d0a2053cb9ae711daS3BucketF23C0DE7", 63 | "s3KeyParameter": "AssetParametersf98b78092dcdd31f5e6d47489beb5f804d4835ef86a8085d0a2053cb9ae711daS3VersionKey5E97B17D", 64 | "artifactHashParameter": "AssetParametersf98b78092dcdd31f5e6d47489beb5f804d4835ef86a8085d0a2053cb9ae711daArtifactHashD85D28D8" 65 | } 66 | } 67 | ], 68 | "/NodejsBuildIntegTest/Destination/Resource": [ 69 | { 70 | "type": "aws:cdk:logicalId", 71 | "data": "Destination920A3C57" 72 | } 73 | ], 74 | "/NodejsBuildIntegTest/ExampleBuild/Project/Role/Resource": [ 75 | { 76 | "type": "aws:cdk:logicalId", 77 | "data": "ExampleBuildProjectRole823D0208" 78 | } 79 | ], 80 | "/NodejsBuildIntegTest/ExampleBuild/Project/Role/DefaultPolicy/Resource": [ 81 | { 82 | "type": "aws:cdk:logicalId", 83 | "data": "ExampleBuildProjectRoleDefaultPolicy1E341966" 84 | } 85 | ], 86 | "/NodejsBuildIntegTest/ExampleBuild/Project/Resource": [ 87 | { 88 | "type": "aws:cdk:logicalId", 89 | "data": "ExampleBuildProjectEF5CAC49" 90 | } 91 | ], 92 | "/NodejsBuildIntegTest/ExampleBuild/Resource/Default": [ 93 | { 94 | "type": "aws:cdk:logicalId", 95 | "data": "ExampleBuild61F1D79B" 96 | } 97 | ], 98 | "/NodejsBuildIntegTest/ExampleBuild/Deploy/AwsCliLayer/Resource": [ 99 | { 100 | "type": "aws:cdk:logicalId", 101 | "data": "ExampleBuildDeployAwsCliLayerF15CA413" 102 | } 103 | ], 104 | "/NodejsBuildIntegTest/ExampleBuild/Deploy/CustomResource-512MiB/Default": [ 105 | { 106 | "type": "aws:cdk:logicalId", 107 | "data": "ExampleBuildDeployCustomResource512MiB0ED95869" 108 | } 109 | ], 110 | "/NodejsBuildIntegTest/ExampleBuild/DownloadEnvFile": [ 111 | { 112 | "type": "aws:cdk:logicalId", 113 | "data": "ExampleBuildDownloadEnvFileC240151B" 114 | } 115 | ], 116 | "/NodejsBuildIntegTest/NodejsBuildCustomResourceHandler25648b212c404f09aa65b6bbb0c44659/ServiceRole/Resource": [ 117 | { 118 | "type": "aws:cdk:logicalId", 119 | "data": "NodejsBuildCustomResourceHandler25648b212c404f09aa65b6bbb0c44659ServiceRoleCB01FBE6" 120 | } 121 | ], 122 | "/NodejsBuildIntegTest/NodejsBuildCustomResourceHandler25648b212c404f09aa65b6bbb0c44659/ServiceRole/DefaultPolicy/Resource": [ 123 | { 124 | "type": "aws:cdk:logicalId", 125 | "data": "NodejsBuildCustomResourceHandler25648b212c404f09aa65b6bbb0c44659ServiceRoleDefaultPolicyCF8879D3" 126 | } 127 | ], 128 | "/NodejsBuildIntegTest/NodejsBuildCustomResourceHandler25648b212c404f09aa65b6bbb0c44659/Resource": [ 129 | { 130 | "type": "aws:cdk:logicalId", 131 | "data": "NodejsBuildCustomResourceHandler25648b212c404f09aa65b6bbb0c446591C4101F8" 132 | } 133 | ], 134 | "/NodejsBuildIntegTest/AssetParameters/9b19c7d81a94bbc19fe2164453716f4d7f651b21f923c5c264fe80f5a939e1db/S3Bucket": [ 135 | { 136 | "type": "aws:cdk:logicalId", 137 | "data": "AssetParameters9b19c7d81a94bbc19fe2164453716f4d7f651b21f923c5c264fe80f5a939e1dbS3Bucket7C24A369" 138 | } 139 | ], 140 | "/NodejsBuildIntegTest/AssetParameters/9b19c7d81a94bbc19fe2164453716f4d7f651b21f923c5c264fe80f5a939e1db/S3VersionKey": [ 141 | { 142 | "type": "aws:cdk:logicalId", 143 | "data": "AssetParameters9b19c7d81a94bbc19fe2164453716f4d7f651b21f923c5c264fe80f5a939e1dbS3VersionKeyD5434FC9" 144 | } 145 | ], 146 | "/NodejsBuildIntegTest/AssetParameters/9b19c7d81a94bbc19fe2164453716f4d7f651b21f923c5c264fe80f5a939e1db/ArtifactHash": [ 147 | { 148 | "type": "aws:cdk:logicalId", 149 | "data": "AssetParameters9b19c7d81a94bbc19fe2164453716f4d7f651b21f923c5c264fe80f5a939e1dbArtifactHashA955B34A" 150 | } 151 | ], 152 | "/NodejsBuildIntegTest/AssetParameters/72f05c779a8bc73c6ec86f6eafc720508792e7526696d3ae45a7fddcfc473c9d/S3Bucket": [ 153 | { 154 | "type": "aws:cdk:logicalId", 155 | "data": "AssetParameters72f05c779a8bc73c6ec86f6eafc720508792e7526696d3ae45a7fddcfc473c9dS3Bucket7BF37945" 156 | } 157 | ], 158 | "/NodejsBuildIntegTest/AssetParameters/72f05c779a8bc73c6ec86f6eafc720508792e7526696d3ae45a7fddcfc473c9d/S3VersionKey": [ 159 | { 160 | "type": "aws:cdk:logicalId", 161 | "data": "AssetParameters72f05c779a8bc73c6ec86f6eafc720508792e7526696d3ae45a7fddcfc473c9dS3VersionKey360A250F" 162 | } 163 | ], 164 | "/NodejsBuildIntegTest/AssetParameters/72f05c779a8bc73c6ec86f6eafc720508792e7526696d3ae45a7fddcfc473c9d/ArtifactHash": [ 165 | { 166 | "type": "aws:cdk:logicalId", 167 | "data": "AssetParameters72f05c779a8bc73c6ec86f6eafc720508792e7526696d3ae45a7fddcfc473c9dArtifactHash61592905" 168 | } 169 | ], 170 | "/NodejsBuildIntegTest/AssetParameters/1d3b5490cd99feddeb525a62c046988997469f2a765d0f12b43cff9d87a284fa/S3Bucket": [ 171 | { 172 | "type": "aws:cdk:logicalId", 173 | "data": "AssetParameters1d3b5490cd99feddeb525a62c046988997469f2a765d0f12b43cff9d87a284faS3Bucket4457F0E8" 174 | } 175 | ], 176 | "/NodejsBuildIntegTest/AssetParameters/1d3b5490cd99feddeb525a62c046988997469f2a765d0f12b43cff9d87a284fa/S3VersionKey": [ 177 | { 178 | "type": "aws:cdk:logicalId", 179 | "data": "AssetParameters1d3b5490cd99feddeb525a62c046988997469f2a765d0f12b43cff9d87a284faS3VersionKeyD347D3A4" 180 | } 181 | ], 182 | "/NodejsBuildIntegTest/AssetParameters/1d3b5490cd99feddeb525a62c046988997469f2a765d0f12b43cff9d87a284fa/ArtifactHash": [ 183 | { 184 | "type": "aws:cdk:logicalId", 185 | "data": "AssetParameters1d3b5490cd99feddeb525a62c046988997469f2a765d0f12b43cff9d87a284faArtifactHash79E5727E" 186 | } 187 | ], 188 | "/NodejsBuildIntegTest/AssetParameters/f98b78092dcdd31f5e6d47489beb5f804d4835ef86a8085d0a2053cb9ae711da/S3Bucket": [ 189 | { 190 | "type": "aws:cdk:logicalId", 191 | "data": "AssetParametersf98b78092dcdd31f5e6d47489beb5f804d4835ef86a8085d0a2053cb9ae711daS3BucketF23C0DE7" 192 | } 193 | ], 194 | "/NodejsBuildIntegTest/AssetParameters/f98b78092dcdd31f5e6d47489beb5f804d4835ef86a8085d0a2053cb9ae711da/S3VersionKey": [ 195 | { 196 | "type": "aws:cdk:logicalId", 197 | "data": "AssetParametersf98b78092dcdd31f5e6d47489beb5f804d4835ef86a8085d0a2053cb9ae711daS3VersionKey5E97B17D" 198 | } 199 | ], 200 | "/NodejsBuildIntegTest/AssetParameters/f98b78092dcdd31f5e6d47489beb5f804d4835ef86a8085d0a2053cb9ae711da/ArtifactHash": [ 201 | { 202 | "type": "aws:cdk:logicalId", 203 | "data": "AssetParametersf98b78092dcdd31f5e6d47489beb5f804d4835ef86a8085d0a2053cb9ae711daArtifactHashD85D28D8" 204 | } 205 | ], 206 | "/NodejsBuildIntegTest/Custom::CDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756C512MiB/ServiceRole/Resource": [ 207 | { 208 | "type": "aws:cdk:logicalId", 209 | "data": "CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756C512MiBServiceRoleBA21DBC1" 210 | } 211 | ], 212 | "/NodejsBuildIntegTest/Custom::CDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756C512MiB/ServiceRole/DefaultPolicy/Resource": [ 213 | { 214 | "type": "aws:cdk:logicalId", 215 | "data": "CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756C512MiBServiceRoleDefaultPolicy96C3E726" 216 | } 217 | ], 218 | "/NodejsBuildIntegTest/Custom::CDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756C512MiB/Resource": [ 219 | { 220 | "type": "aws:cdk:logicalId", 221 | "data": "CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756C512MiB6723FB92" 222 | } 223 | ] 224 | }, 225 | "displayName": "NodejsBuildIntegTest" 226 | }, 227 | "TestDefaultTestDeployAssert12909640": { 228 | "type": "aws:cloudformation:stack", 229 | "environment": "aws://unknown-account/unknown-region", 230 | "properties": { 231 | "templateFile": "TestDefaultTestDeployAssert12909640.template.json", 232 | "validateOnSynth": false 233 | }, 234 | "displayName": "Test/DefaultTest/DeployAssert" 235 | } 236 | } 237 | } -------------------------------------------------------------------------------- /test/soci-index-build.integ.snapshot/SociIndexBuildIntegTest.template.json: -------------------------------------------------------------------------------- 1 | { 2 | "Resources": { 3 | "Image1Index415B9527": { 4 | "Type": "Custom::CDKSociIndexBuild", 5 | "Properties": { 6 | "ServiceToken": { 7 | "Fn::GetAtt": [ 8 | "DeployTimeBuildCustomResourceHandlerdb740fd554364a848a09e6dfcd01f4f306AEFF37", 9 | "Arn" 10 | ] 11 | }, 12 | "type": "SociIndexBuild", 13 | "imageTag": "a8bbd8136347d097316685416937f7ad1e2612d23493dcc4e0ef33d290c09b05", 14 | "repositoryName": "aws-cdk/assets", 15 | "codeBuildProjectName": { 16 | "Ref": "SociIndexBuild024cf76a10034aa4aa4b12c32c09ca3camd642F4A8EFA" 17 | } 18 | }, 19 | "UpdateReplacePolicy": "Delete", 20 | "DeletionPolicy": "Delete" 21 | }, 22 | "DeployTimeBuildCustomResourceHandlerdb740fd554364a848a09e6dfcd01f4f3ServiceRoleB008BAA4": { 23 | "Type": "AWS::IAM::Role", 24 | "Properties": { 25 | "AssumeRolePolicyDocument": { 26 | "Statement": [ 27 | { 28 | "Action": "sts:AssumeRole", 29 | "Effect": "Allow", 30 | "Principal": { 31 | "Service": "lambda.amazonaws.com" 32 | } 33 | } 34 | ], 35 | "Version": "2012-10-17" 36 | }, 37 | "ManagedPolicyArns": [ 38 | { 39 | "Fn::Join": [ 40 | "", 41 | [ 42 | "arn:", 43 | { 44 | "Ref": "AWS::Partition" 45 | }, 46 | ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" 47 | ] 48 | ] 49 | } 50 | ] 51 | } 52 | }, 53 | "DeployTimeBuildCustomResourceHandlerdb740fd554364a848a09e6dfcd01f4f3ServiceRoleDefaultPolicyFECC51DC": { 54 | "Type": "AWS::IAM::Policy", 55 | "Properties": { 56 | "PolicyDocument": { 57 | "Statement": [ 58 | { 59 | "Action": "codebuild:StartBuild", 60 | "Effect": "Allow", 61 | "Resource": { 62 | "Fn::GetAtt": [ 63 | "SociIndexBuild024cf76a10034aa4aa4b12c32c09ca3camd642F4A8EFA", 64 | "Arn" 65 | ] 66 | } 67 | } 68 | ], 69 | "Version": "2012-10-17" 70 | }, 71 | "PolicyName": "DeployTimeBuildCustomResourceHandlerdb740fd554364a848a09e6dfcd01f4f3ServiceRoleDefaultPolicyFECC51DC", 72 | "Roles": [ 73 | { 74 | "Ref": "DeployTimeBuildCustomResourceHandlerdb740fd554364a848a09e6dfcd01f4f3ServiceRoleB008BAA4" 75 | } 76 | ] 77 | } 78 | }, 79 | "DeployTimeBuildCustomResourceHandlerdb740fd554364a848a09e6dfcd01f4f306AEFF37": { 80 | "Type": "AWS::Lambda::Function", 81 | "Properties": { 82 | "Code": { 83 | "S3Bucket": { 84 | "Ref": "AssetParameters9b19c7d81a94bbc19fe2164453716f4d7f651b21f923c5c264fe80f5a939e1dbS3Bucket7C24A369" 85 | }, 86 | "S3Key": { 87 | "Fn::Join": [ 88 | "", 89 | [ 90 | { 91 | "Fn::Select": [ 92 | 0, 93 | { 94 | "Fn::Split": [ 95 | "||", 96 | { 97 | "Ref": "AssetParameters9b19c7d81a94bbc19fe2164453716f4d7f651b21f923c5c264fe80f5a939e1dbS3VersionKeyD5434FC9" 98 | } 99 | ] 100 | } 101 | ] 102 | }, 103 | { 104 | "Fn::Select": [ 105 | 1, 106 | { 107 | "Fn::Split": [ 108 | "||", 109 | { 110 | "Ref": "AssetParameters9b19c7d81a94bbc19fe2164453716f4d7f651b21f923c5c264fe80f5a939e1dbS3VersionKeyD5434FC9" 111 | } 112 | ] 113 | } 114 | ] 115 | } 116 | ] 117 | ] 118 | } 119 | }, 120 | "Role": { 121 | "Fn::GetAtt": [ 122 | "DeployTimeBuildCustomResourceHandlerdb740fd554364a848a09e6dfcd01f4f3ServiceRoleB008BAA4", 123 | "Arn" 124 | ] 125 | }, 126 | "Handler": "index.handler", 127 | "Runtime": "nodejs20.x", 128 | "Timeout": 300 129 | }, 130 | "DependsOn": [ 131 | "DeployTimeBuildCustomResourceHandlerdb740fd554364a848a09e6dfcd01f4f3ServiceRoleDefaultPolicyFECC51DC", 132 | "DeployTimeBuildCustomResourceHandlerdb740fd554364a848a09e6dfcd01f4f3ServiceRoleB008BAA4" 133 | ] 134 | }, 135 | "SociIndexBuild024cf76a10034aa4aa4b12c32c09ca3camd64RoleBDD942DA": { 136 | "Type": "AWS::IAM::Role", 137 | "Properties": { 138 | "AssumeRolePolicyDocument": { 139 | "Statement": [ 140 | { 141 | "Action": "sts:AssumeRole", 142 | "Effect": "Allow", 143 | "Principal": { 144 | "Service": "codebuild.amazonaws.com" 145 | } 146 | } 147 | ], 148 | "Version": "2012-10-17" 149 | } 150 | } 151 | }, 152 | "SociIndexBuild024cf76a10034aa4aa4b12c32c09ca3camd64RoleDefaultPolicy93E560B4": { 153 | "Type": "AWS::IAM::Policy", 154 | "Properties": { 155 | "PolicyDocument": { 156 | "Statement": [ 157 | { 158 | "Action": [ 159 | "logs:CreateLogGroup", 160 | "logs:CreateLogStream", 161 | "logs:PutLogEvents" 162 | ], 163 | "Effect": "Allow", 164 | "Resource": [ 165 | { 166 | "Fn::Join": [ 167 | "", 168 | [ 169 | "arn:", 170 | { 171 | "Ref": "AWS::Partition" 172 | }, 173 | ":logs:", 174 | { 175 | "Ref": "AWS::Region" 176 | }, 177 | ":", 178 | { 179 | "Ref": "AWS::AccountId" 180 | }, 181 | ":log-group:/aws/codebuild/", 182 | { 183 | "Ref": "SociIndexBuild024cf76a10034aa4aa4b12c32c09ca3camd642F4A8EFA" 184 | }, 185 | ":*" 186 | ] 187 | ] 188 | }, 189 | { 190 | "Fn::Join": [ 191 | "", 192 | [ 193 | "arn:", 194 | { 195 | "Ref": "AWS::Partition" 196 | }, 197 | ":logs:", 198 | { 199 | "Ref": "AWS::Region" 200 | }, 201 | ":", 202 | { 203 | "Ref": "AWS::AccountId" 204 | }, 205 | ":log-group:/aws/codebuild/", 206 | { 207 | "Ref": "SociIndexBuild024cf76a10034aa4aa4b12c32c09ca3camd642F4A8EFA" 208 | } 209 | ] 210 | ] 211 | } 212 | ] 213 | }, 214 | { 215 | "Action": [ 216 | "codebuild:BatchPutCodeCoverages", 217 | "codebuild:BatchPutTestCases", 218 | "codebuild:CreateReport", 219 | "codebuild:CreateReportGroup", 220 | "codebuild:UpdateReport" 221 | ], 222 | "Effect": "Allow", 223 | "Resource": { 224 | "Fn::Join": [ 225 | "", 226 | [ 227 | "arn:", 228 | { 229 | "Ref": "AWS::Partition" 230 | }, 231 | ":codebuild:", 232 | { 233 | "Ref": "AWS::Region" 234 | }, 235 | ":", 236 | { 237 | "Ref": "AWS::AccountId" 238 | }, 239 | ":report-group/", 240 | { 241 | "Ref": "SociIndexBuild024cf76a10034aa4aa4b12c32c09ca3camd642F4A8EFA" 242 | }, 243 | "-*" 244 | ] 245 | ] 246 | } 247 | }, 248 | { 249 | "Action": [ 250 | "ecr:BatchCheckLayerAvailability", 251 | "ecr:BatchGetImage", 252 | "ecr:CompleteLayerUpload", 253 | "ecr:DescribeImages", 254 | "ecr:GetDownloadUrlForLayer", 255 | "ecr:InitiateLayerUpload", 256 | "ecr:PutImage", 257 | "ecr:UploadLayerPart" 258 | ], 259 | "Effect": "Allow", 260 | "Resource": { 261 | "Fn::Join": [ 262 | "", 263 | [ 264 | "arn:", 265 | { 266 | "Ref": "AWS::Partition" 267 | }, 268 | ":ecr:", 269 | { 270 | "Ref": "AWS::Region" 271 | }, 272 | ":", 273 | { 274 | "Ref": "AWS::AccountId" 275 | }, 276 | ":repository/aws-cdk/assets" 277 | ] 278 | ] 279 | } 280 | }, 281 | { 282 | "Action": "ecr:GetAuthorizationToken", 283 | "Effect": "Allow", 284 | "Resource": "*" 285 | } 286 | ], 287 | "Version": "2012-10-17" 288 | }, 289 | "PolicyName": "SociIndexBuild024cf76a10034aa4aa4b12c32c09ca3camd64RoleDefaultPolicy93E560B4", 290 | "Roles": [ 291 | { 292 | "Ref": "SociIndexBuild024cf76a10034aa4aa4b12c32c09ca3camd64RoleBDD942DA" 293 | } 294 | ] 295 | } 296 | }, 297 | "SociIndexBuild024cf76a10034aa4aa4b12c32c09ca3camd642F4A8EFA": { 298 | "Type": "AWS::CodeBuild::Project", 299 | "Properties": { 300 | "Artifacts": { 301 | "Type": "NO_ARTIFACTS" 302 | }, 303 | "Environment": { 304 | "ComputeType": "BUILD_GENERAL1_SMALL", 305 | "Image": "aws/codebuild/standard:7.0", 306 | "ImagePullCredentialsType": "CODEBUILD", 307 | "PrivilegedMode": false, 308 | "Type": "LINUX_CONTAINER" 309 | }, 310 | "ServiceRole": { 311 | "Fn::GetAtt": [ 312 | "SociIndexBuild024cf76a10034aa4aa4b12c32c09ca3camd64RoleBDD942DA", 313 | "Arn" 314 | ] 315 | }, 316 | "Source": { 317 | "BuildSpec": "{\n \"version\": \"0.2\",\n \"phases\": {\n \"build\": {\n \"commands\": [\n \"current_dir=$(pwd)\",\n \"wget --quiet -O soci-wrapper.tar.gz https://github.com/tmokmss/soci-wrapper/releases/download/v0.1.2/soci-wrapper-v0.1.2-linux-amd64.tar.gz\",\n \"tar -xvzf soci-wrapper.tar.gz\",\n \"\",\n \"export AWS_ACCOUNT=$(aws sts get-caller-identity --query \\\"Account\\\" --output text)\",\n \"export REGISTRY_USER=AWS\",\n \"export REGISTRY_PASSWORD=$(aws ecr get-login-password --region $AWS_REGION)\",\n \"export REGISTRY=$AWS_ACCOUNT.dkr.ecr.$AWS_REGION.amazonaws.com\",\n \"aws ecr get-login-password --region $AWS_REGION | docker login --username AWS --password-stdin $REGISTRY\",\n \"REPO_NAME=$repositoryName\",\n \"IMAGE_TAG=$imageTag\",\n \"DIGEST=$(aws ecr describe-images --repository-name $REPO_NAME --image-ids imageTag=$IMAGE_TAG --query imageDetails[0].imageDigest --output text)\",\n \"./soci-wrapper $REPO_NAME $DIGEST $AWS_REGION $AWS_ACCOUNT\"\n ]\n },\n \"post_build\": {\n \"commands\": [\n \"echo Build completed on `date`\",\n \"\\nSTATUS='SUCCESS'\\nif [ $CODEBUILD_BUILD_SUCCEEDING -ne 1 ] # Test if the build is failing\\nthen\\nSTATUS='FAILED'\\nREASON=\\\"deploy-time-build 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}\\nEOF\\ncurl -vv -i -X PUT -H 'Content-Type:' -d \\\"@payload.json\\\" \\\"$responseURL\\\"\\n \"\n ]\n }\n }\n}", 318 | "Type": "NO_SOURCE" 319 | }, 320 | "Cache": { 321 | "Type": "NO_CACHE" 322 | }, 323 | "EncryptionKey": "alias/aws/s3" 324 | } 325 | }, 326 | "Image2Index42EE498C": { 327 | "Type": "Custom::CDKSociIndexBuild", 328 | "Properties": { 329 | "ServiceToken": { 330 | "Fn::GetAtt": [ 331 | "DeployTimeBuildCustomResourceHandlerdb740fd554364a848a09e6dfcd01f4f306AEFF37", 332 | "Arn" 333 | ] 334 | }, 335 | "type": "SociIndexBuild", 336 | "imageTag": "cf17887cc251b0d2ad6a734dbfd8c28d46168a4e632883267d3a06e454b1c3ad", 337 | "repositoryName": "aws-cdk/assets", 338 | "codeBuildProjectName": { 339 | "Ref": "SociIndexBuild024cf76a10034aa4aa4b12c32c09ca3camd642F4A8EFA" 340 | } 341 | }, 342 | "UpdateReplacePolicy": "Delete", 343 | "DeletionPolicy": "Delete" 344 | } 345 | }, 346 | "Parameters": { 347 | "AssetParameters9b19c7d81a94bbc19fe2164453716f4d7f651b21f923c5c264fe80f5a939e1dbS3Bucket7C24A369": { 348 | "Type": "String", 349 | "Description": "S3 bucket for asset \"9b19c7d81a94bbc19fe2164453716f4d7f651b21f923c5c264fe80f5a939e1db\"" 350 | }, 351 | "AssetParameters9b19c7d81a94bbc19fe2164453716f4d7f651b21f923c5c264fe80f5a939e1dbS3VersionKeyD5434FC9": { 352 | "Type": "String", 353 | "Description": "S3 key for asset version \"9b19c7d81a94bbc19fe2164453716f4d7f651b21f923c5c264fe80f5a939e1db\"" 354 | }, 355 | "AssetParameters9b19c7d81a94bbc19fe2164453716f4d7f651b21f923c5c264fe80f5a939e1dbArtifactHashA955B34A": { 356 | "Type": "String", 357 | "Description": "Artifact hash for asset \"9b19c7d81a94bbc19fe2164453716f4d7f651b21f923c5c264fe80f5a939e1db\"" 358 | } 359 | } 360 | } -------------------------------------------------------------------------------- /test/soci-index-build.integ.snapshot/TestDefaultTestDeployAssert12909640.template.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /test/soci-index-build.integ.snapshot/asset.9b19c7d81a94bbc19fe2164453716f4d7f651b21f923c5c264fe80f5a939e1db/index.js: -------------------------------------------------------------------------------- 1 | var __create = Object.create; 2 | var __defProp = Object.defineProperty; 3 | var __getOwnPropDesc = Object.getOwnPropertyDescriptor; 4 | var __getOwnPropNames = Object.getOwnPropertyNames; 5 | var __getProtoOf = Object.getPrototypeOf; 6 | var __hasOwnProp = Object.prototype.hasOwnProperty; 7 | var __export = (target, all) => { 8 | for (var name in all) 9 | __defProp(target, name, { get: all[name], enumerable: true }); 10 | }; 11 | var __copyProps = (to, from, except, desc) => { 12 | if (from && typeof from === "object" || typeof from === "function") { 13 | for (let key of __getOwnPropNames(from)) 14 | if (!__hasOwnProp.call(to, key) && key !== except) 15 | __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); 16 | } 17 | return to; 18 | }; 19 | var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod)); 20 | var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); 21 | 22 | // index.ts 23 | var trigger_codebuild_exports = {}; 24 | __export(trigger_codebuild_exports, { 25 | handler: () => handler 26 | }); 27 | module.exports = __toCommonJS(trigger_codebuild_exports); 28 | var import_client_codebuild = require("@aws-sdk/client-codebuild"); 29 | var import_crypto = __toESM(require("crypto")); 30 | var cb = new import_client_codebuild.CodeBuildClient({}); 31 | var handler = async (event, context) => { 32 | console.log(JSON.stringify(event)); 33 | try { 34 | if (event.RequestType == "Create" || event.RequestType == "Update") { 35 | const props = event.ResourceProperties; 36 | const commonEnvironments = [ 37 | { 38 | name: "responseURL", 39 | value: event.ResponseURL 40 | }, 41 | { 42 | name: "stackId", 43 | value: event.StackId 44 | }, 45 | { 46 | name: "requestId", 47 | value: event.RequestId 48 | }, 49 | { 50 | name: "logicalResourceId", 51 | value: event.LogicalResourceId 52 | } 53 | ]; 54 | const newPhysicalId = import_crypto.default.randomBytes(16).toString("hex"); 55 | let command; 56 | switch (props.type) { 57 | case "NodejsBuild": 58 | command = new import_client_codebuild.StartBuildCommand({ 59 | projectName: props.codeBuildProjectName, 60 | environmentVariablesOverride: [ 61 | ...commonEnvironments, 62 | { 63 | name: "input", 64 | value: JSON.stringify(props.sources.map((source) => ({ 65 | assetUrl: `s3://${source.sourceBucketName}/${source.sourceObjectKey}`, 66 | extractPath: source.extractPath, 67 | commands: (source.commands ?? []).join(" && ") 68 | }))) 69 | }, 70 | { 71 | name: "buildCommands", 72 | value: props.buildCommands.join(" && ") 73 | }, 74 | { 75 | name: "destinationBucketName", 76 | value: props.destinationBucketName 77 | }, 78 | { 79 | name: "destinationObjectKey", 80 | value: `${newPhysicalId}.zip` 81 | }, 82 | { 83 | name: "workingDirectory", 84 | value: props.workingDirectory 85 | }, 86 | { 87 | name: "outputSourceDirectory", 88 | value: props.outputSourceDirectory 89 | }, 90 | { 91 | name: "projectName", 92 | value: props.codeBuildProjectName 93 | }, 94 | { 95 | name: "outputEnvFile", 96 | value: props.outputEnvFile.toString() 97 | }, 98 | { 99 | name: "envFileKey", 100 | value: `deploy-time-build/${event.StackId.split("/")[1]}/${event.LogicalResourceId}/${newPhysicalId}.env` 101 | }, 102 | { 103 | name: "envNames", 104 | value: Object.keys(props.environment ?? {}).join(",") 105 | }, 106 | ...Object.entries(props.environment ?? {}).map(([name, value]) => ({ 107 | name, 108 | value 109 | })) 110 | ] 111 | }); 112 | break; 113 | case "SociIndexBuild": 114 | command = new import_client_codebuild.StartBuildCommand({ 115 | projectName: props.codeBuildProjectName, 116 | environmentVariablesOverride: [ 117 | ...commonEnvironments, 118 | { 119 | name: "repositoryName", 120 | value: props.repositoryName 121 | }, 122 | { 123 | name: "imageTag", 124 | value: props.imageTag 125 | }, 126 | { 127 | name: "projectName", 128 | value: props.codeBuildProjectName 129 | } 130 | ] 131 | }); 132 | break; 133 | case "ContainerImageBuild": { 134 | const imageTag = props.imageTag ?? `${props.tagPrefix ?? ""}${newPhysicalId}`; 135 | const buildCommand = props.buildCommand.replaceAll("", imageTag); 136 | command = new import_client_codebuild.StartBuildCommand({ 137 | projectName: props.codeBuildProjectName, 138 | environmentVariablesOverride: [ 139 | ...commonEnvironments, 140 | { 141 | name: "repositoryUri", 142 | value: props.repositoryUri 143 | }, 144 | { 145 | name: "repositoryAuthUri", 146 | value: props.repositoryUri.split("/")[0] 147 | }, 148 | { 149 | name: "buildCommand", 150 | value: buildCommand 151 | }, 152 | { 153 | name: "imageTag", 154 | value: imageTag 155 | }, 156 | { 157 | name: "projectName", 158 | value: props.codeBuildProjectName 159 | }, 160 | { 161 | name: "sourceS3Url", 162 | value: props.sourceS3Url 163 | } 164 | ] 165 | }); 166 | break; 167 | } 168 | default: 169 | throw new Error(`invalid event type ${props}}`); 170 | } 171 | const build = await cb.send(command); 172 | } else { 173 | await sendStatus("SUCCESS", event, context); 174 | } 175 | } catch (e) { 176 | console.log(e); 177 | const err = e; 178 | await sendStatus("FAILED", event, context, err.message); 179 | } 180 | }; 181 | var sendStatus = async (status, event, context, reason) => { 182 | const responseBody = JSON.stringify({ 183 | Status: status, 184 | Reason: reason ?? "See the details in CloudWatch Log Stream: " + context.logStreamName, 185 | PhysicalResourceId: context.logStreamName, 186 | StackId: event.StackId, 187 | RequestId: event.RequestId, 188 | LogicalResourceId: event.LogicalResourceId, 189 | NoEcho: false, 190 | Data: {} 191 | }); 192 | await fetch(event.ResponseURL, { 193 | method: "PUT", 194 | body: responseBody, 195 | headers: { 196 | "Content-Type": "", 197 | "Content-Length": responseBody.length.toString() 198 | } 199 | }); 200 | }; 201 | // Annotate the CommonJS export names for ESM import in node: 202 | 0 && (module.exports = { 203 | handler 204 | }); 205 | -------------------------------------------------------------------------------- /test/soci-index-build.integ.snapshot/asset.a8bbd8136347d097316685416937f7ad1e2612d23493dcc4e0ef33d290c09b05/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM public.ecr.aws/nginx/nginx:1.26 2 | # create dummy file to change the size of a image 3 | ARG DUMMY_FILE_SIZE_MB="10" 4 | # RUN fallocate -l ${DUMMY_FILE_SIZE_MB} dummy.img 5 | RUN dd if=/dev/zero of=dummy.img bs=1M count=${DUMMY_FILE_SIZE_MB} 6 | -------------------------------------------------------------------------------- /test/soci-index-build.integ.snapshot/asset.cf17887cc251b0d2ad6a734dbfd8c28d46168a4e632883267d3a06e454b1c3ad/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM public.ecr.aws/nginx/nginx:1.26 2 | # create dummy file to change the size of a image 3 | ARG DUMMY_FILE_SIZE_MB="10" 4 | # RUN fallocate -l ${DUMMY_FILE_SIZE_MB} dummy.img 5 | RUN dd if=/dev/zero of=dummy.img bs=1M count=${DUMMY_FILE_SIZE_MB} 6 | -------------------------------------------------------------------------------- /test/soci-index-build.integ.snapshot/cdk.out: -------------------------------------------------------------------------------- 1 | {"version":"20.0.0"} -------------------------------------------------------------------------------- /test/soci-index-build.integ.snapshot/integ.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "20.0.0", 3 | "testCases": { 4 | "Test/DefaultTest": { 5 | "stacks": [ 6 | "SociIndexBuildIntegTest" 7 | ], 8 | "diffAssets": true, 9 | "assertionStack": "TestDefaultTestDeployAssert12909640" 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /test/soci-index-build.integ.snapshot/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "20.0.0", 3 | "artifacts": { 4 | "Tree": { 5 | "type": "cdk:tree", 6 | "properties": { 7 | "file": "tree.json" 8 | } 9 | }, 10 | "SociIndexBuildIntegTest": { 11 | "type": "aws:cloudformation:stack", 12 | "environment": "aws://unknown-account/unknown-region", 13 | "properties": { 14 | "templateFile": "SociIndexBuildIntegTest.template.json", 15 | "validateOnSynth": false 16 | }, 17 | "metadata": { 18 | "/SociIndexBuildIntegTest": [ 19 | { 20 | "type": "aws:cdk:asset", 21 | "data": { 22 | "repositoryName": "aws-cdk/assets", 23 | "imageTag": "a8bbd8136347d097316685416937f7ad1e2612d23493dcc4e0ef33d290c09b05", 24 | "id": "a8bbd8136347d097316685416937f7ad1e2612d23493dcc4e0ef33d290c09b05", 25 | "packaging": "container-image", 26 | "path": "asset.a8bbd8136347d097316685416937f7ad1e2612d23493dcc4e0ef33d290c09b05", 27 | "sourceHash": "a8bbd8136347d097316685416937f7ad1e2612d23493dcc4e0ef33d290c09b05", 28 | "buildArgs": { 29 | "DUMMY_FILE_SIZE_MB": "10" 30 | } 31 | } 32 | }, 33 | { 34 | "type": "aws:cdk:asset", 35 | "data": { 36 | "path": "asset.9b19c7d81a94bbc19fe2164453716f4d7f651b21f923c5c264fe80f5a939e1db", 37 | "id": "9b19c7d81a94bbc19fe2164453716f4d7f651b21f923c5c264fe80f5a939e1db", 38 | "packaging": "zip", 39 | "sourceHash": "9b19c7d81a94bbc19fe2164453716f4d7f651b21f923c5c264fe80f5a939e1db", 40 | "s3BucketParameter": "AssetParameters9b19c7d81a94bbc19fe2164453716f4d7f651b21f923c5c264fe80f5a939e1dbS3Bucket7C24A369", 41 | "s3KeyParameter": "AssetParameters9b19c7d81a94bbc19fe2164453716f4d7f651b21f923c5c264fe80f5a939e1dbS3VersionKeyD5434FC9", 42 | "artifactHashParameter": "AssetParameters9b19c7d81a94bbc19fe2164453716f4d7f651b21f923c5c264fe80f5a939e1dbArtifactHashA955B34A" 43 | } 44 | }, 45 | { 46 | "type": "aws:cdk:asset", 47 | "data": { 48 | "repositoryName": "aws-cdk/assets", 49 | "imageTag": "cf17887cc251b0d2ad6a734dbfd8c28d46168a4e632883267d3a06e454b1c3ad", 50 | "id": "cf17887cc251b0d2ad6a734dbfd8c28d46168a4e632883267d3a06e454b1c3ad", 51 | "packaging": "container-image", 52 | "path": "asset.cf17887cc251b0d2ad6a734dbfd8c28d46168a4e632883267d3a06e454b1c3ad", 53 | "sourceHash": "cf17887cc251b0d2ad6a734dbfd8c28d46168a4e632883267d3a06e454b1c3ad", 54 | "buildArgs": { 55 | "DUMMY_FILE_SIZE_MB": "500" 56 | } 57 | } 58 | } 59 | ], 60 | "/SociIndexBuildIntegTest/Image1/Index/Resource/Default": [ 61 | { 62 | "type": "aws:cdk:logicalId", 63 | "data": "Image1Index415B9527" 64 | } 65 | ], 66 | "/SociIndexBuildIntegTest/DeployTimeBuildCustomResourceHandlerdb740fd554364a848a09e6dfcd01f4f3/ServiceRole/Resource": [ 67 | { 68 | "type": "aws:cdk:logicalId", 69 | "data": "DeployTimeBuildCustomResourceHandlerdb740fd554364a848a09e6dfcd01f4f3ServiceRoleB008BAA4" 70 | } 71 | ], 72 | "/SociIndexBuildIntegTest/DeployTimeBuildCustomResourceHandlerdb740fd554364a848a09e6dfcd01f4f3/ServiceRole/DefaultPolicy/Resource": [ 73 | { 74 | "type": "aws:cdk:logicalId", 75 | "data": "DeployTimeBuildCustomResourceHandlerdb740fd554364a848a09e6dfcd01f4f3ServiceRoleDefaultPolicyFECC51DC" 76 | } 77 | ], 78 | "/SociIndexBuildIntegTest/DeployTimeBuildCustomResourceHandlerdb740fd554364a848a09e6dfcd01f4f3/Resource": [ 79 | { 80 | "type": "aws:cdk:logicalId", 81 | "data": "DeployTimeBuildCustomResourceHandlerdb740fd554364a848a09e6dfcd01f4f306AEFF37" 82 | } 83 | ], 84 | "/SociIndexBuildIntegTest/AssetParameters/9b19c7d81a94bbc19fe2164453716f4d7f651b21f923c5c264fe80f5a939e1db/S3Bucket": [ 85 | { 86 | "type": "aws:cdk:logicalId", 87 | "data": "AssetParameters9b19c7d81a94bbc19fe2164453716f4d7f651b21f923c5c264fe80f5a939e1dbS3Bucket7C24A369" 88 | } 89 | ], 90 | "/SociIndexBuildIntegTest/AssetParameters/9b19c7d81a94bbc19fe2164453716f4d7f651b21f923c5c264fe80f5a939e1db/S3VersionKey": [ 91 | { 92 | "type": "aws:cdk:logicalId", 93 | "data": "AssetParameters9b19c7d81a94bbc19fe2164453716f4d7f651b21f923c5c264fe80f5a939e1dbS3VersionKeyD5434FC9" 94 | } 95 | ], 96 | "/SociIndexBuildIntegTest/AssetParameters/9b19c7d81a94bbc19fe2164453716f4d7f651b21f923c5c264fe80f5a939e1db/ArtifactHash": [ 97 | { 98 | "type": "aws:cdk:logicalId", 99 | "data": "AssetParameters9b19c7d81a94bbc19fe2164453716f4d7f651b21f923c5c264fe80f5a939e1dbArtifactHashA955B34A" 100 | } 101 | ], 102 | "/SociIndexBuildIntegTest/SociIndexBuild024cf76a10034aa4aa4b12c32c09ca3camd64/Role/Resource": [ 103 | { 104 | "type": "aws:cdk:logicalId", 105 | "data": "SociIndexBuild024cf76a10034aa4aa4b12c32c09ca3camd64RoleBDD942DA" 106 | } 107 | ], 108 | "/SociIndexBuildIntegTest/SociIndexBuild024cf76a10034aa4aa4b12c32c09ca3camd64/Role/DefaultPolicy/Resource": [ 109 | { 110 | "type": "aws:cdk:logicalId", 111 | "data": "SociIndexBuild024cf76a10034aa4aa4b12c32c09ca3camd64RoleDefaultPolicy93E560B4" 112 | } 113 | ], 114 | "/SociIndexBuildIntegTest/SociIndexBuild024cf76a10034aa4aa4b12c32c09ca3camd64/Resource": [ 115 | { 116 | "type": "aws:cdk:logicalId", 117 | "data": "SociIndexBuild024cf76a10034aa4aa4b12c32c09ca3camd642F4A8EFA" 118 | } 119 | ], 120 | "/SociIndexBuildIntegTest/Image2/Index/Resource/Default": [ 121 | { 122 | "type": "aws:cdk:logicalId", 123 | "data": "Image2Index42EE498C" 124 | } 125 | ] 126 | }, 127 | "displayName": "SociIndexBuildIntegTest" 128 | }, 129 | "TestDefaultTestDeployAssert12909640": { 130 | "type": "aws:cloudformation:stack", 131 | "environment": "aws://unknown-account/unknown-region", 132 | "properties": { 133 | "templateFile": "TestDefaultTestDeployAssert12909640.template.json", 134 | "validateOnSynth": false 135 | }, 136 | "displayName": "Test/DefaultTest/DeployAssert" 137 | } 138 | } 139 | } -------------------------------------------------------------------------------- /tsconfig.dev.json: -------------------------------------------------------------------------------- 1 | // ~~ Generated by projen. To modify, edit .projenrc.ts and run "npx projen". 2 | { 3 | "compilerOptions": { 4 | "alwaysStrict": true, 5 | "declaration": true, 6 | "esModuleInterop": true, 7 | "experimentalDecorators": true, 8 | "inlineSourceMap": true, 9 | "inlineSources": true, 10 | "lib": [ 11 | "es2019" 12 | ], 13 | "module": "CommonJS", 14 | "noEmitOnError": false, 15 | "noFallthroughCasesInSwitch": true, 16 | "noImplicitAny": true, 17 | "noImplicitReturns": true, 18 | "noImplicitThis": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "resolveJsonModule": true, 22 | "strict": true, 23 | "strictNullChecks": true, 24 | "strictPropertyInitialization": true, 25 | "stripInternal": true, 26 | "target": "ES2019" 27 | }, 28 | "include": [ 29 | "src/**/*.ts", 30 | "test/**/*.ts", 31 | ".projenrc.ts", 32 | "projenrc/**/*.ts" 33 | ], 34 | "exclude": [ 35 | "node_modules", 36 | "example", 37 | "test/*.integ.snapshot" 38 | ] 39 | } 40 | --------------------------------------------------------------------------------