GitHub Actions vs Azure Pipelines: 기능이 아니라 상황으로 고르고, OIDC로 시크릿 없애기

GitHub Actions와 Azure Pipelines CI/CD 파이프라인 비교

Azure에 배포하는 파이프라인을 만들 때 GitHub Actions와 Azure Pipelines 중 무엇을 쓸지는, 기능 비교표로는 결론이 안 납니다. 둘 다 할 수 있는 게 거의 같기 때문입니다.

실제로 선택을 가르는 건 코드가 어디 있는가, 조직에 어떤 통제 요구가 있는가 두 가지입니다. 이 글은 그 기준을 정리하고, 어느 쪽을 택하든 반드시 짚어야 할 인증 방식(OIDC)을 함께 다룹니다.

기능이 아니라 상황으로 가르기

상황선택이유
코드가 GitHub에 있다GitHub Actions같은 화면에서 PR·이슈·배포가 이어짐
코드가 Azure Repos에 있다Azure Pipelines연동이 기본. 굳이 나눌 이유 없음
승인 게이트·환경 통제가 엄격하다Azure Pipelines배포 승인·게이트 기능이 더 성숙
온프레미스 빌드 에이전트가 필요둘 다 가능self-hosted runner / agent
오픈소스 프로젝트GitHub Actions공개 저장소 무료, 마켓플레이스 생태계
이미 Azure DevOps 보드·아티팩트를 쓴다Azure Pipelines작업 항목 연결이 자동

가장 강한 기준은 첫 두 줄입니다. 코드가 있는 곳에서 파이프라인을 돌리는 게 거의 항상 맞습니다. GitHub 저장소에 Azure Pipelines를 붙이는 구성도 가능하지만, 로그를 보러 두 사이트를 오가게 되고 권한 관리도 이중이 됩니다.

세 번째 줄은 규제 산업에서 실제로 갈리는 지점입니다. Azure Pipelines의 환경(Environment)과 승인·검사(Approvals and checks) 기능은 “배포 전 특정 그룹의 승인 필요”, “업무 시간에만 배포 가능”, “Azure Monitor 경보가 없을 때만 배포” 같은 조건을 세밀하게 걸 수 있습니다. GitHub도 환경 보호 규칙이 있지만 상대적으로 단순합니다.

무엇을 고르든: 시크릿 대신 OIDC를 쓰세요

이 부분이 이 글에서 가장 중요합니다. 아직도 많은 파이프라인이 서비스 주체의 클라이언트 시크릿을 저장소 시크릿에 넣어두고 씁니다. 문제가 세 가지입니다.

  • 만료됩니다. 보통 1~2년. 만료되는 날 파이프라인이 전부 실패하고, 아무도 이유를 모릅니다
  • 유출되면 즉시 악용됩니다. 로그에 찍히거나, 포크된 저장소에서 새어나갈 수 있습니다
  • 회전 절차가 필요합니다. 주기적으로 갱신하는 운영 부담이 생깁니다

OIDC(워크로드 ID 페더레이션)는 이걸 전부 없앱니다. 파이프라인이 실행될 때 단기 토큰을 발급받아 인증하므로, 저장할 비밀이 아예 없습니다.

Azure 쪽 설정

#!/usr/bin/env bash
set -euo pipefail
APP_NAME="gh-actions-myapp"
SUB="00000000-0000-0000-0000-000000000000"
REPO="myorg/myapp"

# 1) 앱 등록 (시크릿 생성하지 않음)
APP_ID=$(az ad app create --display-name "$APP_NAME" --query appId -o tsv)
az ad sp create --id "$APP_ID"

# 2) 페더레이션 자격 증명 - main 브랜치용
az ad app federated-credential create \
  --id "$APP_ID" \
  --parameters "{
    \"name\": \"gh-main\",
    \"issuer\": \"https://token.actions.githubusercontent.com\",
    \"subject\": \"repo:$REPO:ref:refs/heads/main\",
    \"audiences\": [\"api://AzureADTokenExchange\"]
  }"

# 3) 환경(environment)용 - 승인 게이트가 있는 배포
az ad app federated-credential create \
  --id "$APP_ID" \
  --parameters "{
    \"name\": \"gh-env-prod\",
    \"issuer\": \"https://token.actions.githubusercontent.com\",
    \"subject\": \"repo:$REPO:environment:production\",
    \"audiences\": [\"api://AzureADTokenExchange\"]
  }"

