GitHub Actions의 설계 철학 — 격리, 계층, 명시성
YAML 파싱부터 OIDC 인증까지, GitHub Actions의 모든 핵심 메커니즘이 공유하는 하나의 원칙을 추적한다.
- 01 CI/CD는 자동화 도구인가, 운영 철학인가
- 02 GitHub Actions의 설계 철학 — 격리, 계층, 명시성
- 03 Docker 이미지 빌드, 순서가 전부다
- 04 배포 전략의 진짜 선택 기준은 무엇인가
- 05 GitOps는 배포 도구가 아니라 감사 시스템이다
- 06 CI/CD 파이프라인은 왜 느려지는가
- 07 CI/CD 파이프라인의 다섯 가지 신호
GitHub Actions는 YAML 몇 줄로 CI/CD를 구성하는 도구처럼 보인다. 하지만 ${{ }} 표현식이 언제 평가되는지, Secret은 왜 환경변수로 전달해야 마스킹이 보장되는지, 캐시 키에 왜 lock 파일 해시를 넣어야 하는지 — 이 질문들에 막히는 순간, 그 뒤에 하나의 일관된 설계 철학이 있다는 걸 알게 된다. GitHub Actions의 모든 결정은 어떤 원칙에서 왔는가?
계층이 우선순위다
GitHub Actions의 거의 모든 개념은 계층 구조를 따른다. 환경변수는 Workflow → Job → Step 순으로 우선순위가 높아진다. Job 간 의존성은 needs:로 DAG를 구성하고, Runner는 의존성이 없는 Job을 자동으로 병렬 실행한다. Secret도 마찬가지다 — Repository Secret은 모든 워크플로우에서 접근 가능하고, Environment Secret은 해당 Environment를 지정한 Job에서만 열린다.
이 계층은 단순한 스코핑 규칙이 아니다. 명시적으로 좁힐수록 더 안전하고, 더 예측 가능하다는 철학의 표현이다. Step-level env:는 가장 좁고, 가장 명확하다. Workflow-level env:는 넓고, 하위 Step에서 덮어쓰면 혼란이 생긴다.
env:
LOG_LEVEL: info # Workflow: 가장 낮은 우선순위
jobs:
test:
env:
LOG_LEVEL: debug # Job: 덮어씀
steps:
- env:
LOG_LEVEL: verbose # Step: 이 Step에서만 최우선
run: echo $LOG_LEVEL # verbose
Job DAG도 같은 원칙이다. needs:를 명시하지 않으면 병렬, 명시하면 직렬. 설계자가 의존성을 명시적으로 표현해야 한다는 요구가 곧 병렬화 전략이 된다.
표현식은 어디서 평가되는가
${{ }} 표현식이 모두 같은 시점에 평가된다고 가정하면 디버깅이 불가능해진다. GitHub Actions는 평가 시점을 두 곳으로 나눈다.
서버 측 평가: on: 필터, Job-level if: 조건. 이 값들은 Runner가 할당되기 전, GitHub 서버에서 처리된다. on: push: branches:에 ${{ }} 표현식을 넣어도 작동하지 않는 이유가 여기 있다.
Runner 측 평가: run: 내부의 표현식, Step-level if: 조건. 실제 Runner 머신에서 Step 실행 직전에 평가된다.
jobs:
build:
if: github.event_name == 'push' # 서버에서 평가
steps:
- run: echo "${{ github.sha }}" # Runner에서 평가
이 구분이 Secret 마스킹의 핵심이기도 하다. run: echo "${{ secrets.TOKEN }}" 패턴은 표현식이 먼저 평가되어 실제 값이 커맨드 문자열에 박힌 뒤 실행되므로, Runner의 마스킹 로직이 개입할 여지가 줄어든다. 반면 환경변수로 전달하면 Runner 에이전트가 Secret 값을 마스킹 리스트에 등록한 뒤 모든 출력을 필터링한다.
# 안전한 패턴
- env:
TOKEN: ${{ secrets.API_TOKEN }}
run: curl -H "Authorization: $TOKEN" https://api.example.com
신뢰되지 않은 입력(github.event.head_commit.message, github.event.client_payload.*)을 run: 내부에 직접 ${{ }}로 삽입하면 임의 명령 실행이 가능하다. 반드시 환경변수로 격리하라.
Action 타입은 격리 수준의 선택이다
uses: 키워드로 호출하는 Action은 세 종류가 있고, 각각 격리 수준과 성능이 다르다.
Docker Action: 컨테이너로 완전 격리. 이미지 풀 + 컨테이너 시작 오버헤드가 3~13초다. Python, Ruby 등 Node.js가 아닌 런타임이 필요할 때 선택한다.
JavaScript Action: Node.js 프로세스로 직접 실행. 오버헤드가 0.1~0.5초다. Host Runner와 같은 환경에서 실행되므로 격리는 없지만, 대부분의 용도에서 기본 선택지다.
Composite Action: 현재 Job의 Step으로 직접 실행. 오버헤드가 거의 없다. 여러 Step을 재사용 가능한 단위로 묶을 때 사용한다.
Reusable Workflow는 다르다. 완전히 새로운 Job으로 실행되어 Job 경계의 격리가 생긴다. Composite Action이 “현재 Job에 Step을 추가”한다면, Reusable Workflow는 “새 Job을 추가”한다.
Docker Action은 격리가 강하지만 느리고 비싸다(JavaScript의 약 30배 오버헤드). JavaScript Action은 빠르지만 Host Runner 환경에 의존한다. Composite Action은 가장 빠르지만 복잡한 로직 구현에는 한계가 있다. 격리 필요성과 속도 요구를 함께 고려해 선택해야 한다.
캐시 키는 결정론적이어야 한다
캐시가 동작하는 원리는 단순하다 — 정확한 키 매칭이 먼저, 그 다음 restore-keys 프리픽스 매칭이다. 하지만 키를 잘못 설계하면 캐시가 있어도 매번 미스가 난다.
정적 키(key: npm-cache)는 의존성이 바뀌어도 같은 캐시를 재사용하므로 오래된 의존성이 남는다. github.run_id를 키에 포함하면 매번 다른 키가 생겨 캐시가 절대 적중하지 않는다. 올바른 설계는 lock 파일의 해시를 키에 포함하는 것이다.
- uses: actions/cache@v3
with:
path: ~/.npm
key: npm-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
npm-${{ runner.os }}-
restore-keys는 계층화된 폴백이다. package-lock.json이 바뀌어 정확한 키 매칭이 실패하면, npm-Linux- 프리픽스로 시작하는 가장 최신 캐시를 복구한다. 전부 다운로드하는 것보다 부분 캐시를 복구하는 게 낫다.
actions/setup-java나 actions/setup-node의 cache: 파라미터는 이 패턴을 내부에 자동으로 구성한다. 커스터마이징이 필요 없다면 이 옵션을 먼저 쓰는 게 맞다.
OIDC — 자격증명을 저장하지 않는 인증
장기 자격증명(AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY)을 Secret에 저장하는 방식은 두 가지 문제가 있다. 수동 로테이션이 필요하고, 유출되면 유효기간 동안 악용될 수 있다.
OIDC는 이 문제를 구조적으로 해결한다. GitHub이 OIDC 토큰을 발급하고, 클라우드 프로바이더(AWS STS, GCP 등)가 그 토큰을 검증해 임시 자격증명을 발급한다. 저장할 장기 자격증명이 없다.
jobs:
deploy:
permissions:
id-token: write # OIDC 토큰 발급 권한
steps:
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789:role/github-actions-role
aws-region: us-east-1
OIDC 토큰에는 sub(어떤 레포지토리, 어떤 브랜치), actor(누가 실행), workflow 같은 클레임이 포함된다. AWS의 Trust Policy에서 sub 조건을 반드시 명시해야 한다 — 없으면 GitHub의 모든 레포지토리가 해당 역할을 Assume할 수 있다.
정리
${{ }}의 평가 시점은 서버(트리거/Job 조건)와 Runner(Step 실행)로 나뉜다. 이 구분이 Secret 마스킹과 표현식 인젝션 방어의 기반이다.- 환경변수, Secret 범위, Job 의존성 — 모두 “좁을수록 명확하고 안전하다”는 계층 원칙을 따른다.
- 캐시 키는 lock 파일 해시를 포함해야 결정론적이다.
restore-keys로 계층화된 폴백을 구성하라. - OIDC는 장기 자격증명을 제거하는 구조적 해결책이다. Trust Policy의
sub조건을 반드시 명시하라.
다음 글에서는 Docker 빌드 최적화 — 레이어 캐시 원리와 멀티스테이지 빌드가 빌드 시간을 어떻게 바꾸는지 추적한다.