포스트

Github actions으로 ec2 배포 자동화 하기

Github 와 Github Actions을 이용한 EC2 배포 자동화


  • 이번에는 EC2Spring Boot프로젝트를 Docker로 빌드해서 AWS ECR에서 이미지를 관리하고 EC2에서 docker-compose를 사용해서 서버를 실행하는 배포 자동화를 알아 보도록 하겠다.



Spring Boot API서버 CI/CD 구성


EC2 CI_CD

간단한 구성도

프로젝트 root경로에 위치하는 Dockerfile를 빌드시키고 ECR에 올린 후, EC2에서 Redispull받은 Spring프로젝트를 EC2에 만들어 둔 docker-compose파일을 이용해서 같은 네트워크에서 컨테이너를 만들어 실행 시킨다.




GitHub Actions를 이용한 CD 구성


해당 프로젝트의 GitHub Repository로 접속해 Actions 탭을 클릭


img.png


java with gradle을 검색하고 Configure 클릭


img.png


이름을 알맞게 입력해 주고


img.png


Commit changes... 클릭 후,

초기 커밋 메세지와 상세 설명입력 후 커밋해준다.


img_1.png


여기끼지 CI/CD를 구성하기 위한 yml파일을 미리 만들어 두었다.
다음으로는 환경을 구성하기 위한 사전 작업진행해야 한다.




Submodule 세팅

  • 프로젝트에서 환경을 세팅해 주는 정보는 민감정보이기 때문에 yml파일을 그대로 노출해서 사용하면 위험하다.
  • 그렇기 때문에 private로 관리할 수 있는 Submodule프로젝트를 하나 생성해서 민감정보를 포함하는 yml파일을 관리한다.

  • GitHub Actions에서 해당 프로젝트에 포함되어 있는 Submodule을 사용하려면 Token을 발급하고 발급받은 Token으로 접근 후, 초기화를 진행해 줘야 한다.




Submodule private 프로젝트 생성


우선 submodule로 사용할 private프로젝트를 만들어 준다.


img.png


  1. yongjun-store-submodule로 이름을 한번에 알아볼 수 있도록 설정
  2. private로 비공개 설정
  3. 초기 커밋을 위해서 README를 생성


img.png


위와 같이 원하는 파일만 올려두고 나머지 필요없는 파일들은 전부 제거해 준다.


그 후, submodule을 사용할 프로젝트에 Git Bash로 접속
submodule이 필요한 경로까지 이동해 준다.


img.png


git submodule add {submodule Repo URL} 입력


ex).

1
git submodule add https://github.com/yongjun96/yongjun-store-submodule.git


해당 명령어를 실행하게 되면 root경로에 .gitmodules파일이 생성된다.


img.png


submoduleURLadd해준 경로가 path에 설정된다. 당연히 임의로 수정해도 작동한다.
path.gitmodules이 존재하는 root경로를 기준으로 한다.
그 후, 커밋/푸쉬해 준다면 적용된다.


submodule프로젝트도 관리해줘야 하기 때문에

1
git clone https://github.com/yongjun96/yongjun-store-submodule.git


프로젝트를 clone해 주고

1
2
3
git submodule init

git submodule update


submodule을 초기화하고 update한다.
처음 딱 한번만 진행해 주면 된다.




submodule update 내용 반영하기


submodule프로젝트도 엄연히 독립적인 Repository로 관리가 되기 때문에, 변경 사항이 생겼을 때 무작정 메인 프로젝트에서 수정해서 반영하면 안된다.

img.png


이런 식으로 메인 프로젝트에서 직접 변경 사항을 커밋/푸쉬하게 되면 submoduleGit History가 누락되거나 꼬일 수 있기 때문에 하지 않도록 하자.


그렇다면 어떻게 해야 할까?


방법은 직접 submodule프로젝트로 접근해 변경 사항을 커밋/푸쉬해주면 된다.

img.png

