Infrastructure, Spring

Spring Boot 마이크로서비스를 Kubernetes 클러스터에 배포하기 (Github Actions 사용)

Written by 개발자서동우 · 3 min read >
Spring Boot 쿠버네티스 배포

안녕하세요! Devloo입니다. 🙂 현대 애플리케이션 개발에서는 GitHub Actions와 같은 CI/CD를 통해 마이크로서비스와 컨테이너 오케스트레이션을 연동하는 것이 매우 중요합니다. Spring Boot의 간결함과 유연성을 활용하면 Kubernetes와 함께 분산 애플리케이션의 배포, 확장 및 관리를 원활하게 할 수 있습니다.

이 글에서는 Spring Boot 마이크로서비스를 생성하고 이를 Kubernetes 클러스터에 배포하는 방법을 설명합니다. 이러한 기술들의 결합된 잠재력을 활용하는 간단한 가이드를 제공합니다. 클라우드 네이티브 솔루션을 도입하는 조직이 늘어나면서 Spring Boot와 Kubernetes의 원활한 연동을 이해하는 것이 민첩하고 유지보수가 쉬우며 확장 가능한 애플리케이션을 구축하는 데 필수적입니다.

이 튜토리얼에서는 Gradle을 빌드 시스템으로 사용하여 Spring Boot 마이크로서비스를 만들고 이를 Kubernetes 클러스터에 배포하는 과정을 단계별로 안내해 드리겠습니다.

Spring Boot 마이크로서비스를 Kubernetes 클러스터에 배포하기 (Github Actions 사용)
사진: UnsplashIonela Mat

사전에 필요한 것들

튜토리얼을 시작하기 전에 다음 도구들이 설치되어 있는지 확인하세요:

  • 자바 개발 키트 (JDK)
  • Docker
  • 실행 중인 Kubernetes 클러스터 (로컬 개발의 경우 Minikube와 같은 도구를 사용할 수 있습니다)
  • Gradle (Gradle wrapper를 사용할 경우 Gradle을 수동으로 설치할 필요는 없습니다)

1단계: Spring Boot 프로젝트 설정

Spring Initializer 또는 선호하는 방법을 사용하여 새로운 Spring Boot 프로젝트를 생성합니다. 프로젝트 요구 사항에 따라 Spring Data, Security, JPA 등의 필요한 종속성을 build.gradle 파일에 포함할 수 있습니다. 이 튜토리얼에서는 프로젝트를 단순하게 유지하기 위해 Spring Web 종속성만 포함했습니다.

settings.gradle 파일에서 앱의 이름을 정의합니다:

rootProject.name = 'kube-app'

필요한 종속성을 build.gradle 파일에 추가합니다:

plugins {
    id 'java'
    id 'org.springframework.boot' version '3.2.1'
    id 'io.spring.dependency-management' version '1.1.4'
}

group = 'com.yourcompany'
version = '1.0'

java {
    sourceCompatibility = '21'
}

repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

프로젝트가 올바르게 구성되었는지 확인하려면 다음 명령어로 빌드할 수 있습니다:

./gradlew build

그런 다음 프로젝트 디렉터리에서 다음 명령어로 프로젝트를 실행합니다:

./gradlew bootRun

웹 브라우저를 열고 http://localhost:8080으로 이동하여 마이크로서비스가 실행 중인지 확인합니다.

2단계: Post 레코드와 PostController 생성

Post 엔티티 클래스를 정의하여 게시물의 데이터 구조를 나타냅니다. id, title, content와 같은 필드를 추가합니다.

package com.yourcompany.kubeapp.post;

public record Post(Integer id, String title, String content) {
}

Lombok 같은 프레임워크를 사용하지 않고도 간단하게 불변성을 제공하기 위해 클래스 대신 Record를 사용합니다.

이제 게시물과 관련된 HTTP 요청을 처리하기 위해 REST 컨트롤러 클래스(PostController)를 생성합니다. @RestController 어너테이션을 사용하여 API의 기본 경로를 정의합니다.

package com.yourcompany.kubeapp.post;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;
import java.util.stream.IntStream;

@RestController
public class PostController {

    private final TextPopulator populator;

    public PostController(TextPopulator populator) {
        this.populator = populator;
    }

    @GetMapping("/")
    public String helloWorld() {
        return "Hello World";
    }

    @GetMapping("/posts")
    public List<Post> listPosts() {
        return IntStream.rangeClosed(1, 20)
                .mapToObj(this::createPost)
                .toList();
    }

    private Post createPost(int id) {
        String title = populator.randomTitle();
        String content = String.format("%s %s", populator.randomContent(), populator.randomContent());
        return new Post(id, title, content);
    }
}

PostController//posts 엔드포인트에서 데이터를 반환하도록 설정되어 있습니다.

