
GitHub Actions 실제: CI/CD 파이프라인 직접 구축하기
매번 수동으로 빌드하고 배포하다가 실수로 버그를 프로덕션에 올렸다. GitHub Actions로 자동화한 후 그런 실수가 사라졌다.

매번 수동으로 빌드하고 배포하다가 실수로 버그를 프로덕션에 올렸다. GitHub Actions로 자동화한 후 그런 실수가 사라졌다.
서버를 끄지 않고 배포하는 법. 롤링, 카나리, 블루-그린의 차이점과 실제 구축 전략. DB 마이그레이션의 난제(팽창-수축 패턴)와 AWS CodeDeploy 활용법까지 심층 분석합니다.

ChatGPT는 질문에 답하지만, AI Agent는 스스로 계획하고 도구를 사용해 작업을 완료한다. 이 차이가 왜 중요한지 정리했다.

새벽엔 낭비하고 점심엔 터지는 서버 문제 해결기. '택시 배차'와 '피자 배달' 비유로 알아보는 오토 스케일링과 서버리스의 차이, 그리고 Spot Instance를 활용한 비용 절감 꿀팁.

내 서버가 해킹당하지 않는 이유. 포트와 IP를 검사하는 '패킷 필터링'부터 AWS Security Group까지, 방화벽의 진화 과정.

