Skip to content

필사 모드: GitHub Actions CI/CD 완전 정복: Go 빌드부터 브랜치 전략, 자동 배포까지

한국어
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.
원문 렌더가 준비되기 전까지 텍스트 가이드로 표시합니다.

들어가며

현대 소프트웨어 개발에서 CI/CD는 선택이 아니라 생존 조건입니다. 코드를 작성하고, 테스트를 돌리고, 빌드하고, 배포하는 모든 과정을 수동으로 처리하던 시대는 끝났습니다. GitHub Actions는 이 모든 것을 YAML 파일 하나로 자동화할 수 있게 해주며, 특히 Go 프로젝트와의 궁합이 탁월합니다.

Go는 정적 바이너리 컴파일, 빠른 빌드 속도, 내장 테스트 프레임워크, 크로스 컴파일 지원 등 CI/CD 파이프라인에 최적화된 특성을 가지고 있습니다. 여기에 적절한 브랜치 전략까지 결합하면, 하루에 수십 번 안전하게 배포하는 팀을 만들 수 있습니다.

이 글에서는 GitHub Actions의 아키텍처부터 Go 프로젝트 CI/CD 구축, 브랜치 전략 비교, 실전 마이크로서비스 파이프라인, 그리고 성능 최적화와 비용 절감까지 — DevOps 엔지니어가 알아야 할 모든 것을 다룹니다.

Part 1: GitHub Actions 완전 이해

1-1. GitHub Actions 아키텍처

GitHub Actions는 크게 4가지 계층으로 구성됩니다.

**Workflow, Job, Step, Action 계층 구조**

