์ฝ˜ํ…์ธ  ๋Œ€ํ‘œ ์ด๋ฏธ์ง€ - ๐Ÿš€ GitHub Actions๋กœ CI/CD ๊ตฌ์ถ•ํ•˜๊ธฐ โ€” ๋ฐฐํฌ ์ž๋™ํ™” ์™„์ „ ์ •๋ณต ๊ฐ€์ด๋“œ

๐Ÿš€ GitHub Actions๋กœ CI/CD ๊ตฌ์ถ•ํ•˜๊ธฐ โ€” ๋ฐฐํฌ ์ž๋™ํ™” ์™„์ „ ์ •๋ณต ๊ฐ€์ด๋“œ

"git push ํ–ˆ๋”๋‹ˆ ์•Œ์•„์„œ ํ…Œ์ŠคํŠธํ•˜๊ณ  ์•Œ์•„์„œ ๋ฐฐํฌ๋จ" ๊ทธ ๋งˆ๋ฒ•์˜ ์ •์ฒด

๐Ÿงช ํ…Œ์ŠคํŠธ ยท ๐Ÿณ ๋„์ปค ๋นŒ๋“œ ยท โ˜๏ธ ํด๋ผ์šฐ๋“œ ๋ฐฐํฌ๊นŒ์ง€ ํ•œ ๋ฒˆ์—

๐Ÿ˜ต ํ”„๋กค๋กœ๊ทธ: "์ œ ๋กœ์ปฌ์—์„  ์ž˜ ๋๋Š”๋ฐ์š”?"

์นœ๊ตฌ์•ผ, ์šฐ๋ฆฌ ์†”์งํ•ด์ง€์ž.
๊ฐœ๋ฐœํ•˜๋‹ค ๋ณด๋ฉด ์ด๋Ÿฐ ๋Œ€์‚ฌ ํ•œ ๋ฒˆ์ฏค ํ•ด๋ดค์ง€?

"์–ดโ€ฆ ์ œ ์ปดํ“จํ„ฐ์—์„œ๋Š” ์ž˜ ๋Œ์•„๊ฐ”๋Š”๋ฐ์š”?"

"์•„ ๊ทธ ํŒŒ์ผ ์ปค๋ฐ‹ ์•ˆ ํ–ˆ๋„ค์š” ใ…Žใ…Ž"

"๋ฐฐํฌ๋Š” ์ œ๊ฐ€ ๋ฐค์— ์ง์ ‘ ์„œ๋ฒ„ ๋“ค์–ด๊ฐ€์„œ ํ• ๊ฒŒ์š”โ€ฆ"

์ด ์„ธ ๋ฌธ์žฅ์€ ์‚ฌ์‹ค ๊ฐ™์€ ๋ณ‘์˜ ์ฆ์ƒ์ด์•ผ. ๋ณ‘๋ช…์€ ์ˆ˜๋™ ๋ฐฐํฌ ์ฆํ›„๊ตฐ.
์‚ฌ๋žŒ ์†์ด ๋“ค์–ด๊ฐ€๋Š” ๊ณณ์—” ๋ฐ˜๋“œ์‹œ ์‹ค์ˆ˜๊ฐ€ ๋ผ๊ณ , ์‹ค์ˆ˜๋Š” ๊ผญ ๊ธˆ์š”์ผ ์ €๋…์— ํ„ฐ์ง„๋‹ค.

๊ทธ๋ž˜์„œ ๋‚˜์˜จ ๊ฒŒ CI/CD๊ณ , ๊ทธ์ค‘์—์„œ๋„ GitHub Actions๋Š”
"GitHub ์“ฐ๊ณ  ์žˆ์œผ๋ฉด ๊ทธ๋ƒฅ ๋ฐ”๋กœ ์‹œ์ž‘ ๊ฐ€๋Šฅ"์ด๋ผ๋Š” ์‚ฌ๊ธฐ๊ธ‰ ์ ‘๊ทผ์„ฑ์„ ๊ฐ€์ง„ ๋„๊ตฌ์•ผ.

์ด ๊ธ€์—์„œ๋Š” ๊ฐœ๋… โ†’ ๋ฌธ๋ฒ• โ†’ ์‹ค์ „ ์›Œํฌํ”Œ๋กœ โ†’ ์บ์‹ฑ/์‹œํฌ๋ฆฟ โ†’ ๋ฐฐํฌ โ†’ ๋น„์šฉ/๋ณด์•ˆ๊นŒ์ง€
์ง„์งœ ์‹ค๋ฌด์—์„œ ์“ฐ๋Š” ์ˆœ์„œ๋Œ€๋กœ ์ญ‰ ํ›‘์–ด์ค„๊ฒŒ. ์ปคํ”ผ ํ•œ ์ž” ์ฑ™๊ฒจ์™€. โ˜•

CI / CD ํŒŒ์ดํ”„๋ผ์ธ ํ•œ ์žฅ ์š”์•ฝ git push ๊ฐœ๋ฐœ์ž ์ปค๋ฐ‹ Build ์˜์กด์„ฑ ยท ์ปดํŒŒ์ผ Test ๋‹จ์œ„ ยท ๋ฆฐํŠธ Package Docker ยท ์•„ํ‹ฐํŒฉํŠธ Deploy ์šด์˜ ๋ฐ˜์˜ CI ยท Continuous Integration CD ยท Delivery / Deployment ์‹คํŒจํ•˜๋ฉด ์ฆ‰์‹œ ์ค‘๋‹จ โ†’ ๋‚˜์œ ์ฝ”๋“œ๊ฐ€ ์šด์˜์œผ๋กœ ๊ฐ€์ง€ ๋ชปํ•œ๋‹ค GitHub Actions๋Š” ์ด ์ „ ๊ณผ์ •์„ YAML ํ•œ ์žฅ์œผ๋กœ ์ž๋™ํ™”ํ•œ๋‹ค

๐Ÿง  1์žฅ. CI/CD, ์šฉ์–ด๋ถ€ํ„ฐ ์ œ๋Œ€๋กœ ์ •๋ฆฌํ•˜์ž

CI (Continuous Integration, ์ง€์†์  ํ†ตํ•ฉ)

์ฝ”๋“œ๋ฅผ ์ž์ฃผ, ์ž‘๊ฒŒ ํ†ตํ•ฉํ•˜๊ณ , ํ†ตํ•ฉํ•  ๋•Œ๋งˆ๋‹ค ์ž๋™์œผ๋กœ ๋นŒ๋“œ+ํ…Œ์ŠคํŠธํ•˜๋Š” ์Šต๊ด€์ด์•ผ.
๋ชฉ์ ์€ ๋‹จ์ˆœํ•ด. "๊นจ์ง„ ์ฝ”๋“œ๋ฅผ ์ตœ๋Œ€ํ•œ ๋นจ๋ฆฌ ๋ฐœ๊ฒฌํ•œ๋‹ค."

3๊ฐœ์›” ๋™์•ˆ ๋ธŒ๋žœ์น˜ ํ˜ผ์ž ํŒŒ๊ณ ๋“ค๋‹ค๊ฐ€ ๋จธ์ง€ํ•˜๋ฉด ๊ทธ๊ฑด ํ†ตํ•ฉ์ด ์•„๋‹ˆ๋ผ ์ถฉ๋Œ ๋Œ€์ž”์น˜์ง€.
CI๋Š” "ํ•˜๋ฃจ์— ํ•œ ๋ฒˆ ์ด์ƒ main๊ณผ ๋งŒ๋‚˜๋ผ"๋Š” ๋ฌธํ™” + ๊ทธ๊ฑธ ๋ฐ›์ณ์ฃผ๋Š” ์ž๋™ํ™” ๋„๊ตฌ์˜ ์กฐํ•ฉ์ด์•ผ.

CD, ์‚ฌ์‹ค ๋‘ ๊ฐ€์ง€ ๋œป์ด ์žˆ๋‹ค

์šฉ์–ด์˜๋ฏธ์šด์˜ ๋ฐ˜์˜ ๋ฐฉ์‹
Continuous Delivery
(์ง€์†์  ์ œ๊ณต)
์–ธ์ œ๋“  ๋ฐฐํฌ ๊ฐ€๋Šฅํ•œ ์ƒํƒœ๋กœ ํ•ญ์ƒ ์ค€๋น„๋งˆ์ง€๋ง‰ ๋ฒ„ํŠผ์€ ์‚ฌ๋žŒ์ด ๋ˆ„๋ฆ„
Continuous Deployment
(์ง€์†์  ๋ฐฐํฌ)
ํ…Œ์ŠคํŠธ ํ†ต๊ณผํ•˜๋ฉด ์šด์˜๊นŒ์ง€ ์ž๋™์‚ฌ๋žŒ ๊ฐœ์ž… ์—†์Œ

๐Ÿ’ก ํ˜„์‹ค ํŒ

์Šคํƒ€ํŠธ์—… ์ดˆ๊ธฐ์—” Delivery๋กœ ์‹œ์ž‘ํ•˜๋Š” ๊ฒŒ ์•ˆ์ „ํ•ด.
staging๊นŒ์ง€๋Š” ์™„์ „ ์ž๋™, production์€ ์ˆ˜๋™ ์Šน์ธ(Environment approval).
ํ…Œ์ŠคํŠธ ์ปค๋ฒ„๋ฆฌ์ง€์™€ ๋ชจ๋‹ˆํ„ฐ๋ง์ด ๊ฐ–์ถฐ์ง„ ๋‹ค์Œ์— Deployment๋กœ ๋„˜์–ด๊ฐ€๋ฉด ๋ผ.

์™œ GitHub Actions์ธ๊ฐ€?

1) ๋ณ„๋„ ์„œ๋ฒ„๊ฐ€ ํ•„์š” ์—†๋‹ค
Jenkins์ฒ˜๋Ÿผ ๋งˆ์Šคํ„ฐ ์„œ๋ฒ„๋ฅผ ์„ธ์šฐ๊ณ  ํ”Œ๋Ÿฌ๊ทธ์ธ ์ง€์˜ฅ์„ ๊ฒช์„ ํ•„์š”๊ฐ€ ์—†์–ด. GitHub๊ฐ€ ๋Ÿฌ๋„ˆ๋ฅผ ๋นŒ๋ ค์ค˜.

