CICD

CI/CD with Jenkins&ArgoCD

JAEJUNG 2022. 5. 3. 17:41

이번 포스팅에선 Jenkins와 Bitbucket, Harbor를 통해 CI/CD Pipeline을 구축하는 과정에 대해 설명한다.

 

전체 Architecture는 아래와 같다.

  1. 개발자가 Bitbucket으로 소스코드를 Push한다.
  2. Push event를 트리거 삼아 Webhook이 Jenkins로 전달된다.
  3. Jenkins에선 Bitbucket의 Jenkinsfile을 기반으로 docker image를 build하고 이를 Harbor에 push한다.
  4. ArgoCD는 Bitbucket의 변경사항을 감지하여 이를 EKS Cluster에 배포한다.

 

 

사전 작업

 

Jenkins 서버에 Docker 설치

해당 포스팅에서 Jenkins는 Dockerfile을 빌드하는 역할을 한다.

따라서 Jenkins 서버에서도 Docker가 설치되어 있어야 하며, Docker를 사용할 권한도 있어야 한다.

 

이전 글인 EC2에 Jenkins 구축하기를 참고하여 jenkins가 구축된 서버에 Docker를 설치한다.

[root@jenkins docker]# sudo amazon-linux-extras install -y docker
[root@jenkins docker]# service docker start
[root@jenkins docker]# usermod -aG docker ec2-user

 

Harbor와 Jenkins 연동하기

 

harbor는 https 통신을 기본으로 하기 때문에 접속하기 위해선 인증서를 필요로 한다.

그러나 간단한 테스트를 진행하기 위한 http 통신도 지원하기 때문에 아래 링크를 참고하여 http로 harbor에 접속한다.

(링크: Connecting to Harbor via HTTP)

 

Jenkins docker daemon 설정

여기서 harbor로 접속하는 User는 jenkins이기 때문에 jenkins에서 먼저 설정을 진행한다.

http 통신을 위해선 Jenkins의 Docker daemon에 --insecure-registry 값을 추가해주어야 한다.

 

/etc/docker 하위 경로에 daemon.json 파일을 생성해주고,

key:value 형태로 key값에 insecure-registries, value에 harbor의 IP를 입력한다.

[root@jenkins docker]# vi /etc/docker/daemon.json
{
  "insecure-registries" : ["{harbor's domain}"]
}

 

 

jenkins에서 docker service를 restart 시켜준다.

[root@jenkins docker]# systemctl restart docker

 

 

docker info 명령으로 harbor의 IP가 추가되었는지 확인한다.

[root@jenkins docker]# docker info | grep -A 5 "Insecure Registries"
 Insecure Registries:
  0.0.0.0
  3.39.236.59 #harbor의 ip가 표시된다면 정상 적용 완료
  127.0.0.0/8

 

EKS Node 또한 pod를 배포하는 과정에서 harbor로부터 이미지를 pull하기 필요하기 때문에,

위에서 진행했던 동일한 과정을 EKS Node에서도 진행한다.

다만, EKS Node에는 이미 /etc/docker 경로에 daemon.json 파일이 존재하기 때문에 값을 추가만 해준다.

[root@ip-192-168-1-94 docker]# cat daemon.json
{
  "insecure-registries" : ["{Harbor의 IP}"],
  "bridge": "none",
  ...
}

 

 

값을 추가해준 후에 동일하게 docker service를 restart 시켜준다.

[root@ip-192-168-1-94 docker]# systemctl restart docker

 

 

Jenkins - > Harbor로 push, pull test

harbor에 액세스가 이루어졌으니, 이제 테스트 이미지를 통해 push/pull이 잘 되는지 확인해보자.

 

테스트 Image 준비

테스트 이미지로는 용량이 작은 alpine 이미지로 테스트를 진행한다.

[root@jenkins docker]# docker pull alpine
[root@jenkins docker]# docker images -a
REPOSITORY   TAG       IMAGE ID       CREATED       SIZE
alpine       latest    0ac33e5f5afa   4 weeks ago   5.57MB

 

 

harbor Repository 생성

먼저 harbor 웹 콘솔에 접속해서 새 repository를 생성해준다.