Workflow (.github/workflows/*.yml)

└── Job (하나의 Runner에서 실행)

└── Step (순차 실행)

└── Action (재사용 가능한 단위 작업)

- **Workflow**: `.github/workflows/` 디렉토리의 YAML 파일 하나가 하나의 워크플로우입니다. 이벤트에 의해 트리거됩니다.

- **Job**: 같은 Runner에서 실행되는 Step의 묶음입니다. Job 간에는 기본적으로 병렬 실행되며, `needs` 키워드로 의존성을 설정할 수 있습니다.

- **Step**: Job 내에서 순차적으로 실행되는 개별 작업입니다. 쉘 명령어(`run`) 또는 Action(`uses`)을 실행합니다.

- **Action**: 재사용 가능한 단위 작업입니다. Marketplace에 수천 개의 커뮤니티 Action이 공개되어 있습니다.

**Runner: GitHub-hosted vs Self-hosted**

| 항목 | GitHub-hosted | Self-hosted |

| ------ | ----------------------- | -------------------- |

| 설정 | 제로 설정 | 직접 프로비저닝 필요 |

| OS | Ubuntu, Windows, macOS | 자유 선택 |

| 비용 | 무료 한도 + 분당 과금 | 인프라 비용만 |

| 성능 | 2-core, 7GB RAM (Linux) | 커스텀 가능 |

| 보안 | GitHub 관리 | 직접 관리 |

| Docker | 지원 (Linux만) | 자유 설정 |

| 캐시 | GitHub Cache (10GB) | 로컬 디스크 |

Self-hosted Runner는 AWS Spot Instance에 올리면 비용을 80% 이상 절약할 수 있습니다. 단, 보안 관리와 Runner 가용성을 직접 책임져야 합니다.

**이벤트 트리거**

GitHub Actions는 다양한 이벤트에 반응할 수 있습니다.

on:

코드 푸시 시

push:

branches: [main, develop]

paths:

- 'src/**'

- 'go.mod'

tags:

- 'v*'

PR 이벤트

pull_request:

branches: [main]

types: [opened, synchronize, reopened]

크론 스케줄 (UTC 기준)

schedule:

- cron: '0 2 * * 1' # 매주 월요일 02:00 UTC

수동 트리거 (UI 버튼)

workflow_dispatch:

inputs:

environment:

description: 'Deploy target'

required: true

type: choice

options:

- staging

- production

API 호출로 트리거

repository_dispatch:

types: [deploy-trigger]

**Concurrency Control**

동일한 브랜치에서 여러 워크플로우가 동시에 실행되는 것을 방지합니다.

concurrency:

group: deploy-production

cancel-in-progress: false # 진행 중인 배포는 취소하지 않음

또는 PR 단위로

concurrency:

group: ci-pr-$GITHUB_REF

cancel-in-progress: true # 새 커밋 시 이전 CI 취소

여기서 `cancel-in-progress: true`를 설정하면 새로운 실행이 시작될 때 이전 실행이 자동으로 취소됩니다. PR에서는 보통 이렇게 설정하고, 프로덕션 배포에서는 `false`로 설정합니다.

**YAML 문법 핵심**

name: CI Pipeline

env:

GO_VERSION: '1.23'

REGISTRY: ghcr.io

jobs:

test:

runs-on: ubuntu-latest

조건부 실행

if: github.event_name == 'pull_request'

strategy:

matrix:

go-version: ['1.22', '1.23']

os: [ubuntu-latest, macos-latest]

steps:

- uses: actions/checkout@v4

- name: Setup Go

uses: actions/setup-go@v5

with:

go-version: ${{ matrix.go-version }}

- run: go test ./...

build:

needs: [test] # test Job 완료 후 실행

runs-on: ubuntu-latest

outputs:

image-tag: ${{ steps.meta.outputs.tags }}

steps:

- id: meta

run: echo "tags=v1.0.0" >> "$GITHUB_OUTPUT"

핵심 키워드 정리:

- `env`: 환경 변수 (워크플로우/잡/스텝 레벨)

- `secrets`: 암호화된 비밀 값 참조

- `needs`: Job 간 의존성 정의

- `if`: 조건부 실행

- `matrix`: 여러 조합으로 병렬 실행

- `outputs`: Job 간 데이터 전달

1-2. 핵심 액션 TOP 20

실무에서 가장 많이 사용하는 GitHub Actions를 카테고리별로 정리합니다.

**기본 액션**

1. 소스 코드 체크아웃

- uses: actions/checkout@v4

with:

fetch-depth: 0 # 전체 히스토리 (태그 기반 버전에 필요)

2. Go 설정

- uses: actions/setup-go@v5

with:

go-version: '1.23'

cache: true # Go 모듈 캐시 자동 활성화

3. 캐시 관리

- uses: actions/cache@v4

with:

path: |

~/.cache/go-build

~/go/pkg/mod

key: go-mod-${{ hashFiles('**/go.sum') }}

restore-keys: |

go-mod-

**Docker 관련 액션**

4. Docker 레지스트리 로그인

- uses: docker/login-action@v3

with:

registry: ghcr.io

username: ${{ github.actor }}

password: ${{ secrets.GITHUB_TOKEN }}

5. Docker 빌드 & 푸시

- uses: docker/build-push-action@v6

with:

context: .

push: true

tags: ghcr.io/myorg/myapp:latest

cache-from: type=gha

cache-to: type=gha,mode=max

6. Docker 메타데이터 (태그 자동 생성)

- uses: docker/metadata-action@v5

with:

images: ghcr.io/myorg/myapp

tags: |

type=sha,prefix=

type=ref,event=branch

type=semver,pattern=v{{version}}

**아티팩트 관리**

7. 아티팩트 업로드

- uses: actions/upload-artifact@v4

with:

name: go-binary-linux

path: ./bin/myapp

retention-days: 5

8. 아티팩트 다운로드

- uses: actions/download-artifact@v4

with:

name: go-binary-linux

path: ./bin/

**코드 품질 & 보안**

9. golangci-lint

- uses: golangci/golangci-lint-action@v6

with:

version: v1.62

args: --timeout=5m

10. CodeQL 분석

- uses: github/codeql-action/analyze@v3

with:

languages: go

11. Dependency Review (PR에서 새 의존성 검토)

- uses: actions/dependency-review-action@v4

**릴리스 & 알림**

12. GitHub Release 생성

- uses: softprops/action-gh-release@v2

with:

files: dist/*

generate_release_notes: true

13. PR 라벨 자동 부여

- uses: actions/labeler@v5

with:

repo-token: ${{ secrets.GITHUB_TOKEN }}

14. Slack 알림

- uses: slackapi/slack-github-action@v2

with:

webhook: ${{ secrets.SLACK_WEBHOOK }}

webhook-type: incoming-webhook

payload: |

{

"text": "Deployment complete: ${{ github.sha }}"

}

**커스텀 액션 만들기**

세 가지 유형의 커스텀 액션을 만들 수 있습니다.

1. **Composite Action** (가장 간단, YAML만으로 작성)

.github/actions/go-setup/action.yml

name: 'Go Setup with Cache'

description: 'Setup Go with module and build cache'

inputs:

go-version:

description: 'Go version'

required: false

default: '1.23'

runs:

using: 'composite'

steps:

- uses: actions/setup-go@v5

with:

go-version: ${{ inputs.go-version }}

- uses: actions/cache@v4

with:

path: |

~/.cache/go-build

~/go/pkg/mod

key: go-${{ runner.os }}-${{ hashFiles('**/go.sum') }}

shell: bash

- run: go mod download

shell: bash

2. **JavaScript Action** (Node.js 런타임, 복잡한 로직)

3. **Docker Action** (컨테이너 기반, 어떤 언어든 가능)

1-3. 시크릿과 환경 관리

**시크릿 계층 구조**

GitHub는 세 가지 레벨의 시크릿을 제공합니다.

| 레벨 | 범위 | 사용 사례 |

| ------------ | ----------------------- | -------------------- |

| Repository | 단일 리포지토리 | API 키, DB 비밀번호 |

| Environment | 특정 환경(staging/prod) | 환경별 설정 |

| Organization | 조직 전체 리포지토리 | 공통 레지스트리 인증 |

Environment 시크릿은 Repository 시크릿보다 우선합니다. 같은 이름이면 Environment 값이 사용됩니다.

**OIDC로 클라우드 인증 (비밀키 없이)**

전통적인 방법은 AWS Access Key를 시크릿에 저장하는 것이었지만, OIDC를 사용하면 장기 자격 증명 없이 인증할 수 있습니다.

jobs:

deploy:

permissions:

id-token: write

contents: read

steps:

- uses: aws-actions/configure-aws-credentials@v4

with:

role-to-assume: arn:aws:iam::123456789012:role/github-actions

aws-region: ap-northeast-2

GCP도 마찬가지

- uses: google-github-actions/auth@v2

with:

workload_identity_provider: projects/123/locations/global/workloadIdentityPools/github/providers/github

service_account: github-actions@my-project.iam.gserviceaccount.com

OIDC 인증의 장점:

- 시크릿에 장기 자격 증명을 저장하지 않으므로 유출 위험이 없음

- IAM 역할의 세분화된 권한 제어 가능

- 자격 증명 자동 갱신, 로테이션 불필요

**Environment Protection Rules**

프로덕션 배포에 안전 장치를 설정합니다.

jobs:

deploy-prod:

environment:

name: production

url: https://myapp.example.com

runs-on: ubuntu-latest

steps:

- name: Deploy

run: echo "Deploying to production..."

GitHub UI에서 `production` Environment에 다음을 설정할 수 있습니다:

- **Required Reviewers**: 배포 전 특정 사용자의 승인 필요

- **Wait Timer**: 승인 후 대기 시간 (예: 5분)

- **Deployment Branches**: main 브랜치에서만 배포 허용

- **Custom Rules**: 외부 시스템과 연동한 승인 게이트

Part 2: Go 프로젝트 CI/CD

2-1. Go 빌드 시스템 이해

Go의 빌드 시스템은 CI/CD에 매우 친화적입니다. 단일 정적 바이너리 컴파일, 빠른 빌드 속도, 내장 테스트 프레임워크를 기본 제공합니다.

**go build 핵심 플래그**

기본 빌드

go build -o bin/myapp ./cmd/myapp

프로덕션 빌드 (최적화 플래그)

CGO_ENABLED=0 go build \

-trimpath \

-ldflags="-s -w -X main.version=v1.2.3 -X main.commit=abc123 -X main.buildDate=2026-03-23" \

-o bin/myapp \

./cmd/myapp

각 플래그의 의미:

- `CGO_ENABLED=0`: C 라이브러리 의존성 제거, 순수 Go 정적 바이너리 생성

- `-trimpath`: 빌드 경로 정보 제거 (재현 가능한 빌드)

- `-ldflags="-s -w"`: 디버그 심볼 제거 → 바이너리 크기 20-30% 감소

- `-X main.version=...`: 빌드 시 변수 값 주입 (버전 정보)

**크로스 컴파일**

Go는 다른 언어와 달리 별도의 크로스 컴파일러 설치 없이 환경 변수만으로 크로스 컴파일이 가능합니다.

Linux AMD64

GOOS=linux GOARCH=amd64 go build -o bin/myapp-linux-amd64 ./cmd/myapp

macOS ARM64 (Apple Silicon)

GOOS=darwin GOARCH=arm64 go build -o bin/myapp-darwin-arm64 ./cmd/myapp

Windows AMD64

GOOS=windows GOARCH=amd64 go build -o bin/myapp-windows-amd64.exe ./cmd/myapp

Linux ARM64 (AWS Graviton, Raspberry Pi)

GOOS=linux GOARCH=arm64 go build -o bin/myapp-linux-arm64 ./cmd/myapp

**테스트 명령어**

기본 테스트

go test ./...

레이스 컨디션 탐지 + 커버리지

go test -race -cover -coverprofile=coverage.out ./...

벤치마크

go test -bench=. -benchmem -count=5 ./...

특정 테스트만 실행

go test -run TestUserService -v ./internal/service/

병렬 실행 제어

go test -parallel=4 ./...

타임아웃 설정

go test -timeout=5m ./...

커버리지 리포트 생성

go tool cover -html=coverage.out -o coverage.html

**린트 체계**

go vet: 컴파일러가 잡지 못하는 의심스러운 코드 탐지

go vet ./...

staticcheck: 고급 정적 분석

staticcheck ./...

golangci-lint: 50+ 린터 통합 도구

golangci-lint run --timeout=5m

보안 취약점 스캔

govulncheck ./...

golangci-lint 설정 파일 예시:

.golangci.yml

run:

timeout: 5m

go: '1.23'

linters:

enable:

- errcheck

- gosimple

- govet

- ineffassign

- staticcheck

- unused

- gosec

- revive

- misspell

- prealloc

- exportloopref

- gocritic

linters-settings:

errcheck:

check-type-assertions: true

gocritic:

enabled-tags:

- diagnostic

- performance

- style

revive:

rules:

- name: unexported-return

disabled: true

issues:

exclude-rules:

- path: _test\.go

linters:

- errcheck

- gosec

**의존성 관리**

의존성 정리

go mod tidy

벤더링 (CI에서 네트워크 의존성 제거)

go mod vendor

의존성 보안 스캔

govulncheck ./...

의존성 그래프 확인

go mod graph

특정 모듈 업데이트

go get -u github.com/gin-gonic/gin@latest

2-2. Go CI 워크플로우

실제 Go 프로젝트에서 사용할 수 있는 완전한 CI 워크플로우입니다.

name: Go CI

on:

push:

branches: [main, develop]

pull_request:

branches: [main]

concurrency:

group: ci-${{ github.ref }}

cancel-in-progress: true

env:

GO_VERSION: '1.23'

GOLANGCI_LINT_VERSION: 'v1.62'

permissions:

contents: read

pull-requests: write

jobs:

lint:

name: Lint

runs-on: ubuntu-latest

steps:

- uses: actions/checkout@v4

- uses: actions/setup-go@v5

with:

go-version: ${{ env.GO_VERSION }}

- uses: golangci/golangci-lint-action@v6

with:

version: ${{ env.GOLANGCI_LINT_VERSION }}

args: --timeout=5m

- name: Check go mod tidy

run: |

go mod tidy

git diff --exit-code go.mod go.sum

test:

name: Test (Go ${{ matrix.go-version }} / ${{ matrix.os }})

runs-on: ${{ matrix.os }}

strategy:

fail-fast: false

matrix:

go-version: ['1.22', '1.23']

os: [ubuntu-latest, macos-latest, windows-latest]

exclude:

- os: windows-latest

go-version: '1.22'

steps:

- uses: actions/checkout@v4

- uses: actions/setup-go@v5

with:

go-version: ${{ matrix.go-version }}

cache: true

- name: Run tests with coverage

run: go test -race -coverprofile=coverage.out -covermode=atomic ./...

- name: Upload coverage

if: matrix.os == 'ubuntu-latest' && matrix.go-version == '1.23'

uses: actions/upload-artifact@v4

with:

name: coverage

path: coverage.out

coverage-report:

name: Coverage Report

needs: [test]

runs-on: ubuntu-latest

if: github.event_name == 'pull_request'

steps:

- uses: actions/checkout@v4

- uses: actions/download-artifact@v4

with:

name: coverage

- name: Generate coverage comment

uses: actions/github-script@v7

with:

script: |

const fs = require('fs');

const coverage = fs.readFileSync('coverage.out', 'utf8');

const lines = coverage.split('\n');

const total = lines.length - 2;

await github.rest.issues.createComment({

owner: context.repo.owner,

repo: context.repo.repo,

issue_number: context.issue.number,

body: `## Coverage Report\nTotal statements covered: ${total} lines\nSee artifacts for full report.`

});

security:

name: Security Scan

runs-on: ubuntu-latest

steps:

- uses: actions/checkout@v4

- uses: actions/setup-go@v5

with:

go-version: ${{ env.GO_VERSION }}

- name: Run govulncheck

run: |

go install golang.org/x/vuln/cmd/govulncheck@latest

govulncheck ./...

- name: Run gosec

uses: securego/gosec@master

with:

args: ./...

build:

name: Build

needs: [lint, test]

runs-on: ubuntu-latest

strategy:

matrix:

goos: [linux, darwin, windows]

goarch: [amd64, arm64]

exclude:

- goos: windows

goarch: arm64

steps:

- uses: actions/checkout@v4

with:

fetch-depth: 0

- uses: actions/setup-go@v5

with:

go-version: ${{ env.GO_VERSION }}

cache: true

- name: Build binary

env:

GOOS: ${{ matrix.goos }}

GOARCH: ${{ matrix.goarch }}

CGO_ENABLED: '0'

run: |

VERSION=$(git describe --tags --always --dirty 2>/dev/null || echo "dev")

COMMIT=$(git rev-parse --short HEAD)

BUILD_DATE=$(date -u +"%Y-%m-%dT%H:%M:%SZ")

EXTENSION=""

if [ "$GOOS" = "windows" ]; then

EXTENSION=".exe"

fi

go build \

-trimpath \

-ldflags="-s -w -X main.version=${VERSION} -X main.commit=${COMMIT} -X main.buildDate=${BUILD_DATE}" \

-o "bin/myapp-${GOOS}-${GOARCH}${EXTENSION}" \

./cmd/myapp

- uses: actions/upload-artifact@v4

with:

name: binary-${{ matrix.goos }}-${{ matrix.goarch }}

path: bin/

retention-days: 7

**매트릭스 빌드 상세**

위 워크플로우에서 `matrix` 전략을 사용하면 다음과 같은 조합이 생성됩니다:

| GOOS | GOARCH | 비고 |

| ------- | ------ | ------------------------------ |

| linux | amd64 | 가장 일반적인 서버 환경 |

| linux | arm64 | AWS Graviton, 라즈베리 파이 |

| darwin | amd64 | macOS Intel |

| darwin | arm64 | macOS Apple Silicon (M1/M2/M3) |

| windows | amd64 | Windows 데스크톱 |

`exclude`로 windows/arm64 조합은 제외했습니다. `fail-fast: false`로 설정하면 하나의 조합이 실패해도 나머지는 계속 실행됩니다.

**캐싱 전략**

actions/setup-go의 내장 캐시 (go.sum 해시 기반)

- uses: actions/setup-go@v5

with:

go-version: '1.23'

cache: true # 이것만으로 go module 캐시 활성화

더 세밀한 캐시 제어가 필요할 때

- uses: actions/cache@v4

with:

path: |

~/.cache/go-build

~/go/pkg/mod

key: go-${{ runner.os }}-${{ hashFiles('**/go.sum') }}

restore-keys: |

go-${{ runner.os }}-

캐시 히트 시 `go mod download`가 수초 내에 완료됩니다. 캐시가 없으면 대규모 프로젝트에서 1-2분이 걸릴 수 있습니다.

2-3. Go 멀티 스테이지 Docker 빌드

Go의 정적 바이너리 컴파일 특성을 활용하면, 최종 Docker 이미지를 극도로 작게 만들 수 있습니다.

**빌더 패턴**

============================================

Stage 1: Build

============================================

FROM golang:1.23-alpine AS builder

빌드에 필요한 도구 설치

RUN apk add --no-cache git ca-certificates tzdata

WORKDIR /app

의존성 먼저 복사 (레이어 캐싱 최적화)

COPY go.mod go.sum ./

RUN go mod download

소스 코드 복사

COPY . .

빌드 (정적 바이너리)

ARG VERSION=dev

ARG COMMIT=unknown

RUN CGO_ENABLED=0 GOOS=linux go build \

-trimpath \

-ldflags="-s -w -X main.version=${VERSION} -X main.commit=${COMMIT}" \

-o /app/bin/server \

./cmd/server

============================================

Stage 2: Runtime (Scratch - 최소 이미지)

============================================

FROM scratch

TLS 인증서 (HTTPS 호출에 필요)

COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/

타임존 데이터

COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo

바이너리

COPY --from=builder /app/bin/server /server

비-root 사용자 (보안)

USER 65534:65534

EXPOSE 8080

ENTRYPOINT ["/server"]

**이미지 크기 비교**

| 베이스 이미지 | 크기 | 장점 | 단점 |

| ------------------ | --------------- | -------------------- | ---------------------- |

| golang:1.23 | ~850MB | 디버깅 용이 | 배포에 부적합 |

| golang:1.23-alpine | ~250MB | 빌드 스테이지에 적합 | 여전히 큼 |

| alpine:3.20 | ~7MB + 바이너리 | 쉘 포함, 디버깅 가능 | 공격 표면 존재 |

| distroless | ~2MB + 바이너리 | 최소 필수 파일만 | 쉘 없음 |

| scratch | 0MB + 바이너리 | 가장 작음 | 쉘, 패키지 매니저 없음 |

scratch 기반으로 빌드하면 최종 이미지가 10-15MB 수준으로 줄어듭니다. 원본 빌드 이미지 대비 98% 감소입니다.

**distroless 사용 (디버깅 가능한 최소 이미지)**

FROM gcr.io/distroless/static-debian12:nonroot

COPY --from=builder /app/bin/server /server

USER nonroot:nonroot

EXPOSE 8080

ENTRYPOINT ["/server"]

distroless는 scratch보다 약간 크지만, /etc/passwd, CA 인증서 등 필수 파일이 포함되어 있어 실무에서 더 편리합니다.

**GitHub Actions에서 Docker 빌드**

jobs:

docker:

name: Build and Push Docker Image

needs: [test, lint]

runs-on: ubuntu-latest

permissions:

contents: read

packages: write

steps:

- uses: actions/checkout@v4

with:

fetch-depth: 0

- name: Set up Docker Buildx

uses: docker/setup-buildx-action@v3

- name: Set up QEMU (멀티 플랫폼 빌드)

uses: docker/setup-qemu-action@v3

- name: Login to GHCR

uses: docker/login-action@v3

with:

registry: ghcr.io

username: ${{ github.actor }}

password: ${{ secrets.GITHUB_TOKEN }}

- name: Docker metadata

id: meta

uses: docker/metadata-action@v5

with:

images: ghcr.io/${{ github.repository }}

tags: |

type=sha,prefix=

type=ref,event=branch

type=semver,pattern={{version}}

type=raw,value=latest,enable={{is_default_branch}}

- name: Build and push

uses: docker/build-push-action@v6

with:

context: .

platforms: linux/amd64,linux/arm64

push: ${{ github.event_name != 'pull_request' }}

tags: ${{ steps.meta.outputs.tags }}

labels: ${{ steps.meta.outputs.labels }}

cache-from: type=gha

cache-to: type=gha,mode=max

build-args: |

VERSION=${{ github.ref_name }}

COMMIT=${{ github.sha }}

**Docker Layer Caching 전략**

GitHub Actions에서 Docker 빌드 캐시를 활용하는 방법은 크게 두 가지입니다.

방법 1: GitHub Actions Cache Backend (추천)

cache-from: type=gha

cache-to: type=gha,mode=max

방법 2: Registry Cache

cache-from: type=registry,ref=ghcr.io/myorg/myapp:cache

cache-to: type=registry,ref=ghcr.io/myorg/myapp:cache,mode=max

GitHub Actions Cache Backend는 별도의 레지스트리 설정 없이 GitHub의 캐시 인프라를 직접 활용합니다. 캐시 용량은 리포지토리당 10GB입니다.

**멀티 플랫폼 빌드**

QEMU를 사용하여 단일 Runner에서 amd64와 arm64 이미지를 동시에 빌드할 수 있습니다. arm64 빌드는 에뮬레이션으로 인해 amd64보다 2-3배 느리지만, 별도의 ARM Runner 없이도 가능합니다.

**보안 베스트 프랙티스**

비-root 사용자 실행

USER 65534:65534

읽기 전용 파일시스템 (Kubernetes에서)

securityContext:

readOnlyRootFilesystem: true

헬스 체크

HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \

CMD ["/server", "healthcheck"]

2-4. Go 릴리스 자동화

**GoReleaser**

GoReleaser는 Go 프로젝트의 릴리스를 자동화하는 도구입니다. 크로스 컴파일, 체크섬 생성, changelog, 패키지 매니저 배포를 한 번에 처리합니다.

.goreleaser.yaml

version: 2

before:

hooks:

- go mod tidy

- go generate ./...

builds:

- id: myapp

main: ./cmd/myapp

binary: myapp

env:

- CGO_ENABLED=0

goos:

- linux

- darwin

- windows

goarch:

- amd64

- arm64

ldflags:

- -s -w

- -X main.version={{.Version}}

- -X main.commit={{.Commit}}

- -X main.date={{.Date}}

archives:

- id: default

format: tar.gz

name_template: '{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}'

format_overrides:

- goos: windows

format: zip

checksum:

name_template: 'checksums.txt'

changelog:

sort: asc

filters:

exclude:

- '^docs:'

- '^test:'

- '^ci:'

- Merge pull request

brews:

- repository:

owner: myorg

name: homebrew-tap

homepage: https://github.com/myorg/myapp

description: My application

license: MIT

dockers:

- image_templates:

- 'ghcr.io/myorg/myapp:{{ .Version }}-amd64'

dockerfile: Dockerfile

build_flag_templates:

- '--platform=linux/amd64'

use: buildx

nfpms:

- package_name: myapp

vendor: My Organization

homepage: https://github.com/myorg/myapp

maintainer: team@myorg.com

description: My application

license: MIT

formats:

- deb

- rpm

**GitHub Actions에서 GoReleaser 실행**

name: Release

on:

push:

tags:

- 'v*'

permissions:

contents: write

packages: write

jobs:

release:

runs-on: ubuntu-latest

steps:

- uses: actions/checkout@v4

with:

fetch-depth: 0

- uses: actions/setup-go@v5

with:

go-version: '1.23'

- name: Login to GHCR

uses: docker/login-action@v3

with:

registry: ghcr.io

username: ${{ github.actor }}

password: ${{ secrets.GITHUB_TOKEN }}

- name: Run GoReleaser

uses: goreleaser/goreleaser-action@v6

with:

version: '~> v2'

args: release --clean

env:

GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

태그를 푸시하면 (`git tag v1.2.3 && git push --tags`), 자동으로:

1. 모든 플랫폼용 바이너리가 크로스 컴파일됩니다

2. 체크섬 파일이 생성됩니다

3. Changelog가 자동 생성됩니다

4. GitHub Release가 생성되고 바이너리가 업로드됩니다

5. Docker 이미지가 빌드되어 GHCR에 푸시됩니다

6. Homebrew tap이 업데이트됩니다

**Semantic Release (Conventional Commits 기반)**

Conventional Commits를 사용하면 커밋 메시지에서 자동으로 버전을 결정할 수 있습니다.

feat: add user authentication → v1.1.0 (MINOR)

fix: resolve null pointer error → v1.0.1 (PATCH)

feat!: redesign API endpoints → v2.0.0 (MAJOR)

.github/workflows/semantic-release.yml

name: Semantic Release

on:

push:

branches: [main]

jobs:

release:

runs-on: ubuntu-latest

steps:

- uses: actions/checkout@v4

with:

fetch-depth: 0

- uses: actions/setup-node@v4

with:

node-version: 20

- name: Semantic Release

uses: cycjimmy/semantic-release-action@v4

with:

extra_plugins: |

@semantic-release/changelog

@semantic-release/git

env:

GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

Part 3: 브랜치 전략 완전 비교

브랜치 전략은 팀의 개발 문화, 배포 빈도, 프로젝트 특성에 따라 선택해야 합니다. 여기서는 가장 널리 사용되는 세 가지 전략을 상세히 비교합니다.

3-1. Git Flow

Git Flow는 Vincent Driessen이 2010년에 제안한 브랜치 모델로, 5개의 브랜치 유형을 사용합니다.

main ─────────────────────────────────────────→

↑ ↑ ↑

│ merge │ merge │ hotfix

│ │ │

release/1.0 ─→ release/2.0 ─→ hotfix/urgent ─→

↑ ↑

│ merge │ merge

│ │

develop ──────────────────────────────────────→

↑ ↑ ↑

│ │ │

feature/A feature/B feature/C

**5개 브랜치 유형:**

- **main**: 프로덕션 릴리스만 반영. 항상 배포 가능한 상태.

- **develop**: 다음 릴리스를 위한 통합 브랜치. feature 브랜치들이 여기에 머지됨.

- **feature/xxx**: 새 기능 개발. develop에서 분기하여 develop으로 머지.

- **release/x.y**: 릴리스 준비. develop에서 분기하여 main과 develop에 머지.

- **hotfix/xxx**: 긴급 수정. main에서 분기하여 main과 develop에 머지.

**장점:**

- 릴리스 주기가 명확하게 관리됨

- 버전별 관리가 용이 (v1.0, v2.0 등)

- 프로덕션과 개발 코드가 명확히 분리

- 동시에 여러 릴리스 준비 가능

**단점:**

- 브랜치가 많아 관리가 복잡

- 머지 충돌이 빈번하게 발생

- 배포 속도가 느림 (릴리스 브랜치를 거쳐야 함)

- CI/CD 자동화와 상충하는 부분이 있음

**적합한 경우:**

- 모바일 앱 (App Store/Play Store 릴리스 주기)

- 엔터프라이즈 소프트웨어 (분기별 릴리스)

- 여러 버전을 동시에 지원해야 하는 프로젝트

- 규제가 엄격한 산업 (금융, 의료)

Git Flow용 CI 워크플로우

name: Git Flow CI

on:

push:

branches:

- main

- develop

- 'feature/**'

- 'release/**'

- 'hotfix/**'

pull_request:

branches: [main, develop]

jobs:

ci:

runs-on: ubuntu-latest

steps:

- uses: actions/checkout@v4

- uses: actions/setup-go@v5

with:

go-version: '1.23'

- run: go test ./...

release 또는 hotfix 브랜치에서만 빌드

- name: Build

if: startsWith(github.ref, 'refs/heads/release/') || startsWith(github.ref, 'refs/heads/hotfix/')

run: go build -o bin/myapp ./cmd/myapp

main 브랜치에서만 배포

- name: Deploy

if: github.ref == 'refs/heads/main'

run: echo "Deploying to production..."

3-2. GitHub Flow

GitHub Flow는 Git Flow의 복잡성을 줄인 단순한 브랜치 모델입니다. main 브랜치와 feature 브랜치만 사용합니다.

main ─── PR1 merge ─── PR2 merge ─── PR3 merge ──→

↑ ↑ ↑ ↑

│ │ │ │

└─ feat/A ┘ feat/B ┘ fix/C ┘

**워크플로우:**

1. main에서 feature 브랜치 생성

2. 코드 작성 및 커밋

3. PR 생성 → 코드 리뷰 + CI

4. main에 머지 → 자동 배포

**장점:**

- 매우 단순하고 이해하기 쉬움

- PR 기반으로 코드 리뷰가 자연스럽게 통합

- 머지 즉시 배포되므로 빠른 피드백 루프

- CI/CD 자동화에 가장 적합

**단점:**

- 릴리스 버전 관리가 어려움

- main이 항상 배포 가능해야 하므로 높은 CI 신뢰도 필요

- 대규모 기능 개발 시 PR이 커질 수 있음

**적합한 경우:**

- SaaS 웹 서비스 (하루 여러 번 배포)

- 소규모~중규모 팀 (2-15명)

- 빠른 이터레이션이 필요한 프로젝트

- 마이크로서비스 아키텍처

GitHub Flow용 CI/CD

name: GitHub Flow CI/CD

on:

push:

branches: [main]

pull_request:

branches: [main]

jobs:

ci:

runs-on: ubuntu-latest

steps:

- uses: actions/checkout@v4

- uses: actions/setup-go@v5

with:

go-version: '1.23'

- run: golangci-lint run

- run: go test -race ./...

deploy:

needs: [ci]

runs-on: ubuntu-latest

if: github.ref == 'refs/heads/main' && github.event_name == 'push'

environment:

name: production

url: https://myapp.example.com

steps:

- uses: actions/checkout@v4

- name: Build and Deploy

run: |

go build -o bin/myapp ./cmd/myapp

echo "Deploying to production..."

3-3. Trunk-Based Development

Trunk-Based Development(TBD)는 모든 개발자가 main(trunk)에 직접 커밋하거나, 매우 짧은 수명(1-2일)의 feature 브랜치를 사용하는 전략입니다.

main ── A ── B ── C ── D ── E ── F ── G ── H ──→

↑ ↑ ↑

│ │ │

short/feat short/fix short/feat

(1-2일) (수시간) (1일)

**핵심 원칙:**

- Feature 브랜치의 수명은 최대 1-2일

- 하루에 여러 번 main에 머지

- 미완성 기능은 Feature Flag으로 숨김

- 모든 커밋이 배포 가능한 상태여야 함

**Feature Flag 예시 (Go):**

package feature

type Flags struct {

NewDashboard bool

BetaAPI bool

DarkMode bool

}

func LoadFlags() Flags {

return Flags{

NewDashboard: os.Getenv("FF_NEW_DASHBOARD") == "true",

BetaAPI: os.Getenv("FF_BETA_API") == "true",

DarkMode: os.Getenv("FF_DARK_MODE") == "true",

}

}

// 사용 예

func HandleDashboard(w http.ResponseWriter, r *http.Request) {

flags := LoadFlags()

if flags.NewDashboard {

renderNewDashboard(w, r)

} else {

renderLegacyDashboard(w, r)

}

}

**장점:**

- 머지 충돌이 최소화 (브랜치 수명이 짧으므로)

- CI/CD 파이프라인이 단순

- 코드베이스의 무결성이 항상 유지

- 빠른 피드백과 배포 주기

**단점:**

- Feature Flag 관리 오버헤드

- 매우 높은 테스트 커버리지와 CI 신뢰도 필요

- 미숙한 팀에서는 main을 망가뜨릴 위험

- 대규모 리팩토링이 어려울 수 있음

**적합한 경우:**

- Google, Meta, Netflix 수준의 CI/CD 성숙도를 가진 팀

- 하루 수십 번 이상 배포하는 조직

- 마이크로서비스 아키텍처

- 높은 테스트 자동화 수준

**3가지 전략 비교표:**

| 항목 | Git Flow | GitHub Flow | Trunk-Based |

| ----------- | -------------------- | --------------- | ------------- |

| 브랜치 수 | 5종류 | 2종류 | 1-2종류 |

| 복잡도 | 높음 | 낮음 | 매우 낮음 |

| 배포 빈도 | 주/월 단위 | 일 단위 | 시간 단위 |

| 릴리스 관리 | 명시적 | 암묵적 | Feature Flag |

| 머지 충돌 | 빈번 | 보통 | 최소 |

| CI/CD 요구 | 보통 | 높음 | 매우 높음 |

| 팀 규모 | 대규모 | 소-중규모 | 모든 규모 |

| 적합 분야 | 모바일, 엔터프라이즈 | SaaS, 웹 | 대규모 서비스 |

| 대표 기업 | 전통 SW 기업 | GitHub, Shopify | Google, Meta |

3-4. 브랜치 보호 규칙 설정

어떤 브랜치 전략을 선택하든, 브랜치 보호 규칙은 필수입니다.

**Required Reviews**

Settings → Branches → Branch protection rules → Add rule

- Branch name pattern: main

- Require a pull request before merging

- Required approving reviews: 2

- Dismiss stale reviews when new commits are pushed

- Require review from Code Owners

**Required Status Checks**

- Require status checks to pass before merging

- ci / lint (Required)

- ci / test (Required)

- ci / security (Required)

**CODEOWNERS 파일**

.github/CODEOWNERS

전체 리포지토리 기본 리뷰어

* @myorg/backend-team

Go 코드

*.go @myorg/go-team

인프라 코드

.github/ @myorg/devops-team

Dockerfile @myorg/devops-team

k8s/ @myorg/devops-team

API 스펙

api/ @myorg/api-team @myorg/backend-team

보안 관련

**/security* @myorg/security-team

CODEOWNERS를 설정하면 해당 파일이 변경된 PR에서 자동으로 리뷰어가 지정됩니다.

**Rulesets (GitHub의 새로운 규칙 시스템)**

Branch Protection Rules의 진화된 버전으로, 더 세밀하고 유연한 규칙을 설정할 수 있습니다.

주요 차이점:

- 여러 브랜치/태그 패턴에 하나의 Ruleset 적용 가능

- Organization 레벨에서 관리 가능

- Bypass list로 특정 사용자/팀 예외 처리

- 더 많은 규칙 유형 (commit message 패턴, 파일 경로 제한 등)

Settings → Rules → Rulesets → New ruleset

- Name: Production Protection

- Enforcement: Active

- Bypass list: @myorg/release-managers

- Target branches: main, release/*

- Rules:

- Require a pull request

- Require status checks (ci/lint, ci/test)

- Require signed commits

- Block force pushes

- Require linear history

Part 4: 실전 프로젝트 — Go 마이크로서비스 풀 파이프라인

4-1. 전체 아키텍처

실전에서 사용하는 Go 마이크로서비스의 완전한 CI/CD 파이프라인을 구축합니다.

Developer Push

┌─────────────────┐

│ Lint Job │ ← golangci-lint, go vet

└────────┬────────┘

┌─────────────────┐

│ Test Job │ ← unit test, integration test, coverage

└────────┬────────┘

┌─────────────────┐

│ Build Job │ ← cross-compile, GoReleaser

└────────┬────────┘

┌─────────────────┐

│ Docker Job │ ← multi-stage build, GHCR push

└────────┬────────┘

┌─────────────────┐

│ Deploy Job │ ← K8s rollout, canary

└────────┬────────┘

┌─────────────────┐

│ Smoke Test Job │ ← health check, API validation

└────────┬────────┘

┌─────────────────┐

│ Notify Job │ ← Slack webhook

└─────────────────┘

4-2. 전체 워크플로우 YAML

name: Go Microservice Full Pipeline

on:

push:

branches: [main]

tags: ['v*']

pull_request:

branches: [main]

concurrency:

group: pipeline-${{ github.ref }}

cancel-in-progress: ${{ github.event_name == 'pull_request' }}

env:

GO_VERSION: '1.23'

REGISTRY: ghcr.io

IMAGE_NAME: ${{ github.repository }}

K8S_NAMESPACE: production

permissions:

contents: read

============================================

Job 1: Lint

============================================

jobs:

lint:

name: Lint

runs-on: ubuntu-latest

steps:

- uses: actions/checkout@v4

- uses: actions/setup-go@v5

with:

go-version: ${{ env.GO_VERSION }}

cache: true

- name: golangci-lint

uses: golangci/golangci-lint-action@v6

with:

version: v1.62

args: --timeout=5m --issues-exit-code=1

- name: go vet

run: go vet ./...

- name: Check formatting

run: |

gofmt_output=$(gofmt -l .)

if [ -n "$gofmt_output" ]; then

echo "Files not formatted:"

echo "$gofmt_output"

exit 1

fi

- name: Check go.mod tidy

run: |

go mod tidy

git diff --exit-code go.mod go.sum

============================================

Job 2: Test

============================================

test:

name: Test

runs-on: ubuntu-latest

services:

postgres:

image: postgres:16

env:

POSTGRES_USER: test

POSTGRES_PASSWORD: test

POSTGRES_DB: testdb

ports:

- 5432:5432

options: >-

--health-cmd pg_isready

--health-interval 10s

--health-timeout 5s

--health-retries 5

redis:

image: redis:7

ports:

- 6379:6379

options: >-

--health-cmd "redis-cli ping"

--health-interval 10s

--health-timeout 5s

--health-retries 5

steps:

- uses: actions/checkout@v4

- uses: actions/setup-go@v5

with:

go-version: ${{ env.GO_VERSION }}

cache: true

- name: Run unit tests

run: |

go test -race -count=1 \

-coverprofile=coverage-unit.out \

-covermode=atomic \

$(go list ./... | grep -v /integration/)

env:

ENVIRONMENT: test

- name: Run integration tests

run: |

go test -race -count=1 \

-coverprofile=coverage-integration.out \

-covermode=atomic \

-tags=integration \

./internal/integration/...

env:

DATABASE_URL: postgres://test:test@localhost:5432/testdb?sslmode=disable

REDIS_URL: redis://localhost:6379

- name: Merge coverage

run: |

go install github.com/wadey/gocovmerge@latest

gocovmerge coverage-unit.out coverage-integration.out > coverage.out

go tool cover -func=coverage.out | tail -1

- name: Upload coverage

uses: actions/upload-artifact@v4

with:

name: coverage

path: coverage.out

============================================

Job 3: Security Scan

============================================

security:

name: Security

runs-on: ubuntu-latest

steps:

- uses: actions/checkout@v4

- uses: actions/setup-go@v5

with:

go-version: ${{ env.GO_VERSION }}

- name: govulncheck

run: |

go install golang.org/x/vuln/cmd/govulncheck@latest

govulncheck ./...

- name: gosec

uses: securego/gosec@master

with:

args: '-no-fail -fmt json -out gosec-report.json ./...'

- name: Upload security report

uses: actions/upload-artifact@v4

with:

name: security-report

path: gosec-report.json

============================================

Job 4: Build

============================================

build:

name: Build

needs: [lint, test, security]

runs-on: ubuntu-latest

outputs:

version: ${{ steps.version.outputs.version }}

steps:

- uses: actions/checkout@v4

with:

fetch-depth: 0

- uses: actions/setup-go@v5

with:

go-version: ${{ env.GO_VERSION }}

cache: true

- name: Determine version

id: version

run: |

if [[ "$GITHUB_REF" == refs/tags/v* ]]; then

VERSION="${GITHUB_REF#refs/tags/}"

else

VERSION="dev-$(git rev-parse --short HEAD)"

fi

echo "version=${VERSION}" >> "$GITHUB_OUTPUT"

echo "Building version: ${VERSION}"

- name: Build binaries

run: |

VERSION="${{ steps.version.outputs.version }}"

COMMIT="$(git rev-parse --short HEAD)"

DATE="$(date -u +%Y-%m-%dT%H:%M:%SZ)"

LDFLAGS="-s -w -X main.version=${VERSION} -X main.commit=${COMMIT} -X main.buildDate=${DATE}"

for OS in linux darwin; do

for ARCH in amd64 arm64; do

echo "Building ${OS}/${ARCH}..."

CGO_ENABLED=0 GOOS=${OS} GOARCH=${ARCH} \

go build -trimpath -ldflags="${LDFLAGS}" \

-o "bin/myapp-${OS}-${ARCH}" ./cmd/myapp

done

done

- uses: actions/upload-artifact@v4

with:

name: binaries

path: bin/

============================================

Job 5: Docker Build & Push

============================================

docker:

name: Docker

needs: [build]

runs-on: ubuntu-latest

if: github.event_name == 'push'

permissions:

contents: read

packages: write

outputs:

image-digest: ${{ steps.build-push.outputs.digest }}

image-tag: ${{ steps.meta.outputs.version }}

steps:

- uses: actions/checkout@v4

- name: Set up Docker Buildx

uses: docker/setup-buildx-action@v3

- name: Set up QEMU

uses: docker/setup-qemu-action@v3

- name: Login to GHCR

uses: docker/login-action@v3

with:

registry: ${{ env.REGISTRY }}

username: ${{ github.actor }}

password: ${{ secrets.GITHUB_TOKEN }}

- name: Docker metadata

id: meta

uses: docker/metadata-action@v5

with:

images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}

tags: |

type=sha,prefix=

type=ref,event=branch

type=semver,pattern={{version}}

type=semver,pattern={{major}}.{{minor}}

type=raw,value=latest,enable={{is_default_branch}}

- name: Build and push

id: build-push

uses: docker/build-push-action@v6

with:

context: .

platforms: linux/amd64,linux/arm64

push: true

tags: ${{ steps.meta.outputs.tags }}

labels: ${{ steps.meta.outputs.labels }}

cache-from: type=gha

cache-to: type=gha,mode=max

build-args: |

VERSION=${{ needs.build.outputs.version }}

COMMIT=${{ github.sha }}

============================================

Job 6: Deploy to Kubernetes

============================================

deploy:

name: Deploy

needs: [docker]

runs-on: ubuntu-latest

if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v')

permissions:

id-token: write

contents: read

environment:

name: production

url: https://myapp.example.com

steps:

- uses: actions/checkout@v4

- name: Configure AWS credentials (OIDC)

uses: aws-actions/configure-aws-credentials@v4

with:

role-to-assume: arn:aws:iam::123456789012:role/github-actions-deploy

aws-region: ap-northeast-2

- name: Setup kubectl

uses: azure/setup-kubectl@v4

with:

version: 'v1.30.0'

- name: Update kubeconfig

run: aws eks update-kubeconfig --name my-cluster --region ap-northeast-2

- name: Deploy with canary

run: |

IMAGE="${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@${{ needs.docker.outputs.image-digest }}"

카나리 배포 (10% 트래픽)

kubectl set image deployment/myapp-canary \

myapp="${IMAGE}" \

-n ${{ env.K8S_NAMESPACE }}

kubectl rollout status deployment/myapp-canary \

-n ${{ env.K8S_NAMESPACE }} --timeout=120s

카나리 검증 (5분 대기 후 메트릭 확인)

echo "Waiting for canary metrics..."

sleep 60

전체 롤아웃

kubectl set image deployment/myapp \

myapp="${IMAGE}" \

-n ${{ env.K8S_NAMESPACE }}

kubectl rollout status deployment/myapp \

-n ${{ env.K8S_NAMESPACE }} --timeout=300s

============================================

Job 7: Smoke Test

============================================

smoke-test:

name: Smoke Test

needs: [deploy]

runs-on: ubuntu-latest

steps:

- name: Health check

run: |

for i in $(seq 1 10); do

STATUS=$(curl -s -o /dev/null -w "%{http_code}" https://myapp.example.com/health)

if [ "$STATUS" = "200" ]; then

echo "Health check passed (attempt $i)"

exit 0

fi

echo "Attempt $i failed (status: $STATUS), retrying..."

sleep 10

done

echo "Health check failed after 10 attempts"

exit 1

- name: API validation

run: |

기본 API 엔드포인트 검증

curl -sf https://myapp.example.com/api/v1/version | jq .

curl -sf https://myapp.example.com/api/v1/ready

============================================

Job 8: Notification

============================================

notify:

name: Notify

needs: [smoke-test]

runs-on: ubuntu-latest

if: always()

steps:

- name: Slack notification

uses: slackapi/slack-github-action@v2

with:

webhook: ${{ secrets.SLACK_WEBHOOK }}

webhook-type: incoming-webhook

payload: |

{

"blocks": [

{

"type": "section",

"text": {

"type": "mrkdwn",

"text": "${{ needs.smoke-test.result == 'success' && 'Deployment successful' || 'Deployment failed' }} for *${{ github.repository }}*\n*Branch:* ${{ github.ref_name }}\n*Commit:* ${{ github.sha }}\n*Author:* ${{ github.actor }}"

}

}

]

}

============================================

Job 9: Rollback (실패 시)

============================================

rollback:

name: Rollback

needs: [smoke-test]

runs-on: ubuntu-latest

if: failure()

permissions:

id-token: write

contents: read

steps:

- name: Configure AWS credentials

uses: aws-actions/configure-aws-credentials@v4

with:

role-to-assume: arn:aws:iam::123456789012:role/github-actions-deploy

aws-region: ap-northeast-2

- name: Setup kubectl

uses: azure/setup-kubectl@v4

- name: Rollback deployment

run: |

aws eks update-kubeconfig --name my-cluster --region ap-northeast-2

kubectl rollout undo deployment/myapp -n ${{ env.K8S_NAMESPACE }}

kubectl rollout status deployment/myapp -n ${{ env.K8S_NAMESPACE }} --timeout=120s

echo "Rollback completed successfully"

4-3. Reusable Workflow 패턴

여러 마이크로서비스에서 동일한 CI/CD 파이프라인을 재사용할 수 있습니다.

**공통 CI 워크플로우 (호출되는 쪽)**

.github/workflows/go-ci-reusable.yml

name: Go CI (Reusable)

on:

workflow_call:

inputs:

go-version:

description: 'Go version to use'

required: false

type: string

default: '1.23'

working-directory:

description: 'Working directory'

required: false

type: string

default: '.'

run-integration-tests:

description: 'Run integration tests'

required: false

type: boolean

default: false

secrets:

SLACK_WEBHOOK:

required: false

outputs:

coverage:

description: 'Test coverage percentage'

value: ${{ jobs.test.outputs.coverage }}

jobs:

lint:

runs-on: ubuntu-latest

defaults:

run:

working-directory: ${{ inputs.working-directory }}

steps:

- uses: actions/checkout@v4

- uses: actions/setup-go@v5

with:

go-version: ${{ inputs.go-version }}

- uses: golangci/golangci-lint-action@v6

with:

version: v1.62

working-directory: ${{ inputs.working-directory }}

test:

runs-on: ubuntu-latest

outputs:

coverage: ${{ steps.coverage.outputs.total }}

defaults:

run:

working-directory: ${{ inputs.working-directory }}

steps:

- uses: actions/checkout@v4

- uses: actions/setup-go@v5

with:

go-version: ${{ inputs.go-version }}

cache: true

- name: Unit tests

run: go test -race -coverprofile=coverage.out ./...

- name: Integration tests

if: inputs.run-integration-tests

run: go test -race -tags=integration ./internal/integration/...

- name: Coverage

id: coverage

run: |

TOTAL=$(go tool cover -func=coverage.out | tail -1 | awk '{print $3}')

echo "total=${TOTAL}" >> "$GITHUB_OUTPUT"

**서비스별 워크플로우 (호출하는 쪽)**

service-a/.github/workflows/ci.yml

name: Service A CI

on:

push:

branches: [main]

paths:

- 'services/service-a/**'

pull_request:

paths:

- 'services/service-a/**'

jobs:

ci:

uses: myorg/shared-workflows/.github/workflows/go-ci-reusable.yml@main

with:

go-version: '1.23'

working-directory: services/service-a

run-integration-tests: true

secrets:

SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}

이렇게 하면 10개의 마이크로서비스가 있더라도 CI 파이프라인 로직은 하나만 관리하면 됩니다. 변경이 필요할 때 공통 워크플로우 하나만 수정하면 모든 서비스에 적용됩니다.

4-4. 고급 패턴

**Path-based Triggering (모노레포)**

모노레포에서 변경된 서비스만 빌드하는 패턴입니다.

name: Monorepo CI

on:

push:

branches: [main]

pull_request:

jobs:

detect-changes:

runs-on: ubuntu-latest

outputs:

services: ${{ steps.changes.outputs.changes }}

steps:

- uses: actions/checkout@v4

- uses: dorny/paths-filter@v3

id: changes

with:

filters: |

user-service:

- 'services/user/**'

order-service:

- 'services/order/**'

payment-service:

- 'services/payment/**'

shared-lib:

- 'pkg/**'

build-user:

needs: [detect-changes]

if: needs.detect-changes.outputs.services == 'true' || contains(needs.detect-changes.outputs.services, 'user-service') || contains(needs.detect-changes.outputs.services, 'shared-lib')

uses: ./.github/workflows/go-ci-reusable.yml

with:

working-directory: services/user

build-order:

needs: [detect-changes]

if: contains(needs.detect-changes.outputs.services, 'order-service') || contains(needs.detect-changes.outputs.services, 'shared-lib')

uses: ./.github/workflows/go-ci-reusable.yml

with:

working-directory: services/order

`shared-lib`(공통 라이브러리)가 변경되면 모든 서비스를 다시 빌드합니다. 개별 서비스만 변경되면 해당 서비스만 빌드합니다.

**Deployment Approval Gates**

프로덕션 배포 전에 수동 승인을 요구하는 패턴입니다.

deploy-staging:

environment:

name: staging

staging은 자동 배포

deploy-production:

needs: [deploy-staging]

environment:

name: production # GitHub UI에서 승인 필요

url: https://myapp.example.com

production은 승인 후 배포

GitHub UI에서 Environment 설정에 Required Reviewers를 추가하면, 워크플로우가 해당 Job에 도달했을 때 지정된 리뷰어의 승인을 기다립니다.

**Rollback 자동화**

name: Rollback

on:

workflow_dispatch:

inputs:

revision:

description: 'Rollback to revision (0 for previous)'

required: false

default: '0'

service:

description: 'Service to rollback'

required: true

type: choice

options:

- user-service

- order-service

- payment-service

jobs:

rollback:

runs-on: ubuntu-latest

environment: production

steps:

- name: Configure kubectl

run: |

aws eks update-kubeconfig --name my-cluster

- name: Rollback

run: |

REVISION="${{ github.event.inputs.revision }}"

SERVICE="${{ github.event.inputs.service }}"

if [ "$REVISION" = "0" ]; then

kubectl rollout undo deployment/${SERVICE} -n production

else

kubectl rollout undo deployment/${SERVICE} --to-revision=${REVISION} -n production

fi

kubectl rollout status deployment/${SERVICE} -n production --timeout=120s

**GitHub Packages + GHCR**

GitHub Container Registry(GHCR)는 GitHub에 내장된 Docker 레지스트리입니다.

이미지 푸시

- uses: docker/login-action@v3

with:

registry: ghcr.io

username: ${{ github.actor }}

password: ${{ secrets.GITHUB_TOKEN }}

이미지 네이밍 컨벤션

ghcr.io/OWNER/REPO:TAG

ghcr.io/myorg/user-service:v1.2.3

ghcr.io/myorg/user-service:sha-abc1234

ghcr.io/myorg/user-service:main

GHCR의 장점:

- GitHub 리포지토리와 직접 연결

- GITHUB_TOKEN으로 인증 (별도 시크릿 불필요)

- Private/Public 이미지 모두 지원

- 무료 계정에서도 사용 가능 (Private은 스토리지 한도 있음)

Part 5: 성능 최적화 & 비용 절약

5-1. 빌드 속도 최적화

**캐싱 전략 총정리**

Go Module Cache

- uses: actions/cache@v4

with:

path: ~/go/pkg/mod

key: gomod-${{ runner.os }}-${{ hashFiles('**/go.sum') }}

Go Build Cache

- uses: actions/cache@v4

with:

path: ~/.cache/go-build

key: gobuild-${{ runner.os }}-${{ hashFiles('**/*.go') }}

restore-keys: |

gobuild-${{ runner.os }}-

Docker Layer Cache (GitHub Actions backend)

- uses: docker/build-push-action@v6

with:

cache-from: type=gha

cache-to: type=gha,mode=max

npm Cache (프론트엔드가 있는 경우)

- uses: actions/cache@v4

with:

path: ~/.npm

key: npm-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}

