CloudFormation 을 사용한 이미지 변환 및 리사이징

CloudFormation 을 사용한 이미지 변환 및 리사이징

기존엔 프론트 단에서 이미지를 압축한후 S3 로 이미지를 업로드

그후 CloudFront 를 통하여 캐싱된 이미지를 가져오는 방식을 사용했습니다.

해당 방식을 사용하다 발생한 문제는 프론트 단에서 이미지를 압축하는데 리소스가 든다는 것이었고, 그렇다고 서버측에서 이미지를 바이너리 형태로 받아, 압축하는것또한 서버측 부담이 커져 AWS 에서 제공하는 CloudFormation 을 통해 해당 문제를 해결했습니다.

 

CloudFormation을 이용하면 AWS에서 제공하는 Serverless Image Handler 스택을 한 번에 배포할 수 있습니다. 이 스택은 다음과 같은 주요 기능을 제공합니다.

  1. 온디맨드(On-demand) 이미지 리사이징

    • 이미지를 “업로드 시점”에 리사이징하는 것이 아니라 사용자가 이미지를 요청하는 순간 필요한 규격대로 변환합니다.

      • 예: 1200px 필요하면 1200px로
      • 모바일에서는 600px로
      • 썸네일이면 300px로

      이미지 가공은 전적으로 CloudFront + Lambda@Edge가 수행하고,애플리케이션 서버는 URL만 만들어 내려주면 됩니다.

  2. CloudFormation 을 사용하면, 자동적으로 CDN 을 사용하기 때문에, 캐싱문제를 해결할수 있습니다.

  3. 모든 이미지를 WebP 등으로 자동 변환 가능 합니다.

  4. 이미지 처리 로직을 FE, BE 단에서 처리하지 않아도 됩니다.

 

1. CloudFormation 스택 생성

 

1.1 스택 생성

공식 문서: https://docs.aws.amazon.com/solutions/latest/dynamic-image-transformation-for-amazon-cloudfront/aws-cloudformation-template.html

문서에서 AWS CloudFormation template 섹션에 있는 최신 템플릿 URL을 복사합니다.

예시:

https://solutions-reference.s3.amazonaws.com/dynamic-image-transformation-for-amazon-cloudfront/latest/dynamic-image-transformation-for-amazon-cloudfront.template

AWS 콘솔에서 CloudFormation → 스택 생성으로 이동한 뒤,

  • 템플릿 준비 완료
  • Amazon S3 URL 지정

을 선택하고, 위에서 복사한 템플릿 URL을 그대로 붙여넣습니다.

스크린샷 2025-11-17 오후 10.09.27

 

1.2 스택 세부 정보 지정

  1. Stack 이름

    • CloudFormation 스택 이름입니다. 원하는 이름을 자유롭게 설정하면 됩니다.
  2. CORS Options

    • 이미지 핸들러 API에서 CORS를 허용할지 여부입니다.
    • 보통 프론트에서 직접 이 엔드포인트를 호출한다면 Yes로 두고, 허용할 Origin을 지정합니다.
  3. Image Sources

    • 원본 이미지를 저장하는 S3 버킷 이름들을 적는 곳입니다.
    • 여러 버킷을 사용할 경우 쉼표로 구분해서 입력합니다. 예) test-dev
  4. Demo UI

    • Yes로 설정하면 AWS가 샘플 뷰어(이미지 리사이즈/포맷 테스트용 웹 페이지)를 함께 배포합니다.
    • 처음 도입할 때는 Demo UI를 통해 요청 JSON과 Encoded URL을 쉽게 확인할 수 있으므로 Yes로 설정하는 것을 추천합니다.
  5. Event Logging

    • 이 솔루션에서 발생하는 Lambda 로그를 CloudWatch에 얼마나 오래 보관할지 선택합니다.
    • 특별한 이유가 없다면 기본값으로 두어도 무방합니다.
  6. Image URL Signature

    • URL 서명을 강제할지 여부입니다.

    • 만약 “아무나 임의로 리사이즈 URL을 만들지 못하게” 하고 싶다면 Yes로 두고

      별도의 시크릿을 설정해야 합니다.

    • 내부 서비스이거나 보안 위협이 크지 않다면 초기에는 No로 두고 시작해도 됩니다.

  7. Default Fallback Image

    • 이미지 키가 잘못되었을 때 JSON 에러를 반환하는 대신

      특정 기본 이미지를 내려줄지 여부입니다.

    • Yes로 설정하면, Fallback Image S3 Bucket/Key를 함께 입력해야 합니다.

  8. Auto WebP

    • 브라우저의 Accept 헤더를 보고 WebP를 지원하면 자동으로 WebP로 변환해서 내려줄지 여부입니다.

      이미지를 최대한 가볍게 전달하고 싶다면 Yes로 설정하면 됩니다.

  9. CloudFront PriceClass

    • CloudFront 엣지 로케이션 범위를 선택합니다.

      • PriceClass_All : 전 세계 POP 사용 (가장 빠르지만 상대적으로 비용이 높습니다)
      • PriceClass_200 : 대표적인 리전 위주 (보통 이 옵션으로도 충분합니다)

 

1.3 스택 옵션 구성

해당 단계에서는 태그를 제외한 나머지는 굳이 건드릴 필요가없습니다.

image-20251117222213524

IAM 역할 설정 역시 별도의 조직 정책이 없다면 별도의 Role을 지정하지 않고, CloudFormation이 자동으로 필요한 Execution Role을 생성하도록 두면 됩니다.

  • CloudFormation이 IAM 리소스를 생성하도록 허용 체크만 되어 있으면 추가 설정 없이 스택이 정상적으로 생성됩니다.

 

2. Demo 를 이용한 테스트

테스트를 위해 위 1.2 스택 세부 정보 지정 단계에서 연결했던 S3 버킷에 이미지를 하나 업로드합니다.

스크린샷 2025-11-18 오전 10.37.12

 

그 다음 CloudFront 콘솔에 들어가 보면, 스택 생성 과정에서 자동으로 만들어진 두 개의 CloudFront Distribution 을 확인할 수 있습니다.

스크린샷 2025-11-18 오전 10.33.12

그 후 Demo 페이지의 배포 도메인으로 접속해 Image Source 영역에 저희가 테스트용으로 업로드했던 버킷명과 객체 키(Object Key)를 입력한 뒤 Import 버튼을 클릭합니다.

 

이제 Editor 화면에서 원하는 WidthHeight 값을 입력하면 이미지 변환이 즉시 적용됩니다.

