Serverless FrameworkでEC2をスケジュール起動/停止するテンプレート(Lambda[java])

Serverless Framework

はじめに

  • コンテナ付いてる昨今は、久しくServerless Framework触って無かったのですが、EC2を8時に起動して、20時半に停止する要件が浮上したので、サクッとslsで作りました。
  • ソースはGithubで公開してます。
  • 至極簡単な内容なので、速攻実現出来ると思ってます。

環境のセットアップ

Serverless FrameworkでDeploy

ソースの取得

  • 以下のGithubからソースを取得します。
$ git clone https://github.com/ukitiyan/operation-ec2-instance.git

STS(Eclipse)にインポート

  • STSを起動して、Project Explorer -> 右クリック -> Maven -> Existing Maven Projectsで先程Githubから取得した「operation-ec2-instance」フォルダを選択します。

serverless.yml の修正 + Build

  • serverless.ymlのL37 周辺の設定を適宜修正します。

    • rate: AWS Lambda – Scheduled EventのCron書式
      を参考に UTC で記載
    • instanceId: 対象インスタンスのinstanceIdを記載
    • scheduleは、縦に増やせるので複数インスタンスに対応できます。(それを踏まえて環境変数でinstanceIdを指定してません)
serverless.yml
- schedule:
    rate: cron(30 11 * * ? *)
    input:
      goal: stop
      instanceId: i-XXXXXXXXXXXXXXXXX
- schedule:
    rate: cron(0 23 * * ? *)
    input:
      goal: start
      instanceId: i-XXXXXXXXXXXXXXXXX
  • プロジェクトを右クリック -> Run As -> Maven Install でビルドします。
  • target配下にoperation-ec2-instance.1.0.0.jarが出来上がります。

Deploy

  • 例のごとく、以下のコマンド一発です。
$ cd operation-ec2-instance
$ serverless deploy -v
Serverless: Packaging service...
Serverless: Uploading CloudFormation file to S3...
Serverless: Uploading artifacts...
・
・
Serverless: Stack update finished...
Service Information
service: operation-ec2-instance
stage: prod
region: ap-northeast-1
api keys:
  None
endpoints:
  None
functions:
  aws-java-maven-prod-hello: arn:XXXXXXXX
  • 以下のJsonでコンソールから test するか、設定時間になるまで待って、問題ないか気にしておきましょう。
{
  "goal": "stop",
  "instanceId": "i-XXXXXXXXXXXXXXXXX"
}

まとめ

  • まぁ、やっつけですが。速攻実現出来ました。
  • STSで完結するのが、アプリ屋にとっては本当うれしいです。

続きを読む

いまからはじめるJavaでAWS Lambda(ラムダ) 前編

いまからはじめるJavaでAWS Lambda(ラムダ)

AWS Lambda関数をJava8で記述し、AWS上 Lambda Functionとしてアップロードし、それをJavaクライアントから呼び出す例をステップ・バイ・ステップで紹介します。

想定読者

  • Javaが書けて、はじめてAWS Lambdaをつかう人
  • いままではnode.jsでLambda関数を作っていたが、わけあってJavaでつくってみようとおもう人(=私のような)

記事構成

TL;DR 前編・後編で書きます

  • 【前編】 JavaでLambda関数(クラウド側)と、クライアント(ローカル側)をお手軽に作る←本稿
  • 【後編】 Lambda関数(クラウド側)の同期型、非同期型の呼び出しタイプ(Invocation Type)と、Javaクライアント側の同期、非同期呼び出し、API Gatewayでの公開

AWS Lambda(ラムダ)とは

  • 自前でサーバーを作ったり、管理したりする必要が無く、コードをAWS Lambdaにあげるだけで各種リクエスト(イベント)の処理が可能な素敵なサービスです。

 http://docs.aws.amazon.com/ja_jp/lambda/latest/dg/welcome.html
 https://aws.amazon.com/jp/lambda/faqs/

  • AlexaのスキルもAWS Lambdaで作成することができます。

JavaとAWS Lambda

  • JavaでAWS Lambdaの処理(Lambda関数)を記述することが可能
  • Javaクライアントから、直接Lambda関数を呼び出すことも可能
  • Javaに限らないが、API Gatewayというサービスと連携させると、Lambda関数にendpointをつくって公開することができ、Web APIのようにGETやPOSTといったHTTP(S)経由でも呼び出すことが可能

目次

以下のステップを1つずつ説明しつつ実施していきます

  1. AWS Lambda関数をJavaでコーディングする
  2. アップロード用のjarファイルを作る
  3. jarファイルをAWS Lambdaに登録して実行可能(呼び出し可能)にする
  4. AWSコンソール上でテストする
  5. ローカルJavaから、AWS Lambda関数を呼び出しする

1.AWS Lambda関数をコーディングする

Java用ライブラリ読み込み

aws-lambda-java-coreライブラリを追加します。mavenを使う場合は、以下の記述を追加します

maven
<dependency>
    <groupId>com.amazonaws</groupId>
    <artifactId>aws-lambda-java-core</artifactId>
    <version>1.1.0</version>
</dependency>

Lambda関数の中身をコーディングする

ここでは、関数を呼び出すと結果が返るリクエストレスポンス型(RequestResponse)のLambda関数をつくります。

今回はaws-lambda-java-coreライブラリが用意しているRequestHandlerインタフェースを実装して、POJOを入出力できるLambda関数を実装します。

以下は、姓(lastName)と名(firstName)を入力すると、フルネームを出力してくれるLambda関数のサンプルです。

MyLambda.java
package lambda.cloud;

import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.RequestHandler;

import lambda.cloud.MyLambda.Input;
import lambda.cloud.MyLambda.Output;

public class MyLambda implements RequestHandler<Input, Output> {

  @Override
  public Output handleRequest(Input in, Context context) {

    final Output out = new Output();
    out.in = in;
    out.fullName = in.firstName + "_" + in.lastName;

    return out;
  }

  public static class Input {
    public String firstName;
    public String lastName;
  }

  public static class Output {
    public Input in;
    public String fullName;

  }

}