(Project Access Level을 Public으로 생성하면 docker login 안해도 됨)

 

 

[PUSH COMMAND]를 통해 harbor로 이미지를 push하기 위한 명령어를 확인할 수 있다.

 

 

harbor로 image push

harbor로 이미지를 push하기 위해선 기존 이미지의 태그명을 수정해주어야 한다.

[root@jenkins docker]# docker tag alpine 3.39.236.59/demo/image

[root@jenkins docker]# docker images -a
REPOSITORY               TAG       IMAGE ID       CREATED       SIZE
3.39.236.59/demo/image   latest    0ac33e5f5afa   4 weeks ago   5.57MB
alpine                   latest    0ac33e5f5afa   4 weeks ago   5.57MB

 

 

docker push 명령어를 통해 harbor로 이미지를 push해준다.

[root@jenkins docker]# docker push 3.39.236.59/demo/image

 

 

harbor에서 image가 업로드 된 것을 확인한다.

 

harbor에서 pull test

테스트를 위해 저장돼있는 docker image를 모두 삭제해준다.

docker rmi -f $(docker images -q)

 

 

이제 docker pull 명령어를 통해 harbor에 등록돼있는 이미지를 가져와보자.

[root@jenkins docker]# docker pull 3.39.236.59/demo/image
Using default tag: latest
latest: Pulling from demo/image
df9b9388f04a: Pull complete
Digest: sha256:a777c9c66ba177ccfea23f2a216ff6721e78a662cd17019488c417135299cd89
Status: Downloaded newer image for 3.39.236.59/demo/image:latest
3.39.236.59/demo/image:latest

 

등록된 docker image를 확인하여 harbor에서 image가 정상적으로 pull 됐는지 확인한다.

[root@jenkins docker]# docker images -a
REPOSITORY               TAG       IMAGE ID       CREATED       SIZE
3.39.236.59/demo/image   latest    0ac33e5f5afa   4 weeks ago   5.57MB

 

 

Jenkins에 harbor credential 등록

Jenkins가 파이프라인을 수행하는 과정에서 harbor에 접근할 수 있어야 하기 때문에(빌드된 image를 harbor에 push하는 과정) 사전에 Credential을 생성해준다.

 

harbor에서 Robot Account 생성

 

먼저 Harbor에 접속에서 [Robot Accounts]-[NEW ROBOT ACCOUNT]를 클릭한다.

 

Robot Account는 docker CLI와 helm CLI에서만 사용할 수 있는 특수 계정이며, Harbor interface에 로그인이 불가능하다는 특징이 있다.

일반 User와 다르게 Expiration time을 설정할 수 있고, Project 별 액세스 권한을 줄 수 있기 때문에 간단한 테스트 용도로 생성하기에도 적합하다.

 

Name은 test로 입력하고, [Cover all projects] 옵션은 활성화 시켜준다.

 

 생성하게 되면 Secret 값이 부여되며, [EXPORT TO FILE]을 클릭하여 Credential 값을 보관한다.

 

Jenkins Credential 생성

 

Jenkins 웹 콘솔에 접속하여 [Jenkins 관리]-[Manage Credentials]-[Jenkins]를 클릭한다.

 

 

[Global credentials]를 클릭

 

 

좌측 탭에서 [Add Credentials] 클릭

 

 

Username, Password는 조금 전 생성했던 harbor robot account의 id와 password 값을 입력한다.

ID는 추후 pipeline 내에서 인식할 수 있는 credential의 ID로 harbor_credential로 기입하고,

Description은 harbor credential이라는 것을 알 수 있게끔 간단한 설명을 입력한다.

 

 

생성된 credential 확인

 

Jenkins 플러그인 설치

Jenkins를 사용하면서 추가적인 기능, 다른 서비스들과의 연동 등을 위해 다양한 플러그인을 제공한다.

이번 실습을 위한 plug-in 목록은 다음과 같다.

  • Generic Webhook Trigger Plugin
  • Bitbucket Plugin
  • Pipeline (Jenkins 최신 버전 설치 시 이미 플러그인이 설치되어 있을 수 있음)
  • Blue Ocean
  • Docker
  • Docker Pipeline

 

