GitLab CI

GitLab CI #

GitLab CI menawarkan beberapa keunggulan dibanding GitHub Actions untuk tim yang sudah di ekosistem GitLab: pipeline yang lebih fleksibel dengan include dan extends, environments dengan deployment tracking terintegrasi, dan kemampuan menjalankan pipeline yang sepenuhnya self-hosted tanpa bergantung pada infrastruktur eksternal. Artikel ini membahas pola integrasi Ansible yang komprehensif dengan GitLab CI.

Struktur Pipeline dengan Stages #

# .gitlab-ci.yml
stages:
  - validate       # Lint dan syntax check
  - test           # Unit test dan molecule
  - build          # Build Docker image
  - deploy-staging
  - verify-staging
  - deploy-production

variables:
  ANSIBLE_FORCE_COLOR: "true"
  ANSIBLE_HOST_KEY_CHECKING: "false"
  PY_COLORS: "1"
  IMAGE_TAG: "${CI_COMMIT_SHORT_SHA}"
  REGISTRY: "${CI_REGISTRY}"
  IMAGE_NAME: "${CI_REGISTRY_IMAGE}"

Template dengan extends dan before_script #

GitLab CI mendukung template yang bisa di-extend — menghindari duplikasi konfigurasi:

# Template untuk semua job Ansible
.ansible_base:
  image: python:3.11-slim
  before_script:
    - pip install ansible --quiet
    - ansible-galaxy install -r requirements.yml --force
  cache:
    key: ansible-$CI_COMMIT_REF_SLUG
    paths:
      - ~/.ansible/roles
      - .cache/pip

# Template untuk deployment (butuh SSH)
.deploy_base:
  extends: .ansible_base
  before_script:
    - !reference [.ansible_base, before_script]
    - eval $(ssh-agent -s)
    - echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add -
    - mkdir -p ~/.ssh && chmod 700 ~/.ssh
    - echo "$VAULT_PASSWORD" > /tmp/.vault_pass
    - chmod 600 /tmp/.vault_pass
  after_script:
    - rm -f /tmp/.vault_pass

Stage Validate dan Test #