변환 전·후의 해상도와 용량도 함께 확인할 수 있습니다.

스크린샷 2025-11-18 오전 10.39.48

스크린샷 2025-11-18 오전 10.40.18

  • 해상도: 원본 8636x5828 → 400x400
  • 용량: 원본 4.9mb → 17kb

변환된 결과를 통해 Serverless Image Handler 가 정상적으로 이미지 리사이징 및 최적화를 수행하는 것을 확인할 수 있습니다.

 

3. 실사용

Demo 를 통해 테스트를 완료했습니다.

CloudFormation Stack 배포를 통해 생성된 CloudFront Distribution 을 사용하여, S3 객체에 대해 요청 시점에 원하는 해상도로 변환된 이미지를 받아올 수 있게 되었습니다.

이미지 변환 요청을 수행하기 위해서는, 아래 화면에 표시되는 Request Body JSON 을 Base64 URL-safe 방식으로 인코딩한 뒤, CloudFront CDN URL 뒤에 붙여주는 방식을 사용합니다.

{
  "bucket": "버킷명",
  "key": "image-test.jpg",
  "edits": {
    "resize": {
      "width": 4000,
      "height": 4000,
      "fit": "contain"
    }
  }
}

해당 JSON 전체를 Base64 URL-safe 로 인코딩한 후, 아래와 같은 형태로 요청을 보내면 됩니다.

https://{cloudfront-domain}/{base64-encoded-json}

스크린샷 2025-11-18 오전 10.49.18

 

3.1 Java 예시 — 요청 JSON Base64 인코딩

아래는 Java를 사용해 테스트용 인코딩 로직을 작성한 예시입니다.

public static void main(String[] args) throws JsonProcessingException {

  // JSON payload 구성
  Map<String, Object> payload = Map.of(
    "bucket", "버킷명",
    "key", "image-test.jpg",
    "edits", Map.of(
      "resize", Map.of(
        "width", 4000,
        "height", 4000,
        "fit", "contain"
      )
    )
  );

  // JSON → String 변환
  ObjectMapper mapper = new ObjectMapper();
  String json = mapper.writeValueAsString(payload);

  // Base64 URL-safe 인코딩
  String encoded = Base64.getUrlEncoder()
    .withoutPadding()
    .encodeToString(json.getBytes(StandardCharsets.UTF_8));

  // CloudFront URL 결합
  String cdnUrl = "CDN URL 입력";
  System.out.println(cdnUrl + "/" + encoded);
}

이 방식으로 인코딩된 URL을 요청하면, 해당 이미지가 지정된 해상도(4000 × 4000, fit: contain)로 변환된 뒤 CloudFront 캐시에 저장되어 반환됩니다.

즉, 원본 이미지는 그대로 S3에 두고, 원하는 규격의 이미지를 요청 시 즉시 생성하는 구조 가 완성이 되었습니다.

 

