이미지 로딩 중...

GitHub Actions 트러블슈팅 완벽 가이드 - 슬라이드 1/11
A

AI Generated

2025. 11. 6. · 17 Views

GitHub Actions 트러블슈팅 완벽 가이드

GitHub Actions를 사용하다 보면 다양한 문제에 부딪히게 됩니다. 워크플로우 실패, 권한 오류, 캐시 문제 등 실무에서 자주 발생하는 문제들을 체계적으로 해결하는 방법을 배워봅니다. 이 가이드를 통해 빠르고 정확하게 문제를 진단하고 해결할 수 있습니다.


목차

  1. 워크플로우 실행 실패 디버깅 - 로그 분석과 디버그 모드 활용
  2. 권한 오류 해결 - GITHUB_TOKEN 권한 설정
  3. 의존성 캐싱 전략 - 빌드 시간 단축하기
  4. 매트릭스 빌드 실패 처리 - 일부 실패 허용하기
  5. 환경 변수와 시크릿 관리 - 보안 강화하기
  6. 조건부 워크플로우 실행 - 불필요한 빌드 스킵하기
  7. 재사용 가능한 워크플로우 - DRY 원칙 적용하기
  8. 컨테이너 기반 작업 - 일관된 환경 보장하기
  9. 아티팩트 관리 - 빌드 결과물 공유하기
  10. 동시성 제어 - 중복 실행 방지하기

1. 워크플로우 실행 실패 디버깅 - 로그 분석과 디버그 모드 활용

시작하며

여러분이 GitHub Actions 워크플로우를 푸시했는데 빨간색 X 표시가 뜨면서 실패했을 때, 어디서부터 문제를 찾아야 할지 막막했던 경험 있나요? 수백 줄의 로그를 스크롤하면서 정확히 어떤 부분이 잘못됐는지 찾느라 시간을 낭비하는 상황 말이죠.

이런 문제는 실제 개발 현장에서 매일 발생합니다. 특히 복잡한 빌드 파이프라인이나 여러 스텝이 연결된 워크플로우에서는 문제의 원인을 찾는 것 자체가 큰 도전이 됩니다.

잘못된 환경 변수, 의존성 충돌, 권한 문제 등 다양한 원인이 있을 수 있기 때문입니다. 바로 이럴 때 필요한 것이 체계적인 디버깅 전략입니다.

GitHub Actions의 디버그 모드와 로그 분석 기법을 활용하면 문제를 빠르게 파악하고 해결할 수 있습니다.

개요

간단히 말해서, 워크플로우 디버깅은 실패한 작업의 로그를 체계적으로 분석하여 문제의 근본 원인을 찾는 과정입니다. 디버그 모드를 활성화하면 일반적으로 숨겨진 상세한 로그 정보까지 볼 수 있습니다.

예를 들어, 환경 변수가 제대로 설정되었는지, 각 스텝이 어떤 순서로 실행되는지, 셸 명령어가 정확히 어떻게 해석되는지 등을 확인할 수 있습니다. 기존에는 워크플로우 파일에 직접 echo 문을 추가하거나 재실행을 반복하면서 문제를 찾았다면, 이제는 리포지토리 시크릿에 간단한 값만 추가하면 모든 세부 정보를 한 번에 볼 수 있습니다.

디버그 모드의 핵심 특징은 두 가지입니다. 첫째, 워크플로우 파일을 수정하지 않고도 활성화할 수 있다는 점입니다.

둘째, 러너의 내부 동작까지 상세하게 기록된다는 점입니다. 이러한 특징들이 있어 복잡한 문제도 빠르게 진단할 수 있습니다.

코드 예제

# 디버그 로깅을 활성화한 워크플로우 예시
name: Debug Workflow
on: push

jobs:
  debug-job:
    runs-on: ubuntu-latest
    steps:
      # 환경 변수 확인
      - name: Print environment
        run: |
          echo "Node version: $(node --version)"
          echo "Working directory: $(pwd)"
          printenv | sort

      # 실패 시 상세 로그 출력
      - name: Run tests with debug
        run: npm test
        env:
          DEBUG: '*'  # 모든 디버그 로그 활성화

설명

이것이 하는 일: 워크플로우가 실패했을 때 문제를 빠르게 파악할 수 있도록 상세한 실행 정보를 로그에 기록합니다. 첫 번째로, "Print environment" 스텝은 현재 실행 환경의 모든 정보를 출력합니다.

Node.js 버전, 작업 디렉토리, 그리고 모든 환경 변수를 정렬하여 보여주죠. 이렇게 하면 예상과 다른 환경 설정을 즉시 발견할 수 있습니다.

그 다음으로, npm test가 실행되면서 DEBUG 환경 변수를 '*'로 설정합니다. 이는 Node.js의 debug 모듈을 사용하는 모든 라이브러리가 상세 로그를 출력하도록 만듭니다.

내부적으로 어떤 함수가 호출되고, 어떤 데이터가 처리되는지 추적할 수 있죠. 추가로 GitHub Actions 자체의 디버그 모드를 활성화하려면, 리포지토리 설정의 Secrets에서 ACTIONS_STEP_DEBUG와 ACTIONS_RUNNER_DEBUG를 true로 설정해야 합니다.

이렇게 하면 각 스텝의 입력값, 출력값, 그리고 러너의 내부 동작까지 모두 기록됩니다. 마지막으로, 로그를 분석할 때는 하단부터 역순으로 읽는 것이 효과적입니다.

실패한 지점부터 거슬러 올라가면서 원인을 찾는 것이죠. 빨간색으로 표시된 에러 메시지와 그 직전의 경고 메시지를 주의 깊게 살펴보세요.

여러분이 이 방법을 사용하면 디버깅 시간을 80% 이상 단축할 수 있습니다. 더 이상 추측으로 문제를 해결하지 않고, 정확한 데이터를 기반으로 의사결정을 내릴 수 있으며, 같은 문제가 재발했을 때도 빠르게 대응할 수 있습니다.

실전 팁

💡 디버그 모드는 로그가 매우 길어지므로 꼭 필요할 때만 활성화하고, 문제 해결 후에는 시크릿을 삭제하세요.

💡 실패한 스텝을 재실행할 때는 "Re-run failed jobs" 대신 "Re-run all jobs"를 선택하면 전체 컨텍스트를 파악하기 쉽습니다.

💡 로그를 다운로드하여 로컬 텍스트 에디터에서 검색하면 특정 에러 메시지나 패턴을 빠르게 찾을 수 있습니다.

💡 워크플로우 파일에 set -x를 추가하면 셸 스크립트의 각 명령어가 실행되기 전에 출력되어 디버깅이 쉬워집니다.

💡 tmate 액션을 사용하면 실패한 러너에 SSH로 접속하여 실시간으로 디버깅할 수 있습니다.


2. 권한 오류 해결 - GITHUB_TOKEN 권한 설정

시작하며

