Table of Contents:

Background

This is a step-by-step guide to building an application using Skaffold. From building and running on a local machine to releasing as Kubernetes YAML using CI. Gitlab will be used as an image registry and CI/CD pipeline.

Why Skaffold

Skaffold is a command line tool developed by Google. It helps developers build and deploy applications to Kubernetes easily and rapidly.

The good things:

  • It supports many languages.
  • It allows us to develop and deploy the change to Kubernetes quickly with the watch feature.
  • It lets you test with other APIs on the cluster easily.

The not so good things:

  • You need a working Kubernetes that can deploy from a local machine. You can use local Kubernetes such as Minikube but this could make it less beneficial.
  • It needs an image registry to build on the local machine.

My use case is to develop an application that calls to another application on the Kubernetes cluster, so I can test my application in the real environment very quickly.

How Skaffold works

Skaffold development workflow

The developer makes some changes on the local machine and triggers Skaffold to build and deploy the changes through the stages below.

Skaffold stages:

  • build - build image and push to designate registry
  • render - output Kubernetes deployment artifacts
  • deploy - deploy Kubernetes artifacts to the target server

Prerequisite

Things you will need before you can follow this tutorial:

  • Skaffold CLI installed. (See instructions here)
  • A working Kubernetes cluster and access via kubectl. (See local virtual machine option here).
  • Gitlab account and repository. One repository per application.
  • Docker CLI on the local machine (Docker desktop or Colima is ok).

Docker login to GitLab container registry.

Skaffold needs to push and pull images from the registry so we need to create an access token and login on to the local machine.

  1. Create GitLab Access Token.
    • Open Gitlab menu Access Tokens
    • Create access token with permissions to push/pull image registry

Token permissions

  1. Login using docker on the local machine.
    docker login registry.gitlab.com
    # Username: <your gitlab username>
    # Password: <your access token>
    
  2. [Only for private repository] Createpull image secret from docker config.
    kubectl create secret generic regcred \
     --from-file=.dockerconfigjson=$HOME/.docker/config.json \
     --type=kubernetes.io/dockerconfigjson
     # output: secret/regcred created
    

Skaffold with Java

In this tutorial, we will create a simple Java Spring boot application, test it, and deploy it on the local machine. Then, create GitLab CI that creates the Kubernetes YAML file as a release asset.

1. Clone the sample project as a starting point

git clone -b start git@gitlab.com:pongsatt/skaffold_java.git

It’s a simple Spring boot web application.

Run the app from your favorite IDE or command line.

./mvnw spring-boot:run
# or mvnw.cmd spring-boot:run for windows

Open http://localhost:8080 and you should see. Spring boot app web

2. Add k8s deployment definition

Next, we will add the Kubernetes deployment definition. This could be a plain YAML, Helm, or Kustomize. We will use a plain YAML in this example.

# k8s/app.yaml

apiVersion: v1
kind: Service
metadata:
  name: skaffold-java
spec:
  ports:
    - port: 8080
      name: http
  type: NodePort
  selector:
    app: skaffold-java
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: skaffold-java
  labels:
    app: skaffold-java
spec:
  selector:
    matchLabels:
      app: skaffold-java
  template:
    metadata:
      labels:
        app: skaffold-java
    spec:
      imagePullSecrets: # only need for private image registry
        - name: regcred # from secret creating step
      containers:
        - name: skaffold-java
          image: skaffold-java
          ports:
            - containerPort: 8080

This k8s/app.yaml file contains normal Deployment and Service definition except the container’s image which will be replaced with the actual image in the render stage.

3. Add Skaffold configuration

For Skaffold CLI to know how to work with our project, we need a Skaffold YAML configuration file.

# skaffold.yaml
apiVersion: skaffold/v2beta28
kind: Config
build:
  artifacts:
  - image: skaffold-java
    jib: {} # build docker image using project's jib plugin

This is the simplest version of Skaffold for Java. It tells Skaffold to build an image using the project’s Maven or Gradle plugin which will be added in the next step.

4. Add the Jib plugin to the project

Jib plugin is a library for Maven or Gradle to help Skaffold build Java application images.

Since we use Maven in our example, add this plugin snippet to pom.xml.

<!-- pom.xml -->
...
<plugins>
            ...
    <!-- to support skaffold java -->
    <plugin>
        <groupId>com.google.cloud.tools</groupId>
        <artifactId>jib-maven-plugin</artifactId>
        <version>3.2.0</version>
    </plugin>
</plugins>

You can configure how to build your image in this plugin document.

5. Start development using Skaffold

We have everything we need to start our development. First, set the default repo env so Skaffold knows where to push or pull the image. We will use the GitLab repository’s container registry.

export SKAFFOLD_DEFAULT_REPO=registry.gitlab.com/<your project name>/skaffold_java

Replace registry URL to your gitlab’s project container registry. You can also find the URL from your project “Container Registry” menu.

Run Skaffold development command.

skaffold dev

