Azure Devops To Gitlab

Azure Devops To Gitlab

Reading time1 min
#DevOps#CI/CD#Cloud#AzureDevOps#GitLab#PipelineMigration

Seamless Migration: Efficient CI/CD Pipeline Transition from Azure DevOps to GitLab

Moving CI/CD pipelines from Azure DevOps to GitLab consolidates tooling, often slashing integration overhead and licensing expenses. However, addressing mismatches in pipeline orchestration, runner behavior, and secrets management is non-trivial. Blunt force “YAML conversion” rarely works. Robust migration means catching every nuance—project variables, artifact retention, deployment approvals.


Typical Triggers for Migration

Teams shift from Azure DevOps to GitLab for several reasons:

  • Unified DevOps surface: GitLab combines SCM, CI/CD, security scanning, and even built-in container registry (see 13.12 and later).
  • Lower TCO: License stack reduction; one platform instead of many.
  • Pipeline portability: GitLab’s .gitlab-ci.yml is portable and integrates well into cross-cloud workflows.

Azure DevOps to GitLab: Key Divergences

You’ll encounter at least these friction points:

  • Pipeline Syntax. Azure’s YAML diverges: trigger block, pool, and classic “steps.” GitLab coalesces workflows in .gitlab-ci.yml; concepts map, but not 1:1.
  • Credential Handling. Service connections in Azure vs. Project/Group variables in GitLab.
  • Runner/Agent Coverage. Azure and GitLab both offer ephemeral compute, but tagging, shell choice, and resource scaling differ.
  • Pipeline triggers vs. Events. PR auto-validation? Branch protection checks? Merging semantics? Distinct in each system.

Known issue: Complex multi-stage releases in Azure (especially leveraging environment gates or approval checks) require careful manual translation. GitLab’s environment controls are powerful, but assumptions differ.


Real-World Audit: Identifying Migration Scope

Begin by harvesting details from pipeline definitions and runtime history:

  • List all YAML/Classic pipelines. For each, capture:
    • All stages and jobs
    • Explicit and implicit variables (including those injected via Azure Key Vault)
    • External dependencies: e.g., artifact storage, deployment targets (Kubernetes, App Service...)
    • Branch protection rules, PR triggers, scheduled pipelines

Example snippet from a real Azure DevOps pipeline:

trigger:
  branches:
    include:
      - main
      - hotfix/*
variables:
  - group: ProdSecrets
pool:
  vmImage: 'ubuntu-22.04'
steps:
  - task: UseDotNet@2
    inputs:
      packageType: 'sdk'
      version: '7.0.x'
  - script: dotnet build
  • Note the use of variable groups and task invocations—these have to be explicitly handled when translating to GitLab.

Mapping: Azure Concepts to GitLab

Azure DevOpsGitLab
azure-pipelines.yml.gitlab-ci.yml
Classic/Multi-stage YAMLStages and jobs in single YAML
Variable GroupProject or Group CI/CD Variables
Service ConnectionsProject/group variables (for creds), integrations
Hosted Agents (MS provided)Shared/Specific GitLab Runners
Retention policiesArtifact expiration in job or project settings
Manual Approval GatesEnvironments with when: manual

Example:

  • Azure’s Service Connection for a private Docker registry is typically mirrored by defining CI_REGISTRY_USER and CI_REGISTRY_PASSWORD in GitLab project variables. Unlike Azure, GitLab will not surface credentials in job logs by default—unless you mishandle masking.

Bootstrapping the GitLab Project

  • Initialize repository: Push sources, or mirror directly from Azure.
    git remote add gitlab git@gitlab.com:<group>/<project>.git
    git push gitlab main
    
  • Import variables: Plaintext secrets, connection strings, or opaque tokens—enter all via Settings > CI/CD > Variables. Take care to mark sensitive items as protected and/or masked.
  • Set branch protection: Configure under Repository > Branches, mimicking Azure “policies” (e.g., require MR approval, disallow force-push).

Pipeline Translation: From Azure YAML to GitLab

Given an Azure YAML:

trigger:
  branches:
    exclude:
      - experimental/*
pool:
  vmImage: 'ubuntu-latest'
steps:
  - script: ./build.sh
    displayName: Build

GitLab equivalent:

stages:
  - build

build_job:
  stage: build
  image: ubuntu:22.04
  script:
    - ./build.sh
  except:
    - /^experimental.*/