금요일 오후 5시 30분. 퇴근 준비를 하고 있는데 슬랙에 긴급 메시지가 떴다. "프로덕션에서 결제가 안 됩니다." 식은땀이 났다. 30분 전에 내가 직접 배포한 코드였다.
로컬에서는 완벽하게 작동했다. 테스트도 돌렸다. 그런데 배포하면서 환경변수 하나를 빼먹었다. .env.local에는 있었지만 프로덕션 서버에는 설정 안 한 변수였다. 급하게 수동으로 고쳤지만, 15분간 결제가 먹통이었다.
그날 저녁, 나는 결심했다. "다시는 수동 배포 안 한다." GitHub Actions를 배우기 시작했고, 지금은 코드를 푸시하면 자동으로 테스트, 빌드, 배포까지 끝난다. 실수할 틈이 없다. 이 글은 그때 내가 배운 것들을 정리한 기록이다.
CI/CD가 뭔지 처음 들었을 때는 복잡한 DevOps 세계의 일이라고 생각했다. 하지만 결국 이거였다. 매번 반복하는 지루한 작업을 로봇에게 시키는 것.
레스토랑을 생각해보자. 요리사(개발자)가 요리할 때마다 직접 재료를 씻고, 칼을 갈고, 설거지까지 한다면? 비효율적이다. 보조 요리사(CI/CD)를 고용해서 반복 작업을 맡기면, 요리사는 요리에만 집중할 수 있다.
GitHub Actions는 바로 그 보조 요리사다. 코드를 푸시하면 자동으로:
모든 단계가 자동이고, 한 단계라도 실패하면 다음으로 안 넘어간다. 날것인 재료로 요리를 서빙할 일이 없다.
내가 깨달은 가장 큰 인사이트는 이거였다. CI/CD는 선택이 아니라 필수다. 혼자 개발할 때도 마찬가지다. 미래의 나를 위한 안전장치이기 때문이다.
GitHub Actions는 .github/workflows/ 폴더에 YAML 파일로 정의된다. 처음 봤을 때 복잡해 보였지만, 구조는 단순하다.
name: CI/CD Pipeline
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- run: npm ci
- run: npm test
deploy:
needs: test
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npm run build
- name: Deploy to Vercel
run: vercel --prod --token=${{ secrets.VERCEL_TOKEN }}
핵심 구조:
각 job은 독립적인 서버(runner)에서 실행된다. needs로 의존성을 정의하면 순서를 제어할 수 있다. 위 예시에서 deploy는 test가 성공해야만 실행된다.
on:
push:
branches: [main, develop]
paths:
- 'src/**'
- 'package.json'
특정 브랜치에 푸시할 때만 실행. paths로 특정 파일 변경 시에만 트리거할 수 있다. 문서만 수정했는데 전체 빌드가 돌면 시간 낭비니까.
on:
pull_request:
types: [opened, synchronize, reopened]
PR이 열리거나 업데이트될 때마다 테스트를 돌린다. 팀으로 일할 때 필수다. 머지 전에 문제를 미리 잡을 수 있다.
Schedule 트리거 - 주기적 작업on:
schedule:
- cron: '0 2 * * *' # 매일 새벽 2시
cron 문법을 사용한다. 나는 이걸로 매일 밤 의존성 보안 체크를 돌린다. 자고 일어나면 리포트가 와 있다.
Manual 트리거 - 수동 실행on:
workflow_dispatch:
inputs:
environment:
description: 'Deployment environment'
required: true
default: 'staging'
type: choice
options:
- staging
- production
GitHub UI에서 버튼 클릭으로 실행할 수 있다. 급하게 핫픽스 배포할 때 유용하다.
Node.js 프로젝트는 버전마다 동작이 다를 수 있다. 모든 버전에서 테스트하려면?
jobs:
test:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
node-version: [18, 20, 22]
steps:
- uses: actions/checkout@v4
- name: Setup Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- run: npm ci
- run: npm test
이 설정은 3개 OS × 3개 Node 버전 = 9개 job을 병렬로 실행한다. 한 번에 모든 환경을 검증할 수 있다. 라이브러리를 만들 때 특히 유용했다.
처음 만든 파이프라인은 느렸다. 매번 npm install에 2분씩 걸렸다. 캐싱을 추가하니 30초로 줄었다.
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm' # 이 한 줄로 끝
- run: npm ci
- run: npm run build
actions/setup-node에 cache: 'npm'을 추가하면 자동으로 node_modules를 캐싱한다. pnpm이나 yarn도 지원한다.
더 복잡한 캐싱도 가능하다:
- name: Cache build output
uses: actions/cache@v4
with:
path: |
.next/cache
dist
key: ${{ runner.os }}-build-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-build-
Next.js의 .next/cache를 캐싱하면 빌드 시간이 극적으로 줄어든다.
.env 파일을 레포에 올릴 순 없다. GitHub Secrets를 사용한다.
Settings → Secrets and variables → Actions에서 추가:
VERCEL_TOKENDATABASE_URLSTRIPE_SECRET_KEY워크플로우에서 사용:
- name: Deploy
env:
VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
DATABASE_URL: ${{ secrets.DATABASE_URL }}
run: |
vercel --token=$VERCEL_TOKEN
로그에 출력되면 자동으로 ***로 마스킹된다. 실수로 노출될 걱정이 없다.
Organization secrets도 만들 수 있다. 여러 레포에서 같은 secret을 쓸 때 편하다.
내가 실제로 쓰는 파이프라인:
name: Production Pipeline
on:
push:
branches: [main]
jobs:
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
- run: npm run type-check
test:
needs: 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 test -- --coverage
- name: Upload coverage
uses: codecov/codecov-action@v4
build:
needs: 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 run build
- name: Upload build artifacts
uses: actions/upload-artifact@v4
with:
name: build
path: dist/
deploy:
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/download-artifact@v4
with:
name: build
path: dist/
- name: Deploy to Cloudflare Pages
uses: cloudflare/pages-action@v1
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
projectName: my-project
directory: dist
각 단계가 독립적이고, 한 단계라도 실패하면 멈춘다. 린트 에러가 있으면 테스트조차 안 돌린다. 시간과 비용 절약이다.
- name: Deploy to Vercel
uses: amondnet/vercel-action@v25
with:
vercel-token: ${{ secrets.VERCEL_TOKEN }}
vercel-org-id: ${{ secrets.ORG_ID }}
vercel-project-id: ${{ secrets.PROJECT_ID }}
vercel-args: '--prod'
AWS S3 + CloudFront
- name: Deploy to S3
uses: jakejarvis/s3-sync-action@v0.5.1
with:
args: --delete
env:
AWS_S3_BUCKET: ${{ secrets.AWS_S3_BUCKET }}
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
- name: Invalidate CloudFront
uses: chetan/invalidate-cloudfront-action@v2
env:
DISTRIBUTION: ${{ secrets.CLOUDFRONT_DISTRIBUTION_ID }}
PATHS: '/*'
AWS_REGION: 'us-east-1'
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
Cloudflare Pages (내가 지금 쓰는 것)
- uses: cloudflare/pages-action@v1
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
projectName: codemapo
directory: dist
gitHubToken: ${{ secrets.GITHUB_TOKEN }}
여러 프로젝트에서 비슷한 워크플로우를 쓰다 보면 중복이 생긴다. 재사용 가능한 워크플로우로 만들 수 있다.
.github/workflows/reusable-deploy.ymlname: Reusable Deploy
on:
workflow_call:
inputs:
environment:
required: true
type: string
secrets:
deploy_token:
required: true
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npm run build
- name: Deploy to ${{ inputs.environment }}
run: ./deploy.sh ${{ inputs.environment }}
env:
TOKEN: ${{ secrets.deploy_token }}
다른 워크플로우에서 호출
jobs:
deploy-staging:
uses: ./.github/workflows/reusable-deploy.yml
with:
environment: staging
secrets:
deploy_token: ${{ secrets.STAGING_TOKEN }}
deploy-prod:
uses: ./.github/workflows/reusable-deploy.yml
with:
environment: production
secrets:
deploy_token: ${{ secrets.PROD_TOKEN }}
GitHub Actions는 공개 레포는 무료, 프라이빗 레포는 월 2,000분 무료다. (Pro 계정은 3,000분)
비용 절감 팁:paths 필터로 관련 파일만if 조건으로 불필요한 job 스킵deploy:
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
# main 브랜치 push일 때만 배포
내 경험상 개인 프로젝트 5개 정도는 무료 한도 내에서 충분했다.
처음엔 워크플로우가 자주 실패했다. 디버깅하는 법을 배웠다.
Step 1: 로그 꼼꼼히 읽기 GitHub UI에서 각 step을 클릭하면 상세 로그를 볼 수 있다. 에러 메시지가 명확하게 나온다.
Step 2: Debug 로그 활성화- name: Debug info
run: |
echo "Event: ${{ github.event_name }}"
echo "Ref: ${{ github.ref }}"
echo "Actor: ${{ github.actor }}"
env
환경변수를 전부 출력해서 뭐가 잘못됐는지 파악한다.
Step 3: act로 로컬 테스트 act는 로컬에서 GitHub Actions를 돌려볼 수 있는 도구다.
brew install act
act -j test # test job만 로컬에서 실행
푸시하기 전에 미리 검증할 수 있다. 시행착오가 줄어든다.
Step 4: Timeout 설정 무한 루프에 빠지면 비용이 계속 나간다. Timeout을 설정한다.
jobs:
test:
runs-on: ubuntu-latest
timeout-minutes: 10 # 10분 넘으면 강제 종료
다른 CI/CD 도구도 써봤다. 각각 장단점이 있다.
GitHub Actions 장점:결론적으로, GitHub를 쓰고 있다면 GitHub Actions가 가장 마찰이 적다. 별도 서비스 가입도 필요 없고, 모든 게 한 곳에 있다.
GitHub Actions 파이프라인을 만드는 건 공장 자동화 라인을 설계하는 것과 같다.
한 단계라도 문제가 있으면 다음 단계로 못 간다. 불량품이 고객에게 갈 일이 없다.
처음엔 복잡해 보이지만, 한 번 설정해두면 평생 혜택을 본다. 매번 수동으로 하던 작업이 자동화되면서, 나는 더 중요한 문제에 집중할 수 있게 됐다.
그날 금요일 오후의 긴급 버그 이후, 나는 다시는 수동 배포를 하지 않는다. GitHub Actions가 내 대신 모든 체크리스트를 확인한다. 덕분에 나는 코드 작성에만 집중할 수 있고, 밤에 편하게 잔다. 프로덕션이 깨질 걱정 없이.