본문 바로가기
Study/Devops

[GCP] Cloud Run과 Cloud Build를 사용하여 Spring Boot 배포하기

by 리노 Linho 2022. 12. 5.

시스템 아키텍처

Local 설정

Spring Boot의 application-prod.yml 설정

Github에 DB 정보를 올릴 수 없고, 없으면 서비스를 실행할 수 없기에 보안 문제가 발생하지 않고 DB정보를 컨테이너에 담을 수 있을지 고민했다. 결과적으로 환경변수를 사용하기로 했다.

위와 같이 설정하면 application-prod.yml에 시스템 환경변수 값을 불러올 수 있다.

Dockerfile 설정

Cloud Run의 경우 컨테이너 기반으로 서비스를 실행하기 때문에 도커를 활용하여 빌드하였다.

FROM adoptopenjdk/openjdk11

COPY gradlew .
COPY gradle gradle
COPY build.gradle .
COPY settings.gradle .
COPY src src
RUN chmod +x ./gradlew
RUN ./gradlew build --exclude-task test

RUN cp ./build/libs/*.jar ./app.jar

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

FROM 으로 JAVA 11을 컨테이너에 세팅하고 Spring boot를 빌드합니다.

  • RUN ./gradlew build --exclude-task test : gradlew로 빌드하지만 테스트 코드를 생략한다.
  • EXPOSE 9090 : 9090포트를 오픈한다.

GCP Cloud Run 설정

배포 방법 선정

일회성으로 사용될 컨테이너의 경우 "기존 컨테이너 이미지에서 버전 1개 배포"를 사용한다.

변동사항이 생길때 마다 특정 트리거에 의해 서비스가 컨테이너가 새롭게 실행되게 하려면 "소스 저장소에서 지속적으로 새 버전 배포"를 선택한다. 이를 선택할 경우 Cloud Build와 연동하여 버전 업데이트시 자동으로 컨테이너가 새로운 버전으로 교체된다.

 

이후 서비스 이름과 리전을 선택한 후 아래 항목으로 이동한다.

Cloud Run에서 Continuous deployment를 위한 Cloud Build 설정
( "소스 저장소에서 지속적으로 새 버전 배포" 선택시 )

프로젝트 코드가 담긴 Github 레포지토리를 연결한다.

해당 레포지토리가 트리거로 설정되어 변동사항이 생길시 자동 배포가 이뤄지도록 한다.

트리거 설정 방법은 여러가지가 있다.

브랜치 명을 설정하여 어떤 브랜치에 변경이 확인되었을때 트리거를 발동시킬지 선정한다.

Cloud Run은 도커 이미지를 기반으로 컨테이너를 통해 서비스를 제공함으로 Dockerfile을 선정한다.

CPU 할당 및 가격 책정, 자동 확장 설정, 인그레스, 인증 설정

최근에는 Severless 형태가 뜨고있다. 요약하자면 서버를 상시로 켜놓는 것이 아니라 요청이 발생했을 때만 잠시 사용되는 느낌이다.

자동 확장의 경우 트래픽이나 인스턴스의 리소스 사용량이 증가하면 인스턴스의 갯수를 조정하여 트래픽에 대응할 수 있다. 해당 프로젝트는 많은 양의 트래픽이 발생할 경우가 현재까지 없음으로 인스턴스의 갯수를 1개로 고정했다.

 

추가적으로 인그레스와 이증의 경우 백엔드 서버를 운용 중이기 때문에 "모든 트래픽 허용"하고 인증 또한 "인증되지 않은 호출 허용"으로 선택하였다.

 

컨테이너 설정

컨테이너에서 사용하는 PORT를 우선으로 설정하였다.

application-prod.yml에서 프로덕션용 PORT를 9090으로 설정했기에 9090으로 지정하였다.

GCP 공식 문서에 따르면 수치로 작성하기보단 환경변수로 PORT를 설정하는걸 권장한다.

컨테이너의 스펙을 설정한다.

기본 스펙과 크게 달라진 건 없지만 메모리를 128MB로 진행했을 때 스프링 부트 프레임워크를 실행하면 리소스가 부족하다.

따라서 512MB로 변경하여 진행하였으며 자세한 오류 내용은 글 하단에서 확인할 수 있다.

⭐️  GCP Secret Manager

Spring Boot의 비밀정보를 어떻게 관리하면 될까?

고민의 시작은 "Spring boot에서는 DB 접속 정보와 같이 중요한 내용을 어떻게 저장하는가?" 였다.

먼저 떠오른건 Github Actions를 사용하여 AWS로 배포시에는 Github Actions에 Actions 전용 Secret 키를 담을 수 있었다.

다른 웹 프레임워크에서 사용할때는 빌드하는 과정에서 .env 파일을 하드코딩으로 파일을 제작했다.

이와 비슷한 역할을 하는게 GCP는 Secret Manager가 있다.

AWS도 Secret Manager이 있기 때문에 Github Actions secret key와 GCP secret manager은 동일하지 않다.
다만 GCP는 Cloud build를 통해 배포 workflow를 관리하기 때문에 배포 과정에서도 Secret Manager에 접근할 수 있다.

따라서 Secret Manager를 사용하면 된다는 것을 알게되었고 어떻게 사용하면 좋을지 고민하게 되었다.

고민 요소는 다음과 같다.

  1. Github에 중요한 정보를 업로드할 수 없다.
  2. Docker 컨테이너 이미지에 중요한 정보를 담을 수 없다.
  3. application.yml 자체를 암호화(Jasypt)하는건 보안상 만족스럽지 않다. ➡️ 결과적으로 암호키를 따로 제공해야 한다.
  4. 최소한의 클라우드 비용을 사용하고 싶다 ➡️ Config Server를 따로 구축할 수 없다

결과적으로, Spring Boot에서는 시스템 환경변수를 불러와 비밀 정보를 알 수 있도록하며 Docker 컨테이너 실행시 환경변수로 비밀 정보를 넘겨주기로 했다.

그럼 Spring Boot의 application-prod.yml에 값을 어떻게 넣는가?

글 최상단에 나와있는 application-prod.yml을 보면 ${DB_PASSWD}와 같이 값이 적혀있는데 이는 시스템의 환경 변수중 DB_PASSWD라고 지정되어있는 값을 불러오는 것이다.

따라서 본 프로젝트에서는 빌드된 도커 이미지를 실행시키는 과정에서 환경변수 ENV를 통해 넘겨주기로 했다.

친절하게도 Cloud Run은 환경변수 설정 방법이 매우 잘 나와있다.

Secret Manager 사용방법

Secret Manager는 중요한 정보들을 각각 생성하고 저장해놔야한다.

예를들어, omoji-db-password라는 이름의 객체가 있으면 여기에는 한가지 값 밖에 들어갈 수 없다.

이렇게 될 경우 비밀 정보를 버전 별로 관리할 수 있으며 가장 최신의 정보만 불러오도록 하는 것도 가능하다.

먼저, Cloud run에서는 위와 같이 보안 비밀을 설정하는 탭을 따로 확인할 수 있다.

이전에 Secret Manager로 이동하여 Secret Manager를 활성화 시키고 값을 생성한다.

위 사진처럼 이름에는 식별용 이름을 기입하고 실제 보호되어야 할 값은 "보안 비밀 값"에 추가하면 된다.

직접 작성할 수 도 있고 파일을 업로드할 수 있는데 글쓴이는 보안 비밀 값을 직접 작성하였다.

규칙은 정히져 있지 않고 보호되어야할 값을 입력하면 만들 수 있다.

위 사진 처럼 각각의 DB 정보와 Spring Security를 관리할 값을 입력한다.

이후 Cloud run으로 돌아와 생성한 비밀코드를 환경변 수로 사용할 수 있도록 등록해주어야한다.

Cloud run으로 돌아오면 내가 생성한 보안 비밀키가 생성되어있을 것이다.

여기서 Reference 메서드를 환경 변수로 노출하도록 설정하면 자연스럽게 컨테이너 실행시 환경변수로 등록할 수 있다.

application-prod.yml

application-prod.yml을 참고하여 DB_HOST를 키 이름으로 하고 버전의 경우 최신 버전의 값을 불러오도록 하였다.

 

위 사진처럼 사용한 환경 변수 등록이 모두 끝나면 컨테이너 실행시 환경변수가 설정되는 것을 알 수 있다.

설정을 완료하고 빌드를 시작하면 위 사진 처럼 빌드 성공 여부를 Cloud Build 기록에서 확인할 수 있다.

진행하면서 발생한 오류

⚠️ 컨테이너의 메모리를 128MB로 진행시 Spring Boot 실행시 리소스 부족

오류 코드

Memory limit of 128M exceeded with 130M used. Consider increasing the memory limit, see https://cloud.google.com/run/docs/configuring/memory-limits
Cloud Run container instances that exceed their allowed memory limit are terminated. Consider increasing the memory limit of your Cloud Run service.

해결 방법

메모리를 512MB로 변경하여 문제를 해결하였다.

 

⚠️ GCP에서 Docker 컨테이너 이미지로 빌드시 문제가 발생

오류코드

COPY build/libs/*.jar app.jar

본래 위 코드를 사용하여 jar파일을 루트 폴더로 추출하려 했으나 libs 폴더의 jar파일을 찾지 못했다.

발생한 오류는 다음과 같다.

Step #0 - "Build": Step 11/14 : ARG JAR_FILE=build/libs/*.jar
Step #0 - "Build": ---> Running in a810301deb90
Step #0 - "Build": Removing intermediate container a810301deb90
Step #0 - "Build": ---> 97c9779cb7d0
Step #0 - "Build": Step 12/14 : COPY ${JAR_FILE} app.jar
Step #0 - "Build": COPY failed: no source files were specified
 
build/libs에 빌드된 jar 파일을 app.jar로 jar 파일만 추출하려고 했으나 명령어에서 오류가 발생했다.
빌드에 실패한건지 파일 인식을 못하는건지 확인하기 위해 명령어를 추가하여 확인했다.
Step #0 - "Build": Step 10/14 : RUN ["ls","build/libs"]
Step #0 - "Build": ---> Running in 90d39be4210c
Step #0 - "Build": omoji-0.0.1-SNAPSHOT.jar
Step #0 - "Build": Removing intermediate container 90d39be4210c
Step #0 - "Build": ---> 9f7628997a3e

분명 jar 파일이 요청했던 위치에 있었고 어떤 문제인지 해결방법을 찾아보던 중 와일드 카드의 문제를 알아냈다.

COPY에서 와일드카드 ( * )를 사용시 인식하지 못했다는 문장을 발견하여 쉘 명령어로 처리해보았다.

해결방법

RUN cp ./build/libs/*.jar ./app.jar

정상적으로 작동하는걸 볼 수 있다.

 

나의 삽질 일기

참고사항

https://www.youtube.com/watch?v=JIE89dneaGo