개발놀이터

AWS EKS로 스프링 프로젝트 배포하기 본문

배포/kubernetes

AWS EKS로 스프링 프로젝트 배포하기

마늘냄새폴폴 2024. 9. 26. 22:35

이번 포스팅에선 EKS를 이용해서 스프링 프로젝트를 배포해보도록 하겠습니다. 

 

온프레미스로 k8s를 구축할 땐 말도 드럽게 안듣더니 EKS로 하니까 역시 돈이 좋다는 것을 체감하고 있습니다. 개발 난이도를 엄청나게 낮춰주네요. 

 

이번 실습에선 담백하게 스프링 + MySQL 이렇게 구성했습니다. 이번 실습은 실제로 파드간 연결이 되는지 확인하는 것이 목적이었기 때문에 복잡성을 높이지 않았습니다. 이번 실습으로 실제로 파드간 연결이 되는 것이 확인되었으니 다음 포스팅에서 조금 고도화해서 다뤄보도록 하겠습니다. 

 

이번 포스팅의 중점은 다음과 같습니다. 

 

  • Headless Service를 이용해서 파드간 연결이 가능한지 테스트 하기위한 실습입니다.
  • 복잡성을 최대한 줄이기 위해 Deployment (ReplicaSet, StatefulSet등), PV / PVC를 적용하지 않았습니다. 
  • 파드 메트릭이나 HTTPS와 같은 고도화는 다음 포스팅에서 진행할 예정입니다. 
  • 노드의 최소한의 자원만 사용했습니다. 

그럼 바로 시작해보죠!

 

EKS로 스프링 프로젝트 배포하기

스텝바이스텝으로 하나씩 파헤쳐보겠습니다. 

 

  1. 네임스페이스 생성
  2. ResourceQuota, LimitRange 생성
  3. MySQL 구동을 위한 Secret생성 후 MySQL 구동
  4. MySQL 사용자 만들고 데이터베이스 만들기
  5. 스프링 프로젝트 간단하게 만들기
  6. 스프링 프로젝트 외부에서 접속해보기

이 순서로 진행됩니다!

 

1. 네임스페이스 생성

apiVersion: v1
kind: Namespace
metadata:
  name: database

 

apiVersion: v1
kind: Namespace
metadata:
  name: was

 

우선 두개의 네임스페이스를 생성했습니다. 데이터베이스와 WAS를 위한 네임스페이스입니다. 

 

kubectl apply -f ./{데이터베이스 네임스페이스 yaml파일 이름}

kubectl apply -f ./{WAS 네임스페이스 yaml파일 이름}

 

2. ResourceQuota, LimitRange 생성

네임스페이스를 만들었으니 네임스페이스에 대한 제한조건을 걸어두겠습니다. 이건 사실 필요없는거긴 한데... 어렵지 않은 것이라 그냥 해봤습니다. 

 

apiVersion: v1
kind: ResourceQuota
metadata:
  name: db-quota
  namespace: database
spec:
  hard:
    requests.cpu: "1"
    requests.memory: 1Gi
    limits.cpu: "1"
    limits.memory: 1Gi

 

MySQL의 ResourceQuota는 요청 CPU는 1코어 요청 메모리는 1기가입니다. 

 

apiVersion: v1
kind: LimitRange
metadata:
  name: db-limits
  namespace: database
spec:
  limits:
  - max:
      cpu: "1"
      memory: 1Gi
    min:
      cpu: 500m
      memory: 500Mi
    type: Container

 

이어서 MySQL의 LimitRange입니다. 별로 특별한 것은 없으니 넘어가겠습니다. 

 

apiVersion: v1
kind: ResourceQuota
metadata:
  name: was-quota
  namespace: was
spec:
  hard:
    requests.cpu: "1"   
    requests.memory: 1Gi
    limits.cpu: "1"    
    limits.memory: 1Gi

 

apiVersion: v1
kind: LimitRange
metadata:
  name: was-limits
  namespace: was
spec:
  limits:
  - max:
      cpu: "1"            
      memory: 1Gi         
    min:
      cpu: 500m           
      memory: 500Mi       
    type: Container

 

WAS의 ResourceQuota와 LimitRange까지 모두 작성한 다음 생성해주시면 됩니다. 

 

3. MySQL 구동을 위한 Secret 오브젝트 생성과 MySQL 구동

apiVersion: v1
kind: Secret
metadata:
  namespace: database
  name: mysql-secret
type: Opaque
data:
  MYSQL_ROOT_PASSWORD: cm9vdA==

 

우선 Secret을 만들어줍니다. 여기서 주의해야하는 점이 data에 들어가는 value는 반드시 Base64로 인코딩 해줘야한다는 것입니다. 

 

Secret을 작성하고 생성해주시면 됩니다. 

 

