├── .github └── workflows │ └── continuous-integration-workflow.yml ├── .gitignore ├── HelmChart └── k8sfriendlyaspnetcore │ ├── .helmignore │ ├── Chart.yaml │ ├── templates │ ├── _helpers.tpl │ ├── deployment.yaml │ ├── secret.yaml │ └── service.yaml │ └── values.yaml ├── LICENSE ├── azure-pipelines.yml ├── readme.md └── src ├── .dockerignore ├── Controllers ├── HealthController.cs └── ValuesController.cs ├── Dockerfile ├── LifetimeEventsHostedService.cs ├── Models └── HealthEndpointResponse.cs ├── Program.cs ├── SimpleDockerfile ├── SimpleDockerfile.Alpine ├── Startup.cs ├── appsettings.Development.json ├── appsettings.json └── k8s-friendly-aspnetcore.csproj /.github/workflows/continuous-integration-workflow.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | paths: 5 | - 'src/**' 6 | pull_request: 7 | paths: 'src/**' 8 | 9 | jobs: 10 | buildJob: 11 | name: Compile, test & Dockerize 12 | runs-on: ubuntu-latest 13 | env: 14 | PROJECTFOLDER: 'src/' 15 | ASSEMBLY_VERSION: '1.0.0' 16 | CONFIGURATION: Release 17 | steps: 18 | - uses: actions/checkout@v1 19 | - name: Setup .NET Core 20 | uses: actions/setup-dotnet@v1 21 | with: 22 | dotnet-version: 2.2.108 23 | - name: dotnet restore 24 | run: dotnet restore $PROJECTFOLDER 25 | - name: dotnet build 26 | run: dotnet build $PROJECTFOLDER --configuration $CONFIGURATION /p:Version=$ASSEMBLY_VERSION --no-restore 27 | - name: dotnet test 28 | run: dotnet test $PROJECTFOLDER --configuration $CONFIGURATION --no-build 29 | - name: dotnet publish 30 | run: dotnet publish $PROJECTFOLDER --configuration $CONFIGURATION --no-build --output $GITHUB_WORKSPACE/publish/ 31 | - name: Upload artifact 32 | uses: actions/upload-artifact@v1.0.0 33 | with: 34 | name: files 35 | path: publish/ 36 | - name: Docker Login 37 | if: github.ref == 'refs/heads/master' 38 | uses: Azure/docker-login@v1 39 | with: 40 | # login-server: myregistry.azurecr.io 41 | username: ${{ secrets.DOCKER_HUB_USERNAME }} 42 | password: ${{ secrets.DOCKER_HUB_PASSWORD }} 43 | - name: Build and push Docker image 44 | if: github.ref == 'refs/heads/master' 45 | env: 46 | DOCKERCONTEXT: 'publish/' 47 | DOCKERFILE: 'src/SimpleDockerfile' 48 | DOCKERIMAGE: 'anderslybecker/k8sfriendlyaspnetcore' 49 | run: | 50 | docker build $DOCKERCONTEXT -f $DOCKERFILE -t $DOCKERIMAGE:$ASSEMBLY_VERSION 51 | docker push $DOCKERIMAGE:$ASSEMBLY_VERSION 52 | -------------------------------------------------------------------------------- /.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 | *.suo 8 | *.user 9 | *.userosscache 10 | *.sln.docstates 11 | 12 | # User-specific files (MonoDevelop/Xamarin Studio) 13 | *.userprefs 14 | 15 | # Build results 16 | [Dd]ebug/ 17 | [Dd]ebugPublic/ 18 | [Rr]elease/ 19 | [Rr]eleases/ 20 | x64/ 21 | x86/ 22 | bld/ 23 | [Bb]in/ 24 | [Oo]bj/ 25 | [Ll]og/ 26 | 27 | # Visual Studio 2015/2017 cache/options directory 28 | .vs/ 29 | # Uncomment if you have tasks that create the project's static files in wwwroot 30 | #wwwroot/ 31 | 32 | # Visual Studio 2017 auto generated files 33 | Generated\ Files/ 34 | 35 | # MSTest test Results 36 | [Tt]est[Rr]esult*/ 37 | [Bb]uild[Ll]og.* 38 | 39 | # NUNIT 40 | *.VisualState.xml 41 | TestResult.xml 42 | 43 | # Build Results of an ATL Project 44 | [Dd]ebugPS/ 45 | [Rr]eleasePS/ 46 | dlldata.c 47 | 48 | # Benchmark Results 49 | BenchmarkDotNet.Artifacts/ 50 | 51 | # .NET Core 52 | project.lock.json 53 | project.fragment.lock.json 54 | artifacts/ 55 | **/Properties/launchSettings.json 56 | 57 | # StyleCop 58 | StyleCopReport.xml 59 | 60 | # Files built by Visual Studio 61 | *_i.c 62 | *_p.c 63 | *_i.h 64 | *.ilk 65 | *.meta 66 | *.obj 67 | *.iobj 68 | *.pch 69 | *.pdb 70 | *.ipdb 71 | *.pgc 72 | *.pgd 73 | *.rsp 74 | *.sbr 75 | *.tlb 76 | *.tli 77 | *.tlh 78 | *.tmp 79 | *.tmp_proj 80 | *.log 81 | *.vspscc 82 | *.vssscc 83 | .builds 84 | *.pidb 85 | *.svclog 86 | *.scc 87 | 88 | # Chutzpah Test files 89 | _Chutzpah* 90 | 91 | # Visual C++ cache files 92 | ipch/ 93 | *.aps 94 | *.ncb 95 | *.opendb 96 | *.opensdf 97 | *.sdf 98 | *.cachefile 99 | *.VC.db 100 | *.VC.VC.opendb 101 | 102 | # Visual Studio profiler 103 | *.psess 104 | *.vsp 105 | *.vspx 106 | *.sap 107 | 108 | # Visual Studio Trace Files 109 | *.e2e 110 | 111 | # TFS 2012 Local Workspace 112 | $tf/ 113 | 114 | # Guidance Automation Toolkit 115 | *.gpState 116 | 117 | # ReSharper is a .NET coding add-in 118 | _ReSharper*/ 119 | *.[Rr]e[Ss]harper 120 | *.DotSettings.user 121 | 122 | # JustCode is a .NET coding add-in 123 | .JustCode 124 | 125 | # TeamCity is a build add-in 126 | _TeamCity* 127 | 128 | # DotCover is a Code Coverage Tool 129 | *.dotCover 130 | 131 | # AxoCover is a Code Coverage Tool 132 | .axoCover/* 133 | !.axoCover/settings.json 134 | 135 | # Visual Studio code coverage results 136 | *.coverage 137 | *.coveragexml 138 | 139 | # NCrunch 140 | _NCrunch_* 141 | .*crunch*.local.xml 142 | nCrunchTemp_* 143 | 144 | # MightyMoose 145 | *.mm.* 146 | AutoTest.Net/ 147 | 148 | # Web workbench (sass) 149 | .sass-cache/ 150 | 151 | # Installshield output folder 152 | [Ee]xpress/ 153 | 154 | # DocProject is a documentation generator add-in 155 | DocProject/buildhelp/ 156 | DocProject/Help/*.HxT 157 | DocProject/Help/*.HxC 158 | DocProject/Help/*.hhc 159 | DocProject/Help/*.hhk 160 | DocProject/Help/*.hhp 161 | DocProject/Help/Html2 162 | DocProject/Help/html 163 | 164 | # Click-Once directory 165 | publish/ 166 | 167 | # Publish Web Output 168 | *.[Pp]ublish.xml 169 | *.azurePubxml 170 | # Note: Comment the next line if you want to checkin your web deploy settings, 171 | # but database connection strings (with potential passwords) will be unencrypted 172 | *.pubxml 173 | *.publishproj 174 | 175 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 176 | # checkin your Azure Web App publish settings, but sensitive information contained 177 | # in these scripts will be unencrypted 178 | PublishScripts/ 179 | 180 | # NuGet Packages 181 | *.nupkg 182 | # The packages folder can be ignored because of Package Restore 183 | **/[Pp]ackages/* 184 | # except build/, which is used as an MSBuild target. 185 | !**/[Pp]ackages/build/ 186 | # Uncomment if necessary however generally it will be regenerated when needed 187 | #!**/[Pp]ackages/repositories.config 188 | # NuGet v3's project.json files produces more ignorable files 189 | *.nuget.props 190 | *.nuget.targets 191 | 192 | # Microsoft Azure Build Output 193 | csx/ 194 | *.build.csdef 195 | 196 | # Microsoft Azure Emulator 197 | ecf/ 198 | rcf/ 199 | 200 | # Windows Store app package directories and files 201 | AppPackages/ 202 | BundleArtifacts/ 203 | Package.StoreAssociation.xml 204 | _pkginfo.txt 205 | *.appx 206 | 207 | # Visual Studio cache files 208 | # files ending in .cache can be ignored 209 | *.[Cc]ache 210 | # but keep track of directories ending in .cache 211 | !*.[Cc]ache/ 212 | 213 | # Others 214 | ClientBin/ 215 | ~$* 216 | *~ 217 | *.dbmdl 218 | *.dbproj.schemaview 219 | *.jfm 220 | *.pfx 221 | *.publishsettings 222 | orleans.codegen.cs 223 | 224 | # Including strong name files can present a security risk 225 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 226 | #*.snk 227 | 228 | # Since there are multiple workflows, uncomment next line to ignore bower_components 229 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 230 | #bower_components/ 231 | 232 | # RIA/Silverlight projects 233 | Generated_Code/ 234 | 235 | # Backup & report files from converting an old project file 236 | # to a newer Visual Studio version. Backup files are not needed, 237 | # because we have git ;-) 238 | _UpgradeReport_Files/ 239 | Backup*/ 240 | UpgradeLog*.XML 241 | UpgradeLog*.htm 242 | ServiceFabricBackup/ 243 | *.rptproj.bak 244 | 245 | # SQL Server files 246 | *.mdf 247 | *.ldf 248 | *.ndf 249 | 250 | # Business Intelligence projects 251 | *.rdl.data 252 | *.bim.layout 253 | *.bim_*.settings 254 | *.rptproj.rsuser 255 | 256 | # Microsoft Fakes 257 | FakesAssemblies/ 258 | 259 | # GhostDoc plugin setting file 260 | *.GhostDoc.xml 261 | 262 | # Node.js Tools for Visual Studio 263 | .ntvs_analysis.dat 264 | node_modules/ 265 | 266 | # Visual Studio 6 build log 267 | *.plg 268 | 269 | # Visual Studio 6 workspace options file 270 | *.opt 271 | 272 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 273 | *.vbw 274 | 275 | # Visual Studio LightSwitch build output 276 | **/*.HTMLClient/GeneratedArtifacts 277 | **/*.DesktopClient/GeneratedArtifacts 278 | **/*.DesktopClient/ModelManifest.xml 279 | **/*.Server/GeneratedArtifacts 280 | **/*.Server/ModelManifest.xml 281 | _Pvt_Extensions 282 | 283 | # Paket dependency manager 284 | .paket/paket.exe 285 | paket-files/ 286 | 287 | # FAKE - F# Make 288 | .fake/ 289 | 290 | # JetBrains Rider 291 | .idea/ 292 | *.sln.iml 293 | 294 | # CodeRush 295 | .cr/ 296 | 297 | # Python Tools for Visual Studio (PTVS) 298 | __pycache__/ 299 | *.pyc 300 | 301 | # Cake - Uncomment if you are using it 302 | # tools/** 303 | # !tools/packages.config 304 | 305 | # Tabs Studio 306 | *.tss 307 | 308 | # Telerik's JustMock configuration file 309 | *.jmconfig 310 | 311 | # BizTalk build output 312 | *.btp.cs 313 | *.btm.cs 314 | *.odx.cs 315 | *.xsd.cs 316 | 317 | # OpenCover UI analysis results 318 | OpenCover/ 319 | 320 | # Azure Stream Analytics local run output 321 | ASALocalRun/ 322 | 323 | # MSBuild Binary and Structured Log 324 | *.binlog 325 | 326 | # NVidia Nsight GPU debugger configuration file 327 | *.nvuser 328 | 329 | # MFractors (Xamarin productivity tool) working folder 330 | .mfractor/ 331 | -------------------------------------------------------------------------------- /HelmChart/k8sfriendlyaspnetcore/.helmignore: -------------------------------------------------------------------------------- 1 | # Patterns to ignore when building packages. 2 | # This supports shell glob matching, relative path matching, and 3 | # negation (prefixed with !). Only one pattern per line. 4 | .DS_Store 5 | # Common VCS dirs 6 | .git/ 7 | .gitignore 8 | .bzr/ 9 | .bzrignore 10 | .hg/ 11 | .hgignore 12 | .svn/ 13 | # Common backup files 14 | *.swp 15 | *.bak 16 | *.tmp 17 | *~ 18 | # Various IDEs 19 | .project 20 | .idea/ 21 | *.tmproj 22 | .vscode/ 23 | -------------------------------------------------------------------------------- /HelmChart/k8sfriendlyaspnetcore/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | appVersion: "1.0" 3 | description: A Kubernetes friendly ASP.NET Core sample Helm chart 4 | name: k8sfriendlyaspnetcore 5 | version: 0.1.0 6 | -------------------------------------------------------------------------------- /HelmChart/k8sfriendlyaspnetcore/templates/_helpers.tpl: -------------------------------------------------------------------------------- 1 | {{/* vim: set filetype=mustache: */}} 2 | {{/* 3 | Expand the name of the chart. 4 | */}} 5 | {{- define "DotNetCoreDocker.name" -}} 6 | {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} 7 | {{- end -}} 8 | 9 | {{/* 10 | Create a default fully qualified app name. 11 | We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). 12 | If release name contains chart name it will be used as a full name. 13 | */}} 14 | {{- define "DotNetCoreDocker.fullname" -}} 15 | {{- if .Values.fullnameOverride -}} 16 | {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}} 17 | {{- else -}} 18 | {{- $name := default .Chart.Name .Values.nameOverride -}} 19 | {{- if contains $name .Release.Name -}} 20 | {{- .Release.Name | trunc 63 | trimSuffix "-" -}} 21 | {{- else -}} 22 | {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}} 23 | {{- end -}} 24 | {{- end -}} 25 | {{- end -}} 26 | 27 | {{/* 28 | Create chart name and version as used by the chart label. 29 | */}} 30 | {{- define "DotNetCoreDocker.chart" -}} 31 | {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}} 32 | {{- end -}} 33 | 34 | {{/* 35 | Common labels 36 | */}} 37 | {{- define "DotNetCoreDocker.labels" -}} 38 | app.kubernetes.io/name: {{ include "DotNetCoreDocker.name" . }} 39 | helm.sh/chart: {{ include "DotNetCoreDocker.chart" . }} 40 | app.kubernetes.io/instance: {{ .Release.Name }} 41 | {{- if .Chart.AppVersion }} 42 | app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} 43 | {{- end }} 44 | app.kubernetes.io/managed-by: {{ .Release.Service }} 45 | {{- end -}} 46 | -------------------------------------------------------------------------------- /HelmChart/k8sfriendlyaspnetcore/templates/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: {{ include "DotNetCoreDocker.fullname" . }} 5 | labels: 6 | {{ include "DotNetCoreDocker.labels" . | indent 4 }} 7 | spec: 8 | replicas: {{ .Values.replicaCount }} 9 | selector: 10 | matchLabels: 11 | app.kubernetes.io/name: {{ include "DotNetCoreDocker.name" . }} 12 | app.kubernetes.io/instance: {{ .Release.Name }} 13 | template: 14 | metadata: 15 | labels: 16 | app.kubernetes.io/name: {{ include "DotNetCoreDocker.name" . }} 17 | app.kubernetes.io/instance: {{ .Release.Name }} 18 | spec: 19 | securityContext: 20 | # runAsUser: 1000 21 | # runAsGroup: 1000 22 | # fsGroup: 1000 23 | {{- with .Values.imagePullSecrets }} 24 | imagePullSecrets: 25 | {{- toYaml . | nindent 8 }} 26 | {{- end }} 27 | restartPolicy: Always 28 | containers: 29 | - name: {{ .Chart.Name }} 30 | image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" 31 | imagePullPolicy: {{ .Values.image.pullPolicy }} 32 | securityContext: 33 | allowPrivilegeEscalation: false 34 | ports: 35 | - name: http 36 | containerPort: {{ .Values.containerPort }} 37 | protocol: TCP 38 | - name: https 39 | containerPort: {{ .Values.containerPortHttps }} 40 | protocol: TCP 41 | livenessProbe: 42 | httpGet: 43 | path: /api/health 44 | port: {{ .Values.containerPort }} 45 | readinessProbe: 46 | httpGet: 47 | path: /api/health/ready 48 | port: {{ .Values.containerPort }} 49 | initialDelaySeconds: 10 # Start probing after 10 seconds of container creation. 50 | timeoutSeconds: 1 # Timeout if you don't get a response in 1 second. 51 | periodSeconds: 10 # Probe frequency in seconds. 52 | failureThreshold: 2 # Probe declares failure after 2 attempts. 53 | resources: 54 | requests: 55 | memory: "128Mi" 56 | cpu: "100m" 57 | limits: 58 | memory: "256Mi" 59 | cpu: "500m" 60 | env: 61 | - name: MyVar 62 | value: "blah" 63 | - name: Kestrel__Certificates__Default__Path 64 | value: /certs/aspnetcore-cert.pfx 65 | - name: Kestrel__Certificates__Default__Password 66 | valueFrom: 67 | secretKeyRef: 68 | name: {{ .Release.Name }}-secret 69 | key: certificatePassword 70 | volumeMounts: 71 | - name: certificatesvolume 72 | mountPath: /certs/aspnetcore-cert.pfx 73 | subPath: aspnetcore-cert.pfx 74 | readOnly: true 75 | volumes: 76 | - name: certificatesvolume 77 | secret: 78 | secretName: {{ .Release.Name }}-secret 79 | items: 80 | - key: certificate 81 | path: aspnetcore-cert.pfx -------------------------------------------------------------------------------- /HelmChart/k8sfriendlyaspnetcore/templates/secret.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Secret 3 | metadata: 4 | name: {{ .Release.Name }}-secret 5 | data: 6 | certificatePassword: {{ .Values.certificatePassword | b64enc }} 7 | certificate: {{ .Files.Get "_aspnetcore-cert.pfx" | b64enc }} 8 | -------------------------------------------------------------------------------- /HelmChart/k8sfriendlyaspnetcore/templates/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: {{ include "DotNetCoreDocker.fullname" . }} 5 | labels: 6 | {{ include "DotNetCoreDocker.labels" . | indent 4 }} 7 | spec: 8 | type: {{ .Values.service.type }} 9 | ports: 10 | - port: {{ .Values.service.port }} 11 | targetPort: {{ .Values.containerPortHttps }} 12 | protocol: TCP 13 | name: https 14 | selector: 15 | app.kubernetes.io/name: {{ include "DotNetCoreDocker.name" . }} 16 | app.kubernetes.io/instance: {{ .Release.Name }} 17 | -------------------------------------------------------------------------------- /HelmChart/k8sfriendlyaspnetcore/values.yaml: -------------------------------------------------------------------------------- 1 | # Default values for DotNetCoreDocker. 2 | # This is a YAML-formatted file. 3 | # Declare variables to be passed into your templates. 4 | 5 | replicaCount: 1 6 | 7 | containerPort: 5000 8 | containerPortHttps: 5001 9 | 10 | certificatePassword: "createyourownpassword" 11 | 12 | image: 13 | repository: anderslybecker/k8sfriendlyaspnetcore 14 | tag: v1 15 | pullPolicy: IfNotPresent 16 | 17 | imagePullSecrets: [] 18 | nameOverride: "" 19 | fullnameOverride: "" 20 | 21 | service: 22 | type: LoadBalancer #ClusterIP 23 | port: 443 -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Anders Lybecker 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 | -------------------------------------------------------------------------------- /azure-pipelines.yml: -------------------------------------------------------------------------------- 1 | variables: 2 | solution: '**/*.sln' 3 | buildConfiguration: 'Release' 4 | imgTage: '$(Build.BuildId)' 5 | projects: '**/*.csproj' 6 | assemblyVersion : '1.0.0' 7 | 8 | stages: 9 | - stage: ServiceCICD 10 | displayName: Service CI/CD 11 | jobs: 12 | - job: BuildAndTestSolutionAndPublishBuildArtifacts 13 | displayName: Build and test solution and publish artifacts 14 | pool: 'Hosted Ubuntu 1604' 15 | 16 | steps: 17 | - task: NuGetToolInstaller@0 18 | 19 | - task: DotNetCoreCLI@2 20 | displayName: 'dotnet restore' 21 | inputs: 22 | command: 'restore' 23 | projects: $(projects) 24 | 25 | - task: DotNetCoreCLI@2 26 | displayName: 'dotnet build' 27 | inputs: 28 | command: 'build' 29 | projects: $(projects) 30 | arguments: '--configuration $(BuildConfiguration) --no-restore /p:AssemblyVersion=$(assemblyVersion).$(imgTage)' 31 | 32 | - task: DotNetCoreCLI@2 33 | displayName: 'dotnet test' 34 | inputs: 35 | command: 'test' 36 | projects: '**/*Tests/*.csproj' 37 | arguments: '--configuration $(BuildConfiguration) --no-build' 38 | publishTestResults: true 39 | 40 | - task: DotNetCoreCLI@2 41 | displayName: 'dotnet publish' 42 | inputs: 43 | command: 'publish' 44 | publishWebProjects: false 45 | projects: $(projects) 46 | arguments: '--configuration $(BuildConfiguration) --no-build --output $(build.artifactstagingdirectory)' # could use Build.BinariesDirectory as output 47 | zipAfterPublish: false 48 | 49 | - task: PublishPipelineArtifact@1 50 | displayName: 'Publish Build Artifacts' 51 | inputs: 52 | path: $(build.artifactstagingdirectory) 53 | artifact: drop 54 | 55 | - job: CreateDockerImage 56 | displayName: Build and Push Docker image 57 | pool: 'Hosted Ubuntu 1604' 58 | dependsOn: BuildAndTestSolutionAndPublishBuildArtifacts 59 | condition: eq(variables['Build.SourceBranch'], 'refs/heads/master') # if master branch 60 | 61 | steps: 62 | - task: DownloadPipelineArtifact@2 63 | displayName: 'Download Build Artifacts' 64 | inputs: 65 | artifact: drop 66 | targetPath: '$(Build.SourcesDirectory)/publish' 67 | 68 | - task: Docker@2 69 | displayName: 'Docker build & push' 70 | inputs: 71 | containerRegistry: 'Lybecker Docker Hub' 72 | repository: 'anderslybecker/k8sfriendlyaspnetcore' 73 | command: 'buildAndPush' 74 | Dockerfile: '$(Build.SourcesDirectory)/src/SimpleDockerfile' 75 | buildContext: '$(Build.SourcesDirectory)/publish/src/' 76 | tags: | 77 | $(imgTage) 78 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | Sample project for a Kubernetes friendly ASP.NET core application 2 | - Dockerfile using [Microsoft Container Registry (MCR)](https://azure.microsoft.com/en-us/blog/microsoft-syndicates-container-catalog/) base images 3 | - Dockerfile exposing both HTTP and HTTPS custom ports overwritten default [Kestrel settings](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/servers/kestrel?view=aspnetcore-2.2) 4 | - Helm chart for deployment on Kubernetes cluster 5 | - Mount HTTPS (TLS/SSL) certificate via volume secret, certificate password as secret and use it with Kestrel 6 | - Health endpoint for probes - used for warmup process and to determind if process is responsive 7 | - Graceful shutdown with SIGTERM and SIGKILL - giving the application time to cleanup connections etc. 8 | - Specify the compute resources needed to run and maximum consumption 9 | - Execute as unprivileged account 10 | 11 | [![Build Status](https://lybecker.visualstudio.com/Microsoft/_apis/build/status/Lybecker.k8s-friendly-aspnetcore?branchName=master)](https://lybecker.visualstudio.com/Microsoft/_build/latest?definitionId=23&branchName=master) 12 | 13 | # Test on Kubernetes cluster 14 | Assuming you have a cluster, kubectl and [helm](https://helm.sh) configured. 15 | 16 | > Execute the commands from the repository base folder. 17 | 18 | 1. Clone this repository 19 | 2. Generate self-signed certificate 20 | ```bash 21 | dotnet dev-certs https -v -ep .\helmchart\k8sfriendlyaspnetcore\_aspnetcore-cert.pfx -p createyourownpassword 22 | ``` 23 | 3. Install via Helm 24 | ```bash 25 | helm install helmchart\k8sfriendlyaspnetcore\ --name nameofdeployment 26 | ``` 27 | The Kubernetes deployment will use image [anderslybecker/k8sfriendlyaspnetcore](https://hub.docker.com/r/anderslybecker/k8sfriendlyaspnetcore) from Docker Hub. Overwrite like this: 28 | ```bash 29 | helm install helmchart\k8sfriendlyaspnetcore\ --name nameofdeployment --set image.repository 30 | ``` 31 | 4. Test the endpoint 32 | ```bash 33 | curl --insecure -i https:///api/values 34 | ``` 35 | Replacing the `` with the external IP from the Kubernetes cluster. Check with command 36 | ```bash 37 | helm status nameofdeployment 38 | ``` 39 | 5. Kill the pod and observe the behavior 40 | Observe the process will gracefully shutdown doing any clean-up before terminating. 41 | 42 | Observice the Pod logs with this command: 43 | ```bash 44 | kubectl logs --follow 45 | ``` 46 | > The Pod name will be something like `nameofdeployment-k8sfriendlyaspnetcore-bdfd6b6c7-cl6jm`. Check with helm status command as the previous step. 47 | 48 | > Notice that you can see the Kubernetes health probes polling every few seconds. 49 | 50 | Kill the Pod and watch the logs 51 | ```bash 52 | kubectl kill 53 | ``` 54 | 55 | > Kubernetes will automatically create a new Pod if it is terminated. Verify bu testing the endpoint with curl and check the helm status. 56 | 57 | To remove and purge the helm installation from the Kubernetes cluster run: 58 | ```bash 59 | helm del --purge nameofdeployment 60 | ``` 61 | 62 | # Details 63 | 64 | ## Built Docker image 65 | Run command in the `src` folder where the Dockerfile is located 66 | ```bash 67 | docker build --tag k8sfriendlyaspnetcore:v1 . 68 | ``` 69 | 70 | > Requires [Docker installed and running](https://docs.docker.com/install/) locally 71 | 72 | ## Volume mount HTTPS certificate 73 | Create a self-signed certificate and export it to a file: 74 | 75 | ```bash 76 | dotnet dev-certs https -v -ep .\HelmChart\k8sfriendlyaspnetcore\_aspnetcore-cert.pfx -p createyourownpassword 77 | ``` 78 | 79 | **Note:** Development certificates on dotnet core 3.1 on Linux might be affected by [this OpenSSL issue](https://github.com/openssl/openssl/issues/1418#issuecomment-573388933). On Linux the workaround to create a self-signed certificate is documented in [this blogpost](https://andrewlock.net/creating-and-trusting-a-self-signed-certificate-on-linux-for-use-in-kestrel-and-asp-net-core/#creating-a-certificate-with-dns-san). 80 | 81 | > The certificate is stored in the Helm chart folder. 82 | 83 | ## Try to run the container locally with self-signed certificate 84 | 85 | This is how we will run the container 86 | - exposing port 5000 & 5001 87 | - Set `Kestrel__Certificates__Default__Path` path to the certificate location 88 | - Set `Kestrel__Certificates__Default__Password` value of the password 89 | - mount volume with certificate .\HelmChart\k8sfriendlyaspnetcore\templates\ to /root/.dotnet/https 90 | 91 | With the following command 92 | ```bash 93 | docker run --name myk8sfriendlyaspnetcorecontainer 94 | -p 5000:5000 -p 5001:5001 95 | -e Kestrel__Certificates__Default__Path=/certs/_aspnetcore-cert.pfx 96 | -e Kestrel__Certificates__Default__Password=createyourownpassword 97 | -v .\HelmChart\k8sfriendlyaspnetcore\templates\:/certs 98 | anderslybecker/k8sfriendlyaspnetcore:v1 99 | ``` 100 | > Mounting a volume on Windows does not allow usage of relative paths. Modify the volume mount path to e.g. `c:\code\k8s-friendly-aspnetcore\HelmChart\k8sfriendlyaspnetcore\templates\` 101 | 102 | > Expose both HTTP and HTTPS on non-standard ports. Normally port 80 and 443 is used, but running the ASP.NET process as an unprivileged non-root account requires to use ports above 1024. 103 | 104 | ## Kubernetes liveness and readiness probes 105 | A custom `HealthController` is used for health monitoring. You could use the build-in [health monitoring in ASP.NET Core](https://docs.microsoft.com/en-us/aspnet/core/host-and-deploy/health-checks?view=aspnetcore-2.2). 106 | 107 | >As a self-signed certificate is used, Kubernetes probes will fail, so the `app.UseHttpsRedirection()` is removed in the `Startup` class. Resulting in a HTTP request will not be redirected to HTTPS. 108 | 109 | The Kubernetes deployment specifics the endpoints and ports 110 | ```dockerfile 111 | livenessProbe: 112 | httpGet: 113 | path: /api/health 114 | port: {{ .Values.containerPort }} 115 | readinessProbe: 116 | httpGet: 117 | path: /api/health/ready 118 | port: {{ .Values.containerPort }} 119 | ``` 120 | > The `{{ .Values.containerPort }}` variable is specified in the Helm values file. 121 | 122 | See [Kubernetes deployment example](/HelmChart/k8sfriendlyaspnetcore/templates/deployment.yaml). 123 | 124 | ## Graceful shutdown 125 | Sometimes it is required to buy some time for shutdown of a process. Perhaps a transaction needs to be completed or a connections needs to be properly closed. That is why we need the ability to shutdown gracefully. 126 | 127 | ASP.NET Core exposes [application life-cycle events](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/host/generic-host?view=aspnetcore-2.2#iapplicationlifetime-interface) for this purpose. 128 | 129 | The `LifetimeEventsHostedService` class implements the ASP.NET Core application life-cycle events and simulates a process requiring extra time to shutdown. 130 | 131 | > The default allowed shutdown timeout is 5 seconds, but we can increase it by calling the [`UseShutdownTimeout`](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/host/web-host?view=aspnetcore-2.2#shutdown-timeout) extension method on the WebHostBuilder in Program.Main() method or configuring with the environment variable `ASPNETCORE_SHUTDOWNTIMEOUTSECONDS`. 132 | 133 | > The default max grace termination period for a Kubernetes Pod is 30 sec. But it can be changed via the terminationGracePeriodSeconds setting. 134 | 135 | ### Kubernetes termination lifecycle 136 | How the Kubernetes termination lifecycle works: 137 | 138 | 1. Pod is set to the “Terminating” State and removed from the endpoints list of all Services 139 | 2. [preStop](https://kubernetes.io/docs/concepts/containers/container-lifecycle-hooks/) Hook is executed 140 | 3. SIGTERM signal is sent to the pod 141 | 4. Kubernetes waits for a grace period 142 | 5. SIGKILL signal is sent to pod, and the pod is removed 143 | 144 | ### .NET Core 3.0 changes 145 | 146 | For .NET Core 3.0 the application life-cycle events class used in previous verison [IApplicationLifetime](https://docs.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.hosting.iapplicationlifetime?view=aspnetcore-2.2) is depricated and you should use [IHostApplicationLifetime](https://docs.microsoft.com/en-us/dotnet/api/microsoft.extensions.hosting.ihostapplicationlifetime?view=aspnetcore-3.0). It is just a drop-in replacement in the [`LifetimeEventsHostedService`](/src/LifetimeEventsHostedService.cs#L11) file. 147 | 148 | ## Compute resources needed and maximum 149 | In the deployment file, the requested and maximum compute resources are specified: 150 | 151 | ```dockerfile 152 | resources: 153 | requests: 154 | memory: "128Mi" 155 | cpu: "100m" 156 | limits: 157 | memory: "256Mi" 158 | cpu: "500m" 159 | ``` 160 | See [Kubernetes deployment example](/HelmChart/k8sfriendlyaspnetcore/templates/deployment.yaml). 161 | 162 | ## Execute as unprivileged account 163 | By default a Docker container runs as root user (id: 0), which means the app inside the container can do anything inside the container. To adhere to the principle of least privilege, the app running insinde the container should be running in the context of an unprivileged non-root account. 164 | 165 | To test if a container can run without any issues as an an unprivileged non-root account, try to run it with a random user ID (not 0 as it is root). It does not matter if the user ID exists on the host or in the container. It will override settings inside the `Dockerfile`: 166 | 167 | ```bash 168 | docker run --user $((RANDOM+1)) 169 | ``` 170 | 171 | For at ASP.NET application an user with at least execute permissions is needed to execute the application. 172 | Create the user like this in the `Dockerfile`: 173 | 174 | ```dockerfile 175 | RUN groupadd -r grp &&\ 176 | useradd -r -g grp -d /home/app -s /sbin/nologin -c "Docker image user" app 177 | ``` 178 | -r creates a [system account](https://linux.die.net/man/8/useradd). 179 | 180 | By default a new user ID is created the system assigns the next available ID from the range of user IDs specified in the `login.defs` file. 181 | 182 | If you want to reference the user and group ID in the Kubernetes Security Context, then specify the IDs: 183 | ```dockerfile 184 | RUN groupadd -r 999 grp &&\ 185 | useradd -r -u 999 -g grp -d /home/app -s /sbin/nologin -c "Docker image user" app 186 | ``` 187 | 188 | A complete Dockerfile looks like this: 189 | ```dockerfile 190 | FROM mcr.microsoft.com/dotnet/core/aspnet:2.2 191 | 192 | # Declare ports above 1024 as an unprivileged non-root user cannot bind to > 1024 193 | ENV ASPNETCORE_URLS http://+:5000;https://+:5001 194 | EXPOSE 5000 195 | EXPOSE 5001 196 | 197 | ENV USERNAME=appuser 198 | ENV GROUP=grp 199 | ENV HOME=/home/${USERNAME} 200 | 201 | RUN mkdir -p ${HOME} 202 | 203 | # Create a group and an user (system account) which will execute the app 204 | RUN groupadd -r ${GROUP} &&\ 205 | useradd -r -g ${GROUP} -d ${HOME} -s /sbin/nologin -c "Docker image user" ${USERNAME} 206 | 207 | # Setup the app directory 208 | ENV APP_HOME=${HOME}/app 209 | RUN mkdir ${APP_HOME} 210 | WORKDIR ${APP_HOME} 211 | 212 | # Copy in the application code 213 | ADD . ${APP_HOME} 214 | 215 | # Change the context to the app user 216 | USER ${USERNAME} 217 | 218 | ENTRYPOINT ["dotnet", "k8s-friendly-aspnetcore.dll"] 219 | ``` 220 | See an example of a [multi-stage build Dockerfile](/src/Dockerfile). 221 | 222 | > A unprivileged none-root account cannot bind to ports below 1024, hence the default HTTP port 80 og HTTPS port 443 cannot be used. 223 | 224 | In Kubernetes the Security Context controls how a Pod is excecuted. The Security Context can be configured both at Pod and Container level [Configure a Security Context](https://kubernetes.io/docs/tasks/configure-pod-container/security-context/). 225 | 226 | ```dockerfile 227 | securityContext: 228 | runAsUser: 999 229 | runAsGroup: 999 230 | allowPrivilegeEscalation: false 231 | ``` 232 | See [Kubernetes deployment example](/HelmChart/k8sfriendlyaspnetcore/templates/deployment.yaml). 233 | 234 | 235 | `runAsUser` and `runAsGroup` option to specify the Linux user and group executing the process. It overrides the `USER` instruction of the `Dockerfile`. `AllowPrivilegeEscalation` controls whether a process can gain more privileges than its parent process. 236 | 237 | ### Alpine linux 238 | To use the the [Alpine-based Docker image](https://hub.docker.com/_/microsoft-dotnet-core-aspnet/) available for .NET Core a small changes has to be applied, as the functions for adding users and groups has different names. 239 | 240 | ```dockerfile 241 | RUN addgroup -S ${GROUP} && adduser -S ${USERNAME} -G ${GROUP} -h ${HOME} -s /sbin/nologin 242 | ``` 243 | 244 | See the [SimpleDockerfile.Alpine](/src/SimpleDockerfile.Alpine) file for a complete example. 245 | 246 | > It is possible to use the Alpine Guest user (id: 405), if you do not want to create your own. 247 | 248 | # Gotchas 249 | - The base image from Microsoft Container Registry sets listen to port 80, but can be overwritten by setting `ASPNETCORE_URLS` environment variable in the Dockerfile 250 | - Running ASP.Net Core on a Linux host will result in the warning 251 | ``` 252 | Microsoft.AspNetCore.DataProtection.KeyManagement.XmlKeyManager 253 | No XML encryptor configured. Key {61c34317-3cbe-4d98-ae83-b784e89f1320} may be persisted to storage in unencrypted form. 254 | ``` 255 | It can easily be fixed by supplyying a [Data Protection key](https://docs.microsoft.com/en-us/aspnet/core/security/data-protection/configuration/overview?view=aspnetcore-2.2) in the `Startup` class 256 | 257 | # References 258 | - [Developing a Dockerized Asp.Net Core Application Using Visual Studio Code](https://medium.com/@waelkdouh/developing-a-dockerized-asp-net-core-application-using-visual-studio-code-6ccfc59d6f6) 259 | - [Helm Chart Development Tips and Tricks](https://helm.sh/docs/howto/charts_tips_and_tricks/#helm) 260 | - [Managing ASP.NET Core App Settings on Kubernetes](https://anthonychu.ca/post/aspnet-core-appsettings-secrets-kubernetes/) 261 | - [Health checks in ASP.NET Core](https://docs.microsoft.com/en-us/aspnet/core/host-and-deploy/health-checks?view=aspnetcore-2.2) 262 | - [Graceful termination in Kubernetes with ASP.NET Core](https://blog.markvincze.com/graceful-termination-in-kubernetes-with-asp-net-core/#comment-4509101865) 263 | - [Understanding how uid and gid work in Docker containers](https://medium.com/@mccode/understanding-how-uid-and-gid-work-in-docker-containers-c37a01d01cf) 264 | -------------------------------------------------------------------------------- /src/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | Dockerfile* 4 | docker-compose* 5 | .dockerignore 6 | .git 7 | .gitignore 8 | .env 9 | */bin 10 | */obj 11 | README.md 12 | LICENSE 13 | .vscode -------------------------------------------------------------------------------- /src/Controllers/HealthController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | using Lybecker.K8sFriendlyAspNetCore.Models; 3 | 4 | namespace Lybecker.K8sFriendlyAspNetCore.Controllers 5 | { 6 | [Route("api/[controller]")] 7 | [ApiController] 8 | public class HealthController : ControllerBase 9 | { 10 | // If the process ready to serve requests e.g. done warmup of cache etc. 11 | [HttpGet] 12 | [Route("ready")] 13 | public IActionResult Ready() 14 | { 15 | return Ok(new HealthEndpointResponse 16 | { 17 | Message = "Yes. Ready now." 18 | }); 19 | } 20 | 21 | // Check that the ASP.NET runtime responds 22 | [HttpGet] 23 | public IActionResult Alive() 24 | { 25 | return Ok(new HealthEndpointResponse 26 | { 27 | Message = "Alive. All is well." 28 | }); 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /src/Controllers/ValuesController.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using Microsoft.AspNetCore.Mvc; 3 | 4 | namespace Lybecker.K8sFriendlyAspNetCore.Controllers 5 | { 6 | [Route("api/[controller]")] 7 | [ApiController] 8 | public class ValuesController : ControllerBase 9 | { 10 | // GET api/values 11 | [HttpGet] 12 | public ActionResult> Get() 13 | { 14 | return new string[] { "value1", "value2" }; 15 | } 16 | 17 | // GET api/values/5 18 | [HttpGet("{id}")] 19 | public ActionResult Get(int id) 20 | { 21 | return "value"; 22 | } 23 | 24 | // POST api/values 25 | [HttpPost] 26 | public void Post([FromBody] string value) 27 | { 28 | } 29 | 30 | // PUT api/values/5 31 | [HttpPut("{id}")] 32 | public void Put(int id, [FromBody] string value) 33 | { 34 | } 35 | 36 | // DELETE api/values/5 37 | [HttpDelete("{id}")] 38 | public void Delete(int id) 39 | { 40 | } 41 | } 42 | } -------------------------------------------------------------------------------- /src/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mcr.microsoft.com/dotnet/core/aspnet:2.2 AS base 2 | WORKDIR /app 3 | 4 | # Declare ports above 1024 as an unprivileged non-root user cannot bind to > 1024 5 | ENV ASPNETCORE_URLS http://+:5000;https://+:5001 6 | EXPOSE 5000 7 | EXPOSE 5001 8 | 9 | ENV USERNAME=appuser 10 | ENV GROUP=grp 11 | ENV HOME=/home/${USERNAME} 12 | RUN mkdir -p ${HOME} 13 | 14 | # Create a group and an user (system account) which will execute the app 15 | RUN groupadd -r ${GROUP} &&\ 16 | useradd -r -g ${GROUP} -d ${HOME} -s /sbin/nologin -c "Docker image user" ${USERNAME} 17 | 18 | # Restore packages 19 | FROM mcr.microsoft.com/dotnet/core/sdk:2.2 AS build 20 | WORKDIR /src 21 | COPY ["k8s-friendly-aspnetcore.csproj", "./"] 22 | RUN dotnet restore "./k8s-friendly-aspnetcore.csproj" 23 | COPY . . 24 | # WORKDIR "/src/." 25 | # RUN dotnet build "k8s-friendly-aspnetcore.csproj" -c Release -o /app 26 | 27 | # Pack the application and its dependencies into a folder /app 28 | FROM build AS publish 29 | RUN dotnet publish "k8s-friendly-aspnetcore.csproj" -c Release -o /app 30 | 31 | # Copy the publish output into the base image 32 | FROM base AS final 33 | WORKDIR /app 34 | COPY --from=publish /app . 35 | 36 | # Change to the app user. 37 | USER ${USERNAME} 38 | 39 | ENTRYPOINT ["dotnet", "k8s-friendly-aspnetcore.dll"] -------------------------------------------------------------------------------- /src/LifetimeEventsHostedService.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using System.Threading.Tasks; 3 | using Microsoft.Extensions.Hosting; 4 | using Microsoft.Extensions.Logging; 5 | 6 | namespace Lybecker.K8sFriendlyAspNetCore 7 | { 8 | internal class LifetimeEventsHostedService : IHostedService 9 | { 10 | private readonly ILogger _logger; 11 | private readonly IApplicationLifetime _appLifetime; 12 | 13 | public LifetimeEventsHostedService( 14 | ILogger logger, 15 | IApplicationLifetime appLifetime) 16 | { 17 | _logger = logger; 18 | _appLifetime = appLifetime; 19 | } 20 | 21 | public Task StartAsync(CancellationToken cancellationToken) 22 | { 23 | _appLifetime.ApplicationStarted.Register(OnStarted); 24 | _appLifetime.ApplicationStopping.Register(OnStopping); 25 | _appLifetime.ApplicationStopped.Register(OnStopped); 26 | 27 | return Task.CompletedTask; 28 | } 29 | 30 | public Task StopAsync(CancellationToken cancellationToken) 31 | { 32 | return Task.CompletedTask; 33 | } 34 | 35 | private void OnStarted() 36 | { 37 | _logger.LogInformation("OnStarted has been called."); 38 | 39 | // Perform post-startup activities here 40 | } 41 | 42 | private void OnStopping() 43 | { 44 | _logger.LogInformation("OnStopping has been called. Start cleanup for gracefull shutdown."); 45 | 46 | // simulate long shutdown 47 | for (int i = 1; i <= 10; i++) 48 | { 49 | System.Threading.Thread.Sleep(1000); 50 | _logger.LogInformation($"Cleaning up... processing for {i} secs"); 51 | } 52 | } 53 | 54 | private void OnStopped() 55 | { 56 | _logger.LogInformation("OnStopped has been called."); 57 | 58 | // Perform post-stopped activities here 59 | } 60 | } 61 | } -------------------------------------------------------------------------------- /src/Models/HealthEndpointResponse.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | 3 | namespace Lybecker.K8sFriendlyAspNetCore.Models 4 | { 5 | public class HealthEndpointResponse 6 | { 7 | public HealthEndpointResponse() 8 | { 9 | AssemblyVersion = Assembly.GetEntryAssembly()?.GetName().Version.ToString(); 10 | } 11 | 12 | public string Message { get; set; } 13 | public string AssemblyVersion { get; } 14 | } 15 | } -------------------------------------------------------------------------------- /src/Program.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore; 2 | using Microsoft.AspNetCore.Hosting; 3 | 4 | namespace Lybecker.K8sFriendlyAspNetCore 5 | { 6 | public class Program 7 | { 8 | public static void Main(string[] args) 9 | { 10 | CreateWebHostBuilder(args).Build().Run(); 11 | } 12 | 13 | public static IWebHostBuilder CreateWebHostBuilder(string[] args) => 14 | WebHost.CreateDefaultBuilder(args) 15 | .UseStartup(); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/SimpleDockerfile: -------------------------------------------------------------------------------- 1 | FROM mcr.microsoft.com/dotnet/core/aspnet:2.2 2 | 3 | # Declare ports above 1024 as an unprivileged non-root user cannot bind to > 1024 4 | ENV ASPNETCORE_URLS http://+:5000;https://+:5001 5 | EXPOSE 5000 6 | EXPOSE 5001 7 | 8 | ENV USERNAME=appuser 9 | ENV GROUP=grp 10 | ENV HOME=/home/${USERNAME} 11 | 12 | RUN mkdir -p ${HOME} 13 | 14 | # Create a group and an user (system account) which will execute the app 15 | RUN groupadd -r ${GROUP} &&\ 16 | useradd -r -g ${GROUP} -d ${HOME} -s /sbin/nologin -c "Docker image user" ${USERNAME} 17 | 18 | # Setup the app directory 19 | ENV APP_HOME=${HOME}/app 20 | RUN mkdir ${APP_HOME} 21 | WORKDIR ${APP_HOME} 22 | 23 | # Copy in the application code 24 | ADD . ${APP_HOME} 25 | 26 | # Change the context to the app user 27 | USER ${USERNAME} 28 | 29 | ENTRYPOINT ["dotnet", "k8s-friendly-aspnetcore.dll"] 30 | -------------------------------------------------------------------------------- /src/SimpleDockerfile.Alpine: -------------------------------------------------------------------------------- 1 | FROM mcr.microsoft.com/dotnet/core/aspnet:2.2-alpine3.9 2 | 3 | # Declare ports above 1024 as an unprivileged non-root user cannot bind to > 1024 4 | ENV ASPNETCORE_URLS http://+:5000;https://+:5001 5 | EXPOSE 5000 6 | EXPOSE 5001 7 | 8 | ENV USERNAME=appuser 9 | ENV GROUP=grp 10 | ENV HOME=/home/${USERNAME} 11 | 12 | RUN mkdir -p ${HOME} 13 | 14 | # Create a group and an user (system account) which will execute the app 15 | # Alpine uses addgroup and adduser and not groupadd and useradd 16 | RUN addgroup -S ${GROUP} && adduser -S ${USERNAME} -G ${GROUP} -h ${HOME} -s /sbin/nologin 17 | 18 | # Setup the app directory 19 | ENV APP_HOME=${HOME}/app 20 | RUN mkdir ${APP_HOME} 21 | WORKDIR ${APP_HOME} 22 | 23 | # Copy in the application code 24 | ADD . ${APP_HOME} 25 | 26 | # Change the context to the app user 27 | USER ${USERNAME} 28 | 29 | # Alpine docker image doesn't have bash installed by default. 30 | #RUN apk add --no-cache bash 31 | 32 | ENTRYPOINT ["dotnet", "k8s-friendly-aspnetcore.dll"] -------------------------------------------------------------------------------- /src/Startup.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Builder; 2 | using Microsoft.AspNetCore.Hosting; 3 | using Microsoft.AspNetCore.Mvc; 4 | using Microsoft.Extensions.Configuration; 5 | using Microsoft.Extensions.DependencyInjection; 6 | 7 | namespace Lybecker.K8sFriendlyAspNetCore 8 | { 9 | public class Startup 10 | { 11 | public Startup(IConfiguration configuration) 12 | { 13 | Configuration = configuration; 14 | } 15 | 16 | public IConfiguration Configuration { get; } 17 | 18 | // This method gets called by the runtime. Use this method to add services to the container. 19 | public void ConfigureServices(IServiceCollection services) 20 | { 21 | services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2); 22 | services.AddLogging(); 23 | 24 | // register IHostedService for graceful shutdown 25 | services.AddHostedService(); 26 | } 27 | 28 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. 29 | public void Configure(IApplicationBuilder app, IHostingEnvironment env) 30 | { 31 | if (env.IsDevelopment()) 32 | { 33 | app.UseDeveloperExceptionPage(); 34 | } 35 | else 36 | { 37 | // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. 38 | app.UseHsts(); 39 | } 40 | 41 | // Disabled the HTTPS redirect 42 | // app.UseHttpsRedirection(); 43 | app.UseMvc(); 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /src/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Debug", 5 | "System": "Information", 6 | "Microsoft": "Information" 7 | } 8 | } 9 | } -------------------------------------------------------------------------------- /src/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Warning", 5 | "LifetimeEventsHostedService": "Information", 6 | "System": "Information", 7 | "Microsoft": "Information" 8 | } 9 | }, 10 | "AllowedHosts": "*" 11 | } -------------------------------------------------------------------------------- /src/k8s-friendly-aspnetcore.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp2.2 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | --------------------------------------------------------------------------------