캐시 키 설계 원칙:

- 정확한 매칭을 위한 primary key (해시 포함)

- Fallback을 위한 `restore-keys` (부분 매칭)

- OS별 분리 (크로스 플랫폼 빌드 시)

**병렬 vs 순차 Job 설계**

좋은 설계: 독립적인 Job은 병렬 실행

jobs:

lint: # ──┐

... # ├── 병렬

test: # ──┤

... # ├── 병렬

security: # ──┘

...

build:

needs: [lint, test, security] # 위 3개 모두 완료 후

docker:

needs: [build] # 순차

deploy:

needs: [docker] # 순차

lint, test, security는 서로 의존하지 않으므로 병렬 실행하고, build는 이 세 Job이 모두 성공한 후에 실행합니다. 이렇게 하면 전체 파이프라인 시간이 크게 줄어듭니다.

**Self-hosted Runner on Spot Instances**

actions-runner-controller (ARC)로 K8s에서 Self-hosted Runner 운영

AWS Spot Instance 기반으로 비용 절약

runner-deployment.yaml (Kubernetes)

ARC를 사용하면 워크플로우 수요에 따라

Runner가 자동으로 스케일 업/다운됩니다.

Spot Instance 기반 Self-hosted Runner의 장점:

- GitHub-hosted 대비 70-80% 비용 절감

