ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 이메일 서버 만들기 ( feat. Aws SES )
    Devops 2021. 7. 26. 12:34
    반응형

    간단하게 AWS 를 이용하에 메일 서비스 환경을 만드는방법을 기록 하고자 한다. 일단 AWS 에서 ses 라고 겁색하면 안나올것이다. 아래와 같이 풀네임을 찾아봐야 한다. (https://aws.amazon.com/ko/ses/)

    일단 aws 계정으로 로그인후 aws ses 시작하기를 누르면 콘솔 화면으로 접급이 가능하다. 여기서 메뉴를 확인할수 있는데 처음에 세팅해야하는곳은 Domains 라는 메뉴이다. 해당 메뉴로 가서  일단 도메인을 인증 받아야 사용이 가능하다 현재 도메인이 사용중인게 있다면 해당부분에 tistory.com 처럼 도메인을 입력해주고 검증 버튼을 눌러주면 유효한 도메인일 경우 별 문제 없이 진행된다. 

     

    그다음은 메일 인증이 필요하다 이 메일은 실제로 사용자에게 메일을 보낼때 어떤 메일로 선택해서 보낼수 있을지 미리 세팅 해두는곳이다. 쇼핑몰등을 구축할때 단체 메일이나 광고성 메일로 대표메일을 설정하고 사용하면 좋다. 

     

     

    위에 인증할 이메일을 넣고 검증버튼을 누르면 아래와 같이 확인 메일이 오게 되며 저기서 링크를 눌러 확인하면 인증이 완료 된다. 그다음부터는 바로 사용은 가능하다 샌드박스 형태로 말이다! 

     

    바로 샌드박스 해제로 넘어 가보겠다. 샌드박스 형태는 콘솔로만 테스트가 가능하며 정해진 템플릿으로만 확인이 가능하다. 그렇기 때문에 html 을 이용한 이메일폼을 사용하거나 좀더 대량의 메일을 보내기 ( 테스트는 갯수가 한정적이다 ) 위해선 샌드박스 해제 요청을 해야한다. 아래 캡쳐 해놓은곳에서 Sendig Satatics 메뉴로 가서 진행 하면된다. 

    메뉴에 들어가보면 이런 하단 부분이 보이게 될거고 edit your account details 라는 부분이 있다. 이부분에서 샌드박스 해제 요청을 하면된다.

    아래 내용을 꼭 입력 해야하는데 보통 다른 블로그들에서는 대충 써도 된다고 하는데 난 혹시 몰라서 요구하는 내용이 뭔지 aws 내부 문서를 좀 찾아 보았고 대충 질문에 대해서 맞게 답변을 적어서 입력 했고 통과 되었다. use case 부분에 대한 내용은 여기 따로 기록 해놓는다. 

    -We will create a mailing list.
    -Bounce emails and complaints will be handled by ourselves.
    - Recipients will choose to opt-out themselves and notify us.
    -We have not yet estimated the sending rate and sending quota.

     

    일반적으로 심사에 몇일이나 반나절이나 그정도 걸린다는 말이 많았는데 나같은 경우는 오전에 신청해서 오전중에 처리가 완료 되었다. 

    완료가 되면 동일 화면에서 아래와 같이 enable 상태로 변경된다. 

     

    기본 메일 전송량은 임의로 지정이 된다. 추후 사용량와 reject 비율에 따라 자동 조종 된다고 한다. 그렇기 때문에 확인 필요하며 간단하게 사용 통계 페이지도 같은 화면에서 제공하니 참고하면 좋을것 같다. 

    aws 환경이 구성되 되면 그다음에는 메일을 자동으로 보낼 서비스 개발이 필요하다. 나같은 경우는 카프카를 이용하여 고객들에게 용도별로 메일 보낼수있도록 몇가지 컨슈머들을 작성하여 구성하였다. 

    구성시 주의할점이 aws 에서 제공하는 기본 가이드를 사용하면 sdk v1 을 사용한 동기식 방식을 사용하게 된다. 그럴경우 운영시 대량으로 메일 보낼케이스가 생길시 엄청나게 많은 시간을 사용해야 할수 있기 때문에 나같은경우는 시작부터 비동기식 방식으로 보내는 방법을 찾았고 생각보다 찾아내는데 난이도(?) 가 있었다. 가이드 된게 별로 없다. 

    // aws ses
        implementation( "com.amazonaws:aws-java-sdk-ses:1.11.227")
        implementation( "software.amazon.awssdk:ses:2.4.2")
        implementation("org.springframework.boot:spring-boot-starter-thymeleaf")
        implementation("software.amazon.awssdk:s3:2.4.2")
        implementation("software.amazon.awssdk:ec2:2.4.2")
        implementation("net.minidev:json-smart")

    위에 설정 설정 사항들을 하나하나 보면 일단 aws 에서 제공하는 java 버전의 sdk 를 먼저 설정해야하는데 혹시 몰라서 버전 1,2 를 두개다 포함했다. 첫줄이 v1 , 두번째가 v2 이다. 

    그리고 메일전송시 html 이메일폼을 사용할수 있다. 그렇기 때문에 thymeleaf 엔진을 이용해서 렌더링할수 있도록 포함 시켰다. 그외 아래 있는것들은 혹시나 aws 와 다른 s3,ec2 연동이 필요할지 몰라서 포함 시켜 놓았다. 

    라이브러리를 세팅했으니 이제 환경설정을 해보기로 한다. 

    import com.amazonaws.auth.AWSStaticCredentialsProvider
    import com.amazonaws.auth.BasicAWSCredentials
    
    import com.amazonaws.regions.Regions
    import com.amazonaws.services.simpleemail.AmazonSimpleEmailServiceAsync
    import com.amazonaws.services.simpleemail.AmazonSimpleEmailServiceAsyncClient
    import org.springframework.beans.factory.annotation.Value
    import org.springframework.context.annotation.Bean
    import org.springframework.context.annotation.Configuration
    import software.amazon.awssdk.auth.credentials.AwsBasicCredentials
    import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider
    import software.amazon.awssdk.regions.Region
    import software.amazon.awssdk.services.ses.SesAsyncClient
    
    
    @Configuration
    class AwsSesConfig {
        @Value("\${ses.access-key}")
        private val AWS_ACCESS_KEY_ID: String? = "xxxxxxxxxxx"
    
        @Value("\${ses.secret-key}")
        private val AWS_SECRET_KEY: String? = "xxxxxxxxxxxxxxxxxx"
    
        @Bean
        fun amazonSimpleEmailService(): AmazonSimpleEmailServiceAsync? {
            val basicAWSCredentials = BasicAWSCredentials(AWS_ACCESS_KEY_ID, AWS_SECRET_KEY)
            return AmazonSimpleEmailServiceAsyncClient.asyncBuilder()
                .withCredentials(AWSStaticCredentialsProvider(basicAWSCredentials))
                .withRegion(Regions.AP_NORTHEAST_2)
                .build()
        }
        @Bean
        fun sesAsyncClient() : SesAsyncClient {
            val staticCredentials: StaticCredentialsProvider
                = StaticCredentialsProvider.create(AwsBasicCredentials.create(AWS_ACCESS_KEY_ID, AWS_SECRET_KEY))
            return SesAsyncClient.builder().credentialsProvider(staticCredentials).region(Region.AP_NORTHEAST_2)
                .build()
    
        }
    
    }

    aws 인증을 위한 키값들을 세팅 해서 각각 이메일 서비스 클라이언트들을 빌드하여 제공하는 설정한다. 이부분에서 굉장히 애를 먹었다. 찾기 힘들었다. ㅠㅠ 

    import com.amazonaws.services.simpleemail.AmazonSimpleEmailServiceAsync
    import com.amazonaws.services.simpleemail.model.*
    import com.sellermill.common.config.AwsSesConfig
    import com.sellermill.common.domain.response.CommonResponse
    import com.sellermill.common.utils.AwsSesUtils
    import com.sellermill.common.utils.ThymeleafParser
    import org.apache.logging.log4j.LogManager
    import org.springframework.beans.factory.annotation.Autowired
    import org.springframework.http.HttpStatus
    import org.springframework.stereotype.Service
    import reactor.netty.http.client.HttpClient
    
    
    @Service
    class EmailService (
    
    ){
        @Autowired
        private val awsSesUtils = AwsSesUtils( AwsSesConfig().sesAsyncClient() )
        @Autowired
        private val amazonSimpleEmailServiceAsync: AmazonSimpleEmailServiceAsync? = null
        private val log = LogManager.getLogger()
    
        /**
         * AWS SDK v1 방식
         * @return Unit
         */
        fun sendMailLegacy() : Unit {
            var FROM = "sender@example.com";
            var TO = "recipient@example.com";
            var SUBJECT = "Amazon SES test (AWS SDK for Java)";
            var HTMLBODY = """<h1>Amazon SES test (AWS SDK for Java)</h1>
            <p>This email was sent with <a href='https://aws.amazon.com/ses/'>
            Amazon SES</a> using the <a href='https://aws.amazon.com/sdk-for-java/'>
            AWS SDK for Java</a>
            """
            var request = SendEmailRequest()
                .withDestination(
                    Destination().withToAddresses(TO) // 받는 사람
                )
                .withMessage(
                    Message().withBody(
                        Body().withHtml(Content().withCharset("UTF-8").withData(HTMLBODY))
                    ).withSubject(
                        Content().withCharset("UTF-8").withData(SUBJECT)
                    )
                ).withSource(FROM)
    
            amazonSimpleEmailServiceAsync?.sendEmailAsync(request)
        }
    
        /**
         * AWS SDK V2 방식
         * @param msg Map<String, Any>
         * @return CommonResponse<String>
         */
        fun sendEmail( msg : Map<String , Any>  ) : CommonResponse<String> {
            val client = HttpClient.create()
    
            val template  = msg["template"].toString()
            val data  = msg["data"] as Map<String , Any >?
            val subject = msg["subject"].toString()
            val to = msg["to"].toString()        
            val parser = ThymeleafParser() // html 폼을 사용하기 위한 파서 선언 
            val html = parser.parseHtmlFileToString( template , data ) // html 안에 데이터를 파싱 함 
            awsSesUtils.singleEmailRequest( to, subject,  html) // 메일 전송
            return CommonResponse("OK")
        }
    }

    샘플로 간단하게 넣어 보았다. 간략하게 설명 하면 v1, v2 가 서로 사용하는 방법이 좀 다르며 현재 v2 방식으로 사용중이다. 일단 데이터를 받아서 html 에 선언된 데이터 키값과 매핑할수 있게 세팅을 해주고 보낼대상, 제목, html 순으로 데이터를 세팅해서 전송한다. 

    일단 파싱하는 기능 부터 살펴 보면 

    import org.thymeleaf.context.Context
    import org.thymeleaf.spring5.SpringTemplateEngine
    import org.thymeleaf.templatemode.TemplateMode
    import org.thymeleaf.templateresolver.ClassLoaderTemplateResolver
    
    class ThymeleafParser {
    
        fun parseHtmlFileToString(fileName: String, variableMap: Map<String, Any?>): String {
            // 타임리프 resolver 설정을 잡아준다.
            val templateResolver = ClassLoaderTemplateResolver()
            templateResolver.prefix = "templates/" // templates 경로 아래에 있는 파일을 읽는다
            templateResolver.suffix = ".html" // .html로 끝나는 파일을 읽는다
    
            templateResolver.templateMode = TemplateMode.HTML // 템플릿은 html 형식이다
    
            // 스프링 template 엔진을 thymeleafResolver를 사용하도록 설정
            val templateEngine = SpringTemplateEngine()
            templateEngine.setTemplateResolver(templateResolver)
    
            // 템플릿 엔진에서 사용될 변수를 넣어준다.
            val context = Context()
            context.setVariables(variableMap)
    
            // 지정한 html 파일과 context를 읽어 String으로 반환한다.
    
            return templateEngine.process(fileName, context)
        }
    }

    awsSesUtils 내용을 확인 하면 아래와 같다. 

    import org.springframework.beans.factory.annotation.Autowired
    import org.springframework.stereotype.Component
    import org.thymeleaf.spring5.SpringTemplateEngine
    import software.amazon.awssdk.services.ses.SesAsyncClient
    import software.amazon.awssdk.services.ses.model.Body
    import software.amazon.awssdk.services.ses.model.Content
    import software.amazon.awssdk.services.ses.model.Destination
    import software.amazon.awssdk.services.ses.model.Message
    import software.amazon.awssdk.services.ses.model.SendEmailRequest
    import org.thymeleaf.context.Context
    
    
    @Component
    class AwsSesUtils (
        val sesClient : SesAsyncClient
    ){
        
        fun singleEmailRequest(to: String?, subject: String, html: String) {        
            val sendEmailRequestBuilder = SendEmailRequest.builder()
            sendEmailRequestBuilder.destination(Destination.builder().toAddresses(to).build())
            sendEmailRequestBuilder.message(newMessage(subject, html)).source("no-reply@sellermill.com").build()
            sesClient.sendEmail(sendEmailRequestBuilder.build())
        }
    
        fun newMessage(
            subject: String,
            html: String,
        ): Message? {
            val content =
                Content.builder().data(subject).build()
            return Message.builder().subject(content).body(Body.builder().html { builder: Content.Builder ->
                builder.data(html)
            }.build()).build()
        }
    }

    그리고 혹시 몰라 이메일 폼 사용시 작성 문법도 같이 기록해둔다  th: 로 시작하는 키워드들은 엔진 선언후 사용시 자동 매핑되어 보여진다. 

    each 를 이용하여 리스트 형태도 처리가 가능하다 ( 퍼블리셔 분들의 도움을 받아 사용 중 ) 

    <!DOCTYPE html>
    <html lagn="ko" xmlns:th="http://www.thymeleaf.org" xmlns="http://www.w3.org/1999/xhtml">
    <head>
      <title>상품 주문현황</title>
      <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
      <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
    </head>
    <body>
      <!--
        스토어 로고: th:src="@{ ${logo} }"
        주문자명: th:text="${payment.orderer}"
        주문번호: th:text="${payment.orderCode}"
        주문일시: th:text=${payment.orderDateTime}
    
        주문상품정보(each): th:each="product : ${productList}"
          - 상품이름: th:text="${product.name}"
          - 상품옵션(each): th:each="options: ${optionList}"
            -th:text="options.key" th:text="options.value"
          - 주문상태: th:text="${product.stateName}"
          - 구매수량: th:text="${product.quantity}"
          - 판매가: th:text="${product.price}"
          - 상품구매금액: th:text="${product.discount}"
    
        총 상품금액: th:text="${payment.totalPrice}"
        배송비: th:text="${payment.totalShippingPrice}"
        할인금액: th:text="${payment.totalDiscount}"
        주문금액: th:text="${payment.totalAmount}"
    
        결제금액: th:text="${payment.paymentAmount}"
        결제수단: th:text="${payment.paymentMethod}"
        은행명: th:text="${payment.bankName}"
        입금기한일시: th:text="${payment.depositDeadline}"
        계좌번호: th:text="${payment.accountNumber}"
      -->
     
    </html>

     

    이메일 서비스를 만들때 보통 설치형을 많이 고민하게되고 기업메일 경우 서비스를 돈주고 사용하던가 sendmail 같은 솔루션들을 공부해서 구축하게 된다. 하지만 생각보다 노력대비 서비스의 수준이랄까 ? 노력에 비해 참 별거 없는 서비스 이기도 하다. 

    이번에 aws 를 구성해서 만들었고 실제로 만족하면서 쓰고 있다. 심지어 굉장히 가격이 저렴하고 적은 트래픽이라면 거의 무료로 사용이 가능하다. 아주 메리트있는 서비스중 하나인것 같다. 

    반응형

    'Devops' 카테고리의 다른 글

    Git 문제 해결 - (1)  (0) 2022.09.05
    시스템 성능 테스트 하기! ( feat. Locust )  (2) 2021.08.05
Designed by Tistory.