본문 바로가기
Docker

Springboot docker GitHub Action 연동하여 자동 배포하기

by YoonJong 2023. 1. 20.
728x90

지난 번 로컬배포 및 ec2 배포에 이어서 GitHub Action 을 연동하려고 한다. ( nginx 사용 X )

 

cicd 경험이 없어서 테스트겸 테스트 해보려고 레포지토리를 만들어서 간단한 github action + s3 를 이용해보았다.

https://github.com/dbswhd4932/cicdproject

 

환경변수도 없어서 금방 진행할 수 있었는데 프로젝트에 적용해보려고 하니 정말 많은 오류가 많았고 구글링을 해보아도 내 프로젝트랑 환경이 달라서 금방 적용할 수 없어 많은 시행착오를 겪었다.

 

먼저, 아주 간단히 플로우를 정리하면 아래와 같다.

1. 개발자가 코드를 main 브랜치에 푸시 ( PR 도 가능 )

2. deploy.yml 에서 설정한 flow 대로 실행

3. gradle build 를 진행

4. 도커 허브에 push

5. ec2 에서 도커 허브의 이미지를 pull

6. 이전에 구동되었던 컨테이너를 종료 및 삭제, 이미지를 삭제한다.

7. pull 받은 이미지를 컨테이너로 올려 실행한다.

 

Docker 를 사용하면 어떤 OS 에서도 같은 환경을 만들어주기 때문에 서버에 docker 만깔고 배포만 하면 정상적으로 작동된다. ( 테스트할때는 jdk 깔고 ruby 깔고 여러 환경설정을 해주었는데 아무리 ec2 를 삭제하고 만들어도 docker 만 깔아주면 되어서 정말 편했다. )

 

GitHub Action 을 연동하면, 개발자가 설정에 따라 브랜치에 푸시, PR 등 실행이 되었을 때 자동으로 workflow 를 읽어 실행한다.

 

ec2를 생성하고 권한을 만드는 것은 구글에 너무 자료가 많아 생략한다.

 

Dockerfile 을 생성한다.

Dockerfile 은 배포에 용이하고 컨테이너가 특정행동을 수행하게 할 수 있다.

다양한 설정이 가능하지만, 배포만을 위한 file 을 만들어준다.

( docker-compose 는 사용했다가 rds 를 사용하면서 삭제했다 )

 

FROM : 도커 베이스 이미지( java8 이면 8 로 , java11 이면 11로 )

EXPOSE : 포트

 

ARG : 컨테이너 내에서 사용할 수 있는 변수를 지정

COPY : 선언했던 JAR_FILE 변수를 컨테이너의 app.jar로 복사

ENTRYPOINT : 컨테이너가 시작되었을 때 스크립트 실행

