[한이음 공모전] - 스프링 부트 GitLab CI/CD 파이프라인 완성하기

Yunes·2023년 8월 20일
0

[한이음]

목록 보기
4/5
post-thumbnail

서론

.gitlab-ci.yml 에서 환경변수를 제대로 읽어오기까지 한참 결렸고 이후 ssh 를 연결하는데 너무 큰 장벽이 길을 막아서 한참 방법을 찾아봤다.

드디어 해법을 찾았다.

로컬의 .ssh 디렉토리에서 SSH 키 페어 생성하자

ssh-keygen -t rsa -b 2048 -C "gitlab ssh local key"

위의 명령어에서 -t 는 암호화 알고리즘, -b 는 비트 용량 설정, -C 는 주석을 설정하는 옵션이다.

비밀번호 입력하라는 문구에 비밀번호를 설정해주었다.

결과적으로 .ssh 디렉토리에 id_rsa, id_rsa.pub 생성되었다. 이들은 각각 private key, public key 에 해당하며 이는 RSA 가 양방향 암호화중 RSA 암호화 방식을 채택했기에 공개키, 비밀키를 생성한 것이다.

public SSH key 를 GitLab 계정에 추가하기

먼저 공개키를 클립보드에 복사한다.

tr -d '\n' < ~/.ssh/id_rsa.pub | pbcopy

tr -d '\n' 는 tr (변환) 명령을 사용해서 입력 스트림중 개행문자 \n 를 삭제 -d 한다. 이때의 입력 스트림은 ~/.ssh/id_rsa.pub 에 있는 파일의 내용이다.

< ~/.ssh/id_rsa.pub 는 입력 리디렉션 < 을 사용해서 ~/.ssh/id_rsa.pub 에 있는 파일의 내용을 입력 스트림으로 제공한다.

| pbcopy 는 이전 명령의 내용을 시스템 클립보드에 복사하는 명령어다.

이후 GitLab 에 로그인후 왼쪽 사이드바에서 아바타를 선택한다. 프로필 수정하기를 선택후 SSH Keys 에서 키 추가하기를 선택한다.

위의 명령어에서 복사한 공개키를 Key 에 붙여넣는다.

runner 설치 및 등록

$REFISTRATION_TOKEN 은

을 사용한다.

그뒤 Runner 가 설치되면 아래 그림처럼 인스턴스에 연결되지 않았다는 문구가 뜰텐데

스택 오버플로우에서 아래와 같은 글을 봐서

sudo gitlab-runer verify

명령어를 쳐보니 다음과 같이 해결되었다.

원래 docker 로 runner 를 등록했었던 터라 매번 docker desktop 을 켜주고 runner 가 돌아가는 컨테이너를 run 시켜줬었는데 local 상에 gitlab runner 를 등록해줬다.

설치는 다음과 같은 과정을 거쳤다.

기존 배포파일이었던 deploy.sh 수정

#!/bin/bash

REPOSITORY=/home/ec2-user
GITLAB_PROJECT_NAME=test
PROJECT_NAME=AutomateX

cd $REPOSITORY/$GITLAB_PROJECT_NAME/

echo "> Git Pull"

git pull

echo "> 프로젝트 테스트코드 Test 시작"

./gradlew test

echo "> 프로젝트 Build 시작"

./gradlew build

echo "> step1 디렉토리로 이동"

cd $REPOSITORY

echo "> Build 파일 복사"

