배치 관리 툴 구축하기

Jaeyoung·2024년 5월 12일
0
post-thumbnail

이번에 사내에서 배치 관리 툴을 구축하면서 배치 관리 툴을 구축하기 위해 어떤 요소가 필요하고 어떤 생각을 거치게 되었는지 해당 내용을 공유하고자 작성하게 되었습니다.

배치 어플리케이션이란 ?

배치 어플리케이션은 사전에 정의된 작업을 자동으로 수행하는 소프트웨어입니다. 이러한 어플리케이션은 일반적으로 사용자의 개입 없이 큰 데이터 세트를 처리하거나 복잡한 계산을 반복적으로 수행합니다. 배치 어플리케이션은 주로 데이터 처리, 파일 관리, 시스템 유지보수 작업 등에 사용됩니다.

배치 어플리케이션의 주요 특징

  • 비대화적 작업: 배치 어플리케이션은 대량의 데이터를 한 번에 처리할 수 있도록 설계되어 있으며, 처리 과정 중 사용자와의 상호작용을 필요로 하지 않습니다.
  • 자동 실행: 특정 시간에 예약된 작업을 자동으로 실행할 수 있습니다. 이는 시스템이 비교적 부하가 적은 시간에 작업을 수행하도록 계획할 수 있게 해줍니다.

배치 어플리케이션 예시

  1. 금융 보고: 매일 혹은 매월 금융 거래 데이터를 수집하고 분석하여 보고서를 생성합니다. 이러한 보고서는 경영진이 회사의 재정 상태를 평가하는 데 도움을 줍니다.
  2. 정기적인 푸시 알림 메세지 전송: 정기적으로 푸시 알림 메세지를 사용자에게 전송하는 작업을 수행합니다.
  3. 백업 작업: 데이터의 안정성을 보장하기 위해 정기적으로 데이터베이스나 파일 시스템의 백업을 수행합니다. 이 작업은 보통 시스템 사용이 적은 야간 시간대에 실행됩니다.
  4. 데이터 마이그레이션: 시스템 간에 대량의 데이터를 이동시키는 작업입니다.

왜 배치 관리 툴을 구축하게 되었을까?

기존 배치 시스템 운영 방식

하나의 EC2 인스턴스에서 crontab으로 실행하면서 관리하는 방식으로 운영했습니다.

기존 배치 시스템 문제

  • 하나의 인스턴스에서 모든 배치 Job을 스케줄링 하기 때문에 메모리와 CPU 부족 문제가 발생할 수 있어 배치 Job에 대해 항상 시간대를 겹치지 않도록 설계 해야 했습니다.
  • 한 인스턴스에서 여러 배치 Job이 실행되기 때문에 배치 관련 로그 보기가 쉽지 않았습니다.
  • 배치 Job이 실행되지 않는데도 인스턴스가 항상 구동되어야 했습니다.
  • Scale Out 하기가 쉽지 않습니다.
  • 배치 Job을 수정하거나 추가할 때 VPC 환경마다 설정해 줘야하기 때문에 여러 환경에 SSH로 접속해서 crontab을 설정하는 이슈로 관리하기도 힘들고 휴먼 에러가 발생하기 쉬운 환경이었습니다.
  • 배치 Job에서 오류가 발생했을 때 알림이 존재하지 않아 문제를 바로 해결하기 어려웠습니다.

기존 배치 시스템의 문제점을 해결하기 위한 요구사항

  • 팀원 모두가 쉽게 배치 Job 관리할 수 있어야 합니다
  • 배치 Job에 대한 로그를 쉽게 확인할 수 있어야 합니다.
  • 배치 Job에 대한 실행 이력을 쉽게 확인할 수 있어야 합니다.
  • 배치 Job이 실패한 경우 알림을 받을 수 있어야 합니다.
  • 배치 Job이 실패한 경우 수동으로 배치 Job을 실행할 수 있어야하고
    누가 수동으로 실행했는지 알 수 있어야합니다.
  • 각 배치 Job이 서로 영향을 주지않도록 독립적인 환경에서 실행될 수 있어야 합니다.
  • 배치 Job을 스케줄링으로 실행시킬 수 있어야 합니다.
  • 각 VPC 환경과 관련된 배치 Job을 한 곳에서 관리할 수 있어야합니다.
  • 배치 Job을 실행하는 환경은 유연하게 확장할 수 있어야합니다.

