[Nestjs/AWS SES ] AWS SES 로 메일 전송하기
AWS IAM 사용자를 활성화 시켜기
AWS IAM 에 들어가서 활성화 시킨후 AWS AccessKey를 받아야 한다.
[ AWS IAM이 무엇인가? ]
AWS IAM(AWS Identify and Access Management)
- 리소스에 대한 액세스를 안전하게 제어할 수 있는 웹서비스
- 손쉽게 기존 디렉토리를 연결하여 AWS계정과 클라우드 애플리케이션에 대한 사용자 액세스를 관리하는 서비스다.
- 비유를 하자면, 데이터센터에 접근할 수 있는 권한을 갖는 "출입카드" 와 유사하다고 보면 된다.
- 누가/ 무엇을 / 어떻게 할 것인지에 대해 인증과 인가를 제어하는 서비스이다.
- [사진2] 에서 보면 유저가 aws 서비스에 요청을 보내면서 인증을 위한 유저의 자격증명도 같이 보낸다.
1) IAM ID 센터 활성화하기
- 콘솔로그인 후, AWS IAM 에 들어가면, '활성화' 버튼을 클릭한다.
액세스토큰 얻기
공식다큐먼트: https://docs.aws.amazon.com/ko_kr/powershell/latest/userguide/pstools-appendix-sign-up.html
1) IAM 콘솔 에 들어간다 > 좌측 '사용자' 탭 선택 > '사용자 추가' 버튼 클릭
권한을 추가해야만, 메일 전송 API를 호출할 때, AccessDenied 문제가 생기지 않는다.
MessageRejected: Email address is not verified. The following identities failed the check in region
2) 등록한 사용자 이름 링크를 클릭
3) '보안자격증명 클릭' > '액세스 키 만들기' 버튼 클릭
각자 상황에 맞는 목적으로 액세스 키 사례를 선택한다. 필자의 경우에는 '로컬코드' 로 했다.
프로덕션일경우에는 AWS 컴퓨팅 서비스에서 실행되는 애플리케이션을 하고
로컬호스트상에서 개발을 진행할 경우에는 로컬코드로 하는게 낫다고 판단했다.
맨아래 '위의 권장사항을 이해했으며 액세스 키 생성을 계속하려고 합니다.' 체크를 해야한다.
필자의 경우에는 '설명 태그값'을 입력하지 않고 바로 액세스키를 만들었다.
이 액세스키와 시크릿키를 복붙하거나 csv파일을 다운로드해서 저장을 하자.
액세스키와 시크릿키를 발급하고 정상로그인이 됐음에도 gmail계정한테 메일을 전송 API를 요청했지만 아래와 같은 에러가 나온다.
MessageRejected: Email address is not verified. The following identities failed the check in region AP-NORTHEAST-2: dmsrkd1216@gmail.com
[문제해결방법 링크]
- 올바른 이메일인가? => 🙆♀️
- AWS SES > 확인된 자격증명 > 자격증명 생성하기
- 올바른 aws ses 엔드포인트인가? => 통신은 하고있다보니.. 엔드포인트는 옳은 것같다.
- AWS SES 샌드박스 모드인가? => 솔직히 샌드박스가 무엇이지 모르겠는데 스택오버플로우에도 같은 고민을 봤다.
- 계정이 aws ses 샌드박스에 있는경우에는 발신자 자격증명을 확인하고, 수신자 이메일주소를 확인.
- 발신자가 구글이나 아웃룩같은 다른 이메일이라면인 경우에도 해당한다.
- 샌드박스가 되어있다면, 실제이메일로 전송하려고해도 전송이 안됨을 유의해야한다.
AWS SES 샌드박스 나가기
1) https://console.aws.amazon.com/ses/ 으로 이동 > 지역은 현위치에 가까운 곳으로 설정 > "계정 대시보드" 선택
2) 프로덕션 액세스 권한 요청 버튼 클릭
3) 세부 정보요청
- 마케팅 이메일: 마케팅, 홍보 목적으로 일대다 방식으로 여러 고객에게 메일을 대량으로 보낸다.
- 트랜잭션 이메일: 웹사이트구매, 암호 재설정 요청 과 같이 사용자 작업에 의해 트리거되어 각 수신자에게 일대일로 보낸다.
- 필자의 경우에는 암호재설정, 이벤트발생으로 이메일을 보내게되므로 '트랜잭션 이메일'에 속한다.
- 신청을 하면 24시간 이후에 메일이 오며, 샌드박스를 해제했다. 이후에 메일전송을 하니 정상 동작이 됐다.
[삽질 | 실패한글 | 그냥 넘어가시면 됩니다 :) ]
실제로 nestjs에 했더니 이 과정들에서 얻은 accssToken과 시크릿이 일치하지 않은 에러가 나와서
이글은 지우기에 아깝기에 [삽질] 이라고 표기했다. 이 접은글은 그냥 넘어가면 된다.
(발생에러 메시지)
SignatureDoesNotMatch: The request signature we calculated does not match the signature you provided. Check your AWS Secret Access Key and signing method. Consult the service documentation for details
알고보니 액세스키와 시크릿키를 구하는 부분에서 엉뚱한 부분을 깊이 삽질한거다... 😢
삽질1 ) 자격증명소스 선택하기
삽질2) 보안자격 증명하기
- MFA 할당
MFA(Multi-Factor Authentication) 는 사용자 이름과 암호외에 보안을 한층 더 강화하는 AWS에서 제공하는 웹서비스이다.
AWS 계정과 해당 계정에 속하는 개별 IAM사용자에 대해 MFA를 활성화 할 수 있다.
루트계정에는 MFA를 구성하여 AWS리소스를 보호하는게 좋다.
그러면 MFA를 할당하기 위해서는 QR코드를 조회하여 2개의 MFA 코드를 입력해야한다.
모바일의 Google Authenticator 앱이나 스마트폰 Google OTP 등으로 MFA 코드를 입력한다.
각 MFA 1,2 코드는 6자리이며, 연속된 6개의 코드를 입력하면 된다.
MFA 할당이 완료했고, 사용자 보안자격 증명이 완료됐음을 나타낸다.
삽질3) 사용자 추가하기
- 좌측 메뉴의 사용자 클릭 > 사용자 추가 버튼 클릭
- 사용자 추가하기
1단계 사용자 세부정보 지정 입력
2단계 그룹에 사용자 추가는 선택사항이라서 스킵했다.
3단계 사용자 검토 및 추가 - 사용자 추가 버튼 클릭
사용자 추가가 완료되면 AWS 액세스 포털 URL 과 사용자이름, 일회용 암호를 발급한다. 이 정보들을 복사하여 저장한다.
이후 등록한 이메일을 전송하여, 이메일 인증을 확인한다.
확인후에 리캡챠로 로봇이 아님을 인증한다. 인증이 완료되면 사용자 등록하면 로그인 페이지가 뜨는데
발급받은 '사용자이름' 과 '일회용 암호'를 입력한다. 이후 새로운 암호를 입력하여 비밀번호를 변경한다.
삽질4) AWS SES 사용하기
1) 자격증명 생성 하기
테스트 이메일을 전송하여 입력한 이메일 주소로부터 메일이 전송되는지 확인한다.
메일을 열람하고 확인하고 SES 콘솔을 새로고침하면 인증이 된다.
보안자격증명에서 액세스키를 생성할 수 있다.
2) SMTP 보안인증 생성하기
- SMTP 설정 > 'SMTP 보안인증 생성' 버튼을 클릭한다.
생성버튼을 누르면 IAM User Name 값이 입력되어있다. 생성버튼을 누른다.
- "사용자 SMTP 보안 자격 증명 보기" 에서 SMTP사용자 이름 과 SMTP 비밀번호 를 받는다.
- SMTP 사용자이름은 AWS SES의 access-id 이고
- SMTP 비밀번호는 AWS SES의 secret-key 이다.
이 IAM User Name, SMTP 사용자이름, SMTP 비밀번호 값을 복사하여, 까먹지 않도록 저장을 한다.
물론 '자격증명 다운로드'를 하면 csv파일 형태로 되어있다.
AWS SES 로 메일 보내기
- framework: NestJS
aws-sdk/client-ses 를 설치하기
$ npm install @aws-sdk/client-ses
Nestjs 코드 작성하기
참고블로그의 코드를 참고해서 구현했다. 2가지 방법을 소개한다.
방법 1. aws-ses 패키지의 sendMailCommand() 함수로 전송하기
// email.service.ts
import { MailerService } from '@nestjs-modules/mailer';
import { Injectable, UseFilters } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { HttpExceptionFilter } from '../error/http-exception.filter';
import { EventEmitter2, OnEvent } from '@nestjs/event-emitter';
import { SESClient, SendEmailCommand } from '@aws-sdk/client-ses';
import { SendEmailByNodemailerRequestDto } from './dto/send-email-by-nodemailer-request.dto';
import { SendEmailBySesRequestDto } from './dto/send-email-by-ses-request.dto';
@UseFilters(new HttpExceptionFilter())
@Injectable()
export class EmailService {
private sesClient: SESClient;
constructor(
private readonly configService: ConfigService,
private readonly mailerService: MailerService,
private readonly eventEmitter: EventEmitter2,
) {
this.sesClient = new SESClient({
region: 'ap-northeast-2',
credentials: {
accessKeyId: this.configService.get('AWS_SES_ACCESS_ID'),
secretAccessKey: this.configService.get('AWS_SES_ACCESS_SECRET'),
},
});
}
// AWS SES로 메일 전송하기
async sendMailBySES(requestDto: SendEmailBySesRequestDto) {
try {
const command = new SendEmailCommand({
// 받는사람
Destination: {
CcAddresses: [],
ToAddresses: [requestDto.email],
},
// 메일 메시지
Message: {
// 메일 제목
Subject: {
Charset: 'UTF-8',
Data: requestDto.subject,
},
// 메일 본문
Body: {
Text: {
Charset: 'UTF-8',
Data: requestDto.mailBody,
},
},
},
Source: this.configService.get('AWS_SES_SENDER'),
ReplyToAddresses: [],
});
return await this.sesClient.send(command);
} catch (error) {
throw error;
}
}
...
}
방법 2. Nodemailer 로 전송하기
// email.service.ts
import { MailerService } from '@nestjs-modules/mailer';
import { Injectable, UseFilters } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { EventEmitter2, OnEvent } from '@nestjs/event-emitter';
import { SES, SendRawEmailCommand } from '@aws-sdk/client-ses';
import { HttpExceptionFilter } from '../error/http-exception.filter';
import * as nodemailer from 'nodemailer';
import { SendEmailByNodemailerRequestDto } from './dto/send-email-by-nodemailer-request.dto';
import { SendEmailBySesRequestDto } from './dto/send-email-by-ses-request.dto';
@UseFilters(new HttpExceptionFilter())
@Injectable()
export class EmailService {
private readonly sesNodemailer;
constructor(
private readonly configService: ConfigService,
private readonly mailerService: MailerService,
private readonly eventEmitter: EventEmitter2,
) {
const ses = new SES({
apiVersion: this.configService.get('AWS_SES_API_VERSION'), // 2020-12-01
region: this.configService.get('AWS_SES_REGION'), // ap-northeast-2
credentials: {
accessKeyId: this.configService.get('AWS_SES_ACCESS_ID'),
secretAccessKey: this.configService.get('AWS_SES_ACCESS_SECRET'),
},
});
this.sesNodemailer = nodemailer.createTransport({
SES: { ses, aws: { SendRawEmailCommand } },
});
}
// AWS SES로 메일 전송하기
async sendMailBySES(requestDto: SendEmailBySesRequestDto) {
try {
return await this.sesNodemailer.sendMail({
from: this.configService.get('AWS_SES_SENDER'),
to: requestDto.email,
subject: requestDto.subject,
html: requestDto.mailBody,
});
} catch (error) {
throw error;
}
}
...
}
마치며
AWS는 SaaS(Software as a Service) 중 하나임을 직접 몸으로 겪었다.
물론 스토리지나 클라우드를 활용한 배포나, 원격으로 데이터베이스를 관리하거나, ElasticBeanstalk와 같이 로그를 관리하여
애플리케이션을 상태를 분석하거나 관리하기도 하지만, no-reply 이름의 메일을 전송하여 메일관련 알림 서비스를 할 때도 SES를 활용한다는 것을 새로 알게됐다.
SES를 활용하려면 보안된 자격증명이 필요하며, IAM으로 계정 등록을 해야된다는 사전조건도 경험해봤다. 자세히 깊숙히는 파고들지 못했지만 어떻게 no-reply 이름으로 메일이 전송되는지에 대한 호기심이 있었는데 aws, gcp는 메일 서비스를 제공해줌으로써 직접 서비스를 만들지 않고도 직접 구축할 수 있음을 알았다.
클라우드가 나에게는 너무 낯설어서 조금더 클라우드에 대해서 학습해서 보충해야될거 같단 생각도 든다.
[참고]
[ aws iam ]
[ aws ses api를 사용하여 메일보내기 ]