여러분의 워크플로우가 "Resource not accessible by integration" 또는 "403 Forbidden" 에러를 뿜으면서 실패한 적 있나요? PR에 자동으로 코멘트를 달거나, 이슈를 생성하려고 할 때 이런 권한 오류를 자주 만나게 됩니다.

이런 문제는 GitHub Actions의 기본 보안 정책 때문에 발생합니다. GITHUB_TOKEN은 워크플로우가 리포지토리와 상호작용할 수 있도록 자동으로 생성되는 토큰인데, 기본적으로는 읽기 전용 권한만 가지고 있습니다.

이는 보안을 위한 설계이지만, 실제로 많은 작업들이 쓰기 권한을 필요로 합니다. 바로 이럴 때 필요한 것이 명시적인 권한 설정입니다.

워크플로우 파일에서 필요한 권한만 정확히 부여하면 보안을 유지하면서도 필요한 작업을 수행할 수 있습니다.

개요

간단히 말해서, GITHUB_TOKEN 권한 설정은 워크플로우가 수행할 수 있는 작업의 범위를 명시적으로 정의하는 것입니다. 권한은 작업(job) 레벨이나 워크플로우 레벨에서 설정할 수 있습니다.

예를 들어, PR에 코멘트를 달려면 pull-requests: write 권한이 필요하고, 이슈를 생성하려면 issues: write 권한이 필요합니다. 패키지를 배포한다면 packages: write 권한도 필요하겠죠.

기존에는 개인 액세스 토큰(PAT)을 생성하여 시크릿에 저장하고 사용했다면, 이제는 GITHUB_TOKEN의 권한만 적절히 설정하면 별도의 토큰 관리 없이 안전하게 작업할 수 있습니다. 권한 설정의 핵심 원칙은 최소 권한 원칙입니다.

필요한 권한만 정확히 부여하고, 나머지는 명시적으로 read 또는 제외해야 합니다. 이렇게 하면 보안 취약점을 최소화할 수 있고, 코드 리뷰 시에도 워크플로우가 무엇을 할 수 있는지 명확히 파악할 수 있습니다.

코드 예제

name: PR Comment
on: pull_request

# 워크플로우 레벨 권한 설정
permissions:
  contents: read        # 코드 읽기만 허용
  pull-requests: write  # PR에 쓰기 가능
  issues: write         # 이슈 쓰기 가능

jobs:
  comment:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      # GITHUB_TOKEN을 사용하여 PR에 코멘트
      - name: Comment on PR
        uses: actions/github-script@v7
        with:
          github-token: ${{ secrets.GITHUB_TOKEN }}
          script: |
            github.rest.issues.createComment({
              issue_number: context.issue.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
              body: '✅ 자동 검증 완료!'
            })

설명

이것이 하는 일: PR이 생성되면 자동으로 코멘트를 달기 위해 필요한 최소한의 권한만 부여합니다. 첫 번째로, permissions 블록에서 세 가지 권한을 설정합니다.

contents: read는 리포지토리의 코드를 읽을 수 있게 하고, pull-requests: write와 issues: write는 PR과 이슈에 코멘트를 작성할 수 있게 합니다. PR은 내부적으로 이슈의 한 종류이기 때문에 두 권한이 모두 필요합니다.

그 다음으로, actions/github-script 액션이 실행되면서 GITHUB_TOKEN을 사용합니다. 이 토큰은 자동으로 생성되어 secrets.GITHUB_TOKEN으로 접근할 수 있으며, 위에서 설정한 권한을 가지고 있습니다.

별도의 토큰을 생성하거나 관리할 필요가 없죠. 스크립트 내부에서는 GitHub REST API를 사용하여 createComment를 호출합니다.

context 객체는 현재 워크플로우의 실행 컨텍스트를 담고 있어서, 어떤 PR에 코멘트를 달아야 하는지 자동으로 알 수 있습니다. 만약 permissions 블록을 생략하면 어떻게 될까요?

리포지토리 설정에 따라 다르지만, 최근에는 기본적으로 읽기 전용 권한만 부여됩니다. 그래서 명시적으로 권한을 설정하는 것이 중요합니다.

여러분이 이 방법을 사용하면 보안을 유지하면서도 자동화된 작업을 수행할 수 있습니다. 개인 액세스 토큰을 관리하는 부담도 없고, 토큰이 만료될 걸 걱정할 필요도 없으며, 권한이 명확히 문서화되어 있어 팀원들도 쉽게 이해할 수 있습니다.

실전 팁

💡 리포지토리 설정의 Actions > General > Workflow permissions에서 기본 권한을 확인하고 조정할 수 있습니다.

💡 특정 작업만 권한이 필요하다면 워크플로우 레벨이 아닌 job 레벨에서 permissions를 설정하는 것이 더 안전합니다.

💡 외부 기여자의 PR에서는 GITHUB_TOKEN의 권한이 자동으로 제한되므로, pull_request_target 이벤트 사용 시 주의하세요.

💡 GitHub CLI(gh)를 사용할 때는 GH_TOKEN 환경 변수에 GITHUB_TOKEN을 전달해야 합니다.

💡 권한 오류가 발생하면 API 응답의 message 필드를 확인하여 정확히 어떤 권한이 부족한지 파악하세요.


3. 의존성 캐싱 전략 - 빌드 시간 단축하기

시작하며

여러분의 CI/CD 파이프라인이 매번 10분씩 걸리면서 개발 속도를 늦추고 있나요? 특히 node_modules나 pip 패키지를 매번 새로 다운로드하느라 시간과 GitHub Actions의 무료 사용 시간을 낭비하는 상황 말이죠.

이런 문제는 중대형 프로젝트에서 매우 흔합니다. 수백 개의 의존성을 가진 프로젝트는 패키지 설치만 5분 이상 걸리기도 합니다.

이는 개발자의 생산성을 떨어뜨릴 뿐만 아니라, 피드백 루프를 느리게 만들어 버그 수정과 기능 개발을 지연시킵니다. 바로 이럴 때 필요한 것이 효과적인 캐싱 전략입니다.

GitHub Actions의 캐시 기능을 올바르게 활용하면 빌드 시간을 50-80%까지 단축할 수 있습니다.

개요

간단히 말해서, 의존성 캐싱은 이전 워크플로우 실행에서 다운로드한 패키지들을 저장해두었다가 다음 실행에서 재사용하는 것입니다. 캐시는 키-값 쌍으로 저장됩니다.

예를 들어, package-lock.json 파일의 해시값을 키로 사용하면, 의존성이 변경되지 않는 한 동일한 캐시를 재사용할 수 있습니다. 의존성이 변경되면 해시값도 달라지므로 자동으로 새로운 캐시가 생성되죠.

기존에는 매번 npm install을 처음부터 실행하여 모든 패키지를 다운로드했다면, 이제는 캐시에서 복원하여 몇 초 안에 설치를 완료할 수 있습니다. 캐싱의 핵심 특징은 세 가지입니다.