해당 요구사항들을 만족하기 위해 생각한 방식

AWS Batch 사용

AWS Batch로 위 요구사항들을 만족 시킬 수 있고 금액적인 부분도 절감할 수 있습니다. 하지만 AWS Cloud 방식에 대해 팀원 모두가 이해하고 있어야 관리를 할 수 있다는 부분과 또한 나중에 추가적인 특별한 요구사항이 발생하거나 Cloud 플랫폼을 변경하는 경우 AWS Batch에 종속적이기 때문에 확장에 좀 제한적일 수 있다 생각이 들었습니다.

직접 구현

빠르게 개발해서 적용해야 했기 때문에 많은 고민을 할 수 없어 해당 요구사항을 쉽게 처리할 수 있고 무료인 플랫폼이 무엇이 있을까 생각하다가 오픈소스인 Jenkins을 이용하면 좋을 것 같다는 생각을 하게 되었습니다. 이전에 Jenkins를 사용해본 경험이 있기 때문에 이런 접근을 할 수 있었습니다.

Jenkins에서 지원하는 기능

  • Build Trigger를 여러개 지원하는데 cron식을 이용해 Pipeline을 Build 할 수 있는 방식
  • 수동으로 Pipeline을 실행하고 누가 실행했는지 알 수 있음
  • Pipeline 실행 이력과 로그를 쉽게 확인 가능
  • 여러 플러그인 지원(알림, Pipeline Stage과 관련된 기능 지원 등등)
  • 무료 오픈소스
  • 무수히 많은 자료(커뮤니티 활성화)
  • Jenkins Agent로 독립된 환경에서 Pipeline 실행 가능

Pipeline으로 배치 어플리케이션을 실행시키는 방식으로 어느정도 요구사항들을 충족시킬 수 있다고 판단이 되었고 가이드라인만 잘 작성하고 Pipeline을 템플릿화 시키면 팀원들도 쉽게 관리할 수 있겠다 생각도 했습니다. 그래서 직접 구현하게 되었습니다.

Jenkins

Jenkins는 오픈 소스 자동화 서버로, 소프트웨어 개발의 지속적 통합(CI)과 지속적 배포(CD)를 지원합니다. 다양한 플러그인을 통해 빌드, 테스트 및 배포 프로세스를 자동화하여 개발의 효율성을 크게 향상시킬 수 있습니다. Jenkins는 Java로 작성되었으며, 다양한 운영 체제에서 실행될 수 있습니다. 사용자는 웹 인터페이스를 통해 작업을 구성하고, 실행 결과를 모니터링하며, 상세한 로그 정보를 확인할 수 있습니다.

Jenkins의 주요 특징

  • 플러그인 확장성: Jenkins는 1,000개가 넘는 플러그인을 지원하여, 거의 모든 유형의 CI/CD 시나리오에 맞춤화할 수 있습니다.
  • 마스터-슬레이브 아키텍처: Jenkins는 분산 환경을 지원하며, 여러 서버에서 작업을 병렬로 실행할 수 있습니다.
  • 간편한 설정: 웹 기반 설정을 통해 사용자가 쉽게 Jenkins를 구성하고 관리할 수 있습니다.

Master와 Agent 개념

Jenkins에서는 마스터-에이전트(또는 마스터-슬레이브) 아키텍처를 사용하여 작업 부하를 분산시키고, 다양한 환경에서 작업을 실행할 수 있습니다. 이 구조는 큰 규모의 프로젝트와 다수의 작업을 효율적으로 처리하기 위해 설계되었습니다.

Master:

  • Jenkins 마스터는 전체 Jenkins 인스턴스의 중앙 제어 단위입니다.
  • 마스터의 주된 역할은 Jenkins 서비스의 구성 및 관리, 작업 스케줄링, 결과 모니터링, 그리고 플러그인 관리 등입니다.
  • 마스터는 또한 웹 서버 기능을 수행하여 사용자 인터페이스를 제공하고, 사용자가 Jenkins를 웹 브라우저를 통해 관리할 수 있게 합니다.