- 커스텀 하드웨어 사양 (더 많은 CPU/RAM)

- 로컬 디스크 캐시 (더 빠른 빌드)

- GPU 지원 (ML 프로젝트)

주의사항:

- Spot 인스턴스는 언제든 중단될 수 있으므로 graceful shutdown 처리 필요

- Runner 보안 관리는 직접 해야 함

- Runner 가용성 모니터링 필요

5-2. 비용 최적화

**GitHub Actions 요금 체계 (2025-2026 기준)**

| 플랜 | 무료 분/월 | Linux 단가 | Windows 단가 | macOS 단가 |

| ---------- | ---------- | ---------- | ------------ | ---------- |

| Free | 2,000분 | 무료 | 무료 | 무료 |

| Pro | 3,000분 | 0.008/분 | 0.016/분 | 0.08/분 |

| Team | 3,000분 | 0.008/분 | 0.016/분 | 0.08/분 |

| Enterprise | 50,000분 | 0.008/분 | 0.016/분 | 0.08/분 |

macOS Runner는 Linux 대비 10배 비쌉니다. 가능하면 Linux에서 테스트하고, macOS는 필수적인 경우에만 사용하세요.

**비용 계산 예시**

일일 빌드 횟수: 50회

평균 빌드 시간: 8분

Linux Runner 사용