플러그인 설치를 위해 Jenkins 웹 콘솔에 접속하여 [Jenkins 관리]-[플러그인 관리]를 클릭한다.

 

 

[설치 가능] 탭에서 필요한 플러그인을 체크한 후 [Install without restart]를 클릭하여 설치해준다.

해당 옵션을 통해 여러 플러그인을 동시에 설치할 수 있다.

 

 

아래 [Jenkins 재시작] 버튼을 체크하여 플러그인 설치가 완료된 후 Jenkins 서버를 재시작시켜준다.

 

 

Bitbucket 설정

Bitbucket은 Gitlab, Github와 같은 Git 코드 관리 솔루션이다.

(세 가지 솔루션의 차이점에 대해 알고 싶다면 여기에서 확인)

 

계정 생성

계정 생성은 Bitbucket 에서 진행하며, 본 글에서는 생략하도록 한다.

 

Bitbucket Repository 생성

Bitbucket은 Repository의 상위 개념으로 Project가 존재하며, 관련된 Repository들을 한 Project로 묶을 수 있다.

두 개의 Repository하고, 동일한 Project 아래에서 관리할 것이다.

 

 

Bitbucket 콘솔에서 [Create]-[Project]를 클릭한다.

 

 

Name과 Description을 입력한다.

 

 

아래와 같이 두 개의 Repository를 생성한다.

Default branch의 경우 main으로 지정한다.

 

Jenkins Repository에 파일 업로드

Jenkins-test에 아래 파일들을 업로드해주자.

 

 

Dockerfile

FROM nginx:latest
COPY ./index.html /usr/share/nginx/html/index.html
CMD ["nginx", "-g", "daemon off;"]
EXPOSE 80

 

Jenkinsfile

script 아래 변수명은 각자 지정한 이름으로 설정한다.

  • USER: bitbucket username
  • BITBUCKET_SERVER: bitbucket server url
  • GITOPS_REPO: bitbucket k8s repo name
  • HARBOR_REPO: harbor의 image pull 경로
  • HARBOR_URL: harbor url
  • HARBOR_USERNAME: harbor username
  • GIT_SUFFIX: image registry name

코드

더보기

pipeline {
    agent any
    stages{
        stage('========== Clone repository ==========') {
            steps{
                script{
                    GIT_BRANCH = 'main'    
                    USER = 'mz-ljj'
                    BITBUCKET_SERVER = 'bitbucket.org/mz-ljj'
                    GITOPS_REPO = 'k8s-test'
                   
                    BUILD_ID = "${env.BUILD_NUMBER}"
 
                    HARBOR_REPO = '3.39.236.59/demo/testbuild'
                    HARBOR_URL= 'http://3.39.236.59/'
                    HARBOR_USERNAME = 'admin'
                    GIT_SUFFIX = 'testbuild'
 
                    checkout scm
                }
            }
        }
        stage('========== Build image ==========') {
            steps{
                script{
                    app = docker.build("${HARBOR_REPO}")
                }
            }
        }
 
        stage('========== Push image ==========') {
            steps{
                script{
                    docker.withRegistry("${HARBOR_URL}", "${HARBOR_USERNAME}") {
                    app.push("${env.BUILD_NUMBER}")
                    app.push("latest")
                    }
                }
            }
        }
        stage('========== clone yaml ========') {
            steps{
                script{
                        withCredentials([gitUsernamePassword(credentialsId: 'jenkins', gitToolName: 'Default')]) {
                            sh 'rm -rf k8s-test'
                            sh "git clone https://${USER}@${BITBUCKET_SERVER}/${GITOPS_REPO}.git -b ${GIT_BRANCH}"
                        }
                }
            }
 
        }
        stage('========== Deploy image ===========') {
            steps{
                script{
                    dir("${GITOPS_REPO}") {            
                        withCredentials([gitUsernamePassword(credentialsId: 'jenkins', gitToolName: 'Default')]) {
                            sh 'git config --global init.defaultBranch main'
                            sh 'git branch -m \"main\"'
                            sh "git config --global user.email \"admin@jenkins.com\""
                            sh 'git config --global pull.rebase false'
                            sh "ls -al"
                            sh "sed -i \"s,${HARBOR_REPO}:.*,${HARBOR_REPO}:${env.BUILD_ID},g\" deployment.yaml"
                            sh "cat deployment.yaml"
                            sh "git add ."
                            sh "git commit --allow-empty -m \"[Jenkins] updateing image: to ${env.BUILD_ID}\" "
                            sh "git push origin ${GIT_BRANCH}"
                        }
                    }
                }
            }
        }
    }
}

 