Agent:

  • 에이전트(또는 슬레이브)는 실제 작업을 수행하는 역할을 합니다.
  • 에이전트는 다양한 운영 체제에서 실행될 수 있으며, 마스터로부터 작업을 할당받아 실행합니다.
  • 에이전트는 필요에 따라 동적으로 추가되거나 제거될 수 있으며, 이를 통해 리소스를 효율적으로 활용하고, 병렬 처리를 통해 빌드 시간을 단축할 수 있습니다.

Jenkins Master

Jenkins Master AWS EC2 템플릿 설정

EC2 템플릿을 이용하면 Jenkins Master를 구동하기 편리하게 만들어 줍니다.

AWS IAM

  • AWS S3 읽기 쓰기 권한
  • EC2를 관리하기 위한 권한
    • iam:PassRole
      • EC2 인스턴스에 IAM을 부여하기 위한 권한
    • ec2:CreateTags
      • EC2 템플릿 실행 시 TAG가 설정되어있기 때문에 필요한 권한
    • ec2:RunInstances
      • EC2 인스턴스를 실행하기 위한 권한
    • ec2:TerminateInstances
      • EC2 인스턴스를 종료하기 위한 권한

보안 그룹

  • 인바운드 규칙
    • TCP : 50000 (Jenkins Agent Connect 포트)
      • 각 VPC의 CIDR
    • TCP : 2376 (Jenkins 포트)
      • ALB IP, 개인 PC IP, 회사 IP
    • SSH : 22
      • BASTION
  • 아웃바운드 규칙
    • 모든 포트 IP 허용

EC2 템플릿 사용자 데이터

#!/bin/bash
# 도커 설치
sudo yum update -y
sudo yum install docker -y
sudo service docker start
sudo usermod -aG docker ec2-user

# 백업 파일이 존재하다면 백업 파일 S3에서 다운로드
aws s3 cp [s3 백업 경로] /home/ec2-user
tar -xzvf /home/ec2-user/jenkins-data -C /home/ec2-user
rm /home/ec2-user/jenkins-data

sudo chown ec2-user:ec2-user /home/ec2-user/jenkins-data

# 컨테이너 ID 또는 이름 설정
container_name="jenkins-docker"

# Jenkins 실행
docker run --name $container_name --restart=always --detach --privileged --volume /home/ec2-user/jenkins-data:/var/jenkins_home --publish 2376:8080 --publish 50000:50000 jenkins/jenkins:jdk17

# AWS CLI가 설치되어 있는지 확인하고, 없다면 설치
docker exec -u root $container_name which aws || docker exec -u root $container_name apt-get update && docker exec -u root $container_name apt-get install -y awscli

# rsync 설치 (백업 용도)
docker exec -u root $container_name which rsync || docker exec -u root $container_name apt-get update && docker exec -u root $container_name apt-get install -y rsync

Jenkins Agent

Pipeline Job을 별도의 분리된 환경에서 실행하기 위해서는 Jenkins Agent를 이용해야하는데 각기 다른 VPC 환경에서 실행해야하는 Agent를 어떤 방식으로 Jenkins Master와 연결할 수 있는지 알아보도록 하겠습니다.

Jenkins Agent AWS EC2 템플릿 설정

EC2 템플릿을 이용하면 Jenkins Agent를 Jenkins Master에 자동으로 연결하게 설계할 수 있고 각기 다른 VPC EC2 인스턴스를 설정할 수 있습니다. VPC 환경에 맞는 AGENT를 생성하고 Jenkins Master로 연결할 수 있습니다.

Name 리소스 태그

  • 키 : Name, 값 : ENV-BATCH-AGENT

값에 들어가는 ENV의 값은 환경별로 “STG, DEV” 이렇게 설정 해줍니다. 해당 값에서 ENV 값은 VPC 환경을 식별하기 위해 추가해주는 값으로 항상 명시 해야합니다.

AgentType 리소스 태그

  • 키 : AgentType, 값 : 일반 배치 인 경우 태그를 입력하지 않음 Worker 성 작업인 경우 ENV-worker-agent