Flyway 사용법 (#2)

Flyway 사용법 (#1) 에서 대략적인 Flyway 사용방법을 다뤄 봤습니다.

이번 글에서는 Flyway 사용 중 자주 발생하는 문제와 해결 방법을 정리합니다.

 

  1. SQL 파일과 Entity 의 불일치 문제
  2. Flyway SQL 파일이 무한히 늘어나는 문제

 

1. SQL 파일과 Entity 의 불일치 문제

1.1 문제 개요

Flyway는 SQL 기반으로 DB 스키마를 관리합니다.

하지만 JPA Entity는 코드 기반으로 스키마를 정의하기 때문에, 두 구조가 항상 일치한다는 보장이 없습니다.

 

1.2 ddl-auto: validate 설정

Entity 구조가 DB 스키마와 일치하는지 자동으로 확인할 수 있습니다.

spring:
  jpa:
    hibernate:
      ddl-auto: validate

이 설정을 적용하면 애플리케이션 실행 시 다음이 수행됩니다.

케이스 결과
Entity 필드가 DB에 없음 예외 발생

 

1.3 테스트 환경 구성

Member Entity

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Member {

	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private Long id;

	@Column(nullable = false, length = 50)
	private String nickname;

	@CreationTimestamp
	private LocalDateTime createdAt;
}

V1__init.sql

CREATE TABLE `member`
(
    `id`         BIGINT       NOT NULL AUTO_INCREMENT,
    `nickname`   VARCHAR(100) NOT NULL,
    `created_at` DATETIME     NOT NULL DEFAULT CURRENT_TIMESTAMP,
    PRIMARY KEY (`id`)
);

application.yml

spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/demo
    username: root
    password: 1234
  jpa:
    hibernate:
      ddl-auto: validate
  flyway:
    enabled: true
    baseline-on-migrate: true

 

1.4 검증 결과

위 설정 상태로 애플리케이션을 실행하면 Entity와 SQL 스키마가 일치하므로 정상적으로 구동됩니다.

image-20251105220427388

실행결과 문제없이 잘 실행되는것을 볼수있습니다.

 

1.5 불일치 발생 시 예외 확인

만약 Member Entity 에 Email 컬럼을 아래와 같이 추가후 실행하면 오류가 나는것또한 볼수있습니다.

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Member {

	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private Long id;

	@Column(nullable = false, length = 50)
	private String nickname;

	@Column(nullable = false, length = 50)
	private String email;

	@CreationTimestamp
	private LocalDateTime createdAt;
}
스크린샷 2025-11-05 오후 10.06.09

 

1.6 한계

앞서 살펴본 것처럼 ddl-auto: validate 옵션을 사용하면

Entity → DB 방향의 불일치(즉, Entity에는 존재하지만 DB에는 없는 컬럼)는 감지할 수 있습니다.

하지만 그 반대 방향, 즉 DB → Entity 방향의 불일치는 검증할 수 없습니다.

예를 들어, 아래와 같이 Flyway를 통해 DB에만 컬럼을 추가하더라도 애플리케이션은 아무런 오류 없이 실행됩니다

 

V2__add_email_column.sql

ALTER TABLE `member`
ADD COLUMN `email` VARCHAR(255);
스크린샷 2025-11-05 오후 10.08.48

위 사진에서 보이는것 처럼 문제없이 실행이 됩니다.

 

이러한 한계를 극복하기위해 DB -> Entity 방향 검증을 위한 글을 다음에 써보겠습니다.

 

2. SQL 파일이 무한히 늘어나는 문제

Flyway는 마이그레이션을 버전 기반(V1__, V2__, …) 으로 관리합니다.

파일이 몇 개 수준일 때는 큰 문제가 없지만, 서비스가 장기 운영되면 마이그레이션 파일이 수백, 수천 개로 누적될 수 있습니다.

이럴 경우 유지보수성·빌드 시간·이전 버전 호환성 관리가 어려워집니다.

 

2.2 테스트 사전 준비

스크린샷 2025-11-05 오후 10.32.05

3개의 SQL 파일을 만들어두고 현재 Flyway는 V3 SQL까지 적용이 된 상태입니다.

Flyway는 과거 버전의 모든 SQL 파일을 이력으로 관리합니다. 따라서 이후 버전(V4__...)을 추가해도 V1부터 순차적으로 실행 및 검증 대상에 포함됩니다.

즉 Flyway 의 기본동작은 아래와 같습니다.

상황 Flyway 동작
DB에는 V1~V3 기록이 있음 정상
코드에서 V1, V2 파일이 사라짐 missing migration 에러
코드의 V1, V2 SQL 내용이 변경됨 checksum mismatch 에러

하지만 시간이 지나면

  • 이미 오래전에 반영된 마이그레이션 SQL을 더 이상 관리할 필요가 없고
  • 새 개발자가 프로젝트를 받아도 과거 SQL을 일일이 살펴볼 필요가 없는 상황이 됩니다.

 

2.2 ignore-migration-patterns

application.yml

spring:
  flyway:
    enabled: true
    validate-on-migrate: true
    ignore-migration-patterns: "*:missing"
상황 기본 동작 ignore-migration-patterns 적용 시
DB에는 V1, V2 기록 있음, 코드에서 V1, V2 파일 삭제 missing migration 에러 무시하고 통과
DB에는 V2 기록 있음코드의 V2 내용을 수정 checksum mismatch 에러 여전히 에러 발생

즉, ignore-migration-patterns 옵션은 파일이 “없어진(missing)” 상황만 무시하며, 내용 변경(checksum mismatch)은 절대 허용하지 않습니다.

 

따라서 ignore-migration-patterns이미 DB에는 적용되었지만, 더 이상 소스코드에 보관할 필요 없는 과거 SQL 파일들을 안전하게 삭제하고 싶을 때 사용할 수 있습니다.

 

2.2.1 ignore-migration-patterns 테스트

기존 DB에는 flyway_schema_history가 존재하므로, 과거 파일(V1, V2)을 코드에서 제거하더라도 검증이 통과하도록 해야 합니다.

스크린샷 2025-11-11 오후 8.21.43

이를 위해 아래와 같이 설정합니다.

spring:
  flyway:
    enabled: true
    validate-on-migrate: true
    ignore-migration-patterns: "*:missing"
  • ignore-missing-migrations → 히스토리에 기록은 남아있지만 파일이 없는 버전(V1, V2)을 검증에서 무시합니다. → 따라서 코드에서 과거 파일을 삭제해도 검증 단계에서 실패하지 않습니다.
스크린샷 2025-11-11 오후 8.44.48스크린샷 2025-11-11 오후 8.45.54

따라서 더 이상 관리할 필요 없는 SQL들을 삭제하고도 기존 DB를 유지해야 한다면 ignore-migration-patterns 를 사용할 수 있습니다.

 

2.3 baseLine

baseline 은 이미 DB에 스키마가 존재할 때, “이 시점을 특정 버전(Vx)의 기준점으로 삼겠다”는 의미로 사용합니다.

즉 baseline 을 통하여 현재 스키마 상태를 버전 X부터 시작한 것으로 간주하고,

이후 버전부터 Flyway로 관리하겠다는 뜻입니다.

예를 들어 현재 V1~V3 까지 수동으로(또는 기존 Flyway로) 적용된 DB가 있고,

이제 V1~V3 를 하나로 묶어 새로운 V1로 시작하고 싶다면 baseline 을 사용할 수 있습니다.

 

2.3.1 baseLine 활용

baseline 적용을 위해 flyway_schema_history 테이블을 비우거나 삭제했다고 가정합니다.

스크린샷 2025-11-15 오후 4.43.54

 

application.yml

  flyway:
    enabled: true
    baseline-on-migrate: true
    baseline-version: 1
스크린샷 2025-11-15 오후 4.46.17

이제 서버를 실행하면 위 처럼 history 테이블이 생성되고 BASELINE 이 기록됩니다.

이렇게 하면 기존 스키마 전체가 Flyway 기준의 “V1 적용 완료 상태”가 되며,

이후 컬럼 추가 등의 변경은 V2__...sql 버전부터 적용하면 됩니다.

 

V2__add_number_column.sql

ALTER TABLE member
ADD COLUMN number VARCHAR(255);
스크린샷 2025-11-15 오후 4.48.45

 

이처럼 baseline 을 기준으로 이후 버전들이 정상 적용됩니다.

 

2.4 결론

ignore-migration-patterns을 사용할 때

“히스토리는 그대로 두고, 과거 파일만 깔끔하게 치우고 싶을 때”

  • 이미 운영 DB에는 v1~v50 까지 기록이 있음
  • 하지만 repo에서는 v1~v50 파일을 삭제해도 Flyway가 에러 내지 않게 하고 싶음
  • DB는 유지, 히스토리는 유지
  • 새 DB를 Flyway로 처음부터 풀로 재현할 필요가 없는 경우

즉, 운영과 기존 히스토리는 유지하면서 과거 SQL을 정리하고 싶은 상황에서 사용합니다.

baseline 을 사용할 때

“이미 스키마가 있는 DB의 ‘지금 상태’를 새로운 시작 버전으로 선언하고 싶을 때”

  • 운영 DB를 비울 수 없음
  • 현재 스키마를 기준으로 “여기부터 V1이다” 라고 선언해야 함
  • Flyway 도입 시점, 스쿼시 후 새로운 기준점을 잡고 싶을 때
  • history 테이블을 비우거나 유실된 뒤 기준점을 다시 만들어야 할 때

즉, ‘기존 스키마를 건드리지 않고 버전의 시작점을 다시 설정해야 할 때’ baseline 을 사용합니다.

 

 

Spring WebSocket(STOMP) 통합 테스트 정리

 

1. 통합 테스트를 결정한 이유

WebSocket의 전반적인 흐름을 실제와 동일하게 검증하기 위해 통합 테스트 방식을 선택하였습니다.

현재 STOMP 구조는 다음과 같습니다.

StompDispatcher → 각 command에 맞는 handler → SEND의 경우 @MessageMapping이 선언된 단에서 처리

이 흐름에서 최초 handshake 단계에서 사용자의 인증 및 인가 상태를 확인하며, 필요한 인가 정보는 session에 저장합니다.

이후 각 command에 해당하는 handler는 handshake 과정에서 설정된 session 값을 활용하여 비즈니스 로직을 수행합니다.

이러한 일련의 흐름은 단위 테스트로는 충분히 검증하기 어렵기 때문에, 실제 WebSocket 연결 및 메시지 송수신 과정을 포함하는 통합 테스트로 구성하였습니다.

 

2. 멀티스레드로 인한 문제

아래 코드는 SEND 커맨드를 테스트하기 위한 예시입니다.

// given
// ... 더미 데이터 저장

// when -----------------------------

BlockingQueue<String> messageQueue = new LinkedBlockingDeque<>();
BlockingQueue<String> errorQueue = new LinkedBlockingDeque<>();

// 1. Handshake + STOMP CONNECT
session = connectWithCookies(jwtCreatedBySavedMember.getAccessToken(),
    jwtCreatedBySavedMember.getRefreshToken(), errorQueue);

StompHeaders headers = buildSubscribeHeaders(chatRoom.getId());

// 2. 구독
session.subscribe(headers, new SimpleFrameHandler(messageQueue));

ChatMessageSendRequest payload = ChatMessageSendRequest.builder()
    ...
    .build();

// 3. SEND 요청
session.send("/api/pub/message", payload);

// then -----------------------------
await()
    .atMost(4, SECONDS)
    .untilAsserted(() -> {
        // DB 내 변경사항 검증
    });

// 응답 검증
String result = messageQueue.poll(3, SECONDS);
// ... 응답 검증

WebSocket 통합 테스트는 STOMP 요청을 수신하는 스레드와 메시지를 송신하는 스레드가 분리되어 동작하므로 멀티스레드 환경에서 수행됩니다.

이 때문에 DB 상태 검증 시점이 비동기적으로 지연될 수 있습니다. 이를 해결하기 위해 Awaitility 라이브러리의 await() 메서드를 사용하였습니다.

await() 메서드는 일정 간격으로 DB 쿼리를 반복 실행하여 원하는 변경사항이 발생했는지 확인합니다.

테스트에서는 타임아웃을 4초로 설정하여, 그 안에 기대한 변경사항이 감지되지 않으면 테스트가 실패하도록 하였습니다.

 

3. 트랜잭션(@Transactional) 관련 문제

기존 HTTP 테스트에서는 각 테스트 메서드에 @Transactional을 적용하였습니다.

그러나 WebSocket 통합 테스트는 멀티스레드 기반으로 실행되므로, 테스트 스레드와 STOMP 요청을 처리하는 스레드의 트랜잭션이 서로 분리됩니다.

즉, 테스트 코드에서 더미 데이터를 저장하더라도 해당 데이터가 STOMP 처리 스레드에서는 보이지 않는 문제가 발생합니다.

따라서 STOMP 기반 WebSocket 테스트에서는 @Transactional을 사용하지 않았습니다.

대신 테스트 실행 전후로 DB를 정리(clean-up)하여 항상 초기 상태를 유지하도록 하였습니다.

 

4. 서로 다른 스프링 컨텍스트

 

4.1 HTTP 테스트를 위한 어노테이션

기존 HTTP 통합 테스트에서 사용한 커스텀 어노테이션은 다음과 같습니다.

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@SpringBootTest
@AutoConfigureMockMvc
@Transactional
@Import(WireMockConfig.class)
public @interface ControllerIntegrationTest {
}

위 어노테이션은 @SpringBootTest의 기본 설정(WebEnvironment.MOCK)을 사용합니다.

즉, 애플리케이션 컨텍스트는 로드되지만 서블릿 환경은 Mock으로 구성되며, 실제 내장 톰캣(Tomcat)은 구동되지 않습니다.

image-20251017102700735

 

4.2 Websocket(STOMP) 를 위한 어노테이션

반면 STOMP 기반 WebSocket 테스트는 실제 서버가 존재해야 전체 프로토콜 흐름(핸드셰이크 → CONNECT→SUBSCRIBE → SEND → DISCONNECT)을 검증할 수 있습니다.

그 이유는 STOMP 통신이 양방향 비동기 구조로 동작하기 때문입니다.

따라서 테스트 환경에서도 다음 두 가지 스레드가 모두 필요합니다.

  1. 요청을 처리할 서버 스레드
    • 실제 톰캣(WebSocket 서버)이 기동되어야 클라이언트의 CONNECT, SUBSCRIBE, SEND 등의 프레임 요청을 수신하고 처리할 수 있습니다.
    • 즉, @MessageMapping이 붙은 컨트롤러 메서드나, ChannelInterceptor, StompHandler 등이 실제 네트워크 스레드에서 호출되어야 합니다.
  2. 요청을 발생시키는 클라이언트 스레드
    • 테스트 코드 내에서 StompSession을 통해 서버에 연결하고 메시지를 송수신합니다.
    • 이 스레드는 실제 네트워크 소켓을 열어 서버 스레드와 통신해야 하므로, Mock 서블릿 환경에서는 동작할 수 없습니다.

따라서 실제 서버 스레드를 띄워 요청과 응답이 오가는 환경을 재현하기 위해 @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) 설정을 사용하였습니다.

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureMockMvc
@ActiveProfiles("stomp-test")
public @interface StompHandlerTest {
}
image-20251017103142495

HTTP용 어노테이션(ControllerIntegrationTest)과 STOMP(WebSocket)용 어노테이션 (StompHandlerAnnotation)의 가장 큰 차이점은 @SpringBootTestwebEnvironment 설정입니다.

  • WebEnvironment.MOCK : 실제 서버 미구동(Mock 서블릿 환경)
  • WebEnvironment.RANDOM_PORT : 실제 내장 톰캣 서버 구동

WebEnvironment.RANDOM_PORT를 사용하면 테스트 시 실제 톰캣이 랜덤 포트에서 실행되며, 이를 통해 실질적인 WebSocket 통신이 가능해집니다.

따라서 같은 패키지 내에 존재하는 테스트이더라도, HTTP 기반 테스트와 WebSocket 테스트는 서로 다른 스프링 컨텍스트를 사용합니다.

 

flyway 사용법

Flyway 사용법 (#1)

지금까진 ERDCloud로 전체적인 DB 구조와 컬럼을 관리하고, 변경사항이 생기면 세 가지 단계를 거쳤습니다. 먼저 ERDCloud를 수정하고, 그다음 Spring Boot 코드(Entity)를 수정한 뒤, 마지막으로 DB에 직접 쿼리를 실행해 반영하는 방식이었습니다.

 

하지만 이 과정에는 몇 가지 문제가 있었습니다. 가장 큰 문제는 DB에 직접 쿼리를 날리는 시점과 실제 서비스 배포 시점이 정확히 맞아야 한다는 점이었습니다. 시점이 어긋나면 서비스가 정상적으로 동작하지 않을 위험이 있었습니다. 또한, Entity와 ERDCloud 간 불일치 문제가 자주 발생했습니다. 원래는 ERDCloud에서 DDL을 추출한 뒤, 그중에서 수정되거나 추가된 부분만 직접 쿼리로 DB에 적용하는 방식을 썼는데, 테이블 수가 많아지고 변경사항이 누적되다 보니, Entity와 ERDCloud 간 동기화가 맞지 않으면 언제든 예기치 못한 문제가 생길 수 있었습니다.

 

이런 상황에서 최소한 Entity와 실제 DB 테이블 간에는 불일치가 없어야 한다는 점이 중요했고, 동시에 배포 타이밍에 맞춰 직접 쿼리를 적용해야 하는 리스크를 줄일 필요가 있었습니다. 따라서 이러한 문제들을 해결하기 위해Flyway를 도입하게 되었습니다.

 

1. Spring Boot 적용 방법

실행환경: gradle, springboot 3.5.6

 

1.1 의존성 추가

먼저 프로젝트에 Flyway를 적용하기 위해 빌드 도구에 의존성을 추가해야 합니다.

build.gradle 파일에 flyway-mysql 의존성을 추가합니다.

//flyway
implementation 'org.flywaydb:flyway-mysql'

 

1.2 설정 (application.yml)

그다음 application.yml 혹은 application.properties 파일에서 데이터베이스 연결 정보와 함께 Flyway 설정을 추가합니다.

spring:
  flyway:
    enabled: true       # flyway 활성화
    baseline-on-migrate: true   # DB가 비어있지 않아도 baseline 잡고 시작
    locations: classpath:db  # migration 파일 위치

enabled: true → 실행 시점에 자동으로 DB 마이그레이션 반영 됩니다.

locations → 기본값이 classpath:db/migration이라 커스텀 경로를 사용하지 않는 이상 따로 설정하지 않아도 됩니다.

baseline-on-migrate: true기존 DB가 이미 초기화되어 있더라도, Flyway가 그 상태를 baseline으로 잡고 이후 스크립트부터 적용 해줍니다. 만약 해당 설정값이 false 라면 이미 테이블이 있을때 Flyway는 "스키마가 손상된 상태"라고 판단하고 오류 발생시킵니다. 즉, 기존 DB → Flyway 도입이 불가능해 집니다.

 

1.3 마이그레이션 스크립트 작성 규칙

마이그레이션 스크립트는 정해진 규칙에 맞춰 파일명을 작성해야 합니다.

일반적으로 V숫자__설명.sql 형태를 사용합니다.

예를 들어, 초기 스키마를 정의할 때는 V1__init.sql, 이후 사용자 테이블을 추가한다면 V2__add_user_table.sql 같은 식입니다.

작성한 스크립트 파일은 src/main/resources/db/migration 디렉터리에 두면, 애플리케이션이 실행될 때 자동으로 해당 SQL들이 순서대로 실행되어 DB에 반영됩니다.

 

1.4 실제 구현 예시

image-20250925094409701

v1__init.sql

CREATE TABLE `member`
(
    `id`         BIGINT       NOT NULL AUTO_INCREMENT,
    `nickname`   VARCHAR(100) NOT NULL,
    `created_at` DATETIME     NOT NULL DEFAULT CURRENT_TIMESTAMP,
    PRIMARY KEY (`id`)
);

application.yml

spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/test
    username: root
    password: 1234
  flyway:
    enabled: true       # flyway 활성화
    baseline-on-migrate: true   # DB가 비어있지 않아도 baseline 잡고 시작
    locations: classpath:db  # migration 파일 위치

우리의 예상대로라면, 위, 설정들에 의하여 Spring 실행시 db 에 member 테이블이 생겨야 합니다.

 

스크린샷 2025-09-25 오전 9.45.48

서버를 실행하면 Flyway가 db/migration에 있는 마이그레이션 스크립트를 읽어서 순서대로 실행합니다. 그 결과 DB에는 우리가 작성한 member 테이블이 생성되고, 동시에 Flyway가 자체적으로 관리하는 flyway_schema_history 테이블도 추가됩니다.

 

flyway_schema_historyDB 마이그레이션 이력이 저장되는 버전 관리 테이블이고, 이를 통해 Flyway는 중복 실행을 방지하면서 다음 버전부터 이어서 실행할 수 있게 됩니다.

 

2. 문제점

해당 방법을 사용한다면, 제가 가장 큰 문제라 생각했던, "DB에 직접 쿼리를 날리는 시점과 실제 서비스 배포 시점이 정확히 맞아야 한다는 점 " 해당 문제점이 해결됩니다.

다만, flyway 를 사용하다보니 다음과 같은 2가지 문제점이 존재했습니다.

  1. db/migration 안에 있는 sql 파일이 우리의 entity 와 완벽히 일치한다는 보장이 있는가?
  2. db/migration 안에 있는 sql 파일이 무한정 늘어나면 관리는 어떻게 할것인가?

 

다음글은 해당 문제를 어떻게 해결했는지에 대해 작성해보겠습니다.

slack과 aws의 lambda 연동

[AWS 비용 절감 #3] Slack과 AWS Lambda를 버튼으로 연동하기

AWS 비용 절감 #1: S3 + CloudFront + HTTPS 정적 웹사이트

AWS 비용 절감 #2: EC2 인스턴스 예약 실행으로 비용 최적화

 

앞선 두 가지 방식으로 AWS 비용이 절반가량 줄었다.

image-20250709130010551

하지만 EC2 인스턴스를 예약 실행하는 방식(오전 9시부터 오후 9시까지)을 도입하고 나니 한 가지 문제가 생겼다.

예약된 시간 외에 서버를 사용해야 할 경우, 다음과 같은 두 가지 방법이 있다.

  1. AWS 콘솔에 직접 접속하여 EC2 인스턴스를 시작하고, SSH로 접속해 필요한 컨테이너를 실행한다.
  2. Lambda의 실행 시간을 임시로 변경하여 원하는 시간에 EC2 인스턴스를 실행한다.

위 두 가지 방법 모두 가능하지만, 만약 내가 자리에 없는 상황에서 다른 팀원이 테스트 서버를 사용해야 한다면 즉각적인 대응이 어렵다는 문제가 있다.

이러한 문제를 해결하기 위해 현재 사용 중인 협업 도구인 Slack을 통해 특정 채널에 특정 명령어로 Lambda 를 trigger 해보기로 했다.

 

전체 아키텍처

[Slack User] -> [Slash Command] -> [Slack API] -> [AWS API Gateway] -> [AWS Lambda] -> [Amazon EC2]
  1. 사용자가 Slack 채널에서 특정 명령어(예: /start-server)를 입력한다.
  2. Slack은 이벤트를 AWS API Gateway로 전달한다.
  3. API Gateway는 요청을 받아 연결된 AWS Lambda 함수를 트리거한다.
  4. Lambda 함수는 EC2 인스턴스를 시작하는 코드를 실행하고, 실행 결과를 Slack으로 다시 보낸다

 

Slack App 생성

가장 먼저 Slack과 연동 작업을 수행할 Slack App을 생성한다.

  1. Slack API 사이트 접속: https://api.slack.com/apps 로 이동하여 Create New App 버튼을 클릭한다.

  2. 앱 생성 방식 선택: From scratch (처음부터 만들기)를 선택한다.

  3. 앱 이름 및 워크스페이스 지정:

    • App Name: 앱의 이름을 입력한다. (예: slack-app-test)
    • Workspace: 앱을 설치할 Slack 워크스페이스를 선택한다.
    • Create App 버튼을 클릭한다.

    image-20250709132038140

  4. 기본 정보 확인: 앱이 생성되면 Basic Information 페이지로 이동한다. 여기서 나중에 사용될 Signing Secret과 같은 중요한 정보를 확인할 수 있다.

    image-20250709132348535

  5. 앱 Install

    Install App 에 들어가면 최소 하나의 권한을 줘야 install 을 할수있다는 문구가 뜰텐데,

    스크린샷 2025-07-09 오후 2.07.26

    OAuth & Permissions -> Scopes -> Add an OAuth Scope -> command, incoming-webhook 권한을 주도록 하자.

     

    이후 install 버튼이 활성화 되는걸 확인할수 있고 install 을 누르게 되면 incoming-webhook 을 어느 채널에 게시할건지 물어보는 창이 뜬다. 이때 임시로 webhook 을 받을 채널을 만든후 설정해주자.

 

EC2 제어용 Lambda 함수 생성

Slack 요청을 받아 실제로 EC2 인스턴스를 제어할 Lambda 함수를 생성한다.

  1. AWS Lambda 콘솔 접속: AWS 관리 콘솔에서 Lambda 서비스로 이동하여 함수 생성 버튼을 클릭한다.

  2. 함수 생성:

    • Function name: 함수의 이름을 입력한다. (예: slack-ec2-controller)
    • Runtime: Python 3.13 또는 원하는 언어를 선택한다.
    • Permissions: AmazonEC2FullAccess, AmazonSSMFullAccess 권한을 가진 역활을 연결해준다. (역활이 없다면 위 권한을 가진 역활을 만들어준후 연결해주면 된다.)
    • 추가 권한: 추가 권한은 아래와 같이 설정해준다. slack 에서 요청할 url 이 필요하기 떄문에 URL 을 활성화하고, 인증 유형을 None 으로 설정해준다. (slack 의 signature key를 검증할거기 때문에 괜찮다)

    image-20250709142330245

  3. IAM 역할에 EC2 권한 추가:

    • ec2 에는 SSM 관련 권한을 줘야한다. AmazonSSMManagedInstanceCore 을 가진 IAM 역활을 ec2 에게 부여해준다.
  4. Lambda 함수 코드 작성:

    • 다시 Lambda 함수 Code 탭으로 돌아와 lambda_function.py 파일에 아래 코드를 붙여넣는다. 이 코드는 Slack의 요청을 받아 EC2 인스턴스를 시작하고 결과를 Slack에 알린다.
    import boto3
    import json
    import os
    import time
    import hmac
    import hashlib
    import base64
    import urllib.request
    
    # 환경 변수로부터 필요한 값 읽기
    region = 'ap-northeast-2'
    instance_id = '인스턴스-id'
    signing_secret = os.environ['SLACK_SIGNING_SECRET']
    ssm_doc_path = '/home/ubuntu/nginx/conf.d/service-container.inc' # green, blue 중 최근에 배포된게 뭔지 확인할수 있는 파일 경로
    
    # AWS 클라이언트 초기화
    ec2 = boto3.client('ec2', region_name=region)
    ssm = boto3.client('ssm', region_name=region)
    
    def notify_slack(text):
        webhook_url = os.environ['SLACK_WEBHOOK_URL']
        payload = json.dumps({"text": text}).encode("utf-8")
    
        try:
            req = urllib.request.Request(
                webhook_url,
                data=payload,
                headers={"Content-Type": "application/json"},
                method="POST"
            )
            with urllib.request.urlopen(req) as res:
                print(f"Slack notify status: {res.status}")
        except Exception as e:
            print(f"Slack message failed: {e}")
    
    
    # Slack Signature 검증 함수
    def is_valid_slack_request(headers, body, is_base64_encoded):
        slack_signature = headers.get('x-slack-signature', '')
        slack_timestamp = headers.get('x-slack-request-timestamp', '')
        
        if is_base64_encoded:
            body = base64.b64decode(body).decode('utf-8')
    
        if not slack_signature or not slack_timestamp:
            return False
    
        if abs(time.time() - int(slack_timestamp)) > 60 * 5:
            print("❌ Timestamp too old")
            return False
    
        base_string = f"v0:{slack_timestamp}:{body}".encode("utf-8")
        my_signature = "v0=" + hmac.new(
            signing_secret.encode("utf-8"),
            base_string,
            hashlib.sha256
        ).hexdigest()
    
        return hmac.compare_digest(my_signature, slack_signature)
    
    def lambda_handler(event, context):
        headers = event.get("headers", {})
        body = event.get("body", "")
        is_base64_encoded = event.get("isBase64Encoded", False)
    
        if not is_valid_slack_request(headers, body, is_base64_encoded):
            return {
                "statusCode": 401,
                "body": "Unauthorized"
            }
    
        try:
            response = ec2.describe_instance_status(
                InstanceIds=[instance_id],
                IncludeAllInstances=True
            )
            state = response['InstanceStatuses'][0]['InstanceState']['Name']
            print(f"💡 EC2 현재 상태: {state}")
    
            if state in ['stopped', 'stopping']:
                ec2.start_instances(InstanceIds=[instance_id])
                print("☁️ EC2 시작, SSM 대기 중...")
    
                # SSM PingStatus 확인
                while True:
                    info = ssm.describe_instance_information(
                        Filters=[{'Key': 'InstanceIds', 'Values': [instance_id]}]
                    )
                    instance_info = info.get('InstanceInformationList', [])
                    if instance_info and instance_info[0]['PingStatus'] == 'Online':
                        break
                    time.sleep(5)
    
                # 서비스 up
                pre_command = """
                cd /home/ubuntu
                sudo docker compose config --services | grep -vE "dev-blue|dev-green" | xargs -r docker compose up -d
                """
                ssm.send_command(
                    InstanceIds=[instance_id],
                    DocumentName="AWS-RunShellScript",
                    Parameters={'commands': [pre_command]},
                )
    
                main_command = f"""
                cd /home/ubuntu
                if grep -q "dev-blue" {ssm_doc_path}; then
                    sudo docker compose up -d dev-blue
                elif grep -q "dev-green" {ssm_doc_path}; then
                    sudo docker compose up -d dev-green
                else
                    echo "No active container info."
                fi
                """
                ssm.send_command(
                    InstanceIds=[instance_id],
                    DocumentName="AWS-RunShellScript",
                    Parameters={'commands': [main_command]},
                )
    
                notify_slack("✅ EC2 인스턴스를 시작하고 서비스를 실행했습니다.")
            
            elif state in ['running', 'pending']:
                ec2.stop_instances(InstanceIds=[instance_id])
                notify_slack("🛑 EC2 인스턴스를 중지했습니다.")
            
            else:
                notify_slack(f"⚠️ 현재 상태({state})에서는 작업을 수행할 수 없습니다.")
    
        except Exception as e:
            print(f"❌ 작업 중 오류 발생: {e}")
            notify_slack(f"❌ 작업 중 오류 발생: {e}")
    
        return {
            "statusCode": 200,
            "body": json.dumps({"text": "⏳ 요청을 처리 중입니다..."}),
            "headers": {
                "Content-Type": "application/json"
            }
        }
    
    
  5. 환경 변수 설정:

    • Lambda 구성 탭 > 환경 변수로 이동한다.

    • Edit을 클릭하고 다음 두 변수를 추가한다.

      • SLACK_SIGNING_SECRET: 우리가 만들어줬던 App 의 Signing Secret 을 넣어주면 된다.

        스크린샷 2025-07-09 오후 2.28.30

      • SLACK_WEBHOOK_URL : slack app의 Incoming Webhooks 에 들어가 Webhook URL 로 설정해준다.

    • 일반 구성 -> 편집 으로 들어가 제한시간을 2~3분 정도로 늘려주자

 

Slack 에서 Command 를 통해 Lambda 호출

우리는 Slack App 에

image-20250709143405214

2개의 권한을 줬다.

commands : slack 채널에서 command 를 통해 Lambda 를 트리거 하는 용도이다.

incoming-webhook : 우리가 command 로 Lambda 를 트리거 하고 결과를 특정 채널로 보내주기 위함이다.

 

incoming-webhook 은 위에서 설정을 했고, 이제 남은건 commands 를 통해 람다를 트리거 하는 동작이다.

slack api 의 Slash Commands 에 들어가 Create New Command 를 눌러 새로운 command 를 만들어 준다.

  • command : 원하는 command 명

  • request: url : lambda 의 함수 URL

    image-20250709143812478

그외 설정은 입맛에 맞게 하면 된다.

image-20250709143921997

이런 식으로 설정을 해주고 save 로 마무리 해준다.

 

테스트

이제 모든 설정이 끝났다.

슬랙으로 들어가 slash command 를 통해 ec2 가 꺼지고 켜지는지 테스트 하면 된다.

image-20250709144041900

우리가 원하는 slash command 를 확인할수 있다.

기존 test 서버는 켜져있는 상태였다. 따라서 해당 command 를 입력하면 아래와 같이 중지했다는 응답이 webbhook 에 의해 보인다.

image-20250709144147796

실제 aws ec2 에 들어가서 봐도 마지막 인스턴스가 종료중인걸 확인할 수 있다.

스크린샷 2025-07-09 오후 2.42.17

다시한번 /test-trigger 를 누르면 아래와 같이 서비스를 실행했다는 메세지를 받을수있으며

image-20250709144439788

SSH 로 접속해서 확인하면 원하는 container 들이 모두 켜져있는걸 확인 할 수 있다.스크린샷 2025-07-09 오후 2.45.58

EC2 인스턴스 예약 실행으로 비용 최적화

[AWS 비용 절감 #2] EC2 인스턴스 예약 실행으로 비용 최적화

목표: 개발 서버 EC2 인스턴스를 평일 오전 10:00부터 오후 10:00까지만 활성화하여 불필요한 비용 발생을 방지한다.

구현 방식:

  • EventBridge (이벤트 브리지): 특정 시간에 Lambda 함수를 트리거하는 스케줄러 역할을 한다.
  • Lambda (람다): EC2 인스턴스를 시작하고 중지하는 코드를 실행한다.
  • IAM (Identity and Access Management): Lambda와 EC2가 서로 통신하고 필요한 작업을 수행할 수 있도록 권한을 부여한다.
  • SSM (Systems Manager): Lambda가 EC2 인스턴스 내부에서 명령어를 실행할 수 있도록 돕는다.

블루/그린 배포 환경 고려:

개발 서버는 블루/그린 배포 환경으로 구성되어 있다. Lambda 코드는 EC2 인스턴스 내의 파일을 확인하여 현재 활성화된 서버(블루 또는 그린)를 파악하고, 해당 서버만 실행하도록 구현한다.

 

전체 흐름 요약

  1. IAM 역할 생성: Lambda와 EC2에 필요한 권한을 부여하는 역할을 각각 생성한다.
  2. Lambda 함수 생성: EC2 인스턴스를 시작하고 중지하는 Python 코드로 Lambda 함수를 생성한다.
  3. EC2에 IAM 역할 부여: Lambda가 SSM을 통해 EC2 내부에서 명령어를 실행할 수 있도록 EC2에 생성한 IAM 역할을 연결한다.
  4. EventBridge 규칙 생성: 정해진 시간에 Lambda 함수를 트리거하도록 EventBridge 규칙을 생성하고 Lambda 함수와 연결한다.

 

1. IAM 역할 생성

 

1.1. Lambda를 위한 IAM 역할 생성

경로: IAM > 역할 > 역할 만들기

Lambda가 EC2를 제어하고, EC2 인스턴스가 켜졌을 때 스크립트를 실행하여 Docker 컨테이너를 실행할 수 있도록 미리 IAM 역할을 생성한다.

Lambda 역할 생성

필요 권한:

  • AmazonEC2FullAccess: EC2 인스턴스를 제어하기 위한 권한
  • AmazonSSMFullAccess: SSM을 통해 EC2 내부에서 명령어를 실행하기 위한 권한

위 권한을 추가하고 원하는 역할 이름을 설정하여 IAM 역할 생성을 완료한다.

 

1.2. EC2를 위한 IAM 역할 생성

경로: IAM > 역할 > 역할 만들기

EC2 인스턴스에서 SSM Agent가 정상적으로 동작하기 위해서는 AmazonSSMManagedInstanceCore 권한이 필요하다. 위와 동일한 방법으로 해당 권한을 가진 IAM 역할을 생성한다.

 

2. Lambda 함수 생성

경로: Lambda > 함수 > 함수 생성

Lambda 함수 생성

  • 함수 이름: 원하는 함수 이름을 입력한다.
  • 런타임: Python 3.9 (또는 원하는 언어)를 선택한다.
  • 아키텍처: x86_64를 선택한다.
  • 권한: "기존 역할 사용"을 선택하고, 1.1. Lambda를 위한 IAM 역할 생성에서 생성한 IAM 역할을 연결한다.

Lambda 역할 선택

Lambda 코드 작성

import boto3
import datetime
import time

region = 'ap-northeast-2 (REGION)'
instance_id = 'ec2-인스턴스-id'
ssm_doc_path = '/home/ubuntu/nginx/conf.d/service-container.inc' (ec2 종료되기전 실행했던 blue, green 이 저장되있는파일)

ec2 = boto3.client('ec2', region_name=region)
ssm = boto3.client('ssm', region_name=region)

def lambda_handler(event, context):
    now = datetime.datetime.utcnow()
    hour = now.hour
    minute = now.minute
    
    # 서버를 킨다.
    if hour == 1 and 0 <= minute <= 5: # 10:00 KST = 01:00 UTC
        # EC2 시작
        ec2.start_instances(InstanceIds=[instance_id])
        print("EC2 started.")

        # SSM Agent Online 될 때까지 기다리기
        while True:
            response = ssm.describe_instance_information(
                Filters=[
                    {
                        'Key': 'InstanceIds',
                        'Values': [instance_id]
                    }
                ]
            )
            instance_info = response['InstanceInformationList']
            if instance_info and instance_info[0]['PingStatus'] == 'Online':
                print("SSM Agent is Online!")
                break
            else:
                print("SSM Agent not ready... waiting 5s")
                time.sleep(5)

        # 2️⃣ dev-blue / dev-green 제외 나머지 서비스 up
        pre_command = f"""
        cd /home/ubuntu
        docker compose config --services | grep -vE "dev-blue|dev-green" | xargs -r docker compose up -d
        """
        response_pre = ssm.send_command(
            InstanceIds=[instance_id],
            DocumentName="AWS-RunShellScript",
            Parameters={'commands': [pre_command]},
        )
        print("SSM Run Command for other services sent:", response_pre)

        # 3️⃣ dev-blue / dev-green 중 하나만 up
        command = f"""
        cd /home/ubuntu
        if grep -q "dev-blue" {ssm_doc_path}; then
            docker compose up -d dev-blue
        elif grep -q "dev-green" {ssm_doc_path}; then
            docker compose up -d dev-green
        else
            echo "No active container info."
        fi
        """
        response = ssm.send_command(
            InstanceIds=[instance_id],
            DocumentName="AWS-RunShellScript",
            Parameters={'commands': [command]},
        )
        print("SSM Run Command for dev-blue/green sent:", response)

    # ✅ 2️⃣ 서버 종료
    elif hour == 13 and 0 <= minute <= 1:  # 22:00 KST = 13:00 UTC
        ec2.stop_instances(InstanceIds=[instance_id])
        print("EC2 stopped.")
    else:
        print("No action at this time.")
        print(f"Current time: {hour}:{minute}")

작성한 코드를 Lambda 함수에 적용하고 Deploy 버튼을 클릭한다.

 

Lambda 제한 시간 설정:

경로: 구성 > 일반 구성 > 편집 > 제한 시간

Lambda 함수의 기본 제한 시간은 3초이다. EC2 인스턴스 시작 및 스크립트 실행에 시간이 더 걸릴 수 있으므로 제한 시간을 충분히 늘려준다. (예: 1분)

 

3. EC2에 IAM 역할 부여

경로: EC2 > 인스턴스 > (해당 인스턴스 선택) > 작업 > 보안 > IAM 역할 수정

1.2. EC2를 위한 IAM 역할 생성에서 생성한 AmazonSSMManagedInstanceCore 권한을 가진 IAM 역할을 EC2 인스턴스에 연결한다.

 

4. EventBridge 규칙 생성

경로: Amazon EventBridge > 규칙 > 규칙 생성

EC2 인스턴스를 시작하고 중지하는 두 가지 규칙을 각각 생성한다.

EventBridge 규칙 생성

 

4.1. EC2 시작 규칙

  • 규칙 유형: 일정을 선택한다.

  • Cron 표현식: 0 1 ? * MON-FRI * (매주 월요일부터 금요일까지 01:00 UTC에 실행)

    • 참고: KST 기준 오전 10:00는 UTC 기준 01:00이다.

EventBridge Cron 표현식

  • 대상 선택:

    • 대상: Lambda 함수
    • 함수: 위에서 생성한 Lambda 함수를 선택한다.

 

4.2. EC2 중지 규칙

  • 규칙 유형: 일정을 선택한다.

  • Cron 표현식: 0 13 ? * MON-FRI * (매주 월요일부터 금요일까지 13:00 UTC에 실행)

    • 참고: KST 기준 오후 10:00는 UTC 기준 13:00이다.
  • 대상 선택: 시작 규칙과 동일하게 설정한다.

 

세 줄 요약

  1. IAM 역할을 생성하여 Lambda와 EC2에 필요한 권한을 부여한다.
  2. EC2를 켜고 끄는 Lambda 함수를 작성하고, EventBridge를 사용하여 특정 시간에 함수를 트리거하도록 설정한다.
  3. EC2에 IAM 역할을 연결하여 Lambda가 SSM을 통해 원격으로 명령을 실행할 수 있도록 한다.

+ Recent posts