├── .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 ├── CHANGELOG.md ├── LICENSE ├── Makefile ├── README.md ├── package.json ├── packages ├── ecs-app-update │ └── action.yml └── ecs-run-task │ └── action.yml ├── src ├── constructs │ ├── ecs │ │ ├── app │ │ │ └── index.ts │ │ └── base │ │ │ └── index.ts │ └── internal │ │ ├── alb │ │ └── index.ts │ │ ├── bastion │ │ └── index.ts │ │ ├── customResources │ │ └── highestPriorityRule │ │ │ ├── custom-resource-handler.py │ │ │ └── index.ts │ │ ├── ec │ │ └── index.ts │ │ ├── ecs │ │ ├── iam │ │ │ └── index.ts │ │ ├── management-command │ │ │ └── index.ts │ │ ├── scheduler │ │ │ └── index.ts │ │ ├── web │ │ │ └── index.ts │ │ └── worker │ │ │ └── index.ts │ │ ├── rds │ │ └── index.ts │ │ ├── s3 │ │ └── index.ts │ │ ├── sg │ │ └── index.ts │ │ └── vpc │ │ └── index.ts ├── examples │ └── ecs │ │ ├── .env.example │ │ ├── .gitignore │ │ ├── config │ │ ├── alpha.json │ │ └── dev.json │ │ └── index.ts ├── index.ts └── utils │ └── priority │ └── index.ts ├── test └── .gitignore ├── tsconfig.dev.json └── yarn.lock /.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 | "@stylistic" 12 | ], 13 | "parser": "@typescript-eslint/parser", 14 | "parserOptions": { 15 | "ecmaVersion": 2018, 16 | "sourceType": "module", 17 | "project": "./tsconfig.dev.json" 18 | }, 19 | "extends": [ 20 | "plugin:import/typescript" 21 | ], 22 | "settings": { 23 | "import/parsers": { 24 | "@typescript-eslint/parser": [ 25 | ".ts", 26 | ".tsx" 27 | ] 28 | }, 29 | "import/resolver": { 30 | "node": {}, 31 | "typescript": { 32 | "project": "./tsconfig.dev.json", 33 | "alwaysTryTypes": true 34 | } 35 | } 36 | }, 37 | "ignorePatterns": [ 38 | "*.js", 39 | "*.d.ts", 40 | "node_modules/", 41 | "*.generated.ts", 42 | "coverage", 43 | "!.projenrc.ts", 44 | "!projenrc/**/*.ts" 45 | ], 46 | "rules": { 47 | "indent": [ 48 | "off" 49 | ], 50 | "@stylistic/indent": [ 51 | "error", 52 | 2 53 | ], 54 | "quotes": [ 55 | "error", 56 | "single", 57 | { 58 | "avoidEscape": true 59 | } 60 | ], 61 | "comma-dangle": [ 62 | "error", 63 | "always-multiline" 64 | ], 65 | "comma-spacing": [ 66 | "error", 67 | { 68 | "before": false, 69 | "after": true 70 | } 71 | ], 72 | "no-multi-spaces": [ 73 | "error", 74 | { 75 | "ignoreEOLComments": false 76 | } 77 | ], 78 | "array-bracket-spacing": [ 79 | "error", 80 | "never" 81 | ], 82 | "array-bracket-newline": [ 83 | "error", 84 | "consistent" 85 | ], 86 | "object-curly-spacing": [ 87 | "error", 88 | "always" 89 | ], 90 | "object-curly-newline": [ 91 | "error", 92 | { 93 | "multiline": true, 94 | "consistent": true 95 | } 96 | ], 97 | "object-property-newline": [ 98 | "error", 99 | { 100 | "allowAllPropertiesOnSameLine": true 101 | } 102 | ], 103 | "keyword-spacing": [ 104 | "error" 105 | ], 106 | "brace-style": [ 107 | "error", 108 | "1tbs", 109 | { 110 | "allowSingleLine": true 111 | } 112 | ], 113 | "space-before-blocks": [ 114 | "error" 115 | ], 116 | "curly": [ 117 | "error", 118 | "multi-line", 119 | "consistent" 120 | ], 121 | "@stylistic/member-delimiter-style": [ 122 | "error" 123 | ], 124 | "semi": [ 125 | "error", 126 | "always" 127 | ], 128 | "max-len": [ 129 | "error", 130 | { 131 | "code": 150, 132 | "ignoreUrls": true, 133 | "ignoreStrings": true, 134 | "ignoreTemplateLiterals": true, 135 | "ignoreComments": true, 136 | "ignoreRegExpLiterals": true 137 | } 138 | ], 139 | "quote-props": [ 140 | "error", 141 | "consistent-as-needed" 142 | ], 143 | "@typescript-eslint/no-require-imports": [ 144 | "error" 145 | ], 146 | "import/no-extraneous-dependencies": [ 147 | "error", 148 | { 149 | "devDependencies": [ 150 | "**/test/**", 151 | "**/build-tools/**", 152 | ".projenrc.ts", 153 | "projenrc/**/*.ts" 154 | ], 155 | "optionalDependencies": false, 156 | "peerDependencies": true 157 | } 158 | ], 159 | "import/no-unresolved": [ 160 | "error" 161 | ], 162 | "import/order": [ 163 | "warn", 164 | { 165 | "groups": [ 166 | "builtin", 167 | "external" 168 | ], 169 | "alphabetize": { 170 | "order": "asc", 171 | "caseInsensitive": true 172 | } 173 | } 174 | ], 175 | "import/no-duplicates": [ 176 | "error" 177 | ], 178 | "no-shadow": [ 179 | "off" 180 | ], 181 | "@typescript-eslint/no-shadow": [ 182 | "error" 183 | ], 184 | "key-spacing": [ 185 | "error" 186 | ], 187 | "no-multiple-empty-lines": [ 188 | "error" 189 | ], 190 | "@typescript-eslint/no-floating-promises": [ 191 | "error" 192 | ], 193 | "no-return-await": [ 194 | "off" 195 | ], 196 | "@typescript-eslint/return-await": [ 197 | "error" 198 | ], 199 | "no-trailing-spaces": [ 200 | "error" 201 | ], 202 | "dot-notation": [ 203 | "error" 204 | ], 205 | "no-bitwise": [ 206 | "error" 207 | ], 208 | "@typescript-eslint/member-ordering": [ 209 | "error", 210 | { 211 | "default": [ 212 | "public-static-field", 213 | "public-static-method", 214 | "protected-static-field", 215 | "protected-static-method", 216 | "private-static-field", 217 | "private-static-method", 218 | "field", 219 | "constructor", 220 | "method" 221 | ] 222 | } 223 | ] 224 | }, 225 | "overrides": [ 226 | { 227 | "files": [ 228 | ".projenrc.ts" 229 | ], 230 | "rules": { 231 | "@typescript-eslint/no-require-imports": "off", 232 | "import/no-extraneous-dependencies": "off" 233 | } 234 | } 235 | ] 236 | } 237 | -------------------------------------------------------------------------------- /.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.json linguist-generated 22 | /tsconfig.dev.json linguist-generated 23 | /yarn.lock 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: yarn install --check-files 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 && yarn install --check-files --frozen-lockfile 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 | -------------------------------------------------------------------------------- /.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 | docs 31 | ci 32 | requireScope: false 33 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.ts and run "npx projen". 2 | 3 | name: release 4 | on: 5 | workflow_dispatch: {} 6 | concurrency: 7 | group: ${{ github.workflow }} 8 | cancel-in-progress: false 9 | jobs: 10 | release: 11 | runs-on: ubuntu-latest 12 | permissions: 13 | contents: write 14 | outputs: 15 | latest_commit: ${{ steps.git_remote.outputs.latest_commit }} 16 | tag_exists: ${{ steps.check_tag_exists.outputs.exists }} 17 | env: 18 | CI: "true" 19 | steps: 20 | - name: Checkout 21 | uses: actions/checkout@v4 22 | with: 23 | fetch-depth: 0 24 | - name: Set git identity 25 | run: |- 26 | git config user.name "github-actions" 27 | git config user.email "github-actions@github.com" 28 | - name: Setup Node.js 29 | uses: actions/setup-node@v4 30 | with: 31 | node-version: lts/* 32 | - name: Install dependencies 33 | run: yarn install --check-files --frozen-lockfile 34 | - name: release 35 | run: npx projen release 36 | - name: Check if version has already been tagged 37 | id: check_tag_exists 38 | run: |- 39 | TAG=$(cat dist/releasetag.txt) 40 | ([ ! -z "$TAG" ] && git ls-remote -q --exit-code --tags origin $TAG && (echo "exists=true" >> $GITHUB_OUTPUT)) || (echo "exists=false" >> $GITHUB_OUTPUT) 41 | cat $GITHUB_OUTPUT 42 | - name: Check for new commits 43 | id: git_remote 44 | run: |- 45 | echo "latest_commit=$(git ls-remote origin -h ${{ github.ref }} | cut -f1)" >> $GITHUB_OUTPUT 46 | cat $GITHUB_OUTPUT 47 | - name: Backup artifact permissions 48 | if: ${{ steps.git_remote.outputs.latest_commit == github.sha }} 49 | run: cd dist && getfacl -R . > permissions-backup.acl 50 | continue-on-error: true 51 | - name: Upload artifact 52 | if: ${{ steps.git_remote.outputs.latest_commit == github.sha }} 53 | uses: actions/upload-artifact@v4.4.0 54 | with: 55 | name: build-artifact 56 | path: dist 57 | overwrite: true 58 | - name: Publish tag 59 | run: npx projen publish:git 60 | release_github: 61 | name: Publish to GitHub Releases 62 | needs: 63 | - release 64 | - release_npm 65 | runs-on: ubuntu-latest 66 | permissions: 67 | contents: write 68 | if: needs.release.outputs.tag_exists != 'true' && needs.release.outputs.latest_commit == github.sha 69 | steps: 70 | - uses: actions/setup-node@v4 71 | with: 72 | node-version: lts/* 73 | - name: Download build artifacts 74 | uses: actions/download-artifact@v4 75 | with: 76 | name: build-artifact 77 | path: dist 78 | - name: Restore build artifact permissions 79 | run: cd dist && setfacl --restore=permissions-backup.acl 80 | continue-on-error: true 81 | - name: Release 82 | env: 83 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 84 | GITHUB_REPOSITORY: ${{ github.repository }} 85 | GITHUB_REF: ${{ github.sha }} 86 | 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 87 | release_npm: 88 | name: Publish to npm 89 | needs: release 90 | runs-on: ubuntu-latest 91 | permissions: 92 | id-token: write 93 | contents: read 94 | if: needs.release.outputs.tag_exists != 'true' && needs.release.outputs.latest_commit == github.sha 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 | path: .repo 111 | - name: Install Dependencies 112 | run: cd .repo && yarn install --check-files --frozen-lockfile 113 | - name: Extract build artifact 114 | run: tar --strip-components=1 -xzvf dist/js/*.tgz -C .repo 115 | - name: Move build artifact out of the way 116 | run: mv dist dist.old 117 | - name: Create js artifact 118 | run: cd .repo && npx projen package:js 119 | - name: Collect js artifact 120 | run: mv .repo/dist dist 121 | - name: Release 122 | env: 123 | NPM_DIST_TAG: latest 124 | NPM_REGISTRY: registry.npmjs.org 125 | NPM_CONFIG_PROVENANCE: "true" 126 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 127 | run: npx -p publib@latest publib-npm 128 | -------------------------------------------------------------------------------- /.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: yarn install --check-files --frozen-lockfile 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 | cdk.out 34 | notes 35 | app.yml 36 | base.yml 37 | cdk.context.json 38 | /test-reports/ 39 | junit.xml 40 | /coverage/ 41 | !/.github/workflows/build.yml 42 | /dist/changelog.md 43 | /dist/version.txt 44 | !/.github/workflows/release.yml 45 | !/.mergify.yml 46 | !/.github/workflows/upgrade-main.yml 47 | !/.github/pull_request_template.md 48 | !/test/ 49 | !/tsconfig.dev.json 50 | !/src/ 51 | /lib 52 | /dist/ 53 | !/.eslintrc.json 54 | .jsii 55 | tsconfig.json 56 | !/API.md 57 | !/.projenrc.ts 58 | -------------------------------------------------------------------------------- /.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 | merge_method: squash 12 | commit_message_template: |- 13 | {{ title }} (#{{ number }}) 14 | 15 | {{ body }} 16 | pull_request_rules: 17 | - name: Automatic merge on approval and successful build 18 | actions: 19 | delete_head_branch: {} 20 | queue: 21 | name: default 22 | conditions: 23 | - "#approved-reviews-by>=1" 24 | - -label~=(do-not-merge) 25 | - status-success=build 26 | - status-success=package-js 27 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.ts and run "npx projen". 2 | .npmrc 3 | .nvmrc 4 | .versionrc 5 | .gitattributes 6 | *.tgz 7 | *.gz 8 | *.zip 9 | cdk.out 10 | .cdk.staging 11 | /examples 12 | PUBLISHING.md 13 | .vscode 14 | .projenrc.ts 15 | projenrc 16 | /images 17 | API.md 18 | CHANGELOG.md 19 | CONTRIBUTING.md 20 | SECURITY.md 21 | /.projen/ 22 | /test-reports/ 23 | junit.xml 24 | /coverage/ 25 | permissions-backup.acl 26 | /dist/changelog.md 27 | /dist/version.txt 28 | /.mergify.yml 29 | /test/ 30 | /tsconfig.dev.json 31 | /src/ 32 | !/lib/ 33 | !/lib/**/*.js 34 | !/lib/**/*.d.ts 35 | dist 36 | /tsconfig.json 37 | /.github/ 38 | /.vscode/ 39 | /.idea/ 40 | /.projenrc.js 41 | tsconfig.tsbuildinfo 42 | /.eslintrc.json 43 | !.jsii 44 | /.gitattributes 45 | /.projenrc.ts 46 | /projenrc 47 | -------------------------------------------------------------------------------- /.projen/deps.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": [ 3 | { 4 | "name": "@stylistic/eslint-plugin", 5 | "version": "^2", 6 | "type": "build" 7 | }, 8 | { 9 | "name": "@types/jest", 10 | "type": "build" 11 | }, 12 | { 13 | "name": "@types/node", 14 | "type": "build" 15 | }, 16 | { 17 | "name": "@typescript-eslint/eslint-plugin", 18 | "version": "^8", 19 | "type": "build" 20 | }, 21 | { 22 | "name": "@typescript-eslint/parser", 23 | "version": "^8", 24 | "type": "build" 25 | }, 26 | { 27 | "name": "commit-and-tag-version", 28 | "version": "^12", 29 | "type": "build" 30 | }, 31 | { 32 | "name": "eslint-import-resolver-typescript", 33 | "type": "build" 34 | }, 35 | { 36 | "name": "eslint-plugin-import", 37 | "type": "build" 38 | }, 39 | { 40 | "name": "eslint", 41 | "version": "^9", 42 | "type": "build" 43 | }, 44 | { 45 | "name": "jest", 46 | "type": "build" 47 | }, 48 | { 49 | "name": "jest-junit", 50 | "version": "^16", 51 | "type": "build" 52 | }, 53 | { 54 | "name": "jsii-diff", 55 | "type": "build" 56 | }, 57 | { 58 | "name": "jsii-docgen", 59 | "version": "10.x", 60 | "type": "build" 61 | }, 62 | { 63 | "name": "jsii-pacmak", 64 | "type": "build" 65 | }, 66 | { 67 | "name": "jsii-rosetta", 68 | "version": "5.x", 69 | "type": "build" 70 | }, 71 | { 72 | "name": "jsii", 73 | "version": "~5.6.0", 74 | "type": "build" 75 | }, 76 | { 77 | "name": "projen", 78 | "type": "build" 79 | }, 80 | { 81 | "name": "ts-jest", 82 | "type": "build" 83 | }, 84 | { 85 | "name": "ts-node", 86 | "type": "build" 87 | }, 88 | { 89 | "name": "typescript", 90 | "type": "build" 91 | }, 92 | { 93 | "name": "aws-cdk-lib", 94 | "version": "^2.178.1", 95 | "type": "peer" 96 | }, 97 | { 98 | "name": "constructs", 99 | "version": "^10.0.5", 100 | "type": "peer" 101 | }, 102 | { 103 | "name": "aws-sdk", 104 | "version": "2.1692.0", 105 | "type": "runtime" 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": "jsii --silence-warnings=reserved-word" 92 | } 93 | ] 94 | }, 95 | "default": { 96 | "name": "default", 97 | "description": "Synthesize project files", 98 | "steps": [ 99 | { 100 | "exec": "ts-node --project tsconfig.dev.json .projenrc.ts" 101 | } 102 | ] 103 | }, 104 | "docgen": { 105 | "name": "docgen", 106 | "description": "Generate API.md from .jsii manifest", 107 | "steps": [ 108 | { 109 | "exec": "jsii-docgen -o API.md" 110 | } 111 | ] 112 | }, 113 | "eject": { 114 | "name": "eject", 115 | "description": "Remove projen from the project", 116 | "env": { 117 | "PROJEN_EJECTING": "true" 118 | }, 119 | "steps": [ 120 | { 121 | "spawn": "default" 122 | } 123 | ] 124 | }, 125 | "eslint": { 126 | "name": "eslint", 127 | "description": "Runs eslint against the codebase", 128 | "env": { 129 | "ESLINT_USE_FLAT_CONFIG": "false" 130 | }, 131 | "steps": [ 132 | { 133 | "exec": "eslint --ext .ts,.tsx --fix --no-error-on-unmatched-pattern $@ src test build-tools projenrc .projenrc.ts", 134 | "receiveArgs": true 135 | } 136 | ] 137 | }, 138 | "install": { 139 | "name": "install", 140 | "description": "Install project dependencies and update lockfile (non-frozen)", 141 | "steps": [ 142 | { 143 | "exec": "yarn install --check-files" 144 | } 145 | ] 146 | }, 147 | "install:ci": { 148 | "name": "install:ci", 149 | "description": "Install project dependencies using frozen lockfile", 150 | "steps": [ 151 | { 152 | "exec": "yarn install --check-files --frozen-lockfile" 153 | } 154 | ] 155 | }, 156 | "package": { 157 | "name": "package", 158 | "description": "Creates the distribution package", 159 | "steps": [ 160 | { 161 | "spawn": "package:js", 162 | "condition": "node -e \"if (!process.env.CI) process.exit(1)\"" 163 | }, 164 | { 165 | "spawn": "package-all", 166 | "condition": "node -e \"if (process.env.CI) process.exit(1)\"" 167 | } 168 | ] 169 | }, 170 | "package-all": { 171 | "name": "package-all", 172 | "description": "Packages artifacts for all target languages", 173 | "steps": [ 174 | { 175 | "spawn": "package:js" 176 | } 177 | ] 178 | }, 179 | "package:js": { 180 | "name": "package:js", 181 | "description": "Create js language bindings", 182 | "steps": [ 183 | { 184 | "exec": "jsii-pacmak -v --target js" 185 | } 186 | ] 187 | }, 188 | "post-compile": { 189 | "name": "post-compile", 190 | "description": "Runs after successful compilation", 191 | "steps": [ 192 | { 193 | "spawn": "docgen" 194 | } 195 | ] 196 | }, 197 | "post-upgrade": { 198 | "name": "post-upgrade", 199 | "description": "Runs after upgrading dependencies" 200 | }, 201 | "pre-compile": { 202 | "name": "pre-compile", 203 | "description": "Prepare the project for compilation" 204 | }, 205 | "publish:git": { 206 | "name": "publish:git", 207 | "description": "Prepends the release changelog onto the project changelog, creates a release commit, and tags the release", 208 | "env": { 209 | "CHANGELOG": "dist/changelog.md", 210 | "RELEASE_TAG_FILE": "dist/releasetag.txt", 211 | "PROJECT_CHANGELOG_FILE": "CHANGELOG.md", 212 | "VERSION_FILE": "dist/version.txt" 213 | }, 214 | "steps": [ 215 | { 216 | "builtin": "release/update-changelog" 217 | }, 218 | { 219 | "builtin": "release/tag-version" 220 | }, 221 | { 222 | "exec": "git push --follow-tags origin main" 223 | } 224 | ], 225 | "condition": "git log --oneline -1 | grep -qv \"chore(release):\"" 226 | }, 227 | "release": { 228 | "name": "release", 229 | "description": "Prepare a release from \"main\" branch", 230 | "env": { 231 | "RELEASE": "true", 232 | "MAJOR": "1" 233 | }, 234 | "steps": [ 235 | { 236 | "exec": "rm -fr dist" 237 | }, 238 | { 239 | "spawn": "bump" 240 | }, 241 | { 242 | "spawn": "build" 243 | }, 244 | { 245 | "spawn": "unbump" 246 | }, 247 | { 248 | "exec": "git diff --ignore-space-at-eol --exit-code" 249 | } 250 | ] 251 | }, 252 | "test": { 253 | "name": "test", 254 | "description": "Run tests", 255 | "steps": [ 256 | { 257 | "exec": "jest --passWithNoTests --updateSnapshot", 258 | "receiveArgs": true 259 | }, 260 | { 261 | "spawn": "eslint" 262 | } 263 | ] 264 | }, 265 | "test:watch": { 266 | "name": "test:watch", 267 | "description": "Run jest in watch mode", 268 | "steps": [ 269 | { 270 | "exec": "jest --watch" 271 | } 272 | ] 273 | }, 274 | "unbump": { 275 | "name": "unbump", 276 | "description": "Restores version to 0.0.0", 277 | "env": { 278 | "OUTFILE": "package.json", 279 | "CHANGELOG": "dist/changelog.md", 280 | "BUMPFILE": "dist/version.txt", 281 | "RELEASETAG": "dist/releasetag.txt", 282 | "RELEASE_TAG_PREFIX": "", 283 | "BUMP_PACKAGE": "commit-and-tag-version@^12" 284 | }, 285 | "steps": [ 286 | { 287 | "builtin": "release/reset-version" 288 | } 289 | ] 290 | }, 291 | "upgrade": { 292 | "name": "upgrade", 293 | "description": "upgrade dependencies", 294 | "env": { 295 | "CI": "0" 296 | }, 297 | "steps": [ 298 | { 299 | "exec": "npx npm-check-updates@16 --upgrade --target=minor --peer --no-deprecated --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" 300 | }, 301 | { 302 | "exec": "yarn install --check-files" 303 | }, 304 | { 305 | "exec": "yarn upgrade @stylistic/eslint-plugin @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 aws-sdk" 306 | }, 307 | { 308 | "exec": "npx projen" 309 | }, 310 | { 311 | "spawn": "post-upgrade" 312 | } 313 | ] 314 | }, 315 | "watch": { 316 | "name": "watch", 317 | "description": "Watch & compile in the background", 318 | "steps": [ 319 | { 320 | "exec": "jsii -w --silence-warnings=reserved-word" 321 | } 322 | ] 323 | } 324 | }, 325 | "env": { 326 | "PATH": "$(npx -c \"node --print process.env.PATH\")" 327 | }, 328 | "//": "~~ Generated by projen. To modify, edit .projenrc.ts and run \"npx projen\"." 329 | } 330 | -------------------------------------------------------------------------------- /.projenrc.ts: -------------------------------------------------------------------------------- 1 | import { awscdk, release } from 'projen'; 2 | const project = new awscdk.AwsCdkConstructLibrary({ 3 | author: 'Brian Caffey', 4 | authorEmail: 'briancaffey2010@gmail.com', 5 | projenrcTs: true, 6 | projenrcTsOptions: { 7 | filename: '.projenrc.ts', 8 | }, 9 | authorAddress: '', 10 | authorUrl: 'https://briancaffey.github.io', 11 | license: 'MIT', 12 | copyrightOwner: 'Brian Caffey', 13 | cdkVersion: '2.178.1', 14 | deps: ['aws-sdk@2.1692.0'], 15 | defaultReleaseBranch: 'main', 16 | name: 'cdk-django', 17 | repositoryUrl: 'git@github.com:briancaffey/cdk-django.git', 18 | 19 | // Automation 20 | githubOptions: { 21 | // projenCredentials: github.GithubCredentials.fromApp(), 22 | pullRequestLintOptions: { 23 | semanticTitleOptions: { 24 | types: ['feat', 'fix', 'chore', 'docs', 'ci'], 25 | }, 26 | }, 27 | }, 28 | // https://github.com/projen/projen/issues/1941 29 | // bundledDeps: ['@types/jest@27.4.1'], 30 | majorVersion: 1, 31 | releaseTrigger: { 32 | isContinuous: false, 33 | } as release.ReleaseTrigger, 34 | 35 | // ignore 36 | gitignore: ['cdk.out', 'notes', 'app.yml', 'base.yml', 'cdk.context.json'], 37 | npmignore: [ 38 | '.npmrc', 39 | '.nvmrc', 40 | '.versionrc', 41 | '.gitattributes', 42 | '*.tgz', 43 | '*.gz', 44 | '*.zip', 45 | 'cdk.out', 46 | '.cdk.staging', 47 | '/examples', 48 | 'PUBLISHING.md', 49 | '.vscode', 50 | '.projenrc.ts', 51 | 'projenrc', 52 | '/images', 53 | 'API.md', 54 | 'CHANGELOG.md', 55 | 'CONTRIBUTING.md', 56 | 'SECURITY.md', 57 | ], 58 | description: 'CDK construct library for building Django applications on AWS with ECS Fargate', 59 | packageName: 'cdk-django', 60 | }); 61 | 62 | // release only via manual trigger 63 | project.release?.publisher?.publishToGit({ 64 | changelogFile: 'dist/changelog.md', 65 | versionFile: 'dist/version.txt', 66 | releaseTagFile: 'dist/releasetag.txt', 67 | projectChangelogFile: 'CHANGELOG.md', 68 | gitBranch: 'main', 69 | }); 70 | project.tryFindObjectFile('.github/workflows/release.yml')?.addToArray( 71 | 'jobs.release.steps', 72 | { 73 | name: 'Publish tag', 74 | run: 'npx projen publish:git', 75 | }, 76 | ); 77 | 78 | project.addDevDeps('jsii-docgen@10.x'); 79 | project.addDevDeps('jsii-rosetta@5.x'); 80 | 81 | project.synth(); -------------------------------------------------------------------------------- /API.md: -------------------------------------------------------------------------------- 1 | # API Reference 2 | 3 | ## Constructs 4 | 5 | ### EcsApp 6 | 7 | #### Initializers 8 | 9 | ```typescript 10 | import { EcsApp } from 'cdk-django' 11 | 12 | new EcsApp(scope: Construct, id: string, props: EcsAppProps) 13 | ``` 14 | 15 | | **Name** | **Type** | **Description** | 16 | | --- | --- | --- | 17 | | scope | constructs.Construct | *No description.* | 18 | | id | string | *No description.* | 19 | | props | EcsAppProps | *No description.* | 20 | 21 | --- 22 | 23 | ##### `scope`Required 24 | 25 | - *Type:* constructs.Construct 26 | 27 | --- 28 | 29 | ##### `id`Required 30 | 31 | - *Type:* string 32 | 33 | --- 34 | 35 | ##### `props`Required 36 | 37 | - *Type:* EcsAppProps 38 | 39 | --- 40 | 41 | #### Methods 42 | 43 | | **Name** | **Description** | 44 | | --- | --- | 45 | | toString | Returns a string representation of this construct. | 46 | 47 | --- 48 | 49 | ##### `toString` 50 | 51 | ```typescript 52 | public toString(): string 53 | ``` 54 | 55 | Returns a string representation of this construct. 56 | 57 | #### Static Functions 58 | 59 | | **Name** | **Description** | 60 | | --- | --- | 61 | | isConstruct | Checks if `x` is a construct. | 62 | 63 | --- 64 | 65 | ##### ~~`isConstruct`~~ 66 | 67 | ```typescript 68 | import { EcsApp } from 'cdk-django' 69 | 70 | EcsApp.isConstruct(x: any) 71 | ``` 72 | 73 | Checks if `x` is a construct. 74 | 75 | ###### `x`Required 76 | 77 | - *Type:* any 78 | 79 | Any object. 80 | 81 | --- 82 | 83 | #### Properties 84 | 85 | | **Name** | **Type** | **Description** | 86 | | --- | --- | --- | 87 | | node | constructs.Node | The tree node. | 88 | 89 | --- 90 | 91 | ##### `node`Required 92 | 93 | ```typescript 94 | public readonly node: Node; 95 | ``` 96 | 97 | - *Type:* constructs.Node 98 | 99 | The tree node. 100 | 101 | --- 102 | 103 | 104 | ### EcsBase 105 | 106 | #### Initializers 107 | 108 | ```typescript 109 | import { EcsBase } from 'cdk-django' 110 | 111 | new EcsBase(scope: Construct, id: string, props: EcsBaseProps) 112 | ``` 113 | 114 | | **Name** | **Type** | **Description** | 115 | | --- | --- | --- | 116 | | scope | constructs.Construct | *No description.* | 117 | | id | string | *No description.* | 118 | | props | EcsBaseProps | *No description.* | 119 | 120 | --- 121 | 122 | ##### `scope`Required 123 | 124 | - *Type:* constructs.Construct 125 | 126 | --- 127 | 128 | ##### `id`Required 129 | 130 | - *Type:* string 131 | 132 | --- 133 | 134 | ##### `props`Required 135 | 136 | - *Type:* EcsBaseProps 137 | 138 | --- 139 | 140 | #### Methods 141 | 142 | | **Name** | **Description** | 143 | | --- | --- | 144 | | toString | Returns a string representation of this construct. | 145 | 146 | --- 147 | 148 | ##### `toString` 149 | 150 | ```typescript 151 | public toString(): string 152 | ``` 153 | 154 | Returns a string representation of this construct. 155 | 156 | #### Static Functions 157 | 158 | | **Name** | **Description** | 159 | | --- | --- | 160 | | isConstruct | Checks if `x` is a construct. | 161 | 162 | --- 163 | 164 | ##### ~~`isConstruct`~~ 165 | 166 | ```typescript 167 | import { EcsBase } from 'cdk-django' 168 | 169 | EcsBase.isConstruct(x: any) 170 | ``` 171 | 172 | Checks if `x` is a construct. 173 | 174 | ###### `x`Required 175 | 176 | - *Type:* any 177 | 178 | Any object. 179 | 180 | --- 181 | 182 | #### Properties 183 | 184 | | **Name** | **Type** | **Description** | 185 | | --- | --- | --- | 186 | | node | constructs.Node | The tree node. | 187 | | alb | aws-cdk-lib.aws_elasticloadbalancingv2.ApplicationLoadBalancer | *No description.* | 188 | | albSecurityGroup | aws-cdk-lib.aws_ec2.SecurityGroup | *No description.* | 189 | | appSecurityGroup | aws-cdk-lib.aws_ec2.SecurityGroup | *No description.* | 190 | | assetsBucket | aws-cdk-lib.aws_s3.Bucket | *No description.* | 191 | | databaseInstance | aws-cdk-lib.aws_rds.DatabaseInstance | *No description.* | 192 | | domainName | string | *No description.* | 193 | | elastiCacheHostname | string | *No description.* | 194 | | listener | aws-cdk-lib.aws_elasticloadbalancingv2.ApplicationListener | *No description.* | 195 | | rdsPasswordSecretName | string | *No description.* | 196 | | vpc | aws-cdk-lib.aws_ec2.IVpc | *No description.* | 197 | 198 | --- 199 | 200 | ##### `node`Required 201 | 202 | ```typescript 203 | public readonly node: Node; 204 | ``` 205 | 206 | - *Type:* constructs.Node 207 | 208 | The tree node. 209 | 210 | --- 211 | 212 | ##### `alb`Required 213 | 214 | ```typescript 215 | public readonly alb: ApplicationLoadBalancer; 216 | ``` 217 | 218 | - *Type:* aws-cdk-lib.aws_elasticloadbalancingv2.ApplicationLoadBalancer 219 | 220 | --- 221 | 222 | ##### `albSecurityGroup`Required 223 | 224 | ```typescript 225 | public readonly albSecurityGroup: SecurityGroup; 226 | ``` 227 | 228 | - *Type:* aws-cdk-lib.aws_ec2.SecurityGroup 229 | 230 | --- 231 | 232 | ##### `appSecurityGroup`Required 233 | 234 | ```typescript 235 | public readonly appSecurityGroup: SecurityGroup; 236 | ``` 237 | 238 | - *Type:* aws-cdk-lib.aws_ec2.SecurityGroup 239 | 240 | --- 241 | 242 | ##### `assetsBucket`Required 243 | 244 | ```typescript 245 | public readonly assetsBucket: Bucket; 246 | ``` 247 | 248 | - *Type:* aws-cdk-lib.aws_s3.Bucket 249 | 250 | --- 251 | 252 | ##### `databaseInstance`Required 253 | 254 | ```typescript 255 | public readonly databaseInstance: DatabaseInstance; 256 | ``` 257 | 258 | - *Type:* aws-cdk-lib.aws_rds.DatabaseInstance 259 | 260 | --- 261 | 262 | ##### `domainName`Required 263 | 264 | ```typescript 265 | public readonly domainName: string; 266 | ``` 267 | 268 | - *Type:* string 269 | 270 | --- 271 | 272 | ##### `elastiCacheHostname`Required 273 | 274 | ```typescript 275 | public readonly elastiCacheHostname: string; 276 | ``` 277 | 278 | - *Type:* string 279 | 280 | --- 281 | 282 | ##### `listener`Required 283 | 284 | ```typescript 285 | public readonly listener: ApplicationListener; 286 | ``` 287 | 288 | - *Type:* aws-cdk-lib.aws_elasticloadbalancingv2.ApplicationListener 289 | 290 | --- 291 | 292 | ##### `rdsPasswordSecretName`Required 293 | 294 | ```typescript 295 | public readonly rdsPasswordSecretName: string; 296 | ``` 297 | 298 | - *Type:* string 299 | 300 | --- 301 | 302 | ##### `vpc`Required 303 | 304 | ```typescript 305 | public readonly vpc: IVpc; 306 | ``` 307 | 308 | - *Type:* aws-cdk-lib.aws_ec2.IVpc 309 | 310 | --- 311 | 312 | 313 | ## Structs 314 | 315 | ### EcsAppProps 316 | 317 | #### Initializer 318 | 319 | ```typescript 320 | import { EcsAppProps } from 'cdk-django' 321 | 322 | const ecsAppProps: EcsAppProps = { ... } 323 | ``` 324 | 325 | #### Properties 326 | 327 | | **Name** | **Type** | **Description** | 328 | | --- | --- | --- | 329 | | alb | aws-cdk-lib.aws_elasticloadbalancingv2.IApplicationLoadBalancer | *No description.* | 330 | | appSecurityGroup | aws-cdk-lib.aws_ec2.ISecurityGroup | *No description.* | 331 | | assetsBucket | aws-cdk-lib.aws_s3.Bucket | *No description.* | 332 | | baseStackName | string | *No description.* | 333 | | companyName | string | *No description.* | 334 | | domainName | string | *No description.* | 335 | | elastiCacheHost | string | *No description.* | 336 | | listener | aws-cdk-lib.aws_elasticloadbalancingv2.ApplicationListener | *No description.* | 337 | | rdsInstance | aws-cdk-lib.aws_rds.DatabaseInstance | *No description.* | 338 | | rdsPasswordSecretName | string | *No description.* | 339 | | vpc | aws-cdk-lib.aws_ec2.IVpc | *No description.* | 340 | 341 | --- 342 | 343 | ##### `alb`Required 344 | 345 | ```typescript 346 | public readonly alb: IApplicationLoadBalancer; 347 | ``` 348 | 349 | - *Type:* aws-cdk-lib.aws_elasticloadbalancingv2.IApplicationLoadBalancer 350 | 351 | --- 352 | 353 | ##### `appSecurityGroup`Required 354 | 355 | ```typescript 356 | public readonly appSecurityGroup: ISecurityGroup; 357 | ``` 358 | 359 | - *Type:* aws-cdk-lib.aws_ec2.ISecurityGroup 360 | 361 | --- 362 | 363 | ##### `assetsBucket`Required 364 | 365 | ```typescript 366 | public readonly assetsBucket: Bucket; 367 | ``` 368 | 369 | - *Type:* aws-cdk-lib.aws_s3.Bucket 370 | 371 | --- 372 | 373 | ##### `baseStackName`Required 374 | 375 | ```typescript 376 | public readonly baseStackName: string; 377 | ``` 378 | 379 | - *Type:* string 380 | 381 | --- 382 | 383 | ##### `companyName`Required 384 | 385 | ```typescript 386 | public readonly companyName: string; 387 | ``` 388 | 389 | - *Type:* string 390 | 391 | --- 392 | 393 | ##### `domainName`Required 394 | 395 | ```typescript 396 | public readonly domainName: string; 397 | ``` 398 | 399 | - *Type:* string 400 | 401 | --- 402 | 403 | ##### `elastiCacheHost`Required 404 | 405 | ```typescript 406 | public readonly elastiCacheHost: string; 407 | ``` 408 | 409 | - *Type:* string 410 | 411 | --- 412 | 413 | ##### `listener`Required 414 | 415 | ```typescript 416 | public readonly listener: ApplicationListener; 417 | ``` 418 | 419 | - *Type:* aws-cdk-lib.aws_elasticloadbalancingv2.ApplicationListener 420 | 421 | --- 422 | 423 | ##### `rdsInstance`Required 424 | 425 | ```typescript 426 | public readonly rdsInstance: DatabaseInstance; 427 | ``` 428 | 429 | - *Type:* aws-cdk-lib.aws_rds.DatabaseInstance 430 | 431 | --- 432 | 433 | ##### `rdsPasswordSecretName`Required 434 | 435 | ```typescript 436 | public readonly rdsPasswordSecretName: string; 437 | ``` 438 | 439 | - *Type:* string 440 | 441 | --- 442 | 443 | ##### `vpc`Required 444 | 445 | ```typescript 446 | public readonly vpc: IVpc; 447 | ``` 448 | 449 | - *Type:* aws-cdk-lib.aws_ec2.IVpc 450 | 451 | --- 452 | 453 | ### EcsBaseProps 454 | 455 | #### Initializer 456 | 457 | ```typescript 458 | import { EcsBaseProps } from 'cdk-django' 459 | 460 | const ecsBaseProps: EcsBaseProps = { ... } 461 | ``` 462 | 463 | #### Properties 464 | 465 | | **Name** | **Type** | **Description** | 466 | | --- | --- | --- | 467 | | certificateArn | string | *No description.* | 468 | | domainName | string | *No description.* | 469 | 470 | --- 471 | 472 | ##### `certificateArn`Required 473 | 474 | ```typescript 475 | public readonly certificateArn: string; 476 | ``` 477 | 478 | - *Type:* string 479 | 480 | --- 481 | 482 | ##### `domainName`Required 483 | 484 | ```typescript 485 | public readonly domainName: string; 486 | ``` 487 | 488 | - *Type:* string 489 | 490 | --- 491 | 492 | 493 | 494 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | ## [1.8.2](https://github.com/briancaffey/cdk-django/compare/v1.8.1...v1.8.2) (2025-02-16) 3 | 4 | 5 | ### Bug Fixes 6 | 7 | * **ecs:** fix capacity provider association dependency issue when destroying stacks ([b413f4a](https://github.com/briancaffey/cdk-django/commit/b413f4afe1180fb43e4e244e92b0c584aad1b2f0)) 8 | 9 | ## [1.8.1](https://github.com/briancaffey/cdk-django/compare/v1.8.0...v1.8.1) (2025-02-14) 10 | 11 | 12 | ### Bug Fixes 13 | 14 | * **ecs:** fix dependencies in ecs app stack ([42af37a](https://github.com/briancaffey/cdk-django/commit/42af37a923bd3d9f90b21288d6e3c7c58edd2aaa)) 15 | 16 | ## [1.8.0](https://github.com/briancaffey/cdk-django/compare/v1.7.0...v1.8.0) (2025-02-14) 17 | 18 | 19 | ### Features 20 | 21 | * **s3:** move s3 bucket resources to separate construct ([ffc4213](https://github.com/briancaffey/cdk-django/commit/ffc4213325ceadedb4fe7ecb8901f1a918988133)) 22 | 23 | 24 | ### Bug Fixes 25 | 26 | * **ecs:** fix error with dependencies between service and cluster capacity provider when destroying app stack ([6f9b875](https://github.com/briancaffey/cdk-django/commit/6f9b875f7489f94a22e974e4c0316c0d250bb50b)) 27 | * **lint:** run eslint ([ef91308](https://github.com/briancaffey/cdk-django/commit/ef913084d7ddd6026d1633fa5f423486b2764412)) 28 | 29 | ## [1.7.0](https://github.com/briancaffey/cdk-django/compare/v1.6.0...v1.7.0) (2025-02-11) 30 | 31 | 32 | ### Features 33 | 34 | * **cdk:** bump cdk version to v2.177.0 ([4ae4218](https://github.com/briancaffey/cdk-django/commit/4ae42186930e24d0ac7294eaed1f6760f2cf3b02)) 35 | * **examples:** remove ad hoc and prod examples to align with other iac library structures ([e853f96](https://github.com/briancaffey/cdk-django/commit/e853f96547d60cd8d7b90abfe6783b3ecc1c9307)) 36 | 37 | 38 | ### Bug Fixes 39 | 40 | * **docs:** add section to readme about the release process ([61c0cd7](https://github.com/briancaffey/cdk-django/commit/61c0cd7de1a2790a627d1fcd0989b7123ac52f43)) 41 | * **docs:** generate docs with yarn build ([bb6b55a](https://github.com/briancaffey/cdk-django/commit/bb6b55a94700b45b4d2d5369e11d264945af950c)) 42 | * **docs:** sync readme with other iac library readmes and updated cdk version ([cbdefd7](https://github.com/briancaffey/cdk-django/commit/cbdefd755e38aa1d3848b0db0fcb36586cc20181)) 43 | * **docs:** update readme ([7092e87](https://github.com/briancaffey/cdk-django/commit/7092e87cb74a53c212f9c2b88029816093d3be24)) 44 | * **lint:** run eslint ([c8030f9](https://github.com/briancaffey/cdk-django/commit/c8030f94d9e208bb4238a4fd43eab2e79cb2940a)) 45 | * **misc:** various fixes to fix base and app stack deployments with cdk ([5e43b09](https://github.com/briancaffey/cdk-django/commit/5e43b093f6bbc12f942e25a2e478e0a533b283c9)) 46 | 47 | ## [1.6.0](https://github.com/briancaffey/cdk-django/compare/v1.5.0...v1.6.0) (2025-01-26) 48 | 49 | 50 | ### Features 51 | 52 | * **upgrade:** run projen upgrade, fix issue with jest types ([2778840](https://github.com/briancaffey/cdk-django/commit/2778840534d3f050f7b2f1149d26505a12f3b3a4)) 53 | 54 | 55 | ### Bug Fixes 56 | 57 | * **release:** debug broken release ([ce27e83](https://github.com/briancaffey/cdk-django/commit/ce27e83424970210c43d5ca8f73947c1da603c4a)) 58 | 59 | ## [1.5.0](https://github.com/briancaffey/cdk-django/compare/v1.4.0...v1.5.0) (2024-05-02) 60 | 61 | 62 | ### Features 63 | 64 | * **docgen:** upgrade docgen version to fix release ([db818f6](https://github.com/briancaffey/cdk-django/commit/db818f61b726466428c7407c1b8f5e93a32c2eb9)) 65 | * **upgrade:** upgrade projen and cdk versions ([fd6afd0](https://github.com/briancaffey/cdk-django/commit/fd6afd09727c17f8a12108196bfafa908e9d9ed5)) 66 | 67 | 68 | ### Bug Fixes 69 | 70 | * **typo:** fix typo ([4167100](https://github.com/briancaffey/cdk-django/commit/41671003fcc7d3badc6af29610a31df3c928f5df)) 71 | 72 | ## [1.4.0](https://github.com/briancaffey/cdk-django/compare/v1.3.0...v1.4.0) (2024-03-10) 73 | 74 | 75 | ### Features 76 | 77 | * **cdk:** fixes for base - app compatibility ([9888efb](https://github.com/briancaffey/cdk-django/commit/9888efbd55da0c9bc330ff6ee2e8b7ccb3de103b)) 78 | * **cdk:** upgrade cdk version, refactor ad-hoc example directory ([7bf723b](https://github.com/briancaffey/cdk-django/commit/7bf723b609ef7caf5534dea10edfee9c5f8c5bf1)) 79 | * **gha:** add github action for service update ([b0820a3](https://github.com/briancaffey/cdk-django/commit/b0820a35b89390b1884715d2cef448fbeb179598)) 80 | * **s3:** fix bucket permissions ([e648a7b](https://github.com/briancaffey/cdk-django/commit/e648a7bc2c11bf66b754ff422cb8b26af50916f7)) 81 | 82 | ## [1.3.0](https://github.com/briancaffey/cdk-django/compare/v1.2.1...v1.3.0) (2024-02-25) 83 | 84 | 85 | ### Features 86 | 87 | * **project:** upgrade projen ([0ba65a6](https://github.com/briancaffey/cdk-django/commit/0ba65a6b6205cbdbc1f5546a6b30f8c2ecd3d135)) 88 | 89 | ### [1.2.1](https://github.com/briancaffey/cdk-django/compare/v1.2.0...v1.2.1) (2023-01-12) 90 | 91 | 92 | ### Bug Fixes 93 | 94 | * **upgrade-main:** revert to using default github auth method ([fbe84ca](https://github.com/briancaffey/cdk-django/commit/fbe84ca048c9bcc69983628664127de535eddd5d)) 95 | 96 | ## [1.2.0](https://github.com/briancaffey/cdk-django/compare/v1.1.0...v1.2.0) (2023-01-05) 97 | 98 | 99 | ### Features 100 | 101 | * **context:** added context for both ad hoc base and app constructs, clean up and refactor app constructs ([6c40d08](https://github.com/briancaffey/cdk-django/commit/6c40d084d652f96c9c53b6a6603f52467cb72088)) 102 | * **docs:** regenerate docs with doc gen ([05cff0f](https://github.com/briancaffey/cdk-django/commit/05cff0fe3429183ff0a10fc414f4063e3410dc6c)) 103 | 104 | ## [1.1.0](https://github.com/briancaffey/cdk-django/compare/v1.0.0...v1.1.0) (2023-01-04) 105 | 106 | 107 | ### Features 108 | 109 | * **projenrc:** make updates to release mechanism and other settings changes in projenrc.ts ([5804110](https://github.com/briancaffey/cdk-django/commit/58041106c6605f882708ad563eeebaf4b193f3e3)) 110 | * **projenrcts:** ts file for .projenrc.ts ([228a901](https://github.com/briancaffey/cdk-django/commit/228a901e0bae4ba6ffd95ab433f1e9fd045f2024)) 111 | 112 | ### [0.0.12](https://github.com/briancaffey/cdk-django/compare/v0.0.11...v0.0.12) (2022-12-18) 113 | 114 | 115 | ### Features 116 | 117 | * **release:** change release trigger to use manual releases ([09f430b](https://github.com/briancaffey/cdk-django/commit/09f430bd6064477a195fc1d88a117495ad4330ce)) 118 | 119 | ### [0.0.11](https://github.com/briancaffey/cdk-django/compare/v0.0.10...v0.0.11) (2022-12-18) 120 | 121 | 122 | ### Features 123 | 124 | * **celery:** add celery services ([9f0a6c0](https://github.com/briancaffey/cdk-django/commit/9f0a6c0dd3799deb6957954e3d7ae51eacfd3b21)) 125 | 126 | ### [0.0.11](https://github.com/briancaffey/cdk-django/compare/v0.0.10...v0.0.11) (2022-12-18) 127 | 128 | 129 | ### Features 130 | 131 | * **celery:** add celery services ([9f0a6c0](https://github.com/briancaffey/cdk-django/commit/9f0a6c0dd3799deb6957954e3d7ae51eacfd3b21)) 132 | 133 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2025 Brian Caffey 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 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # base stack 2 | ecs-base-synth: 3 | cdk synth --app='lib/examples/ecs/index.js' -e ExampleEcsBaseStack 4 | 5 | ecs-base-diff: 6 | cdk diff --app='./lib/examples/ecs/index.js' -e ExampleEcsBaseStack 7 | 8 | ecs-base-deploy: 9 | cdk deploy --app='./lib/examples/ecs/index.js' -e ExampleEcsBaseStack 10 | 11 | ecs-base-deploy-approve: 12 | cdk deploy --app='./lib/examples/ecs/index.js' --require-approval never -e ExampleEcsBaseStack 13 | 14 | ecs-base-destroy: 15 | yes | cdk destroy --app='./lib/examples/ecs/index.js' -e ExampleEcsBaseStack 16 | 17 | # app stack 18 | ecs-app-synth: 19 | cdk synth --app='./lib/examples/ecs/index.js' -e ExampleEcsAppStack 20 | 21 | ecs-app-diff: 22 | cdk diff --app='./lib/examples/ecs/index.js' -e ExampleEcsAppStack 23 | 24 | ecs-app-deploy: 25 | cdk deploy --app='./lib/examples/ecs/index.js' -e ExampleEcsAppStack 26 | 27 | # TODO: make sure this includes all services including beat 28 | ecs-app-delete-services: 29 | export AWS_PAGER='' 30 | aws ecs delete-service --cluster alpha-cluster --service alpha-web-ui --force --region us-east-1 31 | aws ecs delete-service --cluster alpha-cluster --service alpha-default --force --region us-east-1 32 | aws ecs delete-service --cluster alpha-cluster --service alpha-gunicorn --force --region us-east-1 33 | 34 | ecs-app-destroy: 35 | cdk destroy --verbose --app='./lib/examples/ecs/index.js' -e ExampleEcsAppStack 36 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `cdk-django` 2 | 3 | ## About this construct library 4 | 5 | `cdk-django` is a library for deploying Django applications to AWS using ECS Fargate. 6 | 7 | `cdk-django` aims to demonstrate best-practices for building web applications in AWS cloud. Executing the code in this project will build cloud resources such as networks, servers and databases that power a web application that can run securely on the public internet. 8 | 9 | You don't need to use this library directly in your code. It is recommended that you use this repo as a guide. It includes common patterns that are used when working with Infrastructure as Code. 10 | 11 | ### Companion application 12 | 13 | This project has a companion repo that contains a Django (Python) web application backend and a Nuxt.js frontend site (server-rendered Vue.js written in TypeScript). This project can be found here: 14 | 15 | [https://github.com/briancaffey/django-step-by-step/](https://github.com/briancaffey/django-step-by-step/) 16 | 17 | ### Related projects 18 | 19 | This project is has been written with the two other main Infrastructure as Code tools: Terraform and Pulumi. You can find these repos here: 20 | 21 | - [terraform-aws-django](https://github.com/briancaffey/terraform-aws-django) 22 | - [pulumi-aws-django](https://github.com/briancaffey/pulumi-aws-django) 23 | 24 | ## Project structure 25 | 26 | There are two main constructs the build resources for the different stacks: 27 | 28 | - `base` 29 | - `app` 30 | 31 | The `base` stack deploys long-lived resources that shouldn't need to be updated frequently, these include: 32 | 33 | - VPC 34 | - ElastiCache 35 | - S3 36 | - Security Groups 37 | - Load balancer 38 | - RDS 39 | 40 | The `app` stack deploys resources primarily for ECS services that run the application processes, these include: 41 | 42 | - ECS cluster for the environment 43 | - web-facing services (for running gunicorn and for running the frontend UI app) 44 | - celery worker for asynchronous task processing 45 | - celery beat for scheduled tasks 46 | - management_command for running migrations and other "pre-update" tasks (collectstatic, loading fixtures, etc.) 47 | - All backend environment variables are configured here (shared between all backend services) 48 | - Route 53 record for the environment (e.g. `.example.com`) 49 | - IAM resources (this might be able to be moved to the base stack) 50 | 51 | ### Local Examples 52 | 53 | It is best to deploy cloud infrastructure with automated pipelines that execute Infrastructure as Code. For testing and development you can deploy locally. 54 | 55 | The `Makefile` in this repo documents the commands to create and destroy infrastructure for the `base` and `app` stacks. For example: 56 | 57 | ```Makefile 58 | # base stack 59 | ecs-base-synth: 60 | cdk synth --app='lib/examples/ecs/index.js' -e ExampleEcsBaseStack 61 | 62 | ecs-base-diff: 63 | cdk diff --app='./lib/examples/ecs/index.js' -e ExampleEcsBaseStack 64 | 65 | ecs-base-deploy: 66 | cdk deploy --app='./lib/examples/ecs/index.js' -e ExampleEcsBaseStack 67 | 68 | ecs-base-deploy-approve: 69 | cdk deploy --app='./lib/examples/ecs/index.js' --require-approval never -e ExampleEcsBaseStack 70 | 71 | ecs-base-destroy: 72 | yes | cdk destroy --app='./lib/examples/ecs/index.js' -e ExampleEcsBaseStack 73 | ``` 74 | 75 | ## Companion application 76 | 77 | To see how `terraform-aws-django` can be used, have a look at [https://github.com/briancaffey/django-step-by-step](https://github.com/briancaffey/django-step-by-step). 78 | 79 | This companion repo includes two main components: a Django application (backend) and a Nuxt.js application (frontend) 80 | 81 | ### Django backend application features 82 | 83 | - email-based authentication flow (confirmation email + forgot password) 84 | - microblog app (users can write posts with text and images, like posts) 85 | - chat app (a simple OpenAI API wrapper) 86 | 87 | ### Nuxt.js frontend client features 88 | 89 | - Vue 3, Nuxt v3.15 90 | - SSR 91 | - shadcn 92 | - tailwindcss 93 | - pinia 94 | - composables 95 | 96 | This construct library focuses on security, best practices, scalability, flexibility and cost-efficiency. 97 | 98 | ## Maintaining this repo 99 | 100 | Make sure you are on the most recent version of CDK: 101 | 102 | ``` 103 | npm i -g aws-cdk 104 | ``` 105 | 106 | This project is managed by [`projen`](https://github.com/projen/projen). To update the application, run the following: 107 | 108 | ``` 109 | npx projen upgrade 110 | ``` 111 | 112 | Update [CDK version](https://github.com/aws/aws-cdk/releases) in `.projenrc.ts` and then run: 113 | 114 | ``` 115 | npx projen 116 | ``` 117 | 118 | Run `npx projen watch` in one terminal. 119 | 120 | 121 | In another terminal, export AWS credentials and environment variables and then run commands in the `Makefile`. 122 | 123 | Here are the environment variables you will need to export: 124 | 125 | ``` 126 | export DOMAIN_NAME=example.com 127 | export CERTIFICATE_ARN=arn:aws:acm:us-east-1:111111111111:certificate/11111111-1111-1111-1111-111111111111 128 | export AWS_REGION=us-east-1 129 | export AWS_ACCESS_KEY_ID=abc 130 | export AWS_SESSION_TOKEN=123 131 | export COMPANY_NAME=abc 132 | export AWS_ACCOUNT_ID=123456789 133 | ``` 134 | 135 | ## Release process 136 | 137 | This project uses `projen` to manage the release process. There is a GitHub Action called `release` that will make a release and publish it to npm. Before running release, make sure you run `yarn build` to build the documentation, and run `yarn eslint` to fix any linting errors. 138 | 139 | ## Issues 140 | 141 | - [https://github.com/projen/projen/issues/3950](https://github.com/projen/projen/issues/3950) -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cdk-django", 3 | "description": "CDK construct library for building Django applications on AWS with ECS Fargate", 4 | "repository": { 5 | "type": "git", 6 | "url": "git@github.com:briancaffey/cdk-django.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 | "post-compile": "npx projen post-compile", 22 | "post-upgrade": "npx projen post-upgrade", 23 | "pre-compile": "npx projen pre-compile", 24 | "publish:git": "npx projen publish:git", 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": "Brian Caffey", 35 | "email": "briancaffey2010@gmail.com", 36 | "url": "https://briancaffey.github.io", 37 | "organization": false 38 | }, 39 | "devDependencies": { 40 | "@stylistic/eslint-plugin": "^2", 41 | "@types/jest": "^27", 42 | "@types/node": "^16 <= 16.18.78", 43 | "@typescript-eslint/eslint-plugin": "^8", 44 | "@typescript-eslint/parser": "^8", 45 | "aws-cdk-lib": "2.178.1", 46 | "commit-and-tag-version": "^12", 47 | "constructs": "10.0.5", 48 | "eslint": "^9", 49 | "eslint-import-resolver-typescript": "^3.7.0", 50 | "eslint-plugin-import": "^2.31.0", 51 | "jest": "^27", 52 | "jest-junit": "^16", 53 | "jsii": "~5.6.0", 54 | "jsii-diff": "^1.106.0", 55 | "jsii-docgen": "10.x", 56 | "jsii-pacmak": "^1.106.0", 57 | "jsii-rosetta": "5.x", 58 | "projen": "^0.91.8", 59 | "ts-jest": "^27", 60 | "ts-node": "^10.9.2", 61 | "typescript": "^4.9.5" 62 | }, 63 | "peerDependencies": { 64 | "aws-cdk-lib": "^2.178.1", 65 | "constructs": "^10.0.5" 66 | }, 67 | "dependencies": { 68 | "aws-sdk": "2.1692.0" 69 | }, 70 | "keywords": [ 71 | "cdk" 72 | ], 73 | "main": "lib/index.js", 74 | "license": "MIT", 75 | "publishConfig": { 76 | "access": "public" 77 | }, 78 | "version": "0.0.0", 79 | "jest": { 80 | "coverageProvider": "v8", 81 | "testMatch": [ 82 | "/@(src|test)/**/*(*.)@(spec|test).ts?(x)", 83 | "/@(src|test)/**/__tests__/**/*.ts?(x)", 84 | "/@(projenrc)/**/*(*.)@(spec|test).ts?(x)", 85 | "/@(projenrc)/**/__tests__/**/*.ts?(x)" 86 | ], 87 | "clearMocks": true, 88 | "collectCoverage": true, 89 | "coverageReporters": [ 90 | "json", 91 | "lcov", 92 | "clover", 93 | "cobertura", 94 | "text" 95 | ], 96 | "coverageDirectory": "coverage", 97 | "coveragePathIgnorePatterns": [ 98 | "/node_modules/" 99 | ], 100 | "testPathIgnorePatterns": [ 101 | "/node_modules/" 102 | ], 103 | "watchPathIgnorePatterns": [ 104 | "/node_modules/" 105 | ], 106 | "reporters": [ 107 | "default", 108 | [ 109 | "jest-junit", 110 | { 111 | "outputDirectory": "test-reports" 112 | } 113 | ] 114 | ], 115 | "preset": "ts-jest", 116 | "globals": { 117 | "ts-jest": { 118 | "tsconfig": "tsconfig.dev.json" 119 | } 120 | } 121 | }, 122 | "types": "lib/index.d.ts", 123 | "stability": "stable", 124 | "jsii": { 125 | "outdir": "dist", 126 | "targets": {}, 127 | "tsc": { 128 | "outDir": "lib", 129 | "rootDir": "src" 130 | } 131 | }, 132 | "//": "~~ Generated by projen. To modify, edit .projenrc.ts and run \"npx projen\"." 133 | } 134 | -------------------------------------------------------------------------------- /packages/ecs-app-update/action.yml: -------------------------------------------------------------------------------- 1 | name: 'Action for updating ECS service' 2 | description: 'Action for updating ECS service' 3 | author: 'Brian Caffey' 4 | inputs: 5 | BASE_ENV: 6 | required: true 7 | description: 'Base env name (e.g. dev)' 8 | APP_ENV: 9 | required: true 10 | description: 'App env name (e.g. alpha)' 11 | VERSION: 12 | required: true 13 | description: 'Application version git tag (e.g. v1.2.3)' 14 | ECR_REPO: 15 | required: true 16 | description: 'ECR repo to use' 17 | CONTAINER_NAME: 18 | required: true 19 | description: 'Name of the container to update' 20 | AWS_REGION: 21 | required: false 22 | description: 'AWS Region' 23 | default: 'us-east-1' 24 | 25 | # Trigger / Inputs 26 | runs: 27 | using: "composite" 28 | steps: 29 | # Note: this assumes that your ECR repo lives in the same AWS account as your ECS cluster 30 | - name: Get current AWS Account 31 | id: get-aws-account 32 | shell: bash 33 | run: | 34 | AWS_ACCOUNT_ID=$(aws sts get-caller-identity | jq -r .Account) 35 | echo "AWS_ACCOUNT_ID=$AWS_ACCOUNT_ID" >> $GITHUB_ENV 36 | 37 | - name: Download existing task definition 38 | id: download-task-definition 39 | shell: bash 40 | run: | 41 | aws ecs describe-task-definition \ 42 | --task-definition ${{ env.FULL_TASK_NAME }} \ 43 | | jq '.taskDefinition' > task-definition.json 44 | 45 | - name: Render new task definition 46 | id: render-new-task-definition 47 | uses: aws-actions/amazon-ecs-render-task-definition@v1 48 | with: 49 | task-definition: task-definition.json 50 | container-name: ${{ inputs.CONTAINER_NAME }} 51 | image: ${{ env.AWS_ACCOUNT_ID }}.dkr.ecr.${{ inputs.AWS_REGION}}.amazonaws.com/${{ inputs.ECR_REPO }}:${{ inputs.VERSION }} 52 | 53 | - name: Deploy new task definition 54 | id: deploy-new-task-definition 55 | uses: aws-actions/amazon-ecs-deploy-task-definition@v1 56 | with: 57 | cluster: ${{ inputs.APP_ENV }}-cluster 58 | service: ${{ inputs.APP_ENV }}-${{ inputs.CONTAINER_NAME }} 59 | task-definition: ${{ steps.render-new-task-definition.outputs.task-definition }} 60 | wait-for-service-stability: true 61 | -------------------------------------------------------------------------------- /packages/ecs-run-task/action.yml: -------------------------------------------------------------------------------- 1 | name: 'Action for running an ECS task in a GHA workflow' 2 | description: 'Action for running ECS task' 3 | author: 'Brian Caffey' 4 | inputs: 5 | BASE_ENV: 6 | required: true 7 | description: 'Base env name (e.g. dev)' 8 | APP_ENV: 9 | required: true 10 | description: 'App env name (e.g. alpha)' 11 | VERSION: 12 | required: true 13 | description: 'Application version git tag (e.g. v1.2.3)' 14 | ECR_REPO: 15 | required: true 16 | description: 'ECR repo to use' 17 | CONTAINER_NAME: 18 | required: true 19 | description: 'Name of the container to update' 20 | AWS_REGION: 21 | required: false 22 | description: 'AWS Region' 23 | default: 'us-east-1' 24 | 25 | # Trigger / Inputs 26 | runs: 27 | using: "composite" 28 | steps: 29 | # Note: this assumes that your ECR repo lives in the same AWS account as your ECS cluster 30 | - name: Get current AWS Account 31 | id: get-aws-account 32 | shell: bash 33 | run: | 34 | AWS_ACCOUNT_ID=$(aws sts get-caller-identity | jq -r .Account) 35 | echo "AWS_ACCOUNT_ID=$AWS_ACCOUNT_ID" >> $GITHUB_ENV 36 | 37 | - name: Download existing task definition 38 | id: download-task-definition 39 | shell: bash 40 | run: | 41 | aws ecs describe-task-definition \ 42 | --task-definition ${{ env.FULL_TASK_NAME }} \ 43 | | jq '.taskDefinition' > task-definition.json 44 | 45 | - name: Render new task definition 46 | id: render-new-task-definition 47 | uses: aws-actions/amazon-ecs-render-task-definition@v1 48 | with: 49 | task-definition: task-definition.json 50 | container-name: ${{ inputs.CONTAINER_NAME }} 51 | image: ${{ env.AWS_ACCOUNT_ID }}.dkr.ecr.${{ inputs.AWS_REGION}}.amazonaws.com/${{ inputs.ECR_REPO }}:${{ inputs.VERSION }} 52 | 53 | - name: Deploy new task definition 54 | id: deploy-new-task-definition 55 | uses: aws-actions/amazon-ecs-deploy-task-definition@v1 56 | with: 57 | cluster: ${{ inputs.APP_ENV }}-cluster 58 | service: ${{ inputs.APP_ENV }}-${{ inputs.CONTAINER_NAME }} 59 | task-definition: ${{ steps.render-new-task-definition.outputs.task-definition }} 60 | 61 | -------------------------------------------------------------------------------- /src/constructs/ecs/app/index.ts: -------------------------------------------------------------------------------- 1 | import { Stack } from 'aws-cdk-lib'; 2 | import { IVpc, ISecurityGroup } from 'aws-cdk-lib/aws-ec2'; 3 | import { Repository } from 'aws-cdk-lib/aws-ecr'; 4 | import { Cluster, EcrImage } from 'aws-cdk-lib/aws-ecs'; 5 | import * as ecs from 'aws-cdk-lib/aws-ecs'; 6 | import { IApplicationLoadBalancer, ApplicationListener } from 'aws-cdk-lib/aws-elasticloadbalancingv2'; 7 | import { DatabaseInstance } from 'aws-cdk-lib/aws-rds'; 8 | import { CnameRecord, HostedZone } from 'aws-cdk-lib/aws-route53'; 9 | import { Bucket } from 'aws-cdk-lib/aws-s3'; 10 | import { Construct } from 'constructs'; 11 | // import { HighestPriorityRule } from '../../internal/customResources/highestPriorityRule'; 12 | import { EcsRoles } from '../../internal/ecs/iam'; 13 | import { ManagementCommandTask } from '../../internal/ecs/management-command'; 14 | import { WebService } from '../../internal/ecs/web'; 15 | import { WorkerService } from '../../internal/ecs/worker'; 16 | 17 | export interface EcsAppProps { 18 | readonly baseStackName: string; 19 | readonly vpc: IVpc; 20 | readonly alb: IApplicationLoadBalancer; 21 | readonly appSecurityGroup: ISecurityGroup; 22 | readonly rdsInstance: DatabaseInstance; 23 | readonly assetsBucket: Bucket; 24 | readonly domainName: string; 25 | readonly listener: ApplicationListener; 26 | readonly elastiCacheHost: string; 27 | readonly rdsPasswordSecretName: string; 28 | readonly companyName: string; 29 | } 30 | 31 | export class EcsApp extends Construct { 32 | 33 | constructor(scope: Construct, id: string, props: EcsAppProps) { 34 | super(scope, id); 35 | 36 | const stackName = Stack.of(this).stackName; 37 | 38 | // custom resource to get the highest available listener rule priority 39 | // then take the next two highest priorities and use them for the frontend and backend listener rule priorities 40 | // https://github.com/aws-samples/aws-cdk-examples/blob/master/typescript/custom-resource/index.ts 41 | 42 | // const highestPriorityRule = new HighestPriorityRule(this, 'HighestPriorityRule', { listener: props.listener }); 43 | 44 | const backendEcrRepo = Repository.fromRepositoryName(this, 'BackendRepo', `${props.companyName}-backend`); 45 | const backendVersion = 'latest'; 46 | const backendImage = new EcrImage(backendEcrRepo, backendVersion); 47 | 48 | const frontendEcrRepo = Repository.fromRepositoryName(this, 'FrontendRepo', `${props.companyName}-frontend`); 49 | const frontendVersion = 'latest'; 50 | const frontendImage = new EcrImage(frontendEcrRepo, frontendVersion); 51 | 52 | const cluster = new Cluster(this, 'Cluster', { 53 | clusterName: `${stackName}-cluster`, 54 | vpc: props.vpc, 55 | }); 56 | 57 | const cfnCapacityProviderAssociations = new ecs.CfnClusterCapacityProviderAssociations(this, 'CapacityProviderAssociations', { 58 | cluster: cluster.clusterName, 59 | capacityProviders: ['FARGATE', 'FARGATE_SPOT'], 60 | defaultCapacityProviderStrategy: [{ 61 | capacityProvider: 'FARGATE', 62 | }], 63 | }); 64 | 65 | const settingsModule = this.node.tryGetContext('config').settingsModule ?? 'backend.settings.production'; 66 | // shared environment variables 67 | let environmentVariables: { [key: string]: string } = { 68 | APP_NAME: stackName, // e.g. alpha 69 | BASE_STACK_NAME: props.baseStackName, // e.g. dev 70 | DB_SECRET_NAME: props.rdsPasswordSecretName, 71 | DOMAIN_NAME: props.domainName, 72 | DJANGO_SETTINGS_MODULE: settingsModule, 73 | FRONTEND_URL: `https://${stackName}.${props.domainName}`, 74 | NVIDIA_API_KEY: '', 75 | POSTGRES_NAME: `${stackName}-db`, 76 | POSTGRES_SERVICE_HOST: props.rdsInstance.dbInstanceEndpointAddress, 77 | S3_BUCKET_NAME: props.assetsBucket.bucketName, 78 | REDIS_SERVICE_HOST: props.elastiCacheHost, 79 | SENTRY_DSN: '', 80 | }; 81 | 82 | const extraEnvVars = this.node.tryGetContext('config').extraEnvVars; 83 | 84 | if (extraEnvVars) { 85 | environmentVariables = { ...extraEnvVars, ...environmentVariables }; 86 | } 87 | 88 | // define ecsTaskRole and taskExecutionRole for ECS 89 | const ecsRoles = new EcsRoles(scope, 'EcsRoles'); 90 | 91 | // allow the task role to read and write to the bucket 92 | props.assetsBucket.grantReadWrite(ecsRoles.ecsTaskRole); 93 | 94 | // Route53 95 | const hostedZone = HostedZone.fromLookup(this, 'HostedZone', { domainName: props.domainName }); 96 | // const cnameRecord = 97 | new CnameRecord(this, 'CnameApiRecord', { 98 | recordName: stackName, 99 | domainName: props.alb.loadBalancerDnsName, 100 | zone: hostedZone, 101 | }); 102 | 103 | // api service 104 | const backendService = new WebService(this, 'ApiService', { 105 | cluster, 106 | environmentVariables, 107 | vpc: props.vpc, 108 | appSecurityGroup: props.appSecurityGroup, 109 | taskRole: ecsRoles.ecsTaskRole, 110 | executionRole: ecsRoles.taskExecutionRole, 111 | image: backendImage, 112 | listener: props.listener, 113 | command: [ 114 | 'gunicorn', 115 | '-t', 116 | '1000', 117 | '-b', 118 | '0.0.0.0:8000', 119 | '--log-level', 120 | 'info', 121 | 'backend.wsgi', 122 | ], 123 | name: 'gunicorn', 124 | port: 8000, 125 | domainName: props.domainName, 126 | pathPatterns: ['/api/*', '/admin/*', '/mtv/*', '/graphql/*'], 127 | priority: 2, //highestPriorityRule.priority + 1, 128 | healthCheckPath: '/api/health-check/', 129 | }); 130 | 131 | const frontendService = new WebService(this, 'FrontendService', { 132 | cluster, 133 | environmentVariables: { 134 | NUXT_PUBLIC_API_BASE: `https://${stackName}.${props.domainName}`, 135 | }, 136 | vpc: props.vpc, 137 | appSecurityGroup: props.appSecurityGroup, 138 | taskRole: ecsRoles.ecsTaskRole, 139 | executionRole: ecsRoles.taskExecutionRole, 140 | image: frontendImage, 141 | listener: props.listener, 142 | command: ['node', '.output/server/index.mjs'], 143 | name: 'web-ui', 144 | port: 3000, 145 | domainName: props.domainName, 146 | pathPatterns: ['/*'], 147 | priority: 3, // highestPriorityRule.priority + 2, 148 | healthCheckPath: '/', 149 | }); 150 | 151 | // worker service 152 | const workerService = new WorkerService(this, 'DefaultCeleryWorker', { 153 | cluster, 154 | environmentVariables, 155 | vpc: props.vpc, 156 | appSecurityGroup: props.appSecurityGroup, 157 | taskRole: ecsRoles.ecsTaskRole, 158 | executionRole: ecsRoles.taskExecutionRole, 159 | image: backendImage, 160 | command: [ 161 | 'celery', 162 | '--app=backend.celery_app:app', 163 | 'worker', 164 | '--loglevel=INFO', 165 | '-Q', 166 | 'default', 167 | ], 168 | name: 'default', 169 | }); 170 | 171 | // scheduler service 172 | const beatService = new WorkerService(this, 'CeleryBeat', { 173 | cluster, 174 | environmentVariables, 175 | vpc: props.vpc, 176 | appSecurityGroup: props.appSecurityGroup, 177 | taskRole: ecsRoles.ecsTaskRole, 178 | executionRole: ecsRoles.taskExecutionRole, 179 | image: backendImage, 180 | command: [ 181 | 'celery', 182 | '--app=backend.celery_app:app', 183 | 'beat', 184 | '--loglevel=INFO', 185 | ], 186 | name: 'beat', 187 | }); 188 | 189 | // management command task definition 190 | // const backendUpdateTask = 191 | new ManagementCommandTask(this, 'update', { 192 | cluster, 193 | environmentVariables, 194 | vpc: props.vpc, 195 | appSecurityGroup: props.appSecurityGroup, 196 | taskRole: ecsRoles.ecsTaskRole, 197 | executionRole: ecsRoles.taskExecutionRole, 198 | image: backendImage, 199 | command: ['python', 'manage.py', 'pre_update'], 200 | name: 'backend_update', 201 | }); 202 | 203 | // // worker service 204 | // new WorkerService(this, 'EcsExecBastion', { 205 | // cluster, 206 | // environmentVariables, 207 | // vpc: props.vpc, 208 | // appSecurityGroup: props.appSecurityGroup, 209 | // taskRole: ecsRoles.ecsTaskRole, 210 | // executionRole: ecsRoles.taskExecutionRole, 211 | // image: backendImage, 212 | // command: ['sh', '-c', 'tail -f /dev/null'], 213 | // name: 'ecs-exec', 214 | // }); 215 | 216 | 217 | // cluster <-> service dependencies 218 | // Get the low-level Cfn resources 219 | const cfnServiceBackend = backendService.service.node.defaultChild as ecs.CfnService; 220 | const cfnServiceFrontend = frontendService.service.node.defaultChild as ecs.CfnService; 221 | const cfnBeatService = beatService.service.node.defaultChild as ecs.CfnService; 222 | const cfnWorkerService = workerService.service.node.defaultChild as ecs.CfnService; 223 | 224 | cfnServiceBackend.addDependency(cfnCapacityProviderAssociations); 225 | cfnServiceFrontend.addDependency(cfnCapacityProviderAssociations); 226 | cfnBeatService.addDependency(cfnCapacityProviderAssociations); 227 | cfnWorkerService.addDependency(cfnCapacityProviderAssociations); 228 | } 229 | } 230 | -------------------------------------------------------------------------------- /src/constructs/ecs/base/index.ts: -------------------------------------------------------------------------------- 1 | import { Stack } from 'aws-cdk-lib'; 2 | import { IVpc, SecurityGroup } from 'aws-cdk-lib/aws-ec2'; 3 | import { ApplicationListener, ApplicationLoadBalancer } from 'aws-cdk-lib/aws-elasticloadbalancingv2'; 4 | import { DatabaseInstance } from 'aws-cdk-lib/aws-rds'; 5 | import { Bucket } from 'aws-cdk-lib/aws-s3'; 6 | import { Construct } from 'constructs'; 7 | import { AlbResources } from '../../internal/alb'; 8 | import { ElastiCacheCluster } from '../../internal/ec'; 9 | import { RdsInstance } from '../../internal/rds'; 10 | import { S3Resources } from '../../internal/s3'; 11 | import { SecurityGroupResources } from '../../internal/sg'; 12 | import { ApplicationVpc } from '../../internal/vpc'; 13 | 14 | export interface EcsBaseProps { 15 | readonly certificateArn: string; 16 | readonly domainName: string; 17 | } 18 | 19 | export class EcsBase extends Construct { 20 | 21 | public vpc: IVpc; 22 | public alb: ApplicationLoadBalancer; 23 | public appSecurityGroup: SecurityGroup; 24 | public albSecurityGroup: SecurityGroup; 25 | public databaseInstance: DatabaseInstance; 26 | public assetsBucket: Bucket; 27 | public domainName: string; 28 | public listener: ApplicationListener; 29 | public elastiCacheHostname: string; 30 | public rdsPasswordSecretName: string; 31 | 32 | constructor(scope: Construct, id: string, props: EcsBaseProps) { 33 | super(scope, id); 34 | 35 | const stackName = Stack.of(this).stackName; 36 | this.domainName = props.domainName; 37 | 38 | const applicationVpc = new ApplicationVpc(scope, 'Vpc'); 39 | this.vpc = applicationVpc.vpc; 40 | 41 | // S3 resources 42 | const assetsBucket = new S3Resources(this, 'StaticAssetsBucket', { 43 | bucketName: `${props.domainName.replace('.', '-')}-${stackName}-assets-bucket`, 44 | forceDestroy: false, 45 | publicReadAccess: false, 46 | }); 47 | this.assetsBucket = assetsBucket.bucket; 48 | 49 | const { albSecurityGroup, appSecurityGroup } = new SecurityGroupResources(this, 'SecurityGroupResources', { 50 | vpc: this.vpc, 51 | }); 52 | this.albSecurityGroup = albSecurityGroup; 53 | this.appSecurityGroup = appSecurityGroup; 54 | 55 | const { alb, listener } = new AlbResources(this, 'AlbResources', { 56 | albSecurityGroup, 57 | vpc: this.vpc, 58 | certificateArn: props.certificateArn, 59 | }); 60 | this.alb = alb; 61 | this.listener = listener; 62 | 63 | // TODO: rename to RdsResources 64 | const rdsInstance = new RdsInstance(this, 'RdsInstance', { 65 | vpc: this.vpc, 66 | appSecurityGroup: appSecurityGroup, 67 | dbSecretName: this.node.tryGetContext('config')?.dbSecretName ?? 'DB_SECRET_NAME', 68 | }); 69 | this.databaseInstance = rdsInstance.rdsInstance; 70 | this.rdsPasswordSecretName = rdsInstance.rdsPasswordSecretName; 71 | 72 | // elasticache cluster 73 | const elastiCacheCluster = new ElastiCacheCluster(this, 'ElastiCacheCluster', { 74 | vpc: this.vpc, 75 | appSecurityGroup: appSecurityGroup, 76 | }); 77 | this.elastiCacheHostname = elastiCacheCluster.elastiCacheHost; 78 | 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/constructs/internal/alb/index.ts: -------------------------------------------------------------------------------- 1 | // import { Stack } from 'aws-cdk-lib'; 2 | import { Duration } from 'aws-cdk-lib'; 3 | import { IVpc, SecurityGroup, SubnetType } from 'aws-cdk-lib/aws-ec2'; 4 | import { ApplicationListener, ApplicationLoadBalancer, ApplicationProtocol, ApplicationTargetGroup, ListenerAction, ListenerCertificate, TargetType } from 'aws-cdk-lib/aws-elasticloadbalancingv2'; 5 | import { Construct } from 'constructs'; 6 | 7 | 8 | interface AlbResourcesProps { 9 | readonly vpc: IVpc; 10 | readonly albSecurityGroup: SecurityGroup; 11 | readonly certificateArn: string; 12 | } 13 | 14 | export class AlbResources extends Construct { 15 | // public rdsInstance: DatabaseInstance; 16 | public readonly alb: ApplicationLoadBalancer; 17 | public readonly listener: ApplicationListener; 18 | 19 | constructor(scope: Construct, id: string, props: AlbResourcesProps) { 20 | super(scope, id); 21 | 22 | const loadBalancer = new ApplicationLoadBalancer(scope, 'LoadBalancer', { 23 | vpc: props.vpc, 24 | securityGroup: props.albSecurityGroup, 25 | internetFacing: true, 26 | vpcSubnets: { 27 | subnetType: SubnetType.PUBLIC, 28 | }, 29 | }); 30 | this.alb = loadBalancer; 31 | 32 | // application target group 33 | // Target group with duration-based stickiness with load-balancer generated cookie 34 | // const defaultTargetGroup = 35 | new ApplicationTargetGroup(this, 'default-target-group', { 36 | targetType: TargetType.INSTANCE, 37 | port: 80, 38 | stickinessCookieDuration: Duration.minutes(5), 39 | vpc: props.vpc, 40 | healthCheck: { 41 | path: '/api/health-check/', // TODO parametrize this 42 | interval: Duration.minutes(5), 43 | timeout: Duration.minutes(2), 44 | healthyThresholdCount: 2, 45 | unhealthyThresholdCount: 3, 46 | port: '80', // TODO parametrize this 47 | }, 48 | }); 49 | 50 | // listener - HTTP 51 | new ApplicationListener(this, 'http-listener', { 52 | loadBalancer: loadBalancer, 53 | port: 80, 54 | protocol: ApplicationProtocol.HTTP, 55 | defaultAction: ListenerAction.redirect({ 56 | protocol: ApplicationProtocol.HTTPS, 57 | port: '443', 58 | permanent: false, 59 | }), 60 | }); 61 | 62 | // listener - HTTPS 63 | const httpsListener = new ApplicationListener(this, 'https-listener', { 64 | loadBalancer: loadBalancer, 65 | port: 443, 66 | protocol: ApplicationProtocol.HTTPS, 67 | certificates: [ListenerCertificate.fromArn(props.certificateArn)], 68 | defaultAction: ListenerAction.fixedResponse(200, { 69 | contentType: 'text/plain', 70 | messageBody: 'Fixed content response', 71 | }), 72 | }); 73 | this.listener = httpsListener; 74 | 75 | } 76 | } -------------------------------------------------------------------------------- /src/constructs/internal/bastion/index.ts: -------------------------------------------------------------------------------- 1 | import { Fn } from 'aws-cdk-lib'; 2 | import { IVpc, SecurityGroup, BastionHostLinux, UserData, CfnInstance, InstanceType, InstanceClass, InstanceSize } from 'aws-cdk-lib/aws-ec2'; 3 | import { Construct } from 'constructs'; 4 | 5 | 6 | interface BastionHostResourcesProps { 7 | readonly instanceClass?: InstanceClass; 8 | readonly instanceSize?: InstanceSize; 9 | readonly rdsAddress: string; 10 | readonly vpc: IVpc; 11 | readonly appSecurityGroup: SecurityGroup; 12 | } 13 | 14 | export class BastionHostResources extends Construct { 15 | public instanceClass: InstanceClass; 16 | public instanceSize: InstanceSize; 17 | public instanceId: string; 18 | 19 | constructor(scope: Construct, id: string, props: BastionHostResourcesProps) { 20 | super(scope, id); 21 | 22 | this.instanceClass = props.instanceClass ?? InstanceClass.T3; 23 | this.instanceSize = props.instanceSize ?? InstanceSize.NANO; 24 | 25 | const socatForwarderString = ` 26 | package_upgrade: true 27 | packages: 28 | - postgresql 29 | - socat 30 | write_files: 31 | - content: | 32 | # /etc/systemd/system/socat-forwarder.service 33 | [Unit] 34 | Description=socat forwarder service 35 | After=socat-forwarder.service 36 | Requires=socat-forwarder.service 37 | 38 | [Service] 39 | Type=simple 40 | StandardOutput=syslog 41 | StandardError=syslog 42 | SyslogIdentifier=socat-forwarder 43 | 44 | ExecStart=/usr/bin/socat -d -d TCP4-LISTEN:5432,fork TCP4:${props.rdsAddress}:5432 45 | Restart=always 46 | 47 | [Install] 48 | WantedBy=multi-user.target 49 | path: /etc/systemd/system/socat-forwarder.service 50 | 51 | runcmd: 52 | - [ systemctl, daemon-reload ] 53 | - [ systemctl, enable, socat-forwarder.service ] 54 | # https://dustymabe.com/2015/08/03/installingstarting-systemd-services-using-cloud-init/ 55 | - [ systemctl, start, --no-block, socat-forwarder.service ] 56 | `; 57 | 58 | const bastionHost = new BastionHostLinux(this, 'BastionHost', { 59 | vpc: props.vpc, 60 | securityGroup: props.appSecurityGroup, 61 | instanceType: InstanceType.of(this.instanceClass, this.instanceSize), 62 | }); 63 | this.instanceId = bastionHost.instanceId; 64 | 65 | const bastionHostUserData = UserData.forLinux({ shebang: '#cloud-config' }); 66 | 67 | bastionHostUserData.addCommands(socatForwarderString); 68 | 69 | const cfnBastionHost = bastionHost.instance.node.defaultChild as CfnInstance; 70 | 71 | cfnBastionHost.addPropertyOverride('UserData', Fn.base64(bastionHostUserData.render())); 72 | 73 | } 74 | } -------------------------------------------------------------------------------- /src/constructs/internal/customResources/highestPriorityRule/custom-resource-handler.py: -------------------------------------------------------------------------------- 1 | # https://github.com/aws-samples/aws-cdk-examples/blob/master/typescript/custom-resource/custom-resource-handler.py 2 | # https://github.com/hashicorp/terraform-provider-aws/blob/main/internal/service/elbv2/listener_rule.go#L969-L1000 3 | 4 | def main(event, context): 5 | import logging as log 6 | import os 7 | 8 | import boto3 9 | import cfnresponse 10 | log.getLogger().setLevel(log.INFO) 11 | 12 | # This needs to change if there are to be multiple resources in the same stack 13 | physical_id = 'TheOnlyCustomResource' 14 | 15 | try: 16 | log.info('Input event: %s', event) 17 | 18 | # Check if this is a Create and we're failing Creates 19 | if event['RequestType'] == 'Create' and event['ResourceProperties'].get('FailCreate', False): 20 | raise RuntimeError('Create failure requested') 21 | 22 | # Do the thing 23 | listener_arn = os.environ.get('LISTENER_ARN') 24 | 25 | elbv2 = boto3.client('elbv2') 26 | 27 | # get the priorities for the listener 28 | response = elbv2.describe_listeners(ListenerArns=[listener_arn]) 29 | 30 | next_marker = None 31 | priorities = [] 32 | 33 | while True: 34 | output = elbv2.describe_rules(ListenerArn=listener_arn, Marker=next_marker) 35 | 36 | for rule in output['Rules']: 37 | priorities.append(rule['Priority']) 38 | 39 | if output['NextMarker'] == None: 40 | break 41 | 42 | next_marker = output['NextMarker'] 43 | 44 | if len(priorities) == 0: 45 | return 0 46 | 47 | priorities.sort() 48 | 49 | # get the rules for this listener using boto3 50 | attributes = { 51 | 'highest_priority': priorities[-1], 52 | } 53 | 54 | cfnresponse.send(event, context, cfnresponse.SUCCESS, attributes, physical_id) 55 | 56 | except Exception as e: 57 | log.exception(e) 58 | # cfnresponse's error message is always "see CloudWatch" 59 | cfnresponse.send(event, context, cfnresponse.FAILED, {}, physical_id) 60 | -------------------------------------------------------------------------------- /src/constructs/internal/customResources/highestPriorityRule/index.ts: -------------------------------------------------------------------------------- 1 | import { readFileSync } from 'fs'; 2 | import * as path from 'path'; 3 | import { CustomResource, Duration } from 'aws-cdk-lib'; 4 | import { ApplicationListener } from 'aws-cdk-lib/aws-elasticloadbalancingv2'; 5 | import { InlineCode, Runtime, SingletonFunction } from 'aws-cdk-lib/aws-lambda'; 6 | import { Provider } from 'aws-cdk-lib/custom-resources'; 7 | import { Construct } from 'constructs'; 8 | 9 | export interface HighestPriorityRuleProps { 10 | listener: ApplicationListener; 11 | } 12 | 13 | export class HighestPriorityRule extends Construct { 14 | public readonly priority: number; 15 | 16 | constructor(scope: Construct, id: string, props: HighestPriorityRuleProps) { 17 | super(scope, id); 18 | 19 | const fn = new SingletonFunction(this, 'Function', { 20 | uuid: 'highest-priority-rule', 21 | code: new InlineCode(readFileSync(path.join(__dirname, 'custom-resource-handler.py'), { encoding: 'utf-8' })), 22 | handler: 'index.main', 23 | timeout: Duration.seconds(100), 24 | runtime: Runtime.PYTHON_3_9, 25 | environment: { 26 | LISTENER_ARN: props.listener.listenerArn, 27 | }, 28 | }); 29 | 30 | const provider = new Provider(this, 'Provider', { 31 | onEventHandler: fn, 32 | }); 33 | 34 | const resource = new CustomResource(this, 'Resource', { 35 | serviceToken: provider.serviceToken, 36 | properties: props, 37 | }); 38 | 39 | this.priority = parseInt(resource.getAtt('Response').toString()); 40 | } 41 | } -------------------------------------------------------------------------------- /src/constructs/internal/ec/index.ts: -------------------------------------------------------------------------------- 1 | import { IVpc, Port, SecurityGroup, SubnetType } from 'aws-cdk-lib/aws-ec2'; 2 | import { CfnSubnetGroup, CfnCacheCluster, CfnParameterGroup } from 'aws-cdk-lib/aws-elasticache'; 3 | import { Construct } from 'constructs'; 4 | 5 | 6 | interface ElastiCacheClusterProps { 7 | readonly vpc: IVpc; 8 | readonly appSecurityGroup: SecurityGroup; 9 | readonly instanceClass?: string; 10 | readonly instanceSize?: string; 11 | } 12 | 13 | export class ElastiCacheCluster extends Construct { 14 | // public rdsInstance: DatabaseInstance; 15 | private instanceClass: string; 16 | private instanceSize: string; 17 | public elastiCacheHost: string; 18 | 19 | 20 | constructor(scope: Construct, id: string, props: ElastiCacheClusterProps) { 21 | super(scope, id); 22 | 23 | // const stackName = Stack.of(this).stackName; 24 | 25 | // instance type from props 26 | this.instanceClass = props.instanceClass ?? 't4g'; 27 | this.instanceSize = props.instanceSize ?? 'micro'; 28 | 29 | const cacheNodeType = `cache.${this.instanceClass}.${this.instanceSize}`; 30 | 31 | // security group 32 | const elastiCacheSecurityGroup = new SecurityGroup(this, 'SecurityGroup', { 33 | vpc: props.vpc, 34 | description: 'Allow all outbound traffic', 35 | allowAllOutbound: true, 36 | }); 37 | 38 | // elastiCacheSecurityGroup.addIngressRule(Peer.anyIpv4(), Port.tcp(6379), 'ElastiCacheRedis'); 39 | elastiCacheSecurityGroup.addIngressRule(props.appSecurityGroup, Port.tcp(6379), 'AppSecurityGroup'); 40 | 41 | // ElastiCache subnet group 42 | const subnetGroup = new CfnSubnetGroup(this, 'SubnetGroup', { 43 | description: 'Subnet group for ElastiCache', 44 | subnetIds: props.vpc.selectSubnets({ subnetType: SubnetType.PRIVATE_WITH_EGRESS }).subnetIds, 45 | }); 46 | 47 | // ElastiCache parameter group 48 | const elastiCacheParameterGroup = new CfnParameterGroup(this, 'ElastiCacheParameterGroup', { 49 | description: 'parameter group for elasticache cluster', 50 | cacheParameterGroupFamily: 'redis7', 51 | properties: {}, 52 | }); 53 | 54 | // ElastiCache cluster 55 | const cacheCluster = new CfnCacheCluster(this, 'CacheCluster', { 56 | cacheNodeType: cacheNodeType, // Node type for a single-node cluster 57 | engine: 'redis', 58 | engineVersion: '7.0', 59 | numCacheNodes: 1, // Single node 60 | cacheSubnetGroupName: subnetGroup.ref, 61 | cacheParameterGroupName: elastiCacheParameterGroup.ref, 62 | vpcSecurityGroupIds: [elastiCacheSecurityGroup.securityGroupId], 63 | }); 64 | 65 | this.elastiCacheHost = cacheCluster.attrRedisEndpointAddress; 66 | 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/constructs/internal/ecs/iam/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Stack, 3 | } from 'aws-cdk-lib'; 4 | import { Effect, PolicyStatement, Role, ServicePrincipal } from 'aws-cdk-lib/aws-iam'; 5 | import { Construct } from 'constructs'; 6 | 7 | // not using any props for now 8 | // export interface EcsRolesProps {}; 9 | 10 | export class EcsRoles extends Construct { 11 | readonly ecsTaskRole: Role; 12 | readonly taskExecutionRole: Role; 13 | 14 | constructor(scope: Construct, id: string) { 15 | super(scope, id); 16 | 17 | const stackName = Stack.of(this).stackName; 18 | 19 | // IAM 20 | const ecsTaskRole = new Role(this, 'EcsTaskRole', { 21 | roleName: `${stackName}EcsTaskRole`, 22 | assumedBy: new ServicePrincipal('ecs-tasks.amazonaws.com'), 23 | }); 24 | 25 | ecsTaskRole.addToPolicy(new PolicyStatement({ 26 | effect: Effect.ALLOW, 27 | actions: [ 28 | 'ecs:*', 29 | 'ec2:*', 30 | 'elasticloadbalancing:*', 31 | 'ecr:*', 32 | 'cloudwatch:*', 33 | 's3:*', 34 | 'rds:*', 35 | 'logs:*', 36 | 'elasticache:*', 37 | 'secretsmanager:*', 38 | ], 39 | resources: ['*'], 40 | })); 41 | 42 | this.ecsTaskRole = ecsTaskRole;; 43 | 44 | const taskExecutionRole = new Role(this, 'TaskExecutionRole', { 45 | roleName: `${stackName}TaskExecutionRole`, 46 | assumedBy: new ServicePrincipal('ecs-tasks.amazonaws.com'), 47 | }); 48 | 49 | // S3 50 | // TODO: tighten persmissions https://stackoverflow.com/a/23667874/6084948 51 | taskExecutionRole.addToPolicy(new PolicyStatement({ 52 | effect: Effect.ALLOW, 53 | actions: ['s3:*'], 54 | resources: ['arn:aws:s3:::*', 'arn:aws:s3:::*/*'], 55 | })); 56 | 57 | // Secrets manager 58 | taskExecutionRole.addToPolicy(new PolicyStatement({ 59 | effect: Effect.ALLOW, 60 | actions: ['secretsmanager:GetSecretValue'], 61 | resources: ['*'], 62 | })); 63 | 64 | // EcsExec SSM 65 | taskExecutionRole.addToPolicy(new PolicyStatement({ 66 | effect: Effect.ALLOW, 67 | actions: [ 68 | 'ssmmessages:CreateControlChannel', 69 | 'ssmmessages:CreateDataChannel', 70 | 'ssmmessages:OpenControlChannel', 71 | 'ssmmessages:OpenDataChannel', 72 | ], 73 | resources: ['*'], 74 | })); 75 | 76 | this.taskExecutionRole = taskExecutionRole; 77 | } 78 | } -------------------------------------------------------------------------------- /src/constructs/internal/ecs/management-command/index.ts: -------------------------------------------------------------------------------- 1 | import { RemovalPolicy, Stack } from 'aws-cdk-lib'; 2 | import { ISecurityGroup, IVpc, SubnetType } from 'aws-cdk-lib/aws-ec2'; 3 | import { 4 | LogDriver, 5 | Cluster, 6 | ContainerImage, 7 | FargateTaskDefinition, 8 | } from 'aws-cdk-lib/aws-ecs'; 9 | import { Role } from 'aws-cdk-lib/aws-iam'; 10 | import { 11 | LogGroup, 12 | LogStream, 13 | RetentionDays, 14 | } from 'aws-cdk-lib/aws-logs'; 15 | import { Construct } from 'constructs'; 16 | 17 | 18 | export interface ManagementCommandTaskProps { 19 | readonly cluster: Cluster; 20 | readonly vpc: IVpc; 21 | readonly cpu?: number; 22 | readonly memorySize?: number; 23 | readonly appSecurityGroup: ISecurityGroup; 24 | readonly image: ContainerImage; 25 | readonly command: string[]; 26 | readonly name: string; 27 | readonly taskRole: Role; 28 | readonly executionRole: Role; 29 | readonly environmentVariables: { [key: string]: string }; 30 | }; 31 | 32 | export class ManagementCommandTask extends Construct { 33 | 34 | /** 35 | * Script to invoke run-task and send task logs to standard output 36 | */ 37 | public executionScript: string; 38 | 39 | constructor(scope: Construct, id: string, props: ManagementCommandTaskProps) { 40 | super(scope, id); 41 | 42 | const stackName = Stack.of(this).stackName; 43 | 44 | // define log group and logstream 45 | const logGroupName = `/ecs/${stackName}/${props.name}/`; 46 | const streamPrefix = props.name; 47 | const logGroup = new LogGroup(this, 'LogGroup', { 48 | logGroupName, 49 | retention: RetentionDays.ONE_DAY, 50 | removalPolicy: RemovalPolicy.DESTROY, 51 | }); 52 | 53 | new LogStream(this, 'LogStream', { 54 | logGroup, 55 | logStreamName: props.name, 56 | }); 57 | 58 | // task definition 59 | const taskDefinition = new FargateTaskDefinition(this, 'TaskDefinition', { 60 | cpu: props.cpu ?? 256, 61 | executionRole: props.executionRole, 62 | taskRole: props.taskRole, 63 | family: `${stackName}-${props.name}`, 64 | }); 65 | 66 | taskDefinition.addContainer(props.name, { 67 | image: props.image, 68 | command: props.command, 69 | containerName: props.name, 70 | environment: props.environmentVariables, 71 | essential: true, 72 | logging: LogDriver.awsLogs({ 73 | streamPrefix, 74 | logGroup, 75 | }), 76 | }); 77 | 78 | const privateSubnets = props.vpc.selectSubnets({ subnetType: SubnetType.PRIVATE_WITH_EGRESS }).subnets.map(s => s.subnetId).join(','); 79 | 80 | // this script is called once on initial setup from GitHub Actions 81 | const executionScript = ` 82 | START_TIME=$(date +%s000) 83 | 84 | TASK_ID=$(aws ecs run-task --cluster ${props.cluster.clusterArn} --task-definition ${taskDefinition.taskDefinitionArn} --launch-type FARGATE --network-configuration "awsvpcConfiguration={subnets=[${privateSubnets}],securityGroups=[${props.appSecurityGroup.securityGroupId}],assignPublicIp=ENABLED}" | jq -r '.tasks[0].taskArn') 85 | 86 | aws ecs wait tasks-stopped --tasks $TASK_ID --cluster ${props.cluster.clusterArn} 87 | 88 | END_TIME=$(date +%s000) 89 | 90 | aws logs get-log-events --log-group-name ${logGroupName} --log-stream-name ${streamPrefix}/${props.name}/\${TASK_ID##*/} --start-time $START_TIME --end-time $END_TIME | jq -r '.events[].message' 91 | `; 92 | 93 | this.executionScript = executionScript; 94 | 95 | } 96 | } -------------------------------------------------------------------------------- /src/constructs/internal/ecs/scheduler/index.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/briancaffey/cdk-django/7decfee3d897d9ea813c17c74d45a9849af60535/src/constructs/internal/ecs/scheduler/index.ts -------------------------------------------------------------------------------- /src/constructs/internal/ecs/web/index.ts: -------------------------------------------------------------------------------- 1 | import { Duration, RemovalPolicy, Stack } from 'aws-cdk-lib'; 2 | import { ISecurityGroup, IVpc } from 'aws-cdk-lib/aws-ec2'; 3 | import { 4 | LogDriver, 5 | Cluster, 6 | ContainerImage, 7 | FargateService, 8 | FargateTaskDefinition, 9 | } from 'aws-cdk-lib/aws-ecs'; 10 | import { 11 | ApplicationListener, 12 | ApplicationListenerRule, 13 | ApplicationProtocol, 14 | ApplicationTargetGroup, 15 | ListenerAction, 16 | ListenerCondition, 17 | TargetType, 18 | } from 'aws-cdk-lib/aws-elasticloadbalancingv2'; 19 | import { Role } from 'aws-cdk-lib/aws-iam'; 20 | import { 21 | LogGroup, 22 | LogStream, 23 | RetentionDays, 24 | } from 'aws-cdk-lib/aws-logs'; 25 | import { Construct } from 'constructs'; 26 | 27 | 28 | export interface WebProps { 29 | readonly name: string; 30 | readonly cluster: Cluster; 31 | readonly vpc: IVpc; 32 | readonly listener: ApplicationListener; 33 | readonly cpu?: number; 34 | readonly memorySize?: number; 35 | readonly appSecurityGroup: ISecurityGroup; 36 | readonly image: ContainerImage; 37 | readonly command: string[]; 38 | readonly useSpot?: boolean; 39 | readonly taskRole: Role; 40 | readonly executionRole: Role; 41 | readonly environmentVariables: { [key: string]: string }; 42 | readonly domainName: string; 43 | readonly pathPatterns: string[]; 44 | readonly port: number; 45 | readonly priority: number; 46 | readonly healthCheckPath: string; 47 | }; 48 | 49 | export class WebService extends Construct { 50 | public service: FargateService; 51 | constructor(scope: Construct, id: string, props: WebProps) { 52 | super(scope, id); 53 | 54 | const stackName = Stack.of(this).stackName; 55 | 56 | // define log group and logstream 57 | const logGroupName = `/ecs/${stackName}/${props.name}/`; 58 | const streamPrefix = props.name; 59 | 60 | // define log group and logstream 61 | const logGroup = new LogGroup(this, 'LogGroup', { 62 | logGroupName, 63 | retention: RetentionDays.ONE_DAY, 64 | removalPolicy: RemovalPolicy.DESTROY, 65 | }); 66 | 67 | new LogStream(this, 'LogStream', { 68 | logGroup, 69 | logStreamName: props.name, 70 | }); 71 | 72 | // task definition 73 | const taskDefinition = new FargateTaskDefinition(this, 'TaskDefinition', { 74 | cpu: props.cpu ?? 256, 75 | executionRole: props.executionRole, 76 | taskRole: props.taskRole, 77 | family: `${stackName}-${props.name}`, 78 | }); 79 | 80 | taskDefinition.addContainer(props.name, { 81 | image: props.image, 82 | command: props.command, 83 | containerName: props.name, 84 | environment: props.environmentVariables, 85 | essential: true, 86 | logging: LogDriver.awsLogs({ 87 | streamPrefix, 88 | logGroup, 89 | }), 90 | portMappings: [{ 91 | containerPort: props.port, 92 | hostPort: props.port, 93 | }], 94 | }); 95 | 96 | const useSpot = props.useSpot ?? false; 97 | 98 | this.service = new FargateService(this, 'Service', { 99 | cluster: props.cluster, 100 | taskDefinition, 101 | assignPublicIp: false, 102 | capacityProviderStrategies: [ 103 | { 104 | capacityProvider: 'FARGATE_SPOT', 105 | weight: useSpot ? 100 : 0, 106 | }, 107 | { 108 | capacityProvider: 'FARGATE', 109 | weight: useSpot ? 0 : 100, 110 | }, 111 | ], 112 | desiredCount: 0, 113 | minHealthyPercent: 50, 114 | enableExecuteCommand: true, 115 | securityGroups: [props.appSecurityGroup], 116 | serviceName: `${stackName}-${props.name}`, 117 | vpcSubnets: { 118 | subnets: props.vpc.privateSubnets, 119 | }, 120 | }); 121 | 122 | const targetGroup = new ApplicationTargetGroup(this, 'TargetGroup', { 123 | targetType: TargetType.IP, 124 | port: props.port, 125 | protocol: ApplicationProtocol.HTTP, 126 | deregistrationDelay: Duration.seconds(30), 127 | healthCheck: { 128 | path: props.healthCheckPath, 129 | interval: Duration.seconds(10), 130 | timeout: Duration.seconds(5), 131 | healthyThresholdCount: 2, 132 | unhealthyThresholdCount: 2, 133 | }, 134 | vpc: props.vpc, 135 | }); 136 | 137 | // const listenerRule = 138 | new ApplicationListenerRule(this, 'ListenerRule', { 139 | listener: props.listener, 140 | priority: props.priority, 141 | // targetGroups: [targetGroup], 142 | conditions: [ 143 | ListenerCondition.pathPatterns(props.pathPatterns), 144 | ListenerCondition.hostHeaders([`${stackName}.${props.domainName}`]), 145 | ], 146 | action: ListenerAction.forward([targetGroup]), 147 | }); 148 | 149 | this.service.attachToApplicationTargetGroup(targetGroup); 150 | 151 | } 152 | } -------------------------------------------------------------------------------- /src/constructs/internal/ecs/worker/index.ts: -------------------------------------------------------------------------------- 1 | import { RemovalPolicy, Stack } from 'aws-cdk-lib'; 2 | import { ISecurityGroup, IVpc } from 'aws-cdk-lib/aws-ec2'; 3 | import { 4 | LogDriver, 5 | Cluster, 6 | ContainerImage, 7 | FargateService, 8 | FargateTaskDefinition, 9 | } from 'aws-cdk-lib/aws-ecs'; 10 | import { Role } from 'aws-cdk-lib/aws-iam'; 11 | import { 12 | LogGroup, 13 | LogStream, 14 | RetentionDays, 15 | } from 'aws-cdk-lib/aws-logs'; 16 | import { Construct } from 'constructs'; 17 | 18 | 19 | export interface WorkerProps { 20 | readonly name: string; 21 | readonly cluster: Cluster; 22 | readonly vpc: IVpc; 23 | readonly cpu?: number; 24 | readonly memorySize?: number; 25 | readonly appSecurityGroup: ISecurityGroup; 26 | readonly image: ContainerImage; 27 | readonly command: string[]; 28 | readonly useSpot?: boolean; 29 | readonly taskRole: Role; 30 | readonly executionRole: Role; 31 | readonly environmentVariables: { [key: string]: string }; 32 | }; 33 | 34 | export class WorkerService extends Construct { 35 | public service: FargateService; 36 | constructor(scope: Construct, id: string, props: WorkerProps) { 37 | super(scope, id); 38 | 39 | const stackName = Stack.of(this).stackName; 40 | 41 | // define log group and logstream 42 | const logGroupName = `/ecs/${stackName}/${props.name}/`; 43 | const streamPrefix = props.name; 44 | 45 | // define log group and logstream 46 | const logGroup = new LogGroup(this, 'LogGroup', { 47 | logGroupName, 48 | retention: RetentionDays.ONE_DAY, 49 | removalPolicy: RemovalPolicy.DESTROY, 50 | }); 51 | 52 | new LogStream(this, 'LogStream', { 53 | logGroup, 54 | logStreamName: props.name, 55 | }); 56 | 57 | // task definition 58 | const taskDefinition = new FargateTaskDefinition(this, 'TaskDefinition', { 59 | cpu: props.cpu ?? 256, 60 | executionRole: props.executionRole, 61 | taskRole: props.taskRole, 62 | family: `${stackName}-${props.name}`, 63 | }); 64 | 65 | taskDefinition.addContainer(props.name, { 66 | image: props.image, 67 | command: props.command, 68 | containerName: props.name, 69 | environment: props.environmentVariables, 70 | essential: true, 71 | logging: LogDriver.awsLogs({ 72 | streamPrefix, 73 | logGroup, 74 | }), 75 | }); 76 | 77 | const useSpot = props.useSpot ?? false; 78 | 79 | this.service = new FargateService(this, 'Service', { 80 | cluster: props.cluster, 81 | taskDefinition, 82 | assignPublicIp: false, 83 | capacityProviderStrategies: [ 84 | { 85 | capacityProvider: 'FARGATE_SPOT', 86 | weight: useSpot ? 100 : 0, 87 | }, 88 | { 89 | capacityProvider: 'FARGATE', 90 | weight: useSpot ? 0 : 100, 91 | }, 92 | ], 93 | desiredCount: 1, 94 | minHealthyPercent: 0, 95 | enableExecuteCommand: true, 96 | securityGroups: [props.appSecurityGroup], 97 | serviceName: `${stackName}-${props.name}`, 98 | vpcSubnets: { 99 | subnets: props.vpc.privateSubnets, 100 | }, 101 | }); 102 | } 103 | } -------------------------------------------------------------------------------- /src/constructs/internal/rds/index.ts: -------------------------------------------------------------------------------- 1 | import * as cdk from 'aws-cdk-lib'; 2 | import { Stack } from 'aws-cdk-lib'; 3 | import { InstanceType, IVpc, Peer, Port, SecurityGroup, SubnetType } from 'aws-cdk-lib/aws-ec2'; 4 | import { DatabaseInstance, DatabaseInstanceEngine, PostgresEngineVersion } from 'aws-cdk-lib/aws-rds'; 5 | import { Secret } from 'aws-cdk-lib/aws-secretsmanager'; 6 | import { Construct } from 'constructs'; 7 | 8 | 9 | interface RdsInstanceProps { 10 | readonly vpc: IVpc; 11 | readonly appSecurityGroup: SecurityGroup; 12 | readonly dbSecretName: string; 13 | readonly instanceClass?: string; 14 | readonly instanceSize?: string; 15 | } 16 | 17 | export class RdsInstance extends Construct { 18 | public rdsInstance: DatabaseInstance; 19 | public rdsPasswordSecretName: string; 20 | private instanceClass: string; 21 | private instanceSize: string; 22 | 23 | 24 | constructor(scope: Construct, id: string, props: RdsInstanceProps) { 25 | super(scope, id); 26 | 27 | const stackName = Stack.of(this).stackName; 28 | 29 | // set instance type from props 30 | this.instanceClass = props.instanceClass ?? 't3'; 31 | this.instanceSize = props.instanceSize ?? 'micro'; 32 | const instanceType = new InstanceType(`${this.instanceClass}.${this.instanceSize}`); 33 | 34 | 35 | const secret = new Secret(scope, 'dbSecret', { 36 | secretName: `${stackName}/rds-postgres-password`, 37 | description: 'secret for rds', 38 | removalPolicy: cdk.RemovalPolicy.DESTROY, 39 | generateSecretString: { 40 | // secretStringTemplate: JSON.stringify({ username: 'postgres' }), 41 | // generateStringKey: 'password', 42 | excludePunctuation: true, 43 | includeSpace: false, 44 | }, 45 | }); 46 | this.rdsPasswordSecretName = secret.secretName; 47 | 48 | const rdsSecurityGroup = new SecurityGroup(this, 'RdsSecurityGroup', { 49 | vpc: props.vpc, 50 | securityGroupName: `${stackName}RdsSecurityGroup`, 51 | }); 52 | 53 | rdsSecurityGroup.addIngressRule(Peer.anyIpv4(), Port.tcp(5432), 'RDS'); 54 | rdsSecurityGroup.addIngressRule(props.appSecurityGroup, Port.tcp(5432), 'AppSecurityGroup'); 55 | 56 | const rdsInstance = new DatabaseInstance(this, 'RdsInstance', { 57 | instanceIdentifier: `${stackName}RdsInstance`, 58 | vpc: props.vpc, 59 | engine: DatabaseInstanceEngine.postgres({ version: PostgresEngineVersion.of('17.2', '17') }), 60 | // credentials: Credentials.fromSecret(secret), 61 | credentials: { username: 'postgres', password: secret.secretValue }, 62 | instanceType, 63 | port: 5432, 64 | securityGroups: [rdsSecurityGroup], 65 | vpcSubnets: { 66 | subnetType: SubnetType.PRIVATE_WITH_EGRESS, 67 | }, 68 | }); 69 | this.rdsInstance = rdsInstance; 70 | } 71 | } -------------------------------------------------------------------------------- /src/constructs/internal/s3/index.ts: -------------------------------------------------------------------------------- 1 | import * as cdk from 'aws-cdk-lib'; 2 | import * as s3 from 'aws-cdk-lib/aws-s3'; 3 | import { Construct } from 'constructs'; 4 | 5 | export interface S3ResourcesProps { 6 | bucketName: string; 7 | forceDestroy: boolean; 8 | publicReadAccess?: boolean; 9 | } 10 | 11 | export class S3Resources extends Construct { 12 | public readonly bucket: s3.Bucket; 13 | public readonly bucketName: string; 14 | 15 | constructor(scope: Construct, id: string, props: S3ResourcesProps) { 16 | super(scope, id); 17 | 18 | const blockPublicAccess = props.publicReadAccess 19 | ? s3.BlockPublicAccess.BLOCK_ACLS 20 | : s3.BlockPublicAccess.BLOCK_ALL; 21 | 22 | this.bucket = new s3.Bucket(this, 'S3Bucket', { 23 | bucketName: props.bucketName, 24 | removalPolicy: props.forceDestroy ? cdk.RemovalPolicy.DESTROY : cdk.RemovalPolicy.RETAIN, 25 | autoDeleteObjects: props.forceDestroy, 26 | encryption: s3.BucketEncryption.S3_MANAGED, 27 | versioned: false, 28 | objectOwnership: s3.ObjectOwnership.BUCKET_OWNER_ENFORCED, 29 | blockPublicAccess: blockPublicAccess, 30 | }); 31 | 32 | this.bucketName = this.bucket.bucketName; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/constructs/internal/sg/index.ts: -------------------------------------------------------------------------------- 1 | import { Stack, Tags } from 'aws-cdk-lib'; 2 | import { IVpc, Peer, Port, SecurityGroup, SubnetType, InterfaceVpcEndpoint, GatewayVpcEndpointAwsService } from 'aws-cdk-lib/aws-ec2'; 3 | import { Construct } from 'constructs'; 4 | 5 | interface SecurityGroupResourcesProps { 6 | readonly vpc: IVpc; 7 | } 8 | 9 | export class SecurityGroupResources extends Construct { 10 | public readonly albSecurityGroup: SecurityGroup; 11 | public readonly appSecurityGroup: SecurityGroup; 12 | public readonly vpceSecurityGroup: SecurityGroup; 13 | 14 | constructor(scope: Construct, id: string, props: SecurityGroupResourcesProps) { 15 | super(scope, id); 16 | 17 | const stackName = Stack.of(this).stackName; 18 | 19 | // ALB Security Group 20 | this.albSecurityGroup = new SecurityGroup(this, 'AlbSecurityGroup', { 21 | vpc: props.vpc, 22 | description: 'ALB security group', 23 | allowAllOutbound: true, 24 | }); 25 | Tags.of(this.albSecurityGroup).add('Name', `${stackName}-alb-sg`); 26 | this.albSecurityGroup.addIngressRule(Peer.anyIpv4(), Port.tcp(80), 'Allow HTTP traffic from anywhere'); 27 | this.albSecurityGroup.addIngressRule(Peer.anyIpv4(), Port.tcp(443), 'Allow HTTPS traffic from anywhere'); 28 | 29 | // Application Security Group 30 | this.appSecurityGroup = new SecurityGroup(this, 'AppSecurityGroup', { 31 | vpc: props.vpc, 32 | description: 'Allows inbound access from the ALB only', 33 | allowAllOutbound: true, 34 | }); 35 | Tags.of(this.appSecurityGroup).add('Name', `${stackName}-ecs-sg`); 36 | this.appSecurityGroup.addIngressRule(this.albSecurityGroup, Port.allTraffic(), 'Allow all traffic from ALB'); 37 | 38 | // VPC Endpoint Security Group 39 | this.vpceSecurityGroup = new SecurityGroup(this, 'VpceSecurityGroup', { 40 | vpc: props.vpc, 41 | description: 'Security Group for VPC Endpoints', 42 | allowAllOutbound: true, 43 | }); 44 | Tags.of(this.vpceSecurityGroup).add('Name', `${stackName}-vpce-sg`); 45 | this.appSecurityGroup.addIngressRule(this.vpceSecurityGroup, Port.tcp(443), 'Allow HTTPS from VPC Endpoint'); 46 | this.vpceSecurityGroup.addIngressRule(this.appSecurityGroup, Port.tcp(443), 'Allow HTTPS to App'); 47 | 48 | // VPC Endpoint - ECR API 49 | const ecrApiEndpoint = new InterfaceVpcEndpoint(this, 'EcrApiEndpoint', { 50 | vpc: props.vpc, 51 | service: { 52 | name: `com.amazonaws.${Stack.of(this).region}.ecr.api`, 53 | port: 443, 54 | }, 55 | subnets: { subnetType: SubnetType.PRIVATE_WITH_EGRESS }, 56 | securityGroups: [this.vpceSecurityGroup], 57 | privateDnsEnabled: true, 58 | }); 59 | Tags.of(ecrApiEndpoint).add('Name', `${stackName}-vpce-ecr-api`); 60 | 61 | // VPC Endpoint - ECR DKR 62 | const ecrDkrEndpoint = new InterfaceVpcEndpoint(this, 'EcrDkrEndpoint', { 63 | vpc: props.vpc, 64 | service: { 65 | name: `com.amazonaws.${Stack.of(this).region}.ecr.dkr`, 66 | port: 443, 67 | }, 68 | subnets: { subnetType: SubnetType.PRIVATE_WITH_EGRESS }, 69 | securityGroups: [this.vpceSecurityGroup], 70 | privateDnsEnabled: true, 71 | }); 72 | Tags.of(ecrDkrEndpoint).add('Name', `${stackName}-vpce-ecr-dkr`); 73 | 74 | // VPC Endpoint - S3 75 | const s3Endpoint = props.vpc.addGatewayEndpoint('S3Endpoint', { 76 | service: GatewayVpcEndpointAwsService.S3, 77 | subnets: [{ subnetType: SubnetType.PRIVATE_WITH_EGRESS }], 78 | }); 79 | Tags.of(s3Endpoint).add('Name', `${stackName}-vpce-s3`); 80 | } 81 | } -------------------------------------------------------------------------------- /src/constructs/internal/vpc/index.ts: -------------------------------------------------------------------------------- 1 | import { Stack, Tags } from 'aws-cdk-lib'; 2 | import { IVpc, SubnetType, Vpc } from 'aws-cdk-lib/aws-ec2'; 3 | 4 | import { Construct } from 'constructs'; 5 | 6 | export class ApplicationVpc extends Construct { 7 | public vpc: IVpc; 8 | constructor(scope: Construct, id: string) { 9 | super(scope, id); 10 | const stackName = Stack.of(this).stackName; 11 | const vpc = new Vpc(this, 'Vpc', { 12 | vpcName: `${stackName}-vpc`, 13 | maxAzs: 2, 14 | natGateways: 1, 15 | enableDnsHostnames: true, 16 | enableDnsSupport: true, 17 | subnetConfiguration: [ 18 | { 19 | cidrMask: 24, 20 | name: 'public', 21 | subnetType: SubnetType.PUBLIC, 22 | }, 23 | { 24 | cidrMask: 24, 25 | name: 'private', 26 | subnetType: SubnetType.PRIVATE_WITH_EGRESS, 27 | }, 28 | ], 29 | }); 30 | this.vpc = vpc; 31 | 32 | // having trouble making sure the VPC resources are getting tagged correctly 33 | Tags.of(vpc).add('env', Stack.of(this).stackName); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/examples/ecs/.env.example: -------------------------------------------------------------------------------- 1 | # copy this file to .env in this directory and run before using cdk commands 2 | export CERTIFICATE_ARN=arn:aws:acm:us-east-1:123456789012:certificate/12345678-1234-1234-1234-123456789012 3 | export DOMAIN_NAME=example.com -------------------------------------------------------------------------------- /src/examples/ecs/.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | -------------------------------------------------------------------------------- /src/examples/ecs/config/alpha.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /src/examples/ecs/config/dev.json: -------------------------------------------------------------------------------- 1 | { 2 | "foo": "bar", 3 | "extraEnvVars": {"alpha": "1", "beta": "2"} 4 | } -------------------------------------------------------------------------------- /src/examples/ecs/index.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import { App, Stack, Tags } from 'aws-cdk-lib'; 3 | import { EcsApp } from '../../constructs/ecs/app'; 4 | import { EcsBase } from '../../constructs/ecs/base'; 5 | 6 | const companyName = process.env.COMPANY_NAME || 'abc'; 7 | const ecsBaseEnvName = process.env.BASE_NAME || 'dev'; 8 | const ecsAppEnvName = process.env.APP_NAME || 'alpha'; 9 | 10 | // TODO: define interfaces for these config and type check them 11 | var ecsBaseEnvConfig = JSON.parse(fs.readFileSync(`src/examples/ecs/config/${ecsBaseEnvName}.json`, 'utf8')); 12 | var ecsAppEnvConfig = JSON.parse(fs.readFileSync(`src/examples/ecs/config/${ecsAppEnvName}.json`, 'utf8')); 13 | 14 | // https://docs.aws.amazon.com/cdk/v2/guide/stack_how_to_create_multiple_stacks.html 15 | const app = new App(); 16 | 17 | const env = { 18 | account: process.env.CDK_DEFAULT_ACCOUNT, 19 | region: process.env.CDK_DEFAULT_REGION, 20 | }; 21 | 22 | const baseStack = new Stack(app, 'ExampleEcsBaseStack', { env, stackName: ecsBaseEnvName }); 23 | baseStack.node.setContext('config', ecsBaseEnvConfig); 24 | 25 | const appStack = new Stack(app, 'ExampleEcsAppStack', { env, stackName: ecsAppEnvName }); 26 | appStack.node.setContext('config', ecsAppEnvConfig); 27 | 28 | const certificateArn = process.env.CERTIFICATE_ARN || 'arn:aws:acm:us-east-1:123456789012:certificate/12345678-1234-1234-1234-123456789012'; 29 | const domainName = process.env.DOMAIN_NAME || 'example.com'; 30 | 31 | const ecsBase = new EcsBase(baseStack, 'EcsBase', { certificateArn, domainName }); 32 | 33 | const ecsApp = new EcsApp(appStack, 'EcsApp', { 34 | baseStackName: ecsBaseEnvName, 35 | vpc: ecsBase.vpc, 36 | alb: ecsBase.alb, 37 | appSecurityGroup: ecsBase.appSecurityGroup, 38 | rdsInstance: ecsBase.databaseInstance, 39 | assetsBucket: ecsBase.assetsBucket, 40 | domainName: ecsBase.domainName, 41 | listener: ecsBase.listener, 42 | elastiCacheHost: ecsBase.elastiCacheHostname, 43 | rdsPasswordSecretName: ecsBase.rdsPasswordSecretName, 44 | companyName, 45 | }); 46 | 47 | /** 48 | * Add tagging for this construct and all child constructs 49 | */ 50 | Tags.of(ecsBase).add('env', ecsBaseEnvName); 51 | Tags.of(ecsApp).add('env', ecsAppEnvName); -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './constructs/ecs/base'; 2 | export * from './constructs/ecs/app'; 3 | -------------------------------------------------------------------------------- /src/utils/priority/index.ts: -------------------------------------------------------------------------------- 1 | import * as AWS from 'aws-sdk'; 2 | 3 | // TODO: use getHighestRulePriority to set listener rule priority for load balancer listener rules 4 | export default async function getHighestRulePriority(listenerArn: string): Promise { 5 | const elb = new AWS.ELBv2(); 6 | const result = await elb.describeRules({ ListenerArn: listenerArn }).promise(); 7 | let highest = 0; 8 | for (const rule of result.Rules ?? []) { 9 | // Skip the default rule 10 | if (rule.Priority && rule.Priority !== 'default') { 11 | const prio = parseInt(rule.Priority, 10); 12 | if (prio > highest) { 13 | highest = prio; 14 | } 15 | } 16 | } 17 | return highest; 18 | } 19 | 20 | // example showing how to use the getHighestRulePriority function 21 | // (async () => { 22 | // const listenerArn = process.argv[2]; 23 | // if (!listenerArn) { 24 | // console.error('Usage: npx ts-node main.ts '); 25 | // process.exit(1); 26 | // } 27 | 28 | // try { 29 | // const highest = await getHighestRulePriority(listenerArn); 30 | // console.log(`Highest rule priority for listener ${listenerArn} is ${highest}`); 31 | // } catch (error) { 32 | // console.error('Error fetching highest rule priority:', error); 33 | // process.exit(1); 34 | // } 35 | // })(); 36 | -------------------------------------------------------------------------------- /test/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/briancaffey/cdk-django/7decfee3d897d9ea813c17c74d45a9849af60535/test/.gitignore -------------------------------------------------------------------------------- /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 | ] 37 | } 38 | --------------------------------------------------------------------------------