
GitHub Actions 실전 가이드: 나만의 자동화 파이프라인 만들기
Push만 하면 테스트·빌드·배포가 알아서 돌아간다. GitHub Actions의 개념부터 YAML 문법, 실전 파이프라인 구축까지. 코드 저장소에 자동화를 내장하는 방법을 처음부터 끝까지 풀어본다.

Push만 하면 테스트·빌드·배포가 알아서 돌아간다. GitHub Actions의 개념부터 YAML 문법, 실전 파이프라인 구축까지. 코드 저장소에 자동화를 내장하는 방법을 처음부터 끝까지 풀어본다.
개발자에게는 오래된 꿈이 있다.
코드를 작성하고, git push를 누르면 — 테스트가 자동으로 돌아가고, 빌드가 만들어지고, 스테이징 서버에 배포되고, Slack에 결과가 날아온다. 수동으로 서버에 접속해서 git pull을 치고, npm run build를 치고, PM2를 재시작하는 시대는 이미 끝났다.
이것을 CI/CD(Continuous Integration / Continuous Delivery)라고 부른다.
CI/CD는 새로운 개념이 아니다. Jenkins, Travis CI, CircleCI 등 많은 도구가 있었지만, 대부분 별도의 서비스 가입과 서버 운영이 필요했다.
2019년 11월, GitHub이 이 판을 뒤집었다. GitHub Actions — .github/workflows/ 폴더에 YAML 파일 하나만 넣으면 되는 CI/CD 플랫폼.
2026년 현재, GitHub Actions는 오픈소스 프로젝트의 사실상 표준 CI/CD 도구가 됐다. 이 글에서는 개념부터 YAML 문법, 트리거 종류, 실전 배포 파이프라인까지 — 처음 접하는 사람도 자신만의 자동화 파이프라인을 만들 수 있도록 풀어본다.

