Deploying to Kubernetes from GitHub Actions with kubectl and Helm is surprisingly straightforward once you grasp the core interplay between your CI/CD pipeline, your Kubernetes cluster, and your Helm chart.
Here’s a quick demo. Imagine we have a simple web application, and we want to deploy it to our Kubernetes cluster whenever we push a new tag to our GitHub repository.
First, let’s set up a basic Helm chart for our application. This chart will define our Kubernetes resources:
# my-app/Chart.yaml
apiVersion: v2
name: my-app
version: 0.1.0
description: A simple web application chart
# my-app/values.yaml
replicaCount: 1
image:
repository: nginx
tag: latest
pullPolicy: IfNotPresent
service:
type: ClusterIP
port: 80
And our deployment manifest:
# my-app/templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "my-app.fullname" . }}
labels:
{{- include "my-app.labels" . | nindent 4 }}
spec:
replicas: {{ .Values.replicaCount }}
selector:
matchLabels:
{{- include "my-app.selectorLabels" . | nindent 6 }}
template:
metadata:
labels:
{{- include "my-app.labels" . | nindent 8 }}
spec:
containers:
- name: {{ .Chart.Name }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
ports:
- containerPort: {{ .Values.service.port }}
name: http
Now, let’s define our GitHub Actions workflow file (.github/workflows/deploy.yml):
name: Deploy to Kubernetes
on:
push:
tags:
- 'v*' # Trigger on tags like v1.0.0
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Set up Helm
uses: azure/setup-helm@v3
with:
version: 'v3.10.1' # Specify your Helm version
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v1
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: us-east-1
- name: Authenticate to Amazon EKS
id: eks
uses: aws-actions/amazon-eks-cluster-controller@v1
with:
cluster-name: my-eks-cluster # Your EKS cluster name
# Optional: if you need to specify the region, uncomment the following line:
# region: us-east-1
- name: Deploy to EKS using Helm
env:
KUBE_CONFIG_DATA: ${{ steps.eks.outputs.kubeconfig }}
run: |
helm upgrade --install my-release ./my-app \
--namespace default \
--set image.tag=${{ github.ref_name }} \
--kubeconfig $KUBE_CONFIG_DATA
This workflow does a few key things:
-
Checks out your code: Gets the latest version of your repository.
-
Sets up Helm: Installs the specified Helm version on the runner.
-
Configures AWS Credentials: Uses your GitHub secrets to authenticate with AWS. This is crucial for EKS.
-
Authenticates to EKS: This action fetches the kubeconfig for your specified EKS cluster. The
steps.eks.outputs.kubeconfigis the magic that makeskubectlandhelmtalk to your cluster. -
Deploys with Helm: It runs
helm upgrade --install.upgradewill install the release if it doesn’t exist, or upgrade it if it does. We pass the kubeconfig data directly via an environment variable, whichhelm(andkubectlunder the hood) will pick up. We also dynamically set theimage.tagusing the Git tag that triggered the workflow (${{ github.ref_name }}).
The most surprising truth about this setup is that you don’t need a persistent kubeconfig file on your CI runner. The amazon-eks-cluster-controller action dynamically generates and provides the necessary credentials for the duration of the job, securely passing them via environment variables.
Let’s trace what happens when you push a tag like v1.2.0:
- The
pushevent with a tag matchingv*triggers theDeploy to Kubernetesworkflow. - The
ubuntu-latestrunner spins up. actions/checkoutfetches your Helm chart and workflow file.setup-helmmakeshelmcommand available.configure-aws-credentialssets up the environment so the next step can talk to AWS.amazon-eks-cluster-controlleruses your AWS credentials to get temporary credentials for your EKS cluster and outputs them askubeconfigdata.- The
Deploy to EKS using Helmstep receives thisKUBE_CONFIG_DATA, making it available as an environment variable. helm upgrade --installis executed. It sees theKUBE_CONFIG_DATAenvironment variable, uses it to connect to your EKS cluster, and then applies your Helm chart, creating or updating your deployment with thenginx:v1.2.0image (or whatever tag you pushed).
The key levers you control here are:
-
Helm Chart: The structure and values in your
Chart.yamlandvalues.yamlfiles define your application’s deployment. -
GitHub Secrets:
AWS_ACCESS_KEY_IDandAWS_SECRET_ACCESS_KEYare essential for authentication. -
EKS Cluster Name:
cluster-namein theamazon-eks-cluster-controlleraction must match your actual cluster. -
Helm Release Name:
my-releaseis the name Helm uses to track your deployed application. -
Namespace:
--namespace defaultspecifies where in your cluster the release will be installed. -
Image Tag Overrides:
--set image.tag=${{ github.ref_name }}is how you dynamically update your application’s image version.
A common, subtle pitfall is not properly handling the image.tag value. If your Helm chart expects a specific value to set the image tag (e.g., image.tag), you must use --set image.tag=... in your helm upgrade command. Simply pushing a Git tag doesn’t automatically update the image in your Helm chart unless you explicitly tell Helm to do so.
Once this is working, the next hurdle is often managing multiple environments (dev, staging, prod) and implementing rollbacks based on deployment health.