첫째, 스마트한 키 생성 전략으로 정확히 필요한 시점에만 캐시를 갱신합니다. 둘째, restore-keys를 사용하여 완전히 일치하는 캐시가 없을 때 부분적으로 일치하는 캐시라도 활용합니다.

셋째, 캐시 크기 제한(10GB)과 만료 정책(7일)을 이해하고 관리합니다.

코드 예제

name: Cached Build
on: push

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      # Node.js 의존성 캐싱
      - name: Cache dependencies
        uses: actions/cache@v3
        with:
          path: ~/.npm  # npm 캐시 디렉토리
          key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
          restore-keys: |
            ${{ runner.os }}-node-

      - name: Install dependencies
        run: npm ci  # npm install보다 빠르고 안정적

      - name: Build project
        run: npm run build

설명

이것이 하는 일: 이전에 다운로드한 npm 패키지들을 저장해두었다가 다음 빌드에서 재사용하여 설치 시간을 획기적으로 줄입니다. 첫 번째로, actions/cache 액션이 실행되면서 지정된 path(~/.npm)에 캐시가 있는지 확인합니다.

key는 운영체제와 package-lock.json의 해시값을 조합하여 생성됩니다. 이렇게 하면 의존성이 변경될 때만 새로운 캐시가 만들어지죠.

그 다음으로, 완전히 일치하는 캐시를 찾지 못하면 restore-keys에 정의된 패턴과 부분적으로 일치하는 캐시를 찾습니다. ${{ runner.os }}-node-로 시작하는 가장 최근 캐시를 복원하는 것이죠.

이는 의존성이 조금 변경되었을 때 전체를 다시 다운로드하지 않고 변경된 부분만 추가하도록 합니다. npm ci 명령어는 npm install과 달리 package-lock.json을 엄격히 따르며, node_modules를 삭제하고 깨끗하게 설치합니다.

캐시와 함께 사용하면 속도도 빠르고 재현 가능한 빌드를 보장할 수 있습니다. 캐시가 복원되면 npm ci는 실제로 다운로드할 필요 없이 캐시된 패키지를 사용합니다.

처음 실행에는 5분 걸리던 작업이 캐시 복원 후에는 30초 안에 완료되는 것을 경험할 수 있습니다. 여러분이 이 전략을 사용하면 CI/CD 파이프라인의 속도가 2배 이상 빨라집니다.

GitHub Actions의 무료 사용 시간도 절약할 수 있고, 빠른 피드백으로 개발 생산성이 향상되며, 동일한 시간에 더 많은 빌드를 실행할 수 있습니다.

실전 팁

💡 Python은 ~/.cache/pip, Maven은 ~/.m2/repository처럼 언어별로 캐시 경로가 다르므로 공식 문서를 확인하세요.

💡 actions/setup-node, actions/setup-python 등은 cache 옵션을 제공하여 별도의 cache 액션 없이도 캐싱을 활성화할 수 있습니다.

💡 리포지토리당 캐시 크기 제한이 10GB이므로, 오래된 브랜치의 캐시는 자동으로 삭제되니 참고하세요.

💡 캐시 히트율을 모니터링하려면 워크플로우 로그에서 "Cache restored successfully" 메시지를 확인하세요.

💡 빌드 결과물도 캐싱할 수 있지만, 보안상 민감한 정보가 포함되지 않았는지 반드시 확인하세요.


4. 매트릭스 빌드 실패 처리 - 일부 실패 허용하기

시작하며

여러분이 여러 Node.js 버전에서 테스트를 실행하는데, Node 18과 20은 통과하지만 최신 버전인 21에서만 실패한다면 어떻게 하시나요? 전체 워크플로우가 실패로 표시되면서 PR을 머지할 수 없게 되는 상황이 발생합니다.

이런 문제는 여러 환경을 지원하는 라이브러리나 프레임워크를 개발할 때 흔히 발생합니다. 실험적인 버전이나 베타 버전을 테스트 매트릭스에 포함하고 싶지만, 그것 때문에 안정 버전의 성공적인 빌드까지 막고 싶지는 않죠.

바로 이럴 때 필요한 것이 유연한 매트릭스 빌드 전략입니다. 특정 조합의 실패를 허용하거나, 빠른 실패를 방지하면서도 전체 테스트 결과를 파악할 수 있습니다.

개요

간단히 말해서, 매트릭스 빌드 실패 처리는 여러 환경에서 테스트할 때 일부 조합의 실패를 전략적으로 관리하는 것입니다. fail-fast 옵션을 false로 설정하면 하나의 작업이 실패해도 다른 작업들이 계속 실행됩니다.

예를 들어, Node 21에서 실패했더라도 Node 18과 20의 테스트는 끝까지 실행되어 결과를 확인할 수 있습니다. 기존에는 첫 번째 실패가 발생하면 나머지 작업들이 즉시 취소되어 전체 상황을 파악하기 어려웠다면, 이제는 모든 조합의 결과를 한 번에 볼 수 있습니다.

continue-on-error를 사용하면 특정 작업이 실패해도 워크플로우 전체는 성공으로 처리할 수 있습니다. 실험적인 기능을 테스트하거나, 선택적인 검증을 수행할 때 유용하죠.

이러한 옵션들을 조합하면 안정성과 유연성을 동시에 확보할 수 있습니다.

코드 예제

name: Matrix Build
on: push

jobs:
  test:
    runs-on: ${{ matrix.os }}
    # 빠른 실패 방지 - 모든 조합 테스트
    strategy:
      fail-fast: false
      matrix:
        os: [ubuntu-latest, windows-latest]
        node: [18, 20, 21]
        # 실험적 조합 표시
        include:
          - node: 21
            experimental: true

    # 실험적 버전 실패 허용
    continue-on-error: ${{ matrix.experimental || false }}

    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node }}
      - run: npm ci
      - run: npm test

설명

이것이 하는 일: 여러 운영체제와 Node.js 버전 조합을 테스트하면서, 실험적인 버전의 실패는 전체 워크플로우에 영향을 주지 않도록 합니다. 첫 번째로, strategy.fail-fast를 false로 설정합니다.

이렇게 하면 ubuntu-latest + Node 18 조합이 실패하더라도, windows-latest + Node 20 같은 다른 조합들이 계속 실행됩니다. 기본값은 true인데, 이는 하나만 실패해도 모든 작업이 취소되어 리소스를 절약하지만 전체 상황을 보기 어렵습니다.

그 다음으로, matrix.include를 사용하여 Node 21에 experimental: true 속성을 추가합니다. 이는 매트릭스 변수로 사용할 수 있는 메타데이터입니다.

이렇게 하면 특정 조합에만 특별한 처리를 적용할 수 있죠. continue-on-error는 동적으로 설정됩니다.