Details:

  • pool.vmImage mapped directly to image.
  • Pre/Post build scripts or required tools (Azure’s “tasks”) must be invoked explicitly—there is no direct equivalent of most Azure Marketplace tasks.
  • exclude pattern under trigger becomes except: in GitLab. More nuanced conditions? Use rules: instead for advanced logic.

Side note: Watch out for shell interpretation differences. Azure's agents use PowerShell for Windows builds by default; GitLab requires explicit shell declaration or image: tag (e.g., mcr.microsoft.com/powershell).


Runners: Shared, Specific, and Self-Hosted

  • Shared Runners: Ideal for quick prototyping, but be wary of concurrency throttle or regional issues.
  • Group/Project Runners: Install runners on your VM, use Docker or shell executor for tighter control.
sudo gitlab-runner register \
  --url https://gitlab.com/ \
  --registration-token <TOKEN> \
  --executor docker \
  --description "selfhosted-ubuntu-2210"

Real-world gotcha: Docker-in-Docker convenience has security trade-offs—use only for trusted codebases.

To direct a job to a specific runner:

build_job:
  tags:
    - selfhosted-ubuntu

Monitor runners using gitlab-runner list on host, inspect /var/log/gitlab-runner/ for troubleshooting.


Handling Secrets and Connections

  • In Azure, variable groups can source secrets from Key Vault. In GitLab, all sensitive values (Azure Service Principal, container registry credentials, etc.) are input at project/group level.
  • Tricky part: Multiline secrets (“CERTIFICATE_PEM”) occasionally need special quoting in GitLab UI to avoid line breaks or parsing errors.

Expected error for unmasked secret:

ERROR: Job failed: exit code 1
##[error]Unhandled variable format: 'some-secret'

Mitigate by using only masked variables for sensitive values and reference via $CI_JOB_TOKEN or $CI_REGISTRY_PASSWORD as needed in scripts.


Triggers and Branch Policies

Example: Only deploy when a merge request is merged to main:

deploy_prod:
  stage: deploy
  script: ./deploy_prod.sh
  environment:
    name: production
    url: https://prod.example.com
  rules:
    - if: '$CI_PIPELINE_SOURCE == "merge_request_event" && $CI_COMMIT_BRANCH == "main"'
      when: manual

Critical: Unlike Azure, GitLab can enforce approvals for production or protected environments in project settings, but users commonly overlook reviewer configuration, leading to unwanted automerges/deploys.


Deployment Verification

Test each migration with controlled rollouts (staging, then production). Use environment: and when: manual annotations to force explicit human approval for high-risk stages.

Sample structure:

stages:
 - build
 - test
 - deploy

test_job:
  stage: test
  script: pytest

deploy_staging:
  stage: deploy
  environment: staging
  script: ./deploy_staging.sh
  when: manual

Always monitor post-migration runs for side effects: missing artifacts, permission issues, or runner timeouts. GitLab’s job traces surface errors (“exit status 137”: OOM, “permission denied”: token/runner misconfig).


Non-Obvious Tips

  • Incremental migration outperforms Big Bang. Move simple pipelines first; validate permissions, artifacts, and job output end to end.
  • The CI Lint utility can catch subtle misconfigurations undetectable by local YAML validation.
  • Use .gitlab-ci.yml includes to de-duplicate logic if porting multiple similar pipelines.
  • Prefer explicit artifact expiration in job definitions:
    artifacts:
      expire_in: '3 days'
    
    to avoid bloated storage from default indefinite retention.
  • Documentation is unavoidable; use Markdown in the repo itself for audit trails—external wikis lag real-world code.

Migrating Azure DevOps pipelines to GitLab means embracing new assumptions: a single YAML orchestrator, more direct secret handling, different runner models. It won’t go perfectly—edge cases (e.g., multi-tenant artifact feeds; tightly integrated approvals) may need workarounds or reengineering. That said, direct translation is possible when audits are thorough, secrets are handled with care, and testing occurs incrementally.

For edge-case migration scripts, troubleshooting runner configuration errors, or example repositories: contact team infra, or dig through the latest public templates—they reveal practical solutions absent from official docs.