Skaffold will start watching the project’s files and when there is a change, it will build and deploy our application to the current Kubernetes cluster.

If things are going as planned, you should see the Spring boot application on the console like this.

...
Waiting for deployments to stabilize...
 - deployment/skaffold-java is ready.
Deployments stabilized in 3.498 seconds
Press Ctrl+C to exit
Watching for changes...
[skaffold-java] 
[skaffold-java]   .   ____          _            __ _ _
[skaffold-java]  /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
[skaffold-java] ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
[skaffold-java]  \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
[skaffold-java]   '  |____| .__|_| |_|_| |_\__, | / / / /
[skaffold-java]  =========|_|==============|___/=/_/_/_/
[skaffold-java]  :: Spring Boot ::        (v2.0.5.RELEASE)
[skaffold-java] 
...

Check if our application runs on the cluster.

kubectl get deployment/skaffold-java

You should see something like this.

NAME            READY   UP-TO-DATE   AVAILABLE   AGE
skaffold-java   1/1     1            1           3m1s

To access the application from the browser using port-forwarding.

kubectl port-forward svc/skaffold-java 8080:8080
# open http://localhost:8080

This sample app also support NodePort so you can access the application using NodePort.

6. Create GitLab CI definition

Now, we got our application running on the local machine. In this step, we will integrate Skaffold with the Gitlab CI pipeline. In this example, the pipeline will build and render Kubernetes YAML as a release asset so people can deploy our application from anywhere.

Create file .gitlab-ci.yml at the root of the project.

# .gitlab-ci.yml
stages:
  - build
  - release

build-job:
  stage: build
  image: gcr.io/k8s-skaffold/skaffold:v1.38.0
  rules:
    - if: $CI_COMMIT_TAG
  variables:
    SKAFFOLD_DEFAULT_REPO: $CI_REGISTRY_IMAGE
  script:
    - docker login -u $CI_REGISTRY_USER -p $CI_JOB_TOKEN $CI_REGISTRY
    - skaffold build --tag $CI_COMMIT_TAG --file-output build.json
    - skaffold render --offline=true -a build.json -o app.yaml
  artifacts:
    paths:
      - app.yaml

release_job:
  stage: release
  image: registry.gitlab.com/gitlab-org/release-cli:latest
  rules:
    - if: $CI_COMMIT_TAG
  script:
    - echo "running release_job"
  artifacts:
    paths:
      - app.yaml
  release:
    name: "Release $CI_COMMIT_TAG"
    description: "Created using the release-cli"
    tag_name: "$CI_COMMIT_TAG"
    assets:
      links:
        - name: "k8s"
          url: "$CI_PROJECT_URL/-/jobs/$CI_JOB_ID/artifacts/raw/app.yaml"
          filepath: "/app.yaml"
          link_type: "other"

Gitlab pipeline

Explanation. There 2 stages, build and release. Both of them are triggered when a tag is created.

  • build
    • Build the image and push it to the project’s registry, then output a build JSON file
    • Render Kubernetes YAML using information from the build JSON file and produces app.yaml as an installable Kubernetes artifact
  • release

Gitlab release page

Push the complete code to your repository and create a tag to see the result.

Skaffold with Go

Building Go application using Skaffold is a little bit different. Follow the below steps to build and release the Go application to GitLab.

1. Clone the sample project

git clone -b start git@gitlab.com:pongsatt/skaffold_go.git

This is an API application using the Go Echo library.

Start the app using your favorite IDE or command line.

go run main.go

Open http://localhost:8080 and you should see. Go application result

2. Add k8s deployment definition

Adding Kubernetes YAML as a deployment template.

# k8s/app.yaml

apiVersion: v1
kind: Service
metadata:
  name: skaffold-go
spec:
  ports:
  - port: 8080
    name: http
  type: NodePort
  selector:
    app: skaffold-go
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: skaffold-go
  labels:
    app: skaffold-go
spec:
  selector:
    matchLabels:
      app: skaffold-go
  template:
    metadata:
      labels:
        app: skaffold-go
    spec:
      containers:
      - name: skaffold-go
        image: skaffold-go
        ports:
          - containerPort: 8080

3. Add Dockerfile

This is different from the java application. Java uses the Jib library so it doesn’t need a Dockerfile. For Go, the image is built by docker, so it needs a Dockerfile.

FROM golang:1.16-alpine as builder

ENV GO111MODULE=on \
    CGO_ENABLED=0 \
    GOOS=linux \
    GOARCH=amd64
  
WORKDIR /src

COPY go.* ./
RUN go mod download

COPY *.go ./

# `skaffold debug` sets SKAFFOLD_GO_GCFLAGS to disable compiler optimizations
ARG SKAFFOLD_GO_GCFLAGS
RUN go build -gcflags="${SKAFFOLD_GO_GCFLAGS}" -o /bin/app . 

FROM scratch