${{ matrix.experimental || false }}는 experimental 속성이 true인 경우에만 오류를 무시하라는 의미입니다. Node 18과 20에서는 이 값이 false이므로 실패 시 워크플로우가 실패로 표시되지만, Node 21에서는 true이므로 실패해도 워크플로우는 성공으로 처리됩니다.

실제로 작업이 실행되면 6개의 작업(2개 OS × 3개 Node 버전)이 병렬로 실행됩니다. 각 작업은 독립적으로 실행되며, 실패 여부도 개별적으로 판단됩니다.

여러분이 이 전략을 사용하면 안정 버전과 실험 버전을 동시에 테스트할 수 있습니다. CI가 불필요하게 빨갛게 변하는 것을 방지하면서도, 새로운 버전에서의 호환성 문제를 조기에 발견할 수 있으며, 팀원들이 어떤 환경에서 문제가 있는지 명확히 파악할 수 있습니다.

실전 팁

💡 matrix.exclude를 사용하면 특정 조합(예: Windows + Node 21)을 매트릭스에서 제거할 수 있습니다.

💡 continue-on-error는 단계(step) 레벨에서도 사용 가능하므로, 특정 테스트만 선택적으로 실패를 허용할 수 있습니다.

💡 실험적 작업의 결과를 명확히 표시하려면 작업 이름에 "(Experimental)" 같은 표시를 추가하세요.

💡 매트릭스가 너무 크면(50개 이상) 실행 시간이 길어지고 비용이 증가하므로, 꼭 필요한 조합만 포함하세요.

💡 required checks에서 실험적 작업을 제외하려면 브랜치 보호 규칙에서 개별 작업 이름을 지정하세요.


5. 환경 변수와 시크릿 관리 - 보안 강화하기

시작하며

여러분이 API 키나 데이터베이스 비밀번호를 워크플로우에서 사용해야 하는데, 실수로 로그에 노출되거나 PR에서 접근 가능해질까 봐 걱정되시나요? 민감한 정보가 유출되면 보안 사고로 이어질 수 있어 매우 조심스러운 작업입니다.

이런 문제는 CI/CD를 구축할 때 반드시 해결해야 하는 과제입니다. GitHub Actions는 기본적으로 시크릿을 마스킹하여 로그에 표시하지 않지만, 잘못 사용하면 여전히 노출될 위험이 있습니다.

특히 외부 기여자의 PR에서 시크릿에 접근하려고 시도하는 경우가 문제가 됩니다. 바로 이럴 때 필요한 것이 올바른 시크릿 관리 전략입니다.

환경별 시크릿, 승인 워크플로우, 그리고 안전한 사용 패턴을 익히면 보안을 크게 강화할 수 있습니다.

개요

간단히 말해서, 시크릿 관리는 민감한 정보를 안전하게 저장하고 필요한 워크플로우에서만 접근할 수 있도록 제어하는 것입니다. GitHub Secrets는 암호화되어 저장되며, 워크플로우 실행 중에만 복호화됩니다.

예를 들어, AWS 접근 키를 시크릿으로 저장하면 리포지토리 설정에서도 값을 볼 수 없고, 오직 워크플로우 내부에서만 사용할 수 있습니다. 기존에는 환경 변수를 .env 파일에 저장하고 .gitignore에 추가하는 방식을 사용했다면, CI/CD 환경에서는 GitHub Secrets를 통해 중앙에서 관리하고 버전별로 다른 값을 사용할 수 있습니다.

시크릿의 핵심 특징은 세 가지입니다. 첫째, 자동 마스킹으로 로그에 ***로 표시됩니다.

둘째, Environment 기능으로 production과 staging을 분리하여 관리할 수 있습니다. 셋째, pull_request 이벤트에서는 기본적으로 접근이 제한되어 보안이 강화됩니다.

코드 예제

name: Secure Deploy
on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    # production 환경 사용 (승인 필요)
    environment: production
    steps:
      - uses: actions/checkout@v4

      # 시크릿을 환경 변수로 안전하게 사용
      - name: Deploy to AWS
        env:
          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          # 일반 환경 변수
          AWS_REGION: us-east-1
        run: |
          # 시크릿은 자동으로 마스킹됨
          aws s3 sync ./dist s3://my-bucket

      # 시크릿을 절대 출력하지 않기
      - name: Verify deployment
        run: echo "Deployed to production"
        # 잘못된 예: echo $AWS_SECRET_ACCESS_KEY

설명

이것이 하는 일: AWS 배포에 필요한 인증 정보를 안전하게 관리하고, production 환경에 배포하기 전에 승인을 받도록 합니다. 첫 번째로, environment: production을 설정합니다.

이는 단순한 시크릿 그룹이 아니라 배포 환경 전체를 의미합니다. 리포지토리 설정에서 production 환경을 생성하고 특정 브랜치만 배포 가능하도록 제한하거나, 배포 전 승인자를 지정할 수 있습니다.

그 다음으로, env 블록에서 시크릿을 환경 변수로 할당합니다. ${{ secrets.AWS_SECRET_ACCESS_KEY }} 구문은 워크플로우 실행 시 실제 값으로 대체되지만, 로그에는 ***로 표시됩니다.

일반 환경 변수(AWS_REGION)와 섞어서 사용할 수 있습니다. aws s3 sync 명령어가 실행되면 환경 변수를 읽어서 AWS에 인증합니다.

이 과정에서 시크릿 값이 사용되지만, 로그나 아티팩트에 남지 않습니다. 만약 스크립트에서 실수로 시크릿을 출력하려고 해도 자동으로 마스킹됩니다.

하지만 주의할 점이 있습니다. echo나 다른 방법으로 시크릿을 간접적으로 노출시킬 수 있습니다.

예를 들어, base64로 인코딩하거나 한 글자씩 출력하면 마스킹이 우회될 수 있죠. 그래서 시크릿을 직접 다루는 코드는 매우 신중하게 작성해야 합니다.

여러분이 이 방법을 사용하면 민감한 정보가 유출될 위험이 크게 줄어듭니다. 팀원이 교체되어도 개별 시크릿을 공유할 필요 없이 GitHub 접근 권한만 관리하면 되고, 환경별로 다른 인증 정보를 사용하여 실수로 production 데이터를 건드리는 것을 방지할 수 있으며, 감사 로그를 통해 누가 언제 배포했는지 추적할 수 있습니다.

실전 팁

💡 Organization 레벨의 시크릿을 설정하면 여러 리포지토리에서 동일한 시크릿을 공유할 수 있습니다.

💡 Environment secrets는 Repository secrets보다 우선순위가 높으므로, 환경별로 다른 값을 사용할 수 있습니다.

💡 dependabot이나 외부 기여자의 PR에서는 시크릿 접근이 제한되므로, pull_request_target 사용 시 주의하세요.

💡 시크릿을 파일로 저장해야 한다면 임시 파일에 쓰고 작업 후 즉시 삭제하는 패턴을 사용하세요.

