본문 바로가기

인프라

NextJS 를 AWS EC2 에 배포하는 경우 비밀번호 찾기 기능이 안 되었던 이유와 해결 방법( with AWS SES + 샌드박스 해지 )

반응형

SMTP 의 기본 포트는 25 이다.

내가 만든 프로젝트에서 비밀번호 찾기 기능을 구현하고, EC2 에 배포했을 때 비밀번호 찾기 기능이 동작하지 않았다. 그 이유로 짐작되었던 것을 오늘 확인하였다. 그건 EC2 에서 외부 네트워크 요청에 대한 인바운드 규칙을 설정해두지 않았기 때문이다.

 

SMTP 의 경우 기본적으로  TCP 25 포트를 사용하기 때문에, EC2의 인바운드 규칙에서 해당 포트로 요청이 들어오는 것을 허용해주어야 한다. 즉, 나는 이것을 설정해두지 않아서 차단이 되었던 것이다(라고 서두에 언급하였지만, 이 방법은 실패하였다. 물론 해당 포트 문제도 있었겠지만 근본적인 부분은 지메일 SMTP 서버에서 리전이 차이가 나는 경우 보안상 이유로 차단을 박는다.). 

 

[해결방법 - 실패 사례] 그러므로 인바운드 규칙에 TCP 25 포트를 열어두자

인바운드 규칙을 수정하기 위해서는 EC2 대시보드로 들어와서 [보안 그룹] 을 선택해야 한다.

 

 

그 후 현재 EC2 와 연결되어 있는 보안 그룹을 선택하여 들어간다.

 

 

그럼 아래와 같이 인바운드 규칙 등의 탭이 보이는데, 여기서 우측의 [인바운드 규칙 편집] 을 클릭하여 들어간다.

 

 

그럼 인바운드 규칙 편집 창이 활성화 되는데 여기서 유형을 [SMTP] 를 선택하고, 허용 아이피 주소를 모든 네트워크 IP 를 허용하는 의미인 0.0.0.0/0 을 지정 후 저장하고 나온다.

 

끝이다. 이제 다시 보내보자

 

결과: 실패

그런데 실패했다. 여전히 같은 문제가 발생하고 있다.  이 문제에 대한 부분은 지메일 자체적으로 리전이 다른 곳에서 요청을 보내는 경우 보안상의 이유로 차단할 수 있다는 것을 노드메일러 공식 사이트에서 언뜻 본 적이 있었는데, 그 이유가 아닐까 싶다.

 

[해결방법 - 성공 사례] 빠르게 다른 방안을 찾아보자 | AWS SES

위 방법이 실패로 돌아가고, 바로 다음 해결 방법을 찾아보았다. AWS 는 정말 다양한 기능을 지원해주고 있는데, 그 중에서도 SMTP 서비스로  SES 를 지원해주고 있었고 EC2 를 사용하는 경우 충분히 많은 이메일 전송을 무료로 사용할 수 있기 때문에 가성비도 좋아 보였다. 

 

또한, 해당 서비스를 이용하면 지메일과 같은 별도의 SMTP 서버에 요청할 필요 없이 SES 자체적으로 SMTP 서비스를 제공해주므로 TLS 가 붙은 보안상 안전한 이메일을 사용자에게 전송해줄 수 있다.

https://aws.amazon.com/ko/ses/?nc2=type_a

 

 

참고로 SES 를 사용하는 여러 가지 방법 중에서 본인이 선택한 방법은 sdk 를 활용한 방법이다.

 

https://docs.aws.amazon.com/ses/latest/dg/send-email-api.html

 

 

또한 SES 서비스 콘솔에서 별도의 자격증명(예를 들어 도메인 자격증명 등)에 대해서는 다루지 않는다는 점을 참고하면 좋을 것 같다. 이후 이를 참고할 수 있도록 영상 자료 링크를 달아 두었다

공식 문서는 뒤죽박죽이라 처음 접하는 입장에서는 혼란을 불러일으킬 수 있으므로 최대한 영상 자료를 활용하자. 국내에서는 없더라도 외국 자료에는 정말 필요한 소스들이 많다. 영어 공부도 덤이다.

 

[기초 셋팅] SES 관리를 위한 IAM 사용자 생성

SES 서비스를 NextJS 서버에서 사용하려면 우선적으로 SES 서비스를 총괄하는 사용자를 생성하는게 좋다. SES 콘솔 자체에서도 만들 수 있는데, 이 경우에는 루트 유저를 대상으로 하므로, 보안을 생각한다면 별도의 사용자를 추가해서 관리하는게 좋다.

 

따라서 [IAM ] - [사용자] - [사용자 생성] 을 클릭하여 들어간다.

 

 

 

