diff --git a/http-service/.helmignore b/http-service/.helmignore new file mode 100644 index 00000000..f0c13194 --- /dev/null +++ b/http-service/.helmignore @@ -0,0 +1,21 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*~ +# Various IDEs +.project +.idea/ +*.tmproj diff --git a/http-service/Chart.yaml b/http-service/Chart.yaml new file mode 100644 index 00000000..f666aef8 --- /dev/null +++ b/http-service/Chart.yaml @@ -0,0 +1,5 @@ +apiVersion: v2 +name: http-service +description: An opinionated Helm chart for language-agnostic HTTP services with sensible defaults +type: application +version: 0.1.0 diff --git a/http-service/Makefile b/http-service/Makefile new file mode 100644 index 00000000..e2017320 --- /dev/null +++ b/http-service/Makefile @@ -0,0 +1,60 @@ +KUBERNETES_VERSION = $${KUBERNETES_VERSION:-"1.33.0"} +RELEASE = $$(basename $$PWD) + +.PHONY: install +install: + helm upgrade -i -f examples/deployment.yaml --wait $(RELEASE) . + +.PHONY: lint +lint: lint-deployment lint-deployment-hpa lint-deployment-ingress lint-deployment-keda lint-deployment-rollout + +.PHONY: lint-deployment +lint-deployment: + @echo "=> Linting examples/deployment.yaml" + helm lint --strict -f examples/deployment.yaml + @echo "=> Validating examples/deployment.yaml" + helm template -f examples/deployment.yaml . | kubeval --strict --ignore-missing-schemas --additional-schema-locations https://raw.githubusercontent.com/yannh/kubernetes-json-schema/master/ --kubernetes-version $(KUBERNETES_VERSION) --exit-on-error + +.PHONY: lint-deployment-hpa +lint-deployment-hpa: + @echo "=> Linting examples/deployment-hpa.yaml" + helm lint --strict -f examples/deployment-hpa.yaml + @echo "=> Validating examples/deployment-hpa.yaml" + helm template -f examples/deployment-hpa.yaml . | kubeval --strict --ignore-missing-schemas --additional-schema-locations https://raw.githubusercontent.com/yannh/kubernetes-json-schema/master/ --kubernetes-version $(KUBERNETES_VERSION) --exit-on-error + +.PHONY: lint-deployment-ingress +lint-deployment-ingress: + @echo "=> Linting examples/deployment-ingress.yaml" + helm lint --strict -f examples/deployment-ingress.yaml + @echo "=> Validating examples/deployment-ingress.yaml" + helm template -f examples/deployment-ingress.yaml . | kubeval --strict --ignore-missing-schemas --additional-schema-locations https://raw.githubusercontent.com/yannh/kubernetes-json-schema/master/ --kubernetes-version $(KUBERNETES_VERSION) --exit-on-error + +.PHONY: lint-deployment-keda +lint-deployment-keda: + @echo "=> Linting examples/deployment-keda.yaml" + helm lint --strict -f examples/deployment-keda.yaml + @echo "=> Validating examples/deployment-keda.yaml" + helm template -f examples/deployment-keda.yaml . | kubeval --strict --ignore-missing-schemas --additional-schema-locations https://raw.githubusercontent.com/yannh/kubernetes-json-schema/master/ --kubernetes-version $(KUBERNETES_VERSION) --exit-on-error + +.PHONY: lint-deployment-rollout +lint-deployment-rollout: + @echo "=> Linting examples/deployment-rollout.yaml" + helm lint --strict -f examples/deployment-rollout.yaml + @echo "=> Validating examples/deployment-rollout.yaml" + helm template -f examples/deployment-rollout.yaml . | kubeval --strict --ignore-missing-schemas --additional-schema-locations https://raw.githubusercontent.com/yannh/kubernetes-json-schema/master/ --kubernetes-version $(KUBERNETES_VERSION) --exit-on-error + +.PHONY: test +test: test-deployment + +.PHONY: test-deployment +test-deployment: + @if helm ls | grep -v "NAME" | cut -f1 | grep -e "^$(RELEASE)$$" > /dev/null; then helm uninstall $(RELEASE); fi + @echo "=> Testing examples/deployment.yaml" + helm upgrade -i --wait -f examples/deployment.yaml $(RELEASE)-deployment . + sleep 30 + helm test $(RELEASE)-deployment --logs + helm uninstall $(RELEASE)-deployment + +.PHONY: uninstall +uninstall: + @if helm ls | grep -v "NAME" | cut -f1 | grep -e "^$(RELEASE)$$" > /dev/null; then helm uninstall $(RELEASE); fi diff --git a/http-service/README.md b/http-service/README.md new file mode 100644 index 00000000..d1e38b5f --- /dev/null +++ b/http-service/README.md @@ -0,0 +1,82 @@ +# http-service + +An opinionated Helm chart for language-agnostic HTTP services with sensible defaults. + +## Install + +\`\`\`sh +helm repo add chatwork https://chatwork.github.io/charts +helm install my-service chatwork/http-service -f values.yaml +\`\`\` + +## Opinionated defaults + +| Concern | Default | Rationale | +|---------|---------|-----------| +| Strategy | \`RollingUpdate\` (maxSurge 25%, maxUnavailable 1) | Zero-downtime deploys | +| PDB | \`pod.disruptionBudget.enabled: true\`, \`maxUnavailable: 1\` | Protect availability during node drain | +| Reloader | \`enabled: true\` | Auto-restart on ConfigMap/Secret changes | +| Datadog tags | \`enabled: true\` | Unified service tags (\`env\`, \`service\`, \`version\`) on Deployment and Pod | +| Graceful shutdown | Required (\`trafficDrainSeconds\` + \`appShutdownTimeoutSeconds\`) | Prevent traffic loss during termination | +| Probes | \`startupProbe\` and \`livenessProbe\` required, \`readinessProbe\` optional | Enforce health check discipline | + +## Required values + +The following values must be provided — the template will fail with an explicit error if they are missing: + +- \`image.repository\` / \`image.tag\` +- \`gracefulShutdown.trafficDrainSeconds\` — seconds to sleep in preStop, waiting for LB deregistration +- \`gracefulShutdown.appShutdownTimeoutSeconds\` — seconds the app gets after SIGTERM +- \`startupProbe\` +- \`livenessProbe\` +- \`datadog.env\` / \`datadog.service\` / \`datadog.version\` (when \`datadog.enabled: true\`) + +## Autoscaling + +Controlled by \`autoscaling.type\`: + +| Type | Description | +|------|-------------| +| \`none\` | Fixed replicas (set \`replicas\` value) | +| \`hpa\` | Kubernetes-native HorizontalPodAutoscaler | +| \`keda\` | KEDA ScaledObject (manages HPA internally) | + +When \`autoscaling.type\` is \`hpa\` or \`keda\`, setting \`replicas\` will cause a template error to prevent conflicts. + +## Argo Rollouts + +Set \`rollout.enabled: true\` to create a Rollout resource that references the Deployment via \`workloadRef\`. The HPA/KEDA \`scaleTargetRef\` automatically switches to target the Rollout. + +\`rollout.strategy\` is required when enabled. + +## Graceful shutdown + +The chart auto-generates \`terminationGracePeriodSeconds\` and a preStop hook: + +\`\`\` +|-- terminationGracePeriodSeconds (drain + app) --| +|-- preStop sleep (drain) --|-- app shutdown ------| + ^ + SIGTERM +\`\`\` + +The preStop sleep holds the container alive while the load balancer finishes deregistering the Pod. + +## Examples + +See [\`examples/\`](examples/) for values files covering common patterns: + +| File | Pattern | +|------|---------| +| \`deployment.yaml\` | Basic deployment with fixed replicas | +| \`deployment-hpa.yaml\` | HPA autoscaling | +| \`deployment-ingress.yaml\` | Ingress with ALB | +| \`deployment-keda.yaml\` | KEDA autoscaling | +| \`deployment-rollout.yaml\` | Argo Rollouts canary | + +## Lint & test + +\`\`\`sh +make lint # lint all examples +make test # deploy + helm test (requires cluster) +\`\`\` diff --git a/http-service/examples/deployment-hpa.yaml b/http-service/examples/deployment-hpa.yaml new file mode 100644 index 00000000..85abdb2c --- /dev/null +++ b/http-service/examples/deployment-hpa.yaml @@ -0,0 +1,64 @@ +# Deployment with HPA autoscaling +image: + repository: nginx + tag: latest + +containerPort: 80 + +gracefulShutdown: + trafficDrainSeconds: 25 + appShutdownTimeoutSeconds: 30 + +startupProbe: + httpGet: + path: / + port: 80 + failureThreshold: 5 + initialDelaySeconds: 10 + periodSeconds: 1 + +livenessProbe: + httpGet: + path: / + port: 80 + failureThreshold: 2 + periodSeconds: 10 + +resources: + requests: + cpu: 100m + memory: 128Mi + limits: + cpu: 100m + memory: 128Mi + +datadog: + enabled: true + env: production + service: nginx-example-hpa + version: "1.0.0" + +autoscaling: + type: hpa + minReplicas: 2 + maxReplicas: 10 + hpa: + metrics: + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: 50 + behavior: + scaleDown: + stabilizationWindowSeconds: 300 + +service: + enabled: true + type: ClusterIP + ports: + http: + targetPort: 80 + port: 80 + protocol: TCP diff --git a/http-service/examples/deployment-ingress.yaml b/http-service/examples/deployment-ingress.yaml new file mode 100644 index 00000000..55976ad9 --- /dev/null +++ b/http-service/examples/deployment-ingress.yaml @@ -0,0 +1,65 @@ +# Deployment with Ingress +image: + repository: nginx + tag: latest + +containerPort: 80 + +replicas: 2 + +gracefulShutdown: + trafficDrainSeconds: 25 + appShutdownTimeoutSeconds: 30 + +startupProbe: + httpGet: + path: / + port: 80 + failureThreshold: 5 + initialDelaySeconds: 10 + periodSeconds: 1 + +livenessProbe: + httpGet: + path: / + port: 80 + failureThreshold: 2 + periodSeconds: 10 + +resources: + requests: + cpu: 100m + memory: 128Mi + limits: + cpu: 100m + memory: 128Mi + +datadog: + enabled: true + env: production + service: nginx-example-ingress + version: "1.0.0" + +service: + enabled: true + type: ClusterIP + ports: + http: + targetPort: 80 + port: 80 + protocol: TCP + +ingress: + enabled: true + ingresses: + main: + ingressClassName: alb + annotations: + alb.ingress.kubernetes.io/scheme: internal + alb.ingress.kubernetes.io/target-type: ip + hosts: + - host: example.com + paths: + - path: "/*" + pathType: ImplementationSpecific + portNumber: 80 diff --git a/http-service/examples/deployment-keda.yaml b/http-service/examples/deployment-keda.yaml new file mode 100644 index 00000000..0ced1aa9 --- /dev/null +++ b/http-service/examples/deployment-keda.yaml @@ -0,0 +1,61 @@ +# Deployment with KEDA autoscaling +image: + repository: nginx + tag: latest + +containerPort: 80 + +gracefulShutdown: + trafficDrainSeconds: 25 + appShutdownTimeoutSeconds: 30 + +startupProbe: + httpGet: + path: / + port: 80 + failureThreshold: 5 + initialDelaySeconds: 10 + periodSeconds: 1 + +livenessProbe: + httpGet: + path: / + port: 80 + failureThreshold: 2 + periodSeconds: 10 + +resources: + requests: + cpu: 100m + memory: 128Mi + limits: + cpu: 100m + memory: 128Mi + +datadog: + enabled: true + env: production + service: nginx-example-keda + version: "1.0.0" + +autoscaling: + type: keda + minReplicas: 2 + maxReplicas: 10 + keda: + pollingInterval: 30 + cooldownPeriod: 300 + triggers: + - type: cpu + metadata: + type: Utilization + value: "50" + +service: + enabled: true + type: ClusterIP + ports: + http: + targetPort: 80 + port: 80 + protocol: TCP diff --git a/http-service/examples/deployment-rollout.yaml b/http-service/examples/deployment-rollout.yaml new file mode 100644 index 00000000..8e4d8077 --- /dev/null +++ b/http-service/examples/deployment-rollout.yaml @@ -0,0 +1,68 @@ +# Deployment with Argo Rollouts canary strategy +image: + repository: nginx + tag: latest + +containerPort: 80 + +gracefulShutdown: + trafficDrainSeconds: 25 + appShutdownTimeoutSeconds: 30 + +startupProbe: + httpGet: + path: / + port: 80 + failureThreshold: 5 + initialDelaySeconds: 10 + periodSeconds: 1 + +livenessProbe: + httpGet: + path: / + port: 80 + failureThreshold: 2 + periodSeconds: 10 + +resources: + requests: + cpu: 100m + memory: 128Mi + limits: + cpu: 100m + memory: 128Mi + +datadog: + enabled: true + env: production + service: nginx-example-rollout + version: "1.0.0" + +autoscaling: + type: hpa + minReplicas: 2 + maxReplicas: 10 + hpa: + metrics: + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: 50 + +rollout: + enabled: true + strategy: + canary: + maxSurge: "25%" + maxUnavailable: 0 + +service: + enabled: true + type: ClusterIP + ports: + http: + targetPort: 80 + port: 80 + protocol: TCP diff --git a/http-service/examples/deployment.yaml b/http-service/examples/deployment.yaml new file mode 100644 index 00000000..442b0cdc --- /dev/null +++ b/http-service/examples/deployment.yaml @@ -0,0 +1,59 @@ +# Basic deployment with fixed replicas +image: + repository: nginx + tag: latest + +containerPort: 80 + +replicas: 2 + +gracefulShutdown: + trafficDrainSeconds: 25 + appShutdownTimeoutSeconds: 30 + +startupProbe: + httpGet: + path: / + port: 80 + failureThreshold: 5 + initialDelaySeconds: 10 + periodSeconds: 1 + +livenessProbe: + httpGet: + path: / + port: 80 + failureThreshold: 2 + periodSeconds: 10 + +resources: + requests: + cpu: 100m + memory: 128Mi + limits: + cpu: 100m + memory: 128Mi + +datadog: + enabled: true + env: production + service: nginx-example + version: "1.0.0" + +service: + enabled: true + type: ClusterIP + ports: + http: + targetPort: 80 + port: 80 + protocol: TCP + +test: + enabled: true + containers: + - name: test + image: + repository: curlimages/curl + tag: latest + command: ["sh", "-c", "curl -sf http://$APP_FULLNAME.$NAMESPACE"] diff --git a/http-service/templates/_helpers.tpl b/http-service/templates/_helpers.tpl new file mode 100644 index 00000000..67aa371a --- /dev/null +++ b/http-service/templates/_helpers.tpl @@ -0,0 +1,59 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "http-service.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +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 "http-service.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "http-service.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "http-service.labels" -}} +helm.sh/chart: {{ include "http-service.chart" . }} +{{ include "http-service.selectorLabels" . }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "http-service.selectorLabels" -}} +app.kubernetes.io/name: {{ include "http-service.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "http-service.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "http-service.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} diff --git a/http-service/templates/configmap.yaml b/http-service/templates/configmap.yaml new file mode 100644 index 00000000..27ba34da --- /dev/null +++ b/http-service/templates/configmap.yaml @@ -0,0 +1,30 @@ +{{- $root := . }} +{{- range $name, $value := .Values.configmaps }} +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "http-service.fullname" $root }}-{{ $name }} + namespace: {{ $root.Release.Namespace }} + labels: + {{- include "http-service.labels" $root | nindent 4 }} + {{- with $value.labels }} + {{- toYaml . | nindent 4 }} + {{- end }} + annotations: + checksum/configmap: '{{ toYaml $value | sha256sum }}' + {{- with $value.annotations }} + {{- toYaml . | nindent 4 }} + {{- end }} +{{- if and (semverCompare ">=1.19-0" $root.Capabilities.KubeVersion.GitVersion) $value.immutable }} +immutable: {{ $value.immutable }} +{{- end }} +{{- with $value.binaryData }} +binaryData: + {{- . | toYaml | nindent 2 }} +{{- end }} +{{- with $value.data }} +data: + {{- . | toYaml | nindent 2 }} +{{- end }} +{{- end }} diff --git a/http-service/templates/deployment.yaml b/http-service/templates/deployment.yaml new file mode 100644 index 00000000..37ad9d12 --- /dev/null +++ b/http-service/templates/deployment.yaml @@ -0,0 +1,197 @@ +{{- $root := . }} +{{- if .Values.datadog.enabled }} +{{- $_ := required "datadog.env is required when datadog.enabled is true" .Values.datadog.env }} +{{- $_ := required "datadog.service is required when datadog.enabled is true" .Values.datadog.service }} +{{- $_ := required "datadog.version is required when datadog.enabled is true" .Values.datadog.version }} +{{- end }} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "http-service.fullname" . }} + namespace: {{ .Release.Namespace }} + annotations: + {{- if .Values.reloader.enabled }} + reloader.stakater.com/auto: "true" + {{- end }} + {{- with .Values.deployment.annotations }} + {{- toYaml . | nindent 4 }} + {{- end }} + labels: + {{- include "http-service.labels" . | nindent 4 }} + {{- if .Values.datadog.enabled }} + tags.datadoghq.com/env: {{ .Values.datadog.env }} + tags.datadoghq.com/service: {{ .Values.datadog.service }} + tags.datadoghq.com/version: {{ .Values.datadog.version | quote }} + {{- end }} + {{- with .Values.deployment.labels }} + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- if not (has .Values.autoscaling.type (list "none" "hpa" "keda")) }} + {{- fail "autoscaling.type must be one of: none, hpa, keda" }} + {{- end }} + {{- if and (ne .Values.autoscaling.type "none") .Values.replicas }} + {{- fail "replicas should not be set when autoscaling.type is hpa or keda — the autoscaler manages replica count" }} + {{- end }} + {{- with .Values.replicas }} + replicas: {{ . }} + {{- end }} + selector: + matchLabels: + {{- include "http-service.selectorLabels" . | nindent 6 }} + strategy: + {{- toYaml .Values.strategy | nindent 4 }} + {{- with .Values.revisionHistoryLimit }} + revisionHistoryLimit: {{ . }} + {{- end }} + template: + metadata: + annotations: + checksum/configmap: '{{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}' + checksum/secret: '{{ include (print $.Template.BasePath "/secret.yaml") . | sha256sum }}' + {{- with .Values.pod.annotations }} + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "http-service.selectorLabels" . | nindent 8 }} + {{- if .Values.datadog.enabled }} + tags.datadoghq.com/env: {{ .Values.datadog.env }} + tags.datadoghq.com/service: {{ .Values.datadog.service }} + tags.datadoghq.com/version: {{ .Values.datadog.version | quote }} + {{- end }} + {{- with .Values.pod.labels }} + {{- toYaml . | nindent 8 }} + {{- end }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.readinessGates }} + readinessGates: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "http-service.serviceAccountName" . }} + {{- with .Values.pod.securityContext }} + securityContext: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.priorityClassName }} + priorityClassName: {{ . }} + {{- end }} + {{- $drainSeconds := required "gracefulShutdown.trafficDrainSeconds is required" .Values.gracefulShutdown.trafficDrainSeconds }} + {{- $appSeconds := required "gracefulShutdown.appShutdownTimeoutSeconds is required" .Values.gracefulShutdown.appShutdownTimeoutSeconds }} + terminationGracePeriodSeconds: {{ add $drainSeconds $appSeconds }} + {{- with .Values.progressDeadlineSeconds }} + progressDeadlineSeconds: {{ . }} + {{- end }} + {{- with .Values.topologySpreadConstraints }} + topologySpreadConstraints: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.volumes }} + volumes: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.extraPodSpec }} + {{- toYaml . | nindent 6 }} + {{- end }} + {{- if .Values.initContainers.enabled }} + initContainers: + {{- range $container := .Values.initContainers.containers }} + - name: {{ include "http-service.fullname" $root }}-{{ $container.name }} + image: {{ $container.image.repository }}:{{ $container.image.tag }} + {{- with $container.image.pullPolicy }} + imagePullPolicy: {{ . }} + {{- end }} + {{- with $container.command }} + command: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with $container.args }} + args: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with $container.workingDir }} + workingDir: {{ . }} + {{- end }} + {{- with $container.env }} + env: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with $container.envFrom }} + envFrom: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with $container.volumeMounts }} + volumeMounts: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- end }} + {{- end }} + containers: + - name: {{ include "http-service.fullname" . }} + image: {{ .Values.image.repository }}:{{ .Values.image.tag }} + imagePullPolicy: {{ .Values.image.pullPolicy }} + {{- with .Values.securityContext }} + securityContext: + {{- toYaml . | nindent 12 }} + {{- end }} + ports: + - containerPort: {{ .Values.containerPort }} + protocol: TCP + {{- with .Values.command }} + command: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.args }} + args: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.env }} + env: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.envFrom }} + envFrom: + {{- toYaml . | nindent 12 }} + {{- end }} + lifecycle: + preStop: + exec: + command: + - sleep + - {{ $drainSeconds | quote }} + {{- $startupProbe := required "startupProbe is required" .Values.startupProbe }} + {{- $livenessProbe := required "livenessProbe is required" .Values.livenessProbe }} + startupProbe: + {{- toYaml $startupProbe | nindent 12 }} + livenessProbe: + {{- toYaml $livenessProbe | nindent 12 }} + {{- with .Values.readinessProbe }} + readinessProbe: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.resources }} + resources: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.volumeMounts }} + volumeMounts: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.extraContainers }} + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/http-service/templates/hpa.yaml b/http-service/templates/hpa.yaml new file mode 100644 index 00000000..cb8064d5 --- /dev/null +++ b/http-service/templates/hpa.yaml @@ -0,0 +1,29 @@ +{{- if eq .Values.autoscaling.type "hpa" }} +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: {{ include "http-service.fullname" . }} + namespace: {{ .Release.Namespace }} + labels: + {{- include "http-service.labels" . | nindent 4 }} +spec: + {{- $hpa := required "autoscaling.hpa is required when type is hpa" .Values.autoscaling.hpa }} + {{- $metrics := required "autoscaling.hpa.metrics is required when type is hpa" $hpa.metrics }} + {{- with $hpa.behavior }} + behavior: + {{- toYaml . | nindent 4 }} + {{- end }} + metrics: + {{- toYaml $metrics | nindent 4 }} + scaleTargetRef: + {{- if .Values.rollout.enabled }} + apiVersion: argoproj.io/v1alpha1 + kind: Rollout + {{- else }} + apiVersion: apps/v1 + kind: Deployment + {{- end }} + name: {{ include "http-service.fullname" . }} + minReplicas: {{ required "autoscaling.minReplicas is required" .Values.autoscaling.minReplicas }} + maxReplicas: {{ required "autoscaling.maxReplicas is required" .Values.autoscaling.maxReplicas }} +{{- end }} diff --git a/http-service/templates/ingress.yaml b/http-service/templates/ingress.yaml new file mode 100644 index 00000000..9dbb0b9f --- /dev/null +++ b/http-service/templates/ingress.yaml @@ -0,0 +1,50 @@ +{{- if .Values.ingress.enabled }} +{{- $root := . }} +{{- range $name, $ingress := .Values.ingress.ingresses }} +{{- $fullName := include "http-service.fullname" $root -}} +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + {{- with $ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} + labels: + {{- include "http-service.labels" $root | nindent 4 }} + {{- with $ingress.labels }} + {{- toYaml . | nindent 4 }} + {{- end }} + name: "{{ $fullName }}-{{ $name }}" + namespace: {{ $root.Release.Namespace }} +spec: + {{- if $ingress.ingressClassName }} + ingressClassName: {{ $ingress.ingressClassName }} + {{- end }} + {{- if $ingress.tls }} + tls: + {{- range $ingress.tls }} + - hosts: + {{- range .hosts }} + - {{ . | quote }} + {{- end }} + secretName: "{{ .secretName }}" + {{- end }} + {{- end }} + rules: + {{- range $ingress.hosts }} + - host: {{ .host | quote }} + http: + paths: + {{- range .paths }} + - path: {{ .path }} + pathType: {{ .pathType }} + backend: + service: + name: {{ $fullName }} + port: + number: {{ .portNumber }} + {{- end }} + {{- end }} +{{ end }} +{{- end }} diff --git a/http-service/templates/pdb.yaml b/http-service/templates/pdb.yaml new file mode 100644 index 00000000..aa359a07 --- /dev/null +++ b/http-service/templates/pdb.yaml @@ -0,0 +1,26 @@ +{{- if .Values.pod.disruptionBudget.enabled }} +apiVersion: policy/v1 +kind: PodDisruptionBudget +metadata: + {{- with .Values.pod.disruptionBudget.annotations }} + annotations: + {{- toYaml . | nindent 4}} + {{- end }} + labels: + {{- include "http-service.labels" . | nindent 4 }} + {{- with .Values.pod.disruptionBudget.labels }} + {{- toYaml . | nindent 4 }} + {{- end }} + name: {{ template "http-service.fullname" . }} + namespace: {{ .Release.Namespace }} +spec: + {{- with .Values.pod.disruptionBudget.maxUnavailable }} + maxUnavailable: {{ . }} + {{- end }} + {{- with .Values.pod.disruptionBudget.minAvailable }} + minAvailable: {{ . }} + {{- end }} + selector: + matchLabels: + {{- include "http-service.selectorLabels" . | nindent 6 }} +{{- end }} diff --git a/http-service/templates/rbac.yaml b/http-service/templates/rbac.yaml new file mode 100644 index 00000000..e70c3861 --- /dev/null +++ b/http-service/templates/rbac.yaml @@ -0,0 +1,72 @@ +{{- if .Values.clusterRole.enabled }} +kind: ClusterRole +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + labels: + {{- include "http-service.labels" . | nindent 4 }} + name: {{ include "http-service.fullname" . }} +rules: + {{- with .Values.clusterRole.rules }} + {{- toYaml . | nindent 2 }} + {{- end }} +--- +kind: ClusterRoleBinding +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + labels: + {{- include "http-service.labels" . | nindent 4 }} + name: {{ include "http-service.fullname" . }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: {{ include "http-service.fullname" . }} +subjects: + - kind: ServiceAccount + name: {{ include "http-service.serviceAccountName" . }} + namespace: {{ .Release.Namespace }} +--- +{{- end }} +{{- if .Values.role.enabled }} +kind: Role +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + labels: + {{- include "http-service.labels" . | nindent 4 }} + name: {{ include "http-service.fullname" . }} + namespace: {{ .Release.Namespace }} +rules: + {{- with .Values.role.rules }} + {{- toYaml . | nindent 2 }} + {{- end }} +--- +kind: RoleBinding +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + labels: + {{- include "http-service.labels" . | nindent 4 }} + name: {{ include "http-service.fullname" . }} + namespace: {{ .Release.Namespace }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: {{ include "http-service.fullname" . }} +subjects: + - kind: ServiceAccount + name: {{ include "http-service.serviceAccountName" . }} + namespace: {{ .Release.Namespace }} +--- +{{- end }} +{{- if .Values.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + annotations: + {{- toYaml .Values.serviceAccount.annotations | nindent 4 }} + labels: + {{- include "http-service.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.labels }} + {{- toYaml . | nindent 4 }} + {{- end }} + name: {{ include "http-service.serviceAccountName" . }} + namespace: {{ .Release.Namespace }} +{{- end -}} diff --git a/http-service/templates/rollout.yaml b/http-service/templates/rollout.yaml new file mode 100644 index 00000000..f9e5f008 --- /dev/null +++ b/http-service/templates/rollout.yaml @@ -0,0 +1,23 @@ +{{- if .Values.rollout.enabled }} +apiVersion: argoproj.io/v1alpha1 +kind: Rollout +metadata: + name: {{ include "http-service.fullname" . }} + namespace: {{ .Release.Namespace }} + labels: + {{- include "http-service.labels" . | nindent 4 }} + {{- with .Values.deployment.labels }} + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + workloadRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ include "http-service.fullname" . }} + {{- with .Values.revisionHistoryLimit }} + revisionHistoryLimit: {{ . }} + {{- end }} + {{- $strategy := required "rollout.strategy is required when rollout.enabled is true" .Values.rollout.strategy }} + strategy: + {{- toYaml $strategy | nindent 4 }} +{{- end }} diff --git a/http-service/templates/scaled-object.yaml b/http-service/templates/scaled-object.yaml new file mode 100644 index 00000000..d2d5c90b --- /dev/null +++ b/http-service/templates/scaled-object.yaml @@ -0,0 +1,31 @@ +{{- if eq .Values.autoscaling.type "keda" }} +apiVersion: keda.sh/v1alpha1 +kind: ScaledObject +metadata: + name: {{ include "http-service.fullname" . }} + namespace: {{ .Release.Namespace }} + labels: + {{- include "http-service.labels" . | nindent 4 }} +spec: + scaleTargetRef: + {{- if .Values.rollout.enabled }} + apiVersion: argoproj.io/v1alpha1 + kind: Rollout + {{- else }} + apiVersion: apps/v1 + kind: Deployment + {{- end }} + name: {{ include "http-service.fullname" . }} + {{- $keda := required "autoscaling.keda is required when type is keda" .Values.autoscaling.keda }} + {{- $triggers := required "autoscaling.keda.triggers is required when type is keda" $keda.triggers }} + minReplicaCount: {{ required "autoscaling.minReplicas is required" .Values.autoscaling.minReplicas }} + maxReplicaCount: {{ required "autoscaling.maxReplicas is required" .Values.autoscaling.maxReplicas }} + {{- with $keda.pollingInterval }} + pollingInterval: {{ . }} + {{- end }} + {{- with $keda.cooldownPeriod }} + cooldownPeriod: {{ . }} + {{- end }} + triggers: + {{- toYaml $triggers | nindent 4 }} +{{- end }} diff --git a/http-service/templates/secret.yaml b/http-service/templates/secret.yaml new file mode 100644 index 00000000..6f6616b3 --- /dev/null +++ b/http-service/templates/secret.yaml @@ -0,0 +1,36 @@ +{{- $root := . }} +{{- range $name, $value := .Values.secrets }} +--- +apiVersion: v1 +kind: Secret +metadata: + name: {{ include "http-service.fullname" $root }}-{{ $name }} + namespace: {{ $root.Release.Namespace }} + labels: + {{- include "http-service.labels" $root | nindent 4 }} + {{- with $value.labels }} + {{- toYaml . | nindent 4 }} + {{- end }} + annotations: + checksum/secret: '{{ toYaml $value | sha256sum }}' + {{- with $value.annotations }} + {{- toYaml . | nindent 4 }} + {{- end }} +{{- if and (semverCompare ">=1.19-0" $root.Capabilities.KubeVersion.GitVersion) $value.immutable }} +immutable: {{ $value.immutable }} +{{- end }} +{{- with $value.data }} +data: +{{- range $k, $v := $value.data }} + {{ $k }}: {{ . | b64enc | quote }} +{{- end }} +{{- with $value.stringData }} +stringData: + {{- . | toYaml | nindent 2 }} +{{- end }} +{{- with $value.type }} +type: + {{- . | toYaml | nindent 2 }} +{{- end }} +{{- end }} +{{- end }} diff --git a/http-service/templates/service.yaml b/http-service/templates/service.yaml new file mode 100644 index 00000000..7ffd6081 --- /dev/null +++ b/http-service/templates/service.yaml @@ -0,0 +1,34 @@ +{{- if .Values.service.enabled }} +{{- $root := . }} +apiVersion: v1 +kind: Service +metadata: + name: {{ include "http-service.fullname" . }} + namespace: {{ .Release.Namespace }} + {{- with .Values.service.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} + labels: + {{- include "http-service.labels" . | nindent 4 }} + {{- with .Values.service.labels }} + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + type: {{ .Values.service.type }} + {{- if and (eq .Values.service.type "ClusterIP") .Values.service.clusterIP }} + clusterIP: {{ .Values.service.clusterIP }} + {{- end }} + ports: + {{- range $name, $v := .Values.service.ports }} + - name: {{ $name }} + targetPort: {{ $v.targetPort }} + port: {{ $v.port }} + {{- if and (eq $root.Values.service.type "NodePort") $v.nodePort }} + nodePort: {{ $v.nodePort }} + {{- end }} + protocol: {{ $v.protocol }} + {{- end }} + selector: + {{- include "http-service.selectorLabels" . | nindent 4 }} +{{- end }} diff --git a/http-service/templates/tests/test.yaml b/http-service/templates/tests/test.yaml new file mode 100644 index 00000000..f974932a --- /dev/null +++ b/http-service/templates/tests/test.yaml @@ -0,0 +1,45 @@ +{{- if .Values.test.enabled -}} +{{- $root := . }} +apiVersion: v1 +kind: Pod +metadata: + name: "{{ include "http-service.fullname" . }}-test" + namespace: {{ .Release.Namespace }} + labels: + helm.sh/chart: {{ include "http-service.chart" . }} + app.kubernetes.io/managed-by: {{ .Release.Service }} + annotations: + "helm.sh/hook": test +spec: + containers: + {{- range $container := .Values.test.containers }} + - name: {{ include "http-service.fullname" $root }}-{{ $container.name }}-test + image: {{ $container.image.repository }}:{{ $container.image.tag }} + {{- with $container.image.pullPolicy }} + imagePullPolicy: {{ . }} + {{- end }} + {{- with $container.command }} + command: + {{- toYaml . | nindent 8 }} + {{- end }} + env: + - name: RELEASE_NAME + value: {{ $root.Release.Name }} + - name: NAMESPACE + value: {{ $root.Release.Namespace }} + - name: APP_FULLNAME + value: {{ include "http-service.fullname" $root }} + {{- with $container.env }} + {{- toYaml $container.env | nindent 8 }} + {{- end }} + {{- with $container.envFrom }} + envFrom: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with $container.resources }} + resources: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- end }} + restartPolicy: Never +{{- end }} diff --git a/http-service/values.yaml b/http-service/values.yaml new file mode 100644 index 00000000..696c0fd4 --- /dev/null +++ b/http-service/values.yaml @@ -0,0 +1,269 @@ +# Default values for example. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. + +nameOverride: # Override name of app + +fullnameOverride: # Override the full qualified app name + +strategy: + type: RollingUpdate + rollingUpdate: + maxSurge: "25%" + maxUnavailable: 1 + +# -- Reloader annotation for auto-restarting on ConfigMap/Secret changes. +# When enabled, adds reloader.stakater.com/auto: "true" to Deployment annotations. +reloader: + enabled: true + +deployment: + annotations: {} + labels: {} + +# -- Fixed replica count (used when autoscaling.type is none) +#replicas: 1 + +revisionHistoryLimit: "" # revisionHistoryLimit + +# -- Datadog unified service tags. +# When enabled, the template auto-generates tags.datadoghq.com/{env,service,version} +# labels on both the Deployment and Pod. Set enabled to false for services +# using alternative observability (e.g. OpenTelemetry). +datadog: + enabled: true + #env: "" # required when enabled + #service: "" # required when enabled + #version: "" # required when enabled + +pod: + annotations: {} + labels: {} + securityContext: {} + disruptionBudget: + enabled: true # PDB enabled by default to protect availability + annotations: {} # PDB annotations + labels: {} # PDB labels + maxUnavailable: 1 # allow at most 1 pod unavailable during disruptions + minAvailable: # PDB minAvailable + +affinity: {} # affinity (e.g. node affinity for ARM, set via Helmfile settings) + +tolerations: [] # tolerations for tainted nodes + +topologySpreadConstraints: [] # pod spread across topology domains + +nodeSelector: {} # nodeSelector + +imagePullSecrets: [] # imagePullSecrets + +readinessGates: [] # readinessGates + +priorityClassName: "" # priorityClassName + +progressDeadlineSeconds: "" # progressDeadlineSeconds + +volumes: [] # pod volumes(initContainers, containers) + +extraPodSpec: {} # arbitrary additional Pod spec fields (escape hatch) + +# -- Main container image +image: + repository: "" + tag: "" + pullPolicy: IfNotPresent + +containerPort: 8080 # port exposed by the main container + +# -- Graceful shutdown configuration (required). +# The template auto-generates: +# - preStop lifecycle hook: sleep for trafficDrainSeconds +# - terminationGracePeriodSeconds: trafficDrainSeconds + appShutdownTimeoutSeconds +# +# Timeline: +# |-- terminationGracePeriodSeconds (drain + app) --| +# |-- preStop sleep (drain) --|-- app shutdown ------| +# ^ +# SIGTERM +# +# Why preStop sleep? When a Pod terminates, Kubernetes removes it from +# Endpoints and runs preStop simultaneously. But the ALB/NLB target group +# deregistration takes time — during that gap, traffic still arrives. +# The sleep holds the container alive until routing fully stops. +#gracefulShutdown: +# trafficDrainSeconds: 25 # wait for LB deregistration to complete +# appShutdownTimeoutSeconds: 30 # time the app gets after SIGTERM + +command: [] # container command override +args: [] # container args override +env: [] # environment variables +envFrom: [] # environment variable sources +resources: {} # container resources (limits/requests) +securityContext: {} # container security context +volumeMounts: [] # container volume mounts + +# -- Health check probes. +# startupProbe and livenessProbe are required in the template (rendering fails if not set). +# readinessProbe is optional. +#startupProbe: +# httpGet: +# path: /healthz +# port: 8080 +# failureThreshold: 5 +# initialDelaySeconds: 10 +# periodSeconds: 1 +#livenessProbe: +# httpGet: +# path: /healthz +# port: 8080 +# failureThreshold: 2 +# periodSeconds: 10 +#readinessProbe: +# httpGet: +# path: /ready +# port: 8080 +# periodSeconds: 5 +# failureThreshold: 3 + +extraContainers: [] # additional sidecar containers (escape hatch) + +initContainers: + enabled: false # if true, you can use initContainers + + containers: [] # initContainers config + # - name: init + # + # image: + # repository: chatwork/init + # tag: latest + # + # command: [] + # args: [] + # + # env: [] + # # - name: DEMO_GREETING + # # value: "Hello from the environment" + # + # envFrom: [] + # # - configMapRef: + # # name: special-config + # volumeMounts: [] + # # - name: volume-name + # # mountPath: /path/to + +configmaps: {} # transform ConfigMap manifest. You can set `binaryData`, `data` +# your-configmap-name: +# labels: +# a: b +# annotations: +# foo: bar +# data: +# key: value + +secrets: {} # transform Secret's manifest. You can set `data`, `stringData` and `type` +# your-secret-name: +# annotations: +# foo: bar +# data: +# key: base64-encoded-value + +autoscaling: + # type: none - fixed replicas, no autoscaler + # type: hpa - Kubernetes-native HorizontalPodAutoscaler + # type: keda - KEDA ScaledObject (manages HPA internally) + type: none + #minReplicas: 1 + #maxReplicas: 10 + + # -- HPA-specific (required when type: hpa) + #hpa: + # metrics: [] + # behavior: {} + + # -- KEDA-specific (required when type: keda) + #keda: + # triggers: [] + # pollingInterval: 30 + # cooldownPeriod: 300 + +# -- Argo Rollouts integration. +# When enabled, creates a Rollout resource that references the Deployment +# via workloadRef for canary/blue-green deployment strategies. +rollout: + enabled: false + #strategy: + # canary: + # maxSurge: "25%" + # maxUnavailable: 0 + +service: + enabled: false # if true, you can use service + type: ClusterIP # service type(ClusterIP, NodePort, LoadBalancer) + ports: {} # service ports + # http: + # # container port + # targetPort: 8080 + # # svc port + # port: 80 + # protocol: TCP + +clusterRole: + enabled: false # if true, you can use clusterRole + rules: [] # clusterRole rules + # - apiGroups: + # - "" + # resources: + # - nodes + # verbs: + # - get + # - list + # - watch + +role: + enabled: false # if true, you can use role + rules: [] # role rules + # - apiGroups: + # - "" + # resources: + # - pods + # verbs: + # - watch + +serviceAccount: + create: false # if true, you can create serviceAccount + name: # if you create serviceAccount, you can set name + labels: {} # service account labels + annotations: {} # serviceAccount annotations + +ingress: + enabled: false # if true, you can use ingress + ingresses: {} # name -> spec map, create as many Ingress resources as needed + # your-ingress-name: + # annotations: {} + # labels: {} + # tls: [] + # - hosts: [] + # secretName: "" + # hosts: [] + # - host: + # paths: + # - path: "/*" + # pathType: ImplementationSpecific + # portNumber: 80 + +test: + enabled: false # if true, you can use helm test + containers: [] # helm test container config + #- name: app1-test + # + # image: + # repository: curl + # tag: latest + # pullPolicy: Always + # # you can use environment vairables $RELEASE_NAME, $NAMESPACE, $APP_FULLNAME + # command: ["curl","$RELEASE_NAME.$NAMESPACE"] + # + # env: [] + # + # envFrom: [] +