현대 오토에버 클라우드 스쿨
AWS- EKS
Gom3rye
2025. 8. 4. 18:34
728x90
반응형
EKS(Elastic Kubernetes Service)
1.EKS 구축
- EKS 구축에 사용하는 도구 설치
=>AWS CLI 설치(eksctl 사용을 하기 위해서 설치)
- 설치 확인: aws --version
- 설치를 하고자 하는 경우는 access-key 와 secret access-key를 발급 받아야 함
사용자 생성할 때 모든 권한을 전부 부여해서 생성
- aws configure --profile 명령을 이용해서 키를 등록
- 프로필 확인: aws configure list-profiles
=>eksctl: EKS 명령을 수행하기 위한 CLI
Windows: https://github.com/eksctl-io/eksctl/releases
Mac: brew install weaveworks/tap/eksctl
Linux 에서는 직접 바이너리 파일을 다운로드 받아서 설치
- 확인: eksctl version
=>kubectl 설치: 쿠버네티스 명령을 사용하기 위해서 사용
- 확인: kubectl version --client
- 구축
=>CloudFormation을 이용해서 VPC를 구축
- 리전 과 가용 용역 및 IP 대역을 설정
- VPC를 생성하고 나면 출력 부분에서 WorkerSubnets 부분을 복사해 둡니다.
subnet-01dd5500f85f1f613,subnet-0df7857a5ca453f37,subnet-0b6d2ba8821995507
=>eks 클러스터 구축(eksctl 명령으로 구축)
eksctl create cluster --vpc-public-subnets WorkerSubnets 값 --name 클러스터이름 --region 리전이름 --version 쿠버네티스버전 --nodegroup-name 노드그룹이름(워커노드그룹) --node-type 생성되는 인스턴스의 타입 --nodes 워커노드개수 --nodes-min 워커노드의최소개수 --nodes-max 워커노드의최대개수
- 정상적으로 생성되면 Stack 이 2개 추가되고 EC2 인스턴스가 워커노드의 개수만큼 생성됩니다.
- 자동으로 control-plane을 생성해주고 현재 로컬 컴퓨터에 설치된 kubectl에 이 control-plane을 사용할 수 있는 context를 설정합니다.
kubectl config get-contexts 명령으로 확인
내용 확인: ~/kube/config(윈도우즈이면 사용자 디렉토리)
- 외부 접속이 가능한지 확인
- nginx 파드 배포
apiVersion: v1
kind: Pod
metadata:
name: nginx-pod
labels:
app: nginx-app
spec:
containers:
- name: nginx-container
image: nginx
ports:
- containerPort: 80
# 리소스 생성
kubectl apply -f 야믈파일경로
# nginx 파드에 포트포워딩을 수행
kubectl port-forward nginx-pod 8080:80
- 외부에서 접속
- EKS는 로컬 컴퓨터와 연결이 되면 로컬 컴퓨터가 Control-Plane으로 인식한다.
Database를 연동하는 BackEnd Application 배포
- 테스트를 위해 로컬에 데이터베이스를 생성
- 도커에 postgresql을 설치
- docker run -d -p 5432:5432 -e POSTGRES_PASSWORD=관리자비밀번호 --name 컨테이너이름 postgres
- 샘플 데이터 생성
- Spring Boot Application 생성
- Spring Boot Application 생성(의존성: Spring Dev tools, Lombok, Data JPA, Spring PostgreSQL, Spring Web)
- build.gradle 파일의 dependencies를 수정
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-web'
compileOnly 'org.projectlombok:lombok'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
runtimeOnly 'org.postgresql:postgresql'
annotationProcessor 'org.projectlombok:lombok'
implementation 'org.apache.commons:commons-lang3:3.9'
testImplementation ('org.springframework.boot:spring-boot-starter-test'){
exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
}
testImplementation 'com.ninja-squad:DbSetup:2.1.0'
testImplementation 'org.assertj:assertj-db:1.3.0'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}
- application.properties를 삭제하고 application.yaml 파일을 추가하고 정적인 데이터를 설정
server:
port: 9000
spring:
application:
name: eks_backend
datasource:
url: jdbc:postgresql://localhost:5432/myworkdb
username: mywork
password: wnddkd
driver-class-name: org.postgresql.Driver
type: com.zaxxer.hikari.HikariDataSource
jpa:
database-platform= org.hibernate.dialect.PostgreSQLDialect
- src/main/resources 디렉토리에 로깅을 위한 logback.xml 파일을 생성해서 작성
<?xml version="1.0" encoding="UTF-8" ?>
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss:SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<root level="info">
<appender-ref ref="STDOUT" />
</root>
</configuration>
- 모든 Entity(데이터베이스 테이블 과 매핑되는 클래스)가 공통으로 가져야 하는 메서드를 소유한 추상 클래스 생성
public abstract class AbstractEntity {
@Override
public String toString() {
return ToStringBuilder.reflectionToString(this);
}
@Override
public boolean equals(Object obj) {
if(obj == null) {
return false;
}
if(obj == this) {
return true;
}
if(obj.getClass() != this.getClass()) {
return false;
}
return EqualsBuilder.reflectionEquals(this, obj);
}
@Override
public int hashCode() {
return HashCodeBuilder.reflectionHashCode(this);
}
}
- Region 테이블과 연동되는 RegionEntity 클래스 생성
@Entity
@Table(name="REGION")
@Getter
@Setter
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;
}
- Location 테이블과 연동되는 LocationEntity 클래스 생성
@Entity
@Table(name = "LOCATION")
public class LocationEntity extends AbstractEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "LOCATION_ID")
private Long locationId;
@Column(name="LOCATION_NAME")
private String locationName;
@ManyToOne
@JoinColumn(name = "REGION_ID")
private RegionEntity region;
@Column(name = "NOTE")
private String note;
}
- BatchProcessing 테이블 과 연동하는 Entity 클래스
@Entity
@Table(name="BATCH_PROCESSING")
@Data
public class BatchProcessingEntity extends AbstractEntity{
@Id
@Column(name = "BATCH_NAME", length=20, nullable=false)
private String batchName;
@Column(name="LAST_EXECUTION_DATE_TIME")
private LocalDateTime lastExecutionDateTime;
@OneToMany(mappedBy="batchProcessing", cascade= CascadeType.ALL)
private List<BatchProcessingFileEntity> fileList;
}
- BATCH_PROCESSING_FILE 테이블 과 연동할 Entity
@Entity
@Table(name="BATCH_PROCESING_FILE")
@Data
public class BatchProcessingFileEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long batchProcessingFileId;
@ManyToOne
@JoinColumn(name = "BATCH_NAME", nullable = false)
private BatchProcessingEntity batchProcessing;
@Column(name = "FILE_NAME", length=300, nullable = false)
private String fileName;
}
- Region 테이블의 CRUD 작업을 위한 Repository 생성
@Repository
public interface RegionRepository extends JpaRepository<RegionEntity, Integer> {
//메서드 이름으로 sql을 생성
//regionName으로 데이터를 조회해주는 메서드
Optional<RegionEntity> findByRegionName(String regionName);
}
- Location 테이블의 CRUD 작업을 위한 Repository 생성
@Repository
public interface LocationRepository extends JpaRepository<LocationEntity,Long> {
//RegionEntity를 대입하면 동일한 값을 갖는 Location을 전부 조회
List<LocationEntity> findByRegion(RegionEntity region);
}
- BatchProcessing 테이블의 CRUD 작업을 위한 Repository 생성
@Repository
public interface BatchProcessingRepository extends JpaRepository<BatchProcessingEntity,Long> {
@Lock(LockModeType.PESSIMISTIC_WRITE)
@QueryHints(@QueryHint(name = "javax.persistence.lock.timeout", value = "0"))
@Query("Select bp from BatchProcessingEntity bp where bp.batchName = :batchName")
Optional<BatchProcessingEntity> fineByIdWithLock(String batchName);
}
- BatchProcessingFile 테이블을 연동할 Repositoy 생성
public interface BatchProcessingFileRepository extends JpaRepository<BatchProcessingFileEntity, Long>{
public void deleteByFileName(String fileName);
}
- RegionRepository를 테스트하기 위한 클래스를 test 디렉토리 패키지에 생성
@SpringBootTest
public class RegionRepositoryTest {
@Autowired
private RegionRepository regionRepository;
@Autowired
private DataSource dataSource;
@Test
//테스트 메서드
public void testFindAll() {
prepareDatabase();
//테이블의 데이터 전체를 가져오는 메서드를 호출해서 그 개수를 리턴
var result = regionRepository.findAll();
assertThat(result).hasSize(4);
}
@Test
public void testFindByRegionName(){
prepareDatabase();
var result = regionRepository.findByRegionName("지역1");
assertThat(result.get().getRegionId()).isEqualTo(1);
}
public void prepareDatabase(){
var oprations = sequenceOf(
deleteAllFrom("location"),
deleteAllFrom("region"),
insertInto("region")
.columns("region_id", "region_name", "creation_timestamp")
.values(1, "지역1", LocalDateTime.now())
.values(2, "지역2", LocalDateTime.now())
.values(3, "지역3", LocalDateTime.now())
.values(4, "지역4", LocalDateTime.now())
.build()
);
var dbSetup = new DbSetup(new DataSourceDestination(dataSource), oprations);
dbSetup.launch();
}
}
- LocationRepository를 테스트하기 위한 클래스를 test 디렉토리 패키지에 생성
@SpringBootTest
public class LocationRepositoryTest {
@Autowired
private LocationRepository locationRepository;
@Autowired
private RegionRepository regionRepository;
@Autowired
private DataSource dataSource;
@Test
public void testFindByRegion() {
//prepare_Database();
var region = regionRepository.findByRegionName("지역1").get();
var result = locationRepository.findByRegion(region);
assertThat(result).hasSize(4);
}
//샘플 데이터베이스 생성해주는 메서드
//테스트하기 전에 무조건 수행하는 메서드
@BeforeEach
public void prepare_Database(){
var oprations = sequenceOf(
deleteAllFrom("location"),
deleteAllFrom("region"),
insertInto("region")
.columns("region_id", "region_name", "creation_timestamp")
.values(1, "지역1", LocalDateTime.now())
.values(2, "지역2", LocalDateTime.now())
.values(3, "지역3", LocalDateTime.now())
.values(4, "지역4", LocalDateTime.now())
.build(),
insertInto("location")
.columns("location_id", "location_name", "region_id", "note")
.values(1, "명소1", 1, "명소1의 상세정보 입니다.")
.values(2, "명소2", 1, "명소2의 상세정보 입니다.")
.values(3, "명소3", 1, "명소3의 상세정보 입니다.")
.values(4, "명소4", 1, "명소4의 상세정보 입니다.")
.build()
);
var dbSetup = new DbSetup(new DataSourceDestination(dataSource), oprations);
dbSetup.launch();
}
}
- 서비스 계층에서 사용할 Region 테이블에 대한 도메인 클래스를 생성
@Getter
@Setter
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 be null");
}
this.regionId = regionId;
this.regionName = regionName;
this.creationTimestamp = creationTimestamp;
}
public Region(RegionEntity regionEntity) {
this(regionEntity.getRegionId(), regionEntity.getRegionName(),
regionEntity.getCreationTimestamp());
}
}
- 서비스 계층에서 사용할 Location 테이블에 대한 도메인 클래스를 생성
public class Location {
private Long locationId;
private String locationName;
private Region region;
private String note;
public Location(Long locationId, String locationName, Region region, String note) {
if (locationName == null) {
throw new IllegalArgumentException("locationName cannot be null.");
}
if (region == null) {
throw new IllegalArgumentException("region cannot be null.");
}
this.locationId = locationId;
this.locationName = locationName;
this.region = region;
this.note = note;
}
public Location(String locationName, Region region, String note) {
this(null, locationName, region, note);
}
public Location(LocationEntity entity) {
this(entity.getLocationId(), entity.getLocationName(), new Region(entity.getRegion()), entity.getNote());
}
public Long getLocationId() {
return locationId;
}
public void setLocationId(Long locationId) {
this.locationId = locationId;
}
public String getLocationName() {
return locationName;
}
public void setLocationName(String locationName) {
this.locationName = locationName;
}
public Region getRegion() {
return region;
}
public void setRegion(Region region) {
this.region = region;
}
public String getNote() {
return note;
}
public void setNote(String note) {
this.note = note;
}
}
- Region 관련 서비스를 위한 인터페이스 생성
public interface RegionService {
//모든 Region을 리턴하는 메서드
public List<Region> getAllRegions();
}
- Region 관련 서비스 구현을 위한 클래스 생성
@Service
@RequiredArgsConstructor
public class RegionServiceImpl implements RegionService {
private final RegionRepository regionRepository;
@Override
public List<Region> getAllRegions() {
//Region 테이블의 모든 데이터를 가져옵니다.
var regionEntities = regionRepository.findAll();
var regionList = new ArrayList<Region>();
//가져온 RegionEntity List를 Region List로 수정
//각각의 데이터에 new Region을 적용해서 List에 추가
regionEntities.forEach(entity -> regionList.add(new Region(entity)));
return regionList;
}
}
- Location 관련 서비스를 위한 인터페이스 생성
public interface LocationService {
//regionId를 가지고 Location을 전부 찾아오는 메서드
public List<Location> getLocationListByRegionId(Integer regionId);
//Location의 List를 받아서 등록하는 메서드
public void registerLocations(List<Location> locationList);
}
- Location 관련 서비스 구현을 위한 클래스 생성
@Service
@RequiredArgsConstructor
public class LocationServiceImpl implements LocationService{
private final LocationRepository locationRepository;
private final RegionRepository regionRepository;
private static final Logger logger = LoggerFactory.getLogger(LocationServiceImpl.class);
@Override
public List<Location> getLocationListByRegionId(Integer regionId) {
var region = regionRepository.findById(regionId);
var locationList = new ArrayList<Location>();
region.ifPresent(r -> {
var locationEntityList = locationRepository.findByRegion(r);
locationEntityList.forEach(entity ->
locationList.add(new Location(entity)));
});
return locationList;
}
@Override
@Transactional
public void registerLocations(List<Location> locationList) {
var regionMap = new HashMap<Integer, RegionEntity>();
locationList.forEach(location -> {
try{
var entity = new LocationEntity();
entity.setLocationName(location.getLocationName());
entity.setRegion(getRegionEntity(location.getRegion(), regionMap));
entity.setNote(location.getNote());
locationRepository.save(entity);
}catch(Exception e){
logger.warn("Skipped data. Error occurred in :" + location, e);
}
});
}
//내부에서 사용할 메서드
private RegionEntity getRegionEntity(
Region region, Map<Integer, RegionEntity> regionMap){
if(regionMap.get(region.getRegionId()) == null){
regionRepository.findById(region.getRegionId()).ifPresent(
entity -> regionMap.put(region.getRegionId(), entity ));
}
return regionMap.get(region.getRegionId());
}
}
- LocationService를 테스트할 클래스를 만들어서 메서드를 만든 후 테스트 수행
@SpringBootTest
public class LocationServiceTest {
@Autowired
private LocationService locationService;
@Autowired
private DataSource dataSource;
@Test
public void testRegisterLocations() {
var locationList = List.of(
new Location("명소5",
new Region(1, "지역1", LocalDateTime.now()),
"명소5의 상세 정보입니다."),
new Location("명소6",
new Region(1, "지역1", LocalDateTime.now()),
"명소6의 상세 정보입니다.")
);
locationService.registerLocations(locationList);
var locationTable = new Table(dataSource, "location");
assertThat(locationTable).hasNumberOfRows(2);
}
@BeforeEach
public void prepareDatabase(){
var oprations = sequenceOf(
deleteAllFrom("location"),
deleteAllFrom("region"),
insertInto("region")
.columns("region_id", "region_name", "creation_timestamp")
.values(1, "지역1", LocalDateTime.now())
.values(2, "지역2", LocalDateTime.now())
.values(3, "지역3", LocalDateTime.now())
.values(4, "지역4", LocalDateTime.now())
.build()
);
var dbSetup = new DbSetup(new DataSourceDestination(dataSource), oprations);
dbSetup.launch();
}
}
- Controller 계층에서 HealthCheck에 사용할 클래스
//Health 체크에 사용할 클래스
@Getter
@Setter
public class HealthDto {
private String status;
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
}
- Controller 계층에서 사용할 RegionDTO 클래스 생성
//DTO: Data Transfer Object
//서로 다른 레이어 사이에서 데이터를 이동할 때 사용하는 클래스
@Getter
@Setter
public class RegionDTO implements Serializable {
private Integer regionId;
private String regionName;
public RegionDTO() {}
public RegionDTO(Region region) {
this.regionId = region.getRegionId();
this.regionName = region.getRegionName();
}
}
- Controller 계층에서 사용할 RegionsDTO 클래스 생성
@Getter
@Setter
public class RegionsDTO {
private List<RegionDTO> regionList = new ArrayList<>();
public RegionsDTO() {}
public RegionsDTO(List<RegionDTO> regionDTOList) {
this.regionList.addAll(regionDTOList);
}
}
- LocationDTO 클래스 생성
@Setter
@Getter
public class LocationDTO {
private Long locationId;
private String locationName;
private RegionDTO region;
private String note;
public LocationDTO(){}
public LocationDTO(Location location){
this.locationId = location.getLocationId();
this.locationName = location.getLocationName();
this.region = new RegionDTO(location.getRegion());
this.note = location.getNote();
}
}
- LocationsDTO 클래스 생성
@Getter
@Setter
public class LocationsDTO {
private List<LocationDTO> locationDTOList = new ArrayList<>();
public LocationsDTO() {}
public LocationsDTO(List<LocationDTO> locationDTOList) {
this.locationDTOList.addAll(locationDTOList);
}
}
- Health Check를 위한 Controller 클래스
@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 API Called");
var health = new HealthDTO();
health.setStatus("OK");
return health;
}
}
- Region Service를 위한 Controller 클래스
@RestController
@RequestMapping("region")
//CORS 설정(동일한 도메인이 아닌 곳에서 javascript api를 이용해서 호출 가능하도록 하는 설정)
@CrossOrigin(origins = "*")
@RequiredArgsConstructor
public class RegionApi {
private static final Logger LOGGER = LoggerFactory.getLogger(RegionApi.class);
private final RegionService regionService;
@GetMapping(produces = MediaType.APPLICATION_JSON_VALUE)
public RegionsDTO getAllRegions() {
LOGGER.info("REGION GET ALL API");
var allRegions = regionService.getAllRegions();
var dtoList = new ArrayList<RegionDTO>();
allRegions.forEach(region -> {
var dto = new RegionDTO(region);
dtoList.add(dto);
});
return new RegionsDTO(dtoList);
}
}
- Location Service를 위한 Controller 클래스
public class LocationApi {
private static final Logger LOGGER = LoggerFactory.getLogger(LocationApi.class);
private final LocationService service;
@GetMapping(value = "/region/{regionId}", produces = MediaType.APPLICATION_JSON_VALUE)
public LocationsDto getLocationListByRegion(@PathVariable("regionId") Integer regionId) {
LOGGER.info("LOCATION LIST BY REGION ID API");
var locationList = service.getLocationListByRegionId(regionId);
var dtoList = new ArrayList<LocationDto>();
locationList.forEach(location -> {
var dto = new LocationDto(location);
dtoList.add(dto);
});
var locationsDto = new LocationsDto(dtoList);
return locationsDto;
}
}
애플리케이션 빌드
빌드 시 수행되는 작업
- 의존성 라이브러리 다운로드
- 프로그램 컴파일
- 테스트 프로그램 컴파일
- 테스트 실행
- 프로그램 실행용 아카이브 파일(jar) 생성
- 명령어
- sudo chmod 755 ./gradlew(윈도우에서 실행시에는 안해도 되지만 리눅스에서는 반드시 수행)
- ./gradlew clean build
- 이미지로 만들 때는 위의 작업을 수행한 후 작업을 해야 하고 git action 이나 jenkins 에서 할 때는 위의 빌드 작업을 task 나 step으로 만들어서 수행합니다.
- 이미지 생성을 위한 Dockerfile 생성
FROM amazoncorretto:17
CMD ["./mvnw", "clean", "package"]
ARG JAR_FILE=target/*.jar
COPY ./build/libs/*.jar app.jar
ENTRYPOINT ["java", "-jar", "app.jar"]
- 이미지 빌드
- docker build -t k8s/eks_backend-app:1.0.0 --build-arg JAR_FILE=build/libs/eks_backend-0.0.1-SNAPSHOT.jar .
이미지를 ECR에 push
- ECR에 이미지를 push 하기 위해서 repository를 생성 (k8s/backend-app)
- 콘솔에서 ECR에 로그인
- aws ecr get-login-password --region 실제리전 | docker login --username AWS --password-stdin aws어카운트아이디.dkr.ecr.실제리전.amazonaws.com
- 이미지 태그 변경
- docker tag k8s/eks_backend-app:1.0.0 208060188195.dkr.ecr.ap-northeast-2.amazonaws.com/k8s/backend-app:1.0.0
- 이미지를 ECR의 Repository에 push
Cloud Formation을 이용해서 RDS를 구축
- 데이터베이스 환경 구축
- 서비스 환경에서는 데이터베이스는 가용 영역 여러 개로 다중화해서 구축
- RDS는 AWS가 제공하는 관계형 데이터베이스의 관리형 서비스로 AWS 관리 콘솔이나 AWS CLI, CloudFormation 등을 이용해서 구축이 가능하다.
- RDS의 문제점은 데이터베이스가 설치된 OS에 로그인 할 수 없는 문제 때문에 데이터베이스 환경 관련 제약 사항이 있음
- 데이터베이스 관리용 서버로 Bastion Host를 구축하여 사용
- Bastion Host란 외부에서 내부 네트워크에 접근할 수 있는 접근점
- 보안성이 높은 인프라와 외부 인터넷을 연결하는 중계 서버로 작동하며 모든 인바운드 트래픽은 Bastion Host를 통과해야 내부 네트워크로 들어 갈 수 있음
- 보통은 AWS에서는 EC2로 구축
- 실제 환경 구축
- CloudFormation에서 rds_ope_cfn.yaml 파일을 이용해서 스택 생성
- EksWorkVPC 풀다운 메뉴 값 중에 eks-work-vpc가 포함된 행 선택
- OpeServeRouteTable은 CloudFormation -> eks-work-base 스택 -> 출력 탭 -> RouteTable값 입력
- CloudFormation에서 rds_ope_cfn.yaml 파일을 이용해서 스택 생성
- 베스천 호스트 접속
- 세션 서비스 관리자(Systems Manager)로 접속
- 세션 관리자에서 세션 시작을 클릭
- 동일한 VPC 안에 있는 어떤 EC2 인스턴스도 베스천 호스트가 될 수 있음
- 아무 인스턴스나 선택해서 세션 시작을 클릭
- 세션에서 명령어를 수행
sudo yum install -y git sudo amazon-linux-extras install -y postgresql11- 데이터베이스 엔드포인트 확인
- CloudFormation에서 생성한 스택을 선택해서 출력 탭을 확인하면 RDSEndpoint를 확인할 수 있다.
- eks-work-db.c5iksoio0fsi.ap-northeast-2.rds.amazonaws.com
- 데이터베이스 유저 와 비밀번호 확인(AWS Secrets Manager가 비밀번호를 생성)
- 개요에서 확인
728x90
반응형