github Actions 만으로 지속적 배포(CD) 못하나?

POII·2023년 5월 29일
4
post-thumbnail

서버를 배포한 지 이틀 째

github 에 코드의 변경을 push 할 때마다 ssh 접속해서 ./deploy.sh 를 입력해주는 과정이 너무 귀찮았다.

귀찮음을 넘어 만약 API 명세가 수정됐는데 서버 배포를 깜빡하게 되면 협업의 방해요소로 작용하게 될 가능성이 컸다.

github 레포에 푸쉬하자마자 서버에 배포해주면 좋을텐데..

라는 고민을 하고 jenkins 를 적용해보려는 찰나.. 브라운🐻 이 한 마디를 던지고 가셨다.

인스턴스 메모리가 작아서 jenkins 적용이 힘들 수도 있어요

그래서 든 생각

Github Action 만으로 자동배포 못하나?

생각해보면 안될 이유도 없었다. 그냥 레포 변경 생기면 Github Actions안에서 내 ec2 인스턴스에 접속해서

미리 작성해놓은 쉘 스크립트(git pull, build, jar실행 하게 작성해놓음) 실행시키면 알아서 빌드 실행되는데..

첫 시도

그렇게 작성한 workflow는 다음과 같다.

name: Deploy to EC2

on:
  push:
    branches:
      - deploy

jobs:
  deploy:
    runs-on: ubuntu-latest

    steps:

      - name: Setup SSH
        uses: webfactory/ssh-agent@v0.5.0
        with:
          ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }}

      - name: Connect to EC2
        run: ssh -o StrictHostKeyChecking=no ubuntu@${{ secrets.EC2_IP }} "~/deploy.sh"

위에서 작성한 workflow는 프로젝트 루트 경로에서 .github 디렉터리를 만들어 위치시켜야 한다.

예시

work flow 설명

work flow를 위에서부터 설명하면 이런 구조다.


이름짓기(Optional)

name: Deploy to EC2

workflow를 대표하는 이름이다. 여기서 기능은 딱히 없고 보기 편하라고 지정했다.

ec2에 배포를 위한 workflow니까 뜻 그대로 Deploy to EC2 라고 명명했다.


트리거 생성

on:
  push:
    branches:
      - deploy

workflow가 언제 실행될 지 지정한다.(트리거를 생성한다.)

우리의 경우 deploy라는 branch에 어떤 내용이 push 됐을 때 workflow가 실행되도록 지정하였다.

트리거란?
간단하게 설명하면 워크플로를 실행하게 하는 이벤트이다.
더 자세하게 알고싶다면 공식문서 참조! 한글이라 보기도 편하다.




작업 할 영역 지정

jobs:
  deploy:
    runs-on: ubuntu-latest
...

Github Action은 기본적으로 우리의 컴퓨터에서 작업을 실행하지 않는다.

Github에서 제공하는 가상 머신에서 작업을 실행한다. 이러한 가상 머신을 runner 라고 부른다.

위 처럼 runs-on태그를 이용해 작업을 실행할 가상 머신의 환경을 선택할 수 있다.

어차피 우리는 ssh로 우테코에서 제공해준 ec2에 접속해서 명령어를 날려주기만 할거니까 적당히 명령어 알기 편한 ubuntu 쓰기로 했다. 버전은 latest로.




상세 작업 지정

		steps:
      - name: Setup SSH
        uses: webfactory/ssh-agent@v0.5.0
        with:
          ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }}

      - name: Connect to EC2
        run: ssh -o StrictHostKeyChecking=no ubuntu@${{ secrets.EC2_IP }} "~/deploy.sh"

steps태그 밑에 실행할 작업들은 순차적으로 나열한다.

먼저 ssh 접속을 위해 private-key 파일을 설정해준다.

ssh-private-key 태그를 사용해 ssh 접속에 사용할 키를 지정해주었다.

그런 후 ssh 명령어를 가상머신에서 실행하여 미리 지정해준 ip를 통해 우리의 ec2에 접속해 deploy.sh 를 실행하도록 하였다.


