diff --git a/charts/service-deployment/Chart.lock b/charts/service-deployment/Chart.lock new file mode 100644 index 0000000..86a1166 --- /dev/null +++ b/charts/service-deployment/Chart.lock @@ -0,0 +1,9 @@ +dependencies: +- name: dockerconfigjson + repository: https://snowplow-devops.github.io/helm-charts + version: 0.1.0 +- name: cloudserviceaccount + repository: https://snowplow-devops.github.io/helm-charts + version: 0.1.0 +digest: sha256:92127ad4fb4b1721b3a51927e4e199bb40ae8d26f0c85369b672845bf061ddaf +generated: "2022-07-25T09:44:35.530345+02:00" diff --git a/charts/service-deployment/Chart.yaml b/charts/service-deployment/Chart.yaml new file mode 100644 index 0000000..422d971 --- /dev/null +++ b/charts/service-deployment/Chart.yaml @@ -0,0 +1,22 @@ +apiVersion: v2 +name: service-deployment +description: A Helm Chart to setup a generic deployment with optional service/hpa bindings +version: 0.1.0 +icon: https://raw.githubusercontent.com/snowplow-devops/helm-charts/master/docs/logo/snowplow.png +home: https://github.com/snowplow-devops/helm-charts +sources: + - https://github.com/snowplow-devops/helm-charts +maintainers: + - name: jbeemster + url: https://github.com/jbeemster + email: jbeemster@users.noreply.github.com +keywords: + - service + - deployment +dependencies: + - name: dockerconfigjson + version: 0.1.0 + repository: "https://snowplow-devops.github.io/helm-charts" + - name: cloudserviceaccount + version: 0.1.0 + repository: "https://snowplow-devops.github.io/helm-charts" diff --git a/charts/service-deployment/README.md b/charts/service-deployment/README.md new file mode 100644 index 0000000..0bcd83e --- /dev/null +++ b/charts/service-deployment/README.md @@ -0,0 +1,79 @@ +# service-deployment + +A helm chart to deploy a generic deployment with optional service bindings. + +## TL;DR + +```bash +helm repo add snowplow-devops https://snowplow-devops.github.io/helm-charts +helm install service-deployment snowplow-devops/service-deployment +``` + +## Introduction + +This chart attempts to take care of all the most common requirements of launching a long-running service that requires auto-scaling: + +- Downloading from private Docker repositories +- Auto-scaling pods based on CPU usage +- Mounting config volumes +- Configuring secrets +- Binding service-accounts with cloud specific IAM policies + +_Note_: This should be a long running process - if you are looking for cron-based execution see our `cron-job` chart. + +This chart won't solve for every possible option of deploying a service - it is meant to serve as an opinionated starting point to get something working decently well. For more flexibility open a PR or fork this chart to suit your specific needs. + +## Installing the Chart + +Install or upgrading the chart with default configuration: + +```bash +helm upgrade --install service-deployment snowplow-devops/service-deployment +``` + +## Uninstalling the Chart + +To uninstall/delete the `service-deployment` release: + +```bash +helm delete service-deployment +``` + +## Configuration + +| Key | Type | Default | Description | +|-----|------|---------|-------------| +| global.cloud | string | `""` | Cloud specific bindings (options: aws, gcp) | +| image.repository | string | `"nginx"` | | +| image.tag | string | `"latest"` | | +| image.isRepositoryPublic | bool | `true` | Whether the repository is public | +| config.command | list | `[]` | | +| config.args | list | `[]` | | +| config.env | string | `nil` | Map of environment variables to use within the job | +| config.secrets | object | `{}` | Map of secrets that will be exposed as environment variables within the job | +| configMaps | list | `[]` | List of config maps to mount to the deployment | +| resources | object | `{}` | Map of resource constraints for the service | +| readinessProbe.httpGet.path | string | `""` | Path for health checks to be performed (note: set to "" to disable) | +| readinessProbe.initialDelaySeconds | int | `5` | | +| readinessProbe.periodSeconds | int | `5` | | +| readinessProbe.timeoutSeconds | int | `5` | | +| readinessProbe.failureThreshold | int | `3` | | +| readinessProbe.successThreshold | int | `2` | | +| terminationGracePeriodSeconds | int | `60` | Grace period for termination of the service | +| hpa.deploy | bool | `true` | Whether to deploy HPA rules | +| hpa.minReplicas | int | `1` | Minimum number of pods to deploy | +| hpa.maxReplicas | int | `20` | Maximum number of pods to deploy | +| hpa.averageCPUUtilization | int | `75` | Average CPU utilization before auto-scaling starts | +| service.deploy | bool | `true` | Whether to setup service bindings (note: only NodePort is supported) | +| service.port | int | `80` | Port to bind and expose the service on | +| service.aws.targetGroupARN | string | `""` | EC2 TargetGroup ARN to bind the service onto | +| service.gcp.networkEndpointGroupName | string | `""` | Name of the Network Endpoint Group to bind onto | +| dockerconfigjson.name | string | `"snowplow-sd-dockerhub"` | Name of the secret to use for the private repository | +| dockerconfigjson.username | string | `""` | Username for the private repository | +| dockerconfigjson.password | string | `""` | Password for the private repository | +| dockerconfigjson.server | string | `"https://index.docker.io/v1/"` | Repository server URL | +| dockerconfigjson.email | string | `""` | Email address for user of the private repository | +| cloudserviceaccount.deploy | bool | `false` | Whether to create a service-account | +| cloudserviceaccount.name | string | `"snowplow-sd-service-account"` | Name of the service-account to create | +| cloudserviceaccount.aws.roleARN | string | `""` | IAM Role ARN to bind to the k8s service account | +| cloudserviceaccount.gcp.serviceAccount | string | `""` | Service Account email to bind to the k8s service account | diff --git a/charts/service-deployment/templates/NOTES.txt b/charts/service-deployment/templates/NOTES.txt new file mode 100644 index 0000000..68c84cd --- /dev/null +++ b/charts/service-deployment/templates/NOTES.txt @@ -0,0 +1,16 @@ +Deployment {{ .Release.Name }} has been installed/updated - to get basic information about the pods: + + kubectl describe pod {{ .Release.Name }} + +{{- if .Values.service.deploy }} + +The service can be accessed via port {{ .Values.service.port }} on the following DNS names from within your cluster: + + {{ include "app.fullname" . }}.{{ .Release.Namespace }}.svc.cluster.local + +To connect to your server from outside the cluster execute the following commands: + + kubectl port-forward --namespace {{ .Release.Namespace }} svc/{{ include "app.fullname" . }} 8080:{{ .Values.service.port }} + +You can then navigate to your service in your browser at localhost:8080 or issue request with tools like cURL. +{{- end }} diff --git a/charts/service-deployment/templates/_helpers.tpl b/charts/service-deployment/templates/_helpers.tpl new file mode 100644 index 0000000..c652013 --- /dev/null +++ b/charts/service-deployment/templates/_helpers.tpl @@ -0,0 +1,23 @@ +{{/* vim: set filetype=mustache: */}} +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "app.fullname" -}} +{{- if .Values.fullnameOverride -}} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}} +{{- else -}} +{{- .Release.Name | trunc 63 | trimSuffix "-" -}} +{{- end -}} +{{- end -}} +{{- define "app.secret.fullname" -}} +{{ include "app.fullname" . }}-secret +{{- end -}} + +{{/* +Define the default NEG name for GCP deployments. +*/}} +{{- define "service.gcp.networkEndpointGroupName" -}} +{{- default .Release.Name .Values.service.gcp.networkEndpointGroupName -}} +{{- end -}} diff --git a/charts/service-deployment/templates/configmaps.yaml b/charts/service-deployment/templates/configmaps.yaml new file mode 100644 index 0000000..d58b719 --- /dev/null +++ b/charts/service-deployment/templates/configmaps.yaml @@ -0,0 +1,11 @@ +{{- if .Values.configMaps }} +{{- range $v := .Values.configMaps }} +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ $v.name }} +binaryData: + {{ $v.key }}: "{{ $v.contentsB64 }}" +--- +{{- end }} +{{- end }} diff --git a/charts/service-deployment/templates/deployment.yaml b/charts/service-deployment/templates/deployment.yaml new file mode 100644 index 0000000..510349c --- /dev/null +++ b/charts/service-deployment/templates/deployment.yaml @@ -0,0 +1,101 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "app.fullname" . }} +spec: + selector: + matchLabels: + app: {{ include "app.fullname" . }} + template: + metadata: + labels: + app: {{ include "app.fullname" . }} + annotations: + {{- if .Values.configMaps }} + {{- range $v := .Values.configMaps }} + checksum/{{ $v.name }}-{{ $v.key }}: "{{ $v.contentsB64 | sha256sum }}" + {{- end }} + {{- end }} + spec: + {{- if .Values.cloudserviceaccount.deploy }} + serviceAccountName: {{ .Values.cloudserviceaccount.name }} + {{- end }} + automountServiceAccountToken: true + terminationGracePeriodSeconds: {{ .Values.terminationGracePeriodSeconds }} + + {{- if not .Values.image.isRepositoryPublic }} + imagePullSecrets: + - name: {{ .Values.dockerconfigjson.name }} + {{- end }} + + {{- if .Values.configMaps }} + volumes: + {{- range $v := .Values.configMaps }} + - configMap: + name: {{ $v.name }} + optional: false + name: {{ $v.name }} + {{- end }} + {{- end }} + + containers: + - name: "{{ include "app.fullname" . }}" + image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" + imagePullPolicy: Always + + {{- if .Values.config.command }} + command: + {{- range $v := .Values.config.command }} + - "{{ $v }}" + {{- end }} + {{- end }} + + {{- if .Values.config.args }} + args: + {{- range $v := .Values.config.args }} + - "{{ $v }}" + {{- end }} + {{- end }} + + {{- if .Values.config.env }} + env: + {{- range $k, $v := .Values.config.env }} + - name: "{{ $k }}" + value: "{{ $v }}" + {{- end }} + {{- end }} + + {{- if .Values.config.secrets }} + envFrom: + - secretRef: + name: {{ include "app.secret.fullname" . }} + {{- end }} + + resources: + {{- toYaml .Values.resources | nindent 10 }} + + {{- if ne .Values.readinessProbe.httpGet.path "" }} + readinessProbe: + httpGet: + path: {{ .Values.readinessProbe.httpGet.path }} + port: {{ .Values.service.port }} + scheme: HTTP + initialDelaySeconds: {{ .Values.readinessProbe.initialDelaySeconds }} + periodSeconds: {{ .Values.readinessProbe.periodSeconds }} + timeoutSeconds: {{ .Values.readinessProbe.timeoutSeconds }} + failureThreshold: {{ .Values.readinessProbe.failureThreshold }} + successThreshold: {{ .Values.readinessProbe.successThreshold }} + {{- end }} + + {{- if .Values.configMaps }} + volumeMounts: + {{- range $v := .Values.configMaps }} + - mountPath: "{{ $v.mountPath }}" + {{- if $v.mountPropagation }} + mountPropagation: {{ $v.mountPropagation }} + {{- else }} + mountPropagation: None + {{- end }} + name: {{ $v.name }} + {{- end }} + {{- end }} diff --git a/charts/service-deployment/templates/hpa.yaml b/charts/service-deployment/templates/hpa.yaml new file mode 100644 index 0000000..f560fed --- /dev/null +++ b/charts/service-deployment/templates/hpa.yaml @@ -0,0 +1,14 @@ +{{- if .Values.hpa.deploy }} +apiVersion: autoscaling/v1 +kind: HorizontalPodAutoscaler +metadata: + name: {{ include "app.fullname" . }} +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ include "app.fullname" . }} + minReplicas: {{ .Values.hpa.minReplicas }} + maxReplicas: {{ .Values.hpa.maxReplicas }} + targetCPUUtilizationPercentage: {{ .Values.hpa.averageCPUUtilization }} +{{- end }} diff --git a/charts/service-deployment/templates/secrets.yaml b/charts/service-deployment/templates/secrets.yaml new file mode 100644 index 0000000..7f7dfab --- /dev/null +++ b/charts/service-deployment/templates/secrets.yaml @@ -0,0 +1,12 @@ +{{- if .Values.config.secrets }} +apiVersion: v1 +kind: Secret +metadata: + namespace: {{ .Release.Namespace }} + name: {{ include "app.secret.fullname" . }} +type: Opaque +data: + {{- range $k, $v := .Values.config.secrets }} + {{ $k }}: "{{ $v | b64enc }}" + {{- end }} +{{- end }} diff --git a/charts/service-deployment/templates/service.yaml b/charts/service-deployment/templates/service.yaml new file mode 100644 index 0000000..9de3576 --- /dev/null +++ b/charts/service-deployment/templates/service.yaml @@ -0,0 +1,20 @@ +{{- if .Values.service.deploy }} +apiVersion: v1 +kind: Service +metadata: + name: {{ include "app.fullname" . }} + {{- if eq .Values.global.cloud "gcp" }} + annotations: + cloud.google.com/app-protocols: '{"http-port": "HTTP"}' + cloud.google.com/neg: '{"exposed_ports": {"{{ .Values.service.port }}":{"name": "{{ include "service.gcp.networkEndpointGroupName" . }}"}}}' + {{- end }} +spec: + type: NodePort + selector: + app: {{ include "app.fullname" . }} + ports: + - name: http-port + port: {{ .Values.service.port }} + protocol: TCP + targetPort: {{ .Values.service.port }} +{{- end }} diff --git a/charts/service-deployment/templates/targetgroupbinding.yaml b/charts/service-deployment/templates/targetgroupbinding.yaml new file mode 100644 index 0000000..1e747a6 --- /dev/null +++ b/charts/service-deployment/templates/targetgroupbinding.yaml @@ -0,0 +1,13 @@ +{{- if .Values.service.deploy }} +{{- if eq .Values.global.cloud "aws" }} +apiVersion: elbv2.k8s.aws/v1beta1 +kind: TargetGroupBinding +metadata: + name: {{ include "app.fullname" . }} +spec: + serviceRef: + name: {{ include "app.fullname" . }} + port: {{ .Values.service.port }} + targetGroupARN: {{ .Values.service.aws.targetGroupARN }} +{{- end }} +{{- end }} diff --git a/charts/service-deployment/values.yaml b/charts/service-deployment/values.yaml new file mode 100644 index 0000000..aa18f70 --- /dev/null +++ b/charts/service-deployment/values.yaml @@ -0,0 +1,100 @@ +global: + # -- Cloud specific bindings (options: aws, gcp) + cloud: "" + +image: + repository: "nginx" + tag: "latest" + # -- Whether the repository is public + isRepositoryPublic: true + +config: + command: [] + # - "/bin/sh" + args: [] + # - "-c" + # - "echo 'Environment $(hello_env)! Secret $(username).'" + + # -- Map of environment variables to use within the job + env: + # hello_env: "world" + + # -- Map of secrets that will be exposed as environment variables within the job + secrets: {} + # username: "password" + +# -- List of config maps to mount to the deployment +configMaps: [] +# - name: "volume-1" +# key: "file.cfg" +# contentsB64: "" # The file contents which have already been base-64 encoded +# mountPath: "/etc/config" # Must be unique +# mountPropagation: None # If unset will default to 'None' + +# -- Map of resource constraints for the service +resources: {} +# limits: +# cpu: 746m +# memory: 900Mi +# requests: +# cpu: 400m +# memory: 512Mi + +readinessProbe: + httpGet: + # -- Path for health checks to be performed (note: set to "" to disable) + path: "" + initialDelaySeconds: 5 + periodSeconds: 5 + timeoutSeconds: 5 + failureThreshold: 3 + successThreshold: 2 + +# -- Grace period for termination of the service +terminationGracePeriodSeconds: 60 + +hpa: + # -- Whether to deploy HPA rules + deploy: true + # -- Minimum number of pods to deploy + minReplicas: 1 + # -- Maximum number of pods to deploy + maxReplicas: 20 + # -- Average CPU utilization before auto-scaling starts + averageCPUUtilization: 75 + +service: + # -- Whether to setup service bindings (note: only NodePort is supported) + deploy: true + # -- Port to bind and expose the service on + port: 80 + aws: + # -- EC2 TargetGroup ARN to bind the service onto + targetGroupARN: "" + gcp: + # -- Name of the Network Endpoint Group to bind onto + networkEndpointGroupName: "" + +dockerconfigjson: + # -- Name of the secret to use for the private repository + name: "snowplow-sd-dockerhub" + # -- Username for the private repository + username: "" + # -- Password for the private repository + password: "" + # -- Repository server URL + server: "https://index.docker.io/v1/" + # -- Email address for user of the private repository + email: "" + +cloudserviceaccount: + # -- Whether to create a service-account + deploy: false + # -- Name of the service-account to create + name: "snowplow-sd-service-account" + aws: + # -- IAM Role ARN to bind to the k8s service account + roleARN: "" + gcp: + # -- Service Account email to bind to the k8s service account + serviceAccount: ""