Setting up a Kubernetes CI/CD Service Account

As Docker containers are becoming more and more the norm of software deployment, Kubernetes clusters across the world are taking off as a convenient infrastructure alternative to plain basic servers.

With deployed applications becoming more resilient thanks to Kubernetes, we should also not forget about our CI/CD routine, as still the most resilient application sometimes fails, we'd not want to be hard-pressed to deliver a fix in time due to self-imposed deployment obstacles.

So let's create a CI/CD user for our cluster, which allows us to keep the cluster credentials safe, and the deployment routine fast.

Creating the service account

First steps first, let's create the plain service account, without any permissions or roles assigned to it, this is fairly straight forward.

SERVICE_ACCOUNT_NAME="cicd-user"
NAMESPACE="default"

kubectl create sa "${SERVICE_ACCOUNT_NAME}" --namespace "${NAMESPACE}"

After the command returns, kubectl, should report serviceaccount/cicd-user created. Good!

Creating the target folder

Next up we create a temporary folder, where later on we want our generated config to be put in.

TARGET_FOLDER="$HOME/tmp/kube"

mkdir -p "${TARGET_FOLDER}"

Extracting secret name and public key

After having created the folder successfully, we need to fetch the secret name of our previously created service account, and extract the public key information of it.

SECRET_NAME=$(kubectl get sa "${SERVICE_ACCOUNT_NAME}" --namespace="${NAMESPACE}" -o json | jq -r .secrets[].name)

kubectl get secret --namespace "${NAMESPACE}" "${SECRET_NAME}" -o json | jq -r '.data["ca.crt"]' | base64 --decode > "${TARGET_FOLDER}/ca.crt"

Mind you, we are using jq here to parse the json output from kubectl.

Fetching the usertoken

Not far until we are done, the next step will be to fetch and save the usertoken out of the service account secret.

USER_TOKEN=$(kubectl get secret --namespace "${NAMESPACE}" "${SECRET_NAME}" -o json | jq -r '.data["token"]' | base64 --decode)

Bringing it all together

Having gathered all the necessary information in the previous steps, it's now time to put it all together and generate our long desired config file for our service account.

KUBECFG_FILE_NAME="$HOME/tmp/kube/k8s-${SERVICE_ACCOUNT_NAME}-${NAMESPACE}-conf"

context=$(kubectl config current-context)

CLUSTER_NAME=$(kubectl config get-contexts "$context" | awk '{print $3}' | tail -n 1)


ENDPOINT=$(kubectl config view \
-o jsonpath="{.clusters[?(@.name == \"${CLUSTER_NAME}\")].cluster.server}")

kubectl config set-cluster "${CLUSTER_NAME}" \
--kubeconfig="${KUBECFG_FILE_NAME}" \
--server="${ENDPOINT}" \
--certificate-authority="${TARGET_FOLDER}/ca.crt" \
--embed-certs=true

kubectl config set-credentials \
"${SERVICE_ACCOUNT_NAME}-${NAMESPACE}-${CLUSTER_NAME}" \
--kubeconfig="${KUBECFG_FILE_NAME}" \
--token="${USER_TOKEN}"


kubectl config set-context \
"${SERVICE_ACCOUNT_NAME}-${NAMESPACE}-${CLUSTER_NAME}" \
--kubeconfig="${KUBECFG_FILE_NAME}" \
--cluster="${CLUSTER_NAME}" \
--user="${SERVICE_ACCOUNT_NAME}-${NAMESPACE}-${CLUSTER_NAME}" \
--namespace="${NAMESPACE}"

kubectl config use-context "${SERVICE_ACCOUNT_NAME}-${NAMESPACE}-${CLUSTER_NAME}" \
--kubeconfig="${KUBECFG_FILE_NAME}"

Assigning a Role via RoleBinding

One little thing is still to do, since we only created a config file and a service account in the previous steps, our service account still doesn't have any permissions to do stuff a CI/CD user should be allowed to do, like deploying for example.

To fix this we'll need to apply the following role and role binding configuration:

kind: Role
apiVersion: rbac.authorization.k8s.io/v1beta1
metadata:
  name: cicd-role
rules:
- apiGroups:
  - ""
  - apps
  - extensions
  resources:
  - '*'
  verbs:
  - '*'
---
kind: RoleBinding
apiVersion: rbac.authorization.k8s.io/v1beta1
metadata:
  name: cicd-role
subjects:
  - kind: ServiceAccount
    name: cicd-user
roleRef:
  kind: Role
  name: cicd-role
  apiGroup: rbac.authorization.k8s.io
cicd-role-config.yaml

Save the above yaml, and then apply it using:

kubectl apply -f cicd-role-config.yaml

But again, mind you, we are referencing the name of the generated service account in the yaml file, if you changed the name during the first step, you ought to change it here too!

Testing it out

The final steps are done, now it's time to check if the effort was worth it.

KUBECONFIG=${KUBECFG_FILE_NAME} kubectl get pods

Congratulations!

Well done! Now you have your cluster setup with a second account for deployment, so that your Cluster administrator can keep his config for himself, you put your generated config to your preferred CI/CD solution, and enjoy hands-free deployments of your projects