AWS 기술 블로그
인프랩의 EC2 스팟 인스턴스를 활용한 Jenkins 기반의 CI/CD 구축 사례
인프랩은 IT 직군에 특화된 온라인 강의 플랫폼 인프런과 <채용 수수료 무료> 커리어 플랫폼인 랠릿 서비스를 운영하는 스타트업입니다. 인프런에서는 개발자를 위한 강의부터 디자이너, 데이터 분석가 등을 위한 폭넓은 학습 기회를 제공합니다. 랠릿 허브를 통해 자신의 이력서를 공유하고 커리어를 관리하는 채용 플랫폼을 운영하여 학습과 성장에 매진할 수 있도록 돕습니다.
인프랩은 SaaS 기반의 CI/CD를 사용하던 중 서비스 규모가 커지고 CI/CD 파이프라인의 개선이 필요한 상황에서 Amazon EC2 스팟 인스턴스를 활용하여 Jenkins 기반의 CI/CD 파이프라인을 구축했습니다. 그 결과, CI/CD 파이프라인 비용을 기존 대비 최대 4.5배 절약할 수 있었으며 개발자의 요구사항에 맞는 커스텀 빌드 환경을 제공할 수 있었습니다.
이번 블로그 포스팅에서는 스팟 인스턴스를 활용하여 안정적이면서도 비용 효율적인 CI/CD 파이프라인을 구축한 인프랩의 경험을 공유하고자 합니다.
스팟 인스턴스의 이해
EC2 스팟 인스턴스는 사용하지 않는 예비 EC2 용량을 통해서 온디맨드 가격보다 저렴한 비용으로 제공되는 인스턴스 입니다. 스팟 인스턴스는 큰 할인율로 EC2 인스턴스를 사용할 수 있게 해주므로 사용자는 최대 90%까지 EC2 비용을 절약할 수 있습니다.
스팟 인스턴스가 실행 중인 동안에는 온디맨드 인스턴스와 완전히 동일합니다. 스팟 인스턴스가 저렴한 비용으로 EC2 인스턴스를 사용할 수 있게 해주는 대신, AWS는 스팟 인스턴스를 회수할 수 있는 옵션을 갖습니다. AWS는 스팟 인스턴스가 회수될 가능성이 있으면 리밸런싱 권고 알림을 보내고 또한 스팟 인스턴스를 회수할 시점이 되면 2분 전에 알림을 보내서, 해당 인스턴스에서 실행 중인 워크로드를 정상적으로 종료하기 위한 작업을 실행할 수 있습니다.
스팟 인스턴스는 웹 서비스, 컨테이너, 배치 작업, CI/CD, 빅데이터 처리, 기계 학습 등 다양한 워크로드에서 활용되고 있습니다. 리밸런싱 권고 알림, 회수 2분 전 알람, 스팟 배치 점수, 스팟 할당 정책 등을 적절히 활용하면 안정적이고 비용 효율적인 워크로드 운영이 가능합니다.
Jenkins 환경의 이전
인프랩은 Node.js 애플리케이션을 구현하면서 CI/CD 파이프라인으로 CircleCI를 사용했습니다. CircleCI는 Github과의 매끄러운 연동 YAML 기반의 설정으로 쉽고 빠르게 CI/CD 환경을 구축할 수 있었습니다. 하지만 서비스의 규모가 커지고 다양한 요구사항이 생기면서, 보다 유연하고 비용 효율적인 방법을 찾으면서 자체 호스팅 방식의 Jenkins로 전환하기로 했습니다.
특히 빌드 에이전트를 이용한 처리 방식과 낮은 관리비용, 그리고 빌드 환경을 직접 수정할 수 있어 커스터마이징이 용이했기 때문에 Jenkins를 선택했습니다. Jenkins를 EC2 인스턴스에서 실행하여 필요에 따라 탄력적으로 서버 리소스를 확장할 수 있었고, 특히 EC2 스팟 인스턴스를 활용함으로써 크게 비용을 절감할 수 있었습니다.
AWS 에서의 Jenkins 구성
AWS에서 Jenkins를 구성할 때 AWS를 위한 Jenkins플러그인을 사용하면 간편하게 필요한 빌드 에이전트를 탄력적으로 확보할 수 있습니다. 다음은 Jenkins EC2 Fleet 플러그인을 사용한 예시로, 빌드 에이전트 Auto Scaling 그룹의 In Service Instances 그래프입니다. 빌드가 급증하는 시점에 확장하고, 뜸해지면 클러스터를 축소해 비용을 절약합니다.
AWS 통합을 지원하는 Jenkins 플러그인 중 Jenkins Fargate, Jenkins EC2, 그리고 Jenkins EC2 Fleet 플러그인의 특징은 다음과 같습니다.
- Jenkins Fargate 플러그인은 Amazon ECS와 AWS Fargate를 조합하여 서버리스 컨테이너를 실행시키는 플러그인 입니다. 해당 플러그인은 EC2 인스턴스를 직접 관리할 필요가 없어 운영 리소스를 절약할 수 있습니다. 하지만, 도커 이미지를 빌드할 수 없고 서버리스 컴퓨팅의 추가 비용과 약 60초 정도의 빌드 시작 지연 시간을 고려해야 합니다.
- Jenkins EC2 플러그인은 인스턴스 타입, EBS 볼륨 크기 등을 설정하여 EC2를 Jenkins Agent로 탄력적으로 운영하는 플러그인 입니다. 이 플러그인은 다양한 커스텀 옵션을 제공하며, 대기중인 인스턴스를 효율적으로 재사용할 수 있는 idle termination 옵션을 제공합니다. 그러나 이 플러그인은 Auto Scaling Group을 지원하지 않고, 스팟 인스턴스 사용 시 중단 알림이나 리밸런싱 권고 시 자동으로 대응하는 기능이 없습니다.
- Jenkins EC2 Fleet 플러그인은 EC2 Fleet 및 Auto Scaling Group을 Agent Pool로 활용할 수 있는 플러그인으로, EC2 Plugin의 모든 기능을 포함하면서도 스팟 인스턴스 중단 시 실패한 작업을 자동으로 재시작 할 수 있는 기능을 제공합니다. 또한 다양한 인스턴스 타입과 자동 스케일링 기능을 지원하여 보다 유연하고 비용 효율적인 운영이 가능합니다. Jenkins EC2 플러그인의 idle termination 옵션을 동일하게 제공합니다.
인프랩에서는 스팟 인스턴스를 효율적으로 활용하고 자동 재시작 기능을 통해 더 안정적인 CI/CD 환경을 위해 Jenkins EC2 Fleet 플러그인을 선택했습니다. 이를 통해 운영 비용을 절감하면서도 CI/CD 파이프라인의 안정성을 제고할 수 있었습니다.
EC2 Fleet 플러그인은 EC2 플러그인과 다르게 Auto Scaling 그룹을 지원합니다. 시작 템플릿(Launch Template)을 이용하면 Jenkins 설정 화면이 아닌 AWS 웹 콘솔, 그리고 Pulumi, Terraform, AWS CDK 등의 IaC 도구로 체계적인 인프라 자원 관리가 가능해집니다.
EC2 스팟 인스턴스를 사용한 Jenkins 기반의 CI/CD 구축
Jenkins 컨트롤러 인스턴스 구성
EC2 온디맨드 인스턴스 한 대 실행하여 Jenkins를 설치합니다. 이 컨트롤러 인스턴스는 에이전트를 관리하고 빌드 이력 및 설정을 관리하게 되며 Built-In Node라고 지칭하기도 합니다.
만약 컨트롤러 인스턴스가 예기치 않게 종료되는 경우 어떻게 대응해야 할까요? Jenkins 는 빌드 이력과 설정을 모두 디스크에 파일 형태로 저장하므로 디스크에 기록되는 데이터가 유실되지 않도록 잘 관리해야 합니다. 네트워크 파일 시스템(NFS)을 제공하는 AWS EFS(Elastic File System)를 이용하여 Jenkins 컨트롤러 인스턴스에 네트워크 드라이브를 마운트 할 수 있습니다.
Jenkins 컨트롤러의 JENKINS_HOME 경로를 EC2 인스턴스의 EBS 볼륨이 아닌, 해당 EFS 네트워크 드라이브 경로로 설정하면 인스턴스가 교체되는 상황에서도 빌드 이력과 설정, artifact 등을 손실하지 않고 유지할 수 있습니다.
컨트롤러를 위한 IAM Role을 생성하여 EC2 Fleet 플러그인이 Auto Scaling 그룹을 관리할 수 있는 권한을 부여합니다. 생성한 Role은 인스턴스 프로필로 등록하여 EC2 인스턴스에 권한을 부여하도록 설정합니다.
Jenkins 빌드 에이전트 리소스 구축
Auto Scaling 그룹을 생성하기 위해 인스턴스를 정의하는 시작 템플릿(Launch Template)을 생성합니다. 각 빌드 환경에서 요구하는 SDK나 도커, CRI-O 같은 컨테이너 이미지 관련 도구를 설치한 AMI를 사용합니다. EC2 Image Builder를 사용하면 이러한 이미지 빌드 과정을 자동화하고 주기적으로 보안 취약점 점검 및 최신 버전의 패키지를 설치하도록 구성할 수 있습니다.
다음은 그 예시입니다.
EC2 Image Builder: 빌드 컴포넌트
- system-tuning/1.0.0
- swap/1.0.0
- korean/1.0.0
- nodejs-18/1.0.0
- jre-17/1.0.0
- utility/1.0.0
- amazon-cloudwatch-agent-linux/1.0.1
- aws-cli-version-2-linux/1.0.4
- docker/1.0.0
- docker-compose/1.0.0
EC2 Image Builder: docker 컴포넌트
```yaml
name: Docker
description: Installs the latest version of the docker
schemaVersion: 1
phases:
- name: build
steps:
- name: InstallDocker
action: ExecuteBash
inputs:
commands:
- sudo yum install -y docker
- sudo systemctl enable docker
- sudo usermod -aG docker ec2-user # sudo 없이도 docker 명령 실행하기 위함
- name: validate
steps:
- name: ValidateDocker
action: ExecuteBash
inputs:
commands:
- if [[ $(sudo systemctl list-unit-files docker.service | wc -l) > 3
]]; then exit 0; else exit 1; fi
```
필요한 경우 docker buildx를 추가로 설치하여 멀티 아키텍처 이미지 빌드를 지원할 수 있습니다.
EC2 Image Builder를 사용하지 않아도 커스텀 AMI를 빌드할 수 있습니다. 가급적 Amazon Linux 2023, Docker 22, Java Development Kit 17 버전 이상을 설치할 것을 권장합니다. Amazon Linux 2의 경우 Docker 구버전에서 컨테이너 볼륨을 호스트와 매핑 시 딜레이가 발생하는 이슈가 있었습니다. 최근에는 Docker 25 이상의 버전을 설치할 수 있도록 개선되었으니 가급적 최신 버전의 운영체제를 이용하시길 바랍니다.
Jenkins EC2 Fleet 플러그인 설치
다음과 같은 순서로 플러그인을 설치하고 Jenkins 빌드 에이전트를 설정합니다. 이 AWS 기술 블로그는 자세한 설치 방법을 가이드하고 있습니다.
- Manage Jenkins > Manage Plugins > Available plugins 에서 EC2 Fleet Jenkins Plugin 체크 후 Install without restart 하기
- 빌드 에이전트를 위한 EC2 Key Pair, IAM Role, 시작 템플릿(Launch Template), Auto Scaling 그룹 생성
- Manage nodes and Clouds > Configure Clouds 설정
Jenkins EC2 Fleet 플러그인 설정
EC2 Fleet 플러그인 설치를 완료했다면 위에서 생성한 Auto Scaling 그룹을 선택하고 SSH 키를 등록하여 Jenkins 컨트롤러에서 빌드 에이전트에 접속할 수 있도록 설정합니다. 이어서 다음과 같은 주요 설정 항목들을 구성합니다.
Max Idle Minutes Before Scaledown
빌드 작업이 완료된 후 Agent를 즉시 종료하지 않고 지정된 시간 동안 대기시키는 옵션입니다. 이 시간을 15~30분으로 설정하면, 빌드 작업이 연속될 경우 기존 에이전트가 유지되어 빈번한 서버 시작과 종료를 방지하고, 이미 설치된 의존성(dependency) 캐시를 활용할 수 있어 빌드 시간을 크게 단축할 수 있습니다.
Minimum Spare Size
이 옵션은 예비 타이어와 유사한 개념으로, 언제든지 빌드를 시작할 수 있게 예비 에이전트를 가동하는 기능입니다. ‘Max Idle Minutes Before Scaledown’ 설정으로 지정된 시간이 경과하면 에이전트들이 종료되지만, ‘Minimum Spare Size’를 1로 설정하면 최소한 1대의 에이전트가 항상 작동하게 됩니다. ‘Minimum Cluster Size’는 최소 실행 인스턴스 수를 유지하는 반면, ‘Minimum Spare Size’는 모든 빌드 에이전트가 사용 중일 때 추가적인 예비 인스턴스를 유지하게 합니다.
Private IP
EC2 인스턴스가 Public IP 및 Private IP를 둘 다 가지고 있는 경우, Jenkins Controller 서버와 Agent 서버 간 통신에 Private IP를 우선적으로 사용하는 옵션입니다. Controller 및 Agent 서버 모두 Private Subnet에 위치하도록 하고, internet-facing ALB를 Jenkins Controller 앞에 두어 신뢰할 수 있는 출처(VPN, Github IP 등)에게만 HTTPS 웹 엔드포인트를 공개하는 것이 좋습니다.
Label
빌드 Job을 실행할 때, 어떤 유형의 인스턴스에서 실행할지 구성할 수 있도록 label을 설정할 수 있습니다. 여러 개의 Auto Scaling 그룹을 생성하여 Jenkinsfile 에서 원하는 인스턴스 타입으로 실행할 수 있도록 구성할 수 있습니다.
- 예시 1: 프로덕션 배포의 경우 온디맨드 인스턴스를 사용해 스팟 인스턴스 중단을 방지하고, Pull Request에 대한 CI/CD 파이프라인에만 스팟 인스턴스를 사용해 비용을 절약하고 안정성을 확보할 수 있습니다.
- 예시 2: arm64, amd64 등 CPU 아키텍처별로 Auto Scaling Group과 레이블을 분리하여 사용할 수 있습니다.
```Jenkinsfile
pipeline {
agent { label 'ec2-spot' } // 스팟 인스턴스만 사용하도록 지정
options {
..
```
이 외에도 다양한 인스턴스 타입을 운영해야 하는 경우 컴퓨팅 최적화, 메모리 최적화 등 각각의 인스턴스 유형에 따라 Auto Scaling 그룹을 생성하여 관리하는 방법도 있습니다.
Number of Executors
Jenkins Agent 서버 한 대에서 동시에 몇 개의 Job을 실행할지 지정하는 옵션입니다. 이 값은 yarn, pnpm 등의 node.js 패키지 매니저의 경우 동시 접근을 막기 위해 lock 파일을 생성하므로 1로 선택하는 것이 좋습니다. 짧은 실행 시간을 가진 Job의 경우 2 이상의 값을 설정하면 필요한 서버 대수를 줄여 비용을 더 절약할 수 있습니다.
위의 과정을 모두 거쳤다면, 코드 저장소마다 Jenkinsfile을 작성하여 멀티 브랜치 파이프라인을 생성합니다. 배치성 작업은 파이프라인을 생성하여 주기적으로 실행할 수도 있습니다.
Jenkinsfile에서 Docker를 이용해 독립적이고 멱등성 있는 빌드 파이프라인을 구축하려면 Jenkins 공식 문서, Using Docker with Pipeline을 참고하시길 바랍니다.
스팟 인스턴스 활용한 CI/CD 구축 전략
인프랩에서는 EC2 스팟 인스턴스를 효과적으로 활용해서 안정적인 CI/CD를 운용하기 위해서 다음과 같은 전략을 수립하고 실행했습니다.
첫째, 온디맨드 인스턴스와 스팟 인스턴스를 혼합하여 구성했습니다. EC2 Auto Scaling 그룹의 Include On-Demand Base Capacity 기능을 활용하면, 실행할 온디맨드 인스턴스의 최소 대수를 설정할 수 있습니다. 스팟 인스턴스의 수요가 급격히 높아지는 시기에도 온디맨드 인스턴스를 1~2대로 실행하면 CI/CD 파이프라인이 아예 중단되는 경우를 방지할 수 있습니다. 하지만 CI/CD 파이프라인의 경우에는 일부 지연이 발생하더라도 서비스에 미치는 영향이 적고 EC2 Fleet 플러그인에서 스팟 인스턴스 중단으로 인한 빌드 실패 시 자동으로 빌드를 재실행하므로, 해당 값을 0으로 설정해도 안정적으로 운영할 수 있습니다.
둘째, 스팟 인스턴스 중단에 대한 대응 전략을 마련했습니다. EC2 Auto Scaling 그룹의 스팟 할당 전략(Spot allocation strategies)을 Price capacity optimized를 선택하여 인스턴스 가격과 중단 위험을 적절히 조정하도록 했습니다. Capacity Rebalancing을 활성화하면 스팟 인스턴스가 중단되기 전에 새로운 인스턴스를 할당하려고 시도하므로 사용 경험을 개선할 수 있게 됩니다. 또한 Capacity Rebalancing을 활성화 해서 인스턴스의 가용성 변화를 모니터링 하고 자동으로 대응하여 가용성을 유지하도록 합니다.
셋째, 보안과 네트워크 구성에 주의를 기울였습니다. Jenkins 컨트롤러와 에이전트를 모두 VPC 프라이빗 서브넷에 배치하여 불필요한 공인 IP 비용을 절약하고 공격 위험을 최소화했습니다. 인터넷 게이트웨이 역할의 ALB를 통해 VPN, Github IP 등 신뢰할 수 있는 출처로부터의 요청만 처리하도록 설정했고, IAM 역할 및 보안 그룹 설정으로 각 인스턴스에 필요한 최소 권한만 부여했습니다.
넷째, Github과의 연동 및 업데이트 계획을 세웠습니다. Github Webhook을 등록하여 Pull Request 생성, 브랜치 병합 등의 이벤트 발생 시 Jenkins 파이프라인을 실행하도록 구성했고, Github OAuth 설정을 Github 계정으로 Jenkins에 로그인 하도록 했습니다. 또한 Branch Source Plugin을 사용하여 Github App으로 Jenkins를 등록해 별도의 토큰 발행 및 유출 우려 없이 안전하게 Jenkins와 연동했습니다. 주기적인 플러그인 및 Jenkins 버전 업데이트로 보안 취약점에 대응했습니다.
다섯째, 의존성 캐싱과 Docker 이미지 레이어 캐싱을 활용하여 빌드 시간을 단축했습니다. EC2 Fleet 플러그인의 Idle Minutes옵션을 15~30분으로 설정하여 최근 빌드에서 다운로드 한 의존성 캐시를 재사용함으로써 1분 30초가 넘는 의존성 라이브러리 설치 시간을 몇 초 단위로 줄여서 빌드 시간을 대폭 단축할 수 있었습니다. 또한 Docker 이미지 레이어 캐싱 기능을 활용하여 소스 코드 및 이미지 빌드 시간도 줄일 수 있었습니다.
이러한 전략을 통해 인프랩은 컴퓨팅 비용을 최대 4.5배까지 절감하며 안정적인 CI/CD 파이프라인을 구축함으로써 개발 효율을 높이고 있습니다.
마무리
인프랩에서는 Jenkins 빌드 에이전트를 100% 스팟 인스턴스로 활용하여 CircleCI 대비 최대 4.5배 저렴하게 CI/CD 파이프라인을 구축 및 운영하고 있습니다. 이러한 노력으로 개발부서의 요구사항을 보다 효과적으로 지원하여 CI/CD 소요시간을 크게 단축했습니다. 이는 개발 프로세스 및 배포 주기를 가속시켜 비즈니스 성장을 뒷받침하는 계기가 되었습니다.
AWS EC2 스팟 인스턴스와 함께 Jenkins를 이용하면 저렴하고 안정적으로 신뢰할 수 있는 CI/CD 파이프라인을 운영할 수 있습니다. 스팟 인스턴스 중단은 간헐적으로 발생하며, 짧은 실행 시간의 빌드 잡을 수행하는 데에 이상적입니다. Jenkins EC2 Fleet 플러그인은 스팟 인스턴스 중단으로 인한 빌드 Job 실패 시 자동으로 Job을 재실행해주므로 스팟 인스턴스의 중단으로 인한 불편함을 느끼지 못했습니다. 또한, 동일 비용의 고사양 장비를 이용하여 빌드 속도를 개선할 수 있는 합리적인 선택지가 될 것입니다.
직접 Jenkins 환경을 관리하고 운영해야 하는 오버헤드가 있지만, 실행 환경을 완벽하게 커스터마이징할 수 있어 원하는 패키지 설치, 특정 방식의 커맨드 실행(도커 컨테이너 내에서 빌드 등), 의존성 캐싱을 위한 최적화 등 다양한 이점을 누릴 수 있었습니다. 이처럼 스팟 인스턴스를 활용한 CI/CD 환경 구축을 통해 비용 절감과 더불어 개발 생산성 향상의 혜택을 거둘 수 있었습니다.
참고 문서 링크
- Jenkins EC2 Fleet 플러그인
- Amazon ECS – Amazon Linux 2023에 Docker 설치
- Amazon Elastic File System – EC2에 EFS 볼륨 마운트
- EC2 스팟 인스턴스를 이용해 Jenkins CI/CD 파이프라인 비용 최적화