以下のように、handleRequestメソッドを実装するだけです。引数 Input はリクエスト、戻り値 Output がレスポンスを示します。

 public Output handleRequest(Input in, Context context) {

2. アップロード用のjarファイルを作る

次にLambda関数としてAWS Lambdaで使えるようにするためにはコードをjarファイル(またはzip)にワンパッケージ化してAWS Lambdaにアップロードする必要があります。

このjarファイルは、いまつくったコードの他、依存しているライブラリなどをひとまとめに統合しておく必要があります。

ひとまとめに統合したjarファイル(つまりfat jar)をつくるためにmaven pom.xmlのplugins以下にmaven-shade-pluginを追加しておきます。

    <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-shade-plugin</artifactId>
        <version>3.1.0</version>
        <configuration>
            <createDependencyReducedPom>false</createDependencyReducedPom>
        </configuration>
        <executions>
            <execution>
                <phase>package</phase>
                <goals>
                    <goal>shade</goal>
                </goals>
            </execution>
        </executions>
    </plugin>

pom.xml

pom.xml全体は、以下のようになります

pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>lambda.cloud</groupId>
    <artifactId>mylambda</artifactId>
    <version>1.0.0</version>
    <packaging>jar</packaging>
    <name>AWS lambda example</name>
    <description>example of AWS lambda
    </description>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>com.amazonaws</groupId>
            <artifactId>aws-lambda-java-core</artifactId>
            <version>1.1.0</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                </configuration>
            </plugin>

            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-shade-plugin</artifactId>
                <version>3.1.0</version>
                <configuration>
                    <createDependencyReducedPom>false</createDependencyReducedPom>
                </configuration>
                <executions>
                    <execution>
                        <phase>package</phase>
                        <goals>
                            <goal>shade</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>

        </plugins>
    </build>

</project>

ソースコードダウンロード

上記サンプルのフルソースコードは以下にあります
https://github.com/riversun/aws_lambda_example_basic_client.git

Eclipseでcloneする場合
File>Import>Git>Projects from Git>Clone URI
https://github.com/riversun/aws_lambda_example_basic_client.git を指定
import as general projectを選択してインポート
– インポート後に、プロジェクト上を右クリックしたメニューでConfigure>Convert to Maven projectを実行

これでEclipse上でMavenプロジェクトとなるので、以後取り扱いが楽になります。

mavenをつかってアップロード用のjarを作成する

  • (1)コマンドラインでjarを作る場合

pom.xmlのあるディレクトリで以下のコマンドを実行します

mvn packgage
  • (2)Eclipse上でjarを作る場合

 Step 1. 右クリックメニューでRun As>Maven buildを選択する
 

 Step 2. Edit configurationsダイアログで、Goalspackage shade:shade と入力しRun
 

(1)(2)いずれの場合でも、/target以下にすべての依存コード・ライブラリを含んだ mylambda-1.0.0.jarが生成されます。

これがAWS Lambdaアップロード用のjarファイルとなります。

3. jarファイルをアップロードしてAWS Lambdaに登録する

AWSコンソールからLambda関数を登録する手順をみていきます

(1)AWS Lambdaを開く
Lambdaサービスを開きます。
img01.png

(2)新しいLambda関数を作る
まだLambda関数をつくっていない場合は以下のような画面になります。

関数の作成(Create function)をクリックします。

img02.png

(3)Lambda関数を[一から作成]する

以下のように設計図(blue print)一覧から選択する画面が出ますが、一から作成(Author from scratch)を選択します。

img03.png

(4)基本的情報画面で名前、ロールを設定する

ここでは、myFunctionという名前のLambda関数をつくっていきます

  • 名前(Name)は「myFunction
  • ロール(Role)は「テンプレートから新しいロールを作成(Create new role from template)
  • ロール名(Role name)は「myRole

入力できたら、関数の作成(Create function)をクリックします

img04.png

(5) jarファイルをアップロードする

img06.png

STEP 1
まず、画面上部にある、ARNを確認しておきます
画面に表示されている、arn:aws:lambda:ap-northeast-1:000000000000:function:myFunction部分をメモしておきます

ご存知ARN(Amazon Resource Name)はAWS リソースを一意に識別するためのもので、Lambda関数実行時にその特定のために使います。

STEP 2
ランタイム(runtime)からJava8を選択する

STEP 3
ハンドラ(Handler)にlambda.cloud.MyLambdaと入力する
ハンドラ名は、パッケージ名.クラス名::メソッド名 のフォーマットで記述します。
さきほど作ったコードにあわせて、パッケージ名が、lambda.cloud、クラス名がMyLambdalambda.cloud.MyLambdaを入力しています。この例では、メソッド名は省略しても動作します。

STEP 4
アップロード(upload)をクリックしてさきほど作ったmylambda-1.0.0.jarをアップロードします。

はい、ここまでで、さきほど自作したLambda関数がAWS Lambdaに登録されました。

4. AWSコンソール上でテストする

アップロードが終わったら、コンソール上からテストしてみます。

(1)テスト用イベントの準備

Lambda関数は 何らかのイベントをトリガーにして起動する という考え方があり、たとえば、S3バケットにオブジェクトが作成されたというイベントをトリガーとして、Lambda関数を発火させる、という使い方があります。

そこで、Lambda関数を実行するための入力のことをイベントと呼びます。

ここでは、イベントをコンソール上から発火させ、Lambda関数の動作を確認します。

画面上部にある、テストイベントの設定(Configure test events)を選択します。

img07.png

すると以下のように、テストイベントの設定(Configure test events)画面が開きますので、ここでテスト用のイベントを作成します。

image08b.jpg

イベント名(Event name)MyEventとして、その下にあるエディットボックスはLambda関数をリクエストするときの本文です。

さきほどつくったPOJOで入出力するLambda関数は、実際の呼び出しではPOJOが自動的にJSONにマップされ、JSONで入出力されます。

そこで以下のようにJSON形式でイベントの本文を入力します

{
  "firstName": "john",
  "lastName": "doe"
}

入力したら、画面下にある作成を押します。

(2)テスト用イベントの実行

いま作成したMyEventを早速実行します。画面右上のテストを押します。

img09.png

ちなみに、「ハンドラーで指定されたファイル名がデプロイパッケージのファイル名と一致しないため、Lambda 関数 「myFunction」はインラインで編集できません。」というメッセージが表示されても気にしなくてOkです。
Lambdaコンソールでは、Java、C# などのコンパイル済み言語のインラインエディタは提供されていません。

(3)実行結果を確認する

テストを押してしばらくすると、結果画面が表示されます。

成功のときは、実行結果: 成功(Execution result:succeeded)と表示されます。その下の▼詳細(details)を押して展開すると結果の詳細を確認できます。

img10.png

Lambda関数の出力も、さきほどのPOJO Outputクラスが以下のようなJSONに変換されます。

入出力用のPOJO
public static class Input {
    public String firstName;
    public String lastName;
  }

  public static class Output {
    public Input in;
    public String fullName;

  }
実行結果
{
  "in": {
    "firstName": "john",
    "lastName": "doe"
  },
  "fullName": "john_doe"
}

5. ローカルJavaから、AWS Lambda関数を呼び出す

さきほど作ったLambda関数 myFunction をJavaプログラムから呼び出します。

ライブラリを読み込む

JavaからAWS Lambda関数をたたく場合には、aws-java-sdk-lambdaライブラリを追加します。mavenを使う場合は、以下を追加します

maven
<dependency>
    <groupId>com.amazonaws</groupId>
    <artifactId>aws-java-sdk-lambda</artifactId>
    <version>1.11.210</version>
</dependency>

Javaクライアント

コードは以下のようになります。(各種設定値はダミーです)

Javaクライアント
public class ExampleLambdaClient {

  public static void main(String[] args) {
    ExampleLambdaClient client = new ExampleLambdaClient();
    client.invokeLambdaFunction();

  }

  private void invokeLambdaFunction() {

    final String AWS_ACCESS_KEY_ID = "ANMNRR35KPTR7PLB3C7D";
    final String AWS_SECRET_ACCESS_KEY = "UKA6EsKY25LJQBEpUvXyYkj8aWKEDnynEZigVPhz";

    AWSCredentials credentials = new BasicAWSCredentials(AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY);

    // ARN
    String functionName = "arn:aws:lambda:ap-northeast-1:000000000000:function:myFunction";

    String inputJSON = "{"firstName":"john","lastName": "doe"}";

    InvokeRequest lmbRequest = new InvokeRequest()
        .withFunctionName(functionName)
        .withPayload(inputJSON);

    lmbRequest.setInvocationType(InvocationType.RequestResponse);

    AWSLambda lambda = AWSLambdaClientBuilder.standard()
        .withRegion(Regions.AP_NORTHEAST_1)
        .withCredentials(new AWSStaticCredentialsProvider(credentials)).build();

    InvokeResult lmbResult = lambda.invoke(lmbRequest);

    String resultJSON = new String(lmbResult.getPayload().array(), Charset.forName("UTF-8"));

    System.out.println(resultJSON);

  }
}

AWSCredentials credentials = new BasicAWSCredentials(AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY);
  • アクセスキーID(AWS_ACCESS_KEY_ID)とシークレットアクセスキー(AWS_SECRET_ACCESS_KEY)をつかってcredentialsをつくります。

InvokeRequest lmbRequest = new InvokeRequest()
        .withFunctionName(functionName)
        .withPayload(inputJSON);
  • リクエストを生成します。#withFunctionNameでは関数名を指定します。ARN(arn:aws:lambda:ap-northeast-1:000000000000:function:myFunction)を指定するか、関数名(myFunction)を指定します。ここではARNを指定しました

  • #withPayloadでリクエストの本文(JSON)を指定します。
    ここではJSON文字列 “{“firstName”:”john”,”lastName”: “doe”}”;を指定してます。


lmbRequest.setInvocationType(InvocationType.RequestResponse);
  • 呼び出しタイプ(invocation type)を指定します
    ここでは InvocationType.RequestResponse を指定しています。これでRequestResponse型の呼び出しタイプになります。RequestResponseにしておくと、処理結果を受け取ることができます。

InvokeResult lmbResult = lambda.invoke(lmbRequest);
String resultJSON = new String(lmbResult.getPayload().array(), Charset.forName("UTF-8"));
  • Lambda関数を呼び出して、結果(JSON)を受け取ります。

Javaクライアントのソースコード

クライアント側のフルソースコードは以下においてあります
https://github.com/riversun/aws_lambda_example_basic_client.git

まとめ

  • Javaでも比較的簡単にAWS lambdaの関数を記述できました
  • Javaのクライアントから直接 Lambda関数を呼び出せました

サンプルはEclipse(Oxygen)で作成しましたが、特段プラグインなど必要ありません
(AWS SDK Plugin for Eclipseも不要。あれば、もっと手軽になりますが)

  • 後編では、Lambda関数およびJavaクライアント側の同期/非同期についてや、API Gatewayについて書きます。

おまけ

  • 2009年頃ですがコードだけ書いたら後はおまかせ、課金は使った分だけというサービスの元祖「Google App Engine (GAE)」が登場したときは衝撃をうけました。独特の制約が多かったものの、とてもお世話になりました。

  • 今度はその元祖!?が、Google Cloud Functionsを投入しています。今現在はβで実績はこれからだとおもいますが、今後はそちらも選択肢に入ってきそうです。

続きを読む

AWSのS3サービスにMavenリポジトリを構築

始めに

Apache Mavenを利用する場合、インハウスリポジトリを構築すると便利なので、これまでWebDAVが使えるWebサーバに環境構築していました。
AWSではS3に構築することが出来るので、費用面からしても断然有利なので、今回はこれを使ってみましょう。

準備環境

準備した環境は以下の通り。
Windows7 Pro
Java 1.8
Maven 3.3.3
Eclipse4.6
Spring Boot 1.5.6.RELEASE(デモ用)

AWS作業

AWSは既存のアカウントでも良いですが、今回はセキュリティ対策のため、S3にアクセス可能なアカウントを用意しました。
アクセスポリシーは、こんな感じで良いと思います。
your-bucketのところは、各自で書き換えてください。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "s3:ListAllMyBuckets"
            ],
            "Resource": "arn:aws:s3:::*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "s3:*"
            ],
            "Resource": [
                "arn:aws:s3:::your-bucket",
                "arn:aws:s3:::your-bucket/*"
            ]
        }
    ]
}

