├── .config └── dotnet-tools.json ├── .editorconfig ├── .github ├── dependabot.yml └── workflows │ ├── _service-build.yml │ ├── _service-deploy.yml │ ├── environments.yml │ ├── platform.yml │ ├── service-internal-grpc-sql-bus.yml │ ├── service-internal-grpc.yml │ ├── service-internal-http-bus.yml │ └── service-public-razor.yml ├── .gitignore ├── .vscode ├── extensions.json ├── launch.json ├── settings.json └── tasks.json ├── Directory.Build.props ├── LICENSE ├── MSA-Template.sln ├── README.md ├── add-service.ps1 ├── docs ├── azure-environment.png ├── azure-platform.png └── azure-service.png ├── global.json ├── infrastructure ├── README.md ├── _includes │ └── helpers.ps1 ├── bicepconfig.json ├── build-service.ps1 ├── config.json ├── deploy-environment.ps1 ├── deploy-platform.ps1 ├── deploy-service.ps1 ├── environment │ ├── app-environment.bicep │ ├── main.bicep │ ├── monitoring.bicep │ ├── network.bicep │ ├── servicebus.bicep │ ├── sql-identity-resources.bicep │ ├── sql-identity.bicep │ └── sql.bicep ├── init-platform.ps1 ├── names.json ├── platform │ ├── github-identity-resources.bicep │ ├── github-identity.bicep │ ├── main.bicep │ └── resources.bicep └── service │ ├── app-environment-pubsub.bicep │ ├── app-grpc.bicep │ ├── app-http.bicep │ ├── app-public.bicep │ ├── keyvault.bicep │ ├── main.bicep │ ├── platform.bicep │ ├── service-identity.bicep │ ├── servicebus.bicep │ ├── sql-migration.ps1 │ ├── sql-user.ps1 │ ├── sql.bicep │ └── storage.bicep ├── nuget.config ├── proto ├── _internal-grpc-sql-bus.proto ├── _internal-grpc.proto ├── google │ └── api │ │ ├── annotations.proto │ │ └── http.proto └── types.proto ├── services ├── _internal-grpc-sql-bus │ ├── InternalGrpcSqlBus.Api │ │ ├── CustomersService.cs │ │ ├── Domain │ │ │ ├── Customer.cs │ │ │ └── CustomersDbContext.cs │ │ ├── InternalGrpcSqlBus.Api.csproj │ │ ├── Migrations │ │ │ ├── 20220906082151_InitialCreate.Designer.cs │ │ │ ├── 20220906082151_InitialCreate.cs │ │ │ └── CustomersDbContextModelSnapshot.cs │ │ ├── Program.cs │ │ ├── Properties │ │ │ └── launchSettings.json │ │ ├── appsettings.Development.json │ │ └── appsettings.json │ ├── InternalGrpcSqlBus.sln │ └── README.md ├── _internal-grpc │ ├── InternalGrpc.Api │ │ ├── InternalGrpc.Api.csproj │ │ ├── InternalGrpcService.cs │ │ ├── Program.cs │ │ ├── Properties │ │ │ └── launchSettings.json │ │ ├── appsettings.Development.json │ │ └── appsettings.json │ ├── InternalGrpc.sln │ └── README.md ├── _internal-http-bus │ ├── InternalHttpBus.Api │ │ ├── InternalHttpBus.Api.csproj │ │ ├── Program.cs │ │ ├── Properties │ │ │ └── launchSettings.json │ │ ├── appsettings.Development.json │ │ └── appsettings.json │ ├── InternalHttpBus.sln │ └── README.md └── _public-razor │ ├── PublicRazor.Web │ ├── Pages │ │ ├── Index.cshtml │ │ ├── Index.cshtml.cs │ │ ├── InternalGrpc.cshtml │ │ ├── InternalGrpc.cshtml.cs │ │ ├── InternalGrpcSqlBus.cshtml │ │ ├── InternalGrpcSqlBus.cshtml.cs │ │ ├── InternalHttpBus.cshtml │ │ ├── InternalHttpBus.cshtml.cs │ │ ├── Shared │ │ │ └── _Layout.cshtml │ │ ├── _ViewImports.cshtml │ │ └── _ViewStart.cshtml │ ├── Program.cs │ ├── Properties │ │ └── launchSettings.json │ ├── PublicRazor.Web.csproj │ ├── appsettings.Development.json │ └── appsettings.json │ ├── PublicRazor.sln │ └── README.md └── shared └── Shared ├── AppInsights ├── AppInsightsExtensions.cs ├── AppInsightsHttpMessageHandler.cs └── ApplicationNameTelemetryInitializer.cs ├── DaprHelpers.cs ├── HealthCheckEndpointsExtensions.cs ├── Shared.csproj └── Types └── DecimalValue.cs /.config/dotnet-tools.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "isRoot": true, 4 | "tools": { 5 | "dotnet-ef": { 6 | "version": "7.0.13", 7 | "commands": [ 8 | "dotnet-ef" 9 | ] 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file 2 | 3 | version: 2 4 | updates: 5 | - package-ecosystem: "github-actions" 6 | directory: "/" 7 | schedule: 8 | interval: "weekly" 9 | 10 | - package-ecosystem: nuget 11 | directory: "/" 12 | schedule: 13 | interval: daily 14 | groups: 15 | azure-sdk: 16 | patterns: 17 | - "Azure.*" 18 | dapr: 19 | patterns: 20 | - "Dapr.*" 21 | dotnet: 22 | patterns: 23 | - "Microsoft.Extensions.*" 24 | - "Microsoft.AspNetCore.*" 25 | - "Microsoft.Data.*" 26 | - "Microsoft.EntityFrameworkCore*" 27 | - "dotnet-*" 28 | grpc: 29 | patterns: 30 | - "Google.Api.CommonProtos" 31 | - "Google.Protobuf" 32 | - "Grpc.*" 33 | -------------------------------------------------------------------------------- /.github/workflows/_service-build.yml: -------------------------------------------------------------------------------- 1 | name: '__Template: Service Build' 2 | 3 | on: 4 | workflow_call: 5 | inputs: 6 | serviceName: 7 | required: true 8 | type: string 9 | servicePath: 10 | required: true 11 | type: string 12 | hostProjectName: 13 | required: true 14 | type: string 15 | 16 | jobs: 17 | 18 | build: 19 | runs-on: ubuntu-latest 20 | 21 | steps: 22 | 23 | - uses: actions/checkout@v4 24 | 25 | - name: Show GitHub context for debugging # TODO: Remove this step 26 | run: | 27 | echo 'event_name: ${{ github.event_name }}' 28 | echo 'ref: ${{ github.ref }}' 29 | 30 | - uses: azure/login@v2 31 | if: github.ref == 'refs/heads/main' && github.event_name != 'pull_request' 32 | with: 33 | client-id: ${{ secrets.AZURE_CLIENT_ID }} 34 | tenant-id: ${{ secrets.AZURE_TENANT_ID }} 35 | subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} 36 | enable-AzPSSession: true 37 | 38 | - name: Docker Login to ACR 39 | if: github.ref == 'refs/heads/main' && github.event_name != 'pull_request' 40 | run: | 41 | set -euo pipefail 42 | access_token=$(az account get-access-token --query accessToken -o tsv) 43 | refresh_token=$(curl https://${{ secrets.REGISTRY_SERVER }}/oauth2/exchange -v -d "grant_type=access_token&service=${{ secrets.REGISTRY_SERVER }}&access_token=$access_token" | jq -r .refresh_token) 44 | docker login -u 00000000-0000-0000-0000-000000000000 --password-stdin ${{ secrets.REGISTRY_SERVER }} <<< "$refresh_token" 45 | 46 | - uses: actions/setup-dotnet@v4 47 | 48 | - run: dotnet --version 49 | 50 | - name: 'Build service' 51 | uses: azure/powershell@v1 52 | with: 53 | inlineScript: | 54 | Set-Location ./infrastructure 55 | ./build-service.ps1 -ServiceName "${{ inputs.serviceName }}" -ServicePath "${{ inputs.servicePath }}" -HostProjectName "${{ inputs.hostProjectName }}" ` 56 | -BuildNumber "${{ github.run_number }}" ` 57 | -UploadArtifacts $${{ (github.ref == 'refs/heads/main' && github.event_name != 'pull_request') }} 58 | azPSVersion: "9.5.0" 59 | 60 | -------------------------------------------------------------------------------- /.github/workflows/_service-deploy.yml: -------------------------------------------------------------------------------- 1 | name: '__Template: Service Deploy' 2 | 3 | on: 4 | workflow_call: 5 | inputs: 6 | service: 7 | required: true 8 | type: string 9 | environment: 10 | required: true 11 | type: string 12 | 13 | jobs: 14 | 15 | deploy: 16 | if: github.ref == 'refs/heads/main' && github.event_name != 'pull_request' 17 | runs-on: ubuntu-latest 18 | environment: ${{ inputs.environment }} 19 | 20 | steps: 21 | 22 | - uses: actions/checkout@v4 23 | 24 | - uses: azure/login@v2 25 | with: 26 | client-id: ${{ secrets.AZURE_CLIENT_ID }} 27 | tenant-id: ${{ secrets.AZURE_TENANT_ID }} 28 | subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} 29 | enable-AzPSSession: true 30 | 31 | - name: 'Deploy Azure resources' 32 | uses: azure/powershell@v1 33 | with: 34 | inlineScript: | 35 | Set-Location ./infrastructure 36 | ./deploy-service.ps1 -Environment ${{ inputs.environment }} -Service ${{ inputs.service }} -BuildNumber ${{ github.run_number }} 37 | azPSVersion: "9.5.0" 38 | -------------------------------------------------------------------------------- /.github/workflows/environments.yml: -------------------------------------------------------------------------------- 1 | name: '2. Environments' 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | permissions: 7 | id-token: write 8 | contents: read 9 | 10 | jobs: 11 | 12 | deploy: 13 | strategy: 14 | matrix: 15 | # TEMPLATE_ADD_ENVIRONMENT Any new environment must be added here to allow the deployment of environment resources via GitHub Actions 16 | environment: [ development, production ] 17 | 18 | runs-on: ubuntu-latest 19 | environment: ${{ matrix.environment }} 20 | 21 | steps: 22 | 23 | - uses: actions/checkout@v4 24 | 25 | - uses: azure/login@v2 26 | with: 27 | client-id: ${{ secrets.AZURE_CLIENT_ID }} 28 | tenant-id: ${{ secrets.AZURE_TENANT_ID }} 29 | subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} 30 | enable-AzPSSession: true 31 | 32 | - name: 'Deploy Azure resources' 33 | uses: azure/powershell@v1 34 | with: 35 | inlineScript: | 36 | Set-Location ./infrastructure 37 | ./deploy-environment.ps1 -Environment ${{ matrix.environment }} 38 | azPSVersion: "9.5.0" 39 | -------------------------------------------------------------------------------- /.github/workflows/platform.yml: -------------------------------------------------------------------------------- 1 | name: '1. Platform' 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | permissions: 7 | id-token: write 8 | contents: read 9 | 10 | jobs: 11 | 12 | deploy: 13 | runs-on: ubuntu-latest 14 | environment: platform 15 | 16 | steps: 17 | 18 | - uses: actions/checkout@v4 19 | 20 | - uses: azure/login@v2 21 | with: 22 | client-id: ${{ secrets.AZURE_CLIENT_ID }} 23 | tenant-id: ${{ secrets.AZURE_TENANT_ID }} 24 | subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} 25 | enable-AzPSSession: true 26 | 27 | - name: 'Deploy Azure resources' 28 | uses: azure/powershell@v1 29 | with: 30 | inlineScript: | 31 | Set-Location ./infrastructure 32 | ./deploy-platform.ps1 33 | azPSVersion: "9.5.0" 34 | -------------------------------------------------------------------------------- /.github/workflows/service-internal-grpc-sql-bus.yml: -------------------------------------------------------------------------------- 1 | name: '_Internal/gRPC/SQL/Bus' 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | paths: 7 | - services/_internal-grpc-sql-bus/** 8 | - proto/_internal-grpc-sql-bus.proto 9 | 10 | permissions: 11 | id-token: write 12 | contents: read 13 | 14 | jobs: 15 | 16 | build: 17 | uses: ./.github/workflows/_service-build.yml 18 | secrets: inherit 19 | with: 20 | serviceName: internal-grpc-sql-bus 21 | servicePath: services/_internal-grpc-sql-bus 22 | hostProjectName: InternalGrpcSqlBus.Api 23 | 24 | deploy: 25 | strategy: 26 | matrix: 27 | # TEMPLATE_ADD_ENVIRONMENT Any new environment the service should be deployed into must be added here 28 | environment: [ development, production ] 29 | 30 | needs: build 31 | uses: ./.github/workflows/_service-deploy.yml 32 | secrets: inherit 33 | with: 34 | service: internal-grpc-sql-bus 35 | environment: ${{ matrix.environment }} 36 | -------------------------------------------------------------------------------- /.github/workflows/service-internal-grpc.yml: -------------------------------------------------------------------------------- 1 | name: '_Internal/gRPC' 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | paths: 7 | - services/_internal-grpc/** 8 | - proto/_internal-grpc.proto 9 | 10 | permissions: 11 | id-token: write 12 | contents: read 13 | 14 | jobs: 15 | 16 | build: 17 | uses: ./.github/workflows/_service-build.yml 18 | secrets: inherit 19 | with: 20 | serviceName: internal-grpc 21 | servicePath: services/_internal-grpc 22 | hostProjectName: InternalGrpc.Api 23 | 24 | deploy: 25 | strategy: 26 | matrix: 27 | # TEMPLATE_ADD_ENVIRONMENT Any new environment the service should be deployed into must be added here 28 | environment: [ development, production ] 29 | 30 | needs: build 31 | uses: ./.github/workflows/_service-deploy.yml 32 | secrets: inherit 33 | with: 34 | service: internal-grpc 35 | environment: ${{ matrix.environment }} 36 | -------------------------------------------------------------------------------- /.github/workflows/service-internal-http-bus.yml: -------------------------------------------------------------------------------- 1 | name: '_Internal/HTTP/Bus' 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | paths: 7 | - services/_internal-http-bus/** 8 | 9 | permissions: 10 | id-token: write 11 | contents: read 12 | 13 | jobs: 14 | 15 | build: 16 | uses: ./.github/workflows/_service-build.yml 17 | secrets: inherit 18 | with: 19 | serviceName: internal-http-bus 20 | servicePath: services/_internal-http-bus 21 | hostProjectName: InternalHttpBus.Api 22 | 23 | deploy: 24 | strategy: 25 | matrix: 26 | # TEMPLATE_ADD_ENVIRONMENT Any new environment the service should be deployed into must be added here 27 | environment: [ development, production ] 28 | 29 | needs: build 30 | uses: ./.github/workflows/_service-deploy.yml 31 | secrets: inherit 32 | with: 33 | service: internal-http-bus 34 | environment: ${{ matrix.environment }} 35 | -------------------------------------------------------------------------------- /.github/workflows/service-public-razor.yml: -------------------------------------------------------------------------------- 1 | name: '_Public/Razor' 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | paths: 7 | - services/_public-razor/** 8 | 9 | permissions: 10 | id-token: write 11 | contents: read 12 | 13 | jobs: 14 | 15 | build: 16 | uses: ./.github/workflows/_service-build.yml 17 | secrets: inherit 18 | with: 19 | serviceName: public-razor 20 | servicePath: services/_public-razor 21 | hostProjectName: PublicRazor.Web 22 | 23 | deploy: 24 | strategy: 25 | matrix: 26 | # TEMPLATE_ADD_ENVIRONMENT Any new environment the service should be deployed into must be added here 27 | environment: [ development, production ] 28 | 29 | needs: build 30 | uses: ./.github/workflows/_service-deploy.yml 31 | secrets: inherit 32 | with: 33 | service: public-razor 34 | environment: ${{ matrix.environment }} 35 | -------------------------------------------------------------------------------- /.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 | [Aa][Rr][Mm]/ 27 | [Aa][Rr][Mm]64/ 28 | bld/ 29 | [Bb]in/ 30 | [Oo]bj/ 31 | [Ll]og/ 32 | [Ll]ogs/ 33 | 34 | # Visual Studio 2015/2017 cache/options directory 35 | .vs/ 36 | # Uncomment if you have tasks that create the project's static files in wwwroot 37 | #wwwroot/ 38 | 39 | # Visual Studio 2017 auto generated files 40 | Generated\ Files/ 41 | 42 | # MSTest test Results 43 | [Tt]est[Rr]esult*/ 44 | [Bb]uild[Ll]og.* 45 | 46 | # NUnit 47 | *.VisualState.xml 48 | TestResult.xml 49 | nunit-*.xml 50 | 51 | # Build Results of an ATL Project 52 | [Dd]ebugPS/ 53 | [Rr]eleasePS/ 54 | dlldata.c 55 | 56 | # Benchmark Results 57 | BenchmarkDotNet.Artifacts/ 58 | 59 | # .NET Core 60 | project.lock.json 61 | project.fragment.lock.json 62 | artifacts/ 63 | 64 | # StyleCop 65 | StyleCopReport.xml 66 | 67 | # Files built by Visual Studio 68 | *_i.c 69 | *_p.c 70 | *_h.h 71 | *.ilk 72 | *.meta 73 | *.obj 74 | *.iobj 75 | *.pch 76 | *.pdb 77 | *.ipdb 78 | *.pgc 79 | *.pgd 80 | *.rsp 81 | *.sbr 82 | *.tlb 83 | *.tli 84 | *.tlh 85 | *.tmp 86 | *.tmp_proj 87 | *_wpftmp.csproj 88 | *.log 89 | *.vspscc 90 | *.vssscc 91 | .builds 92 | *.pidb 93 | *.svclog 94 | *.scc 95 | 96 | # Chutzpah Test files 97 | _Chutzpah* 98 | 99 | # Visual C++ cache files 100 | ipch/ 101 | *.aps 102 | *.ncb 103 | *.opendb 104 | *.opensdf 105 | *.sdf 106 | *.cachefile 107 | *.VC.db 108 | *.VC.VC.opendb 109 | 110 | # Visual Studio profiler 111 | *.psess 112 | *.vsp 113 | *.vspx 114 | *.sap 115 | 116 | # Visual Studio Trace Files 117 | *.e2e 118 | 119 | # TFS 2012 Local Workspace 120 | $tf/ 121 | 122 | # Guidance Automation Toolkit 123 | *.gpState 124 | 125 | # ReSharper is a .NET coding add-in 126 | _ReSharper*/ 127 | *.[Rr]e[Ss]harper 128 | *.DotSettings.user 129 | 130 | # TeamCity is a build add-in 131 | _TeamCity* 132 | 133 | # DotCover is a Code Coverage Tool 134 | *.dotCover 135 | 136 | # AxoCover is a Code Coverage Tool 137 | .axoCover/* 138 | !.axoCover/settings.json 139 | 140 | # Visual Studio code coverage results 141 | *.coverage 142 | *.coveragexml 143 | 144 | # NCrunch 145 | _NCrunch_* 146 | .*crunch*.local.xml 147 | nCrunchTemp_* 148 | 149 | # MightyMoose 150 | *.mm.* 151 | AutoTest.Net/ 152 | 153 | # Web workbench (sass) 154 | .sass-cache/ 155 | 156 | # Installshield output folder 157 | [Ee]xpress/ 158 | 159 | # DocProject is a documentation generator add-in 160 | DocProject/buildhelp/ 161 | DocProject/Help/*.HxT 162 | DocProject/Help/*.HxC 163 | DocProject/Help/*.hhc 164 | DocProject/Help/*.hhk 165 | DocProject/Help/*.hhp 166 | DocProject/Help/Html2 167 | DocProject/Help/html 168 | 169 | # Click-Once directory 170 | publish/ 171 | 172 | # Publish Web Output 173 | *.[Pp]ublish.xml 174 | *.azurePubxml 175 | # Note: Comment the next line if you want to checkin your web deploy settings, 176 | # but database connection strings (with potential passwords) will be unencrypted 177 | *.pubxml 178 | *.publishproj 179 | 180 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 181 | # checkin your Azure Web App publish settings, but sensitive information contained 182 | # in these scripts will be unencrypted 183 | PublishScripts/ 184 | 185 | # NuGet Packages 186 | *.nupkg 187 | # NuGet Symbol Packages 188 | *.snupkg 189 | # The packages folder can be ignored because of Package Restore 190 | **/[Pp]ackages/* 191 | # except build/, which is used as an MSBuild target. 192 | !**/[Pp]ackages/build/ 193 | # Uncomment if necessary however generally it will be regenerated when needed 194 | #!**/[Pp]ackages/repositories.config 195 | # NuGet v3's project.json files produces more ignorable files 196 | *.nuget.props 197 | *.nuget.targets 198 | 199 | # Microsoft Azure Build Output 200 | csx/ 201 | *.build.csdef 202 | 203 | # Microsoft Azure Emulator 204 | ecf/ 205 | rcf/ 206 | 207 | # Windows Store app package directories and files 208 | AppPackages/ 209 | BundleArtifacts/ 210 | Package.StoreAssociation.xml 211 | _pkginfo.txt 212 | *.appx 213 | *.appxbundle 214 | *.appxupload 215 | 216 | # Visual Studio cache files 217 | # files ending in .cache can be ignored 218 | *.[Cc]ache 219 | # but keep track of directories ending in .cache 220 | !?*.[Cc]ache/ 221 | 222 | # Others 223 | ClientBin/ 224 | ~$* 225 | *~ 226 | *.dbmdl 227 | *.dbproj.schemaview 228 | *.jfm 229 | *.pfx 230 | *.publishsettings 231 | orleans.codegen.cs 232 | 233 | # Including strong name files can present a security risk 234 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 235 | #*.snk 236 | 237 | # Since there are multiple workflows, uncomment next line to ignore bower_components 238 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 239 | #bower_components/ 240 | 241 | # RIA/Silverlight projects 242 | Generated_Code/ 243 | 244 | # Backup & report files from converting an old project file 245 | # to a newer Visual Studio version. Backup files are not needed, 246 | # because we have git ;-) 247 | _UpgradeReport_Files/ 248 | Backup*/ 249 | UpgradeLog*.XML 250 | UpgradeLog*.htm 251 | ServiceFabricBackup/ 252 | *.rptproj.bak 253 | 254 | # SQL Server files 255 | *.mdf 256 | *.ldf 257 | *.ndf 258 | 259 | # Business Intelligence projects 260 | *.rdl.data 261 | *.bim.layout 262 | *.bim_*.settings 263 | *.rptproj.rsuser 264 | *- [Bb]ackup.rdl 265 | *- [Bb]ackup ([0-9]).rdl 266 | *- [Bb]ackup ([0-9][0-9]).rdl 267 | 268 | # Microsoft Fakes 269 | FakesAssemblies/ 270 | 271 | # GhostDoc plugin setting file 272 | *.GhostDoc.xml 273 | 274 | # Node.js Tools for Visual Studio 275 | .ntvs_analysis.dat 276 | node_modules/ 277 | 278 | # Visual Studio 6 build log 279 | *.plg 280 | 281 | # Visual Studio 6 workspace options file 282 | *.opt 283 | 284 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 285 | *.vbw 286 | 287 | # Visual Studio LightSwitch build output 288 | **/*.HTMLClient/GeneratedArtifacts 289 | **/*.DesktopClient/GeneratedArtifacts 290 | **/*.DesktopClient/ModelManifest.xml 291 | **/*.Server/GeneratedArtifacts 292 | **/*.Server/ModelManifest.xml 293 | _Pvt_Extensions 294 | 295 | # Paket dependency manager 296 | .paket/paket.exe 297 | paket-files/ 298 | 299 | # FAKE - F# Make 300 | .fake/ 301 | 302 | # CodeRush personal settings 303 | .cr/personal 304 | 305 | # Python Tools for Visual Studio (PTVS) 306 | __pycache__/ 307 | *.pyc 308 | 309 | # Cake - Uncomment if you are using it 310 | # tools/** 311 | # !tools/packages.config 312 | 313 | # Tabs Studio 314 | *.tss 315 | 316 | # Telerik's JustMock configuration file 317 | *.jmconfig 318 | 319 | # BizTalk build output 320 | *.btp.cs 321 | *.btm.cs 322 | *.odx.cs 323 | *.xsd.cs 324 | 325 | # OpenCover UI analysis results 326 | OpenCover/ 327 | 328 | # Azure Stream Analytics local run output 329 | ASALocalRun/ 330 | 331 | # MSBuild Binary and Structured Log 332 | *.binlog 333 | 334 | # NVidia Nsight GPU debugger configuration file 335 | *.nvuser 336 | 337 | # MFractors (Xamarin productivity tool) working folder 338 | .mfractor/ 339 | 340 | # Local History for Visual Studio 341 | .localhistory/ 342 | 343 | # BeatPulse healthcheck temp database 344 | healthchecksdb 345 | 346 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 347 | MigrationBackup/ 348 | 349 | # Ionide (cross platform F# VS Code tools) working folder 350 | .ionide/ 351 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "editorconfig.editorconfig", 4 | "ms-azuretools.vscode-bicep", 5 | "ms-dotnettools.csharp", 6 | "ms-vscode.powershell", 7 | "zxh404.vscode-proto3" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | // Use IntelliSense to find out which attributes exist for C# debugging 5 | // Use hover for the description of the existing attributes 6 | // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md 7 | { 8 | "name": "_InternalGrpc.Api", 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}/services/_internal-grpc/InternalGrpc.Api/bin/Debug/net7.0/InternalGrpc.Api.dll", 14 | "cwd": "${workspaceFolder}/services/_internal-grpc/InternalGrpc.Api", 15 | "stopAtEntry": false, 16 | "serverReadyAction": { 17 | "action": "openExternally", 18 | "pattern": "\\bNow listening on:\\s+(https?://\\S+)" 19 | }, 20 | "launchSettingsProfile": "https" 21 | }, 22 | { 23 | "name": "_InternalGrpcSqlBus.Api", 24 | "type": "coreclr", 25 | "request": "launch", 26 | "preLaunchTask": "build", 27 | // If you have changed target frameworks, make sure to update the program path. 28 | "program": "${workspaceFolder}/services/_internal-grpc-sql-bus/InternalGrpcSqlBus.Api/bin/Debug/net7.0/InternalGrpcSqlBus.Api.dll", 29 | "cwd": "${workspaceFolder}/services/_internal-grpc-sql-bus/InternalGrpcSqlBus.Api", 30 | "stopAtEntry": false, 31 | "serverReadyAction": { 32 | "action": "openExternally", 33 | "pattern": "\\bNow listening on:\\s+(https?://\\S+)" 34 | }, 35 | "launchSettingsProfile": "https" 36 | }, 37 | { 38 | "name": "_InternalHttpBus.Api", 39 | "type": "coreclr", 40 | "request": "launch", 41 | "preLaunchTask": "build", 42 | // If you have changed target frameworks, make sure to update the program path. 43 | "program": "${workspaceFolder}/services/_internal-http-bus/InternalHttpBus.Api/bin/Debug/net7.0/InternalHttpBus.Api.dll", 44 | "cwd": "${workspaceFolder}/services/_internal-http-bus/InternalHttpBus.Api", 45 | "stopAtEntry": false, 46 | "serverReadyAction": { 47 | "action": "openExternally", 48 | "pattern": "\\bNow listening on:\\s+(https?://\\S+)" 49 | }, 50 | "launchSettingsProfile": "https" 51 | }, 52 | { 53 | "name": "_PublicRazor.Web", 54 | "type": "coreclr", 55 | "request": "launch", 56 | "preLaunchTask": "build", 57 | // If you have changed target frameworks, make sure to update the program path. 58 | "program": "${workspaceFolder}/services/_public-razor/PublicRazor.Web/bin/Debug/net7.0/PublicRazor.Web.dll", 59 | "cwd": "${workspaceFolder}/services/_public-razor/PublicRazor.Web", 60 | "stopAtEntry": false, 61 | "serverReadyAction": { 62 | "action": "openExternally", 63 | "pattern": "\\bNow listening on:\\s+(https?://\\S+)" 64 | }, 65 | "launchSettingsProfile": "https" 66 | }, 67 | { 68 | "name": ".NET Core Attach", 69 | "type": "coreclr", 70 | "request": "attach" 71 | } 72 | ] 73 | } 74 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.associations": { 3 | // Bicep can handle comments when loading JSON files. 4 | "config.json": "jsonc", 5 | "names.json": "jsonc", 6 | // ASP.NET Core files that can also handle comments 7 | "appsettings.json": "jsonc", 8 | "appsettings.Development.json": "jsonc", 9 | "launchSettings.json": "jsonc" 10 | }, 11 | "files.insertFinalNewline": true, 12 | // OmniSharp settings 13 | // https://github.com/OmniSharp/omnisharp-vscode/blob/master/src/omnisharp/options.ts 14 | // https://www.strathweb.com/2020/02/hidden-features-of-omnisharp-and-c-extension-for-vs-code/ 15 | "omnisharp.enableEditorConfigSupport": true, 16 | "omnisharp.organizeImportsOnFormat": true, 17 | "dotnet.completion.showCompletionItemsFromUnimportedNamespaces": true, 18 | "dotnet.defaultSolution": "MSA-Template.sln", 19 | "cSpell.words": [ 20 | "Dapr" 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /.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 | "${workspaceFolder}", 11 | "/property:GenerateFullPaths=true", 12 | "/consoleloggerparameters:NoSummary" 13 | ], 14 | "problemMatcher": "$msCompile" 15 | }, 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | Christian Weiss 4 | 5 | enable 6 | enable 7 | true 8 | 9 | 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Christian Weiss 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MSA-Template.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.1.32210.238 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "services", "services", "{84DBDC7F-5C56-4FFC-95D4-1E8BEAA6B451}" 7 | EndProject 8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "_internal-grpc", "_internal-grpc", "{25B76532-40F3-4284-871B-A7B0E8AD25BF}" 9 | ProjectSection(SolutionItems) = preProject 10 | services\_internal-grpc\README.md = services\_internal-grpc\README.md 11 | EndProjectSection 12 | EndProject 13 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "InternalGrpc.Api", "services\_internal-grpc\InternalGrpc.Api\InternalGrpc.Api.csproj", "{53388159-633C-4FA4-B308-C5D262D83F11}" 14 | EndProject 15 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "shared", "shared", "{FF648FB9-64D9-47ED-B625-16E01F54DDD6}" 16 | EndProject 17 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Shared", "shared\Shared\Shared.csproj", "{1A29C5C0-6FC4-43A2-BEBE-054D51562797}" 18 | EndProject 19 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{E84EB295-5A86-4941-A33B-CDF73C4F7020}" 20 | ProjectSection(SolutionItems) = preProject 21 | .editorconfig = .editorconfig 22 | .gitignore = .gitignore 23 | Directory.Build.props = Directory.Build.props 24 | global.json = global.json 25 | LICENSE = LICENSE 26 | nuget.config = nuget.config 27 | README.md = README.md 28 | EndProjectSection 29 | EndProject 30 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "_internal-http-bus", "_internal-http-bus", "{A578A299-6EC4-441D-970A-FD794E6AB9E7}" 31 | ProjectSection(SolutionItems) = preProject 32 | services\_internal-http-bus\README.md = services\_internal-http-bus\README.md 33 | EndProjectSection 34 | EndProject 35 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "InternalHttpBus.Api", "services\_internal-http-bus\InternalHttpBus.Api\InternalHttpBus.Api.csproj", "{B72C0307-0215-48AB-80CB-2B0EA1139E67}" 36 | EndProject 37 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "InternalGrpcSqlBus.Api", "services\_internal-grpc-sql-bus\InternalGrpcSqlBus.Api\InternalGrpcSqlBus.Api.csproj", "{B8484ED6-EC22-41C4-AC4E-1E64CE63E761}" 38 | EndProject 39 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "_internal-grpc-sql-bus", "_internal-grpc-sql-bus", "{24003D62-0728-4F67-908C-5DC121318EF7}" 40 | ProjectSection(SolutionItems) = preProject 41 | services\_internal-grpc-sql-bus\README.md = services\_internal-grpc-sql-bus\README.md 42 | EndProjectSection 43 | EndProject 44 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "_public-razor", "_public-razor", "{ACD65BE0-F674-4E03-B2BF-37478C577C08}" 45 | EndProject 46 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PublicRazor.Web", "services\_public-razor\PublicRazor.Web\PublicRazor.Web.csproj", "{DDA1D41A-B3C3-441D-9CB6-FBD6D98A35D9}" 47 | EndProject 48 | Global 49 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 50 | Debug|Any CPU = Debug|Any CPU 51 | Release|Any CPU = Release|Any CPU 52 | EndGlobalSection 53 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 54 | {53388159-633C-4FA4-B308-C5D262D83F11}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 55 | {53388159-633C-4FA4-B308-C5D262D83F11}.Debug|Any CPU.Build.0 = Debug|Any CPU 56 | {53388159-633C-4FA4-B308-C5D262D83F11}.Release|Any CPU.ActiveCfg = Release|Any CPU 57 | {53388159-633C-4FA4-B308-C5D262D83F11}.Release|Any CPU.Build.0 = Release|Any CPU 58 | {1A29C5C0-6FC4-43A2-BEBE-054D51562797}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 59 | {1A29C5C0-6FC4-43A2-BEBE-054D51562797}.Debug|Any CPU.Build.0 = Debug|Any CPU 60 | {1A29C5C0-6FC4-43A2-BEBE-054D51562797}.Release|Any CPU.ActiveCfg = Release|Any CPU 61 | {1A29C5C0-6FC4-43A2-BEBE-054D51562797}.Release|Any CPU.Build.0 = Release|Any CPU 62 | {B72C0307-0215-48AB-80CB-2B0EA1139E67}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 63 | {B72C0307-0215-48AB-80CB-2B0EA1139E67}.Debug|Any CPU.Build.0 = Debug|Any CPU 64 | {B72C0307-0215-48AB-80CB-2B0EA1139E67}.Release|Any CPU.ActiveCfg = Release|Any CPU 65 | {B72C0307-0215-48AB-80CB-2B0EA1139E67}.Release|Any CPU.Build.0 = Release|Any CPU 66 | {B8484ED6-EC22-41C4-AC4E-1E64CE63E761}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 67 | {B8484ED6-EC22-41C4-AC4E-1E64CE63E761}.Debug|Any CPU.Build.0 = Debug|Any CPU 68 | {B8484ED6-EC22-41C4-AC4E-1E64CE63E761}.Release|Any CPU.ActiveCfg = Release|Any CPU 69 | {B8484ED6-EC22-41C4-AC4E-1E64CE63E761}.Release|Any CPU.Build.0 = Release|Any CPU 70 | {DDA1D41A-B3C3-441D-9CB6-FBD6D98A35D9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 71 | {DDA1D41A-B3C3-441D-9CB6-FBD6D98A35D9}.Debug|Any CPU.Build.0 = Debug|Any CPU 72 | {DDA1D41A-B3C3-441D-9CB6-FBD6D98A35D9}.Release|Any CPU.ActiveCfg = Release|Any CPU 73 | {DDA1D41A-B3C3-441D-9CB6-FBD6D98A35D9}.Release|Any CPU.Build.0 = Release|Any CPU 74 | EndGlobalSection 75 | GlobalSection(SolutionProperties) = preSolution 76 | HideSolutionNode = FALSE 77 | EndGlobalSection 78 | GlobalSection(NestedProjects) = preSolution 79 | {25B76532-40F3-4284-871B-A7B0E8AD25BF} = {84DBDC7F-5C56-4FFC-95D4-1E8BEAA6B451} 80 | {53388159-633C-4FA4-B308-C5D262D83F11} = {25B76532-40F3-4284-871B-A7B0E8AD25BF} 81 | {1A29C5C0-6FC4-43A2-BEBE-054D51562797} = {FF648FB9-64D9-47ED-B625-16E01F54DDD6} 82 | {A578A299-6EC4-441D-970A-FD794E6AB9E7} = {84DBDC7F-5C56-4FFC-95D4-1E8BEAA6B451} 83 | {B72C0307-0215-48AB-80CB-2B0EA1139E67} = {A578A299-6EC4-441D-970A-FD794E6AB9E7} 84 | {B8484ED6-EC22-41C4-AC4E-1E64CE63E761} = {24003D62-0728-4F67-908C-5DC121318EF7} 85 | {24003D62-0728-4F67-908C-5DC121318EF7} = {84DBDC7F-5C56-4FFC-95D4-1E8BEAA6B451} 86 | {ACD65BE0-F674-4E03-B2BF-37478C577C08} = {84DBDC7F-5C56-4FFC-95D4-1E8BEAA6B451} 87 | {DDA1D41A-B3C3-441D-9CB6-FBD6D98A35D9} = {ACD65BE0-F674-4E03-B2BF-37478C577C08} 88 | EndGlobalSection 89 | GlobalSection(ExtensibilityGlobals) = postSolution 90 | SolutionGuid = {D423435A-06C9-47CA-A100-D16D6E066AF7} 91 | EndGlobalSection 92 | EndGlobal 93 | -------------------------------------------------------------------------------- /add-service.ps1: -------------------------------------------------------------------------------- 1 | [CmdletBinding()] 2 | Param ( 3 | 4 | [Parameter(Mandatory=$True)] 5 | [ValidateSet("internal-grpc", "internal-grpc-sql-bus", "internal-http-bus", "public-razor")] 6 | [string]$Template, 7 | 8 | [Parameter(Mandatory=$True)] 9 | [string]$ServiceName, 10 | 11 | [Parameter(Mandatory=$True)] 12 | [string]$NamespaceName 13 | ) 14 | 15 | $ErrorActionPreference = "Stop" 16 | 17 | #$Template = "internal-grpc" 18 | #$ServiceName = "sample-svc" 19 | #$NamespaceName = "SampleSvc" 20 | 21 | 22 | ############################ 23 | "Validating parameters" 24 | 25 | if (![regex]::IsMatch($ServiceName, "^[a-z0-9-]+$")) { throw "ServiceName may only contain lowercase letters, numbers and dash (-), since it will be used for Azure resource names." } 26 | 27 | $templatePath = Join-Path "services" "_$Template" 28 | $newServicePath = Join-Path "services" $ServiceName 29 | 30 | 31 | if (!(Test-Path $templatePath)) { throw "The template $templatePath does not exist" } 32 | if (Test-Path $newServicePath) { throw "The service path $newServicePath already exists" } 33 | 34 | 35 | ############################ 36 | "Copying template folder" 37 | 38 | Copy-Item -Path $templatePath -Destination $newServicePath -Exclude bin,obj -Recurse 39 | 40 | 41 | ############################ 42 | "Renaming project folder" 43 | 44 | $oldProjectFolder = Get-ChildItem -Path $newServicePath -Directory 45 | 46 | $newProjectFolderName = $NamespaceName + $oldProjectFolder.Name.Substring($oldProjectFolder.Name.IndexOf(".")) 47 | $newProjectFolderPath = Join-Path $newServicePath $newProjectFolderName 48 | 49 | Move-Item $oldProjectFolder.FullName $newProjectFolderPath 50 | 51 | 52 | ############################ 53 | "Copying proto file (if it exists)" 54 | 55 | $oldProtoFileName = "_$Template.proto" 56 | $newProtoFileName = "$ServiceName.proto" 57 | $oldProtoPath = Join-Path "proto" $oldProtoFileName 58 | $newProtoPath = Join-Path "proto" $newProtoFileName 59 | if (Test-Path $oldProtoPath) { 60 | $protoContent = Get-Content $oldProtoPath 61 | $protoContent = $protoContent.Replace("csharp_namespace = ""$($oldProjectFolder.Name)""", "csharp_namespace = ""$newProjectFolderName""") 62 | $protoContent | Set-Content $newProtoPath 63 | } 64 | 65 | 66 | ############################ 67 | "Updating project file" 68 | 69 | $oldProjectFile = Get-ChildItem $newProjectFolderPath -Filter "*.csproj" 70 | $newProjectFilePath = Join-Path $newProjectFolderPath "$newProjectFolderName.csproj" 71 | 72 | $projectContent = Get-Content $oldProjectFile 73 | $projectContent = $projectContent.Replace($oldProtoFileName, $newProtoFileName) # proto 74 | $projectContent = $projectContent -replace "(?<=).*(?=<\/UserSecretsId>)", "aspnet-$newProjectFolderName-$((New-Guid).ToString().ToUpper())" # New random User Secrets ID 75 | 76 | $projectContent | Set-Content $newProjectFilePath 77 | Remove-Item $oldProjectFile 78 | 79 | 80 | ############################ 81 | "Updating solution file" 82 | 83 | $oldSlnPath = (Get-ChildItem $newServicePath -Filter "*.sln") 84 | $newSlnPath = Join-Path $newServicePath "$NamespaceName.sln" 85 | 86 | $slnContent = Get-Content $oldSlnPath.FullName 87 | $slnContent = $slnContent.Replace("""$($oldProjectFolder.Name)""", """$newProjectFolderName""") # project name 88 | $slnContent = $slnContent.Replace("$($oldProjectFile.Name)", "$newProjectFolderName.csproj") # project file 89 | $slnContent = $slnContent.Replace("$($oldProjectFolder.Name)", $newProjectFolderName) # Project folder 90 | 91 | $slnContent | Set-Content $newSlnPath 92 | Remove-Item $oldSlnPath 93 | 94 | 95 | ############################ 96 | "Replacing namespaces in C# files" 97 | 98 | $oldNamespace = $oldProjectFolder.Name 99 | $newNamespace = $newProjectFolderName 100 | 101 | $csharpFiles = Get-ChildItem -Path $newProjectFolderPath -Filter "*.cs" -Exclude bin,obj -Recurse -Depth 10 102 | foreach ($csharpFile in $csharpFiles) { 103 | #$csharpFile = $csharpFiles[0] 104 | $fileContent = Get-Content $csharpFile.FullName 105 | $fileContent = $fileContent.Replace("namespace $oldNamespace", "namespace $newNamespace") 106 | $fileContent = $fileContent.Replace("using $oldNamespace", "using $newNamespace") 107 | $fileContent | Set-Content $csharpFile.FullName 108 | } 109 | 110 | 111 | ############################ 112 | "Replacing namespaces in Razor files" 113 | 114 | $razorFiles = Get-ChildItem -Path $newProjectFolderPath -Filter "*.cshtml" -Exclude bin,obj -Recurse -Depth 10 115 | foreach ($razorFile in $razorFiles) { 116 | #$razorFile = $razorFiles[0] 117 | $fileContent = Get-Content $razorFile.FullName 118 | $fileContent = $fileContent.Replace("@model $oldNamespace", "@model $newNamespace") 119 | $fileContent = $fileContent.Replace("@namespace $oldNamespace", "@namespace $newNamespace") 120 | $fileContent = $fileContent.Replace("@using $oldNamespace", "@using $newNamespace") 121 | $fileContent | Set-Content $razorFile.FullName 122 | } 123 | 124 | 125 | ############################ 126 | "Adding project to global solution" 127 | 128 | dotnet sln add $newProjectFilePath 129 | if ($LASTEXITCODE -ne 0) { throw "Project could not be added to global solution" } 130 | 131 | 132 | ############################ 133 | "Creating GitHub workflow" 134 | 135 | $oldWorkflowPath = Join-Path ".github" "workflows" "service-$Template.yml" 136 | $newWorkflowPath = Join-Path ".github" "workflows" "service-$ServiceName.yml" 137 | 138 | $workflowContent = Get-Content $oldWorkflowPath 139 | 140 | $workflowContent[0] = "name: '$ServiceName'" 141 | $workflowContent = $workflowContent.Replace($oldProtoFileName, $newProtoFileName) # Proto file 142 | $workflowContent = $workflowContent.Replace($Template, $ServiceName) # service Name 143 | $workflowContent = $workflowContent.Replace("services/_", "services/") # service path 144 | $workflowContent = $workflowContent.Replace($oldProjectFolder.Name, $newProjectFolderName) 145 | 146 | $workflowContent | Set-Content $newWorkflowPath 147 | 148 | 149 | ############################ 150 | "Compiling service solution (to see if everything works)" 151 | 152 | dotnet build $newServicePath 153 | if ($LASTEXITCODE -ne 0) { throw "Build for new service failed." } 154 | 155 | 156 | ############################ 157 | # "Updating .\infrastructure\config.json" 158 | 159 | # TODO: We need a way to modify the JSON without removing the comments. ConvertFrom-Json & System.Text.Json drops them. 160 | # Newtonsoft.Json seems to keep them but we would have to store the DLL somewhere. 161 | 162 | # $configPath = Join-Path "infrastructure" "config.json" 163 | # $config = Get-Content $configPath | ConvertFrom-Json 164 | 165 | # $config | ConvertTo-Json | Set-Content $configPath 166 | 167 | "Done!" 168 | "" 169 | "You MUST add the new service to .\infrastructure\config.json - this does not yet happen automatically." 170 | 171 | -------------------------------------------------------------------------------- /docs/azure-environment.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cwe1ss/microservices-template/1dbe05fea5d758dc8c4b9737df5c7c38026d2b01/docs/azure-environment.png -------------------------------------------------------------------------------- /docs/azure-platform.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cwe1ss/microservices-template/1dbe05fea5d758dc8c4b9737df5c7c38026d2b01/docs/azure-platform.png -------------------------------------------------------------------------------- /docs/azure-service.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cwe1ss/microservices-template/1dbe05fea5d758dc8c4b9737df5c7c38026d2b01/docs/azure-service.png -------------------------------------------------------------------------------- /global.json: -------------------------------------------------------------------------------- 1 | { 2 | "sdk": { 3 | "version": "7.0.401", 4 | "rollForward": "latestPatch" 5 | } 6 | } -------------------------------------------------------------------------------- /infrastructure/README.md: -------------------------------------------------------------------------------- 1 | Contains all the code that is necessary to deploy the solution to Azure. 2 | -------------------------------------------------------------------------------- /infrastructure/_includes/helpers.ps1: -------------------------------------------------------------------------------- 1 | function Write-Success { 2 | param ( 3 | $Text 4 | ) 5 | 6 | Write-Host -ForegroundColor DarkGreen -NoNewline "✓ " 7 | Write-Host $Text 8 | } 9 | 10 | 11 | # https://github.com/psake/psake/blob/master/src/public/Exec.ps1 12 | function Exec { 13 | <# 14 | .SYNOPSIS 15 | Helper function for executing command-line programs. 16 | .DESCRIPTION 17 | This is a helper function that runs a scriptblock and checks the PS variable $lastexitcode to see if an error occcured. 18 | If an error is detected then an exception is thrown. 19 | This function allows you to run command-line programs without having to explicitly check fthe $lastexitcode variable. 20 | .PARAMETER cmd 21 | The scriptblock to execute. This scriptblock will typically contain the command-line invocation. 22 | .PARAMETER errorMessage 23 | The error message to display if the external command returned a non-zero exit code. 24 | .PARAMETER maxRetries 25 | The maximum number of times to retry the command before failing. 26 | .PARAMETER retryTriggerErrorPattern 27 | If the external command raises an exception, match the exception against this regex to determine if the command can be retried. 28 | If a match is found, the command will be retried provided [maxRetries] has not been reached. 29 | .PARAMETER workingDirectory 30 | The working directory to set before running the external command. 31 | .EXAMPLE 32 | exec { svn info $repository_trunk } "Error executing SVN. Please verify SVN command-line client is installed" 33 | This example calls the svn command-line client. 34 | .LINK 35 | Assert 36 | .LINK 37 | FormatTaskName 38 | .LINK 39 | Framework 40 | .LINK 41 | Get-PSakeScriptTasks 42 | .LINK 43 | Include 44 | .LINK 45 | Invoke-psake 46 | .LINK 47 | Properties 48 | .LINK 49 | Task 50 | .LINK 51 | TaskSetup 52 | .LINK 53 | TaskTearDown 54 | .LINK 55 | Properties 56 | #> 57 | [CmdletBinding()] 58 | param( 59 | [Parameter(Mandatory = $true)] 60 | [scriptblock]$cmd, 61 | 62 | [string]$errorMessage = ($msgs.error_bad_command -f $cmd), 63 | 64 | [int]$maxRetries = 0, 65 | 66 | [string]$retryTriggerErrorPattern = $null, 67 | 68 | [Alias("wd")] 69 | [string]$workingDirectory = $null 70 | ) 71 | 72 | $tryCount = 1 73 | 74 | do { 75 | try { 76 | 77 | if ($workingDirectory) { 78 | Push-Location -Path $workingDirectory 79 | } 80 | 81 | $global:lastexitcode = 0 82 | & $cmd 83 | if ($global:lastexitcode -ne 0) { 84 | throw "Exec: $errorMessage" 85 | } 86 | break 87 | } 88 | catch [Exception] { 89 | if ($tryCount -gt $maxRetries) { 90 | throw $_ 91 | } 92 | 93 | if ($retryTriggerErrorPattern -ne $null) { 94 | $isMatch = [regex]::IsMatch($_.Exception.Message, $retryTriggerErrorPattern) 95 | 96 | if ($isMatch -eq $false) { 97 | throw $_ 98 | } 99 | } 100 | 101 | "Try $tryCount failed, retrying again in 1 second..." 102 | 103 | $tryCount++ 104 | 105 | [System.Threading.Thread]::Sleep([System.TimeSpan]::FromSeconds(1)) 106 | } 107 | finally { 108 | if ($workingDirectory) { 109 | Pop-Location 110 | } 111 | } 112 | } 113 | while ($true) 114 | } 115 | -------------------------------------------------------------------------------- /infrastructure/bicepconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "analyzers": { 3 | "core": { 4 | "rules": { 5 | "use-recent-api-versions": { 6 | "level": "warning" 7 | } 8 | } 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /infrastructure/build-service.ps1: -------------------------------------------------------------------------------- 1 | [CmdletBinding()] 2 | Param ( 3 | 4 | [Parameter(Mandatory=$true)] 5 | [string]$ServiceName, 6 | 7 | [Parameter(Mandatory=$true)] 8 | [string]$ServicePath, 9 | 10 | [Parameter(Mandatory=$true)] 11 | [string]$HostProjectName, 12 | 13 | [Parameter(Mandatory=$true)] 14 | [string]$BuildNumber, 15 | 16 | [Parameter(Mandatory=$false)] 17 | [bool]$UploadArtifacts 18 | ) 19 | 20 | #$ServiceName = "internal-http-bus" 21 | #$ServicePath = "services/_internal-http-bus" 22 | #$HostProjectName = "InternalHttpBus.Api" 23 | #$BuildNumber = "1" 24 | #$UploadArtifacts = $false 25 | 26 | $ErrorActionPreference = "Stop" 27 | 28 | . .\_includes\helpers.ps1 29 | 30 | ############################ 31 | "" 32 | "Loading config" 33 | 34 | $names = Get-Content .\names.json | ConvertFrom-Json 35 | $config = Get-Content .\config.json | ConvertFrom-Json 36 | $serviceDefaults = $config.services | Select-Object -ExpandProperty $ServiceName 37 | 38 | # Naming conventions 39 | $platformGroupName = $($names.platformGroupName).Replace("{platform}", $config.platformAbbreviation) 40 | $platformContainerRegistryName = $($names.platformContainerRegistryName).Replace("{platform}", $config.platformAbbreviation).Replace("-", "") 41 | $platformStorageAccountName = $($names.platformStorageAccountName).Replace("{platform}", $config.platformAbbreviation).Replace("-", "").ToLower() 42 | $platformSqlMigrationStorageContainerName = $names.platformSqlMigrationStorageContainerName 43 | $svcArtifactContainerImageName = $($names.svcArtifactContainerImageName).Replace("{platform}", $config.platformAbbreviation).Replace("{service}", $serviceName) 44 | $svcArtifactSqlMigrationFile = $($names.svcArtifactSqlMigrationFile).Replace("{platform}", $config.platformAbbreviation).Replace("{service}", $serviceName).Replace("{buildNumber}", $BuildNumber) 45 | 46 | $registryServer = $platformContainerRegistryName + '.azurecr.io' 47 | 48 | $solutionFolder = (Get-Item (Join-Path "../" $ServicePath)).FullName 49 | $projectFolder = (Get-Item (Join-Path $solutionFolder $HostProjectName)).FullName 50 | 51 | 52 | ############################ 53 | "" 54 | "Restoring .NET tools" 55 | 56 | Exec { dotnet tool restore } 57 | 58 | 59 | ############################ 60 | "" 61 | "Restoring dependencies" 62 | 63 | Exec { dotnet restore "$solutionFolder" } 64 | 65 | 66 | ############################ 67 | "" 68 | "Building solution" 69 | 70 | Exec { dotnet build "$solutionFolder" -c Release --no-restore } 71 | 72 | 73 | ############################ 74 | # TODO: Running tests 75 | 76 | 77 | ############################ 78 | "" 79 | "Creating SQL migration file" 80 | 81 | if ($serviceDefaults.sqlDatabaseEnabled) { 82 | Exec { dotnet ef migrations script --configuration Release --no-build --idempotent -p "$projectFolder" -o "../artifacts/migration.sql" } 83 | } else { 84 | ".. SKIPPED (sqlDatabaseEnabled=false)" 85 | } 86 | 87 | 88 | ############################ 89 | "" 90 | "Creating docker image" 91 | Exec { dotnet publish "$projectFolder" -c Release --os linux --arch x64 -p:PublishProfile=DefaultContainer -p:ContainerImageName=$svcArtifactContainerImageName -p:ContainerImageTag=$BuildNumber } 92 | 93 | 94 | ############################ 95 | "" 96 | "Tagging docker image with Azure Container Registry" 97 | 98 | if ($UploadArtifacts) { 99 | Exec { docker tag "$($svcArtifactContainerImageName):$BuildNumber" "$registryServer/$($svcArtifactContainerImageName):$BuildNumber" } 100 | } else { 101 | ".. SKIPPED (UploadArtifacts=false)" 102 | } 103 | 104 | 105 | ############################ 106 | "" 107 | "Uploading SQL migration file" 108 | 109 | if (!$serviceDefaults.sqlDatabaseEnabled) { 110 | ".. SKIPPED (sqlDatabaseEnabled=false)" 111 | } elseif (!$UploadArtifacts) { 112 | ".. SKIPPED (UploadArtifacts=false)" 113 | } else { 114 | Get-AzStorageAccount -ResourceGroupName $platformGroupName -Name $platformStorageAccountName ` 115 | | Get-AzStorageContainer -Container $platformSqlMigrationStorageContainerName ` 116 | | Set-AzStorageBlobContent -File "../artifacts/migration.sql" -Blob $svcArtifactSqlMigrationFile -Force ` 117 | | Out-Null 118 | } 119 | 120 | 121 | ############################ 122 | "" 123 | "Pushing docker image to Azure Container Registry" 124 | 125 | if ($UploadArtifacts) { 126 | Exec { docker push "$registryServer/$($svcArtifactContainerImageName):$BuildNumber" } 127 | } else { 128 | ".. SKIPPED (UploadArtifacts=false)" 129 | } 130 | -------------------------------------------------------------------------------- /infrastructure/config.json: -------------------------------------------------------------------------------- 1 | { 2 | // All resources will be deployed into this Azure region. 3 | // You can get a list of all available region names via the PowerShell command `Get-AzLocation | Sort Location | Select DisplayName,Location` 4 | "location": "westeurope", 5 | 6 | // TEMPLATE_MUST_CHANGE: Every resource name in the 'platform'-group will use this abbreviation (see names.json). 7 | // Note that some resources do not allow "-". In that case, the "-" will be removed. 8 | // Keep this as short as possible, since some resource types have very short naming restrictions (e.g. 24 characters for storage accounts) 9 | "platformAbbreviation": "dm-px", 10 | 11 | // Defines the environment-independent settings for each service. 12 | "services": { 13 | // TEMPLATE_ADD_SERVICE: Each new service MUST be added here. 14 | // 15 | // Possible settings for each service: 16 | // * appType: "grpc" | "http" | "public" (required) 17 | // * serviceBusEnabled: Whether the service needs to access Azure Service Bus. (optional, defaults to false) 18 | // * serviceBusTopics: A list of "topics" that this service will *send* messages to. (optional) 19 | // * serviceBusSubscriptions: A list of "topics" that this service wants to *receive* messages from. (optional) 20 | // * sqlDatabaseEnabled: Whether the service needs its own Azure SQL Database. (optional, defaults to false) 21 | 22 | "internal-grpc": { 23 | "appType": "grpc" 24 | }, 25 | "internal-grpc-sql-bus": { 26 | "appType": "grpc", 27 | "serviceBusEnabled": true, 28 | "serviceBusTopics": [ 29 | "customer-created" 30 | ], 31 | "sqlDatabaseEnabled": true 32 | }, 33 | "internal-http-bus": { 34 | "appType": "http", 35 | "serviceBusEnabled": true, 36 | "serviceBusSubscriptions": [ 37 | "customer-created" 38 | ] 39 | }, 40 | "public-razor": { 41 | "appType": "public" 42 | } 43 | }, 44 | 45 | "environments": { 46 | // TEMPLATE_ADD_ENVIRONMENT: Each new environment MUST be added here. 47 | 48 | "development": { 49 | // TEMPLATE_MUST_CHANGE: Every resource in the environment will use this abbreviation (see names.json). 50 | // Note that some resources do not allow "-". In that case, the "-" will be removed. 51 | // Keep this as short as possible, since some resource types have very short naming restrictions (e.g. 24 characters for storage accounts) 52 | "environmentAbbreviation": "dm-px-dev", 53 | 54 | // Address prefix for the VNET. The container apps environment will be deployed into this VNET. 55 | // Do not overlap this with any of your existing IP ranges if you plan to peer the VNET with your existing infrastructure. 56 | "vnetAddressPrefix": "10.130.0.0/16", 57 | 58 | // The IP range for the container Apps environment. Must be part of the 'vnetAddressPrefix'. 59 | "appsSubnetAddressPrefix": "10.130.0.0/21", 60 | 61 | // The list of services that should be deployed into this environment. 62 | // (You do not have to deploy every service into every environment) 63 | "services": { 64 | // TEMPLATE_ADD_SERVICE: Each service MAY be added to any environment it should be deployed to. 65 | // 66 | // Possible settings for each service: 67 | // * app.cpu/app.memor: Must match a pre-defined combination. See https://docs.microsoft.com/en-us/azure/container-apps/containers#configuration 68 | // * app.cpu: "0.25" | "0.5" | "0.75" | "1.0" | "1.25" | "1.5" | "1.75" | "2.0" (optional, defaults to "0.25") 69 | // * app.memory: "0.5Gi" | "1.0Gi" | "1.5Gi" | "2.0Gi" | "2.5Gi" | "3.0Gi" | "3.5Gi" | "4.0Gi" (optional, defaults to "0.5Gi") 70 | // * app.minReplicas: Minimum number of container replicas that should always be running. (optional, defaults to 0) 71 | // * app.maxReplicas: Maximum number of container replicas. (optional, defaults to 10) 72 | // * app.concurrentRequests: A scale-out will happen when more concurrent requests occur (optional, defaults to 10) 73 | // * ingressExternal: Whether the service should have a public endpoint. (optional, defaults to false) 74 | 75 | "internal-grpc": { 76 | "app": { 77 | "cpu": "0.5", 78 | "memory": "1.0Gi", 79 | "minReplicas": 0, 80 | "maxReplicas": 1, 81 | "concurrentRequests": 15 82 | } 83 | }, 84 | "internal-grpc-sql-bus": { 85 | "app": { 86 | "cpu": "0.5", 87 | "memory": "1.0Gi", 88 | "minReplicas": 0, 89 | "maxReplicas": 2 90 | }, 91 | "sqlDatabase": { 92 | "skuName": "Basic", 93 | "skuTier": "Basic", 94 | "skuCapacity": 5 95 | } 96 | }, 97 | "internal-http-bus": { 98 | "ingressExternal": false, // Setting this to true would publicly expose the internal service (e.g. for initial test purposes) 99 | "app": { 100 | "cpu": "0.5", 101 | "memory": "1.0Gi", 102 | "minReplicas": 0, 103 | "maxReplicas": 2 104 | } 105 | }, 106 | "public-razor": { 107 | } 108 | } 109 | }, 110 | 111 | "production": { 112 | // TEMPLATE_MUST_CHANGE: Every resource in the environment will use this name abbreviation. 113 | "environmentAbbreviation": "dm-px-prd", 114 | "vnetAddressPrefix": "10.131.0.0/16", 115 | "appsSubnetAddressPrefix": "10.131.0.0/21", 116 | "services": { 117 | // TEMPLATE_ADD_SERVICE: Each service MAY be added to any environment it should be deployed to. 118 | } 119 | } 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /infrastructure/deploy-environment.ps1: -------------------------------------------------------------------------------- 1 | # Deploys Azure infrastructure resources that are shared by all services in one given environment. 2 | # Must be deployed before any service. 3 | 4 | [CmdletBinding()] 5 | Param ( 6 | 7 | [Parameter(Mandatory=$True)] 8 | [string]$Environment 9 | ) 10 | 11 | $ErrorActionPreference = "Stop" 12 | 13 | #$Environment = "development" 14 | 15 | 16 | ############################ 17 | "Loading config" 18 | 19 | $names = Get-Content .\names.json | ConvertFrom-Json 20 | $config = Get-Content .\config.json | ConvertFrom-Json 21 | $envConfig = $config.environments | Select-Object -ExpandProperty $Environment 22 | 23 | # Naming conventions 24 | $sqlAdminAdGroupName = $($names.sqlAdminAdGroupName).Replace("{environment}", $envConfig.environmentAbbreviation) 25 | 26 | 27 | ############################ 28 | "Loading Azure AD objects" 29 | 30 | $sqlAdminAdGroup = Get-AzAdGroup -DisplayName $sqlAdminAdGroupName 31 | if (!$sqlAdminAdGroup) { throw "AAD group '$sqlAdminAdGroupName' not found. Did you run 'init-platform.ps1' after you added the environment?" } 32 | 33 | 34 | ############################ 35 | "Registering Az providers" 36 | 37 | # New subscriptions that never deployed container apps before require to register the container service provider first. 38 | # Azure Portal and CLI are doing this automatically but Bicep is not. We therefore have to manually register the providers first. 39 | # https://github.com/microsoft/azure-container-apps/issues/451#issuecomment-1282628180 40 | # https://github.com/Azure/bicep/issues/3267 41 | 42 | "* Microsoft.App" 43 | Register-AzResourceProvider -ProviderNamespace Microsoft.App | Out-Null 44 | "* Microsoft.ContainerService" 45 | Register-AzResourceProvider -ProviderNamespace Microsoft.ContainerService | Out-Null 46 | 47 | 48 | ############################ 49 | "Deploying Azure resources" 50 | 51 | New-AzSubscriptionDeployment ` 52 | -Location $config.location ` 53 | -Name ("env-" + (Get-Date).ToString("yyyyMMddHHmmss")) ` 54 | -TemplateFile .\environment\main.bicep ` 55 | -TemplateParameterObject @{ 56 | environment = $Environment 57 | sqlAdminAdGroupId = $sqlAdminAdGroup.Id 58 | } ` 59 | -Verbose | Out-Null 60 | -------------------------------------------------------------------------------- /infrastructure/deploy-platform.ps1: -------------------------------------------------------------------------------- 1 | # Deploys Azure resources that are shared/used by all environments. 2 | # This must be deployed before any environment can be deployed. 3 | 4 | $ErrorActionPreference = "Stop" 5 | 6 | 7 | ############################ 8 | "Loading config" 9 | 10 | $config = Get-Content .\config.json | ConvertFrom-Json 11 | 12 | 13 | ############################ 14 | "Deploying Azure resources" 15 | 16 | New-AzSubscriptionDeployment ` 17 | -Location $config.location ` 18 | -Name ("platform-" + (Get-Date).ToString("yyyyMMddHHmmss")) ` 19 | -TemplateFile .\platform\main.bicep ` 20 | -TemplateParameterObject @{ 21 | deployGitHubIdentity = $false 22 | } ` 23 | -Verbose | Out-Null 24 | -------------------------------------------------------------------------------- /infrastructure/deploy-service.ps1: -------------------------------------------------------------------------------- 1 | # Deploys all Azure resources that are used by one single service. 2 | # It also adds some resources to the environment (e.g. SQL database) and platform (role assignments). 3 | 4 | [CmdletBinding()] 5 | Param ( 6 | 7 | [Parameter(Mandatory=$True)] 8 | [string]$Environment, 9 | 10 | [Parameter(Mandatory=$True)] 11 | [string]$ServiceName, 12 | 13 | [Parameter(Mandatory=$True)] 14 | [string]$BuildNumber 15 | ) 16 | 17 | $ErrorActionPreference = "Stop" 18 | 19 | #$Environment = "development" 20 | #$ServiceName = "customers" 21 | #$BuildNumber = "27" 22 | 23 | 24 | ############################ 25 | "Loading config" 26 | 27 | $config = Get-Content .\config.json | ConvertFrom-Json 28 | 29 | 30 | ############################ 31 | "Deploying Azure resources" 32 | 33 | New-AzSubscriptionDeployment ` 34 | -Location $config.location ` 35 | -Name ("svc-" + (Get-Date).ToString("yyyyMMddHHmmss")) ` 36 | -TemplateFile .\service\main.bicep ` 37 | -TemplateParameterObject @{ 38 | environment = $Environment 39 | serviceName = $ServiceName 40 | buildNumber = $buildNumber 41 | } ` 42 | -Verbose | Out-Null 43 | -------------------------------------------------------------------------------- /infrastructure/environment/app-environment.bicep: -------------------------------------------------------------------------------- 1 | // Deploys the "Container Apps Environment". 2 | // The "container app" for each service will be deployed into its own resource group by the service. 3 | // 4 | // The following resources will be deployed: 5 | // * An "Azure Container Apps environment", used to store all service apps. 6 | 7 | param location string 8 | param tags object 9 | 10 | 11 | /////////////////////////////////// 12 | // Resource names 13 | 14 | param platformGroupName string 15 | param platformLogsName string 16 | param diagnosticSettingsName string 17 | param networkGroupName string 18 | param networkVnetName string 19 | param networkSubnetAppsName string 20 | param monitoringGroupName string 21 | param monitoringAppInsightsName string 22 | param appEnvName string 23 | 24 | 25 | /////////////////////////////////// 26 | // Existing resources 27 | 28 | var platformGroup = resourceGroup(platformGroupName) 29 | var networkGroup = resourceGroup(networkGroupName) 30 | var monitoringGroup = resourceGroup(monitoringGroupName) 31 | 32 | resource platformLogs 'Microsoft.OperationalInsights/workspaces@2022-10-01' existing = { 33 | name: platformLogsName 34 | scope: platformGroup 35 | } 36 | 37 | resource networkVnet 'Microsoft.Network/virtualNetworks@2022-09-01' existing = { 38 | name: networkVnetName 39 | scope: networkGroup 40 | } 41 | 42 | resource networkSubnetApps 'Microsoft.Network/virtualNetworks/subnets@2022-09-01' existing = { 43 | name: networkSubnetAppsName 44 | parent: networkVnet 45 | } 46 | 47 | resource monitoringAppInsights 'Microsoft.Insights/components@2020-02-02' existing = { 48 | name: monitoringAppInsightsName 49 | scope: monitoringGroup 50 | } 51 | 52 | 53 | /////////////////////////////////// 54 | // New resources 55 | 56 | resource appEnv 'Microsoft.App/managedEnvironments@2022-10-01' = { 57 | name: appEnvName 58 | location: location 59 | tags: tags 60 | properties: { 61 | appLogsConfiguration: { 62 | destination: 'azure-monitor' 63 | } 64 | daprAIConnectionString: monitoringAppInsights.properties.ConnectionString 65 | daprAIInstrumentationKey: monitoringAppInsights.properties.InstrumentationKey 66 | vnetConfiguration: { 67 | internal: false 68 | infrastructureSubnetId: networkSubnetApps.id 69 | } 70 | } 71 | } 72 | 73 | resource appEnvDiagnosticSettings 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = { 74 | name: diagnosticSettingsName 75 | scope: appEnv 76 | properties: { 77 | workspaceId: platformLogs.id 78 | logs: [ 79 | { 80 | category: 'ContainerAppConsoleLogs' 81 | enabled: true 82 | } 83 | { 84 | category: 'ContainerAppSystemLogs' 85 | enabled: true 86 | } 87 | ] 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /infrastructure/environment/main.bicep: -------------------------------------------------------------------------------- 1 | // The main entry point for deploying all environment-specific infrastructure resources. 2 | 3 | targetScope = 'subscription' 4 | 5 | param now string = utcNow() 6 | param environment string 7 | param sqlAdminAdGroupId string 8 | 9 | 10 | /////////////////////////////////// 11 | // Configuration 12 | 13 | var names = loadJsonContent('./../names.json') 14 | var config = loadJsonContent('./../config.json') 15 | var envConfig = config.environments[environment] 16 | 17 | var tags = { 18 | product: config.platformAbbreviation 19 | environment: envConfig.environmentAbbreviation 20 | } 21 | 22 | 23 | /////////////////////////////////// 24 | // Resource names 25 | 26 | // Platform 27 | var platformGroupName = replace(names.platformGroupName, '{platform}', config.platformAbbreviation) 28 | var platformLogsName = replace(names.platformLogsName, '{platform}', config.platformAbbreviation) 29 | 30 | // Environment: Network 31 | var networkGroupName = replace(names.networkGroupName, '{environment}', envConfig.environmentAbbreviation) 32 | var networkVnetName = replace(names.networkVnetName, '{environment}', envConfig.environmentAbbreviation) 33 | var networkSubnetAppsName = replace(names.networkSubnetAppsName, '{environment}', envConfig.environmentAbbreviation) 34 | var networkNsgAppsName = replace(names.networkNsgAppsName, '{environment}', envConfig.environmentAbbreviation) 35 | 36 | // Environment: Monitoring 37 | var monitoringGroupName = replace(names.monitoringGroupName, '{environment}', envConfig.environmentAbbreviation) 38 | var monitoringAppInsightsName = replace(names.monitoringAppInsightsName, '{environment}', envConfig.environmentAbbreviation) 39 | var monitoringDashboardName = replace(names.monitoringDashboardName, '{environment}', envConfig.environmentAbbreviation) 40 | 41 | // Environment: SQL 42 | var sqlGroupName = replace(names.sqlGroupName, '{environment}', envConfig.environmentAbbreviation) 43 | var sqlServerAdminUserName = replace(names.sqlServerAdminName, '{environment}', envConfig.environmentAbbreviation) 44 | var sqlServerName = replace(names.sqlServerName, '{environment}', envConfig.environmentAbbreviation) 45 | var sqlAdminAdGroupName = replace(names.sqlAdminAdGroupName, '{environment}', envConfig.environmentAbbreviation) 46 | 47 | // Environment: Service Bus 48 | var serviceBusGroupName = replace(names.serviceBusGroupName, '{environment}', envConfig.environmentAbbreviation) 49 | var serviceBusNamespaceName = replace(names.serviceBusNamespaceName, '{environment}', envConfig.environmentAbbreviation) 50 | 51 | // Environment: Container Apps Environment 52 | var appEnvironmentGroupName = replace(names.appEnvironmentGroupName, '{environment}', envConfig.environmentAbbreviation) 53 | var appEnvironmentName = replace(names.appEnvironmentName, '{environment}', envConfig.environmentAbbreviation) 54 | 55 | 56 | /////////////////////////////////// 57 | // New resources 58 | 59 | resource networkGroup 'Microsoft.Resources/resourceGroups@2022-09-01' = { 60 | name: networkGroupName 61 | location: config.location 62 | tags: tags 63 | } 64 | 65 | resource monitoringGroup 'Microsoft.Resources/resourceGroups@2022-09-01' = { 66 | name: monitoringGroupName 67 | location: config.location 68 | tags: tags 69 | } 70 | 71 | resource serviceBusGroup 'Microsoft.Resources/resourceGroups@2022-09-01' = { 72 | name: serviceBusGroupName 73 | location: config.location 74 | tags: tags 75 | } 76 | 77 | resource appEnvGroup 'Microsoft.Resources/resourceGroups@2022-09-01' = { 78 | name: appEnvironmentGroupName 79 | location: config.location 80 | tags: tags 81 | } 82 | 83 | resource sqlGroup 'Microsoft.Resources/resourceGroups@2022-09-01' = { 84 | name: sqlGroupName 85 | location: config.location 86 | tags: tags 87 | } 88 | 89 | module networkResources 'network.bicep' = { 90 | name: 'env-network-${now}' 91 | scope: networkGroup 92 | params: { 93 | location: config.location 94 | environment: environment 95 | tags: tags 96 | 97 | // Resource names 98 | platformGroupName: platformGroupName 99 | platformLogsName: platformLogsName 100 | diagnosticSettingsName: names.diagnosticSettingsName 101 | networkVnetName: networkVnetName 102 | networkSubnetAppsName: networkSubnetAppsName 103 | networkNsgAppsName: networkNsgAppsName 104 | } 105 | } 106 | 107 | module monitoringResources 'monitoring.bicep' = { 108 | name: 'env-${now}' 109 | scope: monitoringGroup 110 | params: { 111 | location: config.location 112 | environment: environment 113 | tags: tags 114 | 115 | // Resource names 116 | platformGroupName: platformGroupName 117 | platformLogsName: platformLogsName 118 | monitoringAppInsightsName: monitoringAppInsightsName 119 | monitoringDashboardName: monitoringDashboardName 120 | serviceBusGroupName: serviceBusGroupName 121 | serviceBusNamespaceName: serviceBusNamespaceName 122 | } 123 | } 124 | 125 | 126 | module sqlResources 'sql.bicep' = { 127 | name: 'env-sql-${now}' 128 | scope: sqlGroup 129 | dependsOn: [ 130 | networkResources 131 | ] 132 | params: { 133 | location: config.location 134 | tags: tags 135 | sqlAdminAdGroupId: sqlAdminAdGroupId 136 | 137 | // Resource names 138 | platformGroupName: platformGroupName 139 | platformLogsName: platformLogsName 140 | diagnosticSettingsName: names.diagnosticSettingsName 141 | networkGroupName: networkGroupName 142 | networkVnetName: networkVnetName 143 | networkSubnetAppsName: networkSubnetAppsName 144 | sqlServerName: sqlServerName 145 | sqlServerAdminUserName: sqlServerAdminUserName 146 | sqlAdminAdGroupName: sqlAdminAdGroupName 147 | } 148 | } 149 | 150 | module serviceBusResources 'servicebus.bicep' = { 151 | name: 'env-bus-${now}' 152 | scope: serviceBusGroup 153 | params: { 154 | location: config.location 155 | tags: tags 156 | 157 | // Resource names 158 | serviceBusNamespaceName: serviceBusNamespaceName 159 | } 160 | } 161 | 162 | module appsResources 'app-environment.bicep' = { 163 | name: 'env-${now}' 164 | scope: appEnvGroup 165 | dependsOn: [ 166 | networkResources 167 | monitoringResources 168 | serviceBusResources 169 | ] 170 | params: { 171 | location: config.location 172 | tags: tags 173 | 174 | // Resource names 175 | platformGroupName: platformGroupName 176 | platformLogsName: platformLogsName 177 | diagnosticSettingsName: names.diagnosticSettingsName 178 | networkGroupName: networkGroupName 179 | networkVnetName: networkVnetName 180 | networkSubnetAppsName: networkSubnetAppsName 181 | monitoringGroupName: monitoringGroupName 182 | monitoringAppInsightsName: monitoringAppInsightsName 183 | appEnvName: appEnvironmentName 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /infrastructure/environment/monitoring.bicep: -------------------------------------------------------------------------------- 1 | param location string 2 | param environment string 3 | param tags object 4 | 5 | 6 | /////////////////////////////////// 7 | // Resource names 8 | 9 | param platformGroupName string 10 | param platformLogsName string 11 | param monitoringAppInsightsName string 12 | param monitoringDashboardName string 13 | param serviceBusGroupName string 14 | param serviceBusNamespaceName string 15 | 16 | 17 | /////////////////////////////////// 18 | // Configuration 19 | 20 | var names = loadJsonContent('./../names.json') 21 | var config = loadJsonContent('./../config.json') 22 | var envConfig = config.environments[environment] 23 | 24 | 25 | /////////////////////////////////// 26 | // Existing resources 27 | 28 | var platformGroup = resourceGroup(platformGroupName) 29 | 30 | resource platformLogs 'Microsoft.OperationalInsights/workspaces@2022-10-01' existing = { 31 | name: platformLogsName 32 | scope: platformGroup 33 | } 34 | 35 | 36 | /////////////////////////////////// 37 | // New resources 38 | 39 | @description('Application insights is targeted at a single environment so that you can properly use the application map etc, but data is stored in the global Log Analytics workspace.') 40 | resource appInsights 'Microsoft.Insights/components@2020-02-02' = { 41 | name: monitoringAppInsightsName 42 | location: location 43 | tags: tags 44 | kind: 'web' 45 | properties: { 46 | Application_Type: 'web' 47 | WorkspaceResourceId: platformLogs.id 48 | } 49 | } 50 | 51 | resource dashboard 'Microsoft.Portal/dashboards@2020-09-01-preview' = { 52 | name: monitoringDashboardName 53 | location: location 54 | tags: { 55 | 'hidden-title': monitoringDashboardName 56 | } 57 | properties: { 58 | lenses: [ 59 | { 60 | order: 0 61 | parts: [ 62 | // Sample for "ResourceGroupMapPinnedPart 63 | // { 64 | // position: { 65 | // x: 0 66 | // y: 0 67 | // colSpan: 4 68 | // rowSpan: 3 69 | // } 70 | // metadata: { 71 | // type: 'Extension/HubsExtension/PartType/ResourceGroupMapPinnedPart' 72 | // inputs: [ 73 | // { 74 | // name: 'resourceGroup' 75 | // isOptional: true 76 | // } 77 | // { 78 | // name: 'id' 79 | // value: resourceGroup().id 80 | // isOptional: true 81 | // } 82 | // ] 83 | // } 84 | // } 85 | 86 | // Sample for MarkdownPart 87 | // { 88 | // position: { 89 | // x: 4 90 | // y: 0 91 | // colSpan: 4 92 | // rowSpan: 3 93 | // } 94 | // metadata: { 95 | // type: 'Extension/HubsExtension/PartType/MarkdownPart' 96 | // inputs: [] 97 | // settings: { 98 | // content: { 99 | // settings: { 100 | // title: 'Title' 101 | // subtitle: 'Subtitle' 102 | // content: 'Content' 103 | // } 104 | // } 105 | // } 106 | // } 107 | // } 108 | 109 | // Replica Count per Service 110 | { 111 | position: { 112 | x: 0 113 | y: 0 114 | colSpan: 6 115 | rowSpan: 3 116 | } 117 | metadata: { 118 | type: 'Extension/HubsExtension/PartType/MonitorChartPart' 119 | inputs: [ 120 | { 121 | name: 'options' 122 | isOptional: true 123 | } 124 | { 125 | name: 'sharedTimeRange' 126 | isOptional: true 127 | } 128 | ] 129 | settings: { 130 | content: { 131 | options: { 132 | chart: { 133 | title: 'Max Replica Count per Service' 134 | titleKind: 1 135 | visualization: { 136 | disablePinning: true 137 | } 138 | metrics: [ for item in (contains(envConfig, 'services') ? items(envConfig.services) : []): { 139 | resourceMetadata: { 140 | id: resourceId( 141 | replace(replace(names.svcGroupName, '{environment}', envConfig.environmentAbbreviation), '{service}', item.key), 142 | 'Microsoft.App/containerApps', 143 | take(replace(replace(names.svcAppName, '{environment}', envConfig.environmentAbbreviation), '{service}', item.key), 32 /* max allowed length */) 144 | ) 145 | } 146 | name: 'Replicas' 147 | aggregationType: 3 148 | namespace: 'microsoft.app/containerapps' 149 | metricVisualization: { 150 | displayName: 'Replica Count' 151 | resourceDisplayName: item.key 152 | } 153 | } ] 154 | } 155 | } 156 | } 157 | } 158 | } 159 | } 160 | 161 | // Service Bus Deadletter messages 162 | { 163 | position: { 164 | x: 0 165 | y: 3 166 | colSpan: 6 167 | rowSpan: 3 168 | } 169 | metadata: { 170 | type: 'Extension/HubsExtension/PartType/MonitorChartPart' 171 | inputs: [ 172 | { 173 | name: 'options' 174 | value: { 175 | chart: { 176 | title: 'Max Count of dead-lettered messages in a Queue/Topic' 177 | titleKind: 1 178 | metrics: [ 179 | { 180 | resourceMetadata: { 181 | id: resourceId(serviceBusGroupName, 'Microsoft.ServiceBus/namespaces', serviceBusNamespaceName) 182 | } 183 | name: 'DeadletteredMessages' 184 | aggregationType: 3 185 | namespace: 'microsoft.servicebus/namespaces' 186 | metricVisualization: { 187 | displayName: 'Count of dead-lettered messages in a Queue/Topic' 188 | } 189 | } 190 | ] 191 | visualization: { 192 | chartType: 2 193 | legendVisualization: { 194 | isVisible: true 195 | position: 2 196 | hideSubtitle: false 197 | } 198 | axisVisualization: { 199 | x: { 200 | isVisible: true 201 | axisType: 2 202 | } 203 | y: { 204 | isVisible: true 205 | axisType: 1 206 | } 207 | } 208 | } 209 | grouping: { 210 | dimension: 'EntityName' 211 | sort: 1 212 | top: 50 213 | } 214 | timespan: { 215 | relative: { 216 | duration: 86400000 217 | } 218 | showUTCTime: false 219 | grain: 1 220 | } 221 | } 222 | } 223 | isOptional: true 224 | } 225 | { 226 | name: 'sharedTimeRange' 227 | isOptional: true 228 | } 229 | ] 230 | } 231 | } 232 | ] 233 | } 234 | ] 235 | metadata: { 236 | model: { 237 | timeRange: { 238 | value: { 239 | relative: { 240 | duration: 24 241 | timeUnit: 1 242 | } 243 | } 244 | type: 'MsPortalFx.Composition.Configuration.ValueTypes.TimeRange' 245 | } 246 | } 247 | } 248 | } 249 | } 250 | -------------------------------------------------------------------------------- /infrastructure/environment/network.bicep: -------------------------------------------------------------------------------- 1 | param location string 2 | param environment string 3 | param tags object 4 | 5 | 6 | /////////////////////////////////// 7 | // Resource names 8 | 9 | param platformGroupName string 10 | param platformLogsName string 11 | param diagnosticSettingsName string 12 | param networkVnetName string 13 | param networkSubnetAppsName string 14 | param networkNsgAppsName string 15 | 16 | 17 | /////////////////////////////////// 18 | // Configuration 19 | 20 | var config = loadJsonContent('./../config.json') 21 | var envConfig = config.environments[environment] 22 | 23 | 24 | /////////////////////////////////// 25 | // Existing resources 26 | 27 | var platformGroup = resourceGroup(platformGroupName) 28 | 29 | resource platformLogs 'Microsoft.OperationalInsights/workspaces@2022-10-01' existing = { 30 | name: platformLogsName 31 | scope: platformGroup 32 | } 33 | 34 | 35 | /////////////////////////////////// 36 | // New resources 37 | 38 | // https://learn.microsoft.com/en-us/azure/container-apps/firewall-integration#nsg-allow-rules 39 | resource appsNsg 'Microsoft.Network/networkSecurityGroups@2022-09-01' = { 40 | name: networkNsgAppsName 41 | location: location 42 | tags: tags 43 | properties: { 44 | securityRules: [ 45 | // Inbound rules 46 | { 47 | name: 'AllowInternetHttpInbound' 48 | properties: { 49 | priority: 1010 50 | direction: 'Inbound' 51 | description: 'Required for public HTTP->HTTPS redirects' 52 | sourceAddressPrefix: 'Internet' 53 | sourcePortRange: '*' 54 | destinationAddressPrefix: '*' 55 | destinationPortRange: '80' 56 | protocol: 'TCP' 57 | access: 'Allow' 58 | } 59 | } 60 | { 61 | name: 'AllowInternetHttpsInbound' 62 | properties: { 63 | priority: 1020 64 | direction: 'Inbound' 65 | description: 'Required for public HTTPS ingress' 66 | sourceAddressPrefix: 'Internet' 67 | sourcePortRange: '*' 68 | destinationAddressPrefix: '*' 69 | destinationPortRange: '443' 70 | protocol: 'TCP' 71 | access: 'Allow' 72 | } 73 | } 74 | // Outbound rules 75 | { 76 | // https://azure.github.io/PSRule.Rules.Azure/en/rules/Azure.NSG.LateralTraversal/ 77 | name: 'DenyRemoteAccessOutbound' 78 | properties: { 79 | priority: 2010 80 | direction: 'Outbound' 81 | sourceAddressPrefix: 'VirtualNetwork' 82 | sourcePortRange: '*' 83 | destinationAddressPrefix: '*' 84 | destinationPortRanges: [ 85 | '22' 86 | '3389' 87 | ] 88 | protocol: 'TCP' 89 | access: 'Deny' 90 | } 91 | } 92 | ] 93 | } 94 | } 95 | 96 | resource appsNsgDiagnosticSettings 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = { 97 | name: diagnosticSettingsName 98 | scope: appsNsg 99 | properties: { 100 | workspaceId: platformLogs.id 101 | logs: [ 102 | { 103 | categoryGroup: 'allLogs' 104 | enabled: true 105 | } 106 | ] 107 | } 108 | } 109 | 110 | resource vnet 'Microsoft.Network/virtualNetworks@2022-09-01' = { 111 | name: networkVnetName 112 | location: location 113 | tags: tags 114 | properties: { 115 | addressSpace: { 116 | addressPrefixes: [ 117 | envConfig.vnetAddressPrefix 118 | ] 119 | } 120 | subnets: [ 121 | { 122 | name: networkSubnetAppsName 123 | properties: { 124 | addressPrefix: envConfig.appsSubnetAddressPrefix 125 | networkSecurityGroup: { 126 | id: appsNsg.id 127 | } 128 | serviceEndpoints: [ 129 | // TODO: Add any other service endpoints you require 130 | { 131 | service: 'Microsoft.KeyVault' 132 | locations: [ 133 | '${location}' 134 | ] 135 | } 136 | { 137 | service: 'Microsoft.Storage' 138 | locations: [ 139 | '${location}' 140 | ] 141 | } 142 | { 143 | service: 'Microsoft.Sql' 144 | locations: [ 145 | '${location}' 146 | ] 147 | } 148 | ] 149 | } 150 | } 151 | ] 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /infrastructure/environment/servicebus.bicep: -------------------------------------------------------------------------------- 1 | // The entire environment uses one shared Service Bus namespace. 2 | // The necessary queues & topics are added by the service deployments. 3 | 4 | param location string 5 | param tags object 6 | 7 | 8 | /////////////////////////////////// 9 | // Resource names 10 | 11 | param serviceBusNamespaceName string 12 | 13 | 14 | /////////////////////////////////// 15 | // New resources 16 | 17 | resource serviceBusNamespace 'Microsoft.ServiceBus/namespaces@2022-10-01-preview' = { 18 | name: serviceBusNamespaceName 19 | location: location 20 | tags: tags 21 | sku: { 22 | name: 'Standard' 23 | tier: 'Standard' 24 | } 25 | properties: { 26 | minimumTlsVersion: '1.2' 27 | publicNetworkAccess: 'Enabled' 28 | disableLocalAuth: true 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /infrastructure/environment/sql-identity-resources.bicep: -------------------------------------------------------------------------------- 1 | param location string 2 | param tags object 3 | 4 | /////////////////////////////////// 5 | // Resource names 6 | 7 | param sqlServerAdminUserName string 8 | 9 | 10 | /////////////////////////////////// 11 | // New resources 12 | 13 | resource sqlIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = { 14 | name: sqlServerAdminUserName 15 | location: location 16 | tags: tags 17 | } 18 | 19 | 20 | /////////////////////////////////// 21 | // Outputs 22 | 23 | output sqlIdentityClientId string = sqlIdentity.properties.clientId 24 | output sqlIdentityPrincipalId string = sqlIdentity.properties.principalId 25 | -------------------------------------------------------------------------------- /infrastructure/environment/sql-identity.bicep: -------------------------------------------------------------------------------- 1 | targetScope = 'subscription' 2 | 3 | param now string = utcNow() 4 | param environment string 5 | 6 | 7 | /////////////////////////////////// 8 | // Configuration 9 | 10 | var names = loadJsonContent('./../names.json') 11 | var config = loadJsonContent('./../config.json') 12 | var envConfig = config.environments[environment] 13 | 14 | var tags = { 15 | product: config.platformAbbreviation 16 | environment: envConfig.environmentAbbreviation 17 | } 18 | 19 | 20 | /////////////////////////////////// 21 | // Resource names 22 | 23 | var sqlGroupName = replace(names.sqlGroupName, '{environment}', envConfig.environmentAbbreviation) 24 | var sqlServerAdminUserName = replace(names.sqlServerAdminName, '{environment}', envConfig.environmentAbbreviation) 25 | 26 | 27 | /////////////////////////////////// 28 | // New resources 29 | 30 | @description('The SQL group contains the SQL server, its identity and its databases') 31 | resource sqlGroup 'Microsoft.Resources/resourceGroups@2022-09-01' = { 32 | name: sqlGroupName 33 | location: config.location 34 | tags: tags 35 | } 36 | 37 | @description('The managed identity that will be used by the SQL server') 38 | module sqlIdentity 'sql-identity-resources.bicep' = { 39 | name: 'sql-${now}' 40 | scope: sqlGroup 41 | params: { 42 | location: config.location 43 | tags: tags 44 | 45 | // Resource names 46 | sqlServerAdminUserName: sqlServerAdminUserName 47 | } 48 | } 49 | 50 | 51 | /////////////////////////////////// 52 | // Outputs 53 | 54 | output sqlIdentityClientId string = sqlIdentity.outputs.sqlIdentityClientId 55 | output sqlIdentityPrincipalId string = sqlIdentity.outputs.sqlIdentityPrincipalId 56 | -------------------------------------------------------------------------------- /infrastructure/environment/sql.bicep: -------------------------------------------------------------------------------- 1 | // The entire environment uses one shared SQL server instance. 2 | // The necessary databases are added by the service deployments. 3 | 4 | param location string 5 | param tags object 6 | param sqlAdminAdGroupId string 7 | 8 | 9 | /////////////////////////////////// 10 | // Resource names 11 | 12 | param platformGroupName string 13 | param platformLogsName string 14 | param diagnosticSettingsName string 15 | param networkGroupName string 16 | param networkVnetName string 17 | param networkSubnetAppsName string 18 | param sqlServerAdminUserName string 19 | param sqlServerName string 20 | param sqlAdminAdGroupName string 21 | 22 | 23 | /////////////////////////////////// 24 | // Existing resources 25 | 26 | var platformGroup = resourceGroup(platformGroupName) 27 | var networkGroup = resourceGroup(networkGroupName) 28 | 29 | resource platformLogs 'Microsoft.OperationalInsights/workspaces@2022-10-01' existing = { 30 | name: platformLogsName 31 | scope: platformGroup 32 | } 33 | 34 | resource networkVnet 'Microsoft.Network/virtualNetworks@2022-09-01' existing = { 35 | name: networkVnetName 36 | scope: networkGroup 37 | } 38 | 39 | resource networkSubnetApps 'Microsoft.Network/virtualNetworks/subnets@2022-09-01' existing = { 40 | name: networkSubnetAppsName 41 | parent: networkVnet 42 | } 43 | 44 | @description('The SQL identity must have been created beforehand via the init script.') 45 | resource sqlServerAdminUser 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' existing = { 46 | name: sqlServerAdminUserName 47 | } 48 | 49 | 50 | /////////////////////////////////// 51 | // New resources 52 | 53 | resource sqlServer 'Microsoft.Sql/servers@2022-08-01-preview' = { 54 | name: sqlServerName 55 | location: location 56 | tags: tags 57 | identity: { 58 | type: 'UserAssigned' 59 | userAssignedIdentities: { 60 | '${sqlServerAdminUser.id}': {} 61 | } 62 | } 63 | properties: { 64 | administrators: { 65 | administratorType: 'ActiveDirectory' 66 | azureADOnlyAuthentication: true 67 | principalType: 'Group' 68 | login: sqlAdminAdGroupName 69 | sid: sqlAdminAdGroupId 70 | tenantId: subscription().tenantId 71 | } 72 | minimalTlsVersion: '1.2' 73 | primaryUserAssignedIdentityId: sqlServerAdminUser.id 74 | publicNetworkAccess: 'Enabled' 75 | } 76 | } 77 | 78 | @description('Allows apps from the Container Apps-subnet to access the SQL server') 79 | resource appsVnetRule 'Microsoft.Sql/servers/virtualNetworkRules@2022-08-01-preview' = { 80 | name: networkSubnetAppsName 81 | parent: sqlServer 82 | properties: { 83 | ignoreMissingVnetServiceEndpoint: false 84 | virtualNetworkSubnetId: networkSubnetApps.id 85 | } 86 | } 87 | 88 | // TODO We currently need this because the container instances created by the deploymentScripts can not yet be joined to a VNET. 89 | @description('Allows all Azure services to access the SQL server') 90 | resource allowAllWindowsAzureIps 'Microsoft.Sql/servers/firewallRules@2022-08-01-preview' = { 91 | name: 'AllowAllWindowsAzureIps' 92 | parent: sqlServer 93 | properties: { 94 | startIpAddress: '0.0.0.0' 95 | endIpAddress: '0.0.0.0' 96 | } 97 | } 98 | 99 | resource masterDb 'Microsoft.Sql/servers/databases@2022-08-01-preview' = { 100 | parent: sqlServer 101 | location: location 102 | name: 'master' 103 | properties: { 104 | } 105 | } 106 | 107 | resource masterDbDiagnosticSettings 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = { 108 | name: diagnosticSettingsName 109 | scope: masterDb 110 | properties: { 111 | workspaceId: platformLogs.id 112 | logs:[ 113 | { 114 | category: 'SQLSecurityAuditEvents' 115 | enabled: true 116 | } 117 | ] 118 | } 119 | } 120 | 121 | resource sqlAudit 'Microsoft.Sql/servers/auditingSettings@2022-08-01-preview'= { 122 | name: 'default' 123 | parent: sqlServer 124 | properties:{ 125 | auditActionsAndGroups:[ 126 | 'BATCH_COMPLETED_GROUP' 127 | 'SUCCESSFUL_DATABASE_AUTHENTICATION_GROUP' 128 | 'FAILED_DATABASE_AUTHENTICATION_GROUP' 129 | ] 130 | isAzureMonitorTargetEnabled: true 131 | state:'Enabled' 132 | } 133 | } 134 | 135 | @description('Enables Microsoft Defender for Azure SQL') 136 | resource sqlSecurity 'Microsoft.Sql/servers/securityAlertPolicies@2022-08-01-preview' = { 137 | name: 'Default' 138 | parent: sqlServer 139 | properties: { 140 | state: 'Enabled' 141 | emailAccountAdmins: true 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /infrastructure/names.json: -------------------------------------------------------------------------------- 1 | { 2 | // Parameters: 3 | // {platform} 4 | // {environment} 5 | // {service} 6 | // {buildNumber} 7 | // 8 | // Common abbreviation examples: 9 | // https://learn.microsoft.com/en-us/azure/cloud-adoption-framework/ready/azure-best-practices/resource-abbreviations 10 | 11 | "githubIdentityName": "{platform}-github-id", 12 | 13 | "platformGroupName": "{platform}-platform", 14 | "platformContainerRegistryName": "{platform}cr", 15 | "platformLogsName": "{platform}-log", 16 | "platformStorageAccountName": "{platform}st", 17 | 18 | "platformSqlMigrationStorageContainerName": "sql-migration", 19 | 20 | "networkGroupName": "{environment}-network", 21 | "networkVnetName": "{environment}-vnet", 22 | "networkNsgAppsName": "{environment}-apps-nsg", 23 | "networkSubnetAppsName": "apps-snet", 24 | 25 | "monitoringGroupName": "{environment}-monitoring", 26 | "monitoringAppInsightsName": "{environment}-appi", 27 | "monitoringDashboardName": "{environment}-dashboard", 28 | 29 | "serviceBusGroupName": "{environment}-bus", 30 | "serviceBusNamespaceName": "{environment}-bus", 31 | 32 | "sqlGroupName": "{environment}-sql", 33 | "sqlServerName": "{environment}-sql", 34 | "sqlServerAdminName": "{environment}-sql-admin-id", 35 | "sqlAdminAdGroupName": "{environment}-sql-admins", 36 | 37 | "appEnvironmentGroupName": "{environment}-env", 38 | "appEnvironmentName": "{environment}-env", 39 | 40 | "svcGroupName": "{environment}-svc-{service}", 41 | "svcUserName": "{environment}-{service}-id", 42 | "svcAppName": "{environment}-{service}", 43 | "svcKeyVaultName": "{environment}{service}kv", 44 | "svcStorageAccountName": "{environment}{service}st", 45 | "svcSqlDatabaseName": "{environment}-{service}-sqldb", 46 | "svcSqlDeployUserScriptName": "{environment}-{service}-sqldb-user-script", 47 | "svcSqlDeployMigrationScriptName": "{environment}-{service}-sqldb-migration-script", 48 | "svcDaprPubSubName": "pubsub-{service}", 49 | 50 | "svcDataProtectionStorageContainerName": "data-protection", 51 | "svcDataProtectionKeyName": "data-protection", 52 | 53 | "svcArtifactContainerImageName": "{platform}-{service}", 54 | "svcArtifactSqlMigrationFile": "{platform}-{service}-{buildNumber}.sql", 55 | 56 | "diagnosticSettingsName": "logs" 57 | } 58 | -------------------------------------------------------------------------------- /infrastructure/platform/github-identity-resources.bicep: -------------------------------------------------------------------------------- 1 | param location string 2 | param tags object 3 | param githubRepoNameWithOwner string 4 | param githubDefaultBranchName string 5 | 6 | 7 | /////////////////////////////////// 8 | // Resource names 9 | 10 | param githubIdentityName string 11 | 12 | 13 | /////////////////////////////////// 14 | // Configuration 15 | 16 | var config = loadJsonContent('./../config.json') 17 | 18 | // All credentials must be in one list as concurrent writes to /federatedIdentityCredentials are not allowed. 19 | var ghBranchCredentials = [{ 20 | name: 'github-branch-${githubDefaultBranchName}' 21 | subject: 'repo:${githubRepoNameWithOwner}:ref:refs/heads/${githubDefaultBranchName}' 22 | }] 23 | var ghPlatformCredentials = [{ 24 | name: 'github-env-platform' 25 | subject: 'repo:${githubRepoNameWithOwner}:environment:platform' 26 | }] 27 | var ghEnvironmentCredentials = [for item in items(config.environments): { 28 | name: 'github-env-${item.key}' 29 | subject: 'repo:${githubRepoNameWithOwner}:environment:${item.key}' 30 | }] 31 | var githubCredentials = concat(ghBranchCredentials, ghPlatformCredentials, ghEnvironmentCredentials) 32 | 33 | 34 | /////////////////////////////////// 35 | // New resources 36 | 37 | resource githubIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = { 38 | name: githubIdentityName 39 | location: location 40 | tags: tags 41 | } 42 | 43 | // Writing more than one credential concurrently fails with the following error: 44 | // "Concurrent Federated Identity Credentials writes under the same managed identity are not supported" 45 | // ErrorCode: "ConcurrentFederatedIdentityCredentialsWritesForSingleManagedIdentity" 46 | @batchSize(1) 47 | @description('Allows GitHub Actions to deploy from any of the configured environments') 48 | resource federatedCredentials 'Microsoft.ManagedIdentity/userAssignedIdentities/federatedIdentityCredentials@2023-01-31' = [for item in githubCredentials: { 49 | name: item.name 50 | parent: githubIdentity 51 | properties: { 52 | audiences: [ 53 | 'api://AzureADTokenExchange' 54 | ] 55 | issuer: 'https://token.actions.githubusercontent.com' 56 | subject: item.subject 57 | } 58 | }] 59 | 60 | 61 | /////////////////////////////////// 62 | // Outputs 63 | 64 | output githubIdentityClientId string = githubIdentity.properties.clientId 65 | output githubIdentityPrincipalId string = githubIdentity.properties.principalId 66 | -------------------------------------------------------------------------------- /infrastructure/platform/github-identity.bicep: -------------------------------------------------------------------------------- 1 | targetScope = 'subscription' 2 | 3 | param now string = utcNow() 4 | param tags object 5 | param githubRepoNameWithOwner string 6 | param githubDefaultBranchName string 7 | 8 | // Resource names 9 | param platformGroupName string 10 | param githubIdentityName string 11 | 12 | 13 | /////////////////////////////////// 14 | // Existing resources 15 | 16 | @description('This is the built-in Contributor role. See https://docs.microsoft.com/azure/role-based-access-control/built-in-roles#contributor ') 17 | resource contributorRoleDefinition 'Microsoft.Authorization/roleDefinitions@2022-04-01' existing = { 18 | scope: subscription() 19 | name: 'b24988ac-6180-42a0-ab88-20f7382dd24c' 20 | } 21 | 22 | @description('This is the built-in "User Access Administrator" role. See https://docs.microsoft.com/en-us/azure/role-based-access-control/built-in-roles#user-access-administrator ') 23 | resource userAccessAdministratorRoleDefinition 'Microsoft.Authorization/roleDefinitions@2022-04-01' existing = { 24 | scope: subscription() 25 | name: '18d7d88d-d35e-4fb5-a5c3-7773c20a72d9' 26 | } 27 | 28 | resource platformGroup 'Microsoft.Resources/resourceGroups@2022-09-01' existing = { 29 | name: platformGroupName 30 | } 31 | 32 | 33 | /////////////////////////////////// 34 | // New resources 35 | 36 | @description('The managed identity that will be used by GitHub to deploy Azure resources') 37 | module githubIdentity 'github-identity-resources.bicep' = { 38 | name: 'platform-github-${now}' 39 | scope: platformGroup 40 | params: { 41 | location: platformGroup.location 42 | tags: tags 43 | githubRepoNameWithOwner: githubRepoNameWithOwner 44 | githubDefaultBranchName: githubDefaultBranchName 45 | 46 | // Resource names 47 | githubIdentityName: githubIdentityName 48 | } 49 | } 50 | 51 | @description('The managed identity must be able to create & modify Azure resources in the subscription') 52 | resource githubIdentityContributor 'Microsoft.Authorization/roleAssignments@2022-04-01' = { 53 | name: guid(subscription().id, 'github', 'Contributor') 54 | properties: { 55 | roleDefinitionId: contributorRoleDefinition.id 56 | principalId: githubIdentity.outputs.githubIdentityPrincipalId 57 | principalType: 'ServicePrincipal' 58 | } 59 | } 60 | 61 | @description('The managed identity must be able to assign roles to other managed identities') 62 | resource githubIdentityUserAccessAdministrator 'Microsoft.Authorization/roleAssignments@2022-04-01' = { 63 | name: guid(subscription().id, 'github', 'UserAccessAdministrator') 64 | properties: { 65 | roleDefinitionId: userAccessAdministratorRoleDefinition.id 66 | principalId: githubIdentity.outputs.githubIdentityPrincipalId 67 | principalType: 'ServicePrincipal' 68 | } 69 | } 70 | 71 | 72 | /////////////////////////////////// 73 | // Outputs 74 | 75 | output githubIdentityClientId string = githubIdentity.outputs.githubIdentityClientId 76 | output githubIdentityPrincipalId string = githubIdentity.outputs.githubIdentityPrincipalId 77 | -------------------------------------------------------------------------------- /infrastructure/platform/main.bicep: -------------------------------------------------------------------------------- 1 | targetScope = 'subscription' 2 | 3 | param now string = utcNow() 4 | param deployGitHubIdentity bool 5 | param githubRepoNameWithOwner string = '' 6 | param githubDefaultBranchName string = '' 7 | 8 | 9 | /////////////////////////////////// 10 | // Configuration 11 | 12 | var names = loadJsonContent('./../names.json') 13 | var config = loadJsonContent('./../config.json') 14 | 15 | var tags = { 16 | product: config.platformAbbreviation 17 | } 18 | 19 | 20 | /////////////////////////////////// 21 | // Resource names 22 | 23 | var githubIdentityName = replace(names.githubIdentityName, '{platform}', config.platformAbbreviation) 24 | var platformGroupName = replace(names.platformGroupName, '{platform}', config.platformAbbreviation) 25 | var platformContainerRegistryName = replace(replace(names.platformContainerRegistryName, '{platform}', config.platformAbbreviation), '-', '') 26 | var platformLogsName = replace(names.platformLogsName, '{platform}', config.platformAbbreviation) 27 | var platformStorageAccountName = toLower(replace(replace(names.platformStorageAccountName, '{platform}', config.platformAbbreviation), '-', '')) 28 | 29 | 30 | /////////////////////////////////// 31 | // New resources 32 | 33 | resource platformGroup 'Microsoft.Resources/resourceGroups@2022-09-01' = { 34 | name: platformGroupName 35 | location: config.location 36 | tags: tags 37 | } 38 | 39 | @description('The managed identity that will be used by GitHub to deploy Azure resources') 40 | module githubIdentity 'github-identity.bicep' = if (deployGitHubIdentity) { 41 | name: 'platform-github-${now}' 42 | params: { 43 | tags: tags 44 | githubRepoNameWithOwner: githubRepoNameWithOwner 45 | githubDefaultBranchName: githubDefaultBranchName 46 | 47 | // Resource names 48 | githubIdentityName: githubIdentityName 49 | platformGroupName: platformGroup.name 50 | } 51 | } 52 | 53 | module platformResources 'resources.bicep' = { 54 | name: 'platform-${now}' 55 | scope: platformGroup 56 | dependsOn: [ 57 | githubIdentity 58 | ] 59 | params: { 60 | location: config.location 61 | tags: tags 62 | 63 | // Resource names 64 | githubIdentityName: githubIdentityName 65 | platformContainerRegistryName: platformContainerRegistryName 66 | platformLogsName: platformLogsName 67 | platformStorageAccountName: platformStorageAccountName 68 | sqlMigrationContainerName: names.platformSqlMigrationStorageContainerName 69 | } 70 | } 71 | 72 | 73 | output githubIdentityClientId string = deployGitHubIdentity ? githubIdentity.outputs.githubIdentityClientId : '' 74 | output githubIdentityPrincipalId string = deployGitHubIdentity ? githubIdentity.outputs.githubIdentityPrincipalId : '' 75 | output platformContainerRegistryUrl string = platformResources.outputs.platformContainerRegistryUrl 76 | -------------------------------------------------------------------------------- /infrastructure/platform/resources.bicep: -------------------------------------------------------------------------------- 1 | param location string 2 | param tags object 3 | 4 | 5 | /////////////////////////////////// 6 | // Resource names 7 | 8 | param githubIdentityName string 9 | param platformContainerRegistryName string 10 | param platformLogsName string 11 | param platformStorageAccountName string 12 | param sqlMigrationContainerName string 13 | 14 | 15 | /////////////////////////////////// 16 | // Existing resources 17 | 18 | resource githubIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' existing = { 19 | name: githubIdentityName 20 | } 21 | 22 | @description('This is the built-in Storage Blob Data Contributor role. See https://docs.microsoft.com/en-us/azure/role-based-access-control/built-in-roles#storage-blob-data-contributor ') 23 | resource storageBlobDataContributorRoleDefinition 'Microsoft.Authorization/roleDefinitions@2022-04-01' existing = { 24 | scope: subscription() 25 | name: 'ba92f5b4-2d11-453d-a403-e96b0029c9fe' 26 | } 27 | 28 | 29 | /////////////////////////////////// 30 | // New resources 31 | 32 | resource platformStorageAccount 'Microsoft.Storage/storageAccounts@2022-09-01' = { 33 | name: platformStorageAccountName 34 | location: location 35 | tags: tags 36 | kind: 'StorageV2' 37 | sku: { 38 | name: 'Standard_ZRS' 39 | } 40 | properties: { 41 | accessTier: 'Hot' 42 | minimumTlsVersion: 'TLS1_2' 43 | supportsHttpsTrafficOnly: true 44 | allowBlobPublicAccess: false 45 | } 46 | 47 | resource blobServices 'blobServices' = { 48 | name: 'default' 49 | properties: { 50 | deleteRetentionPolicy: { 51 | enabled: true 52 | allowPermanentDelete: true 53 | days: 7 54 | } 55 | } 56 | } 57 | } 58 | 59 | @description('A blob container that will be used to store any SQL migration scripts for all services') 60 | resource sqlMigrationContainer 'Microsoft.Storage/storageAccounts/blobServices/containers@2022-09-01' = { 61 | name: '${platformStorageAccount.name}/default/${sqlMigrationContainerName}' 62 | } 63 | 64 | @description('Allows GitHub to upload artifacts to the storage account') 65 | resource saAccessForGitHub 'Microsoft.Authorization/roleAssignments@2022-04-01' = { 66 | name: guid('githubStorageContributor', platformStorageAccount.id, githubIdentity.id) 67 | scope: platformStorageAccount 68 | properties: { 69 | roleDefinitionId: storageBlobDataContributorRoleDefinition.id 70 | principalId: githubIdentity.properties.principalId 71 | principalType: 'ServicePrincipal' 72 | } 73 | } 74 | 75 | @description('The container registry will store all container images for all services') 76 | resource platformContainerRegistry 'Microsoft.ContainerRegistry/registries@2023-01-01-preview' = { 77 | name: platformContainerRegistryName 78 | location: location 79 | tags: tags 80 | sku: { 81 | name: 'Basic' 82 | } 83 | properties: { 84 | adminUserEnabled: false 85 | } 86 | } 87 | 88 | @description('One global log analytics workspace is used to simplify the operations and querying') 89 | resource platformLogs 'Microsoft.OperationalInsights/workspaces@2022-10-01' = { 90 | name: platformLogsName 91 | location: location 92 | tags: tags 93 | properties: { 94 | retentionInDays: 30 95 | sku: { 96 | name: 'PerGB2018' 97 | } 98 | } 99 | } 100 | 101 | output platformContainerRegistryUrl string = platformContainerRegistry.properties.loginServer 102 | -------------------------------------------------------------------------------- /infrastructure/service/app-environment-pubsub.bicep: -------------------------------------------------------------------------------- 1 | param serviceName string 2 | 3 | 4 | /////////////////////////////////// 5 | // Resource names 6 | 7 | param appEnvironmentName string 8 | param serviceBusGroupName string 9 | param serviceBusNamespaceName string 10 | param svcGroupName string 11 | param svcUserName string 12 | param svcDaprPubSubName string 13 | 14 | 15 | /////////////////////////////////// 16 | // Existing resources 17 | 18 | var serviceBusGroup = resourceGroup(serviceBusGroupName) 19 | var svcGroup = resourceGroup(svcGroupName) 20 | 21 | resource appEnv 'Microsoft.App/managedEnvironments@2022-10-01' existing = { 22 | name: appEnvironmentName 23 | } 24 | 25 | resource serviceBusNamespace 'Microsoft.ServiceBus/namespaces@2022-10-01-preview' existing = { 26 | name: serviceBusNamespaceName 27 | scope: serviceBusGroup 28 | } 29 | 30 | resource svcUser 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' existing = { 31 | name: svcUserName 32 | scope: svcGroup 33 | } 34 | 35 | 36 | /////////////////////////////////// 37 | // New resources 38 | 39 | resource pubsubComponent 'Microsoft.App/managedEnvironments/daprComponents@2022-10-01' = { 40 | name: svcDaprPubSubName 41 | parent: appEnv 42 | properties: { 43 | // https://docs.dapr.io/reference/components-reference/supported-pubsub/setup-azure-servicebus/ 44 | componentType: 'pubsub.azure.servicebus' 45 | version: 'v1' 46 | metadata: [ 47 | { 48 | name: 'azureClientId' 49 | value: svcUser.properties.clientId 50 | } 51 | { 52 | name: 'namespaceName' 53 | // NOTE: Dapr expects just the domain name. 54 | value: replace(replace(serviceBusNamespace.properties.serviceBusEndpoint, 'https://', ''), ':443/', '') 55 | } 56 | { 57 | // Topics and subscriptions for the service are created during deployment by 'servicebus.bicep' (as configured in 'config.json') 58 | name: 'disableEntityManagement' 59 | value: 'true' 60 | } 61 | ] 62 | scopes: [ 63 | serviceName 64 | ] 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /infrastructure/service/app-grpc.bicep: -------------------------------------------------------------------------------- 1 | param location string 2 | param environment string 3 | param serviceName string 4 | param tags object 5 | 6 | 7 | /////////////////////////////////// 8 | // Resource names 9 | 10 | param platformGroupName string 11 | param platformContainerRegistryName string 12 | param appEnvGroupName string 13 | param appEnvName string 14 | param sqlGroupName string 15 | param sqlServerName string 16 | param sqlDatabaseName string 17 | param monitoringGroupName string 18 | param monitoringAppInsightsName string 19 | param svcUserName string 20 | param svcAppName string 21 | param svcArtifactContainerImageWithTag string 22 | 23 | 24 | /////////////////////////////////// 25 | // Configuration 26 | 27 | var config = loadJsonContent('./../config.json') 28 | var envConfig = config.environments[environment] 29 | var serviceDefaults = config.services[serviceName] 30 | var serviceConfig = envConfig.services[serviceName] 31 | 32 | 33 | /////////////////////////////////// 34 | // Existing resources 35 | 36 | var platformGroup = resourceGroup(platformGroupName) 37 | var envGroup = resourceGroup(appEnvGroupName) 38 | var monitoringGroup = resourceGroup(monitoringGroupName) 39 | var sqlGroup = resourceGroup(sqlGroupName) 40 | 41 | resource platformContainerRegistry 'Microsoft.ContainerRegistry/registries@2023-01-01-preview' existing = { 42 | name: platformContainerRegistryName 43 | scope: platformGroup 44 | } 45 | 46 | resource appEnv 'Microsoft.App/managedEnvironments@2022-10-01' existing = { 47 | name: appEnvName 48 | scope: envGroup 49 | } 50 | 51 | resource monitoringAppInsights 'Microsoft.Insights/components@2020-02-02' existing = { 52 | name: monitoringAppInsightsName 53 | scope: monitoringGroup 54 | } 55 | 56 | resource sqlServer 'Microsoft.Sql/servers@2022-08-01-preview' existing = { 57 | name: sqlServerName 58 | scope: sqlGroup 59 | } 60 | 61 | resource sqlDatabase 'Microsoft.Sql/servers/databases@2022-08-01-preview' existing = { 62 | name: sqlDatabaseName 63 | scope: sqlGroup 64 | } 65 | 66 | resource svcUser 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' existing = { 67 | name: svcUserName 68 | } 69 | 70 | 71 | /////////////////////////////////// 72 | // Configuration values 73 | 74 | var sqlDatabaseEnabled = contains(serviceDefaults, 'sqlDatabaseEnabled') ? serviceDefaults.sqlDatabaseEnabled : false 75 | var sqlConnectionString = sqlDatabaseEnabled ? 'Server=${sqlServer.properties.fullyQualifiedDomainName};Database=${sqlDatabase.name};User Id=${svcUser.properties.clientId};Authentication=Active Directory Managed Identity;Connect Timeout=60' : '' 76 | 77 | 78 | /////////////////////////////////// 79 | // New resources 80 | 81 | // TODO: It's not currently possible to dynamically create the environment variables array. 82 | // https://github.com/microsoft/azure-container-apps/issues/391 83 | 84 | resource containerApp 'Microsoft.App/containerApps@2022-10-01' = { 85 | name: svcAppName 86 | location: location 87 | tags: tags 88 | identity: { 89 | type: 'UserAssigned' 90 | userAssignedIdentities: { 91 | '${svcUser.id}': {} 92 | } 93 | } 94 | properties: { 95 | managedEnvironmentId: appEnv.id 96 | configuration: { 97 | dapr: { 98 | appId: serviceName 99 | appPort: 80 100 | appProtocol: 'grpc' 101 | enabled: true 102 | } 103 | ingress: { 104 | external: contains(serviceConfig, 'ingressExternal') ? serviceConfig.ingressExternal : false 105 | targetPort: 80 106 | transport: 'http2' 107 | } 108 | registries: [ 109 | { 110 | server: platformContainerRegistry.properties.loginServer 111 | identity: svcUser.id 112 | } 113 | ] 114 | secrets: [ 115 | ] 116 | } 117 | template: { 118 | containers: [ 119 | { 120 | image: '${platformContainerRegistry.properties.loginServer}/${svcArtifactContainerImageWithTag}' 121 | name: 'app' 122 | resources: { 123 | // TODO: Bicep expects an int even though a string is required. Remove any() if that ever changes. 124 | cpu: any(contains(serviceConfig, 'app') && contains(serviceConfig.app, 'cpu') ? '${serviceConfig.app.cpu}' : '0.25') 125 | memory: contains(serviceConfig, 'app') && contains(serviceConfig.app, 'memory') ? '${serviceConfig.app.memory}' : '0.5Gi' 126 | } 127 | probes: [ 128 | { 129 | type: 'Startup' 130 | httpGet: { 131 | path: '/healthz/startup' 132 | port: 8080 133 | scheme: 'HTTP' 134 | } 135 | initialDelaySeconds: 2 136 | periodSeconds: 2 137 | failureThreshold: 10 138 | } 139 | { 140 | type: 'Liveness' 141 | httpGet: { 142 | path: '/healthz/liveness' 143 | port: 8080 144 | scheme: 'HTTP' 145 | } 146 | periodSeconds: 10 147 | failureThreshold: 3 148 | } 149 | ] 150 | env: [ 151 | { 152 | // https://docs.dapr.io/reference/environment/ 153 | // This is used to set the service name in Application Insights 154 | name: 'APP_ID' 155 | value: serviceName 156 | } 157 | { 158 | // The Azure.Identity SDK needs the "ClientId" for the user-assigned identity, even if there is just one: 159 | // https://github.com/Azure/azure-sdk-for-net/issues/11400#issuecomment-620179175 160 | // If we don't set this, the authentication fails with the following error: https://github.com/Azure/azure-sdk-for-net/issues/13564 161 | name: 'AZURE_CLIENT_ID' 162 | value: svcUser.properties.clientId 163 | } 164 | { 165 | name: 'APPLICATIONINSIGHTS_CONNECTION_STRING' 166 | value: monitoringAppInsights.properties.ConnectionString 167 | } 168 | { 169 | // Will not actually be set if sqlConnectionString is empty 170 | name: 'ASPNETCORE_CONNECTIONSTRINGS__SQL' 171 | value: sqlConnectionString 172 | } 173 | { 174 | name: 'ASPNETCORE_Kestrel__Endpoints__GRPC__Protocols' 175 | value: 'Http2' 176 | } 177 | { 178 | name: 'ASPNETCORE_Kestrel__Endpoints__GRPC__URL' 179 | value: 'http://*:80' 180 | } 181 | { 182 | name: 'ASPNETCORE_Kestrel__Endpoints__WEB__Protocols' 183 | value: 'Http1' 184 | } 185 | { 186 | name: 'ASPNETCORE_Kestrel__Endpoints__WEB__URL' 187 | value: 'http://*:8080' 188 | } 189 | { 190 | // Console logs are sent to Azure Monitor. The default console logger outputs statements to multiple lines, so we use JSON instead. 191 | // https://docs.microsoft.com/en-us/dotnet/core/extensions/console-log-formatter#json 192 | name: 'Logging__Console__FormatterName' 193 | value: 'json' 194 | } 195 | { 196 | // Apps use the Application Insights SDK to log requests and exceptions, so we don't need to output anything to the console. 197 | name: 'Logging__Console__LogLevel__Default' 198 | value: 'Critical' 199 | } 200 | { 201 | // For troubleshooting purposes, we do however output app start/shutdown events. 202 | name: 'Logging__Console__LogLevel__Microsoft.Hosting.Lifetime' 203 | value: 'Information' 204 | } 205 | ] 206 | } 207 | ] 208 | scale: { 209 | minReplicas: contains(serviceConfig, 'app') && contains(serviceConfig.app, 'minReplicas') ? serviceConfig.app.minReplicas : 0 210 | maxReplicas: contains(serviceConfig, 'app') && contains(serviceConfig.app, 'maxReplicas') ? serviceConfig.app.maxReplicas : 10 // Azure default value 211 | rules: [ 212 | { 213 | name: 'http-rule' 214 | http: { 215 | metadata: { 216 | // https://docs.microsoft.com/en-us/azure/container-apps/scale-app#http 217 | // Value must be a string, otherwise it fails with error "ContainerAppInvalidSchema" 218 | concurrentRequests: contains(serviceConfig, 'app') && contains(serviceConfig.app, 'concurrentRequests') ? '${serviceConfig.app.concurrentRequests}' : '10' // Azure default value 219 | } 220 | } 221 | } 222 | ] 223 | } 224 | } 225 | } 226 | } 227 | -------------------------------------------------------------------------------- /infrastructure/service/app-http.bicep: -------------------------------------------------------------------------------- 1 | param location string 2 | param environment string 3 | param serviceName string 4 | param tags object 5 | 6 | 7 | /////////////////////////////////// 8 | // Resource names 9 | 10 | param platformGroupName string 11 | param platformContainerRegistryName string 12 | param appEnvGroupName string 13 | param appEnvName string 14 | param sqlGroupName string 15 | param sqlServerName string 16 | param sqlDatabaseName string 17 | param monitoringGroupName string 18 | param monitoringAppInsightsName string 19 | param svcUserName string 20 | param svcAppName string 21 | param svcArtifactContainerImageWithTag string 22 | 23 | 24 | /////////////////////////////////// 25 | // Configuration 26 | 27 | var config = loadJsonContent('./../config.json') 28 | var envConfig = config.environments[environment] 29 | var serviceDefaults = config.services[serviceName] 30 | var serviceConfig = envConfig.services[serviceName] 31 | 32 | 33 | /////////////////////////////////// 34 | // Existing resources 35 | 36 | var platformGroup = resourceGroup(platformGroupName) 37 | var envGroup = resourceGroup(appEnvGroupName) 38 | var monitoringGroup = resourceGroup(monitoringGroupName) 39 | var sqlGroup = resourceGroup(sqlGroupName) 40 | 41 | resource platformContainerRegistry 'Microsoft.ContainerRegistry/registries@2023-01-01-preview' existing = { 42 | name: platformContainerRegistryName 43 | scope: platformGroup 44 | } 45 | 46 | resource appEnv 'Microsoft.App/managedEnvironments@2022-10-01' existing = { 47 | name: appEnvName 48 | scope: envGroup 49 | } 50 | 51 | resource monitoringAppInsights 'Microsoft.Insights/components@2020-02-02' existing = { 52 | name: monitoringAppInsightsName 53 | scope: monitoringGroup 54 | } 55 | 56 | resource sqlServer 'Microsoft.Sql/servers@2022-08-01-preview' existing = { 57 | name: sqlServerName 58 | scope: sqlGroup 59 | } 60 | 61 | resource sqlDatabase 'Microsoft.Sql/servers/databases@2022-08-01-preview' existing = { 62 | name: sqlDatabaseName 63 | scope: sqlGroup 64 | } 65 | 66 | resource svcUser 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' existing = { 67 | name: svcUserName 68 | } 69 | 70 | 71 | /////////////////////////////////// 72 | // Configuration values 73 | 74 | var sqlDatabaseEnabled = contains(serviceDefaults, 'sqlDatabaseEnabled') ? serviceDefaults.sqlDatabaseEnabled : false 75 | var sqlConnectionString = sqlDatabaseEnabled ? 'Server=${sqlServer.properties.fullyQualifiedDomainName};Database=${sqlDatabase.name};User Id=${svcUser.properties.clientId};Authentication=Active Directory Managed Identity;Connect Timeout=60' : '' 76 | 77 | 78 | /////////////////////////////////// 79 | // New resources 80 | 81 | // TODO: It's not currently possible to dynamically create the environment variables array. 82 | // https://github.com/microsoft/azure-container-apps/issues/391 83 | 84 | resource containerApp 'Microsoft.App/containerApps@2022-10-01' = { 85 | name: svcAppName 86 | location: location 87 | tags: tags 88 | identity: { 89 | type: 'UserAssigned' 90 | userAssignedIdentities: { 91 | '${svcUser.id}': {} 92 | } 93 | } 94 | properties: { 95 | managedEnvironmentId: appEnv.id 96 | configuration: { 97 | dapr: { 98 | appId: serviceName 99 | appPort: 80 100 | appProtocol: 'http' 101 | enabled: true 102 | } 103 | ingress: { 104 | external: contains(serviceConfig, 'ingressExternal') ? serviceConfig.ingressExternal : false 105 | targetPort: 80 106 | transport: 'auto' 107 | } 108 | registries: [ 109 | { 110 | server: platformContainerRegistry.properties.loginServer 111 | identity: svcUser.id 112 | } 113 | ] 114 | secrets: [ 115 | ] 116 | } 117 | template: { 118 | containers: [ 119 | { 120 | image: '${platformContainerRegistry.properties.loginServer}/${svcArtifactContainerImageWithTag}' 121 | name: 'app' 122 | resources: { 123 | // TODO: Bicep expects an int even though a string is required. Remove any() if that ever changes. 124 | cpu: any(contains(serviceConfig, 'app') && contains(serviceConfig.app, 'cpu') ? '${serviceConfig.app.cpu}' : '0.25') 125 | memory: contains(serviceConfig, 'app') && contains(serviceConfig.app, 'memory') ? '${serviceConfig.app.memory}' : '0.5Gi' 126 | } 127 | probes: [ 128 | { 129 | type: 'Startup' 130 | httpGet: { 131 | path: '/healthz/startup' 132 | port: 80 133 | scheme: 'HTTP' 134 | } 135 | initialDelaySeconds: 2 136 | periodSeconds: 2 137 | failureThreshold: 10 138 | } 139 | { 140 | type: 'Liveness' 141 | httpGet: { 142 | path: '/healthz/liveness' 143 | port: 80 144 | scheme: 'HTTP' 145 | } 146 | periodSeconds: 10 147 | failureThreshold: 3 148 | } 149 | ] 150 | env: [ 151 | { 152 | // https://docs.dapr.io/reference/environment/ 153 | // This is used to set the service name in Application Insights 154 | name: 'APP_ID' 155 | value: serviceName 156 | } 157 | { 158 | // The Azure.Identity SDK needs the "ClientId" for the user-assigned identity, even if there is just one: 159 | // https://github.com/Azure/azure-sdk-for-net/issues/11400#issuecomment-620179175 160 | // If we don't set this, the authentication fails with the following error: https://github.com/Azure/azure-sdk-for-net/issues/13564 161 | name: 'AZURE_CLIENT_ID' 162 | value: svcUser.properties.clientId 163 | } 164 | { 165 | name: 'APPLICATIONINSIGHTS_CONNECTION_STRING' 166 | value: monitoringAppInsights.properties.ConnectionString 167 | } 168 | { 169 | // Will not actually be set if sqlConnectionString is empty 170 | name: 'ASPNETCORE_CONNECTIONSTRINGS__SQL' 171 | value: sqlConnectionString 172 | } 173 | { 174 | // Console logs are sent to Azure Monitor. The default console logger outputs statements to multiple lines, so we use JSON instead. 175 | // https://docs.microsoft.com/en-us/dotnet/core/extensions/console-log-formatter#json 176 | name: 'Logging__Console__FormatterName' 177 | value: 'json' 178 | } 179 | { 180 | // Apps use the Application Insights SDK to log requests and exceptions, so we don't need to output anything to the console. 181 | name: 'Logging__Console__LogLevel__Default' 182 | value: 'Critical' 183 | } 184 | { 185 | // For troubleshooting purposes, we do however output app start/shutdown events. 186 | name: 'Logging__Console__LogLevel__Microsoft.Hosting.Lifetime' 187 | value: 'Information' 188 | } 189 | ] 190 | } 191 | ] 192 | scale: { 193 | minReplicas: contains(serviceConfig, 'app') && contains(serviceConfig.app, 'minReplicas') ? serviceConfig.app.minReplicas : 0 194 | maxReplicas: contains(serviceConfig, 'app') && contains(serviceConfig.app, 'maxReplicas') ? serviceConfig.app.maxReplicas : 10 // Azure default value 195 | rules: [ 196 | { 197 | name: 'http-rule' 198 | http: { 199 | metadata: { 200 | // https://docs.microsoft.com/en-us/azure/container-apps/scale-app#http 201 | // Value must be a string, otherwise it fails with error "ContainerAppInvalidSchema" 202 | concurrentRequests: contains(serviceConfig, 'app') && contains(serviceConfig.app, 'concurrentRequests') ? '${serviceConfig.app.concurrentRequests}' : '10' // Azure default value 203 | } 204 | } 205 | } 206 | ] 207 | } 208 | } 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /infrastructure/service/app-public.bicep: -------------------------------------------------------------------------------- 1 | param location string 2 | param environment string 3 | param serviceName string 4 | param tags object 5 | param dataProtectionKeyUri string 6 | param dataProtectionBlobUri string 7 | 8 | 9 | /////////////////////////////////// 10 | // Resource names 11 | 12 | param platformGroupName string 13 | param platformContainerRegistryName string 14 | param appEnvGroupName string 15 | param appEnvName string 16 | param sqlGroupName string 17 | param sqlServerName string 18 | param sqlDatabaseName string 19 | param monitoringGroupName string 20 | param monitoringAppInsightsName string 21 | param svcUserName string 22 | param svcAppName string 23 | param svcArtifactContainerImageWithTag string 24 | 25 | 26 | /////////////////////////////////// 27 | // Configuration 28 | 29 | var config = loadJsonContent('./../config.json') 30 | var envConfig = config.environments[environment] 31 | var serviceDefaults = config.services[serviceName] 32 | var serviceConfig = envConfig.services[serviceName] 33 | 34 | 35 | /////////////////////////////////// 36 | // Existing resources 37 | 38 | var platformGroup = resourceGroup(platformGroupName) 39 | var envGroup = resourceGroup(appEnvGroupName) 40 | var monitoringGroup = resourceGroup(monitoringGroupName) 41 | var sqlGroup = resourceGroup(sqlGroupName) 42 | 43 | resource platformContainerRegistry 'Microsoft.ContainerRegistry/registries@2023-01-01-preview' existing = { 44 | name: platformContainerRegistryName 45 | scope: platformGroup 46 | } 47 | 48 | resource appEnv 'Microsoft.App/managedEnvironments@2022-10-01' existing = { 49 | name: appEnvName 50 | scope: envGroup 51 | } 52 | 53 | resource monitoringAppInsights 'Microsoft.Insights/components@2020-02-02' existing = { 54 | name: monitoringAppInsightsName 55 | scope: monitoringGroup 56 | } 57 | 58 | resource sqlServer 'Microsoft.Sql/servers@2022-08-01-preview' existing = { 59 | name: sqlServerName 60 | scope: sqlGroup 61 | } 62 | 63 | resource sqlDatabase 'Microsoft.Sql/servers/databases@2022-08-01-preview' existing = { 64 | name: sqlDatabaseName 65 | scope: sqlGroup 66 | } 67 | 68 | resource svcUser 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' existing = { 69 | name: svcUserName 70 | } 71 | 72 | 73 | /////////////////////////////////// 74 | // Configuration values 75 | 76 | var sqlDatabaseEnabled = contains(serviceDefaults, 'sqlDatabaseEnabled') ? serviceDefaults.sqlDatabaseEnabled : false 77 | var sqlConnectionString = sqlDatabaseEnabled ? 'Server=${sqlServer.properties.fullyQualifiedDomainName};Database=${sqlDatabase.name};User Id=${svcUser.properties.clientId};Authentication=Active Directory Managed Identity;Connect Timeout=60' : '' 78 | 79 | 80 | /////////////////////////////////// 81 | // New resources 82 | 83 | // TODO: It's not currently possible to dynamically create the environment variables array. 84 | // https://github.com/microsoft/azure-container-apps/issues/391 85 | 86 | resource containerApp 'Microsoft.App/containerApps@2022-10-01' = { 87 | name: svcAppName 88 | location: location 89 | tags: tags 90 | identity: { 91 | type: 'UserAssigned' 92 | userAssignedIdentities: { 93 | '${svcUser.id}': {} 94 | } 95 | } 96 | properties: { 97 | managedEnvironmentId: appEnv.id 98 | configuration: { 99 | dapr: { 100 | appId: serviceName 101 | appPort: 80 102 | appProtocol: 'http' 103 | enabled: true 104 | } 105 | ingress: { 106 | external: true 107 | targetPort: 80 108 | transport: 'auto' 109 | } 110 | registries: [ 111 | { 112 | server: platformContainerRegistry.properties.loginServer 113 | identity: svcUser.id 114 | } 115 | ] 116 | secrets: [ 117 | ] 118 | } 119 | template: { 120 | containers: [ 121 | { 122 | image: '${platformContainerRegistry.properties.loginServer}/${svcArtifactContainerImageWithTag}' 123 | name: 'app' 124 | resources: { 125 | // TODO: Bicep expects an int even though a string is required. Remove any() if that ever changes. 126 | cpu: any(contains(serviceConfig, 'app') && contains(serviceConfig.app, 'cpu') ? '${serviceConfig.app.cpu}' : '0.25') 127 | memory: contains(serviceConfig, 'app') && contains(serviceConfig.app, 'memory') ? '${serviceConfig.app.memory}' : '0.5Gi' 128 | } 129 | probes: [ 130 | { 131 | type: 'Startup' 132 | httpGet: { 133 | path: '/healthz/startup' 134 | port: 80 135 | scheme: 'HTTP' 136 | } 137 | initialDelaySeconds: 2 138 | periodSeconds: 2 139 | failureThreshold: 10 140 | } 141 | { 142 | type: 'Liveness' 143 | httpGet: { 144 | path: '/healthz/liveness' 145 | port: 80 146 | scheme: 'HTTP' 147 | } 148 | periodSeconds: 10 149 | failureThreshold: 3 150 | } 151 | ] 152 | env: [ 153 | { 154 | // https://docs.dapr.io/reference/environment/ 155 | // This is used to set the service name in Application Insights 156 | name: 'APP_ID' 157 | value: serviceName 158 | } 159 | { 160 | // The Azure.Identity SDK needs the "ClientId" for the user-assigned identity, even if there is just one: 161 | // https://github.com/Azure/azure-sdk-for-net/issues/11400#issuecomment-620179175 162 | // If we don't set this, the authentication fails with the following error: https://github.com/Azure/azure-sdk-for-net/issues/13564 163 | name: 'AZURE_CLIENT_ID' 164 | value: svcUser.properties.clientId 165 | } 166 | { 167 | name: 'APPLICATIONINSIGHTS_CONNECTION_STRING' 168 | value: monitoringAppInsights.properties.ConnectionString 169 | } 170 | { 171 | // Will not actually be set if sqlConnectionString is empty 172 | name: 'ASPNETCORE_CONNECTIONSTRINGS__SQL' 173 | value: sqlConnectionString 174 | } 175 | { 176 | // Console logs are sent to Azure Monitor. The default console logger outputs statements to multiple lines, so we use JSON instead. 177 | // https://docs.microsoft.com/en-us/dotnet/core/extensions/console-log-formatter#json 178 | name: 'Logging__Console__FormatterName' 179 | value: 'json' 180 | } 181 | { 182 | // Apps use the Application Insights SDK to log requests and exceptions, so we don't need to output anything to the console. 183 | name: 'Logging__Console__LogLevel__Default' 184 | value: 'Critical' 185 | } 186 | { 187 | // For troubleshooting purposes, we do however output app start/shutdown events. 188 | name: 'Logging__Console__LogLevel__Microsoft.Hosting.Lifetime' 189 | value: 'Information' 190 | } 191 | { 192 | // Used to store ASP.NET Core DataProtection keys 193 | name: 'ASPNETCORE_DataProtectionBlobUri' 194 | value: dataProtectionBlobUri 195 | } 196 | { 197 | // Used to encrypt ASP.NET Core DataProtection keys 198 | name: 'ASPNETCORE_DataProtectionKeyUri' 199 | value: dataProtectionKeyUri 200 | } 201 | ] 202 | } 203 | ] 204 | scale: { 205 | minReplicas: contains(serviceConfig, 'app') && contains(serviceConfig.app, 'minReplicas') ? serviceConfig.app.minReplicas : 0 206 | maxReplicas: contains(serviceConfig, 'app') && contains(serviceConfig.app, 'maxReplicas') ? serviceConfig.app.maxReplicas : 10 // Azure default value 207 | rules: [ 208 | { 209 | name: 'http-rule' 210 | http: { 211 | metadata: { 212 | // https://docs.microsoft.com/en-us/azure/container-apps/scale-app#http 213 | // Value must be a string, otherwise it fails with error "ContainerAppInvalidSchema" 214 | concurrentRequests: contains(serviceConfig, 'app') && contains(serviceConfig.app, 'concurrentRequests') ? '${serviceConfig.app.concurrentRequests}' : '10' // Azure default value 215 | } 216 | } 217 | } 218 | ] 219 | } 220 | } 221 | } 222 | } 223 | -------------------------------------------------------------------------------- /infrastructure/service/keyvault.bicep: -------------------------------------------------------------------------------- 1 | param location string 2 | param tags object 3 | 4 | 5 | /////////////////////////////////// 6 | // Resource names 7 | 8 | param platformGroupName string 9 | param platformLogsName string 10 | param diagnosticSettingsName string 11 | param networkGroupName string 12 | param networkVnetName string 13 | param networkSubnetAppsName string 14 | param svcGroupName string 15 | param svcUserName string 16 | param svcVaultName string 17 | param svcVaultDataProtectionKeyName string 18 | 19 | 20 | /////////////////////////////////// 21 | // Existing resources 22 | 23 | var platformGroup = resourceGroup(platformGroupName) 24 | var networkGroup = resourceGroup(networkGroupName) 25 | var svcGroup = resourceGroup(svcGroupName) 26 | 27 | resource platformLogs 'Microsoft.OperationalInsights/workspaces@2022-10-01' existing = { 28 | name: platformLogsName 29 | scope: platformGroup 30 | } 31 | 32 | resource networkVnet 'Microsoft.Network/virtualNetworks@2022-09-01' existing = { 33 | name: networkVnetName 34 | scope: networkGroup 35 | } 36 | 37 | resource networkSubnetApps 'Microsoft.Network/virtualNetworks/subnets@2022-09-01' existing = { 38 | name: networkSubnetAppsName 39 | parent: networkVnet 40 | } 41 | 42 | resource svcUser 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' existing = { 43 | name: svcUserName 44 | scope: svcGroup 45 | } 46 | 47 | @description('This is the built-in "Key Vault Crypto User" role. See https://docs.microsoft.com/en-us/azure/role-based-access-control/built-in-roles#key-vault-crypto-user ') 48 | resource keyVaultCryptoUserRole 'Microsoft.Authorization/roleDefinitions@2022-04-01' existing = { 49 | scope: subscription() 50 | name: '12338af0-0e69-4776-bea7-57ae8d297424' 51 | } 52 | 53 | @description('This is the built-in "Key Vault Secrets User" role. See https://docs.microsoft.com/en-us/azure/role-based-access-control/built-in-roles#key-vault-secrets-user ') 54 | resource keyVaultSecretsUserRole 'Microsoft.Authorization/roleDefinitions@2022-04-01' existing = { 55 | scope: subscription() 56 | name: '4633458b-17de-408a-b874-0445c86b69e6' 57 | } 58 | 59 | 60 | /////////////////////////////////// 61 | // New resources 62 | 63 | resource vault 'Microsoft.KeyVault/vaults@2022-11-01' = { 64 | name: svcVaultName 65 | location: location 66 | tags: tags 67 | properties: { 68 | sku: { 69 | family: 'A' 70 | name: 'standard' 71 | } 72 | tenantId: tenant().tenantId 73 | enabledForDeployment: false 74 | enabledForDiskEncryption: false 75 | enabledForTemplateDeployment: false 76 | enableRbacAuthorization: true 77 | enableSoftDelete: true 78 | softDeleteRetentionInDays: 30 79 | publicNetworkAccess: 'enabled' // TODO disable public network access 80 | networkAcls: { 81 | bypass: 'None' 82 | virtualNetworkRules: [ 83 | { 84 | id: networkSubnetApps.id 85 | } 86 | ] 87 | } 88 | } 89 | } 90 | 91 | // https://learn.microsoft.com/en-us/azure/key-vault/key-vault-insights-overview 92 | // https://azure.github.io/PSRule.Rules.Azure/en/rules/Azure.KeyVault.Logs/ 93 | // Store audit logs and enables the logs-based visualizations for Key Vault Insights. 94 | resource vaultDiagnosticSettings 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = { 95 | name: diagnosticSettingsName 96 | scope: vault 97 | properties: { 98 | workspaceId: platformLogs.id 99 | logs: [ 100 | { 101 | category: 'AuditEvent' 102 | enabled: true 103 | } 104 | ] 105 | } 106 | } 107 | 108 | resource dataProtectionKey 'Microsoft.KeyVault/vaults/keys@2022-11-01' = { 109 | name: svcVaultDataProtectionKeyName 110 | parent: vault 111 | tags: tags 112 | properties: { 113 | kty: 'RSA' 114 | keySize: 2048 115 | } 116 | } 117 | 118 | @description('Allows the service user to READ secrets') 119 | resource svcUserKeyVaultUser 'Microsoft.Authorization/roleAssignments@2022-04-01' = { 120 | name: guid('keyVaultSecretUser', svcUser.id) 121 | scope: vault 122 | properties: { 123 | roleDefinitionId: keyVaultSecretsUserRole.id 124 | principalId: svcUser.properties.principalId 125 | principalType: 'ServicePrincipal' 126 | } 127 | } 128 | 129 | @description('Allows the service user to USE the keys') 130 | resource svcUserCryptoUser 'Microsoft.Authorization/roleAssignments@2022-04-01' = { 131 | name: guid('keyVaultCryptoUser', svcUser.id) 132 | scope: vault 133 | properties: { 134 | roleDefinitionId: keyVaultCryptoUserRole.id 135 | principalId: svcUser.properties.principalId 136 | principalType: 'ServicePrincipal' 137 | } 138 | } 139 | 140 | 141 | output keyVaultUri string = vault.properties.vaultUri 142 | output dataProtectionKeyUri string = dataProtectionKey.properties.keyUri 143 | -------------------------------------------------------------------------------- /infrastructure/service/main.bicep: -------------------------------------------------------------------------------- 1 | // Contains the main entry point for deploying all Azure resources required by one service. 2 | 3 | targetScope = 'subscription' 4 | 5 | param now string = utcNow() 6 | param environment string 7 | param serviceName string 8 | param buildNumber string 9 | 10 | 11 | /////////////////////////////////// 12 | // Configuration 13 | 14 | var names = loadJsonContent('./../names.json') 15 | var config = loadJsonContent('./../config.json') 16 | var envConfig = config.environments[environment] 17 | var serviceDefaults = config.services[serviceName] 18 | 19 | var sqlDatabaseEnabled = contains(serviceDefaults, 'sqlDatabaseEnabled') ? serviceDefaults.sqlDatabaseEnabled : false 20 | var serviceBusEnabled = contains(serviceDefaults, 'serviceBusEnabled') ? serviceDefaults.serviceBusEnabled : false 21 | 22 | var tags = { 23 | product: config.platformAbbreviation 24 | environment: envConfig.environmentAbbreviation 25 | service: serviceName 26 | } 27 | 28 | 29 | /////////////////////////////////// 30 | // Resource names 31 | 32 | // Platform 33 | var platformGroupName = replace(names.platformGroupName, '{platform}', config.platformAbbreviation) 34 | var platformContainerRegistryName = replace(replace(names.platformContainerRegistryName, '{platform}', config.platformAbbreviation), '-', '') 35 | var platformLogsName = replace(names.platformLogsName, '{platform}', config.platformAbbreviation) 36 | var platformStorageAccountName = toLower(replace(replace(names.platformStorageAccountName, '{platform}', config.platformAbbreviation), '-', '')) 37 | 38 | // Environment: Network 39 | var networkGroupName = replace(names.networkGroupName, '{environment}', envConfig.environmentAbbreviation) 40 | var networkVnetName = replace(names.networkVnetName, '{environment}', envConfig.environmentAbbreviation) 41 | var networkSubnetAppsName = replace(names.networkSubnetAppsName, '{environment}', envConfig.environmentAbbreviation) 42 | 43 | // Environment: Monitoring 44 | var monitoringGroupName = replace(names.monitoringGroupName, '{environment}', envConfig.environmentAbbreviation) 45 | var monitoringAppInsightsName = replace(names.monitoringAppInsightsName, '{environment}', envConfig.environmentAbbreviation) 46 | 47 | // Environment: SQL 48 | var sqlGroupName = replace(names.sqlGroupName, '{environment}', envConfig.environmentAbbreviation) 49 | var sqlServerAdminUserName = replace(names.sqlServerAdminName, '{environment}', envConfig.environmentAbbreviation) 50 | var sqlServerName = replace(names.sqlServerName, '{environment}', envConfig.environmentAbbreviation) 51 | 52 | // Environment: Service Bus 53 | var serviceBusGroupName = replace(names.serviceBusGroupName, '{environment}', envConfig.environmentAbbreviation) 54 | var serviceBusNamespaceName = replace(names.serviceBusNamespaceName, '{environment}', envConfig.environmentAbbreviation) 55 | 56 | // Environment: Container Apps Environment 57 | var appEnvironmentGroupName = replace(names.appEnvironmentGroupName, '{environment}', envConfig.environmentAbbreviation) 58 | var appEnvironmentName = replace(names.appEnvironmentName, '{environment}', envConfig.environmentAbbreviation) 59 | 60 | // Service 61 | var svcGroupName = replace(replace(names.svcGroupName, '{environment}', envConfig.environmentAbbreviation), '{service}', serviceName) 62 | var svcUserName = replace(replace(names.svcUserName, '{environment}', envConfig.environmentAbbreviation), '{service}', serviceName) 63 | var svcAppName = take(replace(replace(names.svcAppName, '{environment}', envConfig.environmentAbbreviation), '{service}', serviceName), 32 /* max allowed length */) 64 | 65 | // Service: Storage 66 | var svcStorageAccountName = take(replace(replace(replace(names.svcStorageAccountName, '{environment}', envConfig.environmentAbbreviation), '{service}', serviceName), '-', ''), 24 /* max allowed length */) 67 | 68 | // Service: Key Vault 69 | var svcKeyVaultName = take(replace(replace(replace(names.svcKeyVaultName, '{environment}', envConfig.environmentAbbreviation), '{service}', serviceName), '-', ''), 24 /* max allowed length */) 70 | 71 | // Service: SQL 72 | var sqlDatabaseName = replace(replace(names.svcSqlDatabaseName, '{environment}', envConfig.environmentAbbreviation), '{service}', serviceName) 73 | var sqlDeployUserScriptName = replace(replace(names.svcSqlDeployUserScriptName, '{environment}', envConfig.environmentAbbreviation), '{service}', serviceName) 74 | var sqlDeployMigrationScriptName = replace(replace(names.svcSqlDeployMigrationScriptName, '{environment}', envConfig.environmentAbbreviation), '{service}', serviceName) 75 | 76 | // Service: Dapr 77 | var svcDaprPubSubName = replace(names.svcDaprPubSubName, '{service}', serviceName) 78 | 79 | // Service: Build artifacts 80 | var svcArtifactContainerImageWithTag = '${replace(replace(names.svcArtifactContainerImageName, '{platform}', config.platformAbbreviation), '{service}', serviceName)}:${buildNumber}' 81 | var svcArtifactSqlMigrationFile = replace(replace(replace(names.svcArtifactSqlMigrationFile, '{platform}', config.platformAbbreviation), '{service}', serviceName), '{buildNumber}', buildNumber) 82 | 83 | 84 | /////////////////////////////////// 85 | // Existing resources 86 | 87 | var platformGroup = resourceGroup(platformGroupName) 88 | var appEnvironmentGroup = resourceGroup(appEnvironmentGroupName) 89 | var sqlGroup = resourceGroup(sqlGroupName) 90 | var serviceBusGroup = resourceGroup(serviceBusGroupName) 91 | 92 | 93 | /////////////////////////////////// 94 | // New resources 95 | 96 | resource svcGroup 'Microsoft.Resources/resourceGroups@2022-09-01' = { 97 | name: svcGroupName 98 | location: config.location 99 | tags: tags 100 | } 101 | 102 | // Create the user assigned identity first, so that we can assign permissions to it before the rest of the service resources is created 103 | module svcIdentity 'service-identity.bicep' = { 104 | name: 'svc-identity-${now}' 105 | scope: svcGroup 106 | params: { 107 | location: config.location 108 | tags: tags 109 | 110 | // Resource names 111 | svcUserName: svcUserName 112 | } 113 | } 114 | 115 | // Allow the identity to access the platform container registry. 116 | // This must be done before we can create the actual container app, as the deployment would fail otherwise. 117 | module platform 'platform.bicep' = { 118 | name: 'svc-platform-${now}' 119 | scope: platformGroup 120 | dependsOn: [ 121 | svcIdentity 122 | ] 123 | params: { 124 | // Resource names 125 | platformContainerRegistryName: platformContainerRegistryName 126 | svcGroupName: svcGroupName 127 | svcUserName: svcUserName 128 | } 129 | } 130 | 131 | module svcStorage 'storage.bicep' = { 132 | name: 'svc-storage-${now}' 133 | scope: svcGroup 134 | dependsOn: [ 135 | svcIdentity 136 | ] 137 | params: { 138 | location: config.location 139 | tags: tags 140 | 141 | // Resource names 142 | networkGroupName: networkGroupName 143 | networkVnetName: networkVnetName 144 | networkSubnetAppsName: networkSubnetAppsName 145 | svcGroupName: svcGroupName 146 | svcUserName: svcUserName 147 | svcStorageAccountName: svcStorageAccountName 148 | svcStorageDataProtectionContainerName: names.svcDataProtectionStorageContainerName 149 | } 150 | } 151 | 152 | module svcVault 'keyvault.bicep' = { 153 | name: 'svc-vault-${now}' 154 | scope: svcGroup 155 | dependsOn: [ 156 | svcIdentity 157 | ] 158 | params: { 159 | location: config.location 160 | tags: tags 161 | 162 | // Resource names 163 | platformGroupName: platformGroupName 164 | platformLogsName: platformLogsName 165 | diagnosticSettingsName: names.diagnosticSettingsName 166 | networkGroupName: networkGroupName 167 | networkVnetName: networkVnetName 168 | networkSubnetAppsName: networkSubnetAppsName 169 | svcGroupName: svcGroupName 170 | svcUserName: svcUserName 171 | svcVaultName: svcKeyVaultName 172 | svcVaultDataProtectionKeyName: names.svcDataProtectionKeyName 173 | } 174 | } 175 | 176 | module svcSql 'sql.bicep' = if (sqlDatabaseEnabled) { 177 | name: 'svc-sql-${now}' 178 | scope: sqlGroup 179 | dependsOn: [ 180 | svcIdentity 181 | ] 182 | params: { 183 | location: config.location 184 | environment: environment 185 | serviceName: serviceName 186 | buildNumber: buildNumber 187 | tags: tags 188 | 189 | // Resource names 190 | platformGroupName: platformGroupName 191 | platformStorageAccountName: platformStorageAccountName 192 | sqlMigrationContainerName: names.platformSqlMigrationStorageContainerName 193 | sqlMigrationFile: svcArtifactSqlMigrationFile 194 | sqlServerName: sqlServerName 195 | sqlServerAdminUserName: sqlServerAdminUserName 196 | sqlDatabaseName: sqlDatabaseName 197 | svcGroupName: svcGroupName 198 | svcUserName: svcUserName 199 | sqlDeployUserScriptName: sqlDeployUserScriptName 200 | sqlDeployMigrationScriptName: sqlDeployMigrationScriptName 201 | } 202 | } 203 | 204 | module svcServiceBus 'servicebus.bicep' = if (serviceBusEnabled) { 205 | name: 'svc-bus-${now}' 206 | scope: serviceBusGroup 207 | dependsOn: [ 208 | svcIdentity 209 | ] 210 | params: { 211 | serviceName: serviceName 212 | 213 | // Resource names 214 | serviceBusNamespaceName: serviceBusNamespaceName 215 | svcGroupName: svcGroupName 216 | svcUserName: svcUserName 217 | } 218 | } 219 | 220 | module svcAppEnvPubSub 'app-environment-pubsub.bicep' = if (serviceBusEnabled) { 221 | name: 'svc-env-${now}' 222 | scope: appEnvironmentGroup 223 | dependsOn: [ 224 | svcServiceBus 225 | ] 226 | params: { 227 | serviceName: serviceName 228 | 229 | // Resource names 230 | appEnvironmentName: appEnvironmentName 231 | serviceBusGroupName: serviceBusGroupName 232 | serviceBusNamespaceName: serviceBusNamespaceName 233 | svcGroupName: svcGroupName 234 | svcUserName: svcUserName 235 | svcDaprPubSubName: svcDaprPubSubName 236 | } 237 | } 238 | 239 | module svcAppGrpc 'app-grpc.bicep' = if (serviceDefaults.appType == 'grpc') { 240 | name: 'svc-app-grpc-${now}' 241 | scope: svcGroup 242 | dependsOn: [ 243 | platform 244 | svcVault 245 | svcSql 246 | svcServiceBus 247 | svcAppEnvPubSub 248 | ] 249 | params: { 250 | location: config.location 251 | environment: environment 252 | serviceName: serviceName 253 | tags: tags 254 | 255 | // Resource names 256 | platformGroupName: platformGroupName 257 | platformContainerRegistryName: platformContainerRegistryName 258 | appEnvGroupName: appEnvironmentGroupName 259 | appEnvName: appEnvironmentName 260 | sqlGroupName: sqlGroupName 261 | sqlServerName: sqlServerName 262 | sqlDatabaseName: sqlDatabaseName 263 | monitoringGroupName: monitoringGroupName 264 | monitoringAppInsightsName: monitoringAppInsightsName 265 | svcUserName: svcUserName 266 | svcAppName: svcAppName 267 | svcArtifactContainerImageWithTag: svcArtifactContainerImageWithTag 268 | } 269 | } 270 | 271 | module svcAppHttp 'app-http.bicep' = if (serviceDefaults.appType == 'http') { 272 | name: 'svc-app-http-${now}' 273 | scope: svcGroup 274 | dependsOn: [ 275 | platform 276 | svcVault 277 | svcSql 278 | svcServiceBus 279 | svcAppEnvPubSub 280 | ] 281 | params: { 282 | location: config.location 283 | environment: environment 284 | serviceName: serviceName 285 | tags: tags 286 | 287 | // Resource names 288 | platformGroupName: platformGroupName 289 | platformContainerRegistryName: platformContainerRegistryName 290 | appEnvGroupName: appEnvironmentGroupName 291 | appEnvName: appEnvironmentName 292 | sqlGroupName: sqlGroupName 293 | sqlServerName: sqlServerName 294 | sqlDatabaseName: sqlDatabaseName 295 | monitoringGroupName: monitoringGroupName 296 | monitoringAppInsightsName: monitoringAppInsightsName 297 | svcUserName: svcUserName 298 | svcAppName: svcAppName 299 | svcArtifactContainerImageWithTag: svcArtifactContainerImageWithTag 300 | } 301 | } 302 | 303 | module svcAppPublic 'app-public.bicep' = if (serviceDefaults.appType == 'public') { 304 | name: 'svc-app-public-${now}' 305 | scope: svcGroup 306 | dependsOn: [ 307 | platform 308 | svcVault 309 | svcSql 310 | svcServiceBus 311 | svcAppEnvPubSub 312 | ] 313 | params: { 314 | location: config.location 315 | environment: environment 316 | serviceName: serviceName 317 | tags: tags 318 | dataProtectionKeyUri: svcVault.outputs.dataProtectionKeyUri 319 | dataProtectionBlobUri: svcStorage.outputs.dataProtectionBlobUri 320 | 321 | // Resource names 322 | platformGroupName: platformGroupName 323 | platformContainerRegistryName: platformContainerRegistryName 324 | appEnvGroupName: appEnvironmentGroupName 325 | appEnvName: appEnvironmentName 326 | sqlGroupName: sqlGroupName 327 | sqlServerName: sqlServerName 328 | sqlDatabaseName: sqlDatabaseName 329 | monitoringGroupName: monitoringGroupName 330 | monitoringAppInsightsName: monitoringAppInsightsName 331 | svcUserName: svcUserName 332 | svcAppName: svcAppName 333 | svcArtifactContainerImageWithTag: svcArtifactContainerImageWithTag 334 | } 335 | } 336 | -------------------------------------------------------------------------------- /infrastructure/service/platform.bicep: -------------------------------------------------------------------------------- 1 | /////////////////////////////////// 2 | // Resource names 3 | 4 | param platformContainerRegistryName string 5 | param svcGroupName string 6 | param svcUserName string 7 | 8 | 9 | /////////////////////////////////// 10 | // Existing resources 11 | 12 | var svcGroup = resourceGroup(svcGroupName) 13 | 14 | resource platformContainerRegistry 'Microsoft.ContainerRegistry/registries@2023-01-01-preview' existing = { 15 | name: platformContainerRegistryName 16 | } 17 | 18 | resource svcUser 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' existing = { 19 | name: svcUserName 20 | scope: svcGroup 21 | } 22 | 23 | 24 | /////////////////////////////////// 25 | // Existing resources 26 | 27 | @description('This is the built-in "AcrPull" role. See https://docs.microsoft.com/en-us/azure/role-based-access-control/built-in-roles#acrpull ') 28 | resource acrPullRoleDefinition 'Microsoft.Authorization/roleDefinitions@2022-04-01' existing = { 29 | scope: subscription() 30 | name: '7f951dda-4ed3-4680-a7ca-43fe172d538d' 31 | } 32 | 33 | 34 | /////////////////////////////////// 35 | // New resources 36 | 37 | // Allows the service to pull images from the Azure Container Registry 38 | resource svcUserAcrPull 'Microsoft.Authorization/roleAssignments@2022-04-01' = { 39 | name: guid('acrPull', svcUser.id) 40 | scope: platformContainerRegistry 41 | properties: { 42 | roleDefinitionId: acrPullRoleDefinition.id 43 | principalId: svcUser.properties.principalId 44 | principalType: 'ServicePrincipal' 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /infrastructure/service/service-identity.bicep: -------------------------------------------------------------------------------- 1 | param location string 2 | param tags object 3 | 4 | 5 | /////////////////////////////////// 6 | // Resource names 7 | 8 | param svcUserName string 9 | 10 | 11 | /////////////////////////////////// 12 | // New resources 13 | 14 | resource svcUser 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = { 15 | name: svcUserName 16 | location: location 17 | tags: tags 18 | } 19 | -------------------------------------------------------------------------------- /infrastructure/service/servicebus.bicep: -------------------------------------------------------------------------------- 1 | param serviceName string 2 | 3 | 4 | /////////////////////////////////// 5 | // Resource names 6 | 7 | param serviceBusNamespaceName string 8 | param svcGroupName string 9 | param svcUserName string 10 | 11 | 12 | /////////////////////////////////// 13 | // Configuration 14 | 15 | var config = loadJsonContent('./../config.json') 16 | var serviceDefaults = config.services[serviceName] 17 | 18 | var serviceBusTopics = contains(serviceDefaults, 'serviceBusTopics') ? serviceDefaults.serviceBusTopics : [] 19 | var serviceBusSubscriptions = contains(serviceDefaults, 'serviceBusSubscriptions') ? serviceDefaults.serviceBusSubscriptions : [] 20 | 21 | // If the service subscribes to a topic that hasn't been deployed yet, its deployment would fail. 22 | // We therefore also create the topic when a subscriber-service is deployed. 23 | var allTopics = union(serviceBusTopics, serviceBusSubscriptions) 24 | 25 | 26 | /////////////////////////////////// 27 | // Existing resources 28 | 29 | var svcGroup = resourceGroup(svcGroupName) 30 | 31 | resource serviceBusNamespace 'Microsoft.ServiceBus/namespaces@2022-10-01-preview' existing = { 32 | name: serviceBusNamespaceName 33 | } 34 | 35 | resource svcUser 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' existing = { 36 | name: svcUserName 37 | scope: svcGroup 38 | } 39 | 40 | @description('This is the built-in "Azure Service Bus Data Receiver" role. See https://learn.microsoft.com/en-us/azure/role-based-access-control/built-in-roles#azure-service-bus-data-receiver ') 41 | resource dataReceiverRoleDefinition 'Microsoft.Authorization/roleDefinitions@2022-04-01' existing = { 42 | scope: subscription() 43 | name: '4f6d3b9b-027b-4f4c-9142-0e5a2a2247e0' 44 | } 45 | 46 | @description('This is the built-in "Azure Service Bus Data Sender" role. See https://learn.microsoft.com/en-us/azure/role-based-access-control/built-in-roles#azure-service-bus-data-sender ') 47 | resource dataSenderRoleDefinition 'Microsoft.Authorization/roleDefinitions@2022-04-01' existing = { 48 | scope: subscription() 49 | name: '69a216fc-b8fb-44d8-bc22-1f3c2cd27a39' 50 | } 51 | 52 | 53 | /////////////////////////////////// 54 | // New resources 55 | 56 | resource topics 'Microsoft.ServiceBus/namespaces/topics@2022-10-01-preview' = [for item in allTopics: { 57 | name: item 58 | parent: serviceBusNamespace 59 | properties: { 60 | } 61 | }] 62 | 63 | resource subscriptions 'Microsoft.ServiceBus/namespaces/topics/subscriptions@2022-10-01-preview' = [for item in serviceBusSubscriptions: { 64 | name: '${serviceBusNamespaceName}/${item}/${serviceName}' 65 | dependsOn: [ 66 | topics 67 | ] 68 | properties: { 69 | } 70 | }] 71 | 72 | resource topicSenderRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = [for (topic, i) in serviceBusTopics: { 73 | name: guid(subscription().id, topic, serviceName, 'Sender') 74 | scope: topics[i] 75 | properties: { 76 | roleDefinitionId: dataSenderRoleDefinition.id 77 | principalId: svcUser.properties.principalId 78 | principalType: 'ServicePrincipal' 79 | } 80 | }] 81 | 82 | resource subscriptionReceiverRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = [for (subscription, i) in serviceBusSubscriptions: { 83 | name: guid(subscription().id, subscription, serviceName, 'Receive') 84 | scope: subscriptions[i] 85 | properties: { 86 | roleDefinitionId: dataReceiverRoleDefinition.id 87 | principalId: svcUser.properties.principalId 88 | principalType: 'ServicePrincipal' 89 | } 90 | }] 91 | -------------------------------------------------------------------------------- /infrastructure/service/sql-migration.ps1: -------------------------------------------------------------------------------- 1 | param( 2 | [string] $ServerName, 3 | [string] $DatabaseName, 4 | [string] $SqlMigrationBlobUrl 5 | ) 6 | 7 | $ErrorActionPreference = "Stop" 8 | 9 | ############################ 10 | "Installing module 'SqlServer'" 11 | 12 | $sqlServerModule = Get-InstalledModule -Name SqlServer -ErrorAction Ignore 13 | if ($sqlServerModule) { 14 | ".. Already installed" 15 | } else { 16 | Install-Module -Name SqlServer -Force 17 | ".. Module installed" 18 | } 19 | 20 | ############################ 21 | "Downloading SQL migration file" 22 | 23 | $blobFile = Get-AzStorageBlobContent -Uri $SqlMigrationBlobUrl -Force 24 | 25 | ############################ 26 | "Aquiring access token for SQL database" 27 | 28 | $token = Get-AzAccessToken -Resource "https://database.windows.net" 29 | 30 | ############################ 31 | "Executing SQL script" 32 | 33 | Invoke-Sqlcmd -ServerInstance $ServerName -Database $DatabaseName -AccessToken $token.Token -InputFile $blobFile.Name 34 | 35 | "Script finished" 36 | -------------------------------------------------------------------------------- /infrastructure/service/sql-user.ps1: -------------------------------------------------------------------------------- 1 | param( 2 | [string] $ServerName, 3 | [string] $DatabaseName, 4 | [string] $UserName 5 | ) 6 | 7 | $ErrorActionPreference = "Stop" 8 | 9 | Write-Output "Aquiring access token" 10 | $token = Get-AzAccessToken -Resource "https://database.windows.net" 11 | 12 | $dbConn = New-Object System.Data.SqlClient.SqlConnection 13 | 14 | try { 15 | $dbConn.ConnectionString = "Server=tcp:$ServerName,1433;Initial Catalog=$DatabaseName;Persist Security Info=False;MultipleActiveResultSets=False;Encrypt=True;TrustServerCertificate=False;" 16 | $dbConn.AccessToken=$token.Token 17 | 18 | Write-Output "Opening connection" 19 | $dbConn.Open() 20 | Write-Output ".. Done" 21 | 22 | Write-Output "Ensuring user is created and has datareader/datawriter roles" 23 | $dbCmd = New-Object System.Data.SqlClient.SqlCommand 24 | $dbCmd.Connection = $dbConn 25 | $dbCmd.CommandText = @" 26 | IF NOT EXISTS (SELECT * FROM sys.database_principals WHERE name = '$UserName') 27 | BEGIN 28 | CREATE USER [$UserName] FROM EXTERNAL PROVIDER 29 | END 30 | 31 | IF IS_ROLEMEMBER('db_datareader', '$UserName') = 0 32 | BEGIN 33 | ALTER ROLE db_datareader ADD MEMBER [$UserName] 34 | END 35 | 36 | IF IS_ROLEMEMBER('db_datawriter', '$UserName') = 0 37 | BEGIN 38 | ALTER ROLE db_datawriter ADD MEMBER [$UserName] 39 | END 40 | "@ 41 | $dbCmd.ExecuteNonQuery() | Out-Null 42 | Write-Output ".. Done" 43 | } 44 | finally { 45 | $dbConn.Close() 46 | } 47 | -------------------------------------------------------------------------------- /infrastructure/service/sql.bicep: -------------------------------------------------------------------------------- 1 | param now string = utcNow() 2 | param location string 3 | param environment string 4 | param serviceName string 5 | param buildNumber string 6 | param tags object 7 | 8 | 9 | /////////////////////////////////// 10 | // Resource names 11 | 12 | param platformGroupName string 13 | param platformStorageAccountName string 14 | param sqlMigrationContainerName string 15 | param sqlMigrationFile string 16 | param sqlServerName string 17 | param sqlServerAdminUserName string 18 | param sqlDatabaseName string 19 | param svcGroupName string 20 | param svcUserName string 21 | param sqlDeployUserScriptName string 22 | param sqlDeployMigrationScriptName string 23 | 24 | 25 | /////////////////////////////////// 26 | // Configuration 27 | 28 | var config = loadJsonContent('./../config.json') 29 | var envConfig = config.environments[environment] 30 | var serviceConfig = envConfig.services[serviceName] 31 | 32 | 33 | /////////////////////////////////// 34 | // Existing resources 35 | 36 | var platformGroup = resourceGroup(platformGroupName) 37 | var svcGroup = resourceGroup(svcGroupName) 38 | 39 | resource platformStorage 'Microsoft.Storage/storageAccounts@2022-09-01' existing = { 40 | name: platformStorageAccountName 41 | scope: platformGroup 42 | } 43 | 44 | resource sqlServer 'Microsoft.Sql/servers@2022-08-01-preview' existing = { 45 | name: sqlServerName 46 | } 47 | 48 | resource sqlServerAdminUser 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' existing = { 49 | name: sqlServerAdminUserName 50 | } 51 | 52 | resource svcUser 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' existing = { 53 | name: svcUserName 54 | scope: svcGroup 55 | } 56 | 57 | 58 | /////////////////////////////////// 59 | // New resources 60 | 61 | resource sqlDatabase 'Microsoft.Sql/servers/databases@2022-08-01-preview' = { 62 | name: sqlDatabaseName 63 | parent: sqlServer 64 | location: location 65 | tags: tags 66 | sku: { 67 | name: serviceConfig.sqlDatabase.skuName 68 | tier: serviceConfig.sqlDatabase.skuTier 69 | capacity: serviceConfig.sqlDatabase.skuCapacity 70 | } 71 | properties: { 72 | } 73 | } 74 | 75 | resource sqlDeployUserScript 'Microsoft.Resources/deploymentScripts@2020-10-01' = { 76 | name: sqlDeployUserScriptName 77 | location: location 78 | tags: tags 79 | kind: 'AzurePowerShell' 80 | identity: { 81 | type: 'UserAssigned' 82 | userAssignedIdentities: { 83 | '${sqlServerAdminUser.id}': {} 84 | } 85 | } 86 | properties: { 87 | forceUpdateTag: '0' // This script must only execute once, so we can always use the same update tag! 88 | containerSettings: { 89 | containerGroupName: sqlDeployUserScriptName 90 | } 91 | azPowerShellVersion: '8.2.0' 92 | retentionInterval: 'P1D' 93 | cleanupPreference: 'Always' 94 | scriptContent: loadTextContent('sql-user.ps1') 95 | arguments: '-ServerName ${sqlServer.properties.fullyQualifiedDomainName} -DatabaseName ${sqlDatabase.name} -UserName ${svcUser.name}' 96 | timeout: 'PT10M' 97 | } 98 | } 99 | 100 | var containerSas = platformStorage.listServiceSAS(platformStorage.apiVersion, { 101 | canonicalizedResource: '/blob/${platformStorage.name}/${sqlMigrationContainerName}/${sqlMigrationFile}' 102 | signedProtocol: 'https' 103 | signedResource: 'b' 104 | signedPermission: 'r' 105 | signedExpiry: dateTimeAdd(now, 'PT1H') 106 | }) 107 | var sqlMigrationBlobUrl = '${platformStorage.properties.primaryEndpoints.blob}${sqlMigrationContainerName}/${sqlMigrationFile}?${containerSas.serviceSasToken}' 108 | 109 | resource deploySqlMigrationScript 'Microsoft.Resources/deploymentScripts@2020-10-01' = { 110 | name: sqlDeployMigrationScriptName 111 | location: location 112 | tags: tags 113 | kind: 'AzurePowerShell' 114 | identity: { 115 | type: 'UserAssigned' 116 | userAssignedIdentities: { 117 | '${sqlServerAdminUser.id}': {} 118 | } 119 | } 120 | properties: { 121 | forceUpdateTag: buildNumber // The migration only needs to be applied once per build 122 | containerSettings: { 123 | containerGroupName: sqlDeployMigrationScriptName 124 | } 125 | azPowerShellVersion: '8.2.0' 126 | retentionInterval: 'P1D' 127 | cleanupPreference: 'Always' 128 | scriptContent: loadTextContent('sql-migration.ps1') 129 | arguments: '-ServerName ${sqlServer.properties.fullyQualifiedDomainName} -DatabaseName ${sqlDatabase.name} -SqlMigrationBlobUrl \\"${sqlMigrationBlobUrl}\\"' 130 | timeout: 'PT10M' 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /infrastructure/service/storage.bicep: -------------------------------------------------------------------------------- 1 | param location string 2 | param tags object 3 | 4 | 5 | /////////////////////////////////// 6 | // Resource names 7 | 8 | param networkGroupName string 9 | param networkVnetName string 10 | param networkSubnetAppsName string 11 | param svcGroupName string 12 | param svcUserName string 13 | param svcStorageAccountName string 14 | param svcStorageDataProtectionContainerName string 15 | 16 | 17 | /////////////////////////////////// 18 | // Existing resources 19 | 20 | var networkGroup = resourceGroup(networkGroupName) 21 | var svcGroup = resourceGroup(svcGroupName) 22 | 23 | resource networkVnet 'Microsoft.Network/virtualNetworks@2022-09-01' existing = { 24 | name: networkVnetName 25 | scope: networkGroup 26 | } 27 | 28 | resource networkSubnetApps 'Microsoft.Network/virtualNetworks/subnets@2022-09-01' existing = { 29 | name: networkSubnetAppsName 30 | parent: networkVnet 31 | } 32 | 33 | resource svcUser 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' existing = { 34 | name: svcUserName 35 | scope: svcGroup 36 | } 37 | 38 | @description('This is the built-in Storage Blob Data Contributor role. See https://docs.microsoft.com/en-us/azure/role-based-access-control/built-in-roles#storage-blob-data-contributor ') 39 | resource storageBlobDataContributorRoleDefinition 'Microsoft.Authorization/roleDefinitions@2022-04-01' existing = { 40 | scope: subscription() 41 | name: 'ba92f5b4-2d11-453d-a403-e96b0029c9fe' 42 | } 43 | 44 | 45 | /////////////////////////////////// 46 | // New resources 47 | 48 | resource svcStorageAccount 'Microsoft.Storage/storageAccounts@2022-09-01' = { 49 | name: svcStorageAccountName 50 | location: location 51 | tags: tags 52 | kind: 'StorageV2' 53 | sku: { 54 | name: 'Standard_ZRS' 55 | } 56 | properties: { 57 | accessTier: 'Hot' 58 | minimumTlsVersion: 'TLS1_2' 59 | supportsHttpsTrafficOnly: true 60 | allowBlobPublicAccess: false 61 | networkAcls: { 62 | defaultAction: 'Deny' 63 | bypass: 'None' 64 | virtualNetworkRules: [ 65 | { 66 | id: networkSubnetApps.id 67 | } 68 | ] 69 | } 70 | } 71 | 72 | resource blobServices 'blobServices' = { 73 | name: 'default' 74 | properties: { 75 | deleteRetentionPolicy: { 76 | enabled: true 77 | allowPermanentDelete: true 78 | days: 7 79 | } 80 | } 81 | } 82 | } 83 | 84 | @description('A blob container that will be used to store the ASP.NET Core Data Protection keys') 85 | resource dataProtectionContainer 'Microsoft.Storage/storageAccounts/blobServices/containers@2022-09-01' = { 86 | name: '${svcStorageAccount.name}/default/${svcStorageDataProtectionContainerName}' 87 | properties: { 88 | publicAccess: 'None' 89 | } 90 | } 91 | 92 | @description('Allows the service user to manage the Data Protection keys and any other blobs the service might require') 93 | resource svcUserblobContributer 'Microsoft.Authorization/roleAssignments@2022-04-01' = { 94 | name: guid('svcUserBlobContributor', svcStorageAccount.id, svcUser.id) 95 | scope: svcStorageAccount 96 | properties: { 97 | roleDefinitionId: storageBlobDataContributorRoleDefinition.id 98 | principalId: svcUser.properties.principalId 99 | principalType: 'ServicePrincipal' 100 | } 101 | } 102 | 103 | 104 | output storageBlobPrimaryEndpoint string = svcStorageAccount.properties.primaryEndpoints.blob 105 | output dataProtectionBlobUri string = '${svcStorageAccount.properties.primaryEndpoints.blob}${svcStorageDataProtectionContainerName}/keys.xml' 106 | -------------------------------------------------------------------------------- /nuget.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /proto/_internal-grpc-sql-bus.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | option csharp_namespace = "InternalGrpcSqlBus.Api"; 4 | 5 | import "google/api/annotations.proto"; 6 | 7 | service Customers { 8 | rpc ListCustomers (ListCustomersRequest) returns (ListCustomersResponse) { 9 | option (google.api.http) = { 10 | get: "/v1/customers" 11 | }; 12 | }; 13 | rpc GetCustomer (GetCustomerRequest) returns (CustomerDto) { 14 | option (google.api.http) = { 15 | get: "/v1/customers/{customer_id=*}" 16 | }; 17 | }; 18 | rpc CreateCustomer (CreateCustomerRequest) returns (CustomerDto) { 19 | option (google.api.http) = { 20 | post: "/v1/customers" 21 | body: "customer" 22 | }; 23 | } 24 | } 25 | 26 | // Service messages 27 | 28 | message CreateCustomerRequest { 29 | CustomerDto customer = 1; 30 | } 31 | 32 | message GetCustomerRequest { 33 | string customer_id = 1; 34 | } 35 | 36 | message ListCustomersRequest { 37 | } 38 | 39 | message ListCustomersResponse { 40 | repeated CustomerDto customers = 1; 41 | } 42 | 43 | message CustomerDto { 44 | string customer_id = 1; 45 | string full_name = 2; 46 | } 47 | 48 | // Events 49 | 50 | message CustomerCreatedEvent { 51 | string customer_id = 1; 52 | } 53 | -------------------------------------------------------------------------------- /proto/_internal-grpc.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | option csharp_namespace = "InternalGrpc.Api"; 4 | 5 | import "google/api/annotations.proto"; 6 | 7 | service InternalGrpcEntities { 8 | rpc ListEntities (ListEntitiesRequest) returns (ListEntitiesResponse) { 9 | option (google.api.http) = { 10 | get: "/v1/entitites" 11 | }; 12 | }; 13 | rpc CreateEntity (CreateEntityRequest) returns (InternalGrpcEntityDto) { 14 | option (google.api.http) = { 15 | post: "/v1/entities" 16 | body: "entity" 17 | }; 18 | } 19 | } 20 | 21 | message CreateEntityRequest { 22 | InternalGrpcEntityDto entity = 1; 23 | } 24 | 25 | message ListEntitiesRequest { 26 | } 27 | 28 | message ListEntitiesResponse { 29 | repeated InternalGrpcEntityDto entities = 1; 30 | } 31 | 32 | message InternalGrpcEntityDto { 33 | string entity_id = 1; 34 | string display_name = 2; 35 | } 36 | -------------------------------------------------------------------------------- /proto/google/api/annotations.proto: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015, Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | syntax = "proto3"; 16 | 17 | package google.api; 18 | 19 | import "google/api/http.proto"; 20 | import "google/protobuf/descriptor.proto"; 21 | 22 | option go_package = "google.golang.org/genproto/googleapis/api/annotations;annotations"; 23 | option java_multiple_files = true; 24 | option java_outer_classname = "AnnotationsProto"; 25 | option java_package = "com.google.api"; 26 | option objc_class_prefix = "GAPI"; 27 | 28 | extend google.protobuf.MethodOptions { 29 | // See `HttpRule`. 30 | HttpRule http = 72295728; 31 | } 32 | -------------------------------------------------------------------------------- /proto/types.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package shared.types; 4 | 5 | // Decimal 6 | // https://docs.microsoft.com/en-us/dotnet/architecture/grpc-for-wcf-developers/protobuf-data-types#creating-a-custom-decimal-type-for-protobuf 7 | // Example: 12345.6789 -> { units = 12345, nanos = 678900000 } 8 | message DecimalValue { 9 | 10 | // Whole units part of the amount 11 | int64 units = 1; 12 | 13 | // Nano units of the amount (10^-9) 14 | // Must be same sign as units 15 | sfixed32 nanos = 2; 16 | } 17 | -------------------------------------------------------------------------------- /services/_internal-grpc-sql-bus/InternalGrpcSqlBus.Api/CustomersService.cs: -------------------------------------------------------------------------------- 1 | using Dapr.Client; 2 | using Grpc.Core; 3 | using InternalGrpc.Api; 4 | using InternalGrpcSqlBus.Api.Domain; 5 | using Microsoft.EntityFrameworkCore; 6 | using Shared; 7 | 8 | namespace InternalGrpcSqlBus.Api; 9 | 10 | public class CustomersService : Customers.CustomersBase 11 | { 12 | private readonly CustomersDbContext _dbContext; 13 | private readonly InternalGrpcEntities.InternalGrpcEntitiesClient _internalGrpcClient; 14 | private readonly DaprClient _daprClient; 15 | private readonly ILogger _logger; 16 | 17 | public CustomersService( 18 | CustomersDbContext dbContext, 19 | InternalGrpcEntities.InternalGrpcEntitiesClient internalGrpcClient, 20 | DaprClient daprClient, 21 | ILogger logger) 22 | { 23 | _dbContext = dbContext; 24 | _internalGrpcClient = internalGrpcClient; 25 | _daprClient = daprClient; 26 | _logger = logger; 27 | } 28 | 29 | public override async Task ListCustomers(ListCustomersRequest request, ServerCallContext context) 30 | { 31 | var customers = await _dbContext.Customers 32 | .AsNoTracking() 33 | .ToListAsync(); 34 | 35 | return new ListCustomersResponse 36 | { 37 | Customers = { customers.Select(ToDto) }, 38 | }; 39 | } 40 | 41 | public override async Task GetCustomer(GetCustomerRequest request, ServerCallContext context) 42 | { 43 | if (string.IsNullOrWhiteSpace(request.CustomerId)) 44 | throw new RpcException(new Status(StatusCode.InvalidArgument, "'customer_id' is missing")); 45 | 46 | var customer = await _dbContext.Customers.FirstOrDefaultAsync(x => x.CustomerId == request.CustomerId, 47 | context.CancellationToken); 48 | 49 | return customer is null 50 | ? throw new RpcException(new Status(StatusCode.NotFound, "customer not found")) 51 | : ToDto(customer); 52 | } 53 | 54 | public override async Task CreateCustomer(CreateCustomerRequest request, ServerCallContext context) 55 | { 56 | if (request.Customer is null) 57 | throw new RpcException(new Status(StatusCode.InvalidArgument, "'customer' is missing")); 58 | 59 | // Call another internal gRPC service to get some data. 60 | // (this doesn't actually do anything with the data - it's just here to show a gRPC call) 61 | 62 | var response = await _internalGrpcClient.ListEntitiesAsync(new ListEntitiesRequest(), cancellationToken: context.CancellationToken); 63 | _logger.LogWarning("External service returned {Response}", response); 64 | 65 | 66 | // Persist data to SQL Database via EF Core 67 | 68 | if (!string.IsNullOrWhiteSpace(request.Customer.CustomerId) 69 | && await _dbContext.Customers.AnyAsync(x => x.CustomerId == request.Customer.CustomerId, context.CancellationToken)) 70 | throw new RpcException(new Status(StatusCode.AlreadyExists, "The given id already exists")); 71 | 72 | var customer = new Customer(request.Customer); 73 | 74 | _dbContext.Customers.Add(customer); 75 | 76 | await _dbContext.SaveChangesAsync(context.CancellationToken); 77 | 78 | 79 | // Publish an event via Dapr pub/sub. 80 | // NOTE: To be safe, this would either require some kind of transactional outbox or to be called in a retry loop. 81 | // 82 | // We must manually construct the cloud event because the .NET SDK doesn't change the default "type" (com.dapr.event.sent) 83 | var evt = DaprHelpers.CreateCloudEvent(new CustomerCreatedEvent 84 | { 85 | CustomerId = customer.CustomerId, 86 | }); 87 | await _daprClient.PublishEventAsync("pubsub-internal-grpc-sql-bus", "customer-created", evt, context.CancellationToken); 88 | _logger.LogWarning("CustomerCreatedEvent event published for {CustomerId}", evt.Data.CustomerId); 89 | 90 | return ToDto(customer); 91 | } 92 | 93 | private static CustomerDto ToDto(Customer customer) 94 | { 95 | return new CustomerDto 96 | { 97 | CustomerId = customer.CustomerId, 98 | FullName = customer.FullName, 99 | }; 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /services/_internal-grpc-sql-bus/InternalGrpcSqlBus.Api/Domain/Customer.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | namespace InternalGrpcSqlBus.Api.Domain; 4 | 5 | public class Customer 6 | { 7 | [MaxLength(36)] 8 | public string CustomerId { get; protected set; } 9 | 10 | [MaxLength(100)] 11 | public string FullName { get; protected set; } 12 | 13 | protected Customer() 14 | { 15 | // EF Core uses this constructor when loading entities from the database. 16 | CustomerId = null!; 17 | FullName = null!; 18 | } 19 | 20 | public Customer(CustomerDto dto) 21 | { 22 | CustomerId = string.IsNullOrWhiteSpace(dto.CustomerId) ? Guid.NewGuid().ToString() : dto.CustomerId; 23 | FullName = dto.FullName; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /services/_internal-grpc-sql-bus/InternalGrpcSqlBus.Api/Domain/CustomersDbContext.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | 3 | namespace InternalGrpcSqlBus.Api.Domain; 4 | 5 | public class CustomersDbContext : DbContext 6 | { 7 | public DbSet Customers => Set(); 8 | 9 | public CustomersDbContext(DbContextOptions options) 10 | : base(options) 11 | { 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /services/_internal-grpc-sql-bus/InternalGrpcSqlBus.Api/InternalGrpcSqlBus.Api.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net7.0 5 | aspnet-InternalGrpcSqlBus.Api-E4EA26A6-FED8-47CF-9154-40BE5F55C9D2 6 | true 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | all 19 | runtime; build; native; contentfiles; analyzers; buildtransitive 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | Protos\_internal-grpc-sql-bus.proto 33 | 34 | 35 | Protos\_internal-grpc.proto 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /services/_internal-grpc-sql-bus/InternalGrpcSqlBus.Api/Migrations/20220906082151_InitialCreate.Designer.cs: -------------------------------------------------------------------------------- 1 | // 2 | using InternalGrpcSqlBus.Api.Domain; 3 | using Microsoft.EntityFrameworkCore; 4 | using Microsoft.EntityFrameworkCore.Infrastructure; 5 | using Microsoft.EntityFrameworkCore.Metadata; 6 | using Microsoft.EntityFrameworkCore.Migrations; 7 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; 8 | 9 | #nullable disable 10 | 11 | namespace InternalGrpcSqlBus.Api.Migrations 12 | { 13 | [DbContext(typeof(CustomersDbContext))] 14 | [Migration("20220906082151_InitialCreate")] 15 | partial class InitialCreate 16 | { 17 | /// 18 | protected override void BuildTargetModel(ModelBuilder modelBuilder) 19 | { 20 | #pragma warning disable 612, 618 21 | modelBuilder 22 | .HasAnnotation("ProductVersion", "7.0.0-preview.7.22376.2") 23 | .HasAnnotation("Relational:MaxIdentifierLength", 128); 24 | 25 | SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); 26 | 27 | modelBuilder.Entity("InternalGrpcSqlBus.Api.Domain.Customer", b => 28 | { 29 | b.Property("CustomerId") 30 | .HasMaxLength(36) 31 | .HasColumnType("nvarchar(36)"); 32 | 33 | b.Property("FullName") 34 | .IsRequired() 35 | .HasMaxLength(100) 36 | .HasColumnType("nvarchar(100)"); 37 | 38 | b.HasKey("CustomerId"); 39 | 40 | b.ToTable("Customers"); 41 | }); 42 | #pragma warning restore 612, 618 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /services/_internal-grpc-sql-bus/InternalGrpcSqlBus.Api/Migrations/20220906082151_InitialCreate.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.Migrations; 2 | 3 | #nullable disable 4 | 5 | namespace InternalGrpcSqlBus.Api.Migrations; 6 | 7 | /// 8 | public partial class InitialCreate : Migration 9 | { 10 | /// 11 | protected override void Up(MigrationBuilder migrationBuilder) 12 | { 13 | migrationBuilder.CreateTable( 14 | name: "Customers", 15 | columns: table => new 16 | { 17 | CustomerId = table.Column(type: "nvarchar(36)", maxLength: 36, nullable: false), 18 | FullName = table.Column(type: "nvarchar(100)", maxLength: 100, nullable: false) 19 | }, 20 | constraints: table => 21 | { 22 | table.PrimaryKey("PK_Customers", x => x.CustomerId); 23 | }); 24 | } 25 | 26 | /// 27 | protected override void Down(MigrationBuilder migrationBuilder) 28 | { 29 | migrationBuilder.DropTable( 30 | name: "Customers"); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /services/_internal-grpc-sql-bus/InternalGrpcSqlBus.Api/Migrations/CustomersDbContextModelSnapshot.cs: -------------------------------------------------------------------------------- 1 | // 2 | using InternalGrpcSqlBus.Api.Domain; 3 | using Microsoft.EntityFrameworkCore; 4 | using Microsoft.EntityFrameworkCore.Infrastructure; 5 | using Microsoft.EntityFrameworkCore.Metadata; 6 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; 7 | 8 | #nullable disable 9 | 10 | namespace InternalGrpcSqlBus.Api.Migrations 11 | { 12 | [DbContext(typeof(CustomersDbContext))] 13 | partial class CustomersDbContextModelSnapshot : ModelSnapshot 14 | { 15 | protected override void BuildModel(ModelBuilder modelBuilder) 16 | { 17 | #pragma warning disable 612, 618 18 | modelBuilder 19 | .HasAnnotation("ProductVersion", "7.0.0-preview.7.22376.2") 20 | .HasAnnotation("Relational:MaxIdentifierLength", 128); 21 | 22 | SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); 23 | 24 | modelBuilder.Entity("InternalGrpcSqlBus.Api.Domain.Customer", b => 25 | { 26 | b.Property("CustomerId") 27 | .HasMaxLength(36) 28 | .HasColumnType("nvarchar(36)"); 29 | 30 | b.Property("FullName") 31 | .IsRequired() 32 | .HasMaxLength(100) 33 | .HasColumnType("nvarchar(100)"); 34 | 35 | b.HasKey("CustomerId"); 36 | 37 | b.ToTable("Customers"); 38 | }); 39 | #pragma warning restore 612, 618 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /services/_internal-grpc-sql-bus/InternalGrpcSqlBus.Api/Program.cs: -------------------------------------------------------------------------------- 1 | using InternalGrpc.Api; 2 | using InternalGrpcSqlBus.Api; 3 | using InternalGrpcSqlBus.Api.Domain; 4 | using Microsoft.EntityFrameworkCore; 5 | using Shared; 6 | 7 | var builder = WebApplication.CreateBuilder(args); 8 | 9 | // Add services to the container. 10 | 11 | // Swagger 12 | builder.Services.AddSwaggerGen(); 13 | 14 | // Application Insights 15 | builder.Services.AddCustomAppInsights(); 16 | 17 | // Dapr 18 | builder.Services.AddDaprClient(); 19 | 20 | // gRPC Server 21 | builder.Services.AddGrpc(options => 22 | { 23 | options.EnableDetailedErrors = true; 24 | }); 25 | builder.Services.AddGrpcReflection(); 26 | builder.Services.AddGrpcSwagger(); 27 | 28 | // gRPC Clients (uses a custom extension method from Shared) 29 | builder.Services.AddDaprGrpcClient("internal-grpc"); 30 | 31 | // EF Core 32 | builder.Services.AddDbContext(options => 33 | { 34 | options.UseSqlServer(builder.Configuration.GetConnectionString("SQL") ?? throw new ArgumentException("SQL Connection String missing")); 35 | }); 36 | 37 | // Health checks 38 | builder.Services.AddHealthChecks() 39 | .AddDbContextCheck(); 40 | 41 | var app = builder.Build(); 42 | 43 | 44 | // Configure the HTTP request pipeline. 45 | 46 | app.UseDeveloperExceptionPage(); 47 | 48 | // Swagger 49 | app.UseSwagger(); 50 | app.UseSwaggerUI(); 51 | 52 | // gRPC Server 53 | app.MapGrpcService(); 54 | app.MapGrpcReflectionService(); 55 | 56 | // Health checks 57 | app.MapCustomHealthCheckEndpoints(); 58 | 59 | app.MapGet("/", () => "Hello from 'internal-grpc-sql-bus'").ExcludeFromDescription(); 60 | 61 | app.Run(); 62 | -------------------------------------------------------------------------------- /services/_internal-grpc-sql-bus/InternalGrpcSqlBus.Api/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/launchsettings.json", 3 | "profiles": { 4 | "http": { 5 | "commandName": "Project", 6 | "dotnetRunMessages": true, 7 | "launchBrowser": true, 8 | "launchUrl": "swagger", 9 | "applicationUrl": "http://localhost:5088", 10 | "environmentVariables": { 11 | "ASPNETCORE_ENVIRONMENT": "Development" 12 | } 13 | }, 14 | "https": { 15 | "commandName": "Project", 16 | "dotnetRunMessages": true, 17 | "launchBrowser": true, 18 | "launchUrl": "swagger", 19 | "applicationUrl": "https://localhost:7088;http://localhost:5088", 20 | "environmentVariables": { 21 | "ASPNETCORE_ENVIRONMENT": "Development" 22 | } 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /services/_internal-grpc-sql-bus/InternalGrpcSqlBus.Api/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "ConnectionStrings": { 3 | "SQL": "Server=(localdb)\\mssqllocaldb;Database=internal-grpc-sql-bus;Trusted_Connection=True;MultipleActiveResultSets=true" 4 | }, 5 | "Logging": { 6 | "LogLevel": { 7 | "Default": "Information", 8 | "Microsoft.AspNetCore": "Warning" 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /services/_internal-grpc-sql-bus/InternalGrpcSqlBus.Api/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | }, 8 | "AllowedHosts": "*" 9 | } 10 | -------------------------------------------------------------------------------- /services/_internal-grpc-sql-bus/InternalGrpcSqlBus.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.30114.105 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "InternalGrpcSqlBus.Api", "InternalGrpcSqlBus.Api\InternalGrpcSqlBus.Api.csproj", "{72EAD5CF-B459-4152-98F2-E05D72B10D82}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Shared", "..\..\shared\Shared\Shared.csproj", "{DC9FFD15-0536-4344-B66A-6C076FFE866E}" 9 | EndProject 10 | Global 11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 12 | Debug|Any CPU = Debug|Any CPU 13 | Release|Any CPU = Release|Any CPU 14 | EndGlobalSection 15 | GlobalSection(SolutionProperties) = preSolution 16 | HideSolutionNode = FALSE 17 | EndGlobalSection 18 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 19 | {72EAD5CF-B459-4152-98F2-E05D72B10D82}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 20 | {72EAD5CF-B459-4152-98F2-E05D72B10D82}.Debug|Any CPU.Build.0 = Debug|Any CPU 21 | {72EAD5CF-B459-4152-98F2-E05D72B10D82}.Release|Any CPU.ActiveCfg = Release|Any CPU 22 | {72EAD5CF-B459-4152-98F2-E05D72B10D82}.Release|Any CPU.Build.0 = Release|Any CPU 23 | {DC9FFD15-0536-4344-B66A-6C076FFE866E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 24 | {DC9FFD15-0536-4344-B66A-6C076FFE866E}.Debug|Any CPU.Build.0 = Debug|Any CPU 25 | {DC9FFD15-0536-4344-B66A-6C076FFE866E}.Release|Any CPU.ActiveCfg = Release|Any CPU 26 | {DC9FFD15-0536-4344-B66A-6C076FFE866E}.Release|Any CPU.Build.0 = Release|Any CPU 27 | EndGlobalSection 28 | EndGlobal 29 | -------------------------------------------------------------------------------- /services/_internal-grpc-sql-bus/README.md: -------------------------------------------------------------------------------- 1 | A complex internal service that: 2 | * exposes a gRPC service (with "customers"-entities) 3 | * acts as client to another gRPC server ("internal-grpc") 4 | * stores data in a SQL database 5 | * publishes a "CustomerCreatedEvent" message to the pubsub-topic "customer-created" (subscribed to by "internal-http-bus") 6 | -------------------------------------------------------------------------------- /services/_internal-grpc/InternalGrpc.Api/InternalGrpc.Api.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net7.0 5 | aspnet-InternalGrpc.Api-C3550839-87C7-4364-B746-5F19E74C5C7E 6 | true 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | Protos\_internal-grpc.proto 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /services/_internal-grpc/InternalGrpc.Api/InternalGrpcService.cs: -------------------------------------------------------------------------------- 1 | using Grpc.Core; 2 | 3 | namespace InternalGrpc.Api; 4 | 5 | public class InternalGrpcService : InternalGrpcEntities.InternalGrpcEntitiesBase 6 | { 7 | private static readonly List Entities = new(); 8 | 9 | public override Task ListEntities(ListEntitiesRequest request, ServerCallContext context) 10 | { 11 | return Task.FromResult(new ListEntitiesResponse 12 | { 13 | Entities = { Entities }, 14 | }); 15 | } 16 | 17 | public override Task CreateEntity(CreateEntityRequest request, ServerCallContext context) 18 | { 19 | if (request.Entity is null) 20 | throw new RpcException(new Status(StatusCode.InvalidArgument, "'entity' is missing")); 21 | 22 | if (!string.IsNullOrWhiteSpace(request.Entity.EntityId) && Entities.Any(x => x.EntityId == request.Entity.EntityId)) 23 | throw new RpcException(new Status(StatusCode.AlreadyExists, "The given id already exists")); 24 | 25 | var entity = request.Entity.Clone(); 26 | 27 | Entities.Add(entity); 28 | 29 | return Task.FromResult(entity); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /services/_internal-grpc/InternalGrpc.Api/Program.cs: -------------------------------------------------------------------------------- 1 | using InternalGrpc.Api; 2 | 3 | var builder = WebApplication.CreateBuilder(args); 4 | 5 | // Add services to the container. 6 | 7 | // ASP.NET Core 8 | builder.Services.AddSwaggerGen(); 9 | 10 | // Application Insights 11 | builder.Services.AddCustomAppInsights(); 12 | 13 | // gRPC Server 14 | builder.Services.AddGrpc(options => 15 | { 16 | options.EnableDetailedErrors = true; 17 | }); 18 | builder.Services.AddGrpcReflection(); 19 | builder.Services.AddGrpcSwagger(); 20 | 21 | // Health checks 22 | builder.Services.AddHealthChecks(); 23 | 24 | var app = builder.Build(); 25 | 26 | // Configure the HTTP request pipeline. 27 | 28 | // Swagger 29 | app.UseSwagger(); 30 | app.UseSwaggerUI(); 31 | 32 | // gRPC Server 33 | app.MapGrpcService(); 34 | app.MapGrpcReflectionService(); 35 | 36 | // Health checks 37 | app.MapCustomHealthCheckEndpoints(); 38 | 39 | 40 | app.MapGet("/", () => "Hello from 'internal-grpc'").ExcludeFromDescription(); 41 | 42 | app.Run(); 43 | -------------------------------------------------------------------------------- /services/_internal-grpc/InternalGrpc.Api/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/launchsettings.json", 3 | "profiles": { 4 | "http": { 5 | "commandName": "Project", 6 | "dotnetRunMessages": true, 7 | "launchBrowser": true, 8 | "launchUrl": "swagger", 9 | "applicationUrl": "http://localhost:5025", 10 | "environmentVariables": { 11 | "ASPNETCORE_ENVIRONMENT": "Development" 12 | } 13 | }, 14 | "https": { 15 | "commandName": "Project", 16 | "dotnetRunMessages": true, 17 | "launchBrowser": true, 18 | "launchUrl": "swagger", 19 | "applicationUrl": "https://localhost:7025;http://localhost:5025", 20 | "environmentVariables": { 21 | "ASPNETCORE_ENVIRONMENT": "Development" 22 | } 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /services/_internal-grpc/InternalGrpc.Api/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /services/_internal-grpc/InternalGrpc.Api/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | }, 8 | "AllowedHosts": "*" 9 | } 10 | -------------------------------------------------------------------------------- /services/_internal-grpc/InternalGrpc.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.3.32819.101 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Shared", "..\..\shared\Shared\Shared.csproj", "{4C589188-6755-44E0-A52A-6D6CE92FDC13}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "InternalGrpc.Api", "InternalGrpc.Api\InternalGrpc.Api.csproj", "{646E6B06-17D9-4044-ADF2-D2F993415800}" 9 | EndProject 10 | Global 11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 12 | Debug|Any CPU = Debug|Any CPU 13 | Release|Any CPU = Release|Any CPU 14 | EndGlobalSection 15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 16 | {4C589188-6755-44E0-A52A-6D6CE92FDC13}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 17 | {4C589188-6755-44E0-A52A-6D6CE92FDC13}.Debug|Any CPU.Build.0 = Debug|Any CPU 18 | {4C589188-6755-44E0-A52A-6D6CE92FDC13}.Release|Any CPU.ActiveCfg = Release|Any CPU 19 | {4C589188-6755-44E0-A52A-6D6CE92FDC13}.Release|Any CPU.Build.0 = Release|Any CPU 20 | {646E6B06-17D9-4044-ADF2-D2F993415800}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {646E6B06-17D9-4044-ADF2-D2F993415800}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {646E6B06-17D9-4044-ADF2-D2F993415800}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {646E6B06-17D9-4044-ADF2-D2F993415800}.Release|Any CPU.Build.0 = Release|Any CPU 24 | EndGlobalSection 25 | GlobalSection(SolutionProperties) = preSolution 26 | HideSolutionNode = FALSE 27 | EndGlobalSection 28 | GlobalSection(ExtensibilityGlobals) = postSolution 29 | SolutionGuid = {90F9C540-C881-4CEF-A0DC-0126465798C3} 30 | EndGlobalSection 31 | EndGlobal 32 | -------------------------------------------------------------------------------- /services/_internal-grpc/README.md: -------------------------------------------------------------------------------- 1 | A simple internal gRPC server that: 2 | * exposes a gRPC service for listing/creating "entitites" (will be used by "internal-grpc-sql-bus" and "public-razor"). 3 | * does not have any external dependencies, so it does not even use Dapr. 4 | -------------------------------------------------------------------------------- /services/_internal-http-bus/InternalHttpBus.Api/InternalHttpBus.Api.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net7.0 5 | true 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | Protos\_internal-grpc-sql-bus.proto 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /services/_internal-http-bus/InternalHttpBus.Api/Program.cs: -------------------------------------------------------------------------------- 1 | using Dapr; 2 | using InternalGrpcSqlBus.Api; 3 | using Microsoft.AspNetCore.Mvc; 4 | 5 | var builder = WebApplication.CreateBuilder(args); 6 | 7 | // Add services to the container. 8 | 9 | builder.Services.AddCustomAppInsights(); 10 | 11 | builder.Services.AddEndpointsApiExplorer(); 12 | builder.Services.AddSwaggerGen(); 13 | 14 | builder.Services.AddDaprClient(); 15 | 16 | builder.Services.AddHealthChecks(); 17 | 18 | var app = builder.Build(); 19 | 20 | // Configure the HTTP request pipeline. 21 | 22 | app.UseSwagger(); 23 | app.UseSwaggerUI(); 24 | 25 | app.UseCloudEvents(); 26 | app.MapSubscribeHandler(); 27 | 28 | app.MapCustomHealthCheckEndpoints(); 29 | 30 | // Custom endpoints 31 | 32 | app.MapGet("/", () => "Hello from 'internal-http-bus'").ExcludeFromDescription(); 33 | 34 | 35 | // The simplest of all demo in-memory stores. 36 | HashSet customerIds = new(); 37 | 38 | app.MapGet("/received-customers", () => Results.Ok(customerIds.ToArray())); 39 | 40 | app.MapPost("/receive-customer-created", [Topic("pubsub-internal-http-bus", "customer-created", $"event.type == \"{nameof(CustomerCreatedEvent)}\"", 1)] (CustomerCreatedEvent evt, ILogger logger) => 41 | { 42 | logger.LogWarning("Customer received: {evt}", evt); 43 | 44 | customerIds.Add(evt.CustomerId); 45 | 46 | return Results.Ok("Customer received"); 47 | }); 48 | 49 | app.MapPost("/receive-fallback", [Topic("pubsub-internal-http-bus", "customer-created")] ([FromBody] CloudEvent evt, ILogger logger) => 50 | { 51 | logger.LogWarning("Fallback event received: {evt}", evt); 52 | 53 | throw new NotSupportedException("No handler for " + evt.Type); 54 | }); 55 | 56 | app.Run(); 57 | -------------------------------------------------------------------------------- /services/_internal-http-bus/InternalHttpBus.Api/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/launchsettings.json", 3 | "profiles": { 4 | "http": { 5 | "commandName": "Project", 6 | "dotnetRunMessages": true, 7 | "launchBrowser": true, 8 | "launchUrl": "swagger", 9 | "applicationUrl": "http://localhost:5071", 10 | "environmentVariables": { 11 | "ASPNETCORE_ENVIRONMENT": "Development" 12 | } 13 | }, 14 | "https": { 15 | "commandName": "Project", 16 | "dotnetRunMessages": true, 17 | "launchBrowser": true, 18 | "launchUrl": "swagger", 19 | "applicationUrl": "https://localhost:7085;http://localhost:5071", 20 | "environmentVariables": { 21 | "ASPNETCORE_ENVIRONMENT": "Development" 22 | } 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /services/_internal-http-bus/InternalHttpBus.Api/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /services/_internal-http-bus/InternalHttpBus.Api/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | }, 8 | "AllowedHosts": "*" 9 | } 10 | -------------------------------------------------------------------------------- /services/_internal-http-bus/InternalHttpBus.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.30114.105 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Shared", "..\..\shared\Shared\Shared.csproj", "{976F22AB-934E-40EC-8F7F-D3788F82DDC2}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "InternalHttpBus.Api", "InternalHttpBus.Api\InternalHttpBus.Api.csproj", "{E337404D-D5C5-4CF2-B194-E7EA86211E47}" 9 | EndProject 10 | Global 11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 12 | Debug|Any CPU = Debug|Any CPU 13 | Release|Any CPU = Release|Any CPU 14 | EndGlobalSection 15 | GlobalSection(SolutionProperties) = preSolution 16 | HideSolutionNode = FALSE 17 | EndGlobalSection 18 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 19 | {976F22AB-934E-40EC-8F7F-D3788F82DDC2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 20 | {976F22AB-934E-40EC-8F7F-D3788F82DDC2}.Debug|Any CPU.Build.0 = Debug|Any CPU 21 | {976F22AB-934E-40EC-8F7F-D3788F82DDC2}.Release|Any CPU.ActiveCfg = Release|Any CPU 22 | {976F22AB-934E-40EC-8F7F-D3788F82DDC2}.Release|Any CPU.Build.0 = Release|Any CPU 23 | {E337404D-D5C5-4CF2-B194-E7EA86211E47}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 24 | {E337404D-D5C5-4CF2-B194-E7EA86211E47}.Debug|Any CPU.Build.0 = Debug|Any CPU 25 | {E337404D-D5C5-4CF2-B194-E7EA86211E47}.Release|Any CPU.ActiveCfg = Release|Any CPU 26 | {E337404D-D5C5-4CF2-B194-E7EA86211E47}.Release|Any CPU.Build.0 = Release|Any CPU 27 | EndGlobalSection 28 | EndGlobal 29 | -------------------------------------------------------------------------------- /services/_internal-http-bus/README.md: -------------------------------------------------------------------------------- 1 | An internal service that: 2 | * uses Dapr pub/sub to subscribe to "CustomerCreatedEvent"-events from the topic "customer-created" (published by "internal-grpc-sql-bus") 3 | * has a "/receive-fallback"-endpoint which subscribes to any message coming to that topic that is not a "CustomerCreatedEvent" event and logs it as an error. 4 | * exposes a HTTP API endpoint "GET /received-customers": It returns data about the "CustomerCreatedEvent"-messages it received from "internal-grpc-sql-bus" 5 | -------------------------------------------------------------------------------- /services/_public-razor/PublicRazor.Web/Pages/Index.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model PublicRazor.Web.Pages.IndexModel 3 | @{ 4 | } 5 | 6 |

Hello from 'public-razor'!

7 | -------------------------------------------------------------------------------- /services/_public-razor/PublicRazor.Web/Pages/Index.cshtml.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc.RazorPages; 2 | 3 | namespace PublicRazor.Web.Pages; 4 | 5 | public class IndexModel : PageModel 6 | { 7 | public void OnGet() 8 | { 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /services/_public-razor/PublicRazor.Web/Pages/InternalGrpc.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model PublicRazor.Web.Pages.InternalGrpcModel 3 | @{ 4 | } 5 | 6 |

Entities from internal-grpc

7 |

Note that 'internal-grpc' persists data to in-memory only, so entities might not be returned if multiple instances are running or if they have been shut down.

8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | @foreach (var entity in Model.Entities) 18 | { 19 | 20 | 21 | 22 | 23 | } 24 | 25 |
Entity IDDisplay Name
@entity.EntityId@entity.DisplayName
26 | 27 |
28 |

Create a new entity

29 |
30 | Display name: 31 | 32 |
33 | 34 | -------------------------------------------------------------------------------- /services/_public-razor/PublicRazor.Web/Pages/InternalGrpc.cshtml.cs: -------------------------------------------------------------------------------- 1 | using InternalGrpc.Api; 2 | using Microsoft.AspNetCore.Mvc; 3 | using Microsoft.AspNetCore.Mvc.RazorPages; 4 | 5 | namespace PublicRazor.Web.Pages; 6 | 7 | public class InternalGrpcModel : PageModel 8 | { 9 | private readonly InternalGrpcEntities.InternalGrpcEntitiesClient _internalGrpcClient; 10 | 11 | public IList Entities { get; private set; } = new List(); 12 | 13 | [BindProperty] 14 | public string? NewEntityDisplayName { get; set; } 15 | 16 | public InternalGrpcModel(InternalGrpcEntities.InternalGrpcEntitiesClient internalGrpcClient) 17 | { 18 | _internalGrpcClient = internalGrpcClient; 19 | } 20 | 21 | public async Task OnGetAsync() 22 | { 23 | var response = await _internalGrpcClient.ListEntitiesAsync(new ListEntitiesRequest(), cancellationToken: HttpContext.RequestAborted); 24 | Entities = response.Entities; 25 | 26 | return Page(); 27 | } 28 | 29 | public async Task OnPostAsync() 30 | { 31 | if (!ModelState.IsValid) 32 | { 33 | return Page(); 34 | } 35 | 36 | if (!string.IsNullOrWhiteSpace(NewEntityDisplayName)) 37 | { 38 | var request = new CreateEntityRequest 39 | { 40 | Entity = new InternalGrpcEntityDto() 41 | { 42 | EntityId = Guid.NewGuid().ToString(), 43 | DisplayName = NewEntityDisplayName, 44 | } 45 | }; 46 | await _internalGrpcClient.CreateEntityAsync(request, cancellationToken: HttpContext.RequestAborted); 47 | } 48 | 49 | return RedirectToPage(); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /services/_public-razor/PublicRazor.Web/Pages/InternalGrpcSqlBus.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model PublicRazor.Web.Pages.InternalGrpcSqlBusModel 3 | @{ 4 | } 5 | 6 |

Customers from internal-grpc-sql-bus

7 |

Data is persisted in a SQL database!

8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | @foreach (var customer in Model.Customers) 18 | { 19 | 20 | 21 | 22 | 23 | } 24 | 25 |
Entity IDDisplay Name
@customer.CustomerId@customer.FullName
26 | 27 |
28 |

Create a new customer

29 |
30 | Full name: 31 | 32 |
33 | 34 | -------------------------------------------------------------------------------- /services/_public-razor/PublicRazor.Web/Pages/InternalGrpcSqlBus.cshtml.cs: -------------------------------------------------------------------------------- 1 | using InternalGrpcSqlBus.Api; 2 | using Microsoft.AspNetCore.Mvc; 3 | using Microsoft.AspNetCore.Mvc.RazorPages; 4 | 5 | namespace PublicRazor.Web.Pages; 6 | 7 | public class InternalGrpcSqlBusModel : PageModel 8 | { 9 | private readonly Customers.CustomersClient _customersClient; 10 | 11 | public IList Customers { get; private set; } = new List(); 12 | 13 | [BindProperty] 14 | public string? NewCustomerFullName { get; set; } 15 | 16 | public InternalGrpcSqlBusModel(Customers.CustomersClient customersClient) 17 | { 18 | _customersClient = customersClient; 19 | } 20 | 21 | public async Task OnGetAsync() 22 | { 23 | var response = await _customersClient.ListCustomersAsync(new ListCustomersRequest(), cancellationToken: HttpContext.RequestAborted); 24 | Customers = response.Customers; 25 | 26 | return Page(); 27 | } 28 | 29 | public async Task OnPostAsync() 30 | { 31 | if (!ModelState.IsValid) 32 | { 33 | return Page(); 34 | } 35 | 36 | if (!string.IsNullOrWhiteSpace(NewCustomerFullName)) 37 | { 38 | var request = new CreateCustomerRequest() 39 | { 40 | Customer = new CustomerDto() 41 | { 42 | CustomerId = Guid.NewGuid().ToString(), 43 | FullName = NewCustomerFullName, 44 | } 45 | }; 46 | await _customersClient.CreateCustomerAsync(request, cancellationToken: HttpContext.RequestAborted); 47 | } 48 | 49 | return RedirectToPage(); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /services/_public-razor/PublicRazor.Web/Pages/InternalHttpBus.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model PublicRazor.Web.Pages.InternalHttpBusModel 3 | @{ 4 | } 5 | 6 |

Customers received by internal-http-bus

7 |

'internal-http-bus' subscribes to CustomerCreated-events from 'internal-grpc-sql-bus' and offers an HTTP endpoint to display the received customer ids.

8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | @foreach (var customerId in Model.CustomerIds) 17 | { 18 | 19 | 20 | 21 | } 22 | 23 |
Customer ID
@customerId
24 | -------------------------------------------------------------------------------- /services/_public-razor/PublicRazor.Web/Pages/InternalHttpBus.cshtml.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc.RazorPages; 2 | 3 | namespace PublicRazor.Web.Pages; 4 | 5 | public class InternalHttpBusModel : PageModel 6 | { 7 | private readonly IHttpClientFactory _httpClientFactory; 8 | 9 | public List CustomerIds { get; private set; } = new(); 10 | 11 | public InternalHttpBusModel(IHttpClientFactory httpClientFactory) 12 | { 13 | _httpClientFactory = httpClientFactory; 14 | } 15 | 16 | public async Task OnGet() 17 | { 18 | var httpClient = _httpClientFactory.CreateClient("internal-http-bus"); 19 | 20 | var receivedCustomers = await httpClient.GetFromJsonAsync>("/received-customers") 21 | ?? new List(); 22 | CustomerIds = receivedCustomers; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /services/_public-razor/PublicRazor.Web/Pages/Shared/_Layout.cshtml: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | Public/Razor 8 | 9 | 10 |
11 |

Public/Razor Pages

12 |

13 |

    14 |
  • internal-grpc (a simple gRPC API that holds data in-memory only)
  • 15 |
  • internal-grpc-sql-bus (a gRPC API that persists data in an Azure SQL database and publishes events to Azure Service Bus)
  • 16 |
  • internal-http-bus (a HTTP API that receives events from Azure Service Bus, stores them in-memory and publishes an HTTP endpoint with details about the events)
  • 17 |
18 |

19 |
20 |
21 | 22 |
23 | @RenderBody() 24 |
25 | 26 | @await RenderSectionAsync("Scripts", required: false) 27 | 28 | 29 | -------------------------------------------------------------------------------- /services/_public-razor/PublicRazor.Web/Pages/_ViewImports.cshtml: -------------------------------------------------------------------------------- 1 | @using PublicRazor.Web 2 | @namespace PublicRazor.Web.Pages 3 | @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers 4 | -------------------------------------------------------------------------------- /services/_public-razor/PublicRazor.Web/Pages/_ViewStart.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | Layout = "_Layout"; 3 | } 4 | -------------------------------------------------------------------------------- /services/_public-razor/PublicRazor.Web/Program.cs: -------------------------------------------------------------------------------- 1 | using Azure.Identity; 2 | using InternalGrpc.Api; 3 | using InternalGrpcSqlBus.Api; 4 | using Microsoft.AspNetCore.DataProtection; 5 | using Shared; 6 | 7 | var builder = WebApplication.CreateBuilder(args); 8 | 9 | // Add services to the container. 10 | 11 | // Azure 12 | var azureCredential = new DefaultAzureCredential(); 13 | 14 | // ASP.NET Core 15 | builder.Services.AddRazorPages(); 16 | 17 | // ASP.NET Core Data Protection (to support e.g. anti-forgery with multiple instances) 18 | var dataProtectionBuilder = builder.Services.AddDataProtection(); 19 | if (!builder.Environment.IsDevelopment()) 20 | { 21 | dataProtectionBuilder.PersistKeysToAzureBlobStorage( 22 | blobUri: new Uri(builder.Configuration["DataProtectionBlobUri"] ?? throw new InvalidOperationException("Config value 'DataProtectionBlobUri' not set")), 23 | tokenCredential: azureCredential); 24 | 25 | dataProtectionBuilder.ProtectKeysWithAzureKeyVault( 26 | keyIdentifier: new Uri(builder.Configuration["DataProtectionKeyUri"] ?? throw new InvalidOperationException("Config value 'DataProtectionKeyUri' not set")), 27 | tokenCredential: azureCredential); 28 | } 29 | 30 | // Application Insights 31 | builder.Services.AddCustomAppInsights(); 32 | 33 | // gRPC Clients (uses a custom extension method from Shared) 34 | builder.Services.AddDaprGrpcClient("internal-grpc"); 35 | builder.Services.AddDaprGrpcClient("internal-grpc-sql-bus"); 36 | 37 | // HTTP Clients (uses a custom extension method from Shared) 38 | builder.Services.AddDaprHttpClient("internal-http-bus"); 39 | 40 | // Health checks 41 | builder.Services.AddHealthChecks(); 42 | 43 | var app = builder.Build(); 44 | 45 | 46 | // Configure the HTTP request pipeline. 47 | 48 | app.UseDeveloperExceptionPage(); 49 | 50 | app.UseStaticFiles(); 51 | 52 | app.UseRouting(); 53 | 54 | app.UseAuthorization(); 55 | 56 | app.MapRazorPages(); 57 | 58 | // Health checks 59 | app.MapCustomHealthCheckEndpoints(); 60 | 61 | app.Run(); 62 | -------------------------------------------------------------------------------- /services/_public-razor/PublicRazor.Web/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "http": { 4 | "commandName": "Project", 5 | "dotnetRunMessages": true, 6 | "launchBrowser": true, 7 | "applicationUrl": "http://localhost:5173", 8 | "environmentVariables": { 9 | "ASPNETCORE_ENVIRONMENT": "Development" 10 | } 11 | }, 12 | "https": { 13 | "commandName": "Project", 14 | "dotnetRunMessages": true, 15 | "launchBrowser": true, 16 | "applicationUrl": "https://localhost:7274;http://localhost:5173", 17 | "environmentVariables": { 18 | "ASPNETCORE_ENVIRONMENT": "Development" 19 | } 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /services/_public-razor/PublicRazor.Web/PublicRazor.Web.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net7.0 5 | true 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | Protos\_internal-grpc-sql-bus.proto 22 | 23 | 24 | Protos\_internal-grpc.proto 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /services/_public-razor/PublicRazor.Web/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /services/_public-razor/PublicRazor.Web/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | }, 8 | "AllowedHosts": "*" 9 | } 10 | -------------------------------------------------------------------------------- /services/_public-razor/PublicRazor.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.30114.105 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PublicRazor.Web", "PublicRazor.Web\PublicRazor.Web.csproj", "{30608E6A-9B12-42E4-9B6B-074C4BA7D4F1}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Shared", "..\..\shared\Shared\Shared.csproj", "{D565BB3D-F73B-45A5-BEA8-B4DF9AFF938F}" 9 | EndProject 10 | Global 11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 12 | Debug|Any CPU = Debug|Any CPU 13 | Release|Any CPU = Release|Any CPU 14 | EndGlobalSection 15 | GlobalSection(SolutionProperties) = preSolution 16 | HideSolutionNode = FALSE 17 | EndGlobalSection 18 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 19 | {30608E6A-9B12-42E4-9B6B-074C4BA7D4F1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 20 | {30608E6A-9B12-42E4-9B6B-074C4BA7D4F1}.Debug|Any CPU.Build.0 = Debug|Any CPU 21 | {30608E6A-9B12-42E4-9B6B-074C4BA7D4F1}.Release|Any CPU.ActiveCfg = Release|Any CPU 22 | {30608E6A-9B12-42E4-9B6B-074C4BA7D4F1}.Release|Any CPU.Build.0 = Release|Any CPU 23 | {D565BB3D-F73B-45A5-BEA8-B4DF9AFF938F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 24 | {D565BB3D-F73B-45A5-BEA8-B4DF9AFF938F}.Debug|Any CPU.Build.0 = Debug|Any CPU 25 | {D565BB3D-F73B-45A5-BEA8-B4DF9AFF938F}.Release|Any CPU.ActiveCfg = Release|Any CPU 26 | {D565BB3D-F73B-45A5-BEA8-B4DF9AFF938F}.Release|Any CPU.Build.0 = Release|Any CPU 27 | EndGlobalSection 28 | EndGlobal 29 | -------------------------------------------------------------------------------- /services/_public-razor/README.md: -------------------------------------------------------------------------------- 1 | A public service that: 2 | * uses ASP.NET Core Razor pages as its frontend architecture 3 | * uses Azure Storage and Azure Key Vault to configure ASP.NET Core DataProtection (required for multi-instance support of anti-forgery tokens, etc.) 4 | * exposes a "/InternalGrpc" page that uses a gRPC-client to talk to the "internal-grpc" service. 5 | * exposes a "/InternalGrpcSqlBus" page that uses a gRPC client to talk to the "internal-grpc-sql-bus" service. 6 | * exposes a "/InternalHttpBus" page that uses DaprClient to talk to the HTTP based "internal-http-bus" service. 7 | -------------------------------------------------------------------------------- /shared/Shared/AppInsights/AppInsightsExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.ApplicationInsights.Extensibility; 2 | using Shared.AppInsights; 3 | 4 | namespace Microsoft.Extensions.DependencyInjection; 5 | 6 | public static class AppInsightsExtensions 7 | { 8 | public static IServiceCollection AddCustomAppInsights(this IServiceCollection services) 9 | { 10 | services.AddApplicationInsightsTelemetry(x => 11 | { 12 | // No need to track performance counters separately as they are tracked in Container Apps anyway. 13 | x.EnablePerformanceCounterCollectionModule = false; 14 | }); 15 | 16 | services.AddSingleton(); 17 | return services; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /shared/Shared/AppInsights/AppInsightsHttpMessageHandler.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.ApplicationInsights; 2 | using Microsoft.ApplicationInsights.DataContracts; 3 | 4 | namespace Shared.AppInsights; 5 | 6 | /// 7 | /// Sends the response body of failed requests to Application Insights to simplify troubleshooting. 8 | /// 9 | public class AppInsightsHttpMessageHandler : DelegatingHandler 10 | { 11 | private readonly TelemetryClient _telemetryClient; 12 | 13 | public AppInsightsHttpMessageHandler(TelemetryClient telemetryClient) 14 | { 15 | _telemetryClient = telemetryClient; 16 | } 17 | 18 | protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) 19 | { 20 | var response = await base.SendAsync(request, cancellationToken); 21 | 22 | if (!response.IsSuccessStatusCode) 23 | { 24 | int responseStatus = (int)response.StatusCode; 25 | 26 | var responseBody = await response.Content.ReadAsStringAsync(cancellationToken); 27 | 28 | _telemetryClient.TrackTrace( 29 | "Http call returned non-success status " + responseStatus, 30 | SeverityLevel.Warning, 31 | new Dictionary 32 | { 33 | {"ResultCode", responseStatus.ToString()}, 34 | {"ResponseBody", responseBody} 35 | }); 36 | } 37 | 38 | return response; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /shared/Shared/AppInsights/ApplicationNameTelemetryInitializer.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.ApplicationInsights.Channel; 2 | using Microsoft.ApplicationInsights.Extensibility; 3 | 4 | namespace Shared.AppInsights; 5 | 6 | /// 7 | /// Sets the "RoleName" for each telemetry. 8 | /// 9 | public class ApplicationNameTelemetryInitializer : ITelemetryInitializer 10 | { 11 | private readonly string _appId; 12 | 13 | public ApplicationNameTelemetryInitializer() 14 | { 15 | // Dapr APP_ID 16 | // https://docs.dapr.io/reference/environment/ 17 | _appId = Environment.GetEnvironmentVariable("APP_ID") ?? string.Empty; 18 | } 19 | 20 | public void Initialize(ITelemetry telemetry) 21 | { 22 | telemetry.Context.Cloud.RoleName = _appId; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /shared/Shared/DaprHelpers.cs: -------------------------------------------------------------------------------- 1 | using Dapr; 2 | using Dapr.Client; 3 | using Grpc.Net.ClientFactory; 4 | using Microsoft.Extensions.DependencyInjection; 5 | using Microsoft.Extensions.DependencyInjection.Extensions; 6 | using Shared.AppInsights; 7 | 8 | namespace Shared; 9 | 10 | public static class DaprHelpers 11 | { 12 | private static Uri? grpcEndpoint; 13 | 14 | /// 15 | /// Get the value of environment variable DAPR_GRPC_PORT 16 | /// 17 | /// The value of environment variable DAPR_GRPC_PORT 18 | public static Uri GetDefaultGrpcEndpoint() 19 | { 20 | if (grpcEndpoint == null) 21 | { 22 | var port = Environment.GetEnvironmentVariable("DAPR_GRPC_PORT"); 23 | port = string.IsNullOrEmpty(port) ? "50001" : port; 24 | grpcEndpoint = new Uri($"http://127.0.0.1:{port}"); 25 | } 26 | 27 | return grpcEndpoint; 28 | } 29 | 30 | public static IHttpClientBuilder AddDaprHttpClient(this IServiceCollection services, string appId) 31 | { 32 | services.TryAddTransient(); 33 | 34 | var baseUrl = (Environment.GetEnvironmentVariable("BASE_URL") ?? "http://localhost") + ":" + (Environment.GetEnvironmentVariable("DAPR_HTTP_PORT") ?? "3500"); 35 | 36 | return services.AddHttpClient(appId, httpClient => 37 | { 38 | httpClient.BaseAddress = new Uri(baseUrl); 39 | httpClient.DefaultRequestHeaders.Add("dapr-app-id", appId); 40 | }).AddHttpMessageHandler(); 41 | } 42 | 43 | public static IHttpClientBuilder AddDaprGrpcClient(this IServiceCollection services, string appId, string? daprEndpoint = null, string? daprApiToken = null) 44 | where TClient : class 45 | { 46 | return services.AddGrpcClient(o => 47 | { 48 | o.Address = daprEndpoint != null ? new Uri(daprEndpoint) : GetDefaultGrpcEndpoint(); 49 | 50 | // Dapr Interceptor 51 | o.InterceptorRegistrations.Add(new InterceptorRegistration(InterceptorScope.Channel, _ => new InvocationInterceptor(appId, daprApiToken))); 52 | }).EnableCallContextPropagation(o => o.SuppressContextNotFoundErrors = true); 53 | } 54 | 55 | /// 56 | /// We must manually construct the cloud event because the .NET SDK doesn't change the default "type" (com.dapr.event.sent) 57 | /// 58 | public static CloudEvent CreateCloudEvent(T message) 59 | { 60 | return new CloudEvent(message) 61 | { 62 | Type = typeof(T).Name 63 | }; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /shared/Shared/HealthCheckEndpointsExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Diagnostics.HealthChecks; 2 | using Microsoft.AspNetCore.Routing; 3 | 4 | namespace Microsoft.AspNetCore.Builder; 5 | 6 | public static class HealthCheckEndpointsExtensions 7 | { 8 | public static IEndpointRouteBuilder MapCustomHealthCheckEndpoints(this IEndpointRouteBuilder app) 9 | { 10 | // https://andrewlock.net/deploying-asp-net-core-applications-to-kubernetes-part-6-adding-health-checks-with-liveness-readiness-and-startup-probes/ 11 | app.MapHealthChecks("/healthz/startup"); // Execute all checks on startup 12 | app.MapHealthChecks("/healthz/liveness", new HealthCheckOptions { Predicate = _ => false }); // Liveness only tests if the app can serve requests 13 | 14 | return app; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /shared/Shared/Shared.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net7.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | Protos\types.proto 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /shared/Shared/Types/DecimalValue.cs: -------------------------------------------------------------------------------- 1 | namespace Shared.Types; 2 | 3 | public partial class DecimalValue 4 | { 5 | private const decimal NanoFactor = 1_000_000_000; 6 | 7 | public DecimalValue(long units, int nanos) 8 | { 9 | Units = units; 10 | Nanos = nanos; 11 | } 12 | 13 | public static implicit operator decimal(DecimalValue grpcDecimal) 14 | { 15 | return grpcDecimal.Units + grpcDecimal.Nanos / NanoFactor; 16 | } 17 | 18 | public static implicit operator DecimalValue(decimal value) 19 | { 20 | var units = decimal.ToInt64(value); 21 | var nanos = decimal.ToInt32((value - units) * NanoFactor); 22 | return new DecimalValue(units, nanos); 23 | } 24 | } 25 | --------------------------------------------------------------------------------