일일 비용: 50 x 8 x $0.008 = $3.20

월간 비용: $3.20 x 22일(영업일) = $70.40

연간 비용: $70.40 x 12 = $844.80

**비용 절약 팁**

1. **캐싱 극대화**: 캐시 히트율이 높을수록 빌드 시간이 줄어듭니다.

2. **조건부 스킵**: 변경 없는 서비스는 빌드하지 않습니다.

- name: Check for changes

id: changes

run: |

if git diff --name-only HEAD~1 | grep -q "^src/"; then

echo "changed=true" >> "$GITHUB_OUTPUT"

else

echo "changed=false" >> "$GITHUB_OUTPUT"

fi

- name: Build

if: steps.changes.outputs.changed == 'true'

run: go build ./...

3. **매트릭스 최적화**: 전체 매트릭스는 main 브랜치에서만, PR에서는 핵심 조합만 실행합니다.

strategy:

matrix:

include:

PR에서는 최소한의 조합만

- os: ubuntu-latest

go: '1.23'

main에서는 전체 매트릭스

- os: macos-latest

go: '1.23'

if: github.ref == 'refs/heads/main'

4. **timeout 설정**: 무한 루프 방지로 예상치 못한 비용 발생을 차단합니다.

jobs:

test:

timeout-minutes: 15

