[AWS] Elastic Beanstalk graceful shutdown (feat. AutoScaling Lifecycle Hook - TERMINATING)
대상
서버가 종료될 때 미리 사전 작업을 해주고 싶은 분들에게 도움이 되는 글입니다.
제가 겪은 문제의 상황
백엔드 앱을 배포할 때마다 사용 중인 접속자가 튕기는 문제가 발생했습니다..
왜냐하면 Websocket은 TCP 연결을 계속 유지하기 때문에 Stateful 했습니다. TCP connection이 맺어진 서버가 블루-그린 배포이든 롤링 배포이든 다운이 되는 순간 모든 클라이언트가 튕겼습니다.
HTTP만 썼으면 세션을 외부에(Redis, PostgresSQL 등) 저장해 두거나 해서 이런 현상을 막을 수 있었겠지만 WebSocket은 애초에 stateful 해서 항상 튕겼습니다. 특히, 클라이언트에서는 현재 WebSocket 연결이 끊기면 main 페이지로 튕기게 되어있어서 기존 유저들이 불편함을 겪어서 이것을 해결할 필요가 있었습니다.
그래서 나온 해결 방법 :: 밤에 배포하기..
하지만 공지를 하고 밤에 배포하는 것도 방법이지만 이것을 더 깔끔하게 해결하는 방법은 없을까?
클라이언트에서 재접속할 수 있도록 방법을 제공해주자!
튕기는 것을 미리 클라이언트에게 알려주자!
이번 글에서는 AWS의 AutoScaling에서 제공해주는 Lifecycle Hook을 이용해서 서버가 종료될 때 미리 사전 작업(종료되기 전에 Client에게 언질주기)을 해주는 법에 대해서 알아보겠습니다.
준비물
- 로드밸런서
- Immutable 배포 방식
- AWS CLI
- AWS SDK
- 클라우드에서 제공해주는 LifeCycle Hook
- 웹소켓 서버 인스턴스 2개 이상
구현 하고 싶은 방식
저는 다음과 같이 구현하고 싶었습니다.
기존 서버를 내리기 전에 health-check를 fail하도록 바꿉니다.
health-check가 실패하기 시작하면 다른 서버로 트래픽을 보내게 됩니다.
이것을 활용해서 새로운 클라이언트의 접속을 막는 것입니다.
이 서버에 연결된 모든 소켓(클라이언트)에게 서버가 종료된다는 이벤트를 보냅니다.
이 서버에 연결된 클라이언트에게 튕길 것이니 재접속할 수 있도록 이벤트를 보내줍니다.
서버 종료 이벤트를 수신한 클라이언트는 서버와의 연결을 끊고 다시 재접속합니다.
그러면 로드밸런서에 의해 (종료하기 위한 작업을 하고 있는 서버가 아닌) 다른 서버에 연결됩니다.
새로 배포한 서버(혹은 서버 그룹)를 붙이고 기존의 서버는 완전히 종료시킵니다.
안전하게 새로운 서버 배포 끝!
아래에서는 위와 같은 시나리오를 실제로 구현해보겠습니다. ㅎㅎ
Elastic Beanstalk으로 Graceful하게 Shutdown하는 방법
현재 저는 배포 환경으로 AWS Elastic Beanstalk과 WebSocket을 편하게 쓰기 위해 Socket.IO + Redis Adapter의 조합으로 쓰고 있습니다. 다른 환경을 쓰더라도 AWS SDK 쓰는 법 빼고는 동일할 것입니다.
1. AWS AutoScaling Lifecycle Hook 설정
AutoScaling Lifecycle Hook이란?
먼저 종료되기 전에 멈추고 우리가 사전 작업을 해줄 수 있도록 Lifecycle Hook을 만듭니다.
AWS에서 제공해주는 Hook을 사용하면 개별 인스턴스가 시작할 때나 종료할 때를 우리가 미리 사전 작업을 할 수 있습니다. Hook이 설정이 됐다면 Timeout되거나 우리가 Continue하라고 알려줄 때까지 기다립니다.
AWS > EC2 > Auto Scaling Groups > Instance management에 가면 설정된 Lifecycle hook에 대해 볼 수 있습니다.
Elastic Beanstalk에서 Lifecycle Hook을 만드는 방법
AutoScaling에서 제공해주는 Lifecycle hooks은 그냥 아래처럼 클릭해서 GUI로 만들거나 CLI 명령어를 통해서 생성할 수 있습니다. 그리고 이것은 우리(사람이)가 직접적으로 만들어주는 것에 해당합니다.
그런데 Elastic Beanstalk을 쓴다면 위와 같이 직접적으로 hook을 생성하면 안 됩니다.
Elastic Beanstalk이 Lifecycle hook을 만들도록 해야 합니다.
생각해보면 Elastic Beanstalk 자동으로 AutoScaling Group도 만들고 그러는데 우리가 중간에 만들어 버리기는 뭔가 꼬일 것 같기도 하고 이상하긴하죵!
우리가 바로 생성하지 말고 Elastic Beanstalk이 생성하도록 해봅시다.
프로젝트의 .ebextensions 디렉터리 아래에 확장자가 .config인 파일을 만들어서 Elastic Beanstalk이 hook을 생성하도록 할 수 있습니다.
Elastic Beanstalk에서 .ebextension config 파일로 Lifecycle Hook 생성하는 법
AutoScaling Group의 Lifecycle hook을 ebextensions config 파일로 만드는 방법은 다음과 같습니다.
config 파일에서 필요로 하는 자원을 명시해주면 됩니다.
Resources:
lifecyclehook:
Type: AWS::AutoScaling::LifecycleHook
Properties:
AutoScalingGroupName: { "Ref" : "AWSEBAutoScalingGroup" }
LifecycleHookName: { "Ref" : "AWSEBAutoScalingGroup" }
LifecycleTransition: autoscaling:원하는Transition
Lifecycle Hook에 설정 값에 대한 설명
위 파일에서는 lifecyclehook에 대한 리소스를 정의합니다.
- Type은 LifecycleHook
- 속성으로는
- AutoScalingGroupName:
- { "Ref" : "AWSEBAutoScalingGroup" }
- Elastic Beanstalk에 의해서 생성된 Auto Scaling Group의 이름을 가져옵니다
- LifecycleHookName:
- 훅 이름입니다.
- 그룹 내에서 이름이 유일
- 최대 255자. "-", "_", "/" 제외한 스페이스랑 특수 문자
- 위에서는 생성된 Auto Scaling Group의 이름으로 설정하도록 했습니다.
- 훅 이름입니다.
- LifecycleTransition:
- 원하는 LifecycleTransition을 가져옵니다
- 실행될 때 사전 작업 - autoscaling:EC2_INSTANCE_LAUNCHING
- 종료될 때 사전 작업 - autoscaling:EC2_INSTANCE_TERMINATING
- 원하는 LifecycleTransition을 가져옵니다
- 다른 옵션들도 있습니다
- Heartbeat timeout
- wait 상태로 얼마나 있을지
- 제가 위에서 언급했듯이 Continue를 시키지 않으면 Hearbeat timeout만큼 기다립니다.
- 기본값이 3600초이기 때문에 잘못하면 서버가 시작하거나 종료되는데 한참 걸릴 수도 있습니다 ㅎㅎ
- Default result
- Timeout시 어떻게 처리할지
- Default result로는 Abandon 또는 Continue가 있다.
- 이것은 중요해서 바로 아래에 추가로 적어뒀습니다.
- 등
- Heartbeat timeout
- AutoScalingGroupName:
Default result: Abandon VS Continue
설정한 LifeCycle Hook에 따라 다르게 동작합니다. 자세한 것은 아래의 QnA 답변을 참고하세용.
(LifeCycle Hook이 EC2_INSTANCE_TERMINATING 경우에 한해서 부족한 영어 실력이지만 간단히 번역해보았습니다..)
LifeCycle Hook을 EC2_INSTANCE_TERMINATING로 설정한 경우에는 다음과 같이 동작합니다.
1. default result를 CONTINUE한 경우
- 종료될 때 먼저 TERMINATING_WAIT가 됩니다.
- 훅이 시간 초과가 되거나 실패한 경우 인스턴스는 종료가 되며 상태가 TERMINATING_PROCEED로 바뀝니다.
- 나중에 인스턴스 Autoscaling group에서 제거됐을 때 인스턴스가 종료가 됩니다.
2. 반대로 default result가 ABANDON인 경우
- LifeCycle 훅이 시간 초과가 되거나 실패한 경우 인스턴스는 종료됩니다.
- 하지만 다른 lifecycle hook이 인스턴스를 건드릴 수 없습니다.
- 반면, default result가 CONTINUE일 때는 다른 lifecycle hook이 완료하도록 허용합니다.
* autoscaling:EC2_INSTANCE_LAUNCHING일 때의 의미랑 autoscaling:EC2_INSTANCE_TERMINATING일 때의 의미가 다릅니다. 자세한 것은 아래 링크 QnA를 참고해주세용.
* https://repost.aws/questions/QUbACYy-XPT2-gJXFfgTmmzw/meaning-of-continue-vs-abandon-in-lifecycle-hooks
Lifecycle Hook(EC2_INSTANCE_TERMINATING)을 생성한다
저는 종료될 때에 특정 작업을 하고 싶습니다. 그래서 LifecycleTransition을 EC2_INSTANCE_TERMINATING으로 설정했습니다.
Resources:
lifecyclehook:
Type: AWS::AutoScaling::LifecycleHook
Properties:
AutoScalingGroupName: { "Ref" : "AWSEBAutoScalingGroup" }
LifecycleHookName: { "Ref" : "AWSEBAutoScalingGroup" }
LifecycleTransition: autoscaling:EC2_INSTANCE_TERMINATING
(Tip) .ebextension과 Lifecycle에 대한 Reference
.ebextension 파일을 설정하는 법은 다음 저장소의 샘플 파일을 참고해서 감을 잡으시면 됩니닷.
GitHub - awsdocs/elastic-beanstalk-samples: This repository contains code and configuration samples (e.g. .ebextensions) for AWS
This repository contains code and configuration samples (e.g. .ebextensions) for AWS Elastic Beanstalk. - GitHub - awsdocs/elastic-beanstalk-samples: This repository contains code and configuration...
github.com
Lifecycle에 관해서는 다음의 명세서를 참고해서 만들면 됩니다.
AWS::AutoScaling::AutoScalingGroup LifecycleHookSpecification - AWS CloudFormation
Thanks for letting us know this page needs work. We're sorry we let you down. If you've got a moment, please tell us how we can make the documentation better.
docs.aws.amazon.com
Elastic Beanstalk의 AutoScaling Group의 Lifecycle Hook 생성됐는지 확인해보자
이제 .ebextensions 파일을 생성하고 Elastic Beanstalk을 배포해봅시다.
저는 이미 익스텐션이 두 개가 있어서 아래와 같이 .ebextensions/03-lifecycle-hook.config 파일을 생성했습니다.
Resources:
lifecyclehook:
Type: AWS::AutoScaling::LifecycleHook
Properties:
AutoScalingGroupName: { "Ref" : "AWSEBAutoScalingGroup" }
LifecycleHookName: { "Ref" : "AWSEBAutoScalingGroup" }
LifecycleTransition: autoscaling:EC2_INSTANCE_TERMINATING
위의 파일을 생성하고 배포 후, AutoScaling Group이 완료가 되면 다음과 같이 새로운 훅이 떴음을 확인할 수 있습니다.
이제 AWS 환경 설정은 끝이 났습니다. 이제 서버가 종료될 때 terminating hook이 작동되면서 Lifecycle State를 InService에서 Terminating:Wait으로 바꾸고 3600(Heartbeat Timeout)초만큼 멈춰춥니다.
그리고 Timeout 전에 모든 처리가 완료되면 Continue 명령을 때려주면 인스턴스가 굳이 timeout까지 안 기다리고 종료할 수 있습니다.
Terminating:Wait인 동안 우리가 하고 싶은 것을 다 하고 서버를 Graceful하게 종료해보겠습니다.
2. NestJS 애플리케이션에서 Lifecycle State를 확인한다
이제 인스턴스의 Lifecycle 상태가 Terminating:Wait인지 확인하는 코드를 구현해보겠습니다.
우리는 다음과 같은 AWS CLI 명령어로 인스턴스의 정보를 불러올 수 있습니다.
aws autoscaling describe-auto-scaling-instances --region 리전 --instance-ids 인스턴스ID
autoscaling 그룹에 있는 특정 인스턴스의 정보를 불러오려면 두 가지의 정보가 필요합니다.
- region
- 해당 인스턴스가 있는 지역입니다.
- instance-ids
- 확인하고 싶은 인스턴스의 ID입니다.
- 생략하면 모든 AutoScaling 인스턴스에 대한 정보가 출력됩니다.
제 모든 인스턴스는 서울에 있기 때문에 ap-northeast-2로 하드코딩했습니다.
제 애플리케이션이 돌고 있는 인스턴스의 ID는 어떻게 알까요?
AWS의 Metadata Service를 통해서 알 수 있습니다.
AWS Metadata Service란?
보통 이런 클라우드 서비스는 인스턴스의 정보를 내부에서 알 수 있도록 다음과 같은 메타데이터 서비스를 제공합니다.
AWS는 http://169.254.169.254 주소로 제공해줍니다.
AWS 인스턴스에 ssh해서 "curl http://169.254.169.254/latest/meta-data/"을 실행해보세요.
제공해주는 메타데이터들을 쭉 나열해줍니다.
AWS 메타데이터 서비스는 두 가지 버전이 있습니다. IMDSv1와 IMDSv2입니다.
위와 같이 그냥 별도의 인증 정보가 없이 요청을 했는데 바로 값을 반환을 해준다면 IMDSv1을 쓰고 있는 것입니다.
저는 보안에 취약한 IMDSv1을 비활성화하고 발급받은 토큰과 함께 GET 요청을 보내도록 하는 IMDSv2를 사용했습니다.
# token 발급
TOKEN=`curl -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600"`
# 지원하는 metadata 리스트 불러오기
curl -H "X-aws-ec2-metadata-token: $TOKEN" -v http://169.254.169.254/latest/meta-data/
자세한 것은 아래 문서를 참고해주세요.
- https://docs.aws.amazon.com/ko_kr/AWSEC2/latest/UserGuide/configuring-instance-metadata-service.html
제공해주는 메타데이터를 보면 instance-id가 있습니다.
URL뒤에 instance-id를 붙여서 가져오면 됩니다.
IMDSv1을 쓰면 다음과 같이 가져올 수 있습니다.
IMDSv2를 쓰면 다음과 같이 발급한 토큰으로 가져올 수 있습니다.
이것으로 제 애플리케이션이 돌고 있는 현재 서버의 인스턴스의 ID를 가져올 수 있습니다.
(참고) metadata에서 Lifecycle state도 가져오면 되는 것 아니야?
네. 아닙니다. ㅠ
문서에 보시면 안 된다고 나와있습니다.
실제로 제가 테스트해본 결과 Console상에서는 인스턴스의 상태가 Terminating:Wait으로 변해있는데 메타데이터에서는 InService로 나왔습니다.
![](https://blog.kakaocdn.net/dn/uAhV2/btrQOPgamkk/C6bM49wGnyhdATpyRIQf2k/img.png)
메타데이터에서는 현재 인스턴스Id를 가져오고 외부에서 요청을 보내서 인스턴스의 Lifecycle State을 가져와야 합니다.
AWS Metadata Service를 통해서 instance-id를 가져오도록 애플리케이션을 구현한다
저는 nestjs를 쓰고 있어서 @nestjs/axios로 다음과 같이 구현했습니다.
getMetadataToken(): Promise<string> {
const request = httpService
.put('http://169.254.169.254/latest/api/token', null, {
headers: {
'X-aws-ec2-metadata-token-ttl-seconds': 10,
},
})
.pipe(map((res) => res.data));
return lastValueFrom(request);
}
getInstanceId(token: string): Promise<string> {
const request = httpService
.get('http://169.254.169.254/latest/meta-data/instance-id', {
headers: {
'X-aws-ec2-metadata-token': token,
},
})
.pipe(map((res) => res.data));
return lastValueFrom(request);
}
// 사용 예시
const token = await getMetadataToken()
const instance_id = await getInstanceId(token)
console.log(instance_id)
그냥 자기가 편한 대로 위의 curl을 그대로 코드로 구현하면 됩니다.
실제 코드는 이렇게 짰습니다.
AWS CLI를 통해 인스턴스의 Lifecycle State 확인해보자
이제 region과 instance-id를 가져왔습니다.
이것으로 저희는 애플리케이션이 실행 중인 특정 인스턴스의 정보를 알 수 있습니다.
aws autoscaling describe-auto-scaling-instances --region ap-northeast-2 --instance-ids i-asdasdasd
다음과 같이 AWS CLI로 현재 나의 LifeCycle을 가져올 수 있었습니다. ㅎㅎ InService이네요.
AWS CLI를 AWS SDK를 사용한 코드로 바꿔보자
AWS CLI에서 되는 것은 높은 확률로 비슷하게 AWS SDK에서도 가능합니다.
저는 이것을 그대로 AWS NestJS 애플리케이션상에서 실행되도록 옮겼습니다.
먼저 NodeJS용인 auto-scaling AWS SDK를 설치했습니다.
yarn add @aws-sdk/client-auto-scaling
그리고 AWS CLI 명령어를 SDK를 쓰는 것으로 바꿔서 구현했습니다.
// autoscaling sdk client 생성
const client = new AutoScalingClient({ region: 'ap-northeast-2' });
// instance Id 가져오기
const token = await getMetadataToken();
const instanceId = await getInstanceId(token);
// AWS SDK를 이용해서 DescribeAutoScalingInstancesCommand 실행하기
// (AWS CLI 명령어랑 동일)
const response: DescribeAutoScalingInstancesCommandOutput =
await client.send(
new DescribeAutoScalingInstancesCommand({
InstanceIds: [instanceId],
MaxRecords: 1,
}),
);
// 결과 출력
console.log(response);
결과는 다음과 같습니다.
그대로 옮겼습니다 ㅎㅎ
(Tip) AWS SDK가 안 되는 경우
AWS SDK에 부여된 IAM이 충분한 권한을 가지고 있는지 체크합니다.
![](https://blog.kakaocdn.net/dn/9f39O/btrQN97yjuX/nZZGlhAY3wmArWsHK2ID10/img.png)
권한이 없다면 다음과 같이 뜹니다.
[백엔드앱] AccessDenied: User: arn:aws:iam::*********:user/IAM명칭 is not authorized to perform: autoscaling:DescribeAutoScalingInstances because no identity-based policy allows the autoscaling:DescribeAutoScalingInstances action
이제 주기적으로 서버의 Lifecycle state이 InService가 아닌 상태인지 확인하면 됩니다.
상태를 체크하다가 Terminating:Wait으로 바뀌면 우리가 종료되기 전에 해줘야 하는 작업을 수행해주면 됩니다.
그리고 상태 값을 InService에서 정확하게 어떤 상태로 바꾸는지 SDK를 보고 확인해봤습니다.
"Terminating:Wait"이 맞네용.
AutoScaling Client SDK에 관한 API 문서는 링크 남겨두겠습니다.
3. Lifecycle state을 주기적으로 확인한다
이 부분은 NestJS에 관한 부분입니다.
주기적으로 Lifecycle state가 Terminating:Wait인지 체크해주기 위해 저는 @nestjs/schedule을 사용했습니다.
npm install --save @nestjs/schedule
이걸 쓰면 리눅스 Cron Job 특유의 표기법으로 백그라운드 작업을 돌릴 수 있습니다.
메타메이터 요청은 비용도 무료니 10초 정도로 자주 체크하도록 해보겠습니다. ㅎㅎ
10초마다 Lifecycle state를 확인하게 하려 Cron 데코레이터를 사용하면 됩니다.
// 10초마다 실행
@Cron('*/10 * * * * *')
async checkLifecycleState() {
const res = await this.client.send(
new DescribeAutoScalingInstancesCommand({
InstanceIds: ["내_인스턴스Id"],
MaxRecords: 1,
}),
);
if (res.AutoScalingInstances[0]?.LifecycleState === 'Terminating:Wait') {
// 여기서
// 사전 작업
// 실행
}
}
4. Health Check를 Fail하도록 바꾼다
Load Balancer 설정을 확인한다
Elastic Beanstalk을 만들 때 Load Balancer의 어떤 경로로 어떻게 health check를 할지 설정하셨을 겁니다.
Health Check이 Fail하도록 바꾼다
해당 Path를 Fail하도록 바꿔주고 Load Balancer가 해당 인스턴스로 패킷을 전달하지 않도록 기다립니다.
저는 Status Code 200이 안 가도록 Exception을 던지고
healthCheck(): string {
if (AppService.terminating) {
throw new ForbiddenException('App is shutting down');
}
return 'Welcome to API Server.';
}
저는 Interval 15초로 두 번 Fail하면 unhealty로 전환되게 설정해놓았기 때문에 다음과 32초를 기다리도록 했습니다.
// HEALTH_CHECK_INTERVAL <- 내 로드밸런서 Process Health Check 설정 확인
// 뒤의 + 2초는 여분은 시간입니다
await this.waitUntilHealthCheckFail(HEALTH_CHECK_INTERVAL * 2 + 2000);
로드밸런서가 unhealthy로 분류한 이후로는 새로운 클라이언트가 구버전 서버로 연결되지 않게 됩니다.
5. 모든 사전 작업이 완료되면 Hook을 Continue한다
제가 가만히 내버려 두면 사전작업이 완료 후에도 Hook을 만들 때 설정한 Heartbeat Timeout만큼 기다립니다.
저는 Immutable 배포 방식을 쓰기 때문에 명시적으로 Continue해주지 않으면 unhealthy상태인 구버전 애플리케이션들이 쓸데없이 2배의 인스턴스가 오래 살아 있게 됩니다.
사전 종료 작업이 끝나면 Continue하도록 Hook에게 알려줘 봅시다.
AWS CLI 명령어로 Hook을 Continue 시켜보자
Lifecycle State을 Terminating:Wait에서 Continue시키는 AWS CLI 명령어는 다음과 같습니다.
aws autoscaling complete-lifecycle-action --lifecycle-action-result CONTINUE --region 리전 --instance-id 인스턴스ID --lifecycle-hook-name 훅의_이름 --auto-scaling-group-name 오토스케일링_그룹명`
리전과 인스턴스ID는 가지고 있습니다.
추가로 Hook 이름과 AutoScalingGroup명이 필요합니다.
그런데 .ebextension에서 우리가 hook을 생성할 때 명칭을 AutoScalingGroup명으로 했습니다. 그렇기 때문에 AutoScalingGroup명만 있으면 됩니다.
그리고 AutoScalingGroup명은 Lifecycle state를 가져올 때 포함되어있습니다.
애플리케이션이 실행 중인 특정 인스턴스의 정보를 가져올 사용한 코드를 그대로 활용하면 되겠습니다.
AWS SDK로 Hook을 Continue 시켜보자
위의 명령어를 그대로 AWS SDK로 변환해서 구현해보겠습니다.
CompleteLifecycleActionCommand API를 사용하면 됩니다. 인자로는 다음의 형식으로 설정 값을 넘겨주면 됩니다.
{
LifecycleHookName: '훅 이름',
AutoScalingGroupName: '오토스케일링_그룹명',
LifecycleActionResult: 'CONTINUE', // 경우에 따라 ABANDON도 가능
InstanceId: '인스턴스ID'
}
Typescript 예시 코드
async continueInstanceTerminatingState(instanceId: string, autoScalingGroupName: string) {
const response = await this.client.send(
new CompleteLifecycleActionCommand({
AutoScalingGroupName: autoScalingGroupName,
InstanceId: instanceId,
LifecycleActionResult: 'CONTINUE',
LifecycleHookName: autoScalingGroupName,
}),
);
console.log(response);
}
await continueInstanceTerminatingState('인스턴스Id', '오토스케일링 그룹명')
예시 코드 출력 결과
CONTINUE시키면 바로 Terminating:Wait에서 Terminiating:Proceed 상태로 변경됩니다.
그리고 곧 이어서 실제로 인스턴스가 종료가 됩니다.
끝입니닷.
제가 Lifecycle hook으로 처음에 언급했던 구현 하고 싶은 것(웹소켓이 종료될 때 미리 클라이언트에 알려주기)에 관한 소스코드입니다.
GitHub - SWM-FIRE/modoco-backend: 모도코의 백엔드 API 및 Socket 서버
모도코의 백엔드 API 및 Socket 서버. Contribute to SWM-FIRE/modoco-backend development by creating an account on GitHub.
github.com
댓글
이 글 공유하기
다른 글
-
[AWS] AWS 비용 절감하기 :: 0. Free Tier가 끝나며 흥미로워진 AWS 비용
[AWS] AWS 비용 절감하기 :: 0. Free Tier가 끝나며 흥미로워진 AWS 비용
2023.08.16 -
[AWS] Route53 Domain 다른 AWS 계정으로 이전하기
[AWS] Route53 Domain 다른 AWS 계정으로 이전하기
2022.11.20 -
[AWS] AWS SES API를 사용해서 메일 보내기 (feat. NodeJS)
[AWS] AWS SES API를 사용해서 메일 보내기 (feat. NodeJS)
2022.10.09 -
[AWS] VPC의 NAT 비용을 줄여보자 :: Ubuntu로 NAT 인스턴스 만들기
[AWS] VPC의 NAT 비용을 줄여보자 :: Ubuntu로 NAT 인스턴스 만들기
2022.09.25