Všechno začalo na Azure Pipelines. Před rokem jsme přesunuli PR validaci do GitHub Actions, ale deploye a E2E testování zůstaly na Azure. Navrhl jsem finální migraci, která to celé dotáhla pod jednu střechu. Tady je architektura a co se po cestě pokazilo.

Problém dvou mozků

Po první částečné migraci měl repozitář CI/CD rozdělenou mezi dva systémy. GitHub Actions řešil lehčí věci — linting, statickou analýzu, unit testy, Docker buildy. Azure Pipelines pořád vlastnil těžké operace — push images do tří cloudových registrů (ECR, GAR, ACR), deploy na tři E2E testovací stacky, běh ~30 PHPUnit test suites, manuální schválení produkce a samotný produkční deploy.

V praxi to bolelo. Přepínání kontextu mezi dvěma systémy s různými UI, různým secret managementem, různými debugovacími nástroji. Azure měl na správu stovek testovacích secrets neohrabaný variable UI. A deploy workflows nešly testovat na feature branchích — jakákoli změna pipeline se musela ověřit přímo na masteru.

Cíl byl jednoduchý: konsolidovat všechno do GitHub Actions a využít nativní funkce — environments s protection rules, matrix strategie, reusable workflows, OIDC autentizaci.

Jeden pipeline, který řídí všechno

Klíčové architektonické rozhodnutí byl jeden orchestrátor workflow (pipeline.yml) s resolve jobem, který klasifikuje každou GitHub událost a vyrobí routing plán. Otevřený PR? Jen CI. Push canary tagu? CI + canary deploy. Push do masteru? CI + E2E deploy + produkční deploy (za schvalovací bránou).

case "$EVENT" in
  pull_request)   IS_PR=true ;;
  merge_group)    IS_MERGE_GROUP=true ;;
  push)
    if [[ "$REF" == "refs/heads/master" ]]; then IS_MASTER=true
    elif [[ "$REF_NAME" == canary-* ]]; then IS_CANARY=true
    elif [[ "$REF_NAME" == dev-* ]]; then IS_DEV_TAG=true
    fi ;;
esac

Resolve job vyplivne JSON plán — typ pipeline, deploy mód, image tag, routovací flagy — a každý downstream job si z něj čte.

Podstatný detail: všechno používá workflow_call (reusable workflows), ne workflow_run. Ten rozdíl je zásadní. workflow_run se spouští jen z default branche, takže deploy pipeline z feature branche neotestujete. S workflow_call orchestrátor volá child workflows přímo a celé to funguje z jakékoli branche.

30 matrix jobů místo 4 paralelních batchů

Tohle bylo jádro migrace. Starý Azure setup seskupoval ~30 test suites do 4 batchů, každý běžel 6-8 suites přes GNU parallel na jednom runneru. Když suite v batchi spadla, museli jste se prohrabávat logem celého batche, abyste zjistili která. Re-run znamenal re-run celého batche.

Nový setup: jeden matrix job per suite. Individuální timeout, individuální pass/fail, individuální re-run.

strategy:
  fail-fast: false
  max-parallel: 30
  matrix:
    include:
      - { suite: api-tests, stack: aws, processes: 1 }
      - { suite: integration-part-1, stack: aws, processes: 5 }
      - { suite: integration-part-2, testsuite: integration, stack: gcp, processes: 9 }
      - { suite: storage-azure, stack: azure, processes: 1 }
      # ~30 suites across aws/azure/gcp

Všimněte si pole testsuite u třetího záznamu. To bylo první překvapení. Suite name (pro zobrazení a lookup secretů) není vždy totéž co PHPUnit --testsuite hodnota. Některé suites se postupem let přejmenovaly v configu, ale ne v phpunit.xml.dist, nebo naopak. Docker run příkaz to řeší fallbackem:

docker run --rm \
  --env-file "$ENV_FILE" \
  app_e2e \
  ./bin/parallel-retry.php \
    --processes=${{ matrix.processes || 1 }} \
    --testsuite=${{ matrix.testsuite || matrix.suite }}