steps:

- name: Test

timeout-minutes: 10

run: go test ./...

**Self-hosted vs GitHub-hosted 비용 비교**

| 항목 | GitHub-hosted | Self-hosted (Spot) |

| ---------------- | ------------- | ------------------ |

| 월 1,000분 기준 | ~8달러 | ~2달러 (EC2 비용) |

| 월 10,000분 기준 | ~80달러 | ~15달러 |

| 월 50,000분 기준 | ~400달러 | ~60달러 |

| 관리 비용 | 0 | 인력 투입 필요 |

| 설정 난이도 | 없음 | 중간~높음 |

월 10,000분 이상 사용한다면 Self-hosted Runner를 검토해볼 가치가 있습니다.

5-3. 모니터링 & 트러블슈팅

**Workflow 실행 시간 추적**

GitHub는 워크플로우 실행 시간과 결과를 기본적으로 기록합니다. 하지만 더 상세한 분석이 필요하다면:

- name: Track job timing

run: |

echo "Job started at: $(date -u +%Y-%m-%dT%H:%M:%SZ)"

START_TIME=$(date +%s)

... 실제 작업 ...

END_TIME=$(date +%s)

DURATION=$((END_TIME - START_TIME))

echo "Duration: ${DURATION} seconds"

**실패율 분석**

