├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE │ ├── Bug_report.md │ ├── Feature_request.md │ └── config.yml ├── PULL_REQUEST_TEMPLATE.md ├── dependabot.yml ├── renovate.json └── workflows │ ├── main-build.yml │ ├── pr-build.yml │ └── release-build.yml ├── .gitignore ├── .vscode ├── extensions.json ├── launch.json └── tasks.json ├── .whitesource ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── RELEASE-PROCESS.md ├── deploy └── deploy-scaledobject.yaml ├── images ├── architecture.png └── architecture.pptx └── src ├── Keda.CosmosDb.Scaler.sln ├── Scaler.Demo ├── OrderGenerator │ ├── Dockerfile │ ├── Keda.CosmosDb.Scaler.Demo.OrderGenerator.csproj │ ├── Program.cs │ └── appsettings.json ├── OrderProcessor │ ├── Dockerfile │ ├── Keda.CosmosDb.Scaler.Demo.OrderProcessor.csproj │ ├── Program.cs │ ├── Properties │ │ └── launchSettings.json │ ├── Worker.cs │ ├── appsettings.json │ ├── deploy-scaledobject.yaml │ └── deploy.yaml ├── README.md └── Shared │ ├── CosmosDbConfig.cs │ ├── Keda.CosmosDb.Scaler.Demo.Shared.csproj │ └── OrderContract.cs ├── Scaler.Tests ├── CosmosDbScalerServiceTests.cs └── Keda.CosmosDb.Scaler.Tests.csproj └── Scaler ├── Dockerfile ├── Keda.CosmosDb.Scaler.csproj ├── Program.cs ├── Properties └── launchSettings.json ├── Protos └── externalscaler.proto ├── Services ├── CosmosDbFactory.cs ├── CosmosDbMetricProvider.cs ├── CosmosDbScalerService.cs ├── ICosmosDbMetricProvider.cs └── ScalerMetadata.cs ├── Startup.cs ├── appsettings.json └── deploy.yaml /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # These owners will be the default owners for everything in 2 | # the repo. Unless a later match takes precedence 3 | * @JatinSanghvi @tomkerkhove 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/Bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Report a bug 3 | about: Create a report to help us improve 4 | labels: bug 5 | --- 6 | 7 | A clear and concise description of what the bug is. 8 | 9 | ## Expected Behavior 10 | 11 | 12 | ## Actual Behavior 13 | 14 | 15 | ## Steps to Reproduce the Problem 16 | 17 | 1. 18 | 2. 19 | 3. 20 | 21 | ## Specifications 22 | 23 | - **KEDA Version:** *Please elaborate* 24 | - **Platform & Version:** *Please elaborate* 25 | - **Kubernetes Version:** *Please elaborate* 26 | - **Scaler(s):** *Please elaborate* 27 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/Feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | labels: needs-discussion,feature-request 5 | --- 6 | 7 | A clear and concise description of what you want to happen. 8 | 9 | ### Use-Case 10 | 11 | Tell us more what you'd like to achieve 12 | 13 | ### Specification 14 | 15 | - [ ] Demand #1 16 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Ask a question or get support 4 | url: https://github.com/kedacore/external-scaler-azure-cosmos-db/discussions/new 5 | about: Ask a question or request support for using KEDA 6 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | _Provide a description of what has been changed_ 8 | 9 | ### Checklist 10 | 11 | - [ ] Commits are signed with Developer Certificate of Origin (DCO) 12 | - [ ] A PR is opened to update the documentation on [our docs repo](https://github.com/kedacore/keda-docs) 13 | 14 | Fixes # 15 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: github-actions 4 | directory: "/" 5 | schedule: 6 | interval: weekly 7 | open-pull-requests-limit: 10 8 | labels: 9 | - enhancement 10 | - dependency-management 11 | - package-ecosystem: gomod 12 | directory: "/" 13 | schedule: 14 | interval: weekly 15 | open-pull-requests-limit: 10 16 | labels: 17 | - enhancement 18 | - dependency-management 19 | - package-ecosystem: docker 20 | directory: "/" 21 | schedule: 22 | interval: weekly 23 | open-pull-requests-limit: 10 24 | labels: 25 | - enhancement 26 | - dependency-management 27 | -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:base", 5 | ":gitSignOff", 6 | "schedule:monthly", 7 | ":disableMajorUpdates", 8 | ":disablePrControls", 9 | ":pinDigestsDisabled" 10 | ], 11 | "packageRules": [ 12 | { 13 | "groupName": ".NET Core Docker containers", 14 | "matchDatasources": [ 15 | "docker" 16 | ], 17 | "matchPackagePrefixes": [ 18 | "mcr.microsoft.com/dotnet/" 19 | ] 20 | }, 21 | { 22 | "groupName": "NuGet packages", 23 | "matchDatasources": [ 24 | "nuget" 25 | ] 26 | } 27 | ], 28 | "assignees": [ 29 | "@JatinSanghvi" 30 | ], 31 | "separateMajorMinor": false 32 | } 33 | -------------------------------------------------------------------------------- /.github/workflows/main-build.yml: -------------------------------------------------------------------------------- 1 | name: Main branch build 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | env: 9 | VERSION_TAG: experimental 10 | 11 | jobs: 12 | build: 13 | name: Build 14 | runs-on: ubuntu-latest 15 | permissions: 16 | contents: read 17 | packages: write 18 | 19 | steps: 20 | - name: Checkout repository 21 | uses: actions/checkout@v4 22 | 23 | - name: Setup dotnet environment 24 | uses: actions/setup-dotnet@v4 25 | with: 26 | dotnet-version: 8 27 | 28 | - name: Install dependencies 29 | run: dotnet restore src 30 | 31 | - name: Build solution 32 | run: dotnet build src --configuration Release --no-restore 33 | 34 | - name: Run unit tests 35 | run: dotnet test src --no-restore 36 | 37 | - name: Build Docker image for order-generator 38 | uses: docker/build-push-action@v6 39 | with: 40 | file: ./src/Scaler.Demo/OrderGenerator/Dockerfile 41 | tags: cosmosdb-order-generator:${{ env.VERSION_TAG }} 42 | 43 | - name: Build Docker image for order-processor 44 | uses: docker/build-push-action@v6 45 | with: 46 | file: ./src/Scaler.Demo/OrderProcessor/Dockerfile 47 | tags: cosmosdb-order-processor:${{ env.VERSION_TAG }} 48 | 49 | - name: Login to GitHub container registry 50 | uses: docker/login-action@v3 51 | with: 52 | registry: ghcr.io 53 | username: ${{ github.actor }} 54 | password: ${{ secrets.GITHUB_TOKEN }} 55 | 56 | - name: Extract Docker metadata 57 | id: meta 58 | uses: docker/metadata-action@v5 59 | with: 60 | images: ghcr.io/${{ github.repository_owner }}/external-scaler-azure-cosmos-db 61 | flavor: | 62 | latest=false 63 | tags: | 64 | type=raw,value=${{ env.VERSION_TAG }} 65 | 66 | # Using metadata ensures all lower-case tag-name even if the GitHub username has upper-case letters. 67 | - name: Build and push Docker image for scaler 68 | uses: docker/build-push-action@v6 69 | with: 70 | file: ./src/Scaler/Dockerfile 71 | tags: ${{ steps.meta.outputs.tags }} 72 | labels: ${{ steps.meta.outputs.labels }} 73 | push: true 74 | -------------------------------------------------------------------------------- /.github/workflows/pr-build.yml: -------------------------------------------------------------------------------- 1 | name: Pull request build 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - main 7 | 8 | env: 9 | VERSION_TAG: experimental 10 | 11 | jobs: 12 | build: 13 | name: Build 14 | runs-on: ubuntu-latest 15 | permissions: 16 | contents: read 17 | packages: write 18 | 19 | steps: 20 | - name: Checkout repository 21 | uses: actions/checkout@v4 22 | 23 | - name: Setup dotnet environment 24 | uses: actions/setup-dotnet@v4 25 | with: 26 | dotnet-version: 8 27 | 28 | - name: Install dependencies 29 | run: dotnet restore src 30 | 31 | - name: Build solution 32 | run: dotnet build src --configuration Release --no-restore 33 | 34 | - name: Run unit tests 35 | run: dotnet test src --no-restore 36 | 37 | - name: Build Docker image for order-generator 38 | uses: docker/build-push-action@v6 39 | with: 40 | file: ./src/Scaler.Demo/OrderGenerator/Dockerfile 41 | tags: cosmosdb-order-generator:${{ env.VERSION_TAG }} 42 | 43 | - name: Build Docker image for order-processor 44 | uses: docker/build-push-action@v6 45 | with: 46 | file: ./src/Scaler.Demo/OrderProcessor/Dockerfile 47 | tags: cosmosdb-order-processor:${{ env.VERSION_TAG }} 48 | 49 | - name: Build Docker image for scaler 50 | uses: docker/build-push-action@v6 51 | with: 52 | file: ./src/Scaler/Dockerfile 53 | tags: cosmosdb-scaler:${{ env.VERSION_TAG }} 54 | -------------------------------------------------------------------------------- /.github/workflows/release-build.yml: -------------------------------------------------------------------------------- 1 | name: Release build 2 | 3 | on: 4 | push: 5 | tags: 6 | - v*.*.* 7 | env: 8 | VERSION_TAG: experimental 9 | 10 | jobs: 11 | build: 12 | name: Build 13 | runs-on: ubuntu-latest 14 | permissions: 15 | contents: read 16 | packages: write 17 | 18 | steps: 19 | - name: Checkout repository 20 | uses: actions/checkout@v4 21 | 22 | - name: Setup dotnet environment 23 | uses: actions/setup-dotnet@v4 24 | with: 25 | dotnet-version: 8 26 | 27 | - name: Install dependencies 28 | run: dotnet restore src 29 | 30 | - name: Build solution 31 | run: dotnet build src --configuration Release --no-restore 32 | 33 | - name: Run unit tests 34 | run: dotnet test src --no-restore 35 | 36 | - name: Build Docker image for order-generator 37 | uses: docker/build-push-action@v6 38 | with: 39 | file: ./src/Scaler.Demo/OrderGenerator/Dockerfile 40 | tags: cosmosdb-order-generator:${{ env.VERSION_TAG }} 41 | 42 | - name: Build Docker image for order-processor 43 | uses: docker/build-push-action@v6 44 | with: 45 | file: ./src/Scaler.Demo/OrderProcessor/Dockerfile 46 | tags: cosmosdb-order-processor:${{ env.VERSION_TAG }} 47 | 48 | - name: Login to GitHub container registry 49 | uses: docker/login-action@v3 50 | with: 51 | registry: ghcr.io 52 | username: ${{ github.actor }} 53 | password: ${{ secrets.GITHUB_TOKEN }} 54 | 55 | - name: Extract Docker metadata 56 | id: meta 57 | uses: docker/metadata-action@v5 58 | with: 59 | images: ghcr.io/${{ github.repository_owner }}/external-scaler-azure-cosmos-db 60 | flavor: | 61 | latest=false 62 | tags: | 63 | type=semver,pattern={{version}} 64 | 65 | # Using metadata ensures all lower-case tag-name even if the GitHub username has upper-case letters. 66 | - name: Build push Docker image for scaler 67 | uses: docker/build-push-action@v6 68 | with: 69 | file: ./src/Scaler/Dockerfile 70 | tags: ${{ steps.meta.outputs.tags }} 71 | labels: ${{ steps.meta.outputs.labels }} 72 | push: true 73 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Ww][Ii][Nn]32/ 27 | [Aa][Rr][Mm]/ 28 | [Aa][Rr][Mm]64/ 29 | bld/ 30 | [Bb]in/ 31 | [Oo]bj/ 32 | [Ll]og/ 33 | [Ll]ogs/ 34 | 35 | # Visual Studio 2015/2017 cache/options directory 36 | .vs/ 37 | # Uncomment if you have tasks that create the project's static files in wwwroot 38 | #wwwroot/ 39 | 40 | # Visual Studio 2017 auto generated files 41 | Generated\ Files/ 42 | 43 | # MSTest test Results 44 | [Tt]est[Rr]esult*/ 45 | [Bb]uild[Ll]og.* 46 | 47 | # NUnit 48 | *.VisualState.xml 49 | TestResult.xml 50 | nunit-*.xml 51 | 52 | # Build Results of an ATL Project 53 | [Dd]ebugPS/ 54 | [Rr]eleasePS/ 55 | dlldata.c 56 | 57 | # Benchmark Results 58 | BenchmarkDotNet.Artifacts/ 59 | 60 | # .NET Core 61 | project.lock.json 62 | project.fragment.lock.json 63 | artifacts/ 64 | 65 | # Tye 66 | .tye/ 67 | 68 | # ASP.NET Scaffolding 69 | ScaffoldingReadMe.txt 70 | 71 | # StyleCop 72 | StyleCopReport.xml 73 | 74 | # Files built by Visual Studio 75 | *_i.c 76 | *_p.c 77 | *_h.h 78 | *.ilk 79 | *.meta 80 | *.obj 81 | *.iobj 82 | *.pch 83 | *.pdb 84 | *.ipdb 85 | *.pgc 86 | *.pgd 87 | *.rsp 88 | *.sbr 89 | *.tlb 90 | *.tli 91 | *.tlh 92 | *.tmp 93 | *.tmp_proj 94 | *_wpftmp.csproj 95 | *.log 96 | *.vspscc 97 | *.vssscc 98 | .builds 99 | *.pidb 100 | *.svclog 101 | *.scc 102 | 103 | # Chutzpah Test files 104 | _Chutzpah* 105 | 106 | # Visual C++ cache files 107 | ipch/ 108 | *.aps 109 | *.ncb 110 | *.opendb 111 | *.opensdf 112 | *.sdf 113 | *.cachefile 114 | *.VC.db 115 | *.VC.VC.opendb 116 | 117 | # Visual Studio profiler 118 | *.psess 119 | *.vsp 120 | *.vspx 121 | *.sap 122 | 123 | # Visual Studio Trace Files 124 | *.e2e 125 | 126 | # TFS 2012 Local Workspace 127 | $tf/ 128 | 129 | # Guidance Automation Toolkit 130 | *.gpState 131 | 132 | # ReSharper is a .NET coding add-in 133 | _ReSharper*/ 134 | *.[Rr]e[Ss]harper 135 | *.DotSettings.user 136 | 137 | # TeamCity is a build add-in 138 | _TeamCity* 139 | 140 | # DotCover is a Code Coverage Tool 141 | *.dotCover 142 | 143 | # AxoCover is a Code Coverage Tool 144 | .axoCover/* 145 | !.axoCover/settings.json 146 | 147 | # Coverlet is a free, cross platform Code Coverage Tool 148 | coverage*.json 149 | coverage*.xml 150 | coverage*.info 151 | 152 | # Visual Studio code coverage results 153 | *.coverage 154 | *.coveragexml 155 | 156 | # NCrunch 157 | _NCrunch_* 158 | .*crunch*.local.xml 159 | nCrunchTemp_* 160 | 161 | # MightyMoose 162 | *.mm.* 163 | AutoTest.Net/ 164 | 165 | # Web workbench (sass) 166 | .sass-cache/ 167 | 168 | # Installshield output folder 169 | [Ee]xpress/ 170 | 171 | # DocProject is a documentation generator add-in 172 | DocProject/buildhelp/ 173 | DocProject/Help/*.HxT 174 | DocProject/Help/*.HxC 175 | DocProject/Help/*.hhc 176 | DocProject/Help/*.hhk 177 | DocProject/Help/*.hhp 178 | DocProject/Help/Html2 179 | DocProject/Help/html 180 | 181 | # Click-Once directory 182 | publish/ 183 | 184 | # Publish Web Output 185 | *.[Pp]ublish.xml 186 | *.azurePubxml 187 | # Note: Comment the next line if you want to checkin your web deploy settings, 188 | # but database connection strings (with potential passwords) will be unencrypted 189 | *.pubxml 190 | *.publishproj 191 | 192 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 193 | # checkin your Azure Web App publish settings, but sensitive information contained 194 | # in these scripts will be unencrypted 195 | PublishScripts/ 196 | 197 | # NuGet Packages 198 | *.nupkg 199 | # NuGet Symbol Packages 200 | *.snupkg 201 | # The packages folder can be ignored because of Package Restore 202 | **/[Pp]ackages/* 203 | # except build/, which is used as an MSBuild target. 204 | !**/[Pp]ackages/build/ 205 | # Uncomment if necessary however generally it will be regenerated when needed 206 | #!**/[Pp]ackages/repositories.config 207 | # NuGet v3's project.json files produces more ignorable files 208 | *.nuget.props 209 | *.nuget.targets 210 | 211 | # Microsoft Azure Build Output 212 | csx/ 213 | *.build.csdef 214 | 215 | # Microsoft Azure Emulator 216 | ecf/ 217 | rcf/ 218 | 219 | # Windows Store app package directories and files 220 | AppPackages/ 221 | BundleArtifacts/ 222 | Package.StoreAssociation.xml 223 | _pkginfo.txt 224 | *.appx 225 | *.appxbundle 226 | *.appxupload 227 | 228 | # Visual Studio cache files 229 | # files ending in .cache can be ignored 230 | *.[Cc]ache 231 | # but keep track of directories ending in .cache 232 | !?*.[Cc]ache/ 233 | 234 | # Others 235 | ClientBin/ 236 | ~$* 237 | *~ 238 | *.dbmdl 239 | *.dbproj.schemaview 240 | *.jfm 241 | *.pfx 242 | *.publishsettings 243 | orleans.codegen.cs 244 | 245 | # Including strong name files can present a security risk 246 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 247 | #*.snk 248 | 249 | # Since there are multiple workflows, uncomment next line to ignore bower_components 250 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 251 | #bower_components/ 252 | 253 | # RIA/Silverlight projects 254 | Generated_Code/ 255 | 256 | # Backup & report files from converting an old project file 257 | # to a newer Visual Studio version. Backup files are not needed, 258 | # because we have git ;-) 259 | _UpgradeReport_Files/ 260 | Backup*/ 261 | UpgradeLog*.XML 262 | UpgradeLog*.htm 263 | ServiceFabricBackup/ 264 | *.rptproj.bak 265 | 266 | # SQL Server files 267 | *.mdf 268 | *.ldf 269 | *.ndf 270 | 271 | # Business Intelligence projects 272 | *.rdl.data 273 | *.bim.layout 274 | *.bim_*.settings 275 | *.rptproj.rsuser 276 | *- [Bb]ackup.rdl 277 | *- [Bb]ackup ([0-9]).rdl 278 | *- [Bb]ackup ([0-9][0-9]).rdl 279 | 280 | # Microsoft Fakes 281 | FakesAssemblies/ 282 | 283 | # GhostDoc plugin setting file 284 | *.GhostDoc.xml 285 | 286 | # Node.js Tools for Visual Studio 287 | .ntvs_analysis.dat 288 | node_modules/ 289 | 290 | # Visual Studio 6 build log 291 | *.plg 292 | 293 | # Visual Studio 6 workspace options file 294 | *.opt 295 | 296 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 297 | *.vbw 298 | 299 | # Visual Studio LightSwitch build output 300 | **/*.HTMLClient/GeneratedArtifacts 301 | **/*.DesktopClient/GeneratedArtifacts 302 | **/*.DesktopClient/ModelManifest.xml 303 | **/*.Server/GeneratedArtifacts 304 | **/*.Server/ModelManifest.xml 305 | _Pvt_Extensions 306 | 307 | # Paket dependency manager 308 | .paket/paket.exe 309 | paket-files/ 310 | 311 | # FAKE - F# Make 312 | .fake/ 313 | 314 | # CodeRush personal settings 315 | .cr/personal 316 | 317 | # Python Tools for Visual Studio (PTVS) 318 | __pycache__/ 319 | *.pyc 320 | 321 | # Cake - Uncomment if you are using it 322 | # tools/** 323 | # !tools/packages.config 324 | 325 | # Tabs Studio 326 | *.tss 327 | 328 | # Telerik's JustMock configuration file 329 | *.jmconfig 330 | 331 | # BizTalk build output 332 | *.btp.cs 333 | *.btm.cs 334 | *.odx.cs 335 | *.xsd.cs 336 | 337 | # OpenCover UI analysis results 338 | OpenCover/ 339 | 340 | # Azure Stream Analytics local run output 341 | ASALocalRun/ 342 | 343 | # MSBuild Binary and Structured Log 344 | *.binlog 345 | 346 | # NVidia Nsight GPU debugger configuration file 347 | *.nvuser 348 | 349 | # MFractors (Xamarin productivity tool) working folder 350 | .mfractor/ 351 | 352 | # Local History for Visual Studio 353 | .localhistory/ 354 | 355 | # BeatPulse healthcheck temp database 356 | healthchecksdb 357 | 358 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 359 | MigrationBackup/ 360 | 361 | # Ionide (cross platform F# VS Code tools) working folder 362 | .ionide/ 363 | 364 | # Fody - auto-generated XML schema 365 | FodyWeavers.xsd 366 | 367 | ## 368 | ## Visual studio for Mac 369 | ## 370 | 371 | 372 | # globs 373 | Makefile.in 374 | *.userprefs 375 | *.usertasks 376 | config.make 377 | config.status 378 | aclocal.m4 379 | install-sh 380 | autom4te.cache/ 381 | *.tar.gz 382 | tarballs/ 383 | test-results/ 384 | 385 | # Mac bundle stuff 386 | *.dmg 387 | *.app 388 | 389 | # content below from: https://github.com/github/gitignore/blob/master/Global/macOS.gitignore 390 | # General 391 | .DS_Store 392 | .AppleDouble 393 | .LSOverride 394 | 395 | # Icon must end with two \r 396 | Icon 397 | 398 | 399 | # Thumbnails 400 | ._* 401 | 402 | # Files that might appear in the root of a volume 403 | .DocumentRevisions-V100 404 | .fseventsd 405 | .Spotlight-V100 406 | .TemporaryItems 407 | .Trashes 408 | .VolumeIcon.icns 409 | .com.apple.timemachine.donotpresent 410 | 411 | # Directories potentially created on remote AFP share 412 | .AppleDB 413 | .AppleDesktop 414 | Network Trash Folder 415 | Temporary Items 416 | .apdisk 417 | 418 | # content below from: https://github.com/github/gitignore/blob/master/Global/Windows.gitignore 419 | # Windows thumbnail cache files 420 | Thumbs.db 421 | ehthumbs.db 422 | ehthumbs_vista.db 423 | 424 | # Dump file 425 | *.stackdump 426 | 427 | # Folder config file 428 | [Dd]esktop.ini 429 | 430 | # Recycle Bin used on file shares 431 | $RECYCLE.BIN/ 432 | 433 | # Windows Installer files 434 | *.cab 435 | *.msi 436 | *.msix 437 | *.msm 438 | *.msp 439 | 440 | # Windows shortcuts 441 | *.lnk 442 | 443 | # JetBrains Rider 444 | .idea/ 445 | *.sln.iml 446 | 447 | ## 448 | ## Visual Studio Code 449 | ## 450 | .vscode/* 451 | !.vscode/settings.json 452 | !.vscode/tasks.json 453 | !.vscode/launch.json 454 | !.vscode/extensions.json 455 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "ms-dotnettools.csharp" 4 | ] 5 | } -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | // Use IntelliSense to find out which attributes exist for C# debugging 6 | // Use hover for the description of the existing attributes 7 | // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md 8 | "name": ".NET Core Launch (web)", 9 | "type": "coreclr", 10 | "request": "launch", 11 | "preLaunchTask": "build", 12 | // If you have changed target frameworks, make sure to update the program path. 13 | "program": "${workspaceFolder}/src/Scaler/bin/Debug/net8.0/Keda.CosmosDb.Scaler.dll", 14 | "args": [], 15 | "cwd": "${workspaceFolder}/src/Scaler", 16 | "stopAtEntry": false, 17 | // Enable launching a web browser when ASP.NET Core starts. For more information: https://aka.ms/VSCode-CS-LaunchJson-WebBrowser 18 | "serverReadyAction": { 19 | "action": "openExternally", 20 | "pattern": "\\bNow listening on:\\s+(https?://\\S+)" 21 | }, 22 | "env": { 23 | "ASPNETCORE_ENVIRONMENT": "Development" 24 | }, 25 | "sourceFileMap": { 26 | "/Views": "${workspaceFolder}/Views" 27 | } 28 | }, 29 | { 30 | "name": ".NET Core Attach", 31 | "type": "coreclr", 32 | "request": "attach" 33 | } 34 | ] 35 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "build", 6 | "command": "dotnet", 7 | "type": "process", 8 | "args": [ 9 | "build", 10 | "/property:GenerateFullPaths=true", 11 | "/consoleloggerparameters:NoSummary" 12 | ], 13 | "group": { 14 | "kind": "build", 15 | "isDefault": true 16 | }, 17 | "problemMatcher": "$msCompile" 18 | }, 19 | { 20 | "label": "test", 21 | "command": "dotnet", 22 | "type": "process", 23 | "args": [ 24 | "test", 25 | "/property:GenerateFullPaths=true", 26 | "/consoleloggerparameters:NoSummary" 27 | ], 28 | "group": { 29 | "kind": "test", 30 | "isDefault": true 31 | }, 32 | "problemMatcher": "$msCompile" 33 | } 34 | ] 35 | } -------------------------------------------------------------------------------- /.whitesource: -------------------------------------------------------------------------------- 1 | { 2 | "scanSettings": { 3 | "baseBranches": [] 4 | }, 5 | "checkRunSettings": { 6 | "vulnerableCheckRunConclusionLevel": "failure", 7 | "displayMode": "diff" 8 | }, 9 | "issueSettings": { 10 | "minSeverityLevel": "LOW" 11 | } 12 | } -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to KEDA 2 | 3 | Thanks for helping make KEDA better 😍. 4 | 5 | There are many areas we can use contributions - ranging from code, documentation, feature proposals, issue triage, samples, and content creation. 6 | 7 | 8 | 9 | **Table of contents** 10 | 11 | - [Project governance](#project-governance) 12 | - [Including Documentation Changes](#including-documentation-changes) 13 | - [Developer Certificate of Origin: Signing your work](#developer-certificate-of-origin-signing-your-work) 14 | - [Every commit needs to be signed](#every-commit-needs-to-be-signed) 15 | - [I didn't sign my commit, now what?!](#i-didnt-sign-my-commit-now-what) 16 | 17 | 18 | 19 | ## Project governance 20 | 21 | You can learn about the governance of KEDA [here](https://github.com/kedacore/governance). 22 | 23 | ## Including Documentation Changes 24 | 25 | For any contribution you make that impacts the behavior or experience of KEDA, please open a corresponding docs request for [keda.sh](https://keda.sh) through [https://github.com/kedacore/keda-docs](https://github.com/kedacore/keda-docs). Contributions that do not include documentation or samples will be rejected. 26 | 27 | ## Developer Certificate of Origin: Signing your work 28 | 29 | ### Every commit needs to be signed 30 | 31 | The Developer Certificate of Origin (DCO) is a lightweight way for contributors to certify that they wrote or otherwise have the right to submit the code they are contributing to the project. Here is the full text of the DCO, reformatted for readability: 32 | ``` 33 | By making a contribution to this project, I certify that: 34 | 35 | (a) The contribution was created in whole or in part by me and I have the right to submit it under the open source license indicated in the file; or 36 | 37 | (b) The contribution is based upon previous work that, to the best of my knowledge, is covered under an appropriate open source license and I have the right under that license to submit that work with modifications, whether created in whole or in part by me, under the same open source license (unless I am permitted to submit under a different license), as indicated in the file; or 38 | 39 | (c) The contribution was provided directly to me by some other person who certified (a), (b) or (c) and I have not modified it. 40 | 41 | (d) I understand and agree that this project and the contribution are public and that a record of the contribution (including all personal information I submit with it, including my sign-off) is maintained indefinitely and may be redistributed consistent with this project or the open source license(s) involved. 42 | ``` 43 | 44 | Contributors sign-off that they adhere to these requirements by adding a `Signed-off-by` line to commit messages. 45 | 46 | ``` 47 | This is my commit message 48 | 49 | Signed-off-by: Random J Developer 50 | ``` 51 | Git even has a `-s` command line option to append this automatically to your commit message: 52 | ``` 53 | $ git commit -s -m 'This is my commit message' 54 | ``` 55 | 56 | Each Pull Request is checked whether or not commits in a Pull Request do contain a valid Signed-off-by line. 57 | 58 | ### I didn't sign my commit, now what?! 59 | 60 | No worries - You can easily replay your changes, sign them and force push them! 61 | 62 | ``` 63 | git checkout 64 | git reset $(git merge-base main ) 65 | git add -A 66 | git commit -sm "one commit on " 67 | git push --force 68 | ``` 69 | -------------------------------------------------------------------------------- /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 2020 The KEDA Authors. 191 | 192 | and others that have contributed code to the public domain. 193 | 194 | Licensed under the Apache License, Version 2.0 (the "License"); 195 | you may not use this file except in compliance with the License. 196 | You may obtain a copy of the License at 197 | 198 | http://www.apache.org/licenses/LICENSE-2.0 199 | 200 | Unless required by applicable law or agreed to in writing, software 201 | distributed under the License is distributed on an "AS IS" BASIS, 202 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 203 | See the License for the specific language governing permissions and 204 | limitations under the License. 205 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # KEDA External Scaler for Azure Cosmos DB 2 | 3 | Event-based autoscaler for your [Azure Cosmos DB](https://azure.microsoft.com/services/cosmos-db/) change feed consumer applications running inside Kubernetes cluster. 4 | 5 | [![Build Status](https://github.com/kedacore/external-scaler-azure-cosmos-db/actions/workflows/main-build.yml/badge.svg?branch=main)](https://github.com/kedacore/external-scaler-azure-cosmos-db/actions?query=workflow%3A"Main+branch+build") 6 | 7 | ## Architecture 8 | 9 | Following diagram shows the different components that are involved for achieving the application scaling, and the relationships between these components. 10 | 11 | ![Scenario](images/architecture.png) 12 | 13 | - **Monitored Container** - The Azure Cosmos DB container that the application needs to monitor for new changes. A Cosmos DB container might contain several logical partitions based on the presence of distinct values of partition keys. Different logical partitions will be grouped under the same **Partition Range** if they are stored on the same physical partition. For more information, please read the documentation on [partitioning overview](https://docs.microsoft.com/azure/cosmos-db/partitioning-overview). In general, for containers that do not contain large amount of data, the count of physical partitions does not exceed 1. 14 | 15 | - **Lease Container** - Another Azure Cosmos DB container that keeps track of changes happening on the monitored container. It stores the list of changes in the **Change Feed**. The [change feed design pattern](https://docs.microsoft.com/azure/cosmos-db/sql/change-feed-design-patterns) supports multiple parallel listeners by keeping independent feeds for each partition range. The listener application instances acquire leases on these individual feeds before processing them. This ensures that a change is not processed by multiple applications. You may have both monitored and lease containers in the same Cosmos DB account, but they can also be situated in different accounts. 16 | 17 | - **KEDA** - KEDA runs as a separate service in Kubernetes cluster. It enables auto-scaling of applications based on internal and more primarily, external events. Check [KEDA documentation](https://keda.sh/docs/concepts/) to learn more. 18 | 19 | - **External Scaler** - While KEDA ships with a set of [built-in scalers](https://keda.sh/docs/scalers/), it also allows users to extend KEDA through support for [external scalers](https://keda.sh/docs/scalers/external/). In this scheme, KEDA will query user's GRPC service to fetch metrics of an event source and will scale the applications accordingly. This is where 'KEDA external scaler for Azure Cosmos DB' plugs itself in. For information on how an external scaler can be implemented, check [KEDA external scaler concept](https://keda.sh/docs/concepts/external-scalers/). 20 | 21 | - **Listener Application(s)** - This represents the application `Deployment` or `StatefulSet` that you would like to scale in and out using KEDA and the external scaler. For information on how to setup the change feed processor in your application that processes changes in Cosmos DB container, read documentation on [change feed processing](https://docs.microsoft.com/azure/cosmos-db/sql/change-feed-processor). 22 | 23 | - **`ScaledObject` Spec** - The specification contains information about the scale target (i.e. the application `Deployment` that needs to be scaled) and the trigger metadata. The external scaler fetches information about the Cosmos DB lease container from the trigger metadata defined in the `ScaledObject` resource. 24 | 25 | The external scaler calls Cosmos DB APIs to estimate the amount of changes pending to be processed. More specifically, the scaler counts the number of partition ranges that have changes remaining to be processed, and requests KEDA to scale the application to that amount. 26 | 27 | > **Note:** The architectural diagram above shows KEDA, external scaler and the target application in different Kubernetes namespaces. This is possible but not necessary. It is a requirement though that the `ScaledObject` and the application `Deployment` reside in the same namespace. 28 | 29 | ## Setup Instructions 30 | 31 | > :warning: **Caution:** The [Java SDK v2](https://github.com/Azure/azure-cosmosdb-java) client library uses a different naming convention for lease documents inside the lease container. This makes it incompatible with [.NET SDK v3](https://github.com/Azure/azure-cosmos-dotnet-v3), the one that the external scaler depends on to estimate the pending changes on change feeds. Hence, if you have a Java-based target consumer application, your change feeds would be having lease documents with incompatible IDs, and the external scaler would be unable to detect any pending change remaining to be consumed. Consequently, it will scale down your application to `minReplicaCount` if defined in the `ScaledObject` or to zero instances. 32 | 33 | ### Deploy KEDA and External Scaler 34 | 35 | 1. Add and update Helm chart repo. 36 | 37 | ```shell 38 | helm repo add kedacore https://kedacore.github.io/charts 39 | helm repo update 40 | ``` 41 | 42 | 1. Install KEDA Helm chart (*or follow one of the other installation methods on [KEDA documentation](https://keda.sh/docs/deploy)*). 43 | 44 | ```shell 45 | helm install keda kedacore/keda --namespace keda --create-namespace 46 | ``` 47 | 48 | 1. Install Azure Cosmos DB external scaler Helm chart. 49 | 50 | ```shell 51 | helm install external-scaler-azure-cosmos-db kedacore/external-scaler-azure-cosmos-db --namespace keda --create-namespace 52 | ``` 53 | 54 | ### Create `ScaledObject` Resource 55 | 56 | Create `ScaledObject` resource that contains the information about your application (the scale target), the external scaler service, Cosmos DB containers, and other scaling configuration values. Check [`ScaledObject` specification](https://keda.sh/docs/concepts/scaling-deployments/) and [`External` trigger specification](https://keda.sh/docs/scalers/external/) for information on different properties supported for `ScaledObject` and their allowed values. 57 | 58 | You can use file `deploy/deploy-scaledobject.yaml` as a template for creating the `ScaledObject`. The trigger metadata properties required to use the external scaler for Cosmos DB are described in [Trigger Specification](#trigger-specification) section below. 59 | 60 | > **Note:** If you are having trouble setting up the external scaler or the listener application, the step-by-step instructions for [deploying the sample application](./src/Scaler.Demo/README.md) might help. 61 | 62 | ## Trigger Specification 63 | 64 | The specification below describes the `trigger` metadata in `ScaledObject` resource for using 'KEDA external scaler for Cosmos DB' to scale your application. 65 | 66 | ```yaml 67 | triggers: 68 | - type: external 69 | metadata: 70 | scalerAddress: external-scaler-azure-cosmos-db.keda:4050 # Mandatory. Address of the external scaler service. 71 | connectionFromEnv: # Mandatory. Environment variable for the connection string of Cosmos DB account with monitored container. 72 | databaseId: # Mandatory. ID of Cosmos DB database containing monitored container. 73 | containerId: # Mandatory. ID of monitored container. 74 | leaseConnectionFromEnv: # Mandatory. Environment variable for the connection string of Cosmos DB account with lease container. 75 | leaseDatabaseId: # Mandatory. ID of Cosmos DB database containing lease container. 76 | leaseContainerId: # Mandatory. ID of lease container. 77 | processorName: # Mandatory. Name of change-feed processor used by listener application. 78 | ``` 79 | 80 | ### Parameter List 81 | 82 | - **`scalerAddress`** - Address of the external scaler service. This would be in format `.:`. If you installed Azure Cosmos DB external scaler Helm chart in `keda` namespace and did not specify custom values, the metadata value would be `external-scaler-azure-cosmos-db.keda:4050`. 83 | 84 | - **`connectionFromEnv`** - Name of the environment variable on the scale target to read the connection string of the Cosmos DB account that contains the monitored container. 85 | 86 | - **`databaseId`** - ID of Cosmos DB database that contains the monitored container. 87 | 88 | - **`containerId`** - ID of the monitored container. 89 | 90 | - **`leaseConnectionFromEnv`** - Name of the environment variable on the scale target to read the connection string of the Cosmos DB account that contains the lease container. This can be same or different from the value of `connection` metadata. 91 | 92 | - **`leaseDatabaseId`** - ID of Cosmos DB database that contains the lease container. This can be same or different from the value of `databaseId` metadata. 93 | 94 | - **`leaseContainerId`** - ID of the lease container containing the change feeds. 95 | 96 | - **`processorName`** - Name of change-feed processor used by listener application. For more information on this, you can refer to [Implementing the change feed processor](https://docs.microsoft.com/azure/cosmos-db/sql/change-feed-processor#implementing-the-change-feed-processor) section. 97 | 98 | > **Note** Ideally, we would have created `TriggerAuthentication` resource that would have prevented us from adding the connection strings in plain text in the `ScaledObject` trigger metadata. However, this is not possible since at the moment, the triggers of `external` type do not support referencing a `TriggerAuthentication` resource ([link](https://keda.sh/docs/scalers/external/#authentication-parameters)). -------------------------------------------------------------------------------- /RELEASE-PROCESS.md: -------------------------------------------------------------------------------- 1 | # Release Process 2 | 3 | The release process of a new version of KEDA external scaler for Azure Cosmos DB involves the following: 4 | 5 | ## 1. Choose release title and tag name 6 | 7 | Check the latest release on [Releases](https://github.com/kedacore/external-scaler-azure-cosmos-db/releases) page. Follow the [Semantic Versioning](https://semver.org/) guidelines for naming the new release. For instance, suppose that the latest release is **v1.2.3**, and that the next release is going to be a MINOR update, then its title would be **v1.3.0** and the associated tag name would be `v1.3.0`. 8 | 9 | ## 2. Create GitHub release 10 | 11 | Open [New release](https://github.com/kedacore/external-scaler-azure-cosmos-db/releases/new) page. Use the values for **Tag** and **Release title** as picked above. Include information on the changes that would get shipped with the new release inside **Release description** section. This includes new features, patches, breaking changes and deprecations. You can get list of all commits since the last release with a link similar to . Do not include information that is not important to users. As an example, a change to issue template is not important. 12 | 13 | Publish the release. This will trigger GitHub action to create a new Docker image from the tagged commit. After the action is executed, you should find a new Docker image with new tag published on the [Packages](https://github.com/orgs/kedacore/packages?repo_name=external-scaler-azure-cosmos-db) page. 14 | 15 | ## 3. Update Helm Chart 16 | 17 | Once the GitHub release is created, update the Helm Chart for the external scaler in [kedacore/charts](https://github.com/kedacore/charts/tree/master/external-scaler-azure-cosmos-db) repository. Depending on the changes that went in the release, this might just take updating the `version` and `appVersion` in [Chart.yaml](https://github.com/kedacore/charts/tree/master/external-scaler-azure-cosmos-db/Chart.yaml) file, or in some cases, might involve adding and updating multiple template files. 18 | 19 | Follow the [Contributing](https://github.com/kedacore/charts/blob/master/CONTRIBUTING.md) guide to create a pull request. After the pull request is completed, create a GitHub release in the repository following the same guide. 20 | 21 | ## 4. Add package version on Artifact Hub 22 | 23 | GitHub repository [kedacore/external-scalers](https://github.com/kedacore/external-scalers) showcases the KEDA official external scalers on [Artifact Hub](https://artifacthub.io/packages/search?repo=keda-official-external-scalers) which includes the external scaler for Azure Cosmos DB. For information on why the files in the repository are laid out in a certain way, you can refer to the [KEDA scalers repositories guide](https://artifacthub.io/docs/topics/repositories/#keda-scalers-repositories). The guide also contains link to spec for `artifacthub-pkg.yml` which you may find helpful. 24 | 25 | You will need to add a new package version for the external scaler. For this: 26 | 27 | 1. Copy file `artifacthub-pkg.yml` from directory `artifacthub/azure-cosmos-db/` to `artifacthub/azure-cosmos-db/`. Update value of `version` property in the copied file. Make other edits as required. 28 | 1. Create a fresh `README.md` inside `artifacthub/azure-cosmos-db/` with information on changes introduced by the new release. You can re-use the release description for this one. 29 | 1. Create pull request with these changes. 30 | -------------------------------------------------------------------------------- /deploy/deploy-scaledobject.yaml: -------------------------------------------------------------------------------- 1 | # Template scaled-object for using KEDA external scaler for Azure Cosmos DB. 2 | 3 | apiVersion: keda.sh/v1alpha1 4 | kind: ScaledObject 5 | metadata: 6 | name: 7 | namespace: default 8 | spec: 9 | pollingInterval: 20 10 | scaleTargetRef: 11 | name: 12 | triggers: 13 | - type: external 14 | metadata: 15 | scalerAddress: external-scaler-azure-cosmos-db.keda:4050 16 | connectionFromEnv: 17 | databaseId: 18 | containerId: 19 | leaseConnectionFromEnv: 20 | leaseDatabaseId: 21 | leaseContainerId: 22 | processorName: 23 | -------------------------------------------------------------------------------- /images/architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kedacore/external-scaler-azure-cosmos-db/bac322ba76a5f0bad785f5678789157790712be0/images/architecture.png -------------------------------------------------------------------------------- /images/architecture.pptx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kedacore/external-scaler-azure-cosmos-db/bac322ba76a5f0bad785f5678789157790712be0/images/architecture.pptx -------------------------------------------------------------------------------- /src/Keda.CosmosDb.Scaler.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.2.32505.173 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Keda.CosmosDb.Scaler", "Scaler\Keda.CosmosDb.Scaler.csproj", "{BF3566AE-346E-4769-BEED-6F08A433A3E8}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Keda.CosmosDb.Scaler.Tests", "Scaler.Tests\Keda.CosmosDb.Scaler.Tests.csproj", "{58416233-F4D2-446D-9891-E753DFC5E748}" 9 | EndProject 10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Keda.CosmosDb.Scaler.Demo.OrderProcessor", "Scaler.Demo\OrderProcessor\Keda.CosmosDb.Scaler.Demo.OrderProcessor.csproj", "{4B598340-B1E2-49E4-B1C4-5026EA26A731}" 11 | EndProject 12 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Keda.CosmosDb.Scaler.Demo.Shared", "Scaler.Demo\Shared\Keda.CosmosDb.Scaler.Demo.Shared.csproj", "{8FC14DFF-B6D4-4EC3-88AE-3B3B4304CB65}" 13 | EndProject 14 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Keda.CosmosDb.Scaler.Demo.OrderGenerator", "Scaler.Demo\OrderGenerator\Keda.CosmosDb.Scaler.Demo.OrderGenerator.csproj", "{D469E3DD-887B-4288-87F4-51B03AE64B0A}" 15 | EndProject 16 | Global 17 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 18 | Debug|Any CPU = Debug|Any CPU 19 | Debug|x64 = Debug|x64 20 | Debug|x86 = Debug|x86 21 | Release|Any CPU = Release|Any CPU 22 | Release|x64 = Release|x64 23 | Release|x86 = Release|x86 24 | EndGlobalSection 25 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 26 | {BF3566AE-346E-4769-BEED-6F08A433A3E8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 27 | {BF3566AE-346E-4769-BEED-6F08A433A3E8}.Debug|Any CPU.Build.0 = Debug|Any CPU 28 | {BF3566AE-346E-4769-BEED-6F08A433A3E8}.Debug|x64.ActiveCfg = Debug|Any CPU 29 | {BF3566AE-346E-4769-BEED-6F08A433A3E8}.Debug|x64.Build.0 = Debug|Any CPU 30 | {BF3566AE-346E-4769-BEED-6F08A433A3E8}.Debug|x86.ActiveCfg = Debug|Any CPU 31 | {BF3566AE-346E-4769-BEED-6F08A433A3E8}.Debug|x86.Build.0 = Debug|Any CPU 32 | {BF3566AE-346E-4769-BEED-6F08A433A3E8}.Release|Any CPU.ActiveCfg = Release|Any CPU 33 | {BF3566AE-346E-4769-BEED-6F08A433A3E8}.Release|Any CPU.Build.0 = Release|Any CPU 34 | {BF3566AE-346E-4769-BEED-6F08A433A3E8}.Release|x64.ActiveCfg = Release|Any CPU 35 | {BF3566AE-346E-4769-BEED-6F08A433A3E8}.Release|x64.Build.0 = Release|Any CPU 36 | {BF3566AE-346E-4769-BEED-6F08A433A3E8}.Release|x86.ActiveCfg = Release|Any CPU 37 | {BF3566AE-346E-4769-BEED-6F08A433A3E8}.Release|x86.Build.0 = Release|Any CPU 38 | {58416233-F4D2-446D-9891-E753DFC5E748}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 39 | {58416233-F4D2-446D-9891-E753DFC5E748}.Debug|Any CPU.Build.0 = Debug|Any CPU 40 | {58416233-F4D2-446D-9891-E753DFC5E748}.Debug|x64.ActiveCfg = Debug|Any CPU 41 | {58416233-F4D2-446D-9891-E753DFC5E748}.Debug|x64.Build.0 = Debug|Any CPU 42 | {58416233-F4D2-446D-9891-E753DFC5E748}.Debug|x86.ActiveCfg = Debug|Any CPU 43 | {58416233-F4D2-446D-9891-E753DFC5E748}.Debug|x86.Build.0 = Debug|Any CPU 44 | {58416233-F4D2-446D-9891-E753DFC5E748}.Release|Any CPU.ActiveCfg = Release|Any CPU 45 | {58416233-F4D2-446D-9891-E753DFC5E748}.Release|Any CPU.Build.0 = Release|Any CPU 46 | {58416233-F4D2-446D-9891-E753DFC5E748}.Release|x64.ActiveCfg = Release|Any CPU 47 | {58416233-F4D2-446D-9891-E753DFC5E748}.Release|x64.Build.0 = Release|Any CPU 48 | {58416233-F4D2-446D-9891-E753DFC5E748}.Release|x86.ActiveCfg = Release|Any CPU 49 | {58416233-F4D2-446D-9891-E753DFC5E748}.Release|x86.Build.0 = Release|Any CPU 50 | {4B598340-B1E2-49E4-B1C4-5026EA26A731}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 51 | {4B598340-B1E2-49E4-B1C4-5026EA26A731}.Debug|Any CPU.Build.0 = Debug|Any CPU 52 | {4B598340-B1E2-49E4-B1C4-5026EA26A731}.Debug|x64.ActiveCfg = Debug|Any CPU 53 | {4B598340-B1E2-49E4-B1C4-5026EA26A731}.Debug|x64.Build.0 = Debug|Any CPU 54 | {4B598340-B1E2-49E4-B1C4-5026EA26A731}.Debug|x86.ActiveCfg = Debug|Any CPU 55 | {4B598340-B1E2-49E4-B1C4-5026EA26A731}.Debug|x86.Build.0 = Debug|Any CPU 56 | {4B598340-B1E2-49E4-B1C4-5026EA26A731}.Release|Any CPU.ActiveCfg = Release|Any CPU 57 | {4B598340-B1E2-49E4-B1C4-5026EA26A731}.Release|Any CPU.Build.0 = Release|Any CPU 58 | {4B598340-B1E2-49E4-B1C4-5026EA26A731}.Release|x64.ActiveCfg = Release|Any CPU 59 | {4B598340-B1E2-49E4-B1C4-5026EA26A731}.Release|x64.Build.0 = Release|Any CPU 60 | {4B598340-B1E2-49E4-B1C4-5026EA26A731}.Release|x86.ActiveCfg = Release|Any CPU 61 | {4B598340-B1E2-49E4-B1C4-5026EA26A731}.Release|x86.Build.0 = Release|Any CPU 62 | {8FC14DFF-B6D4-4EC3-88AE-3B3B4304CB65}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 63 | {8FC14DFF-B6D4-4EC3-88AE-3B3B4304CB65}.Debug|Any CPU.Build.0 = Debug|Any CPU 64 | {8FC14DFF-B6D4-4EC3-88AE-3B3B4304CB65}.Debug|x64.ActiveCfg = Debug|Any CPU 65 | {8FC14DFF-B6D4-4EC3-88AE-3B3B4304CB65}.Debug|x64.Build.0 = Debug|Any CPU 66 | {8FC14DFF-B6D4-4EC3-88AE-3B3B4304CB65}.Debug|x86.ActiveCfg = Debug|Any CPU 67 | {8FC14DFF-B6D4-4EC3-88AE-3B3B4304CB65}.Debug|x86.Build.0 = Debug|Any CPU 68 | {8FC14DFF-B6D4-4EC3-88AE-3B3B4304CB65}.Release|Any CPU.ActiveCfg = Release|Any CPU 69 | {8FC14DFF-B6D4-4EC3-88AE-3B3B4304CB65}.Release|Any CPU.Build.0 = Release|Any CPU 70 | {8FC14DFF-B6D4-4EC3-88AE-3B3B4304CB65}.Release|x64.ActiveCfg = Release|Any CPU 71 | {8FC14DFF-B6D4-4EC3-88AE-3B3B4304CB65}.Release|x64.Build.0 = Release|Any CPU 72 | {8FC14DFF-B6D4-4EC3-88AE-3B3B4304CB65}.Release|x86.ActiveCfg = Release|Any CPU 73 | {8FC14DFF-B6D4-4EC3-88AE-3B3B4304CB65}.Release|x86.Build.0 = Release|Any CPU 74 | {D469E3DD-887B-4288-87F4-51B03AE64B0A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 75 | {D469E3DD-887B-4288-87F4-51B03AE64B0A}.Debug|Any CPU.Build.0 = Debug|Any CPU 76 | {D469E3DD-887B-4288-87F4-51B03AE64B0A}.Debug|x64.ActiveCfg = Debug|Any CPU 77 | {D469E3DD-887B-4288-87F4-51B03AE64B0A}.Debug|x64.Build.0 = Debug|Any CPU 78 | {D469E3DD-887B-4288-87F4-51B03AE64B0A}.Debug|x86.ActiveCfg = Debug|Any CPU 79 | {D469E3DD-887B-4288-87F4-51B03AE64B0A}.Debug|x86.Build.0 = Debug|Any CPU 80 | {D469E3DD-887B-4288-87F4-51B03AE64B0A}.Release|Any CPU.ActiveCfg = Release|Any CPU 81 | {D469E3DD-887B-4288-87F4-51B03AE64B0A}.Release|Any CPU.Build.0 = Release|Any CPU 82 | {D469E3DD-887B-4288-87F4-51B03AE64B0A}.Release|x64.ActiveCfg = Release|Any CPU 83 | {D469E3DD-887B-4288-87F4-51B03AE64B0A}.Release|x64.Build.0 = Release|Any CPU 84 | {D469E3DD-887B-4288-87F4-51B03AE64B0A}.Release|x86.ActiveCfg = Release|Any CPU 85 | {D469E3DD-887B-4288-87F4-51B03AE64B0A}.Release|x86.Build.0 = Release|Any CPU 86 | EndGlobalSection 87 | GlobalSection(SolutionProperties) = preSolution 88 | HideSolutionNode = FALSE 89 | EndGlobalSection 90 | GlobalSection(ExtensibilityGlobals) = postSolution 91 | SolutionGuid = {AD5F4826-691F-4681-B4B4-1F541B173B46} 92 | EndGlobalSection 93 | EndGlobal 94 | -------------------------------------------------------------------------------- /src/Scaler.Demo/OrderGenerator/Dockerfile: -------------------------------------------------------------------------------- 1 | # https://hub.docker.com/_/microsoft-dotnet 2 | 3 | # Restore, build and publish project. 4 | FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build 5 | WORKDIR / 6 | COPY src/Scaler.Demo/OrderGenerator/ src/Scaler.Demo/OrderGenerator/ 7 | COPY src/Scaler.Demo/Shared/ src/Scaler.Demo/Shared/ 8 | 9 | WORKDIR /src/Scaler.Demo/OrderGenerator 10 | RUN dotnet publish --configuration Release --output /app 11 | 12 | # Stage application. 13 | FROM mcr.microsoft.com/dotnet/runtime:8.0 14 | WORKDIR /app 15 | COPY --from=build /app . 16 | ENTRYPOINT ["dotnet", "Keda.CosmosDb.Scaler.Demo.OrderGenerator.dll"] 17 | -------------------------------------------------------------------------------- /src/Scaler.Demo/OrderGenerator/Keda.CosmosDb.Scaler.Demo.OrderGenerator.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net8.0 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/Scaler.Demo/OrderGenerator/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Threading.Tasks; 4 | using Bogus; 5 | using Bogus.DataSets; 6 | using Keda.CosmosDb.Scaler.Demo.Shared; 7 | using Microsoft.Azure.Cosmos; 8 | using Microsoft.Extensions.Hosting; 9 | using Database = Microsoft.Azure.Cosmos.Database; 10 | 11 | namespace Keda.CosmosDb.Scaler.Demo.OrderGenerator 12 | { 13 | internal static class Program 14 | { 15 | private static CosmosDbConfig _cosmosDbConfig; 16 | 17 | public static async Task Main(string[] args) 18 | { 19 | if (args.Length != 1 || !new[] { "generate", "setup", "teardown" }.Contains(args[0])) 20 | { 21 | Console.WriteLine(); 22 | Console.WriteLine("Please use one of the following verbs with the command:"); 23 | Console.WriteLine(" generate : Add new orders to the order-container"); 24 | Console.WriteLine(" setup : Create Cosmos database and order-container"); 25 | Console.WriteLine(" teardown : Delete Cosmos database and containers inside"); 26 | Console.WriteLine(); 27 | return; 28 | } 29 | 30 | // _cosmosDbConfig should be initialized once the host is built. 31 | Host.CreateDefaultBuilder(args) 32 | .ConfigureAppConfiguration(builder => _cosmosDbConfig = CosmosDbConfig.Create(builder.Build())) 33 | .Build(); 34 | 35 | switch (args[0]) 36 | { 37 | case "generate": await GenerateAsync(); break; 38 | case "setup": await SetupAsync(); break; 39 | case "teardown": await TeardownAsync(); break; 40 | } 41 | } 42 | 43 | private static async Task GenerateAsync() 44 | { 45 | int count = ReadOrderCount(); 46 | bool isSingleArticle = ReadIsSingleArticle(); 47 | await CreateOrdersAsync(count, isSingleArticle); 48 | } 49 | 50 | private static int ReadOrderCount() 51 | { 52 | while (true) 53 | { 54 | Console.Write("Let's queue some orders, how many do you want? "); 55 | 56 | if (int.TryParse(Console.ReadLine(), out int count) && count >= 1 && count <= 10000) 57 | { 58 | return count; 59 | } 60 | 61 | Console.WriteLine("That's not a valid amount. Please enter a number between 1 and 10000."); 62 | } 63 | } 64 | 65 | private static bool ReadIsSingleArticle() 66 | { 67 | bool? isSingleArticle = null; 68 | 69 | while (isSingleArticle == null) 70 | { 71 | Console.Write("Do you want to limit orders to single article (to put them in a single partition)? (Y/N) "); 72 | 73 | isSingleArticle = Console.ReadKey().Key switch 74 | { 75 | ConsoleKey.Y => true, 76 | ConsoleKey.N => false, 77 | _ => null, 78 | }; 79 | 80 | Console.WriteLine(); 81 | } 82 | 83 | return isSingleArticle.Value; 84 | } 85 | 86 | private static async Task CreateOrdersAsync(int count, bool isSingleArticle) 87 | { 88 | Container container = new CosmosClient(_cosmosDbConfig.Connection) 89 | .GetContainer(_cosmosDbConfig.DatabaseId, _cosmosDbConfig.ContainerId); 90 | 91 | int remainingCount = count; 92 | string article = isSingleArticle ? new Commerce().Product() : null; 93 | 94 | while (remainingCount > 0) 95 | { 96 | // Do not push all orders together as that may cause requests to get throttled. 97 | int newCount = Math.Min(remainingCount, 20); 98 | 99 | Task[] createOrderTasks = Enumerable.Range(0, newCount) 100 | .Select(_ => CreateOrderAsync(container, article)) 101 | .ToArray(); 102 | 103 | await Task.WhenAll(createOrderTasks); 104 | await Task.Delay(TimeSpan.FromSeconds(2)); 105 | 106 | remainingCount -= newCount; 107 | } 108 | 109 | Console.WriteLine("That's it, see you later!"); 110 | } 111 | 112 | private static async Task CreateOrderAsync(Container container, string article) 113 | { 114 | Customer customer = new Faker() 115 | .RuleFor(customer => customer.FirstName, faker => faker.Name.FirstName()) 116 | .RuleFor(customer => customer.LastName, faker => faker.Name.LastName()); 117 | 118 | Order order = new Faker() 119 | .RuleFor(order => order.Customer, () => customer) 120 | .RuleFor(order => order.Amount, faker => faker.Random.Number(1, 10)) 121 | .RuleFor(order => order.Article, faker => article ?? faker.Commerce.Product()); 122 | 123 | Console.WriteLine($"Creating order {order.Id} - {order.Amount} unit(s) of {order.Article} for {order.Customer.FirstName} {order.Customer.LastName}"); 124 | await container.CreateItemAsync(order); 125 | } 126 | 127 | private static async Task SetupAsync() 128 | { 129 | Console.WriteLine($"Creating database: {_cosmosDbConfig.DatabaseId}"); 130 | 131 | Database database = await new CosmosClient(_cosmosDbConfig.Connection) 132 | .CreateDatabaseIfNotExistsAsync(_cosmosDbConfig.DatabaseId); 133 | 134 | Console.WriteLine($"Creating container: {_cosmosDbConfig.ContainerId} with throughput: {_cosmosDbConfig.ContainerThroughput} RU/s"); 135 | 136 | await database.CreateContainerIfNotExistsAsync( 137 | new ContainerProperties(_cosmosDbConfig.ContainerId, partitionKeyPath: $"/{nameof(Order.Article)}"), 138 | throughput: _cosmosDbConfig.ContainerThroughput); 139 | 140 | Console.WriteLine("Done!"); 141 | } 142 | 143 | private static async Task TeardownAsync() 144 | { 145 | var client = new CosmosClient(_cosmosDbConfig.Connection); 146 | 147 | try 148 | { 149 | Console.WriteLine($"Deleting database: {_cosmosDbConfig.DatabaseId}"); 150 | await client.GetDatabase(_cosmosDbConfig.DatabaseId).DeleteAsync(); 151 | } 152 | catch (CosmosException) 153 | { 154 | Console.WriteLine("Database does not exist"); 155 | } 156 | 157 | Console.WriteLine("Done!"); 158 | } 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /src/Scaler.Demo/OrderGenerator/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "CosmosDbConfig": { 3 | "Connection": "", 4 | "DatabaseId": "StoreDatabase", 5 | "ContainerId": "OrderContainer", 6 | "ContainerThroughput": 11000 7 | } 8 | } -------------------------------------------------------------------------------- /src/Scaler.Demo/OrderProcessor/Dockerfile: -------------------------------------------------------------------------------- 1 | # https://hub.docker.com/_/microsoft-dotnet 2 | 3 | # Restore, build and publish project. 4 | FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build 5 | WORKDIR / 6 | COPY src/Scaler.Demo/OrderProcessor/ src/Scaler.Demo/OrderProcessor/ 7 | COPY src/Scaler.Demo/Shared/ src/Scaler.Demo/Shared/ 8 | 9 | WORKDIR /src/Scaler.Demo/OrderProcessor 10 | RUN dotnet publish --configuration Release --output /app 11 | 12 | # Stage application. 13 | FROM mcr.microsoft.com/dotnet/runtime:8.0 14 | WORKDIR /app 15 | COPY --from=build /app . 16 | ENTRYPOINT ["dotnet", "Keda.CosmosDb.Scaler.Demo.OrderProcessor.dll"] 17 | -------------------------------------------------------------------------------- /src/Scaler.Demo/OrderProcessor/Keda.CosmosDb.Scaler.Demo.OrderProcessor.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/Scaler.Demo/OrderProcessor/Program.cs: -------------------------------------------------------------------------------- 1 | using Keda.CosmosDb.Scaler.Demo.Shared; 2 | using Microsoft.Extensions.DependencyInjection; 3 | using Microsoft.Extensions.Hosting; 4 | using Microsoft.Extensions.Logging; 5 | 6 | namespace Keda.CosmosDb.Scaler.Demo.OrderProcessor 7 | { 8 | internal static class Program 9 | { 10 | public static void Main(string[] args) 11 | { 12 | CreateHostBuilder(args).Build().Run(); 13 | } 14 | 15 | public static IHostBuilder CreateHostBuilder(string[] args) => 16 | Host.CreateDefaultBuilder(args) 17 | .ConfigureLogging(builder => builder.AddSimpleConsole(options => options.TimestampFormat = "yyyy-MM-dd HH:mm:ss ")) 18 | .ConfigureServices((hostContext, services) => 19 | { 20 | services.AddHostedService(); 21 | services.AddSingleton(CosmosDbConfig.Create(hostContext.Configuration)); 22 | }); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Scaler.Demo/OrderProcessor/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "Keda.CosmosDb.Scaler.Demo.OrderProcessor": { 4 | "commandName": "Project", 5 | "environmentVariables": { 6 | "DOTNET_ENVIRONMENT": "Development" 7 | } 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/Scaler.Demo/OrderProcessor/Worker.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Net; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | using Keda.CosmosDb.Scaler.Demo.Shared; 7 | using Microsoft.Azure.Cosmos; 8 | using Microsoft.Extensions.Hosting; 9 | using Microsoft.Extensions.Logging; 10 | 11 | namespace Keda.CosmosDb.Scaler.Demo.OrderProcessor 12 | { 13 | internal sealed class Worker : BackgroundService 14 | { 15 | private readonly CosmosDbConfig _cosmosDbConfig; 16 | private readonly ILogger _logger; 17 | 18 | private ChangeFeedProcessor _processor; 19 | 20 | public Worker(CosmosDbConfig cosmosDbConfig, ILogger logger) 21 | { 22 | _cosmosDbConfig = cosmosDbConfig ?? throw new ArgumentNullException(nameof(cosmosDbConfig)); 23 | _logger = logger ?? throw new ArgumentNullException(nameof(logger)); 24 | } 25 | 26 | public override async Task StartAsync(CancellationToken cancellationToken) 27 | { 28 | Database leaseDatabase = await new CosmosClient(_cosmosDbConfig.LeaseConnection) 29 | .CreateDatabaseIfNotExistsAsync(_cosmosDbConfig.LeaseDatabaseId, cancellationToken: cancellationToken); 30 | 31 | Container leaseContainer = await leaseDatabase 32 | .CreateContainerIfNotExistsAsync( 33 | new ContainerProperties(_cosmosDbConfig.LeaseContainerId, partitionKeyPath: "/id"), 34 | throughput: 400, 35 | cancellationToken: cancellationToken); 36 | 37 | // Change feed processor instance name should be unique for each container application. 38 | string instanceName = $"Instance-{Dns.GetHostName()}"; 39 | 40 | _processor = new CosmosClient(_cosmosDbConfig.Connection) 41 | .GetContainer(_cosmosDbConfig.DatabaseId, _cosmosDbConfig.ContainerId) 42 | .GetChangeFeedProcessorBuilder(_cosmosDbConfig.ProcessorName, ProcessOrdersAsync) 43 | .WithInstanceName(instanceName) 44 | .WithLeaseContainer(leaseContainer) 45 | .Build(); 46 | 47 | await _processor.StartAsync(); 48 | _logger.LogInformation($"Started change feed processor instance {instanceName}"); 49 | } 50 | 51 | public override async Task StopAsync(CancellationToken cancellationToken) 52 | { 53 | await _processor.StopAsync(); 54 | _logger.LogInformation("Stopped change feed processor"); 55 | 56 | await base.StopAsync(cancellationToken); 57 | } 58 | 59 | protected override Task ExecuteAsync(CancellationToken stoppingToken) 60 | { 61 | return Task.CompletedTask; 62 | } 63 | 64 | private async Task ProcessOrdersAsync(IReadOnlyCollection orders, CancellationToken cancellationToken) 65 | { 66 | _logger.LogInformation($"{orders.Count} order(s) received"); 67 | 68 | foreach (Order order in orders) 69 | { 70 | _logger.LogInformation($"Processing order {order.Id} - {order.Amount} unit(s) of {order.Article} bought by {order.Customer.FirstName} {order.Customer.LastName}"); 71 | 72 | // Add delay to fake the time consumed in processing the order. 73 | await Task.Delay(TimeSpan.FromSeconds(2), cancellationToken); 74 | _logger.LogInformation($"Order {order.Id} processed"); 75 | } 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/Scaler.Demo/OrderProcessor/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | }, 9 | "CosmosDbConfig": { 10 | "Connection": "", 11 | "DatabaseId": "StoreDatabase", 12 | "ContainerId": "OrderContainer", 13 | "LeaseConnection": "", 14 | "LeaseDatabaseId": "StoreDatabase", 15 | "LeaseContainerId": "OrderProcessorLeases", 16 | "ProcessorName": "OrderProcessor" 17 | } 18 | } -------------------------------------------------------------------------------- /src/Scaler.Demo/OrderProcessor/deploy-scaledobject.yaml: -------------------------------------------------------------------------------- 1 | # Create KEDA scaled object to scale order processor application. 2 | 3 | apiVersion: keda.sh/v1alpha1 4 | kind: ScaledObject 5 | metadata: 6 | name: cosmosdb-order-processor-scaledobject 7 | namespace: default 8 | spec: 9 | minReplicaCount: 0 10 | maxReplicaCount: 10 11 | pollingInterval: 20 12 | scaleTargetRef: 13 | name: cosmosdb-order-processor 14 | triggers: 15 | - type: external 16 | metadata: 17 | scalerAddress: cosmosdb-scaler.default:4050 18 | connectionFromEnv: CosmosDbConfig__Connection 19 | databaseId: StoreDatabase 20 | containerId: OrderContainer 21 | leaseConnectionFromEnv: CosmosDbConfig__LeaseConnection 22 | leaseDatabaseId: StoreDatabase 23 | leaseContainerId: OrderProcessorLeases 24 | processorName: OrderProcessor 25 | -------------------------------------------------------------------------------- /src/Scaler.Demo/OrderProcessor/deploy.yaml: -------------------------------------------------------------------------------- 1 | # Deploy order processor application. 2 | 3 | apiVersion: apps/v1 4 | kind: Deployment 5 | metadata: 6 | name: cosmosdb-order-processor 7 | namespace: default 8 | spec: 9 | replicas: 1 # A replica is required to be up momentarily to initialize the change-feed. 10 | selector: 11 | matchLabels: 12 | app: cosmosdb-order-processor 13 | template: 14 | metadata: 15 | labels: 16 | app: cosmosdb-order-processor 17 | spec: 18 | containers: 19 | - name: cosmosdb-order-processor 20 | image: /cosmosdb-order-processor:latest 21 | imagePullPolicy: Always 22 | env: 23 | - name: CosmosDbConfig__Connection 24 | value: 25 | - name: CosmosDbConfig__LeaseConnection 26 | value: 27 | -------------------------------------------------------------------------------- /src/Scaler.Demo/README.md: -------------------------------------------------------------------------------- 1 | # KEDA External Scaler for Azure Cosmos DB 2 | 3 | This directory contains the source code for two sample projects to demonstrate the functionality of the external scaler. 4 | 5 | - `Keda.CosmosDb.Scaler.Demo.OrderGenerator` - used to push fake purchase orders to a test Cosmos DB container. 6 | - `Keda.CosmosDb.Scaler.Demo.OrderProcessor` - used to read these orders from the container and process them. 7 | 8 | We will later deploy the order-processor application to Kubernetes cluster and use KEDA along with the external scaler from `Keda.CosmosDb.Scaler` project to scale the application. 9 | 10 | ## Prerequisites 11 | 12 | - [Azure Cosmos DB account](https://azure.microsoft.com/free/cosmos-db/) 13 | - [Docker Hub account](https://hub.docker.com/signup) 14 | - Kubernetes cluster 15 | - [Docker Desktop](https://www.docker.com/products/docker-desktop) 16 | 17 | ## Testing sample application locally on Docker 18 | 19 | 1. Open command prompt or shell and change to the root directory of the cloned repo. 20 | 21 | 1. Run the below commands to build the Docker container images for order-generator and order-processor applications. 22 | 23 | ```text 24 | # docker build --file .\src\Scaler.Demo\OrderGenerator\Dockerfile --force-rm --tag cosmosdb-order-generator . 25 | # docker build --file .\src\Scaler.Demo\OrderProcessor\Dockerfile --force-rm --tag cosmosdb-order-processor . 26 | ``` 27 | 28 | 1. Create test-database and test-container within the database in Cosmos DB account by running the order-generator application inside the container with `setup` option. Make sure to put the connection string of Cosmos DB account in the command below. 29 | 30 | ```text 31 | # docker run --env CosmosDbConfig__Connection="" --interactive --rm --tty cosmosdb-order-generator setup 32 | ``` 33 | 34 | > **Caution** The default application settings provision a throughput of 11,000 RU/s to the test Cosmos DB container. This sets the number of its [physical partitions](https://docs.microsoft.com/azure/cosmos-db/partitioning-overview#physical-partitions) to 2. If you have a free Azure account which offers limited throughput, or if you want to limit the cost of running the sample, be sure to update the throughput to 400 RU/s by setting the value of property `CosmosDb:ConnectionThroughput` to `400` in file `src/Scaler.Demo/OrderGenerator/appsettings.json` before building the container image. That would still allow testing of KEDA scaling between 0 and 1 instances but not upto 2 instances. Also, at any point, you can run order-generator with `teardown` option to delete the database and Cosmos DB container inside. 35 | 36 | 1. Start a second shell instance and run the order-processor application in a new container. You can put the same connection string in both places in the command below. Note that the sample applications are written to handle different Cosmos DB accounts for monitored and lease containers but having two different accounts is not a requirement. 37 | 38 | ```text 39 | # docker run --env CosmosDbConfig__Connection="" --env CosmosDbConfig__LeaseConnection="" --interactive --rm --tty cosmosdb-order-processor 40 | ``` 41 | 42 | The order-processor application will create lease database and container if they do not exist. The default application settings would share the same database between the monitored and lease containers. The order-processor application will then activate a change-feed processor to monitor and process new changes in the monitored container. 43 | 44 | 1. Keep the order-processor application running. Go back to the first shell instance and run order-generator application with `generate` option to add fake orders to the test Cosmos DB container. 45 | 46 | ```text 47 | # docker run --env CosmosDbConfig__Connection="" --interactive --tty --rm cosmosdb-order-generator generate 48 | Let's queue some orders, how many do you want? 3 49 | Do you want to limit orders to single article (to put them in a single partition)? (Y/N) N 50 | Creating order 1f157b6e-c51f-4e02-a492-295e7fd47fbe - 3 unit(s) of Mouse for Cleta Padberg 51 | Creating order ffeb4822-43e6-4b80-a439-d2ad09a278c1 - 9 unit(s) of Soap for Uriel Cormier 52 | Creating order b0e396d7-e140-43f5-872c-f47df6bccf2e - 4 unit(s) of Chair for Granville Auer 53 | That's it, see you later! 54 | ``` 55 | 56 | 1. Go back to the second shell where the order-processor application is running. Check the console output and verify that the orders were processed. 57 | 58 | ```text 59 | 2021-09-03 06:52:34 info: Keda.CosmosDb.Scaler.Demo.OrderProcessor.Worker[0] 60 | Started change feed processor instance Instance-588a09449616 61 | 2021-09-03 06:52:34 info: Microsoft.Hosting.Lifetime[0] 62 | Application started. Press Ctrl+C to shut down. 63 | 2021-09-03 06:52:34 info: Microsoft.Hosting.Lifetime[0] 64 | Hosting environment: Production 65 | 2021-09-03 06:52:34 info: Microsoft.Hosting.Lifetime[0] 66 | Content root path: /app 67 | 2021-09-03 06:52:59 info: Keda.CosmosDb.Scaler.Demo.OrderProcessor.Worker[0] 68 | 1 order(s) received 69 | 2021-09-03 06:52:59 info: Keda.CosmosDb.Scaler.Demo.OrderProcessor.Worker[0] 70 | Processing order e5912b0a-939b-45bc-87de-9baa314ca65e - 9 unit(s) of Soap bought by Uriel Cormier 71 | 2021-09-03 06:53:00 info: Keda.CosmosDb.Scaler.Demo.OrderProcessor.Worker[0] 72 | 2 order(s) received 73 | 2021-09-03 06:53:00 info: Keda.CosmosDb.Scaler.Demo.OrderProcessor.Worker[0] 74 | Processing order 6daae206-027c-4055-8d39-97274de1ab59 - 3 unit(s) of Mouse bought by Cleta Padberg 75 | 2021-09-03 06:53:01 info: Keda.CosmosDb.Scaler.Demo.OrderProcessor.Worker[0] 76 | Order e5912b0a-939b-45bc-87de-9baa314ca65e processed 77 | 2021-09-03 06:53:02 info: Keda.CosmosDb.Scaler.Demo.OrderProcessor.Worker[0] 78 | Order 6daae206-027c-4055-8d39-97274de1ab59 processed 79 | 2021-09-03 06:53:02 info: Keda.CosmosDb.Scaler.Demo.OrderProcessor.Worker[0] 80 | Processing order 135e1fb9-807a-489e-90f0-0cb4c3768a36 - 4 unit(s) of Chair bought by Granville Auer 81 | 2021-09-03 06:53:04 info: Keda.CosmosDb.Scaler.Demo.OrderProcessor.Worker[0] 82 | Order 135e1fb9-807a-489e-90f0-0cb4c3768a36 processed 83 | ``` 84 | 85 | 1. Stop order-processor container from the first shell. 86 | 87 | ```text 88 | # docker container ls --no-trunc --format "{{.Image}} {{.Names}}" 89 | cosmosdb-order-processor jolly_wilbur 90 | # docker container stop jolly_wilbur 91 | jolly_wilbur 92 | ``` 93 | 94 | ## Deploying KEDA and external scaler to cluster 95 | 96 | 1. Follow one of the steps on [Deploying KEDA](https://keda.sh/docs/deploy/) documentation page to deploy KEDA on your Kubernetes cluster. 97 | 98 | 1. Open command prompt or shell and change to the root directory of the cloned repo. 99 | 100 | 1. Build container image for the external scaler and push the image to Docker Hub. Make sure to replace `` in below commands with your Docker ID. 101 | 102 | ```text 103 | # docker build --file .\src\Scaler\Dockerfile --force-rm --tag cosmosdb-scaler . 104 | # docker login --username 105 | # docker tag cosmosdb-scaler:latest /cosmosdb-scaler:latest 106 | # docker push /cosmosdb-scaler:latest 107 | ``` 108 | 109 | 1. Update your Docker ID in the image path in manifest file `src/Scaler/deploy.yaml` and apply it to deploy the external scaler application. 110 | 111 | ```text 112 | kubectl apply --filename=src/Scaler/deploy.yaml 113 | ``` 114 | 115 | ## Deploying sample application to cluster 116 | 117 | 1. Build the Docker container image for order-processor application (if you haven't already) and push the container image to Docker Hub. Make sure to replace `` in below commands with your Docker ID. 118 | 119 | ```text 120 | # docker build --file .\src\Scaler.Demo\OrderProcessor\Dockerfile --force-rm --tag cosmosdb-order-processor . 121 | # docker tag cosmosdb-order-processor:latest /cosmosdb-order-processor:latest 122 | # docker push /cosmosdb-order-processor:latest 123 | ``` 124 | 125 | 1. Update your Docker ID in the image path in manifest file `src/Scaler.Demo/OrderProcessor/deploy.yaml`. Also, update the values of connection strings to point to the test Cosmos DB account. Apply the manifest to deploy the order-processor application. 126 | 127 | ```text 128 | kubectl apply --filename=src/Scaler.Demo/OrderProcessor/deploy.yaml 129 | ``` 130 | 131 | 1. Ensure that the order-processor application is running correctly on the cluster by checking application logs. The application will create lease database and container if they do not exist, hence it is needed to run for a few seconds before we enable auto-scaling for it, as that would immediately bring replicas to 0 if there are no orders pending to be processed. 132 | 133 | ```text 134 | # kubectl get pods 135 | NAME READY STATUS RESTARTS AGE 136 | cosmosdb-order-processor-b59956989-bcbzg 1/1 Running 0 50s 137 | cosmosdb-scaler-64dd48678c-zjcgd 1/1 Running 0 23m 138 | # kubectl logs cosmosdb-order-processor-b59956989-bcbzg 139 | 2021-09-03 08:05:01 info: Keda.CosmosDb.Scaler.Demo.OrderProcessor.Worker[0] 140 | Started change feed processor instance Instance-cosmosdb-order-processor-b59956989-bcbzg 141 | ... 142 | ``` 143 | 144 | 1. Apply the manifest file for scaled object, `src/Scaler.Demo/OrderProcessor/deploy-scaledobject.yaml` 145 | 146 | ```text 147 | kubectl apply --filename=src/Scaler.Demo/OrderProcessor/deploy-scaledobject.yaml 148 | ``` 149 | 150 | > **Note** Ideally, we would have created `TriggerAuthentication` resource that would enable sharing of the connection strings as secrets between the scaled object and the target application. However, this is not possible since at the moment, the triggers of `external` type do not support referencing a `TriggerAuthentication` resource ([link](https://keda.sh/docs/scalers/external/#authentication-parameters)). 151 | 152 | ## Testing auto-scaling for sample application 153 | 154 | 1. Verify that there are no pods for order-processor running after the scaled object was created. 155 | 156 | ```text 157 | # kubectl get pods 158 | NAME READY STATUS RESTARTS AGE 159 | cosmosdb-scaler-64dd48678c-d6dqq 1/1 Running 0 10m 160 | ``` 161 | 162 | 1. Add new orders to the Cosmos DB container by running the order-generator. Select option to generate all orders for the same article. 163 | 164 | ```text 165 | # docker run --env CosmosDbConfig__Connection="" --interactive --tty --rm cosmosdb-order-generator generate 166 | Let's queue some orders, how many do you want? 150 167 | Do you want to limit orders to single article (to put them in a single partition)? (Y/N) Y 168 | ... 169 | ``` 170 | 171 | This would restrict the orders to a single logical partition (and a single physical partition thereof). The external scaler scales the targets according to the number of change feeds that have non-zero pending messages remaining to be processed. The total number of change feeds (with or without pending messages) equals the number of physical partitions in the Cosmos DB container. 172 | 173 | 1. Verify that only one pod is created for the order-processor. It may take a few seconds for the pod to show up. 174 | 175 | ```text 176 | # kubectl get pods 177 | NAME READY STATUS RESTARTS AGE 178 | cosmosdb-order-processor-b59956989-fscsb 1/1 Running 0 30s 179 | cosmosdb-scaler-64dd48678c-d6dqq 1/1 Running 0 20m 180 | ``` 181 | 182 | 1. Now, add more orders to the Cosmos DB container but this time, select option to generate orders of different articles. 183 | 184 | ```text 185 | # docker run --env CosmosDbConfig__Connection="" --interactive --tty --rm cosmosdb-order-generator generate 186 | Let's queue some orders, how many do you want? 250 187 | Do you want to limit orders to single article (to put them in a single partition)? (Y/N) N 188 | ... 189 | ``` 190 | 191 | 1. Verify that two pods are created for the order-processor. 192 | 193 | ```text 194 | NAME READY STATUS RESTARTS AGE 195 | cosmosdb-order-processor-b59956989-88dc5 1/1 Running 0 15s 196 | cosmosdb-order-processor-b59956989-dxp4b 1/1 Running 0 3s 197 | cosmosdb-scaler-64dd48678c-d6dqq 1/1 Running 0 35m 198 | ``` 199 | 200 | 1. You can also verify that both order-processor pods are able to share the processing of orders. 201 | 202 | ```text 203 | # kubectl logs cosmosdb-order-processor-b59956989-dxp4b --tail=4 204 | 2021-09-03 12:57:41 info: Keda.CosmosDb.Scaler.Demo.OrderProcessor.Worker[0] 205 | Order 5ba7f503-0185-49f6-9fce-3da999464049 processed 206 | 2021-09-03 12:57:41 info: Keda.CosmosDb.Scaler.Demo.OrderProcessor.Worker[0] 207 | Processing order ce1f05ad-08ff-4535-858f-3158de41971b - 8 unit(s) of Computer bought by Jaren Tremblay 208 | 209 | # kubectl logs cosmosdb-order-processor-b59956989-88dc5 --tail=4 210 | 2021-09-03 12:57:53 info: Keda.CosmosDb.Scaler.Demo.OrderProcessor.Worker[0] 211 | Order e881c998-1318-411e-8181-fa638335910e processed 212 | 2021-09-03 12:57:53 info: Keda.CosmosDb.Scaler.Demo.OrderProcessor.Worker[0] 213 | Processing order ca17597f-7aa2-4b04-abd8-724139b2c370 - 1 unit(s) of Gloves bought by Donny Shanahan 214 | ``` 215 | 216 | ## Cleaning sample application from cluster 217 | 218 | 1. Delete the scaled object and order-processor application. 219 | 220 | ```text 221 | # kubectl delete scaledobject cosmosdb-order-processor-scaledobject 222 | # kubectl delete deployment cosmosdb-order-processor 223 | ``` 224 | 225 | 1. Optionally, delete the external scaler and KEDA from cluster. The following commands assume that KEDA was installed with Helm. 226 | 227 | ```text 228 | # kubectl delete service cosmosdb-scaler 229 | # kubectl delete deployment cosmosdb-scaler 230 | # helm uninstall keda --namespace keda 231 | # kubectl delete namespace keda 232 | ``` 233 | 234 | 1. The monitored container can be deleted with the below command. The lease container can be deleted on Azure Portal. 235 | 236 | ```text 237 | # docker run --env CosmosDbConfig__Connection="" --interactive --rm --tty cosmosdb-order-generator teardown 238 | ``` 239 | -------------------------------------------------------------------------------- /src/Scaler.Demo/Shared/CosmosDbConfig.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Configuration; 2 | 3 | namespace Keda.CosmosDb.Scaler.Demo.Shared 4 | { 5 | public class CosmosDbConfig 6 | { 7 | public string Connection { get; set; } 8 | public string DatabaseId { get; set; } 9 | public string ContainerId { get; set; } 10 | public int ContainerThroughput { get; set; } 11 | public string LeaseConnection { get; set; } 12 | public string LeaseDatabaseId { get; set; } 13 | public string LeaseContainerId { get; set; } 14 | public string ProcessorName { get; set; } 15 | 16 | public static CosmosDbConfig Create(IConfiguration configuration) 17 | { 18 | return configuration.GetSection(nameof(CosmosDbConfig)).Get(); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Scaler.Demo/Shared/Keda.CosmosDb.Scaler.Demo.Shared.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/Scaler.Demo/Shared/OrderContract.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Newtonsoft.Json; 3 | 4 | namespace Keda.CosmosDb.Scaler.Demo.Shared 5 | { 6 | public class Order 7 | { 8 | [JsonProperty("id")] 9 | public string Id { get; } 10 | 11 | public int Amount { get; set; } 12 | public string Article { get; set; } 13 | public Customer Customer { get; set; } 14 | 15 | public Order() 16 | { 17 | this.Id = Guid.NewGuid().ToString(); 18 | } 19 | } 20 | 21 | public class Customer 22 | { 23 | public string FirstName { get; set; } 24 | public string LastName { get; set; } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Scaler.Tests/CosmosDbScalerServiceTests.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Google.Protobuf.Collections; 3 | using Moq; 4 | using Newtonsoft.Json; 5 | using Xunit; 6 | 7 | namespace Keda.CosmosDb.Scaler.Tests 8 | { 9 | public class CosmosDbScalerServiceTests 10 | { 11 | private readonly CosmosDbScalerService _cosmosDbScalerService; 12 | private readonly Mock _metricProviderMock; 13 | 14 | public CosmosDbScalerServiceTests() 15 | { 16 | _metricProviderMock = new Mock(); 17 | _cosmosDbScalerService = new CosmosDbScalerService(_metricProviderMock.Object); 18 | } 19 | 20 | [Theory] 21 | [InlineData("connectionFromEnv")] 22 | [InlineData("databaseId")] 23 | [InlineData("containerId")] 24 | [InlineData("leaseConnectionFromEnv")] 25 | [InlineData("leaseDatabaseId")] 26 | [InlineData("leaseContainerId")] 27 | [InlineData("processorName")] 28 | public async Task IsActive_ThrowsOnMissingMetadata(string metadataKey) 29 | { 30 | await Assert.ThrowsAsync( 31 | () => _cosmosDbScalerService.IsActive(GetScaledObjectRefWithoutMetadata(metadataKey), null)); 32 | } 33 | 34 | [Fact] 35 | public async Task IsActive_ReturnsFalseOnZeroPartitions() 36 | { 37 | _metricProviderMock.Setup(provider => provider.GetPartitionCountAsync(It.IsAny())).ReturnsAsync(0L); 38 | IsActiveResponse response = await _cosmosDbScalerService.IsActive(GetScaledObjectRef(), null); 39 | Assert.False(response.Result); 40 | } 41 | 42 | [Theory] 43 | [InlineData(1L)] 44 | [InlineData(100L)] 45 | public async Task IsActive_ReturnsFalseOnNonZeroPartitions(long partitionCount) 46 | { 47 | _metricProviderMock.Setup(provider => provider.GetPartitionCountAsync(It.IsAny())).ReturnsAsync(partitionCount); 48 | IsActiveResponse response = await _cosmosDbScalerService.IsActive(GetScaledObjectRef(), null); 49 | Assert.True(response.Result); 50 | } 51 | 52 | [Theory] 53 | [InlineData("connectionFromEnv")] 54 | [InlineData("databaseId")] 55 | [InlineData("containerId")] 56 | [InlineData("leaseConnectionFromEnv")] 57 | [InlineData("leaseDatabaseId")] 58 | [InlineData("leaseContainerId")] 59 | [InlineData("processorName")] 60 | public async Task GetMetrics_ThrowsOnMissingMetadata(string metadataKey) 61 | { 62 | await Assert.ThrowsAsync( 63 | () => _cosmosDbScalerService.GetMetrics(GetGetMetricsRequestWithoutMetadata(metadataKey), null)); 64 | } 65 | 66 | [Theory] 67 | [InlineData(0L)] 68 | [InlineData(1L)] 69 | [InlineData(100L)] 70 | public async Task GetMetrics_ReturnsPartitionCount(long partitionCount) 71 | { 72 | _metricProviderMock.Setup(provider => provider.GetPartitionCountAsync(It.IsAny())).ReturnsAsync(partitionCount); 73 | GetMetricsResponse response = await _cosmosDbScalerService.GetMetrics(GetGetMetricsRequest(), null); 74 | 75 | Assert.Single(response.MetricValues); 76 | 77 | Assert.Equal( 78 | "cosmosdb-partitioncount-example2-com-dummy-lease-database-id-dummy-lease-container-id-dummy-processor-name", 79 | response.MetricValues[0].MetricName); 80 | 81 | Assert.Equal(partitionCount, response.MetricValues[0].MetricValue_); 82 | } 83 | 84 | [Theory] 85 | [InlineData("")] 86 | [InlineData("custom-metric-name")] 87 | public async Task GetMetrics_ReturnsSameMetricNameIfPassed(string requestMetricName) 88 | { 89 | _metricProviderMock.Setup(provider => provider.GetPartitionCountAsync(It.IsAny())).ReturnsAsync(1L); 90 | 91 | // No assertion with request.MetricName since it is ignored. 92 | GetMetricsRequest request = GetGetMetricsRequest(); 93 | request.ScaledObjectRef.ScalerMetadata["metricName"] = requestMetricName; 94 | 95 | GetMetricsResponse response = await _cosmosDbScalerService.GetMetrics(request, null); 96 | 97 | Assert.Single(response.MetricValues); 98 | Assert.Equal(requestMetricName, response.MetricValues[0].MetricName); 99 | } 100 | 101 | [Theory] 102 | [InlineData("connectionFromEnv")] 103 | [InlineData("databaseId")] 104 | [InlineData("containerId")] 105 | [InlineData("leaseConnectionFromEnv")] 106 | [InlineData("leaseDatabaseId")] 107 | [InlineData("leaseContainerId")] 108 | [InlineData("processorName")] 109 | public async Task GetMetricSpec_ThrowsOnMissingMetadata(string metadataKey) 110 | { 111 | await Assert.ThrowsAsync( 112 | () => _cosmosDbScalerService.GetMetricSpec(GetScaledObjectRefWithoutMetadata(metadataKey), null)); 113 | } 114 | 115 | [Fact] 116 | public async Task GetMetricSpec_ReturnsMetricSpec() 117 | { 118 | GetMetricSpecResponse response = await _cosmosDbScalerService.GetMetricSpec(GetScaledObjectRef(), null); 119 | 120 | Assert.Single(response.MetricSpecs); 121 | 122 | Assert.Equal( 123 | "cosmosdb-partitioncount-example2-com-dummy-lease-database-id-dummy-lease-container-id-dummy-processor-name", 124 | response.MetricSpecs[0].MetricName); 125 | 126 | Assert.Equal(1L, response.MetricSpecs[0].TargetSize); 127 | } 128 | 129 | [Theory] 130 | [InlineData("")] 131 | [InlineData("custom-metric-name")] 132 | public async Task GetMetricSpec_ReturnsSameMetricNameIfPassed(string requestMetricName) 133 | { 134 | ScaledObjectRef request = GetScaledObjectRef(); 135 | request.ScalerMetadata["metricName"] = requestMetricName; 136 | 137 | GetMetricSpecResponse response = await _cosmosDbScalerService.GetMetricSpec(request, null); 138 | 139 | Assert.Single(response.MetricSpecs); 140 | Assert.Equal(requestMetricName, response.MetricSpecs[0].MetricName); 141 | } 142 | 143 | [Fact] 144 | public async Task GetMetricSpec_ReturnsNormalizedMetricName() 145 | { 146 | ScaledObjectRef request = GetScaledObjectRef(); 147 | request.ScalerMetadata["leaseConnectionFromEnv"] = "AccountEndpoint=https://example.com:443/;AccountKey=ZHVtbXky"; 148 | request.ScalerMetadata["leaseDatabaseId"] = "Dummy.Lease.Database.Id"; 149 | request.ScalerMetadata["leaseContainerId"] = "Dummy:Lease:Container:Id"; 150 | request.ScalerMetadata["processorName"] = "Dummy%Processor%Name"; 151 | 152 | GetMetricSpecResponse response = await _cosmosDbScalerService.GetMetricSpec(request, null); 153 | 154 | Assert.Single(response.MetricSpecs); 155 | 156 | Assert.Equal( 157 | "cosmosdb-partitioncount-example-com-dummy-lease-database-id-dummy-lease-container-id-dummy-processor-name", 158 | response.MetricSpecs[0].MetricName); 159 | } 160 | 161 | private static GetMetricsRequest GetGetMetricsRequest() 162 | { 163 | return new GetMetricsRequest 164 | { 165 | MetricName = "dummy-metric-name", 166 | ScaledObjectRef = GetScaledObjectRef(), 167 | }; 168 | } 169 | 170 | private static GetMetricsRequest GetGetMetricsRequestWithoutMetadata(string metadataKey) 171 | { 172 | return new GetMetricsRequest 173 | { 174 | MetricName = "dummy-metric-name", 175 | ScaledObjectRef = GetScaledObjectRefWithoutMetadata(metadataKey), 176 | }; 177 | } 178 | 179 | private static ScaledObjectRef GetScaledObjectRefWithoutMetadata(string metadataKey) 180 | { 181 | var scaledObjectRef = GetScaledObjectRef(); 182 | scaledObjectRef.ScalerMetadata.Remove(metadataKey); 183 | 184 | return scaledObjectRef; 185 | } 186 | 187 | private static ScaledObjectRef GetScaledObjectRef() 188 | { 189 | var scaledObjectRef = new ScaledObjectRef 190 | { 191 | Name = "dummy-scaled-object", 192 | Namespace = "dummy-namespace", 193 | }; 194 | 195 | MapField scalerMetadata = scaledObjectRef.ScalerMetadata; 196 | 197 | scalerMetadata["connectionFromEnv"] = "AccountEndpoint=https://example1.com:443/;AccountKey=ZHVtbXkx"; 198 | scalerMetadata["databaseId"] = "dummy-database-id"; 199 | scalerMetadata["containerId"] = "dummy-container-id"; 200 | scalerMetadata["leaseConnectionFromEnv"] = "AccountEndpoint=https://example2.com:443/;AccountKey=ZHVtbXky"; 201 | scalerMetadata["leaseDatabaseId"] = "dummy-lease-database-id"; 202 | scalerMetadata["leaseContainerId"] = "dummy-lease-container-id"; 203 | scalerMetadata["processorName"] = "dummy-processor-name"; 204 | 205 | return scaledObjectRef; 206 | } 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /src/Scaler.Tests/Keda.CosmosDb.Scaler.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | 6 | false 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | runtime; build; native; contentfiles; analyzers; buildtransitive 15 | all 16 | 17 | 18 | runtime; build; native; contentfiles; analyzers; buildtransitive 19 | all 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/Scaler/Dockerfile: -------------------------------------------------------------------------------- 1 | # https://hub.docker.com/_/microsoft-dotnet 2 | 3 | # Restore, build and publish project. 4 | FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build 5 | WORKDIR / 6 | COPY src/Scaler/ src/Scaler/ 7 | 8 | WORKDIR /src/Scaler 9 | RUN dotnet publish --configuration Release --output /app 10 | 11 | # Stage application. 12 | FROM mcr.microsoft.com/dotnet/aspnet:8.0 13 | WORKDIR /app 14 | COPY --from=build /app . 15 | ENTRYPOINT ["dotnet", "Keda.CosmosDb.Scaler.dll"] 16 | -------------------------------------------------------------------------------- /src/Scaler/Keda.CosmosDb.Scaler.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/Scaler/Program.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | using Microsoft.AspNetCore.Hosting; 3 | using Microsoft.AspNetCore.Server.Kestrel.Core; 4 | using Microsoft.Extensions.Hosting; 5 | using Microsoft.Extensions.Logging; 6 | 7 | [assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] // Required by mock generator. 8 | [assembly: InternalsVisibleTo("Keda.CosmosDb.Scaler.Tests")] 9 | 10 | namespace Keda.CosmosDb.Scaler 11 | { 12 | internal static class Program 13 | { 14 | public static void Main(string[] args) 15 | { 16 | CreateHostBuilder(args).Build().Run(); 17 | } 18 | 19 | // Additional configuration is required to successfully run gRPC on macOS. 20 | // For instructions on how to configure Kestrel and gRPC clients on macOS, visit https://go.microsoft.com/fwlink/?linkid=2099682 21 | public static IHostBuilder CreateHostBuilder(string[] args) => 22 | Host.CreateDefaultBuilder(args) 23 | .ConfigureLogging(builder => builder.AddSimpleConsole(options => options.TimestampFormat = "yyyy-MM-dd HH:mm:ss ")) 24 | .ConfigureWebHostDefaults(webBuilder => 25 | { 26 | webBuilder.ConfigureKestrel(kestrelServerOptions => 27 | { 28 | // Setup a HTTP/2 endpoint without TLS. 29 | kestrelServerOptions.ListenAnyIP(port: 4050, listOptions => listOptions.Protocols = HttpProtocols.Http2); 30 | }); 31 | 32 | webBuilder.UseStartup(); 33 | }); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Scaler/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "Keda.CosmosDb.Scaler": { 4 | "commandName": "Project", 5 | "dotnetRunMessages": "true", 6 | "launchBrowser": false, 7 | "applicationUrl": "http://localhost:4050", 8 | "environmentVariables": { 9 | "ASPNETCORE_ENVIRONMENT": "Development" 10 | } 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/Scaler/Protos/externalscaler.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | option csharp_namespace = "Keda.CosmosDb.Scaler"; 4 | 5 | package externalscaler; 6 | 7 | service ExternalScaler { 8 | rpc IsActive(ScaledObjectRef) returns (IsActiveResponse) {} 9 | rpc StreamIsActive(ScaledObjectRef) returns (stream IsActiveResponse) {} 10 | rpc GetMetricSpec(ScaledObjectRef) returns (GetMetricSpecResponse) {} 11 | rpc GetMetrics(GetMetricsRequest) returns (GetMetricsResponse) {} 12 | } 13 | 14 | message ScaledObjectRef { 15 | string name = 1; 16 | string namespace = 2; 17 | map scalerMetadata = 3; 18 | } 19 | 20 | message IsActiveResponse { 21 | bool result = 1; 22 | } 23 | 24 | message GetMetricSpecResponse { 25 | repeated MetricSpec metricSpecs = 1; 26 | } 27 | 28 | message MetricSpec { 29 | string metricName = 1; 30 | int64 targetSize = 2; 31 | } 32 | 33 | message GetMetricsRequest { 34 | ScaledObjectRef scaledObjectRef = 1; 35 | string metricName = 2; 36 | } 37 | 38 | message GetMetricsResponse { 39 | repeated MetricValue metricValues = 1; 40 | } 41 | 42 | message MetricValue { 43 | string metricName = 1; 44 | int64 metricValue = 2; 45 | } -------------------------------------------------------------------------------- /src/Scaler/Services/CosmosDbFactory.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Concurrent; 2 | using Microsoft.Azure.Cosmos; 3 | 4 | namespace Keda.CosmosDb.Scaler 5 | { 6 | internal sealed class CosmosDbFactory 7 | { 8 | // As per https://docs.microsoft.com/dotnet/api/microsoft.azure.cosmos.cosmosclient, it is recommended to 9 | // maintain a single instance of CosmosClient per lifetime of the application. 10 | private readonly ConcurrentDictionary _cosmosClientCache = new(); 11 | 12 | public CosmosClient GetCosmosClient(string connection) 13 | { 14 | return _cosmosClientCache.GetOrAdd(connection, CreateCosmosClient); 15 | } 16 | 17 | private CosmosClient CreateCosmosClient(string connection) 18 | { 19 | return new CosmosClient(connection, new CosmosClientOptions 20 | { 21 | ConnectionMode = ConnectionMode.Gateway, 22 | ApplicationName = "KEDA-External-Scaler" 23 | }); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Scaler/Services/CosmosDbMetricProvider.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Net; 4 | using System.Net.Http; 5 | using System.Threading.Tasks; 6 | using Microsoft.Azure.Cosmos; 7 | using Microsoft.Extensions.Logging; 8 | 9 | namespace Keda.CosmosDb.Scaler 10 | { 11 | internal sealed class CosmosDbMetricProvider : ICosmosDbMetricProvider 12 | { 13 | private readonly CosmosDbFactory _factory; 14 | private readonly ILogger _logger; 15 | 16 | public CosmosDbMetricProvider(CosmosDbFactory factory, ILogger logger) 17 | { 18 | _factory = factory ?? throw new ArgumentNullException(nameof(factory)); 19 | _logger = logger ?? throw new ArgumentNullException(nameof(logger)); 20 | } 21 | 22 | public async Task GetPartitionCountAsync(ScalerMetadata scalerMetadata) 23 | { 24 | try 25 | { 26 | Container leaseContainer = _factory 27 | .GetCosmosClient(scalerMetadata.LeaseConnection) 28 | .GetContainer(scalerMetadata.LeaseDatabaseId, scalerMetadata.LeaseContainerId); 29 | 30 | ChangeFeedEstimator estimator = _factory 31 | .GetCosmosClient(scalerMetadata.Connection) 32 | .GetContainer(scalerMetadata.DatabaseId, scalerMetadata.ContainerId) 33 | .GetChangeFeedEstimator(scalerMetadata.ProcessorName, leaseContainer); 34 | 35 | // It does not help by creating more change-feed processing instances than the number of partitions. 36 | int partitionCount = 0; 37 | 38 | using (FeedIterator iterator = estimator.GetCurrentStateIterator()) 39 | { 40 | while (iterator.HasMoreResults) 41 | { 42 | FeedResponse states = await iterator.ReadNextAsync(); 43 | partitionCount += states.Where(state => state.EstimatedLag > 0).Count(); 44 | } 45 | } 46 | 47 | return partitionCount; 48 | } 49 | catch (CosmosException exception) 50 | { 51 | _logger.LogWarning($"Encountered exception {exception.GetType()}: {exception.Message}"); 52 | } 53 | catch (InvalidOperationException exception) 54 | { 55 | _logger.LogWarning($"Encountered exception {exception.GetType()}: {exception.Message}"); 56 | throw; 57 | } 58 | catch (HttpRequestException exception) 59 | { 60 | var webException = exception.InnerException as WebException; 61 | if (webException?.Status == WebExceptionStatus.ProtocolError) 62 | { 63 | var response = (HttpWebResponse)webException.Response; 64 | _logger.LogWarning($"Encountered error response {response.StatusCode}: {response.StatusDescription}"); 65 | } 66 | else 67 | { 68 | _logger.LogWarning($"Encountered exception {exception.GetType()}: {exception.Message}"); 69 | } 70 | } 71 | 72 | return 0L; 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/Scaler/Services/CosmosDbScalerService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Grpc.Core; 4 | 5 | namespace Keda.CosmosDb.Scaler 6 | { 7 | internal sealed class CosmosDbScalerService : ExternalScaler.ExternalScalerBase 8 | { 9 | private readonly ICosmosDbMetricProvider _metricProvider; 10 | 11 | public CosmosDbScalerService(ICosmosDbMetricProvider metricProvider) 12 | { 13 | _metricProvider = metricProvider ?? throw new ArgumentNullException(nameof(metricProvider)); 14 | } 15 | 16 | public override async Task IsActive(ScaledObjectRef request, ServerCallContext context) 17 | { 18 | var scalerMetadata = ScalerMetadata.Create(request); 19 | 20 | bool isActive = (await _metricProvider.GetPartitionCountAsync(scalerMetadata)) > 0L; 21 | return new IsActiveResponse { Result = isActive }; 22 | } 23 | 24 | public override async Task GetMetrics(GetMetricsRequest request, ServerCallContext context) 25 | { 26 | var scalerMetadata = ScalerMetadata.Create(request.ScaledObjectRef); 27 | 28 | var response = new GetMetricsResponse(); 29 | 30 | response.MetricValues.Add(new MetricValue 31 | { 32 | MetricName = scalerMetadata.MetricName, 33 | MetricValue_ = await _metricProvider.GetPartitionCountAsync(scalerMetadata), 34 | }); 35 | 36 | return response; 37 | } 38 | 39 | public override Task GetMetricSpec(ScaledObjectRef request, ServerCallContext context) 40 | { 41 | var scalerMetadata = ScalerMetadata.Create(request); 42 | 43 | var response = new GetMetricSpecResponse(); 44 | 45 | response.MetricSpecs.Add(new MetricSpec 46 | { 47 | MetricName = scalerMetadata.MetricName, 48 | TargetSize = 1L, 49 | }); 50 | 51 | return Task.FromResult(response); 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Scaler/Services/ICosmosDbMetricProvider.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace Keda.CosmosDb.Scaler 4 | { 5 | internal interface ICosmosDbMetricProvider 6 | { 7 | Task GetPartitionCountAsync(ScalerMetadata scalerMetadata); 8 | } 9 | } -------------------------------------------------------------------------------- /src/Scaler/Services/ScalerMetadata.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Data.Common; 3 | using Newtonsoft.Json; 4 | 5 | namespace Keda.CosmosDb.Scaler 6 | { 7 | [JsonObject(ItemRequired = Required.Always)] 8 | internal sealed class ScalerMetadata 9 | { 10 | private string _metricName; 11 | 12 | [JsonProperty("ConnectionFromEnv")] 13 | public string Connection { get; set; } 14 | public string DatabaseId { get; set; } 15 | public string ContainerId { get; set; } 16 | [JsonProperty("LeaseConnectionFromEnv")] 17 | public string LeaseConnection { get; set; } 18 | public string LeaseDatabaseId { get; set; } 19 | public string LeaseContainerId { get; set; } 20 | public string ProcessorName { get; set; } 21 | 22 | [JsonProperty(Required = Required.DisallowNull)] 23 | public string MetricName 24 | { 25 | get 26 | { 27 | // Normalize metric name. 28 | _metricName ??= 29 | $"cosmosdb-partitioncount-{this.LeaseAccountHost}-{this.LeaseDatabaseId}-{this.LeaseContainerId}-{this.ProcessorName}" 30 | .Replace("/", "-").Replace(".", "-").Replace(":", "-").Replace("%", "-") 31 | .ToLower(); 32 | 33 | return _metricName; 34 | } 35 | 36 | set => _metricName = value; 37 | } 38 | 39 | [JsonIgnore] 40 | private string LeaseAccountHost 41 | { 42 | get 43 | { 44 | var builder = new DbConnectionStringBuilder { ConnectionString = this.LeaseConnection }; 45 | return new Uri((string)builder["AccountEndpoint"]).Host; 46 | } 47 | } 48 | 49 | public static ScalerMetadata Create(ScaledObjectRef scaledObjectRef) 50 | { 51 | return JsonConvert.DeserializeObject(scaledObjectRef.ScalerMetadata.ToString()); 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Scaler/Startup.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Builder; 2 | using Microsoft.AspNetCore.Hosting; 3 | using Microsoft.AspNetCore.Http; 4 | using Microsoft.Extensions.DependencyInjection; 5 | using Microsoft.Extensions.Hosting; 6 | 7 | namespace Keda.CosmosDb.Scaler 8 | { 9 | internal sealed class Startup 10 | { 11 | // This method gets called by the runtime. Use this method to add services to the container. 12 | // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940 13 | public void ConfigureServices(IServiceCollection services) 14 | { 15 | services.AddGrpc(); 16 | 17 | services.AddSingleton(); 18 | services.AddSingleton(); 19 | } 20 | 21 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. 22 | public void Configure(IApplicationBuilder app, IWebHostEnvironment env) 23 | { 24 | if (env.IsDevelopment()) 25 | { 26 | app.UseDeveloperExceptionPage(); 27 | } 28 | 29 | app.UseRouting(); 30 | 31 | app.UseEndpoints(endpoints => 32 | { 33 | endpoints.MapGrpcService(); 34 | 35 | endpoints.MapGet("/", async context => 36 | { 37 | await context.Response.WriteAsync("Communication with gRPC endpoints must be made through a gRPC client. To learn how to create a client, visit: https://go.microsoft.com/fwlink/?linkid=2086909"); 38 | }); 39 | }); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Scaler/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | }, 9 | "AllowedHosts": "*", 10 | "Kestrel": { 11 | "EndpointDefaults": { 12 | "Protocols": "Http2" 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /src/Scaler/deploy.yaml: -------------------------------------------------------------------------------- 1 | # Deploy KEDA external scaler for Azure Cosmos DB. 2 | 3 | apiVersion: apps/v1 4 | kind: Deployment 5 | metadata: 6 | name: cosmosdb-scaler 7 | namespace: default 8 | spec: 9 | replicas: 1 10 | selector: 11 | matchLabels: 12 | app: cosmosdb-scaler 13 | template: 14 | metadata: 15 | labels: 16 | app: cosmosdb-scaler 17 | spec: 18 | containers: 19 | - image: /cosmosdb-scaler:latest 20 | imagePullPolicy: Always 21 | name: cosmosdb-scaler 22 | ports: 23 | - containerPort: 4050 24 | 25 | --- 26 | # Assign hostname to the scaler application. 27 | 28 | apiVersion: v1 29 | kind: Service 30 | metadata: 31 | name: cosmosdb-scaler 32 | namespace: default 33 | spec: 34 | ports: 35 | - port: 4050 36 | targetPort: 4050 37 | selector: 38 | app: cosmosdb-scaler 39 | --------------------------------------------------------------------------------