주의점

ssh에 접속하기 위한 key.pem 파일은 아무데나 올리면 우리의 인스턴스가 채굴에 사용되어, 인스턴스를 제공해준 우테코에서 막대한 손해를 입고 우리는 구구 코치와 상담하러 갈 수도 있다.

우리는 이를 안전하게 사용하기 위해 Repository Secrets 에 저장했다.

Settings - Secrets and Variables/Actions - New repository secret 을 이용해 Github Action에 사용할 repository secret들을 추가할 수 있다.

위 처럼 등록할 수 있다.

자 이제 완벽하다 배포가 잘 되는지 확인해보기 위해 deploy 브랜치에 아무 내용이나 푸쉬해보았고 예상대로 배포가 잘 됐다.

.
.
.
.
.
.
.
.

.
.
.
.

끝 아님

이런 에러가 발생했다.

원인은 우리의 인스턴스가 특정 ip에서만 ssh 연결을 허용하고 있기 때문이다.

인스턴스 설정에서 보안그룹 - 인바운드 규칙을 확인하면 알 수 있다. 잠실 캠퍼스의 ip를 허용하고 있다는 것을 알 수 있다.

아까 설명했듯 Github Actions는 우리의 컴퓨터에서 작업을 실행하지 않는다.

runner가 어디에 있는지 모르지만, 적어도 잠실캠퍼스는 아니다.

즉, ssh로 접속하여 deploy.sh를 실행하게 하도록 할 수 없다.. ㅠㅠ

토미🍅 코치가 ssh 연결에 대해 풀어줄 생각은 없다고 단언했기 때문에 이 방법은 더 이상 시도할 수 없다. 가망이 없다..

그렇게 포기하려던 찰나 에코🗯️가 우리에게 한 마디를 던지고 갔다.

self-hosted runner로 workflow를 ec2에서 직접 실행하도록 할 수 있어. 누누⛄는 성공했던데?



Self-hosted runner?

: 공식 문서에 따르면, self-hosted runner는 GitHub Actions의 일부로 사용자 정의된 머신에서 작업을 실행할 수 있는 환경을 제공합니다.

사용자는 자체 서버, 가상 머신 또는 물리적 머신에 self-hosted runner를 등록하고, GitHub Actions에서 작업을 실행할 때 해당 runner를 대상으로 지정할 수 있습니다. 이렇게 하면 소스 코드와 작업 데이터가 사용자가 소유한 환경에서 안전하게 유지될 수 있으며, 실행 시간과 자원에 대한 더 많은 제어권을 가질 수 있습니다.