Java&Eclipse作業

適当なMavenプロジェクトを作成します。
私はSpring Bootプロジェクトで試しました。
以下は抜粋ですので、必要に応じて書き換えてください。

pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <!-- (省略) -->

  <repositories>
    <repository>
      <id>s3-repos</id>
      <name>AWS S3 Repository</name>
      <url>s3://your-bucket/</url>
    </repository>
  </repositories>

  <distributionManagement>
    <repository>
      <id>aws-snapshot</id>
      <name>AWS S3 Repository</name>
      <url>s3://your-bucket/snapshot</url>
    </repository>
  </distributionManagement>

  <dependencies>
    <dependency>
  <!-- (省略) -->
    </dependency>
  </dependencies>


  <build>
    <plugins>
      <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
      </plugin>
      <!-- Windows環境におけるJunit実行時の文字化け対応 -->
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-surefire-plugin</artifactId>
        <configuration>
          <junitArtifactName>junit:junit</junitArtifactName>
          <encoding>UTF-8</encoding>
          <inputEncoding>UTF-8</inputEncoding>
          <outputEncoding>UTF-8</outputEncoding>
          <argLine>-Dfile.encoding=UTF-8</argLine>
          <!-- <skipTests>true</skipTests> -->
        </configuration>
      </plugin>
    </plugins>
    <extensions>
      <extension>
        <groupId>org.springframework.build</groupId>
        <artifactId>aws-maven</artifactId>
        <version>5.0.0.RELEASE</version>
      </extension>
    </extensions>
  </build>
</project>

あとは、Mavenコマンドで使用するユーザ設定を行います。
Eclipseの場合は、ウィンドウ→設定→Maven→ユーザー設定か、Maven実行時にユーザ設定を指定します。
Maven実行時のパラメータ設定でも良いかと思います。
S3へのアクセスにプロキシ設定が必要な場合は、プロキシサーバの設定も適宜追加します。

setting.xml
<?xml version="1.0" encoding="UTF-8"?>
<settings xmlns="http://maven.apache.org/SETTINGS/1.1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.1.0 http://maven.apache.org/xsd/settings-1.1.0.xsd">
  <proxies>
    <proxy>
      <id>http_proxy</id>
      <active>true</active>
      <protocol>http</protocol>
      <host>xx.xx.xx.xx</host>
      <port>xx</port>
    </proxy>
    <proxy>
      <id>https_proxy</id>
      <active>true</active>
      <protocol>https</protocol>
      <host>xx.xx.xx.xx</host>
      <port>xx</port>
    </proxy>
    <proxy>
      <id>s3_proxy</id>
      <active>true</active>
      <protocol>s3</protocol>
      <host>xx.xx.xx.xx</host>
      <port>xx</port>
    </proxy>
  </proxies>
  <servers>
    <server>
      <id>aws-release</id>
      <username>アクセスキーID</username>
      <password>シークレットアクセスキー</password>
    </server>
    <server>
      <id>aws-snapshot</id>
      <username>アクセスキーID</username>
      <password>シークレットアクセスキー</password>
    </server>
  </servers>
</settings>

実行結果

mvn deplyコマンドを実行して、S3に登録を行います。
snapshotかreleaseかは、アクセスするリポジトリ名を切り替えればOKかと思います。
他にいいやり方があるとは思いますが、とりあえずこれで。

