Using Skaffold for CI/CD with GitLab
Contributed by: @tvvignesh
Skaffold is a tool which is non-opinionated about the CI/CD tool you should use and acts as a platform agnostic solution for both development and deploying to production.
To facilitate building and deploying images in CI/CD, skaffold offers docker images which you can build on top of. If that doesn’t work for your use-case, you can make your own Dockerfile and pull in skaffold and its dependencies using curl.
Let us have a look at how we can use Skaffold with GitLab CI.
Step 1: Getting the project and Dockerfile ready
The first step is to obviously have your project which you want to deploy ready with a Dockerfile setup for the same.
Skaffold supports multiple builders like Docker, Kaniko, Bazel and more and if the builder you are looking for is not supported out of the box, you can script it out as well. We will use Docker in this demonstration.
For instance, if you would like to deploy nginx, your Dockerfile would look something like this:
FROM nginxinc/nginx-unprivileged:1.19.3-alpine
WORKDIR /app/server
COPY ./web ./web
EXPOSE 8080
CMD [ "/bin/bash", "-c", "nginx -g 'daemon off;'" ]
Skaffold can use your dockerfile to automatically build, tag and push images to the registry when needed.
Step 2: Choosing the deployment method
Skaffold supports deployment through kubectl, helm and a lot of other mechanisms. You can have a look at the complete list of deployers available here.
If you choose to deploy through kubectl, you must have all your yaml files ready, or if you would like to deploy through helm charts, you must have all your charts ready.
Step 3: Choosing the image and tag strategy
Skaffold automatically tags images based on different strategies as documented here: https://skaffold.dev/docs/taggers/. The tagging strategy used is configurable, so choose the mechanism which is right for you. By default, skaffold uses the git sha tagger.
If you are using helm as your deployer, you might want to use the helm image strategy if you would like to follow helm specific conventions, as skaffold will pass in all the details for you.
Step 4: Plugins (optional)
If you want to do some kind of processing before deployment like decrypting the secrets in the CI/CD pipeline, skaffold also supports plugins like Helm Secrets. So, if you would like to deploy secrets that have been encrypted using KMS or any other mechanism supported by SOPS you can actually set useHelmSecrets option to true and skaffold will handle everything automatically for you.
Step 5: The skaffold.yaml
file
Your skaffold.yaml
file will change depending on the way you would like to build and deploy your project. It is recommended that you set up skaffold and run it manually before you setup your CI pipeline since the CI pipeline can just call skaffold to do all the builds and deployments which can be tested out locally via skaffold.
A sample skaffold file can look something like this:
apiVersion: skaffold/v2beta8
kind: Config
profiles:
# Specify the profile name which you can run later using skaffold run -p <profilename>
- name: my-profile-name
build:
artifacts:
# Skaffold will use this as your image name and push it here after building
- image: asia.gcr.io/my-project/my-image
# We are using Docker as our builder here
docker:
# Pass the args we want to Docker during build
buildArgs:
NPM_REGISTRY: '{{.NPM_REGISTRY}}'
NPM_TOKEN: '{{.NPM_TOKEN}}'
deploy:
# Using Helm as the deployment strategy
helm:
# Pass the parameters according to https://skaffold.dev/docs/references/yaml/
releases:
- name: my-release
namespace: default
# Using Helm secrets plugin to process secrets before deploying
useHelmSecrets: true
# Location of the chart - here, we use a local chart
chartPath: ./charts/my-chart
# Path to the values file
valuesFiles:
- ./deployment/dev/values.yaml
- ./deployment/dev/secrets.yaml
skipBuildDependencies: true
flags:
upgrade:
- --install
Please have a look at the comments above to understand what the yaml does.
Rather, if you want to deploy via kubectl, your skaffold file can look something like this:
apiVersion: skaffold/v2beta8
kind: Config
profiles:
# Specify the profile name which you can run later using skaffold run -p <profilename>
- name: dev-svc
build:
artifacts:
# Skaffold will use this as your image name and push it here after building
- image: asia.gcr.io/my-project/my-image
# We are using Docker as our builder here
docker:
# Pass the args you want to Docker during build
buildArgs:
# Pass the args we want to Docker during build
NPM_REGISTRY: '{{.NPM_REGISTRY}}'
NPM_TOKEN: '{{.NPM_TOKEN}}'
deploy:
# In case we enable status check, this will be the timeout till which skaffold will wait
statusCheckDeadlineSeconds: 600
# Using kubectl as our deployer
kubectl:
# Location to our yaml files
# Refer https://skaffold.dev/docs/references/yaml/ for more options
manifests:
- k8/dev/*.yml
Step 6: Development & Testing locally
Before we move on to setting up the CI pipeline, we should test it out locally. Use skaffold run
if you are deploying it to the cluster, skaffold build
to just build and push the artifacts, skaffold dev
if you are developing (this will enable auto reload on changes) or debug using skaffold debug
You can find docs about all these workflows here
Also, note that you can also set up file synchronization to enable faster development workflows. You can read more about that here.
Enabling file synchronization would avoid the need to rebuild the images repeatedly during development and testing and this can accelerate the inner dev loop.
Step 7: Setting up authentication with the registry
As you may already know, there are a lot of places where you can host images namely Docker Hub, GCR, GitLab Registry, Quay and so on.
The way you authenticate is specific to the registry provider of your choice. For eg. you may choose to do a username-password
authentication with DockerHub or authenticate using service accounts in GCR
and so on.
NOTE: Username-Password authentication is not recommended for CI/CD pipelines since they can be easily compromised and can sometimes be logged as well.
We will be using GCR in our example. You can refer this page to understand how to authenticate with it.
Step 8: Using DIND Service (Optional)
Docker images are typically not cached if you are running your builds/pipeline within Docker containers(i.e. DIND or Docker-in-Docker) and this can be very slow since your CI runner has to download the images again and again for every build even if some layers are already available.
NOTE: If you are not looking to run your pipeline within Docker containers or are fine with slower pipelines/pipelines without caching, you can completely skip this step and proceed to Step 9.
To avoid this, we can set up a DIND service in our cluster to enable caching and storage of image layers for us. A sample dind deployment file as deployed in Kubernetes can look something like this:
# Setup a DIND deployment within the Kubernetes cluster to cache images as we build images in our pipelines
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-dind
spec:
selector:
matchLabels:
app: my-dind
strategy:
type: Recreate
template:
metadata:
labels:
app: my-dind
spec:
containers:
- image: docker:dind
imagePullPolicy: Always
name: my-dind
ports:
- containerPort: 2375
name: my-dind
env:
- name: DOCKER_HOST
value: tcp://localhost:2375
- name: DOCKER_TLS_CERTDIR
value: ''
securityContext:
privileged: true
volumeMounts:
- name: my-dind-storage
mountPath: /var/lib/docker
resources:
limits:
memory: '2Gi'
cpu: '1000m'
requests:
memory: '1Gi'
cpu: '250m'
volumes:
- name: my-dind-storage
persistentVolumeClaim:
claimName: my-dind-pv-claim
Service File:
# Expose DIND as a service so that all the pipelines can access it
apiVersion: v1
kind: Service
metadata:
name: my-dind-svc
spec:
selector:
app: my-dind
type: LoadBalancer
ports:
- port: 2375
targetPort: 2375
protocol: TCP
name: http
PV Claim:
# Provision a Persistent Volume Claim to be used to store the cached artifacts by DIND
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: my-dind-pv-claim
labels:
app: my-dind
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 200Gi
storageClassName: standard
And we would need to pass the URL to the DIND service when we do builds in the pipeline and all the caching will happen in the DIND storage speeding up the pipeline a lot. Make sure that you clean up the DIND storage periodically or setup cleanup scripts for the same else it might fill up soon.
Step 9: Getting the CI pipeline setup
Now that we have Skaffold ready and working locally, we look at the CI pipeline.
Here is a sample pipeline using GitLab CI and if you are new to it, you can refer the docs here. Also, you might want to look at how to define GitLab CI/CD environment variables here
This pipeline might look complicated at first glance, but most of it related to configuring access for deploying to GCP and configure GitLab’s Docker-in-Docker support to cache artifacts between builds. What is important to note is that we use Skaffold to do all the builds and deployments.
stages:
# The name of the pipeline stage
- my-stage
# The name of the job
my-job:
# The image to be used as the base for this job
image:
name: gcr.io/k8s-skaffold/skaffold:v1.15.0
# Pipeline tags (if you have specified tags to your GitLab Runner)
tags:
- development
stage: my-stage
retry: 2
script:
# Logging in to our gcp account using the service account key (specified in GITLAB variables)
- echo "$GCP_DEV_SERVICE_KEY" > gcloud-service-key.json
- gcloud auth activate-service-account --key-file gcloud-service-key.json
# Specifying the project, zone, cluster to deploy our application in.
- gcloud config set project $PROJECT_DEV_NAME
- gcloud config set compute/zone $PROJECT_DEV_REGION
- gcloud container clusters get-credentials $CLUSTER_DEV_NAME
- kubectl config get-contexts
# Pass in all the environment variables to be passed to skaffold during build and deployment with all the args as documented in https://skaffold.dev/docs/references/cli/ (in our case our cluster context, env vars, namespace and some labels)
- DOCKER_HOST="$DIND_DEV" NPM_REGISTRY="$NPM_REGISTRY_DEV" NPM_TOKEN="$NPM_TOKEN_DEV" skaffold run --kube-context $CLUSTER_DEV_CONTEXT -n default -l skaffold.dev/run-id=deploydep -p dev-svc --status-check
only:
# Run this only for changes in the main branch
refs:
- main
So, ultimately our GitLab CI pipeline does not know anything about the helm charts or build strategy or any deployment methods and relies completely on skaffold to do the job for us.
This works equally well irrespective of whether you choose the Docker Executor or the Kubernetes Executor and would require very little changes between the two.
While GitLab supports many executors (including Docker, Kubernetes, etc.) as documented here, we are working with Kubernetes executor here since it is vendor-agnostic, has the ability to auto-scale up/down, private (if you deploy the Runner in your Kubernetes cluster) and also is well supported for the forseeable future.
NOTE: If you are running Skaffold using the Kubernetes executor, make sure that you are running the runner with appropriate permissions or pod security policies. If you are running using DIND, it requires access to the Docker socket and being in privileged mode which might actually tend to be insecure. An alternative can be to use kaniko, buildkit or other custom builders like Buildah for building the image.
Step 10: That’s all folks
If all went well so far, you can try pushing a commit and see your pipeline running, picking up the changes, building and pushing the image and also deploying the same.
Hope this post was informative. Good luck with your journey with Skaffold.