소프트웨어 개발의 패러다임이 급격하게 변화하고 있습니다. 과거의 거대하고 느린 '폭포수 모델' 개발 방식은 더 이상 오늘날의 시장 속도를 따라잡을 수 없습니다. 고객의 요구는 끊임없이 변하고, 경쟁은 치열하며, 새로운 기술은 하루가 멀다 하고 등장합니다. 이러한 환경에서 살아남고 번영하기 위해 기업과 개발팀은 '민첩성(Agility)'과 '속도(Velocity)'를 핵심 가치로 삼게 되었습니다. 아이디어가 코드로 변환되고, 그 코드가 실제 사용자에게 전달되는 시간을 최대한 단축하는 것이 바로 경쟁력의 척도가 된 것입니다.
이러한 변화의 중심에는 CI/CD(Continuous Integration/Continuous Delivery, Continuous Deployment)라는 개념이 자리 잡고 있습니다. CI/CD는 단순히 새로운 기술이나 도구를 도입하는 것을 넘어, 개발 문화를 근본적으로 바꾸는 철학에 가깝습니다. 코드 변경 사항을 주기적으로 통합하고, 빌드, 테스트, 배포 과정을 자동화함으로써 개발자는 오직 비즈니스 로직과 코드 품질에만 집중할 수 있게 됩니다. 수동으로 진행되던 반복적이고 오류 발생 가능성이 높은 작업들이 자동화된 파이프라인을 통해 빠르고 안정적으로 처리되면서, 전체 개발 생명주기는 놀라울 정도로 효율화됩니다.
하지만 많은 개발자와 팀이 CI/CD 도입의 필요성은 절감하면서도 어디서부터 어떻게 시작해야 할지 막막해합니다. 시장에는 수많은 CI/CD 도구가 존재하며, 각 도구의 철학과 사용법이 달라 초기 진입 장벽이 느껴지기도 합니다. 이 글에서는 CI/CD 분야에서 가장 상징적이고 강력한 두 도구, 바로 Jenkins와 GitHub Actions를 중심으로 CI/CD 파이프라인의 개념부터 실제 구축 전략, 그리고 두 도구의 심층적인 비교 분석까지 다루고자 합니다. 이 글을 통해 여러분은 더 이상 수동 배포의 굴레에서 벗어나, 안정적이고 빠른 배포 파이프라인을 직접 설계하고 운영할 수 있는 견고한 지식과 자신감을 얻게 될 것입니다.
1. CI/CD, 왜 선택이 아닌 필수인가? - 기본 개념의 재정의
CI/CD 파이프라인 구축에 앞서, 우리는 CI와 CD가 각각 무엇을 의미하며, 이들이 현대 소프트웨어 개발에서 왜 그토록 중요한지를 명확히 이해해야 합니다. 이 두 개념은 종종 함께 언급되지만, 뚜렷이 구분되는 역할과 목적을 가집니다.
1.1. 지속적 통합 (Continuous Integration, CI): 통합 지옥에서 벗어나기
지속적 통합(CI)은 여러 개발자가 작업한 코드 변경 사항을 정기적으로, 가급적이면 하루에도 여러 번 중앙 코드 저장소(예: Git Repository)에 병합(merge)하는 개발 프랙티스입니다. CI의 핵심 철학은 '통합'을 개발 주기의 마지막 단계에 몰아서 하는 '빅뱅 통합' 방식의 위험성을 제거하는 데 있습니다.
과거의 개발 방식을 떠올려 봅시다. 각 개발자는 자신만의 로컬 환경이나 별도의 기능 브랜치에서 며칠, 혹은 몇 주 동안 독립적으로 코드를 작성했습니다. 그리고 마감일이 다가오면 모든 개발자의 코드를 한꺼번에 통합하려 시도합니다. 이 과정에서 어떤 일이 벌어질까요? A 개발자가 수정한 부분이 B 개발자의 코드와 충돌하고, C 개발자가 의존하고 있던 라이브러리를 D 개발자가 다른 버전으로 변경하면서 전체 시스템이 깨지는 등, 예측 불가능한 문제들이 동시다발적으로 터져 나옵니다. 이를 '통합 지옥(Integration Hell)'이라 부릅니다.
CI는 이러한 문제를 해결하기 위해 다음과 같은 자동화된 프로세스를 도입합니다.
- 코드 커밋 (Commit): 개발자가 코드 변경 사항을 버전 관리 시스템(VCS)에 푸시(push)합니다.
- 자동화된 빌드 (Automated Build): CI 서버(Jenkins, GitHub Actions Runner 등)가 이 변경 사항을 감지하고, 즉시 소스 코드를 가져와 컴파일하거나 패키징하는 '빌드' 프로세스를 실행합니다. 이 단계에서 컴파일 오류나 의존성 문제가 즉각적으로 발견됩니다.
- 자동화된 테스트 (Automated Test): 빌드가 성공적으로 완료되면, 미리 작성된 테스트 코드(단위 테스트, 통합 테스트 등)를 자동으로 실행합니다. 이를 통해 새로운 코드 변경이 기존 기능에 예기치 않은 버그(회귀 오류)를 유발하지 않았는지 검증합니다.
이 세 가지 과정이 모두 성공적으로 통과해야만 해당 코드 변경은 '안전하다'고 간주됩니다. 만약 빌드나 테스트 단계에서 하나라도 실패하면, CI 시스템은 즉시 관련 개발자에게 알림을 보내 문제를 신속하게 해결하도록 유도합니다. 이처럼 CI는 코드 변경 사항을 작고 점진적인 단위로 자주 통합하고 검증함으로써, 통합으로 인한 리스크를 최소화하고 항상 '릴리스 가능한(releasable)' 상태의 코드베이스를 유지하도록 돕습니다. 이는 팀의 협업 효율성을 극대화하고 코드 품질을 높은 수준으로 유지하는 첫걸음입니다.
1.2. 지속적 제공 (Continuous Delivery) vs. 지속적 배포 (Continuous Deployment)
CI 단계를 성공적으로 통과한 코드는 이제 사용자에게 전달될 준비를 마쳤습니다. 여기서부터 CD의 영역이 시작됩니다. CD는 크게 지속적 제공과 지속적 배포, 두 가지 수준으로 나뉩니다.
지속적 제공 (Continuous Delivery)
지속적 제공은 CI의 결과물(빌드 아티팩트)을 실제 운영 환경(Production)에 배포할 수 있는 상태로까지 만드는 모든 과정을 자동화하는 것을 의미합니다. CI에서 빌드와 테스트가 끝난 애플리케이션은 스테이징(Staging) 환경, QA 환경 등 운영 환경과 유사하게 구성된 여러 테스트 환경에 자동으로 배포됩니다. 이곳에서 더 심층적인 테스트(E2E 테스트, 성능 테스트, 보안 테스트 등)가 이루어질 수 있습니다.
이 모든 자동화된 검증 과정을 통과한 버전은 '운영 배포 후보'가 됩니다. 지속적 제공의 핵심은, 최종 운영 환경으로의 배포 결정은 사람이 직접 내린다는 점입니다. 즉, 비즈니스 담당자나 QA 팀이 특정 시점(예: 신규 기능 론칭일, 주간 정기 배포 시간)에 버튼 하나만 클릭하면, 검증된 버전이 즉시 운영 환경에 배포될 수 있도록 모든 준비를 마쳐놓는 것입니다. 이는 배포 과정의 기술적 리스크는 자동화를 통해 제거하되, 배포 시점에 대한 비즈니스적 통제권은 유지하는 균형 잡힌 접근 방식입니다.
지속적 배포 (Continuous Deployment)
지속적 배포는 지속적 제공에서 한 걸음 더 나아간, 가장 높은 수준의 자동화 단계입니다. 이 방식에서는 CI/CD 파이프라인의 모든 자동화된 테스트를 통과한 코드 변경 사항이 사람의 개입 없이 즉시 운영 환경에 자동으로 배포됩니다. 개발자가 `main` 브랜치에 코드를 머지하는 순간, 그 코드가 몇 분 안에 실제 사용자들에게 서비스되는 것입니다.
이러한 완전 자동화는 매우 강력하지만, 그만큼 탄탄한 자동화 테스트 스위트와 모니터링 시스템, 그리고 문제가 발생했을 때 신속하게 롤백(rollback)할 수 있는 매커니즘을 전제로 합니다. Netflix, Amazon, Google과 같은 대규모 테크 기업들은 하루에도 수백, 수천 번의 배포를 이 방식으로 수행하며, 이를 통해 새로운 아이디어를 매우 빠르게 시장에 테스트하고 고객 피드백을 즉각적으로 반영합니다. 지속적 배포는 개발팀의 자신감과 자동화 역량이 최고 수준에 도달했을 때 비로소 실현 가능한 목표입니다.
결론적으로 CI/CD는 단순히 코드를 서버에 올리는 행위를 자동화하는 것을 넘어, '아이디어 → 코드 → 빌드 → 테스트 → 릴리스 → 배포 → 피드백'으로 이어지는 전체 소프트웨어 개발 생명주기를 더 빠르고, 더 안정적이며, 더 예측 가능하게 만드는 핵심적인 DevOps 문화이자 실천 방법론입니다.
2. Jenkins: 무한한 가능성의 자동화 서버
Jenkins는 CI/CD 분야를 이야기할 때 빼놓을 수 없는, 가장 오래되고 널리 사용되는 오픈소스 자동화 서버입니다. 2011년 Hudson 프로젝트에서 파생된 이래, Jenkins는 전 세계 수많은 개발팀의 빌드, 테스트, 배포 자동화의 중심축 역할을 해왔습니다. Jenkins의 가장 큰 특징이자 장점은 '유연성'과 '확장성'입니다.
2.1. Jenkins의 철학과 아키텍처
Jenkins의 핵심 철학은 '무엇이든 자동화한다(Automate anything)'입니다. 이는 방대한 플러그인(Plugin) 생태계를 통해 실현됩니다. Jenkins 코어 자체는 기본적인 자동화 파이프라인 기능만을 제공하지만, 수천 개가 넘는 플러그인을 통해 거의 모든 종류의 기술 스택, 도구, 클라우드 서비스와 연동할 수 있습니다. Git, Maven, Gradle과 같은 빌드 도구부터 Docker, Kubernetes, AWS, Azure, Google Cloud에 이르기까지, 필요한 플러그인을 설치하기만 하면 Jenkins의 능력을 무한히 확장할 수 있습니다.
Jenkins는 일반적으로 마스터-에이전트(Master-Agent) 아키텍처로 구성됩니다.
- Jenkins Master: 파이프라인의 정의를 관리하고, 빌드를 스케줄링하며, UI를 제공하는 중앙 컨트롤 타워입니다. 사용자는 웹 브라우저를 통해 마스터에 접속하여 작업을 설정하고 실행 결과를 확인합니다.
- Jenkins Agent (또는 Node, 이전에는 Slave): 실제 빌드, 테스트, 배포 작업이 실행되는 워커 머신입니다. 에이전트는 다양한 운영체제(Linux, Windows, macOS)와 환경으로 구성할 수 있으며, 이를 통해 여러 프로젝트의 다양한 빌드 요구사항을 동시에 처리할 수 있습니다. 예를 들어, iOS 앱 빌드는 macOS 에이전트에서, .NET 애플리케이션 빌드는 Windows 에이전트에서, Java 애플리케이션 빌드는 Linux 에이전트에서 실행하도록 구성할 수 있습니다. 이러한 분산 빌드 아키텍처는 Jenkins 마스터의 부하를 줄이고 전체적인 파이프라인 처리량을 높여줍니다.
2.2. Jenkins 설치 및 초기 설정 (Docker 활용)
과거에는 Jenkins를 서버에 직접 설치하는 과정이 다소 복잡했지만, 이제는 Docker를 이용해 매우 간단하게 Jenkins 환경을 구축할 수 있습니다. 다음은 공식 Jenkins LTS(Long-Term Support) 이미지를 사용하여 Docker 컨테이너로 Jenkins를 실행하는 방법입니다.
먼저, Jenkins 데이터를 영속적으로 저장하고 Docker 컨테이너 안에서 Docker 명령어를 사용할 수 있도록 볼륨과 소켓을 마운트해야 합니다. 터미널에서 아래 명령어를 실행하세요.
# Jenkins 데이터를 저장할 Docker 볼륨 생성
docker volume create jenkins_home
# Jenkins 컨테이너 실행
# -p 8080:8080 : 호스트의 8080 포트를 컨테이너의 8080 포트(Jenkins 웹 UI)에 연결
# -p 50000:50000 : Jenkins 에이전트 연결을 위한 JNLP 포트
# -v jenkins_home:/var/jenkins_home : 생성한 볼륨을 Jenkins 홈 디렉토리에 마운트
# -v /var/run/docker.sock:/var/run/docker.sock : 호스트의 Docker 소켓을 컨테이너에 마운트하여 컨테이너 내에서 Docker 명령어 사용 가능 (DooD: Docker-out-of-Docker)
docker run -d -p 8080:8080 -p 50000:50000 --name jenkins-lts -v jenkins_home:/var/jenkins_home -v /var/run/docker.sock:/var/run/docker.sock jenkins/jenkins:lts-jdk11
컨테이너가 실행되면, 웹 브라우저에서 http://<서버_IP>:8080
으로 접속합니다. 처음 접속 시 'Unlock Jenkins' 화면이 나타나며, 관리자 비밀번호를 입력하라고 요청합니다. 비밀번호는 아래 명령어를 통해 컨테이너 로그에서 확인할 수 있습니다.
docker logs jenkins-lts
로그 중간에 알파벳과 숫자로 조합된 초기 비밀번호가 출력됩니다. 이 값을 복사하여 웹 화면에 붙여넣고 'Continue'를 클릭합니다. 다음 화면에서는 플러그인 설치를 선택하게 되는데, 'Install suggested plugins'를 선택하면 Git, Pipeline, SSH 등 필수적인 플러그인들이 자동으로 설치됩니다. 마지막으로 관리자 계정을 생성하면 Jenkins 초기 설정이 완료됩니다.
2.3. Pipeline as Code: Jenkinsfile 작성하기
초기 Jenkins에서는 모든 파이프라인 작업을 웹 UI를 통해 설정했습니다(Freestyle project). 하지만 이 방식은 설정이 복잡해지고, 변경 이력을 추적하기 어려우며, 재사용성이 떨어진다는 단점이 있었습니다. 이러한 문제를 해결하기 위해 등장한 것이 바로 Pipeline as Code 개념과 이를 구현하는 Jenkinsfile입니다.
Jenkinsfile은 Groovy 언어 기반의 스크립트로, CI/CD 파이프라인의 모든 단계를 코드로 정의한 파일입니다. 이 파일을 프로젝트의 소스 코드와 함께 버전 관리 시스템에 포함시키면 다음과 같은 장점을 얻을 수 있습니다.
- 버전 관리: 파이프라인의 모든 변경 사항이 Git 커밋 이력으로 남기 때문에 추적 및 롤백이 용이합니다.
- 코드 리뷰: 파이프라인 변경 사항에 대해 동료의 코드 리뷰를 받을 수 있어 안정성이 향상됩니다.
- 재사용성: 유사한 프로젝트에서 Jenkinsfile을 재사용하거나 템플릿화하여 표준 파이프라인을 쉽게 구축할 수 있습니다.
- 내구성: Jenkins 서버에 문제가 생겨도 소스 코드에 Jenkinsfile이 남아있기 때문에 파이프라인을 신속하게 복구할 수 있습니다.
Jenkinsfile은 크게 두 가지 구문(Syntax)으로 작성할 수 있습니다.
- Declarative Pipeline: 더 현대적이고 구조화된 방식으로, 미리 정해진 구조에 따라 파이프라인을 선언적으로 정의합니다. 배우기 쉽고 가독성이 높아 대부분의 경우 권장됩니다.
- Scripted Pipeline: Groovy 스크립팅의 모든 유연성을 활용할 수 있는 방식으로, 매우 복잡하고 동적인 파이프라인을 구현할 때 사용됩니다. 더 높은 자유도를 제공하지만 작성 및 유지보수가 더 어렵습니다.
Declarative Pipeline 예제 (Node.js 애플리케이션)
다음은 간단한 Node.js 애플리케이션을 빌드하고, 테스트하며, Docker 이미지를 빌드하여 Docker Hub에 푸시하는 Declarative Pipeline 예제입니다.
// Jenkinsfile
pipeline {
// 1. 에이전트(실행 환경) 정의
// 이 파이프라인은 Docker를 사용할 수 있는 어떤 에이전트에서든 실행될 수 있음을 의미
agent any
// 2. 환경 변수 정의
// Docker Hub 사용자 이름과 이미지 이름을 변수로 설정
environment {
DOCKER_HUB_CREDENTIAL_ID = 'dockerhub-credentials' // Jenkins Credentials에 저장된 ID
DOCKER_USERNAME = 'your-dockerhub-username'
IMAGE_NAME = "${DOCKER_USERNAME}/my-node-app"
}
// 3. 파이프라인 단계(Stages) 정의
stages {
// 3-1. 소스 코드 체크아웃 단계
stage('Checkout') {
steps {
// Git 저장소에서 소스 코드를 가져옴
git 'https://github.com/your-username/my-node-app.git'
}
}
// 3-2. 의존성 설치 및 빌드 단계
stage('Build') {
steps {
script {
echo 'Installing dependencies and building...'
// Node.js 16 버전의 Docker 컨테이너 내에서 빌드 스크립트 실행
docker.image('node:16').inside {
sh 'npm install'
sh 'npm run build' // 필요 시 빌드 스크립트 실행
}
}
}
}
// 3-3. 테스트 실행 단계
stage('Test') {
steps {
script {
echo 'Running tests...'
docker.image('node:16').inside {
sh 'npm test'
}
}
}
}
// 3-4. Docker 이미지 빌드 및 푸시 단계
stage('Build & Push Docker Image') {
steps {
script {
// 빌드 번호를 태그로 사용
def imageTag = "latest"
if (env.BRANCH_NAME == 'main') {
imageTag = "v${env.BUILD_NUMBER}"
}
// Docker 이미지 빌드
def customImage = docker.build(IMAGE_NAME, ".")
// Docker Hub에 로그인
docker.withRegistry('https://registry.hub.docker.com', DOCKER_HUB_CREDENTIAL_ID) {
// 이미지에 태그 추가
customImage.push(imageTag)
if (imageTag != "latest") {
customImage.push("latest")
}
}
}
}
}
}
// 4. 파이프라인 종료 후 실행할 작업 정의
post {
// 성공 시 Slack 알림
success {
slackSend(channel: '#deploy', message: "SUCCESS: Job '${env.JOB_NAME} [${env.BUILD_NUMBER}]' (${env.BUILD_URL})")
}
// 실패 시 Slack 알림
failure {
slackSend(channel: '#deploy', message: "FAILURE: Job '${env.JOB_NAME} [${env.BUILD_NUMBER}]' (${env.BUILD_URL})")
}
}
}
이 Jenkinsfile을 프로젝트 루트 디렉토리에 추가하고, Jenkins 웹 UI에서 'New Item' -> 'Pipeline'을 선택한 후, 'Definition' 항목을 'Pipeline script from SCM'으로 변경하고 Git 저장소 URL을 입력하면, Jenkins는 해당 저장소의 Jenkinsfile을 읽어 자동으로 파이프라인을 구성하고 실행합니다.
3. GitHub Actions: 리포지토리 안의 강력한 자동화 엔진
GitHub Actions는 2018년 GitHub에 의해 발표된 CI/CD 및 워크플로우 자동화 도구입니다. GitHub Actions의 가장 큰 특징은 GitHub 리포지토리와 완벽하게 통합되어 있다는 점입니다. 개발자들은 익숙한 GitHub 환경을 벗어나지 않고도 코드 푸시, 풀 리퀘스트 생성, 이슈 등록 등 GitHub에서 발생하는 거의 모든 이벤트를 트리거로 삼아 원하는 자동화 워크플로우를 실행할 수 있습니다.
3.1. GitHub Actions의 철학과 핵심 개념
GitHub Actions의 철학은 '이벤트 기반 자동화'와 '재사용 가능한 컴포넌트'에 있습니다. Jenkins가 중앙 집중형 서버에서 파이프라인을 관리하는 방식이라면, GitHub Actions는 각 리포지토리 내의 .github/workflows
디렉토리에 위치한 YAML 파일을 통해 워크플로우를 정의하는 분산적인 접근 방식을 취합니다.
GitHub Actions를 이해하기 위해서는 몇 가지 핵심 개념을 알아야 합니다.
- Workflow (워크플로우): 하나 이상의 Job으로 구성된 자동화된 프로세스 전체를 의미합니다. 하나의 YAML 파일이 하나의 워크플로우를 정의합니다. 특정 이벤트가 발생했을 때 실행되도록 설정됩니다.
- Event (이벤트): 워크플로우 실행을 유발하는 특정 활동을 의미합니다.
push
,pull_request
,schedule
(주기적 실행),workflow_dispatch
(수동 실행) 등 다양한 이벤트를 사용할 수 있습니다. - Job (잡): 워크플로우 내에서 독립적으로 실행되는 작업 단위입니다. 여러 Step으로 구성되며, 기본적으로 병렬 실행되지만 순차 실행되도록 의존성을 설정할 수 있습니다. 각 Job은 가상 환경의 새로운 인스턴스(Runner)에서 실행됩니다.
- Runner (러너): Job을 실행하는 가상 서버입니다. GitHub가 호스팅하는 러너(GitHub-hosted runner)와 사용자가 직접 구성하는 셀프 호스팅 러너(Self-hosted runner)가 있습니다. GitHub 호스팅 러너는 Ubuntu, Windows, macOS 환경을 제공하며, 별도의 인프라 관리 없이 즉시 사용할 수 있어 매우 편리합니다.
- Step (스텝): Job 내에서 실행되는 개별적인 태스크입니다. 셸 명령어를 직접 실행하거나, 재사용 가능한 Action을 사용할 수 있습니다.
- Action (액션): 워크플로우의 가장 작은 빌딩 블록으로, 복잡한 작업을 캡슐화한 재사용 가능한 코드 단위입니다. 예를 들어
actions/checkout@v3
은 저장소의 코드를 러너로 가져오는 액션이며,actions/setup-node@v3
은 특정 버전의 Node.js를 설치하는 액션입니다. GitHub Marketplace에는 수많은 커뮤니티와 공식 액션들이 존재하여, 사용자는 이를 조합하여 손쉽게 강력한 워크플로우를 구성할 수 있습니다.
3.2. GitHub Actions Workflow 작성하기
GitHub Actions 워크플로우는 프로젝트 루트 디렉토리 아래에 .github/workflows/
폴더를 만들고, 그 안에 YAML 형식의 파일을 작성하여 정의합니다. 파일 이름은 자유롭게 지정할 수 있습니다 (예: ci.yml
, deploy.yml
).
GitHub Actions Workflow 예제 (Node.js 애플리케이션)
앞서 Jenkinsfile로 구현했던 동일한 Node.js 애플리케이션의 CI/CD 파이프라인을 GitHub Actions 워크플로우로 구현해 보겠습니다. 이를 통해 두 도구의 구성 방식 차이를 명확하게 비교할 수 있습니다.
# .github/workflows/ci-cd.yml
# 1. 워크플로우 이름 정의
name: Node.js CI/CD Pipeline
# 2. 워크플로우 트리거(이벤트) 정의
on:
push:
branches: [ "main", "develop" ] # main 또는 develop 브랜치에 push될 때 실행
pull_request:
branches: [ "main" ] # main 브랜치로의 pull request가 생성되거나 업데이트될 때 실행
# 3. 잡(Jobs) 정의
jobs:
# 3-1. 빌드 및 테스트 잡
build-and-test:
# 3-1-1. 실행 환경(러너) 지정
runs-on: ubuntu-latest
# 3-1-2. 스텝(Steps) 정의
steps:
# Step 1: 소스 코드 체크아웃
- name: Checkout repository
uses: actions/checkout@v3 # 'actions/checkout' 액션을 사용하여 코드 가져오기
# Step 2: Node.js 환경 설정
- name: Set up Node.js
uses: actions/setup-node@v3
with:
node-version: '16.x'
cache: 'npm' # npm 의존성 캐싱으로 빌드 속도 향상
# Step 3: 의존성 설치
- name: Install dependencies
run: npm ci # package-lock.json을 기반으로 정확한 버전의 의존성 설치
# Step 4: 애플리케이션 빌드 (필요 시)
- name: Build application
run: npm run build --if-present
# Step 5: 테스트 실행
- name: Run tests
run: npm test
# 3-2. Docker 이미지 빌드 및 푸시 잡
build-and-push-docker:
# 'build-and-test' 잡이 성공해야만 이 잡이 실행되도록 의존성 설정
needs: build-and-test
runs-on: ubuntu-latest
# 'main' 브랜치에 push 이벤트가 발생했을 때만 이 잡을 실행하도록 조건 설정
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
steps:
- name: Checkout repository
uses: actions/checkout@v3
# Step 1: Docker Hub 로그인
- name: Log in to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }} # GitHub Secrets에 저장된 사용자 이름
password: ${{ secrets.DOCKERHUB_TOKEN }} # GitHub Secrets에 저장된 접근 토큰
# Step 2: Docker 이미지 메타데이터 설정 (태그 생성 등)
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
# Step 3: Docker 이미지 빌드 및 푸시
- name: Build and push Docker image
uses: docker/build-push-action@v4
with:
context: .
push: true
tags: |
your-dockerhub-username/my-node-app:latest
your-dockerhub-username/my-node-app:v${{ github.run_number }} # GitHub Actions 실행 번호를 태그로 사용
이 YAML 파일을 리포지토리에 추가하면, GitHub는 자동으로 이 워크플로우를 인식하고 지정된 이벤트(push, pull_request)가 발생할 때마다 해당 잡들을 실행합니다. secrets.DOCKERHUB_USERNAME
과 같은 민감한 정보는 GitHub 리포지토리의 'Settings' > 'Secrets and variables' > 'Actions' 메뉴에서 안전하게 관리할 수 있습니다.
위 예제에서 볼 수 있듯이, GitHub Actions는 YAML의 간결하고 선언적인 문법을 통해 워크플로우를 직관적으로 정의할 수 있습니다. 또한 `uses` 키워드를 통해 Marketplace의 수많은 액션들을 레고 블록처럼 조립하여 강력한 자동화 파이프라인을 빠르게 구축할 수 있다는 것이 큰 장점입니다.
4. Jenkins vs. GitHub Actions: 무엇을 선택할 것인가?
Jenkins와 GitHub Actions는 모두 훌륭한 CI/CD 도구이지만, 서로 다른 철학과 아키텍처를 가지고 있어 특정 상황과 요구사항에 따라 유불리가 달라집니다. 어떤 도구를 선택할지는 프로젝트의 규모, 팀의 기술 성숙도, 인프라 환경, 그리고 자동화의 복잡성 등 다양한 요소를 고려하여 신중하게 결정해야 합니다.
4.1. 심층 비교 분석
평가 항목 | Jenkins | GitHub Actions |
---|---|---|
인프라 및 호스팅 | 주로 자체 호스팅(Self-hosted). 사용자가 직접 서버(On-premise 또는 Cloud VM)를 프로비저닝하고 Jenkins를 설치, 관리해야 함. 초기 설정 및 지속적인 유지보수(업데이트, 보안 패치, 백업) 비용 발생. | 기본적으로 GitHub 호스팅(GitHub-hosted). GitHub가 러너 인프라를 완전 관리형 서비스로 제공. 사용자는 인프라 걱정 없이 워크플로우 코드 작성에만 집중 가능. (필요 시 셀프 호스팅 러너도 지원) |
설정 및 구성 | Jenkinsfile (Groovy)을 통한 Pipeline as Code. Groovy 언어의 모든 기능을 사용할 수 있어 매우 복잡하고 동적인 파이프라인 로직 구현 가능. 하지만 학습 곡선이 가파르고 스크립트가 복잡해지기 쉬움. | YAML 파일을 통한 선언적 워크플로우 정의. 문법이 간단하고 구조적이어서 배우기 쉽고 가독성이 높음. 복잡한 조건부 로직이나 스크립팅에는 한계가 있을 수 있음. |
생태계 및 확장성 | 방대한 플러그인 생태계. 1,800개 이상의 플러그인을 통해 거의 모든 도구 및 서비스와 연동 가능. 오랜 역사만큼 성숙하고 검증된 플러그인이 많음. 커스터마이징의 끝판왕. | GitHub Marketplace. 빠르게 성장하는 액션 생태계. 재사용 가능한 액션을 통해 워크플로우를 쉽게 구성. GitHub 네이티브 기능(PR 코멘트, 이슈 라벨링 등)과의 연동이 매우 강력함. |
사용자 경험 (UX) | 웹 UI는 다소 오래되고 복잡하게 느껴질 수 있음 (Blue Ocean UI로 개선되었으나 여전히). 모든 설정을 UI와 파일 양쪽에서 관리해야 할 수 있어 일관성이 떨어질 수 있음. | GitHub 리포지토리 내에 완벽하게 통합된 깔끔하고 현대적인 UI. 풀 리퀘스트 화면에서 바로 CI 실행 결과를 확인하는 등 개발자 워크플로우에 최적화된 경험 제공. |
비용 | Jenkins 소프트웨어 자체는 무료(오픈소스). 하지만 서버 운영 비용, 스토리지 비용, 유지보수 인력 등 총소유비용(TCO)이 발생함. | Public 리포지토리는 무료. Private 리포지토리는 계정 플랜에 따라 무료 사용 시간(분) 제공, 초과 시 분당 과금. 사용량 기반 과금(Pay-as-you-go) 모델. |
4.2. 어떤 시나리오에 어떤 도구를 선택해야 할까?
Jenkins가 더 적합한 경우:
- 고도의 커스터마이징이 필요한 복잡한 파이프라인: 사내 독점 시스템과의 연동, 매우 특수한 빌드 환경 요구 등 표준화된 워크플로우로 해결하기 어려운 복잡한 요구사항이 있을 때 Jenkins의 유연성과 Groovy 스크립팅 능력은 빛을 발합니다.
- 보안 및 규제가 엄격한 환경: 소스 코드나 빌드 아티팩트를 외부 서비스에 둘 수 없는 금융, 공공, 의료 분야 등에서는 방화벽 내부에 Jenkins 서버를 직접 구축(On-premise)하여 완벽한 통제권을 확보하는 것이 필수적입니다.
- 다양한 기술 스택을 가진 대규모 조직: 여러 팀이 Java, .NET, Python, C++ 등 다양한 언어와 프레임워크를 사용하는 대기업 환경에서, Jenkins는 중앙 집중형 CI/CD 플랫폼으로서 표준화된 파이프라인 템플릿을 제공하고 다양한 에이전트를 통해 모든 빌드 요구를 수용할 수 있습니다.
- 기존 인프라와 깊은 통합이 필요할 때: 이미 자체적인 VM 프로비저닝 시스템, 스토리지, 모니터링 도구 등을 갖추고 있는 경우, Jenkins 플러그인을 통해 이러한 기존 인프라와 긴밀하게 통합하는 것이 더 효율적일 수 있습니다.
GitHub Actions가 더 적합한 경우:
- GitHub를 중심으로 개발하는 프로젝트: 소스 코드 관리를 위해 GitHub를 이미 사용하고 있다면, GitHub Actions는 가장 자연스럽고 마찰이 적은 선택입니다. PR 생성 시 자동 테스트, 이슈 라벨링 자동화 등 GitHub 생태계와의 시너지가 극대화됩니다.
- 빠른 시작과 낮은 유지보수 비용을 원할 때: 인프라 관리에 드는 시간과 노력을 최소화하고 싶은 스타트업이나 소규모 팀에게 GitHub 호스팅 러너는 매우 매력적인 옵션입니다. 별도의 서버 설정 없이 YAML 파일 작성만으로 몇 분 안에 CI 파이프라인을 가동할 수 있습니다.
- 오픈소스 프로젝트: Public 리포지토리에서 무료로 사용할 수 있다는 점은 오픈소스 프로젝트에 매우 큰 이점입니다. 커뮤니티의 기여자(Contributor)가 보낸 풀 리퀘스트를 자동으로 검증하는 데 이상적입니다.
- 클라우드 네이티브 및 서버리스 애플리케이션: Docker 컨테이너를 빌드하여 클라우드 컨테이너 레지스트리에 푸시하거나, 서버리스 함수를 배포하는 등 현대적인 클라우드 기반 애플리케이션의 CI/CD 파이프라인을 구성하는 데 매우 효율적입니다.
결론적으로, 선택은 '어느 것이 더 좋다'의 문제가 아니라 '우리의 상황에 무엇이 더 적합한가'의 문제입니다. Jenkins는 강력한 커스터마이징과 통제력을 제공하는 '설치형 전문가용 공구 세트'에, GitHub Actions는 사용하기 쉽고 GitHub와 완벽하게 통합된 '올인원 스마트 솔루션'에 비유할 수 있습니다.
5. 고급 주제 및 모범 사례
기본적인 CI/CD 파이프라인을 구축했다면, 이제는 파이프라인을 더욱 견고하고 효율적이며 안전하게 만들기 위한 고급 전략들을 고려해야 합니다.
5.1. 파이프라인 보안 (DevSecOps)
DevSecOps는 'Development', 'Security', 'Operations'의 합성어로, 개발 생명주기의 모든 단계에 보안을 통합하는 문화를 의미합니다. CI/CD 파이프라인은 보안을 자동화하기에 가장 이상적인 장소입니다.
- 정적 애플리케이션 보안 테스트 (SAST): SonarQube, Snyk Code, Checkmarx와 같은 도구를 파이프라인에 통합하여, 코드를 컴파일하거나 실행하지 않고도 소스 코드 자체의 보안 취약점을 스캔합니다. 예를 들어, PR이 생성될 때마다 SAST 스캔을 실행하여 새로운 취약점이 유입되는 것을 사전에 차단할 수 있습니다.
- 소프트웨어 구성 분석 (SCA): 애플리케이션이 사용하는 오픈소스 라이브러리 및 의존성의 알려진 취약점을 스캔합니다. GitHub의 Dependabot, Snyk Open Source, OWASP Dependency-Check 등의 도구를 사용하여 오래되거나 취약한 라이브러리를 자동으로 탐지하고 업데이트를 제안받을 수 있습니다.
- 동적 애플리케이션 보안 테스트 (DAST): 실행 중인 애플리케이션에 대해 외부 공격자의 관점에서 웹 취약점(예: SQL 인젝션, XSS)을 스캔합니다. 주로 스테이징 환경에 배포된 후에 실행됩니다.
- 비밀 정보 관리 (Secret Management): API 키, 데이터베이스 암호, SSH 키와 같은 민감 정보를 절대로 소스 코드나 Jenkinsfile에 하드코딩해서는 안 됩니다. Jenkins Credentials, GitHub Secrets, 또는 HashiCorp Vault와 같은 전문 시크릿 관리 도구를 사용하여 안전하게 저장하고, 파이프라인 실행 시에만 동적으로 주입하여 사용해야 합니다.
5.2. 배포 전략 자동화
CD 파이프라인의 마지막 단계인 배포는 서비스 중단을 최소화하고 리스크를 관리하기 위해 다양한 전략을 사용할 수 있습니다. 이러한 전략들 역시 파이프라인을 통해 자동화할 수 있습니다.
- 블루-그린 배포 (Blue-Green Deployment): 현재 운영 중인 환경(Blue)과 동일한 구성의 새로운 환경(Green)을 준비하고, Green 환경에 신규 버전을 배포합니다. 모든 테스트가 완료되면 라우터(로드 밸런서)의 트래픽을 한 번에 Blue에서 Green으로 전환합니다. 문제가 발생하면 즉시 라우터를 다시 Blue로 돌려 신속하게 롤백할 수 있습니다.
- 카나리 배포 (Canary Deployment): 신규 버전을 아주 일부 사용자(예: 전체 트래픽의 1%)에게만 먼저 공개하고, 에러율이나 성능 지표를 모니터링합니다. 안정성이 확인되면 점진적으로 더 많은 사용자에게 트래픽을 확대해 나가는 방식입니다. 리스크를 최소화하며 신규 버전의 실제 운영 환경 성능을 검증할 수 있습니다.
5.3. 파이프라인 최적화
프로젝트가 커질수록 파이프라인 실행 시간도 길어집니다. 개발자의 피드백 루프를 단축하기 위해 파이프라인을 최적화하는 것은 중요합니다.
- 캐싱(Caching): `npm install`, `mvn dependency:go-offline` 등 의존성을 다운로드하는 단계는 시간이 많이 걸립니다. Jenkins의 `cache` 스텝이나 GitHub Actions의 `actions/cache`를 사용하여 다운로드한 의존성을 캐싱하고, 다음 실행 시 재사용하여 빌드 시간을 크게 단축할 수 있습니다.
- 병렬 실행(Parallel Execution): 서로 의존성이 없는 테스트들(예: 프론트엔드 E2E 테스트와 백엔드 통합 테스트)은 Jenkins의 `parallel` 블록이나 GitHub Actions의 `matrix` 전략을 사용하여 동시에 실행함으로써 전체 테스트 시간을 줄일 수 있습니다.
- 최적화된 Docker 이미지: 멀티-스테이지 빌드(Multi-stage build)를 활용하여 최종 배포 이미지에는 빌드에만 필요했던 도구나 소스 코드를 포함하지 않고, 실행에 필요한 최소한의 런타임과 아티팩트만 포함시켜 이미지 크기를 줄이고 보안을 강화할 수 있습니다.
결론: 자동화를 넘어 개발 문화의 혁신으로
지금까지 우리는 CI/CD의 핵심 개념부터 Jenkins와 GitHub Actions라는 강력한 도구를 활용한 파이프라인 구축 방법, 그리고 두 도구의 심층적인 비교 분석에 이르기까지 긴 여정을 함께했습니다. CI/CD 파이프라인을 구축하는 것은 단순히 반복적인 작업을 자동화하여 시간을 절약하는 것 이상의 의미를 가집니다.
잘 설계된 CI/CD 파이프라인은 다음과 같은 근본적인 변화를 가져옵니다.
- 개발자 생산성 향상: 개발자는 배포와 인프라 걱정에서 벗어나 오롯이 코드 작성과 문제 해결에 집중할 수 있습니다.
- 품질 향상 및 리스크 감소: 모든 코드 변경이 자동화된 테스트를 거치므로 버그가 조기에 발견되고, 배포 과정의 인적 실수가 원천적으로 차단되어 서비스 안정성이 높아집니다.
- 빠른 시장 출시(Time to Market) 단축: 아이디어가 실제 제품으로 구현되어 사용자에게 전달되기까지의 리드 타임이 획기적으로 줄어들어 비즈니스 민첩성이 극대화됩니다.
- 자신감 있는 개발 문화 조성: 개발자들은 자신의 변경 사항이 전체 시스템을 망가뜨릴지 모른다는 두려움 없이, 자신감을 갖고 더 자주, 더 작은 단위로 코드를 개선하고 배포할 수 있게 됩니다.
Jenkins와 GitHub Actions 중 어떤 도구를 선택할지는 여러분의 팀과 프로젝트가 처한 고유한 상황에 따라 달라질 것입니다. 중요한 것은 도구 그 자체가 아니라, CI/CD라는 철학을 이해하고 그것을 팀의 문화로 정착시키려는 노력입니다. 작은 프로젝트부터 시작하여 점진적으로 파이프라인을 개선하고 고도화해 나가십시오. 처음에는 간단한 빌드와 테스트 자동화로 시작하더라도, 그 작은 한 걸음이 여러분의 팀을 더 빠르고, 더 강하고, 더 혁신적인 조직으로 이끄는 위대한 여정의 시작이 될 것입니다.