# 4) 권한 부여 (필요한 범위로 최소화)
az role assignment create \
  --assignee "$APP_ID" \
  --role "Contributor" \
  --scope "/subscriptions/$SUB/resourceGroups/rg-prod-app"

subject 값이 핵심입니다. 이 문자열이 정확히 일치해야만 토큰이 발급됩니다.main 브랜치에서 실행될 때만, 또는 production 환경으로 배포할 때만 Azure에 접근할 수 있습니다. 포크된 저장소나 다른 브랜치에서는 인증 자체가 안 됩니다.

이건 시크릿 방식보다 보안이 강해진 것입니다. 시크릿은 어디서 실행되든 통하지만, OIDC는 실행 맥락까지 검증합니다.

GitHub Actions 워크플로

name: Deploy to Azure

on:
  push:
    branches: [main]

permissions:
  id-token: write      # OIDC 토큰 발급에 필수
  contents: read

jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: production
    steps:
      - uses: actions/checkout@v4

      - name: Azure 로그인 (시크릿 없음)
        uses: azure/login@v2
        with:
          client-id: ${{ vars.AZURE_CLIENT_ID }}
          tenant-id: ${{ vars.AZURE_TENANT_ID }}
          subscription-id: ${{ vars.AZURE_SUBSCRIPTION_ID }}

      - name: ACR 로그인 및 이미지 빌드
        run: |
          az acr login --name acrprodkrc01
          docker build -t acrprodkrc01.azurecr.io/myapp:${{ github.sha }} .
          docker push acrprodkrc01.azurecr.io/myapp:${{ github.sha }}

      - name: AKS 배포
        run: |
          az aks get-credentials -n aks-prod-krc-01 -g rg-prod-app --overwrite-existing
          kubectl set image deployment/myapp \
            myapp=acrprodkrc01.azurecr.io/myapp:${{ github.sha }} -n myapp
          kubectl rollout status deployment/myapp -n myapp --timeout=5m

permissions: id-token: write를 빠뜨리면 로그인이 실패합니다. 가장 흔한 실수입니다. 그리고 클라이언트 ID는 비밀이 아니므로 secrets가 아니라 vars에 넣어도 됩니다.

마지막 kubectl rollout status도 중요합니다. 이게 없으면 배포 명령만 보내고 성공으로 처리됩니다. 파드가 실제로 뜨는 데 실패해도 파이프라인은 초록불이 됩니다. 실패 시 파이프라인이 빨간불이 되도록 반드시 넣으세요.

Azure Pipelines 버전

trigger:
  branches:
    include: [main]

pool:
  vmImage: ubuntu-latest

stages:
- stage: Build
  jobs:
  - job: BuildImage
    steps:
    - task: AzureCLI@2
      inputs:
        azureSubscription: 'sc-prod-oidc'   # 워크로드 ID 페더레이션 방식으로 생성
        scriptType: bash
        scriptLocation: inlineScript
        inlineScript: |
          az acr login --name acrprodkrc01
          docker build -t acrprodkrc01.azurecr.io/myapp:$(Build.BuildId) .
          docker push acrprodkrc01.azurecr.io/myapp:$(Build.BuildId)

- stage: DeployProd
  dependsOn: Build
  jobs:
  - deployment: Deploy
    environment: production      # 여기에 승인 게이트가 걸림
    strategy:
      runOnce:
        deploy:
          steps:
          - task: AzureCLI@2
            inputs:
              azureSubscription: 'sc-prod-oidc'
              scriptType: bash
              scriptLocation: inlineScript
              inlineScript: |
                az aks get-credentials -n aks-prod-krc-01 -g rg-prod-app --overwrite-existing
                kubectl set image deployment/myapp \
                  myapp=acrprodkrc01.azurecr.io/myapp:$(Build.BuildId) -n myapp
                kubectl rollout status deployment/myapp -n myapp --timeout=5m

서비스 연결을 만들 때 인증 방식으로 “워크로드 ID 페더레이션”을 선택하면 됩니다. 최근에는 이게 권장 방식이고, 시크릿 방식은 레거시로 취급됩니다.

environment: production이 Azure Pipelines의 강점이 드러나는 부분입니다. 이 환경에 승인자·업무 시간 제한·경보 상태 확인 같은 검사를 붙일 수 있고, 조건을 통과해야만 배포가 진행됩니다.

둘 다 쓰는 경우도 있다

실무에서 흔한 구성이 하나 있습니다. GitHub Actions로 빌드·테스트하고, Azure Pipelines로 운영 배포하는 방식입니다.

