EKS 배포1

복습

EKS

  • VPC 생성
  • IAM에서 eks 사용 권한을 가진 user를 생성해서 로컬에 등록
  • eksctl 명령으로 클러스터 생성
    로컬 컴퓨터가 Master Node 역할을 수행: 다른 컴퓨터에서 수행하고자 하면 Context 정보와 IAM 정보를 복사
    Worker Node는 EC2 인스턴스로 생성

VPC

  • 네트워크 대역 생성
  • 인터넷 게이트웨이 추가

Cloud Formation

  • Ansible 역할을 담당
  • json이나 yaml 파일을 가지고 aws 리소스 생성
  • IoC(Infra of Code)

EKS-DB

  • 스택 생성 > EKS 클러스터가 있는 VPC 선택 > 라우팅 테이블 스택의 [출력][routingtable]값 입력 > IAM리소스 승인 > 생성

1.EKS 클러스터 생성

1)VPC 생성

2)eksctl 명령으로 클러스터 생성

2.EKS 클러스터가 존재하는 VPC 내에 RDS 생성

1)RDS를 Cloud Formation에서 생성할 수 있도록 해주는 템플릿 파일을 작성

2)템플릿 파일을 이용해서 스택 생성

3.Cloud Formation으로 VPC 안에 만든 데이터베이스를 활용

1)세션 관리자를 이용해서 배스천 호스트 접속

  • 기존에 만들어진 Worker Node를 이용해서 RDS 사용
  • Session Manager 서비스에서 [세션 시작]을 눌러서 동일한 VPC 내의 인스턴스를 선택
  • 프로그램 설치
sudo yum install -y git
sudo amazon-linux-extras install -y postgresql11
  • postgresql에 접속을 하기 위해서 필요한 정보를 확인
    • EndPoint: CloudFormation에서 RDS를 만든 스택을 클릭하고 출력 탭을 확인: eks-work-db.cesn3uejbkwe.ap-northeast-2.rds.amazonaws.com 이 End Point는 VPC 내에서만 접속이 가능
      RDS를 만들 때 퍼블릭 IP를 사용하지 않음
    • 관리자(eksdbadmin) 비밀번호: Secret Manager 에서 확인
    • 유저(mywork) 비밀번호: Secret Manager 에서 확인
  • EC2 세션에서 사용자를 생성
    • 유저 생성: createuser -d -U 관리자이름 -P -h 접속URL 사용자이름
    • 접속: psql -U 사용자 -h 접속URL 데이터베이스이름
    • createuser -d -U eksdbadmin -P -h eks-work-db.cesn3uejbkwe.ap-northeast-2.rds.amazonaws.com mywork
  • 비밀번호를 3번 입력하는데 첫 2개의 비밀번호는 사용자의 비밀번호이고 다음 1개는 관리자의 비밀번호
    • 데이터베이스 생성: createdb -U mywork -h eks-work-db.cesn3uejbkwe.ap-northeast-2.rds.amazonaws.com -E UTF8 myworkdb
    • psql -U mywork -h eks-work-db.cesn3uejbkwe.ap-northeast-2.rds.amazonaws.com myworkdb

3.Database를 사용하는 Spring Boot Application을 EKS에 배포하고 외부로 노출

1)프로그램을 로컬에서 테스트하기 위해서 로컬 컴퓨터에 Docker에서 실행되는 postgresql을 설치

docker run -d -p 외부포트번호:5432 -e POSTGRES_PASSWORD="비밀번호" --name 컨테이너이름 postgres

docker run -d -p 5432:5432 -e POSTGRES_PASSWORD="wnddkd" --name postgres postgres
  • 관리자는 postgres로 생성

2)Postgre SQL에 접속해서 샘플 유저 와 데이터베이스를 생성

  • dbeaver 접속
create user mywork password 'wnddkd' superuser;

create database myworkdb owner mywork;

3)새로 만든 유저로 재접속해서 샘플 데이터를 생성

create table region(
	region_id SERIAL primary key,
	region_name VARCHAR(100) not null,
	creation_timestamp TIMESTAMP not null
);

insert into region(region_name, creation_timestamp)
values('서울', current_timestamp);

insert into region(region_name, creation_timestamp)
values('제주', current_timestamp);

insert into region(region_name, creation_timestamp)
values('목포', current_timestamp);

insert into region(region_name, creation_timestamp)
values('광주', current_timestamp);

