This is me, André König - a software engineer from Hamburg, Germany. André König

Securely Deploy to Kubernetes using GitHub Actions

A step-by-step walkthrough on creating a secure automated deployment process in Kubernetes using GitHub Actions, while maintaining the principle of least privilege.

Have you ever found yourself wondering how to automate your Kubernetes deployments using a CI/CD service like GitHub Actions? This can often seem like a daunting task, especially if you're new to Kubernetes and have only recently deployed your first application to your shiny new cluster using kubectl. But don't worry - it doesn't have to be as complicated as it sounds.

I'm here to guide you through the pitfalls and highlight the process step by step. By the end of this tutorial, you'll have a clear understanding of how to deploy using GitHub Actions and Kubernetes. So, let's get started!

How NOT to deploy to Kubernetes

You've already deployed your application via kubectl from your local machine. So, your first thought might be:

Why not just add the ~/.kube/config as a repository secret, so that the GitHub Action can use it?

❌ No! The ~/.kube/config acts like a master key, giving you access to everything on your cluster. It's like leaving your house keys under the doormat - not the best idea.

There is a better approach which follows the principle of least privilege.

How to deploy to Kubernetes securely

Securely deploying to Kubernetes isn't that hard. You need three ingredients for doing it:

  • A Service Account
  • A Cluster Role
  • A Cluster Role Binding
  • A Service Account Token

If these terms are new to you, no worries. We will briefly jump into each of them.

Creating the Resources on the Cluster

Service Account

A Service Account is like a "user" in your cluster. You can create it via kubectl or define a manifest file describing your service account.

It should be project-bound and therefore live in your project repository (e.g. kubernetes/service-account.yaml):

apiVersion: v1
kind: ServiceAccount
metadata:
name: github-actions

Applying it via kubectl apply -f kubernetes/service-account.yaml would create a service account, named github-actions in the default namespace of your cluster.

Cluster Role

A Cluster Role, as the name suggests, is a cluster-wide role with associated permissions. I chose to create one (global) CI/CD role here, so that I can easily associate future project-specific service accounts with it. You can also create project-specific roles if you want more fine-grained control on a project basis. From my experience, the permissions for a deployment are so similar that introducing roles on a project basis comes with a certain overhead.

When creating a cluster-wide role, it is recommended to keep the manifest file close to the rest of your infrastructure code. So if you have an own repository with all your IaC for your Kubernetes cluster, make sure to create a manifest file kubernetes/cluster-roles.yaml:

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: cicd
rules:
- apiGroups:
- '' # core API group
- apps
- networking.k8s.io
resources:
- namespaces
- deployments
- replicasets
- ingresses
- services
- secrets
verbs:
- create
- delete
- deletecollection
- get
- list
- patch
- update
- watch
---
# Your other cluster roles

Applying it via kubectl apply -f kubernetes/cluster-roles.yaml would create a new cluster role with the name cicd. In the example above, it grants all the permissions for managing the resources on the cluster that are required for typical applications. Please make sure to adjust the apiGroups and resources to suit your needs (e.g., for autoscaling, etc.).

Cluster Role Binding

Now that we have a cluster-wide role for cicd and a project-specific service account github-actions, we have to bind them together. With other words:

We give the service account the permissions it requires for deployment to our Kubernetes cluster.

This resource is project-specific and should therefore also live in the repository of the application you want to deploy, e.g. kubernetes/cluster-role-binding.yaml:

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: cicd-getactions
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: cicd
subjects:
- kind: ServiceAccount
name: github-actions

After applying it via kubectl, the subject github-actions (our service account) has a reference to our cluster role cicd.

When we authenticate this service account, we will already be able to deploy the application based on the previously defined permissions. But how do I authenticate this service account? Good question! On to our last resource type.

Service Account Token

A Service Account Token is like your ~/.kube/config, but owned by the Service Account (and therefore restricted to the given permissions). This token is also bound to your project and the manifest file should live in your repository, e.g. kubernetes/service-account-token.yaml:

apiVersion: v1
kind: Secret
type: kubernetes.io/service-account-token
metadata:
name: github-actions-token
annotations:
kubernetes.io/service-account.name: github-actions

As you can see, it is an ordinary Kubernetes secret, but with a specific type that tells Kubernetes that this is a token meant for service accounts. The definition above creates a secret named github-actions-token (in the default namespace; adjust if required) and references the actual service account via an annotation.

After applying it via kubectl, a new token was created on the cluster.

Connecting GitHub Actions with your Cluster

All the resources have been created; it is time to securely connecting to your Kubernetes Cluster as the newly created service account. Microsoft maintains an excellent GitHub Action that lets you do exactly that: azure/k8s-deploy. Although the name might be misleading, this action allows you to interact with any Kubernetes cluster, not just ones that are running on Azure.

# … Previous steps
- name: Set the Kubernetes context
uses: azure/k8s-set-context@v4
with:
method: service-account
k8s-url: ${{ secrets.KUBERNETES_URL }}
k8s-secret: ${{ secrets.KUBERNETES_SECRET }}
- name: Deploy to the Kubernetes cluster
uses: azure/k8s-deploy@v5
with:
pull-images: false
namespace: getactions
manifests: |
kubernetes
images: |
# The full URL of your image
ghcr.io/your-app/service:latest
# … Other steps

To be specific, we also use the azure/k8s-set-context action, which authenticates the GitHub Actions runner against your Kubernetes Cluster using the previously created service account.

This action needs two values, which you should add to your repository as secrets:

  • KUBERNETES_URL: The URL to your Kubernetes API server (e.g. https://your-host.tld:6443).
  • KUBERNETES_SECRET: The Service Account Token we created earlier.

You might wonder how you get the actual value of the Service Account Token. It is just one kubectl command away:

kubectl get secret github-actions-token -o yaml

Copy the whole YAML output and add it as the mentioned repository secret.

That's it. Now these two steps are able to interact with your Kubernetes cluster in a secure way and deploy the respective application to it.

You can find a complete GitHub Actions workflow for deploying to Kubernetes that uses the two steps above in the Kubernetes Deployment Workflow on getactions.dev.

Conclusion

Deploying to Kubernetes using GitHub Actions can be efficiently and securely executed without necessarily using a ~/.kube/config. This can be achieved by creating and integrating a Service Account, Cluster Role, Cluster Role Binding, and a Service Account Token.

By acquiring these components and connecting the GitHub Action with your Kubernetes Cluster, you can automate your deployments securely, following the principle of least privilege, and drastically enhance the workflow and security of your application deployments.


Thank You

I hope that you found this article insightful and valuable for your journey. If so, and you learned something new or would like to give feedback then let's connect on X at @ItsAndreKoenig. Additionally, if you need further assistance or have any queries, feel free to drop me an email or send me an async message.

One last thing!

Let me know how I'm doing by leaving a reaction.


You might also like these articles