Spring Boot 애플리케이션을 실행하고 웹 브라우저나 API 테스트 도구에서 http://localhost:8080/posts로 이동하세요. RapidAPI, Postman 또는 Swagger와 같은 도구를 사용하여 노출된 엔드포인트와 상호작용할 수 있습니다.

프로젝트가 성공적으로 실행되면 Dockerize / Containerize 단계로 넘어갈 수 있습니다.

3단계: Docker 이미지 구성

Spring Boot 마이크로서비스의 Docker 이미지를 정의하기 위해 프로젝트 루트에 Dockerfile을 생성하세요. 다음은 기본 예제입니다:

FROM openjdk:21-slim
WORKDIR /app
COPY build/libs/kube-app-1.0.jar app.jar
EXPOSE 8080

ENTRYPOINT ["java", "-jar", "app.jar"]

여기서 kube-appsettings.gradle 파일의 rootProject.name 속성에서 가져온 프로젝트 이름이고, 버전 정보는 build.gradle 파일의 version 속성에서 가져옵니다. Dockerfile의 아래 지시문을 프로젝트 이름과 버전에 맞게 변경해야 합니다:

COPY build/libs/<your-app-name>-<version>.jar app.jar

Dockerfile을 필요에 맞게 구성한 후, 다음 명령어로 Docker 이미지를 빌드할 수 있습니다:

docker build -t kube-app .

Docker 이미지를 빌드한 후, 다음 명령어를 실행하여 이미지를 실행합니다:

docker run -p 8080:8080 kube-app

마이크로서비스를 설정한 후, 선호하는 웹 브라우저나 Postman 또는 RapidAPI와 같은 REST 도구를 사용하여 http://localhost:8080으로 이동하세요. //posts 엔드포인트에서 예상되는 응답을 확인하세요. 모든 것이 올바르게 구성되고 마이크로서비스가 적절한 값을 반환하면, 설정이 완료된 것입니다.

4단계: GitHub 액세스 토큰과 Docker 레지스트리 시크릿 생성

GitHub Actions 워크플로를 준비하기 전에, 이 링크에서 GitHub 개인 액세스 토큰을 생성해야 합니다. 토큰의 이름을 지정하고 레지스트리 이미지를 읽기 위해 read:packages 범위만 선택하세요. 토큰을 생성한 후에는 이를 안전한 곳에 저장해야 합니다. GitHub 액세스 토큰 페이지에서는 나중에 다시 볼 수 없습니다.

GitHub 개인 액세스 토큰을 생성한 후, 기존 Kubernetes 클러스터에서 다음 명령어를 실행해 GitHub 액세스 토큰을 사용하여 Docker 레지스트리 시크릿을 생성하세요:

kubectl create secret docker-registry my-docker-registry \
  --docker-server=ghcr.io \
  --docker-username=DEPLOYMENT_TOKEN \
  --docker-password=TOKEN_VALUE

시크릿의 이름(my-docker-registry-secret)에 주의하세요. 이 시크릿은 나중에 Kubernetes deployment.yaml 파일에서 다음과 같이 GitHub 프라이빗 패키지/Docker 레지스트리에서 애플리케이션의 Docker 이미지를 가져오는 데 사용됩니다:

spec:
  containers:
  - name: kube-app
    image: ghcr.io/mustafaguc/kube-app:latest
  imagePullSecrets:
  - name: my-docker-registry

이 과정을 통해 Kubernetes 클러스터가 GitHub 레지스트리에서 Docker 이미지를 가져올 수 있게 됩니다.

5단계: Kubernetes 구성 파일 가져오기 및 GitHub Actions 시크릿 변수 생성

GitHub Actions가 Kubernetes 클러스터에 접근할 수 있도록 하려면 Kubernetes 클러스터 구성 파일의 내용을 복사하여 GitHub 저장소에 GitHub Actions 시크릿 변수로 저장해야 합니다.

Kubernetes 클러스터 구성 파일을 복사하려면 클러스터에서 다음 명령어를 실행하세요:

kubectl config view --minify --raw > kubeconfig.yaml

kubeconfig 파일에는 Kubernetes 클러스터에 접근하기 위한 민감한 정보가 포함되어 있으므로 이를 안전하게 보관해야 합니다.

kubeconfig.yaml의 내용을 클립보드에 복사합니다. GitHub 저장소 설정으로 이동하여 이 링크에서 GitHub Actions 시크릿을 생성하세요. 시크릿 변수의 이름을 KUBECONFIG로 지정하고 클립보드에서 복사한 kubeconfig.yaml의 내용을 붙여넣습니다. 시크릿 변수의 이름은 나중에 GitHub Actions 워크플로 파일에서 사용되므로 중요합니다.

6단계: Kubernetes 배포, 서비스 및 인그레스 리소스 생성