변경 사항을 커밋/푸쉬해 주었다면 메인 프로젝트에 Git Bash로 접근해서 submodule경로 까지 이동해 준다.


img.png


이렇게 이동하면 밑줄 친 것 처럼 변경 사항이 있다고 표시된다.

1
2
3
git fetch

git merge origin/main


위와 같이 fetch를 하고 main브런치에 merge하면 submodule의 변경 사항이 메인 프로젝트에 merge된다.

img.png




운영 환경에 맞춰 yml 구성


그리고 운영 환경에 맞춰 yml을 다르게 구성해야 하는 경우

img.png


현재 yml이 위치한 경로를 기준으로 import를 이용해 submoduleyml파일을 여러개 가져와서 적용 시킬 수 있다.




Submodule Token 생성


프로젝트의 Settings가 아닌 계정의 Settings로 들어가 준다.

img.png


하단에 위치한 Developer settings 클릭

img.png


Personal Access Tokens - Tokens (classic) - Generate new token (classic) 클릭


img.png


note에 토큰의 이름을 입력해 주고 repo권한만 체크해 준 후, 원하는 Expiration 토큰 기한을 설정해 준다.
No expiration를 선택하면 기한 없이 계속 유효한 토큰이 생성되지만 보안상 추천하지 않는다.


img.png


이렇게 발급하게 되면 토큰 값을 복사해 놓는다.




환경 변수 세팅


Spring boot RepositorySettings - Secrets and variables - Actions 이동

img_1.png


세팅해야 하는 환경 변수는 크게 3가지로 나뉜다.

  1. ECR에 접속하기 위한 AWS_ACCESS_KEY_IDAWS_SECRET_ACCESS_KEY / AWS 계정의 AWS_ACCOUNT_IDAWS_REGION
  2. EC2에 접근하기 위한 EC2_HOST, EC2_USERNAME, EC2_PRIVATE_KEY, EC2_SSH_PORT
  3. submodule의 접근하기 위한 SUBMODULE_TOKEN

img.png


자세히 알아보면

1 . AWS_ACCESS_KEY_ID : AWS - IAM 대시보드 - 사용자 - 사용 중인 사용자 선택 - 보안 자격 증명 - 엑세스 키 만들기 or 사용 중인 엑세스 키 확인 으로 값을 받을 수 있다. SECRET_ACCESS_KEY를 까먹었을 경우 기존에 사용하던 엑세스 키를 삭제하고 다시 만들어야 한다… 꼭 안전하게 기록하고 보관하자.

2 . AWS_ACCOUNT_ID : AWS의 해당 계정 ID를 복사해서 입력하면 된다.

img_1.png

3 . AWS_REGION : 자신이 사용하고 있는 리전을 입력하면 된다. 굳이 환경 변수로 사용하지 않아도 된다. 기본 적으로 한국을 사용할 것 이기 때문에 아시아 태평양 (서울)ap-northeast-2를 입력한다.

4 . AWS_SECRET_ACCESS_KEY : AWS_ACCESS_KEY_ID와 한 쌍으로 AWS_ACCESS_KEY_ID를 발급 받을 때 한번만 값을 확인 할 수 있다. 발급 받은 SECRET_ACCESS_KEY를 입력해 준다.

5 . EC2_HOST : EC2에 접근할 때 필요한 퍼블릭 IPv4 주소 입력 ex). 127.0.0.1

6 . EC2_PRIVATE_KEY : EC2 내부에 있는 ~/.ssh/id_rsa의 값을 입력해야 한다. 아래 에서 자세히 서술 예정.

7 . EC2_SSH_PORT : EC2에 접근할 때 사용할 포트 번호를 지정한다. ex). 22

8 . EC2_USERNAME : centos를 사용하고 있으므로 centosusernameec2-user를 입력

9 . SUBMODULE_TOKEN : 위에서 생성한 SUBMODULE_TOKEN의 토큰 값을 입력해 주면 된다.