[INFO] --- maven-deploy-plugin:2.8.2:deploy (default-deploy) @ ews ---
[INFO] Uploading: s3://your-bucket/snapshot/com/example/1.0.0/sample-1.0.0.jar
[INFO] Configuring Proxy. Proxy Host: xx.xx.xx.xx Proxy Port: xx
[INFO] Uploaded: s3://your-bucket/snapshot/com/example/1.0.0/sample-1.0.0.jar (2000 KB at 1000.0 KB/sec)
[INFO] Uploading: s3://your-bucket/snapshot/com/example/1.0.0/sample-1.0.0.pom
[INFO] Uploaded: s3://your-bucket/snapshot/com/example/1.0.0/sample-1.0.0.pom (5 KB at 1.0 KB/sec)
[INFO] Downloading: s3://your-bucket/snapshot/com/example/maven-metadata.xml
[INFO] Uploading: s3://your-bucket/snapshot/com/example/maven-metadata.xml
[INFO] Uploaded: s3://your-bucket/snapshot/com/example/maven-metadata.xml (300 B at 0.1 KB/sec)

考察

インターネットで調べたものの、手順がよく分からなかったので、実際にやってみました。
他のプロジェクトからうまく引っ張れるかどうか試していませんが、jarファイルを一般公開したくないなぁって言う場合には、かなり使えるんじゃ無いでしょうか。

続きを読む

Lambda(Java)を使ってLINE Message APIとやり取りする

使用サービス

  • AWS API Gateway
  • AWS Lambda
  • LINE Message API

開発環境等

  • Java 1.8
  • Eclipse
  • Maven

目的

  • Line Message APIとの話の仕方を知りたい
  • AWS Lambdaの使い方を知りたい。

この記事で説明を省いていること

  • LINEのアカウント開設やWebhook URLの設定

  • API Gatewayの設定の仕方

  • Line Message API側で”Server IP Whitelist”を設定し、AWS側でも認証させる仕組み

  • リクエストの送信元がLINEであることを確認するために署名検証

  • Lineで設定されるChannel Access TokenはLambdaの環境変数から取得する仕組み

手順

1. LINE Message API

  1. アカウント開設
  2. Webhook URLの設定

2. AWS API Gateway

  1. リソース作成
  2. 作成したリソースとLambda Functionを連携

3. Lambda Function

3.1. JSONをPOJOで受け取るためのInputクラス作成
3.2. Outputクラス作成
3.3. InputからreplyToken, text取得し、Outputに格納。合わせてReply Messageに必要な情報もセット。
3.4. OutputをJSONに変換
3.5. リクエストを生成し送信
3.6. 全体コード

3.1. JSONをPOJOで受け取るためのInputクラス作成

下記のJSONオブジェクトが送信されるので、Inputクラスもそれに合わせて構築する

{
  "events": [
    {
      "replyToken": "nHuyWiB7yP5Zw52FIkcQobQuGDXCTA",
      "type": "message",
      "timestamp": 1462629479859,
      "source": {
        "type": "user",
        "userId": "U206d25c2ea6bd87c17655609a1c37cb8"
      },
      "message": {
        "id": "325708",
        "type": "text",
        "text": "Hello, world"
      }
    },
    {
      "replyToken": "nHuyWiB7yP5Zw52FIkcQobQuGDXCTA",
      "type": "follow",
      "timestamp": 1462629479859,
      "source": {
        "type": "user",
        "userId": "U206d25c2ea6bd87c17655609a1c37cb8"
      }
    }
  ]
}
Input.java
package jp.linebot;

import java.util.List;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Input {

    private Events[] events;

}
Events.java
package jp.linebot;

import org.joda.time.DateTime;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Events {
    private String replyToken;
    private String type;
    private Long timestamp;
    private Source source;
    private Message message;    
}
Source.java
package jp.linebot;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Source {
    private String type;
    private String userId;
}
Message.java
package jp.linebot;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Message {
    private String id;
    private String type;
    private String text;    
}

3.2. Outputクラス作成

Output.java
package jp.linebot;

import java.util.ArrayList;
import java.util.List;

import lombok.Data;

@Data
public class Output {

    private String replyToken;
    private List<Messages> messages = new ArrayList<>();
}
Messages.java
package jp.linebot;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Messages {

    private String type;
    private String text;
}

3.3. InputからreplyToken, text取得し、Outputに格納。合わせてReply Messageに必要な情報もセット。

Output output = new Output();
output.setReplyToken(input.getEvents()[0].getReplyToken());
Messages outMessage = new Messages();
outMessage.setType("text");
outMessage.setText(input.getEvents()[0].getMessage().getText() + "?");
output.getMessages().add(outMessage);

3.4. OutputをJSONに変換

Gson gson = new Gson();
context.getLogger().log(gson.toJson(output));

3.5. リクエストを生成し送信

httpPost = new HttpPost("https://api.line.me/v2/bot/message/reply");
httpPost.setHeader("Content-Type", "application/json");
httpPost.setHeader("Authorization", "Bearer " + "{Channel Access Token}");
StringEntity entity = new StringEntity(gson.toJson(output), StandardCharsets.UTF_8);
httpPost.setEntity(entity);

try (CloseableHttpClient client = HttpClients.createDefault();
     CloseableHttpResponse resp = client.execute(httpPost);
     BufferedReader br = new BufferedReader(new InputStreamReader(resp.getEntity().getContent(), StandardCharsets.UTF_8)))
{
        int statusCode = resp.getStatusLine().getStatusCode();
        switch (statusCode) {
        case 200:
            br.readLine();
            break;
        default:
        }
    } catch (final ClientProtocolException e) {
    } catch (final IOException e) {
}
return null;

3.6. 全体コード

LambdaFunctionHandler.java
package jp.linebot;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.nio.charset.StandardCharsets;

import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;

import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.RequestHandler;
import com.google.gson.Gson;

public class LambdaFunctionHandler implements RequestHandler<Input, Object> {

    @Override
    public Object handleRequest(Input input, Context context) {
        // TODO Auto-generated method stub

        context.getLogger().log("token : " + input.getEvents()[0].getReplyToken());
        context.getLogger().log("text : " + input.getEvents()[0].getMessage().getText());

        Output output = new Output();
        output.setReplyToken(input.getEvents()[0].getReplyToken());
        Messages outMessage = new Messages();
        outMessage.setType("text");
        outMessage.setText(input.getEvents()[0].getMessage().getText() + "?");
        output.getMessages().add(outMessage);

        HttpPost httpPost = new HttpPost("https://api.line.me/v2/bot/message/reply");
        httpPost.setHeader("Content-Type", "application/json");
        httpPost.setHeader("Authorization", "Bearer " + "{Channel Access Token}");

        Gson gson = new Gson();
        context.getLogger().log(gson.toJson(output));
        StringEntity entity = new StringEntity(gson.toJson(output), StandardCharsets.UTF_8);
        httpPost.setEntity(entity);
        try (CloseableHttpClient client = HttpClients.createDefault();
                CloseableHttpResponse resp = client.execute(httpPost);
                BufferedReader br = new BufferedReader(new InputStreamReader(resp.getEntity().getContent(), StandardCharsets.UTF_8)))
        {
            int statusCode = resp.getStatusLine().getStatusCode();
            switch (statusCode) {
            case 200:
                br.readLine();
                break;
            default:
            }
        } catch (final ClientProtocolException e) {
        } catch (final IOException e) {
        }
        return null;
    }

}