apiVersion: v1
kind: Pod
metadata:
  name: mysql-pod1
  labels:
    type: db
    log: dev
  namespace: database
spec:
  nodeSelector:
    node: db
  containers:
  - name: mysql-container1
    image: mysql:8
    ports:
    - containerPort: 3306
    env:
    - name: MYSQL_ROOT_PASSWORD
      valueFrom:
        secretKeyRef:
          name: mysql-secret
          key: MYSQL_ROOT_PASSWORD
    volumeMounts:
    - name: mysql-storage
      mountPath: /var/lib/mysql
  volumes:
  - name: mysql-storage
    hostPath:
      path: /data/mysql
      type: DirectoryOrCreate

 

이제 볼게 조금 많아졌는데 위에서부터 천천히 살펴보겠습니다. 

 

  • labels는 이 파드에 어떤 라벨을 붙일 것인지를 나타냅니다. 후에 이 라벨은 서비스가 파드를 바라볼 때 사용됩니다. 
  • nodeSelector는 어떤 노드에 배치되었으면 좋겠는지 적는 것입니다. 이 nodeSelector가 설정되어있으면 우리가 정한 노드로 들어가지만 설정되어있지 않다면 아무 노드에나 들어가게됩니다. 만약 이 설정을 사용하고싶다면 반드시 노드에 라벨링이 되어있어야합니다. 

    kubectl label nodes {노드이름} {라벨키}={라벨값}

    이렇게 붙일 수 있고 노드 이름은 kubectl get nodes 로 확인할 수 있습니다. 
  • ports 부분에서 우리는 현재 Deployment를 만들 생각이 없기 때문에 컨테이너 포트만 정해주면 상관없어서 우선 저렇게 작성했습니다. 
  • 이제 우리가 만든 Secret을 사용할 수 있는 env 설정입니다. name 에는 환경변수의 키값을 적고 valueFrom에서 우리가 작성한 Secret을 바라보게 만들어줬습니다. 

    여기서 중요한 것은 Secreta을 만들 때의 name과 secretKeyRef 에서 적을 name이 동일해야 바인딩이 됩니다. 
  • volumeMounts는 hostPath의 볼륨을 만들어줬습니다. hostPath 볼륨으로 만들어주면 노드가 사라지는 순간 없어지는 볼륨이어서 제 실습에 딱 적당합니다. 

    만약 노드가 사라져도 볼륨이 유지되는 것을 원한다면 PV / PVC를 만들어서 연결해야합니다. 나중에 포스팅할 것 같지만 일단 지금은 넘어가도록 하겠습니다.

이제 MySQL 파드를 생성해보겠습니다. 

 

잠깐!

쿠버네티스는 데이터베이스를 배포하기에 적합한 기술이 아닙니다. 만약 실제로 데이터베이스를 사용해야할 일이 있다면 쿠버네티스 클러스터 환경이 아닌 별도의 서버를 두는 것이 합리적입니다. 

 

쿠버네티스는 stateless 애플리케이션에 적합하도록 설계되었습니다. 

쿠버네티스는 설계될 때부터 애플리케이션의 생명주기가 짧도록 설계했습니다. 즉, 쿠버네티스 입장에서 애플리케이션이라 함은 언제든지 죽을 수 있고 언제든지 되살릴 수 있는 존재입니다. 

 

이는 stateful의 특징이 강력한 애플리케이션인 데이터베이스에는 적합하지 않습니다. 

 

물론, 쿠버네티스에서도 이런 stateful 애플리케이션을 위한 StatefulSet이라는 오브젝트를 지원해주긴 합니다만 이는 stateful한 애플리케이션 중에서도 적당히 stateful한 애플리케이션에 적합한 오브젝트입니다. 

 

따라서, 만약 실제로 데이터베이스를 쿠버네티스와 연결하고 싶다면 별도의 서버를 두는 것이 좋습니다. 이에 대한 내용은 아래의 링크에 자세히 설명되어있습니다. 

 

https://coding-review.tistory.com/451

 

데이터베이스 서버와 애플리케이션 서버를 분리해보자 : 개념

저는 항상 의문점이 있었던 것이 애플리케이션 서버와 데이터베이스 서버를 나누게 되면 이 둘은 어떻게 통신을 하는가? 였습니다. 제가 기존에 배포하던 방식인 EC2 인스턴스에 애플리케이션과

coding-review.tistory.com

 

 

다시 본론으로 돌아와서 파드를 생성하고 상태를 확인해보겠습니다. 

 

 

잘 배포가 되었네요. 

 

4. MySQL 사용자를 만들고 데이터베이스 생성하기

우리는 사용자를 만들기 위해서 우선 파드 내부로 접근해야합니다. 현재 파드에는 하나의 컨테이너밖에 없기 때문에 아래의 명령어가 통하는것이지 만약 여러개의 컨테이너가 있다면 컨테이너 이름을 특정지어줘야합니다. 

 