# Define GOTRACEBACK to mark this container as using the Go language runtime
# for `skaffold debug` (https://skaffold.dev/docs/workflows/debug/).
ENV GOTRACEBACK=single
COPY --from=builder /bin/app /bin/app
CMD ["/bin/app"]

This is a 2-stages build. In the first stage build an executable file and copied to the bare minimum image in the 2nd stage. For a faster build but larger and less secure image, use one stage build Dockerfile below.

FROM golang:1.16-alpine

ENV GO111MODULE=on \
    CGO_ENABLED=0 \
    GOOS=linux \
    GOARCH=amd64
  
WORKDIR /src

COPY go.* ./
RUN go mod download

COPY *.go ./

# `skaffold debug` sets SKAFFOLD_GO_GCFLAGS to disable compiler optimizations
ARG SKAFFOLD_GO_GCFLAGS
RUN go build -gcflags="${SKAFFOLD_GO_GCFLAGS}" -o /bin/app . 

CMD ["/bin/app"]

4. Add Skaffold configuration

A simple Skaffold configuration for Go looks like this.

# skaffold.yaml
apiVersion: skaffold/v2beta28
kind: Config
build:
  artifacts:
  - image: skaffold-go
deploy:
  kubectl:
    manifests:
      - k8s/*

This config shows that we will deploy our application using kubectl and k8s/ folder as a deployment template.

5. Start development using Skaffold

We are ready to start the development.

Setting the default repo env so Skaffold knows to push or pull images from our GitLab’s registry

export SKAFFOLD_DEFAULT_REPO=registry.gitlab.com/<your project name>/skaffold_go

If you use Colima or something else rather than standard docker, make sure to set a correct docker host.

Colima example.

export DOCKER_HOST=unix:///$HOME/.colima/docker.sock

Run Skaffold development command.

skaffold dev

You should see the Echo output on the console.

...
Starting deploy...
 - service/skaffold-go created
 - deployment.apps/skaffold-go created
Waiting for deployments to stabilize...
 - deployment/skaffold-go is ready.
Deployments stabilized in 2.833 seconds
Press Ctrl+C to exit
Watching for changes...
[skaffold-go] 
[skaffold-go]    ____    __
[skaffold-go]   / __/___/ /  ___
[skaffold-go]  / _// __/ _ \/ _ \
[skaffold-go] /___/\__/_//_/\___/ v4.7.2
[skaffold-go] High performance, minimalist Go web framework
[skaffold-go] https://echo.labstack.com
[skaffold-go] ____________________________________O/_______
[skaffold-go]                                     O\
[skaffold-go] ⇨ http server started on [::]:8080

Check if our application runs on the cluster.

kubectl get deployment/skaffold-go

To access the application from the browser using port-forwarding.

kubectl port-forward svc/skaffold-go 8080:8080
# open http://localhost:8080

This sample app also support NodePort so you can access the application using NodePort.

6. Create Gitlab CI definition

# .gitlab-ci.yml
stages:
  - build
  - release

services:
  - docker:dind # use docker-in-docker service so docker cli is available

variables:
  DOCKER_DRIVER: overlay2
  DOCKER_TLS_CERTDIR: ""
  DOCKER_HOST: tcp://docker:2375 # docker socket on docker-in-docker service
  SKAFFOLD_DEFAULT_REPO: $CI_REGISTRY_IMAGE

build-job:
  stage: build
  image: gcr.io/k8s-skaffold/skaffold:v1.38.0
  rules:
    - if: $CI_COMMIT_TAG
  script:
    - docker login -u $CI_REGISTRY_USER -p $CI_JOB_TOKEN $CI_REGISTRY
    - skaffold build --tag $CI_COMMIT_TAG --file-output build.json
    - skaffold render --offline=true -a build.json -o app.yaml
  artifacts:
    paths:
      - app.yaml

release_job:
  stage: release
  image: registry.gitlab.com/gitlab-org/release-cli:latest
  rules:
    - if: $CI_COMMIT_TAG
  script:
    - echo "running release_job"
  artifacts:
    paths:
      - app.yaml
  release:
    name: "Release $CI_COMMIT_TAG"
    description: "Created using the release-cli"
    tag_name: "$CI_COMMIT_TAG"
    assets:
      links:
        - name: "k8s"
          url: "$CI_PROJECT_URL/-/jobs/$CI_JOB_ID/artifacts/raw/app.yaml"
          filepath: "/app.yaml"
          link_type: "other"

Go Gitlab-ci is different from Java because it needs docker CLI to build an image on the pipeline’s runner. So we need to add docker context to the build stage (using DIND service). The rest is the same as the Java pipeline.

Push the complete code to your repository and create a tag to see the result.


Conclusion

This tutorial is a guide to using Skaffold for Java and Go. You also learn to configure the Gitlab registry and pipeline and then integrate it with Skaffold. This is a basic understanding to use Skaffold. In the real world, helm or Kustomize can be used instead of kubectl to support more complex use cases. Moreover, the end state of the pipeline could be deployed to the cluster instead of releasing a YAML file.

Happy building software!!