개발자는 GitHub에서 PR을 올리고 CI 결과를 바로 봅니다. 운영 배포는 승인 게이트가 있는 Azure Pipelines에서 별도로 실행됩니다. 개발 편의와 배포 통제를 분리하는 구성입니다.

다만 두 시스템을 유지하는 비용이 생깁니다. 규제 요구가 명확하지 않다면 한쪽으로 통일하는 편이 낫습니다.

파이프라인이 느릴 때 손볼 곳

도구 선택보다 체감에 큰 영향을 주는 게 실행 시간입니다. 대개 아래 세 곳에서 시간이 샙니다.

1) 의존성 캐시를 안 쓰는 것

매 실행마다 npm cipip install을 처음부터 하면 몇 분씩 걸립니다. 캐시 액션 한 줄로 크게 줄어듭니다.

- uses: actions/setup-node@v4
  with:
    node-version: 20
    cache: 'npm'            # package-lock.json 해시로 자동 캐시

# Docker 레이어 캐시 (buildx)
- uses: docker/setup-buildx-action@v3
- uses: docker/build-push-action@v6
  with:
    push: true
    tags: acrprodkrc01.azurecr.io/myapp:latest
    cache-from: type=registry,ref=acrprodkrc01.azurecr.io/myapp:buildcache
    cache-to: type=registry,ref=acrprodkrc01.azurecr.io/myapp:buildcache,mode=max

Docker 레이어 캐시를 레지스트리에 두는 방식이 특히 효과적입니다. 러너가 매번 새로 뜨더라도 이전 빌드의 레이어를 재사용합니다.

2) 순차로 돌릴 필요 없는 것을 순차로 돌리는 것

린트·단위 테스트·타입 체크는 서로 의존하지 않습니다. 병렬 잡으로 나누면 전체 시간이 가장 느린 하나로 줄어듭니다.

jobs:
  lint:
    runs-on: ubuntu-latest
    steps: [...]
  test:
    runs-on: ubuntu-latest
    steps: [...]
  typecheck:
    runs-on: ubuntu-latest
    steps: [...]

  build:
    needs: [lint, test, typecheck]    # 셋 다 통과해야 진행
    runs-on: ubuntu-latest
    steps: [...]

3) 변경되지 않은 것까지 빌드하는 것

모노레포에서 특히 큽니다. 문서만 고쳤는데 전체 서비스를 빌드하고 배포까지 하면 시간과 비용이 모두 낭비됩니다. 경로 필터로 걸러냅니다.

on:
  push:
    branches: [main]
    paths:
      - 'services/api/**'
      - '.github/workflows/deploy-api.yml'
    paths-ignore:
      - '**.md'
      - 'docs/**'

Azure Pipelines에서도 trigger.paths.include / exclude로 같은 구성이 가능합니다.

비용 구조 차이

  • GitHub Actions — 공개 저장소는 무료, 비공개는 플랜별 무료 분(minutes) 이후 사용량 과금. 자체 러너를 쓰면 실행 시간 과금이 없습니다
  • Azure Pipelines — 무료 병렬 작업이 제한적으로 제공되고, 추가 병렬 작업은 월 단위 구매입니다. 자체 에이전트도 병렬 작업 수를 삽니다

빌드가 잦고 오래 걸리는 프로젝트라면 자체 호스팅 러너/에이전트를 검토할 만합니다. Azure VM이나 AKS에 올리면 실행 시간 과금 없이 쓸 수 있고, VNet 안의 리소스에 직접 접근할 수 있다는 부가 이점도 있습니다. Spot VM으로 러너를 돌리면 비용이 더 내려갑니다.

정리

  1. 코드가 있는 곳에서 파이프라인을 돌린다 — 이게 가장 강한 기준
  2. 승인 게이트·환경 통제가 엄격하면 Azure Pipelines
  3. 어느 쪽이든 시크릿 대신 OIDC를 쓴다 (만료도, 유출 위험도 없음)
  4. OIDC의 subject브랜치·환경까지 제한한다
  5. GitHub Actions는 permissions: id-token: write를 빠뜨리지 않는다
  6. 배포 단계에 rollout status를 넣어 거짓 성공을 막는다

이미지 빌드 단계를 최적화하려면 Docker 멀티스테이지 빌드를, 배포 후 파드가 안 뜨는 문제는 AKS CrashLoopBackOff 원인 추적을 참고하세요.

댓글 남기기