GitHub Actions를 이해하려면 5가지 개념을 알아야 한다. 이 5개가 전부다.
Workflow는 자동화 전체를 정의하는 YAML 파일이다. .github/workflows/에 저장하며, ci.yml, deploy.yml 등 여러 개를 만들 수 있다.
Job은 워크플로우 안의 독립적 실행 단위다. 기본적으로 병렬 실행되며, needs로 순서를 지정한다. 각 Job은 별도의 Runner에서 실행되므로 Job 간 파일 시스템은 공유되지 않는다.
Step은 Job 안에서 순차 실행되는 개별 명령이다. run으로 쉘 명령을, uses로 외부 Action을 실행한다. 같은 Job의 Step들은 파일 시스템을 공유한다.
Action은 재사용 가능한 코드 묶음이다. uses: actions/checkout@v4처럼 소유자/이름@버전으로 참조한다.
Runner는 워크플로우가 실행되는 서버다. GitHub-hosted(Ubuntu, Windows, macOS)를 쓰거나, Self-hosted Runner를 등록할 수 있다.
GitHub Actions 워크플로우는 YAML 형식으로 작성한다. 들여쓰기로 구조를 표현하는 데이터 직렬화 언어다.
name: Hello World
on: push
jobs:
greet:
runs-on: ubuntu-latest
steps:
- name: Say hello
run: echo "Hello, GitHub Actions!"
이 7줄짜리 파일만으로도 동작하는 워크플로우가 완성된다.
push, pull_request, schedule 등.
ubuntu-latest, windows-latest, macos-latest.
run은 쉘 명령, uses는 외부 Action.
name: CI Pipeline
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4 # 코드 체크아웃 (거의 모든 워크플로우에 필수)
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm test
env:
NODE_ENV: test # 환경 변수 설정
deploy:
needs: build # build Job이 성공한 후에 실행
runs-on: ubuntu-latest
steps:
- name: Deploy
run: echo "Deploying..."
| 구분 | uses | run |
|---|---|---|
| 역할 | 외부 Action을 가져와 실행 | 쉘 명령을 직접 실행 |
| 형식 | uses: owner/repo@version | run: npm ci && npm test |
| 입력값 | with: 키워드로 전달 | 쉘 변수나 env:로 전달 |
| 적합한 경우 | 복잡한 로직을 재사용할 때 | 간단한 명령을 실행할 때 |
on 키워드 아래에 정의하는 트리거는 워크플로우의 시작 조건이다. 실전에서 자주 쓰는 4가지를 알아보자.
on:
push:
branches: [main, develop]
paths:
- 'src/**' # src 폴더가 변경됐을 때만
- '!src/**/*.test.ts' # 테스트 파일 변경은 제외
paths 필터로 특정 파일이 변경됐을 때만 실행하게 할 수 있다.
on:
pull_request:
branches: [main]
PR 코드 리뷰 전에 자동으로 테스트를 돌리는 데 사용한다.
on:
schedule:
- cron: '0 0 * * *' # 매일 자정(UTC) 실행
┌───── 분 (0~59)
│ ┌───── 시 (0~23)
│ │ ┌───── 일 (1~31)
│ │ │ ┌───── 월 (1~12)
│ │ │ │ ┌───── 요일 (0~6, 0=일요일)
│ │ │ │ │
* * * * *
0 0 * * * → 매일 자정(UTC)
0 */6 * * * → 6시간마다
30 9 * * 1-5 → 평일 오전 9:30(UTC)
0 0 1 * * → 매월 1일 자정
주의: UTC 기준이므로 한국 시간(KST = UTC+9)으로 환산해야 한다.
on:
workflow_dispatch:
inputs:
environment:
type: choice
options: [staging, production]
GitHub UI에서 수동으로 실행하며 입력값을 받을 수 있다. 긴급 배포에 유용하다.
이론은 충분하다. 가장 기본적이면서도 가장 중요한 워크플로우 — PR마다 lint와 테스트를 자동으로 돌리는 CI 파이프라인을 만든다.
name: CI
on:
pull_request:
branches: [main]
push:
branches: [main]
jobs:
lint:
name: Lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
- run: npm ci
- run: npm run lint
test:
name: Test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
- run: npm ci
- run: npm test
typecheck:
name: Type Check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
- run: npm ci
- run: npx tsc --noEmit
needs가 없으므로 3개 Job이 병렬 실행되어 전체 시간을 단축한다. cache: 'npm'으로 의존성 설치를 빠르게 하고, npm ci로 package-lock.json 기반의 재현 가능한 빌드를 보장한다.
Branch Protection Rule과 함께 사용하면 더 강력하다. Settings → Branches에서 "Require status checks to pass"를 활성화하면, 하나라도 실패한 PR은 머지할 수 없다.
CI/CD 파이프라인에서는 API 키, 배포 토큰 같은 민감한 정보를 다뤄야 한다. GitHub Actions는 이를 위해 Secrets 기능을 제공한다.
GitHub 리포지토리 → Settings → Secrets and variables → Actions → New repository secret에서 등록한다. 한 번 저장하면 값을 다시 볼 수 없고, 로그에도 ***로 마스킹된다.
steps:
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: ap-northeast-2
Environment 기능으로 staging/production을 분리할 수 있다. environment: production을 설정하면 required reviewers(필수 승인자)를 지정할 수 있어, 프로덕션 배포 전 승인을 요구할 수 있다.
jobs:
deploy-staging:
environment: staging
runs-on: ubuntu-latest
steps:
- run: ./deploy.sh
env:
DB_PASSWORD: ${{ secrets.DB_PASSWORD }}
deploy-production:
needs: deploy-staging
environment: production
runs-on: ubuntu-latest
steps:
- run: ./deploy.sh