insert into region(region_name, creation_timestamp)
values('부산', current_timestamp);

insert into region(region_name, creation_timestamp)
values('대구', current_timestamp);

select *
from region;

4)Spring Boot Application 생성

  • 프로젝트 이름: backend

  • 의존성

Lombok

DataJPA
Spring PostgreSQL

SpringWeb

5)데이터베이스 관련 작업 및 테스트

  • application.properties에 데이터베이스 접속 정보 와 JPA 설정 정보를 추가
spring.datasource.url=jdbc:postgresql://localhost:5432/myworkdb
spring.datasource.username=mywork
spring.datasource.password=wnddkd
spring.datasource.driver-class-name=org.postgresql.Driver

spring.datasource.type=com.zaxxer.hikari.HikariDataSource
spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect
  • 자바 애플리케이션에 자주 사용하는 클래스를 소유한 외부 라이브러리를 설치: build.gradle 파일의 dependencies 에 추가
implementation 'org.apache.commons:commons-lang3:3.9'
  • Entity 클래스들이 공통으로 가질 메서드를 소유한 추상 클래스를 생성: persistence.entity.AbstractEntity
import org.apache.commons.lang3.builder.EqualsBuilder;
import org.apache.commons.lang3.builder.HashCodeBuilder;
import org.apache.commons.lang3.builder.ToStringBuilder;
public abstract class AbstractEntity {
   @Override
   public String toString() {
       return ToStringBuilder.reflectionToString(this);
   }
  
   @Override
   public int hashCode() {
       return HashCodeBuilder.reflectionHashCode(this);
   }
  
   @Override
   public boolean equals(Object obj) {
       if(obj == null){
           return false;
       }
       if(obj == this){
           return true;
       }
       if(obj.getClass() != getClass()){
           return false;
       }
       return EqualsBuilder.reflectionEquals(this, obj);
   }
}
  • JPA에서 테이블 과 매핑되는 Entity 클래스를 생성: persisence.entity.Location
import jakarta.persistence.*;

import java.time.LocalDateTime;

@Entity
@Table(name= "REGION")
public class RegionEntity extends AbstractEntity{
   @Id
   @GeneratedValue(strategy = GenerationType.IDENTITY)
   @Column(name = "REGION_ID")
   private Integer regionId;

   @Column(name = "REGION_NAME")
   private String regionName;

   @Column(name="CREATION_TIMESTAMP")
   private LocalDateTime creationTimestamp;

   public Integer getRegionId() {
       return regionId;
   }

   public void setRegionId(Integer regionId) {
       this.regionId = regionId;
   }

   public String getRegionName() {
       return regionName;
   }

   public void setRegionName(String regionName) {
       this.regionName = regionName;
   }

   public LocalDateTime getCreationTimestamp() {
       return creationTimestamp;
   }

   public void setCreationTimestamp(LocalDateTime creationTimestamp) {
       this.creationTimestamp = creationTimestamp;
   }
}
  • Region 테이블에 CRUD 작업을 수행할 수 있는 레포지토리 인터페이스 생성: RegionRepository
import com.adamsoft.backend.persistence.entity.RegionEntity;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import java.util.Optional;

//RegionEntity 와 연결된 테이블에 CRUD 작업을 수행할 수 있는 기본 메서드를 구현한
//인스턴스를 자동으로 생성
@Repository
public interface RegionRepository extends JpaRepository<RegionEntity, Integer> {
   //기본적으로 추가되는 메서드
   //Entity를 매개변수로 받아서 삽입, 수정, 삭제하는 메서드
   //매개변수 없이 모든 데이터를 읽어오는 메서드
   //기본키를 매개변수로 받아서 하나의 데이터를 읽어오는 메서드
  
   //regionName을 가지고 데이터를 조회하는 메서드
   Optional<RegionEntity> findByRegionName(String regionName);
}
  • test 패키지에 RegionRepository를 테스트 할 수 있는 클래스를 만들고 테스트를 수행
    • build.gradle 의 dependencies 에 의존성을 추가하고 리빌드
testImplementation 'com.ninja-squad:DbSetup:2.1.0'

RegionRepositoryTest 클래스를 만들고 작성
import com.adamsoft.backend.persistence.repository.RegionRepository;
import com.ninja_squad.dbsetup.DbSetup;
import com.ninja_squad.dbsetup.destination.DataSourceDestination;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.test.context.SpringBootTest;

