├── .eslintrc.json ├── .gitattributes ├── .github ├── pull_request_template.md └── workflows │ ├── auto-approve.yml │ ├── build.yml │ ├── pull-request-lint.yml │ ├── release.yml │ └── upgrade-main.yml ├── .gitignore ├── .gitpod.yml ├── .mergify.yml ├── .npmignore ├── .projen ├── deps.json ├── files.json └── tasks.json ├── .projenrc.js ├── API.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── NOTICE ├── README.md ├── package.json ├── src ├── index.ts ├── integ.default.ts ├── keycloak.ts └── keycloak.ts.orig ├── test ├── __snapshots__ │ ├── integ.snapshot.test.ts.snap │ ├── integ.snapshot.test.ts.snap.orig │ └── integ.snapshot.test.ts.snap.rej ├── cluster-quarkus.test.ts ├── cluster-wildfly.test.ts └── integ.snapshot.test.ts ├── tsconfig.dev.json └── yarn.lock /.eslintrc.json: -------------------------------------------------------------------------------- 1 | // ~~ Generated by projen. To modify, edit .projenrc.js and run "npx projen". 2 | { 3 | "env": { 4 | "jest": true, 5 | "node": true 6 | }, 7 | "root": true, 8 | "plugins": [ 9 | "@typescript-eslint", 10 | "import" 11 | ], 12 | "parser": "@typescript-eslint/parser", 13 | "parserOptions": { 14 | "ecmaVersion": 2018, 15 | "sourceType": "module", 16 | "project": "./tsconfig.dev.json" 17 | }, 18 | "extends": [ 19 | "plugin:import/typescript" 20 | ], 21 | "settings": { 22 | "import/parsers": { 23 | "@typescript-eslint/parser": [ 24 | ".ts", 25 | ".tsx" 26 | ] 27 | }, 28 | "import/resolver": { 29 | "node": {}, 30 | "typescript": { 31 | "project": "./tsconfig.dev.json", 32 | "alwaysTryTypes": true 33 | } 34 | } 35 | }, 36 | "ignorePatterns": [ 37 | "*.js", 38 | "*.d.ts", 39 | "node_modules/", 40 | "*.generated.ts", 41 | "coverage", 42 | "!.projenrc.js" 43 | ], 44 | "rules": { 45 | "indent": [ 46 | "off" 47 | ], 48 | "@typescript-eslint/indent": [ 49 | "error", 50 | 2 51 | ], 52 | "quotes": [ 53 | "error", 54 | "single", 55 | { 56 | "avoidEscape": true 57 | } 58 | ], 59 | "comma-dangle": [ 60 | "error", 61 | "always-multiline" 62 | ], 63 | "comma-spacing": [ 64 | "error", 65 | { 66 | "before": false, 67 | "after": true 68 | } 69 | ], 70 | "no-multi-spaces": [ 71 | "error", 72 | { 73 | "ignoreEOLComments": false 74 | } 75 | ], 76 | "array-bracket-spacing": [ 77 | "error", 78 | "never" 79 | ], 80 | "array-bracket-newline": [ 81 | "error", 82 | "consistent" 83 | ], 84 | "object-curly-spacing": [ 85 | "error", 86 | "always" 87 | ], 88 | "object-curly-newline": [ 89 | "error", 90 | { 91 | "multiline": true, 92 | "consistent": true 93 | } 94 | ], 95 | "object-property-newline": [ 96 | "error", 97 | { 98 | "allowAllPropertiesOnSameLine": true 99 | } 100 | ], 101 | "keyword-spacing": [ 102 | "error" 103 | ], 104 | "brace-style": [ 105 | "error", 106 | "1tbs", 107 | { 108 | "allowSingleLine": true 109 | } 110 | ], 111 | "space-before-blocks": [ 112 | "error" 113 | ], 114 | "curly": [ 115 | "error", 116 | "multi-line", 117 | "consistent" 118 | ], 119 | "@typescript-eslint/member-delimiter-style": [ 120 | "error" 121 | ], 122 | "semi": [ 123 | "error", 124 | "always" 125 | ], 126 | "max-len": [ 127 | "error", 128 | { 129 | "code": 150, 130 | "ignoreUrls": true, 131 | "ignoreStrings": true, 132 | "ignoreTemplateLiterals": true, 133 | "ignoreComments": true, 134 | "ignoreRegExpLiterals": true 135 | } 136 | ], 137 | "quote-props": [ 138 | "error", 139 | "consistent-as-needed" 140 | ], 141 | "@typescript-eslint/no-require-imports": [ 142 | "error" 143 | ], 144 | "import/no-extraneous-dependencies": [ 145 | "error", 146 | { 147 | "devDependencies": [ 148 | "**/test/**", 149 | "**/build-tools/**" 150 | ], 151 | "optionalDependencies": false, 152 | "peerDependencies": true 153 | } 154 | ], 155 | "import/no-unresolved": [ 156 | "error" 157 | ], 158 | "import/order": [ 159 | "warn", 160 | { 161 | "groups": [ 162 | "builtin", 163 | "external" 164 | ], 165 | "alphabetize": { 166 | "order": "asc", 167 | "caseInsensitive": true 168 | } 169 | } 170 | ], 171 | "no-duplicate-imports": [ 172 | "error" 173 | ], 174 | "no-shadow": [ 175 | "off" 176 | ], 177 | "@typescript-eslint/no-shadow": [ 178 | "error" 179 | ], 180 | "key-spacing": [ 181 | "error" 182 | ], 183 | "no-multiple-empty-lines": [ 184 | "error" 185 | ], 186 | "@typescript-eslint/no-floating-promises": [ 187 | "error" 188 | ], 189 | "no-return-await": [ 190 | "off" 191 | ], 192 | "@typescript-eslint/return-await": [ 193 | "error" 194 | ], 195 | "no-trailing-spaces": [ 196 | "error" 197 | ], 198 | "dot-notation": [ 199 | "error" 200 | ], 201 | "no-bitwise": [ 202 | "error" 203 | ], 204 | "@typescript-eslint/member-ordering": [ 205 | "error", 206 | { 207 | "default": [ 208 | "public-static-field", 209 | "public-static-method", 210 | "protected-static-field", 211 | "protected-static-method", 212 | "private-static-field", 213 | "private-static-method", 214 | "field", 215 | "constructor", 216 | "method" 217 | ] 218 | } 219 | ] 220 | }, 221 | "overrides": [ 222 | { 223 | "files": [ 224 | ".projenrc.js" 225 | ], 226 | "rules": { 227 | "@typescript-eslint/no-require-imports": "off", 228 | "import/no-extraneous-dependencies": "off" 229 | } 230 | } 231 | ] 232 | } 233 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.js and run "npx projen". 2 | 3 | *.snap linguist-generated 4 | /.eslintrc.json linguist-generated 5 | /.gitattributes linguist-generated 6 | /.github/pull_request_template.md linguist-generated 7 | /.github/workflows/auto-approve.yml 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 | /.gitpod.yml linguist-generated 14 | /.mergify.yml linguist-generated 15 | /.npmignore linguist-generated 16 | /.npmrc linguist-generated 17 | /.projen/** linguist-generated 18 | /.projen/deps.json linguist-generated 19 | /.projen/files.json linguist-generated 20 | /.projen/tasks.json linguist-generated 21 | /API.md linguist-generated 22 | /LICENSE linguist-generated 23 | /package.json linguist-generated 24 | /tsconfig.dev.json linguist-generated 25 | /yarn.lock linguist-generated -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | Fixes # -------------------------------------------------------------------------------- /.github/workflows/auto-approve.yml: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.js and run "npx projen". 2 | 3 | name: auto-approve 4 | on: 5 | pull_request_target: 6 | types: 7 | - labeled 8 | - opened 9 | - synchronize 10 | - reopened 11 | - ready_for_review 12 | jobs: 13 | approve: 14 | runs-on: ubuntu-latest 15 | permissions: 16 | pull-requests: write 17 | if: contains(github.event.pull_request.labels.*.name, 'auto-approve') && (github.event.pull_request.user.login == 'pahud') 18 | steps: 19 | - uses: hmarr/auto-approve-action@v2.2.1 20 | with: 21 | github-token: ${{ secrets.GITHUB_TOKEN }} 22 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.js 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@v3 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@v3 24 | with: 25 | node-version: 18.x 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 | - name: Upload patch 36 | if: steps.self_mutation.outputs.self_mutation_happened 37 | uses: actions/upload-artifact@v3 38 | with: 39 | name: .repo.patch 40 | path: .repo.patch 41 | - name: Fail build on mutation 42 | if: steps.self_mutation.outputs.self_mutation_happened 43 | run: |- 44 | echo "::error::Files were changed during build (see build log). If this was triggered from a fork, you will need to update your branch." 45 | cat .repo.patch 46 | exit 1 47 | - name: Backup artifact permissions 48 | run: cd dist && getfacl -R . > permissions-backup.acl 49 | continue-on-error: true 50 | - name: Upload artifact 51 | uses: actions/upload-artifact@v3 52 | with: 53 | name: build-artifact 54 | path: dist 55 | self-mutation: 56 | needs: build 57 | runs-on: ubuntu-latest 58 | permissions: 59 | contents: write 60 | if: always() && needs.build.outputs.self_mutation_happened && !(github.event.pull_request.head.repo.full_name != github.repository) 61 | steps: 62 | - name: Checkout 63 | uses: actions/checkout@v3 64 | with: 65 | token: ${{ secrets.PROJEN_GITHUB_TOKEN }} 66 | ref: ${{ github.event.pull_request.head.ref }} 67 | repository: ${{ github.event.pull_request.head.repo.full_name }} 68 | - name: Download patch 69 | uses: actions/download-artifact@v3 70 | with: 71 | name: .repo.patch 72 | path: ${{ runner.temp }} 73 | - name: Apply patch 74 | run: '[ -s ${{ runner.temp }}/.repo.patch ] && git apply ${{ runner.temp }}/.repo.patch || echo "Empty patch. Skipping."' 75 | - name: Set git identity 76 | run: |- 77 | git config user.name "github-actions" 78 | git config user.email "github-actions@github.com" 79 | - name: Push changes 80 | env: 81 | PULL_REQUEST_REF: ${{ github.event.pull_request.head.ref }} 82 | run: |- 83 | git add . 84 | git commit -s -m "chore: self mutation" 85 | git push origin HEAD:$PULL_REQUEST_REF 86 | package-js: 87 | needs: build 88 | runs-on: ubuntu-latest 89 | permissions: {} 90 | if: "! needs.build.outputs.self_mutation_happened" 91 | steps: 92 | - uses: actions/setup-node@v3 93 | with: 94 | node-version: 18.x 95 | - name: Download build artifacts 96 | uses: actions/download-artifact@v3 97 | with: 98 | name: build-artifact 99 | path: dist 100 | - name: Restore build artifact permissions 101 | run: cd dist && setfacl --restore=permissions-backup.acl 102 | continue-on-error: true 103 | - name: Prepare Repository 104 | run: mv dist .repo 105 | - name: Install Dependencies 106 | run: cd .repo && yarn install --check-files --frozen-lockfile 107 | - name: Create js artifact 108 | run: cd .repo && npx projen package:js 109 | - name: Collect js Artifact 110 | run: mv .repo/dist dist 111 | package-python: 112 | needs: build 113 | runs-on: ubuntu-latest 114 | permissions: {} 115 | if: "! needs.build.outputs.self_mutation_happened" 116 | steps: 117 | - uses: actions/setup-node@v3 118 | with: 119 | node-version: 18.x 120 | - uses: actions/setup-python@v4 121 | with: 122 | python-version: 3.x 123 | - name: Download build artifacts 124 | uses: actions/download-artifact@v3 125 | with: 126 | name: build-artifact 127 | path: dist 128 | - name: Restore build artifact permissions 129 | run: cd dist && setfacl --restore=permissions-backup.acl 130 | continue-on-error: true 131 | - name: Prepare Repository 132 | run: mv dist .repo 133 | - name: Install Dependencies 134 | run: cd .repo && yarn install --check-files --frozen-lockfile 135 | - name: Create python artifact 136 | run: cd .repo && npx projen package:python 137 | - name: Collect python Artifact 138 | run: mv .repo/dist dist 139 | -------------------------------------------------------------------------------- /.github/workflows/pull-request-lint.yml: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.js 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 | jobs: 14 | validate: 15 | name: Validate PR title 16 | runs-on: ubuntu-latest 17 | permissions: 18 | pull-requests: write 19 | steps: 20 | - uses: amannn/action-semantic-pull-request@v5.0.2 21 | env: 22 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 23 | with: 24 | types: |- 25 | feat 26 | fix 27 | chore 28 | requireScope: false 29 | githubBaseUrl: ${{ github.api_url }} 30 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.js and run "npx projen". 2 | 3 | name: release 4 | on: 5 | push: 6 | branches: 7 | - main 8 | workflow_dispatch: {} 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 | env: 17 | CI: "true" 18 | steps: 19 | - name: Checkout 20 | uses: actions/checkout@v3 21 | with: 22 | fetch-depth: 0 23 | - name: Set git identity 24 | run: |- 25 | git config user.name "github-actions" 26 | git config user.email "github-actions@github.com" 27 | - name: Setup Node.js 28 | uses: actions/setup-node@v3 29 | with: 30 | node-version: 18.x 31 | - name: Install dependencies 32 | run: yarn install --check-files --frozen-lockfile 33 | - name: release 34 | run: npx projen release 35 | - name: Check for new commits 36 | id: git_remote 37 | run: echo "latest_commit=$(git ls-remote origin -h ${{ github.ref }} | cut -f1)" >> $GITHUB_OUTPUT 38 | - name: Backup artifact permissions 39 | if: ${{ steps.git_remote.outputs.latest_commit == github.sha }} 40 | run: cd dist && getfacl -R . > permissions-backup.acl 41 | continue-on-error: true 42 | - name: Upload artifact 43 | if: ${{ steps.git_remote.outputs.latest_commit == github.sha }} 44 | uses: actions/upload-artifact@v3 45 | with: 46 | name: build-artifact 47 | path: dist 48 | release_github: 49 | name: Publish to GitHub Releases 50 | needs: release 51 | runs-on: ubuntu-latest 52 | permissions: 53 | contents: write 54 | if: needs.release.outputs.latest_commit == github.sha 55 | steps: 56 | - uses: actions/setup-node@v3 57 | with: 58 | node-version: 18.x 59 | - name: Download build artifacts 60 | uses: actions/download-artifact@v3 61 | with: 62 | name: build-artifact 63 | path: dist 64 | - name: Restore build artifact permissions 65 | run: cd dist && setfacl --restore=permissions-backup.acl 66 | continue-on-error: true 67 | - name: Prepare Repository 68 | run: mv dist .repo 69 | - name: Collect GitHub Metadata 70 | run: mv .repo/dist dist 71 | - name: Release 72 | env: 73 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 74 | GITHUB_REPOSITORY: ${{ github.repository }} 75 | GITHUB_REF: ${{ github.ref }} 76 | 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 77 | release_npm: 78 | name: Publish to npm 79 | needs: release 80 | runs-on: ubuntu-latest 81 | permissions: 82 | contents: read 83 | if: needs.release.outputs.latest_commit == github.sha 84 | steps: 85 | - uses: actions/setup-node@v3 86 | with: 87 | node-version: 18.x 88 | - name: Download build artifacts 89 | uses: actions/download-artifact@v3 90 | with: 91 | name: build-artifact 92 | path: dist 93 | - name: Restore build artifact permissions 94 | run: cd dist && setfacl --restore=permissions-backup.acl 95 | continue-on-error: true 96 | - name: Prepare Repository 97 | run: mv dist .repo 98 | - name: Install Dependencies 99 | run: cd .repo && yarn install --check-files --frozen-lockfile 100 | - name: Create js artifact 101 | run: cd .repo && npx projen package:js 102 | - name: Collect js Artifact 103 | run: mv .repo/dist dist 104 | - name: Release 105 | env: 106 | NPM_DIST_TAG: latest 107 | NPM_REGISTRY: registry.npmjs.org 108 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 109 | run: npx -p publib@latest publib-npm 110 | release_pypi: 111 | name: Publish to PyPI 112 | needs: release 113 | runs-on: ubuntu-latest 114 | permissions: 115 | contents: read 116 | if: needs.release.outputs.latest_commit == github.sha 117 | steps: 118 | - uses: actions/setup-node@v3 119 | with: 120 | node-version: 18.x 121 | - uses: actions/setup-python@v4 122 | with: 123 | python-version: 3.x 124 | - name: Download build artifacts 125 | uses: actions/download-artifact@v3 126 | with: 127 | name: build-artifact 128 | path: dist 129 | - name: Restore build artifact permissions 130 | run: cd dist && setfacl --restore=permissions-backup.acl 131 | continue-on-error: true 132 | - name: Prepare Repository 133 | run: mv dist .repo 134 | - name: Install Dependencies 135 | run: cd .repo && yarn install --check-files --frozen-lockfile 136 | - name: Create python artifact 137 | run: cd .repo && npx projen package:python 138 | - name: Collect python Artifact 139 | run: mv .repo/dist dist 140 | - name: Release 141 | env: 142 | TWINE_USERNAME: ${{ secrets.TWINE_USERNAME }} 143 | TWINE_PASSWORD: ${{ secrets.TWINE_PASSWORD }} 144 | run: npx -p publib@latest publib-pypi 145 | -------------------------------------------------------------------------------- /.github/workflows/upgrade-main.yml: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.js 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@v3 19 | with: 20 | ref: main 21 | - name: Setup Node.js 22 | uses: actions/setup-node@v3 23 | with: 24 | node-version: 18.x 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 | - name: Upload patch 35 | if: steps.create_patch.outputs.patch_created 36 | uses: actions/upload-artifact@v3 37 | with: 38 | name: .repo.patch 39 | path: .repo.patch 40 | pr: 41 | name: Create Pull Request 42 | needs: upgrade 43 | runs-on: ubuntu-latest 44 | permissions: 45 | contents: read 46 | if: ${{ needs.upgrade.outputs.patch_created }} 47 | steps: 48 | - name: Checkout 49 | uses: actions/checkout@v3 50 | with: 51 | ref: main 52 | - name: Download patch 53 | uses: actions/download-artifact@v3 54 | with: 55 | name: .repo.patch 56 | path: ${{ runner.temp }} 57 | - name: Apply patch 58 | run: '[ -s ${{ runner.temp }}/.repo.patch ] && git apply ${{ runner.temp }}/.repo.patch || echo "Empty patch. Skipping."' 59 | - name: Set git identity 60 | run: |- 61 | git config user.name "github-actions" 62 | git config user.email "github-actions@github.com" 63 | - name: Create Pull Request 64 | id: create-pr 65 | uses: peter-evans/create-pull-request@v4 66 | with: 67 | token: ${{ secrets.PROJEN_GITHUB_TOKEN }} 68 | commit-message: |- 69 | chore(deps): upgrade dependencies 70 | 71 | Upgrades project dependencies. See details in [workflow run]. 72 | 73 | [Workflow Run]: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} 74 | 75 | ------ 76 | 77 | *Automatically created by projen via the "upgrade-main" workflow* 78 | branch: github-actions/upgrade-main 79 | title: "chore(deps): upgrade dependencies" 80 | labels: auto-approve,auto-merge 81 | body: |- 82 | Upgrades project dependencies. See details in [workflow run]. 83 | 84 | [Workflow Run]: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} 85 | 86 | ------ 87 | 88 | *Automatically created by projen via the "upgrade-main" workflow* 89 | author: github-actions 90 | committer: github-actions 91 | signoff: true 92 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.js 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 | !/.github/workflows/auto-approve.yml 8 | !/package.json 9 | !/LICENSE 10 | !/.npmignore 11 | logs 12 | *.log 13 | npm-debug.log* 14 | yarn-debug.log* 15 | yarn-error.log* 16 | lerna-debug.log* 17 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 18 | pids 19 | *.pid 20 | *.seed 21 | *.pid.lock 22 | lib-cov 23 | coverage 24 | *.lcov 25 | .nyc_output 26 | build/Release 27 | node_modules/ 28 | jspm_packages/ 29 | *.tsbuildinfo 30 | .eslintcache 31 | *.tgz 32 | .yarn-integrity 33 | .cache 34 | !/.projenrc.js 35 | /test-reports/ 36 | junit.xml 37 | /coverage/ 38 | !/.github/workflows/build.yml 39 | /dist/changelog.md 40 | /dist/version.txt 41 | !/.github/workflows/release.yml 42 | !/.mergify.yml 43 | !/.github/workflows/upgrade-main.yml 44 | !/.github/pull_request_template.md 45 | !/.npmrc 46 | !/test/ 47 | !/tsconfig.dev.json 48 | !/src/ 49 | /lib 50 | /dist/ 51 | !/.eslintrc.json 52 | .jsii 53 | tsconfig.json 54 | !/API.md 55 | !/.gitpod.yml 56 | cdk.out 57 | cdk.context.json 58 | images 59 | yarn-error.log 60 | dependabot.yml 61 | -------------------------------------------------------------------------------- /.gitpod.yml: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.js and run "npx projen". 2 | 3 | image: public.ecr.aws/pahudnet/gitpod-workspace:latest 4 | tasks: 5 | - command: npx projen upgrade 6 | init: yarn gitpod:prebuild 7 | github: 8 | prebuilds: 9 | addCheck: true 10 | addBadge: true 11 | addLabel: true 12 | branches: true 13 | pullRequests: true 14 | pullRequestsFromForks: true 15 | vscode: 16 | extensions: 17 | - dbaeumer.vscode-eslint 18 | - ms-azuretools.vscode-docker 19 | - AmazonWebServices.aws-toolkit-vscode 20 | -------------------------------------------------------------------------------- /.mergify.yml: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.js and run "npx projen". 2 | 3 | queue_rules: 4 | - name: default 5 | update_method: merge 6 | conditions: 7 | - "#approved-reviews-by>=1" 8 | - -label~=(do-not-merge) 9 | - status-success=build 10 | - status-success=package-js 11 | - status-success=package-python 12 | pull_request_rules: 13 | - name: Automatic merge on approval and successful build 14 | actions: 15 | delete_head_branch: {} 16 | queue: 17 | method: squash 18 | name: default 19 | commit_message_template: |- 20 | {{ title }} (#{{ number }}) 21 | 22 | {{ body }} 23 | conditions: 24 | - "#approved-reviews-by>=1" 25 | - -label~=(do-not-merge) 26 | - status-success=build 27 | - status-success=package-js 28 | - status-success=package-python 29 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.js and run "npx projen". 2 | /.projen/ 3 | /test-reports/ 4 | junit.xml 5 | /coverage/ 6 | permissions-backup.acl 7 | /dist/changelog.md 8 | /dist/version.txt 9 | /.mergify.yml 10 | /test/ 11 | /tsconfig.dev.json 12 | /src/ 13 | !/lib/ 14 | !/lib/**/*.js 15 | !/lib/**/*.d.ts 16 | dist 17 | /tsconfig.json 18 | /.github/ 19 | /.vscode/ 20 | /.idea/ 21 | /.projenrc.js 22 | tsconfig.tsbuildinfo 23 | /.eslintrc.json 24 | !.jsii 25 | cdk.out 26 | cdk.context.json 27 | images 28 | yarn-error.log 29 | dependabot.yml 30 | -------------------------------------------------------------------------------- /.projen/deps.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": [ 3 | { 4 | "name": "@types/jest", 5 | "version": "^27", 6 | "type": "build" 7 | }, 8 | { 9 | "name": "@types/node", 10 | "version": "^16", 11 | "type": "build" 12 | }, 13 | { 14 | "name": "@typescript-eslint/eslint-plugin", 15 | "version": "^6", 16 | "type": "build" 17 | }, 18 | { 19 | "name": "@typescript-eslint/parser", 20 | "version": "^6", 21 | "type": "build" 22 | }, 23 | { 24 | "name": "eslint-import-resolver-node", 25 | "type": "build" 26 | }, 27 | { 28 | "name": "eslint-import-resolver-typescript", 29 | "type": "build" 30 | }, 31 | { 32 | "name": "eslint-plugin-import", 33 | "type": "build" 34 | }, 35 | { 36 | "name": "eslint", 37 | "version": "^8", 38 | "type": "build" 39 | }, 40 | { 41 | "name": "jest-junit", 42 | "version": "^15", 43 | "type": "build" 44 | }, 45 | { 46 | "name": "jest", 47 | "version": "^27", 48 | "type": "build" 49 | }, 50 | { 51 | "name": "jsii-diff", 52 | "type": "build" 53 | }, 54 | { 55 | "name": "jsii-docgen", 56 | "type": "build" 57 | }, 58 | { 59 | "name": "jsii-pacmak", 60 | "type": "build" 61 | }, 62 | { 63 | "name": "jsii-rosetta", 64 | "version": "1.x", 65 | "type": "build" 66 | }, 67 | { 68 | "name": "jsii", 69 | "version": "1.x", 70 | "type": "build" 71 | }, 72 | { 73 | "name": "npm-check-updates", 74 | "version": "^16", 75 | "type": "build" 76 | }, 77 | { 78 | "name": "projen", 79 | "type": "build" 80 | }, 81 | { 82 | "name": "standard-version", 83 | "version": "^9", 84 | "type": "build" 85 | }, 86 | { 87 | "name": "ts-jest", 88 | "version": "^27", 89 | "type": "build" 90 | }, 91 | { 92 | "name": "typescript", 93 | "type": "build" 94 | }, 95 | { 96 | "name": "@types/babel__traverse", 97 | "version": "7.18.2", 98 | "type": "override" 99 | }, 100 | { 101 | "name": "@types/prettier", 102 | "version": "2.6.0", 103 | "type": "override" 104 | }, 105 | { 106 | "name": "aws-cdk-lib", 107 | "version": "^2.100.0", 108 | "type": "peer" 109 | }, 110 | { 111 | "name": "constructs", 112 | "version": "^10.0.5", 113 | "type": "peer" 114 | } 115 | ], 116 | "//": "~~ Generated by projen. To modify, edit .projenrc.js and run \"npx projen\"." 117 | } 118 | -------------------------------------------------------------------------------- /.projen/files.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | ".eslintrc.json", 4 | ".gitattributes", 5 | ".github/pull_request_template.md", 6 | ".github/workflows/auto-approve.yml", 7 | ".github/workflows/build.yml", 8 | ".github/workflows/pull-request-lint.yml", 9 | ".github/workflows/release.yml", 10 | ".github/workflows/upgrade-main.yml", 11 | ".gitignore", 12 | ".gitpod.yml", 13 | ".mergify.yml", 14 | ".npmrc", 15 | ".projen/deps.json", 16 | ".projen/files.json", 17 | ".projen/tasks.json", 18 | "LICENSE", 19 | "tsconfig.dev.json" 20 | ], 21 | "//": "~~ Generated by projen. To modify, edit .projenrc.js and run \"npx projen\"." 22 | } 23 | -------------------------------------------------------------------------------- /.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 | }, 37 | "steps": [ 38 | { 39 | "builtin": "release/bump-version" 40 | } 41 | ], 42 | "condition": "! git log --oneline -1 | grep -q \"chore(release):\"" 43 | }, 44 | "clobber": { 45 | "name": "clobber", 46 | "description": "hard resets to HEAD of origin and cleans the local repo", 47 | "env": { 48 | "BRANCH": "$(git branch --show-current)" 49 | }, 50 | "steps": [ 51 | { 52 | "exec": "git checkout -b scratch", 53 | "name": "save current HEAD in \"scratch\" branch" 54 | }, 55 | { 56 | "exec": "git checkout $BRANCH" 57 | }, 58 | { 59 | "exec": "git fetch origin", 60 | "name": "fetch latest changes from origin" 61 | }, 62 | { 63 | "exec": "git reset --hard origin/$BRANCH", 64 | "name": "hard reset to origin commit" 65 | }, 66 | { 67 | "exec": "git clean -fdx", 68 | "name": "clean all untracked files" 69 | }, 70 | { 71 | "say": "ready to rock! (unpushed commits are under the \"scratch\" branch)" 72 | } 73 | ], 74 | "condition": "git diff --exit-code > /dev/null" 75 | }, 76 | "compat": { 77 | "name": "compat", 78 | "description": "Perform API compatibility check against latest version", 79 | "steps": [ 80 | { 81 | "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)" 82 | } 83 | ] 84 | }, 85 | "compile": { 86 | "name": "compile", 87 | "description": "Only compile", 88 | "steps": [ 89 | { 90 | "exec": "jsii --silence-warnings=reserved-word" 91 | } 92 | ] 93 | }, 94 | "default": { 95 | "name": "default", 96 | "description": "Synthesize project files", 97 | "steps": [ 98 | { 99 | "exec": "node .projenrc.js" 100 | } 101 | ] 102 | }, 103 | "docgen": { 104 | "name": "docgen", 105 | "description": "Generate API.md from .jsii manifest", 106 | "steps": [ 107 | { 108 | "exec": "jsii-docgen -o API.md" 109 | } 110 | ] 111 | }, 112 | "eject": { 113 | "name": "eject", 114 | "description": "Remove projen from the project", 115 | "env": { 116 | "PROJEN_EJECTING": "true" 117 | }, 118 | "steps": [ 119 | { 120 | "spawn": "default" 121 | } 122 | ] 123 | }, 124 | "eslint": { 125 | "name": "eslint", 126 | "description": "Runs eslint against the codebase", 127 | "steps": [ 128 | { 129 | "exec": "eslint --ext .ts,.tsx --fix --no-error-on-unmatched-pattern src test build-tools .projenrc.js" 130 | } 131 | ] 132 | }, 133 | "gitpod:prebuild": { 134 | "name": "gitpod:prebuild", 135 | "description": "Prebuild setup for Gitpod", 136 | "steps": [ 137 | { 138 | "exec": "yarn install --frozen-lockfile --check-files" 139 | }, 140 | { 141 | "exec": "npx projen compile" 142 | } 143 | ] 144 | }, 145 | "install": { 146 | "name": "install", 147 | "description": "Install project dependencies and update lockfile (non-frozen)", 148 | "steps": [ 149 | { 150 | "exec": "yarn install --check-files" 151 | } 152 | ] 153 | }, 154 | "install:ci": { 155 | "name": "install:ci", 156 | "description": "Install project dependencies using frozen lockfile", 157 | "steps": [ 158 | { 159 | "exec": "yarn install --check-files --frozen-lockfile" 160 | } 161 | ] 162 | }, 163 | "package": { 164 | "name": "package", 165 | "description": "Creates the distribution package", 166 | "steps": [ 167 | { 168 | "exec": "if [ ! -z ${CI} ]; then rsync -a . .repo --exclude .git --exclude node_modules && rm -rf dist && mv .repo dist; else npx projen package-all; fi" 169 | } 170 | ] 171 | }, 172 | "package-all": { 173 | "name": "package-all", 174 | "description": "Packages artifacts for all target languages", 175 | "steps": [ 176 | { 177 | "spawn": "package:js" 178 | }, 179 | { 180 | "spawn": "package:python" 181 | } 182 | ] 183 | }, 184 | "package:js": { 185 | "name": "package:js", 186 | "description": "Create js language bindings", 187 | "steps": [ 188 | { 189 | "exec": "jsii-pacmak -v --target js" 190 | } 191 | ] 192 | }, 193 | "package:python": { 194 | "name": "package:python", 195 | "description": "Create python language bindings", 196 | "steps": [ 197 | { 198 | "exec": "jsii-pacmak -v --target python" 199 | } 200 | ] 201 | }, 202 | "post-compile": { 203 | "name": "post-compile", 204 | "description": "Runs after successful compilation", 205 | "steps": [ 206 | { 207 | "spawn": "docgen" 208 | } 209 | ] 210 | }, 211 | "post-upgrade": { 212 | "name": "post-upgrade", 213 | "description": "Runs after upgrading dependencies" 214 | }, 215 | "pre-compile": { 216 | "name": "pre-compile", 217 | "description": "Prepare the project for compilation" 218 | }, 219 | "release": { 220 | "name": "release", 221 | "description": "Prepare a release from \"main\" branch", 222 | "env": { 223 | "RELEASE": "true", 224 | "MAJOR": "2" 225 | }, 226 | "steps": [ 227 | { 228 | "exec": "rm -fr dist" 229 | }, 230 | { 231 | "spawn": "bump" 232 | }, 233 | { 234 | "spawn": "build" 235 | }, 236 | { 237 | "spawn": "unbump" 238 | }, 239 | { 240 | "exec": "git diff --ignore-space-at-eol --exit-code" 241 | } 242 | ] 243 | }, 244 | "test": { 245 | "name": "test", 246 | "description": "Run tests", 247 | "steps": [ 248 | { 249 | "exec": "jest --passWithNoTests --updateSnapshot", 250 | "receiveArgs": true 251 | }, 252 | { 253 | "spawn": "eslint" 254 | } 255 | ] 256 | }, 257 | "test:watch": { 258 | "name": "test:watch", 259 | "description": "Run jest in watch mode", 260 | "steps": [ 261 | { 262 | "exec": "jest --watch" 263 | } 264 | ] 265 | }, 266 | "unbump": { 267 | "name": "unbump", 268 | "description": "Restores version to 0.0.0", 269 | "env": { 270 | "OUTFILE": "package.json", 271 | "CHANGELOG": "dist/changelog.md", 272 | "BUMPFILE": "dist/version.txt", 273 | "RELEASETAG": "dist/releasetag.txt", 274 | "RELEASE_TAG_PREFIX": "" 275 | }, 276 | "steps": [ 277 | { 278 | "builtin": "release/reset-version" 279 | } 280 | ] 281 | }, 282 | "upgrade": { 283 | "name": "upgrade", 284 | "description": "upgrade dependencies", 285 | "env": { 286 | "CI": "0" 287 | }, 288 | "steps": [ 289 | { 290 | "exec": "yarn upgrade npm-check-updates" 291 | }, 292 | { 293 | "exec": "npm-check-updates --upgrade --target=minor --peer --dep=dev,peer,prod,optional --filter=@types/jest,@types/node,@typescript-eslint/eslint-plugin,@typescript-eslint/parser,eslint-import-resolver-node,eslint-import-resolver-typescript,eslint-plugin-import,eslint,jest-junit,jest,jsii-diff,jsii-docgen,jsii-pacmak,npm-check-updates,projen,standard-version,ts-jest,typescript,aws-cdk-lib,constructs" 294 | }, 295 | { 296 | "exec": "yarn install --check-files" 297 | }, 298 | { 299 | "exec": "yarn upgrade @types/jest @types/node @typescript-eslint/eslint-plugin @typescript-eslint/parser eslint-import-resolver-node eslint-import-resolver-typescript eslint-plugin-import eslint jest-junit jest jsii-diff jsii-docgen jsii-pacmak npm-check-updates projen standard-version ts-jest typescript aws-cdk-lib constructs" 300 | }, 301 | { 302 | "exec": "npx projen" 303 | }, 304 | { 305 | "spawn": "post-upgrade" 306 | } 307 | ] 308 | }, 309 | "watch": { 310 | "name": "watch", 311 | "description": "Watch & compile in the background", 312 | "steps": [ 313 | { 314 | "exec": "jsii -w --silence-warnings=reserved-word" 315 | } 316 | ] 317 | } 318 | }, 319 | "env": { 320 | "PATH": "$(npx -c \"node --print process.env.PATH\")" 321 | }, 322 | "//": "~~ Generated by projen. To modify, edit .projenrc.js and run \"npx projen\"." 323 | } 324 | -------------------------------------------------------------------------------- /.projenrc.js: -------------------------------------------------------------------------------- 1 | const { awscdk, DevEnvironmentDockerImage, Gitpod } = require('projen'); 2 | 3 | const AUTOMATION_TOKEN = 'PROJEN_GITHUB_TOKEN'; 4 | 5 | const project = new awscdk.AwsCdkConstructLibrary({ 6 | author: 'Pahud Hsieh', 7 | authorAddress: 'pahudnet@gmail.com', 8 | description: 'CDK construct library that allows you to create KeyCloak service on AWS in TypeScript or Python', 9 | cdkVersion: '2.100.0', 10 | jsiiFqn: 'projen.AwsCdkConstructLibrary', 11 | name: 'cdk-keycloak', 12 | majorVersion: 2, 13 | repositoryUrl: 'https://github.com/aws-samples/cdk-keycloak.git', 14 | // devDeps: [ 15 | // '@types/prettier@<2.6.0', 16 | // ], 17 | // jestOptions: { jestVersion: '^28.0.0' }, 18 | depsUpgradeOptions: { 19 | ignoreProjen: false, 20 | workflowOptions: { 21 | labels: ['auto-approve', 'auto-merge'], 22 | secret: AUTOMATION_TOKEN, 23 | }, 24 | }, 25 | autoApproveOptions: { 26 | secret: 'GITHUB_TOKEN', 27 | allowedUsernames: ['pahud'], 28 | }, 29 | catalog: { 30 | announce: false, 31 | twitter: 'pahudnet', 32 | }, 33 | defaultReleaseBranch: 'main', 34 | publishToPypi: { 35 | distName: 'cdk-keycloak', 36 | module: 'cdk_keycloak', 37 | }, 38 | keywords: [ 39 | 'cdk', 40 | 'keycloak', 41 | 'aws', 42 | ], 43 | }); 44 | 45 | project.package.addField('resolutions', { 46 | 'pac-resolver': '^5.0.0', 47 | 'set-value': '^4.0.1', 48 | 'ansi-regex': '^5.0.1', 49 | 'got': '^12.1.0', 50 | '@types/babel__traverse': '7.18.2', 51 | }); 52 | 53 | 54 | const gitpodPrebuild = project.addTask('gitpod:prebuild', { 55 | description: 'Prebuild setup for Gitpod', 56 | }); 57 | // install and compile only, do not test or package. 58 | gitpodPrebuild.exec('yarn install --frozen-lockfile --check-files'); 59 | gitpodPrebuild.exec('npx projen compile'); 60 | 61 | let gitpod = new Gitpod(project, { 62 | dockerImage: DevEnvironmentDockerImage.fromImage('public.ecr.aws/pahudnet/gitpod-workspace:latest'), 63 | prebuilds: { 64 | addCheck: true, 65 | addBadge: true, 66 | addLabel: true, 67 | branches: true, 68 | pullRequests: true, 69 | pullRequestsFromForks: true, 70 | }, 71 | }); 72 | 73 | gitpod.addCustomTask({ 74 | init: 'yarn gitpod:prebuild', 75 | // always upgrade after init 76 | command: 'npx projen upgrade', 77 | }); 78 | 79 | gitpod.addVscodeExtensions( 80 | 'dbaeumer.vscode-eslint', 81 | 'ms-azuretools.vscode-docker', 82 | 'AmazonWebServices.aws-toolkit-vscode', 83 | ); 84 | 85 | const common_exclude = ['cdk.out', 'cdk.context.json', 'images', 'yarn-error.log', 'dependabot.yml']; 86 | project.npmignore.exclude(...common_exclude); 87 | project.gitignore.exclude(...common_exclude); 88 | 89 | 90 | project.synth(); 91 | -------------------------------------------------------------------------------- /API.md: -------------------------------------------------------------------------------- 1 | # API Reference 2 | 3 | **Classes** 4 | 5 | Name|Description 6 | ----|----------- 7 | [ContainerService](#cdk-keycloak-containerservice)|*No description* 8 | [Database](#cdk-keycloak-database)|Represents the database instance or database cluster. 9 | [KeyCloak](#cdk-keycloak-keycloak)|*No description* 10 | [KeycloakVersion](#cdk-keycloak-keycloakversion)|Keycloak version. 11 | 12 | 13 | **Structs** 14 | 15 | Name|Description 16 | ----|----------- 17 | [AutoScaleTask](#cdk-keycloak-autoscaletask)|The ECS task autoscaling definition. 18 | [ContainerServiceProps](#cdk-keycloak-containerserviceprops)|*No description* 19 | [DatabaseConfig](#cdk-keycloak-databaseconfig)|Database configuration. 20 | [DatabaseProps](#cdk-keycloak-databaseprops)|*No description* 21 | [KeyCloakProps](#cdk-keycloak-keycloakprops)|*No description* 22 | 23 | 24 | 25 | ## class ContainerService 26 | 27 | 28 | 29 | __Implements__: [IConstruct](#constructs-iconstruct), [IDependable](#constructs-idependable) 30 | __Extends__: [Construct](#constructs-construct) 31 | 32 | ### Initializer 33 | 34 | 35 | 36 | 37 | ```ts 38 | new ContainerService(scope: Construct, id: string, props: ContainerServiceProps) 39 | ``` 40 | 41 | * **scope** ([Construct](#constructs-construct)) *No description* 42 | * **id** (string) *No description* 43 | * **props** ([ContainerServiceProps](#cdk-keycloak-containerserviceprops)) *No description* 44 | * **certificate** ([aws_certificatemanager.ICertificate](#aws-cdk-lib-aws-certificatemanager-icertificate)) The ACM certificate. 45 | * **database** ([Database](#cdk-keycloak-database)) The RDS database for the service. 46 | * **keycloakSecret** ([aws_secretsmanager.ISecret](#aws-cdk-lib-aws-secretsmanager-isecret)) The secrets manager secret for the keycloak. 47 | * **keycloakVersion** ([KeycloakVersion](#cdk-keycloak-keycloakversion)) Keycloak version for the container image. 48 | * **vpc** ([aws_ec2.IVpc](#aws-cdk-lib-aws-ec2-ivpc)) The VPC for the service. 49 | * **autoScaleTask** ([AutoScaleTask](#cdk-keycloak-autoscaletask)) Autoscaling for the ECS Service. __*Default*__: no ecs service autoscaling 50 | * **bastion** (boolean) Whether to create the bastion host. __*Default*__: false 51 | * **circuitBreaker** (boolean) Whether to enable the ECS service deployment circuit breaker. __*Default*__: false 52 | * **containerImage** ([aws_ecs.ContainerImage](#aws-cdk-lib-aws-ecs-containerimage)) Overrides the default image. __*Default*__: quay.io/keycloak/keycloak:${KEYCLOAK_VERSION} 53 | * **env** (Map) The environment variables to pass to the keycloak container. __*Optional*__ 54 | * **hostname** (string) The hostname to use for the keycloak server. __*Optional*__ 55 | * **internetFacing** (boolean) Whether to put the put the load balancer in the public or private subnets. __*Default*__: true 56 | * **nodeCount** (number) Number of keycloak node in the cluster. __*Default*__: 1 57 | * **privateSubnets** ([aws_ec2.SubnetSelection](#aws-cdk-lib-aws-ec2-subnetselection)) VPC subnets for keycloak service. __*Optional*__ 58 | * **publicSubnets** ([aws_ec2.SubnetSelection](#aws-cdk-lib-aws-ec2-subnetselection)) VPC public subnets for ALB. __*Optional*__ 59 | * **stickinessCookieDuration** ([Duration](#aws-cdk-lib-duration)) The sticky session duration for the keycloak workload with ALB. __*Default*__: one day 60 | * **taskCpu** (number) The number of cpu units used by the keycloak task. __*Default*__: 4096 61 | * **taskMemory** (number) The amount (in MiB) of memory used by the keycloak task. __*Default*__: 8192 62 | 63 | 64 | 65 | ### Properties 66 | 67 | 68 | Name | Type | Description 69 | -----|------|------------- 70 | **applicationLoadBalancer** | [aws_elasticloadbalancingv2.ApplicationLoadBalancer](#aws-cdk-lib-aws-elasticloadbalancingv2-applicationloadbalancer) | 71 | **service** | [aws_ecs.FargateService](#aws-cdk-lib-aws-ecs-fargateservice) | 72 | 73 | 74 | 75 | ## class Database 76 | 77 | Represents the database instance or database cluster. 78 | 79 | __Implements__: [IConstruct](#constructs-iconstruct), [IDependable](#constructs-idependable) 80 | __Extends__: [Construct](#constructs-construct) 81 | 82 | ### Initializer 83 | 84 | 85 | 86 | 87 | ```ts 88 | new Database(scope: Construct, id: string, props: DatabaseProps) 89 | ``` 90 | 91 | * **scope** ([Construct](#constructs-construct)) *No description* 92 | * **id** (string) *No description* 93 | * **props** ([DatabaseProps](#cdk-keycloak-databaseprops)) *No description* 94 | * **vpc** ([aws_ec2.IVpc](#aws-cdk-lib-aws-ec2-ivpc)) The VPC for the database. 95 | * **auroraServerless** (boolean) enable aurora serverless. __*Default*__: false 96 | * **auroraServerlessV2** (boolean) enable aurora serverless v2. __*Default*__: false 97 | * **backupRetention** ([Duration](#aws-cdk-lib-duration)) database backup retension. __*Default*__: 7 days 98 | * **clusterEngine** ([aws_rds.IClusterEngine](#aws-cdk-lib-aws-rds-iclusterengine)) The database cluster engine. __*Default*__: rds.AuroraMysqlEngineVersion.VER_3_04_0 99 | * **databaseSubnets** ([aws_ec2.SubnetSelection](#aws-cdk-lib-aws-ec2-subnetselection)) VPC subnets for database. __*Optional*__ 100 | * **instanceEngine** ([aws_rds.IInstanceEngine](#aws-cdk-lib-aws-rds-iinstanceengine)) The database instance engine. __*Default*__: MySQL 8.0.34 101 | * **instanceType** ([aws_ec2.InstanceType](#aws-cdk-lib-aws-ec2-instancetype)) The database instance type. __*Default*__: r5.large 102 | * **maxCapacity** (number) The maximum number of Aurora Serverless V2 capacity units. __*Default*__: 10 103 | * **minCapacity** (number) The minimum number of Aurora Serverless V2 capacity units. __*Default*__: 0.5 104 | * **removalPolicy** ([RemovalPolicy](#aws-cdk-lib-removalpolicy)) Controls what happens to the database if it stops being managed by CloudFormation. __*Default*__: RemovalPolicy.RETAIN 105 | * **singleDbInstance** (boolean) Whether to use single RDS instance rather than RDS cluster. __*Default*__: false 106 | 107 | 108 | 109 | ### Properties 110 | 111 | 112 | Name | Type | Description 113 | -----|------|------------- 114 | **clusterEndpointHostname** | string | 115 | **clusterIdentifier** | string | 116 | **connections** | [aws_ec2.Connections](#aws-cdk-lib-aws-ec2-connections) | 117 | **secret** | [aws_secretsmanager.ISecret](#aws-cdk-lib-aws-secretsmanager-isecret) | 118 | **vpc** | [aws_ec2.IVpc](#aws-cdk-lib-aws-ec2-ivpc) | 119 | 120 | 121 | 122 | ## class KeyCloak 123 | 124 | 125 | 126 | __Implements__: [IConstruct](#constructs-iconstruct), [IDependable](#constructs-idependable) 127 | __Extends__: [Construct](#constructs-construct) 128 | 129 | ### Initializer 130 | 131 | 132 | 133 | 134 | ```ts 135 | new KeyCloak(scope: Construct, id: string, props: KeyCloakProps) 136 | ``` 137 | 138 | * **scope** ([Construct](#constructs-construct)) *No description* 139 | * **id** (string) *No description* 140 | * **props** ([KeyCloakProps](#cdk-keycloak-keycloakprops)) *No description* 141 | * **certificateArn** (string) ACM certificate ARN to import. 142 | * **keycloakVersion** ([KeycloakVersion](#cdk-keycloak-keycloakversion)) The Keycloak version for the cluster. 143 | * **auroraServerless** (boolean) Whether to use aurora serverless. __*Default*__: false 144 | * **auroraServerlessV2** (boolean) Whether to use aurora serverless v2. __*Default*__: false 145 | * **autoScaleTask** ([AutoScaleTask](#cdk-keycloak-autoscaletask)) Autoscaling for the ECS Service. __*Default*__: no ecs service autoscaling 146 | * **backupRetention** ([Duration](#aws-cdk-lib-duration)) database backup retension. __*Default*__: 7 days 147 | * **bastion** (boolean) Create a bastion host for debugging or trouble-shooting. __*Default*__: false 148 | * **clusterEngine** ([aws_rds.IClusterEngine](#aws-cdk-lib-aws-rds-iclusterengine)) The database cluster engine. __*Default*__: rds.AuroraMysqlEngineVersion.VER_3_04_0 149 | * **containerImage** ([aws_ecs.ContainerImage](#aws-cdk-lib-aws-ecs-containerimage)) Overrides the default image. __*Default*__: quay.io/keycloak/keycloak:${KEYCLOAK_VERSION} 150 | * **databaseInstanceType** ([aws_ec2.InstanceType](#aws-cdk-lib-aws-ec2-instancetype)) Database instance type. __*Default*__: r5.large 151 | * **databaseMaxCapacity** (number) The maximum number of Aurora Serverless V2 capacity units. __*Default*__: 10 152 | * **databaseMinCapacity** (number) The minimum number of Aurora Serverless V2 capacity units. __*Default*__: 0.5 153 | * **databaseRemovalPolicy** ([RemovalPolicy](#aws-cdk-lib-removalpolicy)) Controls what happens to the database if it stops being managed by CloudFormation. __*Default*__: RemovalPolicy.RETAIN 154 | * **databaseSubnets** ([aws_ec2.SubnetSelection](#aws-cdk-lib-aws-ec2-subnetselection)) VPC subnets for database. __*Default*__: VPC isolated subnets 155 | * **env** (Map) The environment variables to pass to the keycloak container. __*Optional*__ 156 | * **hostname** (string) The hostname to use for the keycloak server. __*Optional*__ 157 | * **instanceEngine** ([aws_rds.IInstanceEngine](#aws-cdk-lib-aws-rds-iinstanceengine)) The database instance engine. __*Default*__: MySQL 8.0.34 158 | * **internetFacing** (boolean) Whether to put the load balancer in the public or private subnets. __*Default*__: true 159 | * **nodeCount** (number) Number of keycloak node in the cluster. __*Default*__: 2 160 | * **privateSubnets** ([aws_ec2.SubnetSelection](#aws-cdk-lib-aws-ec2-subnetselection)) VPC private subnets for keycloak service. __*Default*__: VPC private subnets 161 | * **publicSubnets** ([aws_ec2.SubnetSelection](#aws-cdk-lib-aws-ec2-subnetselection)) VPC public subnets for ALB. __*Default*__: VPC public subnets 162 | * **singleDbInstance** (boolean) Whether to use single RDS instance rather than RDS cluster. __*Default*__: false 163 | * **stickinessCookieDuration** ([Duration](#aws-cdk-lib-duration)) The sticky session duration for the keycloak workload with ALB. __*Default*__: one day 164 | * **taskCpu** (number) The number of cpu units used by the keycloak task. __*Default*__: 4096 165 | * **taskMemory** (number) The amount (in MiB) of memory used by the keycloak task. __*Default*__: 8192 166 | * **vpc** ([aws_ec2.IVpc](#aws-cdk-lib-aws-ec2-ivpc)) VPC for the workload. __*Optional*__ 167 | 168 | 169 | 170 | ### Properties 171 | 172 | 173 | Name | Type | Description 174 | -----|------|------------- 175 | **applicationLoadBalancer** | [aws_elasticloadbalancingv2.ApplicationLoadBalancer](#aws-cdk-lib-aws-elasticloadbalancingv2-applicationloadbalancer) | 176 | **keycloakSecret** | [aws_secretsmanager.ISecret](#aws-cdk-lib-aws-secretsmanager-isecret) | 177 | **vpc** | [aws_ec2.IVpc](#aws-cdk-lib-aws-ec2-ivpc) | 178 | **db**? | [Database](#cdk-keycloak-database) | __*Optional*__ 179 | 180 | ### Methods 181 | 182 | 183 | #### addDatabase(props) 184 | 185 | 186 | 187 | ```ts 188 | addDatabase(props: DatabaseProps): Database 189 | ``` 190 | 191 | * **props** ([DatabaseProps](#cdk-keycloak-databaseprops)) *No description* 192 | * **vpc** ([aws_ec2.IVpc](#aws-cdk-lib-aws-ec2-ivpc)) The VPC for the database. 193 | * **auroraServerless** (boolean) enable aurora serverless. __*Default*__: false 194 | * **auroraServerlessV2** (boolean) enable aurora serverless v2. __*Default*__: false 195 | * **backupRetention** ([Duration](#aws-cdk-lib-duration)) database backup retension. __*Default*__: 7 days 196 | * **clusterEngine** ([aws_rds.IClusterEngine](#aws-cdk-lib-aws-rds-iclusterengine)) The database cluster engine. __*Default*__: rds.AuroraMysqlEngineVersion.VER_3_04_0 197 | * **databaseSubnets** ([aws_ec2.SubnetSelection](#aws-cdk-lib-aws-ec2-subnetselection)) VPC subnets for database. __*Optional*__ 198 | * **instanceEngine** ([aws_rds.IInstanceEngine](#aws-cdk-lib-aws-rds-iinstanceengine)) The database instance engine. __*Default*__: MySQL 8.0.34 199 | * **instanceType** ([aws_ec2.InstanceType](#aws-cdk-lib-aws-ec2-instancetype)) The database instance type. __*Default*__: r5.large 200 | * **maxCapacity** (number) The maximum number of Aurora Serverless V2 capacity units. __*Default*__: 10 201 | * **minCapacity** (number) The minimum number of Aurora Serverless V2 capacity units. __*Default*__: 0.5 202 | * **removalPolicy** ([RemovalPolicy](#aws-cdk-lib-removalpolicy)) Controls what happens to the database if it stops being managed by CloudFormation. __*Default*__: RemovalPolicy.RETAIN 203 | * **singleDbInstance** (boolean) Whether to use single RDS instance rather than RDS cluster. __*Default*__: false 204 | 205 | __Returns__: 206 | * [Database](#cdk-keycloak-database) 207 | 208 | #### addKeyCloakContainerService(props) 209 | 210 | 211 | 212 | ```ts 213 | addKeyCloakContainerService(props: ContainerServiceProps): ContainerService 214 | ``` 215 | 216 | * **props** ([ContainerServiceProps](#cdk-keycloak-containerserviceprops)) *No description* 217 | * **certificate** ([aws_certificatemanager.ICertificate](#aws-cdk-lib-aws-certificatemanager-icertificate)) The ACM certificate. 218 | * **database** ([Database](#cdk-keycloak-database)) The RDS database for the service. 219 | * **keycloakSecret** ([aws_secretsmanager.ISecret](#aws-cdk-lib-aws-secretsmanager-isecret)) The secrets manager secret for the keycloak. 220 | * **keycloakVersion** ([KeycloakVersion](#cdk-keycloak-keycloakversion)) Keycloak version for the container image. 221 | * **vpc** ([aws_ec2.IVpc](#aws-cdk-lib-aws-ec2-ivpc)) The VPC for the service. 222 | * **autoScaleTask** ([AutoScaleTask](#cdk-keycloak-autoscaletask)) Autoscaling for the ECS Service. __*Default*__: no ecs service autoscaling 223 | * **bastion** (boolean) Whether to create the bastion host. __*Default*__: false 224 | * **circuitBreaker** (boolean) Whether to enable the ECS service deployment circuit breaker. __*Default*__: false 225 | * **containerImage** ([aws_ecs.ContainerImage](#aws-cdk-lib-aws-ecs-containerimage)) Overrides the default image. __*Default*__: quay.io/keycloak/keycloak:${KEYCLOAK_VERSION} 226 | * **env** (Map) The environment variables to pass to the keycloak container. __*Optional*__ 227 | * **hostname** (string) The hostname to use for the keycloak server. __*Optional*__ 228 | * **internetFacing** (boolean) Whether to put the put the load balancer in the public or private subnets. __*Default*__: true 229 | * **nodeCount** (number) Number of keycloak node in the cluster. __*Default*__: 1 230 | * **privateSubnets** ([aws_ec2.SubnetSelection](#aws-cdk-lib-aws-ec2-subnetselection)) VPC subnets for keycloak service. __*Optional*__ 231 | * **publicSubnets** ([aws_ec2.SubnetSelection](#aws-cdk-lib-aws-ec2-subnetselection)) VPC public subnets for ALB. __*Optional*__ 232 | * **stickinessCookieDuration** ([Duration](#aws-cdk-lib-duration)) The sticky session duration for the keycloak workload with ALB. __*Default*__: one day 233 | * **taskCpu** (number) The number of cpu units used by the keycloak task. __*Default*__: 4096 234 | * **taskMemory** (number) The amount (in MiB) of memory used by the keycloak task. __*Default*__: 8192 235 | 236 | __Returns__: 237 | * [ContainerService](#cdk-keycloak-containerservice) 238 | 239 | 240 | 241 | ## class KeycloakVersion 242 | 243 | Keycloak version. 244 | 245 | 246 | 247 | ### Properties 248 | 249 | 250 | Name | Type | Description 251 | -----|------|------------- 252 | **version** | string | cluster version number. 253 | *static* **V12_0_4** | [KeycloakVersion](#cdk-keycloak-keycloakversion) | Keycloak version 12.0.4. 254 | *static* **V15_0_0** | [KeycloakVersion](#cdk-keycloak-keycloakversion) | Keycloak version 15.0.0. 255 | *static* **V15_0_1** | [KeycloakVersion](#cdk-keycloak-keycloakversion) | Keycloak version 15.0.1. 256 | *static* **V15_0_2** | [KeycloakVersion](#cdk-keycloak-keycloakversion) | Keycloak version 15.0.2. 257 | *static* **V16_1_1** | [KeycloakVersion](#cdk-keycloak-keycloakversion) | Keycloak version 16.1.1. 258 | *static* **V17_0_1** | [KeycloakVersion](#cdk-keycloak-keycloakversion) | Keycloak version 17.0.1. 259 | *static* **V18_0_2** | [KeycloakVersion](#cdk-keycloak-keycloakversion) | Keycloak version 18.0.2. 260 | *static* **V19_0_3** | [KeycloakVersion](#cdk-keycloak-keycloakversion) | Keycloak version 19.0.3. 261 | *static* **V20_0_5** | [KeycloakVersion](#cdk-keycloak-keycloakversion) | Keycloak version 20.0.5. 262 | *static* **V21_0_0** | [KeycloakVersion](#cdk-keycloak-keycloakversion) | Keycloak version 21.0.0. 263 | *static* **V21_0_1** | [KeycloakVersion](#cdk-keycloak-keycloakversion) | Keycloak version 21.0.1. 264 | *static* **V22_0_4** | [KeycloakVersion](#cdk-keycloak-keycloakversion) | Keycloak version 22.0.4. 265 | 266 | ### Methods 267 | 268 | 269 | #### *static* of(version) 270 | 271 | Custom cluster version. 272 | 273 | ```ts 274 | static of(version: string): KeycloakVersion 275 | ``` 276 | 277 | * **version** (string) custom version number. 278 | 279 | __Returns__: 280 | * [KeycloakVersion](#cdk-keycloak-keycloakversion) 281 | 282 | 283 | 284 | ## struct AutoScaleTask 285 | 286 | 287 | The ECS task autoscaling definition. 288 | 289 | 290 | 291 | Name | Type | Description 292 | -----|------|------------- 293 | **max**? | number | The maximal count of the task number.
__*Default*__: min + 5 294 | **min**? | number | The minimal count of the task number.
__*Default*__: nodeCount 295 | **targetCpuUtilization**? | number | The target cpu utilization for the service autoscaling.
__*Default*__: 75 296 | 297 | 298 | 299 | ## struct ContainerServiceProps 300 | 301 | 302 | 303 | 304 | 305 | 306 | Name | Type | Description 307 | -----|------|------------- 308 | **certificate** | [aws_certificatemanager.ICertificate](#aws-cdk-lib-aws-certificatemanager-icertificate) | The ACM certificate. 309 | **database** | [Database](#cdk-keycloak-database) | The RDS database for the service. 310 | **keycloakSecret** | [aws_secretsmanager.ISecret](#aws-cdk-lib-aws-secretsmanager-isecret) | The secrets manager secret for the keycloak. 311 | **keycloakVersion** | [KeycloakVersion](#cdk-keycloak-keycloakversion) | Keycloak version for the container image. 312 | **vpc** | [aws_ec2.IVpc](#aws-cdk-lib-aws-ec2-ivpc) | The VPC for the service. 313 | **autoScaleTask**? | [AutoScaleTask](#cdk-keycloak-autoscaletask) | Autoscaling for the ECS Service.
__*Default*__: no ecs service autoscaling 314 | **bastion**? | boolean | Whether to create the bastion host.
__*Default*__: false 315 | **circuitBreaker**? | boolean | Whether to enable the ECS service deployment circuit breaker.
__*Default*__: false 316 | **containerImage**? | [aws_ecs.ContainerImage](#aws-cdk-lib-aws-ecs-containerimage) | Overrides the default image.
__*Default*__: quay.io/keycloak/keycloak:${KEYCLOAK_VERSION} 317 | **env**? | Map | The environment variables to pass to the keycloak container.
__*Optional*__ 318 | **hostname**? | string | The hostname to use for the keycloak server.
__*Optional*__ 319 | **internetFacing**? | boolean | Whether to put the put the load balancer in the public or private subnets.
__*Default*__: true 320 | **nodeCount**? | number | Number of keycloak node in the cluster.
__*Default*__: 1 321 | **privateSubnets**? | [aws_ec2.SubnetSelection](#aws-cdk-lib-aws-ec2-subnetselection) | VPC subnets for keycloak service.
__*Optional*__ 322 | **publicSubnets**? | [aws_ec2.SubnetSelection](#aws-cdk-lib-aws-ec2-subnetselection) | VPC public subnets for ALB.
__*Optional*__ 323 | **stickinessCookieDuration**? | [Duration](#aws-cdk-lib-duration) | The sticky session duration for the keycloak workload with ALB.
__*Default*__: one day 324 | **taskCpu**? | number | The number of cpu units used by the keycloak task.
__*Default*__: 4096 325 | **taskMemory**? | number | The amount (in MiB) of memory used by the keycloak task.
__*Default*__: 8192 326 | 327 | 328 | 329 | ## struct DatabaseConfig 330 | 331 | 332 | Database configuration. 333 | 334 | 335 | 336 | Name | Type | Description 337 | -----|------|------------- 338 | **connections** | [aws_ec2.Connections](#aws-cdk-lib-aws-ec2-connections) | The database connnections. 339 | **endpoint** | string | The endpoint address for the database. 340 | **identifier** | string | The databasae identifier. 341 | **secret** | [aws_secretsmanager.ISecret](#aws-cdk-lib-aws-secretsmanager-isecret) | The database secret. 342 | 343 | 344 | 345 | ## struct DatabaseProps 346 | 347 | 348 | 349 | 350 | 351 | 352 | Name | Type | Description 353 | -----|------|------------- 354 | **vpc** | [aws_ec2.IVpc](#aws-cdk-lib-aws-ec2-ivpc) | The VPC for the database. 355 | **auroraServerless**? | boolean | enable aurora serverless.
__*Default*__: false 356 | **auroraServerlessV2**? | boolean | enable aurora serverless v2.
__*Default*__: false 357 | **backupRetention**? | [Duration](#aws-cdk-lib-duration) | database backup retension.
__*Default*__: 7 days 358 | **clusterEngine**? | [aws_rds.IClusterEngine](#aws-cdk-lib-aws-rds-iclusterengine) | The database cluster engine.
__*Default*__: rds.AuroraMysqlEngineVersion.VER_3_04_0 359 | **databaseSubnets**? | [aws_ec2.SubnetSelection](#aws-cdk-lib-aws-ec2-subnetselection) | VPC subnets for database.
__*Optional*__ 360 | **instanceEngine**? | [aws_rds.IInstanceEngine](#aws-cdk-lib-aws-rds-iinstanceengine) | The database instance engine.
__*Default*__: MySQL 8.0.34 361 | **instanceType**? | [aws_ec2.InstanceType](#aws-cdk-lib-aws-ec2-instancetype) | The database instance type.
__*Default*__: r5.large 362 | **maxCapacity**? | number | The maximum number of Aurora Serverless V2 capacity units.
__*Default*__: 10 363 | **minCapacity**? | number | The minimum number of Aurora Serverless V2 capacity units.
__*Default*__: 0.5 364 | **removalPolicy**? | [RemovalPolicy](#aws-cdk-lib-removalpolicy) | Controls what happens to the database if it stops being managed by CloudFormation.
__*Default*__: RemovalPolicy.RETAIN 365 | **singleDbInstance**? | boolean | Whether to use single RDS instance rather than RDS cluster.
__*Default*__: false 366 | 367 | 368 | 369 | ## struct KeyCloakProps 370 | 371 | 372 | 373 | 374 | 375 | 376 | Name | Type | Description 377 | -----|------|------------- 378 | **certificateArn** | string | ACM certificate ARN to import. 379 | **keycloakVersion** | [KeycloakVersion](#cdk-keycloak-keycloakversion) | The Keycloak version for the cluster. 380 | **auroraServerless**? | boolean | Whether to use aurora serverless.
__*Default*__: false 381 | **auroraServerlessV2**? | boolean | Whether to use aurora serverless v2.
__*Default*__: false 382 | **autoScaleTask**? | [AutoScaleTask](#cdk-keycloak-autoscaletask) | Autoscaling for the ECS Service.
__*Default*__: no ecs service autoscaling 383 | **backupRetention**? | [Duration](#aws-cdk-lib-duration) | database backup retension.
__*Default*__: 7 days 384 | **bastion**? | boolean | Create a bastion host for debugging or trouble-shooting.
__*Default*__: false 385 | **clusterEngine**? | [aws_rds.IClusterEngine](#aws-cdk-lib-aws-rds-iclusterengine) | The database cluster engine.
__*Default*__: rds.AuroraMysqlEngineVersion.VER_3_04_0 386 | **containerImage**? | [aws_ecs.ContainerImage](#aws-cdk-lib-aws-ecs-containerimage) | Overrides the default image.
__*Default*__: quay.io/keycloak/keycloak:${KEYCLOAK_VERSION} 387 | **databaseInstanceType**? | [aws_ec2.InstanceType](#aws-cdk-lib-aws-ec2-instancetype) | Database instance type.
__*Default*__: r5.large 388 | **databaseMaxCapacity**? | number | The maximum number of Aurora Serverless V2 capacity units.
__*Default*__: 10 389 | **databaseMinCapacity**? | number | The minimum number of Aurora Serverless V2 capacity units.
__*Default*__: 0.5 390 | **databaseRemovalPolicy**? | [RemovalPolicy](#aws-cdk-lib-removalpolicy) | Controls what happens to the database if it stops being managed by CloudFormation.
__*Default*__: RemovalPolicy.RETAIN 391 | **databaseSubnets**? | [aws_ec2.SubnetSelection](#aws-cdk-lib-aws-ec2-subnetselection) | VPC subnets for database.
__*Default*__: VPC isolated subnets 392 | **env**? | Map | The environment variables to pass to the keycloak container.
__*Optional*__ 393 | **hostname**? | string | The hostname to use for the keycloak server.
__*Optional*__ 394 | **instanceEngine**? | [aws_rds.IInstanceEngine](#aws-cdk-lib-aws-rds-iinstanceengine) | The database instance engine.
__*Default*__: MySQL 8.0.34 395 | **internetFacing**? | boolean | Whether to put the load balancer in the public or private subnets.
__*Default*__: true 396 | **nodeCount**? | number | Number of keycloak node in the cluster.
__*Default*__: 2 397 | **privateSubnets**? | [aws_ec2.SubnetSelection](#aws-cdk-lib-aws-ec2-subnetselection) | VPC private subnets for keycloak service.
__*Default*__: VPC private subnets 398 | **publicSubnets**? | [aws_ec2.SubnetSelection](#aws-cdk-lib-aws-ec2-subnetselection) | VPC public subnets for ALB.
__*Default*__: VPC public subnets 399 | **singleDbInstance**? | boolean | Whether to use single RDS instance rather than RDS cluster.
__*Default*__: false 400 | **stickinessCookieDuration**? | [Duration](#aws-cdk-lib-duration) | The sticky session duration for the keycloak workload with ALB.
__*Default*__: one day 401 | **taskCpu**? | number | The number of cpu units used by the keycloak task.
__*Default*__: 4096 402 | **taskMemory**? | number | The amount (in MiB) of memory used by the keycloak task.
__*Default*__: 8192 403 | **vpc**? | [aws_ec2.IVpc](#aws-cdk-lib-aws-ec2-ivpc) | VPC for the workload.
__*Optional*__ 404 | 405 | 406 | 407 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 4 | opensource-codeofconduct@amazon.com with any additional questions or comments. 5 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional 4 | documentation, we greatly value feedback and contributions from our community. 5 | 6 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary 7 | information to effectively respond to your bug report or contribution. 8 | 9 | 10 | ## Reporting Bugs/Feature Requests 11 | 12 | We welcome you to use the GitHub issue tracker to report bugs or suggest features. 13 | 14 | When filing an issue, please check existing open, or recently closed, issues to make sure somebody else hasn't already 15 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: 16 | 17 | * A reproducible test case or series of steps 18 | * The version of our code being used 19 | * Any modifications you've made relevant to the bug 20 | * Anything unusual about your environment or deployment 21 | 22 | 23 | ## Contributing via Pull Requests 24 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: 25 | 26 | 1. You are working against the latest source on the *main* branch. 27 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. 28 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted. 29 | 30 | To send us a pull request, please: 31 | 32 | 1. Fork the repository. 33 | 2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. 34 | 3. Ensure local tests pass. 35 | 4. Commit to your fork using clear commit messages. 36 | 5. Send us a pull request, answering any default questions in the pull request interface. 37 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. 38 | 39 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and 40 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). 41 | 42 | 43 | ## Finding contributions to work on 44 | Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any 'help wanted' issues is a great place to start. 45 | 46 | 47 | ## Code of Conduct 48 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 49 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 50 | opensource-codeofconduct@amazon.com with any additional questions or comments. 51 | 52 | 53 | ## Security issue notifications 54 | If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. 55 | 56 | 57 | ## Licensing 58 | 59 | See the [LICENSE](LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. 60 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![NPM version](https://badge.fury.io/js/cdk-keycloak.svg)](https://badge.fury.io/js/cdk-keycloak) 2 | [![PyPI version](https://badge.fury.io/py/cdk-keycloak.svg)](https://badge.fury.io/py/cdk-keycloak) 3 | [![release](https://github.com/aws-samples/cdk-keycloak/actions/workflows/release.yml/badge.svg)](https://github.com/aws-samples/cdk-keycloak/actions/workflows/release.yml) 4 | 5 | # `cdk-keycloak` 6 | 7 | CDK construct library that allows you to create [KeyCloak](https://www.keycloak.org/) on AWS in TypeScript or Python 8 | 9 | > **Note** 10 | > 11 | > This project has been migrated to CDK v2. 12 | > 13 | > CDK v1 compatible version is deprecated now. 14 | 15 | # Sample 16 | 17 | For Keycloak 17+ versions, please specify hostname for the Keycloak server. 18 | 19 | ```ts 20 | import { KeyCloak } from 'cdk-keycloak'; 21 | 22 | const app = new cdk.App(); 23 | 24 | const env = { 25 | region: process.env.CDK_DEFAULT_REGION, 26 | account: process.env.CDK_DEFAULT_ACCOUNT, 27 | }; 28 | 29 | const stack = new cdk.Stack(app, 'keycloak-demo', { env }); 30 | new KeyCloak(stack, 'KeyCloak', { 31 | hostname: 'keycloak.example.com', 32 | certificateArn: 'arn:aws:acm:us-east-1:123456789012:certificate/293cf875-ca98-4c2e-a797-e1cf6df2553c', 33 | keycloakVersion: KeycloakVersion.V22_0_4, 34 | }); 35 | ``` 36 | 37 | # Keycloak version pinning 38 | 39 | Use `keycloakVersion` to specify the version. 40 | 41 | ```ts 42 | new KeyCloak(stack, 'KeyCloak', { 43 | hostname, 44 | certificateArn, 45 | keycloakVersion: KeycloakVersion.V22_0_4, 46 | }); 47 | ``` 48 | 49 | To specify any other verion not defined in the construct, use `KeycloakVersion.of('x.x.x')`. This allows you to specify any new version as soon as it's available. However, as new versions will not always be tested and validated with this construct library, make sure you fully backup and test before you use any new version in the production environment. 50 | 51 | 52 | # Aurora Serverless support 53 | 54 | The `KeyCloak` construct provisions the **Amaozn RDS cluster for MySQL** with **2** database instances under the hood, to opt in **Amazon Aurora Serverless**, use `auroraServerless` to opt in Amazon Aurora Serverless cluster. Please note only some regions are supported, check [Supported features in Amazon Aurora by AWS Region and Aurora DB engine](https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/Concepts.AuroraFeaturesRegionsDBEngines.grids.html) for availability. 55 | 56 | ```ts 57 | // Aurora Serverless v1 58 | new KeyCloak(stack, 'KeyCloak', { 59 | hostname, 60 | certificateArn, 61 | keycloakVersion, 62 | auroraServerless: true, 63 | }); 64 | 65 | // Aurora Serverless v2 66 | new KeyCloak(stack, 'KeyCloak', { 67 | hostname, 68 | certificateArn, 69 | keycloakVersion, 70 | auroraServerlessV2: true, 71 | }); 72 | ``` 73 | 74 | Behind the scene, a default RDS cluster for MySQL with 2 database instances will be created. 75 | 76 | # Opt-in for Single RDS instance 77 | 78 | To create single RDS instance for your testing or development environment, use `singleDbInstance` to turn on the 79 | single db instance deployment. 80 | 81 | Plesae note this is not recommended for production environment. 82 | 83 | ```ts 84 | new KeyCloak(stack, 'KeyCloak', { 85 | hostname, 86 | certificateArn, 87 | keycloakVersion, 88 | singleDbInstance: true, 89 | }); 90 | 91 | ``` 92 | 93 | # Service Auto Scaling 94 | 95 | Define `autoScaleTask` for the ecs service task autoscaling. For example: 96 | 97 | ```ts 98 | new KeyCloak(stack, 'KeyCloak', { 99 | hostname, 100 | certificateArn, 101 | keycloakVersion, 102 | auroraServerlessV2: true, 103 | nodeCount: 2, 104 | autoScaleTask: { 105 | min: 2, 106 | max: 10, 107 | targetCpuUtilization: 60, 108 | }, 109 | }); 110 | 111 | ``` 112 | 113 | 114 | # Customize fargate task settings 115 | 116 | Define `taskCpu` or `taskMemory` for overriding the defaults for the ecs service task. 117 | Could be useful for development environments. For example: 118 | 119 | ```ts 120 | new KeyCloak(stack, 'KeyCloak', { 121 | hostname, 122 | certificateArn, 123 | keycloakVersion, 124 | nodeCount: 1, 125 | taskCpu: 512, 126 | taskMemory: 2048, 127 | }); 128 | 129 | ``` 130 | 131 | # Deploy in existing Vpc Subnets 132 | 133 | You can deploy the workload in the existing Vpc and subnets. The `publicSubnets` are for the ALB, `privateSubnets` for the keycloak container tasks and `databaseSubnets` for the database. 134 | 135 | The best practice is to specify isolated subnets for `databaseSubnets`, however, in some cases might have no existing isolates subnets then the private subnets are also acceptable. 136 | 137 | Consider the sample below: 138 | 139 | ```ts 140 | new KeyCloak(stack, 'KeyCloak', { 141 | hostname: 'keycloak.example.com', 142 | keycloakVersion: KeycloakVersion.V22_0_4, 143 | certificateArn: 'arn:aws:acm:us-east-1:123456789012:certificate/293cf875-ca98-4c2e-a797-e1cf6df2553c', 144 | vpc: ec2.Vpc.fromLookup(stack, 'Vpc', { vpcId: 'vpc-0417e46d' }), 145 | publicSubnets: { 146 | subnets: [ 147 | ec2.Subnet.fromSubnetId(stack, 'pub-1a', 'subnet-5bbe7b32'), 148 | ec2.Subnet.fromSubnetId(stack, 'pub-1b', 'subnet-0428367c'), 149 | ec2.Subnet.fromSubnetId(stack, 'pub-1c', 'subnet-1586a75f'), 150 | ], 151 | }, 152 | privateSubnets: { 153 | subnets: [ 154 | ec2.Subnet.fromSubnetId(stack, 'priv-1a', 'subnet-0e9460dbcfc4cf6ee'), 155 | ec2.Subnet.fromSubnetId(stack, 'priv-1b', 'subnet-0562f666bdf5c29af'), 156 | ec2.Subnet.fromSubnetId(stack, 'priv-1c', 'subnet-00ab15c0022872f06'), 157 | ], 158 | }, 159 | databaseSubnets: { 160 | subnets: [ 161 | ec2.Subnet.fromSubnetId(stack, 'db-1a', 'subnet-0e9460dbcfc4cf6ee'), 162 | ec2.Subnet.fromSubnetId(stack, 'db-1b', 'subnet-0562f666bdf5c29af'), 163 | ec2.Subnet.fromSubnetId(stack, 'db-1c', 'subnet-00ab15c0022872f06'), 164 | ], 165 | }, 166 | }); 167 | ``` 168 | 169 | # AWS China Regions 170 | 171 | This library support AWS China regions `cn-north-1` and `cn-northwest-1` and will auto select local docker image mirror to accelerate the image pulling. You don't have to do anything. 172 | 173 | ## Security 174 | 175 | See [CONTRIBUTING](CONTRIBUTING.md#security-issue-notifications) for more information. 176 | 177 | ## License 178 | 179 | This project is licensed under the Apache-2.0 License. 180 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cdk-keycloak", 3 | "description": "CDK construct library that allows you to create KeyCloak service on AWS in TypeScript or Python", 4 | "repository": { 5 | "type": "git", 6 | "url": "https://github.com/aws-samples/cdk-keycloak.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 | "gitpod:prebuild": "npx projen gitpod:prebuild", 19 | "package": "npx projen package", 20 | "package-all": "npx projen package-all", 21 | "package:js": "npx projen package:js", 22 | "package:python": "npx projen package:python", 23 | "post-compile": "npx projen post-compile", 24 | "post-upgrade": "npx projen post-upgrade", 25 | "pre-compile": "npx projen pre-compile", 26 | "release": "npx projen release", 27 | "test": "npx projen test", 28 | "test:watch": "npx projen test:watch", 29 | "unbump": "npx projen unbump", 30 | "upgrade": "npx projen upgrade", 31 | "watch": "npx projen watch", 32 | "projen": "npx projen" 33 | }, 34 | "author": { 35 | "name": "Pahud Hsieh", 36 | "email": "pahudnet@gmail.com", 37 | "organization": false 38 | }, 39 | "devDependencies": { 40 | "@types/jest": "^27", 41 | "@types/node": "^16", 42 | "@typescript-eslint/eslint-plugin": "^6", 43 | "@typescript-eslint/parser": "^6", 44 | "aws-cdk-lib": "2.100.0", 45 | "constructs": "10.0.5", 46 | "eslint": "^8", 47 | "eslint-import-resolver-node": "^0.3.9", 48 | "eslint-import-resolver-typescript": "^2.7.1", 49 | "eslint-plugin-import": "^2.28.1", 50 | "jest": "^27", 51 | "jest-junit": "^15", 52 | "jsii": "1.x", 53 | "jsii-diff": "^1.90.0", 54 | "jsii-docgen": "^1.8.110", 55 | "jsii-pacmak": "^1.90.0", 56 | "jsii-rosetta": "1.x", 57 | "npm-check-updates": "^16", 58 | "projen": "^0.74.11", 59 | "standard-version": "^9", 60 | "ts-jest": "^27", 61 | "typescript": "^4.9.5" 62 | }, 63 | "peerDependencies": { 64 | "aws-cdk-lib": "^2.100.0", 65 | "constructs": "^10.0.5" 66 | }, 67 | "resolutions": { 68 | "pac-resolver": "^5.0.0", 69 | "set-value": "^4.0.1", 70 | "ansi-regex": "^5.0.1", 71 | "got": "^12.1.0", 72 | "@types/babel__traverse": "7.18.2" 73 | }, 74 | "keywords": [ 75 | "aws", 76 | "cdk", 77 | "keycloak" 78 | ], 79 | "main": "lib/index.js", 80 | "license": "Apache-2.0", 81 | "version": "0.0.0", 82 | "jest": { 83 | "testMatch": [ 84 | "/src/**/__tests__/**/*.ts?(x)", 85 | "/(test|src)/**/*(*.)@(spec|test).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 | "python": { 128 | "distName": "cdk-keycloak", 129 | "module": "cdk_keycloak" 130 | } 131 | }, 132 | "tsc": { 133 | "outDir": "lib", 134 | "rootDir": "src" 135 | } 136 | }, 137 | "awscdkio": { 138 | "twitter": "pahudnet", 139 | "announce": false 140 | }, 141 | "//": "~~ Generated by projen. To modify, edit .projenrc.js and run \"npx projen\"." 142 | } 143 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './keycloak'; 2 | -------------------------------------------------------------------------------- /src/integ.default.ts: -------------------------------------------------------------------------------- 1 | import { 2 | aws_ec2 as ec2, 3 | } from 'aws-cdk-lib'; 4 | import * as cdk from 'aws-cdk-lib'; 5 | 6 | import { KeyCloak, KeycloakVersion } from './index'; 7 | 8 | export class IntegTesting { 9 | readonly stack: cdk.Stack[]; 10 | constructor() { 11 | const app = new cdk.App(); 12 | 13 | const env = { 14 | region: process.env.CDK_DEFAULT_REGION || 'us-east-1', 15 | account: process.env.CDK_DEFAULT_ACCOUNT, 16 | }; 17 | 18 | const stack = new cdk.Stack(app, 'keycloak-demo', { env }); 19 | 20 | // create a default keycloak workload with minimal required props 21 | new KeyCloak(stack, 'KeyCloak', { 22 | certificateArn: stack.node.tryGetContext('ACM_CERT_ARN') || 'MOCK_ARN', 23 | keycloakVersion: KeycloakVersion.V22_0_4, 24 | hostname: 'hostname for keycloak server', 25 | auroraServerlessV2: true, 26 | nodeCount: 2, 27 | autoScaleTask: { 28 | min: 2, 29 | max: 10, 30 | targetCpuUtilization: 60, 31 | }, 32 | }); 33 | 34 | this.stack = [stack]; 35 | } 36 | } 37 | 38 | 39 | export class IntegTestingExistingVpcSubnets { 40 | readonly stack: cdk.Stack[]; 41 | constructor() { 42 | const app = new cdk.App(); 43 | 44 | const env = { 45 | region: process.env.CDK_DEFAULT_REGION, 46 | account: process.env.CDK_DEFAULT_ACCOUNT, 47 | }; 48 | 49 | const stack = new cdk.Stack(app, 'keycloak-demo', { env }); 50 | 51 | new KeyCloak(stack, 'KeyCloak', { 52 | certificateArn: stack.node.tryGetContext('ACM_CERT_ARN') || 'MOCK_ARN', 53 | keycloakVersion: KeycloakVersion.V15_0_2, 54 | vpc: ec2.Vpc.fromLookup(stack, 'Vpc', { vpcId: 'vpc-0417e46d' }), 55 | publicSubnets: { 56 | subnets: [ 57 | ec2.Subnet.fromSubnetId(stack, 'pub-1a', 'subnet-5bbe7b32'), 58 | ec2.Subnet.fromSubnetId(stack, 'pub-1b', 'subnet-0428367c'), 59 | ec2.Subnet.fromSubnetId(stack, 'pub-1c', 'subnet-1586a75f'), 60 | ], 61 | }, 62 | privateSubnets: { 63 | subnets: [ 64 | ec2.Subnet.fromSubnetId(stack, 'priv-1a', 'subnet-0e9460dbcfc4cf6ee'), 65 | ec2.Subnet.fromSubnetId(stack, 'priv-1b', 'subnet-0562f666bdf5c29af'), 66 | ec2.Subnet.fromSubnetId(stack, 'priv-1c', 'subnet-00ab15c0022872f06'), 67 | ], 68 | }, 69 | databaseSubnets: { 70 | subnets: [ 71 | ec2.Subnet.fromSubnetId(stack, 'db-1a', 'subnet-0e9460dbcfc4cf6ee'), 72 | ec2.Subnet.fromSubnetId(stack, 'db-1b', 'subnet-0562f666bdf5c29af'), 73 | ec2.Subnet.fromSubnetId(stack, 'db-1c', 'subnet-00ab15c0022872f06'), 74 | ], 75 | }, 76 | }); 77 | this.stack = [stack]; 78 | } 79 | } 80 | 81 | new IntegTesting(); 82 | 83 | // new IntegTestingExistingVpcSubnets(); 84 | 85 | -------------------------------------------------------------------------------- /src/keycloak.ts: -------------------------------------------------------------------------------- 1 | import * as cdk from 'aws-cdk-lib'; 2 | import { 3 | aws_certificatemanager as certmgr, 4 | aws_ec2 as ec2, aws_ecs as ecs, aws_elasticloadbalancingv2 as elbv2, 5 | aws_iam as iam, 6 | aws_logs as logs, 7 | aws_rds as rds, 8 | aws_secretsmanager as secretsmanager, 9 | } from 'aws-cdk-lib'; 10 | import { Construct } from 'constructs'; 11 | 12 | // regional availibility for aurora serverless 13 | // see https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/Concepts.AuroraFeaturesRegionsDBEngines.grids.html 14 | const AURORA_SERVERLESS_SUPPORTED_REGIONS = [ 15 | 'us-east-1', 16 | 'us-east-2', 17 | 'us-west-1', 18 | 'us-west-2', 19 | 'ap-south-1', 20 | 'ap-northeast-1', 21 | 'ap-northeast-2', 22 | 'ap-southeast-1', 23 | 'ap-southeast-2', 24 | 'ca-central-1', 25 | 'eu-central-1', 26 | 'eu-west-1', 27 | 'eu-west-2', 28 | 'eu-west-3', 29 | 'cn-northwest-1', 30 | ]; 31 | 32 | /** 33 | * Keycloak version 34 | */ 35 | export class KeycloakVersion { 36 | /** 37 | * Keycloak version 12.0.4 38 | */ 39 | public static readonly V12_0_4 = KeycloakVersion.of('12.0.4'); 40 | 41 | /** 42 | * Keycloak version 15.0.0 43 | */ 44 | public static readonly V15_0_0 = KeycloakVersion.of('15.0.0'); 45 | 46 | /** 47 | * Keycloak version 15.0.1 48 | */ 49 | public static readonly V15_0_1 = KeycloakVersion.of('15.0.1'); 50 | 51 | /** 52 | * Keycloak version 15.0.2 53 | */ 54 | public static readonly V15_0_2 = KeycloakVersion.of('15.0.2'); 55 | 56 | /** 57 | * Keycloak version 16.1.1 58 | */ 59 | public static readonly V16_1_1 = KeycloakVersion.of('16.1.1'); 60 | 61 | /** 62 | * Keycloak version 17.0.1 63 | */ 64 | public static readonly V17_0_1 = KeycloakVersion.of('17.0.1'); 65 | 66 | /** 67 | * Keycloak version 18.0.2 68 | */ 69 | public static readonly V18_0_2 = KeycloakVersion.of('18.0.2'); 70 | 71 | /** 72 | * Keycloak version 19.0.3 73 | */ 74 | public static readonly V19_0_3 = KeycloakVersion.of('19.0.3'); 75 | 76 | /** 77 | * Keycloak version 20.0.5 78 | */ 79 | public static readonly V20_0_5 = KeycloakVersion.of('20.0.5'); 80 | 81 | /** 82 | * Keycloak version 21.0.0 83 | */ 84 | public static readonly V21_0_0 = KeycloakVersion.of('21.0.0'); 85 | 86 | /** 87 | * Keycloak version 21.0.1 88 | */ 89 | public static readonly V21_0_1 = KeycloakVersion.of('21.0.1'); 90 | 91 | /** 92 | * Keycloak version 22.0.4 93 | */ 94 | public static readonly V22_0_4 = KeycloakVersion.of('22.0.4'); 95 | 96 | /** 97 | * Custom cluster version 98 | * @param version custom version number 99 | */ 100 | public static of(version: string) { return new KeycloakVersion(version); } 101 | /** 102 | * 103 | * @param version cluster version number 104 | */ 105 | private constructor(public readonly version: string) { } 106 | } 107 | 108 | interface dockerImageMap { 109 | 'aws': string; 110 | 'aws-cn': string; 111 | } 112 | 113 | const KEYCLOAK_DOCKER_IMAGE_URI_MAP: dockerImageMap = { 114 | 'aws': 'quay.io/keycloak/keycloak:', 115 | 'aws-cn': '048912060910.dkr.ecr.cn-northwest-1.amazonaws.com.cn/quay/keycloak/keycloak:', 116 | }; 117 | 118 | /** 119 | * The ECS task autoscaling definition 120 | */ 121 | export interface AutoScaleTask { 122 | /** 123 | * The minimal count of the task number 124 | * 125 | * @default - nodeCount 126 | */ 127 | readonly min?: number; 128 | /** 129 | * The maximal count of the task number 130 | * 131 | * @default - min + 5 132 | */ 133 | readonly max?: number; 134 | /** 135 | * The target cpu utilization for the service autoscaling 136 | * 137 | * @default 75 138 | */ 139 | readonly targetCpuUtilization?: number; 140 | } 141 | 142 | export interface KeyCloakProps { 143 | /** 144 | * The Keycloak version for the cluster. 145 | */ 146 | readonly keycloakVersion: KeycloakVersion; 147 | /** 148 | * The environment variables to pass to the keycloak container 149 | */ 150 | readonly env?: { [key: string]: string }; 151 | /** 152 | * VPC for the workload 153 | */ 154 | readonly vpc?: ec2.IVpc; 155 | /** 156 | * ACM certificate ARN to import 157 | */ 158 | readonly certificateArn: string; 159 | /** 160 | * Create a bastion host for debugging or trouble-shooting 161 | * 162 | * @default false 163 | */ 164 | readonly bastion?: boolean; 165 | /** 166 | * Number of keycloak node in the cluster 167 | * 168 | * @default 2 169 | */ 170 | readonly nodeCount?: number; 171 | /** 172 | * VPC public subnets for ALB 173 | * 174 | * @default - VPC public subnets 175 | */ 176 | readonly publicSubnets?: ec2.SubnetSelection; 177 | /** 178 | * VPC private subnets for keycloak service 179 | * 180 | * @default - VPC private subnets 181 | */ 182 | readonly privateSubnets?: ec2.SubnetSelection; 183 | /** 184 | * VPC subnets for database 185 | * 186 | * @default - VPC isolated subnets 187 | */ 188 | readonly databaseSubnets?: ec2.SubnetSelection; 189 | /** 190 | * Database instance type 191 | * 192 | * @default r5.large 193 | */ 194 | readonly databaseInstanceType?: ec2.InstanceType; 195 | /** 196 | * The database instance engine 197 | * 198 | * @default - MySQL 8.0.34 199 | */ 200 | readonly instanceEngine?: rds.IInstanceEngine; 201 | /** 202 | * The database cluster engine 203 | * 204 | * @default rds.AuroraMysqlEngineVersion.VER_3_04_0 205 | */ 206 | readonly clusterEngine?: rds.IClusterEngine; 207 | /** 208 | * Whether to use aurora serverless. When enabled, the `databaseInstanceType` and 209 | * `engine` will be ignored. The `rds.DatabaseClusterEngine.AURORA_MYSQL` will be used as 210 | * the default cluster engine instead. 211 | * 212 | * @default false 213 | */ 214 | readonly auroraServerless?: boolean; 215 | /** 216 | * Whether to use aurora serverless v2. When enabled, the `databaseInstanceType` will be ignored. 217 | * 218 | * @default false 219 | */ 220 | readonly auroraServerlessV2?: boolean; 221 | /** 222 | * Whether to use single RDS instance rather than RDS cluster. Not recommended for production. 223 | * 224 | * @default false 225 | */ 226 | readonly singleDbInstance?: boolean; 227 | /** 228 | * database backup retension 229 | * 230 | * @default - 7 days 231 | */ 232 | readonly backupRetention?: cdk.Duration; 233 | /** 234 | * The sticky session duration for the keycloak workload with ALB. 235 | * 236 | * @default - one day 237 | */ 238 | readonly stickinessCookieDuration?: cdk.Duration; 239 | /** 240 | * Autoscaling for the ECS Service 241 | * 242 | * @default - no ecs service autoscaling 243 | */ 244 | readonly autoScaleTask?: AutoScaleTask; 245 | 246 | /** 247 | * Whether to put the load balancer in the public or private subnets 248 | * 249 | * @default true 250 | */ 251 | readonly internetFacing?: boolean; 252 | 253 | /** 254 | * The hostname to use for the keycloak server 255 | */ 256 | readonly hostname?: string; 257 | 258 | /** 259 | * The minimum number of Aurora Serverless V2 capacity units. 260 | * 261 | * @default 0.5 262 | */ 263 | readonly databaseMinCapacity?: number; 264 | 265 | /** 266 | * The maximum number of Aurora Serverless V2 capacity units. 267 | * 268 | * @default 10 269 | */ 270 | readonly databaseMaxCapacity?: number; 271 | 272 | /** 273 | * Controls what happens to the database if it stops being managed by CloudFormation 274 | * 275 | * @default RemovalPolicy.RETAIN 276 | */ 277 | readonly databaseRemovalPolicy?: cdk.RemovalPolicy; 278 | 279 | /** 280 | * Overrides the default image 281 | * 282 | * @default quay.io/keycloak/keycloak:${KEYCLOAK_VERSION} 283 | */ 284 | readonly containerImage?: ecs.ContainerImage; 285 | 286 | /** 287 | * The number of cpu units used by the keycloak task. 288 | * 289 | * @default 4096 290 | * @see FargateTaskDefinitionProps 291 | */ 292 | readonly taskCpu?: number; 293 | 294 | /** 295 | * The amount (in MiB) of memory used by the keycloak task. 296 | * 297 | * @default 8192 298 | * @see FargateTaskDefinitionProps 299 | */ 300 | readonly taskMemory?: number; 301 | 302 | } 303 | 304 | export class KeyCloak extends Construct { 305 | readonly vpc: ec2.IVpc; 306 | readonly db?: Database; 307 | readonly applicationLoadBalancer: elbv2.ApplicationLoadBalancer; 308 | readonly keycloakSecret: secretsmanager.ISecret; 309 | constructor(scope: Construct, id: string, props: KeyCloakProps) { 310 | super(scope, id); 311 | 312 | const region = cdk.Stack.of(this).region; 313 | const regionIsResolved = !cdk.Token.isUnresolved(region); 314 | 315 | if (props.auroraServerless && regionIsResolved && !AURORA_SERVERLESS_SUPPORTED_REGIONS.includes(region)) { 316 | throw new Error(`Aurora serverless is not supported in ${region}`); 317 | } 318 | 319 | this.keycloakSecret = this._generateKeycloakSecret(); 320 | this.vpc = props.vpc ?? getOrCreateVpc(this); 321 | 322 | this.db = this.addDatabase({ 323 | vpc: this.vpc, 324 | databaseSubnets: props.databaseSubnets, 325 | instanceType: props.databaseInstanceType, 326 | instanceEngine: props.instanceEngine, 327 | clusterEngine: props.clusterEngine, 328 | auroraServerless: props.auroraServerless, 329 | auroraServerlessV2: props.auroraServerlessV2, 330 | singleDbInstance: props.singleDbInstance, 331 | backupRetention: props.backupRetention, 332 | maxCapacity: props.databaseMaxCapacity, 333 | minCapacity: props.databaseMinCapacity, 334 | removalPolicy: props.databaseRemovalPolicy, 335 | }); 336 | const keycloakContainerService = this.addKeyCloakContainerService({ 337 | database: this.db, 338 | vpc: this.vpc, 339 | keycloakVersion: props.keycloakVersion, 340 | publicSubnets: props.publicSubnets, 341 | privateSubnets: props.privateSubnets, 342 | keycloakSecret: this.keycloakSecret, 343 | certificate: certmgr.Certificate.fromCertificateArn(this, 'ACMCert', props.certificateArn), 344 | bastion: props.bastion, 345 | nodeCount: props.nodeCount, 346 | stickinessCookieDuration: props.stickinessCookieDuration, 347 | autoScaleTask: props.autoScaleTask, 348 | env: props.env, 349 | internetFacing: props.internetFacing ?? true, 350 | hostname: props.hostname, 351 | containerImage: props.containerImage, 352 | taskCpu: props.taskCpu, 353 | taskMemory: props.taskMemory, 354 | }); 355 | 356 | this.applicationLoadBalancer = keycloakContainerService.applicationLoadBalancer; 357 | if (!cdk.Stack.of(this).templateOptions.description) { 358 | cdk.Stack.of(this).templateOptions.description = '(SO8021) - Deploy keycloak on AWS with cdk-keycloak construct library'; 359 | } 360 | } 361 | public addDatabase(props: DatabaseProps): Database { 362 | return new Database(this, 'Database', props); 363 | } 364 | public addKeyCloakContainerService(props: ContainerServiceProps) { 365 | return new ContainerService(this, 'KeyCloakContainerService', props); 366 | } 367 | private _generateKeycloakSecret(): secretsmanager.ISecret { 368 | return new secretsmanager.Secret(this, 'KCSecret', { 369 | generateSecretString: { 370 | generateStringKey: 'password', 371 | excludePunctuation: true, 372 | passwordLength: 12, 373 | secretStringTemplate: JSON.stringify({ username: 'keycloak' }), 374 | }, 375 | }); 376 | } 377 | } 378 | 379 | export interface DatabaseProps { 380 | /** 381 | * The VPC for the database 382 | */ 383 | readonly vpc: ec2.IVpc; 384 | /** 385 | * VPC subnets for database 386 | */ 387 | readonly databaseSubnets?: ec2.SubnetSelection; 388 | /** 389 | * The database instance type 390 | * 391 | * @default r5.large 392 | */ 393 | readonly instanceType?: ec2.InstanceType; 394 | /** 395 | * The database instance engine 396 | * 397 | * @default - MySQL 8.0.34 398 | */ 399 | readonly instanceEngine?: rds.IInstanceEngine; 400 | /** 401 | * The database cluster engine 402 | * 403 | * @default rds.AuroraMysqlEngineVersion.VER_3_04_0 404 | */ 405 | readonly clusterEngine?: rds.IClusterEngine; 406 | /** 407 | * enable aurora serverless 408 | * 409 | * @default false 410 | */ 411 | readonly auroraServerless?: boolean; 412 | /** 413 | * enable aurora serverless v2 414 | * 415 | * @default false 416 | */ 417 | readonly auroraServerlessV2?: boolean; 418 | 419 | /** 420 | * Whether to use single RDS instance rather than RDS cluster. Not recommended for production. 421 | * 422 | * @default false 423 | */ 424 | readonly singleDbInstance?: boolean; 425 | /** 426 | * database backup retension 427 | * 428 | * @default - 7 days 429 | */ 430 | readonly backupRetention?: cdk.Duration; 431 | /** 432 | * The minimum number of Aurora Serverless V2 capacity units. 433 | * 434 | * @default 0.5 435 | */ 436 | readonly minCapacity?: number; 437 | /** 438 | * The maximum number of Aurora Serverless V2 capacity units. 439 | * 440 | * @default 10 441 | */ 442 | readonly maxCapacity?: number; 443 | 444 | /** 445 | * Controls what happens to the database if it stops being managed by CloudFormation 446 | * 447 | * @default RemovalPolicy.RETAIN 448 | */ 449 | readonly removalPolicy?: cdk.RemovalPolicy; 450 | } 451 | 452 | /** 453 | * Database configuration 454 | */ 455 | export interface DatabaseConfig { 456 | /** 457 | * The database secret. 458 | */ 459 | readonly secret: secretsmanager.ISecret; 460 | /** 461 | * The database connnections. 462 | */ 463 | readonly connections: ec2.Connections; 464 | /** 465 | * The endpoint address for the database. 466 | */ 467 | readonly endpoint: string; 468 | /** 469 | * The databasae identifier. 470 | */ 471 | readonly identifier: string; 472 | } 473 | 474 | /** 475 | * Represents the database instance or database cluster 476 | */ 477 | export class Database extends Construct { 478 | readonly vpc: ec2.IVpc; 479 | readonly clusterEndpointHostname: string; 480 | readonly clusterIdentifier: string; 481 | readonly secret: secretsmanager.ISecret; 482 | readonly connections: ec2.Connections; 483 | private readonly _mysqlListenerPort: number = 3306; 484 | 485 | constructor(scope: Construct, id: string, props: DatabaseProps) { 486 | super(scope, id); 487 | this.vpc = props.vpc; 488 | let config; 489 | if (props.auroraServerless) { 490 | config = this._createServerlessCluster(props); 491 | } else if (props.auroraServerlessV2) { 492 | config = this._createServerlessV2Cluster(props); 493 | } else if (props.singleDbInstance) { 494 | config = this._createRdsInstance(props); 495 | } else { 496 | config = this._createRdsCluster(props); 497 | } 498 | this.secret = config.secret; 499 | // allow internally from the same security group 500 | config.connections.allowInternally(ec2.Port.tcp(this._mysqlListenerPort)); 501 | // allow from the whole vpc cidr 502 | config.connections.allowFrom(ec2.Peer.ipv4(props.vpc.vpcCidrBlock), ec2.Port.tcp(this._mysqlListenerPort)); 503 | this.clusterEndpointHostname = config.endpoint; 504 | this.clusterIdentifier = config.identifier; 505 | this.connections = config.connections; 506 | printOutput(this, 'DBSecretArn', config.secret.secretArn); 507 | printOutput(this, 'clusterEndpointHostname', this.clusterEndpointHostname); 508 | printOutput(this, 'clusterIdentifier', this.clusterIdentifier); 509 | } 510 | private _createRdsInstance(props: DatabaseProps): DatabaseConfig { 511 | const dbInstance = new rds.DatabaseInstance(this, 'DBInstance', { 512 | vpc: props.vpc, 513 | databaseName: 'keycloak', 514 | vpcSubnets: props.databaseSubnets, 515 | engine: props.instanceEngine ?? rds.DatabaseInstanceEngine.mysql({ 516 | version: rds.MysqlEngineVersion.VER_8_0_34, 517 | }), 518 | storageEncrypted: true, 519 | backupRetention: props.backupRetention ?? cdk.Duration.days(7), 520 | credentials: rds.Credentials.fromGeneratedSecret('admin'), 521 | instanceType: props.instanceType ?? new ec2.InstanceType('r5.large'), 522 | parameterGroup: rds.ParameterGroup.fromParameterGroupName(this, 'ParameterGroup', 'default.mysql8.0'), 523 | deletionProtection: true, 524 | removalPolicy: props.removalPolicy ?? cdk.RemovalPolicy.RETAIN, 525 | }); 526 | return { 527 | connections: dbInstance.connections, 528 | endpoint: dbInstance.dbInstanceEndpointAddress, 529 | identifier: dbInstance.instanceIdentifier, 530 | secret: dbInstance.secret!, 531 | }; 532 | } 533 | // create a RDS for MySQL DB cluster 534 | private _createRdsCluster(props: DatabaseProps): DatabaseConfig { 535 | const instanceProps = { 536 | instanceType: props.instanceType ?? new ec2.InstanceType('r5.large'), 537 | isFromLegacyInstanceProps: true, 538 | }; 539 | const dbCluster = new rds.DatabaseCluster(this, 'DBCluster', { 540 | engine: props.clusterEngine ?? rds.DatabaseClusterEngine.auroraMysql({ 541 | version: rds.AuroraMysqlEngineVersion.VER_3_04_0, 542 | }), 543 | defaultDatabaseName: 'keycloak', 544 | deletionProtection: true, 545 | credentials: rds.Credentials.fromGeneratedSecret('admin'), 546 | vpc: props.vpc, 547 | vpcSubnets: props.databaseSubnets, 548 | writer: rds.ClusterInstance.provisioned('Writer', { 549 | instanceType: instanceProps.instanceType, 550 | isFromLegacyInstanceProps: instanceProps.isFromLegacyInstanceProps, 551 | }), 552 | readers: [ 553 | rds.ClusterInstance.provisioned('Reader', { 554 | instanceType: instanceProps.instanceType, 555 | isFromLegacyInstanceProps: instanceProps.isFromLegacyInstanceProps, 556 | }), 557 | ], 558 | parameterGroup: rds.ParameterGroup.fromParameterGroupName(this, 'ParameterGroup', 'default.aurora-mysql8.0'), 559 | backup: { 560 | retention: props.backupRetention ?? cdk.Duration.days(7), 561 | }, 562 | storageEncrypted: true, 563 | removalPolicy: props.removalPolicy ?? cdk.RemovalPolicy.RETAIN, 564 | }); 565 | return { 566 | connections: dbCluster.connections, 567 | endpoint: dbCluster.clusterEndpoint.hostname, 568 | identifier: dbCluster.clusterIdentifier, 569 | secret: dbCluster.secret!, 570 | }; 571 | } 572 | private _createServerlessCluster(props: DatabaseProps): DatabaseConfig { 573 | const dbCluster = new rds.ServerlessCluster(this, 'AuroraServerlessCluster', { 574 | engine: rds.DatabaseClusterEngine.AURORA_MYSQL, 575 | vpc: props.vpc, 576 | defaultDatabaseName: 'keycloak', 577 | vpcSubnets: props.databaseSubnets, 578 | credentials: rds.Credentials.fromGeneratedSecret('admin'), 579 | backupRetention: props.backupRetention ?? cdk.Duration.days(7), 580 | deletionProtection: true, 581 | removalPolicy: props.removalPolicy ?? cdk.RemovalPolicy.RETAIN, 582 | parameterGroup: rds.ParameterGroup.fromParameterGroupName(this, 'ParameterGroup', 'default.aurora-mysql5.7'), 583 | }); 584 | return { 585 | connections: dbCluster.connections, 586 | endpoint: dbCluster.clusterEndpoint.hostname, 587 | identifier: dbCluster.clusterIdentifier, 588 | secret: dbCluster.secret!, 589 | }; 590 | } 591 | // create a RDS for MySQL DB cluster with Aurora Serverless v2 592 | private _createServerlessV2Cluster(props: DatabaseProps): DatabaseConfig { 593 | const instanceProps = { 594 | // Specify serverless Instance Type 595 | instanceType: new ec2.InstanceType('serverless'), 596 | isFromLegacyInstanceProps: true, 597 | }; 598 | const dbCluster = new rds.DatabaseCluster(this, 'DBCluster', { 599 | engine: props.clusterEngine ?? rds.DatabaseClusterEngine.auroraMysql({ 600 | version: rds.AuroraMysqlEngineVersion.VER_3_04_0, 601 | }), 602 | defaultDatabaseName: 'keycloak', 603 | deletionProtection: true, 604 | credentials: rds.Credentials.fromGeneratedSecret('admin'), 605 | vpc: props.vpc, 606 | vpcSubnets: props.databaseSubnets, 607 | writer: rds.ClusterInstance.provisioned('Writer', { 608 | instanceType: instanceProps.instanceType, 609 | isFromLegacyInstanceProps: instanceProps.isFromLegacyInstanceProps, 610 | }), 611 | readers: [ 612 | rds.ClusterInstance.provisioned('Reader', { 613 | instanceType: instanceProps.instanceType, 614 | isFromLegacyInstanceProps: instanceProps.isFromLegacyInstanceProps, 615 | }), 616 | ], 617 | // Set default parameter group for Aurora MySQL 8.0 618 | parameterGroup: rds.ParameterGroup.fromParameterGroupName(this, 'ParameterGroup', 'default.aurora-mysql8.0'), 619 | backup: { 620 | retention: props.backupRetention ?? cdk.Duration.days(7), 621 | }, 622 | storageEncrypted: true, 623 | removalPolicy: props.removalPolicy ?? cdk.RemovalPolicy.RETAIN, 624 | }); 625 | // Set Serverless V2 Scaling Configuration 626 | // TODO: Use cleaner way to set scaling configuration. 627 | // https://github.com/aws/aws-cdk/issues/20197 628 | ( 629 | dbCluster.node.findChild('Resource') as rds.CfnDBCluster 630 | ).serverlessV2ScalingConfiguration = { 631 | minCapacity: props.minCapacity ?? 0.5, 632 | maxCapacity: props.maxCapacity ?? 10, 633 | }; 634 | return { 635 | connections: dbCluster.connections, 636 | endpoint: dbCluster.clusterEndpoint.hostname, 637 | identifier: dbCluster.clusterIdentifier, 638 | secret: dbCluster.secret!, 639 | }; 640 | } 641 | } 642 | 643 | export interface ContainerServiceProps { 644 | /** 645 | * The environment variables to pass to the keycloak container 646 | */ 647 | readonly env?: { [key: string]: string }; 648 | /** 649 | * Keycloak version for the container image 650 | */ 651 | readonly keycloakVersion: KeycloakVersion; 652 | /** 653 | * The VPC for the service 654 | */ 655 | readonly vpc: ec2.IVpc; 656 | /** 657 | * VPC subnets for keycloak service 658 | */ 659 | readonly privateSubnets?: ec2.SubnetSelection; 660 | /** 661 | * VPC public subnets for ALB 662 | */ 663 | readonly publicSubnets?: ec2.SubnetSelection; 664 | /** 665 | * The RDS database for the service 666 | */ 667 | readonly database: Database; 668 | /** 669 | * The secrets manager secret for the keycloak 670 | */ 671 | readonly keycloakSecret: secretsmanager.ISecret; 672 | /** 673 | * The ACM certificate 674 | */ 675 | readonly certificate: certmgr.ICertificate; 676 | /** 677 | * Whether to create the bastion host 678 | * @default false 679 | */ 680 | readonly bastion?: boolean; 681 | /** 682 | * Whether to enable the ECS service deployment circuit breaker 683 | * @default false 684 | */ 685 | readonly circuitBreaker?: boolean; 686 | /** 687 | * Number of keycloak node in the cluster 688 | * 689 | * @default 1 690 | */ 691 | readonly nodeCount?: number; 692 | /** 693 | * The sticky session duration for the keycloak workload with ALB. 694 | * 695 | * @default - one day 696 | */ 697 | readonly stickinessCookieDuration?: cdk.Duration; 698 | 699 | /** 700 | * Autoscaling for the ECS Service 701 | * 702 | * @default - no ecs service autoscaling 703 | */ 704 | readonly autoScaleTask?: AutoScaleTask; 705 | 706 | /** 707 | * Whether to put the put the load balancer in the public or private subnets 708 | * 709 | * @default true 710 | */ 711 | readonly internetFacing?: boolean; 712 | 713 | /** 714 | * The hostname to use for the keycloak server 715 | */ 716 | readonly hostname?: string; 717 | 718 | /** 719 | * Overrides the default image 720 | * 721 | * @default quay.io/keycloak/keycloak:${KEYCLOAK_VERSION} 722 | */ 723 | readonly containerImage?: ecs.ContainerImage; 724 | 725 | /** 726 | * The number of cpu units used by the keycloak task. 727 | * 728 | * @default 4096 729 | * @see FargateTaskDefinitionProps 730 | */ 731 | readonly taskCpu?: number; 732 | 733 | /** 734 | * The amount (in MiB) of memory used by the keycloak task. 735 | * 736 | * @default 8192 737 | * @see FargateTaskDefinitionProps 738 | */ 739 | readonly taskMemory?: number; 740 | } 741 | 742 | export class ContainerService extends Construct { 743 | readonly service: ecs.FargateService; 744 | readonly applicationLoadBalancer: elbv2.ApplicationLoadBalancer; 745 | constructor(scope: Construct, id: string, props: ContainerServiceProps) { 746 | super(scope, id); 747 | 748 | let containerPort = 8443; 749 | let protocol = elbv2.ApplicationProtocol.HTTPS; 750 | let entryPoint = undefined; 751 | let workingDirectory = undefined; 752 | const image = props.containerImage ?? ecs.ContainerImage.fromRegistry(this.getKeyCloakDockerImageUri(props.keycloakVersion.version)); 753 | const isQuarkusDistribution = parseInt(props.keycloakVersion.version.split('.')[0]) > 16; 754 | let environment: {[key: string]: string} = { 755 | DB_ADDR: props.database.clusterEndpointHostname, 756 | DB_DATABASE: 'keycloak', 757 | DB_PORT: '3306', 758 | DB_USER: 'admin', 759 | DB_VENDOR: 'mysql', 760 | JDBC_PARAMS: 'useSSL=false', 761 | JGROUPS_DISCOVERY_PROTOCOL: 'JDBC_PING', 762 | // We don't need to specify `initialize_sql` string into `JGROUPS_DISCOVERY_PROPERTIES` property, 763 | // because the default `initialize_sql` is compatible with MySQL. (See: https://github.com/belaban/JGroups/blob/master/src/org/jgroups/protocols/JDBC_PING.java#L55-L60) 764 | // But you need to specify `initialize_sql` for PostgreSQL, because `varbinary` schema is not supported. (See: https://github.com/keycloak/keycloak-containers/blob/d4ce446dde3026f89f66fa86b58c2d0d6132ce4d/docker-compose-examples/keycloak-postgres-jdbc-ping.yml#L49) 765 | // JGROUPS_DISCOVERY_PROPERTIES: '', 766 | // KEYCLOAK_LOGLEVEL: 'DEBUG', 767 | PROXY_ADDRESS_FORWARDING: 'true', 768 | }; 769 | let secrets: {[key: string]: cdk.aws_ecs.Secret} = { 770 | DB_PASSWORD: ecs.Secret.fromSecretsManager(props.database.secret, 'password'), 771 | KEYCLOAK_USER: ecs.Secret.fromSecretsManager(props.keycloakSecret, 'username'), 772 | KEYCLOAK_PASSWORD: ecs.Secret.fromSecretsManager(props.keycloakSecret, 'password'), 773 | }; 774 | let portMappings: ecs.PortMapping[] = [ 775 | { containerPort: containerPort }, // HTTPS web port 776 | { containerPort: 7600 }, // jgroups-tcp 777 | { containerPort: 57600 }, // jgroups-tcp-fd 778 | { containerPort: 55200, protocol: ecs.Protocol.UDP }, // jgroups-udp 779 | { containerPort: 54200, protocol: ecs.Protocol.UDP }, // jgroups-udp-fd 780 | ]; 781 | 782 | // if this is a quarkus distribution 783 | if (isQuarkusDistribution) { 784 | containerPort = 8080; 785 | protocol = elbv2.ApplicationProtocol.HTTP; 786 | entryPoint = 'sh,-c,touch cache-ispn-jdbc-ping.xml && echo " " > cache-ispn-jdbc-ping.xml && cp cache-ispn-jdbc-ping.xml /opt/keycloak/conf/cache-ispn-jdbc-ping.xml && /opt/keycloak/bin/kc.sh build && /opt/keycloak/bin/kc.sh start'.split(','); 787 | workingDirectory = '/opt/keycloak'; 788 | environment = { 789 | KC_DB: 'mysql', 790 | KC_DB_URL_DATABASE: 'keycloak', 791 | KC_DB_URL_HOST: props.database.clusterEndpointHostname, 792 | KC_DB_URL_PORT: '3306', 793 | KC_DB_USERNAME: 'admin', 794 | KC_HOSTNAME: props.hostname!, 795 | KC_HOSTNAME_STRICT_BACKCHANNEL: 'true', 796 | KC_PROXY: 'edge', 797 | KC_CACHE_CONFIG_FILE: 'cache-ispn-jdbc-ping.xml', 798 | }; 799 | secrets = { 800 | KC_DB_PASSWORD: ecs.Secret.fromSecretsManager(props.database.secret, 'password'), 801 | KEYCLOAK_ADMIN: ecs.Secret.fromSecretsManager(props.keycloakSecret, 'username'), 802 | KEYCLOAK_ADMIN_PASSWORD: ecs.Secret.fromSecretsManager(props.keycloakSecret, 'password'), 803 | }; 804 | portMappings = [ 805 | { containerPort: containerPort }, // web port 806 | { containerPort: 7800 }, // jgroups-tcp 807 | { containerPort: 57800 }, // jgroups-tcp-fd 808 | ]; 809 | } 810 | 811 | const vpc = props.vpc; 812 | const cluster = new ecs.Cluster(this, 'Cluster', { vpc, containerInsights: true }); 813 | cluster.node.addDependency(props.database); 814 | const executionRole = new iam.Role(this, 'TaskRole', { 815 | assumedBy: new iam.CompositePrincipal( 816 | new iam.ServicePrincipal('ecs.amazonaws.com'), 817 | new iam.ServicePrincipal('ecs-tasks.amazonaws.com'), 818 | ), 819 | }); 820 | const taskDefinition = new ecs.FargateTaskDefinition(this, 'TaskDef', { 821 | cpu: props.taskCpu ?? 4096, 822 | memoryLimitMiB: props.taskMemory ?? 8192, 823 | executionRole, 824 | }); 825 | 826 | const logGroup = new logs.LogGroup(this, 'LogGroup', { 827 | retention: logs.RetentionDays.ONE_MONTH, 828 | removalPolicy: cdk.RemovalPolicy.RETAIN, 829 | }); 830 | 831 | const kc = taskDefinition.addContainer('keycloak', { 832 | image, 833 | entryPoint, 834 | workingDirectory, 835 | environment: Object.assign(environment, props.env), 836 | secrets, 837 | logging: ecs.LogDrivers.awsLogs({ 838 | streamPrefix: 'keycloak', 839 | logGroup, 840 | }), 841 | }); 842 | kc.addPortMappings(...portMappings); 843 | 844 | // we need extra privileges to fetch keycloak docker images from China mirror site 845 | taskDefinition.executionRole?.addManagedPolicy(iam.ManagedPolicy.fromAwsManagedPolicyName('AmazonEC2ContainerRegistryReadOnly')); 846 | 847 | this.service = new ecs.FargateService(this, 'Service', { 848 | cluster, 849 | taskDefinition, 850 | circuitBreaker: props.circuitBreaker ? { rollback: true } : undefined, 851 | desiredCount: props.nodeCount ?? 2, 852 | healthCheckGracePeriod: cdk.Duration.seconds(120), 853 | }); 854 | // we need to allow traffic from the same secret group for keycloak cluster with jdbc_ping 855 | if (isQuarkusDistribution) { 856 | this.service.connections.allowFrom(this.service.connections, ec2.Port.tcp(7800), 'kc jgroups-tcp'); 857 | this.service.connections.allowFrom(this.service.connections, ec2.Port.tcp(57800), 'kc jgroups-tcp-fd'); 858 | } else { 859 | this.service.connections.allowFrom(this.service.connections, ec2.Port.tcp(7600), 'kc jgroups-tcp'); 860 | this.service.connections.allowFrom(this.service.connections, ec2.Port.tcp(57600), 'kc jgroups-tcp-fd'); 861 | this.service.connections.allowFrom(this.service.connections, ec2.Port.udp(55200), 'kc jgroups-udp'); 862 | this.service.connections.allowFrom(this.service.connections, ec2.Port.udp(54200), 'kc jgroups-udp-fd'); 863 | } 864 | 865 | if (props.autoScaleTask) { 866 | const minCapacity = props.autoScaleTask.min ?? props.nodeCount ?? 2; 867 | const scaling = this.service.autoScaleTaskCount({ 868 | minCapacity, 869 | maxCapacity: props.autoScaleTask.max ?? minCapacity + 5, 870 | }); 871 | scaling.scaleOnCpuUtilization('CpuScaling', { 872 | targetUtilizationPercent: props.autoScaleTask.targetCpuUtilization ?? 75, 873 | }); 874 | }; 875 | 876 | this.applicationLoadBalancer = new elbv2.ApplicationLoadBalancer(this, 'ALB', { 877 | vpc, 878 | vpcSubnets: props.internetFacing ? props.publicSubnets : props.privateSubnets, 879 | internetFacing: props.internetFacing, 880 | }); 881 | printOutput(this, 'EndpointURL', `https://${this.applicationLoadBalancer.loadBalancerDnsName}`); 882 | 883 | const listener = this.applicationLoadBalancer.addListener('HttpsListener', { 884 | protocol: elbv2.ApplicationProtocol.HTTPS, 885 | certificates: [{ certificateArn: props.certificate.certificateArn }], 886 | }); 887 | listener.addTargets('ECSTarget', { 888 | targets: [this.service], 889 | healthCheck: { 890 | healthyThresholdCount: 3, 891 | }, 892 | // set slow_start.duration_seconds to 60 893 | // see https://docs.aws.amazon.com/cli/latest/reference/elbv2/modify-target-group-attributes.html 894 | slowStart: cdk.Duration.seconds(60), 895 | stickinessCookieDuration: props.stickinessCookieDuration ?? cdk.Duration.days(1), 896 | port: containerPort, 897 | protocol, 898 | }); 899 | 900 | // allow task execution role to read the secrets 901 | props.database.secret.grantRead(taskDefinition.executionRole!); 902 | props.keycloakSecret.grantRead(taskDefinition.executionRole!); 903 | 904 | // allow ecs task connect to database 905 | props.database.connections.allowDefaultPortFrom(this.service); 906 | 907 | 908 | // create a bastion host 909 | if (props.bastion === true) { 910 | const bast = new ec2.BastionHostLinux(this, 'Bast', { 911 | vpc, 912 | instanceType: new ec2.InstanceType('m5.large'), 913 | }); 914 | props.database.connections.allowDefaultPortFrom(bast); 915 | } 916 | } 917 | private getImageUriFromMap(map: dockerImageMap, version: string, id: string): string { 918 | const stack = cdk.Stack.of(this); 919 | if (cdk.Token.isUnresolved(stack.region)) { 920 | const mapping: { [k1: string]: { [k2: string]: any } } = {}; 921 | for (let [partition, uri] of Object.entries(map)) { 922 | uri += version; 923 | mapping[partition] = { uri }; 924 | } 925 | const imageMap = new cdk.CfnMapping(this, id, { mapping }); 926 | return imageMap.findInMap(cdk.Aws.PARTITION, 'uri'); 927 | } else { 928 | if (stack.region.startsWith('cn-')) { 929 | return map['aws-cn'] += version; 930 | } else { 931 | return map.aws += version; 932 | } 933 | } 934 | } 935 | private getKeyCloakDockerImageUri(version: string): string { 936 | return this.getImageUriFromMap(KEYCLOAK_DOCKER_IMAGE_URI_MAP, version, 'KeycloakImageMap'); 937 | } 938 | } 939 | 940 | /** 941 | * Create or import VPC 942 | * @param scope the cdk scope 943 | */ 944 | function getOrCreateVpc(scope: Construct): ec2.IVpc { 945 | // use an existing vpc or create a new one 946 | return scope.node.tryGetContext('use_default_vpc') === '1' ? 947 | ec2.Vpc.fromLookup(scope, 'Vpc', { isDefault: true }) : 948 | scope.node.tryGetContext('use_vpc_id') ? 949 | ec2.Vpc.fromLookup(scope, 'Vpc', { vpcId: scope.node.tryGetContext('use_vpc_id') }) : 950 | new ec2.Vpc(scope, 'Vpc', { maxAzs: 3, natGateways: 1 }); 951 | } 952 | 953 | function printOutput(scope: Construct, id: string, key: string | number) { 954 | new cdk.CfnOutput(scope, id, { value: String(key) }); 955 | } 956 | -------------------------------------------------------------------------------- /src/keycloak.ts.orig: -------------------------------------------------------------------------------- 1 | import * as certmgr from '@aws-cdk/aws-certificatemanager'; 2 | import * as ec2 from '@aws-cdk/aws-ec2'; 3 | import * as ecs from '@aws-cdk/aws-ecs'; 4 | import * as elbv2 from '@aws-cdk/aws-elasticloadbalancingv2'; 5 | import * as iam from '@aws-cdk/aws-iam'; 6 | import * as logs from '@aws-cdk/aws-logs'; 7 | import * as rds from '@aws-cdk/aws-rds'; 8 | import * as secretsmanager from '@aws-cdk/aws-secretsmanager'; 9 | import * as cdk from '@aws-cdk/core'; 10 | 11 | 12 | // regional availibility for aurora serverless 13 | // see https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/Concepts.AuroraFeaturesRegionsDBEngines.grids.html 14 | const AURORA_SERVERLESS_SUPPORTED_REGIONS = [ 15 | 'us-east-1', 16 | 'us-east-2', 17 | 'us-west-1', 18 | 'us-west-2', 19 | 'ap-south-1', 20 | 'ap-northeast-1', 21 | 'ap-northeast-2', 22 | 'ap-southeast-1', 23 | 'ap-southeast-2', 24 | 'ca-central-1', 25 | 'eu-central-1', 26 | 'eu-west-1', 27 | 'eu-west-2', 28 | 'eu-west-3', 29 | 'cn-northwest-1', 30 | ]; 31 | 32 | /** 33 | * Keycloak version 34 | */ 35 | export class KeycloakVersion { 36 | /** 37 | * Keycloak version 12.0.4 38 | */ 39 | public static readonly V12_0_4 = KeycloakVersion.of('12.0.4'); 40 | 41 | /** 42 | * Keycloak version 15.0.0 43 | */ 44 | public static readonly V15_0_0 = KeycloakVersion.of('15.0.0'); 45 | 46 | /** 47 | * Keycloak version 15.0.1 48 | */ 49 | public static readonly V15_0_1 = KeycloakVersion.of('15.0.1'); 50 | 51 | /** 52 | * Keycloak version 15.0.2 53 | */ 54 | public static readonly V15_0_2 = KeycloakVersion.of('15.0.2'); 55 | 56 | /** 57 | * Custom cluster version 58 | * @param version custom version number 59 | */ 60 | public static of(version: string) { return new KeycloakVersion(version); } 61 | /** 62 | * 63 | * @param version cluster version number 64 | */ 65 | private constructor(public readonly version: string) { } 66 | } 67 | 68 | // const KEYCLOAK_VERSION = '12.0.4'; 69 | 70 | interface dockerImageMap { 71 | 'aws': string; 72 | 'aws-cn': string; 73 | } 74 | 75 | const KEYCLOAK_DOCKER_IMAGE_URI_MAP: dockerImageMap = { 76 | 'aws': 'jboss/keycloak:', 77 | 'aws-cn': '048912060910.dkr.ecr.cn-northwest-1.amazonaws.com.cn/dockerhub/jboss/keycloak:', 78 | }; 79 | 80 | /** 81 | * The ECS task autoscaling definition 82 | */ 83 | export interface AutoScaleTask { 84 | /** 85 | * The minimal count of the task number 86 | * 87 | * @default - nodeCount 88 | */ 89 | readonly min?: number; 90 | /** 91 | * The maximal count of the task number 92 | * 93 | * @default - min + 5 94 | */ 95 | readonly max?: number; 96 | /** 97 | * The target cpu utilization for the service autoscaling 98 | * 99 | * @default 75 100 | */ 101 | readonly targetCpuUtilization?: number; 102 | } 103 | 104 | export interface KeyCloakProps { 105 | /** 106 | * The Keycloak version for the cluster. 107 | */ 108 | readonly keycloakVersion: KeycloakVersion; 109 | /** 110 | * The environment variables to pass to the keycloak container 111 | */ 112 | readonly env?: { [key: string]: string }; 113 | /** 114 | * VPC for the workload 115 | */ 116 | readonly vpc?: ec2.IVpc; 117 | /** 118 | * ACM certificate ARN to import 119 | */ 120 | readonly certificateArn: string; 121 | /** 122 | * Create a bastion host for debugging or trouble-shooting 123 | * 124 | * @default false 125 | */ 126 | readonly bastion?: boolean; 127 | /** 128 | * Number of keycloak node in the cluster 129 | * 130 | * @default 2 131 | */ 132 | readonly nodeCount?: number; 133 | /** 134 | * VPC public subnets for ALB 135 | * 136 | * @default - VPC public subnets 137 | */ 138 | readonly publicSubnets?: ec2.SubnetSelection; 139 | /** 140 | * VPC private subnets for keycloak service 141 | * 142 | * @default - VPC private subnets 143 | */ 144 | readonly privateSubnets?: ec2.SubnetSelection; 145 | /** 146 | * VPC subnets for database 147 | * 148 | * @default - VPC isolated subnets 149 | */ 150 | readonly databaseSubnets?: ec2.SubnetSelection; 151 | /** 152 | * Database instance type 153 | * 154 | * @default r5.large 155 | */ 156 | readonly databaseInstanceType?: ec2.InstanceType; 157 | /** 158 | * The database instance engine 159 | * 160 | * @default - MySQL 8.0.21 161 | */ 162 | readonly instanceEngine?: rds.IInstanceEngine; 163 | /** 164 | * The database cluster engine 165 | * 166 | * @default rds.AuroraMysqlEngineVersion.VER_2_09_1 167 | */ 168 | readonly clusterEngine?: rds.IClusterEngine; 169 | /** 170 | * Whether to use aurora serverless. When enabled, the `databaseInstanceType` and 171 | * `engine` will be ignored. The `rds.DatabaseClusterEngine.AURORA_MYSQL` will be used as 172 | * the default cluster engine instead. 173 | * 174 | * @default false 175 | */ 176 | readonly auroraServerless?: boolean; 177 | /** 178 | * Whether to use aurora serverless v2. When enabled, the `databaseInstanceType` and 179 | * `engine` will be ignored. 180 | * 181 | * @default false 182 | */ 183 | readonly auroraServerlessV2?: boolean; 184 | /** 185 | * Whether to use single RDS instance rather than RDS cluster. Not recommended for production. 186 | * 187 | * @default false 188 | */ 189 | readonly singleDbInstance?: boolean; 190 | /** 191 | * database backup retension 192 | * 193 | * @default - 7 days 194 | */ 195 | readonly backupRetention?: cdk.Duration; 196 | /** 197 | * The sticky session duration for the keycloak workload with ALB. 198 | * 199 | * @default - one day 200 | */ 201 | readonly stickinessCookieDuration?: cdk.Duration; 202 | /** 203 | * Autoscaling for the ECS Service 204 | * 205 | * @default - no ecs service autoscaling 206 | */ 207 | readonly autoScaleTask?: AutoScaleTask; 208 | } 209 | 210 | export class KeyCloak extends cdk.Construct { 211 | readonly vpc: ec2.IVpc; 212 | readonly db?: Database; 213 | constructor(scope: cdk.Construct, id: string, props: KeyCloakProps) { 214 | super(scope, id); 215 | 216 | const region = cdk.Stack.of(this).region; 217 | const regionIsResolved = !cdk.Token.isUnresolved(region); 218 | 219 | if (props.auroraServerless && regionIsResolved && !AURORA_SERVERLESS_SUPPORTED_REGIONS.includes(region)) { 220 | throw new Error(`Aurora serverless is not supported in ${region}`); 221 | } 222 | 223 | this.vpc = props.vpc ?? getOrCreateVpc(this); 224 | this.db = this.addDatabase({ 225 | vpc: this.vpc, 226 | databaseSubnets: props.databaseSubnets, 227 | instanceType: props.databaseInstanceType, 228 | instanceEngine: props.instanceEngine, 229 | clusterEngine: props.clusterEngine, 230 | auroraServerless: props.auroraServerless, 231 | auroraServerlessV2: props.auroraServerlessV2, 232 | singleDbInstance: props.singleDbInstance, 233 | backupRetention: props.backupRetention, 234 | }); 235 | this.addKeyCloakContainerService({ 236 | database: this.db, 237 | vpc: this.vpc, 238 | keycloakVersion: props.keycloakVersion, 239 | publicSubnets: props.publicSubnets, 240 | privateSubnets: props.privateSubnets, 241 | keycloakSecret: this._generateKeycloakSecret(), 242 | certificate: certmgr.Certificate.fromCertificateArn(this, 'ACMCert', props.certificateArn), 243 | bastion: props.bastion, 244 | nodeCount: props.nodeCount, 245 | stickinessCookieDuration: props.stickinessCookieDuration, 246 | autoScaleTask: props.autoScaleTask, 247 | env: props.env, 248 | }); 249 | if (!cdk.Stack.of(this).templateOptions.description) { 250 | cdk.Stack.of(this).templateOptions.description = '(SO8021) - Deploy keycloak on AWS with cdk-keycloak construct library'; 251 | } 252 | } 253 | public addDatabase(props: DatabaseProps): Database { 254 | return new Database(this, 'Database', props); 255 | } 256 | public addKeyCloakContainerService(props: ContainerServiceProps) { 257 | return new ContainerService(this, 'KeyCloakContainerSerivce', props); 258 | } 259 | private _generateKeycloakSecret(): secretsmanager.ISecret { 260 | return new secretsmanager.Secret(this, 'KCSecret', { 261 | generateSecretString: { 262 | generateStringKey: 'password', 263 | excludePunctuation: true, 264 | passwordLength: 12, 265 | secretStringTemplate: JSON.stringify({ username: 'keycloak' }), 266 | }, 267 | }); 268 | } 269 | } 270 | 271 | export interface DatabaseProps { 272 | /** 273 | * The VPC for the database 274 | */ 275 | readonly vpc: ec2.IVpc; 276 | /** 277 | * VPC subnets for database 278 | */ 279 | readonly databaseSubnets?: ec2.SubnetSelection; 280 | /** 281 | * The database instance type 282 | * 283 | * @default r5.large 284 | */ 285 | readonly instanceType?: ec2.InstanceType; 286 | /** 287 | * The database instance engine 288 | * 289 | * @default - MySQL 8.0.21 290 | */ 291 | readonly instanceEngine?: rds.IInstanceEngine; 292 | /** 293 | * The database cluster engine 294 | * 295 | * @default rds.AuroraMysqlEngineVersion.VER_2_09_1 296 | */ 297 | readonly clusterEngine?: rds.IClusterEngine; 298 | /** 299 | * enable aurora serverless 300 | * 301 | * @default false 302 | */ 303 | readonly auroraServerless?: boolean; 304 | /** 305 | * enable aurora serverless v2 306 | * 307 | * @default false 308 | */ 309 | readonly auroraServerlessV2?: boolean; 310 | 311 | /** 312 | * Whether to use single RDS instance rather than RDS cluster. Not recommended for production. 313 | * 314 | * @default false 315 | */ 316 | readonly singleDbInstance?: boolean; 317 | /** 318 | * database backup retension 319 | * 320 | * @default - 7 days 321 | */ 322 | readonly backupRetention?: cdk.Duration; 323 | } 324 | 325 | /** 326 | * Database configuration 327 | */ 328 | export interface DatabaseCofig { 329 | /** 330 | * The database secret. 331 | */ 332 | readonly secret: secretsmanager.ISecret; 333 | /** 334 | * The database connnections. 335 | */ 336 | readonly connections: ec2.Connections; 337 | /** 338 | * The endpoint address for the database. 339 | */ 340 | readonly endpoint: string; 341 | /** 342 | * The databasae identifier. 343 | */ 344 | readonly identifier: string; 345 | } 346 | 347 | /** 348 | * Represents the database instance or database cluster 349 | */ 350 | export class Database extends cdk.Construct { 351 | readonly vpc: ec2.IVpc; 352 | readonly clusterEndpointHostname: string; 353 | readonly clusterIdentifier: string; 354 | readonly secret: secretsmanager.ISecret; 355 | readonly connections: ec2.Connections; 356 | private readonly _mysqlListenerPort: number = 3306; 357 | 358 | constructor(scope: cdk.Construct, id: string, props: DatabaseProps) { 359 | super(scope, id); 360 | this.vpc = props.vpc; 361 | let config; 362 | if (props.auroraServerless) { 363 | config = this._createServerlessCluster(props); 364 | } else if (props.auroraServerlessV2) { 365 | config = this._createServerlessV2Cluster(props); 366 | } else if (props.singleDbInstance) { 367 | config = this._createRdsInstance(props); 368 | } else { 369 | config = this._createRdsCluster(props); 370 | } 371 | this.secret = config.secret; 372 | // allow internally from the same security group 373 | config.connections.allowInternally(ec2.Port.tcp(this._mysqlListenerPort)); 374 | // allow from the whole vpc cidr 375 | config.connections.allowFrom(ec2.Peer.ipv4(props.vpc.vpcCidrBlock), ec2.Port.tcp(this._mysqlListenerPort)); 376 | this.clusterEndpointHostname = config.endpoint; 377 | this.clusterIdentifier = config.identifier; 378 | this.connections = config.connections; 379 | printOutput(this, 'DBSecretArn', config.secret.secretArn); 380 | printOutput(this, 'clusterEndpointHostname', this.clusterEndpointHostname); 381 | printOutput(this, 'clusterIdentifier', this.clusterIdentifier); 382 | } 383 | private _createRdsInstance(props: DatabaseProps): DatabaseCofig { 384 | const dbInstance = new rds.DatabaseInstance(this, 'DBInstance', { 385 | vpc: props.vpc, 386 | databaseName: 'keycloak', 387 | vpcSubnets: props.databaseSubnets, 388 | engine: props.instanceEngine ?? rds.DatabaseInstanceEngine.mysql({ 389 | version: rds.MysqlEngineVersion.VER_8_0_21, 390 | }), 391 | storageEncrypted: true, 392 | backupRetention: props.backupRetention ?? cdk.Duration.days(7), 393 | credentials: rds.Credentials.fromGeneratedSecret('admin'), 394 | instanceType: props.instanceType ?? new ec2.InstanceType('r5.large'), 395 | parameterGroup: rds.ParameterGroup.fromParameterGroupName(this, 'ParameterGroup', 'default.mysql8.0'), 396 | deletionProtection: true, 397 | removalPolicy: cdk.RemovalPolicy.RETAIN, 398 | }); 399 | return { 400 | connections: dbInstance.connections, 401 | endpoint: dbInstance.dbInstanceEndpointAddress, 402 | identifier: dbInstance.instanceIdentifier, 403 | secret: dbInstance.secret!, 404 | }; 405 | } 406 | // create a RDS for MySQL DB cluster 407 | private _createRdsCluster(props: DatabaseProps): DatabaseCofig { 408 | const dbCluster = new rds.DatabaseCluster(this, 'DBCluster', { 409 | engine: props.clusterEngine ?? rds.DatabaseClusterEngine.auroraMysql({ 410 | version: rds.AuroraMysqlEngineVersion.VER_2_09_1, 411 | }), 412 | defaultDatabaseName: 'keycloak', 413 | deletionProtection: true, 414 | credentials: rds.Credentials.fromGeneratedSecret('admin'), 415 | instanceProps: { 416 | vpc: props.vpc, 417 | vpcSubnets: props.databaseSubnets, 418 | instanceType: props.instanceType ?? new ec2.InstanceType('r5.large'), 419 | }, 420 | parameterGroup: rds.ParameterGroup.fromParameterGroupName(this, 'ParameterGroup', 'default.aurora-mysql5.7'), 421 | backup: { 422 | retention: props.backupRetention ?? cdk.Duration.days(7), 423 | }, 424 | storageEncrypted: true, 425 | removalPolicy: cdk.RemovalPolicy.RETAIN, 426 | }); 427 | return { 428 | connections: dbCluster.connections, 429 | endpoint: dbCluster.clusterEndpoint.hostname, 430 | identifier: dbCluster.clusterIdentifier, 431 | secret: dbCluster.secret!, 432 | }; 433 | } 434 | private _createServerlessCluster(props: DatabaseProps): DatabaseCofig { 435 | const dbCluster = new rds.ServerlessCluster(this, 'AuroraServerlessCluster', { 436 | engine: rds.DatabaseClusterEngine.AURORA_MYSQL, 437 | vpc: props.vpc, 438 | defaultDatabaseName: 'keycloak', 439 | vpcSubnets: props.databaseSubnets, 440 | credentials: rds.Credentials.fromGeneratedSecret('admin'), 441 | backupRetention: props.backupRetention ?? cdk.Duration.days(7), 442 | deletionProtection: true, 443 | removalPolicy: cdk.RemovalPolicy.RETAIN, 444 | parameterGroup: rds.ParameterGroup.fromParameterGroupName(this, 'ParameterGroup', 'default.aurora-mysql5.7'), 445 | }); 446 | return { 447 | connections: dbCluster.connections, 448 | endpoint: dbCluster.clusterEndpoint.hostname, 449 | identifier: dbCluster.clusterIdentifier, 450 | secret: dbCluster.secret!, 451 | }; 452 | } 453 | // create a RDS for MySQL DB cluster with Aurora Serverless v2 454 | private _createServerlessV2Cluster(props: DatabaseProps): DatabaseCofig { 455 | const dbCluster = new rds.DatabaseCluster(this, 'DBCluster', { 456 | engine: rds.DatabaseClusterEngine.auroraMysql({ 457 | // Engine Version Manually Specified since Aurora MySQL 3.02.0 is not listed in CDK v1 currently 458 | version: { auroraMysqlFullVersion: '8.0.mysql_aurora.3.02.0', auroraMysqlMajorVersion: '8.0', _combineImportAndExportRoles: true } as rds.AuroraMysqlEngineVersion, 459 | }), 460 | defaultDatabaseName: 'keycloak', 461 | deletionProtection: true, 462 | credentials: rds.Credentials.fromGeneratedSecret('admin'), 463 | instanceProps: { 464 | vpc: props.vpc, 465 | vpcSubnets: props.databaseSubnets, 466 | // Specify serverless Instance Type 467 | instanceType: new ec2.InstanceType('serverless'), 468 | }, 469 | // Set default parameter group for Aurora MySQL 8.0 470 | parameterGroup: rds.ParameterGroup.fromParameterGroupName(this, 'ParameterGroup', 'default.aurora-mysql8.0'), 471 | backup: { 472 | retention: props.backupRetention ?? cdk.Duration.days(7), 473 | }, 474 | storageEncrypted: true, 475 | removalPolicy: cdk.RemovalPolicy.RETAIN, 476 | }); 477 | // Set Serverless V2 Scaling Configuration 478 | // TODO: Use cleaner way to set scaling configuration. 479 | // https://github.com/aws/aws-cdk/issues/20197 480 | ( 481 | dbCluster.node.findChild('Resource') as rds.CfnDBCluster 482 | ).serverlessV2ScalingConfiguration = { 483 | minCapacity: 0.5, 484 | maxCapacity: 10, 485 | }; 486 | return { 487 | connections: dbCluster.connections, 488 | endpoint: dbCluster.clusterEndpoint.hostname, 489 | identifier: dbCluster.clusterIdentifier, 490 | secret: dbCluster.secret!, 491 | }; 492 | } 493 | } 494 | 495 | export interface ContainerServiceProps { 496 | /** 497 | * The environment variables to pass to the keycloak container 498 | */ 499 | readonly env?: { [key: string]: string }; 500 | /** 501 | * Keycloak version for the container image 502 | */ 503 | readonly keycloakVersion: KeycloakVersion; 504 | /** 505 | * The VPC for the service 506 | */ 507 | readonly vpc: ec2.IVpc; 508 | /** 509 | * VPC subnets for keycloak service 510 | */ 511 | readonly privateSubnets?: ec2.SubnetSelection; 512 | /** 513 | * VPC public subnets for ALB 514 | */ 515 | readonly publicSubnets?: ec2.SubnetSelection; 516 | /** 517 | * The RDS database for the service 518 | */ 519 | readonly database: Database; 520 | /** 521 | * The secrets manager secret for the keycloak 522 | */ 523 | readonly keycloakSecret: secretsmanager.ISecret; 524 | /** 525 | * The ACM certificate 526 | */ 527 | readonly certificate: certmgr.ICertificate; 528 | /** 529 | * Whether to create the bastion host 530 | * @default false 531 | */ 532 | readonly bastion?: boolean; 533 | /** 534 | * Whether to enable the ECS service deployment circuit breaker 535 | * @default false 536 | */ 537 | readonly circuitBreaker?: boolean; 538 | /** 539 | * Number of keycloak node in the cluster 540 | * 541 | * @default 1 542 | */ 543 | readonly nodeCount?: number; 544 | /** 545 | * The sticky session duration for the keycloak workload with ALB. 546 | * 547 | * @default - one day 548 | */ 549 | readonly stickinessCookieDuration?: cdk.Duration; 550 | 551 | /** 552 | * Autoscaling for the ECS Service 553 | * 554 | * @default - no ecs service autoscaling 555 | */ 556 | readonly autoScaleTask?: AutoScaleTask; 557 | } 558 | 559 | export class ContainerService extends cdk.Construct { 560 | readonly service: ecs.FargateService; 561 | constructor(scope: cdk.Construct, id: string, props: ContainerServiceProps) { 562 | super(scope, id); 563 | 564 | const vpc = props.vpc; 565 | const cluster = new ecs.Cluster(this, 'Cluster', { vpc }); 566 | cluster.node.addDependency(props.database); 567 | const taskRole = new iam.Role(this, 'TaskRole', { 568 | assumedBy: new iam.CompositePrincipal( 569 | new iam.ServicePrincipal('ecs.amazonaws.com'), 570 | new iam.ServicePrincipal('ecs-tasks.amazonaws.com'), 571 | ), 572 | }); 573 | const taskDefinition = new ecs.FargateTaskDefinition(this, 'TaskDef', { 574 | cpu: 4096, 575 | memoryLimitMiB: 8192, 576 | executionRole: taskRole, 577 | }); 578 | 579 | const logGroup = new logs.LogGroup(this, 'LogGroup', { 580 | retention: logs.RetentionDays.ONE_MONTH, 581 | removalPolicy: cdk.RemovalPolicy.RETAIN, 582 | }); 583 | 584 | const kc = taskDefinition.addContainer('keycloak', { 585 | image: ecs.ContainerImage.fromRegistry(this.getKeyCloakDockerImageUri(props.keycloakVersion.version)), 586 | environment: Object.assign({ 587 | DB_ADDR: props.database.clusterEndpointHostname, 588 | DB_DATABASE: 'keycloak', 589 | DB_PORT: '3306', 590 | DB_USER: 'admin', 591 | DB_VENDOR: 'mysql', 592 | // KEYCLOAK_LOGLEVEL: 'DEBUG', 593 | PROXY_ADDRESS_FORWARDING: 'true', 594 | JDBC_PARAMS: 'useSSL=false', 595 | JGROUPS_DISCOVERY_PROTOCOL: 'JDBC_PING', 596 | // We don't need to specify `initialize_sql` string into `JGROUPS_DISCOVERY_PROPERTIES` property, 597 | // because the default `initialize_sql` is compatible with MySQL. (See: https://github.com/belaban/JGroups/blob/master/src/org/jgroups/protocols/JDBC_PING.java#L55-L60) 598 | // But you need to specify `initialize_sql` for PostgreSQL, because `varbinary` schema is not supported. (See: https://github.com/keycloak/keycloak-containers/blob/d4ce446dde3026f89f66fa86b58c2d0d6132ce4d/docker-compose-examples/keycloak-postgres-jdbc-ping.yml#L49) 599 | // JGROUPS_DISCOVERY_PROPERTIES: '', 600 | }, props.env), 601 | secrets: { 602 | DB_PASSWORD: ecs.Secret.fromSecretsManager(props.database.secret, 'password'), 603 | KEYCLOAK_USER: ecs.Secret.fromSecretsManager(props.keycloakSecret, 'username'), 604 | KEYCLOAK_PASSWORD: ecs.Secret.fromSecretsManager(props.keycloakSecret, 'password'), 605 | }, 606 | logging: ecs.LogDrivers.awsLogs({ 607 | streamPrefix: 'keycloak', 608 | logGroup, 609 | }), 610 | }); 611 | kc.addPortMappings( 612 | { containerPort: 8443 }, // HTTPS web port 613 | { containerPort: 7600 }, // jgroups-tcp 614 | { containerPort: 57600 }, // jgroups-tcp-fd 615 | { containerPort: 55200, protocol: ecs.Protocol.UDP }, // jgroups-udp 616 | { containerPort: 54200, protocol: ecs.Protocol.UDP }, // jgroups-udp-fd 617 | ); 618 | 619 | // we need extra privileges to fetch keycloak docker images from China mirror site 620 | taskDefinition.executionRole?.addManagedPolicy(iam.ManagedPolicy.fromAwsManagedPolicyName('AmazonEC2ContainerRegistryReadOnly')); 621 | 622 | this.service = new ecs.FargateService(this, 'Service', { 623 | cluster, 624 | taskDefinition, 625 | circuitBreaker: props.circuitBreaker ? { rollback: true } : undefined, 626 | desiredCount: props.nodeCount ?? 2, 627 | healthCheckGracePeriod: cdk.Duration.seconds(120), 628 | }); 629 | // we need to allow traffic from the same secret group for keycloak cluster with jdbc_ping 630 | this.service.connections.allowFrom(this.service.connections, ec2.Port.tcp(7600), 'kc jgroups-tcp'); 631 | this.service.connections.allowFrom(this.service.connections, ec2.Port.tcp(57600), 'kc jgroups-tcp-fd'); 632 | this.service.connections.allowFrom(this.service.connections, ec2.Port.udp(55200), 'kc jgroups-udp'); 633 | this.service.connections.allowFrom(this.service.connections, ec2.Port.udp(54200), 'kc jgroups-udp-fd'); 634 | 635 | if (props.autoScaleTask) { 636 | const minCapacity = props.autoScaleTask.min ?? props.nodeCount ?? 2; 637 | const scaling = this.service.autoScaleTaskCount({ 638 | minCapacity, 639 | maxCapacity: props.autoScaleTask.max ?? minCapacity + 5, 640 | }); 641 | scaling.scaleOnCpuUtilization('CpuScaling', { 642 | targetUtilizationPercent: props.autoScaleTask.targetCpuUtilization ?? 75, 643 | }); 644 | }; 645 | 646 | const alb = new elbv2.ApplicationLoadBalancer(this, 'ALB', { 647 | vpc, 648 | vpcSubnets: props.publicSubnets, 649 | internetFacing: true, 650 | }); 651 | printOutput(this, 'EndpointURL', `https://${alb.loadBalancerDnsName}`); 652 | 653 | const listener = alb.addListener('HttpsListener', { 654 | protocol: elbv2.ApplicationProtocol.HTTPS, 655 | certificates: [{ certificateArn: props.certificate.certificateArn }], 656 | }); 657 | 658 | listener.addTargets('ECSTarget', { 659 | targets: [this.service], 660 | healthCheck: { 661 | healthyThresholdCount: 3, 662 | }, 663 | // set slow_start.duration_seconds to 60 664 | // see https://docs.aws.amazon.com/cli/latest/reference/elbv2/modify-target-group-attributes.html 665 | slowStart: cdk.Duration.seconds(60), 666 | stickinessCookieDuration: props.stickinessCookieDuration ?? cdk.Duration.days(1), 667 | port: 8443, 668 | protocol: elbv2.ApplicationProtocol.HTTPS, 669 | }); 670 | 671 | // allow task execution role to read the secrets 672 | props.database.secret.grantRead(taskDefinition.executionRole!); 673 | props.keycloakSecret.grantRead(taskDefinition.executionRole!); 674 | 675 | // allow ecs task connect to database 676 | props.database.connections.allowDefaultPortFrom(this.service); 677 | 678 | 679 | // create a bastion host 680 | if (props.bastion === true) { 681 | const bast = new ec2.BastionHostLinux(this, 'Bast', { 682 | vpc, 683 | instanceType: new ec2.InstanceType('m5.large'), 684 | }); 685 | props.database.connections.allowDefaultPortFrom(bast); 686 | } 687 | } 688 | private getImageUriFromMap(map: dockerImageMap, version: string, id: string): string { 689 | const stack = cdk.Stack.of(this); 690 | if (cdk.Token.isUnresolved(stack.region)) { 691 | const mapping: { [k1: string]: { [k2: string]: any } } = {}; 692 | for (let [partition, uri] of Object.entries(map)) { 693 | uri += version; 694 | mapping[partition] = { uri }; 695 | } 696 | const imageMap = new cdk.CfnMapping(this, id, { mapping }); 697 | return imageMap.findInMap(cdk.Aws.PARTITION, 'uri'); 698 | } else { 699 | if (stack.region.startsWith('cn-')) { 700 | return map['aws-cn'] += version; 701 | } else { 702 | return map.aws += version; 703 | } 704 | } 705 | } 706 | private getKeyCloakDockerImageUri(version: string): string { 707 | return this.getImageUriFromMap(KEYCLOAK_DOCKER_IMAGE_URI_MAP, version, 'KeycloakImageMap'); 708 | } 709 | } 710 | 711 | /** 712 | * Create or import VPC 713 | * @param scope the cdk scope 714 | */ 715 | function getOrCreateVpc(scope: cdk.Construct): ec2.IVpc { 716 | // use an existing vpc or create a new one 717 | return scope.node.tryGetContext('use_default_vpc') === '1' ? 718 | ec2.Vpc.fromLookup(scope, 'Vpc', { isDefault: true }) : 719 | scope.node.tryGetContext('use_vpc_id') ? 720 | ec2.Vpc.fromLookup(scope, 'Vpc', { vpcId: scope.node.tryGetContext('use_vpc_id') }) : 721 | new ec2.Vpc(scope, 'Vpc', { maxAzs: 3, natGateways: 1 }); 722 | } 723 | 724 | function printOutput(scope: cdk.Construct, id: string, key: string | number) { 725 | new cdk.CfnOutput(scope, id, { value: String(key) }); 726 | } 727 | -------------------------------------------------------------------------------- /test/__snapshots__/integ.snapshot.test.ts.snap.rej: -------------------------------------------------------------------------------- 1 | *************** 2 | *** 203,208 **** 3 | "UpdateReplacePolicy": "Retain", 4 | }, 5 | "KeyCloakKCSecretF8498E5C": Object { 6 | "Properties": Object { 7 | "GenerateSecretString": Object { 8 | "ExcludePunctuation": true, 9 | --- 210,216 ---- 10 | "UpdateReplacePolicy": "Retain", 11 | }, 12 | "KeyCloakKCSecretF8498E5C": Object { 13 | + "DeletionPolicy": "Delete", 14 | "Properties": Object { 15 | "GenerateSecretString": Object { 16 | "ExcludePunctuation": true, 17 | *************** 18 | *** 212,217 **** 19 | }, 20 | }, 21 | "Type": "AWS::SecretsManager::Secret", 22 | }, 23 | "KeyCloakKeyCloakContainerSerivceALBE100B67D": Object { 24 | "DependsOn": Array [ 25 | --- 220,226 ---- 26 | }, 27 | }, 28 | "Type": "AWS::SecretsManager::Secret", 29 | + "UpdateReplacePolicy": "Delete", 30 | }, 31 | "KeyCloakKeyCloakContainerSerivceALBE100B67D": Object { 32 | "DependsOn": Array [ 33 | *************** 34 | *** 1175,1180 **** 35 | "Type": "AWS::EC2::VPCGatewayAttachment", 36 | }, 37 | "keycloakdemoKeyCloakDatabaseAuroraServerlessClusterSecretBBC2EF3A3fdaad7efa858a3daf9490cf0a702aeb": Object { 38 | "Properties": Object { 39 | "Description": Object { 40 | "Fn::Join": Array [ 41 | --- 1184,1190 ---- 42 | "Type": "AWS::EC2::VPCGatewayAttachment", 43 | }, 44 | "keycloakdemoKeyCloakDatabaseAuroraServerlessClusterSecretBBC2EF3A3fdaad7efa858a3daf9490cf0a702aeb": Object { 45 | + "DeletionPolicy": "Delete", 46 | "Properties": Object { 47 | "Description": Object { 48 | "Fn::Join": Array [ 49 | *************** 50 | *** 1195,1200 **** 51 | }, 52 | }, 53 | "Type": "AWS::SecretsManager::Secret", 54 | }, 55 | }, 56 | } 57 | --- 1205,1238 ---- 58 | }, 59 | }, 60 | "Type": "AWS::SecretsManager::Secret", 61 | + "UpdateReplacePolicy": "Delete", 62 | + }, 63 | + }, 64 | + "Rules": Object { 65 | + "CheckBootstrapVersion": Object { 66 | + "Assertions": Array [ 67 | + Object { 68 | + "Assert": Object { 69 | + "Fn::Not": Array [ 70 | + Object { 71 | + "Fn::Contains": Array [ 72 | + Array [ 73 | + "1", 74 | + "2", 75 | + "3", 76 | + "4", 77 | + "5", 78 | + ], 79 | + Object { 80 | + "Ref": "BootstrapVersion", 81 | + }, 82 | + ], 83 | + }, 84 | + ], 85 | + }, 86 | + "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI.", 87 | + }, 88 | + ], 89 | }, 90 | }, 91 | } 92 | -------------------------------------------------------------------------------- /test/cluster-quarkus.test.ts: -------------------------------------------------------------------------------- 1 | import { App, assertions, Stack } from 'aws-cdk-lib'; 2 | import * as kc from '../src'; 3 | // import '@aws-cdk/assert/jest'; 4 | import { KeycloakVersion } from '../src'; 5 | test('create the default cluster', () => { 6 | // GIVEN 7 | const app = new App(); 8 | const stack = new Stack(app, 'testing-stack'); 9 | 10 | // WHEN 11 | new kc.KeyCloak(stack, 'KeyCloak', { 12 | certificateArn: 'MOCK_ARN', 13 | keycloakVersion: KeycloakVersion.V22_0_4, 14 | }); 15 | 16 | // THEN 17 | const t = assertions.Template.fromStack(stack); 18 | t.hasResourceProperties('AWS::RDS::DBCluster', { 19 | Engine: 'aurora-mysql', 20 | DBClusterParameterGroupName: 'default.aurora-mysql8.0', 21 | DBSubnetGroupName: { 22 | Ref: 'KeyCloakDatabaseDBClusterSubnetsE36F1B1B', 23 | }, 24 | EngineVersion: '8.0.mysql_aurora.3.04.0', 25 | MasterUsername: 'admin', 26 | MasterUserPassword: { 27 | 'Fn::Join': [ 28 | '', 29 | [ 30 | '{{resolve:secretsmanager:', 31 | { 32 | Ref: 'testingstackKeyCloakDatabaseDBClusterSecret754146743fdaad7efa858a3daf9490cf0a702aeb', 33 | }, 34 | ':SecretString:password::}}', 35 | ], 36 | ], 37 | }, 38 | VpcSecurityGroupIds: [ 39 | { 40 | 'Fn::GetAtt': ['KeyCloakDatabaseDBClusterSecurityGroup843B4392', 'GroupId'], 41 | }, 42 | ], 43 | }); 44 | // we should have 2 db instances in the cluster 45 | t.resourceCountIs('AWS::RDS::DBInstance', 2); 46 | // we should have 2 secrets 47 | t.resourceCountIs('AWS::SecretsManager::Secret', 2); 48 | // we should have ecs service 49 | t.hasResourceProperties('AWS::ECS::Service', { 50 | Cluster: { 51 | Ref: 'KeyCloakKeyCloakContainerServiceCluster4583BCAE', 52 | }, 53 | DeploymentConfiguration: { 54 | MaximumPercent: 200, 55 | MinimumHealthyPercent: 50, 56 | }, 57 | DesiredCount: 2, 58 | EnableECSManagedTags: false, 59 | HealthCheckGracePeriodSeconds: 120, 60 | LaunchType: 'FARGATE', 61 | LoadBalancers: [ 62 | { 63 | ContainerName: 'keycloak', 64 | ContainerPort: 8080, 65 | TargetGroupArn: { 66 | Ref: 'KeyCloakKeyCloakContainerServiceALBHttpsListenerECSTargetGroup65B43774', 67 | }, 68 | }, 69 | ], 70 | NetworkConfiguration: { 71 | AwsvpcConfiguration: { 72 | AssignPublicIp: 'DISABLED', 73 | SecurityGroups: [ 74 | { 75 | 'Fn::GetAtt': [ 76 | 'KeyCloakKeyCloakContainerServiceSecurityGroup7433DA3A', 77 | 'GroupId', 78 | ], 79 | }, 80 | ], 81 | Subnets: [ 82 | { 83 | Ref: 'KeyCloakVpcPrivateSubnet1SubnetA692DFFF', 84 | }, 85 | { 86 | Ref: 'KeyCloakVpcPrivateSubnet2SubnetC8682D75', 87 | }, 88 | ], 89 | }, 90 | }, 91 | TaskDefinition: { 92 | Ref: 'KeyCloakKeyCloakContainerServiceTaskDef6AD61714', 93 | }, 94 | }); 95 | }); 96 | 97 | test('with aurora serverless', () => { 98 | // GIVEN 99 | const app = new App(); 100 | const stack = new Stack(app, 'testing-stack'); 101 | 102 | // WHEN 103 | new kc.KeyCloak(stack, 'KeyCloak', { 104 | certificateArn: 'MOCK_ARN', 105 | auroraServerless: true, 106 | keycloakVersion: KeycloakVersion.V22_0_4, 107 | }); 108 | 109 | // THEN 110 | const t = assertions.Template.fromStack(stack); 111 | t.hasResourceProperties('AWS::RDS::DBCluster', { 112 | Engine: 'aurora-mysql', 113 | DBClusterParameterGroupName: 'default.aurora-mysql5.7', 114 | EngineMode: 'serverless', 115 | }); 116 | // we should have 0 db instance in the cluster 117 | t.resourceCountIs('AWS::RDS::DBInstance', 0); 118 | // we should have 2 secrets 119 | t.resourceCountIs('AWS::SecretsManager::Secret', 2); 120 | // we should have ecs service 121 | t.hasResourceProperties('AWS::ECS::Service', { 122 | Cluster: { 123 | Ref: 'KeyCloakKeyCloakContainerServiceCluster4583BCAE', 124 | }, 125 | DeploymentConfiguration: { 126 | MaximumPercent: 200, 127 | MinimumHealthyPercent: 50, 128 | }, 129 | DesiredCount: 2, 130 | EnableECSManagedTags: false, 131 | HealthCheckGracePeriodSeconds: 120, 132 | LaunchType: 'FARGATE', 133 | LoadBalancers: [ 134 | { 135 | ContainerName: 'keycloak', 136 | ContainerPort: 8080, 137 | TargetGroupArn: { 138 | Ref: 'KeyCloakKeyCloakContainerServiceALBHttpsListenerECSTargetGroup65B43774', 139 | }, 140 | }, 141 | ], 142 | NetworkConfiguration: { 143 | AwsvpcConfiguration: { 144 | AssignPublicIp: 'DISABLED', 145 | SecurityGroups: [ 146 | { 147 | 'Fn::GetAtt': [ 148 | 'KeyCloakKeyCloakContainerServiceSecurityGroup7433DA3A', 149 | 'GroupId', 150 | ], 151 | }, 152 | ], 153 | Subnets: [ 154 | { 155 | Ref: 'KeyCloakVpcPrivateSubnet1SubnetA692DFFF', 156 | }, 157 | { 158 | Ref: 'KeyCloakVpcPrivateSubnet2SubnetC8682D75', 159 | }, 160 | ], 161 | }, 162 | }, 163 | TaskDefinition: { 164 | Ref: 'KeyCloakKeyCloakContainerServiceTaskDef6AD61714', 165 | }, 166 | }); 167 | }); 168 | 169 | test('with aurora serverless v2', () => { 170 | // GIVEN 171 | const app = new App(); 172 | const stack = new Stack(app, 'testing-stack'); 173 | 174 | // WHEN 175 | new kc.KeyCloak(stack, 'KeyCloak', { 176 | certificateArn: 'MOCK_ARN', 177 | auroraServerlessV2: true, 178 | keycloakVersion: KeycloakVersion.V22_0_4, 179 | }); 180 | 181 | // THEN 182 | const t = assertions.Template.fromStack(stack); 183 | t.hasResourceProperties('AWS::RDS::DBCluster', { 184 | Engine: 'aurora-mysql', 185 | DBClusterParameterGroupName: 'default.aurora-mysql8.0', 186 | DBSubnetGroupName: { 187 | Ref: 'KeyCloakDatabaseDBClusterSubnetsE36F1B1B', 188 | }, 189 | EngineVersion: '8.0.mysql_aurora.3.04.0', 190 | MasterUsername: 'admin', 191 | MasterUserPassword: { 192 | 'Fn::Join': [ 193 | '', 194 | [ 195 | '{{resolve:secretsmanager:', 196 | { 197 | Ref: 'testingstackKeyCloakDatabaseDBClusterSecret754146743fdaad7efa858a3daf9490cf0a702aeb', 198 | }, 199 | ':SecretString:password::}}', 200 | ], 201 | ], 202 | }, 203 | ServerlessV2ScalingConfiguration: { 204 | MaxCapacity: 10, 205 | MinCapacity: 0.5, 206 | }, 207 | VpcSecurityGroupIds: [ 208 | { 209 | 'Fn::GetAtt': ['KeyCloakDatabaseDBClusterSecurityGroup843B4392', 'GroupId'], 210 | }, 211 | ], 212 | }); 213 | // we should have 2 db instances in the cluster 214 | t.resourceCountIs('AWS::RDS::DBInstance', 2); 215 | // we should have db instance with db.serverless instance class 216 | t.hasResourceProperties('AWS::RDS::DBInstance', { 217 | DBInstanceClass: 'db.serverless', 218 | }); 219 | // we should have 2 secrets 220 | t.resourceCountIs('AWS::SecretsManager::Secret', 2); 221 | // we should have ecs service 222 | t.hasResourceProperties('AWS::ECS::Service', { 223 | Cluster: { 224 | Ref: 'KeyCloakKeyCloakContainerServiceCluster4583BCAE', 225 | }, 226 | DeploymentConfiguration: { 227 | MaximumPercent: 200, 228 | MinimumHealthyPercent: 50, 229 | }, 230 | DesiredCount: 2, 231 | EnableECSManagedTags: false, 232 | HealthCheckGracePeriodSeconds: 120, 233 | LaunchType: 'FARGATE', 234 | LoadBalancers: [ 235 | { 236 | ContainerName: 'keycloak', 237 | ContainerPort: 8080, 238 | TargetGroupArn: { 239 | Ref: 'KeyCloakKeyCloakContainerServiceALBHttpsListenerECSTargetGroup65B43774', 240 | }, 241 | }, 242 | ], 243 | NetworkConfiguration: { 244 | AwsvpcConfiguration: { 245 | AssignPublicIp: 'DISABLED', 246 | SecurityGroups: [ 247 | { 248 | 'Fn::GetAtt': [ 249 | 'KeyCloakKeyCloakContainerServiceSecurityGroup7433DA3A', 250 | 'GroupId', 251 | ], 252 | }, 253 | ], 254 | Subnets: [ 255 | { 256 | Ref: 'KeyCloakVpcPrivateSubnet1SubnetA692DFFF', 257 | }, 258 | { 259 | Ref: 'KeyCloakVpcPrivateSubnet2SubnetC8682D75', 260 | }, 261 | ], 262 | }, 263 | }, 264 | TaskDefinition: { 265 | Ref: 'KeyCloakKeyCloakContainerServiceTaskDef6AD61714', 266 | }, 267 | }); 268 | }); 269 | 270 | test('with single rds instance', () => { 271 | // GIVEN 272 | const app = new App(); 273 | const stack = new Stack(app, 'testing-stack'); 274 | 275 | // WHEN 276 | new kc.KeyCloak(stack, 'KeyCloak', { 277 | certificateArn: 'MOCK_ARN', 278 | singleDbInstance: true, 279 | keycloakVersion: KeycloakVersion.V22_0_4, 280 | }); 281 | 282 | // THEN 283 | const t = assertions.Template.fromStack(stack); 284 | // we should have no cluster 285 | t.resourceCountIs('AWS::RDS::DBCluster', 0); 286 | // we should have 1 db instance in the cluster 287 | t.resourceCountIs('AWS::RDS::DBInstance', 1); 288 | t.hasResourceProperties('AWS::RDS::DBInstance', { 289 | DBInstanceClass: 'db.r5.large', 290 | AllocatedStorage: '100', 291 | CopyTagsToSnapshot: true, 292 | DBParameterGroupName: 'default.mysql8.0', 293 | DBSubnetGroupName: { 294 | Ref: 'KeyCloakDatabaseDBInstanceSubnetGroup71BF616F', 295 | }, 296 | Engine: 'mysql', 297 | EngineVersion: '8.0.34', 298 | MasterUsername: 'admin', 299 | MasterUserPassword: { 300 | 'Fn::Join': [ 301 | '', 302 | [ 303 | '{{resolve:secretsmanager:', 304 | { 305 | Ref: 'testingstackKeyCloakDatabaseDBInstanceSecretA1C7CB093fdaad7efa858a3daf9490cf0a702aeb', 306 | }, 307 | ':SecretString:password::}}', 308 | ], 309 | ], 310 | }, 311 | StorageType: 'gp2', 312 | VPCSecurityGroups: [ 313 | { 314 | 'Fn::GetAtt': ['KeyCloakDatabaseDBInstanceSecurityGroupC897947D', 'GroupId'], 315 | }, 316 | ], 317 | }); 318 | // we should have 2 secrets 319 | t.resourceCountIs('AWS::SecretsManager::Secret', 2); 320 | // we should have ecs service 321 | t.hasResourceProperties('AWS::ECS::Service', { 322 | Cluster: { 323 | Ref: 'KeyCloakKeyCloakContainerServiceCluster4583BCAE', 324 | }, 325 | DeploymentConfiguration: { 326 | MaximumPercent: 200, 327 | MinimumHealthyPercent: 50, 328 | }, 329 | DesiredCount: 2, 330 | EnableECSManagedTags: false, 331 | HealthCheckGracePeriodSeconds: 120, 332 | LaunchType: 'FARGATE', 333 | LoadBalancers: [ 334 | { 335 | ContainerName: 'keycloak', 336 | ContainerPort: 8080, 337 | TargetGroupArn: { 338 | Ref: 'KeyCloakKeyCloakContainerServiceALBHttpsListenerECSTargetGroup65B43774', 339 | }, 340 | }, 341 | ], 342 | NetworkConfiguration: { 343 | AwsvpcConfiguration: { 344 | AssignPublicIp: 'DISABLED', 345 | SecurityGroups: [ 346 | { 347 | 'Fn::GetAtt': [ 348 | 'KeyCloakKeyCloakContainerServiceSecurityGroup7433DA3A', 349 | 'GroupId', 350 | ], 351 | }, 352 | ], 353 | Subnets: [ 354 | { 355 | Ref: 'KeyCloakVpcPrivateSubnet1SubnetA692DFFF', 356 | }, 357 | { 358 | Ref: 'KeyCloakVpcPrivateSubnet2SubnetC8682D75', 359 | }, 360 | ], 361 | }, 362 | }, 363 | TaskDefinition: { 364 | Ref: 'KeyCloakKeyCloakContainerServiceTaskDef6AD61714', 365 | }, 366 | }); 367 | }); 368 | 369 | test('with env', () => { 370 | // GIVEN 371 | const app = new App(); 372 | const stack = new Stack(app, 'testing-stack'); 373 | 374 | // WHEN 375 | new kc.KeyCloak(stack, 'KeyCloak', { 376 | keycloakVersion: KeycloakVersion.V22_0_4, 377 | certificateArn: 'MOCK_ARN', 378 | env: { 379 | JAVA_OPTS: '-DHelloWorld', 380 | }, 381 | hostname: 'keycloak.test', 382 | }); 383 | 384 | // THEN 385 | const t = assertions.Template.fromStack(stack); 386 | t.hasResourceProperties('AWS::ECS::TaskDefinition', { 387 | ContainerDefinitions: [ 388 | { 389 | Environment: [ 390 | { 391 | Name: 'KC_DB', 392 | Value: 'mysql', 393 | }, 394 | { 395 | Name: 'KC_DB_URL_DATABASE', 396 | Value: 'keycloak', 397 | }, 398 | { 399 | Name: 'KC_DB_URL_HOST', 400 | Value: { 401 | 'Fn::GetAtt': ['KeyCloakDatabaseDBCluster06E9C0E1', 'Endpoint.Address'], 402 | }, 403 | }, 404 | { 405 | Name: 'KC_DB_URL_PORT', 406 | Value: '3306', 407 | }, 408 | { 409 | Name: 'KC_DB_USERNAME', 410 | Value: 'admin', 411 | }, 412 | { 413 | Name: 'KC_HOSTNAME', 414 | Value: 'keycloak.test', 415 | }, 416 | { 417 | Name: 'KC_HOSTNAME_STRICT_BACKCHANNEL', 418 | Value: 'true', 419 | }, 420 | { 421 | Name: 'KC_PROXY', 422 | Value: 'edge', 423 | }, 424 | { 425 | Name: 'KC_CACHE_CONFIG_FILE', 426 | Value: 'cache-ispn-jdbc-ping.xml', 427 | }, 428 | { 429 | Name: 'JAVA_OPTS', 430 | Value: '-DHelloWorld', 431 | }, 432 | ], 433 | Essential: true, 434 | Image: { 435 | 'Fn::FindInMap': [ 436 | 'KeyCloakKeyCloakContainerServiceKeycloakImageMapE15D4544', 437 | { 438 | Ref: 'AWS::Partition', 439 | }, 440 | 'uri', 441 | ], 442 | }, 443 | LogConfiguration: { 444 | LogDriver: 'awslogs', 445 | Options: { 446 | 'awslogs-group': { 447 | Ref: 'KeyCloakKeyCloakContainerServiceLogGroup770A4A22', 448 | }, 449 | 'awslogs-stream-prefix': 'keycloak', 450 | 'awslogs-region': { 451 | Ref: 'AWS::Region', 452 | }, 453 | }, 454 | }, 455 | Name: 'keycloak', 456 | PortMappings: [ 457 | { 458 | ContainerPort: 8080, 459 | Protocol: 'tcp', 460 | }, 461 | { 462 | ContainerPort: 7800, 463 | Protocol: 'tcp', 464 | }, 465 | { 466 | ContainerPort: 57800, 467 | Protocol: 'tcp', 468 | }, 469 | ], 470 | Secrets: [ 471 | { 472 | Name: 'KC_DB_PASSWORD', 473 | ValueFrom: { 474 | 'Fn::Join': [ 475 | '', 476 | [ 477 | { 478 | Ref: 'KeyCloakDatabaseDBClusterSecretAttachment50401C92', 479 | }, 480 | ':password::', 481 | ], 482 | ], 483 | }, 484 | }, 485 | { 486 | Name: 'KEYCLOAK_ADMIN', 487 | ValueFrom: { 488 | 'Fn::Join': [ 489 | '', 490 | [ 491 | { 492 | Ref: 'KeyCloakKCSecretF8498E5C', 493 | }, 494 | ':username::', 495 | ], 496 | ], 497 | }, 498 | }, 499 | { 500 | Name: 'KEYCLOAK_ADMIN_PASSWORD', 501 | ValueFrom: { 502 | 'Fn::Join': [ 503 | '', 504 | [ 505 | { 506 | Ref: 'KeyCloakKCSecretF8498E5C', 507 | }, 508 | ':password::', 509 | ], 510 | ], 511 | }, 512 | }, 513 | ], 514 | }, 515 | ], 516 | Cpu: '4096', 517 | ExecutionRoleArn: { 518 | 'Fn::GetAtt': ['KeyCloakKeyCloakContainerServiceTaskRoleE227375A', 'Arn'], 519 | }, 520 | Family: 'testingstackKeyCloakKeyCloakContainerServiceTaskDef1B636EF3', 521 | Memory: '8192', 522 | NetworkMode: 'awsvpc', 523 | RequiresCompatibilities: ['FARGATE'], 524 | TaskRoleArn: { 525 | 'Fn::GetAtt': ['KeyCloakKeyCloakContainerServiceTaskDefTaskRole509DDBD7', 'Arn'], 526 | }, 527 | }); 528 | }); 529 | 530 | 531 | test('with customized task settings', () => { 532 | // GIVEN 533 | const app = new App(); 534 | const stack = new Stack(app, 'testing-stack'); 535 | 536 | // WHEN 537 | new kc.KeyCloak(stack, 'KeyCloak', { 538 | keycloakVersion: KeycloakVersion.V22_0_4, 539 | certificateArn: 'MOCK_ARN', 540 | hostname: 'keycloak.test', 541 | taskCpu: 512, 542 | taskMemory: 1024, 543 | }); 544 | 545 | // THEN 546 | const t = assertions.Template.fromStack(stack); 547 | t.hasResourceProperties('AWS::ECS::TaskDefinition', { 548 | ContainerDefinitions: [ 549 | { 550 | Name: 'keycloak', 551 | }, 552 | ], 553 | Cpu: '512', 554 | Memory: '1024', 555 | }); 556 | }); 557 | -------------------------------------------------------------------------------- /test/cluster-wildfly.test.ts: -------------------------------------------------------------------------------- 1 | import { App, assertions, Stack } from 'aws-cdk-lib'; 2 | import * as kc from '../src'; 3 | // import '@aws-cdk/assert/jest'; 4 | import { KeycloakVersion } from '../src'; 5 | test('create the default cluster', () => { 6 | 7 | // GIVEN 8 | const app = new App(); 9 | const stack = new Stack(app, 'testing-stack'); 10 | 11 | // WHEN 12 | new kc.KeyCloak(stack, 'KeyCloak', { 13 | certificateArn: 'MOCK_ARN', 14 | keycloakVersion: KeycloakVersion.V15_0_2, 15 | }); 16 | 17 | // THEN 18 | const t = assertions.Template.fromStack(stack); 19 | t.hasResourceProperties('AWS::RDS::DBCluster', { 20 | Engine: 'aurora-mysql', 21 | DBClusterParameterGroupName: 'default.aurora-mysql8.0', 22 | DBSubnetGroupName: { 23 | Ref: 'KeyCloakDatabaseDBClusterSubnetsE36F1B1B', 24 | }, 25 | EngineVersion: '8.0.mysql_aurora.3.04.0', 26 | MasterUsername: 'admin', 27 | MasterUserPassword: { 28 | 'Fn::Join': [ 29 | '', 30 | [ 31 | '{{resolve:secretsmanager:', 32 | { 33 | Ref: 'testingstackKeyCloakDatabaseDBClusterSecret754146743fdaad7efa858a3daf9490cf0a702aeb', 34 | }, 35 | ':SecretString:password::}}', 36 | ], 37 | ], 38 | }, 39 | VpcSecurityGroupIds: [ 40 | { 41 | 'Fn::GetAtt': [ 42 | 'KeyCloakDatabaseDBClusterSecurityGroup843B4392', 43 | 'GroupId', 44 | ], 45 | }, 46 | ], 47 | }); 48 | // we should have 2 db instances in the cluster 49 | t.resourceCountIs('AWS::RDS::DBInstance', 2); 50 | // we should have 2 secrets 51 | t.resourceCountIs('AWS::SecretsManager::Secret', 2); 52 | // we should have ecs service 53 | t.hasResourceProperties('AWS::ECS::Service', { 54 | Cluster: { 55 | Ref: 'KeyCloakKeyCloakContainerServiceCluster4583BCAE', 56 | }, 57 | DeploymentConfiguration: { 58 | MaximumPercent: 200, 59 | MinimumHealthyPercent: 50, 60 | }, 61 | DesiredCount: 2, 62 | EnableECSManagedTags: false, 63 | HealthCheckGracePeriodSeconds: 120, 64 | LaunchType: 'FARGATE', 65 | LoadBalancers: [ 66 | { 67 | ContainerName: 'keycloak', 68 | ContainerPort: 8443, 69 | TargetGroupArn: { 70 | Ref: 'KeyCloakKeyCloakContainerServiceALBHttpsListenerECSTargetGroup65B43774', 71 | }, 72 | }, 73 | ], 74 | NetworkConfiguration: { 75 | AwsvpcConfiguration: { 76 | AssignPublicIp: 'DISABLED', 77 | SecurityGroups: [ 78 | { 79 | 'Fn::GetAtt': [ 80 | 'KeyCloakKeyCloakContainerServiceSecurityGroup7433DA3A', 81 | 'GroupId', 82 | ], 83 | }, 84 | ], 85 | Subnets: [ 86 | { 87 | Ref: 'KeyCloakVpcPrivateSubnet1SubnetA692DFFF', 88 | }, 89 | { 90 | Ref: 'KeyCloakVpcPrivateSubnet2SubnetC8682D75', 91 | }, 92 | ], 93 | }, 94 | }, 95 | TaskDefinition: { 96 | Ref: 'KeyCloakKeyCloakContainerServiceTaskDef6AD61714', 97 | }, 98 | }); 99 | }); 100 | 101 | test('with aurora serverless', () => { 102 | 103 | // GIVEN 104 | const app = new App(); 105 | const stack = new Stack(app, 'testing-stack'); 106 | 107 | // WHEN 108 | new kc.KeyCloak(stack, 'KeyCloak', { 109 | certificateArn: 'MOCK_ARN', 110 | auroraServerless: true, 111 | keycloakVersion: KeycloakVersion.V15_0_2, 112 | }); 113 | 114 | // THEN 115 | const t = assertions.Template.fromStack(stack); 116 | t.hasResourceProperties('AWS::RDS::DBCluster', { 117 | Engine: 'aurora-mysql', 118 | DBClusterParameterGroupName: 'default.aurora-mysql5.7', 119 | EngineMode: 'serverless', 120 | }); 121 | // we should have 0 db instance in the cluster 122 | t.resourceCountIs('AWS::RDS::DBInstance', 0); 123 | // we should have 2 secrets 124 | t.resourceCountIs('AWS::SecretsManager::Secret', 2); 125 | // we should have ecs service 126 | t.hasResourceProperties('AWS::ECS::Service', { 127 | Cluster: { 128 | Ref: 'KeyCloakKeyCloakContainerServiceCluster4583BCAE', 129 | }, 130 | DeploymentConfiguration: { 131 | MaximumPercent: 200, 132 | MinimumHealthyPercent: 50, 133 | }, 134 | DesiredCount: 2, 135 | EnableECSManagedTags: false, 136 | HealthCheckGracePeriodSeconds: 120, 137 | LaunchType: 'FARGATE', 138 | LoadBalancers: [ 139 | { 140 | ContainerName: 'keycloak', 141 | ContainerPort: 8443, 142 | TargetGroupArn: { 143 | Ref: 'KeyCloakKeyCloakContainerServiceALBHttpsListenerECSTargetGroup65B43774', 144 | }, 145 | }, 146 | ], 147 | NetworkConfiguration: { 148 | AwsvpcConfiguration: { 149 | AssignPublicIp: 'DISABLED', 150 | SecurityGroups: [ 151 | { 152 | 'Fn::GetAtt': [ 153 | 'KeyCloakKeyCloakContainerServiceSecurityGroup7433DA3A', 154 | 'GroupId', 155 | ], 156 | }, 157 | ], 158 | Subnets: [ 159 | { 160 | Ref: 'KeyCloakVpcPrivateSubnet1SubnetA692DFFF', 161 | }, 162 | { 163 | Ref: 'KeyCloakVpcPrivateSubnet2SubnetC8682D75', 164 | }, 165 | ], 166 | }, 167 | }, 168 | TaskDefinition: { 169 | Ref: 'KeyCloakKeyCloakContainerServiceTaskDef6AD61714', 170 | }, 171 | }); 172 | }); 173 | 174 | test('with aurora serverless v2', () => { 175 | 176 | // GIVEN 177 | const app = new App(); 178 | const stack = new Stack(app, 'testing-stack'); 179 | 180 | // WHEN 181 | new kc.KeyCloak(stack, 'KeyCloak', { 182 | certificateArn: 'MOCK_ARN', 183 | auroraServerlessV2: true, 184 | keycloakVersion: KeycloakVersion.V15_0_2, 185 | }); 186 | 187 | // THEN 188 | const t = assertions.Template.fromStack(stack); 189 | t.hasResourceProperties('AWS::RDS::DBCluster', { 190 | Engine: 'aurora-mysql', 191 | DBClusterParameterGroupName: 'default.aurora-mysql8.0', 192 | DBSubnetGroupName: { 193 | Ref: 'KeyCloakDatabaseDBClusterSubnetsE36F1B1B', 194 | }, 195 | EngineVersion: '8.0.mysql_aurora.3.04.0', 196 | MasterUsername: 'admin', 197 | MasterUserPassword: { 198 | 'Fn::Join': [ 199 | '', 200 | [ 201 | '{{resolve:secretsmanager:', 202 | { 203 | Ref: 'testingstackKeyCloakDatabaseDBClusterSecret754146743fdaad7efa858a3daf9490cf0a702aeb', 204 | }, 205 | ':SecretString:password::}}', 206 | ], 207 | ], 208 | }, 209 | ServerlessV2ScalingConfiguration: { 210 | MaxCapacity: 10, 211 | MinCapacity: 0.5, 212 | }, 213 | VpcSecurityGroupIds: [ 214 | { 215 | 'Fn::GetAtt': [ 216 | 'KeyCloakDatabaseDBClusterSecurityGroup843B4392', 217 | 'GroupId', 218 | ], 219 | }, 220 | ], 221 | }); 222 | // we should have 2 db instances in the cluster 223 | t.resourceCountIs('AWS::RDS::DBInstance', 2); 224 | // we should have db instance with db.serverless instance class 225 | t.hasResourceProperties('AWS::RDS::DBInstance', { 226 | DBInstanceClass: 'db.serverless', 227 | }); 228 | // we should have 2 secrets 229 | t.resourceCountIs('AWS::SecretsManager::Secret', 2); 230 | // we should have ecs service 231 | t.hasResourceProperties('AWS::ECS::Service', { 232 | Cluster: { 233 | Ref: 'KeyCloakKeyCloakContainerServiceCluster4583BCAE', 234 | }, 235 | DeploymentConfiguration: { 236 | MaximumPercent: 200, 237 | MinimumHealthyPercent: 50, 238 | }, 239 | DesiredCount: 2, 240 | EnableECSManagedTags: false, 241 | HealthCheckGracePeriodSeconds: 120, 242 | LaunchType: 'FARGATE', 243 | LoadBalancers: [ 244 | { 245 | ContainerName: 'keycloak', 246 | ContainerPort: 8443, 247 | TargetGroupArn: { 248 | Ref: 'KeyCloakKeyCloakContainerServiceALBHttpsListenerECSTargetGroup65B43774', 249 | }, 250 | }, 251 | ], 252 | NetworkConfiguration: { 253 | AwsvpcConfiguration: { 254 | AssignPublicIp: 'DISABLED', 255 | SecurityGroups: [ 256 | { 257 | 'Fn::GetAtt': [ 258 | 'KeyCloakKeyCloakContainerServiceSecurityGroup7433DA3A', 259 | 'GroupId', 260 | ], 261 | }, 262 | ], 263 | Subnets: [ 264 | { 265 | Ref: 'KeyCloakVpcPrivateSubnet1SubnetA692DFFF', 266 | }, 267 | { 268 | Ref: 'KeyCloakVpcPrivateSubnet2SubnetC8682D75', 269 | }, 270 | ], 271 | }, 272 | }, 273 | TaskDefinition: { 274 | Ref: 'KeyCloakKeyCloakContainerServiceTaskDef6AD61714', 275 | }, 276 | }); 277 | }); 278 | 279 | test('with single rds instance', () => { 280 | 281 | // GIVEN 282 | const app = new App(); 283 | const stack = new Stack(app, 'testing-stack'); 284 | 285 | // WHEN 286 | new kc.KeyCloak(stack, 'KeyCloak', { 287 | certificateArn: 'MOCK_ARN', 288 | singleDbInstance: true, 289 | keycloakVersion: KeycloakVersion.V15_0_2, 290 | }); 291 | 292 | // THEN 293 | const t = assertions.Template.fromStack(stack); 294 | // we should have no cluster 295 | t.resourceCountIs('AWS::RDS::DBCluster', 0); 296 | // we should have 1 db instance in the cluster 297 | t.resourceCountIs('AWS::RDS::DBInstance', 1); 298 | t.hasResourceProperties('AWS::RDS::DBInstance', { 299 | DBInstanceClass: 'db.r5.large', 300 | AllocatedStorage: '100', 301 | CopyTagsToSnapshot: true, 302 | DBParameterGroupName: 'default.mysql8.0', 303 | DBSubnetGroupName: { 304 | Ref: 'KeyCloakDatabaseDBInstanceSubnetGroup71BF616F', 305 | }, 306 | Engine: 'mysql', 307 | EngineVersion: '8.0.34', 308 | MasterUsername: 'admin', 309 | MasterUserPassword: { 310 | 'Fn::Join': [ 311 | '', 312 | [ 313 | '{{resolve:secretsmanager:', 314 | { 315 | Ref: 'testingstackKeyCloakDatabaseDBInstanceSecretA1C7CB093fdaad7efa858a3daf9490cf0a702aeb', 316 | }, 317 | ':SecretString:password::}}', 318 | ], 319 | ], 320 | }, 321 | StorageType: 'gp2', 322 | VPCSecurityGroups: [ 323 | { 324 | 'Fn::GetAtt': [ 325 | 'KeyCloakDatabaseDBInstanceSecurityGroupC897947D', 326 | 'GroupId', 327 | ], 328 | }, 329 | ], 330 | }); 331 | // we should have 2 secrets 332 | t.resourceCountIs('AWS::SecretsManager::Secret', 2); 333 | // we should have ecs service 334 | t.hasResourceProperties('AWS::ECS::Service', { 335 | Cluster: { 336 | Ref: 'KeyCloakKeyCloakContainerServiceCluster4583BCAE', 337 | }, 338 | DeploymentConfiguration: { 339 | MaximumPercent: 200, 340 | MinimumHealthyPercent: 50, 341 | }, 342 | DesiredCount: 2, 343 | EnableECSManagedTags: false, 344 | HealthCheckGracePeriodSeconds: 120, 345 | LaunchType: 'FARGATE', 346 | LoadBalancers: [ 347 | { 348 | ContainerName: 'keycloak', 349 | ContainerPort: 8443, 350 | TargetGroupArn: { 351 | Ref: 'KeyCloakKeyCloakContainerServiceALBHttpsListenerECSTargetGroup65B43774', 352 | }, 353 | }, 354 | ], 355 | NetworkConfiguration: { 356 | AwsvpcConfiguration: { 357 | AssignPublicIp: 'DISABLED', 358 | SecurityGroups: [ 359 | { 360 | 'Fn::GetAtt': [ 361 | 'KeyCloakKeyCloakContainerServiceSecurityGroup7433DA3A', 362 | 'GroupId', 363 | ], 364 | }, 365 | ], 366 | Subnets: [ 367 | { 368 | Ref: 'KeyCloakVpcPrivateSubnet1SubnetA692DFFF', 369 | }, 370 | { 371 | Ref: 'KeyCloakVpcPrivateSubnet2SubnetC8682D75', 372 | }, 373 | ], 374 | }, 375 | }, 376 | TaskDefinition: { 377 | Ref: 'KeyCloakKeyCloakContainerServiceTaskDef6AD61714', 378 | }, 379 | }); 380 | }); 381 | 382 | test('with env', () => { 383 | // GIVEN 384 | const app = new App(); 385 | const stack = new Stack(app, 'testing-stack'); 386 | 387 | // WHEN 388 | new kc.KeyCloak(stack, 'KeyCloak', { 389 | keycloakVersion: KeycloakVersion.V15_0_2, 390 | certificateArn: 'MOCK_ARN', 391 | env: { 392 | JAVA_OPTS: '-DHelloWorld', 393 | }, 394 | }); 395 | 396 | // THEN 397 | const t = assertions.Template.fromStack(stack); 398 | t.hasResourceProperties('AWS::ECS::TaskDefinition', { 399 | 400 | ContainerDefinitions: [ 401 | { 402 | Environment: [ 403 | { 404 | Name: 'DB_ADDR', 405 | Value: { 406 | 'Fn::GetAtt': [ 407 | 'KeyCloakDatabaseDBCluster06E9C0E1', 408 | 'Endpoint.Address', 409 | ], 410 | }, 411 | }, 412 | { 413 | Name: 'DB_DATABASE', 414 | Value: 'keycloak', 415 | }, 416 | { 417 | Name: 'DB_PORT', 418 | Value: '3306', 419 | }, 420 | { 421 | Name: 'DB_USER', 422 | Value: 'admin', 423 | }, 424 | { 425 | Name: 'DB_VENDOR', 426 | Value: 'mysql', 427 | }, 428 | { 429 | Name: 'JDBC_PARAMS', 430 | Value: 'useSSL=false', 431 | }, 432 | { 433 | Name: 'JGROUPS_DISCOVERY_PROTOCOL', 434 | Value: 'JDBC_PING', 435 | }, 436 | { 437 | Name: 'PROXY_ADDRESS_FORWARDING', 438 | Value: 'true', 439 | }, 440 | { 441 | Name: 'JAVA_OPTS', 442 | Value: '-DHelloWorld', 443 | }, 444 | ], 445 | Essential: true, 446 | Image: { 447 | 'Fn::FindInMap': [ 448 | 'KeyCloakKeyCloakContainerServiceKeycloakImageMapE15D4544', 449 | { 450 | Ref: 'AWS::Partition', 451 | }, 452 | 'uri', 453 | ], 454 | }, 455 | LogConfiguration: { 456 | LogDriver: 'awslogs', 457 | Options: { 458 | 'awslogs-group': { 459 | Ref: 'KeyCloakKeyCloakContainerServiceLogGroup770A4A22', 460 | }, 461 | 'awslogs-stream-prefix': 'keycloak', 462 | 'awslogs-region': { 463 | Ref: 'AWS::Region', 464 | }, 465 | }, 466 | }, 467 | Name: 'keycloak', 468 | PortMappings: [ 469 | { 470 | ContainerPort: 8443, 471 | Protocol: 'tcp', 472 | }, 473 | { 474 | ContainerPort: 7600, 475 | Protocol: 'tcp', 476 | }, 477 | { 478 | ContainerPort: 57600, 479 | Protocol: 'tcp', 480 | }, 481 | { 482 | ContainerPort: 55200, 483 | Protocol: 'udp', 484 | }, 485 | { 486 | ContainerPort: 54200, 487 | Protocol: 'udp', 488 | }, 489 | ], 490 | Secrets: [ 491 | { 492 | Name: 'DB_PASSWORD', 493 | ValueFrom: { 494 | 'Fn::Join': [ 495 | '', 496 | [ 497 | { 498 | Ref: 'KeyCloakDatabaseDBClusterSecretAttachment50401C92', 499 | }, 500 | ':password::', 501 | ], 502 | ], 503 | }, 504 | }, 505 | { 506 | Name: 'KEYCLOAK_USER', 507 | ValueFrom: { 508 | 'Fn::Join': [ 509 | '', 510 | [ 511 | { 512 | Ref: 'KeyCloakKCSecretF8498E5C', 513 | }, 514 | ':username::', 515 | ], 516 | ], 517 | }, 518 | }, 519 | { 520 | Name: 'KEYCLOAK_PASSWORD', 521 | ValueFrom: { 522 | 'Fn::Join': [ 523 | '', 524 | [ 525 | { 526 | Ref: 'KeyCloakKCSecretF8498E5C', 527 | }, 528 | ':password::', 529 | ], 530 | ], 531 | }, 532 | }, 533 | ], 534 | }, 535 | ], 536 | Cpu: '4096', 537 | ExecutionRoleArn: { 538 | 'Fn::GetAtt': [ 539 | 'KeyCloakKeyCloakContainerServiceTaskRoleE227375A', 540 | 'Arn', 541 | ], 542 | }, 543 | Family: 'testingstackKeyCloakKeyCloakContainerServiceTaskDef1B636EF3', 544 | Memory: '8192', 545 | NetworkMode: 'awsvpc', 546 | RequiresCompatibilities: [ 547 | 'FARGATE', 548 | ], 549 | TaskRoleArn: { 550 | 'Fn::GetAtt': [ 551 | 'KeyCloakKeyCloakContainerServiceTaskDefTaskRole509DDBD7', 552 | 'Arn', 553 | ], 554 | }, 555 | }); 556 | }); 557 | -------------------------------------------------------------------------------- /test/integ.snapshot.test.ts: -------------------------------------------------------------------------------- 1 | import { assertions } from 'aws-cdk-lib'; 2 | import { IntegTesting } from '../src/integ.default'; 3 | 4 | test('integ snapshot validation', () => { 5 | const integ = new IntegTesting(); 6 | integ.stack.forEach(stack => { 7 | const t = assertions.Template.fromStack(stack); 8 | expect(t).toMatchSnapshot(); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /tsconfig.dev.json: -------------------------------------------------------------------------------- 1 | // ~~ Generated by projen. To modify, edit .projenrc.js 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 | ".projenrc.js", 30 | "src/**/*.ts", 31 | "test/**/*.ts" 32 | ], 33 | "exclude": [ 34 | "node_modules" 35 | ] 36 | } 37 | --------------------------------------------------------------------------------