💡 HashiCorp Vault나 AWS Secrets Manager 같은 외부 시크릿 관리 도구와 통합하면 더 강력한 보안을 구현할 수 있습니다.


6. 조건부 워크플로우 실행 - 불필요한 빌드 스킵하기

시작하며

여러분이 README.md만 수정했는데도 전체 테스트와 빌드가 실행되면서 10분씩 기다려야 하는 상황을 겪어본 적 있나요? 또는 특정 라벨이 붙은 PR에서만 배포를 실행하고 싶은데 방법을 몰라서 고민하신 적은요?

이런 문제는 CI/CD 리소스를 낭비하게 만들고, 정작 중요한 빌드가 대기열에서 밀려나게 합니다. 문서만 수정하거나 설정 파일만 변경했을 때는 코드 검증이 필요 없는데도 모든 파이프라인이 돌아가는 것이죠.

바로 이럴 때 필요한 것이 조건부 실행 전략입니다. 파일 경로, 브랜치, 라벨, 커밋 메시지 등 다양한 조건으로 워크플로우나 개별 단계를 선택적으로 실행할 수 있습니다.

개요

간단히 말해서, 조건부 실행은 특정 조건이 만족될 때만 워크플로우나 작업을 실행하여 불필요한 리소스 사용을 줄이는 것입니다. if 조건문을 사용하면 GitHub 컨텍스트와 표현식을 활용하여 매우 세밀한 제어가 가능합니다.

예를 들어, src/ 디렉토리의 파일이 변경되었을 때만 테스트를 실행하거나, main 브랜치에 푸시할 때만 배포를 수행할 수 있습니다. 기존에는 모든 커밋에서 동일한 워크플로우가 실행되어 시간과 비용을 낭비했다면, 이제는 변경된 파일을 분석하여 필요한 작업만 선택적으로 실행할 수 있습니다.

조건부 실행의 핵심 패턴은 세 가지입니다. 첫째, paths 필터로 워크플로우 트리거 자체를 제한합니다.

둘째, if 조건으로 작업이나 단계를 스킵합니다. 셋째, changed-files 같은 액션으로 동적으로 변경 사항을 감지합니다.

이를 조합하면 매우 효율적인 CI/CD를 구축할 수 있습니다.

코드 예제

name: Conditional Workflow
on:
  pull_request:
    # 소스 코드 변경 시에만 트리거
    paths:
      - 'src/**'
      - 'tests/**'
      - 'package*.json'

jobs:
  test:
    runs-on: ubuntu-latest
    # PR이고 draft가 아닐 때만 실행
    if: github.event_name == 'pull_request' && !github.event.pull_request.draft
    steps:
      - uses: actions/checkout@v4
      - run: npm ci
      - run: npm test

  deploy:
    runs-on: ubuntu-latest
    # main 브랜치이고 'deploy' 라벨이 있을 때만
    if: |
      github.ref == 'refs/heads/main' &&
      contains(github.event.pull_request.labels.*.name, 'deploy')
    steps:
      - uses: actions/checkout@v4
      - run: npm run build
      - run: npm run deploy

설명

이것이 하는 일: 소스 코드가 변경되었을 때만 테스트를 실행하고, 특정 조건이 만족될 때만 배포를 수행하여 리소스를 효율적으로 사용합니다. 첫 번째로, on.pull_request.paths에서 모니터링할 경로를 지정합니다.

src/, tests/, package*.json 중 하나라도 변경되면 워크플로우가 트리거됩니다. 반대로 README.md나 docs/ 같은 파일만 변경되면 워크플로우 자체가 실행되지 않아 시간과 비용을 절약할 수 있습니다.

그 다음으로, test 작업의 if 조건을 확인합니다. github.event_name == 'pull_request'는 PR 이벤트인지 검증하고, !github.event.pull_request.draft는 draft PR이 아닌지 확인합니다.

draft PR은 아직 작업 중인 상태이므로 테스트를 스킵하는 것이 효율적입니다. deploy 작업은 더 복잡한 조건을 사용합니다.

멀티라인 문자열(|)로 여러 조건을 결합하는데, main 브랜치여야 하고 동시에 PR에 'deploy' 라벨이 붙어있어야 합니다. contains() 함수는 배열에서 특정 값을 찾는 데 사용됩니다.

만약 조건이 만족되지 않으면 어떻게 될까요? 작업은 "skipped" 상태로 표시되며, 리소스를 전혀 사용하지 않습니다.

워크플로우 전체가 성공으로 처리되므로 required checks에도 문제가 없습니다. 여러분이 이 패턴을 사용하면 CI/CD 비용을 30-50% 절감할 수 있습니다.

문서나 설정 변경 시 불필요한 대기 시간이 사라지고, 중요한 빌드가 빠르게 처리되며, GitHub Actions 무료 사용 한도를 더 효율적으로 활용할 수 있습니다.

실전 팁

💡 paths-ignore를 사용하면 특정 파일 변경 시 워크플로우를 실행하지 않도록 할 수 있습니다.

💡 && 와 || 연산자를 조합할 때는 괄호를 사용하여 우선순위를 명확히 하세요.

💡 dorny/paths-filter 액션을 사용하면 변경된 파일 그룹별로 다른 작업을 실행할 수 있습니다.

💡 github.event.head_commit.message를 검사하여 커밋 메시지에 [skip ci]가 있으면 스킵하는 패턴도 유용합니다.

💡 required status checks를 설정할 때는 조건부로 스킵되는 작업도 고려하여 필수 체크를 설정하세요.


7. 재사용 가능한 워크플로우 - DRY 원칙 적용하기

시작하며

여러분이 여러 리포지토리에서 동일한 테스트, 빌드, 배포 로직을 반복해서 복사-붙여넣기 하고 있나요? 하나의 워크플로우를 수정할 때마다 10개의 리포지토리를 모두 업데이트해야 하는 상황은 정말 비효율적입니다.

이런 문제는 마이크로서비스 아키텍처나 모노레포를 관리할 때 특히 심각합니다. 보안 패치나 새로운 최적화를 적용하려면 수십 개의 파일을 일일이 수정해야 하고, 하나라도 놓치면 일관성이 깨지게 됩니다.

바로 이럴 때 필요한 것이 재사용 가능한 워크플로우(Reusable Workflows)입니다. 공통 로직을 한 곳에 정의하고 여러 곳에서 호출하면 유지보수가 훨씬 쉬워집니다.

개요

간단히 말해서, 재사용 가능한 워크플로우는 다른 워크플로우에서 호출할 수 있는 템플릿 워크플로우입니다. workflow_call 트리거를 사용하여 워크플로우를 정의하고, inputs와 secrets를 매개변수로 받을 수 있습니다.

예를 들어, Node.js 애플리케이션의 표준 테스트 절차를 한 번 정의해두면, 모든 프로젝트에서 일관되게 사용할 수 있습니다. 기존에는 각 리포지토리마다 동일한 워크플로우를 복사했다면, 이제는 중앙 리포지토리에서 관리하고 버전을 지정하여 호출할 수 있습니다.

