Orchestrating Kubernetes Deployments via GitLab CI kubectl Integration

The integration of GitLab CI/CD with Kubernetes via kubectl represents a critical evolution in modern DevOps, shifting the paradigm from manual cluster management to a fully automated, declarative state of deployment. At its core, this synergy allows developers to treat their infrastructure as code, ensuring that the transition from a commit in a repository to a running pod in a cluster is seamless, repeatable, and verifiable. By utilizing the .gitlab-ci.yml configuration file, teams can define a rigorous pipeline that encompasses building source code, executing unit tests, containerizing the application, and finally, instructing the Kubernetes API to update the desired state of the cluster. This process eliminates the "it works on my machine" syndrome by utilizing standardized Docker images for the build environment and leveraging the GitLab Agent for Kubernetes to establish a secure communication channel between the CI runner and the cluster's control plane.

Architectural Foundation of GitLab Kubernetes Integration

To establish a functional link between GitLab and a Kubernetes cluster, a series of configuration steps must be executed to ensure that the GitLab master and the runners can communicate with the Kubernetes API server.

One primary requirement involves the configuration of outbound requests within the GitLab admin section. Specifically, navigating to Settings -> Network -> Outbound requests and enabling the option to "Allow requests to the local network from web hooks and services" is mandatory. This configuration is essential because it permits internal communication between the GitLab master and the Kubernetes API, as well as between the GitLab CI runner and the GitLab master. Without this permission, the network security policies of the GitLab instance would block the necessary handshakes required to trigger deployments.

When connecting an existing cluster, the API URL must be precisely defined. In internal setups, this is often specified as http://kubernetes.default:443. This internal address ensures that the traffic remains within the cluster network, reducing latency and increasing security. Furthermore, the cluster CA certificate must be provided to ensure that the communication is encrypted and that the identity of the API server is verified, preventing man-in-the-middle attacks.

GitLab Runner Deployment and Verification

Once a Kubernetes cluster is added via the GitLab UI, the system provides a streamlined method for deploying the necessary execution environment.

In the Applications tab of the Kubernetes cluster settings, the GitLab runner can be installed directly. This runner is automatically deployed into a specific namespace designated as gitlab-managed-apps. The use of a dedicated namespace ensures that the CI/CD infrastructure is isolated from the actual application workloads, preventing resource contention and simplifying permission management.

To verify the successful deployment of the runner, the following command is utilized:

bash kubectl get pod -n gitlab-managed-apps

A successful output will show a pod, such as runner-gitlab-runner-5649dbf49-5mnjv, in the Running status. Once the pod is active, the final verification step occurs in the Overview -> Runners section of the GitLab UI. The presence of an IP address and a version number confirms that the runner has successfully established a heartbeat with the GitLab master. If these details are missing, the primary troubleshooting step is to inspect the pod logs to identify network timeouts or authentication failures.

Constructing the .gitlab-ci.yml Pipeline

The .gitlab-ci.yml file serves as the blueprint for the entire automation process. It defines the stages, the environment, and the scripts required to move code from a repository to a production cluster.

The Build and Test Cycle

A robust pipeline typically begins with a build stage. For Java-based applications, the Maven Docker image is commonly used. The initial step is the compilation of source code:

bash mvn compile

Following compilation, the pipeline must validate the code through automated testing. This is achieved by running JUnit tests:

bash mvn test

If the tests fail, the pipeline halts, preventing unstable code from reaching the cluster. Upon successful testing, the application is containerized. Using the Jib Maven plugin is an efficient choice here, as it allows for the creation of Docker images in a docker-less mode, meaning the build environment does not need a Docker daemon installed to produce a valid image.

Container Registry Management

GitLab provides a built-in Docker Registry that stores the images produced by the pipeline. For a Node.js application, the process involves tagging the image with the registry path and pushing it using the CI_JOB_TOKEN for authentication.

The sequence of commands for tagging and pushing is as follows:

bash docker tag my-app:latest registry.gitlab.com/your-username/your-project-name:my-app:latest docker login -u gitlab-ci-token -p $CI_JOB_TOKEN registry.gitlab.com docker push registry.gitlab.com/your-username/your-project-name:my-app:latest

This ensures that the Kubernetes cluster can pull the specific version of the image required for the deployment.

Advanced kubectl Execution and Context Management

Executing kubectl commands within a CI/CD pipeline requires a valid configuration and a correctly set context to ensure the commands are targeted at the right cluster and namespace.

Solving the Localhost Connection Error

A common failure point in GitLab CI/CD is the error message stating that the connection to the server localhost:8080 was refused. This occurs when kubectl cannot find a valid configuration or context, causing it to fall back to a default local setting.

To resolve this, the KUBE_CONTEXT must be explicitly exported and the current context must be set using the kubeconfig. The following configuration pattern is required:

bash export KUBE_CONTEXT="<user>/<repo>:<agent-name>" kubectl config --kubeconfig=${KUBE_CONFIG} use-context <user>/<repo>:<agent-name>

This ensures that the kubectl binary knows exactly which agent and cluster it is communicating with, bypassing the default local lookup.

Deploying to Production

The deploy stage in the .gitlab-ci.yml file typically uses a specialized image such as bitnami/kubectl:latest. To ensure the deployment only happens on the master branch and requires a manual trigger for safety, the following configuration is used:

yaml deploy: stage: deploy environment: name: production image: name: bitnami/kubectl:latest entrypoint: [""] when: manual only: - master script: - cat ${KUBECONFIG} - kubectl version - kubectl cluster-info - kubectl apply -f kubernetes/deployment.yaml - kubectl apply -f kubernetes/service.yaml

