키즈노트 CI/CD 파이프라인 진화기(4)
키즈노트 CI/CD 파이프라인 진화기: 없던 것을 만들고, 만든 것을 통합하기까지
Part 4 — 마지막 퍼즐: Backend Pipeline 구축
1. Backend를 마지막으로 한 이유
Frontend, iOS, Android 파이프라인 작업이 완료된 후에야 Backend 파이프라인 논의가 본격적으로 시작됐다.
순서가 이렇게 된 건 Backend가 가장 복잡하거나 리스크가 높아서가 아니었다. 앞선 세 플랫폼을 거치면서 쌓인 경험을 Backend에 총동원하기 위한 흐름이었다. Frontend에서 파이프라인 스크립트 작성 방식의 기준을 잡았고, Mobile에서 팀 간 협업 방식을 배웠다. Backend는 그 위에서 시작할 수 있었다.
돌이켜보면 Backend는 가장 마지막에 했기 때문에 가능했던 작업이었다.
Frontend를 먼저 하지 않았다면 파이프라인 구조를 설계할 수 없었을 것이고,
Mobile을 먼저 하지 않았다면 각 팀과 협업하며 요구사항을 조율하는 방법도 몰랐을 것이다.
Backend는 앞선 경험들을 모두 활용해야만 풀 수 있는 문제였다.
하지만 현실은 녹록지 않았다. 논의가 시작된 시점에 인프라 관련 변동 사항과 ISMS-P 대비로 Backend 개발자들 전부 바쁜 상황이었다. 여러 개발자들이 퇴사한 상태였고, 각 서비스에 대한 인수인계도 제대로 이루어지지 않은 시점이었다.
2. Backend의 특수성 — 빌드가 없다
Backend 파이프라인은 처음부터 다른 플랫폼과 결이 달랐다.
키즈노트 Backend는 Django 프레임워크 기반이었다. Django는 iOS의 Xcode 빌드나 Android의 Gradle 빌드처럼 별도의 빌드 과정이 없다. 소스 코드를 타겟 서버에 가져와 그대로 실행하는 구조다. Gunicorn으로 구동되는 systemd 서비스 형태였다.
이 특성이 파이프라인 설계 첫 번째 난관이었다.
처음에는 Frontend와 동일한 방식으로 접근했다.
Jenkins Agent에서 소스를 가져오고, 이를 타겟 서버로 전달한 뒤 서비스를 재시작하는 구조였다.
하지만 배포 테스트를 반복하면서 의문이 생겼다.
왜 굳이 타겟 서버에 이미 존재하는 소스를 두고 Jenkins가 다시 소스를 전달해야 하는 걸까?
그래서 개발자들에게 물어보기도 하고, 기존 fabfile을 분석하고, 여러 차례 배포 테스트를 진행했다.
그 과정에서 Django Backend 서비스는 Jenkins가 전달한 소스를 사용하는 것이 아니라, 타겟 서버에 클론되어 있는 프로젝트 디렉토리를 기준으로 Gunicorn 서비스가 동작하고 있다는 사실을 알게 되었다.
결국 Backend 파이프라인은 “소스를 전달하는 구조”가 아니라 “타겟 서버의 소스를 갱신하는 구조”가 되어야 했다.
기존 Frontend 방식대로 Jenkins 에이전트에서 소스를 가져와 타겟 서버로 전송하려 했더니 문제가 생겼다. 파일 접근 권한 문제로 빌드 에러가 발생했고, 불필요한 소스 전송 과정이 배포 속도를 늦췄다. 이를 해결하기 위해 배포 플로우를 다음과 같이 구성했다.
1
Git Pull (에이전트) → 소스 갱신 (타겟 서버 직접) → 소스 환경변수 치환 → 서버 프로세스 재시작 → 배포 검증
- Git Pull 단계: Jenkins 에이전트에서 레포지토리 소스를 가져온다. 이 소스는 Sentry 설정 등 에이전트 워크스페이스에서 처리할 작업에만 사용한다.
- 소스 갱신 단계: 타겟 서버에서 직접 Git pull을 실행해 소스를 갱신한다. 에이전트를 거치지 않고 타겟 서버가 직접 최신 소스를 가져오는 방식이다.
- 소스 환경변수 치환 단계: 타겟 서버의 소스에서 서비스 간 엔드포인트, 시크릿 키, Sentry 릴리스 정보 등 환경 변수를 치환한다. Vault에서 민감 정보를 가져와
sed로 치환하는 로직이 핵심이었다. - 서버 프로세스 재시작 단계: Gunicorn 서비스를 reload 또는 restart한다.
- 배포 검증 단계: 배포 완료 후 HTTP 상태 코드 또는 응답 내용으로 서비스 정상 여부를 확인한다.
3. 배포 대상의 복잡함 — parallel 도입
당시 Backend 인프라는 수백 대 규모의 VM으로 운영되고 있었다.
하나의 프로젝트 안에도 api, admin, web, worker 등 여러 애플리케이션이 존재했고, 각 애플리케이션은 다시 여러 대의 VM 뒤에서 로드밸런싱되고 있었다.
즉, 하나의 배포 요청은 단순히 서버 한 대를 배포하는 작업이 아니었다.
수십 대 이상의 서버를 동시에 갱신해야 하는 작업에 가까웠다.
처음에는 순차 배포를 고려했다.
하지만 배포 시간이 지나치게 길어졌고, 특정 서버에서 장애가 발생하면 전체 배포가 중단되는 문제도 있었다.
그래서 병렬 처리 구조를 도입하기로 했다.
단순히 배포 속도를 높이기 위한 목적만은 아니었다.
특정 서버 배포 실패가 다른 서버 배포에 영향을 주지 않도록 실패를 격리(Isolation)하는 것이 더 중요한 목표였다.
그래서 이 시점에 처음으로 Jenkins parallel을 이용한 병렬화 도입을 검토했다.
1
2
3
4
5
6
7
8
// 각 배포 타겟을 병렬로 처리
def tasks = [:]
selectTargetList.each() { targetName ->
tasks["${targetName}"] = {
// 각 타겟별 배포 로직
}
}
parallel tasks
parallel 도입으로 여러 애플리케이션을 동시에 배포할 수 있게 됐다. 하지만 스크립트 복잡도가 이때부터 본격적으로 올라가기 시작했다. 각 타겟별 서버명, 서비스명, 경로가 달랐고, 실패한 타겟만 추적해서 에러를 기록하는 로직도 필요했다. 그래서 failList와 errorDetailList를 별도로 두고 각 배포 타겟별 실패 이력을 수집하는 구조를 만들었다.
1
2
3
4
5
6
7
8
9
// 실패 추적 로직
def failList = []
def errorDetailList = []
// 각 타겟에서 예외 발생 시 failList에 추가
catch (Exception e) {
failList.add("${targetName}")
errorDetailList.add("${targetName} : ${nowStageName}")
}
4. 환경변수 치환의 난관 — 이스케이프 처리
Backend 파이프라인에서 가장 많은 시간을 쏟은 부분이 환경변수 치환이었다.
서비스 간 엔드포인트, 시크릿 키, 도메인 정보를 sed 명령어로 소스 파일에 치환하는 로직이었는데, 특수 문자가 포함된 값에서 sed가 제대로 동작하지 않는 문제가 계속 발생했다. /, ^, ., [, ], $, & 등의 문자가 sed 정규식에서 특수한 의미를 가졌기 때문이었다.
이를 해결하기 위해 sedEscape() 함수를 별도로 구현했다.
1
2
3
4
5
6
7
8
9
10
11
12
13
def sedEscape(escapeItem) {
def escapeString = ['/', '^', '.', '[', ']', '$', '&', '(', ')', '*', '{', '}', '?', '+', '|', '\\n']
def escapedStringList = []
escapeItem.toList().each() { sedItem ->
if (sedItem in escapeString) {
escapedStringList.add('\\' + sedItem)
} else {
escapedStringList.add(sedItem)
}
}
return escapedStringList.join('')
}
Vault에서 가져온 민감 정보를 이 함수로 이스케이프 처리한 뒤 sed에 넘기는 방식으로 해결했다. 단순해 보이는 문제였지만 실제로는 케이스마다 다르게 터지는 바람에 해결하는 데 상당한 시간이 걸렸다.
5. JVM 64KB 제한과 Shell Script 분리
스크립트 복잡도가 올라가면서 예상치 못한 문제가 생겼다.
JVM 메서드 크기 제한 64KB 초과였다. Groovy 코드가 컴파일될 때 하나의 메서드가 JVM의 64KB 제한을 넘어서면 빌드 자체가 실패한다. 배포해야 하는 프로젝트의 복잡도가 올라갈수록 스크립트가 길어졌고, 이 문제가 현실로 다가왔다.
해결 방법은 두 가지였다.
첫째, 긴 쉘 커맨드를 별도 .sh 파일로 분리하여 파이프라인 스크립트에서 호출하는 방식으로 전환했다. Groovy 코드 내에 인라인으로 쉘 명령어를 작성하는 대신, 쉘 스크립트 파일을 만들어 sh 명령어로 실행했다.
둘째, 이 시점부터 Shared Library 기반으로 코드를 작성하는 방향을 본격적으로 검토하기 시작했다. 공통 로직을 함수로 분리하고 재사용할 수 있는 구조가 필요하다는 게 명확해진 시점이었다. Backend 파이프라인 작업과 Shared Library 준비가 투트랙으로 진행되기 시작했다.
6. 7개 서비스, 각각 다른 출발점
파이프라인으로 전환해야 할 Backend 서비스는 총 7개였다.
| 구분 | 서비스 수 | 기존 방식 |
|---|---|---|
| Django (fabfile 사용) | 5개 | 프록시 서버 접속 후 fabfile 실행 |
| Django (fabfile 없음) | 1개 | 명령어 30개 이상 수동 입력 |
| PHP | 1개 | 별도 빌드 배포 환경 없음 |
Backend 파이프라인 구축에서 가장 어려웠던 점은 서비스마다 출발선이 달랐다는 점이었다.
어떤 서비스는 fabfile이 있었고,
어떤 서비스는 배포 담당자가 퇴사하면서 배포 절차 자체가 문서화되어 있지 않았다.
심지어 어떤 서비스는 빌드 환경과 애플리케이션 별로 각각 30개가 넘는 명령어를 사람이 그때그때마다 직접 입력해 배포하고 있었다.
같은 Backend라고 부르고 있었지만 실제로는 서비스마다 완전히 다른 문제를 해결해야 했다.
우선순위는 고통이 가장 큰 곳부터였다. 명령어 30개 이상을 수동으로 입력해 배포하던 광고 로직 담당 서비스를 가장 먼저 파이프라인으로 전환했다. 이어서 PHP 서비스, 그리고 나머지 fabfile 기반 서비스들 순으로 진행했다.
fabfile 기반 서비스는 fabfile 소스를 분석하는 것부터 시작했다. 실은 fabfile은 수년간 수많은 사람들이 구축해오면서 보완해온, 실제 운영을 견뎌온 결과물이었다.
비록 코드 품질은 좋지 않았고 수정이 필요한 부분도 많았지만, 그 안에는 운영 경험이 녹아 있었다.
당시 인수인계가 거의 이루어지지 않은 상태에서, 결국 남아 있는 것은 서버와 fabfile뿐이었다.
그래서 나에게 fabfile은 단순한 배포 스크립트가 아니라, 과거 운영자들의 의사결정이 기록된 문서에 가까웠다.
그렇기에 나는 기존 로직을 버리지 않았다.
먼저 fabfile이 실제로 무엇을 하는지 분석하는 일을 먼저 진행하였다. 그리고 그 과정을 Jenkins Pipeline으로 옮긴 뒤,그 위에 부족한 부분을 보완하는 방식을 선택했다. Jenkins Pipeline으로 옮길 때는 개발자가 프록시 서버에 접속해 fabfile을 실행하기까지의 모든 과정을 옮기는 방향으로 진행하였다.
명령어 기반 서비스와 PHP 서비스는 오히려 파이프라인으로 옮기기 쉬웠다. 이미 정해진 명령어 순서가 있었고, 그걸 그대로 스크립트로 옮기면 됐다.
7. 팀 간 협업 — 데이터로 설득하다
Backend 팀과의 협업은 Frontend, Mobile과 전혀 달랐다.
인수인계가 제대로 이루어지지 않은 상황이라 빌드 배포 로직에 대해 담당자들로부터 어떠한 답변도 기대하기 어려웠다. “바쁘다”는 답변이 전부였다. fabfile 분석이 유일한 참조 수단이었는데, 그 fabfile조차 제대로 동작하는 게 신기할 정도로 수정이 필요한 부분이 많았다.
막상 파이프라인을 완성해도 문제가 있었다. fabfile에 익숙한 개발자들에게 Jenkins는 거부감이었다. 파이프라인을 구성하는 초기 과정부터 함께 소통했던 개발자들은 자연스럽게 Jenkins를 사용했지만, 나머지는 여전히 fabfile로 배포를 진행했다.
처음에는 Jenkins 사용을 권유했다.
하지만 잘 되지 않았다. 그리고 시간이 지나며 알게 됐다.
문제는 Jenkins가 아니었다.
개발자들은 바빴다.
배포 도구를 배우는 것보다 당장 개발해야 할 기능이 더 중요했다.
결국 필요한 것은 설득이 아니라 근거였다. 이에 나는 설득 방법을 바꿨다. 권유보다 데이터였다.
fabfile로 배포했을 때와 Jenkins로 배포했을 때를 항목별로 비교해 엑셀로 정리했다. 배포 시간, 오류 발생률, 재현 가능성, 이력 추적 가능 여부 등을 객관적인 수치로 보여주면서 Jenkins로 배포했을 때 월등히 효율적이고 안정적이라는 데이터를 팀 회의와 채널을 통해 보여주었다.
그리고 나서야 그 이후부터 조금씩 Jenkins 사용 비율이 늘어나기 시작했다.
이 과정에서 배운 게 있었다. Backend 개발자들은 수없이 많은 요청에 치여 개발 외에 신경 쓸 여유가 없었다. DevOps와 어떻게 소통해야 하는지도 막막해했다. 이들을 움직이는 건 감정적 설득이 아니라 근거 있는 데이터였다.
기술적으로 더 좋은 도구를 만드는 것과 사람들이 실제로 사용하는 것은 전혀 다른 문제였다.
좋은 파이프라인을 만드는 것보다 어려운 것은 사람들이 그 파이프라인을 신뢰하게 만드는 일이었다.
기본에 충실하다
파이프라인을 설계할 때 그 어떠한 요청도 없는 상황이 오히려 막막하기도 했지만, 반대로 생각했다. 내가 만드는 파이프라인이 기준이 된다는 생각으로 기본에 충실했다.
기존 fabfile의 배포 로직을 버리지 않았다. 잘 동작하던 로직을 베이스로 하되, 보완하거나 추가해야 할 내용들을 올리는 방향으로 진행했다. 온고지신(溫故知新)이었다.
8. 전 플랫폼 파이프라인化 완료
Backend 파이프라인 작업이 마무리되면서 전 플랫폼이 Jenkins Pipeline으로 통일됐다.
| 플랫폼 | 이전 | 이후 |
|---|---|---|
| Frontend | Freestyle Job (20~30개) | Scripted Pipeline |
| iOS | Jenkins 연동 없음 | Scripted Pipeline |
| Android | Jenkins 연동 없음 | Scripted Pipeline |
| Backend | fabfile / 수동 명령어 | Scripted Pipeline |
파이프라인 스크립트는 처음부터 Git 레포에서 관리해왔다. 하지만 아직 Jenkins 잡이 레포에서 직접 스크립트를 가져오는 방식은 아니었다. 잡 설정 안에 스크립트가 복사되어 있는 형태였다.
그리고 이 시점에서 중복 코드가 눈에 띄기 시작했다.
Slack 알림 로직, 에러 처리 패턴, 타임아웃 처리 방식이 플랫폼마다 비슷하게 반복되고 있었다. 하나를 고치면 네 곳을 다 찾아서 고쳐야 하는 상황이었다. 다음 단계가 필요한 시점이었다.
9. 회고
Backend 파이프라인 작업을 통해 가장 크게 배운 것은 자동화를 하기 전에 운영 구조를 이해해야 한다는 점이었다.
당시에는 인수인계도 부족했고 문서도 거의 없었다.
결국 남아 있는 것은 fabfile과 서버뿐이었다.
그래서 코드를 읽고, 배포를 반복하고, 개발자들과 이야기하며 조각난 정보들을 하나씩 맞춰나갈 수밖에 없었다.
돌이켜보면 Backend 파이프라인 구축은 Jenkins 스크립트를 작성하는 작업이라기보다 기존 운영 방식을 이해하고 그것을 자동화 가능한 형태로 재정리하는 과정에 가까웠다.
Frontend에서는 파이프라인을 만드는 방법을 배웠고,
Mobile에서는 조직과 협업하는 방법을 배웠고,
Backend에서는 운영 환경을 해석하는 방법을 배웠다.
그리고 세 경험이 합쳐지면서 단순히 Jenkins를 다루는 사람이 아니라 운영 환경을 이해하고, 조직과 협업하며, 자동화를 설계하는 엔지니어로 한 단계 성장할 수 있었다.
그리고 그 경험은 이후 Shared Library 설계와 Git 기반 파이프라인 전환의 밑거름이 되었다.
플랫폼별 파이프라인 구축은 끝났지만, 중복 코드 문제는 여전히 남아 있었다.
다음 단계는 ‘파이프라인을 만드는 것’이 아니라 ‘파이프라인을 관리 가능한 구조로 만드는 것’이었다.
다음 글에서는 전 플랫폼 파이프라인化 완료 이후, 중복 코드를 제거하고 GitOps 초석을 다지는 과정을 다룬다. Shared Library 도입과 Git 기반 전환이 어떻게 이루어졌는지, 그리고 그 과정에서 어떤 논쟁과 결정이 있었는지를 이야기할 것이다.
본 시리즈는 총 6부로 구성됩니다.
| 파트 | 제목 |
|---|---|
| Part 1 | 시작 전야: 플랫폼마다 달랐던 배포 풍경 |
| Part 2 | 첫 번째 삽: Frontend Pipeline 만들기 |
| Part 3 | Mobile로의 확장: iOS와 Android Pipeline 구축 |
| Part 4 | 마지막 퍼즐: Backend Pipeline 구축 ← 현재 글 |
| Part 5 | 중복을 코드로: Shared Library와 Git 기반 전환 |
| Part 6 | 회고: 없던 것을 만들고 통합하기까지 |