参考リンク

続きを読む

Apache Sparkによる大規模データの分散処理による機械学習(回帰分析) by Amazon EMR

EMRでApache Sparkを使用するに当たって、必要なデータを処理するためのコードと入力データを用意するだけで、面倒な環境構築を行わず、わずかばかりの設定を加えるだけですぐに使用することができます。

ここでは、最初にScalaによるEMR上でのSpark処理、次に他のサービスのトリガーをきっかけに動くJavaによるLambdaでEMRを呼び出す手順を注意点も含めて詳細なメモを書きます。

Scalaによる回帰分析を行う実行ファイルの作成

環境構築

scalaをインストール。

$ cd /tmp
$ curl -O http://downloads.typesafe.com/scala/2.11.8/scala-2.11.8.tgz
$ tar xzf scala-2.11.8.tgz
$ mkdir -p /usr/local/src
$ mv scala-2.11.8 /usr/local/src/scala

$ export PATH=$PATH:/usr/local/src/scala/bin
$ export SCALA_HOME=/usr/local/src/scala

$ which scala

Scalaのパッケージマネージャであるsbtをインストール。

$ brew install sbt
$ which sbt

プロジェクトの作成

プロジェクトを置くディレクトリを作成。

mkdir SparkExampleApp

ビルドの設定ファイルの作成。

vim build.sbt

以下のように記述する。

name := "Spark Sample Project"
version := "1.0"
scalaVersion := "2.11.8"

libraryDependencies ++= Seq(
  "org.apache.spark" %% "spark-core" % "2.0.0",
  "org.apache.spark" %% "spark-mllib" % "2.0.0",
  "org.apache.spark" %% "spark-sql" % "2.0.0",
  "com.databricks" %% "spark-csv" % "1.4.0"
)

実行ファイルの作成。

mkdir -p src/main/scala
vim src/main/scala/SparkExampleApp.scala

SparkExampleApp.scalaは以下のものを使用。

import org.apache.spark.sql.SparkSession
import org.apache.spark.ml.{Pipeline, PipelineModel}
import org.apache.spark.ml.feature._
import org.apache.spark.ml.regression.LinearRegression

object SparkExampleApp {

  def main(args: Array[String]) {
    val spark = SparkSession
      .builder()
      .appName("Spark Sample App")
      .getOrCreate()

    import spark.implicits._

    spark.sparkContext.hadoopConfiguration.set("fs.s3n.awsAccessKeyId", "AKIA****************")
    spark.sparkContext.hadoopConfiguration.set("fs.s3n.awsSecretAccessKey", "****************************************")

    val filePath = args(0)
    val df = spark.sqlContext.read
        .format("com.databricks.spark.csv")
        .option("header", "true")
        .option("inferSchema", "true")
        .load(filePath)

    val assembler = new VectorAssembler()
      .setInputCols(Array("x"))
      .setOutputCol("features")

    val polynomialExpansion = new PolynomialExpansion()
      .setInputCol(assembler.getOutputCol)
      .setOutputCol("polyFeatures")
      .setDegree(4)

    val linearRegression = new LinearRegression()
      .setLabelCol("y")
      .setFeaturesCol(polynomialExpansion.getOutputCol)
      .setMaxIter(100)
      .setRegParam(0.0)

    val pipeline = new Pipeline()
      .setStages(Array(assembler, polynomialExpansion, linearRegression))


    val Array(trainingData, testData) = df.randomSplit(Array(0.7, 0.3))
    val model = pipeline.fit(trainingData)

    val outputFilePath = args(1)
    model.transform(testData)
      .select("x", "prediction")
      .write
      .format("com.databricks.spark.csv")
      .option("header", "false")
      .save(outputFilePath)

  }
}

jarファイルの作成のために以下のコマンドを実行。

sbt package

生成されたtarget/scala-2.11/spark-sample-project_2.11-1.0.jarをS3のsample-bucket/sample-emr/srcにアップロードしておく。

Apache Sparkの設定

http://spark.apache.org/downloads.html
上のURL上で以下のように入力してApache Sparkのデータをダウンロードしてくる。

  1. Choose a Spark release: 2.0.1(Oct 03 2016)
  2. Choose a package type: Pre-build for Hadoop 2.7 and later
  3. Choose a download type: Direct download
  4. Download Spark: spark-2.0.1-bin-hadoop2.7.tgz

ダウンロードしたファイルに移動する。

cd ~/Downloads/spark-2.0.1-bin-hadoop2.7

conf以下にspark-defaults.confというファイルを以下の内容で作成する。

vim conf/spark-defaults.conf
spark-defaults.conf
spark.jars.packages  com.amazonaws:aws-java-sdk:1.7.4,org.apache.hadoop:hadoop-aws:2.7.1

実験用データの作成

ruby -e 'puts "x,y";Range.new(0,5).step(0.05).each {|i| puts "#{i},#{Math.sin(i)+Random.rand(-0.3..0.3)}"}' > data.csv

sample.jpg

グラフの表示

brew install gnuplot
gnuplot
gnuplot> set datafile separator ","
gnuplot> set terminal jpeg
gnuplot> set output "sample.jpg"
gnuplot> plot "data.csv" every ::1
qlmanage -p sample.jpg

実行

S3にバケットを作成しておき、以下のようなバケットポリシーを作成しておく。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "GetObject",
            "Effect": "Allow",
            "Principal": "*",
            "Action": "s3:GetObject",
            "Resource": "arn:aws:s3:::sample-bucket/*"
        },
        {
            "Sid": "ListBucket",
            "Effect": "Allow",
            "Principal": "*",
            "Action": "s3:ListBucket",
            "Resource": "arn:aws:s3:::sample-bucket"
        }
    ]
}

Sparkの実行

./bin/spark-submit --class SparkExampleApp --master local ~/workspace/SparkExampleApp/target/scala-2.11/spark-sample-project_2.11-1.0.jar s3n://sample-bucket/sample-emr/data.csv s3n://sample-bucket/sample-emr/output.csv

実行後の結果は以下のようになる。

sample.jpg

EMRの実行

1.マネジメントコンソールからEMRを選択。
2.[クラスターを作成]を選択して以下のように記述する。

クラスター名: SampleCluster
ログ記録: チェック
S3フォルダー: s3://sample-bucket/sample-emr/logs
起動モード: ステップ実行

ステップタイプ: Spark アプリケーション

[設定]を選択し、ステップを追加の設定を行う。

3.ステップを追加で以下のように入力して、[追加]を選択。

名前: SparkApplication
デプロイモード: クラスター
Spark-submitオプション: –class SparkExampleApp
アプリケーションの場所: s3://sample-bucket/sample-emr/src/spark-sample-project_2.11-1.0.jar
引数: s3n://sample-bucket/sample-emr/data.csv s3n://sample-bucket/sample-emr/output/output.csv
失敗時の操作: クラスターを終了。

ベンダー: Amazon
リリース: emr-5.0.3
アプリケーション: Hadoop 2.7.3, Spark 2.0.1

インスタンスタイプ: m3.xlarge
インスタンス数: 2