Spring Boot 마이크로서비스 애플리케이션을 Kubernetes 클러스터에 배포하고 실행하려면 배포, 서비스 및 인그레스 리소스를 생성해야 합니다. Kubernetes 클러스터는 애플리케이션 실행을 위해 deployment.yaml 파일만 필요하지만, 클러스터 내 마이크로서비스 간 통신과 애플리케이션을 외부에 노출하기 위해 서비스 및 인그레스 리소스도 정의해야 합니다. 이를 더 쉽게 관리하기 위해 세 가지 리소스 파일을 하나의 project.yaml 리소스 파일로 통합했습니다.

# 배포 설정
---
apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: kube-app
  name: kube-app
  namespace: default
spec:
  selector:
    matchLabels:
      app: kube-app
  template:
    metadata:
      labels:
        app: kube-app
      name: kube-app
      namespace: default
    spec:
      containers:
        - image: ghcr.io/mustafaguc/kube-app:latest
          imagePullPolicy: Always
          name: container-0
          ports:
            - containerPort: 8080
              name: http
              protocol: TCP
          resources: {}
      dnsPolicy: ClusterFirst
      imagePullSecrets:
        - name: my-docker-registry
      restartPolicy: Always


# 서비스 설정
---
apiVersion: v1
kind: Service
metadata:
  name: kube-app
  namespace: default
spec:
  ports:
    - name: http
      port: 80
      protocol: TCP
      targetPort: 8080
  selector:
    app: kube-app
  sessionAffinity: None
  type: ClusterIP


# 인그레스 설정
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: kube-app
  namespace: default
spec:
  ingressClassName: nginx
  rules:
    - host: kube-app.example.com
      http:
        paths:
          - backend:
              service:
                name: kube-app
                port:
                  name: http
            path: /
            pathType: Prefix
중요한 설정 값

배포, 서비스, 인그레스 이름 및 매치 레이블(matchLabels):
제공된 구성에서 kube-app은 배포, 서비스 및 인그레스의 이름으로 사용됩니다. 이러한 이름을 필요에 따라 변경할 수 있으며, 선택자 레이블과 일치하도록 조정하면 됩니다.

Docker 이미지:
ghcr.io/<github-username>/<repository-name>:latest Docker 이미지를 사용합니다. 이 값을 프로젝트에 맞게 변경해야 합니다.

Docker 이미지 풀 시크릿:
my-docker-registry 시크릿 값은 GitHub 프라이빗 Docker 레지스트리에서 Docker 이미지를 가져오기 위해 사용됩니다. 이 시크릿은 기존 Kubernetes 클러스터에서 생성되어야 합니다. 이 시크릿의 구성은 4단계에서 설명되었습니다.

Kubernetes에는 다양한 구성 방법이 있지만, 이 글에서는 단순하고 간결하게 설명하기 위해 상세한 구성은 생략했습니다.

7단계: GitHub Actions 워크플로우 생성

GitHub Actions를 이용한 Kubernetes 배포 워크플로우는 GitHub 저장소에서 Kubernetes 클러스터로 애플리케이션을 직접 배포하는 중요한 자동화 과정입니다. 이 워크플로우는 Kubernetes 환경에서 배포 과정을 자동화하고 간소화하며, 신뢰성을 높여 더 효율적이고 협업적인 개발 생태계를 조성합니다.

Gradle 기반의 Java Spring Boot 마이크로서비스 애플리케이션을 Kubernetes 클러스터에 빌드, 푸시 및 배포하려면 다음과 같은 GitHub Actions 워크플로우를 사용할 수 있습니다:

# 이 워크플로우는 Gradle을 사용하여 Java 프로젝트를 빌드하고, 종속성을 캐시/복원하여 워크플로우 실행 시간을 개선합니다.
# 자세한 내용은 https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-java-with-gradle 를 참조하세요.

name: Java Gradle Build & Docker Push

on:
  push:
    branches: [ "main" ]
  pull_request:
    branches: [ "main" ]

env:
  # 비어 있을 경우 Docker Hub용 docker.io 사용
  REGISTRY: ghcr.io
  # github.repository를 <account>/<repo>로 사용
  IMAGE_NAME: ${{ github.repository }}