2) ์ฝ”๋“œ์™€ ๊ฐ™์€ ๋ ˆํฌ์— ์‚ฐ๋‹ค
.github/workflows/*.yml. ์„ค์ •์ด ์ฝ”๋“œ ๋ฆฌ๋ทฐ ๋Œ€์ƒ์ด ๋˜๊ณ , PR๋กœ ๋ณ€๊ฒฝ ์ด๋ ฅ์ด ๋‚จ์•„. ์ด๊ฒŒ ์ง„์งœ ํฌ๋‹ค.

3) ๋งˆ์ผ“ํ”Œ๋ ˆ์ด์Šค ์ƒํƒœ๊ณ„
actions/checkout, actions/setup-node, docker/build-push-actionโ€ฆ ๋‚จ์ด ๋งŒ๋“  ๋ธ”๋ก์„ ์กฐ๋ฆฝํ•˜๋ฉด ๋.

4) ์ด๋ฒคํŠธ๊ฐ€ GitHub ๊ทธ ์ž์ฒด
PR ์—ด๋ฆผ, ์ด์Šˆ ๋ผ๋ฒจ ๋ถ™์Œ, ๋ฆด๋ฆฌ์ฆˆ ์ƒ์„ฑ, ์ฝ”๋ฉ˜ํŠธ ์ž‘์„ฑโ€ฆ GitHub์—์„œ ์ผ์–ด๋‚˜๋Š” ๊ฑฐ์˜ ๋ชจ๋“  ์ผ์ด ํŠธ๋ฆฌ๊ฑฐ๊ฐ€ ๋  ์ˆ˜ ์žˆ์–ด.

5) ํผ๋ธ”๋ฆญ ๋ ˆํฌ๋Š” ๋ฌด๋ฃŒ
์˜คํ”ˆ์†Œ์Šค๋Š” ํ‘œ์ค€ ๋Ÿฌ๋„ˆ ์‚ฌ์šฉ ์‹œ ๋น„์šฉ์ด ๋“ค์ง€ ์•Š์•„. ํ”„๋ผ์ด๋น—์€ ๋ฌด๋ฃŒ ๋ถ„(minutes) ํ•œ๋„ ํ›„ ๊ณผ๊ธˆ.

๐Ÿงฉ 2์žฅ. ๊ตฌ์กฐ ํ•ด๋ถ€ โ€” Workflow, Job, Step, Action

GitHub Actions๋Š” ๋Ÿฌ์‹œ์•„ ์ธํ˜•(๋งˆํŠธ๋ฃŒ์‹œ์นด) ๊ตฌ์กฐ์•ผ. ์ด ๊ณ„์ธต๋งŒ ์ดํ•ดํ•˜๋ฉด YAML์ด ๊ฐ‘์ž๊ธฐ ์ฝํ˜€.

Actions ๊ณ„์ธต ๊ตฌ์กฐ (Workflow โ†’ Job โ†’ Step) WORKFLOW (ci.yml) ํŠธ๋ฆฌ๊ฑฐ: on: push / pull_request JOB: test runs-on: ubuntu-latest STEP ยท actions/checkout@v4 STEP ยท actions/setup-node@v4 STEP ยท run: npm ci STEP ยท run: npm test JOB: deploy needs: test (test ์„ฑ๊ณต ์‹œ์—๋งŒ) STEP ยท docker build & push STEP ยท ์„œ๋ฒ„ ๋ฐฐํฌ / k8s apply STEP ยท ์Šฌ๋ž™ ์•Œ๋ฆผ ์ „์†ก โ€ป Job์€ ๊ธฐ๋ณธ ๋ณ‘๋ ฌ, needs๋กœ ์ˆœ์„œ ์ œ์–ด

์šฉ์–ด ์ •๋ฆฌํ‘œ

์š”์†Œ์„ค๋ช…๊ธฐ์–ต ํฌ์ธํŠธ
WorkflowYAML ํŒŒ์ผ 1๊ฐœ = ์ž๋™ํ™” ํ”„๋กœ์„ธ์Šค 1๊ฐœ.github/workflows/ ์•ˆ์— ์œ„์น˜
Event(on)์›Œํฌํ”Œ๋กœ๋ฅผ ๊นจ์šฐ๋Š” ๋ฐฉ์•„์‡ push, pull_request, schedule, workflow_dispatchโ€ฆ
Job๊ฐ™์€ ๋Ÿฌ๋„ˆ(๊ฐ€์ƒ ๋จธ์‹ )์—์„œ ์‹คํ–‰๋˜๋Š” ๋ฌถ์Œ๊ธฐ๋ณธ ๋ณ‘๋ ฌ ์‹คํ–‰
StepJob ์•ˆ์˜ ํ•œ ์ค„ ๋ช…๋ น ๋˜๋Š” Action ํ˜ธ์ถœrun ๋˜๋Š” uses
Action์žฌ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ์‹คํ–‰ ๋‹จ์œ„(ํŒจํ‚ค์ง€)๋งˆ์ผ“ํ”Œ๋ ˆ์ด์Šค์—์„œ ๊ฐ€์ ธ์˜ด
Runner์‹ค์ œ๋กœ ์ผํ•˜๋Š” ๋จธ์‹ GitHub-hosted / Self-hosted

โš ๏ธ ์ดˆ๋ณด ์ตœ๋Œ€ ํ•จ์ •

Job์€ ์„œ๋กœ ๋‹ค๋ฅธ ๋จธ์‹ ์—์„œ ๋ˆ๋‹ค. ๊ทธ๋ž˜์„œ job A์—์„œ ๋งŒ๋“  dist/ ํด๋”๊ฐ€ job B์— ์ ˆ๋Œ€ ๊ทธ๋ƒฅ ์•ˆ ๋„˜์–ด๊ฐ€.
๋„˜๊ธฐ๊ณ  ์‹ถ์œผ๋ฉด actions/upload-artifact + actions/download-artifact๋ฅผ ์จ์•ผ ํ•ด.
๋ฐ˜๋Œ€๋กœ ๊ฐ™์€ Job ์•ˆ์˜ Step๋ผ๋ฆฌ๋Š” ํŒŒ์ผ ์‹œ์Šคํ…œ์„ ๊ณต์œ ํ•œ๋‹ค.

โœ๏ธ 3์žฅ. ์ฒซ ์›Œํฌํ”Œ๋กœ โ€” Hello, Automation

๋ ˆํฌ ๋ฃจํŠธ์— .github/workflows/ci.yml ๋งŒ๋“ค๊ณ  ์ด๊ฑฐ ๋ถ™์—ฌ๋„ฃ์–ด ๋ด.

name: CI

on:
  push:
    branches: [ main, develop ]
  pull_request:
    branches: [ main ]

jobs:
  hello:
    runs-on: ubuntu-latest
    steps:
      - name: ์ฝ”๋“œ ๋‚ด๋ ค๋ฐ›๊ธฐ
        uses: actions/checkout@v4

      - name: ์ธ์‚ฌํ•˜๊ธฐ
        run: echo "์•ˆ๋…• $GITHUB_ACTOR! ์ปค๋ฐ‹ $GITHUB_SHA ํ™•์ธํ–ˆ๋‹ค."

      - name: ํŒŒ์ผ ๊ตฌ๊ฒฝ
        run: ls -al

push ํ•˜๊ณ  ๋ ˆํฌ์˜ Actions ํƒญ ์—ด์–ด๋ด. ์ดˆ๋ก ์ฒดํฌ ๋œจ๋ฉด ์„ฑ๊ณต. ๐ŸŽ‰
์ด๊ฒŒ CI/CD์˜ ์ฒซ ์‹ฌ์žฅ ๋ฐ•๋™์ด์•ผ.

๊ฐ ์ค„์ด ํ•˜๋Š” ์ผ

name: โ€” Actions ํƒญ์— ๋ณด์ด๋Š” ์›Œํฌํ”Œ๋กœ ์ด๋ฆ„. ์•ˆ ์“ฐ๋ฉด ํŒŒ์ผ๋ช…์ด ๋“ค์–ด๊ฐ€.

on: โ€” ํŠธ๋ฆฌ๊ฑฐ. branches ํ•„ํ„ฐ๋กœ ๋ถˆํ•„์š”ํ•œ ์‹คํ–‰(=๋น„์šฉ)์„ ์ค„์ผ ์ˆ˜ ์žˆ์–ด.

runs-on: โ€” ๋Ÿฌ๋„ˆ OS. ubuntu-latest๊ฐ€ ๊ฐ€์žฅ ๋น ๋ฅด๊ณ  ์ €๋ ดํ•ด. windows-latest๋Š” 2๋ฐฐ, macos-latest๋Š” 10๋ฐฐ ๊ณผ๊ธˆ(ํ”„๋ผ์ด๋น— ๊ธฐ์ค€).

actions/checkout@v4 โ€” ๊ฑฐ์˜ ๋ชจ๋“  ์›Œํฌํ”Œ๋กœ์˜ 1๋ฒˆ ์Šคํ…. ์ด๊ฑฐ ์—†์œผ๋ฉด ๋Ÿฌ๋„ˆ์—” ์ฝ”๋“œ๊ฐ€ ์—†๋‹ค. ์ง„์งœ ์—†๋‹ค.

๐Ÿงท ๋ฒ„์ „ ํ•€ ๊ณ ์ • ์Šต๊ด€

@v4์ฒ˜๋Ÿผ ๋ฉ”์ด์ € ํƒœ๊ทธ๋ฅผ ์“ฐ๋Š” ๊ฒŒ ๊ธฐ๋ณธ์ด๊ณ , ๋ณด์•ˆ์ด ์ค‘์š”ํ•œ ์กฐ์ง์€ @a1b2c3... ์ปค๋ฐ‹ SHA๋กœ ๊ณ ์ •ํ•ด. ํƒœ๊ทธ๋Š” ์žฌ์ง€์ •๋  ์ˆ˜ ์žˆ์–ด์„œ ๊ณต๊ธ‰๋ง ๊ณต๊ฒฉ ํ‘œ๋ฉด์ด ๋˜๋‹ˆ๊นŒ.

๐ŸŽฏ 4์žฅ. ํŠธ๋ฆฌ๊ฑฐ(on) ์ด์ •๋ฆฌ โ€” ์–ธ์ œ ๋Œ๋ฆด ๊ฒƒ์ธ๊ฐ€

์ž์ฃผ ์“ฐ๋Š” ์ด๋ฒคํŠธ

on:
  # 1) ํ‘ธ์‹œ โ€” ๊ฒฝ๋กœ ํ•„ํ„ฐ๋กœ ๋ชจ๋…ธ๋ ˆํฌ ์ตœ์ ํ™”
  push:
    branches: [ main ]
    paths:
      - 'apps/web/**'
      - '!**/*.md'          # ๋ฌธ์„œ๋งŒ ๋ฐ”๋€Œ๋ฉด ์‹คํ–‰ ์•ˆ ํ•จ

  # 2) PR โ€” ์–ด๋–ค ์•ก์…˜์— ๋ฐ˜์‘ํ• ์ง€ ์ง€์ •
  pull_request:
    types: [ opened, synchronize, reopened ]

  # 3) ํƒœ๊ทธ ํ‘ธ์‹œ โ€” ๋ฆด๋ฆฌ์ฆˆ ๋ฐฐํฌ์šฉ
  push:
    tags: [ 'v*.*.*' ]

  # 4) ์Šค์ผ€์ค„ โ€” ํฌ๋ก  (UTC ๊ธฐ์ค€!)
  schedule:
    - cron: '0 18 * * *'     # ๋งค์ผ ํ•œ๊ตญ์‹œ๊ฐ„ 03:00

  # 5) ์ˆ˜๋™ ์‹คํ–‰ + ์ž…๋ ฅ๊ฐ’
  workflow_dispatch:
    inputs:
      environment:
        description: '๋ฐฐํฌ ๋Œ€์ƒ'
        type: choice
        options: [ staging, production ]
        default: staging

  # 6) ๋‹ค๋ฅธ ์›Œํฌํ”Œ๋กœ๊ฐ€ ๋๋‚œ ๋’ค
  workflow_run:
    workflows: [ "CI" ]
    types: [ completed ]

๐Ÿ“Œ cron์€ UTC๋‹ค

