- May 13, 2024
- 7 Min Read
- kubernetes
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: v1kind: ServiceAccountmetadata: 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/v1kind: ClusterRolemetadata: name: cicdrules: - 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/v1kind: ClusterRoleBindingmetadata: name: cicd-getactionsroleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: cicdsubjects: - 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: v1kind: Secrettype: kubernetes.io/service-account-tokenmetadata: 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.