PR-Agent로 구축하는 AI 보조 코드 리뷰 파이프라인 — GitLab CI/CD 3-Layer 아키텍처와 함께
환경: GitLab Self-hosted, GitLab CI/CD, PR-Agent, EKS, Kaniko, ArgoCD
핵심 주제: CI/CD 컴포넌트 아키텍처, PR-Agent 통합, AI 코드 리뷰 자동화, 3-레이어 파이프라인 설계
배경 / 문제 상황
EKS 플랫폼이 안정화되고 여러 서비스가 올라오기 시작하면서, CI/CD가 새로운 병목이 됐다.
처음에는 서비스마다 .gitlab-ci.yml을 복사해서 붙여넣었다. 빌드 방식이 같고 배포 대상도 같으니 큰 문제가 없을 거라 생각했다. 문제는 서비스가 8개, 10개로 늘어나면서 드러났다.
복사 붙여넣기의 저주: 빌드 이미지 버전을 하나 바꾸려면 10개 저장소를 돌아다니며 수정해야 한다. 하나라도 빠뜨리면 불일치가 발생한다. GitOps 배포 로직에 버그가 있어도 고치려면 10번 같은 수정을 반복해야 한다.
환경의 파편화: 어떤 서비스는 Dev 배포를 Kubernetes(k3s)로, 어떤 서비스는 SSH+Docker로 한다. Stage/Prod는 모두 ArgoCD GitOps다. 이 조합을 하나의 파이프라인 템플릿에 우겨넣으려다가 if/else와 제어 변수가 넘쳐나는 “신 파이프라인”이 탄생했다. 나 외에는 아무도 수정하지 못하는 코드가 됐다.
코드 리뷰 부재: 소규모 팀에서 인프라 코드 리뷰가 형식적으로 흘렀다. 리뷰어가 바빠서 “LGTM” 한 줄로 머지되는 MR이 늘어났다. 기술 부채가 조용히 쌓이고 있었다.
두 가지 문제를 동시에 풀어야 했다. 재사용 가능하고 확장 가능한 CI/CD 아키텍처, 그리고 자동화된 코드 품질 검증.
기술 선택 배경
CI/CD: 3-Layer 컴포넌트 아키텍처
GitLab CI의 include와 extends 키워드를 활용한 계층형 아키텍처를 설계했다. 핵심 아이디어는 레고 블록이다 — 기능 단위(Primitives)를 만들고, 팀별 요구사항에 맞게 조립(Workflows)하고, 각 서비스는 한 줄로 가져다 쓴다(Consumer).
이 아키텍처가 필요한 이유는 명확하다: Dev 환경 배포 방식이 서비스마다 달라도 Stage/Prod GitOps 로직은 모든 서비스가 공유한다. 공통 로직은 한 곳에서 관리하고, 차이점만 팀별로 정의해야 한다.
PR-Agent: AI 코드 리뷰 자동화
PR-Agent는 Codium AI가 오픈소스로 공개한 AI 코드 리뷰 도구다. GitLab MR(Merge Request)이 열릴 때 자동으로 작동해서 변경 내용 요약, 잠재적 버그 분석, 개선 제안, 보안 이슈 경고를 남긴다.
선택 이유는 두 가지다. 첫째, GitLab CI 파이프라인에 Job 하나를 추가하는 방식으로 도입된다 — 기존 워크플로우 변경 없이 통합 가능하다. 둘째, AWS Bedrock 연동을 지원해서 Claude 모델을 리뷰어로 사용할 수 있다. 외부 API에 코드를 전송하는 보안 우려를 내부 AWS 인프라로 해소할 수 있다.
CI/CD 아키텍처 설계 — 5단계 진화 과정
현재의 3-Layer 아키텍처에 도달하기까지 5단계를 거쳤다. 각 단계의 실패에서 다음 단계의 원칙이 나왔다.
1단계 (복붙 시대): echo "build start"만 찍는 껍데기 파이프라인. 문법 확인 용도.
2단계 (모놀리식): 실제 빌드/배포를 하지만 수동 개입이 필요한 반자동. 서비스별로 파이프라인을 복사해서 관리.
3단계 (과잉 엔지니어링): “모든 것을 자동화”라는 일념으로 모든 예외를 하나의 파이프라인에 처리. SKIP_TEST, FORCE_DEPLOY, IS_HOTFIX 같은 제어 변수가 10개 이상. 작성자 외 아무도 건드리지 못하는 코드.
4단계 (덜어냄): 1년에 한 번 쓸 옵션들을 삭제. 기본값으로 90%의 케이스를 처리하고 특별한 경우에만 옵션을 켜도록. “설정보다 관습(Convention over Configuration)”.
5단계 (컴포넌트 기반): 현재 아키텍처. Primitives + Workflows + Consumer 3개 레이어로 분리.
구현 과정
Step 1. 중앙 CI/CD 템플릿 저장소 구성
모든 파이프라인 로직을 platform-gitlab-ci-templates라는 전용 저장소로 이전했다. 서비스 저장소의 .gitlab-ci.yml은 이 중앙 저장소를 참조하는 단 몇 줄로 줄어든다.
디렉토리 구조는 기술 스택이 아닌 비즈니스 도메인 기준으로 설계했다:
platform-gitlab-ci-templates/
│
├── common/
│ └── utils.yml # 공통 유틸 (debug hook, slack 알림)
│
├── services/ # 실행 중인 서버 애플리케이션
│ ├── components/
│ │ ├── build/
│ │ │ ├── kaniko-build.yml # Docker 이미지 빌드
│ │ │ └── trivy-scan.yml # 이미지 취약점 스캔
│ │ └── deploy/
│ │ ├── gitops-update.yml # ArgoCD Manifest Repo 업데이트
│ │ ├── k8s-direct.yml # kubectl 직접 배포 (k3s Dev용)
│ │ └── ssh-docker-run.yml # SSH 직접 배포 (레거시용)
│ │
│ └── pipelines/ # 조립된 워크플로우 (Wrapper)
│ ├── web-service-std.yml # 표준: Build → GitOps
│ └── legacy-service-ssh.yml # 레거시: Build → SSH
│
└── packages/ # 저장소에 등록되는 산출물
├── components/
│ ├── build/
│ │ └── helm-package.yml # Helm Chart 패키징
│ └── deploy/
│ └── helm-oci-push.yml # OCI Registry Push
│
└── pipelines/
└── helm-chart-release.yml # Lint → Package → Push
핵심 네이밍 원칙: kaniko.yml(너무 포괄적) 대신 kaniko-build.yml(기술-행위). gitops.yml 대신 gitops-update.yml. 파일명만 봐도 용도를 알 수 있어야 한다.
Step 2. Primitives 구현 — 순수 로직 레이어
Primitives는 환경변수에만 의존하고, spec:inputs를 사용하지 않는 순수 실행 로직이다:
# services/components/build/kaniko-build.yml
.build-kaniko:
image:
name: gcr.io/kaniko-project/executor:debug
entrypoint: [""]
variables:
SERVICE_NAME: "test-service" # 로컬 테스트용 기본값
IMAGE_TAG: "latest"
REGISTRY_URL: ""
script:
- |
/kaniko/executor \
--context "$CI_PROJECT_DIR/$SERVICE_ROOT" \
--dockerfile "$CI_PROJECT_DIR/$SERVICE_ROOT/Dockerfile" \
--destination "$REGISTRY_URL/$SERVICE_NAME:$IMAGE_TAG" \
--destination "$REGISTRY_URL/$SERVICE_NAME:latest"
tags:
- k8s-general
# services/components/deploy/gitops-update.yml
.deploy-gitops:
image: alpine/git:latest
variables:
GITOPS_REPO: ""
SERVICE_NAME: ""
IMAGE_TAG: ""
ENV_PATH: ""
script:
- |
# Manifest 저장소 클론
git clone https://oauth2:$GITOPS_TOKEN@$GITOPS_REPO /tmp/manifests
cd /tmp/manifests
# 이미지 태그 업데이트 (values.yaml 내 image.tag)
sed -i "s|tag:.*|tag: $IMAGE_TAG|g" $ENV_PATH/values.yaml
git config user.email "ci@platform"
git config user.name "GitLab CI"
git add -A
git commit -m "chore: update $SERVICE_NAME image to $IMAGE_TAG [skip ci]"
git push
tags:
- k8s-general
Step 3. Pipeline Wrapper 구현
파이프라인 파일은 복잡한 로직을 수행하지 않는다. spec:inputs로 사용자 입력을 받고, 전역 변수로 매핑한 뒤, Primitives를 조립한다:
# services/pipelines/web-service-std.yml
spec:
inputs:
SERVICE_NAME:
description: "서비스 이름 (kebab-case)"
SERVICE_ROOT:
default: "."
description: "Dockerfile 위치 (기본: 프로젝트 루트)"
IMAGE_TAG:
default: "$CI_COMMIT_SHORT_SHA"
--- # ← spec과 본문 구분자 (누락 시 파싱 오류)
# [Wrapper Pattern] 입력값을 전역 컨텍스트로 변환
# Job 레벨이 아닌 전역에서 한 번만 매핑 — 가독성 극대화
variables:
SERVICE_NAME: $[[ inputs.SERVICE_NAME ]]
SERVICE_ROOT: $[[ inputs.SERVICE_ROOT ]]
IMAGE_TAG: $[[ inputs.IMAGE_TAG ]]
REGISTRY_URL: $AWS_REGISTRY_URL # 그룹 CI/CD Variables에서 주입
include:
- local: '/services/components/build/kaniko-build.yml'
- local: '/services/components/deploy/gitops-update.yml'
stages: [build, deploy]
# Job은 껍데기만 — 실제 로직은 Primitives에
build-image:
stage: build
extends: .build-kaniko
rules:
- if: '$CI_COMMIT_BRANCH == "main"'
- if: '$CI_COMMIT_BRANCH == "dev"'
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
deploy-prod:
stage: deploy
extends: .deploy-gitops
variables:
ENV_PATH: "manifests/prod/$SERVICE_NAME"
rules:
- if: '$CI_COMMIT_BRANCH == "main"'
when: manual # Prod 배포는 수동 승인
deploy-dev:
stage: deploy
extends: .deploy-gitops
variables:
ENV_PATH: "manifests/dev/$SERVICE_NAME"
rules:
- if: '$CI_COMMIT_BRANCH == "dev"'
Step 4. Consumer — 서비스 저장소 파이프라인
서비스 팀의 .gitlab-ci.yml은 이것이 전부다:
# 서비스 저장소 .gitlab-ci.yml
include:
- project: 'infra/platform-gitlab-ci-templates'
ref: 'v2.1.0' # 안정적인 버전 태그 고정
file: '/services/pipelines/web-service-std.yml'
inputs:
SERVICE_NAME: 'payment-api'
3줄. 새로운 서비스가 온보딩되어도 이 파일만 만들면 빌드-배포 파이프라인이 완성된다. GitOps 로직을 수정해야 한다면 gitops-update.yml 하나만 바꾸면 모든 서비스에 반영된다.
Helm 차트 저장소는 별도 파이프라인으로 관리한다:
# platform-helm-charts/.gitlab-ci.yml
include:
- project: 'infra/platform-gitlab-ci-templates'
ref: 'v2.1.0'
file: '/packages/pipelines/helm-chart-release.yml'
inputs:
CHART_NAME: 'app-base'
CHART_PATH: 'charts/app-base'
# 차트가 여러 개면 include 블록을 추가하면 됨 (확장성)
Helm 릴리즈 파이프라인은 Git Tag 여부로 Prod/Dev를 자동 분기한다:
- Git Tag(
v1.0.0):1.0.0버전으로 패키징 → Prod OCI 저장소 Push, 버전 중복 시 실패 - 일반 브랜치:
0.0.0-dev-{SHA}스냅샷 버전 →/dev서브패스로 격리 Push
Step 5. PR-Agent 통합 — AI 코드 리뷰
PR-Agent를 GitLab CI 파이프라인에 Job으로 추가한다. MR이 열릴 때만 실행되도록 rules를 설정한다:
# services/components/review/pr-agent.yml
.pr-agent-review:
image: codiumai/pr-agent:latest
variables:
# AWS Bedrock + Claude 설정
CONFIG_LLM_PROVIDER: "bedrock"
CONFIG_LLM_MODEL: "anthropic.claude-3-5-sonnet-20241022-v2:0"
AWS_DEFAULT_REGION: "ap-northeast-2"
# IRSA 또는 환경변수로 AWS 인증
script:
- python -m pr_agent.cli --pr_url $CI_MERGE_REQUEST_PROJECT_URL/merge_requests/$CI_MERGE_REQUEST_IID review
- python -m pr_agent.cli --pr_url $CI_MERGE_REQUEST_PROJECT_URL/merge_requests/$CI_MERGE_REQUEST_IID describe
- python -m pr_agent.cli --pr_url $CI_MERGE_REQUEST_PROJECT_URL/merge_requests/$CI_MERGE_REQUEST_IID improve
allow_failure: true # AI 리뷰 실패가 파이프라인 블록으로 이어지지 않게
tags:
- k8s-general
파이프라인 Wrapper에서 MR 이벤트 시 PR-Agent Job이 자동으로 포함된다:
# web-service-std.yml에 추가
include:
- local: '/services/components/build/kaniko-build.yml'
- local: '/services/components/deploy/gitops-update.yml'
- local: '/services/components/review/pr-agent.yml'
ai-code-review:
extends: .pr-agent-review
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
AWS Bedrock 연동의 핵심은 GitLab Runner가 AWS API를 호출할 수 있는 권한이다. Runner가 EKS 위에서 동작한다면 IRSA로 자격증명을 주입할 수 있다. Runner의 ServiceAccount에 Bedrock 호출 권한이 있는 IAM 역할을 연결하면 별도 액세스 키 없이 인증된다.
PR-Agent가 MR에 남기는 내용:
/describe: MR 변경 내용 자동 요약 및 PR 제목/설명 개선 제안/review: 잠재적 버그, 로직 오류, 보안 이슈, 코드 품질 문제 코멘트/improve: 구체적인 코드 개선 제안 (인라인 diff 형태)
관리자 테스트 전략 — 시스템 변수 스푸핑
복잡한 파이프라인을 테스트할 때 흔한 실수는 FORCE_BUILD, FORCE_DEPLOY 같은 제어 변수를 코드에 박아넣는 것이다. 이런 변수들이 쌓이면 파이프라인이 다시 신 스크립트가 된다.
더 나은 방법은 GitLab의 시스템 변수를 Web UI에서 주입해서 파이프라인을 속이는 것이다.
GitLab UI > Run pipeline > Variables에 추가:
CI_COMMIT_BRANCH = dev → dev 브랜치인 척
CI_COMMIT_TAG = v1.0.0 → 릴리즈 태그인 척
SKIP_BUILD_JOBS = true → 빌드 생략, 배포만 테스트
DEBUG_JOB_TARGET = deploy-prod → 해당 Job에서 sleep 진입
DEBUG_SLEEP_DURATION = 600 → 600초 대기 (이 사이에 Runner Pod에 exec)
DEBUG_JOB_TARGET과 DEBUG_SLEEP_DURATION은 모든 Primitive에 공통으로 들어가는 디버그 훅이다:
.helm-base:
before_script:
- |
if [ -n "$DEBUG_JOB_TARGET" ] && [ "$DEBUG_JOB_TARGET" == "$CI_JOB_NAME" ]; then
echo "DEBUG: 잡 '$CI_JOB_NAME'에서 정지합니다."
echo "이 사이에 Runner Pod에 kubectl exec으로 접속하세요."
sleep ${DEBUG_SLEEP_DURATION:-300}
fi
특정 Job에서 파이프라인을 일시 정지시키고 Runner Pod 내부에서 환경변수를 확인하거나 스크립트를 직접 실행해볼 수 있다. 프로덕션 이슈를 디버깅할 때 특히 유용하다.
핵심 트러블슈팅
문제 1: spec:inputs와 --- 구분자 누락
증상: 파이프라인 파일에 spec:inputs를 추가했는데 gitlab-ci.yml 파싱 오류 발생.
원인: GitLab CI에서 spec 섹션과 실제 파이프라인 본문은 반드시 --- YAML 문서 구분자로 나뉘어야 한다. 이 구분자가 없으면 파서가 spec 내용을 파이프라인 정의로 해석한다.
# 틀린 방식
spec:
inputs:
SERVICE_NAME:
variables: # ← 오류: spec 블록 안에 있는 것으로 해석됨
SERVICE_NAME: $[[ inputs.SERVICE_NAME ]]
# 올바른 방식
spec:
inputs:
SERVICE_NAME:
--- # ← 반드시 필요
variables:
SERVICE_NAME: $[[ inputs.SERVICE_NAME ]]
문제 2: Job 이름에서 inputs 변수 사용 불가
증상: Job 이름을 helm-lint-${{ inputs.CHART_NAME }}으로 설정하려 했더니 파싱 오류.
원인: GitLab CI에서 Job 이름에는 환경변수($CHART_NAME)를 사용할 수 없다. spec:inputs로 받은 값은 $[[ inputs.CHART_NAME ]] 문법으로만 접근 가능한데, Job 이름에서는 이 문법만 허용된다.
# 동작하지 않음
helm-lint-${CHART_NAME}: # 환경변수 사용 불가
# 동작하는 방식
helm-lint-$[[ inputs.CHART_NAME ]]: # inputs 직접 참조만 가능
문제 3: PR-Agent GitLab API 인증 실패
증상: PR-Agent Job이 401 Unauthorized로 실패. MR에 코멘트를 달지 못함.
원인: PR-Agent가 GitLab API로 코멘트를 달기 위해 Personal Access Token 또는 Bot Token이 필요하다. CI/CD Variable로 GITLAB_TOKEN을 주입하지 않으면 인증 실패.
해결: GitLab 그룹 설정에서 Bot 계정용 Access Token을 생성하고, CI/CD Variable로 GITLAB_TOKEN을 등록한다. 이 토큰에는 api 스코프가 필요하다.
.pr-agent-review:
variables:
GITLAB_TOKEN: $GITLAB_PR_AGENT_TOKEN # 그룹 CI/CD Variables에서 주입
OPENAI_KEY: "" # Bedrock 사용 시 불필요하지만 필드 요구 시 빈 값
문제 4: Kaniko의 Container Registry 인증
증상: Kaniko로 빌드 후 이미지를 GitLab Container Registry에 Push할 때 401 오류.
원인: Kaniko는 Docker 데몬 없이 동작하므로 일반적인 docker login으로 인증할 수 없다. /kaniko/.docker/config.json에 인증 정보를 직접 주입해야 한다.
.build-kaniko:
before_script:
- |
# GitLab Registry 인증 정보를 Kaniko 설정 파일로 주입
mkdir -p /kaniko/.docker
echo "{\"auths\":{\"$CI_REGISTRY\":{\"auth\":\"$(echo -n "gitlab-ci-token:$CI_JOB_TOKEN" | base64)\"}}}" \
> /kaniko/.docker/config.json
CI_JOB_TOKEN은 GitLab CI가 자동으로 제공하는 임시 토큰이다. 별도 자격증명 관리 없이 안전하게 Registry 인증이 가능하다.
결과 및 개선 효과
온보딩 속도 단축: 새 서비스가 추가될 때 .gitlab-ci.yml 3줄을 작성하면 빌드-배포 파이프라인이 완성된다. 이전에는 기존 파이프라인을 복사해서 경로와 변수를 수정하는 데 하루가 걸렸다.
공통 로직 중앙 관리: GitOps 배포 로직을 수정하면 모든 서비스에 자동으로 반영된다. 이전에는 수정 후 10개 저장소를 돌아다니며 동일한 변경을 반복했다.
PR-Agent 도입 효과: MR당 평균 리뷰 시간이 줄었다. AI가 빠르게 변경 내용을 요약하고 명백한 이슈를 먼저 지적하기 때문에 리뷰어가 핵심 로직에 집중할 수 있다. 특히 인프라 코드(Terraform, Helm) 변경에서 오타나 불필요한 권한 설정을 AI가 먼저 잡아냈다.
버전 태그 기반 안정성: 서비스들이 ref: 'v2.1.0'처럼 안정된 버전을 참조하므로, 중앙 템플릿의 실험적 변경이 프로덕션 파이프라인에 영향을 미치지 않는다.
보강 메모: 모델 교체는 운영 호환성 검증이다
PR-Agent 글에서 더 살릴 수 있는 운영 증거는 “AI 코드 리뷰를 붙였다”가 아니라, 모델 교체를 실제 운영 컴포넌트처럼 검증했다는 점이다.
gpt-5.1-codex 적용 시 PR-Agent 내부 로직이 reasoning_effort='minimal'을 강제 주입했고, 해당 모델은 minimal 값을 허용하지 않아 main model 호출이 실패했다. 문제를 더 헷갈리게 만든 것은 fallback 모델 o4-mini가 리뷰 댓글을 남겼다는 점이다. 겉으로는 “리뷰가 달렸다”처럼 보였지만, 실제 품질 저하와 지연은 main model 실패 + fallback 수행의 결과였다.
이 사례는 AI 도구 운영에서도 기존 플랫폼 운영 원칙이 그대로 필요하다는 메시지로 살릴 수 있다.
- 성공 여부는 UI 결과가 아니라 main/fallback 로그로 판단해야 한다.
- 모델명 교체는 API 스펙, wrapper library 파라미터 변환, 토큰 한도까지 함께 검증해야 한다.
- Pod 내부 핫픽스는 재시작 시 사라지므로 운영 해결책이 아니라 진단 단계로만 써야 한다.
마무리 및 다음 스텝
CI/CD 아키텍처의 진화를 요약하면 한 문장이다: “유연함이 곧 복잡함이다.” 모든 케이스를 하나의 파이프라인에서 처리하려다가 아무도 쓰지 않는 파이프라인을 만들었다. 선택과 집중 — 기본 케이스를 극도로 단순하게 만들고, 예외 케이스는 확장 포인트를 열어두는 것이 맞는 방향이었다.
PR-Agent는 코드 리뷰의 “1차 방어선” 역할이다. 명백한 이슈를 AI가 잡고, 인간 리뷰어는 아키텍처 판단과 비즈니스 로직 검증에 집중한다.
다음 단계로 두 가지를 계획 중이다.
첫째, Trivy 보안 스캔 통합. kaniko-build.yml로 이미지를 빌드한 직후 trivy-scan.yml로 이미지 취약점을 자동 스캔하는 단계를 추가한다. High/Critical 취약점이 있으면 파이프라인을 실패시켜 취약한 이미지가 배포되는 것을 차단한다.
둘째, Helm 차트 버전 자동화. 현재는 Chart.yaml의 버전을 수동으로 관리한다. Semantic Release나 Conventional Commits를 CI에 연동해서 커밋 메시지 기반으로 버전이 자동 증가하도록 만들 예정이다. 버전 관리의 인지 부하를 줄이고, 변경 이력이 Git 태그에 자동으로 기록되게 하는 것이 목표다.
돌아보며
- 조직의 CI/CD를 5단계 진화를 거쳐 재사용 가능한 3-Layer 플랫폼 컴포넌트(Primitives → Workflows → Consumer)로 표준화한 설계 능력 — “유연함이 곧 복잡함”이라는 기준으로 덜어내는 판단까지 포함
- AI 도구(PR-Agent + AWS Bedrock)를 데모가 아니라 운영 컴포넌트로 취급한 경험 — 모델 교체를 API 스펙, 파라미터 호환성, main/fallback 로그 검증까지 포함한 릴리스로 다룸
- 서비스 저장소가
.gitlab-ci.yml3줄로 파이프라인을 소비하게 만든 Platform-as-a-Product 관점