GitHub CLI로 워크플로우 실행 이력을 조회할 수 있습니다.

최근 20회 실행 결과

gh run list --workflow=ci.yml --limit=20

실패한 실행만 필터

gh run list --workflow=ci.yml --status=failure --limit=10

특정 실행의 로그 확인

gh run view 12345678 --log

**Step Debug Logging**

문제를 진단하기 어려울 때 디버그 로깅을 활성화합니다.

방법 1: Repository Secret으로 설정

ACTIONS_STEP_DEBUG = true

ACTIONS_RUNNER_DEBUG = true

방법 2: 수동 실행 시 디버그 활성화

on:

workflow_dispatch:

GitHub UI에서 "Enable debug logging" 체크박스 선택

디버그 모드에서는 각 Step의 상세한 실행 로그, 환경 변수, 파일 시스템 상태 등을 확인할 수 있습니다.

**act로 로컬 테스트**

`act`는 GitHub Actions 워크플로우를 로컬에서 실행할 수 있는 도구입니다. Docker를 사용하여 Runner 환경을 에뮬레이션합니다.

설치 (macOS)

brew install act

기본 실행 (push 이벤트)

act push

특정 Job만 실행

act -j test

시크릿 전달

act push --secret-file .env.secrets

특정 이벤트 데이터로 실행

act pull_request --eventpath event.json

사용 가능한 워크플로우 목록

act -l

act의 제한사항:

- Service Container 미지원 (Postgres, Redis 등)

- OIDC 인증 미지원

- GitHub API 호출 제한

- 일부 Action이 로컬에서 동작하지 않을 수 있음

그래도 기본적인 YAML 문법 검증과 스텝 실행 테스트에는 매우 유용합니다.

