7 min read
Development
Continuous Delivery of Kubernetes Applications Using Only GitHub Actions

This summary was generated by AI. Please refer to the article for accurate details.
Read the summary
We built a simple, effective way to deliver Kubernetes apps using only GitHub Actions—no extra tools needed. Our team runs payment services on Kubernetes and wanted to keep things lean. We focused on just two steps: updating Kubernetes config files and pushing new container images. We didn’t use Argo CD, Flux, or any other CD tool. Instead, we used GitHub Actions, which our team already uses daily. This means less training, fewer tools to manage, and faster onboarding for new members. The setup works by having both app code and Kubernetes configs in one repo. When someone pushes code, GitHub Actions automatically creates a pull request. It then updates the container image tag using a composite action, which runs after each image build. The image version follows Semantic Versioning—so minor updates increase the version number by one. We found that image updates happen immediately when a PR is merged. If you add a new app during an open PR, it won’t update until the PR includes the latest changes. Also, if a pipeline overwrites an image tag, it might overwrite an older version—so we recommend checking the tag value before updating. This method works best for small teams with limited DevOps expertise. It’s simple, transparent, and lets you see errors right in GitHub. You still need proper permissions—like updating content and opening pull requests—set in GitHub Actions. And while it doesn’t offer real-time reconciliation, it’s reliable when the pipeline runs. The result? A fully GitOps workflow, managed entirely in GitHub. No extra tools, no complex setups—just a smooth, continuous delivery process that fits small teams.
Continuous Delivery of Kubernetes Applications Using Only GitHub Actions
Hello. My name is Narazaki, and I work in the Toyota Woven City Payment Solution Development Group.
Our team is responsible for developing the payment infrastructure application for Woven by Toyota at Toyota Woven City. We build cross-functional payment solutions, covering everything from the backend to the web front end and mobile applications.
The payment backend runs on Kubernetes and is developed using various cloud-native tools.
This time, while following GitOps—an approach where infrastructure configuration files are managed and modified using Git, key to building and maintaining stable Kubernetes applications—we aim to implement the continuous delivery (CD) process using only GitHub Actions, instead of the commonly used cloud-native CD tools. The CD process in this setup is limited to:
- Applying changes to Kubernetes configuration files
- Updating the container image
While there are more advanced CD strategies like Blue/Green and Canary deployments, this approach starts small. This setup is designed for teams that already have a DevOps workflow and want to continuously and efficiently deliver Kubernetes applications with minimal developers and no additional tools—using only GitHub Actions, which they already use daily. The repository assumes that both the application code and Kubernetes configuration management files are maintained in the same repository. (Technically, it might be possible to run this across repositories depending on permission settings, but let’s not get into that here.)
For GitLab users, there’s an excellent tool called Auto DevOps, so this isn’t a 'GitHub and GitHub Actions are the best!' kind of post. But don’t worry, I’m not making that claim!
Cloud-Native CI/CD Tools for Kubernetes
What tools come to mind when you think of CI/CD for Kubernetes applications?
And so on. Both tools are powerful and highly useful for leveraging Kubernetes to its full potential. They also allow for flexible and secure updates to Kubernetes configuration files and application images, enabling GitOps practices.
On the other hand, they require tool-specific knowledge and expertise. For smaller teams without dedicated DevOps specialists, maintaining them continuously can be a challenge—wouldn’t you agree? Running a CD tool itself requires Kubernetes, and the tool also needs Kubernetes configuration files to manage those same configuration files.
In this article, we‘ll explore how to set up the pipeline shown in the figure below using only GitHub Actions. Kubernetes runs on a generic cluster, not tied to any specific cloud provider. This setup requires a container registry. The configuration management file uses Kustomize as an example, but it can be applied to other tools like Helm, Terraform, and more.
Demo
Consider a repository that includes folders for both Kubernetes configuration files and applications. The folder structure is as follows: This section omits specific code, Dockerfile contents, and application source code.
├── .github
│ ├── actions
│ │ └── image-tag-update
│ │ └── action.yaml
│ └── workflows
│ ├── build-go.yaml
│ ├── build-java.yaml
│ ├── build-node.yaml
│ └── kubectl.yaml
├── go-app
│ ├── src/
│ └── Dockerfile
├── java-app
│ ├── src/
│ └── Dockerfile
├── k8s
│ ├── service-go.yaml
│ ├── service-java.yaml
│ ├── service-node.yaml
│ └── kustomization.yaml
└── node-app
├── src/
└── Dockerfile
Each application follows the structure below:
apiVersion: apps/v1
kind: Deployment
metadata:
name: app
spec:
...
template:
...
spec:
containers:
- name: some-server
image: Go-placeholder # put the same string as in kcustomization as a placeholder
All placeholders are centrally managed in kustomization.yaml:
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: techblog
resources:
- service-go.yaml
- service-java.yaml
- service-node.yaml
images:
- name: go-placeholder
newName: go-app
newTag: v1.1.1
- name: java-placeholder
newName: java-app
newTag: v2.7.9alpha
- name: node-placeholder
newName: node-app
newTag: latest
First, to apply the Kubernetes configuration file, configure the following GitHub Actions workflow.
name: kubectl
on:
pull_request:
branches:
- "**"
paths:
- "K8s/**" the location of the #Kubernetes manifest file
push:
branches:
- main
paths:
- "k8s/**"
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: azure/setup-kubectl@v4
- env:
KUBECONFIG_CONTENTS: ${ secrets.KUBECONFIG_contents }} # Put kubeconfig in GitHub secrets beforehand
run: |
echo "${KUBECONFIG_CONTENTS}" > $HOME/.kube/config
chmod 600 $HOME/.kube/config
- run: kubectl apply --dry-run=server -k ./k8s >> $GITHUB_STEP_SUMMARY
- if: Github.ref == 'refs/heads/main # Changes are actually applied only on the main branch
run: kubectl apply -k k8s/
This pipeline applies a standard Kubernetes configuration when using a kubeconfig with administrator privileges. Adjust the method of retrieving the kubeconfig based on the cluster’s configuration approach, such as for different cloud environments. Next, automatically create a pull request when pushing an application, or set up a composite action to update the container’s image tags.
name: image-tag-update
description: 'Task to update image tags in kustomization when container images are updated.
inputs:
target_app:
description: 'Target applications,
required: true
tag_value:
description: 'New Container Image Tag '
required: true
token:
description: 'Tokens with PR and content update privileges.
required: true
runs:
using: 'composite'
steps:
- uses: actions/checkout@v4
id: check-branch-exists
continue-on-error: true
with:
ref: "Image-tag-update" # Default branch name for tag updates
- uses: Actions/checkout@v4 # Checkout cannot fall back to the default branch if the specified branch is missing
if: steps.check-branch-exists.outcome == 'failure'
with:
ref: main
- uses: Mikefarah/yq@master # Replace the value of the target placeholder tag with yq
with:
cmd: yq eval '(.images[] | select(.name == "'"${{ inputs.target_app }}-placeholder"'")).newTag = "'"${{ inputs.tag_value }}"'"' -i k8s/kustomization.yaml
- uses: peter-evans/create-pull-request@v6
if: steps.check-branch-exists.outcome == 'failure' # Create a new pull request if no pull requests exist
with:
title: 'Update Container Image.
body: |
Update `${{ inputs.target_app }}'
branch: "image-tag-update"
- uses: stefanzweifel/git-auto-commit-action@v5
if: steps.check-branch-exists.outcome == 'success' # Add a commit to the existing branch if checkout succeeds
with:
commit_message: "Image update for ${{ inputs.target_app }}"
The composite runs during image creation for each application. For multiple applications, it’s a good idea to add them after each image is created.
...
- uses: docker/setup-buildx-action@v3
- uses: docker/build-push-action@v6
with:
file: ./Dockerfile
push: true
tags: ${ env.tag }} # some tag
- uses: ./.github/actions/image_update
if: github.ref == 'refs/heads/main'
with:
target_app: go
tag_value: ${{ env.tag }}
token: ${ secrets.GITHUB_token }} # A github token with content and Pull Request editing privileges
When the application runs,the container image updates automatically, letting you deploy a new one with a Pull Request! (Tag derivation is handled by your workflow.) The example below shows the minor version increment.)
- name: go-placeholder
newName: go-app
- newTag: v1.1.1
+ newTag: v1.1.2
Operational considerations
Timing of deployment
Image update Pull Request deploy immediately upon merging. If you want to release updates along with infrastructure changes, you can either add the fixes to the same branch or merge them when the timing is right.
Add a new container application
For example, if you add a Python application in the above setup while an image update Pull Request is still open, updating the Python image tag won’t take effect unless the Pull Request includes the latest changes.
Cut back
It’s easy to undo—just revert the commit.
Timing of Reconcile
While many GitOps Tools offer near real-time reconciliation to minimize drift, this method only works when the CD pipeline is running. It’s important to choose the right tool based on the number of teammates and their permissions to update the Kubernetes cluster.
You’re interacting with the Container Registry indirectly.
While some retrieve the latest container image directly from the container registry, this approach operates differently. It's advisable to include a verification step for each container registry to ensure the container exists.
About permission settings for GitHub Actions
You’ll need update permissions for contents and pull-requests. Set permissions in Actions settings, GitHub Apps, and more. Learn more here .
Overwritten by a container image that was executed later
The CD tool determines the newer version by checking the container image tag, following conventions like Semantic Versioning. The workflow above will overwrite the image tag in the later-executed pipeline, regardless of the tag’s value. If this behavior is an issue, consider checking the value before deciding whether to overwrite it.
Summary
With this approach, GitOps can be fully managed on GitHub, enabling a simple and efficient continuous delivery process for Kubernetes applications. Since CD tool errors can also be consolidated in GitHub Actions, it’s convenient to check execution results and errors just like in the usual CI process. Kubernetes offers a wide range of tools, making selection challenging. However, by choosing the right tools for my workflow, I aim to improve the productivity of Kubernetes application development.
関連記事 | Related Posts
We are hiring!
Cloud Engineer(トヨタグループ内製化支援)/Cloud Solution G/東京・名古屋・大阪・福岡
トヨタグループのクラウド活用を、最前線で支える。1社にとどまらないスケールで、移行・生成AI・内製化まで幅広く挑戦できるポジションです。
【Java/Python】事業や組織の成長に携わるプロダクトエンジニア(AWS環境)/KINTO CXシステム開発G/東京・名古屋
KINTO CXシステム開発グループについてTOYOTAのクルマのサブスクリプションサービスである『 KINTO 』を中心とした国内向けサービスのプロジェクト立ち上げから運用保守に至るまでの運営管理を行っています。大手EC、SIer、ベンチャー、Webサービス企業など、さまざまなスペシャリストが集まり、KINTOサービスを開発しています。
Follow Us
KINTOテクノロジーズの最新情報をSNSで発信中!イベント・テックブログ更新情報もお届けします。