アクセス権限: デフォルト
EMRロール: EMR_DefaultRole
EC2 インスタンスプロファイル: EMR_EC2_DefaultRole

4.[クラスターを作成]を選択。

5.デプロイモードはクライアントとクラスターのうちのクラスターを選択。
6.失敗時の操作は、次へ、キャンセルして待機、クラスターを終了からクラスターを終了を選択。

7.起動モードはクラスター、ステップ実行のうちのステップ実行を選択。
8.ステップタイプはストリーミングプログラミング、Hiveプログラム、Pigプログラム、Spark アプリケーション、カスタムJARからSpark アプリケーションを選択。

しばらくすると、EMRのクラスターが終了し、出力先にファイルが生成されていることが確認できる。

JavaによるLambdaからのEMR呼び出し

Eclipseでプロジェクト作成

Apache Mavenを利用してプロジェクト管理する場合。

1.File > New > Other…からMaven > Maven Project
デフォルト設定のまま進めるので、3回[Next]を選択。

Group Id: com.sample.lambda
Artifact Id: com.sapmle.lambda
Version: 0.0.1-SNAPSHOT
Package: com.sample.lambda

[Finish]を選択。

2.pom.xmlを選択してOverviewタブ中の以下を編集。
Name: lambda-java

3.Dependenciesタブを選択。
[Add…]を選択し、以下のように記述して、aws-lambda-java-core: 1.0.0aws-lambda-java-eventsaws-java-sdk-coreaws-java-sdk-emrを追加する。

Group Id: com.amazonaws
Artifact Id: aws-lambda-java-core
Version: 1.0.0
Group Id: com.amazonaws
Artifact Id: aws-lambda-java-events
Version: 1.0.0
Group Id: com.amazonaws
Artifact Id: aws-java-sdk-core
Version: 1.11.49
Group Id: com.amazonaws
Artifact Id: aws-java-sdk-emr
Version: 1.11.49

次に重要なのが

pom.xmlを右クリックして、Maven > Add Plugin を選択し、以下のように入力する。

Group Id: org.apache.maven.plugins
Artifact Id: maven-shade-plugin
バージョン: 2.3

プロジェクトをビルドするために、Package Exploreからcom.sample.lambdaを右クリックして、Run As > Maven Build を実行する。

作成したpom.xmlは以下のようになる。

pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>com.sample.lambda</groupId>
  <artifactId>com.sample.lambda</artifactId>
  <version>0.0.1-SNAPSHOT</version>
  <packaging>jar</packaging>

  <url>http://maven.apache.org</url>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  </properties>

  <dependencies>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>3.8.1</version>
      <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>com.amazonaws</groupId>
        <artifactId>aws-lambda-java-core</artifactId>
        <version>1.0.0</version>
    </dependency>
    <dependency>
        <groupId>com.amazonaws</groupId>
        <artifactId>aws-lambda-java-events</artifactId>
        <version>1.0.0</version>
    </dependency>
    <dependency>
        <groupId>com.amazonaws</groupId>
        <artifactId>aws-java-sdk-emr</artifactId>
        <version>1.11.49</version>
    </dependency>
    <dependency>
        <groupId>com.amazonaws</groupId>
        <artifactId>aws-java-sdk-core</artifactId>
        <version>1.11.49</version>
    </dependency>
  </dependencies>
  <name>lambda-java</name>
  <build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-shade-plugin</artifactId>
            <version>2.3</version>
        </plugin>
    </plugins>
  </build>
</project>

4.src/main/java中のcom.sample.lambdaのSampleCreateEMRClusterHandler.javaを以下のように作成する。

SampleCreateEMRClusterHandler.java
package com.sample.lambda;

import java.util.ArrayList;
import java.util.List;

import com.amazonaws.auth.AWSCredentialsProvider;
import com.amazonaws.auth.EnvironmentVariableCredentialsProvider;
import com.amazonaws.regions.Region;
import com.amazonaws.regions.Regions;

import com.amazonaws.services.elasticmapreduce.AmazonElasticMapReduceClient;
import com.amazonaws.services.elasticmapreduce.model.ActionOnFailure;
import com.amazonaws.services.elasticmapreduce.model.Application;
import com.amazonaws.services.elasticmapreduce.model.HadoopJarStepConfig;
import com.amazonaws.services.elasticmapreduce.model.InstanceGroupConfig;
import com.amazonaws.services.elasticmapreduce.model.InstanceRoleType;
import com.amazonaws.services.elasticmapreduce.model.JobFlowInstancesConfig;
import com.amazonaws.services.elasticmapreduce.model.RunJobFlowRequest;
import com.amazonaws.services.elasticmapreduce.model.RunJobFlowResult;
import com.amazonaws.services.elasticmapreduce.model.StepConfig;
import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.RequestHandler;

public class SampleCreateEMRClusterHandler implements RequestHandler<Object, String> {

    public String handleRequest(Object event, Context context) {
        AWSCredentialsProvider cp = new EnvironmentVariableCredentialsProvider();
        AmazonElasticMapReduceClient emr = new AmazonElasticMapReduceClient(cp);
        emr.setRegion(Region.getRegion(Regions.US_EAST_1));

        JobFlowInstancesConfig instanceConfig = new JobFlowInstancesConfig()
                .withKeepJobFlowAliveWhenNoSteps(false)
                .withInstanceGroups(buildInstanceGroupConfigs());

        RunJobFlowRequest request = new RunJobFlowRequest()
                .withName("SampleCluster")
                .withReleaseLabel("emr-5.0.3")
                .withLogUri("s3://sample-bucket/sample-emr/logs/")
                .withServiceRole("EMR_DefaultRole")
                .withJobFlowRole("EMR_EC2_DefaultRole")
                .withVisibleToAllUsers(true)
                .withInstances(instanceConfig)
                .withApplications(buildApplications())
                .withSteps(buildStepConfigs());

        RunJobFlowResult result = emr.runJobFlow(request);
        return "Process complete.";
    }

    private List<Application> buildApplications() {
        List<Application> apps = new ArrayList<Application>();
        apps.add(new Application().withName("Hadoop"));
        apps.add(new Application().withName("Spark"));
        return apps;
    }

    private List<InstanceGroupConfig> buildInstanceGroupConfigs() {
        List<InstanceGroupConfig> result = new ArrayList<InstanceGroupConfig>();
        InstanceGroupConfig masterInstance = new InstanceGroupConfig()
                .withName("MasterNode")
                .withInstanceRole(InstanceRoleType.MASTER)
                .withInstanceCount(1)
                .withInstanceType("m3.xlarge");
        result.add(masterInstance);

        InstanceGroupConfig coreInsetance = new InstanceGroupConfig()
                .withName("CoreNode")
                .withInstanceRole(InstanceRoleType.CORE)
                .withInstanceCount(1)
                .withInstanceType("m3.xlarge");
        result.add(coreInsetance);

        return result;
    }