재사용 가능한 워크플로우의 핵심 장점은 세 가지입니다. 첫째, DRY(Don't Repeat Yourself) 원칙을 적용하여 중복을 제거합니다.

둘째, 한 곳에서 수정하면 모든 호출자에게 자동으로 반영됩니다. 셋째, 조직 전체의 CI/CD 표준을 강제할 수 있습니다.

코드 예제

# .github/workflows/reusable-test.yml
# 재사용 가능한 워크플로우 정의
name: Reusable Test Workflow
on:
  workflow_call:
    # 입력 매개변수 정의
    inputs:
      node-version:
        required: true
        type: string
      run-lint:
        required: false
        type: boolean
        default: true
    # 시크릿 매개변수
    secrets:
      NPM_TOKEN:
        required: false

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ inputs.node-version }}

      - run: npm ci
        env:
          NPM_TOKEN: ${{ secrets.NPM_TOKEN }}

      - name: Lint
        if: inputs.run-lint
        run: npm run lint

      - run: npm test

# .github/workflows/main.yml
# 재사용 가능한 워크플로우 호출
name: Main Workflow
on: push

jobs:
  call-test:
    uses: ./.github/workflows/reusable-test.yml
    with:
      node-version: '20'
      run-lint: true
    secrets:
      NPM_TOKEN: ${{ secrets.NPM_TOKEN }}

설명

이것이 하는 일: 공통 테스트 로직을 한 번 정의하고, 다른 워크플로우에서 Node 버전과 린트 실행 여부를 지정하여 재사용합니다. 첫 번째로, reusable-test.yml에서 workflow_call 트리거를 정의합니다.

이는 이 워크플로우가 직접 실행되는 것이 아니라 다른 워크플로우에서 호출될 것임을 의미합니다. inputs 섹션에서는 node-version(필수)과 run-lint(선택, 기본값 true)를 정의하고, secrets 섹션에서는 NPM_TOKEN을 받습니다.

그 다음으로, 워크플로우 내부에서 ${{ inputs.node-version }}처럼 전달받은 값을 사용합니다. 이렇게 하면 호출자가 원하는 Node.js 버전을 지정할 수 있습니다.

run-lint 입력에 따라 lint 단계를 조건부로 실행하므로, 린트가 필요 없는 프로젝트에서는 스킵할 수 있습니다. main.yml에서는 uses 키워드로 재사용 가능한 워크플로우를 호출합니다.

같은 리포지토리 내의 워크플로우는 ./.github/workflows/파일명.yml 형식으로, 다른 리포지토리의 워크플로우는 org/repo/.github/workflows/파일명.yml@ref 형식으로 호출할 수 있습니다. with 블록에서는 inputs를 전달하고, secrets 블록에서는 시크릿을 전달합니다.

시크릿은 자동으로 전달되지 않으므로 명시적으로 지정해야 합니다. 이는 보안을 위한 설계입니다.

여러분이 이 패턴을 사용하면 조직 전체의 CI/CD 품질이 향상됩니다. 베스트 프랙티스를 한 곳에서 관리하여 모든 프로젝트에 적용할 수 있고, 보안 패치나 성능 개선을 한 번에 배포할 수 있으며, 새로운 프로젝트를 시작할 때 검증된 워크플로우를 즉시 사용할 수 있습니다.

실전 팁

💡 재사용 가능한 워크플로우는 최대 4단계까지 중첩하여 호출할 수 있습니다.

💡 outputs를 정의하면 재사용 가능한 워크플로우에서 호출자에게 값을 반환할 수 있습니다.

💡 다른 조직의 public 리포지토리에서도 워크플로우를 호출할 수 있지만, 보안 검토가 필요합니다.

💡 버전 관리를 위해 태그나 브랜치를 명시적으로 지정하세요 (예: @v1, @main).

💡 .github 리포지토리를 만들어 조직 전체의 재사용 가능한 워크플로우를 중앙 관리할 수 있습니다.


8. 컨테이너 기반 작업 - 일관된 환경 보장하기

시작하며

여러분의 워크플로우가 로컬에서는 잘 돌아가는데 GitHub Actions에서는 실패하거나, 반대로 Actions에서는 성공하는데 로컬 개발 환경과 다른 결과가 나오는 경험을 해보셨나요? 환경 차이 때문에 발생하는 이런 문제는 디버깅하기도 어렵고 시간도 많이 낭비됩니다.

이런 문제는 시스템 라이브러리, 도구 버전, 환경 변수 등의 미묘한 차이 때문에 발생합니다. 특히 복잡한 빌드 도구 체인이나 네이티브 의존성을 가진 프로젝트에서는 더욱 심각하죠.

바로 이럴 때 필요한 것이 컨테이너 기반 작업입니다. Docker 이미지로 실행 환경을 완벽히 통제하면 로컬, CI, production 모두에서 동일한 환경을 보장할 수 있습니다.

개요

간단히 말해서, 컨테이너 기반 작업은 GitHub Actions 작업을 Docker 컨테이너 안에서 실행하여 환경 일관성을 보장하는 것입니다. container 키워드를 사용하면 작업 전체가 지정한 Docker 이미지 안에서 실행됩니다.

예를 들어, 특정 버전의 Python과 필요한 시스템 패키지가 미리 설치된 커스텀 이미지를 사용하면, 매번 동일한 환경에서 테스트할 수 있습니다. 기존에는 setup 액션으로 도구를 설치하고 환경을 구성했다면, 이제는 이미 완벽히 구성된 Docker 이미지를 가져와서 즉시 사용할 수 있습니다.

컨테이너 기반 작업의 핵심 장점은 세 가지입니다. 첫째, 완벽한 재현성으로 "내 컴퓨터에서는 되는데" 문제를 해결합니다.

둘째, 복잡한 의존성을 이미지에 미리 구축하여 워크플로우 실행 시간을 단축합니다. 셋째, 보안 격리로 신뢰할 수 없는 코드를 안전하게 실행할 수 있습니다.

코드 예제

name: Container Job
on: push

jobs:
  test-in-container:
    runs-on: ubuntu-latest
    # Docker 컨테이너에서 작업 실행
    container:
      image: node:20-alpine
      # 환경 변수 설정
      env:
        NODE_ENV: test
      # 컨테이너 옵션
      options: --cpus 2 --memory 4g

    steps:
      - uses: actions/checkout@v4

      # 컨테이너 안에서 실행됨
      - name: Install dependencies
        run: npm ci

      - name: Run tests
        run: npm test

  # 서비스 컨테이너와 함께 사용
  test-with-database:
    runs-on: ubuntu-latest
    container: node:20
    services:
      # PostgreSQL 서비스 컨테이너
      postgres:
        image: postgres:15
        env:
          POSTGRES_PASSWORD: postgres
        options: >-
          --health-cmd pg_isready
          --health-interval 10s

    steps:
      - uses: actions/checkout@v4
      - run: npm ci
      - name: Run integration tests
        env:
          DATABASE_URL: postgres://postgres:postgres@postgres:5432/test
        run: npm run test:integration