그럼 바로 아래 화면이 나오는데 식별하기 쉬운 사용자 이름을 입력하고, [다음] 으로 넘어간다.

 

 

 

그 후 정책을 생성하는 화면으로 넘어가는데, 여기서 [직접 정책 연결] 을 선택하고 하단에 ses 라고 검색하면 [AmazonSESFullAccess] 라는 정책이 보이면 이를 체크하고 다음으로 넘어간다.

 

 

 

그 다음에는 연결할 정책과 태그 지정하는 부분이 나오는데, 별도로 지정할 부분은 없다. 태그를 달아두면 나중에 검색하고 찾아보기 편하다고 하는데, 필요하다면 설정하고 사용자 생성을 클릭하자.

 

 

 

[매우 중요] AccessKey 만들기

그 다음에 바로 해주어야 하는 것은 sdk 를 사용하는 경우 ses 서비스에 접근할 수 있도록 해주는 accesskey 를 발급 받는 것이다. 이 부분을 놓치고 sdk 로 넘어가면 당연히 접근 권한이 없으므로 인증이 실패한다.

 

우선 생성한 사용자 계정을 클릭해서 들어가자

 

 

 

그럼  하단에 보면 [보안 자격 증명] 탭이 보이는데, 그것을 클릭하고 최하단에 [액세스 키 만들기] 를 클릭한다

 

 

 

그러면 보안 사례 같은 안내 화면 같은 것이 뜨는데, 액세스키를 너무 오랫동안 동일한 것을 사용하면 위험하므로 이것 보다는 다른 것을 사용해보는 게 어때? 라고 넌지시 권장사항을 소개하는 화면이다.  나 같은 경우에는 기타를 클릭하고 바로 다음으로 넘어 갔다.

 

 

 

그 다음에는 키 식별을 쉽게하기 위한 태그 지정에 대한 부분인데, 필요 없으면 넘어가면 되고, 나중에 유용할 것 같다 싶으면 하나 정해서 만들어두고 키 생성을 클릭하고 넘어가자

 

 

 

그럼 마지막으로 키를 안전한 곳에 저장하라는 메시지가 나오며, 이를 저장할 수 있도록 .csv 이 있는데 그것을 다운로드 해주고 다음으로 넘어가자(해당 화면은 이 이후로 절대 볼 수 없으므로 키를 잃어버리지 않도록 주의하자)

 

 

SES 서비스 들어가기

그 다음에 해주어야 하는 것은 AWS SES 콘솔로 들어가서 SES 서비스를 하나 생성해야 한다.

 

 

이후 SES 생성 및 추가적인 도메인 자격증명에 대한 부분은  https://youtu.be/uv6O22OLFoU?si=nJ6kGldwudcnYYlf 여기서 3분대 부터 참고하면 된다(Web Wizard 라는 외국 개발 유튜버의 강의 영상이다). 해당 포스트 보다는 영상을 보면서 하나하나 따라가는 것이 이해하기 쉬울 수도 있다.

 

SDK 를 사용해서 메일 전송하기 [ V2 버전 이다.   V3 버전에 대한 진행은  하단 ]

.env 파일에 accessKey 와 scrept accessKey 외 필요 환경변수 등록하기

우선 sdk 를 사용하기 전에 ses 에 접근할 수 있는 자격증명을 위한 액세스 키를 환경변수에 등록하자.

 

.env or .env.production

 

그리고 지역(리전) 에 대한 정보도 환경변수에 담아두자 지역 정보는 AWS 우측 상단에 리전 리스트를 확인하면 바로 확인할 수 있다.

 

 

 

NextJS 로 돌아와서 aws sdk 패키지 및 import 하기

우선 로컬 환경에서 sdk 를 사용하기 위해 다음 패키지를 설치한다.

npm install aws-sdk

 

그 다음 메일 전송을 위한 별도의 파일(ex. mail.ts / js 등)을 생성하고, 상단에 aws-sdk 를 import 해온다.

import AWS from 'aws-sdk';

 

SES 자격증명 파일 구성과 인스턴스 연결

앞서 환경변수로 등록해두었던 액세스 키를 여기서 사용한다.  다음과 같이 앞서 생성해둔 환경변수를 가져와서 할당 해준다.

 

const SES_CONFIG={
  accessKey:process.env.AWS_SES_ACCESS_KEY,
  secretAccessKey:process.env.AWS_SES_ACCESS_SECRET,
  region:process.env.AWS_SES_REGION
  
}

 

그 다음에는 해당 자격증명 구성 옵션을 SES 인스턴스에 전달하여 연결하는 코드를 작성해야 한다.  앞서 import 했던 AWS  클래스를 사용하여  해당 인자로 앞서 생성한 옵션을 넣어주면 AWS SES 에 접근할 수 있는 인스턴스가 생성된다.