Když je testsuite definovaný v matrixu, použij ho. Jinak fallback na suite. Je to legacy závislost, které se časem zbavíme, ale zatím ten fallback stačí — namapovat všechno správně stálo tak dvě tři iterace.

OIDC všude (skoro)

Jeden z přínosů migrace: OIDC autentizace pro cloudové registry. Žádné statické credentials pro push Docker images do AWS ECR nebo GCP Artifact Registry. Workflow se autentizuje přes GitHub OIDC token a cloud provider mu důvěřuje na základě identity repozitáře.

- uses: aws-actions/configure-aws-credentials@v4
  with:
    role-to-assume: arn:aws:iam::ACCOUNT_ID:role/ecr-push-role
    aws-region: us-east-1

Role ARN je hardcoded ve workflow — je to veřejný resource identifier, ne secret. Totéž pro GCP workload identity provider. Nula statických credentials pro tyhle dva providery.

Záludnost, na kterou se snadno zapomene: id-token: write permission. Bez něj OIDC token request tiše selže. Zapomněl jsem na to minimálně dvakrát v nových workflows.

Výjimka bylo Azure Container Registry. Azure OIDC implementace neumí omezit přístup na jednotlivé container repozitáře v rámci registru — je to buď celý registr, nebo nic. Workaround: scope map tokeny (v podstatě username/password pár omezený na konkrétní repa). Ne ideální, ale Azure nám pro granularitu na úrovni repo nedal jinou možnost.

Kam dát 200 secretů

Každá E2E suite potřebuje hromadu environment variables — API tokeny, storage URL, backend credentials. Přes 30 suites a 3 cloud stacky je to zhruba 200 secretů. Azure Pipelines je měl jako pipeline variables. Kam s nimi v GitHub Actions?

Ne do GitHub Environment variables — tam jsou limity na počet a náš generátor je často aktualizuje. Místo toho: GCP Secret Manager jako externí store.

Naming konvence je self-describing: <prefix>--<stack>--<suite>--<VAR_NAME>. Třeba: e2e-tests--aws--common--API_TOKEN. Helper script za běhu zjistí secrety listingem všeho, co matchuje pattern {prefix}--{stack}--{suite}--*, z posledního segmentu vytáhne jméno proměnné a postaví Docker --env-file.

Žádný statický config file. Přidejte secret do GCP a suite si ho automaticky vyzvedne.

Zábavná část byl parser. Legacy Azure variable names používaly __ jako delimiter — SOME_VAR__API_TOKEN__AWS. Jenže samotné variable names můžou obsahovat __. Parser musel splitovat zprava, aby to správně rozřezal. Chyba v tomhle znamenala tiché mismatchování, kdy suite loadovala secrety z jiného stacku.

Produkční deploye bez pálení runnerů

Azure Pipelines používal ManualValidation@0 pro produkční schvalovací brány. Fungovalo to, ale runner seděl zabraný celou dobu čekání — až 24 hodin, pokud nikdo rychle neschválil.

GitHub Environments tohle řeší čistě. Environment production má required reviewers a branch restriction (jen master). Deploy job cílí na tento environment a GitHub job pozastaví, dokud někdo neschválí. Žádný runner není alokovaný během čekání. Vestavěný audit trail — kdo co a kdy schválil.

RFC vs. realita

Než jsem začal, napsal jsem detailní RFC — architektonické diagramy, task checklisty, plán fázového rollout. Enormně to pomohlo pro velká rozhodnutí (orchestrátor pattern, matrix strategie, volba secret store). Ale checklist se začal odchylovat skoro okamžitě, jakmile začala implementace.

Naming mismatchování mezi matrix configem, PHPUnit testsuites a legacy Azure proměnnými v RFC nebyly. id-token: write permission gotcha v RFC nebyla. Fakt, že workflow_run triggeruje jen z default branche — na to jsem přišel tvrdou cestou, když první přístup selhal.

Plánovací dokumenty jsou mapy, ne terén. Mapa mě dostala na správný kontinent, ale navigovat ulicemi vyžadovalo projít je pěšky.

Předchozí příspěvek

Related Posts