.gitlab-ci.yml 에서 환경변수를 제대로 읽어오기까지 한참 결렸고 이후 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 암호화 방식을 채택했기에 공개키, 비밀키를 생성한 것이다.
먼저 공개키를 클립보드에 복사한다.
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 에 붙여넣는다.
$REFISTRATION_TOKEN 은
을 사용한다.
그뒤 Runner 가 설치되면 아래 그림처럼 인스턴스에 연결되지 않았다는 문구가 뜰텐데
스택 오버플로우에서 아래와 같은 글을 봐서
sudo gitlab-runer verify
명령어를 쳐보니 다음과 같이 해결되었다.
원래 docker 로 runner 를 등록했었던 터라 매번 docker desktop 을 켜주고 runner 가 돌아가는 컨테이너를 run 시켜줬었는데 local 상에 gitlab runner 를 등록해줬다.
설치는 다음과 같은 과정을 거쳤다.
#!/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
variables 는 protected 인데 해당 브랜치가 protected repository 가 아니여서 variable 을 읽지 못하고 있던 것이었다
수정후 확인해보니 잘 나오며 variable 을 사용할때는 "" 로 감싸든 말든 상관 없는 것 같다.
위의 오류는 AMI 로 linux2 를 사용중에 ssh-agent, ssh-add 명령어를 사용하려면 yum install -y openssh-clients
를 해야 하는데 yum install -y openssh
를 사용하고 있어서 발생했던 오류이다.
사실 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 해결
그러나 해결되지 않았다.
swagger 도 돌아가고 AWS 에 확인해보면 분명히 EC2 인스턴스는 살아 있었다. GitLab CD 설정을 하느라 인스턴스에서 무언가 조작하다가 실수로 EC2 로 접속하는 창구를 부숴버린 것 같았다.
stackoverflow 를 보면 이게 로컬에서 설정하란 것인지 EC2 서버 상에서 설정하란 것인지 잘 이해가 되지 않았다. chmod 는 권한 설정인데 이걸 600 으로 하든 644 로 하든 나랑 관련된 것은 세개의 숫자중 앞의 숫자뿐일 텐데 다른 그룹의 권한을 바꿨다고 왜 되었다는 것인지 이해가 잘 되지 않았다.
그러다가 아래 글을 읽었다.
천천히 글을 읽다보니
이 부분을 보고 원인을 찾았다. 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 뒤에 불필요한 " 를 하나 추가해서 발생했던 오류였다.
와.. 이건 진짜 왜 발생할까
오류를 보면 /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 에 다시 넣어주니 위의 경고창이 사라졌다.
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 를 구현했다.
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 스크립트를 실행한다.
#!/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 구축하기