import javax.sql.DataSource;

import java.time.LocalDateTime;

import static com.ninja_squad.dbsetup.Operations.*;
import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat;

//테스트 클래스라는 어노테이션
//이 클래스는 빌드를 할 때 테스트 용으로 사용이 되고 빌드 결과물을 만들 때 자동 소멸
@SpringBootTest
public class RegionRepositoryTest {
   @Autowired
   private RegionRepository regionRepository;

   @Autowired
   @Qualifier("dataSource")
   private DataSource dataSource;

   @Test
   @Tag("DBRequired")
   public void testFindAll(){
       prepareDatabase();

       //전체 데이터 가져오기 테스트: 개수가 맞는지 확인
       var result = regionRepository.findAll();
       assertThat(result).hasSize(4);
   }

   //테스트 메서드 호출할 때 마다
   @BeforeEach
   public void prepareDatabase(){
       var operations = sequenceOf(
               deleteAllFrom("region"),
               insertInto("region")
                       .columns("region_id", "region_name", "creation_timestamp")
                       .values(1, "지역1", LocalDateTime.now())
                       .values(2, "지역1", LocalDateTime.now())
                       .values(3, "지역1", LocalDateTime.now())
                       .values(4, "지역1", LocalDateTime.now())
                       .build()
       );
       var dbSetup = new DbSetup(new DataSourceDestination(dataSource),operations);
       dbSetup.launch();
   }
}

6)Service 클래스를 만들고 테스트

  • Service 계층에서 사용할 model 클래스를 생성: domain.model.Region
import com.adamsoft.backend.persistence.entity.RegionEntity;

import java.time.LocalDateTime;

public class Region {
   private Integer regionId;
   private String regionName;
   private LocalDateTime creationTimestamp;

   //각각의 항목을 받아서 인스턴스를 생성하는 메서드
   public Region(Integer regionId, String regionName, LocalDateTime creationTimestamp) {
       if(regionName == null){
           throw new IllegalArgumentException("regionName cannot b null");
       }
       this.regionId = regionId;
       this.regionName = regionName;
       this.creationTimestamp = creationTimestamp;
   }

   //Entity를 받아서 인스턴스를 생성하는 메서드
   public Region(RegionEntity regionEntity) {
       this(regionEntity.getRegionId(),
               regionEntity.getRegionName(),
               regionEntity.getCreationTimestamp());
   }

   public Integer getRegionId() {
       return regionId;
   }

   public void setRegionId(Integer regionId) {
       this.regionId = regionId;
   }

   public String getRegionName() {
       return regionName;
   }

   public void setRegionName(String regionName) {
       this.regionName = regionName;
   }

   public LocalDateTime getCreationTimestamp() {
       return creationTimestamp;
   }

   public void setCreationTimestamp(LocalDateTime creationTimestamp) {
       this.creationTimestamp = creationTimestamp;
   }
}
  • Service 클래스 생성: domain.service.RegionService
    • 원칙적으로 이 계층은 템플릿 메서드 패턴을 적용
    • 서비스 인터페이스 생성
import com.adamsoft.backend.domain.model.Region;

import java.util.List;

public interface RegionService {
   //매개변수 없이 전체 데이터를 가져오는 메서드
   public List<Region> getAllRegions();
}
  • 클래스를 생성: domain.service.RegionServiceImpl
import com.adamsoft.backend.domain.model.Region;
import com.adamsoft.backend.domain.service.RegionService;
import com.adamsoft.backend.persistence.repository.RegionRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;

@Service
@RequiredArgsConstructor
public class RegionServiceImpl implements RegionService {
   private final RegionRepository regionRepository;
  
   @Override
   public List<Region> getAllRegions() {
       var regionEntities = regionRepository.findAll();
       var regionList = new ArrayList<Region>();
       regionEntities.forEach(entity -> regionList.add(new Region(entity)));
       return regionList;
   }
}
  • Service 계층 테스트: test 패키지에 클래스를 만들고 테스트
import com.adamsoft.backend.domain.model.Region;
import com.adamsoft.backend.domain.service.RegionService;
import com.ninja_squad.dbsetup.DbSetup;
import com.ninja_squad.dbsetup.destination.DataSourceDestination;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.test.context.SpringBootTest;

import javax.sql.DataSource;
import java.time.LocalDateTime;
import java.util.List;