EC2_PRIVATE_KEY 확인 하기


일단 해당 부분에서 좀 많은 시간을 소모했다.

EC2_PRIVATE_KEY로 사용하는 id_rsagitHub의 접근을 허용해 주는 SSH 공개 키id_rsa.pub의 지식이 없어서 엄청나게 많은 시도를 하면서 커밋 내역이 쌓여 버렸다..

뒤에 설명할 yml 파일 세팅에서 push했을 때 이벤트를 실행하도록 했기 때문에 yml을 수정하고 push하면서 커밋 내역이 엄청 쌓인 것이다.

제대로 세팅되기 전에는 RepositoryFork해서 테스트 해봤어야 했는데, 빨리 해결해야 된다는 마음에 그렇게 하지 못해서 아쉬운 부분이다.




먼저 EC2에 접속해야 한다.
접속은 AWS에서 EC2 인스턴스 연결로 해도 되고 xshell이라는 프로그램을 이용해도 된다.


img.png


1
2
3
cd ~

cd .ssh

이동 해주면 기본적으로 id_ed25519id_rsa가 존재할 것이다. 나의 경우, id_rsa를 사용할 것이다.


1
cat id_rsa

해당 명령어로 id_rsa의 값을 복사해 준다.


주석을 포함해서 전부 복사해 준다.

—–BEGIN OPENSSH PRIVATE KEY—–

—–END OPENSSH PRIVATE KEY—–


6번 EC2_PRIVATE_KEY 값은 해당 값이 들어가야 한다.
SSH로 접근하려면 해당 인스턴스비공개 SSH 키id_rsa값이 필요하기 때문이다.




yml 작성

  • 이제 사전 세팅은 끝났다.
  • GitHub Actions를 사용하기 위해 ymlCI/CD명령을 구성하면 된다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
name: CI/CD Pipeline

on:
  push:
    branches:
      - main

jobs:
  build:
    runs-on: ubuntu-latest
    
    steps:
    - name: GitHub Action에서 현재 Repository를 체크아웃 및 서브모듈 사용
      uses: actions/checkout@v2
      with:
        submodules: 'recursive'
        token: ${{ secrets.SUBMODULE_TOKEN }}


    - name: Docker Buildx, 다중 플랫폼 이미지를 생성하고 관리하기 위해 필요한 Docker 환경을 설정
      uses: docker/setup-buildx-action@v1


    - name: 이미지 빌드
      run: docker build -t yongjun-store .


    - name: 이미지에 latest 태그 추가
      run: docker tag yongjun-store:latest ${{ secrets.AWS_ACCOUNT_ID }}.dkr.ecr.ap-northeast-2.amazonaws.com/yongjun-store:latest


    - name: Login to AWS ECR
      env:
        AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
        AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
      run: aws ecr get-login-password --region ap-northeast-2 | docker login --username AWS --password-stdin ${{ secrets.AWS_ACCOUNT_ID }}.dkr.ecr.ap-northeast-2.amazonaws.com


    - name: Push Docker image to Amazon ECR
      run: docker push ${{ secrets.AWS_ACCOUNT_ID }}.dkr.ecr.ap-northeast-2.amazonaws.com/yongjun-store:latest


    - name: SSH로 EC2를 접근해 명령 시작
      uses: appleboy/ssh-action@master
      with:
        host: ${{ secrets.EC2_HOST }}
        username: ${{ secrets.EC2_USERNAME }}
        key: ${{ secrets.EC2_PRIVATE_KEY }}
        port: ${{ secrets.EC2_SSH_PORT }}
        script: |
          ssh ${{ secrets.EC2_USERNAME }}@${{ secrets.EC2_HOST }} -p ${{ secrets.EC2_SSH_PORT }} "sudo docker stop $(docker ps -a -q)"
          ssh ${{ secrets.EC2_USERNAME }}@${{ secrets.EC2_HOST }} -p ${{ secrets.EC2_SSH_PORT }} "sudo docker rm $(docker ps -a -q)"
          ssh ${{ secrets.EC2_USERNAME }}@${{ secrets.EC2_HOST }} -p ${{ secrets.EC2_SSH_PORT }} "sudo docker rmi ${{ secrets.AWS_ACCOUNT_ID }}.dkr.ecr.ap-northeast-2.amazonaws.com/yongjun-store"          
          ssh ${{ secrets.EC2_USERNAME }}@${{ secrets.EC2_HOST }} -p ${{ secrets.EC2_SSH_PORT }} "aws ecr get-login-password --region ap-northeast-2 | docker login --username AWS --password-stdin ${{ secrets.AWS_ACCOUNT_ID }}.dkr.ecr.ap-northeast-2.amazonaws.com"
          ssh ${{ secrets.EC2_USERNAME }}@${{ secrets.EC2_HOST }} -p ${{ secrets.EC2_SSH_PORT }} "sudo docker pull --pull always ${{ secrets.AWS_ACCOUNT_ID }}.dkr.ecr.ap-northeast-2.amazonaws.com/yongjun-store:latest"
          ssh ${{ secrets.EC2_USERNAME }}@${{ secrets.EC2_HOST }} -p ${{ secrets.EC2_SSH_PORT }} "sudo docker-compose -f /home/ec2-user/docker-compose.yml down && docker-compose -f /home/ec2-user/docker-compose.yml up -d"

