본문 바로가기

Infra/AWS

Github Webhook(+generic webhook trigger plugin)과 Jenkins, CodeDeploy 를 사용하여 CI/CD 구축하기

프로페서 롤 프로젝트 CI/CD를 비용상의 문제로 Travis CI + CodeDeploy로 구축했던 구버전에서 Jenkins + CodeDeploy로 변경하게 되었습니다. Travis CI 에 비해 파이프라인(Groovy), 다양한 플러그인, 그리고 가장 중요한 "참고자료(=구글자료)"가 훨씬 방대하기 때문입니다. 소나큐브를 적용하기도 훨씬 원활할 것이라고 생각했습니다.

Jenkins 세팅은 아래 조건에서 진행하였습니다.

 

AWS EC2 free tier(t2.micro)

 

처음에는 프리티어에서 젠킨스가 돌아갈지 걱정이 많았는데 누군가의 경험에 기반한 강력한 주장으로 진행하게 되었습니다. 물론 설치 과정, 세팅 과정에서 조금 자주 뻗긴 했습니다.

갑자기 SSH 세션이 끊겨버려서 왜 이러나했더니 위 그래프에서 볼 수 있듯이 스파이크가 마구 치고 있었고 임계치를 훨씬 넘어버려 경보가 두어개 쌓여있었습니다. 결과적으로 초기 세팅때만 좀 이러고 뒤로는 나름 순탄한 프리티어 생활이 가능했습니다.(t2.micro 의 기준 CPU Utilization 은 10% 입니다.)

본 포스팅은 크게 Chapter1 Jenkins 세팅, Chapter2 Github Webhook 설정, Chapter 3 Jenkins 쉘스크립트를 이용한 CI 구현, Chapter 4 CodeDeploy 를 통한 CD 작업 순서대로 진행하겠습니다.

 

Chapter 1. Jenkins 세팅

Step1. EC2 생성

AWS Console 에 로그인 후 EC2 서비스 > 인스턴스 시작 탭을 선택합니다.

단계 1: AMI 선택

아마존 리눅스 2 로 진행합니다.

image

 

단계 2: 인스턴스 유형 선택

프리티어 인스턴스를 선택합니다.

image

 

단계 3: 인스턴스 세부 정보 구성

별도의 VPC 구성을 하지 않았기 때문에 default vpc, subnet 을 선택합니다.

또, 외부 인터넷 망을 통해 젠킨스에 접속해야 하기 때문에 퍼블릭 IP 자동할당 옵션도 체크해주어야 합니다.(디폴트 VPC의 기본 서브넷이라면 자동 옵션이 활성화이지만 저는 활성화로 체크하겠습니다.)

IAM 역할은 인스턴스 생성 후 Attach 가능하기 때문에 지금은 설정하지 않습니다. 그외 설정은 건드리지 않아도 됩니다.

image

(위 스크린샷 정보대로만 설정하고 여기 나오지 않은 옵션은 건드리지 않아도 됩니다.)

+만약 기본 서브넷이 없거나 AZ b 만 있다면 새 서브넷 생성을 통해 a 또는 c AZ의 서브넷을 만들도록 합니다. a 또는 c AZ 서브넷이 이미 존재한다면 사용해도 무방합니다.

image

 

 

단계 4: 스토리지 추가

1GB가 소중하므로 프리티어 최대치인 30GB 로 늘려줍니다.

image

 

단계 5: 태그 추가

추후 검색이 용이하도록 하기 위해 Name 태그는 습관적으로 설정하는게 좋습니다.

image

 

단계 6. 보안그룹 구성

보안그룹은 인스턴스 레벨의 방화벽입니다. 이 인스턴스는 세가지 접근이 있을 수 있습니다.

1. 루트 유저 SSH Connection 요청t: 22번포트, 내 ip 주소

2. 개발자/배포담당자 등 CICD 사용 유저: 8080 포트(젠킨스는 디폴트가 8080 포트입니다.), 내 ip주소 

3. Github Webhook 요청: Github Webhook 은 젠킨스에 POST 메서드로 훅을 날립니다. 즉, 8080포트를 public 하게 열어서 이 요청을 받도록 할 것인데, https://developer.github.com/changes/2019-03-29-webhooks-ip-changes/ 이런 게시글을 보아서 저는 140.82.112.0/20, 192.30.252.0/22 사이더대역만 허용할 것입니다.