    private List<StepConfig> buildStepConfigs() {
        List<StepConfig> result = new ArrayList<StepConfig>();

        final String[] args = {
                "spark-submit",
                "--deploy-mode", "cluster",
                "--class", "SparkExampleApp",
                "s3://sample-bucket/sample-emr/src/spark-sample-project_2.11-1.0.jar", 
                "s3n://sample-bucket/sample-emr/data.csv",
                "s3n://sample-bucket/sample-emr/output/output.csv"
        };

        final StepConfig sparkStep = new StepConfig()
                .withName("SparkProcess")
                .withActionOnFailure(ActionOnFailure.TERMINATE_CLUSTER)
                .withHadoopJarStep(new HadoopJarStepConfig()
                        .withJar("command-runner.jar")
                        .withArgs(args));
        result.add(sparkStep);

        return result;
    }

}

以上実行コードの完成。プロジェクトを再度ビルドする。

(追記) TerminalからのMavenによるビルド方法

Mavenをインストールしていない場合は以下の手順でインストールする。

Mavenを以下のURLからダウンロードしてくる。
http://maven.apache.org/download.cgi

mv apache-maven-3.3.9 /usr/local

.zshrcに以下を追加

export M3_HOME=/usr/local/apache-maven-3.3.9
M3=$M3_HOME/bin
export PATH=$M3:$PATH
source ~/.zshrc

プロジェクト直下にterminalで移動し、以下を実行する。

bashを使用している場合は、~/.zshrc~/.bashrcに置き換えて考える。

mvn package

target以下にcom.sapmle.lambda-0.0.1-SNAPSHOT.jarが生成される。こちらがLambdaにアップロードするものとなる。

IAMロールの作成

マネジメントコンソールからIdentity and Access Management
[ロール]から[新しいロールの作成]を選択。

手順 1: ロール名の設定

以下のように入力して[次のステップ]を選択。
ロール名: sampleRole

手順 2: ロールタイプの選択

AWS Lambdaを選択。

手順 3: 信頼性の確立

信頼関係はLambdaに設定されて飛ばされる。

手順 4: ポリシーのアタッチ

AmazonElasticMapReduceRoleを選択して、[次のステップ]を選択。

手順 5: 確認

内容を確認したら、[ロールの作成]を選択。

Lambdaの作成

マネジメントコンソールからLamdaを選択。
[Create a Lambda function]を選択。

Select blueprint

Blank functionを選択。

Configure triggers

ここでは何も設定しないが、必要に応じてLambdaを呼び出す元となるサービスを指定する。
[Next]を選択。

Configure function

Name: SampleLambda
Runtime*: Java8

Code entry type: Upload a .ZIP or JAR file
Function package: [先ほど作成したJARファイルを指定]

Handler*: com.sample.lambda.SampleCreateEMRClusterHandler::handleRequest
Role*: Choose an existing role
Existing role: sampleRole

[Next]を選択。

Review

最後に設定内容を確認して、実際にTestを実行してみてエラーが出ないか確認してみる。

SNSによるLambdaのトリガー処理

SNSから先程作成したLambdaをトリガーするTopicを生成する。
ここでは、Node.jsから呼び出す例を取り上げます。

node.js
var AWS = require('aws-sdk');
AWS.config.update({
        accessKeyId: 'AKIAXXXXXXXXXXXXXXXX',
        secretAccessKey: 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX',
        region: 'us-east-1'
});

publishSNSTopic();

const publishSNSTopic = () => {
    var sns = new AWS.SNS({
        apiVersion: "2010-03-31",
        region: "us-east-1"
    });

    sns.publish({
        Message: 'Start Job by triggering lambda for EMR',
        Subject: "Trigger Lambda for EMR",
        TopicArn: "arn:aws:sns:us-east-1:xxxxxxxxxxxx:<トピック名>"
    }, function(err, data) {
        if (err) console.log("Failure to publish topic by SNS");
    });
}
$ node index.js

注意点

  • URI schemeはs3ではなくs3nを指定。
    s3: S3ブロックファイルシステム
    s3n: S3ネイティブファイルシステム

以前はs3nを使っていたが、今はs3を推奨。
サービス提供者のドキュメントを読んでそれに合わせることが大事。

  • EMRでログ記録はチェックするようにする
    エラーが出た時のデバッグの根拠になるから。

  • EMR: 出力先にファイルがあるとエラーになる。(output.csv/)

  • com/amazonaws/auth/AWSCredentialsProvider: class java.lang.NoClassDefFoundError

maven-shade-pluginを忘れずに追加することが重要。
https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/java-create-jar-pkg-maven-and-eclipse.html
https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/java-create-jar-pkg-maven-no-ide.html

  • java.lang.ClassNotFoundException: com.amazonaws.ClientConfigurationFactory

aws-java-sdk-coreaws-java-sdk-emrのバージョンを1.11.49に統一

http://stackoverflow.com/questions/36796268/java-lang-classnotfoundexception-com-amazonaws-clientconfigurationfactory

  • spark-submitコマンド実行時の注意点
spark-submit --deploy-mode cluster --class SparkExampleApp s3n://sample-bucket/sample-emr/src/spark-sample-project_2.11-1.0.jar s3n://sample-bucket/sample-emr/data.csv s3n://sample-bucket/sample-emr/output/output.csv

じゃなくて

spark-submit --deploy-mode cluster --class SparkExampleApp s3://sample-bucket/sample-emr/src/spark-sample-project_2.11-1.0.jar s3n://sample-bucket/sample-emr/data.csv s3n://sample-bucket/sample-emr/output/output.csv

原因は、EMRでS3を呼び出すときに、昔はs3nを使っており、今はs3を推奨しており、ScalaのAWSをライブラリはs3nで認識するのに対して、EMRはs3じゃないと受け付けないことが原因。

参考

Scalaをsbtでビルド
http://qiita.com/kanuma1984/items/6f599c815cc8f9232228

EMRでSpark
http://www.atmarkit.co.jp/ait/articles/1609/27/news018.html

JavaによるLambda作成
http://docs.aws.amazon.com/ja_jp/ElasticMapReduce/latest/DeveloperGuide/calling-emr-with-java-sdk.html

Lambda+Java
http://qiita.com/Keisuke69/items/fc39a2f464d14480a432

Lambda+EMR
http://qiita.com/yskazuma/items/b67b1f3f8c39a7a19051
http://qiita.com/RyujiKawazoe/items/534f4b069ebea2f7d26c

続きを読む

JavaとAmazonProductAdvertisingAPI, CurrencyAPIで世界各国のAmazonの商品価格比較ツール作ってみた(2017/01/29)

hello world!

自己紹介

はじめましてalpacatomと申します。M1の院生です。
やっと論文の佳境を超えて、少しばかり時間が出来たので、貴重な土日を犠牲に初投稿させて頂きます。
新参者ゆえ、どうぞ温かく見守ってください。改善案や意見などして頂けると助かります。
今回は、Amazon Product Advertising APIとCurrency APIを使って、6カ国のAmazonでの商品の価格を比較するツールを作りました。もちろん送料込みです。

Note: 使えるようになるまでの準備が相当面倒くさいので、覚悟しておいてください。

動機 (アイスブレイク)

「みんな大好き『ビッグバン★セオリー』のBlu-rayディスクが欲しい」 → 「でも(院生は)お金がない」 → 「あれ!?日本より米国Amazonの方が安くね?」 ← 今ココ
と至り、結果よく分からなかった上、国ごとのAmazonでの値段が違うことを知ったので作りました。

Tips: ビッグバン★セオリーとは?
超面白い海外コメディドラマ。研究者を目指している人もそうでない人にもオススメ出来る作品で、特に院生の方は必見です。
ちなみに私はSeason1-6まで二週しました。(Netflix最高!)

