Overview
GitLab has been recognized as a Leader in the Gartner Magic Quadrant for DevOps Platforms for three consecutive years (2023‑2025), a status confirmed by Gartner that validates its integrated approach to DevSecOps. This article guides you through building a production‑grade CI/CD pipeline for Kubernetes using GitLab CI, leveraging its built‑in integration with Kubernetes clusters. By following this guide, you will learn to automate builds, tests, security scans, and deployments to Kubernetes.
Although the benefits of a robust platform are clear, the evaluation process often involves hidden costs. According to Tech Insider, enterprise teams waste 8 to 12 weeks assessing various CI/CD platforms. This lengthy evaluation period can delay project timelines and divert engineering focus from delivering value. By selecting a platform with proven leadership, such as GitLab, organizations can streamline their evaluation process and accelerate their move to production, thereby reducing waste.
Requirements
Before you begin, ensure you have the following:
- A GitLab account (GitLab.com or self‑managed GitLab instance with CI enabled).
- A Kubernetes cluster (e.g., GKE, EKS, AKS, or a local cluster for testing).
- GitLab Runner installed and registered with your GitLab project, configured to use a Docker executor or Kubernetes executor.
- Basic knowledge of Docker, Kubernetes, and Git.
- Tools:
kubectlconfigured to access your cluster,helm(optional), and Git. - Version requirements: GitLab 15.0 or later (for environment management features), Kubernetes 1.21+.
If you are using a self‑managed GitLab instance, ensure your runners have network access to your Kubernetes cluster.
Step 1: Prepare Your GitLab Repository
A well-structured repository is the prerequisite for efficient GitLab CI/CD. Define your branching strategy early, such as Trunk‑Based Development or Git Flow, to align automation triggers with your release cadence. Initialize your .gitlab-ci.yml at the repository root to immediately establish your pipeline definition and signal CI readiness to the team.
Leverage GitLab’s CI Lint tool to validate your YAML pipeline syntax before committing, preventing common parsing errors that break builds across branches. Integrate mandatory merge request approvals and pipeline status checks to enforce code review discipline directly within the merge request widget. This structured configuration minimizes friction in collaborative workflows.
Standardize your runtime by committing a Dockerfile or explicit dependency manifest to the repository. Defining the exact build context allows GitLab runners to execute fully reproducible builds regardless of the underlying runner environment. Preparing these configuration artifacts upfront drastically reduces debugging time during pipeline execution and ensures consistency across all stages of development.
Step 2: Configure the .gitlab-ci.yml File
Begin by defining your pipeline stages and jobs within the .gitlab-ci.yml file. This configuration dictates the execution flow and automated checks for your repository.
A critical best practice is to always pin job image versions in your configuration. This ensures pipeline reproducibility and safety by avoiding unexpected changes from version updates. Adapting this from common advice in CI systems, pinning versions maintains consistent build environments across runs.
Structure your jobs with explicit image tags that reference specific version numbers rather than latest. This approach minimizes debugging time and enforces stable pipeline conditions for your team.
Additionally, leverage GitLab’s predefined variables and CI/CD templates to streamline your initial setup. These tools help maintain consistency across projects without redundant configuration efforts.
Step 3: Define the Build and Test Stages
Separate your build logic from your test execution to maintain pipeline clarity and efficiency. Output compiled artifacts using the artifacts keyword, restricting them to a specific expiration time to conserve storage. Explicitly defining artifact paths prevents bloated job dependencies and ensures test stages only receive necessary compiled code.
Optimize the test stage by parallelizing workloads across multiple jobs. Use the parallel attribute to partition your test suite, and integrate JUnit report generation via reports:junit to surface results directly in merge requests. This immediate feedback loop keeps developers firmly in their flow without context switching to the pipeline log.
Implement caching for your project dependencies using a cache:key that references your lock file. This standard practice dramatically reduces pipeline execution time on subsequent commits. Combine this with a strict needs declaration to ensure test jobs only begin after the build stage completes, establishing a predictable and resource-optimized execution graph across your entire pipeline definition.
Step 4: Integrate GitLab’s Built‑in Security Scanners
GitLab provides built‑in DevSecOps capabilities through its security scanning templates. To integrate them into your pipeline, include the appropriate template and configure scanners for your technology stack.
Add the following to your .gitlab-ci.yml after your build and test stages:
include:
- template: Jobs/SAST.gitlab-ci.yml
- template: Jobs/Dependency-Scanning.gitlab-ci.yml
- template: Jobs/Container-Scanning.gitlab-ci.yml
stages:
- build
- test
- scan
- deploy
# Your existing build and test jobs...
# Security scanning jobs will run in the 'scan' stage
# Configure them as needed (e.g., SAST_EXCLUDED_PATHS, DS_DEFAULT_BRANCH_IMAGE)
GitLab’s SAST scanner analyzes source code for vulnerabilities, Dependency Scanning checks for vulnerabilities in project dependencies, and Container Scanning examines container images for known vulnerabilities. These scanners generate reports that are automatically displayed in merge request widgets and pipeline views.
Customize scanning by setting variables. For example, to include only critical severities in the merge request:
variables:
SAST_EXCLUDED_ANALYZERS: "eslint,secrets"
DS_DEFAULT_BRANCH_IMAGE: "registry.gitlab.com/your-project/your-image:latest"
By integrating these scanners directly into your pipeline, you ensure that every commit is automatically checked for vulnerabilities before deployment, shifting security left and meeting DevSecOps best practices.
Step 5: Secure Secrets Injection in Pipeline Integrations
Integrating pipelines with external services demands rigorous control over access credentials. GitLab provides several mechanisms to handle secrets securely:
- Masked and Protected CI/CD Variables: Store sensitive values such as API keys and passwords as CI/CD variables in GitLab. Mark them as masked to prevent them from appearing in job logs, and protect them to restrict usage to protected branches and tags.
- OIDC for Kubernetes: Use GitLab’s OpenID Connect (OIDC) to authenticate with your Kubernetes cluster without storing static service account tokens. Configure a role binding in your cluster and use the
id_tokensfield in your pipeline to generate short‑lived tokens. - HashiCorp Vault Integration: If you use Vault, GitLab can retrieve secrets at runtime using the Vault authenticator. Configure a Vault role and policy, and use the
vaultkeyword in your job.
Example of using OIDC for Kubernetes authentication in a deployment job:
deploy:
stage: deploy
id_tokens:
K8S_TOKEN:
aud: "k8s-cluster.example.com"
script:
- kubectl config set-cluster mycluster --server=https://k8s-api.example.com
- kubectl config set-credentials gitlab-ci --token $K8S_TOKEN
- kubectl config set-context mycontext --cluster=mycluster --user=gitlab-ci
- kubectl config use-context mycontext
- kubectl apply -f k8s/deployment.yaml
This eliminates the need to store static Kubernetes credentials in GitLab variables, reducing the attack surface. Avoid hardcoding secrets in your repository; always use GitLab’s secure variable storage or external identity providers.
Step 6: Deploy to Kubernetes
GitLab CI offers native Kubernetes integration through the Kubernetes cluster integration feature and environment management. You can deploy using kubectl or Helm.
First, ensure your Kubernetes cluster is integrated with GitLab. In your project, navigate to Settings > CI/CD > Kubernetes clusters and connect your cluster. This sets up the necessary Kubernetes namespace and service account.
For a basic kubectl deployment, add a deploy job to your pipeline:
deploy-staging:
stage: deploy
environment:
name: staging
url: https://staging.example.com
script:
- kubectl apply -f k8s/namespace.yaml
- kubectl apply -f k8s/deployment.yaml -f k8s/service.yaml
only:
- develop
For Helm‑based deployments, use the helm command:
deploy-production:
stage: deploy
environment:
name: production
url: https://app.example.com
script:
- helm upgrade --install my-app ./helm-chart --namespace=production
only:
- main
To verify the rollout, add a job that checks the status:
verify-deployment:
stage: verify
script:
- kubectl rollout status deployment/my-app --namespace=staging --timeout=5m
needs: ["deploy-staging"]
Use GitLab environments to track deployments, view live pods, and roll back if necessary. Environment‑specific variables can be defined in GitLab CI/CD settings per environment.
Step 7: Run the Pipeline and Verify
Trigger your pipeline by pushing code to the repository. GitLab automatically runs the pipeline defined in your .gitlab-ci.yml. Navigate to CI/CD > Pipelines in your GitLab project to monitor the execution of every stage and job in real time.
Deep‑dive into each job’s logs to confirm successful execution. Verify that the build compiles without errors, tests pass, security scans complete without critical findings, and deployment succeeds. In the job log, you can see the applied Kubernetes resources and the rollout status. Use the environment page (Operations > Environments) to see deployed application versions and access the application URL.
Check that the application is accessible by curling the endpoint or opening the URL in a browser. For Kubernetes deployments, verify the pods are running:
kubectl get pods -n staging
If you have configured GitLab environments with a health check URL, GitLab can automatically monitor the deployment status. After a successful pipeline, you should see a green checkmark on the merge request indicating all stages passed. This immediate feedback loop allows developers to ship code with confidence.
Expected Outcome
By following the steps outlined above, your team will establish a production‑grade CI/CD pipeline that automatically:
- Builds a Docker image of your application and pushes it to GitLab’s Container Registry.
- Executes unit and integration tests with immediate pass/fail feedback in merge requests.
- Performs static analysis (SAST), dependency scanning, and container scanning, with results displayed in the merge request widget.
- Deploys the application to a Kubernetes cluster using rolling updates, with environment‑specific configurations (e.g., staging, production).
- Verifies deployment success by checking rollout status and application health.
GitLab Environments will track every deployment, providing a history of releases and allowing easy rollback. The pipeline becomes a single source of truth for all deployments, enhancing visibility and collaboration across teams. Security scans are integrated as a mandatory gate, ensuring vulnerabilities are caught before reaching production.
Ultimately, this structured approach builds a scalable DevSecOps foundation on Kubernetes, empowering teams to release high‑quality software with confidence and accelerating time to market.
Troubleshooting
YAML configuration errors are often the first failure point in your pipeline. Use GitLab’s CI Lint tool (accessible via the project page or API) to validate your .gitlab-ci.yml before pushing, preventing unnecessary runner time and developer delays.
Runner environment inconsistencies can arise from unpinned image tags. Ensure every job references a specific Docker image tag rather than latest to guarantee consistency across runs. Cache misses, evident in prolonged restore times, usually indicate a stale lock file or a mismatched cache key in your pipeline configuration.
Secret injection failures manifest as authentication errors. Verify that the variable name in your pipeline matches exactly what the application expects, and that the variable is properly configured as a masked or protected variable in GitLab. Security scan failures from tools like SAST or Container Scanning require inspecting the specific artifact path or report details in the log.
Silent pipeline failures occur when job conditions or dependency chains mask skipped steps. A passing pipeline status that ignores a critical job regression can slip through. Use granular test reports and security scan thresholds as merge request checks to ensure the pass/fail status reflects the true health of the entire pipeline.
Common Mistakes
- Neglecting to pin runner image tags and dependency versions. This is considered an opinionated finding by the Source Intelligence Map: while correlated with build failures, it reflects expert guidance rather than a causal law. To ensure reproducibility, always use specific image tags and lock dependency versions.
- Hardcoded credentials in YAML configurations. The Source Intelligence Map treats this as a definitive, high‑certainty security risk. The recommended remediation, tagged as the highest‑confidence in the report, is adopting short‑lived identity federation via OIDC or workload identity pools. In GitLab, use CI/CD variables (masked and protected) or OIDC for Kubernetes to eliminate static secrets.
- Masking pass/fail status. The map captures cases where pass/fail masking delayed regression detection. The corrective action is embedding granular test outcomes, security scan thresholds, and performance benchmarks directly into the merge request check widget. In GitLab, leverage merge request integration for linting, test reports, and security scanner reports to ensure the pipeline status reflects true health.
Frequently Asked Questions
How can I define multiple deployment environments in GitLab CI?
Use the environment keyword in your deploy jobs to define environments such as staging and production. GitLab automatically tracks deployments per environment and provides a timeline. Environment‑specific variables can be set in the GitLab UI (Settings > CI/CD > Variables) scoped to an environment, or by using the environment:variables keyword in your job.
How does GitLab CI integrate with a private Kubernetes cluster?
You can integrate a private Kubernetes cluster by adding it in your project’s settings (Operations > Kubernetes). GitLab will create a service account and namespace. Alternatively, you can use OIDC authentication with cluster‑specific role bindings to avoid storing static credentials. Ensure your GitLab Runner has network access to the cluster.
How can I use GitLab’s Container Registry in my CI/CD pipeline?
GitLab’s built‑in Container Registry is automatically tied to your project. In your pipeline, you can log in to the registry using the CI job token: docker login $CI_REGISTRY -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD. Then push images to $CI_REGISTRY_IMAGE. This image can be used in subsequent jobs for testing or deployment.
Tips
- Cache dependencies wisely: Use GitLab’s cache feature with a key based on your lock file to speed up dependency installation across pipeline runs. For example:
cache:key: $CI_COMMIT_SHAbut better to use the lock file path. - Parallelize test execution: Split your test suite across parallel jobs using the
parallelkeyword to reduce total pipeline duration. - Choose the right runner type: For Kubernetes deployments, using a Kubernetes executor can provide dynamic scaling and avoid network latency. For build jobs, consider using a Docker executor with caching enabled.
- Use Kaniko for building Docker images: Kaniko builds container images without requiring Docker‑in‑Docker, integrating seamlessly with GitLab’s shared runners and improving security.
- Implement health checks in your Kubernetes deployments: Define liveness and readiness probes in your pod specification to ensure your application is truly healthy before receiving traffic.
- Use rolling updates: Configure your Kubernetes Deployment with
strategy:rollingUpdateto ensure zero‑downtime deployments. You can control the maximum surge and unavailable pods. - Namespace management: Isolate environments (staging, production) into separate Kubernetes namespaces. Use GitLab CI environment scoping to manage variables per namespace.
Conclusion
Adopting a structured DevSecOps framework fundamentally transforms the development lifecycle. By methodically eliminating common pipeline anti‑patterns—such as unpinned dependencies and hardcoded secrets—you directly accelerate releases while fortifying your security posture. This approach ensures a smooth transition from requirements to deployment, creating a reliable, audit‑ready pipeline that serves as the single source of truth. Ultimately, embedding security as an integrated feature of the workflow empowers teams to ship high‑quality code with confidence and consistency across every release.