구간 별로 잘라서 알아 보자.



보이는 대로 main브런치가 push될 때 작업을 수행한다는 뜻이다.
ubuntu환경의 최신 버전에서 작업을 수행한다.

1
2
3
4
5
6
7
8
on:
  push:
    branches:
      - main

jobs:
  build:
    runs-on: ubuntu-latest




해당 구간은 name에도 적어 놓았듯 현재 Repository를 체크아웃하고 recursivesubmodule을 초기화 하고 token을 이용해 접근 권한을 얻는다.

1
2
3
4
5
    - name: GitHub Action에서 현재 Repository를 체크아웃 및 서브모듈 사용
      uses: actions/checkout@v2
      with:
        submodules: 'recursive'
        token: ${{ secrets.SUBMODULE_TOKEN }}




buildx는 다양한 플랫폼 및 아키텍처에 대한 빌드 작업을 수행할 수 있도록 Docker CLI를 확장하는 기능이다. 컨테이너 이미지다양한 플랫폼에서 사용할 수 있도록 빌드하고 배포하기 위해서 설정했다.

1
2
    - name: Docker Buildx, 다중 플랫폼 이미지를 생성하고 관리하기 위해 필요한 Docker 환경을 설정
      uses: docker/setup-buildx-action@v1




프로젝트 root경로에 존재하는 Dockerfile을 빌드하는 구간이다.

1
2
    - name: 이미지 빌드
      run: docker build -t yongjun-store .


Dockerfile의 이름은 항상 Dockerfile이여야 한다. 여러 Dockerfile을 사용하고 싶다면 다른 경로에 Dockerfile을 생성해서 사용하면 된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
FROM openjdk:17-alpine AS builder

COPY gradlew build.gradle settings.gradle ./
COPY gradle ./gradle
COPY src/main ./src/main

RUN chmod +x gradlew
# gradle 이 로컬에 설치되지 않아도 gradle을 사용할 수 있게 해줌
RUN ./gradlew clean build

FROM openjdk:17-alpine

COPY --from=builder /build/libs/yongjun-store-0.0.1-SNAPSHOT.jar /app.jar

ENTRYPOINT ["java", "-jar", "-Dspring.profiles.active=prod", "/app.jar"]

간단하게 jar를 빌드해서 prod환경으로 실행할 수 있도록 구성했다.




이미지의 이름을 ECR에서 관리하는 이미지명과 같도록 수정하고 태그 또한 항상 latestpush하도록 설정해 주었다.