ํ•œ๊ตญ์‹œ๊ฐ„(KST)์€ UTC+9. "๋งค์ผ ์˜ค์ „ 9์‹œ"๋ฅผ ์›ํ•˜๋ฉด 0 0 * * *.
๊ทธ๋ฆฌ๊ณ  GitHub ํฌ๋ก ์€ ์ •ํ™•ํ•œ ์‹œ๊ฐ ๋ณด์žฅ์ด ์—†์–ด. ๋ถ€ํ•˜๊ฐ€ ํฌ๋ฉด ์ˆ˜ ๋ถ„~์ˆ˜์‹ญ ๋ถ„ ์ง€์—ฐ๋˜๊ฑฐ๋‚˜ ์Šคํ‚ต๋  ์ˆ˜ ์žˆ์œผ๋‹ˆ ๊ฒฐ์ œ ๋งˆ๊ฐ์ฒ˜๋Ÿผ ์ค‘์š”ํ•œ ์ž‘์—…์€ ๋‹ค๋ฅธ ์Šค์ผ€์ค„๋Ÿฌ๋ฅผ ์จ.

pull_request vs pull_request_target

์™ธ๋ถ€ ํฌํฌ์—์„œ ์˜จ PR์€ ์‹œํฌ๋ฆฟ์— ์ ‘๊ทผํ•  ์ˆ˜ ์—†์–ด(์˜๋„๋œ ๋ณด์•ˆ ์„ค๊ณ„).
pull_request_target์€ ๋ฒ ์ด์Šค ๋ธŒ๋žœ์น˜ ๊ถŒํ•œ์œผ๋กœ ์‹คํ–‰๋ผ์„œ ์‹œํฌ๋ฆฟ์„ ์“ธ ์ˆ˜ ์žˆ๋Š”๋ฐ,
์—ฌ๊ธฐ์„œ PR ์ฝ”๋“œ๋ฅผ ์ฒดํฌ์•„์›ƒํ•ด ์‹คํ–‰ํ•˜๋ฉด ์‹œํฌ๋ฆฟ ์œ ์ถœ ์ทจ์•ฝ์ ์ด ๋ผ.

๐Ÿšจ ์ ˆ๋Œ€ ๊ธˆ์ง€ ํŒจํ„ด

pull_request_target + ref: ${{ github.event.pull_request.head.sha }} ์ฒดํฌ์•„์›ƒ + npm install(์Šคํฌ๋ฆฝํŠธ ์‹คํ–‰) ์กฐํ•ฉ.
๋‚จ์˜ ์ฝ”๋“œ๋ฅผ ๋‚ด ์‹œํฌ๋ฆฟ ๊ถŒํ•œ์œผ๋กœ ์‹คํ–‰์‹œ์ผœ์ฃผ๋Š” ๊ฑฐ์•ผ. ์‹ค์ œ๋กœ ์œ ๋ช… ์˜คํ”ˆ์†Œ์Šค๋“ค์ด ์ด๊ฑธ๋กœ ํ„ธ๋ ธ์–ด.

๐Ÿ› ๏ธ 5์žฅ. ์‹ค์ „ ์›Œํฌํ”Œ๋กœ โ‘  Node.js ์•ฑ CI

์ด์ œ ์ง„์งœ ์“ธ ๋งŒํ•œ ๊ฑธ ๋งŒ๋“ค์–ด๋ณด์ž. ๋ฆฐํŠธ โ†’ ํƒ€์ž…์ฒดํฌ โ†’ ํ…Œ์ŠคํŠธ โ†’ ๋นŒ๋“œ.

name: Node CI

on:
  push:
    branches: [ main ]
  pull_request:

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

jobs:
  quality:
    runs-on: ubuntu-latest
    timeout-minutes: 15

    strategy:
      fail-fast: false
      matrix:
        node: [ 18, 20, 22 ]

    steps:
      - uses: actions/checkout@v4

      - name: Node ${{ matrix.node }} ์„ธํŒ…
        uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node }}
          cache: 'npm'          # ๋‚ด๋ถ€์ ์œผ๋กœ ์บ์‹œ ์ฒ˜๋ฆฌ

      - name: ์˜์กด์„ฑ ์„ค์น˜
        run: npm ci              # install ์•„๋‹˜! ci ์ž„

      - name: ๋ฆฐํŠธ
        run: npm run lint

      - name: ํƒ€์ž… ์ฒดํฌ
        run: npx tsc --noEmit

      - name: ํ…Œ์ŠคํŠธ + ์ปค๋ฒ„๋ฆฌ์ง€
        run: npm test -- --coverage

      - name: ๋นŒ๋“œ
        run: npm run build

      - name: ๋นŒ๋“œ ๊ฒฐ๊ณผ ์ €์žฅ
        if: matrix.node == 20
        uses: actions/upload-artifact@v4
        with:
          name: web-dist
          path: dist/
          retention-days: 7

์—ฌ๊ธฐ์„œ ๊ผญ ์ฑ™๊ธธ 3๊ฐ€์ง€

โ‘  concurrency โ€” ๋ˆ๊ณผ ์‹œ๊ฐ„ ์ ˆ์•ฝ์˜ ์‹ 

๊ฐ™์€ ๋ธŒ๋žœ์น˜์— ์—ฐ์† ํ‘ธ์‹œํ•˜๋ฉด ์ด์ „ ์‹คํ–‰์„ ์ž๋™ ์ทจ์†Œํ•ด. ์ฝ”๋“œ ๋ฆฌ๋ทฐ ์ค‘ 3๋ฒˆ ํ‘ธ์‹œํ–ˆ๋‹ค๊ณ  3๋ฒˆ ๋‹ค ๋Œ๋ฆด ์ด์œ  ์—†์ง€.

โ‘ก npm ci vs npm install

ci๋Š” package-lock.json์„ ๊ทธ๋Œ€๋กœ ์„ค์น˜ํ•˜๊ณ  node_modules๋ฅผ ์ง€์šด ๋’ค ๊น”์•„. ๊ฒฐ๊ณผ๊ฐ€ ํ•ญ์ƒ ๋™์ผ = ์žฌํ˜„ ๊ฐ€๋Šฅํ•œ ๋นŒ๋“œ. CI์—์„  ๋ฌด์กฐ๊ฑด ci.

โ‘ข strategy.matrix โ€” ์กฐํ•ฉ ํญ๋ฐœ ์ฃผ์˜

Node 3๊ฐœ ร— OS 3๊ฐœ = 9๋ฒˆ ์‹คํ–‰ = ๋น„์šฉ 9๋ฐฐ. ํ•„์š”ํ•œ ์กฐํ•ฉ๋งŒ ๊ณจ๋ผ ์“ฐ๊ณ , include/exclude๋กœ ๋‹ค๋“ฌ์–ด.

โฑ๏ธ timeout-minutes๋Š” ํ•„์ˆ˜ ์Šต๊ด€

๊ธฐ๋ณธ ํƒ€์ž„์•„์›ƒ์ด 360๋ถ„(6์‹œ๊ฐ„)์ด์•ผ. ๋ฌดํ•œ ๋Œ€๊ธฐ ๊ฑธ๋ฆฐ ํ…Œ์ŠคํŠธ ํ•˜๋‚˜๊ฐ€ 6์‹œ๊ฐ„ ๊ณผ๊ธˆ๋  ์ˆ˜ ์žˆ์–ด. ํ•ญ์ƒ 10~30๋ถ„์œผ๋กœ ๊ฑธ์–ด๋‘์ž.

โšก 6์žฅ. ์บ์‹ฑ โ€” ํŒŒ์ดํ”„๋ผ์ธ ์†๋„๋ฅผ 2๋ฐฐ๋กœ

CI๊ฐ€ ๋А๋ฆฌ๋ฉด ์‚ฌ๋žŒ๋“ค์€ CI๋ฅผ ์•ˆ ๋ณธ๋‹ค. ๊ทธ๋Ÿฌ๋ฉด CI๋Š” ์ฃฝ๋Š”๋‹ค.
๊ทธ๋ž˜์„œ ์บ์‹ฑ์€ ๊ธฐ๋Šฅ์ด ์•„๋‹ˆ๋ผ ๋ฌธํ™” ์œ ์ง€ ์žฅ์น˜์•ผ.

      - name: ์˜์กด์„ฑ ์บ์‹œ
        uses: actions/cache@v4
        with:
          path: |
            ~/.npm
            .next/cache
          key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
          restore-keys: |
            ${{ runner.os }}-node-

์บ์‹œ ํ‚ค ์„ค๊ณ„ ์›์น™

๊ตฌ์„ฑ์—ญํ• 
runner.osOS๋ณ„ ๋ฐ”์ด๋„ˆ๋ฆฌ ์ถฉ๋Œ ๋ฐฉ์ง€
hashFiles(lockํŒŒ์ผ)์˜์กด์„ฑ์ด ๋ฐ”๋€Œ๋ฉด ํ‚ค๊ฐ€ ๋ฐ”๋€œ = ์ƒˆ ์บ์‹œ
restore-keys์ •ํ™•ํžˆ ์ผ์น˜ ์•ˆ ํ•˜๋ฉด ๋ถ€๋ถ„ ์ผ์น˜๋กœ๋ผ๋„ ๋ณต์›

๐Ÿ“ฆ ์–ธ์–ด๋ณ„ ์ถ”์ฒœ

โ€ข Node: setup-node์˜ cache: npm|yarn|pnpm
โ€ข Python: setup-python์˜ cache: pip
โ€ข Java: setup-java์˜ cache: gradle|maven
โ€ข Go: setup-go๋Š” ๊ธฐ๋ณธ ์บ์‹œ ๋‚ด์žฅ
โ†’ ์ „์šฉ setup ์•ก์…˜์˜ ์บ์‹œ ์˜ต์…˜์ด ๊ฐ€์žฅ ๊ฐ„๋‹จํ•˜๊ณ  ์•ˆ์ „ํ•˜๋‹ค.

โš ๏ธ ์บ์‹œ ํ•œ๋„

๋ ˆํฌ๋‹น ์บ์‹œ ์ด๋Ÿ‰์— ์ƒํ•œ(์ผ๋ฐ˜์ ์œผ๋กœ 10GB)์ด ์žˆ๊ณ , ๋„˜์œผ๋ฉด ์˜ค๋ž˜๋œ ๊ฒƒ๋ถ€ํ„ฐ ์ž๋™ ์‚ญ์ œ๋ผ. 7์ผ๊ฐ„ ๋ฏธ์‚ฌ์šฉ ์บ์‹œ๋„ ์ œ๊ฑฐ๋ผ.
๊ทธ๋ฆฌ๊ณ  ์บ์‹œ๋Š” ์‹ ๋ขฐ ์ €์žฅ์†Œ๊ฐ€ ์•„๋‹ˆ๋‹ค โ€” ๋น„๋ฐ€๊ฐ’์ด๋‚˜ ๋ฐ˜๋“œ์‹œ ํ•„์š”ํ•œ ์‚ฐ์ถœ๋ฌผ์€ ์บ์‹œ์— ๋‘์ง€ ๋งˆ. ์•„ํ‹ฐํŒฉํŠธ๋‚˜ ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ๋ฅผ ์จ.

์บ์‹œ vs ์•„ํ‹ฐํŒฉํŠธ, ํ—ท๊ฐˆ๋ฆฌ์ง€ ๋ง์ž