# Stage: validate
lint:
  extends: .ansible_base
  stage: validate
  script:
    - pip install ansible-lint --quiet
    - ansible-lint --profile production
    - |
      for playbook in playbooks/*.yml; do
        ansible-playbook "$playbook" --syntax-check -i inventory/staging/
      done      
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH

check-production:
  extends: .deploy_base
  stage: validate
  variables:
    SSH_PRIVATE_KEY: $PROD_SSH_KEY
    VAULT_PASSWORD: $VAULT_PASS_PROD
  script:
    - ansible-playbook -i inventory/production/ site.yml
        --check --diff
        --vault-password-file /tmp/.vault_pass
  rules:
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH

# Stage: test — Molecule parallel
.molecule_base:
  extends: .ansible_base
  stage: test
  services:
    - docker:dind
  variables:
    DOCKER_HOST: tcp://docker:2376
    DOCKER_TLS_CERTDIR: "/certs"
  before_script:
    - pip install ansible molecule molecule-plugins[docker] --quiet
    - ansible-galaxy install -r requirements.yml --force

molecule-common:
  extends: .molecule_base
  script: cd roles/common && molecule test

molecule-nginx:
  extends: .molecule_base
  script: cd roles/nginx && molecule test

molecule-postgresql:
  extends: .molecule_base
  script: cd roles/postgresql && molecule test

Build dan Push ke GitLab Container Registry #

build-image:
  stage: build
  image: docker:24
  services:
    - docker:dind
  variables:
    DOCKER_TLS_CERTDIR: "/certs"
  script:
    - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
    - docker build -t $IMAGE_NAME:$IMAGE_TAG .
    - docker tag $IMAGE_NAME:$IMAGE_TAG $IMAGE_NAME:latest
    - docker push $IMAGE_NAME:$IMAGE_TAG
    - docker push $IMAGE_NAME:latest
    - echo "IMAGE_FULL_TAG=$IMAGE_NAME:$IMAGE_TAG" > build.env
  artifacts:
    reports:
      dotenv: build.env      # Teruskan variabel ke job berikutnya
  rules:
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH

Deployment dengan GitLab Environments #

deploy-staging:
  extends: .deploy_base
  stage: deploy-staging
  variables:
    SSH_PRIVATE_KEY: $STAGING_SSH_KEY
    VAULT_PASSWORD: $VAULT_PASS_STAGING
  script:
    - ansible-playbook -i inventory/staging/ playbooks/deploy.yml
        -e "app_version=$IMAGE_TAG"
        --vault-password-file /tmp/.vault_pass
  environment:
    name: staging
    url: https://staging.company.com
    deployment_tier: staging
  rules:
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH

verify-staging:
  stage: verify-staging
  image: curlimages/curl:latest
  script:
    - sleep 15
    - curl -f https://staging.company.com/health
    - |
      VERSION=$(curl -s https://staging.company.com/api/version | python3 -c "import json,sys; print(json.load(sys.stdin)['version'])")
      [ "$VERSION" = "$IMAGE_TAG" ] || (echo "Version mismatch: expected $IMAGE_TAG, got $VERSION" && exit 1)      
  rules:
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH

deploy-production:
  extends: .deploy_base
  stage: deploy-production
  variables:
    SSH_PRIVATE_KEY: $PROD_SSH_KEY
    VAULT_PASSWORD: $VAULT_PASS_PROD
  script:
    - ansible-playbook -i inventory/production/ playbooks/deploy.yml
        -e "app_version=$IMAGE_TAG"
        --vault-password-file /tmp/.vault_pass
  environment:
    name: production
    url: https://app.company.com
    deployment_tier: production
  when: manual               # Harus dipicu manual — tidak otomatis
  rules:
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
      when: manual
      allow_failure: false

Dynamic Child Pipeline #

Untuk monorepo dengan banyak service, gunakan child pipeline yang di-generate secara dinamis:

# .gitlab-ci.yml (parent pipeline)
generate-child-pipeline:
  stage: validate
  image: python:3.11-slim
  script:
    - python3 scripts/generate-pipeline.py > generated-pipeline.yml
  artifacts:
    paths:
      - generated-pipeline.yml

trigger-child:
  stage: deploy-staging
  trigger:
    include:
      - artifact: generated-pipeline.yml
        job: generate-child-pipeline
    strategy: depend
# scripts/generate-pipeline.py
# Generate pipeline berdasarkan service yang berubah

import subprocess
import yaml
import sys

# Deteksi service yang berubah
changed = subprocess.run(
    ['git', 'diff', '--name-only', 'HEAD~1'],
    capture_output=True, text=True
).stdout.split()

services = set()
for path in changed:
    if path.startswith('services/'):
        service = path.split('/')[1]
        services.add(service)

# Generate job untuk setiap service yang berubah
pipeline = {'stages': ['deploy'], 'jobs': {}}
for service in services:
    pipeline['jobs'][f'deploy-{service}'] = {
        'stage': 'deploy',
        'script': [
            f'ansible-playbook -i inventory/staging/ playbooks/deploy-{service}.yml'
        ]
    }

print(yaml.dump(pipeline))

Masked Variables untuk Secret #

# Di GitLab Settings → CI/CD → Variables
# Gunakan opsi:
# - Masked: nilai tidak muncul di log pipeline
# - Protected: hanya tersedia di protected branch
# - File: nilai disimpan sebagai file, bukan environment variable

# Contoh variabel yang harus di-mask:
# VAULT_PASS_PROD     → masked + protected
# PROD_SSH_KEY        → masked + protected + file type
# REGISTRY_PASSWORD   → masked

Ringkasan #

  • Gunakan .template_name: dengan extends: untuk menghindari duplikasi konfigurasi antar job — perubahan di template langsung berlaku di semua job yang meng-extend-nya.
  • !reference untuk me-reuse before_script dari template lain — memungkinkan komposisi yang lebih fleksibel dari extends saja.
  • GitLab Environments memberikan deployment tracking otomatis — history semua deployment ke setiap environment tersedia di UI GitLab.
  • artifacts.reports.dotenv untuk meneruskan variabel dari satu job ke job berikutnya — cara yang tepat untuk berbagi image tag antara build job dan deploy job.
  • when: manual di deployment production — memerlukan klik manual di UI GitLab, pipeline tidak otomatis melanjutkan ke production.
  • Masked + Protected variables untuk semua secret — masked mencegah nilai muncul di log, protected memastikan hanya branch yang dilindungi yang bisa mengaksesnya.

← Sebelumnya: GitHub Actions   Berikutnya: Environment Management →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact