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:

  1. Checks out your code: Gets the latest version of your repository.

  2. Sets up Helm: Installs the specified Helm version on the runner.

  3. Configures AWS Credentials: Uses your GitHub secrets to authenticate with AWS. This is crucial for EKS.

  4. Authenticates to EKS: This action fetches the kubeconfig for your specified EKS cluster. The steps.eks.outputs.kubeconfig is the magic that makes kubectl and helm talk to your cluster.

  5. Deploys with Helm: It runs helm upgrade --install. upgrade will install the release if it doesn’t exist, or upgrade it if it does. We pass the kubeconfig data directly via an environment variable, which helm (and kubectl under the hood) will pick up. We also dynamically set the image.tag using 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 push event with a tag matching v* triggers the Deploy to Kubernetes workflow.
  • The ubuntu-latest runner spins up.
  • actions/checkout fetches your Helm chart and workflow file.
  • setup-helm makes helm command available.
  • configure-aws-credentials sets up the environment so the next step can talk to AWS.
  • amazon-eks-cluster-controller uses your AWS credentials to get temporary credentials for your EKS cluster and outputs them as kubeconfig data.
  • The Deploy to EKS using Helm step receives this KUBE_CONFIG_DATA, making it available as an environment variable.
  • helm upgrade --install is executed. It sees the KUBE_CONFIG_DATA environment variable, uses it to connect to your EKS cluster, and then applies your Helm chart, creating or updating your deployment with the nginx:v1.2.0 image (or whatever tag you pushed).

The key levers you control here are:

  • Helm Chart: The structure and values in your Chart.yaml and values.yaml files define your application’s deployment.

  • GitHub Secrets: AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY are essential for authentication.

  • EKS Cluster Name: cluster-name in the amazon-eks-cluster-controller action must match your actual cluster.

  • Helm Release Name: my-release is the name Helm uses to track your deployed application.

  • Namespace: --namespace default specifies 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.

Want structured learning?

Take the full Github-actions course →