import static com.ninja_squad.dbsetup.Operations.*;

@SpringBootTest
public class RegionServiceTest {
   @Autowired
   private RegionService regionService;

   @Autowired
   @Qualifier("dataSource")
   private DataSource dataSource;

   //테스트 메서드 호출할 때 마다
   @BeforeEach
   public void prepareDatabase(){
       var operations = sequenceOf(
               deleteAllFrom("region"),
               insertInto("region")
                       .columns("region_id", "region_name", "creation_timestamp")
                       .values(1, "지역1", LocalDateTime.now())
                       .values(2, "지역1", LocalDateTime.now())
                       .values(3, "지역1", LocalDateTime.now())
                       .values(4, "지역1", LocalDateTime.now())
                       .build()
       );
       var dbSetup = new DbSetup(new DataSourceDestination(dataSource),operations);
       dbSetup.launch();
   }

   @Test
   @Tag("DBRequired")
   public void testServiceRegion(){
       List<Region> regionList = regionService.getAllRegions();
       Assertions.assertEquals(regionList.size(), 4);
   }
}

7)요청에 따라 필요한 서비스를 호출하고 응답하는 계층을 생성하고 테스트

  • Dto 클래스
    • 실제 데이터를 리턴하지 않고 Health Check를 위한 DTO 클래스: presentation.dto.HealthDto
public class HealthDto {
   private String status;

   public String getStatus() {
       return status;
   }

   public void setStatus(String status) {
       this.status = status;
   }
}

Region 결과를 위한 Dto: presentation.dto.RegionDto

public class RegionDto {
   private Integer regionId;
   private String regionName;
public RegionDto(){}
 
public RegionDto(Region region) {
   this.regionId = region.getRegionId();
   this.regionName = region.getRegionName();
}


   public Integer getRegionId() {
       return regionId;
   }

   public void setRegionId(Integer regionId) {
       this.regionId = regionId;
   }

   public String getRegionName() {
       return regionName;
   }

   public void setRegionName(String regionName) {
       this.regionName = regionName;
   }
}
  • Controller가 리턴할 때 Region의 목록을 넘겨주므로 Region의 List를 가진 클래스를 생성: presentation.dto.RegionsDto
import java.util.ArrayList;
import java.util.List;

public class RegionsDto {

   private List<RegionDto> regionDtoList = new ArrayList<RegionDto>();

   public RegionsDto(){

   }

   public RegionsDto(List<RegionDto> regionDtoList) {
       this.regionDtoList = regionDtoList;
   }

   public List<RegionDto> getRegionDtoList() {
       return regionDtoList;
   }

   public void setRegionDtoList(List<RegionDto> regionDtoList) {
       this.regionDtoList = regionDtoList;
   }
}
  • Controller 클래스
HealthCheck를 위한 Controller 클래스: presentation.api.HealthApi
import com.adamsoft.backend.presentation.dto.HealthDto;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("health")
public class HealthApi {
   private static final Logger LOGGER =
           LoggerFactory.getLogger(HealthApi.class);
  
   @GetMapping(produces = MediaType.APPLICATION_JSON_VALUE)
   public HealthDto getHealth(){
       LOGGER.info("Health GET API Called");
      
       var health = new HealthDto();
       health.setStatus("OK");
       return health;
   }
}
  • Region 요청에 응답할 Controller 클래스: presentation.api.RegionApi
import com.adamsoft.backend.domain.service.RegionService;
import com.adamsoft.backend.presentation.dto.RegionDto;
import com.adamsoft.backend.presentation.dto.RegionsDto;
import lombok.RequiredArgsConstructor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.ArrayList;

@RestController
@RequestMapping("/")
@RequiredArgsConstructor
public class RegionApi {
   private final RegionService regionService;
   private static final Logger LOGGER =
           LoggerFactory.getLogger(HealthApi.class);
  
   @GetMapping(produces = MediaType.APPLICATION_JSON_VALUE)
   public RegionsDto getAllRegions(){
       LOGGER.info("getAllRegions");
      
       var allRegions = regionService.getAllRegions();
       var dtoList = new ArrayList<RegionDto>();
       allRegions.forEach(region -> {
           var dto = new RegionDto(region);
           dtoList.add(dto);
       });
       var regionsDto = new RegionsDto(dtoList);
       return regionsDto;
   }
}
  • Controller 클래스 테스트