index.html

deploy test 1:59 pm

 

k8s repository에 파일 업로드

k8s-test에 아래 파일들을 업로드해준다.

 

 

deployment.yaml

(미리 harbor에 nginx 이미지를 업로드해둔다.)

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
  namespace: default
  labels:
    app: nginx
spec:
  replicas: 1
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: 3.39.236.59/demo/testbuild:1
        ports:
        - containerPort: 8080
      imagePullSecrets:
      - name: harbor

 

LoadBalancer.yaml

apiVersion: v1
kind: Service
metadata:
 name: nginx-service
 namespace: default
 annotations:
   service.beta.kubernetes.io/aws-load-balancer-type: external
   service.beta.kubernetes.io/aws-load-balancer-nlb-target-type: ip
   service.beta.kubernetes.io/aws-load-balancer-healthcheck-healthy-threshold: "2"
   service.beta.kubernetes.io/aws-load-balancer-healthcheck-unhealthy-threshold: "2"
   service.beta.kubernetes.io/aws-load-balancer-cross-zone-load-balancing-enabled: "true"
   service.beta.kubernetes.io/aws-load-balancer-scheme: internet-facing
   service.beta.kubernetes.io/aws-load-balancer-target-group-attributes: "deregistration_delay.timeout_seconds=10, deregistration_delay.connection_termination.enabled=true" # supported values ["deregistration_delay.timeout_seconds=10, stickiness.enabled=true,stickiness.type=source_ip, proxy_protocol_v2.enabled=true, deregistration_delay.connection_termination.enabled=true, preserve_client_ip.enabled=true", stickiness.enabled=true,stickiness.type=source_ip]
spec:
 ports:
   - port: 80
     name: tcp-80
     targetPort: 80
     protocol: TCP
 type: LoadBalancer
 selector:
   app: nginx

 

Jenkins 파이프라인 설정

 

bitbucket webhook 설정

우리는 git repository에서 push가 발생했을 때 Jenkins에서 자동으로 빌드가 돼야 한다.

그러기 위해선 먼저 git repo에서 이벤트 발생 시 Jenkins가 이를 감지할 수 있는 수단이 필요하다.

그 역할을 이 webhook이 수행해줄 것이다.

 

 

Title은 자유롭게 입력하고,

URL의 경우 http://{Jenkins 서버의 IP}:8080/bitbucket-hook/으로 기입한다.

 

 

bitbucket token 생성

 

app password를 생성하면 최초 1회만 확인이 가능하기 때문에, 화면에 표시된 값을 기록해두어야 한다.

 

 

이제 Jenkins 웹 콘솔에서 새로운 Pipeline을 생성해보자.

 

 

먼저 General 탭에서 [Build when a change is pushed to BitBucket], [Generic on branch created] 옵션을 클릭해준다.

 

옵션을 아래와 같이 설정하고 Repository URL에 Bitbucket Repository의 URL을 입력한다.

 

# 아래와 같은 오류가 발생하면 Jenkins 서버에 git이 설치되지 않아서 표시되는 것으로 jenkins에 git을 설치해준다.

sudo yum install -y git

 

 

Repository URL을 입력하면 아래와 같은 오류가 발생한다.

이는 BitBucket의 Credential이 없어서 발생하는 오류이다.

 

 

아래 Credentials에서 [Add]-[Jenkins]를 클릭한다.

 

 

Username은 Bitbucket의 ID, Password 부분은 조금 전 생성했던 app password를 입력한다.

(id는 bitbucket_credential로 설정)

 

 

Branch는 main, Script Path는 Jenkinsfile로 입력한다.

 

