1. 記事一覧 >
  2. ブログ記事
category logo

LocalStack Kotless(Kotlin) SendGridのオフライン動作環境構築

(更新) (公開)

はじめに

LocalStack、Kotlin のサーバーレスフレームワーク Kotless、メール配信サービスの SendGrid のモック環境を構築しました。
メール配信サービスの SendGrid は、本来クラウドのサービス(SMTP or Web API)ですが、モックを使ってオフラインで動作確認する方法を見つけて使ってみました。


見つけた SendGrid のモックは、以下です。
参考記事:SendGrid 用の Mail モックコンテナを作りました(https://engineer.blog.lancers.jp/docker/sendgrid-maildev/)
ソースコード:https://github.com/yKanazawa/sendgrid-dev


上記参考記事にもありますが、SMTP サーバーも MailDev を利用して、モックで構築しました。
結果、メール送信プログラムのデプロイ先(AWS のモック)、メール配信 API(SendGrid のモック)、SMTP サーバー、全てモックでメール送信プログラムの動作確認ができるようになりました。
全てモックのため、オフラインで動作確認可能です。

LocalStack、Kotless、MailDev、SendGridのモック環境 図


AWS - SendGrid - SMTP サーバー - メールボックス の疑似環境全てをオフラインに閉じ込めてテストできます。
これにより、メール送信プログラムの実装をミスっても問題無くなります。


ネットワーク設定などにより、事故が起きる可能性はあります。

本記事情報の誤り、見落とし、考慮不足等により何らかの問題が生じても、一切責任を負いません。

デプロイ先、向き先を

LocalStack → 本物のAWS

sendgrid-dev → 本物のSendGrid

と置き換えれば、動作するはずですが、本物に置き換えての動作確認はしていません。


【検証環境】

VMware Workstation Pro 16

 Ubuntu 20.04.2 LTS

  Docker 20.10.14

  openjdk 17.0.3

  Gradle 7.3.3

  Kotlin 1.5.31

  LocalStack 0.11.2

 Ubuntu 20.04.2 LTS

  node 14.19.3

  npm 6.14.17

  maildev 2.0.5

  sendgrid-dev 0.9.1

  go 1.18.2


MailDev インストール

Docker を使わず、ビルドしていきましたので、ビルドの手順になります。


node,npm インストール

apt でインストールした nodejs v10.19.0 の場合、TextEncoder is not defined となり、maildev の起動に失敗したため、v14.x をインストールしました。

v10のmaildev起動時エラー
# maildev
/usr/local/lib/node_modules/maildev/node_modules/whatwg-url/lib/encoding.js:2
const utf8Encoder = new TextEncoder();
                    ^

ReferenceError: TextEncoder is not defined

v14.xインストール手順
# curl -sL https://deb.nodesource.com/setup_14.x -o nodesource_setup.sh
# bash nodesource_setup.sh
# apt update
# apt -y install nodejs
# node -v
v14.19.3

# npm -v
6.14.17

maildev インストール

maildev は、コマンドとして使いたいので、グローバルに npm install します。

# npm install -g maildev
# maildev -V
2.0.5

特に指定が無い場合、メールデータは、/tmp/maildev-3621 のように /tmp/ 配下になります。maildev- の後の数字部分は毎回ランダムに決まるようです。

SMTP は 1025、Webは 1080 ポートで待ち受けます。それぞれ、-s -w オプションで変更可能です。


MailDev 動作確認

SMTP サーバー、Web 画面の動作確認を行います。

MailDev を起動します。

# mkdir /home/admin/mail
# maildev --mail-directory /home/admin/mail

なんとなく、メールデータを消えないところに置きたかったので、--mail-directory で指定して起動しています。


動作確認用に telnet、nkf をインストールします。

# apt install -y telnet
# apt install -y nkf

nkf は、日本語のメールを送信したいため、インストールしました。
コピペ用に MIME エンコードの値を知りたいためにインストールしただけで、必須ではありません。


以下のように base64 の値をあらかじめ調べておきます。
メール件名:ISO-2022-JP の base64 エンコードヘッダ形式
メール本文:ISO-2022-JP の base64 エンコード
です。

# echo 日本語件名 | nkf -jM
=?ISO-2022-JP?B?GyRCRnxLXDhsN29MPhsoQg==?=

# echo 日本語本文 | nkf -jMB
GyRCRnxLXDhsS1xKOBsoQgo=

調べた値を使って、以下のように
件名:日本語件名
本文:日本語本文
のメールを telnet で送信します。

# telnet localhost 1025
HELO maildev.example.com
MAIL FROM: <from@maildev.example.com>
RCPT TO: <rcpt@maildev.example.com>
data
From: "sender" <sender@maildev.example.com>
To: "rcpt" <rcpt@maildev.example.com>
Subject: =?ISO-2022-JP?B?GyRCRnxLXDhsN29MPhsoQg==?=
MIME-Version: 1.0
Content-Type: text/plain; charset="iso-2022-jp"
Content-Transfer-Encoding: base64

GyRCRnxLXDhsS1xKOBsoQgo=
.
quit

MailDev動作確認 telnet localhost 1025 図

Web 画面(http://localhost:1080/)へアクセスしてみます。


MailDev動作確認 文字化け


日本語が文字化けして表示されました。


iconv 組み込みによって、これを解消します。


iconv 組み込み

日本語文字化け解消のため、maildev に iconv を追加します。

# apt -y install build-essential
# cd /usr/lib/node_modules/maildev
# npm install iconv

# apt -y install build-essential は、gyp ERR! stack Error: not found: make とエラーになったため、make を事前にインストールしています。


maildev 再動作確認

CTRL+C で止めて再度起動します。

# maildev --mail-directory /home/admin/mail

Web 画面(http://localhost:1080/)へアクセスしてみます。

MailDev動作確認 文字化け解消


文字化けが解消しました!


sendgrid-dev インストール

SendGrid Web API のモック sendgrid-dev をインストールします。

go インストール

sendgrid-dev は、Go 言語(golang)製のため、go をダウンロードして、インストールします。

# wget https://go.dev/dl/go1.18.2.linux-amd64.tar.gz
# tar zxf go1.18.2.linux-amd64.tar.gz -C /usr/local/
# vi ~/.profile

以下を追記します。

export PATH=$PATH:/usr/local/go/bin
# source ~/.profile
# go version
go version go1.18.2 linux/amd64

直接置いて、パスを通すだけです。最新バージョンは、go公式ページ(https://go.dev/dl/)で確認できます。


sendgrid-dev ビルド

https://github.com/yKanazawa/sendgrid-dev からソースコードの zip をダウンロードして、go run でビルド&起動します。

# unzip sendgrid-dev-master.zip
# cd sendgrid-dev-master
# go run main.go
SENDGRID_DEV_API_SERVER :3030
SENDGRID_DEV_API_KEY SG.xxxxx
SENDGRID_DEV_SMTP_SERVER 127.0.0.1:1025
SENDGRID_DEV_SMTP_USERNAME
SENDGRID_DEV_SMTP_PASSWORD

   ____    __
  / __/___/ /  ___
 / _// __/ _ \/ _ \
/___/\__/_//_/\___/ v3.3.10-dev
High performance, minimalist Go web framework
https://echo.labstack.com
____________________________________O/_______
                                    O\
? http server started on [::]:3030

ポート::3030
API キー:SG.xxxxx
SMTP サーバー:127.0.0.1:1025
で起動します。


SMTP サーバーは、MailDev がちょうど、localhost:1025ポートですので、何も変更する必要は有りませんでした。(デフォルトで、MailDev との連携が想定されているからです。)
変更する場合、環境変数をセットしてから起動します。


sendgrid-dev 動作確認

(maildev が起動しているものとします。)

# apt -y install curl
# curl --request POST \
  --url http://localhost:3030/v3/mail/send \
  --header 'Authorization: Bearer SG.xxxxx' \
  --header 'Content-Type: application/json' \
  --data '{"personalizations": [{
    "to": [{"email": "to@example.com"}]}],
    "from": {"email": "from@example.com"},
    "subject": "Test Subject",
    "content": [{"type": "text/plain", "value": "Test Content"}]
  }'

sendgrid-dev 動作確認 curl 図

Web 画面(http://localhost:1080/)へアクセスしてみます。

sendgrid-dev 動作確認 curlのメール受信


メールを受信しています!


Kotless デプロイ

デプロイ先:LocalStack(AWS Lambda のローカル環境版)
デプロイするもの:Kotlin のサーバーレスフレームワーク Kotless プログラム
を準備します。


LocalStack & Kotless の詳しいことは、当ブログ別記事「Kotless と LocalStack で疑似サーバーレス - AWS S3 ダウンロード」に書きましたので、大部分端折って、プログラム作成部分だけ書きます。 LocalStack の準備、JDK、IntelliJ インストールの部分や、設定の意味は、上記記事を見てください。


/home/share/sendgrid
に Kotlin のプロジェクトを新規作成します。

IntelliJ IDEA Kotless プロジェクト新規作成


settings.gradle.ktsgradle.propertiesbuild.gradle.kts をそれぞれ以下のようにします。

settings.gradle.kts
rootProject.name = "sendgrid"

pluginManagement {
    resolutionStrategy {
        this.eachPlugin {
            if (requested.id.id == "io.kotless") {
                useModule("io.kotless:gradle:${this.requested.version}")
            }
        }
    }

    repositories {
        maven(url = uri("https://packages.jetbrains.team/maven/p/ktls/maven"))
        gradlePluginPortal()
        mavenCentral()
    }
}

gradle.properties
kotlin.code.style=official
awsSdkKotlinVersion=0.+

build.gradle.kts
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
import io.kotless.plugin.gradle.dsl.kotless

plugins {
    //kotlin("jvm") version "1.6.20"
    kotlin("jvm") version "1.5.31" apply true
    id("io.kotless") version "0.2.0" apply true
}

group = "org.example.sendgrid"
version = "1.0-SNAPSHOT"

repositories {
    mavenCentral()
    maven(url = uri("https://packages.jetbrains.team/maven/p/ktls/maven"))
}

val awsSdkKotlinVersion: String by project

dependencies {
    testImplementation(kotlin("test"))
    implementation("io.kotless", "kotless-lang", "0.2.0")
    implementation("io.kotless", "kotless-lang-aws", "0.2.0")
    implementation("com.sendgrid", "sendgrid-java", "4.9.2")
}

tasks.test {
    useJUnitPlatform()
}

tasks.withType<KotlinCompile> {
    kotlinOptions.jvmTarget = "1.8"
}

kotless {
    config {
        aws {
            storage {
                bucket = "kotless.sendgrid.example.com"
            }

            profile = "example"
            region = "eu-west-1"
        }
    }
    extensions {
        local {
            port = 8080
            //enables AWS emulation (disabled by default)
            useAWSEmulation = true
        }
    }
}

メール送信プログラム src/main/kotlin/org/example/sendgrid/Main.kt を作成します。

Main.kt
package org.example.sendgrid

import com.sendgrid.Method
import com.sendgrid.Request
import com.sendgrid.SendGrid
import com.sendgrid.helpers.mail.Mail
import com.sendgrid.helpers.mail.objects.Content
import com.sendgrid.helpers.mail.objects.Email
import io.kotless.dsl.lang.http.Get
import org.slf4j.LoggerFactory
import java.io.IOException

private val logger = LoggerFactory.getLogger("sendgrid")

@Get("/")
fun main() = "Hello world!"

@Get("/sendmail")
fun sendmail(mailTo: String = ""): String = sendBySendGrid(mailTo)

fun sendBySendGrid(mailTo: String): String  {
    logger.info("sendBySendGrid start")
    val from = Email("test@example.com")
    val subject = "Sending with SendGrid is Fun/SendGridで送信するのは楽しいです"
    val to = Email(mailTo)
    val content = Content("text/plain", "and easy to do anywhere, even with Kotlin\nKotlinを使用しても、どこでも簡単に実行できます")
    val mail = Mail(from, subject, to, content)

    val sg = SendGrid("SG.xxxxx", true)
    sg.host = "192.168.12.206:3030"
    val request = Request()
    logger.info("sendBySendGrid request")
    try {
        request.method = Method.POST
        request.endpoint = "mail/send"
        request.body = mail.build()
        val response = sg.api(request)
        logger.info("sendBySendGrid response:${response.statusCode}")
        return(response.statusCode.toString())
    } catch (ex: IOException) {
        logger.info("sendBySendGrid Error")
        throw ex
    }
}

プログラムは、
Kotlin から SendGrid を利用してメール送信する(https://sendgrid.kke.co.jp/blog/?p=8471)
を参考にしました。
というか、ほぼ、そのままですが、
今回オフラインで動作させたいため、以下の部分が重要になります。


val sg = SendGrid("SG.xxxxx", true)
SendGrid の最初の引数は、API キーですが、2番目の引数は、今回のようなテスト環境の場合、true にする必要があります。 これにより、https:// ではなく、http:// でアクセスするようになります。
sendgrid-dev は、http:// のため、true にする必要があります。


sg.host = "192.168.12.206:3030"
sendgrid-dev のホスト:ポートにしないと、本物の SendGrid へ行ってしまいます。
本物の SendGrid へ行ってしまったときは、401 が返ります。


LocalStack に Kotless プログラムをデプロイして、リクエストを出してみます。

# cd /home/share/sendgrid
# chmod 755 gradlew
# ./gradlew local

LocalStackにKotlessプログラムをデプロイ 図


(別端末で)

$ curl http://localhost:8080
Hello world!

@Get("/")
fun main() = "Hello world!"
が動作して、
正常にデプロイされていることが確認できました!


メール送信動作確認

MailDev(SMTP サーバー)
sendgrid-dev(SendGrid モック API)
は起動しているものとします。


http://localhost:8080/sendmail
を起動します。

$ curl http://localhost:8080/sendmail?mailTo=hogehoge@example.com
202

202 が返れば正常です。


Kotless メール送信動作確認 図

Web 画面(http://localhost:1080/)へアクセスしてみます。

Kotless メール送信動作確認


メールを受信しています!


ヨシ!!


エラーまとめ




エラー内容:

# maildev

/usr/local/lib/node_modules/maildev/node_modules/whatwg-url/lib/encoding.js:2
const utf8Encoder = new TextEncoder();
ReferenceError: TextEncoder is not defined


原因:
node のバージョンが低い。


対処内容:

Node v14 にする。

# curl -sL https://deb.nodesource.com/setup_14.x -o nodesource_setup.sh
# bash nodesource_setup.sh
# apt -y install nodejs
# node -v
v14.19.3




エラー内容:

# cd /usr/lib/node_modules/maildev
# npm install iconv

gyp ERR! build error
gyp ERR! stack Error: not found: make
gyp ERR! stack at getNotFoundError (/usr/lib/node_modules/npm/node_modules/which/which.js:13:12)


原因:
make がインストールされていない。


対処内容:

# apt -y install build-essential




エラー内容:

$ curl http://localhost:8080/sendmail?mailTo=hogehoge@example.com

May 29, 2022 7:33:05 PM org.apache.http.impl.execchain.RetryExec execute
INFO: I/O exception (java.net.NoRouteToHostException) caught when processing request to {s}->https://192.168.12.202:3030: No route to host 19:33:05.209 [qtp233996206-16] ERROR i.k.dsl.app.http.RoutesDispatcher - Failed on call of function sendmail


原因:
sendgrid-dev へ https:// でアクセスしている。


対処内容:
http:// でアクセスするように修正。(二番目の引数にtrueをセットすることにより、テストモードにする。)

    val sg = SendGrid("SG.xxxxx", true)
    sg.host = "192.168.12.206:3030"




loading...