├── .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 | Entity ID
13 | Display Name
14 |
15 |
16 |
17 | @foreach (var entity in Model.Entities)
18 | {
19 |
20 | @entity.EntityId
21 | @entity.DisplayName
22 |
23 | }
24 |
25 |
26 |
27 |
28 | Create a new entity
29 |
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 | Entity ID
13 | Display Name
14 |
15 |
16 |
17 | @foreach (var customer in Model.Customers)
18 | {
19 |
20 | @customer.CustomerId
21 | @customer.FullName
22 |
23 | }
24 |
25 |
26 |
27 |
28 | Create a new customer
29 |
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 | Customer ID
13 |
14 |
15 |
16 | @foreach (var customerId in Model.CustomerIds)
17 | {
18 |
19 | @customerId
20 |
21 | }
22 |
23 |
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 |
--------------------------------------------------------------------------------