CacheArtifact
๋ชฉ์ ์žฌ์‚ฌ์šฉ์œผ๋กœ ์†๋„ ํ–ฅ์ƒ๊ฒฐ๊ณผ๋ฌผ ๋ณด๊ด€/์ „๋‹ฌ
๋ฒ”์œ„๋ธŒ๋žœ์น˜ ๊ฐ„ ๊ณต์œ (์ œํ•œ์ )๊ฐ™์€ ์›Œํฌํ”Œ๋กœ ์‹คํ–‰ ๋‚ด Job ๊ฐ„ ์ „๋‹ฌ + ๋‹ค์šด๋กœ๋“œ
์‚ฌ๋ผ์ง์ž๋™ ์ œ๊ฑฐ ๊ฐ€๋Šฅ๋ณด์กด ๊ธฐ๊ฐ„๊นŒ์ง€ ์œ ์ง€
์—†์œผ๋ฉด?๋‹ค์‹œ ๋งŒ๋“ค๋ฉด ๋จ (๋ฌดํ•ด)ํŒŒ์ดํ”„๋ผ์ธ ์‹คํŒจ (์น˜๋ช…์ )

๐Ÿ” 7์žฅ. ์‹œํฌ๋ฆฟ๊ณผ ํ™˜๊ฒฝ โ€” ๋น„๋ฐ€์€ ์ฝ”๋“œ์— ๋‘์ง€ ์•Š๋Š”๋‹ค

๋ฐฐํฌ์—” ํ•„์—ฐ์ ์œผ๋กœ ๋น„๋ฐ€์ด ๋ถ™์–ด. API ํ‚ค, SSH ํ‚ค, ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ ํ† ํฐโ€ฆ
์ด๊ฑธ YAML์— ํ•˜๋“œ์ฝ”๋”ฉํ•˜๋Š” ์ˆœ๊ฐ„ ๊นƒ ํžˆ์Šคํ† ๋ฆฌ์— ์˜์›ํžˆ ๋ฐ•์ œ๋ผ.

์‹œํฌ๋ฆฟ 3๊ณ„์ธต

๋ ˆ๋ฒจ๋ฒ”์œ„์ถ”์ฒœ ์šฉ๋„
Repository secretํ•ด๋‹น ๋ ˆํฌ์ผ๋ฐ˜ ํ† ํฐ
Environment secretํŠน์ • ํ™˜๊ฒฝ(production ๋“ฑ)์šด์˜ ์ž๊ฒฉ์ฆ๋ช… + ์Šน์ธ ๊ทœ์น™
Organization secret์กฐ์ง ์ „์ฒด๊ณตํ†ต ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ ๊ณ„์ •
jobs:
  deploy-prod:
    runs-on: ubuntu-latest
    environment:
      name: production          # ์Šน์ธ/๋ณดํ˜ธ ๊ทœ์น™ ์ ์šฉ
      url: https://example.com
    steps:
      - uses: actions/checkout@v4
      - name: ๋ฐฐํฌ
        env:
          API_KEY: ${{ secrets.PROD_API_KEY }}
          DB_URL:  ${{ secrets.PROD_DB_URL }}
        run: ./scripts/deploy.sh

โœ… Environment์˜ ์ง„์งœ ๊ฐ•์ 

โ€ข Required reviewers: ํŠน์ • ์ธ์›์ด ์Šน์ธํ•ด์•ผ Job ์ง„ํ–‰
โ€ข Wait timer: N๋ถ„ ๋Œ€๊ธฐ(์ทจ์†Œ ๊ธฐํšŒ ํ™•๋ณด)
โ€ข Deployment branches: main ๋ธŒ๋žœ์น˜์—์„œ๋งŒ ์šด์˜ ๋ฐฐํฌ ํ—ˆ์šฉ
โ†’ "์‹ค์ˆ˜๋กœ feature ๋ธŒ๋žœ์น˜์—์„œ ์šด์˜ ๋ฐฐํฌ" ์‚ฌ๊ณ ๋ฅผ ๊ตฌ์กฐ์ ์œผ๋กœ ์ฐจ๋‹จํ•ด.

GITHUB_TOKEN๊ณผ ๊ถŒํ•œ ์ตœ์†Œํ™”

๋ชจ๋“  ์‹คํ–‰๋งˆ๋‹ค ์ž๋™์œผ๋กœ GITHUB_TOKEN์ด ๋ฐœ๊ธ‰๋ผ. ํŽธํ•œ๋ฐ, ๊ธฐ๋ณธ ๊ถŒํ•œ์ด ๋„“์œผ๋ฉด ์œ„ํ—˜ํ•ด.

permissions:
  contents: read        # ๊ธฐ๋ณธ์€ ์ฝ๊ธฐ๋งŒ
  # ํ•„์š”ํ•œ Job์—์„œ๋งŒ ์•„๋ž˜์ฒ˜๋Ÿผ ์Šน๊ฒฉ
jobs:
  release:
    permissions:
      contents: write
      packages: write
      id-token: write   # OIDC์šฉ

๐ŸŒŸ ์žฅ๊ธฐ ํ‚ค๋ฅผ ์—†์• ๋Š” ๋ฒ•: OIDC

AWS/GCP/Azure์— ๋ฐฐํฌํ•  ๋•Œ Access Key๋ฅผ ์‹œํฌ๋ฆฟ์— ์ €์žฅํ•˜๋Š” ์‹œ๋Œ€๋Š” ๋๋‚ฌ์–ด.
GitHub์ด ๋ฐœ๊ธ‰ํ•œ ์งง์€ ์ˆ˜๋ช…์˜ ID ํ† ํฐ์œผ๋กœ ํด๋ผ์šฐ๋“œ ์—ญํ• ์„ ์ง์ ‘ ๋งก๋Š”(assume) ๋ฐฉ์‹์ด ํ‘œ์ค€์ด์•ผ.

jobs:
  deploy:
    runs-on: ubuntu-latest
    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
      - run: aws s3 sync ./dist s3://my-bucket --delete

๐Ÿ”‘ OIDC๊ฐ€ ์ข‹์€ ์ด์œ 

์˜๊ตฌ ํ‚ค๊ฐ€ ์กด์žฌํ•˜์ง€ ์•Š์œผ๋‹ˆ ์œ ์ถœ๋  ํ‚ค ์ž์ฒด๊ฐ€ ์—†๋‹ค. ๊ฒŒ๋‹ค๊ฐ€ IAM ์‹ ๋ขฐ ์ •์ฑ…์—์„œ "์ด ๋ ˆํฌ, ์ด ๋ธŒ๋žœ์น˜, ์ด ํ™˜๊ฒฝ๋งŒ"์œผ๋กœ ์กฐ๊ฑด์„ ์ขํž ์ˆ˜ ์žˆ์–ด.

๐Ÿ™ˆ ๋งˆ์Šคํ‚น์€ ๋งŒ๋Šฅ์ด ์•„๋‹ˆ๋‹ค

๋กœ๊ทธ์— ์‹œํฌ๋ฆฟ์ด ๊ทธ๋Œ€๋กœ ์ฐํžˆ๋ฉด GitHub์ด ***๋กœ ๊ฐ€๋ ค์ฃผ์ง€๋งŒ, base64 ์ธ์ฝ”๋”ฉยท๋ถ€๋ถ„ ์ถœ๋ ฅยทJSON ์žฌ์กฐ๋ฆฝ ๊ฐ™์€ ๋ณ€ํ˜•์€ ๋ชป ์žก์•„. echo $SECRET ๋””๋ฒ„๊น… ์Šต๊ด€์€ ๋ฒ„๋ฆฌ์ž.

๐Ÿณ 8์žฅ. ์‹ค์ „ ์›Œํฌํ”Œ๋กœ โ‘ก Docker ๋นŒ๋“œ & GHCR ํ‘ธ์‹œ

์ปจํ…Œ์ด๋„ˆ ์‹œ๋Œ€์˜ CD๋Š” ๋Œ€์ฒด๋กœ "์ด๋ฏธ์ง€ ๋งŒ๋“ค์–ด ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ์— ์˜ฌ๋ฆฌ๊ณ , ์„œ๋ฒ„๊ฐ€ ๊ทธ๊ฑธ ๋‹น๊ฒจ๊ฐ„๋‹ค"๋กœ ์ •๋ฆฌ๋ผ.

name: Build & Push Image

on:
  push:
    branches: [ main ]
    tags: [ 'v*.*.*' ]

env:
  REGISTRY: ghcr.io
  IMAGE_NAME: ${{ github.repository }}

jobs:
  docker:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write

    steps:
      - uses: actions/checkout@v4

      - name: Buildx ์…‹์—…
        uses: docker/setup-buildx-action@v3

      - name: GHCR ๋กœ๊ทธ์ธ
        uses: docker/login-action@v3
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: ํƒœ๊ทธ/๋ผ๋ฒจ ์ž๋™ ์ƒ์„ฑ
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
          tags: |
            type=ref,event=branch
            type=semver,pattern={{version}}
            type=sha,format=short

      - name: ๋นŒ๋“œ & ํ‘ธ์‹œ
        uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
          cache-from: type=gha
          cache-to: type=gha,mode=max
          platforms: linux/amd64,linux/arm64

ํฌ์ธํŠธ ํ•ด์„ค

cache-from/to: type=gha โ€” Docker ๋ ˆ์ด์–ด ์บ์‹œ๋ฅผ GitHub ์บ์‹œ์— ์ €์žฅ. ๋‘ ๋ฒˆ์งธ ๋นŒ๋“œ๋ถ€ํ„ฐ ์ฒด๊ฐ ์†๋„๊ฐ€ ํ™• ๋‹ฌ๋ผ์ ธ.

metadata-action โ€” ๋ธŒ๋žœ์น˜๋ฉด :main, ํƒœ๊ทธ v1.2.3์ด๋ฉด :1.2.3, ํ•ญ์ƒ :sha-abc1234. ํƒœ๊ทธ ๊ทœ์น™์„ ์†์œผ๋กœ ์งœ์ง€ ๋งˆ.

platforms ๋ฉ€ํ‹ฐ์•„์น˜ โ€” Apple Silicon ๊ฐœ๋ฐœ์ž + ARM ์„œ๋ฒ„(Graviton) ๋Œ€์‘. ๋‹จ, ์—๋ฎฌ๋ ˆ์ด์…˜์€ ๋А๋ ค์„œ ๋นŒ๋“œ ์‹œ๊ฐ„์ด ๋Š˜์–ด๋‚˜๋‹ˆ ํ•„์š”ํ•  ๋•Œ๋งŒ.

latest ํƒœ๊ทธ๋งŒ ์“ฐ์ง€ ๋ง๊ธฐ โ€” ๋กค๋ฐฑ์ด ๋ถˆ๊ฐ€๋Šฅํ•ด์ ธ. ๋ฐ˜๋“œ์‹œ ๋ถˆ๋ณ€ ํƒœ๊ทธ(sha ๋˜๋Š” semver)๋ฅผ ๋ฐฐํฌ์— ์‚ฌ์šฉํ•ด.

๐Ÿงช ๋ณด๋„ˆ์Šค: ์ด๋ฏธ์ง€ ์ทจ์•ฝ์  ์Šค์บ”