const AWS_SES = new AWS.SES(SES_CONFIG)

 

 

메일 보내기 함수 선언

이제 본격적으로 메일 보내기 함수를 구성해야 한다. 지금 부터 작성하는 코드는 모두 AWS 에서 사용하도록 지정해두었기 때문에, 절대 외우거나 할 필요가 전혀 없다. 그 점을 인지하고 따라오길 바란다.

 

const SES_CONFIG = {
  accessKeyId: process.env.AWS_SES_ACCESS_KEY,
  secretAccessKey: process.env.AWS_SES_ACCESS_SECRET,
  region: process.env.AWS_SES_REGION

}

const AWS_SES = new AWS.SES(SES_CONFIG)

async function sendMailWithAwsSes(recipientEmail: string, recipientName: string) {


}

 

우선 메일을 전송하기 위한 함수부터 선언한다. recipientEmail: string, recipientName: string 는 각각 수신자 이메일과 수신자 이름을 의미한다 .즉, 받는 사람의 이메일과 닉네임을 뜻한다.

 

메일 보내기 함수 매개변수 생성

해당 함수를 통해 메일을 보내기 위해서는 우선적으로 필요한 정보들이 담긴 매개변수를 만들어주어야 한다. 각 항목에 대한 내용은 주석으로 달아두었으니 참고해서 작성하자

 

Source 에 보면 환경변수가 등록되어 있는데, 타입스크립트를 사용하는 환경에서는 undefined 가 나올 수 있으므로, 단축평가를 사용하든, 분기처리를 하든 해서 string 타입이 나오도록 해주어야 한다. 안 그러면 AWS 의 sendMail 함수의 매개변수로 전달할 때 오버로드 불일치 에러가 발생할 수 있다.

const AWS_SES = new AWS.SES(SES_CONFIG)

async function sendMailWithAwsSes(recipientEmail: string, recipientName: string, path:string) {
  
  const params = {
    Source:process.env.AWS_SES_SENDER, // (보내는사람 === 본인) 발신자 이메일 주소
    Destination : { // 메일의 목적지는 어디?
      ToAddresses: [ 
        recipientEmail // ㄴ 당연히 수신자 메일로 보내지
      ]
    },
    ReplyToAddresses:[], // 수신자가 답하면 어디로?
    Message: { // 어떤 내용을 담아서 보낼 것인지?
      Body : { // ---> 본문 내용
        // HTML 말고도 Text : {Charset, Data} 도 가능-> 텍스트로 전송
        Html: {
          Charset:'UTF-8',
          Data:` <html>
          <head>
              <style>
                  /* CSS 스타일 설정 */
              </style>
          </head>
          <body>
              <div class="container">
               
              </div>
          </body>
          </html>`
        }
      },
      Subject: { // --> 메일 제목
        Charset:'UTF-8',
        Data:`안녕하세요, ${recipientName}!`
      }
    }
  }
}

 

매개변수를 AWS Sendmail 의 인자로 전달하기

그 다음에는 실제 메일을 전송하는 역할을 하는 AWS 인스턴스의 메소드인 sendMail 의 인자로 앞서 설정한 params 변수를 전달해야 한다. 그 다음에는 .promise() 를 호출하여 비동기적으로 그 응답결과가 res 변수에 담기도록 셋팅한다. 안 그러면 콜백함수를 통해 그 결과를 확인해야 하는데, 코드가 지저분해지고 보기 불편해지므로 이렇게 처리한다.

  try {
    const res = await AWS_SES.sendEmail(params).promise();
    console.log("이메일 보냄", res)
  } catch(error) {
    console.error(error)
  }

 

 

aws sdk v3 으로 이메일 전송하기

앞서 과정은 과거 버전인 v2 을 이용해서 보낸 것이다. 해당 방법은 2025년 까지만 유효하며 그 뒤로는 사용이 불가능하기 때문에, 최신 버전인 v3 으로 마이그레이션 하거나 애초에 최신 버전을 사용해야 한다.

 

v3 는 v2 버전과 다르게 필요한 클라이언트를 ES6 모듈을 사용하여 필요한 모듈만 import 하여 사용할 수 있도록 바뀌었고, 그와 함께 사용자의 편의성을 높이기 위한 여러가지 업데이트가 이루어졌다.

 

SES 에 대한 NodeJS 버전의 최신 패키지 정보는 https://www.npmjs.com/package/@aws-sdk/client-ses 여기서 확인할 수 있다. 해당 문서를 참고하고, 실제 필요한 과정은 공식 문서를 따라 가면 된다.

 

패키지 설치

ses 패키지만 사용할 것이므로 다음과 같이 입력하여 설치한다. 패키지 매니저가 다른 경우에는 앞서 공유한 링크로 들어가서 확인하여 설치하자

