클라우드 서비스를 AWS에서 Google Cloud로 이전하는 과정에서 CI/CD 구성을 다시 잡아야 했다. 단순히 “자동 배포”를 붙이는 수준이 아니라, 혼자 개발하더라도 실수로 서비스가 흔들리지 않도록 흐름을 정리하는 것이 목적이었다. 이 글은 그 과정에서 확정한 브랜치 전략, Cloud Build 트리거 구성, GitHub 보호 규칙과 release-please 적용까지를 한 번에 정리한 기록이다.
main과 develop으로 분리한 이유와 운영 방식main을 보호하는 방법main과 develop을 분리한 이유브랜치를 분리하는 목적은 명확하다. 완성되지 않은 코드가 프로덕션으로 유입되는 것을 구조적으로 차단하기 위함이다. main 하나만 운용할 경우, 작업 중인 기능이 의도치 않게 배포되는 상황을 피하기 어렵다. 특히 배포 주기가 짧고 변경이 잦은 환경에서는 이런 위험이 더 자주 현실화된다.
따라서 다음과 같이 역할을 분리했다.
main: 실제 사용자가 접속하는 프로덕션 코드develop: 내부 검증을 위한 개발/테스트 코드feat/*, fix/*: 작업 단위 브랜치(작게 쪼개어 PR로 병합)프로젝트에서는 다음과 같이 서버와 연결했다.
main → greenmate-backend (프로덕션)develop → greenmate-backend-dev (개발 서버)일상적인 흐름은 가능한 단순하게 유지하는 편이 좋다. 작업은 항상 develop을 기준으로 분기하고, PR의 목표 브랜치는 develop으로 고정한다.
git checkout develop git pull origin develop git checkout -b feat/add-login git commit -m "feat: 로그인 기능 추가" gh pr create --base develop
브랜치 네이밍 규칙은 아래 정도로 통일하면 관리가 편하다.
feat/기능명fix/버그명refactor/대상chore/작업명CI/CD는 “언제 무엇을 실행할지”를 잘게 나누는 것이 핵심이다. 한 파이프라인에 모든 것을 몰아넣으면 초기에 편해 보이지만, 문제가 발생했을 때 원인 파악이 어렵고 유지보수 비용이 커진다. 그래서 Cloud Build 트리거를 세 개로 분리했다.
develop 병합 시: 개발 서버 배포main 병합 시: 프로덕션 배포PR이 열리면 자동으로 빌드, 린트/타입 체크, 테스트를 수행한다. 머지 전 검증이 통과되어야 하므로, PR의 품질을 일정 수준 이상으로 유지할 수 있다.
# cloudbuild/ci.yaml steps: - name: "gcr.io/cloud-builders/docker" id: "Build" args: ["build", "-t", "greenmate-service:${SHORT_SHA}", "."] - name: "gcr.io/cloud-builders/docker" id: "Lint and Type Check" args: - -c - | docker run greenmate-service:${SHORT_SHA} uv run ruff check app tests docker run greenmate-service:${SHORT_SHA} uv run mypy app waitFor: ["Build"] - name: "gcr.io/cloud-builders/docker" id: "Test" args: - -c - | docker run greenmate-service:${SHORT_SHA} pytest -v waitFor: ["Build"]
waitFor: ["Build"]로 설정하면 빌드 완료 후 린트/타입 체크와 테스트가 병렬로 수행된다. PR 피드백 속도를 조금이라도 줄이는 데 도움이 된다.
develop 병합 시 개발 서버 배포develop에 병합되면 개발 서버에 자동 배포되도록 구성했다. 개발자는 병합까지만 수행하면 되며, 이후 과정은 파이프라인이 처리한다.
# cloudbuild/cd-cloudrun-dev.yaml substitutions: _SERVICE_NAME: "greenmate-backend-dev" _DATABASE_NAME: "greenmate_dev" steps: - id: "Build" # docker build ... - id: "Push to Artifact Registry" waitFor: ["Build"] - id: "Run Migrations" args: - -c - | uv run alembic upgrade head waitFor: ["Push to Artifact Registry"] - id: "Deploy to Cloud Run" args: - -c - | gcloud run deploy greenmate-backend-dev waitFor: ["Run Migrations"]
배포 순서는 이미지 업로드 → DB 마이그레이션 → 배포로 고정했다. 이미지가 먼저 반영된 뒤 DB가 따라오지 않으면 서비스가 즉시 깨질 수 있으므로, 이 순서를 강제하는 편이 안전하다.
main 병합 시 프로덕션 배포프로덕션 배포 파이프라인은 개발 서버와 구조를 동일하게 유지하고, 환경 차이는 설정값으로만 분리했다.
# cloudbuild/cd-cloudrun.yaml substitutions: _SERVICE_NAME: "greenmate-backend" _DATABASE_NAME: "greenmate"
예를 들어 프로덕션에는 다음을 적용했다.
구조를 같게 유지하면, 환경이 늘어나거나 파이프라인을 수정할 때 실수를 줄일 수 있다.
문서 수정만으로 배포가 수행되는 것은 비용과 피로도를 함께 증가시킨다. Cloud Build 트리거의 includedFiles를 활용하면 배포에 영향이 있는 변경만 감지할 수 있다.
includedFiles: - "app/**" - "alembic/**" - "pyproject.toml" - "Dockerfile"
main 보호CI/CD가 있어도 main에 직접 push가 가능하면 사고 여지는 남는다. 따라서 Ruleset으로 main을 보호하는 구성을 함께 적용했다.
권장 규칙은 다음과 같다.
main PR은 develop에서만 허용추가로, main으로 들어가는 PR의 출발 브랜치를 develop로 제한했다. 작업 브랜치에서 main으로 곧바로 PR을 여는 경로를 차단하기 위함이다.
# .github/workflows/pr-source-check.yml name: PR Source Branch Check on: pull_request: branches: [main] jobs: check-source: runs-on: ubuntu-latest steps: - name: Check source branch run: | SOURCE="${{ github.head_ref }}" if [[ "$SOURCE" != "develop" && ! "$SOURCE" =~ ^release-please-- ]]; then echo "::error::main으로의 PR은 develop에서만 가능합니다!" exit 1 fi
release-please가 생성하는 브랜치는 예외로 허용한다.
버전 관리와 changelog 작성은 반복 작업이면서도 누락되기 쉽다. release-please를 사용하면 Conventional Commits 기반으로 버전 상승과 릴리스 노트를 자동으로 생성할 수 있다.
git commit -m "feat: 로그인 기능 추가" # minor git commit -m "fix: 로그인 버그 수정" # patch git commit -m "feat!: API 응답 변경" # major
// release-please-config.json { "release-type": "python", "include-v-in-tag": true, "changelog-sections": [ {"type": "feat", "section": "Features"}, {"type": "fix", "section": "Bug Fixes"}, {"type": "perf", "section": "Performance Improvements"}, {"type": "docs", "section": "Documentation", "hidden": true}, {"type": "chore", "section": "Miscellaneous", "hidden": true} ] }
# .github/workflows/release-please.yml name: Release Please on: push: branches: [main] jobs: release-please: runs-on: ubuntu-latest steps: - uses: googleapis/release-please-action@v4 with: config-file: release-please-config.json manifest-file: .release-please-manifest.json
main에 변경이 쌓이면 release-please가 Release PR을 생성하고, 이를 병합하면 태그/릴리스/체인지로그가 자동 생성되는 구조다.
초기에는 Squash merge를 사용했으나, develop과 main의 커밋 히스토리가 지속적으로 어긋나는 문제가 있었다. Squash merge는 PR의 커밋들을 “새 커밋 1개”로 재작성하여 main에 반영하므로, develop에 남아 있는 기존 커밋들과 main의 커밋이 동일한 변경이라도 다른 커밋으로 취급된다. 결과적으로 GitHub에서 “뒤쳐짐/앞서감” 상태가 자주 발생했고, 이를 맞추는 작업이 번거로웠다.
따라서 최종적으로 Merge commit만 허용했다.
히스토리는 다소 길어질 수 있으나, 브랜치 간 동기화 비용이 줄어들어 운영 측면에서 이득이 컸다.
git checkout -b feat/new-feature git commit -m "feat: 새 기능 추가" gh pr create --base develop # 프로덕션 배포 시 gh pr create --base main --head develop
develop 병합 시: dev 배포 + DB 마이그레이션main 병합 시: prod 배포 + Release PR 생성참고 링크