SendGrid x Spring BootをPivotal Web Services(Cloud Foundry)で試す

@kikutaro_さんがSendGriderになられたのを祝って(?)、自分もSendGridをCloud Foundryで使ってみます。

Pivotal Web ServicesではMarketplaceにSendGridサービスが用意されており、SendGridのアカウントを作らなくてもcf create-serviceでアカウントを払い出すことができます。

ただし、残念なことに現状Send Gridのサービスインスタンスから渡されるクレデンシャル情報は

  • hostname
  • username
  • password

であり、API Keyが渡ってこないため、PWSのSendGridサービスではWeb API V2SMTP APIのみ利用可能です。Web API V3は利用できません。 (PWSのSendGridサービスを使わず、普通にAPI Keyを払い出せばV3 APIももちろん使えます。)

V2 APIやSMTP APIでも十分なケースは多いのと思うので、それぞれをJavaアプリ(主にSpring Boot)で使う方法を紹介します。

Web API V2を使う

まずはSendGridサービスインスタンスを作成する。cf marketplacesendgridサービスのプランを確認すると、freeプランが無償で使えることがわかリマす。

$ cf marketplace | grep sendgrid
sendgrid         free, bronze*, silver*                                                               Email Delivery. Simplified.

sendgridサービスのfreeプランのサービスインスタンスをdemo-sendgridという名前で作成します。

cf create-service sendgrid free demo-sendgrid

後でこのサービスインスタンスをアプリケーションにバインドしますが、バインドするとアプリケーションには環境変数VCAP_SERVICESの中に

{
  "sendgrid": [
   {
    "credentials": {
     "hostname": "smtp.sendgrid.net",
     "password": "zqaT47p6zdx30890",
     "username": "e6g8xCyB29"
    },
    "label": "sendgrid",
    "name": "demo-sendgrid",
    "plan": "free",
    "provider": null,
    "syslog_drain_url": null,
    "tags": [
     "iPhone",
     "Web-based",
     "Email",
     "smtp",
     "Analytics",
     "Mac",
     "Developer Tools",
     "Retail",
     "Inventory management",
     "iPad",
     "BI \u0026 Analytics",
     "Email Delivery",
     "Communication"
    ],
    "volume_mounts": []
   }
  ]
 }

というJSON形式で接続情報が渡ります。このJSONをパースしてcredentialsを取りに行くのが基本的な使い方です。

今すぐ始めるCloud Foundry #hackt #hackt_k from Toshiaki Maki

次にSpring Bootの雛形を作成します。

$ curl start.spring.io/starter.tgz \
       -d artifactId=hello-sendgrid \
       -d baseDir=hello-sendgrid \
       -d dependencies=web \
       -d applicationName=HelloSendgridApplication | tar -xzvf -

pom.xmlにV2 API用のJavaクライアントライブラリを追加します。

		<dependency>
			<groupId>com.sendgrid</groupId>
			<artifactId>sendgrid-java</artifactId>
			<version>2.2.2</version>
		</dependency>

HelloSendgridApplicationクラスにSendGridを使うコードを追記します。

package com.example;

import java.io.IOException;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.sendgrid.SendGrid;
import com.sendgrid.SendGridException;

@SpringBootApplication
@RestController
public class HelloSendgridApplication {

	public static void main(String[] args) {
		SpringApplication.run(HelloSendgridApplication.class, args);
	}

	@Autowired
	SendGrid sendGrid;

	@GetMapping
	SendGrid.Response send(@RequestParam String message) throws SendGridException {
		SendGrid.Email email = new SendGrid.Email()
				.addTo("makingx@example.com")
				.setFrom("other@example.com")
				.setSubject("Hello World")
				.setText(message);
		return sendGrid.send(email);
	}

	@Bean
	SendGrid sendGrid(ObjectMapper objectMapper) throws IOException {
		JsonNode credentials = objectMapper
				.readValue(System.getenv("VCAP_SERVICES"), JsonNode.class).get("sendgrid")
				.get(0).get("credentials");
		String username = credentials.get("username").asText();
		String password = credentials.get("password").asText();
		return new SendGrid(username, password);
	}

}

