분류 전체보기
-
CSV 기반 Elasticsearch 인덱스 재구성 및 Reindexing2025.06.17
-
JPQL 실행 시 Flush와 영속성 컨텍스트 동작 확인2024.07.22
-
API 통신 시 null 값 처리: 포함 vs. 미포함의 장단점2024.07.22
[AWS 비용 절감 #3]slack과 aws의 lambda 연동
[AWS 비용 절감 #3] Slack과 AWS Lambda를 버튼으로 연동하기
AWS 비용 절감 #1: S3 + CloudFront + HTTPS 정적 웹사이트 AWS 비용 절감 #2: EC2 인스턴스 예약 실행으로 비용 최적화
앞선 두 가지 방식으로 AWS 비용이 절반가량 줄었다.
하지만 EC2 인스턴스를 예약 실행하는 방식(오전 9시부터 오후 9시까지)을 도입하고 나니 한 가지 문제가 생겼다.
예약된 시간 외에 서버를 사용해야 할 경우, 다음과 같은 두 가지 방법이 있다.
- AWS 콘솔에 직접 접속하여 EC2 인스턴스를 시작하고, SSH로 접속해 필요한 컨테이너를 실행한다.
- Lambda의 실행 시간을 임시로 변경하여 원하는 시간에 EC2 인스턴스를 실행한다.
위 두 가지 방법 모두 가능하지만, 만약 내가 자리에 없는 상황에서 다른 팀원이 테스트 서버를 사용해야 한다면 즉각적인 대응이 어렵다는 문제가 있다.
이러한 문제를 해결하기 위해 현재 사용 중인 협업 도구인 Slack을 통해 특정 채널에 특정 명령어로 Lambda 를 trigger 해보기로 했다.
전체 아키텍처
[Slack User] -> [Slash Command] -> [Slack API] -> [AWS API Gateway] -> [AWS Lambda] -> [Amazon EC2]
- 사용자가 Slack 채널에서 특정 명령어(예:
/start-server
)를 입력한다. - Slack은 이벤트를 AWS API Gateway로 전달한다.
- API Gateway는 요청을 받아 연결된 AWS Lambda 함수를 트리거한다.
- Lambda 함수는 EC2 인스턴스를 시작하는 코드를 실행하고, 실행 결과를 Slack으로 다시 보낸다
Slack App 생성
가장 먼저 Slack과 연동 작업을 수행할 Slack App을 생성한다.
Slack API 사이트 접속: https://api.slack.com/apps 로 이동하여
Create New App
버튼을 클릭한다.앱 생성 방식 선택:
From scratch
(처음부터 만들기)를 선택한다.앱 이름 및 워크스페이스 지정:
- App Name: 앱의 이름을 입력한다. (예:
slack-app-test
) - Workspace: 앱을 설치할 Slack 워크스페이스를 선택한다.
Create App
버튼을 클릭한다.
- App Name: 앱의 이름을 입력한다. (예:
기본 정보 확인: 앱이 생성되면
Basic Information
페이지로 이동한다. 여기서 나중에 사용될 Signing Secret과 같은 중요한 정보를 확인할 수 있다.앱 Install
Install App 에 들어가면 최소 하나의 권한을 줘야 install 을 할수있다는 문구가 뜰텐데,
OAuth & Permissions -> Scopes -> Add an OAuth Scope -> command, incoming-webhook 권한을 주도록 하자.
이후 install 버튼이 활성화 되는걸 확인할수 있고 install 을 누르게 되면 incoming-webhook 을 어느 채널에 게시할건지 물어보는 창이 뜬다. 이때 임시로 webhook 을 받을 채널을 만든후 설정해주자.
EC2 제어용 Lambda 함수 생성
Slack 요청을 받아 실제로 EC2 인스턴스를 제어할 Lambda 함수를 생성한다.
AWS Lambda 콘솔 접속: AWS 관리 콘솔에서 Lambda 서비스로 이동하여
함수 생성
버튼을 클릭한다.함수 생성:
- Function name: 함수의 이름을 입력한다. (예:
slack-ec2-controller
) - Runtime:
Python 3.13
또는 원하는 언어를 선택한다. - Permissions:
AmazonEC2FullAccess
,AmazonSSMFullAccess
권한을 가진 역활을 연결해준다. (역활이 없다면 위 권한을 가진 역활을 만들어준후 연결해주면 된다.) - 추가 권한: 추가 권한은 아래와 같이 설정해준다. slack 에서 요청할 url 이 필요하기 떄문에 URL 을 활성화하고, 인증 유형을 None 으로 설정해준다. (slack 의 signature key를 검증할거기 때문에 괜찮다)
- Function name: 함수의 이름을 입력한다. (예:
IAM 역할에 EC2 권한 추가:
- ec2 에는 SSM 관련 권한을 줘야한다.
AmazonSSMManagedInstanceCore
을 가진 IAM 역활을 ec2 에게 부여해준다.
- ec2 에는 SSM 관련 권한을 줘야한다.
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" } }
- 다시 Lambda 함수
환경 변수 설정:
Lambda 구성 탭 > 환경 변수로 이동한다.
Edit
을 클릭하고 다음 두 변수를 추가한다.SLACK_SIGNING_SECRET
: 우리가 만들어줬던 App 의 Signing Secret 을 넣어주면 된다.SLACK_WEBHOOK_URL
: slack app의 Incoming Webhooks 에 들어가 Webhook URL 로 설정해준다.
일반 구성 -> 편집 으로 들어가 제한시간을 2~3분 정도로 늘려주자
Slack 에서 Command 를 통해 Lambda 호출
우리는 Slack App 에
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
그외 설정은 알아서 하자.
이런 식으로 설정을 해주고 save 로 마무리 해준다.
테스트
이제 모든 설정이 끝났다.
슬랙으로 들어가 slash command 를 통해 ec2 가 꺼지고 켜지는지 테스트 하면 된다.
우리가 원하는 slash command 를 확인할수 있다.
기존 test 서버는 켜져있는 상태였다. 따라서 해당 command 를 입력하면 아래와 같이 중지했다는 응답이 webbhook 에 의해 보인다.
실제 aws ec2 에 들어가서 봐도 마지막 인스턴스가 종료중인걸 확인할 수 있다.
다시한번 /test-trigger 를 누르면 아래와 같이 서비스를 실행했다는 메세지를 받을수있으며
SSH 로 접속해서 확인하면 원하는 container 들이 모두 켜져있는걸 확인 할 수 있다.
'AWS' 카테고리의 다른 글
[AWS 비용 절감 #2] EC2 인스턴스 예약 실행으로 비용 최적화 (2) | 2025.07.04 |
---|---|
[AWS 비용 절감 #1] S3 + CloudFront + HTTPS 정적 웹사이트 (0) | 2025.05.19 |
[AWS 비용 절감 #2] 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 인스턴스 내의 파일을 확인하여 현재 활성화된 서버(블루 또는 그린)를 파악하고, 해당 서버만 실행하도록 구현한다.
전체 흐름 요약
- IAM 역할 생성: Lambda와 EC2에 필요한 권한을 부여하는 역할을 각각 생성한다.
- Lambda 함수 생성: EC2 인스턴스를 시작하고 중지하는 Python 코드로 Lambda 함수를 생성한다.
- EC2에 IAM 역할 부여: Lambda가 SSM을 통해 EC2 내부에서 명령어를 실행할 수 있도록 EC2에 생성한 IAM 역할을 연결한다.
- EventBridge 규칙 생성: 정해진 시간에 Lambda 함수를 트리거하도록 EventBridge 규칙을 생성하고 Lambda 함수와 연결한다.
1. IAM 역할 생성
1.1. Lambda를 위한 IAM 역할 생성
경로: IAM > 역할 > 역할 만들기
Lambda가 EC2를 제어하고, EC2 인스턴스가 켜졌을 때 스크립트를 실행하여 Docker 컨테이너를 실행할 수 있도록 미리 IAM 역할을 생성한다.
필요 권한:
AmazonEC2FullAccess
: EC2 인스턴스를 제어하기 위한 권한AmazonSSMFullAccess
: SSM을 통해 EC2 내부에서 명령어를 실행하기 위한 권한
위 권한을 추가하고 원하는 역할 이름을 설정하여 IAM 역할 생성을 완료한다.
1.2. EC2를 위한 IAM 역할 생성
경로: IAM > 역할 > 역할 만들기
EC2 인스턴스에서 SSM Agent가 정상적으로 동작하기 위해서는 AmazonSSMManagedInstanceCore
권한이 필요하다. 위와 동일한 방법으로 해당 권한을 가진 IAM 역할을 생성한다.
2. Lambda 함수 생성
경로: Lambda > 함수 > 함수 생성
- 함수 이름: 원하는 함수 이름을 입력한다.
- 런타임:
Python 3.9
(또는 원하는 언어)를 선택한다. - 아키텍처:
x86_64
를 선택한다. - 권한: "기존 역할 사용"을 선택하고, 1.1. Lambda를 위한 IAM 역할 생성에서 생성한 IAM 역할을 연결한다.
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 인스턴스를 시작하고 중지하는 두 가지 규칙을 각각 생성한다.
4.1. EC2 시작 규칙
규칙 유형: 일정을 선택한다.
Cron 표현식:
0 1 ? * MON-FRI *
(매주 월요일부터 금요일까지 01:00 UTC에 실행)- 참고: KST 기준 오전 10:00는 UTC 기준 01:00이다.
대상 선택:
- 대상:
Lambda 함수
- 함수: 위에서 생성한 Lambda 함수를 선택한다.
- 대상:
4.2. EC2 중지 규칙
규칙 유형: 일정을 선택한다.
Cron 표현식:
0 13 ? * MON-FRI *
(매주 월요일부터 금요일까지 13:00 UTC에 실행)- 참고: KST 기준 오후 10:00는 UTC 기준 13:00이다.
대상 선택: 시작 규칙과 동일하게 설정한다.
세 줄 요약
- IAM 역할을 생성하여 Lambda와 EC2에 필요한 권한을 부여한다.
- EC2를 켜고 끄는 Lambda 함수를 작성하고, EventBridge를 사용하여 특정 시간에 함수를 트리거하도록 설정한다.
- EC2에 IAM 역할을 연결하여 Lambda가 SSM을 통해 원격으로 명령을 실행할 수 있도록 한다.
'AWS' 카테고리의 다른 글
[AWS 비용 절감 #3]slack과 aws의 lambda 연동 (3) | 2025.07.09 |
---|---|
[AWS 비용 절감 #1] S3 + CloudFront + HTTPS 정적 웹사이트 (0) | 2025.05.19 |
CSV 기반 Elasticsearch 인덱스 재구성 및 Reindexing
기존 products_v1 인덱스를 초기화한 후, CSV 파일을 기반으로 products_v2 인덱스를 생성하고, 해당 데이터를 다시 products_v1 으로 reindex하려고 한다.
이 작업은 Kibana UI와 Dev Tools 콘솔을 통해 진행한다.
CSV 파일 기반 index 생성
kibana -> Analytics -> Machine Learning -> Visualize data from a file
- CSV 파일 업로드
- CSV에 헤더가 있다면 → Override settings → Has header row 체크
- 원하는 인덱스 이름 입력 (products_v2 등)
- 인덱스 생성 완료
Kibana -> Analytics -> Discover
Create data view 를 클릭했다면 Discover 에 우리가 추가한 index 를 기반으로 한 dataview 가 생성된다. 굳이 dataview 를 통해 index 를 볼필요가 없다면 data view 를 만들지 않아도 상관없다.
Reindex
products_v2 인덱스가 준비되었으면, 기존 products_v1 데이터를 삭제한 후, products_v2 의 데이터를 기반으로 products_v1 을 다시 채운다.
Kibana -> Management -> Dev Tools
Kibana 의 Dev Tools 를 들어가면 우리가 원하는 스타일의 명령어를 입력할수 있다.
POST products_v1/_delete_by_query
{
"query": {
"match_all": {}
}
}
위 명령어를 통해 products_v1 인덱스의 모든 문서만 삭제한다.
인덱스 자체는 삭제하지 않아야 하므로, DELETE 가 아닌 _delete_by_query 를 사용한다.
위 명령어를 통해 모든 products_v1 의 모든 데이터를 삭제했다면 이제 아래 명령어를 통해 products_v2 기반으로 products_v1 을 reindex 를 하면 된다.
POST _reindex
{
"source": {
"index": "products_v2"
},
"dest": {
"index": "products_v1"
}
}
products_v2의 데이터를 기반으로 products_v1 인덱스를 재구성한다.
기존 products_v1의 매핑 및 설정은 유지되며, 데이터만 덮어씌워진다.
3줄 요약
- CSV 파일 기반 products_v2 인덱스 생성
- 기존 products_v1 문서 삭제 (_delete_by_query)
- products_v2 → products_v1로 데이터 복사 (Reindex)
[AWS 비용 절감 #1] S3 + CloudFront + HTTPS 정적 웹사이트
EC2 → S3 + CloudFront + HTTPS 정적 웹사이트 배포 전환
기존에 정적파일을 배포하는데 EC2 를 사용했다.
해당 방법보다 S3 + CloudFront + ACM 을 활용하는 방법이 비용 절감이 가능하며 캐싱기능을 통해 더 빠른 응답이 가능하기 때문에, 해당 방법으로 바꾸려고 한다.
전체 흐름 요약
- S3 버킷 생성 및 파일 업로드 → 정적 파일 업로드
- CloudFront 생성 → OAC 설정 + CNAME + SSL 인증(SSL 인증을 위한 가비아 DNS 설정)
- S3 정책 설정
- 가비아 DNS 설정
- CloudFront 오류 페이지 설정 → 403/404 처리
1. S3 버킷 생성 및 Build 파일 업로드
S3 버킷 생성
이때 S3 의 퍼블릭 액세스 차단 설정은 풀지 않은 상태로 버킷을 만든다 (추후 OAC 를 통해 CloudFront 에서만 S3 객체에 접근하기 위함이다.)
해당 단계에서는 버킷 이름을 작성하는것 외엔 따로 설정할것 없이 버킷을 만든다.
버킷에 build 파일 업로드
2. CloudFront 생성
우리의 목표는 CloudFront 를 활용하여 우리가 원하는 도메인을 통해 정적 파일에 접근하는 것이며, 이때 통신은 HTTPS 를 통해 하길 원한다.
CloudFront 생성
Origin domain -> 우리가 원하는 버킷(방금 생성하고 build 파일을올린)을 골라준다.
OAC 생성
원본 액세스 -> OAC 설정을 하기위해 원본 액세스 탭에서 원본 액세스 제어 설정(권장) 을 클릭한후 Create New OAC 를 통해 새로운 OAC 를 생성해준다.
OAC 생성후 아래로 내려와 설정 탭에서 다음과 같은 설정을 해준다.
CNAME 탭에 우리의 도메인 을 포함한 설정하고 싶은 도메인을 추가한다.
ex) 내 도메인이 test.com 이라면 아래와 같이 사용할 도메인 및 서브 도메인을 추가해준다.
SSL 인증서 발급
아직 인증서를 생성한게 없으니 인증서 선택에는 아무런 인증서가 뜨지 않는다. 따라서 인증서 요청 버튼을 통해 우리가 원하는 도메인 및 서브 도메인에 대한 SSL 인증서를 받아준다.
도메인 이름에 위에서 설정한 CNAME 과 동일한 도메인을 추가해준다.
그후 요청을 누르면 도메인 탭에 우리가 추가해준 도메인들과 각각에 해당하는 CNAME 이름 CNAME 값 이 떠잇는걸 확인할수있다.
가비아 DNS 설정 (SSL 인증을 위한)
경로 : My 가비아 → 도메인 → DNS 관리 툴 → (원하는 도메인 오른쪽의 "관리")
우린 가비아 에서 도메인을 구입했기 때문에 해당 사이트에 들어가 도메인의 DNS 관리 툴을 사용하여 인증서 발급이 원활하게 해주면 된다.
가비아에서 CNAME 레코드를 추가하려면 다음과 같은 경로로 이동한다:
여기서 CNAME 레코드를 2개 추가해야 한다. 하나는 test.com용, 다른 하나는 www.test.com용이다.
만약 AWS에서 아래처럼 값을 받았다면:
도메인 | CNAME 이름 (호스트) | CNAME 값 |
---|---|---|
test.com | _abcde123.test.com | _xyz.acm-validations.aws. |
www.test.com | _abcde123.www.test.com | _uvw.acm-validations.aws. |
→ 가비아에서는 이렇게 입력하면 된다:
타입 | 호스트 | 값 |
---|---|---|
CNAME | _abcde123 | _xyz.acm-validations.aws. |
CNAME | _abcde123.www | _uvw.acm-validations.aws. |
가비아에서 해당 세팅 완료시 AWS 에서 검증 대기 중 이였던 상태가 모두 성공 으로 바뀔것이다.
CloudFront 배포 페이지로 돌아가 Refresh 버튼을 눌러 인증서를 새로고침하면 우리가 생성한 SSL 인증서가 보일것이다.
해당 SSL 인증서를 설정해주면 우리가 지정한 도메인에 대한 SSL 인증서가 적용 완료된다.
기본값 루트 객체 에 index.html을 기본 루트 객체로 지정해 CloudFront 설정을 마무리한다.
3. S3 정책 설정
CloudFront 생성을 완료했다면, 아래와같이 생성된 페이지로 이동하면서 상단에 S3에 정책을 추가해달라는 문구가 뜰것이다.
경로 : 정책을 업데이트하려면 S3 버킷 권한으로 이동합니다. 를 클릭하여 정책 설정 페이지로 이동 해주면 된다.
S3 버킷에서 권한 탭을 눌러 버킷 정책 탭에 편집을 통해 복사한 정책을 붙여넣어 준다.
해당 정책을 붙여넣기 해줌으로서 우리가 생성한 CloudFront 에서 S3 버킷의 접근이 가능해진다.
{
"Version": "2008-10-17",
"Id": "PolicyForCloudFrontPrivateContent",
"Statement": [
{
"Sid": "AllowCloudFrontServicePrincipal",
"Effect": "Allow",
"Principal": {
"Service": "cloudfront.amazonaws.com"
},
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::버킷이름/*",
"Condition": {
"StringEquals": {
"AWS:SourceArn": "CloudFront의 ARN"
}
}
}
]
}
정책 연결후엔 우리가 배포한 CloudFront 의 도메인 이름을 통해 해당 정적 파일 에 접근이 가능해진다.
4. 가비아 DNS 설정(CNAME 레코드 추가)
경로 : My 가비아 → 도메인 → DNS 관리 툴 → (원하는 도메인 오른쪽의 "관리")
이젠 우리가 원하는 S3 + CloudFront + https 설정을 완료했다.
최종적으론 test.com 또는 www.test.com 으로 접속시 우리가 cloudFront 를 통해 배포한 정적 파일이 보이면 된다.
가비아를 사용한다하면, 가비아의 DNS 설정에 들어가 CNAME 레코드를 생성해주면 된다.
이때 cloudFront 도메인이 https://cloudFront.net 처럼 되어있다면
cloudFront.net. 처럼 변경한후 추가해주면 된다.
이렇게하면 우리가 목표했던 S3 에 올린 정적인 파일을 CloudFront 를 통해 원하는 도메인을 통해 접근가능하도록 배포하며, 이때 통신은 Https 를 통해 가능하게 만들수있다.
5. CloudFront 오류 페이지 설정 (403/404 처리)
경로 : CloudFront -> 오류페이지
추가적으로 내 정적인 파일에선 새로고침시 403 에러가 떳다 따라서 CloudFront 의 오류페이지에 404, 403 예외에 대한 응답을 추가해줬다.
추후 EC2 를 사용했을때보다 CloudFront 를 사용함으로서 얼마만큼의 비용절감이 되었는지 정리하겠다.
'AWS' 카테고리의 다른 글
[AWS 비용 절감 #3]slack과 aws의 lambda 연동 (3) | 2025.07.09 |
---|---|
[AWS 비용 절감 #2] EC2 인스턴스 예약 실행으로 비용 최적화 (2) | 2025.07.04 |
JPQL 실행 시 Flush와 영속성 컨텍스트 동작 확인
JPQL 실행 시 Flush와 영속성 컨텍스트 동작 확인
이 테스트는 두 가지 주요 목적을 가지고 있습니다:
- JPQL 실행 전
flush
가 실행되는지 확인 - 영속성 컨텍스트에 동일한 ID를 가진 엔티티가 존재할 경우, JPQL로 가져온 값이 영속성 컨텍스트의 값으로 대체되지 않는지 확인
- JPQL 실행 전
@Test
@DisplayName("닉네임 변경 요청시 해당 회원의 닉네임을 변경한다.")
void updateMemberWithNickNameRequest() throws Exception {
//given
Member member = saveMemberAndMemberImage();
Long memberId = member.getId();
Jwt jwtCreatedBySavedMember = generateTokenWithMemberId(memberId);
MemberUpdateRequest memberUpdateRequest = MemberUpdateRequest.builder()
.nickname(CHANGED_NICK_NAME)
.build();
String jsonRequest = objectMapper.writeValueAsString(memberUpdateRequest);
//when & then
mockMvc.perform(
MockMvcRequestBuilders.patch("/api/members/{memberId}", memberId)
.header(AUTHORIZATION_STRING, JWT_TOKEN_PREFIX + jwtCreatedBySavedMember.getAccessToken())
.contentType(MediaType.APPLICATION_JSON)
.content(jsonRequest))
.andExpect(status().isOk());
Member updatedMember = memberProvider.findById(memberId);
assertThat(updatedMember.getNickname()).isEqualTo(CHANGED_NICK_NAME);
}
테스트 설명
Member 저장
Member member = saveMemberAndMemberImage(); Long memberId = member.getId();
이 메서드에 의해
member
와image
가 저장됩니다.@GeneratedValue(strategy = GenerationType.IDENTITY)
로 설정되어 있기 때문에member
가 저장될 때 바로 저장 쿼리가 실행됩니다. 이 상태에서 영속성 컨텍스트와 데이터베이스 모두에member
가 존재합니다. 이때Member
엔티티의isWithdrawal
필드는 데이터베이스에 의해 기본값으로 초기화되지만, 해당필드의 초기화 상태가 영속성 컨텍스트에는 반영되지 않습니다.
MockMvc 요청
mockMvc.perform( MockMvcRequestBuilders.patch("/api/members/{memberId}", memberId) .header(AUTHORIZATION_STRING, JWT_TOKEN_PREFIX + jwtCreatedBySavedMember.getAccessToken()) .contentType(MediaType.APPLICATION_JSON) .content(jsonRequest)) .andExpect(status().isOk());
이 요청은
member
의 닉네임을 변경합니다.updateMember
메서드는 JPQL을 통해member
를 조회합니다. 이때 JPQLmemberQueryService.findById(memberId)
이 실행되기 전에flush
가 자동으로 실행됩니다.@Transactional public void updateMember(Long memberIdFromJwt, Long memberId, MemberUpdateRequest memberUpdateRequest) { validateMemberId(memberIdFromJwt, memberId); Member member = memberQueryService.findById(memberId); // JPQL 실행 전 flush() 호출 Optional.ofNullable(memberUpdateRequest.getAlert()) .ifPresent(member::updateAlert); Optional.ofNullable(memberUpdateRequest.getNickname()) .ifPresent(member::updateNickname); }
entityManager.clear()
를 호출하지 않았으므로,entityManager
에는 여전히saveMemberAndMemberImage()
메서드에 의해 저장된member
가 존재합니다. 이 상태에서 JPQL로 가져온member
는 영속성 컨텍스트에 이미 존재하는member
로 대체되지 않습니다.(JPQL 을 통해 값을 가져오더라도, 영속성 컨텍스트안에 같은
ID
를 가진 객체가 있다면 JPQL 을 통해 가져온 값을 버리고 영속성 컨텍스트 안에 값을 유지하기 때문에 DB에 의해 기본값으로 초기화된 엔티티가 현재 영속성 컨텍스트 안의 엔티티로 대체되지 않습니다.)
Update 와 Flush
Member updatedMember = memberProvider.findById(memberId); assertThat(updatedMember.getNickname()).isEqualTo(CHANGED_NICK_NAME);
이 코드에서
memberProvider.findById(memberId)
는 JPQL 쿼리를 실행하며, 쿼리 실행 전에flush
가 호출됩니다.flush
가 호출될때,mockMvc.perform()
에 의해 실행된Member
엔티티 업데이트 쿼리가 나갈것입니다. 트랜잭션의 전파에 의해 test 코드에 걸어놓은 트랜잭션이 끝나지 않았으므로mockMvc.perform
에 의해 실행된 update 쿼리는mockMvc.perform
이 실행이 완료된 후에도 아직 실행되지 않기 때문입니다.
❗️이때 중요한것은 우린 아직 entityManager.clear()
를 한번도 호출하지 않았다는 점입니다. Member
엔티티 안에는 DB의 기본값에 의해 초기화되는 isWithdrawal
이라는 필드가 있는데, 아직 영속성 컨텍스트 안에 있는 entity
는 clear()
가 된 적이 없고, 따라서 DB에 의해 초기화된 필드는 영속성 컨텍스트 안에 적용이 되지 않았을 것입니다.
이를 검증하기 위해 아래와 같이 최종적으로 찾은 updatedMember
의 isWithdrawal
필드를 가져와보면 null
인것을 확인할수 있으며
만약 isWithdrawal
이 초기화된 상태를 보고싶다면 movkMvc.perform()
메서드 호출 이전 또는 이후에 entityManager
의 clear
메서드를 실행해주면 잘초기화된 isWithdrawal
값을 볼수있습니다.
참고로 mockMvc.perform
이후에 clear()
를 호출하는 로직에서 entityManger.flush()
가 추가된 이유는 만약 entityManger.clear()
만 호출시 mockMvc.perform()
에 의해 업데이트된 member
엔티티의 변경사항이 모두 detach
되면서 업데이트 쿼리가 나가지 않습니다. 따라서 mockMvc.perform
이후에는 entityManger.flush()
를 추가해 member
엔티티의 update
정보가 사라지지 않도록 합니다.
결론
JPQL이 실행되기 전에
flush
가 실행되는지 확인:mockMvc.perform()
이후,JPQL
로 인해update
쿼리가 실행되는 것을 확인했습니다. 이는JPQL
이 실행되기 전에flush
가 자동으로 호출된다는 것을 의미합니다.
영속성 컨텍스트에 동일한 ID의 엔티티가 존재할 때 JPQL로 가져온 값이 대체되지 않는지 확인:
entityManager.clear()
를 호출하지 않으면,JPQL
로 가져온 값이 영속성 컨텍스트에 이미 존재하는 값으로 대체되지 않는다는 것을 확인했습니다. 예를 들어,member.getIsWithdrawal()
이null
인 것을 확인했습니다.
사실, 엔티티 클래스 안에 초기화 로직을 넣거나 다른 방법으로 영속성 컨텍스트와 데이터베이스 간의 불일치를 줄이는 것이 가장 좋은 방법이지만, 이번 테스트는 이 불일치를 확인하기 위해 수행되었습니다.
'Spring' 카테고리의 다른 글
API 통신 시 null 값 처리: 포함 vs. 미포함의 장단점 (0) | 2024.07.22 |
---|---|
JPA에서 단방향 및 양방향 일대일 관계의 외래키 처리와 지연 로딩 문제 (1) | 2024.07.22 |
[Spring] DynamicInsert 사용 이유 (0) | 2024.07.22 |
Optional 클래스의 orElseThrow (0) | 2024.07.22 |
[Spring] Enum 타입으로 Exception 구현하기 (0) | 2024.07.22 |
API 통신 시 null 값 처리: 포함 vs. 미포함의 장단점
API 통신 시 null 값 처리: 포함 vs. 미포함의 장단점
JSON 기반의 API 통신을 할 때, 값이 null일 경우 해당 값을 요청이나 응답에 포함시켜야 하는지에 대해 고민해볼 필요가 있습니다. 이 글에서는 이 주제에 대해 StackOverflow
글을 기반하여 정리해보겠습니다.
StackOverflow
에서 같은 고민을 하는 사람들의 의견을 참고한 결과, Twitter와 같은 대형 플랫폼의 경우 "someGenericProperty":null
과 같은 26바이트를 차지하는 필드를 없애는 것만으로도 300GB 이상의 트래픽을 줄일 수 있다고 합니다. 하루에 수십억 건의 API 요청이 오가는 플랫폼에서는 이는 상당한 절감 효과를 가져올 수 있습니다.
그러나, null 값을 포함시키지 않는 것이 항상 최선의 선택은 아니라고 합니다. 일관성을 유지하는 것이 API 사용자의 혼란을 줄이고 예측 가능한 동작을 보장하며 디버깅을 용이하게 할 수 있습니다.
위 글을 기반으로 대략적으로 정리를 해보자면 내용은 아래와 같습니다.
일관성 유지:
- API 사용자는 모든 응답에서 예상 가능한 구조를 기대합니다. 필드가 항상 존재하면 API의 사용성과 신뢰성이 높아집니다.
잠재적 문제점:
- 특정 데이터 요소가 부재할 경우, 그 원인을 파악하기 어려울 수 있습니다. 예를 들어, 필드가 누락된 이유가 데이터가 없는 것인지, 아니면 데이터 전달 중 문제가 발생한 것인지 알기 어려울 수 있습니다.
- 필드가 예상대로 존재하지 않으면 소비자 애플리케이션의 안정성이 저하될 수 있습니다.
구현 관련 고려사항:
- null 값을 제외하려면 서버 측에서 추가 로직이 필요합니다. 이는 코드 복잡성을 증가시키고 유지보수 부담을 가중시킬 수 있습니다.
성능 관련:
- 네트워크 대기 시간과 대역폭을 고려해야 합니다. 대부분의 경우, 약간의 대역폭을 희생하더라도 일관된 응답 구조가 선호됩니다.
예외 상황:
- 극도로 제한된 대역폭 환경에서는 필드를 제외하는 것이 더 나을 수 있습니다.
- 희소 데이터 구조를 처리할 때도 필드를 제외하는 것이 유리할 수 있습니다.
결론적으로 예외적인 상황이 아닌 이상, null 값을 포함하더라도 해당 필드를 응답에 추가하는 것이 API 사용의 일관성을 유지하는 데 도움이 된다고 합니다.
'Spring' 카테고리의 다른 글
JPQL 실행 시 Flush와 영속성 컨텍스트 동작 확인 (0) | 2024.07.22 |
---|---|
JPA에서 단방향 및 양방향 일대일 관계의 외래키 처리와 지연 로딩 문제 (1) | 2024.07.22 |
[Spring] DynamicInsert 사용 이유 (0) | 2024.07.22 |
Optional 클래스의 orElseThrow (0) | 2024.07.22 |
[Spring] Enum 타입으로 Exception 구현하기 (0) | 2024.07.22 |