Jenkins Github Nomad 를 이용한 CI/CD 시스템 구축
nomad 란?
목표
개발자가 Github
에 소스를 Push 하면,
Webhook 을 이용해 Jenkins
에 노티합니다.
Jenkins
가 소스를 가져와 컴파일/패키징/Dockerize 해서,
AWS ECR
에 배포합니다.
그리고, Nomad Server(Host)
에 소스가 바뀌었음을 노티합니다.
Client(Worker)
는 주기적으로 Nomad Server(Host) 에 접속해서,
새로운 이미지가 있는지 확인하고,
새로운 이미지가 있으면 이미지를 가져와 실행합니다.
설치
Jenkins 설치
인스턴스 생성
Amazon Linux 2 AMI
로 인스턴스를 생성합니다.
메모리를 2G 로 합니다.
Jenkins/Docker/Git 설치
Jenkins 를 설치합니다.
sudo yum update -y
sudo wget -O /etc/yum.repos.d/jenkins.repo \
https://pkg.jenkins.io/redhat-stable/jenkins.repo
sudo rpm --import https://pkg.jenkins.io/redhat-stable/jenkins.io.key
sudo yum upgrade
sudo yum install jenkins java-1.8.0-openjdk-devel -y
sudo systemctl daemon-reload
sudo systemctl start jenkins
sudo systemctl status jenkins
Docker 를 설치합니다. 도커 그룹에 jenkins 를 추가해 줍니다.
sudo yum install docker -y
docker -v
sudo service docker start
sudo systemctl enable docker.service
sudo usermod -aG docker jenkins
# 젠킨스를 재실행합니다.
sudo systemctl restart jenkins
Git 을 설치합니다.
sudo yum install git -y
git version
자바 버전을 11 로 올립니다.
sudo yum install java-11-amazon-corretto-headless -y
sudo alternatives --config java
java -version
nomad 를 설치합니다.
wget https://releases.hashicorp.com/nomad/1.1.3/nomad_1.1.3_linux_amd64.zip
unzip nomad_1.1.3_linux_amd64.zip
sudo chown root:root nomad
sudo chmod 777 nomad
sudo mv nomad /usr/local/bin/
nomad -version
Nomad v1.1.3 (8c0c8140997329136971e66e4c2337dfcf932692)
방화벽 설정
내 아이피부터의 모든 인바운드 액세스를 허용하는 보안그룹을 생성합니다.
인스턴스에 생성한 보안그룹을 추가해 줍니다.
비밀번호 입력
http://<인스턴스 퍼블릭 아이피>:8080
에 접속합니다.
아래 명령으로 확인되는 비밀번호를 입력해 줍니다.
sudo cat /var/lib/jenkins/secrets/initialAdminPassword
로그인 후
Install suggested plugins
을 눌러줍니다.
새 관리자 계정을 생성하고, 디폴트 관리자 계정을 삭제해 주는게 권장됩니다.
Jenkins URL
젠킨스 플러그인 설치
젠킨스 메인화면
> Jenkins 관리
> 플러그인 관리
로 이동합니다.
Docker Pipeline
, Amazon ECR
, GitHub Integration
플러그인을 젠킨스에 설치해 줍니다.
Nomad Server(Host) 설치
인스턴스 생성
Amazon Linux 2 AMI
로 인스턴스를 생성합니다.
메모리를 1G 로 합니다.
바이너리 설치
wget https://releases.hashicorp.com/nomad/1.1.3/nomad_1.1.3_linux_amd64.zip
unzip nomad_1.1.3_linux_amd64.zip
sudo chown root:root nomad
sudo chmod 777 nomad
sudo mv nomad /usr/local/bin/
nomad -version
Nomad v1.1.3 (8c0c8140997329136971e66e4c2337dfcf932692)
설정파일 생성
sudo mkdir /etc/nomad.d
sudo vi /etc/nomad.d/server.hcl
---------------------------
datacenter = "dc1"
data_dir = "/var/lib/nomad/"
bind_addr = "0.0.0.0"
server {
enabled = true
bootstrap_expect = 1
}
---------------------------
서비스에 등록
sudo vi /lib/systemd/system/nomad.service
---------------------------
[Unit]
Description=Nomad
Documentation=https://nomadproject.io/docs/
Wants=network-online.target
After=network-online.target
# When using Nomad with Consul it is not necessary to start Consul first. These
# lines start Consul before Nomad as an optimization to avoid Nomad logging
# that Consul is unavailable at startup.
#Wants=consul.service
#After=consul.service
[Service]
ExecReload=/bin/kill -HUP $MAINPID
ExecStart=/usr/local/bin/nomad agent -config /etc/nomad.d
KillMode=process
KillSignal=SIGINT
LimitNOFILE=65536
LimitNPROC=infinity
Restart=on-failure
RestartSec=2
StartLimitBurst=3
StartLimitIntervalSec=10
TasksMax=infinity
OOMScoreAdjust=-1000
[Install]
WantedBy=multi-user.target
---------------------------
sudo systemctl daemon-reload
sudo systemctl enable nomad
sudo systemctl start nomad
sudo systemctl status nomad
Nomad Client(Worker) 설치
인스턴스 생성
Amazon Linux 2 AMI
로 인스턴스를 생성합니다.
메모리를 2G 로 합니다.
바이너리 설치
wget https://releases.hashicorp.com/nomad/1.1.3/nomad_1.1.3_linux_amd64.zip
unzip nomad_1.1.3_linux_amd64.zip
sudo chown root:root nomad
sudo chmod 777 nomad
sudo mv nomad /usr/local/bin/
nomad -version
Nomad v1.1.3 (8c0c8140997329136971e66e4c2337dfcf932692)
Docker 를 설치합니다.
sudo yum install docker -y
docker -v
sudo service docker start
sudo systemctl enable docker.service
설정파일 생성
sudo mkdir /etc/nomad.d
sudo vi /etc/nomad.d/client.hcl
---------------------------
# bind_addr = "127.0.0.1"
datacenter = "dc1" # 클러스터명
data_dir = "/var/lib/nomad/"
client {
enabled = true
servers = ["XXX.XXX.XXX.XXX"] # 노마드 서버 프라이빗 아이피
}
---------------------------
서비스에 등록
$HOME
변수를 설정해야 합니다.
sudo vi /lib/systemd/system/nomad.service
---------------------------
[Unit]
Description=Nomad
Documentation=https://nomadproject.io/docs/
Wants=network-online.target
After=network-online.target
# When using Nomad with Consul it is not necessary to start Consul first. These
# lines start Consul before Nomad as an optimization to avoid Nomad logging
# that Consul is unavailable at startup.
#Wants=consul.service
#After=consul.service
[Service]
ExecReload=/bin/kill -HUP $MAINPID
ExecStart=/usr/local/bin/nomad agent -config /etc/nomad.d
KillMode=process
KillSignal=SIGINT
LimitNOFILE=65536
LimitNPROC=infinity
Restart=on-failure
RestartSec=2
StartLimitBurst=3
StartLimitIntervalSec=10
TasksMax=infinity
OOMScoreAdjust=-1000
Environment="HOME=/root"
[Install]
WantedBy=multi-user.target
---------------------------
sudo systemctl daemon-reload
sudo systemctl enable nomad
sudo systemctl start nomad
sudo systemctl status nomad
보안그룹 설정
systemctl status nomad
Aug 05 05:16:21 ip-172-31-6-247.ap-northeast-2.compute.internal nomad[3351]: 2021-08-05T05:16:21.007Z [ERROR] client: error registering: error="rpc error: failed to get conn: dial tcp 172.31.14.210:4647: i/o timeout"
방화벽이 열려있지 않아 접속에 실패합니다.
AWS 보안그룹 allow-nomad
를 생성합니다.
규칙은 추가하지 않아도 됩니다.
AWS 보안그룹 protect-nomad
를 생성합니다.
인바인드 규칙에 allow-nomad
보안그룹에서의 모든 트래픽을 허용해 줍니다.
서버에는 protect-nomad
를, 클라이언트에는 allow-nomad
를 추가해줍니다.
sudo systemctl restart nomad
systemctl status nomad
클러스터 접속에 성공합니다.
Aug 05 05:21:41 ip-172-31-6-247.ap-northeast-2.compute.internal nomad[3405]: 2021-08-05T05:21:41.366Z [INFO] client: node registration complete
Github Public Repo 생성
여기 를 참고하여 Spring Boot App 을 생성합니다.
생성한 App 을 Github 에 Public 으로 배포합니다.
Repo 명은 TestPublic 으로 합니다.
Github Public Repo 연동
젠킨스 아이템 생성
젠킨스 메인화면에서, New Item 을 선택합니다.
Freestyle project 를 선택하고 아이템명을 입력합니다.
소스 코드 관리 에서 Git 을 선택합니다.
GitHub Repository 의 Clone URL 항목을 복사해서 Repositoy URL 에 입력합니다.
빌드를 해보면 소스 가져오기가 정상적으로 작동하는 것을 확인할 수 있습니다.
GitHub Webhook 설정
GitHub 의 지정한 브랜치에 소스가 push 되면 webhook 으로 젠킨스에게 알려주어 빌드를 유발하도록 설정합니다.
Payload URL 은 http://<젠킨스 서버 아이피>:8080/github-webhook/ 로 합니다.
ec2 보안그룹 설정
AWS EC2 보안그룹에 인바운드 허용에 아래 아이피들을 추가해 줍니다.
"192.30.252.0/22",
"185.199.108.0/22",
"140.82.112.0/20"
생성한 보안그룹을 젠킨스 인스턴스에 추가해 줍니다.
젠킨스가 8080 포트에서 실행중이므로, 8080 포트를 개방해 줍니다.
젠킨스 아이템 수정
젠킨스 아이템 구성에서 빌드유발
에 GitHub hook trigger for GITScm polling
을 체크해줍니다.
테스트하기
PC 에서 스프링 앱을 수정하고, github 에 배포합니다.
소스 가져오기가 자동으로 이루어지는 것을 확인할 수 있습니다.
Jar 빌드
젠킨스 아이템 구성에서 Build
에 Invoke Gradle script
선택 후,
Use Gradle Wrapper
를 선택하고 Make gradlew executable
를 체크합니다.
PC 에서 소스 수정 후 Push 를 하면, 젠킨스에서 소스가 빌드되는것을 확인할 수 있습니다.
파이프라인 방식으로 수정
기존 젠킨스 아이템을 삭제합니다.
새 젠킨스 아이템을 생성합니다.
Freestyle project
대신 Pipeline
으로 생성합니다.
Build Triggers
에서 GitHub hook trigger for GITScm polling
를 체크합니다.
Pipeline script
에 아래 내용을 입력합니다.
node {
stage("Get Source") {
git url: "https://github.com/skyer9/TestPublic",
branch: "master"
}
stage("Build") {
sh "chmod 700 gradlew"
sh "./gradlew clean"
sh "./gradlew bootJar"
}
}
Build Now
를 클릭하면 패키징이 성공하는 것을 확인할 수 있습니다.
PC 에서 소스 수정후 Github 에 배포해도 패키징이 이루어지는 것을 확인할 수 있습니다.
Dockerize
도커 이미지를 생성합니다.
PC 에서 프로젝트에 Dockerfile
을 추가합니다.
FROM openjdk:11.0.6-jre
EXPOSE 8080
ARG JAR_FILE=build/libs/*.jar
COPY ${JAR_FILE} app.jar
ENTRYPOINT ["java", "-jar", "/app.jar"]
젠킨스 아이템 Pipeline script
에 아래 내용을 입력합니다.
node {
stage("Get Source") {
git url: "https://github.com/skyer9/TestPublic",
branch: "master"
}
stage("Build") {
sh "chmod 700 gradlew"
sh "./gradlew clean"
sh "./gradlew bootJar"
}
stage("Dockerize") {
sh "docker build -t skyer9/testpublic:0.1.${build_number} ."
sh "docker build -t skyer9/testpublic:latest ."
}
}
PC 에서 소스를 수정하고 push 하면, 젠킨스 서버에 도커 이미지가 생성됩니다.
sudo docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
skyer9/testpublic 0.1.12 6274d4459b08 23 minutes ago 302MB
openjdk 11.0.6-jre d4db31b4991e 16 months ago 285MB
AWS ECR 에 Push 하기
AWS 설정
AWS ECR 을 생성하지 않았으면 생성해 줍니다.
myrepo
리포를 생성합니다.
IAM 에서 ecr_user 계정을 생성하고, AmazonEC2ContainerRegistryFullAccess
권한을 부여합니다.
젠킨스 Credential 추가
젠킨스 메인화면
> Jenkins 관리
> Manage Credentials
로 이동합니다.
ecr_user
계정을 젠킨스 Credential 에 추가합니다.
젠킨스 아이템 수정
젠킨스 아이템 Pipeline script
에 아래 내용을 입력합니다.
node {
stage("Get Source") {
git url: "https://github.com/skyer9/TestPublic",
branch: "master"
}
stage("Build") {
sh "chmod 700 gradlew"
sh "./gradlew clean"
sh "./gradlew bootJar"
}
// repo 에 하나의 이미지만 올리게 되어있나?
stage("Dockerize") {
sh "docker build -t myrepo:0.1.${build_number} ."
sh "docker build -t myrepo:latest ."
}
stage("Push to ECR") {
// ecr:<Your region>:<Your Jenkins credential ID>
// myrepo : 이미지명과 ECR 리포지토리 이름은 일치해야 한다.
docker.withRegistry('https://********.dkr.ecr.ap-northeast-2.amazonaws.com', 'ecr:ap-northeast-2:ecr_user') {
docker.image('myrepo:latest').push()
}
}
}
Nomad Worker 설정 변경
아래 파일을 PATH
에 넣습니다.
wget https://amazon-ecr-credential-helper-releases.s3.us-east-2.amazonaws.com/0.5.0/linux-amd64/docker-credential-ecr-login
sudo chown root:root docker-credential-ecr-login
sudo chmod 777 docker-credential-ecr-login
sudo mv docker-credential-ecr-login /usr/local/bin/
ecr_user 계정을 설정합니다.
nomad 가 root 로 실행되고 있으므로 root 계정에 설정합니다.
sudo su -
pip3 install --upgrade awscli
aws configure
AWS Access Key ID [None]: AKIA3V24********
AWS Secret Access Key [None]: vUeBm5VAO7de3********
Default region name [None]: ap-northeast-2
Default output format [None]:
exit
sudo vi /etc/nomad.d/client.hcl
---------------------------
# bind_addr = "127.0.0.1"
datacenter = "dc1" # 클러스터명
data_dir = "/var/lib/nomad/"
client {
enabled = true
servers = ["XXX.XXX.XXX.XXX"] # 노마드 서버 프라이빗 아이피
}
plugin "docker" {
config {
auth {
# Nomad will prepend "docker-credential-" to the helper value and call
# that script name.
helper = "ecr-login"
}
}
}
---------------------------
노마드를 재실행합니다.
sudo systemctl restart nomad
sudo systemctl status nomad
job 생성
Nomad Server(Host) 에 job 을 생성합니다.
vi myjob.nomad
job "website" {
datacenters = ["dc1"]
type = "service"
constraint {
attribute = "${attr.kernel.name}"
value = "linux"
}
# rolling updates
update {
stagger = "10s"
max_parallel = 1
}
group "cache" {
# restart policy
restart {
attempts = 10
interval = "5m"
mode = "delay"
}
network {
port "lb" { static = 8080 }
}
# Define a task to run
task "website" {
driver = "docker"
config {
image = "https://********.dkr.ecr.ap-northeast-2.amazonaws.com/myrepo:latest"
ports = ["lb"]
}
resources {
cpu = 500 # 500 Mhz
memory = 1024 # 1024 MB
}
}
}
}
정상적으로 작동하는 것을 확인합니다.
sudo su -
cd /home/ec2-user
nomad job run myjob.nomad
exit
nomad job status website
정상적으로 작동하면, Client 서버에 이미지가 다운받아집니다.
sudo docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
********.dkr.ecr.ap-northeast-2.amazonaws.com/myrepo latest cc257084ba9f About an hour ago 302MB
젠킨스에서 nomad 실행
import groovy.text.GStringTemplateEngine
import groovy.json.*
def nomadTemplate = '''
job "website" {
datacenters = ["dc1"]
type = "service"
# rolling updates
update {
stagger = "10s"
max_parallel = 1
}
group "cache" {
# restart policy
restart {
attempts = 10
interval = "5m"
mode = "delay"
}
network {
port "lb" { static = ${port} }
}
# Define a task to run
task "website" {
driver = "docker"
config {
image = "https://********.dkr.ecr.ap-northeast-2.amazonaws.com/myrepo:0.1.${build_number}"
ports = ["lb"]
}
resources {
cpu = 500 # 500 Mhz
memory = 512 # 512 MB
}
}
}
}
'''
node {
stage("Get Source") {
git url: "https://github.com/skyer9/TestPublic",
branch: "master"
}
stage("Build") {
sh "chmod 700 gradlew"
sh "./gradlew bootJar"
}
// repo 에 하나의 이미지만 올리게 되어있나?
stage("Dockerize") {
sh "docker build -t myrepo:0.1.${build_number} ."
}
stage("Push to ECR") {
// ecr:<Your region>:<Your Jenkins credential ID>
// myrepo : 이미지명과 ECR 리포지토리 이름은 일치해야 한다.
docker.withRegistry('https://********.dkr.ecr.ap-northeast-2.amazonaws.com', 'ecr:ap-northeast-2:ecr_user') {
docker.image("myrepo:0.1.${build_number}").push()
}
}
stage("Run Nomad") {
// Nomad 서버 프라이빗 아이피
env.NOMAD_ADDR = "http://XXX.XXX.XXX.XXX:4646"
def bindMap = [
port: "8080",
build_number: "${build_number}"
]
def javaNomadText = new GStringTemplateEngine().createTemplate(nomadTemplate).make(bindMap).toString()
writeFile file: 'myjob.nomad', text: javaNomadText
sh "/usr/local/bin/nomad job run myjob.nomad"
sh "rm myjob.nomad"
}
}
젠킨스 서버 보안그룹에 allow-nomad
를 추가합니다.
빌드를 해보면 오류가 발생합니다.
위 링크를 클릭해 들어가서 Allow
를 눌러줍니다.
이 단계는 3~4회 반복해서 발생하므로 반복해서 Allow
를 눌러줍니다.
http://<클라이언트서버 퍼블릭 아이피>:8080/hello 에 접속하면,
스프링 부트 앱이 실행되는 것을 확인할 수 있습니다.
PC 에서 소스를 수정해서 배포했을 때,
변경사항이 반영되는 것을 확인할 수 있습니다.
Github Private Repo 연동
Github Private Repo 생성
여기 를 참고하여 Spring Boot App 을 생성합니다.
생성한 App 을 Github 에 Private 으로 배포합니다.
Repo 명은 TestPrivate 으로 합니다.
Jenkins 키 생성
젠킨스 서버에서 아래 명령을 입력합니다.
sudo su -s /bin/bash jenkins
whoami
mkdir /var/lib/jenkins/.ssh
cd /var/lib/jenkins/.ssh
ssh-keygen -t rsa -b 4096 -C "skyer9@gmail.com" -f /var/lib/jenkins/.ssh/github
cat /var/lib/jenkins/.ssh/github.pub
위에서 확인된 공개키를 아래와 같이 Github 에 등록합니다.
젠킨스 키 등록
cat /var/lib/jenkins/.ssh/github
위에서 확인되는 비밀키를 젠킨스에 등록합니다.
Github Webhook 설정
Payload URL 는 http://<젠킨스 퍼블릭 아이피>:8080/github-webhook/ 로 합니다.
젠킨스 아이템 생성
AWS ECR 에서 myrepoprivate
리포를 생성합니다.
젠킨스 아이템을 pipeline 으로 생성하고 아래 script 를 입력합니다.
깃허브 URL 이 ssh 로 변경된 것을 확인할 수 있습니다.
stage("Get Source") {
git url: "git@github.com:skyer9/TestPrivate.git",
branch: "master",
credentialsId: "my_jenkins_token"
}
import groovy.text.GStringTemplateEngine
import groovy.json.*
def nomadTemplate = '''
job "website" {
datacenters = ["dc1"]
type = "service"
# rolling updates
update {
stagger = "10s"
max_parallel = 1
}
group "cache" {
# restart policy
restart {
attempts = 10
interval = "5m"
mode = "delay"
}
network {
port "lb" { static = ${port} }
}
# Define a task to run
task "website" {
driver = "docker"
# user = "nobody"
config {
image = "https://********.dkr.ecr.ap-northeast-2.amazonaws.com/myrepoprivate:0.1.${build_number}"
ports = ["lb"]
}
resources {
cpu = 500 # 500 Mhz
memory = 512 # 512 MB
}
}
}
}
'''
node {
stage("Get Source") {
git url: "git@github.com:skyer9/TestPrivate.git",
branch: "master",
credentialsId: "my_jenkins_token"
}
stage("Build") {
sh "chmod 700 gradlew"
sh "./gradlew clean"
sh "./gradlew bootJar"
}
// repo 에 하나의 이미지만 올리게 되어있나?
stage("Dockerize") {
sh "docker build -t myrepoprivate:0.1.${build_number} ."
}
stage("Push to ECR") {
// ecr:<Your region>:<Your Jenkins credential ID>
// myrepo : 이미지명과 ECR 리포지토리 이름은 일치해야 한다.
docker.withRegistry('https://********.dkr.ecr.ap-northeast-2.amazonaws.com', 'ecr:ap-northeast-2:ecr_user') {
docker.image("myrepoprivate:0.1.${build_number}").push()
}
}
stage("Run Nomad") {
// Nomad 서버 프라이빗 아이피
env.NOMAD_ADDR = "http://172.31.8.155:4646"
def bindMap = [
port: "8080",
build_number: "${build_number}"
]
def javaNomadText = new GStringTemplateEngine().createTemplate(nomadTemplate).make(bindMap).toString()
writeFile file: 'myjob.nomad', text: javaNomadText
sh "/usr/local/bin/nomad job run myjob.nomad"
sh "rm myjob.nomad"
}
}
TODO
스프링 부트 앱이 root 로 실행되고 있다.
클라이언트 노드가 여러 대 인 경우 로드밸런싱은 어떻게 할까?
서버 노드가 여러대이면?
클라이언트 노드의 스케일 out/in 은?