이제 저장을 한 후 [Build Now]를 눌러 수동으로 빌드를 해보자.

 

[블루 오션 열기]를 클릭하면 빌드되는 과정을 대시보드 형태로 한 눈에 확인할 수 있다.

 

Harbor에 들어가서 빌드된 이미지를 확인해보자.

 

 

Webhook으로 자동 빌드 확인

수동 빌드가 아닌 git repo에 코드가 변경됐을 때 Jenkins에서 자동으로 빌드가 되는지도 확인해보자.

 

Bitbucket Repo에서 index.html 파일에 임의로 내용을 변경 후 commit을 클릭한다.

 

 

[Repository settings]-[Webhook] 탭에서 View requests를 확인한다.

 

로그를 확인해보면 push 이벤트에 대해 정상적으로 webhook이 보내진 것을 확인할 수 있다.

 

 

 

Jenkins로 돌아가보면 push 이벤트를 트리거 삼아 빌드가 실행되는 것을 확인할 수 있다.

 

ArgoCD로 앱 배포

bitbucket push 후 jenkins에서 자동으로 빌드가 실행되는 것까지 확인하였다.

이 과정까지가 CI(Continuous Integration)이며, 이제 CD(Continuous Deployment)를 완성시켜보자.

 

EKS에 harbor 인증정보 추가

 

먼저 harbor의 계정정보가 담긴 secret을 생성해주어야 한다.

 

deployment.yaml 파일을 보면 deployment가 배포될 때 spec.template.spec.imagePullSecret 값을 참조하여 컨테이너 이미지를 받아온다.

...
spec:
  replicas: 1
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: 3.39.236.59/demo/testbuild:1
        ports:
        - containerPort: 8080
      imagePullSecrets:
      - name: harbor

 

아래 명령어로 secret 오브젝트를 생성해준다.

kubectl create secret docker-registry harbor --docker-server=http://<harbor 주소> --docker-username=admin --docker-password=Harbor12345

 

이제 ArgoCD에 Repository를 추가하고, app을 배포해보자

ArgoCD 설치 및 접속을 참고하여 ArgoCD 웹 콘솔에 접속한다.

 

[Settings]-[Repositories] 클릭

 

Repository URL: bitbucket의 k8s-test repository url

Username: Bitbucket 계정 ID

Password: Bitbucket 계정 app password

 

[CONNECTION STATUS]가 "Successful"로 표시되면 정상적으로 Repository와 연결된 것이며, "Create application"을 클릭한다.

 

Application Name: demo

Project: default

[SKIP SCHEMA VALADTAION] 옵션 활성화

Repository URL: bitbucket k8s-test repository url

Path: . 입력

Cluster URL: EKS Cluster

Namespace: default

 

현재 sync 옵션이 manual여서 OutOfSync인 상태로, SYNC를 눌러 Repository와 동기화시켜준다.

 

service와 deployment가 정상적으로 배포됐으며, service에 접속해보자.

 

service 도메인은 HOSTNAMES에서 확인할 수 있다.

 

현재 웹 서버에 아래와 같은 문구가 띄어져있는 상태이다.

 

이제 index.html 파일을 수정한 후 자동으로 빌드를 거쳐 pod에 배포되는지 확인해보자.

 

1. bitbucket에서 code 수정 후 commit

 

2. Jenkins 빌드

 

3. Harbor에 image push

 

4. ArgoCD sync

현재 bitbucket과 sync를 맞추고 있기 때문에 변경사항이 생기면 아래와 같이 OutOfSync 상태가 된다.

다시 한번 sync를 맞춘 후 service에 접속해보자.

 

-> 최종 배포된 Pod 상태 확인

 

결론

Jenkins와 ArgoCD를 활용한 CI/CD 파이프라인 구성을 통해 Source Repository에 소스코드를 push해주기만 하면

빌드 과정을 통해 인프라 자동화를 구현할 수 있었다.

'CICD' 카테고리의 다른 글

Argo CD  (0) 2021.10.13
AWS CodePipeline을 이용한 깃옵스(GitOps) 구현하기  (0) 2021.09.04