만약 일반 배치라고 하면 AgentType를 넣어주지 않아도 됩니다. 만약 Worker성 작업인 경우 필수적으로 ENV-worker-agent 즉 worker-agent로 끝나는 값을 넣어주어야합니다. 해당 값에서 ENV 값은 VPC 환경을 식별하기 위해 추가해주는 값으로 항상 명시 해야합니다.

AWS IAM

  • AWS S3 읽기 권한
  • ec2:DescribeTags
    • EC2 인스턴스의 태그를 조회하기 위한 권한 (Jenkins Master와 연결시 필요)

보안 그룹

  • 인바운드 규칙
    • SSH : 22
      • BASTION
  • 아웃바운드 규칙
    • 모든 포트 IP 허용

사용자 데이터 스크립트

인스턴스가 실행할 때 Jenkins Agent를 실행 및 Jenkins Master에 연결하고 다른 기본적인 환경 설정을 하기 위해 필요합니다.

#!/bin/bash
# 도커 설치 및 실행
sudo yum update -y
sudo yum install docker -y
sudo service docker start

# 사용자 ec2-user를 docker 그룹에 추가하여 Docker 명령을 실행할 때마다 sudo 권한이 필요하지 않도록 하는 명령어
sudo usermod -aG docker ec2-user

# Jenkins Master에 연결하기 위한 스크립트 S3에서 가져옴
sudo aws s3 cp [s3 connect-agent-scripts 경로] /home/ec2-user/

# 어떤 환경에서 배치 Agent가 실행중인지 식별하기 위해 작성
ENV=batch

# 연결 스크립트 압축 풀기
sudo tar -xzvf /home/ec2-user/connect-batch-agent-scripts -C /home/ec2-user
sudo rm connect-batch-agent-scripts

cd /home/ec2-user/connect-agent

# jenkins_agent_start 스크립트 ENV 환경변수와 실행
sudo ENV="$ENV" sh jenkins_agent_start.sh

Connect Agent Script (JNLP)

Jenkins Agent가 Jenkins Master에 연결하기 위한 Script입니다.

S3 저장소

사용자 데이터에서 Connect Agent Script를 다운받아 실행할 수 있도록 S3 저장소에 해당 스크립트 들을 저장하도록 합니다.

Jenkins Agent Dockerfile

FROM jenkins/agent

USER root