HealthAPI를 테스트하기 위한 클래스를 만들고 테스트
import com.adamsoft.backend.presentation.api.HealthApi;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

import static org.assertj.core.api.AssertionsForClassTypes.assertThat;

@SpringBootTest
public class HealthApiTest {

   @Test
   public void testHealthOk(){
       var api = new HealthApi();
       var health = api.getHealth();
       assertThat(health.getStatus()).isEqualTo("OK");
   }
}```

- RegionApi 메서드를 테스트하기 위한 클래스를 생성하고 테스트
```java
import com.adamsoft.backend.persistence.repository.RegionRepository;
import com.adamsoft.backend.presentation.api.RegionApi;
import org.junit.jupiter.api.Test;
import org.mockito.Mock;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat;

@SpringBootTest
public class RegionApiTest {
   @Autowired
   private RegionApi regionApi;

   @Mock
   private RegionRepository regionRepository;

   @Test
   public void testGetAllRegions() {
       var result = regionApi.getAllRegions();
       assertThat(result.getRegionDtoList()).hasSize(4);
   }
}

8)URL 테스트

  • 서버 애플리케이션을 실행

  • 브라우저나 Postman API 나 Selenium 같은 도구를 이용해서 확인

9)운용 환경으로 이전을 위한 작업

  • 외부에서 접속할 수 있는 데이터베이스를 생성

    • RDS에서 데이터베이스 생성
  • 생성한 데이터베이스에 테이블과 샘플 데이터를 추가

create table region(
	region_id SERIAL primary key,
	region_name VARCHAR(100) not null,
	creation_timestamp TIMESTAMP not null
);
insert into region(region_name, creation_timestamp)
values('서울', current_timestamp);
insert into region(region_name, creation_timestamp)
values('제주', current_timestamp);
insert into region(region_name, creation_timestamp)
values('목포', current_timestamp);
insert into region(region_name, creation_timestamp)
values('광주', current_timestamp);
  • 서버 프로젝트의 application.properties를 수정해서 실행

10)현재 프로젝트를 도커 이미지로 만들어서 ECR에 업로드

  • ECR에 레포지토리를 생성: k8s/backend-app

  • 자신의 계정: 641022061021.dkr.ecr.ap-northeast-2.amazonaws.com/k8s/backend-app

  • 이미지 빌드를 위한 도커 파일 생성

FROM amazoncorretto:17

CMD ["./mvnw", "clean", "package"]

ARG JAR_FILE=target/*.jar

COPY ./build/libs/*.jar app.jar

ENTRYPOINT ["java", "-jar", "app.jar"]
  • 프로젝트 빌드
  • 빌드 시 수행하는 사항 의존성 라이브러리를 다운로드

프로그램을 컴파일

테스트 프로그램 컴파일

테스트 실행

프로그램 실행을 위한 아카이브 파일(JAR, WAR 등, 테스트 부분은 제거) 생성

빌드 수행: ./gradlew clean build

  • 이미지 생성
docker build -t 이미지이름 .
  • 이미지 이름이 계정.dkr.ecr.리전이름.amazonaws.com/레포지토리이름:태그 으로 만들어져야 합니다.
docker build -t 641022061021.dkr.ecr.ap-northeast-2.amazonaws.com/k8s/backend-app:1.0.0 .
  • ECR에 로그인
aws ecr get-login-password --region 실제리전 | docker login --username AWS --password-stdin 계정.dkr.ecr.실제리전.amazonaws.com
  • ECR에 푸시
docker push 이미지

docker push 641022061021.dkr.ecr.ap-northeast-2.amazonaws.com/k8s/backend-app:1.0.0

11)업로드 된 이미지 EKS에 배포

  • 네임스페이스 생성
  • 네임스페이스 용 yaml 생성: create_namespace_k8s.yaml
apiVersion: v1
kind: Namespace
metadata:
  name: eks-work
  • 실행: kubectl apply -f create_namespace_k8s.yaml

  • 현재 컨텍스트 확인: kubectl config get-contexts

  • 네임스페이스 반영

kubectl config set-context 컨텍스트이름 --cluster 클러스터이름 --user AUTHINFO값 --namespace 네임스페이스이름

kubectl config use-context 컨텍스트이름
  • 현재 컨텍스트 확인: kubectl config get-contexts

  • Application 배포

    • 배포를 위한 야믈 파일 생성
  • Application 외부 공개