image

(내 아이피주소는http://www.myipaddress.com/show-my-ip-address/에서 확인할 수 있습니다)

마지막으로 기존 키페어가 있다면 기존 키페어를, 없다면 새로 생성하여 잘 저장해둔 뒤 인스턴스를 시작합니다.

 

Step 2. jenkins 설치 및 세팅

먼저, pem 키를 사용하여 인스턴스에 접근하기 위해 읽기권한을 부여합니다.

sudo chmod 400 pemkey경로/pemkey명.pem

그리고 ssh 접속을 수행합니다. 만약 timeout 에러가 난다면 보안 그룹에 22 번 포트로 올바른 IP 를 할당했는지, 인스턴스 생성 시 퍼블릭 IP 를 할당했는지를 확인합니다. 인스턴스의 퍼블릭 아이피는 인스턴스 > 설명의 IPv4 퍼블릭 IP 에서 확인할 수 있습니다. 퍼블릭 DNS를 사용하여도 무관합니다.

ssh -i pemkey풀경로 ec2-user@퍼블릭IP또는DNS

 

pem파일 identity 를 통해 해당 인스턴스에 접속합니다. 아마존 리눅스 2의 기본 유저 네임은 ec2-user 입니다.

image

 

접속에 성공하면 jenkins 를 설치하기 전에 java 를 먼저 설치해줍니다. jenkins 는 java 기반이기 때문에 자바가 없으면 실행이 안됩니다.

image

sudo yum update -y
sudo yum install -y java-1.8.0-openjdk-devel.x86_64
java -version

 

1.8 버전이 올바르게 출력되면 젠킨스 설치를 시작합니다.

sudo wget -O /etc/yum.repos.d/jenkins.repo http://pkg.jenkins-ci.org/redhat/jenkins.repo
sudo rpm --import https://pkg.jenkins.io/redhat/jenkins.io.key
sudo yum install jenkins -y
sudo service jenkins start

image

위 메시지가 출력되면 인터넷 탭을 열어 퍼블릭 아이피주소:8080 경로로 가봅니다. 이때도 마찬가지로 로딩이 계속되고 창이 뜨지 않는다면 보안그룹 8080 포트에 자신의 아이피가 제대로 할당되어있는지 확인합니다.

image

젠킨스 설치 후 최초 화면은 위와 같습니다. 다시 ssh로 돌아가 화면에 써있는 경로에서 초기 비밀번호 값을 확인합니다. root 권한으로 조회해야함에 주의합니다.

sudo cat /var/lib/jenkins/secrets/initialAdminPassword

출력되는 값을 빈칸에 입력합니다.

올바른 패스워드를 입력했다면 아래 화면으로 넘어갑니다. Install suggested plugins 를 선택합니다. Gradle, Git 과 같이 기본적으로 필요한 플러그인들이 포함되어있습니다.

image

이제 유저를 세팅하면 젠킨스를 사용할 수 있습니다. 다음 페이지에서 Jenkins URL 이 나오면 Save and Finish 를 통해 세팅을 완료해줍니다.

image

젠킨스 세팅을 완료하였습니다

 

Chapter 2. Github Webhook 설정하기

이제 간단한 Application 을 Github 에 업로드한 것으로 가정하고 웹훅으로 Jenkins 잡을 실행시켜보겠습니다.

저는 깃허브에서 기본적으로 제공하는 webhook 이 젠킨스의 특정 잡을 지정하지 않고 trigger 옵션이 체크된 모든 잡에 요청을 보낸다는 것이 좋지 않다고 느꼈습니다. 또, 페이로드 데이터를 통해 푸쉬된 브랜치 정보를 더 편하게 얻어오고 싶었습니다. 그런 이유로 jenkins 에서 제공하는 Generic Webhook Trigger Plugin 을 선택했습니다.

Jenkins 관리 > 플러그인 설치 > Generic Webhook Trigger Plugin 을 찾아 설치합니다.

image

설명에서 알 수 있다시피 JSON 페이로드 데이터를 쉽게 추출할 수 있습니다. 깃허브 뿐 아니라 깃랩, 지라 등 많은 솔루션과 연동이 가능합니다.

Step 1. Webhooks 설정

레파지토리 > Settings 탭에서 Webhooks 을 들어가 Add webhook 을 누릅니다. Content type 은 둘다 해본 결과 urlencoded 이든 json 이든 결과에 차이는 없었습니다. 

+ 인줄 알았으나 파이프라인 잡으로 실행 시 json 으로 설정해 주지 않으면 값을 사용할 수 없었습니다. application/json 으로 선택해주세요.

image

 

웹훅은 PayloadURL로 POST 요청을 합니다.

젠킨스 generic webhook trigger는 "젠킨스url/generic-webhook-trigger/invoke" 요청을 통해 트리거링됩니다. 보안그룹 포트를 8080으로 열었기 때문에 포트 역시 8080으로 작성하도록 합니다.

또, 토큰 값을 파라미터로 전달할 수 있습니다. 이 토큰 값을 통해 특정 잡을 지정할 것 입니다. 원하는 토큰 값을 파라미터로 줍니다. 저는 아래 암호생성 커맨드를 통해 토큰 값을 정했습니다.

ruby -rsecurerandom -e 'puts SecureRandom.hex(20)'

 

그럼 풀 payloadurl은 다음과 같아집니다.

[jenkins-url]/generic-webhook-trigger/invoke?token=[token값]

참고로 기본 깃허브 웹훅은 요청은 "젠킨스url/github-webhook/" url 로 전달됩니다.

 

Content type 은 application/json 으로 설정하여 브랜치 값을 추출하도록 하겠습니다.

 

Secret 칸은 문자열을 입력하고 Jenkins 서버에서 이 값을 검증하게 함으로써 요청을 한정지을 수 있습니다. 여기서는 비워두고 진행하겠습니다.

그리고 트리거는 Just push event 로 선택한 뒤 Add Webhook 을 선택합니다. 정리하면 다음과 같습니다. (Secret 값은 무시해주세요)

image

그럼 젠킨스 웹훅은 모든 Push 이벤트에 대해서 hook 이 실행되나요?

처음 제 바람은 특정 브랜치의 Push 이벤트에 대해서만 웹훅이 실행되는 것이었습니다. 엄청난 구글링 끝에 알아낸 것은 "브랜치 각각에 대한 웹훅 설정은 없다" 입니다. 웹훅은 레포지토리와 연결되어있고 브랜치 정보는 ref 값으로 전송됩니다. 따라서 브랜치에 대한 필터링은 깃허브 Job 에서 수행해야합니다. 위에서도 언급했지만 이를 쉽게 처리하기위해 generic webhook trigger plugin 을 선택했습니다.

https://stackoverflow.com/questions/46140233/github-webhooks-triggered-globally-instead-of-per-branch/46201893

image

체크가 떨어지면 정상적으로 웹훅 등록이 된 것입니다.

 

+ 토큰을 추가했기 때문에 아래와 같은 에러 메시지가 발생할 수도 있습니다. 토큰 파라미터와 일치하는 특정 잡을 찾지 못했다는 메시지로 Jenkins 서버에 정상적으로 접근했다는 의미이기 때문에 괜찮습니다.

 

Step 2. Jenkins 세팅

빌드 시 깃허브의 레포지토리의 소스코드를 불러와야하는데 이때 ssh 인증이 필요합니다. 공개키 방식의 인증으로 젠킨스에는 서버의 프라이빗 키를, 깃허브에는 퍼블릭 키를 설정하여 인증을 수행할 수 있도록 합니다.

젠킨스가 설치되어있는 인스턴스에서 다음을 수행합니다.

cd .ssh
ssh-keygen
ls 

 

입력하라고 나오는 모든 설정은 입력하지 않아도 무관합니다.

출력되어 나온 리스트에서 id_rsa 가 프라이빗키, id_rsa.pub 이 퍼블릭키 입니다. 프라이빗 키를 젠킨스에 등록하겠습니다.

Credentials > Jenkins > Global credentials > Add Credentials

kind는 SSH Username with private key 를 선택하고, Username은 식별할 수 있는 문자를, private key는 인스턴스에서

cat id_rsa

해서 출력된 값을 입력합니다. 기존엔 직접 입력하지 않고 jenkins의 ssh 폴더에서 찾는 방식도 있었지만 현재 2.218 버전 기준으로 보안상의 문제로 지원하지 않습니다. BEGIN ~ END 까지 포함하여 전체를 복사-붙여넣기 해야함에 주의합니다.

image

 

깃허브 레포지토리 > Setting > Deploy keys 에서 퍼블릭키를 등록해줍니다. 젠킨스에서 레포지토리에 쓰기를 할 일은 없으므로 Allow wirte access 는 체크하지 않습니다.

cat id_rsa.pub

 

 

이제 기본 세팅은 모두 끝났습니다. 본격적인 CICD 잡을 구성하기 위해 새 작업 만들기를 선택합니다. job 은 freestyle project로 진행하겠습니다.

소스코드 관리에서 깃헙과 제대로 연결이 되어있는지 확인하겠습니다. Git을 선택하고 Repository URL 에는 레포지토리 ssh URL 을, Credentials 에는 위에서 등록한 프라이빗 키를 선택합니다.

image

만약, 아래와 같은 경고 메시지가 뜬다면 서버에 깃이 설치되지 않은 것입니다.

sudo yum insatll git

인스턴스로 돌아가 커맨드를 통해 깃을 설치해줍니다.

 

image

 

아무 경고 메시지가 출력되지 않는다면 정상적으로 인증 된 것입니다.

사진에는 없지만 Branch Specifier에는 빌드할 브랜치를 지정합니다. 여기서는 master 브랜치만 빌드하기 위해 master 라고 쓰겠습니다.

여기서 지정하는 브랜치는 "clone 대상" 브랜치 입니다. 즉, 브랜치 필터링과는 다릅니다.
master 브랜치로 지정되어 있는 경우라고 가정하겠습니다. dev 브랜치에 푸쉬를 하고 master 브랜치에는 빌드할 것이 없으면 이 잡은 어떤 소스도 clone 받지 않습니다. 

하지만 지정 브랜치를 dev 로 바꾼 뒤 master 브랜치로 푸쉬를 하면 어떻게 될까요? 

이전에 dev 로 푸쉬한 소스코드를 clone 하게 됩니다. 현재 푸쉬한 브랜치는 master 임에도 말이죠.
그렇기 때문에 이 Branch Specifier 를 브랜치 필터링 용도로 사용하면 안됩니다. 웹훅 사용 시 브랜치 필터링은 잡에서 직접 해주어야합니다.

 

 

빌드유발에서는 Github의 Generic Webhook Trggier 옵션을 선택합니다.(없다면 플러그인이 제대로 설치 되었는지, 설치되었는데도 없다면 플러그인 설치 탭에서 재시작을 한번 하는 것을 추천합니다.)

 

토큰 탭에 Payload URL 파라미터로 추가했던 토큰 값을 입력합니다. 이를 통해 특정 잡에만 웹훅을 줄 수 있습니다.

 

이제 웹훅이 정상적으로 젠킨스로 오는지 확인하기 위해 간단한 스크립트를 작성하겠습니다. 웹훅은 Payload 로 브랜치 정보, 레포정보, 커밋 정보등을 반환합니다. 여기서 이 페이로드를 통해 할 작업은 웹훅을 발동시킨 브랜치가 "master" 브랜치인지 확인하기 위해 브랜치 정보를 받아오는 것입니다.

 

image

 

Post content parameters 에서 ref 즉, 브랜치 정보를 얻어올 수 있습니다. 이 변수를 이용해서 master 브랜치가 아닌 경우엔믄 잡을 실행하지 않을 것입니다.

 

원래는 Value filter 에서 정규식을 통해 같은 작업을 할 수 있습니다. 하지만 지금 처럼 Token 옵션을 사용하는 경우에는 토큰 패스 시 무조건 잡이 실행되기 때문에 스크립트에서 막아야 합니다.(https://stackoverflow.com/questions/51354532/unable-to-filter-the-github-webhook-trigger-in-jenkins)

 

이제 Build 에 Execute shell 을 작성하고

echo '========= start ============='
if [ $ref != "refs/heads/master" ]
    then echo "master branch 가 아닙니다!"
    exit
fi
echo "CI 수행"

위와 같이 작성함으로써 브랜치 필터링이 제대로 되는지 확인합니다.

dev 브랜치에서 소스를 수정하고 푸쉬합니다. Jenkins 잡이 실행된 뒤 Console output 을 보면

image

정상적으로 브랜치 필터링이 된 것을 확인할 수 있습니다.

참고로 이 상태의 젠킨스 잡을 직접 실행하면 ref 에는 빈값이 들어오고 if 문은 문법 오류로 지나치며 CI 를 수행합니다. 공백 체크도 해주어야 하지만 테스트 때문에 직접 실행시키는 일이 따로 하지 않았습니다.

 

 

Chapter 3, Jenkins + CodeDeploy를 이용한 CI/CD 구현

본격적인 작업에 앞서 인스턴스에 IAM 역할을 부여하도록 하겠습니다. 일반적으로 로컬에서 AWS 서비스 사용할 때는 accesskey, secret access key 를 직접 할당했을텐데요, AWS VPC 내의 인스턴스와 서비스는 이처럼 직접 키를 박는 대신 그 서비스에 접근할 수 있는 Role 을 부여함으로써, 또는 그 서비스가 해당 인스턴스에 접근할 수 있는 Role 접근함으로써 더 안전하게 시스템을 구현할 수 있습니다. 그리고 역할을 필요한만큼만 제한하기 떄문에 비정상적인 요청을 피할 수 있습니다. 이를 최소권한의 법칙이라고 하기도 합니다.

Jenkins 서버는 다음의 롤을 필요로 합니다. 사용대상은 물론 EC2 입니다.

젠킨스 서버는 s3 에 개정파일을 푸쉬하고, CodeDeploy 배포생성을 요청합니다. 그렇기 때문에 아래의 권한이 필요합니다. FullAccess 보다 더 쪼갤 수도 있겠지만 있는걸로 쓰겠습니다.

image

 

Step 1. CI 스크립트 작성

jenkins job 실행 시 소스코드 관리에 작성한 레포지토리의 소스코드가 젠킨스의 workspace 에 다운로드됩니다. 직접 확인하고 싶다면

cd /var/lib/jenkins/workspace/job네임 

디렉토리에서 확인해보면 됩니다.

image

제 경우에는 위와같이 다운로드 되어있습니다.

그렇다면 이 위치에서 빌드 명령어를 수행하면 CI 작업은 끝이 납니다. 젠킨스 스크립트를 다음처럼 추가해줍니다.

echo '========= start ============='
if [ $ref != "refs/heads/master" ]
    then echo "master branch 가 아닙니다!"
    exit
fi
echo "==============CI 수행==========="
./gradlew clean build

이제 workspace/job 폴더에는 build/libs 아래에 jar 패키지가 생성되었습니다.(gradle 프로젝트 기준)

이제 이것을 실행하면 배포는 끝납니다. 하지만 그 전에 소스코드 상에 추가해주어야 할 내용이 있습니다.

 

+ 이 스크립트 이전에 invoke gradle script 를 통한 빌드도 가능합니다. 하지만 master 브랜치가 아니면 빌드도 수행하지 않도록 하고 싶어서 스크립트에 직접 같이 작성하였습니다.

 

Step 2. appspec.yml 세팅

우리는 CodeDeploy 를 이용하여 배포합니다. 이때 명세서로 작동하는 것이 "appspec.yml" 파일입니다. CodeDeploy는 이 yml 파일에 명시된대로 배포를 수행합니다. 이 파일은 s3 에 위치해야합니다.(github 옵션도 제공합니다.)

appspec.yml 파일은 1. s3에 직접 넣거나 2. 소스코드와 함께 두거나 할 수 있습니다. 여기서 생각해볼 점은 과연 appspec.yml 파일은 누가 작성하는지 입니다.

개발자? 배포담당자? 인프라담당자?

빠른 소스코드 수정과 배포를 목표로 하는 CI/CD 에 맞추어서 생각해보면 "개발자"가 작성하는 것이 자연스러울 것입니다. 즉 개발자는 소스코드를 수정함과 동시에 이 appspec.yml 파일도 언제든지 수정할 수 있어야합니다. 그래야 빠른 배포가 가능해집니다.

s3 에 yml 파일을 그때그때 넣는다면 소스코드를 푸쉬하기전에 개정파일을 따로 수정하고 저장하고 다시 올리는 번잡한 과정을 수행해야합니다. 그러므로 소스코드와 함께 이 파일을 넣도록 하겠습니다.

프로젝트 루트 디렉토리 아래에 deploy 디렉토리를 생성하고 appspec.yml 파일을 생성해줍니다.

그리고 다음과 같이 내용을 채워줍니다.

version: 0.0
os: linux
files:
  - source: /
    destination: /home/ec2-user/deploy
hooks:
  AfterInstall:
    - location: scripts/run-application.sh
      timeout: 300
      runas: root

file에는 설치할 source 의 위치와 destination 을 입력합니다. appspec.yml 이 위치한 폴더 전체(프로젝트/deploy)를 앱서버 /home/ec2-user/deploy폴더의 libs 디렉토리에 설치할 것 입니다. 그리고 설치 후 scripts/run-application.sh 를 실행하도록 세팅합니다.

디렉토리 구조는 다음과 같습니다.

image

jar 를 실행할 스크립트인 run-application.sh 는 다음을 참고하시기 바랍니다. 간단한 스크립트로 실무에서는 health check 와 같은 좀 더 디테일한 작업이 필요합니다.

function stopJar(){
  pkill -f 'java -jar'
}
function run(){
  nohup java -jar /home/ec2-user/deploy/libs/*.jar > /dev/null 2> /dev/null < /dev/null &
}

stopJar
run

 

그리고 이 자르파일은 배포인스턴스의 scripts 디렉토리의 run-application.sh 에 의해 실행될 것입니다. root 권한으로 말이죠. 설치 이후에 발생할 훅 이벤트이기 떄문에 hooks의 AfterInstall 에 넣는 것이 적합합니다.

 

더 많은 옵션은 https://docs.aws.amazon.com/ko_kr/codedeploy/latest/userguide/reference-appspec-file.html#appspec-reference-server 에서 확인할 수 있습니다.

 

Step 2. 배포 인스턴스 구성

이제 배포할 준비를 마쳤습니다. 배포 인스턴스를 구성하는 방법은 생략하도록 하겠습니다. 다음의 몇가지 경우만 체크하면 됩니다.

1. 스프링부트 프로젝트라면 java 만 깔려있으면 됩니다.

2. ssh 포트와 8080 포트를 자신의 아이피를 허용하도록 보안그룹을 적용해야합니다.

3. CodeDeployAgent 를 설치합니다.

4. 앱서버는 S3 버킷에 접근하여 위에서 배포한 개정파일을 가져와야합니다. 때문에 S3 접근 권한이 필요합니다.

 

3, 4에 대한 자세한 방법은 https://a1010100z.tistory.com/entry/AWSSpring-boot-AWS-CodeDeploy%EB%A1%9C-%EC%9B%B9-%EC%95%A0%ED%94%8C%EB%A6%AC%EC%BC%80%EC%9D%B4%EC%85%98-%EB%B0%B0%ED%8F%AC%ED%95%98%EA%B8%B0?category=820407 포스팅에서 확인 할 수 있습니다.

 

Step 3. CodeDeploy 구성

이제 CD 를 세팅하기 위해 CodeDepioy 서비스를 접속합니다.

참고로 EC2 에 한정하여 CodeDeploy 서비스는 무료입니다. 물론 오토스케일링 그룹을 대상으로 하여 로드밸런싱을 옵션을 선택하는 등 부가적인 리소스를 사용하면 당연히 EC2 온디맨드 요금이 부과됩니다.

애플리케이션을 적절한 이름과 EC2/온프레미스 컴퓨팅 유형으로 생성하고 배포그룹을 생성하도록 하겠습니다.

image

없다면 생성해주어야 합니다. 관리형 정책이기 때문에 쉽게 생성할 수 있습니다.

image

 

환경구성에는 배포를 진행 할, CodeDeployAgent 가 설치되어있는 배포인스턴스를 지정해줍니다. Name 태그를 등록해주었다면 그것으로 찾을 수 있습니다.

image

 

단일 인스턴스 배포이기 때문에 로드밸런서 옵션은 해제합니다.

배포가 실패할 경우를 대비하여 롤백 기능은 허용해주었습니다.

image

그룹 정의를 완료하였다면 배포 생성을 해야하는데 그 전에 s3 에 appspec.yml 을 보관할 버킷을 생성합니다. 인스턴스 리전과 버킷 리전이 같아야 함을 주의합니다. 배포 인스턴스에서 Role 로 접근할 것이기 때문에 모든 퍼블릭 액세스 차단 설정을 해제하지 않아도 됩니다.

 

Step4. 젠킨스 스크립트 수정

CI 작업을 마저 수행하기 위해 젠킨스 스크립트를 수정하겠습니다.

먼저, 위에서 생성한 s3 버킷에 프로젝트의 deploy 디렉토리를 모두 푸쉬해야합니다.

다음 작업들은 AWS CLI 를 이용합니다. 사전에 인스턴스에 각 서비스를 이용할 수 있는 IAM 을 부여했기 때문에 키를 직접 넣을 필요는 없습니다.

아래 스크립트를 추가합니다.

aws deploy push \
  --application-name professor-lol \ #codedeploy application name
  --description "professor-lol revision file" \
  --ignore-hidden-files \
  --s3-location s3://professor-lol-revision-file/deploy.zip \  # s3 버킷/이름
  --region ap-northeast-2 \
  --source . 

 

appspec.yml 이 포함된 개정디렉터리를 s3에 push 하는 CLI 입니다. 이렇게 작성하면 자동으로 zip 번들링까지 수행해줍니다. amazon linux2 를 선택하였다면 aws cli 는 기본으로 설치되어있습니다. 만약 그렇지 않으면 aws --version 을 통해 젠킨스 인스턴스에 aws cli 가 설치되어있는지 확인해야 합니다.

개정파일을 s3 에 푸쉬하였으니 이제 위에서 만든 배포 그룹에 대한 배포 생성을 하면 됩니다. 역시나 젠킨스에서 cli 로 수행하겠습니다.

aws deploy create-deployment \
--application-name "professor-lol" \
--s3-location bucket="professor-lol-revision-file",key=deploy.zip,bundleType=zip \
--deployment-group-name "professor-lol-app-dpg" \
--description "create deployment" \
--region ap-northeast-2

 

 

최종 젠킨스 스크립트는 다음과 같습니다.

echo '========= start ============='
if [ $ref != "refs/heads/master" ]
    then echo "master branch 가 아닙니다!"
    exit
fi

echo "==============CI 수행==========="
./gradlew clean build


# 기존 libs 삭제 후 
rm -rf deploy/libs 

# 새로운 libs 디렉토리 
mv build/libs/ deploy/

echo '=========== 개정파일 push ==============='
aws deploy push \
  --application-name "professor-lol" \
  --description "professor-lol revision file" \
  --ignore-hidden-files \
  --s3-location "s3://professor-lol-revision-file/deploy.zip" \
  --source ./deploy \
  --region ap-northeast-2

echo "================배포생성 =================="
aws deploy create-deployment --application-name "professor-lol" --s3-location bucket="professor-lol-revision-file",key=deploy.zip,bundleType=zip --deployment-group-name "professor-lol-app-dpg" --description "create deployment" --region ap-northeast-2

 

 

Step 5. 마무리

이제 레포지토리에 지금까지 작성한 소스코드를 다시 푸쉬하겠습니다. 물론 위에서 master 로 브랜치를 한정지었으니 마스터로 푸쉬해줍니다.

image

jenkins 는 정상적으로 동작합니다.(89번째 삽질의 흔적)

이렇게 끝이 아닙니다. jenkins 는 CI 툴로 빌드까지만 보장하고 배포는 CodeDeploy 에게 CLI로 요청했습니다

경과를 보기 위해 AWS > CodeDeploy > 배포에서 배포 경과를 확인 할 수 있습니다.

하지만 이 코드디플로이 역시 스크립트의 실행을 보장할 뿐입니다. 만약 배포스크립트가 잘못 짜져있는데 스크립트 종료는 되지 않는다면(예를들어, 기존 jar 가 종료되지 않고 새로운 jar를 배포한다면 스크립트를 실패로 보지 않습니다.) 배포가 정상적이지 않지만 CodeDeploy 는 성공하게 됩니다. 이것이 여기에서는 생략했지만 헬스체크, 모니터링 관제가 필요한 이유입니다.

이렇게 CodeDeploy 를 통한 배포까지 완료하였습니다.

 

 

번외) Jenkins ? 보안 ?

제가 실무에서 쓰는 Jenkins 는 내부망 내에서만 접근이 가능합니다. 따라서 Github Webhooks 을 사용하는 것은 사실상 불가능합니다.

위에서 보안을 신경쓰는 척 하면서 웹훅의 아이피 대역만 jenkins 보안그룹으로 추가해줬습니다. 하지만 그 아이피 대역은 젠킨스만 쓰는 대역일까요? 당연히 아닙니다. 웹훅 요청 파라미터로 Job 을 구분하기는 하지만 이미 8080으로 접근한 이후입니다. 또, 이 파라미터는 주소에 함께 노출됩니다. 

그래서 저는 https://developer.github.com/webhooks/securing/ 에서 참고해보려고 했으나, 이 방식은 젠킨스로 넘기기 전에 한번 헤더의 토큰을 검증하는 별도의 "프록시 서버"를 띄워 올바른 출처의 요청이면 젠킨스로 보내고 그렇지 않으면 거절할 수 있게 하는 용도였습니다. 물론 이 로직은 직접 구현해야합니다. (젠킨스 내에서 직접 체크해보고 싶었는데 해더에 담긴 씨큐어 헤더값을 도저히 얻을 수가 없었습니다.)

저는 과연 토큰 검증을 위핸 서버를 또 띄우는게 맞는걸까 라는 생각이 들었습니다. 웹훅을 통해 오는 페이로드 데이터를 다시 젠킨스로 넘겨주는 로직을 짜는 것도 막막했습니다.

일반적으로 엔터프라이즈 레벨에 사용하는 젠킨스는 웹훅 방식보다는 폴링 방식을 사용한다고 합니다. 웹훅 트리거링방식보다 더 많은 자원을 잡아먹긴 하겠지만 말입니다, https://github.com/woohgit/blog.wooh.hu/blob/master/content/post/trigger-jenkins-build-securely-using-github-and-aws.md 관련 내용은 위 링크를 추천합니다.

 

 

+ Github Action 도 써보니 편하더군여 ..! (https://github.com/appleboy/jenkins-action)

 

 

++ 파이프라인 스크립트를 추가합니다

def gitUrl = "git@github.com:csbsjy/hello-spring.git"

pipeline {

    agent any  

    stages {
        stage('Check Branch'){
           steps{
               sh ''' 
                if [ $ref != "refs/heads/master" ]
                    then echo "master branch 가 아닙니다!"
                    exit 1
                fi
               '''
           }
        }

        stage('Build') {           
            steps {
                git branch: "master", url: "${gitUrl}", credentialsId: "jenkins-server"
                sh "./gradlew clean build"
            }
        }        

        stage('Push RevisionFile ') {
            steps {  
               sh("""\
                aws deploy push \
                --application-name 'professor-lol'\
                --description 'professor-lol revision file'\
                --ignore-hidden-files \
                --s3-location 's3://professor-lol-revision-file/deploy.zip'\
                --source ./deploy \
                --region 'ap-northeast-2'
                """
               )

            }
        }

        stage('Call CodeDeploy'){
            steps{
                sh '''
                rm -rf deploy/libs 
                mv build/libs/ deploy/
                aws deploy create-deployment \
                --application-name "professor-lol" \
                --s3-location bucket="professor-lol-revision-file",key=deploy.zip,bundleType=zip \
                --deployment-group-name "jenkins-test" \
                --description "create deployment" \
                --region "ap-northeast-2"
                '''
            }
        }



    }

}