今回はCloud Foundry独自の方法を使わず、素直にJacksonでVCAP_SERVICESをパースして、demo-sendgridサービスインスタンスのusernamepasswordを取得しました。この方法はSpring BootだけでなくどのJavaアプリでも使えます。

このアプリケーションをデプロイするためのmanifest.ymlを作成します。

applications:
- name: hello-sendgrid
  memory: 256m
  buildpack: java_buildpack
  path: ./target/hello-sendgrid-0.0.1-SNAPSHOT.jar
  services:
  - demo-sendgrid

後はビルドしてcf push

./mvnw clean package -DskipTests=true
cf push

何か送ってみます。

$ curl "https://hello-sendgrid.cfapps.io?message=💩" | jq .

{
  "code": 200,
  "message": "{\"message\":\"success\"}",
  "status": true
}

しばらくするとメールが無事送信されました。簡単ですね。

image

次にSpring Boot限定の設定方法を紹介します。Spring Bootでは自動で環境変数VCAP_SERVICESをパースし次のようなフラットなプロパティとして扱えるようになっています。

image

demo-sendgridサービスインスタンスのusernamepasswordvcap.services.demo-sendgrid.credentials.usernamevcap.services.demo-sendgrid.credentials.passwordというプロパティで取得可能です。

したがって、SendGridインスタンスの作成は次のようにシンプルになります。

package com.example;

import java.io.IOException;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import com.sendgrid.SendGrid;
import com.sendgrid.SendGridException;

@SpringBootApplication
@RestController
public class HelloSendgridApplication {

	public static void main(String[] args) {
		SpringApplication.run(HelloSendgridApplication.class, args);
	}

	@Autowired
	SendGrid sendGrid;

	@GetMapping
	SendGrid.Response send(@RequestParam String message) throws SendGridException {
		SendGrid.Email email = new SendGrid.Email()
				.addTo("makingx@example.com")
				.setFrom("other@example.com")
				.setSubject("Hello World")
				.setText(message);
		return sendGrid.send(email);
	}

	@Bean
	SendGrid sendGrid(
			@Value("${vcap.services.demo-sendgrid.credentials.username}") String username,
			@Value("${vcap.services.demo-sendgrid.credentials.password}") String password)
			throws IOException {
		return new SendGrid(username, password);
	}

}

後はビルドしてcf push

./mvnw clean package -DskipTests=true
cf push

SMTP APIを使う

次にSMTP APIを使ってみます。この場合はSendGridクライアントライブラリは不要でJavaMail API(をラップしたSpringのMainSender)を使います。

com.sendgrid:sendgrid-java:2.2.2<dependency>を削除して、次の<dependency>を追加します。

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-mail</artifactId>
		</dependency>

MailSenderを使うようにアプリケーションを修正します。

package com.example;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.mail.MailSender;
import org.springframework.mail.SimpleMailMessage;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@SpringBootApplication
@RestController
public class HelloSendgridApplication {

	public static void main(String[] args) {
		SpringApplication.run(HelloSendgridApplication.class, args);
	}

	@Autowired
	MailSender mailSender;

	@GetMapping
	void send(@RequestParam String message) {
		SimpleMailMessage email = new SimpleMailMessage();
		email.setTo("makingx@example.com");
		email.setFrom("other@example.com");
		email.setSubject("Hello World");
		email.setText(message);
		mailSender.send(email);
	}
}

Spring BootのSMTP AutoConfigureを使うには次のプロパティを定義すれば良いです。

  • spring.mail.host
  • spring.mail.username
  • spring.mail.password

application-cloud.propertiesに次の設定を行ってください。

spring.mail.host=${vcap.services.demo-sendgrid.credentials.hostname}
spring.mail.username=${vcap.services.demo-sendgrid.credentials.username}
spring.mail.password=${vcap.services.demo-sendgrid.credentials.password}

VCAP_SERVICESの値を設定しています。Cloud FoundryにSpring Bootアプリケーションをデプロイするとcloudプロファイルが適用されるため、application-cloud.propertiesに書いたプロパティはCloud Foundryにデプロイされた時だけ有効になります。ローカル環境で試す時はapplication.propertiesにローカル用の設定を書いておけば環境ごとの設定切り替えが自動で行われて便利です。

