From 315e713e289e518079b1012153b1fa99e346a67f Mon Sep 17 00:00:00 2001 From: Jorrit Salverda Date: Wed, 6 Nov 2019 17:05:24 +0100 Subject: [PATCH] Create official helm chart (#7) lint, package, test and publish helm chart to helm.estafette.io --- .estafette.yaml | 71 ++++++++++++-- README.md | 32 ++----- bootstrap-secret.yaml | 15 --- estafette-gcp-service-account/.helmignore | 22 +++++ estafette-gcp-service-account/Chart.yaml | 5 + .../templates/NOTES.txt | 2 + .../templates/_helpers.tpl | 66 +++++++++++++ .../templates/clusterrole.yaml | 17 ++++ .../templates/clusterrolebinding.yaml | 16 ++++ .../templates/deployment.yaml | 96 +++++++++++++++++++ .../templates/secret.yaml | 13 +++ .../templates/serviceaccount.yaml | 9 ++ estafette-gcp-service-account/values.yaml | 94 ++++++++++++++++++ kubernetes.yaml | 89 ----------------- rbac.yaml | 30 ------ 15 files changed, 413 insertions(+), 164 deletions(-) delete mode 100644 bootstrap-secret.yaml create mode 100644 estafette-gcp-service-account/.helmignore create mode 100644 estafette-gcp-service-account/Chart.yaml create mode 100644 estafette-gcp-service-account/templates/NOTES.txt create mode 100644 estafette-gcp-service-account/templates/_helpers.tpl create mode 100644 estafette-gcp-service-account/templates/clusterrole.yaml create mode 100644 estafette-gcp-service-account/templates/clusterrolebinding.yaml create mode 100644 estafette-gcp-service-account/templates/deployment.yaml create mode 100644 estafette-gcp-service-account/templates/secret.yaml create mode 100644 estafette-gcp-service-account/templates/serviceaccount.yaml create mode 100644 estafette-gcp-service-account/values.yaml delete mode 100644 kubernetes.yaml delete mode 100644 rbac.yaml diff --git a/.estafette.yaml b/.estafette.yaml index 4651d2d..4e29650 100644 --- a/.estafette.yaml +++ b/.estafette.yaml @@ -2,18 +2,18 @@ builder: track: dev labels: - app-group: estafette-various + app-group: estafette-controllers team: estafette-team language: golang version: semver: major: 1 - minor: 0 + minor: 2 stages: build: - image: golang:1.13.0-alpine3.10 + image: golang:1.13.4-alpine3.10 env: CGO_ENABLED: 0 GOOS: linux @@ -49,10 +49,42 @@ stages: action: push repositories: - estafette - when: - status == 'succeeded' && - branch == 'master' && - server == 'gocd' + + lint-helm-chart: + image: extensions/helm:dev + action: lint + prerelease: true + + package-helm-chart: + image: extensions/helm:dev + action: package + prerelease: true + + test-helm-chart: + services: + - name: kubernetes + image: bsycorp/kind:latest-1.12 + ports: + - port: 8443 + - port: 10080 + readiness: + path: /kubernetes-ready + timeoutSeconds: 180 + image: extensions/helm:dev + action: test + prerelease: true + values: + - serviceAccountProjectID=my-project-id + + clone-charts-repo: + image: extensions/git-clone:dev + repo: helm-charts + branch: master + + publish-helm-chart: + image: extensions/helm:dev + action: publish + prerelease: true slack-notify: image: extensions/slack-build-status:dev @@ -60,4 +92,27 @@ stages: channels: - '#build-status' when: - status == 'failed' \ No newline at end of file + status == 'failed' + +releases: + release: + clone: true + stages: + package-helm-chart: + image: extensions/helm:dev + action: package + + clone-charts-repo: + image: extensions/git-clone:dev + repo: helm-charts + branch: master + + publish-helm-chart: + image: extensions/helm:dev + action: publish + purgePrerelease: true + + create-github-release: + image: extensions/github-release:dev + version: ${ESTAFETTE_BUILD_VERSION_MAJOR}.${ESTAFETTE_BUILD_VERSION_MINOR}.0 + closeMilestone: true \ No newline at end of file diff --git a/README.md b/README.md index a953608..8302d5a 100644 --- a/README.md +++ b/README.md @@ -8,39 +8,27 @@ This small Kubernetes application creates and renews Let's Encrypt SSL certifica In order to create GCP service accounts and store their keyfiles in Kubernetes secrets. This improves developer self-service. -## Usage +## Installation -As a Kubernetes administrator, you first need to deploy the rbac.yaml file which set role and permissions. -Then deploy the application to Kubernetes cluster using the manifest below. - -``` -cat rbac.yaml | kubectl apply -f - -``` - -Create a google service account with keyfile and the following roles for bootstrapping only: +Create a google service account with keyfile and the following roles: ``` Service Account Admin Service Account Key Admin ``` -Create a secret with the bootstrap key only once: +Prepare using Helm: ``` -cat bootstrap-secret.yaml | TEAM_NAME=tooling GOOGLE_SERVICE_ACCOUNT= envsubst | kubectl apply -f - +brew install kubernetes-helm +kubectl -n kube-system create serviceaccount tiller +kubectl create clusterrolebinding tiller --clusterrole=cluster-admin --serviceaccount=kube-system:tiller +helm init --service-account tiller --wait ``` -Then create the deployment and other resources with +Then install or upgrade with Helm: ``` -cat kubernetes.yaml | TEAM_NAME=tooling SERVICE_ACCOUNT_PREFIX=dev SERVICE_ACCOUNT_PROJECT_ID=my-gcp-sa-container-project-id KEY_ROTATION_AFTER_HOURS=360 envsubst | kubectl apply -f - +helm repo add estafette https://helm.estafette.io +helm upgrade --install estafette-gcp-service-account --namespace estafette estafette/estafette-gcp-service-account ``` - -The bootstrap service account will be replaced with a dedicated service account, which now needs the same roles as well: - -``` -Service Account Admin -Service Account Key Admin -``` - -From now on the keys in the secrets will be rotated every KEY_ROTATION_AFTER_HOURS hours. \ No newline at end of file diff --git a/bootstrap-secret.yaml b/bootstrap-secret.yaml deleted file mode 100644 index d37fc32..0000000 --- a/bootstrap-secret.yaml +++ /dev/null @@ -1,15 +0,0 @@ ---- -apiVersion: v1 -kind: Secret -metadata: - name: estafette-gcp-service-account-secrets - namespace: estafette - labels: - app: estafette-gcp-service-account - team: ${TEAM_NAME} - annotations: - estafette.io/gcp-service-account: 'true' - estafette.io/gcp-service-account-name: 'estafette-gcp-service-account' -type: Opaque -data: - service-account-key.json: ${GOOGLE_SERVICE_ACCOUNT} \ No newline at end of file diff --git a/estafette-gcp-service-account/.helmignore b/estafette-gcp-service-account/.helmignore new file mode 100644 index 0000000..50af031 --- /dev/null +++ b/estafette-gcp-service-account/.helmignore @@ -0,0 +1,22 @@ +# 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 +.vscode/ diff --git a/estafette-gcp-service-account/Chart.yaml b/estafette-gcp-service-account/Chart.yaml new file mode 100644 index 0000000..903ef7c --- /dev/null +++ b/estafette-gcp-service-account/Chart.yaml @@ -0,0 +1,5 @@ +apiVersion: v1 +appVersion: "1.0" +description: Kubernetes controller to fetch GCP service account keyfiles for annotated secrets +name: estafette-gcp-service-account +version: 0.1.0 diff --git a/estafette-gcp-service-account/templates/NOTES.txt b/estafette-gcp-service-account/templates/NOTES.txt new file mode 100644 index 0000000..250868f --- /dev/null +++ b/estafette-gcp-service-account/templates/NOTES.txt @@ -0,0 +1,2 @@ +1. Get the application logs by running this command: +kubectl logs -f -l app.kubernetes.io/name={{ include "estafette-gcp-service-account.name" . }},app.kubernetes.io/instance={{ .Release.Name }} -n {{ .Release.Namespace }} \ No newline at end of file diff --git a/estafette-gcp-service-account/templates/_helpers.tpl b/estafette-gcp-service-account/templates/_helpers.tpl new file mode 100644 index 0000000..59065f3 --- /dev/null +++ b/estafette-gcp-service-account/templates/_helpers.tpl @@ -0,0 +1,66 @@ +{{/* vim: set filetype=mustache: */}} +{{/* +Expand the name of the chart. +*/}} +{{- define "estafette-gcp-service-account.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 "estafette-gcp-service-account.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 "estafette-gcp-service-account.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{/* +Common labels +*/}} +{{- define "estafette-gcp-service-account.labels" -}} +app.kubernetes.io/name: {{ include "estafette-gcp-service-account.name" . }} +helm.sh/chart: {{ include "estafette-gcp-service-account.chart" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- range $key, $value := .Values.extraLabels }} +{{ $key }}: {{ $value }} +{{- end }} +{{- end -}} + +{{/* +Create the name of the service account to use +*/}} +{{- define "estafette-gcp-service-account.serviceAccountName" -}} +{{- if .Values.serviceAccount.create -}} + {{ default (include "estafette-gcp-service-account.fullname" .) .Values.serviceAccount.name }} +{{- else -}} + {{ default "default" .Values.serviceAccount.name }} +{{- end -}} +{{- end -}} + +{{/* +Create the tag of the image to use +*/}} +{{- define "estafette-gcp-service-account.imageTag" -}} +{{ default .Chart.AppVersion .Values.image.tag }} +{{- end -}} \ No newline at end of file diff --git a/estafette-gcp-service-account/templates/clusterrole.yaml b/estafette-gcp-service-account/templates/clusterrole.yaml new file mode 100644 index 0000000..37e4545 --- /dev/null +++ b/estafette-gcp-service-account/templates/clusterrole.yaml @@ -0,0 +1,17 @@ +{{- if .Values.rbac.enable -}} +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: {{ include "estafette-gcp-service-account.fullname" . }} + labels: +{{ include "estafette-gcp-service-account.labels" . | indent 4 }} +rules: +- apiGroups: [""] # "" indicates the core API group + resources: + - secrets + verbs: + - get + - list + - update + - watch +{{- end -}} diff --git a/estafette-gcp-service-account/templates/clusterrolebinding.yaml b/estafette-gcp-service-account/templates/clusterrolebinding.yaml new file mode 100644 index 0000000..9445c8f --- /dev/null +++ b/estafette-gcp-service-account/templates/clusterrolebinding.yaml @@ -0,0 +1,16 @@ +{{- if .Values.rbac.enable -}} +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: {{ include "estafette-gcp-service-account.fullname" . }} + labels: +{{ include "estafette-gcp-service-account.labels" . | indent 4 }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: {{ include "estafette-gcp-service-account.fullname" . }} +subjects: +- kind: ServiceAccount + name: {{ template "estafette-gcp-service-account.serviceAccountName" . }} + namespace: {{ .Release.Namespace }} +{{- end -}} diff --git a/estafette-gcp-service-account/templates/deployment.yaml b/estafette-gcp-service-account/templates/deployment.yaml new file mode 100644 index 0000000..9a9f1d3 --- /dev/null +++ b/estafette-gcp-service-account/templates/deployment.yaml @@ -0,0 +1,96 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "estafette-gcp-service-account.fullname" . }} + namespace: {{ .Release.Namespace }} + labels: +{{ include "estafette-gcp-service-account.labels" . | indent 4 }} +spec: + replicas: {{ .Values.replicaCount }} + strategy: + type: Recreate + selector: + matchLabels: + app.kubernetes.io/name: {{ include "estafette-gcp-service-account.name" . }} + app.kubernetes.io/instance: {{ .Release.Name }} + template: + metadata: + labels: + app.kubernetes.io/name: {{ include "estafette-gcp-service-account.name" . }} + app.kubernetes.io/instance: {{ .Release.Name }} + {{- if .Chart.AppVersion }} + app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} + {{- end }} + {{- range $key, $value := .Values.extraPodLabels }} + {{ $key }}: {{ $value }} + {{- end }} + annotations: + prometheus.io/scrape: "true" + prometheus.io/port: "9101" + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ template "estafette-gcp-service-account.serviceAccountName" . }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + containers: + - name: {{ .Chart.Name }} + securityContext: + {{- toYaml .Values.securityContext | nindent 12 }} + image: "{{ .Values.image.repository }}:{{ template "estafette-gcp-service-account.imageTag" . }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + {{- with .Values.extraArgs }} + args: + {{- toYaml . | nindent 12 }} + {{- end }} + env: + - name: GOOGLE_APPLICATION_CREDENTIALS + value: /secrets/service-account-key.json + - name: MODE + value: {{ .Values.mode | quote }} + - name: SERVICE_ACCOUNT_PROJECT_ID + value: {{ .Values.serviceAccountProjectID | quote }} + - name: KEY_ROTATION_AFTER_HOURS + value: {{ .Values.keyRotationAfterHours | quote }} + - name: PURGE_KEYS_AFTER_HOURS + value: {{ .Values.purgeKeysAfterHours | quote }} + - name: ALLOW_DISABLE_KEY_ROTATION_OVERRIDE + value: {{ .Values.allowDisableKeyRotationOverride | quote }} + {{- range $key, $value := .Values.extraEnv }} + - name: {{ $key }} + value: {{ $value }} + {{- end }} + ports: + - name: metrics + containerPort: 9101 + protocol: TCP + livenessProbe: + httpGet: + path: /metrics + port: metrics + initialDelaySeconds: 30 + timeoutSeconds: 1 + resources: + {{- toYaml .Values.resources | nindent 12 }} + volumeMounts: + - name: app-secrets + mountPath: /secrets + terminationGracePeriodSeconds: 300 + volumes: + - name: app-secrets + secret: + secretName: {{ include "estafette-gcp-service-account.fullname" . }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/estafette-gcp-service-account/templates/secret.yaml b/estafette-gcp-service-account/templates/secret.yaml new file mode 100644 index 0000000..03bd24a --- /dev/null +++ b/estafette-gcp-service-account/templates/secret.yaml @@ -0,0 +1,13 @@ +apiVersion: v1 +kind: Secret +metadata: + name: {{ include "estafette-gcp-service-account.fullname" . }} + namespace: {{ .Release.Namespace }} + labels: +{{ include "estafette-gcp-service-account.labels" . | indent 4 }} + annotations: + estafette.io/gcp-service-account: 'true' + estafette.io/gcp-service-account-name: 'estafette-gcp-service-account' +type: Opaque +data: + service-account-key.json: {{.Values.secret.googleServiceAccountKeyfileJson | toString | b64enc}} \ No newline at end of file diff --git a/estafette-gcp-service-account/templates/serviceaccount.yaml b/estafette-gcp-service-account/templates/serviceaccount.yaml new file mode 100644 index 0000000..b0a7e1a --- /dev/null +++ b/estafette-gcp-service-account/templates/serviceaccount.yaml @@ -0,0 +1,9 @@ +{{- if .Values.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ template "estafette-gcp-service-account.serviceAccountName" . }} + namespace: {{ .Release.Namespace }} + labels: +{{ include "estafette-gcp-service-account.labels" . | indent 4 }} +{{- end -}} diff --git a/estafette-gcp-service-account/values.yaml b/estafette-gcp-service-account/values.yaml new file mode 100644 index 0000000..be04f21 --- /dev/null +++ b/estafette-gcp-service-account/values.yaml @@ -0,0 +1,94 @@ +# Default values for estafette-gcp-service-account. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. + +replicaCount: 1 + +image: + repository: estafette/estafette-gcp-service-account + # The tag can be set to override the appVersion getting used as the image tag + tag: + pullPolicy: IfNotPresent + +imagePullSecrets: [] +nameOverride: "" +fullnameOverride: "" + +serviceAccount: + # Specifies whether a service account should be created + create: true + # The name of the service account to use. + # If not set and create is true, a name is generated using the fullname template + name: + +rbac: + # Specifies whether roles and bindings should be created + enable: true + +podSecurityContext: {} + # fsGroup: 2000 + +securityContext: {} + # capabilities: + # drop: + # - ALL + # readOnlyRootFilesystem: true + # runAsNonRoot: true + # runAsUser: 1000 + +resources: + requests: + cpu: 50m + memory: 100Mi + limits: + cpu: 100m + memory: 350Mi + +nodeSelector: {} + +tolerations: [] + +affinity: + nodeAffinity: + preferredDuringSchedulingIgnoredDuringExecution: + - weight: 10 + preference: + matchExpressions: + - key: cloud.google.com/gke-preemptible + operator: In + values: + - "true" + +# pass (additional) arguments to the container +extraArgs: [] + +# use to set extra environment variables +extraEnv: {} + +# use to add extra labels +extraLabels: {} + +# use to add extra labels to podspec for getting their values in prometheus +extraPodLabels: {} + +secret: + # sets a json keyfile for a gcp service account + googleServiceAccountKeyfileJson: "{}" + +# the mode determines whether the controller can create service accounts, sets iam roles for them and rotate keys or is allowed to do less +# normal - can create service accounts and rotate keys +# convenient - can set roles, create service accounts and rotate keys (risky) +# rotate_keys_only - can rotate keys +mode: normal + +# gcp project id for a centralized project to use for service accounts +serviceAccountProjectID: + +# number of hours before a key gets rotated +keyRotationAfterHours: 168 + +# number of hours before old keys get purged from a service account; needs to be larger than the rotation; we set it to twice +purgeKeysAfterHours: 336 + +# if set to true secrets can be annotated to disable key rotation; useful for applications that don't handle key rotation well, otherwise they'll probably start erroring after the purgeKeysAfterHours number of hours after they started +allowDisableKeyRotationOverride: true \ No newline at end of file diff --git a/kubernetes.yaml b/kubernetes.yaml deleted file mode 100644 index 87c2ad5..0000000 --- a/kubernetes.yaml +++ /dev/null @@ -1,89 +0,0 @@ -apiVersion: v1 -kind: Namespace -metadata: - name: estafette ---- -apiVersion: v1 -kind: ServiceAccount -metadata: - name: estafette-gcp-service-account - namespace: estafette - labels: - app: estafette-gcp-service-account - team: ${TEAM_NAME} ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: estafette-gcp-service-account - namespace: estafette - labels: - app: estafette-gcp-service-account - team: ${TEAM_NAME} -spec: - replicas: 1 - strategy: - type: Recreate - revisionHistoryLimit: 10 - selector: - matchLabels: - app: estafette-gcp-service-account - template: - metadata: - labels: - app: estafette-gcp-service-account - team: ${TEAM_NAME} - version: ${VERSION} - annotations: - prometheus.io/scrape: "true" - prometheus.io/port: "9101" - spec: - serviceAccount: estafette-gcp-service-account - terminationGracePeriodSeconds: 120 - affinity: - nodeAffinity: - preferredDuringSchedulingIgnoredDuringExecution: - - weight: 10 - preference: - matchExpressions: - - key: cloud.google.com/gke-preemptible - operator: In - values: - - "true" - containers: - - name: estafette-gcp-service-account - image: estafette/estafette-gcp-service-account:${GO_PIPELINE_LABEL} - imagePullPolicy: IfNotPresent - env: - - name: GOOGLE_APPLICATION_CREDENTIALS - value: /secrets/service-account-key.json - - name: MODE - value: "${MODE}" - - name: SERVICE_ACCOUNT_PROJECT_ID - value: "${SERVICE_ACCOUNT_PROJECT_ID}" - - name: KEY_ROTATION_AFTER_HOURS - value: "${KEY_ROTATION_AFTER_HOURS}" - - name: PURGE_KEYS_AFTER_HOURS - value: "${PURGE_KEYS_AFTER_HOURS}" - - name: ALLOW_DISABLE_KEY_ROTATION_OVERRIDE - value: "${ALLOW_DISABLE_KEY_ROTATION_OVERRIDE}" - resources: - requests: - cpu: 50m - memory: 100Mi - limits: - cpu: 100m - memory: 350Mi - livenessProbe: - httpGet: - path: /metrics - port: 9101 - initialDelaySeconds: 30 - timeoutSeconds: 1 - volumeMounts: - - name: estafette-gcp-service-account-secrets - mountPath: /secrets - volumes: - - name: estafette-gcp-service-account-secrets - secret: - secretName: estafette-gcp-service-account-secrets \ No newline at end of file diff --git a/rbac.yaml b/rbac.yaml deleted file mode 100644 index 69b6128..0000000 --- a/rbac.yaml +++ /dev/null @@ -1,30 +0,0 @@ -apiVersion: rbac.authorization.k8s.io/v1beta1 -kind: ClusterRole -metadata: - name: estafette-gcp-service-account - labels: - app: estafette-gcp-service-account -rules: -- apiGroups: [""] # "" indicates the core API group - resources: - - secrets - verbs: - - get - - list - - update - - watch ---- -apiVersion: rbac.authorization.k8s.io/v1beta1 -kind: ClusterRoleBinding -metadata: - name: estafette-gcp-service-account - labels: - app: estafette-gcp-service-account -roleRef: - apiGroup: rbac.authorization.k8s.io - kind: ClusterRole - name: estafette-gcp-service-account -subjects: -- kind: ServiceAccount - name: estafette-gcp-service-account - namespace: estafette