GitHub Actions 공식 문서 : (https://docs.github.com/en/actions/hosting-your-own-runners))

라고 공식 문서에 써있다.

요약하면 workflow를 Github 에서 제공하는 가상머신이 아니라, 우리가 직접 지정한 환경에서 실행할 수 있다는 뜻이다.

즉, 우리의 ec2 인스턴스에서 특정 이벤트(트리거)가 발생할 때 마다 workflow를 직접 실행할 수 있다.


ec2인스턴스를 Self-hosted runenr로 등록하기

self-hosted-runner를 다음과 같은 과정으로 등록할 수 있다.

Repository에 들어가서

Settings - Runners - New self-hosted runner를 클릭한다.

그러면 밑과 같이 runner의 image, architecture를 선택할 수 있는 창으로 이동한다.

우리의 ec2는 ubuntu 이기 때문에 Linux를 선택해주고,

(혹시 아직 운영체제를 공부하지 않은 분들을 위해 설명하자면, ubuntu는 Linux 기반의 운영체제이다.)

Architecture는 ARM64를 선택해준다.

그럼 밑과 같은 명령어가 쭉 표시된다.

그냥 우리 인스턴스에 들어가서 해당 명령어를 실행해주면 등록이 완료된다.

config.sh 를 실행하면 아래와 같은 화면이 표시된다.

runner group을 입력하라는 뜻인데, 이는 여러대의 runner에서 작업을 실행하고 싶을 때 사용할 수 있는 기능이다.

지금은 무시하자~ 그냥 엔터를 누르면 Default 그룹으로 설정된다.

그러면 다음으로는 runner 이름을 지정하라는 메시지가 표시된다.

runner 이름으로 사용할 것을 입력해주자.

마지막으로 runnerlabel을 입력하라는 창이 표시된다.

labelrunner를 구별할 때 사용된다. 이번에는 별도의 커스텀 label 대신 기본으로 등록되는 self-hosted label 만 사용할 것이기 때문에 그냥 엔터를 눌러주자.

그러면 러너에 대한 설정은 완료된다.

진짜 마지막으로 등록한 self-hosted runner안에서 작업을 실행할 디렉터리를 설정할 수 있다.

따로 입력하지 않으면 actions-runners 디렉터리 하위에 _work 디렉터리가 생성되고 그곳에서 작업이 실행된다.

이것까지 입력하면 진짜로 설정이 완료되고

./run.sh 파일을 실행하면 우리의 인스턴스가 Github Actions의 트리거가 언제 발생하는지 listen 하는 상태가 된다.

그냥 실행하면 터미널을 종료하는 순간 listen 상태도 종료되므로, nohup& 를 사용해 파일을 실행하는 것을 추천한다.

그렇게 실행하고 다시 Settings- Runners로 들어가보면 우리의 인스턴스가 지정한 이름으로 잘 등록되어, listen하고 있는 것을 확인할 수 있다.


다시 workflow 작성하기

이렇게 runner를 등록하고 다음과 같이 workflow를 수정하였다.

name: Deploy to EC2

on:
  push:
    branches:
      - deploy

jobs:
  deploy:
    runs-on: self-hosted
    defaults:
      run:
        working-directory: ../..
    steps:
      - name: deploy
        run: |
          chmod +x ./deploy.sh
          ./deploy.sh

다시 처음부터 어떤 부분이 바뀌었는지 살펴보자

    runs-on: self-hosted

기존 workflow에서는 runs-on 태그에 ubuntu-latest 를 지정하여, Github에서 제공하는 ubuntu 가상머신을 이용하도록 하였다.

그러나 이번에는 self-hosted라는 label 을 이용하여 우리가 등록한 self-hosted runner(우리의 ec2 인스턴스) 가 작업을 실행하도록 하였다.

    defaults:
      run:
        working-directory: ../..

defaults 태그를 이용하여 작업을 실행하기 전 실행할 디렉터리를 바꿔주도록 하였다.

왠지 모르겠지만 자꾸 디렉터리가 처음에 설정해준 .../_work 디렉터리가 아닌 …/_work/jwp-shopping-order/jwp-shopping-order 에서 작업이 실행된다.. 아직도 이유를 파악하지 못했다.

해결을 돕고싶다면 클릭..

혹시 이유 아시는 분 알려주시면 정말 감사하겠습니다 .ㅠ..

이 문제 해결하고 싶으신 분들 참고 하시라고 쉘 스크립트도 첨부합니다.

```
cd jwp-shopping-order

git checkout deploy

git pull

echo "> 빌드를 시작합니다."

./gradlew bootJar

cd build/libs

CURRENT_PID=$(pgrep -f jwp-shopping-order.jar)

if [ -z "$CURRENT_PID" ]; then
	echo "> 실행중인 프로그램이 없습니다"
else
	echo "> 실행중인 프로그램을 종료합니다"
	kill -15 $CURRENT_PID
	sleep 3
fi

echo "> 새 어플리케이션을 실행합니다"
nohup java -jar jwp-shopping-order.jar &
```
    steps:
      - name: deploy
        run: |
          chmod +x ./deploy.sh
          ./deploy.sh

| 를 이용해서 여러줄의 명령을 실행하도록 할 수 있다.

먼저 실행 권한이 없을 수도 있으니 ./deploy.sh(미리 작성해둔 배포 스크립트) 에 chmod +x 명령으로 실행 권한을 부여하고 이를 실행하도록 하였다.

결과는 성공!!

인 줄 알았는데 ec2에서 실행 중인 프로그램을 확인해보니..

아무 파일도 실행되고 있지 않았다…..

어째서… 우리 좋았잖아.. 위에서 Started JwpCartApplication 이라고 했잖아!!!

이유는 여기서 찾을 수 있었다..

Terminate orphan process : pid (14324) (java)

원인분석

Github Actions는 각 작업들이 독립적인 환경에서 실행된다. (이는 워크플로우 실행 환경의 자원 관리와 격리를 보장하고, 워크플로우 간에 서로 영향을 주지 않도록 하기 위함이다)

이를 위해 각 작업을 별도의 실행 컨텍스트를 만들어 실행한다.

각 작업이 실행되는 동안, 해당 작업에 정의된 단계(steps)들이 만들어진 컨텍스트에서 순차적으로 실행되고, 작업이 완료되면 해당 작업의 실행 컨텍스트는 종료된다.

이 때, 해당 컨텍스트 안에서 실행된 프로세스도 전부 종료된다…

근데 여기서 한 가지 의문이 생겼다.

누누❄️는 어떻게 했지?

직접 찾아가서 확인해본 결과 누누와 우리의 차이점은 쉘 스크립트를 실행할 때 권한을 su로 했다는 것 뿐이었다.

이유는 8080대신 80포트를 이용하기 위해서(well-known port 인 80 포트를 이용하려면 su 권한이 필요하다)

그런데 그 이유만으로 프로세스가 종료되지 않았다..



왜 누누☃️만 종료가 안될까

찾아본 결과 이는 Github Actions의 동작 원리와 관련이 있다.

GitHub Actions에서 작업은 보안을 위해 특정 사용자 권한으로 실행된다.

그리고 모든 작업이 끝나면 위에서 설명한대로 실행했던 컨텍스트를 종료함과 함께 프로세스도 전부 종료시킨다.

하지만 여기서 종료되는 프로세스는 Github Actions에서 사용하는 사용자 권한으로 실행된 프로세스에 국한된다.

Linux 기반의 운영체제에서 sudo로 명령어를 실행하면 super user, 즉 root 사용자의 권한으로 프로세스를 실행하게 된다.

이렇게 실행된 프로세스는 Github Actions에서 사용하는 일반 사용자의 권한으로 실행된 프로세스가 아니므로 Github Actions는 이를 종료하지 않는다.

또, 만약 Github Actions가 이를 종료하려고 하더라도, 일개 사용자의 권한으로는 막강한 super user의 권한으로 실행한 프로세스를 종료할 수 조차 없다.



결론

name: Deploy to EC2

on:
  push:
    branches:
      - deploy

jobs:
  deploy:
    runs-on: self-hosted
    defaults:
      run:
        working-directory: ../..
    steps:
      - name: deploy
        run: |
          chmod +x ./deploy.sh
          sudo ./deploy.sh

이처럼 sudo 명령어만 붙여주게 되면 우리가 의도했던 대로 deploy 파일을 실행하여 배포를 진행하고 해당 프로세스를 종료하지 않는다.

다만 찝찝한점은 super user 권한으로 실행된다는 점.. 만약 super user를 이용할 수 없는 환경이거나, 이용해서는 안되면 환경이라면 이 방법은 적합하지 않다.

다만 이번 미션에서는 jenkins도 이용할 수 없고(아마), ssh 접속도 특정 ip에 제한되어 있다.

이런 제한된 환경이라는 것을 고려했을 때, 나쁘지 않았던 배포 방법이 아니었을까 싶다.



Thanks to.

제나

토리

에코

누누

브라운

토미

profile
https://github.com/poi1649/learning

2개의 댓글

comment-user-thumbnail
2023년 7월 16일

포포이요포이요 잘보고 갑니당

1개의 답글