Jenkins Github Nomad 를 이용한 CI/CD 시스템 구축

By | 2021년 8월 4일
Table of Contents

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 빌드

젠킨스 아이템 구성에서 BuildInvoke 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 에 넣습니다.

docker-credential-ecr-login

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 은?

답글 남기기