ํ‘ธ์‹œ ์ „์— Trivy ๊ฐ™์€ ์Šค์บ๋„ˆ๋ฅผ ๋„ฃ์œผ๋ฉด CVE๊ฐ€ ์‹ฌ๊ฐ๋„ ๊ธฐ์ค€์œผ๋กœ ๊ฑธ๋Ÿฌ์ ธ.
severity: CRITICAL,HIGH + exit-code: 1 ์กฐํ•ฉ์ด๋ฉด ์œ„ํ—˜ ์ด๋ฏธ์ง€๋Š” ๋ฐฐํฌ ์ž์ฒด๊ฐ€ ์ฐจ๋‹จ๋ผ.

๐Ÿšข 9์žฅ. ์‹ค์ „ ์›Œํฌํ”Œ๋กœ โ‘ข ๋ฐฐํฌ ํŒจํ„ด 4์ข…

๋ฐฐํฌ ์ „๋žต 4์ข… ๋น„๊ต Recreate v1 ์ข…๋ฃŒ ๋‹ค์šดํƒ€์ž„ v2 ์‹œ์ž‘ ๊ฐ€์žฅ ๋‹จ์ˆœ ๋น„์šฉ ์ตœ์ € ์„œ๋น„์Šค ์ค‘๋‹จ Rolling v1 v2 ์ˆœ์ฐจ ๊ต์ฒด ๋ฌด์ค‘๋‹จ ์ถ”๊ฐ€ ๋น„์šฉ ์ ์Œ ๋ฒ„์ „ ํ˜ผ์žฌ Blue / Green BLUE GREEN ํŠธ๋ž˜ํ”ฝ ์Šค์œ„์น˜ ์ฆ‰์‹œ ๋กค๋ฐฑ ๊ฒ€์ฆ ์—ฌ์œ  ๋น„์šฉ 2๋ฐฐ Canary v2 5% v2 25% v2 100% ์ง€ํ‘œ ๋ณด๋ฉฐ ํ™•๋Œ€ ๋ฆฌ์Šคํฌ ์ตœ์†Œ ๊ด€์ธก ํ•„์ˆ˜ ๊ตฌ์„ฑ ๋ณต์žก

โ‘  ์ •์  ์‚ฌ์ดํŠธ โ†’ GitHub Pages

name: Deploy Pages

on:
  push:
    branches: [ main ]

permissions:
  contents: read
  pages: write
  id-token: write

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: 20, cache: npm }
      - run: npm ci && npm run build
      - uses: actions/upload-pages-artifact@v3
        with: { path: ./dist }

  deploy:
    needs: build
    runs-on: ubuntu-latest
    environment:
      name: github-pages
      url: ${{ steps.deployment.outputs.page_url }}
    steps:
      - id: deployment
        uses: actions/deploy-pages@v4

โ‘ก ๋ฆฌ๋ˆ…์Šค ์„œ๋ฒ„ โ†’ SSH ๋ฐฐํฌ

      - name: SSH๋กœ ๋ฐฐํฌ
        uses: appleboy/ssh-action@v1
        with:
          host: ${{ secrets.SSH_HOST }}
          username: ${{ secrets.SSH_USER }}
          key: ${{ secrets.SSH_PRIVATE_KEY }}
          script: |
            set -e
            cd /srv/app
            docker compose pull
            docker compose up -d --remove-orphans
            docker image prune -f

๐Ÿ’ก set -e ๊ผญ ๋„ฃ์ž

์—†์œผ๋ฉด ์ค‘๊ฐ„ ๋ช…๋ น์ด ์‹คํŒจํ•ด๋„ ์Šคํฌ๋ฆฝํŠธ๊ฐ€ ๊ณ„์† ์ง„ํ–‰๋ผ์„œ "๋ฐฐํฌ ์„ฑ๊ณต"์œผ๋กœ ํ‘œ์‹œ๋œ ์‹คํŒจ ๋ฐฐํฌ๊ฐ€ ์ƒ๊ธด๋‹ค. ์ด๊ฒŒ ์ œ์ผ ๋ฌด์„œ์šด ์œ ํ˜•์˜ ๋ฒ„๊ทธ์•ผ.

โ‘ข Kubernetes โ†’ ์ด๋ฏธ์ง€ ํƒœ๊ทธ ๊ต์ฒด

      - name: kubeconfig ์ค€๋น„
        run: |
          mkdir -p ~/.kube
          echo "${{ secrets.KUBE_CONFIG }}" | base64 -d > ~/.kube/config
          chmod 600 ~/.kube/config

      - name: ๋กค๋ง ์—…๋ฐ์ดํŠธ
        run: |
          kubectl set image deployment/web \
            web=ghcr.io/${{ github.repository }}:sha-${GITHUB_SHA::7} \
            -n production
          kubectl rollout status deployment/web -n production --timeout=180s

      - name: ์‹คํŒจ ์‹œ ๋กค๋ฐฑ
        if: failure()
        run: kubectl rollout undo deployment/web -n production

โ‘ฃ ์„œ๋ฒ„๋ฆฌ์Šค / ๋งค๋‹ˆ์ง€๋“œ ํ”Œ๋žซํผ

Vercel, Netlify, Cloud Run, Lambda ๋“ฑ์€ ๋Œ€๋ถ€๋ถ„ ๊ณต์‹ Action์ด๋‚˜ CLI๊ฐ€ ์žˆ์–ด์„œ
npx vercel deploy --prod, gcloud run deploy, serverless deploy ํ•œ ์ค„๋กœ ๋๋‚˜.
์ด ๊ฒฝ์šฐ ํ•ต์‹ฌ์€ ์ธ์ฆ์„ OIDC๋กœ ์ฒ˜๋ฆฌํ–ˆ๋Š”๊ฐ€์•ผ.

๐Ÿงฌ 10์žฅ. ์žฌ์‚ฌ์šฉ ์›Œํฌํ”Œ๋กœ & Composite Action

์„œ๋น„์Šค๊ฐ€ 10๊ฐœ๋ฉด ์›Œํฌํ”Œ๋กœ๋„ 10๊ฐœ. ๊ทธ๋Ÿฐ๋ฐ ๋‚ด์šฉ์€ ๊ฑฐ์˜ ๋ณต๋ถ™์ด์ง€.
์ด๋•Œ ๋ณต๋ถ™์„ ๋ชจ๋“ˆํ™”ํ•˜๋Š” ๋‘ ๊ฐ€์ง€ ๋ฐฉ๋ฒ•์ด ์žˆ์–ด.

Reusable Workflow (Job ๋‹จ์œ„ ์žฌ์‚ฌ์šฉ)

# .github/workflows/reusable-node-ci.yml
name: Reusable Node CI
on:
  workflow_call:
    inputs:
      node-version:
        type: string
        default: '20'
      working-directory:
        type: string
        default: '.'
    secrets:
      NPM_TOKEN:
        required: false

jobs:
  ci:
    runs-on: ubuntu-latest
    defaults:
      run:
        working-directory: ${{ inputs.working-directory }}
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ inputs.node-version }}
          cache: npm
      - run: npm ci
      - run: npm test
# ํ˜ธ์ถœํ•˜๋Š” ์ชฝ
jobs:
  web:
    uses: ./.github/workflows/reusable-node-ci.yml
    with:
      node-version: '20'
      working-directory: apps/web
    secrets: inherit

Composite Action (Step ๋‹จ์œ„ ์žฌ์‚ฌ์šฉ)

# .github/actions/setup-project/action.yml
name: 'Setup Project'
description: '๋…ธ๋“œ ์„ธํŒ… + ์˜์กด์„ฑ ์„ค์น˜'
inputs:
  node-version:
    default: '20'
runs:
  using: 'composite'
  steps:
    - uses: actions/setup-node@v4
      with:
        node-version: ${{ inputs.node-version }}
        cache: npm
    - run: npm ci
      shell: bash      # composite์—์„œ๋Š” shell ํ•„์ˆ˜!
๊ตฌ๋ถ„Reusable WorkflowComposite Action
์žฌ์‚ฌ์šฉ ๋‹จ์œ„Job(๋“ค) ์ „์ฒดStep ๋ฌถ์Œ
runs-on ์ง€์ •๊ฐ€๋Šฅ๋ถˆ๊ฐ€(ํ˜ธ์ถœ Job ๋”ฐ๋ผ๊ฐ)
์‹œํฌ๋ฆฟ ์ „๋‹ฌsecrets: ๋ช…์‹œ/inherit์ž…๋ ฅ๊ฐ’์œผ๋กœ ์ „๋‹ฌ
์ ํ•ฉํ•œ ์ƒํ™ฉํ‘œ์ค€ CI ํŒŒ์ดํ”„๋ผ์ธ ํ†ต์ผ๊ณตํ†ต ์ค€๋น„ ์Šคํ… ์ถ•์•ฝ

๐Ÿ—๏ธ ์กฐ์ง ํ‘œ์ค€ ๋งŒ๋“ค๊ธฐ

org/.github-workflows ๊ฐ™์€ ๊ณต์šฉ ๋ ˆํฌ์— ์žฌ์‚ฌ์šฉ ์›Œํฌํ”Œ๋กœ๋ฅผ ๋ชจ์•„๋‘๊ณ , ๊ฐ ์„œ๋น„์Šค๋Š” uses: org/repo/.github/workflows/ci.yml@v1๋กœ ํ˜ธ์ถœ.
๋ณด์•ˆ ์ •์ฑ…ยทํ…Œ์ŠคํŠธ ๊ธฐ์ค€์„ ํ•œ ๊ณณ์—์„œ ์—…๋ฐ์ดํŠธํ•  ์ˆ˜ ์žˆ์–ด์„œ ํ”Œ๋žซํผ ํŒ€์ด ์ •๋ง ์ข‹์•„ํ•˜๋Š” ํŒจํ„ด์ด์•ผ.

๐ŸŽ›๏ธ 11์žฅ. ์กฐ๊ฑด๋ถ€ ์‹คํ–‰, ํ‘œํ˜„์‹, ์ถœ๋ ฅ๊ฐ’

if ํ‘œํ˜„์‹

      - name: main์ผ ๋•Œ๋งŒ
        if: github.ref == 'refs/heads/main'
        run: echo "์šด์˜ ๋ธŒ๋žœ์น˜"

      - name: PR์— ํŠน์ • ๋ผ๋ฒจ ์žˆ์„ ๋•Œ๋งŒ
        if: contains(github.event.pull_request.labels.*.name, 'deploy-preview')
        run: ./preview.sh

      - name: ์ด์ „ ์Šคํ… ์‹คํŒจํ•ด๋„ ์‹คํ–‰
        if: always()
        run: ./cleanup.sh

      - name: ์‹คํŒจํ–ˆ์„ ๋•Œ๋งŒ ์•Œ๋ฆผ
        if: failure()
        run: ./notify-slack.sh

      - name: ์ปค๋ฐ‹ ๋ฉ”์‹œ์ง€์— skip ์—†์„ ๋•Œ
        if: "!contains(github.event.head_commit.message, '[skip deploy]')"
        run: ./deploy.sh

Step/Job ์ถœ๋ ฅ๊ฐ’ ์ฃผ๊ณ ๋ฐ›๊ธฐ