1
2
    - name: 이미지에 latest 태그 추가
      run: docker tag yongjun-store:latest ${{ secrets.AWS_ACCOUNT_ID }}.dkr.ecr.ap-northeast-2.amazonaws.com/yongjun-store:latest




ECRDocker Imagepush하기 위해 미리 세팅해 둔 환경 변수를 이용해서 접근해 준다.

1
2
3
4
5
    - name: Login to AWS ECR
      env:
        AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
        AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
      run: aws ecr get-login-password --region ap-northeast-2 | docker login --username AWS --password-stdin ${{ secrets.AWS_ACCOUNT_ID }}.dkr.ecr.ap-northeast-2.amazonaws.com




빌드한 Docker Imagepush

1
2
    - name: Push Docker image to Amazon ECR
      run: docker push ${{ secrets.AWS_ACCOUNT_ID }}.dkr.ecr.ap-northeast-2.amazonaws.com/yongjun-store:latest




여기서 부터는 SSHEC2에 접근하여 docker-composeredis와 같은 네트워크를 공유하는 Spring Boot프로젝트를 실행한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
    - name: SSH로 EC2를 접근해 명령 시작
      uses: appleboy/ssh-action@master
      with:
        host: ${{ secrets.EC2_HOST }}
        username: ${{ secrets.EC2_USERNAME }}
        key: ${{ secrets.EC2_PRIVATE_KEY }}
        port: ${{ secrets.EC2_SSH_PORT }}
        script: |
          ssh ${{ secrets.EC2_USERNAME }}@${{ secrets.EC2_HOST }} -p ${{ secrets.EC2_SSH_PORT }} "sudo docker stop $(docker ps -a -q)"
          ssh ${{ secrets.EC2_USERNAME }}@${{ secrets.EC2_HOST }} -p ${{ secrets.EC2_SSH_PORT }} "sudo docker rm $(docker ps -a -q)"
          ssh ${{ secrets.EC2_USERNAME }}@${{ secrets.EC2_HOST }} -p ${{ secrets.EC2_SSH_PORT }} "sudo docker rmi ${{ secrets.AWS_ACCOUNT_ID }}.dkr.ecr.ap-northeast-2.amazonaws.com/yongjun-store"          
          ssh ${{ secrets.EC2_USERNAME }}@${{ secrets.EC2_HOST }} -p ${{ secrets.EC2_SSH_PORT }} "aws ecr get-login-password --region ap-northeast-2 | docker login --username AWS --password-stdin ${{ secrets.AWS_ACCOUNT_ID }}.dkr.ecr.ap-northeast-2.amazonaws.com"
          ssh ${{ secrets.EC2_USERNAME }}@${{ secrets.EC2_HOST }} -p ${{ secrets.EC2_SSH_PORT }} "sudo docker pull --pull always ${{ secrets.AWS_ACCOUNT_ID }}.dkr.ecr.ap-northeast-2.amazonaws.com/yongjun-store:latest"
          ssh ${{ secrets.EC2_USERNAME }}@${{ secrets.EC2_HOST }} -p ${{ secrets.EC2_SSH_PORT }} "sudo docker-compose -f /home/ec2-user/docker-compose.yml down && docker-compose -f /home/ec2-user/docker-compose.yml up -d"


1 . 먼저 현재 실행 중인 컨테이너를 종료시킨다.

1
sudo docker stop $(docker ps -a -q)

2 . 존재하는 컨테이너를 전부 삭제시킨다.

1
sudo docker rm $(docker ps -a -q)

3 . ECR에서 pull받았던 이전 이미지를 삭제시킨다.

1
sudo docker rmi ${{ secrets.AWS_ACCOUNT_ID }}.dkr.ecr.ap-northeast-2.amazonaws.com/yongjun-store

4 . ECR에 로그인 해 준다.

1
aws ecr get-login-password --region ap-northeast-2 | docker login --username AWS --password-stdin ${{ secrets.AWS_ACCOUNT_ID }}.dkr.ecr.ap-northeast-2.amazonaws.com

