7.1 Spring Boot 애플리케이션 개발하기
Spring Boot 예제를 바탕으로 앱 개발, 컨테이너 이미지 빌드, OKE에 배포하는 과정을 확인해 봅니다.
Spring Boot 기반 마이크로 서비스 만들기
Spring Initializr를 사용하여 기본 프로젝트 소스파일을 기반으로 개발을 하게 됩니다.
Spring Initializr을 통해 프로젝트 파일을 만듭니다.
방법 1. Spring Initializr를 사용하여 기본 프로젝트 소스파일을 만듭니다.
아래 그림과 같이 프로젝트 정보를 입력하고 Generate를 클릭하여 소스파일을 생성합니다.
다운로드 받은 파일을 Cloud Shell에 업로드 합니다.
Cloud Shell에서 업로드된 파일을 unzip으로 압축해제 합니다.
방법 2. Spring Initializr를 브라우저 대신 아래 명령을 통해 Cloud Shell에서 바로 기본 프로젝트 소스파일을 만듭니다.
curl https://start.spring.io/starter.tgz -d baseDir=rest-service -d name=rest-service -d artifactId=rest-service -d javaVersion=1.8 -d dependencies=web,actuator | tar -xzvf -
rest-service 폴더로 이동합니다.
요청에 대한 응답 메시지를 아래와 같은 JSON 메시지 응답하는 코드를 구현합니다.
{ "id": 1, "content": "Hello, World!" }
아래 코드를 복사하여 자바 클래스파일(src/main/java/com/example/restservice/Greeting.java)을 에 작성합니다.
package com.example.restservice; public class Greeting { private final long id; private final String content; public Greeting(long id, String content) { this.id = id; this.content = content; } public long getId() { return id; } public String getContent() { return content; } }
/greeting URL로 HTTP Get 요청에 대한 처리 결과, 여기서는 응답 메시지 전달을 위한 RestController 코드를 src/main/java/com/example/restservice/GreetingController.java 위치에 작성합니다.
package com.example.restservice; import java.util.concurrent.atomic.AtomicLong; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; @RestController public class GreetingController { private static final String template = "Hello, %s!"; private final AtomicLong counter = new AtomicLong(); @GetMapping("/greeting") public Greeting greeting(@RequestParam(value = "name", defaultValue = "World") String name) { return new Greeting(counter.incrementAndGet(), String.format(template, name)); } }
Kubernetes에서는 컨테이너 기동후 준비시간(readiness), 헬스체크를(liveness)를 Spring Boot에 활성화하기 src/main/resources/application.properties 파일에 다음 설정을 추가합니다.
management.health.probes.enabled=true
실행을 위해 코드를 빌드합니다.
./mvnw clean package
빌드된 JAR 파일을 실행합니다.
java -jar target/rest-service-0.0.1-SNAPSHOT.jar
아래와 같이 서비스가 빠르게 실행되고, 내장 Tomcat을 통해 8080 포트로 실행되는 것을 빠르게 실행되는 것을 알 수 있습니다.
. ____ _ __ _ _ /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \ ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \ \\/ ___)| |_)| | | | | || (_| | ) ) ) ) ' |____| .__|_| |_|_| |_\__, | / / / / =========|_|==============|___/=/_/_/_/ :: Spring Boot :: (v2.6.4) 2022-03-07 00:27:56.538 INFO 2806 --- [ main] c.e.restservice.RestServiceApplication : Starting RestServiceApplication v0.0.1-SNAPSHOT using Java 1.8.0_322 on 6d0b991bde66 with PID 2806 (/home/winter/rest-service/target/rest-service-0.0.1-SNAPSHOT.jar started by winter in /home/winter/rest-service) 2022-03-07 00:27:56.544 INFO 2806 --- [ main] c.e.restservice.RestServiceApplication : No active profile set, falling back to 1 default profile: "default" 2022-03-07 00:27:59.015 INFO 2806 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port(s): 8080 (http) 2022-03-07 00:27:59.037 INFO 2806 --- [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat] 2022-03-07 00:27:59.038 INFO 2806 --- [ main] org.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/9.0.58] 2022-03-07 00:27:59.134 INFO 2806 --- [ main] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext 2022-03-07 00:27:59.134 INFO 2806 --- [ main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 2409 ms 2022-03-07 00:28:00.235 INFO 2806 --- [ main] o.s.b.a.e.web.EndpointLinksResolver : Exposing 1 endpoint(s) beneath base path '/actuator' 2022-03-07 00:28:00.290 INFO 2806 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path '' 2022-03-07 00:28:00.327 INFO 2806 --- [ main] c.e.restservice.RestServiceApplication : Started RestServiceApplication in 4.615 seconds (JVM running for 5.413)
테스트를 위해 브라우저 탭을 하나 더 열고 동일한 Oracle Cloud 계정으로 접속하여 Cloud Shell을 실행합니다.
두 번째 Cloud Shell에서 서비스를 테스트합니다.
curl http://localhost:8080/greeting; echo
{"id":1,"content":"Hello, World!"}
첫 번째 Cloud Shell에서 실행되는 앱을 중지합니다.
Container Image 만들기
쿠버네티스에서 실행하기 위해서는 구동할 서비스 애플리케이션을 컨테이너화 하여야 합니다. Docker 클라이언트를 통해 컨테이너 이미지를 만듭니다.
프로젝트 폴더에 Dockerfile을 아래와 같이 만듭니다. openjdk:8-jdk-alpine 베이스 이미지를 사용하여 빌드된 JAR 파일을 이미지 내부로 복사하고 java -jar로 실행하게 하는 예시입니다.
FROM openjdk:8-jdk-alpine ARG JAR_FILE=target/*.jar COPY ${JAR_FILE} app.jar ENTRYPOINT ["java","-jar","app.jar"]
이미지를 빌드합니다.
docker build -t spring-boot-greeting:1.0 .
현재 로컬(여기서는 Cloud Shell)에 있는 이미지를 조회합니다.
$ docker images REPOSITORY TAG IMAGE ID CREATED SIZE spring-boot-greeting 1.0 a80b8a33c501 About a minute ago 124MB openjdk 8-jdk-alpine a3562aa0b991 2 years ago 105MB
OCIR에 이미지 등록하기
OCIR에 컨테이너 이미지를 푸시하기 위해서는 다음과 같은 이미지 태그 네이밍 규칙을 따라야 합니다. 아래 정보를 확인합니다.
<region-key or region-identifier>.ocir.io/<tenancy-namespace>/<repo-name>:<tag>
- region-key: 지금은 Region Key, Region Identifier 둘다 지원하므로, 서울은 icn, ap-seoul-1, 춘천은 yny, ap-chuncheon-1을 쓰면 됩니다. 전체 주소 정보는 OCIR Available Endpoint에서 확인하세요.
- tenancy-namespace: OCI 콘솔 Tenancy 상세 정보에서 Object Storage Namespace로 확인하거나, Cloud Shell에서 oci os ns get으로 확인합니다.
- repo-name: 이미지 이름, 경로가 있는 경우 경로를 포함한 이름
winter@cloudshell:rest-service (ap-chuncheon-1)$ oci os ns get { "data": "axjowrxxxxxx" } winter@cloudshell:rest-service (ap-chuncheon-1)$ echo $OCI_REGION ap-chuncheon-1
OCIR 등록을 위해 기존 이미지에 추가로 태그를 답니다.
docker tag spring-boot-greeting:1.0 ap-chuncheon-1.ocir.io/${tenancy_namespace}/spring-boot-greeting:1.0
동일한 이미지에 태그가 추가된 것을 알 수 있습니다.
winter@cloudshell:rest-service (ap-chuncheon-1)$ docker images REPOSITORY TAG IMAGE ID CREATED SIZE ap-chuncheon-1.ocir.io/axjowrxxxxxx/spring-boot-greeting 1.0 a80b8a33c501 6 minutes ago 124MB spring-boot-greeting 1.0 a80b8a33c501 6 minutes ago 124MB openjdk 8-jdk-alpine a3562aa0b991 2 years ago 105MB
OCIR에 이미지를 Push 하기 위해서는 Docker CLI로 OCIR에 로그인이 필요합니다. Username 및 Password는 다음과 같습니다.
- Username:
<tenancy-namespace>/<user-name>
형식으로 user-name은 OCI 서비스 콘솔에서 유저 Profile에서 보이는 유저명을 사용합니다. - Password: 사용자의 Auth Token을 사용합니다. My Profile > Auth tokens > Generate token 을 통해 생성합니다.
아래와 같이 Docker CLI로 로그인합니다.
# IDCS 유저인 경우 docker login ap-chuncheon-1.ocir.io -u ${tenancy_namespace}/oracleidentitycloudservice/~~~ # OCI Native 유저인 경우 docker login ap-chuncheon-1.ocir.io -u ${tenancy_namespace}/~~~
winter@cloudshell:~ (ap-chuncheon-1)$ docker login ap-chuncheon-1.ocir.io -u ${tenancy_namespace}/winter Password: WARNING! Your password will be stored unencrypted in /home/winter/.docker/config.json. Configure a credential helper to remove this warning. See https://docs.docker.com/engine/reference/commandline/login/#credentials-store Login Succeeded
- Username:
OCIR를 위해 단 이미지 태그를 사용하여 이미지를 Push합니다.
docker push ap-chuncheon-1.ocir.io/${tenancy_namespace}/spring-boot-greeting:1.0
OKE에 마이크로 서비스 배포하기
OCIR에 이미지를 사용하여 OKE에 컨테이너를 기동하기 위해서는 OKE에서 OCIR 이미지에 접근하는 권한이 필요합니다. OCIR Private Repository로 등록했기 때문에 OKE에 접속을 위한 secret를 생성합니다. 이미 Cloud Shell에서 Docker CLI로 OCI에 로그인 했으므로 해당 정보를 이용하여 생성합니다.
kubectl create secret generic ocir-secret \ --from-file=.dockerconfigjson=$HOME/.docker/config.json \ --type=kubernetes.io/dockerconfigjson
다음 YAML 파일을 이용해 OKE에 배포합니다. Load Balancer 사용도 함께 진행하기 위해 Service Type도 함께 배포합니다.
image 주소는 각자 환경에 맞게 수정합니다.
kubectl apply -f <<EOF - apiVersion: apps/v1 kind: Deployment metadata: labels: app: spring-boot-greeting name: spring-boot-greeting-deployment spec: replicas: 1 selector: matchLabels: app: spring-boot-greeting template: metadata: labels: app: spring-boot-greeting spec: containers: - name: spring-boot-greeting image: ap-chuncheon-1.ocir.io/${tenancy_namespace}/spring-boot-greeting:1.0 imagePullSecrets: - name: ocir-secret --- apiVersion: v1 kind: Service metadata: name: spring-boot-greeting-service spec: selector: app: spring-boot-greeting ports: - protocol: TCP port: 80 targetPort: 8080 type: LoadBalancer EOF
kubectl get all 명령으로 배포된 자원을 확인합니다.
$kubectl get all NAME READY STATUS RESTARTS AGE pod/spring-boot-greeting-deployment-84c4865b98-7rmrp 1/1 Running 0 34s NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE service/kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 10h service/spring-boot-greeting-service LoadBalancer 10.96.75.242 150.xxx.xxx.xxx 80:32418/TCP 35s NAME READY UP-TO-DATE AVAILABLE AGE deployment.apps/spring-boot-greeting-deployment 1/1 1 1 35s NAME DESIRED CURRENT READY AGE replicaset.apps/spring-boot-greeting-deployment-84c4865b98 1 1 1 35s
Pod가 정상적으로 기동하였습니다. LoadBalancer의 EXTERNAL-IP를 통해 서비스를 요청합니다.
curl http://150.xxx.xxx.xxx/greeting; echo
{"id":1,"content":"Hello, World!"}
이 글은 개인으로서, 개인의 시간을 할애하여 작성된 글입니다. 글의 내용에 오류가 있을 수 있으며, 글 속의 의견은 개인적인 의견입니다.