FROM openjdk:17-jdk
EXPOSE 8080
ARG JAR_FILE=build/libs/*.jar
COPY ${JAR_FILE} app.jar
ENTRYPOINT ["java","-jar","/app.jar"]

 

build/libs/ 에서 jar 파일을 찾는데, 나는 특정 jar 를 찾지 않고 * 로 해주었다.

원래 build 를 진행하면 plain 이 있는 파일 1개, 없는 파일 1개가 생성되는데 grdle 에서 아래 설정을 통해 plain 생성을 막을 수 있다.

jar {
    enabled = false
}

 

 

이제 deploy.yml 을 작성한다.

name: Java CI CD with Gradle
on:
  push:
    branches:
      - main
  pull_request:
    branches:
      - main

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - name: 저장소 Checkout
        uses: actions/checkout@v3

      - name: 셋업 JDK 17
        uses: actions/setup-java@v2
        with:
          java-version: '17'
          distribution: 'adopt'

      - name: gradlew 권한 부여
        run: chmod +x ./gradlew
        shell: bash

      - name: gradle 빌드
        run: ./gradlew build
        shell: bash

      # spring 어플리케이션 Docker Image 빌드
      - name: 도커 build & 도커 허브 push
        run: |
          docker login -u ${{ secrets.DOCKER_USERNAME }} -p ${{ secrets.DOCKER_PASSWORD }}
          docker build -t ${{ secrets.DOCKER_USERNAME }}/${{ secrets.DOCKER_REPO }} .
          docker push ${{ secrets.DOCKER_USERNAME }}/${{ secrets.DOCKER_REPO }}

      # 서버에서 Docker 이미지 실행
      - name: 도커 허브 pull & 도커 run
        uses: appleboy/ssh-action@master
        with:
          host: ${{ secrets.EC2_WAS_IP }}
          username: ec2-user
          key: ${{ secrets.PRIVATE_KEY }}
          script: | 
            docker stop ${{ secrets.DOCKER_REPO }}
            docker rm ${{ secrets.DOCKER_REPO }}
            docker rmi ${{ secrets.DOCKER_USERNAME }}/${{ secrets.DOCKER_REPO }}
            docker pull ${{ secrets.DOCKER_USERNAME }}/${{ secrets.DOCKER_REPO }}
            docker run --name ${{ secrets.DOCKER_REPO }} -e AWS_ACCESS_KEY_ID=${{ secrets.AWS_ACCESS_KEY_ID }} -e AWS_SECRET_KEY=${{ secrets.AWS_SECRET_ACCESS_KEY }} -e AWS_REGION=ap-northeast-2 -e cloud.aws.credentials.access-key=${{ secrets.CLOUD_AWS_CREDENTIALS_ACCESS_KEY }} -e cloud.aws.credentials.secret-key=${{ secrets.CLOUD_AWS_CREDENTIALS_SECRET_KEY }} -e cloud.aws.s3.bucket=${{ secrets.S3_BUCKET }} -e cloud.aws.region.static=ap-northeast-2 -e spring.datasource.url=${{ secrets.DATABASE_URL }} -e spring.datasource.username=${{ secrets.DATABASE_USERNAME }} -e spring.datasource.password=${{ secrets.DATABASE_PASSWORD }} -p 8080:8080 -d ${{ secrets.DOCKER_USERNAME }}/${{ secrets.DOCKER_REPO }} .

많이 길지는 않지만 하나하나 살펴보려고한다.

깃헙액션과 연동하는 부분에 있어서 정말 많은 블로그를 참고했는데, 자신에게 맞는 설정이 필요하다.

 

name: Java CI CD with Gradle
on:
  push:
    branches:
      - main
  pull_request:
    branches:
      - main

name  : 아무거나 지정해도 상관없다.

main 브랜치에 푸시 또는 main 브랜치에 PR 을 진행하면 깃헙액션이 작동한다.

 

jobs:
  build:
    runs-on: ubuntu-latest

자신에게 맞는 버전을 사용하면 된다고하는데, 우분투 랑 리눅스랑 이전버전 최신버전을 전부 테스트해보았는데 고정으로 사용해도 상관없는 것 같다. 전부다 작동 잘된다.

 

steps:
  - name: 저장소 Checkout
    uses: actions/checkout@v3

Git으로 체크아웃, Repository 세팅 등을 진행하며 v3 는 버전을 의미한다.

https://stackoverflow.com/questions/73473617/what-is-the-difference-between-github-actions-checkoutv3-and-v2

 

- name: 셋업 JDK 17
  uses: actions/setup-java@v2
  with:
    java-version: '17'
    distribution: 'adopt'

자바 17버전을 사용하고 있기 때문에, 17로 세팅한다.

17버전을 다운로드 받아 세팅해준다.

11버전을 사용하면 11버전으로 세팅해야한다.

 

- name: gradlew 권한 부여
  run: chmod +x ./gradlew
  shell: bash

- name: gradle 빌드
  run: ./gradlew build
  shell: bash

gradlew 실행권한은 처음에 막혀있다.

따라서 chmod 로 권한을 부여하고 빌드를 진행한다.

 

# spring 어플리케이션 Docker Image 빌드
- name: 도커 build & 도커 허브 push
  run: |
    docker login -u ${{ secrets.DOCKER_USERNAME }} -p ${{ secrets.DOCKER_PASSWORD }}
    docker build -t ${{ secrets.DOCKER_USERNAME }}/${{ secrets.DOCKER_REPO }} .
    docker push ${{ secrets.DOCKER_USERNAME }}/${{ secrets.DOCKER_REPO }}

먼저 | 표시는 한줄로 표현하는 것이 부족할때 줄 나눠서 작성할 때 사용한다 . 

도커 허브에 푸시하려면 도커에 로그인이 필요하다. 

-u 도커허브아이디 -p 도커허브패스워드 로 작성하면 된다.

 

build -t 를 잘 확인해보자.

도커 허브에 이미지를 푸시하기 위해서는 이미지 이름을 잘 생성해야 한다.

( 문법인가.. 앞에 도커허브아이디를 안붙이면 푸시가 안되었다 )

예를 들어 도커허브 아이디가 test1234 이면 docker build -t test1234/repo이름 이렇게 작성되는 거라고 생각하면 된다.

docker push 또한 build 한 이미지를 push 하는 것이기 때문에 똑같이 작성한다.

 

# 서버에서 Docker 이미지 실행
- name: 도커 허브 pull & 도커 run
  uses: appleboy/ssh-action@master
  with:
    host: ${{ secrets.EC2_WAS_IP }}
    username: ec2-user
    key: ${{ secrets.PRIVATE_KEY }}
    script: | 
      docker stop ${{ secrets.DOCKER_REPO }}
      docker rm ${{ secrets.DOCKER_REPO }}
      docker rmi ${{ secrets.DOCKER_USERNAME }}/${{ secrets.DOCKER_REPO }}
      docker pull ${{ secrets.DOCKER_USERNAME }}/${{ secrets.DOCKER_REPO }}
      docker run --name ${{ secrets.DOCKER_REPO }} -e AWS_ACCESS_KEY_ID=${{ secrets.AWS_ACCESS_KEY_ID }} -e AWS_SECRET_KEY=${{ secrets.AWS_SECRET_ACCESS_KEY }} -e AWS_REGION=ap-northeast-2 -e cloud.aws.credentials.access-key=${{ secrets.CLOUD_AWS_CREDENTIALS_ACCESS_KEY }} -e cloud.aws.credentials.secret-key=${{ secrets.CLOUD_AWS_CREDENTIALS_SECRET_KEY }} -e cloud.aws.s3.bucket=${{ secrets.S3_BUCKET }} -e cloud.aws.region.static=ap-northeast-2 -e spring.datasource.url=${{ secrets.DATABASE_URL }} -e spring.datasource.username=${{ secrets.DATABASE_USERNAME }} -e spring.datasource.password=${{ secrets.DATABASE_PASSWORD }} -p 8080:8080 -d ${{ secrets.DOCKER_USERNAME }}/${{ secrets.DOCKER_REPO }} .

 

분명 도커허브에 이미지가 잘들어갔는데 pull 이 안되어서 고생했다.

이미지가 이상한가 하고 ec2 에서 직접 pull 하고 컨테이너화 시키면 잘 배포가 되었다.

 

users 는 고정이다. 

with 부터 보면되는데 host 는 인스턴스의 탄력적 IP 주소를 넣어주면된다. (탄력적 안쓰면 퍼블릭 IPv4)

다른 곳에서는 퍼블릭 IPv4 DNS 를 사용하면 된다고 하는데, 탄력적 IP 로 해도 잘되어서 이걸로 사용했다.

만약 못읽어오면 바꿔서 진행해보면 될거 같다.

 

username 은 ec2-user 를 사용한다 ( 우분투는 ubuntu 로 사용하는 거 같다 )

 

key 부분이 도대체 뭐가 들어가야 하는지 엄청해맸다.

ssh 키를 넣으라고 하는데 무슨 말인지를 몰라서 cmd 에서 ssh 키를 생성하고 ec2 .ssh 로 들어가서 인증키를 바꿔버렸다.

그러니까 당연히 ec2 를 재접속하려고하니 에러가 발생했다. ec2를 삭제하고 다시 생성했다...

no supported authentication methods available (server sent publickey gssapi-keyex gssapi-with-mic)

 

PRIVATE_KEY에는 처음에 ec2 생성할 때 키페어에서 rsa 키를 생성하는데 해당 키값을 넣어주면된다.

생성할때 꼭 저장해두니, 그 파일을 메모장으로 열어서 처음부터 끝까지 복사해서 넣어주면된다.

( ---BEGIN 부터 PRIVATE KEY ---) 까지

 

그리고 스크립트를 작성한다.

이부분은 ec2 환경에서 직접 실행한다고 생각하면 생각보다 금방 작성할 수 있었다.

위에서 도커허브에 push까지 했으니, pull 하고 컨테이너를 만들면되는데, 만약에 실행되고 있는 컨테이너랑 이미지가 있으면 먼저 stop 시키고 컨테이너 삭제 -> 이미지 삭제 하고 pull 받은 이미지를 실행시키자! 라고 생각하면된다.

 

 

docker stop ${{ secrets.DOCKER_REPO }} : 컨테이너 중지
docker rm ${{ secrets.DOCKER_REPO }} : 컨테이너 삭제
docker rmi ${{ secrets.DOCKER_USERNAME }}/${{ secrets.DOCKER_REPO }} : 이미지 삭제
docker pull ${{ secrets.DOCKER_USERNAME }}/${{ secrets.DOCKER_REPO }} : 도커허브 이미지 pull

 

 

docker run --name ${{ secrets.DOCKER_REPO }} 

-e AWS_ACCESS_KEY_ID=${{ secrets.AWS_ACCESS_KEY_ID }} 

-e AWS_SECRET_KEY=${{ secrets.AWS_SECRET_ACCESS_KEY }} 

-e AWS_REGION=ap-northeast-2 

-e cloud.aws.credentials.access-key=${{ secrets.CLOUD_AWS_CREDENTIALS_ACCESS_KEY }} 

-e cloud.aws.credentials.secret-key=${{ secrets.CLOUD_AWS_CREDENTIALS_SECRET_KEY }} 

-e cloud.aws.s3.bucket=${{ secrets.S3_BUCKET }} -e cloud.aws.region.static=ap-northeast-2 

-e spring.datasource.url=${{ secrets.DATABASE_URL }} 

-e spring.datasource.username=${{ secrets.DATABASE_USERNAME }} 

-e spring.datasource.password=${{ secrets.DATABASE_PASSWORD }} -p 8080:8080 

-d ${{ secrets.DOCKER_USERNAME }}/${{ secrets.DOCKER_REPO }} .

 

run 부분은 보기쉽게 줄바꿈했는데 일자로 쭉 작성해야한다. 엔터치고 줄바꿈하면 에러발생

run 할 때 -e(환경변수) 줄바꿈하면 에러가 난다.

 

모든 환경변수를 -e로 지정해주어야 한다. 

지정해주지 않으면 해당 값을 찾을 수 없다고 나온다 ( value 를 못찾는다 )

처음에는 왜 지정해주어야 하지?? 라고 생각했다. 

로컬에서 전부 정해주면 되는거 아닌가 라고 생각했는데 도커는 리눅스 환경에서 실행했기 때문에 로컬에서 설정한 파일을 읽을 수가 없었다.

따라서 10개면 10개 모두 작성해주는 것이 필요하다.

 

 

마지막으로 지금까지  ${{ secrets.xxxxx }} 에 들어간 값은 꼭 아래에서 넣어주어야 한다.

 

key / value 값처럼 설정할 수 있으며, name 은 대/소문자 언더바(_) 만 사용이 가능하다.

주의? 해야할 것이 저장하고 나서 다시 Secret 값을 볼 수 없다 ( 편집을 해도 빈칸으로 나온다 )

필요시 따로 값을 저장해야 한다.

 

 

여기까지 하면 이제 스프링부트 + 깃헙액션 + 도커를 이용한 배포설정이 완료되었다.

개인프로젝트라 API 밖에없어서 cicd 테스트를 따로 간단하게 만들어 진행했으며, PR 또한 브랜치를 하나 만들어서 보내보는 형식으로 테스트했다.

@RestController
public class CicdHealthCkeck {

    @GetMapping("/hello")
    public String hello() {
        return "cicd test! ";
    }
}

 

성공!

 

 

 


아래와 같은 에러가 발생하면 먼저 ec2 권한을 살펴보자.

dial tcp [ec2의 IP]: ***: i/0 timeout

ssh 에서 자기 자신IP 만 설정되어있을 수 있다. 방화벽문제이므로 22 포트를 열어주면 해결된다.

https://www.inflearn.com/questions/546424/ci-cd%EB%B6%80%EB%B6%84-appleboy-ssh-action-master-%EC%A7%88%EB%AC%B8%EC%9E%85%EB%8B%88%EB%8B%A4

 

CI/CD부분 appleboy/ssh-action@master 질문입니다. - 인프런 | 질문 & 답변

name: web-character-project CI/CD on:   push:     branches: [master] jobs:   build:     name: Build     runs-on:...

www.inflearn.com

 

728x90

댓글