5 . 최신버전이미지pull받는다.

1
sudo docker pull --pull always ${{ secrets.AWS_ACCOUNT_ID }}.dkr.ecr.ap-northeast-2.amazonaws.com/yongjun-store:latest

6 . docker-compose 파일에 정의된 모든 컨테이너, 네트워크, 볼륨, 및 기타 서비스들을 중지하고 제거
6-1 . docker-compose를 다시 실행

1
sudo docker-compose -f /home/ec2-user/docker-compose.yml down && docker-compose -f /home/ec2-user/docker-compose.yml up -d




docker-compose.yml 작성


  • /home/ec2-user/docker-compose.yml해당 경로에 docker-compose.yml을 생성해서 관리한다.
  • docker-compose.yml또한 이름은 docker-compose.yml고정으로 사용하며, 다른 compose파일을 사용하고 싶다면 경로를 다르게 설정해서 사용해야 한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
version: '3.4'

services:
  redis:
    image: redis
    container_name: redis
    ports:
      - "6379:6379"
    networks:
      - my-network

  yongjun-store:
    image: ${{ secrets.AWS_ACCOUNT_ID }}.dkr.ecr.ap-northeast-2.amazonaws.com/yongjun-store
    container_name: yongjun-store
    ports:
      - "80:8081"
    networks:
      - my-network
    depends_on:
      - redis

networks:
  my-network:
    driver: bridge


1 . redisyongjun-store를 같은 네트워크 상에서 구동 시킬 수 있도록 하기위해 구성했다.

2 . image:를 통해서 image의 이름을 지정해주고 container_name:으로 원하는 컨테이너 이름을 설정해 준다.

3 . ports:로 사용하고 싶은 port를 지정해 주고, networks:를 이용해서 같은 network를 공유하도록 둘 다 같은 my-network를 입력해 주었다.

4 . depends_on:를 이용해서 yongjun-storeredis를 의존하도록 지정하고 redis서비스가 시작될 때까지 대기한 후에 시작 되도록 설정.

5 . networks:설정으로 my-network:라는 네트워크를 생성하고, driver:를 사용하여 네트워크 유형을 bridge로 지정

6 . bridge네트워크는 Docker기본 네트워크 드라이버 중 하나로, 여러 컨테이너가 동일한 네트워크에서 통신할 수 있도록 해준다.




redis 설치

redisEC2서버에 직접 다운받아서 이미지를 관리하는 방식으로 사용하도록 구성했다.

1
2
3
4
# redis가 존재하지 않으면 알아서 latest로 pull받고 6379포트로 실행
docker run --name redis -d -p 6379:6379 redis
# 간단하게 redis-cli를 실행하고, redis컨테이너에 연결하여 ping / pong 확인 해보기
docker run -it --link redis:redis --rm redis redis-cli -h redis -p 6379


--link redis:redis해당 옵션은 구식이기 때문에 사용을 권장하지는 않지만, 간단한 테스트를 할 때는 사용하기 좋다.




배포 해보기


모든 세팅이 끝났고 코드를 push해서 배포해 보자.

img.png


정상적으로 빌드가 성공한 모습.


img.png


서버에서도 정상적올 컨테이너가 실행되었다.


img.png


Swagger에 접속도 성공하였다.




마무리


이번에 CI/CD프론트백엔드 전부 jenkins가 아닌, GitHub Actions을 통해서 구현해 보았다.

jenkins는 서버를 따로 만들어 관리해 줘야 한다는 불편함이 있었는데 GitHub Actions은 그런 불편함을
해소해 주었다.
하지만 private프로젝트는 일정 시간 이후에는 과금이 된다는 점이 부담이 될 수도 있다는 생각이 많이 들었다.
CI/CD 테스트를 진행할 때 fork를 따로 만들어서 진행해야 한다는 사실도 알게 되었다.

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.