Amazon SNS のHTTPエンドポイントを手軽に実装するSpring Cloud AWSを使ってみた
はじめに
好物はインフラとフロントエンドのかじわらゆたかです。 SpringFrameworkのクラウド向けライブラリを用いることで簡単に SNSのエンドポイントが実装できたので、その紹介です。
Amazon SNS to HTTP
Amazon SNSは受けた通知をHTTPエンドポイントに通知することができます。 ですが、HTTPエンドポイントを実装する為には、エンドポイントに対して受信登録確認用の処理を実装する必要があります。
HTTP/HTTPS エンドポイントへの Amazon SNS メッセージの送信 - Amazon Simple Notification Service
Spring Cloud AWSを用いることで、アノテーションをつけるのみで実装されることとなり、 に受信したいメッセージ受信処理をすぐに実装することが可能となります。
検証環境
- macOS El Capitan 10.11.6
- java version "1.8.0_131"
下準備
以下のbuild.gradleで環境を構築していきます。
buildscript { ext { springBootVersion = '1.5.2.RELEASE' } repositories { mavenCentral() } dependencies { classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}") } } apply plugin: 'java' apply plugin: 'eclipse' apply plugin: 'org.springframework.boot' version = '0.0.1-SNAPSHOT' sourceCompatibility = 1.8 repositories { mavenCentral() } dependencies { compile('org.springframework.cloud:spring-cloud-starter-aws-messaging') compile('org.springframework.boot:spring-boot-starter-web') testCompile('org.springframework.boot:spring-boot-starter-test') } dependencyManagement { imports { mavenBom "org.springframework.cloud:spring-cloud-dependencies:Dalston.RELEASE" } }
SNSのHTTPエンドポイントを実装する
Spring Cloud AWSにはAmazon SNSのエンドポイント実装用の下記のアノテーションが実装されています。
- @NotificationSubscriptionMapping
- @NotificationMessageMapping
- @NotificationUnsubscribeMapping
これらを実装したコントローラを実装することで、エンドポイントが容易に実装できます。 Spring Cloud AWSのオフィシャルサイトに書いてあるサンプルと同様のものを実装してみました。
@Controller @RequestMapping("/") public class SNSEndpointController { @NotificationSubscriptionMapping public void handleSubscriptionMessage(NotificationStatus status) throws IOException { //We subscribe to start receive the message status.confirmSubscription(); } @NotificationMessageMapping public void handleNotificationMessage(@NotificationSubject String subject, @NotificationMessage String message) { System.out.println("Subject:" + subject); System.out.println("Message:" + message); } @NotificationUnsubscribeConfirmationMapping public void handleUnsubscribeMessage(NotificationStatus status) { //e.g. the client has been unsubscribed and we want to "re-subscribe" status.confirmSubscription(); } }
動かしてみる for Local
Spring Cloud AWSは他にもAmazon S3やAWS CloudFormationと連携するように実装されており、 SNSと連携するエンドポイントを実装したいはずが上記の連携機能により以下の様なエラーとなってしまいます。
$ SPRING_PROFILES_ACTIVE=local gradle clean bootRun Starting a Gradle Daemon, 1 incompatible Daemon could not be reused, use --status for details :compileJava :processResources :classes :findMainClass :bootRun . ____ _ __ _ _ /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \ ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \ \\/ ___)| |_)| | | | | || (_| | ) ) ) ) ' |____| .__|_| |_|_| |_\__, | / / / / =========|_|==============|___/=/_/_/_/ :: Spring Boot :: (v1.5.2.RELEASE) 2017-04-25 19:32:51.718 INFO 46234 --- [ main] j.c.SpringCloudAwsDemoApplication : Starting SpringCloudAwsDemoApplication on HL00088.local with PID 46234 (/Users/kajiwarayutaka/Downloads/SpringCloudAWSDemo/build/classes/main started by kajiwarayutaka in /Users/kajiwarayutaka/Downloads/SpringCloudAWSDemo) 2017-04-25 19:32:51.722 INFO 46234 --- [ main] j.c.SpringCloudAwsDemoApplication : No active profile set, falling back to default profiles: default 2017-04-25 19:32:51.792 INFO 46234 --- [ main] ationConfigEmbeddedWebApplicationContext : Refreshing org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@365c30cc: startup date [Tue Apr 25 19:32:51 JST 2017]; root of context hierarchy 2017-04-25 19:33:00.626 WARN 46234 --- [ main] ationConfigEmbeddedWebApplicationContext : Exception encountered during context initialization - cancelling refresh attempt: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'org.springframework.cloud.aws.context.support.io.ResourceLoaderBeanPostProcessor#0': Cannot resolve reference to bean 'amazonS3' while setting constructor argument; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'amazonS3': Invocation of init method failed; nested exception is java.lang.IllegalStateException: There is not EC2 meta data available, because the application is not running in the EC2 environment. Region detection is only possible if the application is running on a EC2 instance 2017-04-25 19:33:00.631 ERROR 46234 --- [ main] o.s.b.f.s.DefaultListableBeanFactory : Destroy method on bean with name 'org.springframework.boot.autoconfigure.internalCachingMetadataReaderFactory' threw an exception java.lang.IllegalStateException: ApplicationEventMulticaster not initialized - call 'refresh' before multicasting events via the context: org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@365c30cc: startup date [Tue Apr 25 19:32:51 JST 2017]; root of context hierarchy at org.springframework.context.support.AbstractApplicationContext.getApplicationEventMulticaster(AbstractApplicationContext.java:404) [spring-context-4.3.7.RELEASE.jar:4.3.7.RELEASE] at org.springframework.context.support.ApplicationListenerDetector.postProcessBeforeDestruction(ApplicationListenerDetector.java:97) ~[spring-context-4.3.7.RELEASE.jar:4.3.7.RELEASE] at org.springframework.beans.factory.support.DisposableBeanAdapter.destroy(DisposableBeanAdapter.java:253) ~[spring-beans-4.3.7.RELEASE.jar:4.3.7.RELEASE] at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.destroyBean(DefaultSingletonBeanRegistry.java:578) [spring-beans-4.3.7.RELEASE.jar:4.3.7.RELEASE] at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.destroySingleton(DefaultSingletonBeanRegistry.java:554) [spring-beans-4.3.7.RELEASE.jar:4.3.7.RELEASE] at org.springframework.beans.factory.support.DefaultListableBeanFactory.destroySingleton(DefaultListableBeanFactory.java:961) [spring-beans-4.3.7.RELEASE.jar:4.3.7.RELEASE] at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.destroySingletons(DefaultSingletonBeanRegistry.java:523) [spring-beans-4.3.7.RELEASE.jar:4.3.7.RELEASE] at org.springframework.beans.factory.support.DefaultListableBeanFactory.destroySingletons(DefaultListableBeanFactory.java:968) [spring-beans-4.3.7.RELEASE.jar:4.3.7.RELEASE] at org.springframework.context.support.AbstractApplicationContext.destroyBeans(AbstractApplicationContext.java:1033) [spring-context-4.3.7.RELEASE.jar:4.3.7.RELEASE] at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:555) [spring-context-4.3.7.RELEASE.jar:4.3.7.RELEASE] at org.springframework.boot.context.embedded.EmbeddedWebApplicationContext.refresh(EmbeddedWebApplicationContext.java:122) [spring-boot-1.5.2.RELEASE.jar:1.5.2.RELEASE] at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:737) [spring-boot-1.5.2.RELEASE.jar:1.5.2.RELEASE] at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:370) [spring-boot-1.5.2.RELEASE.jar:1.5.2.RELEASE] at org.springframework.boot.SpringApplication.run(SpringApplication.java:314) [spring-boot-1.5.2.RELEASE.jar:1.5.2.RELEASE] at org.springframework.boot.SpringApplication.run(SpringApplication.java:1162) [spring-boot-1.5.2.RELEASE.jar:1.5.2.RELEASE] at org.springframework.boot.SpringApplication.run(SpringApplication.java:1151) [spring-boot-1.5.2.RELEASE.jar:1.5.2.RELEASE] at jp.classmethod.SpringCloudAwsDemoApplication.main(SpringCloudAwsDemoApplication.java:10) [main/:na] 2017-04-25 19:33:00.637 ERROR 46234 --- [ main] o.s.boot.SpringApplication : Application startup failed org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'org.springframework.cloud.aws.context.support.io.ResourceLoaderBeanPostProcessor#0': Cannot resolve reference to bean 'amazonS3' while setting constructor argument; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'amazonS3': Invocation of init method failed; nested exception is java.lang.IllegalStateException: There is not EC2 meta data available, because the application is not running in the EC2 environment. Region detection is only possible if the application is running on a EC2 instance at org.springframework.beans.factory.support.BeanDefinitionValueResolver.resolveReference(BeanDefinitionValueResolver.java:359) ~[spring-beans-4.3.7.RELEASE.jar:4.3.7.RELEASE] at org.springframework.beans.factory.support.BeanDefinitionValueResolver.resolveValueIfNecessary(BeanDefinitionValueResolver.java:108) ~[spring-beans-4.3.7.RELEASE.jar:4.3.7.RELEASE] at org.springframework.beans.factory.support.ConstructorResolver.resolveConstructorArguments(ConstructorResolver.java:634) ~[spring-beans-4.3.7.RELEASE.jar:4.3.7.RELEASE] at org.springframework.beans.factory.support.ConstructorResolver.autowireConstructor(ConstructorResolver.java:145) ~[spring-beans-4.3.7.RELEASE.jar:4.3.7.RELEASE] at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.autowireConstructor(AbstractAutowireCapableBeanFactory.java:1193) ~[spring-beans-4.3.7.RELEASE.jar:4.3.7.RELEASE] at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1095) ~[spring-beans-4.3.7.RELEASE.jar:4.3.7.RELEASE] at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:513) ~[spring-beans-4.3.7.RELEASE.jar:4.3.7.RELEASE] at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:483) ~[spring-beans-4.3.7.RELEASE.jar:4.3.7.RELEASE] at org.springframework.beans.factory.support.AbstractBeanFactory$1.getObject(AbstractBeanFactory.java:306) ~[spring-beans-4.3.7.RELEASE.jar:4.3.7.RELEASE] at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:230) ~[spring-beans-4.3.7.RELEASE.jar:4.3.7.RELEASE] at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:302) ~[spring-beans-4.3.7.RELEASE.jar:4.3.7.RELEASE] at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:202) ~[spring-beans-4.3.7.RELEASE.jar:4.3.7.RELEASE] at org.springframework.context.support.PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(PostProcessorRegistrationDelegate.java:166) ~[spring-context-4.3.7.RELEASE.jar:4.3.7.RELEASE] at org.springframework.context.support.AbstractApplicationContext.invokeBeanFactoryPostProcessors(AbstractApplicationContext.java:686) ~[spring-context-4.3.7.RELEASE.jar:4.3.7.RELEASE] at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:524) ~[spring-context-4.3.7.RELEASE.jar:4.3.7.RELEASE] at org.springframework.boot.context.embedded.EmbeddedWebApplicationContext.refresh(EmbeddedWebApplicationContext.java:122) ~[spring-boot-1.5.2.RELEASE.jar:1.5.2.RELEASE] at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:737) [spring-boot-1.5.2.RELEASE.jar:1.5.2.RELEASE] at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:370) [spring-boot-1.5.2.RELEASE.jar:1.5.2.RELEASE] at org.springframework.boot.SpringApplication.run(SpringApplication.java:314) [spring-boot-1.5.2.RELEASE.jar:1.5.2.RELEASE] at org.springframework.boot.SpringApplication.run(SpringApplication.java:1162) [spring-boot-1.5.2.RELEASE.jar:1.5.2.RELEASE] at org.springframework.boot.SpringApplication.run(SpringApplication.java:1151) [spring-boot-1.5.2.RELEASE.jar:1.5.2.RELEASE] at jp.classmethod.SpringCloudAwsDemoApplication.main(SpringCloudAwsDemoApplication.java:10) [main/:na] Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'amazonS3': Invocation of init method failed; nested exception is java.lang.IllegalStateException: There is not EC2 meta data available, because the application is not running in the EC2 environment. Region detection is only possible if the application is running on a EC2 instance at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1628) ~[spring-beans-4.3.7.RELEASE.jar:4.3.7.RELEASE] at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:555) ~[spring-beans-4.3.7.RELEASE.jar:4.3.7.RELEASE] at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:483) ~[spring-beans-4.3.7.RELEASE.jar:4.3.7.RELEASE] at org.springframework.beans.factory.support.AbstractBeanFactory$1.getObject(AbstractBeanFactory.java:306) ~[spring-beans-4.3.7.RELEASE.jar:4.3.7.RELEASE] at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:230) ~[spring-beans-4.3.7.RELEASE.jar:4.3.7.RELEASE] at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:302) ~[spring-beans-4.3.7.RELEASE.jar:4.3.7.RELEASE] at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:197) ~[spring-beans-4.3.7.RELEASE.jar:4.3.7.RELEASE] at org.springframework.beans.factory.support.BeanDefinitionValueResolver.resolveReference(BeanDefinitionValueResolver.java:351) ~[spring-beans-4.3.7.RELEASE.jar:4.3.7.RELEASE] ... 21 common frames omitted Caused by: java.lang.IllegalStateException: There is not EC2 meta data available, because the application is not running in the EC2 environment. Region detection is only possible if the application is running on a EC2 instance at org.springframework.util.Assert.state(Assert.java:70) ~[spring-core-4.3.7.RELEASE.jar:4.3.7.RELEASE] at org.springframework.cloud.aws.core.region.Ec2MetadataRegionProvider.getRegion(Ec2MetadataRegionProvider.java:39) ~[spring-cloud-aws-core-1.2.0.RELEASE.jar:1.2.0.RELEASE] at org.springframework.cloud.aws.core.config.AmazonWebserviceClientFactoryBean.createInstance(AmazonWebserviceClientFactoryBean.java:98) ~[spring-cloud-aws-core-1.2.0.RELEASE.jar:1.2.0.RELEASE] at org.springframework.cloud.aws.core.config.AmazonWebserviceClientFactoryBean.createInstance(AmazonWebserviceClientFactoryBean.java:44) ~[spring-cloud-aws-core-1.2.0.RELEASE.jar:1.2.0.RELEASE] at org.springframework.beans.factory.config.AbstractFactoryBean.afterPropertiesSet(AbstractFactoryBean.java:134) ~[spring-beans-4.3.7.RELEASE.jar:4.3.7.RELEASE] at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.invokeInitMethods(AbstractAutowireCapableBeanFactory.java:1687) ~[spring-beans-4.3.7.RELEASE.jar:4.3.7.RELEASE] at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1624) ~[spring-beans-4.3.7.RELEASE.jar:4.3.7.RELEASE] ... 28 common frames omitted :bootRun FAILED FAILURE: Build failed with an exception. * What went wrong: Execution failed for task ':bootRun'. > Process 'command '/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/bin/java'' finished with non-zero exit value 1 * Try: Run with --stacktrace option to get the stack trace. Run with --info or --debug option to get more log output. BUILD FAILED
特にlocalの環境で動かそうとしてエラーとなるのは困るので、これらの機能を無効にしたいと思います。 Springの設定ファイルに記載することで無効にすることが可能です。
cloud: aws: region: auto: false static: ap-northeast-1 stack: auto: false
動かしてみる for Amazon EC2
EC2では、Regionの取得は行えるはずなので、以下の様な設定ファイルを追加で配置しました。
cloud: aws: region: auto: false static: ap-northeast-1
Local環境で配置するJarファイルを作成する必要があるため、ビルドはLocalの設定ファイルで動くようにして実施します。
$ SPRING_PROFILES_ACTIVE=local gradle clean build Starting a Gradle Daemon (subsequent builds will be faster) :clean :compileJava :processResources :classes :findMainClass :jar :bootRepackage :assemble :compileTestJava :processTestResources UP-TO-DATE :testClasses :test :check :build BUILD SUCCESSFUL Total time: 16.697 secs
ビルドしたJarを動かすEC2を立ち上げます。その際に、Amazon SNSへAdmin権限をもたせたIAM Roleを付与しました また、セキュリティグループは80番をフルアクセスとしました。 EC2を立ち上げ、立ち上げたEC2に上記でビルドしたJarファイルを配置します。 なお、上記のBuildGradleを用いた場合Java8の環境で動かす必要があるため、以下の記事を参考に導入を行います。
また、SNSにTopicを作成し、SubscriptionとしてProtcolにHTTP、Endpointに対象のURLを入力します。
Create Subscriptionを押下し、Pendingになった後、リストを更新することでSubscriptionとして登録されます。
Management Consoleから先ほど作成したSNSに対してPublishしてみたいと思います。
標準出力に以下の用にSNSでPublishしたTopicがSpringのControllerで受け取れたことがわかります。
まとめ
SNS連携を行うエンドポイント実装を行うのは意外と面倒だという話を同僚から聞いていたのですが、 Spring Cloud AWSを用いることで容易にできることがわかりました。
SNSは様々なAWSのサービスとも連携できるので、これらを用いることでAWSサービスからの連携した結果を受け取ってさらに処理をすすめるといったこともできそうです。