あと後々には、Superdry(極度乾燥)しなさい のバッグの購入を考えている…などが主な動機です。

参考: Superdry (極度乾燥)しなさい

Currency API

本題に入ります。
為替レートの情報を得るためのAPIで、登録すれば使えるようになります。
今回はFreeのものを使用しています。
https://currencylayer.com/
ここでCurrencylayerのAPIキーを取得します。

Amazon Product Advertising API

以下の記事を参考にさせて頂きました。
http://qiita.com/someone7140/items/df3514b3d139c1b94ef9
https://www.panzee.biz/archives/6222
なので、このセクションの作業をする場合には参考にすると捗ります。

Amazonアソシエイトタグ

ここが正念場です。
比較したい国の登録を済ませ、Associateタグを手に入れます。(今回は日本をベースとしているので、日本は必須です。)
StackOverflowのとある記事によりますと、各国で登録しないと使えない的なことが投稿されていたため、必要な国”全部”で登録します。
https://affiliate-program.amazon.com/
https://associates.amazon.ca/
https://affiliate-program.amazon.co.uk/
https://partnernet.amazon.de/
https://partenaires.amazon.fr/
https://affiliate.amazon.co.jp/

AWSアクセスキーとシークレットキー

  • 上記サイトの説明を読みつつアクセスキーとシークレットキーを取得する。
    色々試しましたが、何故か日本のキーだけ別にしないと動かなかった(むしろ2つだけで動く)ので、日本とそれ以外の二つ取得しました。
    これで準備完了です。

使い方

Eclipseで開発したので、Eclipseを使っている方はGithub (https://github.com/alpacatom/ComparingPrices-via-AmazonAPI) から引っ張ってきたプロジェクトをimportし、上記で取得した鍵を(Currency.javaとSearchAnItem.javaにて)設定すれば使えます。
検索には、必要なパラメータがいくつかあります。
– ASIN番号 (参考 https://www.amazon.com/gp/help/customer/display.html?nodeId=200202190#find_asins)
– 重さ(kg) (重さによって海外からの発送料が変わるため、デフォルトは1kg以下)
– 探したい品物のジャンル(例. BooksやDVDなど。詳しくはShipmentCharge-*.txtの1列目を参考にしてください。)
が必要になります。

mainクラス

以下にmain関数だけ載せます。流れとしてはCSVファイルから送料表を引っ張り、CurrencyAPIとAmazonAPIを叩いて為替レートと商品情報の価格を取得します。
詳しくはGithub (https://github.com/alpacatom/ComparingPrices-via-AmazonAPI) にて

AmazonAPI.java
    public static void main(String[] args) {
        // Initializing
        ItemData arr[] = new ItemData[ItemData.COUNTRY_NUM];
        for(int i=0;i<ItemData.COUNTRY_NUM;i++){
            arr[i] = new ItemData();
        }
        String line[] = new String[ShipmentCharge.CSV_MAX_LINE_SIZE];
        ShipmentCharge SC[][] = new ShipmentCharge[ItemData.COUNTRY_NUM][ShipmentCharge.CSV_MAX_LINE_SIZE];
        // Get Shipment Charges
        for(int j=0;j<ItemData.COUNTRY_NUM;j++){
            line = ItemData.ReadFile(ShipmentCharge.ShipmentTXT[j]);
            String stmp[] = new String[ShipmentCharge.CSV_MAX_COLUM_SIZE];
            int k=0;
            while(line[k]!=null){
                stmp = MyParser.parseCSV(line[k]);
                SC[j][k] = ShipmentCharge.MakeTable(SC[j][k], stmp, j);
                k++;
            }
        }
        // Calculate the lowest Shipment Charges each countires.
        Double[] dtmp = new Double[6];
        dtmp = ShipmentCharge.LowestCalcSC(SC,ItemData.category);
        // Get Currencies via currencylayer
        String json = Currency.GetCurrencies();
        Double CurrenciesRate[] = new Double[ItemData.COUNTRY_NUM];
        CurrenciesRate = MyParser.getCurrencies(json);
        // Get Price and Country via Amazon Product API
        String xml[] = new String[ItemData.COUNTRY_NUM];
        final String tag[] = {"CurrencyCode","FormattedPrice"};

        for(int i=0;i<ItemData.COUNTRY_NUM;i++){
            arr[i].setCountry(ItemData.Countries[i]);
            xml[i] = SearchAnItem.SearchByID(ItemData.getItemID(), i);
            arr[i].setShitpment(dtmp[i]);
            arr[i].setCurrenciesCode(MyParser.getTagElmFromXML(xml[i],tag[0]));
            arr[i].setPrice( MyParser.Convert2Double(MyParser.getTagElmFromXML(xml[i],tag[1])));
            arr[i].setCurrenciesRate(CurrenciesRate[i]);
            arr[i].PrintAll();
        }
    }

実行結果

全部ひっくるめて計算した結果をUSDドルで表示します。なのでTOTAL:が低いほど”安い”ということを示しています。
以下がビッグバン★セオリーSeason1-9のBDの値段(送料込)です。お分かりいただけだたろうか、日本語不要(英語字幕は有)であれば 115*(Max-min) = 2990円ほど得するのである。
ad1b905491ba78d3da57cbb32752f150.png

所感

ここまで読んでくださってありがとうございました、また何か作ったら投稿しようと思います。

得たもの

  • Eclipseの使い方 (今回はじめてだった)
  • Javaのコーディング (講義以外で使ったことなかった)
  • いろんなAPI・外部ライブラリに触れた (HTTPリクエストの送り方やJSONおよびXMLからの要素抽出など)
  • 正規表現の使い方 (汚いけど初めて自分で作った)

失ったもの

  • 土日 (ここ重要)

やり残したこと

  • デバッグと例外処理はほぼやってません。(稀にHTTPリクエストを送るところでコケる? 時間があったら直したい。)
  • 複数の品物の注文に対応させこと。 今回は単品注文しか想定しておらず、複数注文する場合の計算が変わるため
  • URLからASIN番号を引っ張れるようにすること。いちいちASIN番号を探すのがめんどくさい。
  • 特急便の送料も選択可能にすること。時間とお金間でコスパがいいところを選びたい需要があるかも
  • ベースとなる国(現在は日本)も選択可能にすること。送料の規定が変わってしまったらまたやり直しなため。(値段を調べるのが面倒なので多分やりません。)

参考文献

Amazon Product API document
http://docs.aws.amazon.com/AWSECommerceService/latest/DG/Welcome.html
Currencylayer API document
https://currencylayer.com/documentation
ファイル入出力
https://www.caveofprogramming.com/java/java-file-reading-and-writing-files-in-java.html
正規表現
https://docs.oracle.com/javase/jp/6/api/java/util/regex/Pattern.html
http://qiita.com/ymsr5612/items/7c8811b5cf37d700adc4
Base64 エンコード/デコード (ここでEclipseにjarがimportできなくてハマった)
http://criticalbreak5.seesaa.net/article/420080828.html
XML via DOM(結局使ってない)
http://www.fireproject.jp/feature/xml/programing/java-dom.html
JSON
http://www.task-notes.com/entry/20150919/1442639772

続きを読む