AWS SES를 발신, 반송 및 수신 이메일에 사용하기 위해 제가 구성한 설정을 공유하고자 합니다. SES 서비스에는 확실히 미묘한 부분이 있으며, 정확히 어떻게 작동하는지 이해하는 데 상당한 시행착오가 필요했습니다. 이는 단계별로 따를 수 있는 가이드라기보다 뇌가 덤프(brain-dump)에 가깝습니다. 필요 없을 수도 있지만, 사용은 본인의 책임 하에 이루어져야 합니다. 그리고 타인이 작성한 코드를 구현할 때는 반드시 항상 내용을 읽고 이해해야 합니다.
배경:
저는 AWS에서 Discourse를 배포하고, 신뢰성과 중복성을 보장하기 위해 가능한 모든 AWS 서비스를 활용하고 있습니다. 개발자로서 저는 명령줄과 코드에 더 익숙하며, IaC 자동화를 사용하길 원했습니다. 제 전체 환경은 Terraform으로 배포되고 있지만, 웹 콘솔을 통해 항목들을 가능한 한 정렬해 보았습니다. IAM 및 정책 문서는 이 글의 범위를 벗어납니다만, 필요한 부분이 있는 곳은 언급해 두었습니다.
단일 애플리케이션을 위해 Postfix 인스턴스를 실행하는 것은 과할 것 같았습니다. POP3 메일박스를 사용하는 것은 너무 90년대적인 방식입니다. 그래서 저는 AWS의 깊은 숲으로 들어갔습니다.
제 탐구에 도움이 된 매우 유용한 게시물들을 몇 가지 찾았습니다.
- AWS SES / AWS Lambda mail receiver endpoint code?
- How to use Amazon SES for sending emails to users?
- Configure VERP to handle bouncing e-mails
mail-receiver 컨테이너도 Discourse가 메시지를 처리하는 방식을 이해하는 데 도움이 되었습니다.
- Configure direct-delivery incoming email for self-hosted sites with Mail-Receiver
- Update mail-receiver to the release version
처음에는 AWS 웹훅 엔드포인트가 수신 메시지를 처리할 것으로 기대했지만, 코드를 살펴본 후에는 그렇게 되지 않음을 깨달았습니다. @dltj 님의 훌륭한 예시를 기반으로 제 람다 수신기 코드를 작성했습니다. S3 대신 메시지 전달에 SNS를 사용하기로 결정했습니다.
사전 요구 사항
- AWS 계정
- DNS 및 이메일 관련 레코드 유형에 대한 실무 지식
- 변경 사항을 수행할 수 있는 도메인(또는 서브도메인)
참고 사항
- 문서화된 모든 항목은 동일한 AWS 리전 내에서 생성되어야 합니다
- **굵은 이탤릭체 텍스트 이처럼 은 구현에 따라 달라지는 값입니다
- 이탤릭체 텍스트 는 변수 이름, 고정 값 또는 UI 요소의 이름입니다
단계
-
이메일 수신을 지원하는 AWS 리전 중 하나에서 Simple Email Service(SES) 도메인 식별자, your.domain, 을 생성합니다.
-
도메인 식별자를 검증합니다.
-
피드백 알림을 위한 Simple Notification Service(SNS) 토픽, feedback-sns-topic, 을 생성합니다.
a.aws_sns_topic_arn_allowlist설정에 feedback-sns-topic 토픽의 ARN을 추가합니다. -
your.domain 도메인 식별자를 구성합니다.
a. 이메일 피드백 전달을 활성화합니다.
b. 반송 및 불만(전달이 아님) 피드백 알림에 SNS feedback-sns-topic 토픽을 사용하도록 구성합니다. -
SNS feedback-sns-topic 토픽에 구독을 생성합니다.
a. 프로토콜은 HTTPS입니다(아직도 HTTP를 사용 중이시죠?)
b. 엔드포인트를 https://your.domain/webhooks/aws 로 설정합니다(VERP 게시물 참조)
c. 원시 메시지 전달 이 비활성화 되어 있는지 확인합니다 -
수신 이메일을 위한 다른 SNS 토픽, incoming-sns-topic, 을 생성합니다.
-
기존 활성 규칙 세트가 없다면 SES 이메일 수신 규칙 세트, inbound-mail-set, 를 생성합니다. 이미 존재한다면, 활성 규칙 세트는 하나만 존재할 수 있으므로 그것을 사용합니다.
-
inbound-mail-set 수신 규칙 세트에 수신 규칙을 생성합니다.
a. 수신자 조건을 your.domain 으로 설정합니다.
b. SNS 토픽 incoming-sns-topic 에 게시하는 작업을 추가하고, 인코딩을 Base64 로 설정합니다. -
Discourse 인스턴스에서 system 사용자를 위한 API 키를 생성하고, email 리소스에 대해 receive email 작업을 허용합니다.
-
Secret Manager에서 다음 키와 각 값이 포함된 시크릿, email-handler-secret, 을 생성합니다:
- api_endpoint - https://your.domain/admin/email/handle_mail
- api_key - 9단계에서 생성한 값
- api_username - 9단계에서 다른 것을 사용하지 않았다면 system
-
requests 및 aws-lambda-powertools 라이브러리를 포함하는 python3.10 런타임용 람다 레이어, lambda-receiver-layer, 를 생성합니다.
-
python3.10 런타임용 람다 함수, email-receiver-lambda, 를 수신기 코드를 사용하여 생성합니다:
# Copyright (c) 2023 Derek J. Lambert
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
import json
import os
from typing import TypedDict
import requests
from aws_lambda_powertools import Logger
from aws_lambda_powertools.utilities import parameters
from aws_lambda_powertools.utilities.data_classes import event_source
from aws_lambda_powertools.utilities.data_classes.sns_event import SNSEvent, SNSEventRecord
from aws_lambda_powertools.utilities.typing import LambdaContext
class Secret(TypedDict):
api_endpoint: str
api_username: str
api_key: str
service = os.getenv('AWS_LAMBDA_FUNCTION_NAME')
logger = Logger(log_uncaught_exceptions=True, service=service)
try:
SECRET_NAME = os.environ['SECRET_NAME']
except KeyError as e:
raise RuntimeError(f'Missing {e} environment variable')
AWS_EXTENSION_PORT = os.getenv('PARAMETERS_SECRETS_EXTENSION_HTTP_PORT', 2773)
EXTENSION_ENDPOINT = f'http://localhost:{AWS_EXTENSION_PORT}/secretsmanager/get?secretId={SECRET_NAME}'
def get_secret() -> Secret:
return parameters.get_secret(SECRET_NAME, transform='json')
def handle_record(record: SNSEventRecord):
sns = record.sns
sns_message = json.loads(sns.message)
try:
message_type = sns_message['notificationType']
message_mail = sns_message['mail']
message_content = sns_message['content']
message_receipt = sns_message['receipt']
except KeyError as exc:
raise RuntimeError(f'Key {exc} missing from message')
try:
receipt_action = message_receipt['action']
except KeyError as exc:
raise RuntimeError(f'Key {exc} missing from receipt')
try:
action_encoding = receipt_action['encoding']
except KeyError as exc:
raise RuntimeError(f'Key {exc} missing from action')
try:
mail_source = message_mail['source']
mail_destination = ','.join(message_mail['destination'])
except KeyError as exc:
raise RuntimeError(f'Key {exc} missing from mail')
logger.info(f'Processing SNS {message_type} {sns.get_type} record with MessageId {sns.message_id} from {mail_source} to {mail_destination}')
# 'email' is deprecated, but just in case something is configured incorrectly
body_key = 'email_encoded' if action_encoding == 'BASE64' else 'email'
request_body = {
body_key: message_content
}
secret = get_secret()
headers = {
'Api-Username': secret['api_username'],
'Api-Key': secret['api_key'],
}
response = requests.post(url=secret['api_endpoint'], headers=headers, json=request_body)
logger.info(response.text)
response.raise_for_status()
@event_source(data_class=SNSEvent)
@logger.inject_lambda_context
def lambda_handler(event: SNSEvent, context: LambdaContext):
for record in event.records:
handle_record(record)
-
email-receiver-lambda 람다 함수를 구성합니다:
a. 레이어 lambda-receiver-layer 를 추가합니다
b. AWS Parameter Store 에 대한 리전별 레이어를 추가합니다
c. 값이 email-handler-secret 인 환경 변수 SECRET_NAME 을 추가합니다
d. 추가적인 세부 정보를 기록하고 싶다면, 값이 true 인 환경 변수 POWERTOOLS_LOGGER_LOG_EVENT 를 추가합니다 -
람다 함수 email-receiver-lambda 에 시크릿 email-handler-secret 에 대한 IAM 권한 secretsmanager:GetSecretValue 를 부여합니다.
-
SNS 토픽 incoming-sns-topic 에 구독을 생성합니다.
a. 프로토콜은 AWS Lambda입니다
b. 엔드포인트를 email-receiver-lambda 의 ARN으로 설정합니다 -
incoming-sns-topic 토픽의 SNS 구독이 email-receiver-lambda 를 호출하려면 IAM 권한이 필요하지만, 콘솔을 통해 구성할 경우 자동으로 처리된다고 믿습니다.
디버깅 목적이나 일반적인 자기 괴롭힘을 위해, 알림을 모니터링할 수 있도록 두 SNS 토픽 중 하나에 이메일 구독을 추가할 수 있습니다.
몇 번에 걸쳐 이 내용을 작성했지만, 모든 것이 포함된 것 같습니다. 시간이 허락하는 한 일반적인 질문에 답변해 드릴 수 있습니다.