This configuration ensures that the production environment is protected by a manual gate, while the kubectl apply commands ensure that the deployment and service manifests are synchronized with the cluster state.

Secret Management and Registry Authentication

For a Kubernetes cluster to pull images from a private GitLab registry, it must possess the correct credentials. This is handled by creating a docker-registry secret within the cluster.

Deploy Token Configuration

A deploy token must be created with the read_registry scope. This token and its associated username are stored as CI/CD variables:

  • CONTAINER_REGISTRY_ACCESS_TOKEN
  • CONTAINER_REGISTRY_ACCESS_USERNAME

Automating Secret Creation

The pipeline can automate the creation and deletion of these secrets to maintain security and cleanliness. Using an image like portainer/kubectl-shell:latest, the following sequence is implemented in the .gitlab-ci.yml:

```yaml
create-registry-secret:
stage: setup
image: "portainer/kubectl-shell:latest"
variables:
AGENTKUBECONTEXT: my-group/optional-subgroup/my-repository:testing
before
script:
- kubectl config use-context $AGENTKUBECONTEXT
script:
- kubectl delete secret gitlab-registry-auth -n flux-system --ignore-not-found
- kubectl create secret docker-registry gitlab-registry-auth -n flux-system --docker-password="${CONTAINER
REGISTRYACCESSTOKEN}" --docker-username="${CONTAINERREGISTRYACCESSUSERNAME}" --docker-server="${CIREGISTRY}"
environment:
name: container-registry-secret
on_stop: delete-registry-secret

delete-registry-secret:
stage: stop
image: ""
variables:
AGENTKUBECONTEXT: my-group/optional-subgroup/my-repository:testing
before
script:
- kubectl config use-context $AGENT_KUBECONTEXT
script:
- kubectl delete secret -n flux-system gitlab-registry-auth
```

This approach ensures that the gitlab-registry-auth secret exists in the flux-system namespace during the deployment window and is removed when the environment is stopped, adhering to the principle of least privilege.

Integration Comparison: Traditional CI vs. Agent-based Deployment

The method of interaction between GitLab and Kubernetes has evolved. Below is a comparison of the traditional Kubeconfig method versus the GitLab Agent for Kubernetes.

Feature Traditional Kubeconfig GitLab Agent for Kubernetes
Connection Type Push-based (GitLab to K8s) Pull-based/Hybrid
Security Requires exposing API server Agent resides inside the cluster
Context Setup Manual KUBECONFIG variable AGENT_KUBECONTEXT variable
Network Req. Inbound access to API Outbound access to GitLab
Secret Mgmt Manual Secret Injection Integrated with GitLab CI/CD

Technical Implementation for Node.js Microservices

For developers implementing a simple Node.js application, the codebase must be structured to support containerization. A basic app.js utilizing Express.js is a standard starting point:

```javascript
const express = require('express');
const app = express();
const port = 8080;

app.get('/', (req, res) => {
res.send('Hello, World!');
});

app.listen(port, () => {
console.log(App running on http://localhost:${port});
});
```

This is supported by a package.json that defines the dependencies and test scripts:

json { "name": "my-app", "version": "1.0.0", "description": "A simple Node.js app", "main": "app.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "dependencies": { "express": "^4.17.1" } }

The CI/CD pipeline then takes this code, runs npm test within a container, and applies the deployment.yaml and service.yaml files to the cluster using kubectl.

Comprehensive Troubleshooting and Log Analysis

When a pipeline fails during the kubectl execution phase, a systematic approach to troubleshooting is required.

  1. Pod Status Verification: The first step is always to check the status of the runner. If the runner is not Running in the gitlab-managed-apps namespace, the pipeline will fail before any kubectl commands are even attempted.
  2. Context Validation: If the error connection to the server localhost:8080 was refused appears, the failure is not in the cluster, but in the CI runner's configuration. The KUBE_CONTEXT must be checked for typos and the kubectl config use-context command must be verified.
  3. Registry Permissions: If the pod starts but enters an ImagePullBackOff state, the issue lies in the registry secret. One must verify that the CONTAINER_REGISTRY_ACCESS_TOKEN has the read_registry scope and that the secret was created in the correct namespace (e.g., flux-system).
  4. Network Outbound Settings: If the runner cannot communicate with the master, the admin setting "Allow requests to the local network from web hooks and services" must be re-verified.

Analysis of Deployment Strategies

The integration of kubectl within GitLab CI/CD allows for various deployment strategies that enhance reliability. By using a manual trigger (when: manual), organizations can implement a "human-in-the-loop" verification process, where a lead engineer reviews the test results before promoting the build to production.

Furthermore, the use of the GitLab Agent allows for a hybrid approach where Flux can be used for GitOps-style synchronization, while kubectl is used for imperative tasks like creating secrets or triggering immediate rollouts. This combination provides the best of both worlds: the stability of declarative state management via Flux and the flexibility of imperative control via GitLab CI/CD.

The transition from a monolith to microservices, as evidenced in Django and DRF migrations, heavily relies on this pipeline. By separating the deployment of each microservice into its own pipeline or job, teams can update individual components of the system without risking the stability of the entire application, provided that the kubectl apply commands are scoped to the correct namespaces and resources.

Sources

  1. Piotr Minkowski
  2. GitLab Forum
  3. GitLab Documentation
  4. Ahmad W Khan Blog
  5. Dev.to - Arby the Coder

Related Posts