jobs:
  prepare:
    runs-on: ubuntu-latest
    outputs:
      version: ${{ steps.ver.outputs.value }}
    steps:
      - uses: actions/checkout@v4
      - id: ver
        run: echo "value=$(node -p "require('./package.json').version")" >> $GITHUB_OUTPUT

  release:
    needs: prepare
    runs-on: ubuntu-latest
    steps:
      - run: echo "๋ฐฐํฌ ๋ฒ„์ „ ${{ needs.prepare.outputs.version }}"

โ˜ ๏ธ ์Šคํฌ๋ฆฝํŠธ ์ธ์ ์…˜ ์ฃผ์˜

run: echo "${{ github.event.issue.title }}" ์ฒ˜๋Ÿผ ์‚ฌ์šฉ์ž ์ž…๋ ฅ์„ run์— ์ง์ ‘ ๋ณด๊ฐ„ํ•˜๋ฉด, ์ œ๋ชฉ์— ์…ธ ๋ช…๋ น์„ ์‹ฌ์–ด ์‹คํ–‰์‹œํ‚ฌ ์ˆ˜ ์žˆ์–ด.
๋ฐ˜๋“œ์‹œ env๋กœ ํ•œ ๋ฒˆ ๋ฐ›์•„์„œ ์“ฐ์ž:

        env:
          TITLE: ${{ github.event.issue.title }}
        run: echo "$TITLE"

์•Œ์•„๋‘๋ฉด ํŽธํ•œ ์ปจํ…์ŠคํŠธ

ํ‘œํ˜„์‹์˜๋ฏธ
github.sha์ปค๋ฐ‹ ํ•ด์‹œ(์ด๋ฏธ์ง€ ํƒœ๊ทธ๋กœ ์ตœ์ )
github.ref_name๋ธŒ๋žœ์น˜/ํƒœ๊ทธ ์ด๋ฆ„
github.event_nameํŠธ๋ฆฌ๊ฑฐ ์ด๋ฒคํŠธ ์ข…๋ฅ˜
github.actor์‹คํ–‰์„ ์œ ๋ฐœํ•œ ์‚ฌ์šฉ์ž
runner.osLinux / Windows / macOS
job.statussuccess / failure / cancelled

๐Ÿ–ฅ๏ธ 12์žฅ. Self-hosted Runner โ€” ์–ธ์ œ ํ•„์š”ํ•œ๊ฐ€

GitHub ๋Ÿฌ๋„ˆ๋Š” ๋งค๋ฒˆ ๊นจ๋—ํ•œ ์ƒˆ VM์ด๋ผ ์•ˆ์ „ํ•˜์ง€๋งŒ, ์ด๋Ÿฐ ์ƒํ™ฉ์—” ํ•œ๊ณ„๊ฐ€ ์žˆ์–ด.

๋‚ด๋ถ€๋ง ์ ‘๊ทผ โ€” ์‚ฌ๋‚ด DB, VPN ๋’ค์˜ ์Šคํ…Œ์ด์ง• ์„œ๋ฒ„์— ๋ถ™์–ด์•ผ ํ•  ๋•Œ.

๊ณ ์‚ฌ์–‘/ํŠน์ˆ˜ ํ•˜๋“œ์›จ์–ด โ€” GPU ํ…Œ์ŠคํŠธ, ๋Œ€์šฉ๋Ÿ‰ ๋นŒ๋“œ, ๊ธด ์ปดํŒŒ์ผ.

๋Œ€๋Ÿ‰ ์‚ฌ์šฉ์œผ๋กœ ๋น„์šฉ ์—ญ์ „ โ€” ์›” ์ˆ˜๋งŒ ๋ถ„ ์“ฐ๋ฉด ์ž์ฒด ๋Ÿฌ๋„ˆ๊ฐ€ ์ €๋ ดํ•  ์ˆ˜ ์žˆ์–ด.

์บ์‹œ ๋กœ์ปฌํ™” โ€” ๊ฑฐ๋Œ€ํ•œ ์˜์กด์„ฑ ์บ์‹œ๋ฅผ ๋””์Šคํฌ์— ์ƒ์ฃผ์‹œ์ผœ ์†๋„ ํ™•๋ณด.

๐Ÿšซ ํผ๋ธ”๋ฆญ ๋ ˆํฌ์— self-hosted runner ์“ฐ์ง€ ๋งˆ

GitHub ๊ณต์‹ ๊ถŒ๊ณ ์‚ฌํ•ญ์ด์•ผ. ๋ˆ„๊ตฌ๋‚˜ PR๋กœ ์ž„์˜ ์ฝ”๋“œ๋ฅผ ๋ณด๋‚ผ ์ˆ˜ ์žˆ๊ณ , self-hosted๋Š” ์ƒํƒœ๊ฐ€ ์œ ์ง€๋˜๋ฏ€๋กœ ๋Ÿฌ๋„ˆ ์ž์ฒด๊ฐ€ ์นจํ•ด๋  ์ˆ˜ ์žˆ์–ด.
์จ์•ผ ํ•œ๋‹ค๋ฉด ephemeral(1ํšŒ์šฉ) ๋Ÿฌ๋„ˆ + ์ปจํ…Œ์ด๋„ˆ ๊ฒฉ๋ฆฌ + ๋„คํŠธ์›Œํฌ ์ตœ์†Œ ๊ถŒํ•œ์ด ๊ธฐ๋ณธ์ด๋‹ค.

โš™๏ธ ํ˜„์‹ค์  ๊ตฌ์„ฑ

์ฟ ๋ฒ„๋„คํ‹ฐ์Šค ํ™˜๊ฒฝ์ด๋ฉด ARC(Actions Runner Controller)๋กœ Job๋งˆ๋‹ค Pod๋ฅผ ๋„์šฐ๊ณ  ๋๋‚˜๋ฉด ์‚ญ์ œํ•˜๋Š” ๋ฐฉ์‹์ด ๊ฐ€์žฅ ๊น”๋”ํ•ด. ์˜คํ† ์Šค์ผ€์ผ + ๊ฒฉ๋ฆฌ๋ฅผ ๋™์‹œ์— ์–ป๋Š”๋‹ค.

๐Ÿ’ธ 13์žฅ. ๋น„์šฉ๊ณผ ์†๋„ ์ตœ์ ํ™” ์ฒดํฌ๋ฆฌ์ŠคํŠธ

ํ”„๋ผ์ด๋น— ๋ ˆํฌ๋Š” ๋ถ„(minutes) ๋‹จ์œ„ ๊ณผ๊ธˆ์ด๊ณ , OS๋ณ„ ๋ฐฐ์ˆ˜๊ฐ€ ์žˆ์–ด.

๋Ÿฌ๋„ˆ๋ถ„๋‹น ๊ณผ๊ธˆ ๋ฐฐ์ˆ˜๋ฉ”๋ชจ
Linuxร—1๊ธฐ๋ณธ ์„ ํƒ์ง€
Windowsร—2๊ผญ ํ•„์š”ํ•  ๋•Œ๋งŒ
macOSร—10iOS ๋นŒ๋“œ ์ „์šฉ

์ตœ์ ํ™” 12๊ณ„๋ช…

1. paths / paths-ignore๋กœ ๋ฌธ์„œยท์„ค์ • ๋ณ€๊ฒฝ ์‹œ CI ์Šคํ‚ต

2. concurrency: cancel-in-progress๋กœ ์ค‘๋ณต ์‹คํ–‰ ์ œ๊ฑฐ

3. ์บ์‹œ ์ ๊ทน ํ™œ์šฉ (์˜์กด์„ฑ + Docker ๋ ˆ์ด์–ด)

4. Job ๋ณ‘๋ ฌํ™” โ€” ๋ฆฐํŠธ/ํ…Œ์ŠคํŠธ/๋นŒ๋“œ๋ฅผ ๋‚˜๋ˆ  ๋™์‹œ ์‹คํ–‰

5. ๋ฌด๊ฑฐ์šด E2E๋Š” PR๋งˆ๋‹ค ๋ง๊ณ  main ๋ณ‘ํ•ฉ ํ›„ / ์•ผ๊ฐ„์—

6. fetch-depth: 1 (checkout ๊ธฐ๋ณธ๊ฐ’)๋กœ ์–•์€ ํด๋ก  ์œ ์ง€

7. timeout-minutes ํ•ญ์ƒ ์„ค์ •

8. matrix ์กฐํ•ฉ ์ตœ์†Œํ™”, fail-fast ์ „๋žต ์˜์‹์ ์œผ๋กœ ์„ ํƒ

9. ์•„ํ‹ฐํŒฉํŠธ retention-days ์งง๊ฒŒ (์Šคํ† ๋ฆฌ์ง€๋„ ๊ณผ๊ธˆ)

10. ๋ฉ€ํ‹ฐ์Šคํ…Œ์ด์ง€ Dockerfile๋กœ ์ด๋ฏธ์ง€ยท๋นŒ๋“œ ์‹œ๊ฐ„ ์ถ•์†Œ

11. ํ…Œ์ŠคํŠธ ์ƒค๋”ฉ(๋ถ„ํ• )์œผ๋กœ ๊ธด ํ…Œ์ŠคํŠธ๋ฅผ ๋ณ‘๋ ฌ ์ฒ˜๋ฆฌ

12. ์กฐ์ง ๋‹จ์œ„ Usage ๋ฆฌํฌํŠธ๋ฅผ ์›” 1ํšŒ ํ™•์ธ โ€” ๋ฒ”์ธ์€ ๋Š˜ ํ•˜๋‚˜์˜ ์›Œํฌํ”Œ๋กœ๋‹ค

๐ŸŽฏ ๋ชฉํ‘œ ์ˆ˜์น˜

PR CI๋Š” 10๋ถ„ ์ด๋‚ด๊ฐ€ ์‹ฌ๋ฆฌ์  ๋งˆ์ง€๋…ธ์„ ์ด์•ผ.
15๋ถ„ ๋„˜์–ด๊ฐ€๋ฉด ๊ฐœ๋ฐœ์ž๋Š” ๊ฒฐ๊ณผ๋ฅผ ๊ธฐ๋‹ค๋ฆฌ์ง€ ์•Š๊ณ  ๋‹ค๋ฅธ ์ผ์„ ์‹œ์ž‘ํ•˜๊ณ , ์ปจํ…์ŠคํŠธ ์Šค์œ„์นญ ๋น„์šฉ์ด ํญ๋ฐœํ•ด. ๊ฒฐ๊ตญ "CI ๋ฌด์‹œ ๋ฌธํ™”"๊ฐ€ ์ƒ๊ธด๋‹ค.

๐Ÿงฏ 14์žฅ. ๋””๋ฒ„๊น… ์ƒ์กด ๊ฐ€์ด๋“œ

์ž์ฃผ ๋‚˜๋Š” ์—๋Ÿฌ์™€ ํ•ด๊ฒฐ