설명

이것이 하는 일: Node.js 애플리케이션을 Alpine Linux 기반 컨테이너에서 테스트하고, PostgreSQL과 통합 테스트를 수행합니다. 첫 번째로, test-in-container 작업은 node:20-alpine 이미지를 사용합니다.

이 이미지는 Node.js 20이 설치된 경량 Alpine Linux입니다. container.env로 NODE_ENV를 test로 설정하고, options로 CPU와 메모리 제한을 지정하여 리소스 사용을 통제합니다.

그 다음으로, 모든 steps가 이 컨테이너 안에서 실행됩니다. actions/checkout은 리포지토리를 컨테이너 내부로 체크아웃하고, npm ci와 npm test는 컨테이너의 Node.js 환경에서 실행됩니다.

이렇게 하면 로컬에서 docker run -it node:20-alpine으로 실행하는 것과 동일한 환경이 보장됩니다. test-with-database 작업은 더 복잡한 시나리오를 보여줍니다.

services 섹션에서 postgres 컨테이너를 정의하면, Docker 네트워크를 통해 작업 컨테이너에서 접근할 수 있습니다. 서비스 이름(postgres)이 호스트명으로 사용되므로, DATABASE_URL에서 @postgres:5432로 연결할 수 있습니다.

health-cmd와 health-interval 옵션은 PostgreSQL이 준비될 때까지 기다리는 헬스체크를 구성합니다. 이렇게 하면 데이터베이스가 완전히 시작되기 전에 테스트가 실행되어 실패하는 것을 방지할 수 있습니다.

여러분이 컨테이너를 사용하면 환경 관련 버그가 크게 줄어듭니다. 팀원들이 모두 동일한 환경에서 작업하게 되고, production과 개발 환경의 차이를 최소화할 수 있으며, 복잡한 설치 과정을 이미지에 미리 구축하여 CI 시간을 단축할 수 있습니다.

실전 팁

💡 컨테이너 작업은 Linux 러너에서만 지원되며, macOS와 Windows 러너에서는 사용할 수 없습니다.

💡 GitHub Container Registry(ghcr.io)에 커스텀 이미지를 푸시하면 빠르게 pull할 수 있고 비용도 절약됩니다.

💡 서비스 컨테이너는 작업이 끝나면 자동으로 정리되므로, 데이터 유출을 걱정하지 않아도 됩니다.

💡 volumes 옵션으로 호스트와 컨테이너 간 파일을 공유할 수 있지만, 보안에 주의하세요.

💡 멀티 스테이지 Docker 빌드를 사용하면 이미지 크기를 줄여 pull 시간을 단축할 수 있습니다.


9. 아티팩트 관리 - 빌드 결과물 공유하기

시작하며

여러분이 빌드한 결과물을 다음 작업에서 사용하거나, 디버깅을 위해 다운로드하거나, 릴리스에 첨부하고 싶을 때 어떻게 하시나요? 워크플로우의 각 작업은 독립적인 러너에서 실행되기 때문에, 파일을 공유하는 방법이 필요합니다.

이런 문제는 빌드와 배포를 분리하거나, 테스트 커버리지 리포트를 저장하거나, 여러 환경용 바이너리를 생성할 때 발생합니다. 임시 파일을 git에 커밋하는 것은 리포지토리를 오염시키고, 외부 스토리지를 사용하는 것은 복잡합니다.

바로 이럴 때 필요한 것이 GitHub Actions의 아티팩트 기능입니다. 빌드 결과물을 안전하게 저장하고 다른 작업이나 워크플로우에서 사용할 수 있습니다.

개요

간단히 말해서, 아티팩트는 워크플로우 실행 중 생성된 파일을 저장하고 공유하는 메커니즘입니다. actions/upload-artifact로 파일을 업로드하면 GitHub 서버에 저장되며, 같은 워크플로우의 다른 작업이나 나중에 실행되는 워크플로우에서 actions/download-artifact로 다운로드할 수 있습니다.

예를 들어, 빌드 작업에서 생성한 dist/ 디렉토리를 업로드하고, 배포 작업에서 다운로드하여 사용할 수 있습니다. 기존에는 빌드와 배포를 하나의 작업에서 처리해야 했다면, 이제는 분리하여 빌드는 빠른 러너에서, 배포는 특정 권한이 있는 러너에서 실행할 수 있습니다.

아티팩트의 핵심 특징은 세 가지입니다. 첫째, 작업 간 데이터 전달의 표준 방법을 제공합니다.

둘째, 90일(기본값)동안 보관되어 나중에 다운로드할 수 있습니다. 셋째, 압축과 병렬 업로드로 빠르고 효율적입니다.

코드 예제

name: Build and Deploy
on: push

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'

      - run: npm ci
      - run: npm run build

      # 빌드 결과물 업로드
      - name: Upload build artifacts
        uses: actions/upload-artifact@v4
        with:
          name: dist-files
          path: dist/
          retention-days: 30  # 30일 보관

  test-artifacts:
    needs: build  # build 작업 완료 후 실행
    runs-on: ubuntu-latest
    steps:
      # 아티팩트 다운로드
      - name: Download build artifacts
        uses: actions/download-artifact@v4
        with:
          name: dist-files
          path: dist/

      # 다운로드한 파일 사용
      - name: Test artifacts
        run: ls -la dist/

  deploy:
    needs: build
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'
    steps:
      - uses: actions/download-artifact@v4
        with:
          name: dist-files
          path: dist/

      - name: Deploy to production
        run: |
          # dist/ 디렉토리의 파일들 배포
          echo "Deploying files..."

설명

이것이 하는 일: 빌드 작업에서 생성한 dist/ 디렉토리를 아티팩트로 저장하고, 테스트와 배포 작업에서 다운로드하여 사용합니다. 첫 번째로, build 작업에서 npm run build를 실행하여 dist/ 디렉토리에 빌드 결과물을 생성합니다.

그 다음 actions/upload-artifact@v4를 사용하여 이 디렉토리를 "dist-files"라는 이름의 아티팩트로 업로드합니다. retention-days를 30으로 설정하여 30일 동안만 보관하도록 합니다(기본값은 90일).

그 다음으로, test-artifacts 작업은 needs: build로 빌드 작업이 완료될 때까지 기다립니다. 빌드가 성공하면 actions/download-artifact@v4로 "dist-files" 아티팩트를 다운로드합니다.

path를 지정하면 해당 경로에 파일이 복원됩니다. deploy 작업도 동일하게 needs: build를 사용하지만, 추가로 if 조건으로 main 브랜치일 때만 실행되도록 제한합니다.