spring.mail.host=stmp.local
spring.mail.username=test@local
spring.mail.password=test

後はビルドしてcf push

./mvnw clean package -DskipTests=true
cf push

Spring Cloud Connectorを使う

Spring Cloud Connectorを利用すると環境変数VCAP_SERVICESから接続情報を取得してデータアクセス必要なオブジェクト(RDBMSの場合はDataSource、Redisの場合はRedisConnectionFactory)を環境に依存せずに簡単に定義できます。SMTPも対応しています

次の<dependency>を追加します。

		<dependency>
			<groupId>org.springframework.cloud</groupId>
			<artifactId>spring-cloud-spring-service-connector</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.cloud</groupId>
			<artifactId>spring-cloud-cloudfoundry-connector</artifactId>
		</dependency>

MailSenderをクラウド情報から自動で生成する定義をCloudConfigクラスに行います。

package com.example;

import org.springframework.cloud.config.java.AbstractCloudConfig;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.mail.MailSender;

@Profile("cloud")
@Configuration
public class CloudConfig extends AbstractCloudConfig {
    @Bean
	MailSender mailSender() {
		return connectionFactory().service(MailSender.class);
	}
}

application-cloud.propertiesは削除してください。SMTPサービスインスタンスが一つしかバインドされていない場合は、サービスインスタンス名の指定は不要です。 ちなみにバインドされたサービスインスタンスがSMTPかどうかの判定はtagsmtpが含まれているかどうか等です。

後はビルドしてcf push

./mvnw clean package -DskipTests=true
cf push

Web API V3を使う

折角なのでV3 APIを使う方法も紹介します。こちらはAPI Keyが必要なので、もはやPWSのSendGridサービスインスタンスは不要です。 アプリケーションんからアンバインドします。

cf unbind-service hello-sendgrid demo-sendgrid

spring-boot-starter-mailspring-cloud-*-connector<dependency>も不要で、代わりにV3 API用のクライアントライブラリを追加します。

		<dependency>
			<groupId>com.sendgrid</groupId>
			<artifactId>sendgrid-java</artifactId>
			<version>3.1.0</version>
		</dependency>

V3 API用のクライアントライブラリを使うようにアプリケーションを修正します。API Keyは環境変数SENDGRID_API_KEYに渡すことにします。

package com.example;

import java.io.IOException;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import com.sendgrid.*;

@SpringBootApplication
@RestController
public class HelloSendgridApplication {

	public static void main(String[] args) {
		SpringApplication.run(HelloSendgridApplication.class, args);
	}

	@Autowired
	SendGrid sendGrid;

	@GetMapping
	Response send(@RequestParam String message) throws IOException {
		Email from = new Email("other@example.com");
		String subject = "Hello World";
		Email to = new Email("makingx@example.com");
		Content content = new Content("text/plain", message);
		Mail mail = new Mail(from, subject, to, content);
		Request request = new Request();
		request.method = Method.POST;
		request.endpoint = "mail/send";
		request.body = mail.build();
		return sendGrid.api(request);
	}

	@Bean
	SendGrid sendGrid() {
		return new SendGrid(System.getenv("SENDGRID_API_KEY"));
	}
}

API Keyは公式サイト代理店サイトでアカウントを作成して取得してください。

追記

manifest.ymlを次のように変更します。

applications:
- name: hello-sendgrid
  memory: 256m
  buildpack: java_buildpack
  path: ./target/hello-sendgrid-0.0.1-SNAPSHOT.jar
  env:
    SENDGRID_API_KEY: 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

後はビルドしてcf push

./mvnw clean package -DskipTests=true
cf push

☝️のアプリをベースにSendGridで色々遊んでみてください。 (ちなみにPivotal Web Servicesからの通知メールにもSendGridが使われています。)

また、Cloud Foundryをもう少し触りたくなったらWorkshop資料を見て色々試してください。 拙著「はじめてのSpring Boot [改訂版] 」の4章でも学べます。

はじめてのSpring Boot―スプリング・フレームワークで簡単Javaアプリ開発 (I・O BOOKS)