[DevOps] Jenkins + GitLab + ECR + EC2 기반 CI/CD 파이프라인 구축 가이드
목적
GitLab 저장소의 stage/main 브랜치에 Push가 발생하면, Jenkins가 자동으로 Docker 이미지를 빌드하여 ECR에 푸시하고, SSM을 통해 EC2에 배포하는 CI/CD 파이프라인을 구축한다.
본 문서는 ECR(Elastic Container Registry)을 사용한 Docker 이미지 관리와 SSM(Systems Manager)을 통한 EC2 배포 방식을 다루며, 스테이징/운영 환경 분리 구성을 포함한다.
개발서버 CI/CD 구축은 이전 포스트를 참고한다.
본 문서의 모든 IP/경로/인증정보는 블로그 공개를 위해 플레이스홀더로 마스킹했다.
민감정보 마스킹 규칙
| 원본 유형 | 마스킹 |
|---|---|
| GitLab 서버 IP | <GITLAB_SERVER_IP> |
| Jenkins 서버 IP | <JENKINS_SERVER_IP> |
| AWS 계정 ID | <AWS_ACCOUNT_ID> |
| ECR 레지스트리 | <ECR_REGISTRY> |
| EC2 인스턴스 ID (스테이징) | <EC2_INSTANCE_ID_STG> |
| EC2 인스턴스 ID (운영) | <EC2_INSTANCE_ID_PRD> |
| Jenkins Credentials ID | <JENKINS_CREDENTIAL_ID> |
| AWS Credentials ID | <AWS_CREDENTIAL_ID> |
환경 정보 요약
서버 구성
| 역할 | 환경 | 브랜치 | OS | Docker |
|---|---|---|---|---|
| GitLab | - | - | - | - |
| Jenkins | - | - | Windows | Docker Desktop |
| EC2 (스테이징) | stage | stage | Amazon Linux 2023 | 20.10.25 |
| EC2 (운영) | prod | main | Amazon Linux 2023 | 20.10.25 |
ECR 레포지토리
| 환경 | 레포지토리 |
|---|---|
| 스테이징 | <ECR_REGISTRY>/insite-analysis-stg |
| 운영 | <ECR_REGISTRY>/insite-analysis-prd |
배포 경로
| 환경 | EC2 경로 |
|---|---|
| 스테이징/운영 | /home/ssm-user/jupyter/insite-analysis |
아키텍처
전체 배포 흐름
flowchart TB
subgraph DEV["개발자 환경"]
A[개발자 PC]
end
subgraph GITLAB["GitLab Server"]
B[("insite-analysis.git<br/>(stage/main 브랜치)")]
end
subgraph JENKINS["Jenkins Server"]
C["Pipeline Job<br/>(SCM 참조)"]
C1["Stage 1: Git Clone"]
C2["Stage 2: ECR Login & Build"]
C3["Stage 3: Push to ECR"]
C4["Stage 4: Deploy via SSM"]
C5["Stage 5: Cleanup"]
end
subgraph AWS["AWS Cloud"]
subgraph ECR["ECR"]
D[("insite-analysis-stg<br/>insite-analysis-prd")]
end
subgraph EC2["EC2 Instance"]
E["deploy.sh 실행"]
F["docker pull"]
subgraph MODES["실행 모드"]
G["scheduler<br/>(상시 실행, 매월 1일 02:00 배치)"]
H["api<br/>(API 서버, 향후)"]
end
end
end
A -->|"git push"| B
B -->|"Webhook"| C
C --> C1 --> C2 --> C3 --> C4 --> C5
C3 -->|"docker push"| D
C4 -->|"SSM send-command"| E
E --> F
F -->|"docker pull"| D
F -->|"docker run"| G
개발서버 vs 스테이징/운영 비교
| 항목 | 개발서버 | 스테이징/운영 |
|---|---|---|
| 이미지 저장소 | 로컬 (서버 빌드) | ECR |
| 파일 전송 | sshPublisher | - (ECR pull) |
| 배포 방식 | SSH | SSM |
| Jenkins 파이프라인 | 직접 스크립트 | SCM 참조 (groovy 파일) |
사전 준비
1. ECR 레포지토리 생성
AWS Console → ECR → Create repository
| 환경 | 레포지토리 이름 |
|---|---|
| 스테이징 | insite-analysis-stg |
| 운영 | insite-analysis-prd |
설정:
- Visibility: Private
- Image tag mutability: Mutable
- Scan on push: Disabled (선택)
2. EC2 Docker 설치
# Amazon Linux 2023 기준
sudo yum update -y
sudo yum install -y docker
sudo systemctl enable docker
sudo systemctl start docker
# 버전 확인
docker --version
# Docker version 20.10.25
3. EC2 IAM 역할 설정
EC2 인스턴스에 다음 권한이 필요하다:
AmazonEC2RoleforSSM(SSM 접근)AmazonEC2ContainerRegistryReadOnly(ECR pull)
4. Jenkins AWS Credentials 설정
Jenkins 관리 → Credentials → System → Global credentials
| 항목 | 값 |
|---|---|
| Kind | AWS Credentials |
| ID | <AWS_CREDENTIAL_ID> |
| Access Key ID | (IAM 사용자 Access Key) |
| Secret Access Key | (IAM 사용자 Secret Key) |
필요한 IAM 권한:
ecr:GetAuthorizationTokenecr:BatchCheckLayerAvailabilityecr:GetDownloadUrlForLayerecr:BatchGetImageecr:PutImageecr:InitiateLayerUploadecr:UploadLayerPartecr:CompleteLayerUploadssm:SendCommandssm:GetCommandInvocation
5. Jenkins Pipeline 저장소 설정
파이프라인 스크립트를 별도 Git 저장소에서 관리한다.
pipeline.git/
└── insite-analysis/
├── stg/
│ └── insite-analysis-stg.groovy
└── prd/
└── insite-analysis-prd.groovy
구성 절차
1. EC2 디렉토리 생성
# EC2에서 실행
mkdir -p /home/ssm-user/jupyter/insite-analysis
mkdir -p /home/ssm-user/jupyter/insite-analysis/output
mkdir -p /home/ssm-user/jupyter/insite-analysis/logs
mkdir -p /home/ssm-user/jupyter/insite-analysis/.credentials
2. deploy.sh 작성
EC2에 배포 스크립트를 생성한다. Jenkins가 SSM을 통해 이 스크립트를 실행한다.
스테이징용 deploy.sh
#!/bin/bash
set -e
DEPLOY_PATH="/home/ssm-user/jupyter/insite-analysis"
ECR_REGISTRY="<ECR_REGISTRY>"
IMAGE_NAME="insite-analysis-stg"
cd ${DEPLOY_PATH}
# ECR 로그인
aws ecr get-login-password --region ap-northeast-2 | sudo docker login --username AWS --password-stdin ${ECR_REGISTRY}
# 이미지 pull
sudo docker pull ${ECR_REGISTRY}/${IMAGE_NAME}:latest
sudo docker tag ${ECR_REGISTRY}/${IMAGE_NAME}:latest ${IMAGE_NAME}:latest
# 기존 스케줄러 컨테이너 정지 및 삭제
sudo docker stop insite-scheduler 2>/dev/null || true
sudo docker rm insite-scheduler 2>/dev/null || true
# 스케줄러 컨테이너 시작
sudo docker run -d --name insite-scheduler -e ENV=stg \
-v ${DEPLOY_PATH}/output:/app/output \
-v ${DEPLOY_PATH}/logs:/app/logs \
-v ${DEPLOY_PATH}/.credentials:/app/.credentials \
-v ~/.aws:/root/.aws:ro \
${IMAGE_NAME}:latest scheduler
# dangling 이미지 정리
sudo docker images | grep "<none>" | awk '{print $3}' | xargs -r sudo docker rmi -f || true
echo "Deploy completed: $(date)"
운영용 deploy.sh
스테이징과 동일하되 IMAGE_NAME과 ENV만 변경:
IMAGE_NAME="insite-analysis-prd"
# ...
sudo docker run -d --name insite-scheduler -e ENV=prd \
권한 부여:
chmod +x /home/ssm-user/jupyter/insite-analysis/deploy.sh
3. .credentials 배치
Google Sheets API 등 인증 파일을 배치한다.
# .credentials 폴더에 인증 파일 복사
ls /home/ssm-user/jupyter/insite-analysis/.credentials/
# credentials.json (Google Sheets API)
4. Jenkins 파이프라인 groovy 작성
Jenkins에서 Pipeline script from SCM 방식을 사용하면 파이프라인 스크립트를 Git 저장소에서 관리할 수 있다.
- SCM(Source Code Management): Git 같은 버전 관리 시스템
- 장점: 파이프라인 변경 이력 추적, 코드 리뷰 가능, 여러 프로젝트에서 재사용
groovy 파일을 별도의 pipeline.git 저장소에 저장하고, Jenkins가 빌드 시 이 파일을 참조한다.
스테이징 파이프라인 (insite-analysis-stg.groovy)
pipeline {
agent any
environment {
AWS_DEFAULT_REGION = 'ap-northeast-2'
AWS_ACCOUNT_ID = '<AWS_ACCOUNT_ID>'
ECR_REGISTRY = "${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_DEFAULT_REGION}.amazonaws.com"
IMAGE_REPO_NAME = 'insite-analysis-stg'
IMAGE_TAG = 'latest'
// Git
APP_GIT_URL = 'http://<GITLAB_SERVER_IP>/csp/insite-analysis.git'
APP_GIT_BRANCH = 'stage'
// EC2 배포 대상
EC2_INSTANCE_ID = '<EC2_INSTANCE_ID_STG>'
DEPLOY_PATH = '/home/ssm-user/jupyter/insite-analysis'
}
stages {
stage('Git Clone') {
steps {
deleteDir()
checkout([
$class: 'GitSCM',
branches: [[name: "${APP_GIT_BRANCH}"]],
extensions: [[$class: 'CleanBeforeCheckout']],
userRemoteConfigs: [[
url: APP_GIT_URL,
credentialsId: '<JENKINS_CREDENTIAL_ID>'
]]
])
}
}
stage('ECR Login & Build Docker Image') {
steps {
withCredentials([[$class: 'AmazonWebServicesCredentialsBinding', credentialsId: '<AWS_CREDENTIAL_ID>']]) {
bat "aws ecr get-login-password --region ${AWS_DEFAULT_REGION} | docker login --username AWS --password-stdin ${ECR_REGISTRY}"
bat "docker build --no-cache -t ${IMAGE_REPO_NAME}:${IMAGE_TAG} ."
}
}
}
stage('Push to ECR') {
steps {
withCredentials([[$class: 'AmazonWebServicesCredentialsBinding', credentialsId: '<AWS_CREDENTIAL_ID>']]) {
bat "docker tag ${IMAGE_REPO_NAME}:${IMAGE_TAG} ${ECR_REGISTRY}/${IMAGE_REPO_NAME}:${IMAGE_TAG}"
bat "docker push ${ECR_REGISTRY}/${IMAGE_REPO_NAME}:${IMAGE_TAG}"
}
}
}
stage('Deploy to EC2 via SSM') {
steps {
withCredentials([[$class: 'AmazonWebServicesCredentialsBinding', credentialsId: '<AWS_CREDENTIAL_ID>']]) {
script {
def commandId = bat(
script: "aws ssm send-command --instance-ids ${EC2_INSTANCE_ID} --document-name AWS-RunShellScript --parameters commands=[\"bash ${DEPLOY_PATH}/deploy.sh\"] --query Command.CommandId --output text",
returnStdout: true
).trim().readLines().last()
echo "SSM Command ID: ${commandId}"
// Wait for command completion
bat "aws ssm wait command-executed --command-id ${commandId} --instance-id ${EC2_INSTANCE_ID} || true"
}
}
}
}
stage('Cleanup Local Images') {
steps {
bat "docker rmi ${ECR_REGISTRY}/${IMAGE_REPO_NAME}:${IMAGE_TAG} || true"
bat "docker rmi ${IMAGE_REPO_NAME}:${IMAGE_TAG} || true"
}
}
}
post {
success {
echo 'Deployment completed successfully'
}
failure {
echo 'Deployment failed'
}
}
}
운영 파이프라인 (insite-analysis-prd.groovy)
스테이징과 동일하되 다음 값만 변경:
IMAGE_REPO_NAME = 'insite-analysis-prd'
APP_GIT_BRANCH = 'main'
EC2_INSTANCE_ID = '<EC2_INSTANCE_ID_PRD>'
파이프라인 Stage 설명
| Stage | 동작 |
|---|---|
| Git Clone | GitLab에서 stage/main 브랜치 체크아웃 |
| ECR Login & Build | ECR 로그인 후 Docker 이미지 빌드 |
| Push to ECR | 이미지에 태그 추가 후 ECR에 푸시 |
| Deploy to EC2 via SSM | SSM을 통해 EC2의 deploy.sh 실행 |
| Cleanup Local Images | Jenkins 서버의 로컬 이미지 정리 |
5. Jenkins 파이프라인 생성
Jenkins → 새 Item → Pipeline 선택
스테이징 파이프라인 설정
| 항목 | 값 |
|---|---|
| 이름 | insite-analysis-stg-pipeline |
| Build Triggers | Build when a change is pushed to GitLab |
| - Push Events | ☑ |
| - Opened Merge Request Events | ☑ |
| - Accepted Merge Request Events | ☑ |
| Pipeline Definition | Pipeline script from SCM |
| SCM | Git |
| Repository URL | http://<GITLAB_SERVER_IP>/csp/pipeline.git |
| Credentials | <JENKINS_CREDENTIAL_ID> |
| Branch | */main |
| Script Path | insite-analysis/stg/insite-analysis-stg.groovy |
Note: Branch
*/main은 pipeline.git 저장소의 브랜치이다. 애플리케이션 소스(insite-analysis.git)의 브랜치는 groovy 파일 내APP_GIT_BRANCH에서 지정한다.
운영 파이프라인 설정
스테이징과 동일하되:
| 항목 | 값 |
|---|---|
| 이름 | insite-analysis-prd-pipeline |
| Script Path | insite-analysis/prd/insite-analysis-prd.groovy |
6. GitLab Webhook 설정
GitLab → 프로젝트 → Settings → Webhooks
스테이징 Webhook
| 항목 | 값 |
|---|---|
| URL | http://<JENKINS_SERVER_IP>:18080/project/insite-analysis-stg-pipeline |
| Trigger | Push Events, Merge Request Events |
| Branch filter | stage |
운영 Webhook
| 항목 | 값 |
|---|---|
| URL | http://<JENKINS_SERVER_IP>:18080/project/insite-analysis-prd-pipeline |
| Trigger | Push Events, Merge Request Events |
| Branch filter | main |
동작 검증
1. 수동 빌드 테스트
Jenkins → insite-analysis-stg-pipeline → Build Now
Console Output에서 확인:
[Pipeline] stage (Git Clone)
...
[Pipeline] stage (ECR Login & Build Docker Image)
...
[Pipeline] stage (Push to ECR)
...
[Pipeline] stage (Deploy to EC2 via SSM)
SSM Command ID: abc123-def456-...
...
Deployment completed successfully
2. ECR 이미지 확인
# AWS CLI로 확인
aws ecr describe-images --repository-name insite-analysis-stg --region ap-northeast-2
또는 AWS Console → ECR → insite-analysis-stg에서 이미지 확인
3. EC2 컨테이너 확인
# EC2에서 실행
sudo docker ps | grep insite-scheduler
# 컨테이너가 실행 중인지 확인
sudo docker logs insite-scheduler
# ==================================================
# VOC Analysis Scheduler Started
# Schedule: Every 1st day of month at 02:00 KST
# ==================================================
4. 배치 수동 실행 테스트
# 스테이징
sudo docker run --rm -e ENV=stg \
-v /home/ssm-user/jupyter/insite-analysis/output:/app/output \
-v /home/ssm-user/jupyter/insite-analysis/logs:/app/logs \
-v /home/ssm-user/jupyter/insite-analysis/.credentials:/app/.credentials \
-v ~/.aws:/root/.aws:ro \
insite-analysis-stg:latest batch --help
트러블슈팅
ECR 로그인 실패
증상: no basic auth credentials
해결:
- AWS Credentials가 Jenkins에 올바르게 설정되었는지 확인
- IAM 사용자에 ECR 권한이 있는지 확인
- 토큰 만료 시 재로그인 필요 (12시간 유효)
SSM 명령 실패
증상: InvalidInstanceId 또는 타임아웃
해결:
- EC2 인스턴스에 SSM Agent가 설치/실행 중인지 확인:
sudo systemctl status amazon-ssm-agent - EC2 IAM 역할에
AmazonEC2RoleforSSM정책이 있는지 확인 - EC2가 SSM 엔드포인트에 접근 가능한지 확인 (VPC 엔드포인트 또는 인터넷 게이트웨이)
Docker pull 권한 오류
증상: denied: Your authorization token has expired
해결:
- deploy.sh에서 ECR 로그인이 먼저 실행되는지 확인
- EC2 IAM 역할에
AmazonEC2ContainerRegistryReadOnly권한 확인
배치 타임아웃
증상: HTML 생성 단계에서 타임아웃
해결:
- EC2 인스턴스 타입 업그레이드 (t3.micro → t3.medium)
- EBS 볼륨 용량 확장 (8GB → 20GB)
폰트 렌더링 오류
증상: HTML 리포트에서 한글이 깨짐
해결:
- Dockerfile에
fonts-nanum설치 확인 fc-cache -fv실행 확인
Jenkins bat vs sh
증상: sh 명령어가 인식되지 않음
해결:
- Jenkins가 Windows에서 실행되는 경우
sh대신bat사용 - Linux Jenkins의 경우
bat대신sh사용
결론
GitLab + Jenkins + ECR + EC2 기반 CI/CD 파이프라인을 구축하면 stage/main 브랜치에 Push만으로 자동 배포가 가능하다.
핵심 포인트:
- ECR: Docker 이미지를 AWS에서 중앙 관리
- SSM: SSH 없이 EC2에 명령 실행 (보안 강화)
- Pipeline script from SCM: groovy 파일을 Git으로 버전 관리
- 환경 분리: 스테이징/운영 파이프라인 분리, ENV 변수로 설정 구분
배포 순서:
develop → stage → main
(개발서버) (스테이징) (운영)
MR(Merge Request)을 통해 단계별로 머지하여 배포한다.