Github Actions를 사용해서 CICD 구축해보기 실습 with GCP - 준비편
서론
원래 나누어서 쓸 생각이 없었는데 글을 쓰다보니 길어져서 준비편, 실행편으로 나누게 되었다.
이 글은 대학생, 취준생 또는 백엔드를 전혀 모르는 개발자를 대상으로 스프링 애플리케이션이 뭔지, CI/CD가 뭔지, 클라우드 플랫폼이 뭔지 모르는 분들에게 따라하면서 백엔드 개발의 프로세스를 경험할 수 있게 하는 것을 목적으로 작성되었다.
지금부턴 약간의 TMI
이 글을 쓰게 된 계기는 얼마 전 Google Developer Groups Campus에서 대학생,취준생들을 대상으로 나의 인턴 경험담을 발표를 했었던게 계기가 되었다.
거기서 나는 최대한 과거의 나를 상상하며 학생들의 눈높이에 맞춰 발표를 준비했었다.
최대한 학생들이 공감할 수 있을만한 내용으로 준비하고 싶어서 몇 번이고 피피티를 지웠다 다시 만들었는지 모를 정도로 열심히 준비를 해갔다.
발표 후 운영진 분들께서 발표에대한 피드백을 메일로 보내주셨는데 그 내용은 발표를 들었던 사람들이 어떤 세션의 발표가 좋았는지를 투표한 그래프를 보여주셨다.
그런데 그 그래프에서 놀랍게도 98%의 인원이 내 발표에 만족했다는 투표를 해주셨다.
그게 무척이나 기뻤고 감동적이었어서 지금 이 글을 쓰게된 계기가 되었다.
피드백을 받고 나서 왜 그분들이 나의 발표에 좋은 평가를 해주셨는지 곰곰히 생각해보니 발표가 학생들의 눈높이를 맞춘 것에 만족해주시지 않았나 생각이 들었다.
나 역시 학교다닐 때는 실제 개발자들은 어떻게 일하는지? 어떤 기술을 쓰는지? 어떤 것에 관심이있어하는지 늘 궁금하고 알고 싶어서 현직 개발자들의 발표를 찾았던 기억이 난다.
그러나 유튜브나 세미나에서 실제 개발자들의 발표를 들어봤아도 학부생인 내가 듣기에는 통 이해하기 어려운 내용들이 많았었던 기억이 난다.
마치 더하기 빼기도 못하는 사람이 미적분 강의를 듣는 느낌이랄까?
지금은 내가 개발자가 되었고 (비록 0년차지만) 개발자가 되고 싶어하는 사람들에게 도움을 줄 수 있는 일이 뭐가 있을까 곰곰히 생각해보다가 백엔드 개발의 일련의 프로세스를 공유하면 참 좋을 것 같다는 생각까지 미치게 되었다.
Github Actions를 소개하는 글이지만, A to Z까지 백엔드 개발자가 애플리케이션을 만들고, 배포, 자동화까지 하는 일련의 과정을 모두 포함하고 있으니 즐겁게 읽어주셨으면 좋겠다!
그리고 지금부터 이 글을 하나 하나 잘 따라오면 Github Actions를 이용해 멋진 CI/CD 과정을 경험해볼 수 있게 될 것이다!
Github Actions란
Github Actions은 MS에서 만든 CI/CD 도구이다.
CI/CD란 지속적 통합/지속적 제공이라는 뜻으로, 애플리케이션의 빌드와 배포를 자동화하는 기술을 의미한다.
소프트웨어의 특성상 변경이 잦고, 잦은 배포를 해야하는 점을 미루어 보았을 때 이러한 빌드 - 배포 과정을 손으로 직접하게되면 시간도 많이 걸리고 비효율적이다.
개발자는 더 많은 일들을 해야하기 때문에 이러한 자동화 기술에 관심을 가지는 것은 필수인데, 이 CI/CD를 도와주는 전통적인 도구 중 대표적인 도구가 바로 Jenkins다.
필자의 팀 또한 Jekins를 주로 CI/CD 도구로 사용하고 있으며 옆 팀에서는 CI는 젠킨스를, CD로는 Spinnaker를 사용하는 팀도 있으니 CI/CD 도구가 꽤 다양하다는 것을 알 수 있다. (그 외에도 트래비스, 밤부 같은 CI/CD 도구도 존재)
특히 젠킨스의 경우 개인적으로는 UI가 좀 올드하다고 느껴졌고 Jenkinsfile에 대한 적절한 예제를 찾기가 좀 어려웠어서 초보자가 사용하기에는 다소 어려움이 있다고 느껴졌다.
또한 젠킨스 서버를 별도로 설치해야하는 귀차니즘은 덤.
Github Actions는 이러한 전통적 CI/CD 도구들에 비해 별도의 설치 없이 yaml 파일 작성 하나로 모든 CI/CD를 끝낼 수 있을 정도로 간단하면서 강력한 기능을 제공하는 도구이다.
그리고 다양한 클라우드 플랫폼에 대한 예제를 제공하고 있으며, 그 문서에 대한 설명도 따로 깃허브 페이지로 존재할 정도로 굉장히 신경을 많이 쓴 모습을 느낄 수 있어서 배우기도 쉬웠다.
가격 정책의 경우 20인 이하의 프로젝트에서는 완전 무료이다. ( 그 이상은 요금을 내야한다. 요금표는 여기 요금표)
그렇기에 토이프로젝트를 하는 개발자들이나 대학생들에게 아주 안성맞춤인 서비스라고 할 수 있겠다.
필자 또한 사이드 프로젝트에서 이 도구를 처음 사용해보았는데 무척 간편하고 좋아서 좀 더 많은 사람들에게 알리고자 이 글을 쓰기로 마음먹었다.
추후에는 Jenkins를 비롯한 CI/CD 도구들을 완전히 대체할 수 있지 않을까 라는 발칙한 상상을 하게 만들 만큼 좋은 도구라고 느꼈고 카카오 깃허브에서도 Github Actions가 추가되었으면 하는 바램이 있다.
뭘 해볼 것인가
이번 글에서는 간단한 Springboot 애플리케이션을 도커를 사용해 구글 클라우드 플랫폼에서 제공하는 GCE에 배포해보고,
Github Actions를 사용해 CI/CD를 구축하는 실습을 해볼 예정이다.
( 해석 : GCE는 쉽게 말해 구글 클라우드 플랫폼에서 제공하는 클라우드 컴퓨터 1대라고 생각하면 된다.)
본격적으로 실습 준비
이번 실습을 위해 준비해야할 것은 다음과 같다.
Intellij
OpenJDK 11 사용
Springboot 프로젝트를 개발할 수 있는 IDE, 여기서는 Intellij 사용
Docker 다운 링크
Google Cloud Platform 회원가입 구글 클라우드 플랫폼
Goole Cloud Platform(이하 GCP)는 처음 가입시 300달러의 1년간 무료 크레딧을 제공하기 때문에 토이프로젝트를 하는 개발자가 쉽게 사용하기 좋다. ( 정정 : 정책의 변경으로 1년에서 3개월로 바뀌었다. ㅜㅜ )
실습 시작 - 스프링 부트 설정 -
자 실습에 앞서 먼저 스프링 부트 프로젝트를 만들어보겠다.
JVM 버전은 11을 사용할건데 설치는 쉬우니까 따로 설명하지 않겠다. (구글에 자바 11 설치법 검색)
위 사이트에 접속하면 스프링 부트 애플리케이션을 만들 수 있다.
위 그림과 동일한 설정으로 만들면 된다.
설정이 끝나면 아래의 Generate를 누르면 .zip 파일이 생성될 것이다.
적당한 위치에 알집을 풀어주자.
(언어의 경우 Java를 선택해도 무관하다. Kotlin을 고른 이유는 필자가 가장 익숙한 언어라서)
인텔리제이를 켜서 open or Import로 새로운 프로젝트의 경로를 찾아주자.
그럼 대략 이런 형태의 프로젝트가 만들어질 것이다.
여기서 간단한 Controller를 하나 만들거고, 사용자가 루트페이지로 접근하면
Welcome을 출력하게 할 것이다.
controller 패키지를 하나 만들고 거기에 RootController 클래스를 만들었다.
이 클래스의 welcome 메소드는 루트로 들어오는 요청에 welcome이라는 메시지를 반환하는 핸들러이다.
인텔리제이 상단 구석에 나뭇잎 모양에 뭔가 시동 마크가 박힌 것을 선택하고 재생 버튼을 눌러보자.
큰 문제없이 실행됐다면 위와 같이 콘솔 로그가 찍히는 것을 확인할 수 있다.
그 후 브라우저에 localhost:8080을 입력하면 welcome이라는 반가운 문구를 확인할 수 있게 된다.
자 만약 이 서버가 이미 어떤 물리장비 혹은 클라우드에 배포가 되어있다고 가정하고, 저 welcome
문구를 hello bro
라고 바꿔달라는 요청이 있다고 가정해보자.
지금은 IDE의 도움을 받아서 비교적 편하게 실행했지만, 만약 다른 물리 장비나 클라우드에 접속해서 CLI에서 작업해야하는 경우라면 이야기는 달라진다.
build도 직접 해야하고, 빌드 결과로 나온 jar 파일도 직접 실행해주어야한다.
이러한 과정을 매 수정마다 한다고 생각해보라
얼마나 짜증날까? 이것이 CI/CD가 필요한 이유이다.
개발자는 깃허브에 커밋 그리고 푸시만 하면 알아서 빌드 - 배포의 과정을 자동화 해주게 된다.
이러한 기술은 개발자의 귀한 시간을 아끼고 더 가치있는 곳에 쓸 수 있게 해주는 선순환의 구조가 되므로 반드시 익혀야 한다.
그래서 우리는 지금부터 이 서비스를 도커 이미지로 만들고, GCP에 푸시 후 그 이미지를 실행시키는 장비를 만들어 볼 것이다.
도커와 VM
도커는 컨테이너 기반의 가상화 플랫폼을 의미한다.
혹시 VirtualBox를 써본 경험이 있는가?
VirtualBox를 사용해봤다면 실행하는데 굉장히 느리고 무겁다는 느낌을 받을 수 있다.
그 이유는 버츄얼 박스와 같은 VM(Virtual Machine) 솔루션들은 하드웨어를 가상화하는 방식이기 때문에 OS 레이어 위에 추가로 Hypervisor가 올라가기 때문이다.
이러한 가상머신은 호스트 OS 위에 새로운 레이어가 자리잡기 때문에 매우 무겁고 느려서 실제 서비스 환경에서 사용하기엔 다소 무리가 있었다.
그러나 이 때 컨테이너 기반의 가상화를 제공하는 도커가 등장했고 (물론 리눅스 컨테이너가 최초긴 하지만 자세한 역사는 생략하자) 프로세스를 격리하는 방식을 사용함으로써 더 빠르고 가볍게 가상화 기술을 사용할 수 있게 되었다.
도커 컨테이너와 이미지 (간단하게)
이미지 : 애플리케이션을 빌드하면 이미지가 된다. 이미지를 실행하면 컨테이너가 된다.
컨테이너 : 이미지가 실행 되서 실제 프로세스에 자원을 할당받은 상태
도커 이미지화
우리는 지금부터 우리가 방금 만든 스프링 프로젝트를 도커 이미지로 만들어서 GCP에 존재하는 이미지 저장소에 우리의 도커 이미지를 푸시할 것이다.
도커 이미지는 다음과 같이 프로젝트 내부에 Dockerfile을 정의해서 만들어줄 수 있다.
FROM adoptopenjdk/openjdk11:latest AS TEMP_BUILD_IMAGE
ENV APP_HOME=/usr/app/
WORKDIR $APP_HOME
COPY build.gradle.kts settings.gradle.kts gradlew $APP_HOME/
COPY gradle $APP_HOME/gradle/
RUN ./gradlew -x test --info || return 0
COPY . .
RUN ./gradlew -x test build
FROM adoptopenjdk/openjdk11:latest
ENV ARTIFACT_NAME=harry.jar
ENV APP_HOME=/usr/app/
WORKDIR $APP_HOME
COPY --from=TEMP_BUILD_IMAGE $APP_HOME/build/libs/$ARTIFACT_NAME .
EXPOSE 8080
ENTRYPOINT java -jar $ARTIFACT_NAME
(Dockerfile 작성에 대한 자세한 설명은 이 글에선 생략하도록 하겠다)
도커 파일은 간단하다. 앞서 우리는 JDK 11을 사용한 프로젝트를 개발하고 있으므로 같은 이미지를 도커허브로 부터 내려받는다.
도커 허브는 도커 이미지들이 저장된 저장소이다.
그 후 jar파일 이름을 명시해줘야하는데, 우리는 아직 jar 파일 이름을 명시하지않았다.
build.gradle.kts에 다음과 같이 테스크를 추가해주자.
tasks.bootJar {
mainClassName = "me.harry.example.ExampleApplicationKt"
archiveFileName.set("harry.jar")
}
필자와 똑같은 이름의 패키지 명을 사용했다면 그대로 복-붙 해도 되지만 다른 이름을 사용했다면 수정해주도록 하자.
archiveFileName에서 빌드될 jar파일의 이름을 명시해줄 수 있다.
이제 빌드하면 harry.jar라는 jar 파일이 생성된다.
여기서 왜 ExampleApplication이 아니라 끝에 Kt가 붙는지 궁금한 사람이 있을 수 있다. (사소한 것에도 의문을 가지는 것은 개발자의 좋은 자세다!)
그 이유는 코틀린의 경우 컴파일이 되면 자바와 마찬가지로 .class 파일이 생성 된다. (이것이 코틀린이 자바와 100% 상호 운용될 수 있는 비밀이다)
이 때 .Kt 파일에 포함된 static 메소드들은 .Kt 파일의 이름 + 접미사로 Kt가 붙게 된다.
잘 보면 알겠지만, ExampleApplication이 메소드로 main을 갖고 있는 것이 아니다.
main은 static 메소드이다.
자바의 경우 static 메소드라고 하더라도 클래스가 감싸고 있어야하는데 코틀린에서는
컴파일 시점에 파일이름 + Kt라는 클래스를 생성하고, 그 클래스가 스태틱 메소드를 갖게 한다.
이제 왜 우리가 Gradle Task에서 mainClassName의 메인 클래스이름 마지막에 Kt를 붙이는지 이해가 됐기를 바란다.
이제 애플리케이션을 빌드할 준비는 끝났다!
다음으로는 도커 커맨드라인 명령어로 방금 만든 Dockerfile을 빌드해서 이미지로 만들어주자.
주의
빌드 하기 전에 반드시 디펜던시 매니지먼트 플러그인의 버전을 1.0.9로 맞춰야한다.
현재 스프링 부트 프로젝트를 생성하면 디펜던시 매니지먼트 플러그인 버전이 1.0.10으로 설정되어있는데 이 경우 에러가 나고 있으므로 버전을 다운그레이드 해주자.
버전을 1.0.10 -> 1.0.9로 낮춰주고 인텔리제이 상단에 보이는 코끼리 모양을 눌러 의존성 변경을 반영해주자.
필자의 노트북에서만 문제일 수도 있으나 Gradle 빌드에도 다음과 같은 에러가 발생하고 있다.
gradlew 파일과 gradlew.bat 파일을 아래에 있는 내용으로 덮어쓰기 해주자.
gradlew
#!/usr/bin/env sh
#
# Copyright 2015 the original author or authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn () {
echo "$*"
}
die () {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin or MSYS, switch paths to Windows format before running java
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=`expr $i + 1`
done
case $i in
0) set -- ;;
1) set -- "$args0" ;;
2) set -- "$args0" "$args1" ;;
3) set -- "$args0" "$args1" "$args2" ;;
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=`save "$@"`
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
exec "$JAVACMD" "$@"
gradlew.bat
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto init
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto init
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:init
@rem Get command-line arguments, handling Windows variants
if not "%OS%" == "Windows_NT" goto win9xME_args
:win9xME_args
@rem Slurp the command line arguments.
set CMD_LINE_ARGS=
set _SKIP=2
:win9xME_args_slurp
if "x%~1" == "x" goto execute
set CMD_LINE_ARGS=%*
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
도커 이미지 빌드 시작
인텔리제이에서는 자체 터미널을 제공한다.
이 터미널을 사용해 도커 이미지를 빌드해주자.
docker build -t test .
빌드가 잘 완료됐다면 다음 명령어를 입력하여 컨테이너를 실행해보자.
docker -it run /bin/bash
그러면 스프링 로그가 찍히면서 서버가 뜨는 것을 확인할 수 있을 것이다.
여기까지 했다면 성공이다.
이제 방금 만든 이미지를 GCP 도커 이미지 저장소로 푸시해야한다.
그러기 위해선 GCP에 프로젝트를 만들어야한다.
GCP에 접속하자.
GCP 셋팅
여기서 몇 가지 셋팅을 마치면 GCP에서 사용할 수 있는 크레딧 300달러를 준다.
GCP 콘솔로 진입했다면 다음과 같은 화면이 나올 것이다.
이곳이 메인 화면이다.
여기서 상단에 보이는 하얀색 박스를 선택해보자
여기서 진입할 프로젝트를 변경하거나 선택할 수 있다.
아마 최초 사용자라면 프로젝트명을 정했을테니 그 프로젝트가 선택되어있을 것이다.
GCP에 도커 이미지를 푸시하기 위해서는 다음과 같은 명령어 규칙을 따른다.
docker push [HOSTNAME]/[PROJECT-ID]/[IMAGE]
Project ID는 프로젝트를 눌러보면 옆에 Project-id라고 별도의 고유값이 생성되는 것을 확인할 수 있다.
이미지는 우리가 설정한 도커 이미지 이름이다. 우리의 경우 test가 되겠다.
자 이제 GCP의 우리 프로젝트 도커 이미지 저장소에 이미지를 푸시해보자.
docker push gcr.io/depromeet-285004/test
그러면 다음과 같은 오류가 발생할 것이다.
The push refers to repository [gcr.io/depromeet-285004/test]
An image does not exist locally with the tag: gcr.io/depromeet-285004/test
이는 우리가 도커 이미지를 위와 같은 방식으로 태깅을 해줘야하기 때문이다.
다시 빌드를 해주자.
docker build -t gcr.io/depromeet-285004/test .
그리고 나서 다시 푸시를 시도하자.
docker push gcr.io/depromeet-285004/test
그러면 푸시가 잘되는 듯 싶다가 다음과 같은 에러가 나올 것이다.
unauthorized: You don't have the needed permissions to perform this operation, and you may have invalid credentials. To authenticate your request, follow the steps in: https://cloud.google.com/container-registry/docs/advanced-authentication
그렇다 인생도 코딩도 역시 호락호락하지 않다.
저 링크를 따라 들어가보면, gcloud 명령어를 사용해 사용자 인증을 하는 방식을 권장하는 것을 알 수 있다.
gcloud는 한글로 된 가이드가 친절하게 잘 나와있다. (역시 구글)
이 부분은 공식 가이드가 워낙 친절하니까 따라해서 설치만 하면 된다. (압축풀고 쉘스크립트 실행하는 정도 수준)
https://cloud.google.com/sdk/docs
필자도 gcloud가 없어서 설치했더니 이렇게 잘 설치됐다고 친절하게 알려주기까지 한다. (역시 구글)
여기서 각자 OS에 맞는 gcloud를 설치한 뒤 gcloud를 통해 인증을 해주자.
그러면 대략 zone을 설정하라는 말이 나오는데, 필자는 us-central1-a로 했다.
존의 경우 클라우드 장비들이 설치될 위치를 지정하는 건데, 필자의 경우 이미 물리장비를 만들어놓은게 있어서 여기로 설정했다.
gcloud auth login
입력하면 구글 로그인 창이 뜬다. 프로젝트를 생성해둔 계정으로 로그인하자.
그리고 나서
gcloud auth configure-docker
입력해서 설정을 저장해주자.
여기까지 하고 다시
docker push gcr.io/depromeet-285004/test
푸시를 해주면 이제 정상적으로 이미지가 푸시된 것을 확인할 수 있다.
만약 위와 같은 방법으로도 오류가 발생한다면?
로컬에 도커가 설치되어있는지 다시 한번 확인
Container Registry API가 사용중인지 확인하자
GCP 컨테이너 저장소에 도커 이미지 업로드 확인
그러면 GCP 컨테이너 저장소에 우리의 도커 이미지가 잘 업로드된 것을 확인할 수 있다.
여기까지하면 거의 다 했다고 할 수 있다.
이제 남은 것은 우리가 방금 올린 이미지를 기반으로 한 Compute Engine (컴퓨터)를 생성하고 그 장비가 우리의 스프링 애플리케이션을 실행하게 하는 것이다.
이제 다음 과정인 장비 생성과 대망의 깃허브 CI/CD 구축은 다음 글에서 설명하도록 하겠다.