"Node.js 18에서는 되는데 20에서는 안 된다" — 이런 문제를 방지하려면 여러 환경에서 동시에 테스트해야 한다. Matrix Strategy가 이것을 자동화한다.
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [18, 20, 22]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- run: npm ci
- run: npm test
이 설정 하나로 3개의 Job이 자동 생성되어 병렬 실행된다. OS와 버전을 조합하면 다차원 Matrix도 가능하다 — os: [ubuntu-latest, windows-latest] + node-version: [18, 20, 22]면 6개 Job이 생성된다. exclude로 특정 조합을 제외할 수도 있다.
strategy: fail-fast: false로 설정하면 실패한 조합과 관계없이 모든 조합이 끝까지 실행된다. 어떤 버전에서 실패하는지 전부 확인하고 싶을 때 유용하다.
CI가 느리면 아무도 안 쓴다. 캐싱은 CI 속도를 개선하는 가장 효과적인 방법이다.
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm' # 또는 'yarn', 'pnpm'
이 한 줄로 ~/.npm 캐시가 자동 저장/복원된다.
더 세밀한 제어가 필요하면 actions/cache를 직접 사용한다.
- name: Cache node_modules
uses: actions/cache@v4
with:
path: node_modules
key: ${{ runner.os }}-node-${{ hashFiles('package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
hashFiles('package-lock.json') — package-lock.json이 바뀔 때만 새 캐시가 만들어진다. 의존성이 변하지 않았으면 캐시를 재활용한다.
Docker 이미지를 빌드하는 워크플로우에서는 Docker 레이어 캐시도 중요하다. docker/build-push-action의 cache-from: type=gha, cache-to: type=gha,mode=max 옵션으로 GitHub Actions 캐시 스토리지를 Docker BuildKit 캐시로 활용하면 이미지 빌드 시간이 50~80% 단축된다.
컨테이너 배포 파이프라인 — Docker 이미지를 빌드하고, AWS ECR에 푸시한 뒤, ECS 서비스를 업데이트한다.
name: Deploy to ECS
on:
push:
branches: [main]
env:
AWS_REGION: ap-northeast-2
ECR_REPOSITORY: myapp
ECS_SERVICE: myapp-service
ECS_CLUSTER: production
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 20, cache: 'npm' }
- run: npm ci && npm test
deploy:
needs: test
runs-on: ubuntu-latest
environment: production
steps:
- uses: actions/checkout@v4
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: ${{ env.AWS_REGION }}
- name: Login to ECR
id: login-ecr
uses: aws-actions/amazon-ecr-login@v2
- name: Build and push image
id: build-image
run: |
IMAGE=${{ steps.login-ecr.outputs.registry }}/$ECR_REPOSITORY:${{ github.sha }}
docker build -t $IMAGE .
docker push $IMAGE
echo "image=$IMAGE" >> $GITHUB_OUTPUT
- name: Update task definition
id: task-def
uses: aws-actions/amazon-ecs-render-task-definition@v1
with:
task-definition: task-definition.json
container-name: myapp
image: ${{ steps.build-image.outputs.image }}
- name: Deploy to ECS
uses: aws-actions/amazon-ecs-deploy-task-definition@v2
with:
task-definition: ${{ steps.task-def.outputs.task-definition }}
service: ${{ env.ECS_SERVICE }}
cluster: ${{ env.ECS_CLUSTER }}
wait-for-service-stability: true
github.sha(커밋 해시)를 이미지 태그로 사용하면 모든 배포를 추적할 수 있다. wait-for-service-stability: true로 배포 실패 시 워크플로우도 실패로 표시된다.
role-to-assume 옵션으로 IAM Role을 직접 assume하면 장기 자격증명 없이 배포할 수 있다.정적 사이트를 S3에 업로드하고 CloudFront 캐시를 무효화하는 파이프라인이다.
name: Deploy Static Site
on:
push:
branches: [main]
paths: ['src/**', 'public/**', 'package.json']
jobs:
deploy:
runs-on: ubuntu-latest
environment: production
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 20, cache: 'npm' }
- run: npm ci && npm run build
- name: Configure AWS
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
aws-region: ap-northeast-2
- name: Sync hashed assets (long cache)
run: |
aws s3 sync out/ s3://${{ vars.S3_BUCKET }} --delete \
--cache-control "public, max-age=31536000, immutable" \
--exclude "*.html" --exclude "*.json"
- name: Sync HTML/JSON (no cache)
run: |
aws s3 sync out/ s3://${{ vars.S3_BUCKET }} \
--cache-control "public, max-age=0, must-revalidate" \
--exclude "*" --include "*.html" --include "*.json"
- name: Invalidate CloudFront
run: |
aws cloudfront create-invalidation \
--distribution-id ${{ vars.CLOUDFRONT_DIST_ID }} --paths "/*"
S3 동기화를 두 번 하는 이유:
| 파일 종류 | Cache-Control | 이유 |
|---|---|---|
| JS, CSS, 이미지 (해시 포함) | max-age=31536000, immutable | 파일명에 해시가 있어 내용 변경 시 파일명도 바뀜. 영구 캐싱 가능 |
| HTML, JSON | max-age=0, must-revalidate | 항상 같은 URL이므로 최신 버전을 받아야 함 |
대부분의 일반적인 작업은 이미 Marketplace에 Action으로 만들어져 있다.
| 카테고리 | Action | 용도 |
|---|---|---|
| 기본 | actions/checkout@v4 | 코드 체크아웃 |
| 언어 | actions/setup-node@v4 | Node.js 설정 + 캐시 |
| 캐시 | actions/cache@v4 | 의존성 캐싱 |
| Docker | docker/build-push-action@v5 | 이미지 빌드 + 푸시 |
| AWS | aws-actions/configure-aws-credentials@v4 | AWS 자격증명 (OIDC) |
| 보안 | github/codeql-action/analyze@v3 | 코드 취약점 분석 |
Action을 참조할 때는 @v4 같은 메이저 버전 태그를 사용하라. @main은 호환성이 깨질 수 있어 위험하다. .github/dependabot.yml에 package-ecosystem: "github-actions"를 설정하면 새 버전이 나올 때 자동으로 업데이트 PR이 생성된다.
GitHub-hosted Runner는 편리하지만 한계가 있다.
| 항목 | GitHub-hosted | Self-hosted |
|---|---|---|
| 관리 | GitHub이 관리 | 직접 관리 |
| 비용 | 공개 리포: 무료 / 비공개: 월 2,000분 | 서버 비용만 |
| 스펙 | 2 vCPU, 7GB RAM | 원하는 만큼 |
| 네트워크 | 공용 IP (내부망 접근 불가) | VPC 내부, 사내 DB 접근 가능 |
| GPU | 불가 | 가능 (ML 학습, CUDA 빌드) |
| 깨끗한 환경 | 매번 새 VM | 직접 정리 필요 |
설정은 간단하다. Settings → Actions → Runners에서 설치 스크립트가 제공되고, 워크플로우에서는 runs-on: [self-hosted, linux, gpu]처럼 라벨로 지정한다.
steps:
- name: Deploy to production
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
run: ./deploy.sh
- name: Notify on failure
if: failure()
run: curl -X POST ${{ secrets.SLACK_WEBHOOK_URL }} ...
concurrency:
group: deploy-${{ github.ref }}
cancel-in-progress: true
같은 브랜치에서 여러 번 push하면 이전 워크플로우를 취소하고 최신 것만 실행한다.
jobs:
test:
timeout-minutes: 15
기본 타임아웃은 6시간이다. 반드시 적절한 값을 설정하라.
여러 리포지토리에서 같은 CI를 쓴다면 on: workflow_call로 재사용 가능한 워크플로우를 만들고, 다른 리포에서 uses: my-org/shared-workflows/.github/workflows/reusable-ci.yml@main으로 호출할 수 있다.
permissions: contents: read만 필요한데 write-all을 주지 마라.
@main은 절대 금지.
Free 플랜은 월 2,000분, Team은 3,000분, Enterprise는 50,000분이 무료다. Windows는 2배, macOS는 10배 비용이므로 ubuntu-latest가 가장 저렴하다. 공개 리포지토리는 완전 무료. paths 필터, concurrency, 캐싱, timeout-minutes를 적극 활용해서 낭비를 줄여라.
GitHub Actions는 단순한 CI/CD 도구가 아니다. 코드 저장소에 자동화를 내장하는 플랫폼이다.
시작은 작게 하라. PR에 lint 하나 돌리는 것부터 시작해서, 테스트를 추가하고, 배포를 자동화하고, 점진적으로 확장하면 된다. .github/workflows/ci.yml 파일 하나가 팀 전체의 개발 문화를 바꿀 수 있다.
코드는 혼자 배포되지 않는다. 하지만 자동화를 설정해 놓으면, Push 한 번으로 세상에 나갈 수 있다.
GitHub Actions 고급 주제(Composite Action, OIDC 상세 설정, Terraform 연동, 모노레포 전략 등)는 후속 글에서 다룰 예정입니다.