cp $REPOSITORY/$GITLAB_PROJECT_NAME/build/libs/*.jar $PROJECT_NAME.jar

echo "> 현재 구동중인 애플리케이션 pid 확인"

CURRENT_PID=$(pgrep -f ${PROJECT_NAME}.jar)

echo "현재 구동 중인 애플리케이션 pid: $CURRENT_PID"

if [ -z "$CURRENT_PID" ]; then
        echo "> 현재 구동 중인 애플리케이션이 없으므로 종료하지 않습니다."
else
        echo "> kill -15 $CURRENT_PID"
        kill -15 $CURRENT_PID
        sleep 5
fi

echo "> 새 애플리케이션 배포"

JAR_NAME=$(ls -tr $REPOSITORY/ | grep jar | tail -n 1)

echo "> JAR Name: $JAR_NAME"

nohup java -jar $REPOSITORY/$JAR_NAME 2>&1

기존엔 GitLab CI/CD 기능을 사용하지 못하고 있었으니 deploy.sh 만 실행해도 빌드, 테스트, 무중단 배포가 이루어지도록 셸 스크립트를 구성했지만 이제 빌드와 테스트를 .gitlab-ci.yml 에서 이루어지도록 하기 위해 빌드와 배포 부분을 제거했다.

#!/bin/bash

REPOSITORY=/home/ec2-user
GITLAB_PROJECT_NAME=test
PROJECT_NAME=AutomateX

cd $REPOSITORY/$GITLAB_PROJECT_NAME/

echo "> Git Pull"

git pull

echo "> step1 디렉토리로 이동"

cd $REPOSITORY

echo "> Build 파일 복사"

cp $REPOSITORY/$GITLAB_PROJECT_NAME/build/libs/*.jar $PROJECT_NAME.jar

echo "> 현재 구동중인 애플리케이션 pid 확인"

CURRENT_PID=$(pgrep -f ${PROJECT_NAME}.jar)

echo "현재 구동 중인 애플리케이션 pid: $CURRENT_PID"

if [ -z "$CURRENT_PID" ]; then
        echo "> 현재 구동 중인 애플리케이션이 없으므로 종료하지 않습니다."
else
        echo "> kill -15 $CURRENT_PID"
        kill -15 $CURRENT_PID
        sleep 5
fi

echo "> 새 애플리케이션 배포"

JAR_NAME=$(ls -tr $REPOSITORY/ | grep jar | tail -n 1)

echo "> JAR Name: $JAR_NAME"

nohup java -jar $REPOSITORY/$JAR_NAME 2>&1

gitlab variables 를 못읽고 있다면?

variables 는 protected 인데 해당 브랜치가 protected repository 가 아니여서 variable 을 읽지 못하고 있던 것이었다

수정후 확인해보니 잘 나오며 variable 을 사용할때는 "" 로 감싸든 말든 상관 없는 것 같다.

너무 높은 CI/CD 의 진입장벽..

위의 오류는 AMI 로 linux2 를 사용중에 ssh-agent, ssh-add 명령어를 사용하려면 yum install -y openssh-clients 를 해야 하는데 yum install -y openssh 를 사용하고 있어서 발생했던 오류이다.

ec2 에 갑자기 접속이 막혔다.

사실 Gitlab 에서 ssh -o StrictHostKeyChecking=no ec2-user@"$DEPLOY_SERVER_IP" '~/test/deploy.sh' 명령어를 사용할때

나는 HostName 을 추가해서 ssh -i ~~~.pem <유저이름>@<탄력적IP> 가 아니라 ssh HostName 으로 EC2 에 접속하고 있었기에 저 명령어가 맞는지 확인하려다가 갑자기 오류가 떴던 것이다.

전에 코딩온에서 다른 크루원의 경우 EC2 인스턴스를 생성했을때 해당 오류와 비슷하게 Permission denied 가 떠서 해결방법을 찾지 못해 바로 EC2 인스턴스를 그냥 새로 만들어줬었다.

로컬에서 sudo vi /etc/ssh/sshd_config 으로 접속후 아래의 주석을 해제하라는 글을 봤다. 퐁스 - Permission denined 해결

그러나 해결되지 않았다.

드디어 Permission denied 에 대한 해결방법을 찾았다.

swagger 도 돌아가고 AWS 에 확인해보면 분명히 EC2 인스턴스는 살아 있었다. GitLab CD 설정을 하느라 인스턴스에서 무언가 조작하다가 실수로 EC2 로 접속하는 창구를 부숴버린 것 같았다.

stackoverflow 를 보면 이게 로컬에서 설정하란 것인지 EC2 서버 상에서 설정하란 것인지 잘 이해가 되지 않았다. chmod 는 권한 설정인데 이걸 600 으로 하든 644 로 하든 나랑 관련된 것은 세개의 숫자중 앞의 숫자뿐일 텐데 다른 그룹의 권한을 바꿨다고 왜 되었다는 것인지 이해가 잘 되지 않았다.

그러다가 아래 글을 읽었다.

Solving EC2 ssh Permission Denied (publickey,gssapi-keyex,gssapi-with-mic) Issue: A Comprehensive Guide for Data Scientists

천천히 글을 읽다보니

이 부분을 보고 원인을 찾았다. GitLab 설정을 하느라 로컬상에서 ssh-keygen 으로 생성한 공개키를 authorized_keys 에 넣어야 하는줄 알고 ec2 에서 authorized_keys 를 제거하고 새로 만들어줬었다. 그러고 해당 공개키를 넣어줬는데 이 공개키가 키페어를 통해 만든 공개키어야 한다는 점을 몰랐던 것이다.

즉, EC2 를 만들때 붙여주는 키페어는 비밀키이고 이를 통해 공개키를 만들어서 EC2 인스턴스 안에서 .ssh/authorized_keys 에 공개키를 넣어줘야 한다.

생각해보면 맨 처음엔 이 과정을 하지 않았고 위의 방법은 기존 인스턴스에 키페어를 추가하는 방법이라 맨 처음엔 자동으로 되는 것 같기도 하다.

그래서 authrized_keys 에 엉뚱한 값이 들어가 있으니 ssh 로 접속을 하지 못했던 것이다.

그래서 EC2 에서 키페어를 하나 생성하고 권한을 600 으로 설정한 뒤 아래 명령어를 입력하면 공개키가 출력된다.

저 공개키를 복사해서 ec2 인스턴스에 들어가 sudo vi .ssh/authorized_keys 혹은 vi .ssh/authorized_keys 를 통해 복사한 공개키를 붙여넣어 줬다.

이 과정은 AWS 공식 홈페이지에서 키페어를 추가 또는 교체하는 방법 페이지를 참고했다.

CI/CD 한다고 비비다가 괜히 멀쩡한 인스턴스 날릴뻔했다.

그럼 기존의 sshKey 로 공개키를 만들어 같은 방법으로 vi .ssh/authorized_keys 에 넣어주면 기존 HostName 으로도 접속할 수 있지 않을까?

복구했다. 🫠 추가로 만든 키페어와 HostName 은 필요없으니 제거했다.

아무튼 찾고 싶었던 것은 다음 명령어다

ssh -i {YOUR_KEY_PAIR_FILE.pem} {USER_NAME}@{AWS_PUBLIC_DNS}

이걸 지금까지 ssh {HostName} 로 써오느라 잊고 있었다.

추가로

unexpected EOF while looking for matching `"'

오류가 떴는데 이건

- ssh -o StrictHostKeyChecking=no -i "$SSH_Key" ec2-user@"DEPLOY_SERVER_IP" 'bash ~/test/deploy.sh'

위와 같이 쳐야 하는데

- ssh -o StrictHostKeyChecking=no -i "$SSH_Key" ec2-user"@"DEPLOY_SERVER_IP" 'bash ~/test/deploy.sh'

ec2-user 뒤에 불필요한 " 를 하나 추가해서 발생했던 오류였다.

WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED!

와.. 이건 진짜 왜 발생할까

오류를 보면 /root/.ssh/known_hosts 에 무언가 정보가 바뀌었다는 것 같은데

GitLab Settings - CI/CD - Variables 에 등록하는 이 환경변수와 관련이 있을 것으로 보였다.

깃랩을 쓰다보니 경로가 보이면 이게 EC2 인스턴스를 말하는 것인지 로컬을 말하는 것인지 알수가 없다. 그래서 더 삽질한 것 같다.

결과적으로 저것은 로컬상에서 찾을 수 있었다.

gitlab.com 과 관련있는 ssh-keygen 된 부분이 여러개 있었다.

ssh-keygen -R gitlab.com

이걸 업데이트해주고 나면 .ssh/known_hosts 에 gitlab.com 관련된 부분이 1개로 갱신된다. ( 기존에 여러개가 들어 있었다. )

vi known_hosts 를 통해 해당 값을 가져오고 이걸 Gitlab variables 에 다시 넣어주니 위의 경고창이 사라졌다.

안끝나는 Jobs..

1시간을 초과해서 Job 이 종료되었다.

원인을 찾았다.

// 내 코드
nohup java -jar $REPOSITORY/$JAR_NAME 2>&1

// 조사후 수정한 코드
nohup java -jar $REPOSITORY/$JAR_NAME &> /dev/null &

& 는 백그라운드에서 명령을 실행하게 한다.

&> /dev/null 는 표준 출력과 표준 오류를 모두 null 장치로 리디렉션해서 모든 출력을 효과적으로 폐기한다.

내 코드는 출력이 캡쳐되나 터미널이나 nohup.out 파일으로 이동한다.

수정한 코드는 출력이 삭제되고 프로세스가 백그라운드에서 실행된다.

nohup 은 no hang up 의 약자로 내가 세션과 연결을 종료해도 지금 실행시킨 프로그램을 종료하지 않도록 하는 명려어이다.

nohup 은 실행하고자 하는 프로그램 명령어 앞에 nohup 만 붙이면 사용할 수 있다.

& 를 프로그램 실행시 맨 끝에 붙여주면 해당 프로그램이 백그라운드로 실행된다.

0 : 표준입력
1 : 표준 출력
2 : 표준 에러

2>&1 는 표준 에러를 표준 출력이 쓰여지는 파일에 리디렉션한다.

ps -ef 을 통해 현재 프로세스가 백그라운드에서 실행중인지 확인할 수 있다.

ps -ef | grep "java -jar /home/ec2-user/AutomateX.jar"

ps : 프로세스 상태, 활성 프로세스에 대한 정보를 표시
-e : 현재 터미널 세션과 연결되어 있는지 여부에 상관없이 모든 프로세스 선택
-f : 프로세스의 전체 형식 목록을 표시 ( 사용자, 프로세스 ID (PID), 시작시간, 프로세스를 시작한 명령어 등 )

nohup 은 프로그램을 데몬의 형태로 실행하기에 세션이 종료되어도 프로그램은 종료되지 않는다.
&(백그라운드) 를 nohup 과 같이 사용시 프로그램을 종료없이 백그라운드에서 실행할 수 있다.

따라서 Jobs 가 끝나지 않은게 아니라 그냥 로컬에서 npm run start 혹은 node index.js 를 한 것처럼 프로그램이 그냥 정상적으로 배포후 실행이 계속 되고 있었던 중에 time out 이 된 것이고

수정한 뒤엔 Foreground 가 아니라 Background 에서 실행되고 있으니 Jobs 은 그대로 종료되더라도 EC2 인스턴스의 Background 에서 계속 실행이 될 수 있던 것이다.

드디어 CI/CD 를 구현했다.

결과

.gitlab-ci.yml

image: amazoncorretto:17

stages:
  - build
  - test
  - deploy

cache:
  paths:
    - .gradle/wrapper
    - .gradle/caches

build:
  stage: build
  script: ./gradlew clean build
  artifacts:
    paths:
      - build/libs/*.jar
    expire_in: 1 week  
  only:
      - main

test:
  stage: test
  script: ./gradlew test


deploy-to-ec2:
  stage: deploy
  before_script:
    - command -v ssh-agent >/dev/null || ( yum update -y && yum install -y openssh-clients )  # openssh-clients 패키지 설치
    - eval $(ssh-agent -s)
    - echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add -
    - mkdir -p ~/.ssh
    - chmod 700 ~/.ssh
    - echo "$SSH_KNOWN_HOSTS" >> ~/.ssh/known_hosts
    - chmod 644 ~/.ssh/known_hosts

  script:
    - ssh -o StrictHostKeyChecking=no -i "$SSH_KEY" ec2-user@"$DEPLOY_SERVER_IP" 'bash ~/test/deploy.sh'
  only: 
    - main

command -v ssh-agent >/dev/null || ( yum update -y && yum install -y openssh-clients )

command -v ssh-agent >/dev/null : 시스템에서 ssh-agent 명령을 사용할 수 있는지 확인한다.

  • command -v : 주어진 명령에 대한 실행 파일을 찾는데 사용된다.
  • >/dev/null : 표준 출력을 null 장치로 리디렉션하여 출력을 억제한다.
  • || 논리합, 선행 명령이 실패할때만 뒤에 오는 명령이 실행된다.

( yum update -y && yum install -y openssh-clients ) : 이전 ssh-agent 검사가 실패시 이 명령이 실행된다.

  • yum update -y : 시스템의 패키지 저장소를 업데이트하고 설치된 모든 패키지를 최신 버전으로 업데이트한다. -y 플래그는 업데이트 과정중 나타나는 모든 프롬프트에 yes 라고 자동으로 대답한다.
  • yum install -y openssh-clients : OpenSSH 클라이언트 패키지를 설치한다. 이 패키지가 설치되어야 ssh-agent, ssh-add 를 실행할 수 있다.

eval $(ssh-agent -s)

eval $(ssh-agent -s) : 현재 셀에서 SSH 에이전트 세션을 시작하는데 사용된다.

  • ssh-agent -s : -s 플래그와(환경변수 할당) 함께 ssh-agent 프로그램을 실행한다.
  • ssh-agent 가 환경변수를 설정하는데 필요한 셸 명령을 출력하도록 지시한다.
  • $(...) : 괄호 안에 있는 명령을 캡처하여 명령줄로 직접 대체한다.
  • eval : ssh-agent -s 의 출력을 가져와 수동으로 입력한 것처럼 처리한다. 이렇게 하면 SSH 연결을 설정할 때마다 키 암호를 수동으로 처리하지 않고도 에이전트를 사용하여 SSH 키를 관리할 수 있다.

echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add -

echo "$SSH_PRIVATE_KEY" : echo 명령을 사용하여 환경변수 SSH_PRIVATE_KEY 의 내용을 출력한다. 환경변수를 "" 로 감싸면 공백이나 특수 문자가 포함된 경우에도 변수의 값을 올바르게 해석한다.
| tr -d '\r' :

  • | 기호는 이전 명령의 출력을 다음 명령의 입력으로 전달하는 파이프 연산자이다.
  • tr : 문자를 번역하거나 삭제하는데 사용된다. -d 와 함께 사용하여 캐리지 리턴 \r 문자를 삭제하는데 사용되었다. 줄바꿈 문자 \n 를 줄 끝 문자로 사용하는 Unix 환경에서 텍스트 파일을 처리할때 캐리지 리턴 문자로 인해 문제가 발생할 수 있다.
  • | ssh-add - : 제공된 SSH 개인키를 SSH 에이전트에 추가하는 명령이다. 뒤의 - 는 표준 입력에서 키를 읽어야 한다는 것을 나타낸다. 이 명령어는 키를 저장하지 않고 SSH 에이전트에 개인키를 추가할 수 있다.

- mkdir -p ~/.ssh
- chmod 700 ~/.ssh
- echo "$SSH_KNOWN_HOSTS" >> ~/.ssh/known_hosts
- chmod 644 ~/.ssh/known_hosts

mkdir -p ~/.ssh : 아직 존재하지 않는 경우 홈 디렉토리에 .ssh 디렉토리를 만들고 -p 프래그는 필요에 따라 상위 디렉토리를 생성한다.
chmod 700 ~/.ssh : .ssh 의 권한을 소유자만 읽고 쓰고 실행할 수 있도록 설정한다.
echo "$SSH_KNOWN_HOSTS" >> ~/.ssh/known_hosts : .ssh 디렉토리의 known_hosts 파일에 SSH_KNOWN_HOSTS 환경변수의 내용을 추가한다.
chmod 644 ~/.ssh/known_hosts : known_hosts 의 파일을 소유자가 읽고 쓸 수 있고 다른 사람들은 읽기만 가능하도록 권한을 설정한다.

echo "$SSH_KNOWN_HOSTS" >> ~/.ssh/known_hosts 명령어는 중복 내용이 추가될 수 있기에 처음 배포후 다음의 명령어로 수정해야겠다.

if ! ssh-keygen -F "$HOSTNAME" -f ~/.ssh/known_hosts >/dev/null; then
    echo "$SSH_KNOWN_HOSTS" >> ~/.ssh/known_hosts
fi

ssh -o StrictHostKeyChecking=no -i "$SSH_KEY" ec2-user@"$DEPLOY_SERVER_IP" 'bash ~/test/deploy.sh'

ssh : 원격 서버에 ssh 연결을 시작한다.
-o StrictHostKeyChecking=no : 엄격한 호스트 키 확인을 비활성화한다.
-i "$SSH_KEY" : 인증에 사용할 개인 키파일의 경로를 지정한다. 이 자리엔 예를 들어 "sshKey.pem" 이 들어올 수 있고 GitLab variables 에 File 타입으로 생성해뒀다.
ec2-user@"$DEPLOY_SERVER_IP" : 연결하려는 원격 서버의 사용자 이름과 IP 주소를 지정한다.
'bash ~/test/deploy.sh' : SSH 연결이 설정된 후 원격 서버에서 실행될 명령어다. 전체 명령이 단일 인수로 처리되도록 '' 로 감쌌다. 이 경우 bash 셸을 실행하고 원격 서버의 ~/test/deploy.sh 스크립트를 실행한다.

deploy.sh

#!/bin/bash

REPOSITORY=/home/ec2-user
GITLAB_PROJECT_NAME=test
PROJECT_NAME=AutomateX

cd $REPOSITORY/$GITLAB_PROJECT_NAME/

echo "> Git Pull"

git pull

echo "> step1 디렉토리로 이동"

cd $REPOSITORY

echo "> Build 파일 복사"

cp $REPOSITORY/$GITLAB_PROJECT_NAME/build/libs/*.jar $PROJECT_NAME.jar

echo "> 현재 구동중인 애플리케이션 pid 확인"

CURRENT_PID=$(pgrep -f ${PROJECT_NAME}.jar)

echo "현재 구동 중인 애플리케이션 pid: $CURRENT_PID"

if [ -z "$CURRENT_PID" ]; then
        echo "> 현재 구동 중인 애플리케이션이 없으므로 종료하지 않습니다."
else
        echo "> kill -15 $CURRENT_PID"
        kill -15 $CURRENT_PID
        sleep 5
fi

echo "> 새 애플리케이션 배포"

JAR_NAME=$(ls -tr $REPOSITORY/ | grep jar | tail -n 1)

echo "> JAR Name: $JAR_NAME"

nohup java -jar $REPOSITORY/$JAR_NAME 2>&1

전체를 빌드, 테스트, 배포하는데 총 3분정도 걸렸다.

빌드와 배포는 only: - main 옵션을 넣어둬서 로컬에선 테스트만 jobs 로 돌리고 main 으로 머지될때 build, test, deploy 가 자동으로 실행되도록 설정했다.

GitLab 으로 CI/CD 파이프라인은 이렇게 구현했다.

레퍼런스

blog
juhyun10 - .gitlab-ci.yml 총정리
김도리개발자 - 스프링부트 GitLab CI/CD
Lurutia - GitLab CI/CD
린아저씨의 잡학사전 - nohup, &
leesomyoung - CI/CD를 위한 gitlab pipeline 구축하기

profile
미래의 나를 만들어나가는 한 개발자의 블로그입니다.

0개의 댓글

관련 채용 정보