# AWS CLI 설치, GIT 설치, rsync
RUN apt-get update && apt-get install -y \
    awscli \
    rsync \
    jq \
  && rm -rf /var/lib/apt/lists/*

USER jenkins

create_jenkins_agent.sh

해당 스크립트는 Jenkins Node를 생성하기 위한 스크립트 입니다. Jenkins Node를 생성해야 해당 Node로 Agent가 연결할 수 있기 때문에 연결하기전 필수적으로 실행해야하는 스크립트입니다.


# ENV 환경변수가 존재하는지 확인
if [ -z "$ENV" ]; then
  echo "환경 값이 설정 되지 않아 연결할 수 없습니다"
  exit
fi

# EC2의 Instance Id를 가져옴
INSTANCE_ID=$(ec2-metadata -i | awk '{print $2}')

# Jenkins Master의 Url 설정
JENKINS_URL="Jenkins Master Url"

# Jenkins Credential 정보
JENKINS_USER="user"
JENKINS_TOKEN="token"

# Node 이름은 Instance Id와 EC2-AGENT 조합으로 설정
NODE_NAME="$INSTANCE_ID-EC2-AGENT"

# Node에서 한번에 실행할 수 있는 작업의 갯수 설정 기본적으로 2로 설정
EXECUTOR_SIZE=2

# EC2의 리소스태그에서 Agent의 타입을 가져와 label 설정 (아무것도 설정하지 않았다면 batch-agent)
AGENT_TYPE=$(aws ec2 describe-tags --filters "Name=resource-id,Values=$(ec2-metadata -i | awk '{print $2}')" "Name=key,Values=AgentType" --query 'Tags[0].Value' --output text)
[[ "$AGENT_TYPE" == "None" ]] && AGENT_TYPE="batch-agent"

if [[ "$AGENT_TYPE" == *worker-agent ]]; then
   EXECUTOR_SIZE=1
fi

# AGENT를 등록하는 groovy 스크립트
ADD_AGENT_SCRIPT="import hudson.model.*
import hudson.slaves.*

String nodeName = '${NODE_NAME}'
String nodeDescription = 'A new jenkins agent node'
String remoteFS = '/home/jenkins'
int numExecutors = '${EXECUTOR_SIZE}'
String labelString = '${ENV}-${AGENT_TYPE}'
Node.Mode nodeMode = Node.Mode.NORMAL

DumbSlave newSlave = new DumbSlave(nodeName,
                                   nodeDescription,
                                   remoteFS,
                                   Integer.toString(numExecutors),
                                   nodeMode,
                                   labelString,
                                   new JNLPLauncher(),
                                   RetentionStrategy.INSTANCE,
                                   new LinkedList<>())

Jenkins.instance.addNode(newSlave)
"

curl -X POST "$JENKINS_URL/scriptText" \
     --data-urlencode "script=$ADD_AGENT_SCRIPT" \
     --user "$JENKINS_USER:$JENKINS_TOKEN"

connect_jenkins_agent.sh

해당 스크립트는 Jenkins Node에 연결하기 위한 스크립트 입니다.

# EC2의 Instance Id를 가져옴
INSTANCE_ID=$(ec2-metadata -i | awk '{print $2}')

# Jenkins Master의 ip 설정
# 만약 도메인이 ALB에 설정되어 있으면 HTTP로만 통신이 가능하기 때문에 
# 50000번 포트로 TCP 연결 할 수 없어 고정 Public IP로 연결 해야함
JENKINS_URL="Jenkins Master Url"

# Jenkins Credential 정보
JENKINS_USER="user"
JENKINS_TOKEN="token"

# Node 이름은 create_jenkins_agent.sh에서 생성했던 Node 이름으로 설정
NODE_NAME="$INSTANCE_ID-EC2-AGENT"

# Jenkins Node의 정보를 가져오기 위한 Groovy 스크립트 (Node의 비밀 키를 알아야 연결할 수 있음)
GET_AGENT_SCRIPT="import jenkins.model.Jenkins; 
import hudson.slaves.SlaveComputer; 

Jenkins jenkins = Jenkins.getInstance(); 
SlaveComputer computer = (SlaveComputer) jenkins.getComputer('$NODE_NAME'); 
if (computer != null) { computer.getJnlpMac(); } else { ''; }" 

# Jenkins Rest API 호출을 통해 Groovy 스크립트 실행 및 에이전트 비밀 키 얻기
SECRET=$(curl -s -u $JENKINS_USER:$JENKINS_TOKEN "$JENKINS_URL/scriptText" --data-urlencode "script=$GET_AGENT_SCRIPT" | grep 'Result:' | cut -d ' ' -f2)

# jenkins-agent 폴더를 마운트 하기 위해 폴더 생성 및 권한 설정
sudo mkdir -p /home/ec2-user/jenkins-agent/
sudo chown ec2-user:ec2-user /home/ec2-user/jenkins-agent

# jenkins agent 연결
echo 'Agent 연결'
docker run -d -i --rm -v /home/ec2-user/jenkins-agent:/home/jenkins/agent --name jenkins-agent jenkins/agent java -jar /usr/share/jenkins/agent.jar -url "$JENKINS_URL" -secret "$SECRET" -name "$NODE_NAME" -workDir "/home/jenkins/agent"

jenkins_agent_start.sh

해당 스크립트는 Jenkins Agent를 실행하는 스크립트로 Jenkins Node 생성, Jenkins Agent 연결, Jenkins Master가 존재하는지 체크합니다.

if [ -z "$ENV" ]; then
  echo "환경 값이 설정 되지 않아 연결할 수 없습니다"
  exit
fi

# EC2의 Instance Id를 가져옴
INSTANCE_ID=$(ec2-metadata -i | awk '{print $2}')

# Agent 이름 설정(Jenkins Node 이름)
AGENT_NAME="$INSTANCE_ID-EC2-AGENT"

# Jenkins Credential 정보
USER="user"
TOKEN="token"

# Jenkins Master의 Url 설정
JENKINS_URL="Jenkins Master Url"

# Jenkins Node의 정보를 가져옴
RESPONSE=$(curl -s --user $USER:$TOKEN "$JENKINS_URL/computer/$AGENT_NAME/api/json")

echo 'Agent 연결확인'

# Jenkins Node가 존재하는지 확인
if echo "$RESPONSE" | grep -q "offline"; then
		
		# Offline 상태인 경우에 연결을 시도합니다.
    if echo "$RESPONSE" | grep -q '"offline":false' ; then
        echo "Agent '$AGENT_NAME'이 이미 연결 되어 있습니다."
    else
        sudo docker stop jenkins
	      echo "Agent '$AGENT_NAME'이 연결되지 않았기 때문에 연결을 시도합니다."
        sudo sh connect_jenkins_agent.sh
    fi
else
    sudo docker stop jenkins-agent
    echo "Agent '$AGENT_NAME'가 존재하지 않기 때문에 Agent를 생성합니다"
    sudo sh /home/ec2-user/deploy/scripts/create_jenkins_agent.sh
    echo "Agent '$AGENT_NAME' 연결 시도중..."
    sudo sh /home/ec2-user/deploy/scripts/connect_jenkins_agent.sh
fi

Pipeline에서 Jenkins Agent 이용하기

Shared Library

Shared Library를 통해 Jenkins Pipeline에서 반복적인 함수들을 정의해서 사용할 수 있습니다.

(root)
+- src                     # Groovy source files
|   +- org
|       +- foo
|           +- Bar.groovy  # for org.foo.Bar class
+- vars
|   +- foo.groovy          # for global 'foo' variable
|   +- foo.txt             # help for 'foo' variable
+- resources               # resource files (external libraries only)
|   +- org
|       +- foo
|           +- bar.json    # static helper data for org.foo.Bar

Shared Library의 폴더구조는 위와 같습니다. 일반적으로는 Global로 사용해야하기 때문에 vars 폴더에 groovy 스크립트를 작성합니다. groovy script는 아래와 같이선언해 사용할 수 있습니다.

def call() {
    로직
}

CREATE EC2 Agent

agent_tamplate_name을 인자로 받아 EC2 인스턴스 탬플릿을 실행하고 EC2 인스턴스의 id를 반환합니다.

def call(String agent_template_name) {
    return sh(script: "aws ec2 run-instances --launch-template LaunchTemplateName='${agent_template_name}' --query 'Instances[0].InstanceId' --output text", returnStdout: true).trim()
}

Terminate EC2 Agent

EC2 인스턴스 id를 받아 해당 인스턴스를 종료합니다.

def call(String instance_id) {
    sh "aws ec2 terminate-instances --instance-ids '${instance_id}'"
}

EC2 Agent를 이용한 Pipeline

위에서 다룬 Shared Library를 활용하여 아래와 같이 Pipeline을 구성할 수 있습니다. pipeline이 종료되면 EC2 Agent도 종료해 줘야하기 때문에 post always에서 EC2 Instance를 terminate 시켜줍니다.

def instance_id = ''

pipeline {
    aagent {
        node {
            label 'master'
        }
    }
    
    stages {
        stage('start ec2-agent') {
            steps{
                script {
                    instance_id=create_ec2_agent("ECHO-BATCH-JENKINS-AGENT")
                }
            }
        }

        stage('Do Something') {
            agent { label "${instance_id}-EC2-AGENT" }
            steps {
                script {
                    sh "Do Something"
                }
            }
        }
    }

    post {
        always{
            terminate_ec2_agent("${instance_id}")
        }
    }
}

유의할 점은 배치와 관련된 작업을 실행하는 Stage에서는 항상 agent를 아래와 같이 설정해야합니다.

stage('작업') {
	agent { label "${instance_id}-EC2-AGENT" }
		steps {
			script {
				작업
			}
		}
}

Batch 실행 알림

Shard Library Code

Batch Start Send Slack

import java.text.SimpleDateFormat

def call(String environment, String appName) {

    def dateFormat = new SimpleDateFormat("MMMM dd, yyyy, 'at' hh:mm a")
    dateFormat.setTimeZone(TimeZone.getTimeZone("Asia/Seoul"))

    def currentDate = dateFormat.format(new Date())

    def jobName = env.JOB_NAME.tokenize('/').last().toUpperCase()

    def message = [
            [
                    "type": "header",
                    "text": [
                            "type": "plain_text",
                            "text": ":cool-roomba:  Start Batch (${environment}) :cool-roomba:"
                    ]
            ],
            [
                    "type": "divider"
            ],
            [
                    "type": "section",
                    "fields": [
                            [
                                    "type": "mrkdwn",
                                    "text": "*App Name:*\n${appName}"
                            ],
                            [
                                    "type": "mrkdwn",
                                    "text": "*Job Name:*\n${jobName}"
                            ],
                            [
                                    "type": "mrkdwn",
                                    "text": "*When Build Start:*\n${currentDate}"
                            ],
                            [
                                    "type": "mrkdwn",
                                    "text": "*Build Cause:*\n${currentBuild.rawBuild.getCauses()[0].getShortDescription()}"
                            ]
                    ]
            ],
            [
                    "type": "actions",
                    "elements": [
                            [
                                    "type": "button",
                                    "text": [
                                            "type": "plain_text",
                                            "text": "Job Status",
                                            "emoji": true
                                    ],
                                    "style": "primary",
                                    "url": "${env.BUILD_URL}/job/${env.JOB_NAME}/${env.BUILD_NUMBER}"
                            ]
                    ]
            ]
    ]

    slackSend(blocks: message)
}

Slack Message Image

Image1

Build Trigger 스케줄링 설정

Configure

Image2

  • 스케줄링 설정을 하려면 Build periodically를 체크해줍니다.
  • 스케줄링 설정은 아래와 같은 규칙을 가지고 있습니다. 해당 규칙은 CRON 식을 따르고 있습니다.
  • 예를 들어 만약에 15분 마다 동작해야한다고 하면 “15 * * * *” 이렇게 설정 해주면 됩니다.
  • H를 사용하여 병목 현상을 막을 수 있는데 사용법은 “H/15 * * * *” 이렇게 사용하면 되는데 해시 값을 통해 적절한 시간에 분산해서 실행 해주게 됩니다. 하지만 H는 정확한 시간에 동작하지 않을 수 있기 때문에 정확한 시간에 동작 해야하는 경우 사용하면 안됩니다. Image3

Pipeline Sample Template

def instance_id = ''
def startBuildDate = new Date()

pipeline {
    aagent {
        node {
            label 'master'
        }
    }

    environment {
        BATCH_JOB = 'BATCH_JOB'
        APP_NAME = 'batch-app'
        ENV = 'develop'
        VPC_ENV = 'DEV'
    }

    stages {

        stage('start ec2-agent') {
            steps{
                script {
                    def response = job_start_send_slack(env.ENV, env.APP_NAME)
                    threadId = response.threadId
                    instance_id=create_ec2_agent("${VPC_ENV}-BATCH-JENKINS-AGENT")
                }
            }
        }

        stage('Checkout Batch Application') {
            agent { label "${instance_id}-EC2-AGENT" }
            steps {
                script {
                    fetch_batch_application(env.ENV ,env.APP_NAME)
                }
            }
        }

        stage('Check Batch Jar') {
            agent { label "${instance_id}-EC2-AGENT" }
            steps {
                script {
                    exist_batch_jar(env.APP_NAME)
                }
            }
        }

        stage('XXXX 배치 작업') {
            agent { label "${instance_id}-EC2-AGENT" }
            steps {
                script {
                    start_batch_timestamp_job(env.BATCH_JOB, env.PROFILE)
                }
            }
        }
    }

    post {
        always{
            terminate_ec2_agent("${instance_id}")
        }
        success {
            job_success_send_slack(startBuildDate, threadId)
        }
        failure {
            job_fail_send_slack(startBuildDate, threadId)
        }
    }
}

Offline Node 제거

인스턴스를 생성하면서 매번 새 노드를 생성하기 때문에 생성한 노드를 사용하고 난 후 정리해줘야합니다.

아래 파이프라인을 실행해 Offline 노드를 제거 합니다.

pipeline {
    agent {
        node {
            label 'master'
        }
    }
    
    stages {
        stage('Remove Offline Nodes') {
            steps {
                script {
                    def jenkinsInstance = Jenkins.get()

                    // 모든 노드 중에서 오프라인 노드만 필터링
                    def offlineNodes = jenkinsInstance.nodes.findAll { node ->
                        node.toComputer().isOffline()
                    }

                    // 오프라인 노드 제거
                    for (def node : offlineNodes) {
                        removeNode(node)
                    }
                    
                }
            }
        }
    }
}

Jenkins의 스크립트 보안 정책으로 인해 이러한 종류의 스크립트는 별도의 권한을 추가해 줘야하는데 스크립트 실행하면서 아래와 같은 권한을 하나씩 추가해줘야합니다.

method hudson.model.AbstractCIBase getNodes
method hudson.model.Computer isOffline
method hudson.model.Node toComputer
staticMethod jenkins.model.Jenkins get

Backup Jenkins Data

Jenkins로 운영하다보면 주기적으로 데이터들을 백업해야하는데 아래와 같은 파이프라인으로 백업을 S3저장소로 할 수 있습니다. 백업 도중에 pipeline이 실행될 수 있기 때문에 안전한 유틸리티인 resync를 통해 tmp 폴더로 옮기고 tmp 폴더에 있는 데이터를 S3 저장소에 압축해서 저장하도록 하였습니다.

pipeline {
    agent {
        node {
            label 'master'
        }
    }

    stages {
        
        stage('Prepare Backup') {
            steps {
                script {
                    sh 'rm -rf /tmp/jenkins_backup'
                    sh 'mkdir -p /tmp/jenkins_backup'
                    // rsync를 사용하여 /var/jenkins_home의 내용을 /tmp/jenkins_backup으로 복사합니다.
                    sh 'rsync -av /var/jenkins_home/ /tmp/jenkins_backup/'
                }
            }
        }

        stage('tar jenkins_home') {
            steps {
                script {
                    // /tmp/jenkins_backup 디렉토리를 tar로 압축합니다.
                    sh 'tar -zcvf /tmp/jenkins_home_backup.tar.gz -C /tmp/jenkins_backup .'
                    sh 'ls -l /tmp/jenkins_home_backup.tar.gz'
                }
            }
        }
        
        
        stage('Backup S3') {
            steps {
                script {
                    // 생성된 tar 파일을 S3로 복사합니다.
                    sh "aws s3 cp /tmp/jenkins_home_backup.tar.gz [S3 저장소 경로]"
                }
            }
        }
    }
}

번외 - Worker Application 배포

Worker Application 같은 경우는 지속적으로 실행되기 때문에 배포시에는 기존 Application을 종료하고 배포된 Application으로 재시작 해야하기 때문에 아래와 같은 스크립트를 실행해 배포합니다.

ENV="환경 정보"

# 변수 설정
JENKINS_URL="Jenkins Master Url"
JENKINS_USER="user"
JENKINS_TOKEN="token"
SEARCH_STRING="${ENV}-worker"

worker_jobs=$(curl -s -u $JENKINS_USER:$JENKINS_TOKEN "$JENKINS_URL/api/json?tree=jobs%5Bname%5D" | jq -r ".jobs[] | select(.name | contains(\"$SEARCH_STRING\")) | .name")

if [ -z "$worker_jobs" ]; then
  echo "존재하는 Worker Job이 없습니다"
  exit
fi

for JOB_NAME in "${worker_jobs[@]}"; do
  BUILD_NUMBER=$(curl -s "$JENKINS_URL/job/$JOB_NAME/api/json?tree=lastBuild%5Bnumber%5D" --user $JENKINS_USER:$JENKINS_TOKEN | jq -r '.lastBuild.number')

  echo "$JOB_NAME : $BUILD_NUMBER 종료"
  curl -X POST "$JENKINS_URL/job/$JOB_NAME/$BUILD_NUMBER/stop" --user "$JENKINS_USER:$JENKINS_TOKEN"

  echo "$JOB_NAME 재실행"
  curl -X POST "$JENKINS_URL/job/$JOB_NAME/buildWithParameters?ENV=${ENV}" --user "$JENKINS_USER:$JENKINS_TOKEN"
done
```![](https://velog.velcdn.com/images/kjy0302014/post/f55f5f34-8fbf-4101-9f33-f4aac90c83cc/image.md)
profile
Programmer

0개의 댓글