์ฆ์ƒ์›์ธํ•ด๊ฒฐ
ํŒŒ์ผ์ด ์—†๋‹ค๊ณ  ํ•จcheckout ๋ˆ„๋ฝ1๋ฒˆ ์Šคํ…์— ์ถ”๊ฐ€
๊ถŒํ•œ ๊ฑฐ๋ถ€(403)permissions ๋ถ€์กฑํ•ด๋‹น Job์— write ๊ถŒํ•œ ๋ถ€์—ฌ
์‹œํฌ๋ฆฟ์ด ๋นˆ ๊ฐ’ํฌํฌ PR / ์ด๋ฆ„ ์˜คํƒ€ / Environment ๋ฏธ์ง€์ •์Šค์ฝ”ํ”„ ํ™•์ธ
๋กœ์ปฌ์€ ๋˜๋Š”๋ฐ CI๋งŒ ์‹คํŒจ๋Œ€์†Œ๋ฌธ์ž ๊ตฌ๋ถ„(Linux) ยท ์‹œ๊ฐ„๋Œ€ ยท ๋กœ์ผ€์ผ๊ฒฝ๋กœ ๋Œ€์†Œ๋ฌธ์ž, TZ ๊ณ ์ •
YAML ํŒŒ์‹ฑ ์—๋Ÿฌํƒญ ๋ฌธ์ž, ๋“ค์—ฌ์“ฐ๊ธฐ ๋ถˆ์ผ์น˜์ŠคํŽ˜์ด์Šค 2์นธ ํ†ต์ผ
์›Œํฌํ”Œ๋กœ๊ฐ€ ์•„์˜ˆ ์•ˆ ๋Œ์•„๊ฐ๊ฒฝ๋กœ ์˜ค๋ฅ˜ / ๊ธฐ๋ณธ ๋ธŒ๋žœ์น˜ ์•„๋‹˜ / ๋ฌธ๋ฒ• ์˜ค๋ฅ˜.github/workflows/ ํ™•์ธ

๋””๋ฒ„๊ทธ ๋ฌด๊ธฐ 3์ข…

โ‘  ๋””๋ฒ„๊ทธ ๋กœ๊ทธ ์ผœ๊ธฐ โ€” ๋ ˆํฌ ์‹œํฌ๋ฆฟ/๋ณ€์ˆ˜์— ACTIONS_STEP_DEBUG: true ์ถ”๊ฐ€ํ•˜๋ฉด ์ƒ์„ธ ๋กœ๊ทธ๊ฐ€ ๋‚˜์™€.

โ‘ก ํ™˜๊ฒฝ ๋คํ”„

      - run: |
          echo "=== env ==="; env | sort
          echo "=== pwd ==="; pwd; ls -al
          echo "=== versions ==="; node -v; npm -v

โ‘ข ๋กœ์ปฌ ์žฌํ˜„ โ€” act ๊ฐ™์€ ๋„๊ตฌ๋กœ ์›Œํฌํ”Œ๋กœ๋ฅผ ๋กœ์ปฌ ๋„์ปค์—์„œ ๋Œ๋ ค๋ณผ ์ˆ˜ ์žˆ์–ด. ์™„๋ฒฝํžˆ ๋™์ผํ•˜์ง„ ์•Š์ง€๋งŒ YAML ๋ฌธ๋ฒ•/ํ๋ฆ„ ๊ฒ€์ฆ์—” ์ถฉ๋ถ„ํ•ด.

๐Ÿ“ Job Summary ํ™œ์šฉ

$GITHUB_STEP_SUMMARY์— ๋งˆํฌ๋‹ค์šด์„ ์“ฐ๋ฉด ์‹คํ–‰ ๊ฒฐ๊ณผ ํŽ˜์ด์ง€์— ์˜ˆ์˜๊ฒŒ ํ‘œ์‹œ๋ผ.
ํ…Œ์ŠคํŠธ ์ปค๋ฒ„๋ฆฌ์ง€, ๋ฐฐํฌ๋œ ๋ฒ„์ „, ๋ณ€๊ฒฝ ํŒŒ์ผ ๋ชฉ๋ก ๊ฐ™์€ ๊ฑธ ๋„ฃ์–ด๋‘๋ฉด ํŒ€์›๋“ค์ด ๋กœ๊ทธ๋ฅผ ํŒŒํ—ค์น˜์ง€ ์•Š์•„๋„ ๋ผ.

      - run: |
          echo "### โœ… ๋ฐฐํฌ ์™„๋ฃŒ" >> $GITHUB_STEP_SUMMARY
          echo "- ๋ฒ„์ „: \`${GITHUB_SHA::7}\`" >> $GITHUB_STEP_SUMMARY

๐ŸŒฑ 15์žฅ. ์„ฑ์ˆ™ํ•œ ํŒŒ์ดํ”„๋ผ์ธ์œผ๋กœ ๊ฐ€๋Š” ๋กœ๋“œ๋งต

CI/CD ์„ฑ์ˆ™๋„ 5๋‹จ๊ณ„ Lv.1 ์ˆ˜๋™ SSH ์ง์ ‘ ๋ฐฐํฌ Lv.2 CI ํ…Œ์ŠคํŠธ ์ž๋™ํ™” Lv.3 CD staging ์ž๋™ ๋ฐฐํฌ Lv.4 ์šด์˜์ž๋™ ์Šน์ธ + ๋กค๋ฐฑ Lv.5 ๊ด€์ธก์ฃผ๋„ ์นด๋‚˜๋ฆฌ + ์ž๋™๋กค๋ฐฑ ํ•œ ๋ฒˆ์— Lv.5๋กœ ๊ฐ€์ง€ ๋ง ๊ฒƒ โ€” ํ•œ ๋‹จ๊ณ„์”ฉ, ํŒ€์ด ์‹ ๋ขฐ๋ฅผ ์Œ“๋Š” ์†๋„๋กœ

๋‹จ๊ณ„๋ณ„ ์‹คํ–‰ ๊ณผ์ œ

Lv.1 โ†’ Lv.2
PR์— npm test ํ•˜๋‚˜๋งŒ ๋ถ™์—ฌ. ๊ทธ๋ฆฌ๊ณ  Branch protection์—์„œ "status check ํ•„์ˆ˜"๋กœ ๊ฑธ์–ด. ์ด ์ˆœ๊ฐ„๋ถ€ํ„ฐ ๊นจ์ง„ ์ฝ”๋“œ๊ฐ€ main์— ๋ชป ๋“ค์–ด์˜จ๋‹ค.

Lv.2 โ†’ Lv.3
์ด๋ฏธ์ง€ ๋นŒ๋“œ + ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ ํ‘ธ์‹œ + staging ์ž๋™ ๋ฐฐํฌ. ์—ฌ๊ธฐ์„œ ๋ถˆ๋ณ€ ํƒœ๊ทธ ์Šต๊ด€์„ ๋“ค์—ฌ๋†“๋Š” ๊ฒŒ ํ•ต์‹ฌ์ด์•ผ.

Lv.3 โ†’ Lv.4
production Environment + Required reviewers. ๋กค๋ฐฑ ์Šคํฌ๋ฆฝํŠธ๋ฅผ ๋ฏธ๋ฆฌ ๋งŒ๋“ค๊ณ  ์‹ค์ œ๋กœ ํ•œ ๋ฒˆ ๋Œ๋ ค๋ด. ์•ˆ ์จ๋ณธ ๋กค๋ฐฑ์€ ์—†๋Š” ๋กค๋ฐฑ์ด๋‹ค.

Lv.4 โ†’ Lv.5
์—๋Ÿฌ์œจยท์ง€์—ฐ์‹œ๊ฐ„ ์ง€ํ‘œ๋ฅผ ๋ณด๊ณ  ํŠธ๋ž˜ํ”ฝ์„ ์ ์ง„ ํ™•๋Œ€. ์ž„๊ณ„์น˜ ์ดˆ๊ณผ ์‹œ ์ž๋™ ๋กค๋ฐฑ. ์—ฌ๊ธฐ๊นŒ์ง€ ์˜ค๋ฉด ๋ฐฐํฌ๊ฐ€ ์ด๋ฒคํŠธ๊ฐ€ ์•„๋‹ˆ๋ผ ์ผ์ƒ์ด ๋ผ.

"๋ฐฐํฌ๊ฐ€ ๋ฌด์„œ์šด ํŒ€์€ ๋ฐฐํฌ๋ฅผ ๋“œ๋ฌผ๊ฒŒ ํ•˜๊ณ ,
๋ฐฐํฌ๋ฅผ ๋“œ๋ฌผ๊ฒŒ ํ•˜๋Š” ํŒ€์€ ๋ฐฐํฌ๊ฐ€ ๋” ๋ฌด์„œ์›Œ์ง„๋‹ค."

์ด ์•…์ˆœํ™˜์„ ๋Š๋Š” ์œ ์ผํ•œ ๋„๊ตฌ๊ฐ€ ์ž๋™ํ™”์•ผ.

๐Ÿงพ 16์žฅ. ์‹ค๋ฌด ํ†ตํ•ฉ ์˜ˆ์‹œ โ€” ํ•˜๋‚˜๋กœ ํ•ฉ์ณ๋ณด๊ธฐ

name: CI/CD Pipeline

on:
  push:
    branches: [ main ]
  pull_request:

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

permissions:
  contents: read