npm i @aws-sdk/client-ses

 

SES 클라이언트 등 가져오기

ses 서비스를 이용하기 위해서 아래 두 가지를 import 해와야 한다.

import { SESClient, SendEmailCommand } from '@aws-sdk/client-ses'

 

SES Config 구성 및 SESClient 인스턴스 생성하기

앞서 v2 에서 설정한 것 하고의 차이점은 accessKey 들을 credentials 라는 객체로 다시 감싸주어야 한다.

const SES_CONFIG = {
  credentials: {
    accessKeyId: process.env.AWS_SES_ACCESS_KEY || '',
    secretAccessKey: process.env.AWS_SES_ACCESS_SECRET || '',
  },
  region: process.env.AWS_SES_REGION

 

 

그 다음에는 SESClient의 인자로 SES_CONFIG 를 전달하여 new 키워드로 해당 클래스를 호출하면 SES에 접근가능한 정보를 담고 있는 client 인스턴스를 생성한다.

const client = new SESClient(SES_CONFIG)

 

메일 전송 시 매개변수 역할을 하는 params 객체 생성하기

이 부분은 v2 부분에서 다룬 것과 동일하게 다음 코드를 참고해서 필요한 내용을 가득 채우면 된다. 

  const params = {
    Source: process.env.AWS_SES_SENDER, // (보내는사람 === 본인) 발신자 이메일 주소
    Destination: { // 메일의 목적지는 어디?
      ToAddresses: [
        recipientEmail // ㄴ 당연히 수신자 메일로 보내지
      ]
    },
    ReplyToAddresses: [], // 수신자가 답하면 어디로?
    Message: { // 어떤 내용을 담아서 보낼 것인지?
      Body: { // ---> 본문 내용
        // HTML 말고도 Text : {Charset, Data} 도 가능-> 텍스트로 전송
        Html: {
          Charset: 'UTF-8',
          Data: ` <html>
          <head>
              <style>
                  /* CSS 스타일 설정 */
              </style>
          </head>
          <body>
              <div class="container">
               
              </div>
          </body>
          </html>`
        }
      },
      Subject: { // --> 메일 제목
        Charset: 'UTF-8',
        Data: `안녕하세요, ${recipientName}!`
      }
    }
  }

 

메일 전송을 위한 전송 작성하기

앞서 상단에서 import 해왔던 SendMailCommand 의 인자로 방금 작성한 params 를 넘겨준다.

const sendEmailCommend = new SendEmailCommand(params)

 

그 후 SESClient의 인스턴스인 client 를 통해 접근 가능한 .send 메소드에 접근하여 해당 메소드의 인자로  sendEmailCommand 인스턴스를 넘겨준다.

const response = await client.send(sendEmailCommend)

 

 

여기 까지 진행 후 파일을 실행하거나 아니면 위 로직들을 함수로 모듈화하여 다른 파일에서 import 후 호출하면 response 객체를 콘솔에 출력해보면 아래와 같은 형식으로 200 번대 코드를 반환해주는 것을 볼 수 있다.

 

 

 

그 후 수신자 계정으로 가서 확인 해보면 설정한 params 에 맞게 HTML 파일이 본문에 전달되어 있는 것을 확인하면 성공이다.

 

 

다만 현재 추가적인 문제를 경험할 수 있다.

AWS SES 서비스를 이용할 때, 처음에는 다들 샌드박스 상태에서 진행이 될 것이다. 이는 테스트 환경과 같으며 이를 프로덕션으로 올리지 않으면, 자격증명이 인증된 도메인 계정을 제외하고는 이메일을 받아볼 수가 없다.

 

따라서 SES 의 설정 시작 페이지로 가서 샌드박스 해지 신청을 직접 해주어야 한다. 아래 이미지를 보면 우측 하단에 주황색 타원형이 보일텐데, 이 부분에 신청을 하지 않은 경우라면 프로덕션 전환 관련 버튼이 보일 것이다. 이를 클릭해서 안내대로 작성해서 신청서를 전송하면 된다.

 

 

단 해당 신청서를 작성할 때 주의할 점이 있다. 아마 대충적어서 보내면 즉시 AWS SES 고객지원센터에서 사용하는 봇이 양식에 대한 추가적인 정보를 수집한다고 이메일이나 아니면 Toast 가 사이트 상단에 뜰수도 있다. 이를 클릭해서 들어간 뒤 안내된 메시지대로 상세한 내용을 담아서 보내야 한다.

 

본인의 경우에는 오늘 보냈는데, 언제 회신 될지는 모르겠다. 회신 이후에는 상태가 [고객 작업 완료] 로 바뀌게 되는데, 24시간 이내 조치 내용이 전달된다고 하니 기다려 봐야 한다.

반응형