Continuous Integration and Continuous Deployment (CI/CD) are essential practices in modern software development. They help teams deliver code changes more frequently and reliably. In this post, I’ll share how I set up a CI/CD pipeline using GitLab CI/CD and Kaniko to build and deploy my Go application to Kubernetes without requiring Docker on the build server.
What Is CI/CD?
Continuous Integration (CI) is the practice of automating the integration of code changes from multiple contributors into a single software project. It involves frequent merging of code changes into a central repository, followed by automated builds and tests.
Continuous Deployment (CD) takes this a step further by automating the deployment of code changes to production environments after they pass the necessary tests. Together, CI/CD pipelines help streamline the development process, reduce errors, and enable faster delivery of features and fixes.
Why Use Containers?
Containers are lightweight, portable units that bundle an application with all its dependencies, ensuring consistency across different environments. Using containers in CI/CD pipelines offers several benefits:
- Consistency: The application runs the same way in development, testing, and production environments.
- Isolation: Containers isolate applications from the host system and other containers.
- Scalability: Containers can be easily scaled horizontally to handle increased loads.
- Efficiency: They are lightweight and start quickly, improving resource utilization.
In a previous post, I explained how to create a Docker image for a Go application. With a Dockerfile ready, the next step is to automate the build and deployment process using CI/CD.
Setting Up GitLab CI/CD
GitLab CI/CD is an integrated tool that comes with GitLab, allowing you to automate the build, test, and deployment stages of your application. It uses a file called .gitlab-ci.yml
in the root of your repository to define the pipeline configuration.
Benefits of GitLab CI/CD
- Integrated with GitLab: Seamless integration with GitLab repositories.
- Automation: Automatically triggers pipelines on code commits or merge requests.
- Visibility: Provides a clear view of pipeline stages, logs, and artifacts.
- Customization: Highly customizable to fit various workflows and environments.
Initial CI/CD Pipeline Using Docker
Here’s how I initially set up my .gitlab-ci.yml
file:
stages:
- dockerize
- deploy
variables:
# Define Docker image name and tag
IMAGE_TAG: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
dockerize:
stage: dockerize
image: docker:24.0.5
services:
- docker:dind
script:
- docker build -t $IMAGE_TAG .
- docker tag $IMAGE_TAG $CI_REGISTRY_IMAGE:latest
- echo $CI_REGISTRY_PASSWORD | docker login -u $CI_REGISTRY_USER --password-stdin $CI_REGISTRY
- docker push $IMAGE_TAG
- docker push $CI_REGISTRY_IMAGE:latest
deploy:
stage: deploy
image:
name: bitnami/kubectl:1.31
entrypoint: [""]
script:
- mkdir -p ~/.kube
- echo "$KUBECONFIG_BASE64" | base64 -d > ~/.kube/config
- kubectl apply -f deployment/astring/deployment.yaml
- kubectl apply -f deployment/astring/service.yaml
- kubectl apply -f deployment/astring/ingress.yaml
- kubectl rollout restart deployment/astring-backend -n astring
Explanation of the Pipeline
Dockerize Stage
- Stage:
dockerize
- Image: Uses the Docker image
docker:24.0.5
. - Services:
docker:dind
(Docker-in-Docker) to enable Docker commands within the container. - Script:
- Builds the Docker image with the tag
$IMAGE_TAG
. - Tags the image as
latest
. - Logs into the GitLab Container Registry.
- Pushes both tags to the registry.
- Builds the Docker image with the tag
Deploy Stage
- Stage:
deploy
- Image: Uses
bitnami/kubectl:1.31
for Kubernetes commands. - Script:
- Decodes the base64-encoded Kubernetes config and sets up
~/.kube/config
. - Applies the Kubernetes deployment, service, and ingress manifests.
- Restarts the deployment to apply the new image.
- Decodes the base64-encoded Kubernetes config and sets up
Issues with This Approach
While this setup works, it has some drawbacks:
- Docker Dependency: It requires Docker to be installed on the GitLab Runner host. Using
docker:dind
can be resource-intensive and may not be supported in all environments. - Security Concerns: Exposing the Docker socket (
/var/run/docker.sock
) can be risky, as it provides elevated privileges within the container. - Incompatibility with Containerd: If your Kubernetes cluster uses Containerd (like K3s does) instead of Docker, building images with Docker-in-Docker isn’t straightforward.
- Resource Usage: Running Docker inside Docker can consume significant resources, slowing down your CI/CD pipeline.
Introducing Kaniko: Building Images Without Docker
To address these issues, I switched to Kaniko, an open-source tool designed to build container images from a Dockerfile, inside a container or Kubernetes cluster, without requiring a Docker daemon.
Benefits of Using Kaniko
- No Docker Daemon Required: Kaniko doesn’t depend on a Docker daemon, making it suitable for containerized environments and Kubernetes clusters.
- Security: Reduces the security risks associated with privileged containers and Docker socket exposure.
- Compatibility: Works seamlessly in environments using Containerd or other container runtimes.
- Caching: Supports caching of image layers to speed up the build process.
Updated CI/CD Pipeline with Kaniko
Here’s the updated .gitlab-ci.yml
using Kaniko:
stages:
- dockerize
- deploy
variables:
# Define Docker image name and tag
IMAGE_TAG: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
dockerize:
stage: dockerize
image:
name: gcr.io/kaniko-project/executor:v1.23.2-debug
entrypoint: [""]
script:
- echo "$CONFIG_YML" > config.yml
- echo "$PUBLIC_KEY" > publicKey.pem
- echo "$PRIVATE_KEY" > privateKey.pem
- /kaniko/executor \
--context "${CI_PROJECT_DIR}" \
--dockerfile "${CI_PROJECT_DIR}/Dockerfile" \
--destination "${CI_REGISTRY_IMAGE}:latest" \
--cache=true
deploy:
stage: deploy
image:
name: bitnami/kubectl:1.31
entrypoint: [""]
script:
- mkdir -p ~/.kube
- echo "$KUBECONFIG_BASE64" | base64 -d > ~/.kube/config
- kubectl apply -f deployment/astring/deployment.yaml
- kubectl apply -f deployment/astring/service.yaml
- kubectl apply -f deployment/astring/ingress.yaml
- kubectl rollout restart deployment/astring-backend -n astring
Explanation of the Updated Pipeline
Dockerize Stage with Kaniko
- Stage:
dockerize
- Image: Uses Kaniko executor image
gcr.io/kaniko-project/executor:v1.23.2-debug
. - Entrypoint: Set to
[""]
to override the default entrypoint. - Script:
- Writes necessary configuration and key files from environment variables.
- Runs the Kaniko executor:
-context
: Specifies the build context (the directory containing the Dockerfile).-dockerfile
: Path to the Dockerfile.-destination
: Registry location where the image will be pushed.-cache=true
: Enables caching to speed up subsequent builds.
Advantages of Using Kaniko
- No Docker Daemon: Kaniko builds images in userspace without requiring a Docker daemon.
- Security: Runs in a standard Kubernetes cluster without elevated privileges.
- Performance: Supports caching of intermediate layers, reducing build times for subsequent builds.
- Compatibility: Works well in environments using Containerd or other runtimes.
Understanding the CI/CD Pipeline Steps
1. Building and Pushing the Docker Image
The dockerize
stage handles building and pushing the Docker image to the GitLab Container Registry.
- Environment Variables:
CONFIG_YML
,PUBLIC_KEY
,PRIVATE_KEY
: These are environment variables stored in GitLab’s CI/CD settings, containing the content of configuration and key files needed for the build.
- Kaniko Executor:
- Kaniko reads the Dockerfile and builds the image layer by layer.
- It pushes the final image to the specified destination (
$CI_REGISTRY_IMAGE:latest
). - Caching speeds up builds by reusing layers that haven’t changed.
2. Deploying to Kubernetes
The deploy
stage applies the Kubernetes manifests to deploy the application.
- Kubeconfig Setup:
- The Kubernetes configuration file is base64-encoded and stored in the
KUBECONFIG_BASE64
environment variable. - It’s decoded and saved to
~/.kube/config
to authenticate with the Kubernetes cluster.
- The Kubernetes configuration file is base64-encoded and stored in the
- Applying Manifests:
kubectl apply
commands are used to create or update the deployment, service, and ingress resources.
- Rolling Restart:
kubectl rollout restart
triggers a rolling update of the deployment, ensuring the new image is pulled and the application is updated without downtime.
Conclusion
By integrating GitLab CI/CD with Kaniko, I streamlined the build and deployment process for my Go application without relying on a Docker daemon. This approach enhances security, improves compatibility with Containerd-based Kubernetes clusters, and leverages caching to speed up builds.
Key Takeaways
- CI/CD Automation: Automating the build and deployment process reduces errors and accelerates delivery.
- Kaniko Advantages: Kaniko allows building container images in environments without Docker, enhancing security and compatibility.
- GitLab CI/CD Flexibility: GitLab CI/CD pipelines are highly customizable to fit various workflows and can integrate with different tools.