Orchestrating Kubernetes Continuous Delivery via Skaffold and GitHub Actions

The synchronization of local development environments with remote Kubernetes clusters often presents a significant friction point in the software delivery lifecycle. Skaffold emerges as the critical command line tool designed to mitigate this friction by facilitating continuous development for Kubernetes applications. By automating the workflow of building, pushing, and deploying, Skaffold transforms the iterative process of modifying source code into a streamlined pipeline. When integrated with GitHub Actions, this capability extends from a developer's local machine into a robust CI/CD pipeline, enabling the seamless transition of containerized workloads from a commit in a repository to a running state in a production-grade cluster, such as Amazon EKS.

The integration of Skaffold within GitHub Actions is not merely a matter of execution but a strategic orchestration of environment setup, credential management, and image lifecycle handling. The process involves the installation of the Skaffold binary, the configuration of Kubernetes context via kubectl, and the management of container registries. Because Skaffold abstracts the complexities of the build-push-deploy cycle, it allows developers to define their infrastructure as code within a skaffold.yaml file, which the GitHub Action then executes against a target cluster. This synergy ensures that the exact same logic used for local testing is applied during the automated deployment phase, reducing the "it works on my machine" phenomenon.

Skaffold Integration Mechanisms in GitHub Actions

There are multiple methodologies for incorporating Skaffold into a GitHub Actions workflow, ranging from the use of specialized third-party actions to manual binary installation via shell scripts.

The use of specialized actions provides a standardized way to ensure the correct version of the tool is present on the runner. One such implementation is the heypigeonhq/[email protected] action. This specific action is designed to install Skaffold on the host machine of the GitHub runner. It offers flexibility in versioning, allowing the user to specify an exact version (for example, 2.7.0) or utilize the latest release. The impact of using a version-locked installation is the guarantee of build reproducibility; by pinning the version, teams avoid unexpected breaking changes that might occur if a new version of Skaffold were released and automatically pulled by the runner.

Alternatively, the skaffold-github-action provides a more comprehensive set of parameters for controlling the execution environment. This action supports a wide array of configurations to fine-tune how Skaffold interacts with the Kubernetes cluster and the container registry.

The following table outlines the available configuration parameters for the skaffold-github-action:

Name Description Default
skaffold-version Set Skaffold version 1.39.2
container-structure-test-version Set Container Structure Test version 1.11.0
kubectl-version Set Kubernetes CLI version 1.25.0
working-directory Set current working directory similar to Github Actions run ${{ github.workspace }}
filename Path or URL to the Skaffold config file skaffold.yaml
command Default command for Skaffold to execute diagnose
file-output Filename to write build images to n/a
repository Default repository value (overrides global config) n/a
insecure-registries Target registries for built images which are not secure n/a
image Set Skaffold profile name n/a
tag The optional custom tag to use for images which overrides the current Tagger configuration n/a
push Push the built images to the specified image repository n/a
concurrency Number of concurrently running builds n/a

For environments where third-party actions are not permitted or where maximum control over the installation process is required, manual installation via a run step is the standard approach. This typically involves using curl to fetch the binary from Google's storage buckets.

Example of manual installation sequence:

bash curl -Lo skaffold https://storage.googleapis.com/skaffold/releases/latest/skaffold-linux-amd64 chmod +x skaffold sudo mv skaffold /usr/local/bin

This sequence ensures that the Skaffold binary is placed in the system path, making it available for all subsequent steps in the job.

Automating Deployments to Amazon EKS

Deploying to Amazon Elastic Kubernetes Service (EKS) requires a specific sequence of authentication and configuration steps to ensure that the GitHub Action runner has the necessary permissions to modify the cluster state.

The prerequisites for this workflow include an active AWS account and a configured AWS profile on the local machine for initial setup. Within the GitHub Actions environment, the workflow must expose repository secrets as environment variables to maintain security. These secrets typically include AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, EKS_CLUSTER, and EKS_REGION.

The logical flow of a deployment pipeline to EKS follows a rigorous set of steps:

  • Expose Repository Secrets as environment variables
  • Install Node.js dependencies for the application (e.g., using npm install)
  • Execute automated testing (e.g., npm test)
  • Log in to the Docker registry using docker/login-action@v1
  • Install kubectl via a shell command to enable cluster communication
  • Install the Skaffold binary
  • Implement caching for Skaffold image builds and configurations
  • Verify and configure the AWS CLI profile
  • Establish a connection to the EKS cluster
  • Execute the build and deployment using skaffold run
  • Verify the deployment status

A concrete implementation of the main.yml workflow for EKS deployment looks as follows:

```yaml
name: 'Build & Deploy to EKS'
on:
push:
branches:
- main
env:
AWSACCESSKEYID: ${{ secrets.AWSACCESSKEYID }}
AWSSECRETACCESSKEY: ${{ secrets.AWSSECRETACCESSKEY }}
EKSCLUSTER: ${{ secrets.EKSCLUSTER }}
EKSREGION: ${{ secrets.EKSREGION }}
DOCKERID: ${{ secrets.DOCKERID }}
DOCKERPW: ${{ secrets.DOCKERPW }}

jobs:
deploy:
name: Deploy
runs-on: ubuntu-latest
env:
ACTIONSALLOWUNSECURECOMMANDS: 'true'
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: '14'
- run: npm install
- run: npm test
- name: Login to Docker Hub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKER
ID }}
password: ${{ secrets.DOCKER_PW }}
- name: Install kubectl
run: |
curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"
```

This structure ensures that the application is tested before any deployment attempt is made, preventing broken code from reaching the cluster.

Skaffold Configuration and Build Artifacts