kubectl exec -it -n {네임스페이스 명} {파드 명} -- /bin/bash

 

이제 MySQL로 접근하면?

 

mysql -u root -p

 

우리가 Secret에서 정한 패스워드를 입력하면 접속 완료!

 

 

이제 사용자를 만들어봅시다. 

 

create user 'ks'@'%' identified by 'root';

grant all privileges on *.* to 'ks'@'%';

flush privileges;

exit

 

이렇게 명령어를 작성하고 나오면 됩니다. 

 

5. 스프링 프로젝트 간단하게 만들기

스프링은 제 포스팅에서 너무 많이 얘기해서 간단하게 짚고 넘어가겠습니다. 

 

MySQL과 JPA에 대한 의존성을 추가해주고 entity를 만들고 repository만들고 controller만들었습니다. 

 

package com.example.transaction.entity;

import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Entity
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Member {

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "member_id")
    private Long id;

    private String name;

    private int age;
}

 

package com.example.transaction.repository;

import com.example.transaction.entity.Member;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.Optional;

public interface MemberRepository extends JpaRepository<Member, Long> {

    Optional<Member> findByName(String name);
}

 

package com.example.transaction.controller;

import com.example.transaction.entity.Member;
import com.example.transaction.repository.MemberRepository;
import com.example.transaction.service.MemberServiceA;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequiredArgsConstructor
public class MemberController {

    private final MemberRepository memberRepository;

    @GetMapping("/hello-k8s")
    public String getMember() {
        Member findMember = memberRepository.findByName("garlicpollpoll").orElseThrow(() -> new IllegalStateException("존재하는 회원이 없습니다."));

        return "hello-k8s I'm " + findMember.getName();
    }
}

 

준비 끝!

 

그리고 스프링 프로젝트에서 가장 중요한 application.yml을 보면

 

spring:
  datasource:
    url: jdbc:mysql://mysql.database.svc.cluster.local:3306/transaction?serverTimezone=UTC&characterEncoding=UTF-8
    username: ks
    password: root
    driver-class-name: com.mysql.cj.jdbc.Driver
  jpa:
    hibernate:
      ddl-auto: create
    open-in-view: false
    properties:
      hibernate:
        show_sql: true
        format_sql: true
        dialect: org.hibernate.dialect.MySQLDialect
logging:
  level:
    org:
      springframework:
        transaction:
          interceptor: trace

 

역시 데이터베이스와 연결하는 것이 가장 중요하죠. 

 

이것을 짚고 넘어가기 전에 우선 MySQL의 서비스부터 만들어보겠습니다. 

 

apiVersion: v1
kind: Service
metadata:
  name: mysql
  namespace: database
spec:
  ports:
  - port: 3306
    name: mysql
  clusterIP: None
  selector:
    type: db

 

이 서비스를 생성하면 이제 MySQL파드에 접근할 수 있는것인데요. 저는 clusterIP를 None으로 주었습니다. 이것이 의미하는 것은 Headless Service를 만들겠다는 의미입니다. 

 

일단 이것에 대한 자세한 내용은 아래의 링크에 나와있습니다!

 

https://coding-review.tistory.com/547

 

쿠버네티스 이론 : 서비스 (Cluster IP, Node Port, LoadBalancer)

이번 포스팅은 쿠버네티스의 꽃인 파드와 짝을 이루는 서비스입니다. 서비스는 파드를 외부로 노출시켜주는 역할을 하게 되는데, 앞선 쿠버네티스 이론 : 파드 편에서 잠시 언급했지만 라벨을

coding-review.tistory.com

 

우선 서비스를 만들면 DNS Server에 이 서비스의 이름으로 올라가게됩니다. 위 포스팅에서도 언급되지만, 네임스페이스가 같다면 서비스 이름으로만으로도 파드에 접근할 수 있게 됩니다. 

 

제 서비스의 예시로는 mysql:3306이렇게 붙을 수 있다는 것이죠. 하지만 네임스페이스가 다르다면 이렇게 붙을 수는 없습니다. 

 

Headless Service를 만들면 DNS Server에 다음과 같은 규칙으로 DNS가 만들어집니다. 

 

{Headless Service명}.{네임스페이스명}.svc.{DNS서버 명}.local

 

이렇게말이죠. 때문에 MySQL 파드에 붙고싶을 땐 Headless Service를 만들고 이렇게 붙으면 됩니다. 

 

mysql.database.svc.cluster.local:3306

 

 

그럼 스프링에서 MySQL로 붙을 수 있습니다. 

 

이제 마지막인 스프링을 외부에서 붙어보겠습니다. 

 

6. 스프링 프로젝트 외부에서 붙어보기