jobs:
  # 1) ํ’ˆ์งˆ ๊ฒ€์ฆ (๋ชจ๋“  PR)
  test:
    runs-on: ubuntu-latest
    timeout-minutes: 15
    services:
      postgres:
        image: postgres:16
        env:
          POSTGRES_PASSWORD: test
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-retries 5
        ports: [ '5432:5432' ]
    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 test
        env:
          DATABASE_URL: postgres://postgres:test@localhost:5432/postgres
      - name: ์š”์•ฝ ๊ธฐ๋ก
        if: always()
        run: echo "### ํ…Œ์ŠคํŠธ ๊ฒฐ๊ณผ: ${{ job.status }}" >> $GITHUB_STEP_SUMMARY

  # 2) ์ด๋ฏธ์ง€ ๋นŒ๋“œ (main๋งŒ)
  build:
    needs: test
    if: github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write
    outputs:
      tag: ${{ steps.t.outputs.tag }}
    steps:
      - uses: actions/checkout@v4
      - id: t
        run: echo "tag=sha-${GITHUB_SHA::7}" >> $GITHUB_OUTPUT
      - uses: docker/setup-buildx-action@v3
      - uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
      - uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: ghcr.io/${{ github.repository }}:${{ steps.t.outputs.tag }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

  # 3) staging ์ž๋™ ๋ฐฐํฌ
  deploy-staging:
    needs: build
    runs-on: ubuntu-latest
    environment: staging
    steps:
      - run: echo "staging ๋ฐฐํฌ ${{ needs.build.outputs.tag }}"

  # 4) production ์Šน์ธ ํ›„ ๋ฐฐํฌ
  deploy-prod:
    needs: [ build, deploy-staging ]
    runs-on: ubuntu-latest
    environment:
      name: production
      url: https://example.com
    steps:
      - run: echo "production ๋ฐฐํฌ ${{ needs.build.outputs.tag }}"
      - name: ์‹คํŒจ ์•Œ๋ฆผ
        if: failure()
        run: echo "๋กค๋ฐฑ ์ ˆ์ฐจ ์‹œ์ž‘"

๐Ÿงฉ services: ๋ธ”๋ก

ํ…Œ์ŠคํŠธ์— DB/Redis๊ฐ€ ํ•„์š”ํ•  ๋•Œ Docker ์ปจํ…Œ์ด๋„ˆ๋ฅผ ์‚ฌ์ด๋“œ์นด๋กœ ๋„์›Œ์ค˜.
--health-cmd๋กœ ์ค€๋น„ ์™„๋ฃŒ๋ฅผ ๊ธฐ๋‹ค๋ฆฌ๊ฒŒ ํ•˜๋Š” ๊ฒŒ ํ•ต์‹ฌ์ด์•ผ. ์•ˆ ํ•˜๋ฉด "connection refused" ํ”Œ๋ ˆ์ดํ‚ค ํ…Œ์ŠคํŠธ์— ์‹œ๋‹ฌ๋ฆฐ๋‹ค.

๐Ÿ”’ 17์žฅ. ๋ณด์•ˆ ํ•˜๋“œ๋‹ ์ตœ์ข… ์ ๊ฒ€

1. ์ตœ์†Œ ๊ถŒํ•œ โ€” ์›Œํฌํ”Œ๋กœ ์ตœ์ƒ๋‹จ permissions: contents: read ๊ธฐ๋ณธ๊ฐ’, ํ•„์š”ํ•œ Job๋งŒ ์Šน๊ฒฉ.

2. ์•ก์…˜ ๋ฒ„์ „ ๊ณ ์ • โ€” ์‹ ๋ขฐ ์กฐ์ง ์™ธ์˜ ์•ก์…˜์€ ์ปค๋ฐ‹ SHA๋กœ ํ•€.

3. ํ—ˆ์šฉ ๋ชฉ๋ก โ€” ์กฐ์ง ์„ค์ •์—์„œ ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ Action์„ ํ™”์ดํŠธ๋ฆฌ์ŠคํŠธ๋กœ ์ œํ•œ.

4. OIDC ์ „ํ™˜ โ€” ์žฅ๊ธฐ ํด๋ผ์šฐ๋“œ ํ‚ค ์ œ๊ฑฐ.

5. ์ธ์ ์…˜ ์ฐจ๋‹จ โ€” ์‚ฌ์šฉ์ž ์ž…๋ ฅ์€ ํ•ญ์ƒ env ๊ฒฝ์œ .

6. Environment ๋ณดํ˜ธ โ€” ์šด์˜ ๋ฐฐํฌ๋Š” ์Šน์ธ + ๋ธŒ๋žœ์น˜ ์ œํ•œ.

7. ์‹œํฌ๋ฆฟ ์Šค์บ๋‹ + Dependabot โ€” ์œ ์ถœ ํƒ์ง€์™€ ์˜์กด์„ฑ ํŒจ์น˜ ์ž๋™ํ™”.

8. ๋กœ๊ทธ ์œ„์ƒ โ€” ๋””๋ฒ„๊ทธ์šฉ ์‹œํฌ๋ฆฟ ์ถœ๋ ฅ ๊ธˆ์ง€, ์‹คํ–‰ ๋กœ๊ทธ ์ ‘๊ทผ ๊ถŒํ•œ ๊ด€๋ฆฌ.

9. ํฌํฌ PR ์ •์ฑ… โ€” ์ฒซ ๊ธฐ์—ฌ์ž ์›Œํฌํ”Œ๋กœ๋Š” ์Šน์ธ ํ›„ ์‹คํ–‰์œผ๋กœ ์„ค์ •.

10. ์•„ํ‹ฐํŒฉํŠธ ์ ๊ฒ€ โ€” .env, ํ‚ค ํŒŒ์ผ์ด ์•„ํ‹ฐํŒฉํŠธ์— ์„ž์ด์ง€ ์•Š๋Š”์ง€ ํ™•์ธ.

๐Ÿงจ ์‹ค์ œ๋กœ ์ž์ฃผ ํ„ฐ์ง€๋Š” ์‚ฌ๊ณ 

โ‘  ๋กœ์ปฌ .env๋ฅผ ๊ทธ๋Œ€๋กœ ์ปค๋ฐ‹ โ†’ ํžˆ์Šคํ† ๋ฆฌ์— ์˜๊ตฌ ๊ธฐ๋ก
โ‘ก ์‹ ๋ขฐ ์•ˆ ๋˜๋Š” ์•ก์…˜์ด ์—…๋ฐ์ดํŠธ๋˜๋ฉฐ ์‹œํฌ๋ฆฟ ํƒˆ์ทจ(๊ณต๊ธ‰๋ง ๊ณต๊ฒฉ)
โ‘ข ํผ๋ธ”๋ฆญ ๋ ˆํฌ self-hosted ๋Ÿฌ๋„ˆ๊ฐ€ ํฌ๋ฆฝํ†  ๋งˆ์ด๋‹์— ์•…์šฉ
์„ธ ๊ฐ€์ง€ ๋ชจ๋‘ ์„ค์ • ํ•œ ์ค„๋กœ ๋ง‰์„ ์ˆ˜ ์žˆ๋Š” ๊ฒƒ๋“ค์ด์•ผ.

๐ŸŽ“ ์—ํ•„๋กœ๊ทธ: ์ž๋™ํ™”๋Š” ๊ฒฐ๊ตญ 'ํŒ€์˜ ํƒœ๋„'

์—ฌ๊ธฐ๊นŒ์ง€ ์™”์œผ๋ฉด ์ด์ œ ์•Œ ๊ฑฐ์•ผ.
GitHub Actions๋Š” YAML ๋ฌธ๋ฒ• ์‹ธ์›€์ด ์•„๋‹ˆ๋ผ ํŒ€์˜ ์•ฝ์†์„ ์ฝ”๋“œ๋กœ ๊ตณํžˆ๋Š” ์ž‘์—…์ด๋ผ๋Š” ๊ฑธ.

"ํ…Œ์ŠคํŠธ๋Š” ํ†ต๊ณผํ•ด์•ผ ๋จธ์ง€ํ•œ๋‹ค"
"์šด์˜ ๋ฐฐํฌ๋Š” ๋‘ ๋ช…์ด ์Šน์ธํ•œ๋‹ค"
"๋กค๋ฐฑ์€ 3๋ถ„ ์•ˆ์— ๊ฐ€๋Šฅํ•ด์•ผ ํ•œ๋‹ค"

์ด๋Ÿฐ ๊ทœ์น™์„ ์‚ฌ๋žŒ ๊ธฐ์–ต์— ๋งก๊ธฐ๋ฉด ๋ฌด๋„ˆ์ง€๊ณ , ํŒŒ์ดํ”„๋ผ์ธ์— ์ƒˆ๊ธฐ๋ฉด ์œ ์ง€๋œ๋‹ค.
๊ทธ๊ฒŒ DevOps์˜ ๋ณธ์งˆ์ด์•ผ. ๋„๊ตฌ๊ฐ€ ๋ฌธํ™”๋ฅผ ์ง€์ผœ์ฃผ๋Š” ๊ตฌ์กฐ๋ฅผ ๋งŒ๋“œ๋Š” ๊ฑฐ.

๐ŸŒณ ์˜ค๋Š˜ ๋‹น์žฅ ํ•  ์ˆ˜ ์žˆ๋Š” ๊ฒƒ

โ‘  .github/workflows/ci.yml ๋งŒ๋“ค๊ณ  ํ…Œ์ŠคํŠธ ํ•œ ์ค„ ๋Œ๋ฆฌ๊ธฐ
โ‘ก Branch protection์— status check ํ•„์ˆ˜๋กœ ๊ฑธ๊ธฐ
โ‘ข concurrency์™€ timeout-minutes ์ถ”๊ฐ€ํ•˜๊ธฐ
์ด ์„ธ ๊ฐœ๋งŒ ํ•ด๋„ ์–ด์ œ๋ณด๋‹ค ํ›จ์”ฌ ๋‹จ๋‹จํ•œ ๋ ˆํฌ๊ฐ€ ๋ผ. ์ง„์งœ์•ผ.

ํด๋ผ์šฐ๋“œ/DevOps ์˜์—ญ์€ ํ˜ผ์ž ํŒŒ๋ฉด ์‚ฝ์งˆ ์‹œ๊ฐ„์ด ์œ ๋… ๊ธธ์–ด.
์ด๋ฏธ ๊ฒช์–ด๋ณธ ์‚ฌ๋žŒ์˜ 30๋ถ„ ์ƒ๋‹ด์ด ํ˜ผ์ž 3์ผ ํ—ค๋งค๋Š” ๊ฑธ ๋Œ€์ฒดํ•˜๋Š” ๋ถ„์•ผ๊ฑฐ๋“ .
๊ทธ๋ž˜์„œ ์žฌ๋Šฅ๋„ท ๊ฐ™์€ ์žฌ๋Šฅ ๊ณต์œ  ํ”Œ๋žซํผ์—์„œ ์ธํ”„๋ผยทCI/CD ์‹ค๋ฌด์ž์—๊ฒŒ ํŒŒ์ดํ”„๋ผ์ธ ๋ฆฌ๋ทฐ๋ฅผ ๋ฐ›์•„๋ณด๋Š” ๊ฑด
๊ฝค ํ˜„์‹ค์ ์ธ ์„ ํƒ์ด์•ผ. ๋‚จ์˜ ์›Œํฌํ”Œ๋กœ ํ•˜๋‚˜ ์ฝ๋Š” ๊ฒƒ๋งŒ์œผ๋กœ๋„ ๋ฐฐ์šฐ๋Š” ๊ฒŒ ๋งŽ์œผ๋‹ˆ๊นŒ.

๊ทธ๋ฆฌ๊ณ  ์ง€๊ธˆ ์ฝ๊ณ  ์žˆ๋Š” ์žฌ๋Šฅ๋„ท '์ง€์‹์ธ์˜ ์ˆฒ'์ฒ˜๋Ÿผ
์ •๋ฆฌ๋œ ์ง€์‹์„ ๋จผ์ € ํ›‘๊ณ  ์†์„ ๋Œ€๋ฉด, ์‚ฝ์งˆ์˜ ์ ˆ๋Œ€๋Ÿ‰์ด ํ™•์‹คํžˆ ์ค„์–ด๋“ ๋‹ค.

๊ธˆ์š”์ผ ์ €๋…์— ๋งˆ์Œ ํŽธํžˆ ํ‡ด๊ทผํ•˜๋Š” ๊ฐœ๋ฐœ์ž์™€
์ƒˆ๋ฒฝ์— ์„œ๋ฒ„ ๋“ค์–ด๊ฐ€๋Š” ๊ฐœ๋ฐœ์ž์˜ ์ฐจ์ด๋Š”
์‹ค๋ ฅ์ด ์•„๋‹ˆ๋ผ ํŒŒ์ดํ”„๋ผ์ธ์ด์—ˆ๋‹ค.

์ž, ์ด์ œ ํ„ฐ๋ฏธ๋„ ์—ด๊ณ  mkdir -p .github/workflows ๋ถ€ํ„ฐ ๊ฐ€๋ณด์ž. ๐Ÿš€
์ฒซ ์ดˆ๋ก ์ฒดํฌ์˜ ๊ทธ ์พŒ๊ฐ, ์ง์ ‘ ๋А๊ปด๋ด์•ผ ํ•ด.

๐Ÿ€ Happy Automating! ๐Ÿ€

๋Œ“๊ธ€ ์ž‘์„ฑ

์ด ๊ธ€์— ๋Œ€ํ•œ ์—ฌ๋Ÿฌ๋ถ„์˜ ์ƒ๊ฐ์„ ๋“ค๋ ค์ฃผ์„ธ์š”

๋Œ“๊ธ€ 0