GitLab CI/CD: Architecting, Deploying, and Optimizing Pipelines

Continuous Deployment with GitLab

Job Replace Placeholders Tokens

In this guide, you’ll learn how to parameterize Kubernetes YAML manifests and dynamically inject environment-specific values during CI/CD. By leveraging envsubst, you can maintain a single set of manifests for development, staging, and production.

Prerequisites

Ensure you have the following installed locally:

node --version   # v8.11.3 (or higher)
npm --version    # 6.1.0 (or higher)

1. Manifest Files with Placeholders

Your repository structure should contain a single kubernetes/manifest/ folder with:

  • deployment.yaml
  • ingress.yaml
  • service.yaml

Each file uses Bash-style placeholders (${VAR}) to be substituted in CI/CD.

kubernetes/manifest/deployment.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: solar-system
  labels:
    app: solar-system
  namespace: ${NAMESPACE}
spec:
  replicas: ${REPLICAS}
  selector:
    matchLabels:
      app: solar-system
  template:
    metadata:
      labels:
        app: solar-system
    spec:
      containers:
        - name: solar-system
          image: ${K8S_IMAGE}
          imagePullPolicy: Always
          ports:
            - containerPort: 3000
              name: http
              protocol: TCP
      envFrom:
        - secretRef:
            name: mongo-db-creds

kubernetes/manifest/ingress.yaml

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: solar-system
  namespace: ${NAMESPACE}
  annotations:
    kubernetes.io/ingress.class: nginx
    kubernetes.io/tls-acme: "true"
spec:
  rules:
    - host: solar-system.${NAMESPACE}.${INGRESS_IP}.nip.io
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: solar-system
                port:
                  number: 3000
  tls:
    - hosts:
        - solar-system.${NAMESPACE}.${INGRESS_IP}.nip.io
      secretName: ingress-local-tls

kubernetes/manifest/service.yaml

apiVersion: v1
kind: Service
metadata:
  name: solar-system
  labels:
    app: solar-system
  namespace: ${NAMESPACE}
spec:
  type: NodePort
  ports:
    - port: 3000
      targetPort: 3000
      protocol: TCP
  selector:
    app: solar-system

2. Placeholder Reference Table

PlaceholderDescriptionExample
NAMESPACEKubernetes namespace for deploymentdevelopment
REPLICASNumber of pod replicas2
K8S_IMAGEDocker image referencesiddharth67/solar-system:123
INGRESS_IPExternal IP of Ingress controller139.84.208.48

3. Defining CI/CD Environment Variables

Configure global variables in your .gitlab-ci.yml under variables: or via the [GitLab UI][GitLab CI/CD Variables].

variables:
  DOCKER_USERNAME: siddharth67
  IMAGE_VERSION: $CI_PIPELINE_ID
  K8S_IMAGE: $DOCKER_USERNAME/solar-system:$IMAGE_VERSION
  MONGO_URI: 'mongodb+srv://supercluster.d83j5.mongodb.net/superData'
  MONGO_USERNAME: superuser
  MONGO_PASSWORD: $M_DB_PASSWORD

Add non-masked variables (NAMESPACE, REPLICAS) in CI/CD → Variables:

The image shows a GitLab CI/CD settings page with a list of environment variables, including keys like `DEV_KUBE_CONFIG`, `DOCKER_PASSWORD`, and `REPLICAS`, all masked for security.

Warning

Never commit secrets (e.g., database credentials) directly in your YAML files. Always use masked or protected CI/CD variables.

4. Retrieving the Ingress Controller IP

At runtime, fetch the external IP of your Ingress controller:

kubectl -n ingress-nginx get service ingress-nginx-controller \
  -o jsonpath="{.status.loadBalancer.ingress[0].ip}"

Note

You can learn more about the Ingress resource in the Kubernetes Ingress documentation.

5. CI Job: dev-deploy

The dev-deploy job below installs kubectl and envsubst (from GNU gettext), exports INGRESS_IP, and replaces placeholders in your manifests.

k8s_dev_deploy:
  stage: dev-deploy
  image:
    name: alpine:3.7
  before_script:
    # Install kubectl
    - wget -qO kubectl \
        https://storage.googleapis.com/kubernetes-release/release/\
$(wget -qO - https://storage.googleapis.com/kubernetes-release/release/stable.txt)/bin/linux/amd64/kubectl
    - chmod +x kubectl && mv kubectl /usr/bin/kubectl

    # Install envsubst
    - apk add --no-cache gettext
    - envsubst -V

  script:
    # Configure kubectl
    - export KUBECONFIG=$DEV_KUBE_CONFIG
    - kubectl version --client -o yaml
    - kubectl config get-contexts
    - kubectl get nodes

    # Get Ingress IP
    - export INGRESS_IP=$(
        kubectl -n ingress-nginx get service ingress-nginx-controller \
          -o jsonpath="{.status.loadBalancer.ingress[0].ip}"
      )
    - echo "Ingress IP: $INGRESS_IP"

    # Substitute placeholders in manifests
    - for file in kubernetes/manifest/*.yaml; do
        echo "Processing $file"
        envsubst < "$file" | kubectl apply -f -
      done

Commit and push to trigger the pipeline. You’ll see a successful dev-deploy stage:

The image shows a GitLab pipeline interface for a NodeJS project named "Solar System," indicating a successful pipeline run with a job labeled "dev-deploy."

6. Verifying the Logs

Key sections in the job logs:

$ apk add --no-cache gettext
(1/1) Installing gettext (0.19.8.1-r0)
...
$ envsubst -V
envsubst (GNU gettext-runtime) 0.19.8.1

$ export INGRESS_IP=139.84.208.48
$ echo $INGRESS_IP
139.84.208.48

# deployment.yaml after substitution:
namespace: development
replicas: 2
image: siddharth67/solar-system:123

# ingress.yaml after substitution:
host: solar-system.development.139.84.208.48.nip.io

# service.yaml after substitution:
namespace: development

Here you can confirm all four placeholders (NAMESPACE, REPLICAS, K8S_IMAGE, INGRESS_IP) have been correctly injected.


You’re now ready to apply templated manifests across multiple environments, ensuring consistency and reusability.

References

Watch Video

Watch video content

Previous
Job Configuring Kubeconfig file