보너스: 실전 팁 모음

Conventional Commits 컨벤션

팀에서 일관된 커밋 메시지를 사용하면 자동 changelog 생성, semantic versioning 자동화가 가능합니다.

feat: 새 기능 추가 (MINOR 버전 증가)

fix: 버그 수정 (PATCH 버전 증가)

docs: 문서 변경

style: 코드 포맷팅 (기능 변경 없음)

refactor: 리팩토링 (기능 변경 없음)

perf: 성능 개선

test: 테스트 추가/수정

ci: CI 설정 변경

chore: 기타 변경

Breaking Change (MAJOR 버전 증가)

feat!: API 엔드포인트 변경

**커밋 메시지 린트 (GitHub Actions)**

name: Commit Lint

on:

pull_request:

jobs:

commitlint:

runs-on: ubuntu-latest

steps:

- uses: actions/checkout@v4

with:

fetch-depth: 0

- uses: wagoid/commitlint-github-action@v6

with:

configFile: .commitlintrc.yml

Dependabot 자동 의존성 업데이트

.github/dependabot.yml

version: 2

updates:

- package-ecosystem: gomod

directory: /

schedule:

interval: weekly

day: monday

open-pull-requests-limit: 10

reviewers:

- myorg/backend-team

labels:

- dependencies

- go

commit-message:

prefix: 'deps'

- package-ecosystem: docker

directory: /

schedule:

interval: weekly

labels:

- dependencies

- docker

- package-ecosystem: github-actions

directory: /

schedule:

interval: weekly

labels:

- dependencies

- ci

GitHub Actions 보안 하드닝

1. 최소 권한 원칙

permissions:

contents: read # 기본적으로 읽기만

2. Action 버전 SHA 고정

- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1

3. 외부 Action 감사

Marketplace Action 사용 전 소스 코드 확인

4. OpenSSF Scorecard

- uses: ossf/scorecard-action@v2

with:

results_file: scorecard-results.sarif

실전 퀴즈

**A1.**

concurrency group은 동일한 그룹 내에서 워크플로우 실행이 겹치지 않도록 제어합니다.

- **PR에서 `cancel-in-progress: true`**: 개발자가 새 커밋을 푸시하면 이전 CI 실행이 의미 없으므로 취소하여 Runner 비용을 절약합니다.

- **프로덕션 배포에서 `cancel-in-progress: false`**: 진행 중인 배포를 중간에 취소하면 서비스가 불안정한 상태에 놓일 수 있으므로, 현재 배포가 완료될 때까지 새 배포를 대기시킵니다.

핵심은 "취소해도 안전한가?"를 기준으로 판단하는 것입니다. CI 테스트는 취소해도 무해하지만, 프로덕션 배포는 원자적으로 완료되어야 합니다.

**A2.**

`CGO_ENABLED=0`은 C 라이브러리(libc 등)에 대한 동적 링크를 비활성화하고, 순수 Go 코드만으로 정적 바이너리를 생성합니다.

Docker 이미지에 미치는 영향:

1. **scratch 이미지 사용 가능**: C 라이브러리가 없어도 바이너리가 독립적으로 실행됩니다. scratch는 0바이트 베이스 이미지이므로 최종 이미지가 바이너리 크기(10-15MB)와 거의 같습니다.

2. **보안 강화**: 공격 표면이 최소화됩니다. 쉘, 패키지 매니저, 시스템 라이브러리가 없으므로 CVE에 노출될 가능성이 극히 낮습니다.

3. **이식성**: 어떤 Linux 커널에서도 동일하게 동작합니다.

단, `net` 패키지의 DNS resolver나 SQLite 같은 CGO 의존 라이브러리를 사용하는 경우에는 주의가 필요합니다. Go의 내장 DNS resolver(`netgo` 빌드 태그)를 사용하거나, 순수 Go 구현체로 대체해야 합니다.

**A3.**

Feature Flag 없이 미완성 기능을 main에 머지하면 다음 문제가 발생합니다:

1. **사용자 노출 위험**: main에 머지된 코드는 즉시 배포될 수 있으므로, 미완성 기능이 사용자에게 노출됩니다.

2. **불안정한 프로덕션**: 미완성 로직으로 인한 예기치 않은 에러, 크래시, 데이터 손상 가능성이 있습니다.

3. **롤백 어려움**: 여러 개발자의 커밋이 섞여 있으므로 특정 기능만 롤백하기 어렵습니다.

4. **배포 차단**: 미완성 기능 때문에 다른 완성된 기능까지 배포가 지연될 수 있습니다.

Feature Flag의 핵심 가치는 "배포(deploy)와 릴리스(release)를 분리"하는 것입니다. 코드는 main에 있지만, Feature Flag를 통해 사용자에게는 보이지 않게 할 수 있습니다. 기능이 완성되면 Flag를 켜서 릴리스하고, 문제가 발생하면 Flag를 끄는 것만으로 롤백이 가능합니다.

**A4.**

기존 시크릿 기반 인증의 문제점:

- 장기 자격 증명(Access Key/Secret Key)을 GitHub에 저장해야 합니다.

- 키가 유출되면 만료되지 않으므로 무제한 접근이 가능합니다.

- 키 로테이션을 수동으로 관리해야 합니다.

- 여러 리포지토리에서 같은 키를 공유하면 위험이 증폭됩니다.

OIDC 인증의 장점:

1. **단기 토큰**: 워크플로우 실행 시에만 유효한 임시 토큰을 발급받습니다. 실행이 끝나면 토큰이 자동으로 만료됩니다.

2. **시크릿 저장 불필요**: GitHub에 AWS/GCP 자격 증명을 저장하지 않으므로 유출 위험이 없습니다.

3. **세분화된 권한**: IAM 역할의 trust policy에서 특정 리포지토리, 브랜치, 환경에서만 접근을 허용할 수 있습니다.

4. **감사 추적**: 클라우드 제공자의 로그에서 어떤 워크플로우가 언제 접근했는지 추적할 수 있습니다.

동작 원리: GitHub Actions가 OIDC 토큰을 발행하면, AWS STS의 AssumeRoleWithWebIdentity API를 호출하여 임시 자격 증명을 받습니다. 이 토큰은 기본 1시간만 유효합니다.

**A5.**

모노레포에서 공유 라이브러리(예: pkg/, lib/, common/)가 변경되면, 해당 라이브러리에 의존하는 모든 서비스를 다시 빌드하고 테스트해야 합니다.

구현 방법:

1. **paths-filter로 변경 감지**: dorny/paths-filter 액션으로 어떤 디렉토리가 변경되었는지 감지합니다.

2. **의존성 그래프 기반 빌드**: 공유 라이브러리 변경 시 의존하는 서비스를 모두 빌드합니다.

3. **실전 패턴**:

- 공유 라이브러리 변경이 감지되면 모든 서비스 빌드를 트리거합니다.

- 개별 서비스 변경이면 해당 서비스만 빌드합니다.

- `go.sum` 변경은 전체 빌드를 트리거할 수 있습니다.

4. **최적화**: 공유 라이브러리의 변경 범위를 분석하여 영향받는 서비스만 빌드할 수도 있지만, 복잡도가 높아지므로 대부분의 팀에서는 "공유 라이브러리 변경 시 전체 빌드"로 충분합니다.

핵심 원칙: **안전성(모든 서비스 빌드)과 효율성(변경된 서비스만 빌드) 사이의 균형**을 팀의 상황에 맞게 선택합니다. 서비스 수가 적으면 전체 빌드가 충분하고, 수십 개가 넘으면 의존성 그래프 기반 빌드를 고려합니다.

참고 자료

1. GitHub Actions Documentation - Workflow syntax reference

2. GitHub Actions - Reusable workflows

3. GitHub Actions - OIDC for cloud providers

4. Go Documentation - Command go (build, test, vet)

5. Go Documentation - Module reference

6. golangci-lint - Go linters aggregator

7. GoReleaser Documentation - Release automation for Go

8. Docker Documentation - Multi-stage builds

9. Google Distroless Container Images

10. actions/cache - Caching dependencies

11. docker/build-push-action - Build and push Docker images

12. Git Flow - A successful Git branching model (Vincent Driessen)

13. GitHub Flow - Understanding the GitHub flow

14. Trunk-Based Development - trunkbaseddevelopment.com

15. GitHub - Branch protection rules

16. GitHub - Repository rulesets

17. CODEOWNERS - About code owners

18. Semantic Versioning 2.0.0

19. Conventional Commits specification

20. act - Run GitHub Actions locally

21. GitHub - Understanding GitHub Actions billing

22. AWS OIDC - GitHub Actions OpenID Connect

23. Kubernetes - Rolling update deployment

24. GitHub Container Registry (GHCR) Documentation

25. OpenSSF Scorecard - Security health metrics for open source

현재 단락 (1/1729)

현대 소프트웨어 개발에서 CI/CD는 선택이 아니라 생존 조건입니다. 코드를 작성하고, 테스트를 돌리고, 빌드하고, 배포하는 모든 과정을 수동으로 처리하던 시대는 끝났습니다. Gi...

작성 글자: 0원문 글자: 42,485작성 단락: 0/1729