The core of the Skaffold operation is the skaffold.yaml file, which defines how artifacts are built and deployed. This file is critical for the CI/CD pipeline as it tells the GitHub Action which images to build and where to push them.

In a professional setup, the skaffold.yaml can utilize different build strategies. For example, using Jib for Java applications allows for building images without a Docker daemon.

Example Jib configuration:

yaml image: myapp jib: project: . type: maven # or gradle args: - -DskipTests

Alternatively, a custom build script can be defined to handle complex build requirements that cannot be expressed in standard YAML configurations.

Example custom build configuration:

yaml apiVersion: skaffold/v4beta6 kind: Config build: artifacts: - image: myapp custom: buildCommand: ./build.sh dependencies: paths: - src/** - Dockerfile

For full-stack applications, Skaffold allows the definition of multiple artifacts. This is particularly useful for architectures involving a frontend and a backend API.

In a full-stack scenario, the configuration might include:

  • A React Frontend: Using a specific Dockerfile.dev and syncing source files like .js, .jsx, .ts, .tsx, and .css.
  • A Python API: Defining the context and Dockerfile for the backend service.

The tagPolicy in these configurations often uses sha256 to ensure that every build results in a unique, immutable image tag, which is a best practice for Kubernetes deployments to avoid "ImagePullPolicy" issues and ensure atomic rollbacks.

Addressing the Cache Challenge in GitHub Actions

A recurring technical hurdle in the integration of Skaffold with GitHub Actions is the persistence of image caches. In a local environment, Skaffold utilizes a local cache directory (typically ~/.skaffold/cache) to avoid rebuilding images that have not changed. However, because GitHub Actions runners are ephemeral, this cache is lost after every job execution.

This leads to a situation where Skaffold rebuilds images on every run, significantly increasing the pipeline duration. Users have reported that while "Found Locally" is visible during local execution, the remote runner always initiates a fresh build.

To resolve this, developers must implement an explicit caching strategy within the GitHub Actions workflow. This involves using the actions/cache action to persist the ~/.skaffold/cache directory across different runs. Without this, the efficiency of Skaffold's incremental build process is nullified in the CI environment.

The impact of failing to cache these artifacts is a slower feedback loop for developers. In a high-velocity environment, the difference between a 5-minute build (cached) and a 15-minute build (fresh) can lead to significant productivity losses.

Comparative Deployment Strategies: GitHub Actions vs GitLab CI

While GitHub Actions is a primary target for Skaffold integration, comparing it with other CI systems like GitLab CI highlights the different architectural approaches to container orchestration.

In GitHub Actions, the environment is typically set up dynamically using a series of steps and actions. In contrast, GitLab CI often leverages a pre-built Docker image that already contains the necessary tools.

Example GitLab CI configuration:

yaml deploy: stage: deploy image: gcr.io/k8s-skaffold/skaffold:latest script: - skaffold run -p prod --default-repo=$CI_REGISTRY_IMAGE only: - main

The GitHub Actions approach provides more granular control over the tool versions (via setup-skaffold) and the environment setup (via azure/k8s-set-context), whereas the GitLab approach favors a containerized, immutable environment.

For those using GitHub Actions, the final deployment command often looks like this:

bash skaffold run -p prod --default-repo=gcr.io/my-project

This command triggers the production profile (-p prod) and overrides the default repository to ensure images are pushed to the correct Google Container Registry (GCR) path.

Detailed Technical Analysis of the Workflow

The integration of Skaffold into GitHub Actions represents a shift from "scripted" deployments to "declarative" deployments. In a traditional script, a developer might write a series of docker build, docker push, and kubectl apply commands. This is fragile because it requires the developer to manually manage image tags and ensure that the manifest files are updated with the new tags.

Skaffold eliminates this fragility by acting as the orchestrator. It handles the tagging process automatically and updates the Kubernetes manifests before applying them to the cluster. When this is executed within a GitHub Action, the result is a highly resilient pipeline.

The use of azure/k8s-set-context@v3 within the workflow is a critical step for security and connectivity:

yaml - name: Configure kubectl uses: azure/k8s-set-context@v3 with: kubeconfig: ${{ secrets.KUBECONFIG }}

By passing the KUBECONFIG as a secret, the workflow establishes a secure tunnel to the cluster without exposing sensitive credentials in the logs. This, combined with the ACTIONS_ALLOW_UNSECURE_COMMANDS: 'true' environment variable, ensures that the runner can execute the necessary shell commands to interact with the remote API server.

The overall effectiveness of this setup depends on the skaffold.yaml being properly tuned. Using useBuildkit: true and push: false for local development, while switching to push: true and specific profiles for CI, allows the same configuration file to serve both the developer's inner loop and the production outer loop.

Conclusion

The convergence of Skaffold and GitHub Actions provides a powerful framework for Kubernetes application delivery. By automating the build, push, and deploy cycle, it removes the manual overhead associated with container orchestration. The ability to pin versions through specialized actions like heypigeonhq/setup-skaffold ensures stability, while the flexible configuration of skaffold.yaml allows for complex, multi-service architectures to be managed as a single unit. However, the transition from local to remote environments requires careful attention to the caching of the .skaffold/cache directory and the secure management of cluster credentials. When these elements are correctly aligned, the result is a deployment pipeline that is not only fast and reproducible but also transparent and easy to maintain across the entire software development lifecycle.

Sources

  1. Setup Skaffold GitHub Action
  2. Skaffold GitHub Action
  3. Automate Deployments to Amazon EKS with Skaffold and GitHub Actions
  4. Skaffold Issue 8803 - Cache in GitHub Actions
  5. Kubernetes Skaffold Development Workflow

Related Posts