jobs:
  build:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write
      id-token: write

    steps:
    - uses: actions/checkout@v3
    - name: Set up JDK 21
      uses: actions/setup-java@v3
      with:
        java-version: '21'
        distribution: 'temurin'
    - name: Build with Gradle
      uses: gradle/gradle-build-action@bd5760595778326ba7f1441bcf7e88b49de61a25 # v2.6.0
      with:
        arguments: build

    - name: Install cosign
      if: github.event_name != 'pull_request'
      uses: sigstore/cosign-installer@6e04d228eb30da1757ee4e1dd75a0ec73a653e06 #v3.1.1
      with:
        cosign-release: 'v2.1.1'

    - name: Set up Docker Buildx
      uses: docker/setup-buildx-action@f95db51fddba0c2d1ec667646a06c2ce06100226 # v3.0.0

    - name: Log into registry ${{ env.REGISTRY }}
      if: github.event_name != 'pull_request'
      uses: docker/login-action@343f7c4344506bcbf9b4de18042ae17996df046d # v3.0.0
      with:
        registry: ${{ env.REGISTRY }}
        username: ${{ github.actor }}
        password: ${{ secrets.GITHUB_TOKEN }}

    - name: Extract Docker metadata
      id: meta
      uses: docker/metadata-action@96383f45573cb7f253c731d3b3ab81c87ef81934 # v5.0.0
      with:
        images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}

    - name: Build and push Docker image
      id: build-and-push
      uses: docker/build-push-action@0565240e2d4ab88bba5387d719585280857ece09 # v5.0.0
      with:
        context: .
        push: ${{ github.event_name != 'pull_request' }}
        tags: |
          ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
          ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
        labels: ${{ steps.meta.outputs.labels }}
        cache-from: type=gha
        cache-to: type=gha,mode=max

  deploy:
    name: Deploy
    needs: [ build ]
    runs-on: ubuntu-latest
    steps:
      - name: Set the Kubernetes context
        uses: azure/k8s-set-context@v3
        with:
          method: kubeconfig
          kubeconfig: ${{ secrets.KUBECONFIG }}

      - name: Checkout source code
        uses: actions/checkout@v3

      - name: Deploy to the Kubernetes cluster
        uses: azure/k8s-deploy@v4
        with:
          skip-tls-verify: true
          manifests: |
            kubernetes/project.yaml
          images: |
            ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}

이 간소화된 워크플로우는 다음의 기본 단계로 구성됩니다:

빌드 단계:
  • 코드 체크아웃: 빌드 프로세스를 시작하기 위해 저장소에서 소스 코드를 가져옵니다.
  • Gradle 빌드: Gradle 빌드 프로세스를 실행하여 Spring Boot 마이크로서비스 애플리케이션을 컴파일하고 준비합니다.
  • Docker 빌드: 애플리케이션을 캡슐화하고 이식성을 보장하는 Docker 이미지를 생성합니다.
  • Docker 푸시: 새로 빌드한 Docker 이미지를 안전한 프라이빗 GitHub 레지스트리에 업로드하여 버전 관리된 이미지를 중앙 저장소에 저장합니다.
배포 단계:
  • 코드 체크아웃: 이후 배포 단계를 위해 소스 코드를 다시 가져옵니다.
  • Kubernetes 컨텍스트 설정: 지정된 클러스터에서 kubectl 명령을 실행할 수 있도록 Kubernetes 컨텍스트를 구성합니다.
  • Kubernetes 클러스터에 배포: kubectl을 사용하여 프로젝트를 Kubernetes 클러스터에 배포하고, 업데이트된 애플리케이션을 원활하게 통합합니다.

이 워크플로우는 사용과 적용이 쉽도록 설계되었으며, 최소한의 수정만으로도 활용할 수 있습니다. 필수적인 커스터마이징은 5단계에서 설명한 대로 KUBECONFIG 값을 GitHub Actions 시크릿으로 정의하는 것입니다. 이를 통해 Kubernetes 클러스터에 안전한 접근을 보장할 수 있습니다.

위의 7가지 단계를 잘 따라해보시면, Spring Boot 마이크로서비스를 Kubernetes 클러스터에서 원활하게 자동으로 빌드하고 배포할 수 있어 소프트웨어 개발 생애 주기에서 효율성과 일관성을 높일 수 있습니다.

이번 시간에는 Kubernetes와 Github Actions를 활용한 Spring Boot 마이크로서비스 배포하는 방법을 알아 보았습니다. 궁금하신 사항은 편하게 댓글로 남겨주세요 ! 끝까지 읽어주셔서 감사합니다 🙂

Written by 개발자서동우
안녕하세요! 저는 기술 분야에서 활동 중인 개발자 서동우입니다. 명품 플랫폼 (주)트렌비의 창업 멤버이자 CTO로 활동했으며, AI 기술회사 (주)헤드리스의 공동 창업자이자 CTO로서 역할을 수행했습니다. 다양한 스타트업에서 일하며 회사의 성장과 더불어 비즈니스 상황에 맞는 기술 선택, 개발팀 구성 및 문화 정착에 깊은 경험을 쌓았습니다. 개발 관련 고민은 언제든지 편하게 연락주세요 :) https://linktr.ee/dannyseo Profile

Leave a Reply

Your email address will not be published. Required fields are marked *