이렇게 하면 빌드는 모든 브랜치에서 실행되지만, 배포는 main에서만 수행됩니다. 중요한 점은 각 작업이 독립적인 러너에서 실행되므로, 아티팩트 없이는 파일을 공유할 수 없다는 것입니다.

아티팩트는 GitHub 서버를 중간 저장소로 사용하여 이 문제를 해결합니다. 여러분이 아티팩트를 사용하면 워크플로우를 더 유연하게 설계할 수 있습니다.

빌드를 한 번만 수행하고 여러 환경에 배포할 수 있으며, 실패한 빌드의 결과물을 다운로드하여 로컬에서 디버깅할 수 있고, 테스트 리포트나 커버리지를 자동으로 보관할 수 있습니다.

실전 팁

💡 와일드카드 패턴을 사용하여 여러 파일을 선택할 수 있습니다 (예: path: 'dist/**/*.js').

💡 if-no-files-found 옵션을 warn 또는 error로 설정하면 파일이 없을 때 명확한 피드백을 받을 수 있습니다.

💡 대용량 아티팩트는 스토리지 용량을 빠르게 소모하므로, retention-days를 짧게 설정하세요.

💡 actions/download-artifact에서 name을 생략하면 모든 아티팩트를 다운로드합니다.

💡 GitHub CLI의 gh run download 명령으로 로컬에서도 아티팩트를 다운로드할 수 있습니다.


10. 동시성 제어 - 중복 실행 방지하기

시작하며

여러분이 빠르게 여러 커밋을 푸시했을 때, 동일한 워크플로우가 여러 개 동시에 실행되면서 리소스를 낭비하거나, 배포가 동시에 여러 번 시도되어 충돌이 발생한 경험 있나요? 특히 배포 워크플로우는 동시에 실행되면 안 되는 경우가 많습니다.

이런 문제는 활발한 개발이 진행되는 프로젝트에서 흔히 발생합니다. 여러 팀원이 동시에 작업하거나, CI/CD 파이프라인이 긴 경우 대기열에 워크플로우가 쌓이면서 불필요한 비용이 발생하고 실제 필요한 작업이 지연됩니다.

바로 이럴 때 필요한 것이 동시성 제어(Concurrency Control)입니다. 동일한 그룹의 워크플로우가 중복 실행되지 않도록 하거나, 이전 실행을 취소하고 최신 것만 실행할 수 있습니다.

개요

간단히 말해서, 동시성 제어는 특정 그룹의 워크플로우가 동시에 여러 개 실행되지 않도록 제한하는 메커니즘입니다. concurrency 키워드로 그룹을 정의하고, cancel-in-progress 옵션으로 동작을 제어합니다.

예를 들어, PR별로 그룹을 만들면 같은 PR에 새로운 커밋이 푸시될 때 이전 워크플로우를 취소하고 최신 커밋만 테스트할 수 있습니다. 기존에는 모든 커밋이 독립적으로 워크플로우를 실행하여 대기열이 길어지고 최신 결과를 보기까지 오래 걸렸다면, 이제는 불필요한 실행을 자동으로 취소하여 빠른 피드백을 받을 수 있습니다.

동시성 제어의 핵심 전략은 두 가지입니다. 첫째, PR이나 브랜치별로 그룹을 나누어 관련된 워크플로우만 제한합니다.

둘째, cancel-in-progress를 상황에 맞게 설정하여 테스트는 취소하지만 배포는 대기시킵니다. 이를 통해 효율성과 안전성을 모두 확보할 수 있습니다.

코드 예제

name: CI with Concurrency
on:
  push:
    branches: [main]
  pull_request:

# 워크플로우 레벨 동시성 제어
concurrency:
  # PR별로 그룹 생성, 푸시는 브랜치별
  group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
  # 진행 중인 실행 취소
  cancel-in-progress: true

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm ci
      - run: npm test

  deploy:
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'
    # 배포는 별도 동시성 그룹
    concurrency:
      group: production-deploy
      # 배포는 취소하지 않고 대기
      cancel-in-progress: false
    steps:
      - uses: actions/checkout@v4
      - run: npm run build
      - run: npm run deploy

설명

이것이 하는 일: PR에서는 최신 커밋만 테스트하고 이전 실행은 취소하며, 배포는 순차적으로 실행되도록 보장합니다. 첫 번째로, 워크플로우 레벨의 concurrency.group을 설정합니다.

${{ github.workflow }}는 워크플로우 이름이고, ${{ github.event.pull_request.number || github.ref }}는 PR 번호 또는 브랜치 참조입니다. 이렇게 하면 "CI with Concurrency-123" (PR #123) 또는 "CI with Concurrency-refs/heads/feature-branch" 같은 그룹이 생성됩니다.

그 다음으로, cancel-in-progress: true로 설정하여 동일한 그룹의 새로운 워크플로우가 시작되면 진행 중인 것을 취소합니다. 예를 들어, PR에 첫 커밋을 푸시하여 워크플로우가 실행 중일 때 두 번째 커밋을 푸시하면, 첫 번째 워크플로우는 즉시 취소되고 두 번째만 실행됩니다.

deploy 작업은 별도의 concurrency 그룹을 사용합니다. production-deploy 그룹은 워크플로우와 무관하게 전역적이므로, 여러 워크플로우에서 배포를 시도해도 한 번에 하나씩만 실행됩니다.

cancel-in-progress: false이므로 취소되지 않고 대기열에서 순서를 기다립니다. 만약 cancel-in-progress 없이 concurrency만 설정하면 어떻게 될까요?

새로운 워크플로우는 "Pending" 상태로 대기하고, 이전 워크플로우가 완료되면 순차적으로 실행됩니다. 이는 배포처럼 순서가 중요한 경우에 유용합니다.

여러분이 동시성 제어를 사용하면 CI/CD가 훨씬 효율적으로 동작합니다. 불필요한 워크플로우 실행이 줄어들어 비용이 절감되고, 최신 코드의 결과를 더 빠르게 확인할 수 있으며, 배포 충돌을 방지하여 안정성이 향상됩니다.

실전 팁

💡 동시성 그룹 이름에 리포지토리 이름을 포함하면 모노레포에서 서브 프로젝트별로 독립적인 그룹을 만들 수 있습니다.

💡 배포 작업은 항상 cancel-in-progress: false를 사용하여 중간에 취소되지 않도록 하세요.

💡 긴 실행 시간의 워크플로우(예: 통합 테스트)에서 동시성 제어를 사용하면 큰 비용 절감 효과가 있습니다.

💡 github.head_ref를 사용하면 PR의 헤드 브랜치 이름으로 그룹을 만들 수 있지만, null일 수 있으니 폴백을 설정하세요.

💡 동시성 제어로 취소된 워크플로우는 "Cancelled" 상태로 표시되며, required checks에서 실패로 처리되지 않습니다.


#GitHub Actions#CI/CD#Workflow#Debugging#DevOps#JavaScript

댓글 (0)

댓글을 작성하려면 로그인이 필요합니다.