우선 스프링 파드를 띄우려면 이미지가 있어야겠죠? bootJar를 이용해서 JAR파일을 만들어주고 Dockerfile을 만들어줍니다. 

 

FROM openjdk:17-jdk

ARG JAR_FILE=./transaction-0.0.1-SNAPSHOT.jar

COPY ${JAR_FILE} app.jar

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

 

그리고 ECR에 이미지를 푸시할겁니다. 

 

그러려면 ECR에 접근해야겠죠? ECR에 접근하는데 필요한 IAM 정책은 이미 전 포스팅에서 설정했으니 넘어가고 바로 ECR로 이미지를 푸시해보겠습니다. 

 

ECR 레파지토리를 만들면 (프라이빗으로 만들어야합니다.) "푸시 명령어 보기"라는 버튼이 있습니다. 그걸 그대로 복사해서 쉘 스크립트 파일을 만들어줄겁니다. 

 

저는 이렇게 됩니다. 

 

aws ecr get-login-password --region ap-northeast-2 | docker login --username AWS --password-stdin {사용자ID}.dkr.ecr.ap-northeast-2.amazonaws.com

docker build --platform linux/amd64 -t k8s/ecr .

docker tag k8s/ecr:latest {사용자ID}.dkr.ecr.ap-northeast-2.amazonaws.com/k8s/ecr:latest

docker push {사용자ID}.dkr.ecr.ap-northeast-2.amazonaws.com/k8s/ecr:latest

 

사용자 ID에는 AWS 서비스를 아무데나 들어가도 나오는 ARN에 적혀있습니다. 

 

저는 IAM에서 보는 것을 선호하는데 IAM에 들어가서 아래로 내려보시면 숫자로 적혀있는 일련번호가 있습니다. 그걸 적어주시면 됩니다. 

 

자 이제 이 쉘스크립트를 실행하면 이미지가 저절로 푸시됩니다. 

 

그리고 파드를 만들어보겠습니다. 

 

apiVersion: v1
kind: Pod
metadata:
  name: spring-pod
  namespace: was
  labels:
    type: was
    log: dev
spec:
  nodeSelector:
    deploy: was
  containers:
  - name: was-container
    image: {사용자ID}.dkr.ecr.ap-northeast-2.amazonaws.com/k8s/ecr:latest
    ports:
    - containerPort: 8080

 

이미지에는 ECR로 들어가서 

 

 

요 URI를 입력하면 됩니다. 

 

그리고 파드를 생성해줍니다. 

 

이후에 서비스를 만드는데 MySQL때와는 다르게 LoadBalancer 타입으로 만들어줘야합니다. 

 

apiVersion: v1
kind: Service
metadata:
  name: spring-svc
  namespace: was
spec:
  type: LoadBalancer
  selector:
    type: was
  ports:
  - protocol: TCP
    port: 80
    targetPort: 8080
    nodePort: 30000
  externalTrafficPolicy: Cluster

 

이렇게 적고 서비스를 생성하면?

 

 

로드밸런서가 생성됩니다. 

 

참고로 이 로드밸런서는 ALB가 아닌 NLB입니다. 때문에 패킷의 TCP 헤더에 적힌 곳으로 날아간다는 특징 때문에 HTTPS가 적용이 안됩니다. 때문에 우리가 내부적으로 NGINX같은 웹서버로 적용시켜줘야합니다. 

 

이 로드밸런서로 들어가면 리스너도 설정되어있고 헬스체크까지 설정되어있는 것을 볼 수 있습니다. 

 

저는 80포트로 열어줬기 때문에 보안그룹도 80포트가 열려있네요. 

 

그리고 서비스를 조회해보겠습니다. 

 

 

저기 적혀있는 EXTERNAL_IP가 로드밸런서에서도 볼 수 있는 그 DNS값입니다. 이제 저걸 검색창에 치면?

 

 

이렇게 스프링 페이지가 뜹니다!

 

저는 80포트로 열어줘서 포트를 적지 않았지만 8080포트로 열었다면 뒤에 포트를 적어야합니다.

 

그리고 우리가 만든 API로 접근해보면?

 

 

이렇게 잘 나오는 것을 확인할 수 있습니다. 

 

 

마치며

이렇게 스프링 프로젝트를 EKS로 실습해봤습니다. 온프레미스로 진행할 때와는 전혀 다른 속도감이네요. 정말 맘에듭니다. 

 

아! 참고로 쿠버네티스 클러스터 안에 있는 로드밸런서나 노드그룹은 삭제하는 것이 지갑 건강상 좋습니다! 

 

다음 포스팅에선 이 실습을 조금 강화시켜보겠습니다. 

 

오늘도 정말 긴 포스팅이었네요. 긴 글 읽어주셔서 감사합니다. 오늘도 즐거운 하루 되세요~