AWS EC2 Mysql レプリケーション環境構築

はじめに

本番環境はRDSのMulti A-Z でレプリケーションしているのですが、selectはslaveに、FOR UPDATEはmasterになどクエリの向き先をしっかり確認しておく必要があったので勉強がてらEC2でレプリケーション環境を構築したので自分用にメモ。

やること

・ インスタンスを2つ起動
・ レプリケーション設定
・ 接続確認

EC2インスタンス起動

セキュリティ設定
2つのサーバのMysqlが互いに通信できるように、
Inboundで3306ポートに対して、互いのPrivateIPを設定します。

レプリケーション設定

Master-Server

my.conf

/etc/my.cnf
[mysqld]
server-id=101
log_bin

server-idは、1から(2^32)-1)間の正整数で、サーバ間で重複しないように設定する(任意の値でOK)
ただし、1、2はserver-idがないときのデフォルトとして使用されるため極力使わないほうがいい

Slave-ServerのUser作成とアクセスの許可

mysql > grant replication slave on *.* to 'replication'@'Slave-Srever Private IP' identified by 'password'

ステータスの確認

mysql > show master status;
+-------------------+----------+--------------+------------------+
| File              | Position | Binlog_Do_DB | Binlog_Ignore_DB |
+-------------------+----------+--------------+------------------+
| mysqld-bin.000001 |     1234 |              |                  |
+-------------------+----------+--------------+------------------+

Slave-Server

my.conf

/etc/my.cnf
[mysqld]
server-id=201
replicate-do-db=db名(対象DB指定)

Master-Server登録

mysql > change master to master_host = 'Master-Server Private IP',
        master_user = 'replication',
        master_password = 'pass',
        master_log_file = 'mysqld-bin.000001', //Master-Serverの情報
        master_log_pos = 1234 //Master-Serverの情報

Slave-ServerのUser作成とアクセスの許可

mysql > grant all privileges on *.* to name@'Master-Srever Private IP' identified by 'password';

ステータスの確認

mysql > show slave status \G;
*************************** 1. row ***************************
               Slave_IO_State: Waiting for master to send event
                  Master_Host: Master-Server Private IP
                  Master_User: replication
                  Master_Port: 3306
                Connect_Retry: 60
              Master_Log_File: mysqld-bin.000001
          Read_Master_Log_Pos: 2258
               Relay_Log_File: mysqld-relay-bin.000004
                Relay_Log_Pos: 10195
        Relay_Master_Log_File: mysqld-bin.000001
             Slave_IO_Running: Yes
            Slave_SQL_Running: Yes

Slave_IO_Running: Yes、Slave_SQL_Running: Yesで設定OK

接続確認

各サーバーからホスト指定でログインできれば接続はできている状態となります。
下記のエラーが出る場合はEC2インスタンスのセキュリティを見直す必要があります。

ERROR 1130 (HY000): Host 'Private IP' is not allowed to connect to this MySQL server

さいごに

その都度、各mysqlの再起動や必要であれば権限の反映FLUSH PRIVILEGESが必要になるかと思います。
自分用のメモとして記述しましたが、不備がありましたらご教授いただけますと幸いです。

続きを読む

aws-sam-localだって!?これは試さざるを得ない!

2017-08-11にaws-sam-localのベータ版がリリースされました。
単に「早速試したで!」と言う記事を書いても良かったのですが、少し趣向を変えてサーバレス界隈の開発環境のこれまでの推移を語った上で、aws-sam-localの使ってみた感想もお話しようかと思います。

サーバレス開発環境の今昔

サーバレス自体がかなり最近になって生まれた風潮なので昔とか言うのも問題はあるかと思いますが、とにかくサーバレスなるものの開発環境について私にわかる範囲でお話しようと思います。誤りや不正確な点については編集リクエストやコメントを頂けると幸いです。

なお、サーバレスという言葉は一般的な名詞ですが、私がAWS上でしかサーバレスに触れていないため、AzureやGCPなどには触れず、もっぱらLambdaの話になってしまうことをあらかじめご了承ください。

Lambdaのデプロイは辛かった

Lambdaの基本的なデプロイ方法はZIPで固めてアップロードです。
直接ZIPをLambdaに送るか、あらかじめS3に置いておいてLambdaにはそのURLを教えるかといった選択肢はありましたが、手動でZIPに固めてAWSに送るという手順は不可避でした。なんかもうすでに辛い。

さらに言うとLambdaに送りつけられるのはLambdaで実行するコードだけ。性質上Lambdaは単体で使われることはほとんどなく、他のサービスと連携することがほとんどなのにその辺は自分で管理するしかありませんでした。辛い。

CloudFormationで管理することは可能でしたが、CloudFormationテンプレートを書くのがかなりダルいことと、CloudFormationの更新とZIPのアップロードを別途行う必要があって手順が煩雑化しやすいため、「もうええわ」と手動管理してることが多かったと思われます。

また、ローカル環境で実行するには一工夫必要でした。

颯爽登場!Serverlessフレームワーク

そんな時に颯爽と現れたのがServerlessフレームワークでした。
ServerlessフレームワークにおいてはLambdaファンクション及び関連するリソースを独自のyamlファイルで管理します。結局は一度CloudFormationテンプレートに変換されるのですが、CloudFormationテンプレートよりも単純な形式で記述できたのが流行った一因かと思います。また、sls deployコマンドでLambdaのコードのアップロードおCloudFormationスタックの更新を一括で行ってくれたため、デプロイの手順は従来よりもはるかに簡略化されたかと思われます。

Lambdaテストしづらい問題

デプロイに関する問題はServerlessフレームワークや、ほぼ同時期に現れたSAMによって改善されましたが、開発プロセスにおいて大きな課題がありました。

テストし辛ぇ…

上記の通りLambdaは性質上他のサービスと連携することが多いため、その辺をローカル環境でどうテストするかに多くの開発者が頭を抱えました。対策として

  1. モッククラスを作って、実際のサービスのような振る舞いをさせる
  2. プロダクションとは別のリージョンに環境を再現して、そこで実行する

といった方法がありましたが、それぞれ

  1. モッククラスの実装がすこぶるダルい 下手したらロジック本体より時間かかる
  2. クラウドにデプロイしないとテストできないため、時間がかかる

といったデメリットがありました。

LocalStackとaws-sam-local

サーバレス開発者の嘆きを聞いたAtlassianがローカル環境でAWSのサービスのエンドポイントを再現するなんとも素敵なツールを作り上げました。それがLocalStackです。
再現されているサービスの数が物足りなく感じられたり、サードパーティ製であることに一抹の不安を覚えたりする人もいるかと思いますが、これ以上を求めるなら自分で作るぐらいしかないのが現状かと思います。

そしてaws-sam-local。こちらはLocalStackと少し趣が異なります。LocalStackが連携するサービスのエンドポイントを再現して提供するのに対して、aws-sam-localは実行環境の提供という意味合いが強いです。そして重要なことはAWSの公式がサポートしているということです。公式がサポートしているということです。大事なことなので(ry
「実行するのはローカルでNode.jsなりPythonなりで動かせばええやん」と思いがちですが、ランタイムのバージョンなどを本番環境と確実に揃えられるのは大きな利点です。
まだベータ版が出たばっかなので今後に期待といったところでしょう

aws-sam-local触ってみた

それでは実際に触ってみましょう。
ちなみに当方環境は

  • OS: macOS Sierra 10.12.6
  • Docker for Mac: 17.06.0-ce-mac19

です。

事前準備とインストール

公式のInstallationの項目を読み進めますが、事前にDockerを使えるようにしておく必要があります。

Macだったら普通にDocker For Macをインストールしておけば問題ありません。
一方Windowsだと

スクリーンショット 2017-08-16 14.48.18.png

まさかのDocker Toolbox推奨。 Docker For Windowsェ…
そしてaws-sam-localのインストールですが、私は-gオプション排斥論者なのでローカルインストールします

npm install aws-sam-local

実装

今回はこちらを参考にAPIゲートウェイから呼び出すLambdaを実装します。
ほぼ丸パクリですが一部アレンジしてますのでソースものっけます。

template.yaml
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: SAM Local test
Resources:
  HelloWorld:
    Type: AWS::Serverless::Function
    Properties:
      Handler: index.handler
      Runtime: nodejs6.10
      Events:
        GetResource:
          Type: Api
          Properties:
            Path: /resource/{resourceId}
            Method: put

ランタイムをnodejs6.10に変更してます。
新しく作る場合にわざわざ古いバージョンを使う必要もありませんので。

余談ですが、WebStormのCloudFormation用のプラグインは今の所SAMには対応してないのか、Type: AWS::Serverless::Functionのところにめっちゃ赤線を引かれます。

index.js
/**
 * Created by yuhomiki on 2017/08/16.
 */

"use strict";

const os = require("os");
console.log("Loading function");


const handler = (event, context, callback) => {
  return callback(null, {
    statusCode: 200,
    headers: { "x-custom-header" : "my custom header value" },
    body: "Hello " + event.body + os.EOL
  });
};

exports.handler = handler;

完全に書き方の趣味の問題です。
内容は参考ページのものと全く同じです。

package.json
{
  "name": "sam_test",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "invoke-local": "sam local invoke HelloWorld -e event.json",
    "validate": "sam validate",
    "api-local": "sam local start-api"
  },
  "author": "Mic-U",
  "license": "MIT",
  "dependencies": {
    "aws-sam-local": "^0.1.0"
  }
}

aws-sam-localをローカルインストールしているので、package.jsonのscriptsに追記しています。

実行

それでは実行してみましょう

上記のpackage.jsonに記載した

  • invoke-local
  • validate
  • api-local

を実行していきます。

invoke-local

Lambdaファンクションをローカル環境で実行します。
Lambdaファンクションに渡すevent変数はワンライナーで定義することも可能ですが、あらかじめJSONファイルを作っといた方が取り回しがいいです。

json.event.json
{
  "body": "MIC"
}

実行結果はこんな感じ

スクリーンショット 2017-08-16 15.25.42.png

まず最初にdocker pullしてランタイムに応じたDockerイメージをダウンロードします。
その後はコンテナ内でLambdaファンクションを実行し、最後にcallbackに与えた引数を出力といった流れです。
ログの形式がすごくLambdaですね。あとタイムゾーンもUTCになっていますね。
メモリの使用量をローカルで確認できるのは嬉しいですね。

-dオプションをつけることでデバッグもできるようです。
公式のgithubにはご丁寧にVSCodeでデバッグしてる様子がgifで上げられてます。

validate

テンプレートファイルのチェックをします。
デフォルトではカレントディレクトリのtemplate.yamlファイルをチェックしますが、-tオプションで変更することが可能です。

失敗するとこんな感じに怒られます。

スクリーンショット 2017-08-16 15.33.47.png

成功した時は「Valid!」とだけ言ってきます。きっと必要以上に他人に関わりたくないタイプなのでしょう。

api-local

sam local start-apiコマンドはローカル環境にAPIサーバを立ち上げます。
ホットリロード機能がついてるので、立ち上げっぱなしでもソースを修正したら自動で反映されます。いい感じですね。

スクリーンショット 2017-08-16 15.40.58.png

立ち上がるとこんなメッセージが出るので、あとはCURLなりPostManなりで煮るなり焼くなり好きにしましょう。

CURLの結果はこんな感じ
スクリーンショット 2017-08-16 15.51.39.png

所感

Lambdaのローカル実行環境を公式が用意したことに大きな意義があるかと思います。
Dockerさえあればすぐに使えることと、SAMテンプレートを書かざるをえないのでInfrastructure as Codeが自然と根付いていくのも個人的には好感を持てます。

ただし、まだベータ版なこともあって機能的にもの足りない部分があるのも事実です。
具体的にはやはりDynamoDBとかもテンプレートから読み取ってDockerコンテナで用意してくれたらなーと思います。LocalStackやDynamoDB Localでできないこともないでしょうが、DynamoDB Localに関してはテンプレートからテーブル作ったりするの多分無理なのでマイグレーション用のコードを書くことになりますし、LocalStackに関しては実はあまり真面目に使ったことないのでわかりませんが環境構築に一手間かかりそう。ていうかできれば一つのツールで完結させたい。

SAMしかりaws-sam-localしかり、AWS側としてもより開発がしやすくなるような環境づくりをしていくような姿勢を見せているので、今後のアップデートに期待したいところですね。

続きを読む

AWS + Nginx + PHP + Laravel

nginx + php + LaravelをAWS上に構築してみる

nginx

  • インストールと起動
$ sudo yum -y install nginx
・・・・・
完了しました!

$ sudo service nginx start
Starting nginx:                                            [  OK  ]
  • バージョンやconfigurationの内容を知りたいときは下記コマンド
$ nginx -V
  • configurationで使いそうなやつメモ
設定 説明 デフォルト
–error-log-path HTTPアクセスログのエラーのパス /var/log/nginx/error.log
–http-log-path HTTPアクセスログのパス /var/log/nginx/access.log
–conf-path nginxの設定ファイルのパス /etc/nginx/nginx.conf
–http-proxy-temp-path プロキシを実行している場合、ここで指定したディレクトリが一時ファイルの格納パスになる /var/lib/nginx/tmp/proxy
  • モジュールで気になるところメモあたり(他にもあったけど、メモるの面倒でdown)
モジュール名 説明 利用場面 デフォルト
http_ssl https対応(OpenSSLライブラリが必要)。 プロキシ 有効
http_realip L7ロードバランサなどの後に配置する場合有効にする必要あり。複数のクライアントが同一IPアドレスから通信してくるように見える環境で使用。 プロキシ 有効
http_geoip クライアントのIPアドレスに基づく地理的位置に応じた処理を行うための様々な変数を設定 Web、プロキシ 有効
http_stub_status Nginx自身の統計情報の収集を手助けする Web、プロキシ 有効

※有効化(–with-<モジュール名>module)、無効化(–without-<モジュール名>module)

PHP7のインストール

  • CentOS6用のPHP7のリポジトリを追加(これがないとインストールできないくさい)
$ sudo yum install --enablerepo=webtatic-testing 
                 php70w php70w-devel php70w-fpm php70w-mysql 
                 php70w-mbstring php70w-pdo
  • 他にも必要であればインストールしておく(json系とか)

nginxとphpの紐付け

  • index.phpのセット

    • /var/www/default ディレクトリ作成
    • ここにindex.phpを配置 (最初はとりあえずphpinfoを吐くだけ)
  • /etc/php-fpm.d/www.confの編集 (backupを取った上で編集)
$ diff -uN www.conf.backup_20160710 www.conf
--- www.conf.backup_20160710    2016-07-10 08:00:45.267704077 +0000
+++ www.conf    2016-07-10 08:01:38.451085053 +0000
@@ -5,9 +5,11 @@
 ; Note: The user is mandatory. If the group is not set, the default user's group
 ;       will be used.
 ; RPM: apache Choosed to be able to access some dir as httpd
-user = apache
+; user = apache
+user = nginx
 ; RPM: Keep a group allowed to write in log dir.
-group = apache
+; group = apache
+group = nginx
  • /etc/nginx/nginx.confの編集 (backupを取った上で編集)
$ diff -uN nginx.conf.backup_20160710 nginx.conf
--- nginx.conf.backup_20160710  2016-07-10 07:49:38.694839828 +0000
+++ nginx.conf  2016-07-10 07:59:49.564346085 +0000
@@ -32,13 +32,14 @@
     # for more information.
     include /etc/nginx/conf.d/*.conf;

-    index   index.html index.htm;
+    index   index.php index.html index.htm;

     server {
         listen       80 default_server;
         listen       [::]:80 default_server;
         server_name  localhost;
-        root         /usr/share/nginx/html;
+        #root         /usr/share/nginx/html;
+        root         /var/www/default;

         # Load configuration files for the default server block.
         include /etc/nginx/default.d/*.conf;
@@ -46,8 +47,17 @@
         location / {
         }

-        # redirect server error pages to the static page /40x.html
+        # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
         #
+        location ~ .php$ {
+            root           /var/www/default;
+            fastcgi_pass   127.0.0.1:9000;
+            fastcgi_index  index.php;
+            fastcgi_param  SCRIPT_FILENAME  /var/www/default$fastcgi_script_name;
+            include        fastcgi_params;
+        }
+
+        # redirect server error pages to the static page /40x.html
         error_page 404 /404.html;
             location = /40x.html {
         }
@@ -64,16 +74,6 @@
         #    proxy_pass   http://127.0.0.1;
         #}

-        # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
-        #
-        #location ~ .php$ {
-        #    root           html;
-        #    fastcgi_pass   127.0.0.1:9000;
-        #    fastcgi_index  index.php;
-        #    fastcgi_param  SCRIPT_FILENAME  /scripts$fastcgi_script_name;
-        #    include        fastcgi_params;
-        #}
-
  • 再起動して、phpinfoページが見れればOK (http://<>)
$ sudo service php-fpm start
Starting php-fpm:                                          [  OK  ]
$ sudo service nginx restart
Stopping nginx:                                            [  OK  ]
Starting nginx:                                            [  OK  ]
  • ついでにサーバ起動時などに自動で起動するものも設定
$ sudo chkconfig nginx on
$ sudo chkconfig php-fpm on

nginxとphp-fpmの接続をsocketにする

  • php-fpmの設定変更
$ diff -uN www.conf.backup_20160710 www.conf
--- www.conf.backup_20160710    2016-07-10 08:00:45.267704077 +0000
+++ www.conf    2016-07-10 08:19:03.630366042 +0000
@@ -19,7 +21,8 @@
 ;                            (IPv6 and IPv4-mapped) on a specific port;
 ;   '/path/to/unix/socket' - to listen on a unix socket.
 ; Note: This value is mandatory.
-listen = 127.0.0.1:9000
+; listen = 127.0.0.1:9000
+listen = /var/run/php-fpm/php-fpm.sock

@@ -32,6 +35,8 @@
 ;                 mode is set to 0660
 ;listen.owner = nobody
 ;listen.group = nobody
+listen.owner = nginx
+listen.group = nginx
 ;listen.mode = 0660
  • nginxの設定変更
$ diff -uN nginx.conf.backup_20160710 nginx.conf
--- nginx.conf.backup_20160710  2016-07-10 07:49:38.694839828 +0000
+++ nginx.conf  2016-07-10 08:20:37.741301066 +0000
@@ -46,8 +47,17 @@
-            fastcgi_pass   127.0.0.1:9000;
+            fastcgi_pass   unix:/var/run/php-fpm/php-fpm.sock;
  • 再起動
$ sudo service php-fpm restart
Stopping php-fpm:                                          [  OK  ]
Starting php-fpm:                                          [  OK  ]
$ sudo service nginx restart
Stopping nginx:                                            [  OK  ]
Starting nginx:                                            [  OK  ]

Laravel5を入れてみる

  • Composerをインストール
$ curl -sS https://getcomposer.org/installer | php
$ sudo mv /home/ec2-user/composer.phar /usr/local/bin/composer
  • Laravelのインストール
$ sudo /usr/local/bin/composer global require "laravel/installer"
Changed current directory to /root/.composer
Using version ^1.3 for laravel/installer
./composer.json has been created
Loading composer repositories with package information
Updating dependencies (including require-dev)
  - Installing symfony/process (v3.1.2)
    Downloading: 100%         

  - Installing symfony/polyfill-mbstring (v1.2.0)
    Downloading: 100%         

  - Installing symfony/console (v3.1.2)
    Downloading: 100%         

  - Installing guzzlehttp/promises (1.2.0)
    Downloading: 100%         

  - Installing psr/http-message (1.0)
    Downloading: 100%         

  - Installing guzzlehttp/psr7 (1.3.1)
    Downloading: 100%         

  - Installing guzzlehttp/guzzle (6.2.0)
    Downloading: 100%         

  - Installing laravel/installer (v1.3.3)
    Downloading: 100%         

symfony/console suggests installing symfony/event-dispatcher ()
symfony/console suggests installing psr/log (For using the console logger)
Writing lock file
Generating autoload files
  • php-xmlのインストール (laravelで必要になる)
$ sudo yum install --enablerepo=webtatic-testing php70w-xml
  • プロジェクト作成
$ pwd
/var/www/default
$ sudo /usr/local/bin/composer create-project --prefer-dist laravel/laravel darmaso
Installing laravel/laravel (v5.2.31)
  - Installing laravel/laravel (v5.2.31)
    Downloading: 100%         

Created project in darmaso
> php -r "copy('.env.example', '.env');"
Loading composer repositories with package information
Updating dependencies (including require-dev)
・・・・・ (下記の結果と同じ)

$ cd darmaso
$ sudo /usr/local/bin/composer install
Loading composer repositories with package information
Updating dependencies (including require-dev)
・・・・・
Writing lock file
Generating autoload files
> IlluminateFoundationComposerScripts::postUpdate
> php artisan optimize
Generating optimized class loader

※php-xmlをインストールしておかないと、下記のようなエラーが出るので注意
Your requirements could not be resolved to an installable set of packages.

  Problem 1
    - phpunit/phpunit 4.8.9 requires ext-dom * -> the requested PHP extension dom is missing from your system.
・・・・・
    - Installation request for phpunit/phpunit ~4.0 -> satisfiable by phpunit/phpunit[4.0.0, 4.0.1, 4.0.10, 4.0.11, 4.0.12, 4.0.13, 4.0.14, 4.0.15, 4.0.16, 4.0.17, 4.0.18, 4.0.19, 4.0.2, 4.0.20, 〜
・・・・・
  To enable extensions, verify that they are enabled in those .ini files:
    - /etc/php.ini
    - /etc/php.d/bz2.ini
    - /etc/php.d/calendar.ini
・・・・・
  You can also run `php --ini` inside terminal to see which files are used by PHP in CLI mode.
  • Applicationキーの生成 (composerでインストールした場合セットされているらしいが念のため)
$ sudo php artisan key:generate
Application key [base64:YVeCf2A+5IjUbk2qVL4HhPiecBdYuo8irJrEYjJKZWY=] set successfully.
  • Laravel用にnginx設定を修正し、再起動
$ diff -uN nginx.conf.backup_20160710 nginx.conf
+        #root         /var/www/default;
+        root         /var/www/default/darmaso/public;
・・・・・
         location / {
+            try_files $uri $uri/ /index.php?$query_string;
         }
・・・・・
+            #root           /var/www/default;
+            root           /var/www/default/darmaso/public;
・・・・・
+            #fastcgi_param  SCRIPT_FILENAME  /var/www/default$fastcgi_script_name;
+            fastcgi_param  SCRIPT_FILENAME  /var/www/default/darmaso/public$fastcgi_script_name;

$ sudo service php-fpm restart
$ sudo service nginx restart
  • これで動作確認するとエラーになるので下記の設定をしてみる
$ sudo chmod -R 777 storage/
$ sudo chmod -R 777 vendor/

※本来は、サーバアカウントをちゃんと定義してやるべきだが、今回は試しなのでこのままでOKとする

  • 一部の設定を変えてみる
config/app.php
$ diff -uN config/app.php.backup_20160710 config/app.php
--- config/app.php.backup_20160710  2016-07-10 09:37:07.881735079 +0000
+++ config/app.php  2016-07-10 09:40:54.263419145 +0000
@@ -52,7 +52,7 @@
     |
     */

-    'timezone' => 'UTC',
+    'timezone' => 'Asia/Tokyo',

     /*
     |--------------------------------------------------------------------------
@@ -65,7 +65,7 @@
     |
     */

-    'locale' => 'en',
+    'locale' => 'jp',

     /*
     |--------------------------------------------------------------------------
@@ -78,7 +78,7 @@
     |
     */

-    'fallback_locale' => 'en',
+    'fallback_locale' => 'jp',

これで構築した環境にアクセスしたところ、無事いけました!
設定内容が荒いところもありますが、上記まででPHP+Nginx自体はいけちゃいますね。

Nginxの設定はあまり大したことはできませんでしたが、今後は色々と勉強してみようと思いますmm

参考

続きを読む

serverless frameworkを使ってデプロイ

serverless frameworkってなんなん

YAMLに設定を書いておくと、CLIでAWSのデプロイ/設定が行えます。
簡単に言うと、デプロイの自動化ができます。

環境

項目 version
node 6.10.2
serverless framework 1.19.0

インストール

serverless frameworkをglobalにインストール。

npm install -g serverless

config credentials

serverless framework docs AWS – Config Credentials

プロジェクト作成

serverless frameworkのコマンドを使用してプロジェクトを作成します。

mkdir serverless-sample
serverless create -t aws-nodejs

以下内容のファイルが作成されます。試してみたら見れますがw

handler.js
'use strict';

module.exports.hello = (event, context, callback) => {
  const response = {
    statusCode: 200,
    body: JSON.stringify({
      message: 'Go Serverless v1.0! Your function executed successfully!',
      input: event,
    }),
  };

  callback(null, response);

  // Use this code if you don't use the http event with the LAMBDA-PROXY integration
  // callback(null, { message: 'Go Serverless v1.0! Your function executed successfully!', event });
};
serverless.yml
# Welcome to Serverless!
#
# This file is the main config file for your service.
# It's very minimal at this point and uses default values.
# You can always add more config options for more control.
# We've included some commented out config examples here.
# Just uncomment any of them to get that config option.
#
# For full config options, check the docs:
#    docs.serverless.com
#
# Happy Coding!

service: serverless-sample

# You can pin your service to only deploy with a specific Serverless version
# Check out our docs for more details
# frameworkVersion: "=X.X.X"

provider:
  name: aws
  runtime: nodejs6.10

# you can overwrite defaults here
#  stage: dev
#  region: us-east-1

# you can add statements to the Lambda function's IAM Role here
#  iamRoleStatements:
#    - Effect: "Allow"
#      Action:
#        - "s3:ListBucket"
#      Resource: { "Fn::Join" : ["", ["arn:aws:s3:::", { "Ref" : "ServerlessDeploymentBucket" } ] ]  }
#    - Effect: "Allow"
#      Action:
#        - "s3:PutObject"
#      Resource:
#        Fn::Join:
#          - ""
#          - - "arn:aws:s3:::"
#            - "Ref" : "ServerlessDeploymentBucket"
#            - "/*"

# you can define service wide environment variables here
#  environment:
#    variable1: value1

# you can add packaging information here
#package:
#  include:
#    - include-me.js
#    - include-me-dir/**
#  exclude:
#    - exclude-me.js
#    - exclude-me-dir/**

functions:
  hello:
    handler: handler.hello

#    The following are a few example events you can configure
#    NOTE: Please make sure to change your handler code to work with those events
#    Check the event documentation for details
#    events:
#      - http:
#          path: users/create
#          method: get
#      - s3: ${env:BUCKET}
#      - schedule: rate(10 minutes)
#      - sns: greeter-topic
#      - stream: arn:aws:dynamodb:region:XXXXXX:table/foo/stream/1970-01-01T00:00:00.000
#      - alexaSkill
#      - iot:
#          sql: "SELECT * FROM 'some_topic'"
#      - cloudwatchEvent:
#          event:
#            source:
#              - "aws.ec2"
#            detail-type:
#              - "EC2 Instance State-change Notification"
#            detail:
#              state:
#                - pending
#      - cloudwatchLog: '/aws/lambda/hello'
#      - cognitoUserPool:
#          pool: MyUserPool
#          trigger: PreSignUp

#    Define function environment variables here
#    environment:
#      variable2: value2

# you can add CloudFormation resource templates here
#resources:
#  Resources:
#    NewResource:
#      Type: AWS::S3::Bucket
#      Properties:
#        BucketName: my-new-bucket
#  Outputs:
#     NewOutput:
#       Description: "Description for the output"
#       Value: "Some output value"

region設定

regionの設定を追記します。

serverless.yml
--- 省略 ---
provider:
  name: aws
  runtime: nodejs6.10
  region: ap-northeast-1
--- 省略 ---

API Gateway設定

上記ロジックを呼ぶエンドポイントの設定を追記。

serverless.yml
--- 省略 ---
functions:
  hello:
    handler: handler.hello
    events:
      - http:
          path: serverless-sample
          method: get
--- 省略 ---

デプロイ

serverless frameworkのコマンドを使用してデプロイします。
serverless.ymlに記載の内容でデプロイされます。

serverless deploy

参考

serverless

続きを読む

AWS Batch を動かしてみる

概要

AWS Batchを一通り動かしてみる。

↓の続きです
AWSでバッチ処理をするときの方法を考える

実践

※参考にしたサイト
API Gateway + LambdaでAWS BatchのJobを実行する

1. AWS Batchを作成

AWS Batchを開きます。

スクリーンショット 2017-08-14 17.20.26.jpg

色々入力するところがあるけど全部デフォルトでいいです。
スクリーンショット 2017-08-14 19.04.19.jpg

スクリーンショット 2017-08-14 19.48.45.jpg
そしてCreate。

スクリーンショット 2017-08-14 17.25.46.jpg
実行直後はPriorityが1になっていればとりあえず問題ないはずです。
しばらく経つとするとRUNABLEに入り、SUCCEEDEDに入ればJob完了です。

LambdaからBatchを起動する際にBatchのjobQueueArnが必要なのでJob queuesから確認してメモしておきます。
スクリーンショット 2017-08-14 20.29.01.jpg

2. AWS Batchを起動するLambdaの作成

まずLambda用の新規ロール作成からです。
下記のようなポリシーのロールを作成しておきます。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "",
            "Effect": "Allow",
            "Action": [
                "batch:SubmitJob"
            ],
            "Resource": [
                "*"
            ]
        }
    ]
}

ロールを作成したらLambdaを作っていきます。
スクリーンショット 2017-08-14 17.30.50.jpg

設計図は使用せずに 「一から作成」 を選択。
スクリーンショット 2017-08-14 17.31.03.jpg

トリガーは追加せずに「次へ」。
スクリーンショット 2017-08-14 17.31.12.jpg

関数の設定です。
適当に名前を付けて、サンプル通りPython3.6を選びます。
スクリーンショット 2017-08-14 17.42.22.jpg

Lambdaの関数コードは下記に書き換え。

import boto3

def lambda_handler(event, context):

    client = boto3.client('batch')

    JOB_NAME = event['JobNeme']
    JOB_QUEUE = "arn:aws:batch:ap-northeast-1:xxxxxxxxxxxx:job-queue/first-run-job-queue"
    JOB_DEFINITION = "first-run-job-definition:1"

    response = client.submit_job(
        jobName = JOB_NAME,
        jobQueue = JOB_QUEUE,
        jobDefinition = JOB_DEFINITION
        )
    print(response)
    return 0

※8行目 JOB_QUEUE = <1.で作成したBtachのQueue arnに書き換え>

ロールは「テンプレートから新規作成し、テンプレートは “AWS Batchアクセス権限” を選びます」
スクリーンショット 2017-08-14 17.42.40.jpg

次へ
スクリーンショット 2017-08-14 17.42.49.jpg

ざっくり確認しておきます
スクリーンショット 2017-08-14 17.43.23.jpg

スクリーンショット 2017-08-14 17.43.29.jpg

おめでとうございます。
スクリーンショット 2017-08-14 17.44.07.jpg

3. LambdaのためのAPI Gatewayを作る

  1. 先に作成したLambdaのトリガータブを開き「+トリガーを追加」します
    スクリーンショット 2017-08-14 17.44.41.jpg

  2. API Gatewayをトリガーとして設定して送信します。
    スクリーンショット 2017-08-14 17.46.24.jpg

  3. メソッドの作成からPOSTを追加します。
    スクリーンショット 2017-08-14 17.47.34.jpg

  4. 先に作成したLambdaのリージョンを選び、Lambda関数を選択します。
    スクリーンショット 2017-08-14 17.49.06.jpg

  5. APIをデプロイします。
    スクリーンショット 2017-08-14 17.49.35.jpg

  6. 実験ですがProdで特に問題ないです。
    スクリーンショット 2017-08-14 17.49.44.jpg

  7. デプロイが完了するとURLが生成されます。
    スクリーンショット 2017-08-14 17.49.55.jpg

4. 動作確認

  1. 生成されたurlにcurlでPOSTしてみます。
$ curl -X POST -d '{ "JobName" : "test" }' https://xxxxxxxxxx.execute-api.us-east-2.amazonaws.com/prod/ToBatch/
  1. コマンドラインに応答はありませんが、BatchのJob queuesに新しいJobが入ってきます。
    スクリーンショット 2017-08-14 18.30.40.jpg

5. 完了

一旦これで実用的な形になったと思います。
あとはJobに自分の処理内容を乗せておけば、APIでいつでも実行可能となります。

続きを読む

StepFunctionsで始める分散処理Lambda

プロローグ ~こんなことをやりたかった~

業務で実装しているシステムで、定期的にこんな処理をする必要がありました。

  1. DynamoDBからデバイスのIDの一覧を取得する
  2. 各IDについて、DynamoDBにアクセスして生データを取得し、サマライズして別のテーブルに格納

こんなことをLambdaにやらせてたのですが、一つのLambdaファンクションに丸投げしてたのが災いして、デバイスが増えてきたりすると非常に辛い感じになってきました。

ですので、これを機に今まで真面目に触らなかったStepFunctionsに手を出して見ました。

ちなみにまだ実験がてらちょっといじってみたぐらいですので、記事中のソースソースコードはかなりしょぼいです。

出来上がりはこちら

Step Functionsって何?

って方のために手短に説明しますと、 Lambda同士の連携をいい感じに管理するツール と思えばほぼ間違いないです。

スクリーンショット 2017-08-11 14.55.19.png

これはデフォルトで用意されているブループリントの図ですが、視覚的にワークフローを把握しながらLambdaファンクションを組み立てることができます。

また、あるLambdaファンクションの結果に応じて次の処理を切り替えるといったことも簡単にできるようになります。

実装編

大まかな流れ

今回はあくまでStepFunctionsの使用感を知るための実験が主目的なので、処理を簡略化します。

まずは以下のLambdaファンクションを用意します。

  1. LambdaA: 処理対象のIDの一覧を取得(実験なのでIDは適当な文字列をハードコーディング)
  2. LambdaB: 配列で受け取ったID一つ一つについて、LambdaCを非同期で並列にinvokeする
  3. LambdaC: 受け取ったIDをコンソールに出力

LambdaBからLambdaCへの受け渡しもStepFunctionsで行いたいところですが、同一のLambdaを不特定多数並列に立ち上げるのは辛そうなので、ここはLambda内でinvokeします。

よって処理の流れは以下のようになります。

  1. LambdaAでIDの一覧を取得(実験なのでIDは適当な文字列をハードコーディング)
  2. 特に意味はないが10秒ほどスリープする
  3. LambdaBはLambdaAから受け取ったIDの一覧を使い、一つのIDに対して一つのLambdaCを非同期でinvokeする
  4. LambdaCはLambdaBから受け取ったIDをコンソールに出力。処理が並列化されていることをわかりやすくするため、2秒ほどスリープしてからLambdaBに結果を返す
  5. 全てのLambdaCの処理が終わったらLambdaBも終了する

うーん、「Lambda」がゲシュタルト崩壊しそうですね
ではこれらをServerless Frameworkで実装しようかと思います

ここで注意事項

StepFunctionsで定義した一連の処理の流れをStateMachineと言いますが、 State Machineは一度作成すると編集できません。変更したいなら都度削除して作り直さなければいけません。

幸いにして2017年2月にCloudFormationがStepFunctionsをサポートしましたので、これを利用しましょう。

AWSコンソールのGUIで毎回入力し直すよりは楽かと思います。

各種設定ファイル作成

ではServerlessで実装するための設定ファイルを書いていきましょう。

serverless.yml

まずはおなじみのserverless.ymlです

serverless.yml

service: step-test

provider:
  name: aws
  runtime: nodejs6.10

# you can overwrite defaults here
  stage: dev
  region: ap-northeast-1
  memorySize: 256
  timeOut: 30
  iamRoleStatements:
    - Effect: "Allow"
      Action:
        - "lambda:InvokeFunction"
      Resource:
        - arn:aws:lambda:${self:provider.region}:${self:custom.config.accountId}:function:${self:service}-${self:custom.config.stage}-parallel

custom:
  config:
    accountId: "your Account ID" # 自分のAWSアカウントのID
    stage: ${opt:stage, self:provider.stage}

functions:
  # LambdaA
  first:
    handler: handler.first
  # LambdaB  
  second:
    handler: handler.second
    environment:
      TARGET_LAMBDA_ARN: ${self:service}-${self:custom.config.stage}-parallel
  # LambdaC
  parallel:
    handler: handler.parallel
resources: ${file(./resources/state_machine.yml)}

できるだけ使い回せるように色々と変数を使ってます。
変数の基本的な使い方はここを見ていただくとして、いくつかポイントになるところを解説します。

  • ${file(./resources/state_machine.yml)}

    • 別のファイルから読み込みます。これの中身はCloudFormationテンプレートです(後述)
  • stage: ${opt:stage, self:provider.stage}
    • コマンドラインオプションで –stageが与えられればそれを、なければprovidor.stageの値(この場合は”dev”)をデフォルトで使用。Lambdaファンクションの名前に関わります。

あと大事な点として、LambdaBはinvokeするためにLambdaCの名前を知っておく必要があります。
今回は環境変数として設定するようにしてあります。

CloudFormationテンプレート

StateMachineをCloudFormationテンプレートで作成します。
こちらを参考に

resources/state_machine.yml
AWSTemplateFormatVersion: "2010-09-09"
Description: "Create Step Function StateMachine"
Resources:
  InvokeLambdaRole:
      Type: AWS::IAM::Role
      Properties:
        AssumeRolePolicyDocument:
          Version: "2012-10-17"
          Statement:
            - Sid: StepFunctionsAssumeRolePolicy
              Effect: Allow
              Principal:
                Service:
                  Fn::Join: [ ".", [ states, Ref: "AWS::Region", amazonaws, com ] ]
              Action: sts:AssumeRole
        Path: /
        ManagedPolicyArns:
          - arn:aws:iam::aws:policy/service-role/AWSLambdaRole
  TestStateMachine:
    Type: "AWS::StepFunctions::StateMachine"
    Properties:
      RoleArn:
        Fn::GetAtt: [ InvokeLambdaRole, Arn ]
      DefinitionString: |-
        {
          "Comment": "An example of the Amazon States Language using wait states",
          "StartAt": "FirstState",
          "States": {
            "FirstState": {
              "Type": "Task",
              "Resource": "arn:aws:lambda:${self:provider.region}:${self:custom.config.accountId}:function:${self:service}-${self:custom.config.stage}-first",
              "Next": "wait_using_seconds"
            },
            "wait_using_seconds": {
              "Type": "Wait",
              "Seconds": 10,
              "Next": "FinalState"
            },
            "FinalState": {
              "Type": "Task",
              "Resource": "arn:aws:lambda:${self:provider.region}:${self:custom.config.accountId}:function:${self:service}-${self:custom.config.stage}-second",
              "End": true
            }
          }
        }
Outputs:
  StateMachineArn:
    Value:
      Ref: TestStateMachine
  StateMachineName:
    Value:
      Fn::GetAtt: [ TestStateMachine, Name ]


StateMachineの定義の部分を取り出すとこうなります

{
  "Comment": "An example of the Amazon States Language using wait states",
    "StartAt": "FirstState",
    "States": {
      "FirstState": {
        "Type": "Task",
        "Resource": "arn:aws:lambda:${self:provider.region}:${self:custom.config.accountId}:function:${self:service}-${self:custom.config.stage}-first",
        "Next": "wait_using_seconds"
      },
      "wait_using_seconds": {
        "Type": "Wait",
        "Seconds": 10,
        "Next": "FinalState"
      },
      "FinalState": {
        "Type": "Task",
        "Resource": "arn:aws:lambda:${self:provider.region}:${self:custom.config.accountId}:function:${self:service}-${self:custom.config.stage}-second",
        "End": true
      }
  }
}

これに関しては用意されているWaitStateのブループリントをほぼそのまま流用しています。

Lambdaファンクション実装

さて、Lambdaファンクション本体を実装していきますが、上述の通り実験用なのでかなり簡素です。

LambdaA

こいつの役割はIDの一覧を返すことですが、実験用なのでハードコーディングしたIDの配列を返します。
実用化のさいはAPIなりDBなりにアクセスして取得するようになるでしょう

functions/first.js
"use strict";

const firstFunction = (event, context, callback) => {
  console.log("Call First Function");
  callback(null, {
    ids: ["a", "b", "c", "d", "e", "f"]
  });
};

module.exports = firstFunction;

Node.jsで実装する場合は、callbackの第二引数に入れた値がStateMachineに受け渡されます。
他の言語の場合は異なる可能性がありますので、お使いの言語に合わせて実装しましょう。

LambdaB

LambdaBはIDの一覧を分解して、それぞれに対してLambdaCをinvokeします。
LambdaAの結果をどうやって受け取るのかが気になるところでしたが、StepFunctionsで連携させた場合、LambdaAでcallbackの第二引数に入れた値がそのままevent変数として渡されます。

functions/second.js
"use strict";

const AWS = require("aws-sdk");

const secondFunction = (event, context, callback) => {
  console.log("Call Second Function");
  console.log(event);
  const targetLambdaArn = process.env.TARGET_LAMBDA_ARN;

  if (!targetLambdaArn) {
    console.log("no target");
    return callback(null);
  }
  console.log(targetLambdaArn);
  const lambda = new AWS.Lambda();
  const ids = event.ids;

  Promise.all(ids.map((id) => {
    return lambda.invoke({
      FunctionName: targetLambdaArn,
      Payload: JSON.stringify({id: id})
    }).promise();
  })).then(() => {
    return callback(null);
  }).catch((err) => {
    return callback(err);
  });
};

module.exports = secondFunction;

ついでにNode.jsでのlambda.invoke()ですが、FunctionNameはARNでも関数名でもいいようです。基本的に関数名で問題ないでしょうが、より確実性を求めるならARNで指定するのもありでしょう。

LambdaC

LambdaCでは各IDに応じた処理を行います。
実際はそのIDを使ってDBにアクセスみたいな処理になると思いますが、今回は単純にコンソールに出力するだけです。
また、ちゃんと並列になってるかの確認もしたいので、現在のタイムスタンプを出力して2秒ほど待ってからcallbackします。
並列処理になっていれば各タイムスタンプはほぼ同じ時刻を示すはずです。

functions/parallel.js
"use strict";

const moment = require("moment");

const parallelFunction = (event, context, callback) => {
  console.log(event);
  console.log(`now: ${moment().format("X")}`);

  setTimeout(() => {
    callback(null);
  }, 2000)
};

module.exports = parallelFunction;

実行編

ではデプロイします。

sls deploy

うまくいっていればStepFunctionsのコンソールにStateMachineが追加されています。

スクリーンショット 2017-08-11 15.53.21.png

「New execution」をクリックして、最初のファンクションに与えるインプットを入力するとStateMachineが動き出します。

実行中はこんな感じで今どの処理をやってるか確認できます。
この図ですとFirstState(LambdaA)の処理が正常に終了し、Waiting中です。

スクリーンショット 2017-08-11 15.55.55.png

ログ確認

では懸案だったLambdaCの並列分散処理はうまくいっているのか、ログを見てみましょう。

スクリーンショット 2017-08-11 15.57.47.png

ほぼ同時にLogStreamが複数できているので、どうやら分散化はできているようです。

次にコンソールに出力したタイムスタンプを見てみます。
配列内で先頭と末尾であったaとfを比べてみましょう。

スクリーンショット 2017-08-11 16.02.08.png

スクリーンショット 2017-08-11 16.02.39.png

タイムスタンプはmoment().format("X")で出力しているので、秒単位のUNIXタイムスタンプです。
ログを見たところタイムスタンプは同じなので、1秒以内に両者は実行されていたことになります。2秒スリープを入れているのにほぼ同時に実行されているので、並列に実行されているとみなして良いかと思います。

エピローグ ~感想と今後の課題~

さて、初めてStepFunctionsを使ってみたわけですが、当初は「StepFunctionsの中にStateMachineがあって…え〜っと…よくわからん」な感じでしたが、いざ使ってみたらそんなに難しいものではありませんでした。

複雑な処理とかはできるだけLambdaを分割してStepFunctionsで連携させたいところですね。プロダクションにも取り入れるつもりです。

問題なのはStateMachineを実行する手段がコンソールから手動実行かAPIをコールするぐらいで、イベントをトリガーにできないこと。
イベントドリブンで色々やろうとしたら、まずはイベントをトリガーにLambdaを起動し、その中でStateMachineを動かすという手段を取らなければいけません。正直言ってこれはあまりイケてない。

今後の機能追加に期待ですね。

以上、これからStepFunctionsを使おうとしている方の参考になれば幸いです。

参考

続きを読む

AWS Step Functionsを動かしてみる

概要

AWS Step Functionsでバッチ処理っぽいLambdaを動かしてみる。

↓の続きです
AWSでバッチ処理をするときの方法を考える

実践

※参考にしたソースコード
簡単なサーバレスアプリ構築で分かるAWS Lambdaの実装方法の基本
(内容はバケットからバケットにファイルをコピーするLambda)

1. ファイルを放り込むバケット、コピー先のバケットの2つを作成します。

2. 次項で作成するLambdaのIAMロールを作成

ポリシーは↓な感じで
スクリーンショット 2017-08-07 19.38.01.jpg

3. Lambdaを作る

・Blank Function
・トリガーなし
・ランタイム Node.js 6.10
・コード エントリ タイプはインライン編集
・ロールは手順2で作成したものを選択
・タイムアウトは念のため10秒
・他はデフォルト
参考サイトからコードを丸コピします。

'use strict';

// AWS SDK モジュールの読み込み
const aws = require('aws-sdk');
const s3 = new aws.S3();

exports.handler = function(event, context) {
  console.log('Received event:', JSON.stringify(event, null, 2));

  // アップロードされたS3 Bucketの名前とコンテンツのパスを取得
  const uploadBucket = event.Records[0].s3.bucket.name;
  const key = event.Records[0].s3.object.key;

  // getObject APIを使用してS3のコンテンツを取得
  const params = {
    Bucket: uploadBucket,
    Key: key
  };
  s3.getObject(params, function(err, data) {
     if (err) {
      console.log(err, err.stack);
      context.done(err, err.stack);
    } else {
      console.log('data: ', data);

      // コピー先のS3 Bucketの名前を定義してアップロード
      const copyBucket = 'sample-copy-bucket';
      const params = {
        Bucket: copyBucket,
        Key: key,
        Body: data.Body
      };
      s3.putObject(params, function(err, data) {
          if (err) {
            console.log(err, err.stack);
            context.done(err, err.stack);
          } else {
            console.log('data: ', data);
            context.succeed('complete!');
          }
      });
    }
  });
};

※16行目 Bucket: ‘<ファイルを入れるバケットに書き換え>’
※27行目 const copyBucket = ‘<ファイルをコピーするバケットに書き換え>’;

上記で保存します。

4. Step Functinosの作成

AWSコンソールからStep Funcionsを開き、ステートマシンの作成をします。
おもむろにステートマシンに名前をつけ、
次に設計図を選択します。
“Hello World”だと中でLambdaを呼ぶコードが書かれていないので、
Lambdaを呼ぶコードが書いてあるっぽい”Wait ステート”を選んでおきます。
スクリーンショット 2017-08-08 12.06.01.jpg

ステップ3のコードが色々やってて長いので、
余計な部分をそぎ落として↓にしました。

{
  "Comment": "An example of the Amazon States Language using wait states",
  "StartAt": "FirstState",
  "States": {
    "FirstState": {
      "Type": "Task",
      "Resource": "arn:aws:lambda:REGION:ACCOUNT_ID:function:FUNCTION_NAME",
      "End": true
    }
  }
}

※7行目 “Resource”: “<さっき作成したLambdaのarnに置き換える>”,

ビジュアルワークフローの更新ボタンを押して、
StartからEndまで線が繋がっていればおそらく問題ありません。
スクリーンショット 2017-08-08 12.10.24.jpg

上記でステートマシンの作成を押下して、Step Functionsは完成です。

5. 作成したStep Functinosの動作確認

・ファイルを入れるS3バケットに適当なファイル(sample.txt)をアップロードしておきます。
・さっき作成したStep Functinosを選び “新しい実行” を押下します。
・実行のjson画面になりますので、下記を貼り付けます。

{
  "Records": [
    {
      "eventVersion": "2.0",
      "eventTime": "1970-01-01T00:00:00.000Z",
      "requestParameters": {
        "sourceIPAddress": "127.0.0.1"
      },
      "s3": {
        "configurationId": "testConfigRule",
        "object": {
          "eTag": "0123456789abcdef0123456789abcdef",
          "sequencer": "0A1B2C3D4E5F678901",
          "key": "sample.txt",
          "size": 1024
        },
        "bucket": {
          "arn": "arn:aws:s3:::mybucket",
          "name": "sample-upload-bucket",
          "ownerIdentity": {
            "principalId": "EXAMPLE"
          }
        },
        "s3SchemaVersion": "1.0"
      },
      "responseElements": {
        "x-amz-id-2": "EXAMPLE123/5678abcdefghijklambdaisawesome/mnopqrstuvwxyzABCDEFGH",
        "x-amz-request-id": "EXAMPLE123456789"
      },
      "awsRegion": "us-east-1",
      "eventName": "ObjectCreated:Put",
      "userIdentity": {
        "principalId": "EXAMPLE"
      },
      "eventSource": "aws:s3"
    }
  ]
}

※18行目 “arn”: “arn:aws:s3:::<ファイルを入れるバケット名>”,

貼り付けたら “実行の開始” で動作確認ができます。
↓みたいな画面が出たら成功。
スクリーンショット 2017-08-08 12.39.17.jpg

動作確認は以上です。

拡張

作成したサンプルでは “FistState” がいきなり最後の処理になってしましましたが、
↓みたいに “FistState” の後ろに処理をくっつけていけば色々できそう。
スクリーンショット 2017-08-08 12.46.55.jpg

Step Functionsをもっと簡単に作ってみる

AWS SAMでStep Functionsを作ってみる←検証中
ServerlessFrameworkでStep Functionsを作ってみる←検証中

続きを読む

CognitoでAdminCreateUser�時の招待メッセージをカスタマイズする

概要

CognitoではLabmdaトリガーを使って様々な処理を行うことが出来ます。
そのうちの一つとして送信するメッセージのカスタマイズがあるのですが、AdminCreateUserがトリガーイベントの場合のドキュメントがなかったのでメモしておきます。
AWSのドキュメントが更新されたら不要になります。

参考

詳細

AdminCreateUserは管理者がユーザを作成するAPIです。
管理者がユーザを作成すると、一時パスワードが作成されてユーザにメールが届きます。
ユーザがログイン後にパスワードを変更するとCONFIRMED状態になります。

先に結論として、うまく動作したLambdaのソースコードを載せておきます。

def lambda_handler(event, context):
    if event["triggerSource"] == "CustomMessage_AdminCreateUser":
        event["response"] = {
            "emailSubject": "Custom Subject",
            "emailMessage": "Your username is " + event["request"]["usernameParameter"] + "<br>Your temporary password is " + event["request"]["codeParameter"]
        }


    return event

カスタムメッセージのレスポンスは以下のようにドキュメントに記載されています。

"response": {
    "smsMessage": "string",
    "emailMessage": "string",
    "emailSubject": "string";
}

SMSのメッセージとメールの件名・本文が変更可能なようです。
emailMessageの説明は以下のようになっています。

emailMessage
ユーザーに送信されるカスタム E メールメッセージ。リクエストで受信される codeParameter 値を含める必要があります。

なるほど codeParameter を入れれば良いのか!ということでドキュメントのサンプルコードを参考に以下のようなコードを書きます。

event["response"]["emailSubject"] = "Welcome to the service"
event["response"]["emailMessage"] = "Your confirmation code is " + event["request"]["codeParameter"]

しかし、件名は変わるものの本文は変わりません。

ドキュメントどおりにやっているのに動かない…ということで他の人がどうやっているのか調べていったところ、レスポンスに usernameParameter を含めていることが分かりました。
これはリクエストに含まれており、 codeParameter のようなものです。
そこで usernameParameter を入れたところあっさり成功しました。

まとめ

usernameParameter についてAWSのドキュメントを探しても全く見つかりませんでしたが、AdminCreateUser時の招待メッセージをカスタマイズしたい場合は必要なようでした。

続きを読む

Amazon API Gateway + AWS Lambdaで、iOSでの定期購読をリアルタイムでSlackへ投稿する

2017年7月18日のnewsで、自動更新登録を含むすべてのレシートに、カスタマーの登録のステータスに関するリアルタイムの情報が含まれるように、なったためリアルタイムでslack通知するためにサクッと作りました。
Amazon API Gateway + AWS Lambda のレシピ用意されてて簡単だったので、ぜひ :roller_coaster:

構成

server-notifications.jpeg

  1. iTunes Connect

    • Server notifications for auto-renewable subscriptions
  2. Amazon API Gateway
  3. AWS Lambda
  4. Slack

Lambda 準備

Amazon API Gateway + AWS Lambda の構成はレシピ用意されている。
今回は、python3.6のfunctionを利用(node.js・python2.7も用意されている)

Lambda code

Webhook URLを取得して以下のコード
http://qiita.com/vmmhypervisor/items/18c99624a84df8b31008

import json
import urllib.request
import time

print('Loading function')

def respond(err, res=None):
    return {
        'statusCode': '400' if err else '200',
        'body': err.message if err else json.dumps(res),
        'headers': {
            'Content-Type': 'application/json',
        },
    }

class Receipt:
    def __init__(self, params):
         # 必要に応じて追加する
        self.notification_type = params['notification_type']
        self.product_id        = params['latest_receipt_info']['product_id']
        self.bid               = params['latest_receipt_info']['bid']

def send_slack(channel, receipt): 
    # slack webhook URLは入れ替え 
    url = 'https://hooks.slack.com/services/xxxxxxxxx/xxxxxxxxxxx'
    attachments = [
            {
                "fallback": "iOS Subscription Report",
                "color": "#36a64f",
                "author_name": "mikan",
                "author_link": "http://mikan.link",
                "author_icon": "http://flickr.com/icons/bobby.jpg",
                "title": "購読レポート",
                "text": "App: " + receipt.bid,
                "fields": [
                    {
                        "title": "購読",
                        "value": receipt.notification_type,
                        "short": False
                    },
                    {
                        "title": "商品id",
                        "value": receipt.product_id,
                        "short": False
                    }
                ],
                "image_url": "http://my-website.com/path/to/image.jpg",
                "thumb_url": "http://example.com/path/to/thumb.png",
                "footer": "Server notifications for auto-renewable subscriptions",
                "footer_icon": "https://platform.slack-edge.com/img/default_application_icon.png",
                "ts": time.time()
            }
        ]
    params = {"channel": channel, "username": "dekopon", "attachments": attachments}
    params = json.dumps(params).encode("utf-8")
    req = urllib.request.Request(url, data=params, method="POST")
    res = urllib.request.urlopen(req)

def lambda_handler(event, context):
    params = json.loads(json.dumps(event))
    body = params['body']
    receipt = Receipt(json.loads(body))
    send_slack("playground", receipt)
    return respond(None, "Post slack successfully!")

処理できるかテスト

以下のような方法で、jsonをPOSTしてみてテスト
sample jsonは記事下部に掲載しておきました。

  • Lambdaでテストイベントを設定して実行
  • curlやDHCでPOST実行

Subscription Status URL を設定

iTunes ConnectのMy Appsから、対象アプリを選択して、Subscription Status URLに作成したAPI GateWayのエンドポイントを指定
App StoreApp InformationSubscription Status URL

https://help.apple.com/itunes-connect/developer/#/dev0067a330b

あとは購読してもらえるものを作り込みましょう :rocket:

sample json

{
    "body": {
        "auto_renew_product_id": "link.mikan.sub.sample", 
        "auto_renew_status": "true", 
        "environment": "PROD", 
        "latest_receipt": "xxxxxxxxx", 
        "latest_receipt_info": {
            "app_item_id": "xxxxxxxxx", 
            "bid": "link.mikan.sample", 
            "bvrs": "1", 
            "expires_date": "123456", 
            "expires_date_formatted": "2017-09-05 22:19:54 Etc/GMT", 
            "expires_date_formatted_pst": "2017-09-05 15:19:54 America/Los_Angeles", 
            "item_id": "xxxxxxxxx", 
            "original_purchase_date": "2017-08-05 22:19:56 Etc/GMT", 
            "original_purchase_date_ms": "1501971596000", 
            "original_purchase_date_pst": "2017-08-05 15:19:56 America/Los_Angeles", 
            "original_transaction_id": "730000169590752", 
            "product_id": "link.mikan.sub.sample", 
            "purchase_date": "2017-08-05 22:19:54 Etc/GMT", 
            "purchase_date_ms": "1501971594000", 
            "purchase_date_pst": "2017-08-05 15:19:54 America/Los_Angeles", 
            "quantity": "1", 
            "transaction_id": "xxxxxxxxx", 
            "unique_identifier": "xxxxxxxxx", 
            "unique_vendor_identifier": "xxxxxxxxx", 
            "version_external_identifier": "xxxxxxxxx", 
            "web_order_line_item_id": "xxxxxxxxx"
        }, 
        "notification_type": "INITIAL_BUY", 
        "password": "xxxxxxxxx"
    }
}

参考

Lambda 関数を公開するための API を作成する
Amazon API Gatewayを使ってAWS LambdaをSDKなしでHTTPS越しに操作する
Advanced StoreKit – WWDC 2017 – Videos – Apple Developer

続きを読む

API Gateway + Lambdaで任意のURLにリダイレクトする方法

Web開発をしているときに、クライアントからアクセスされたサーバーでリダレクトさせたい場合があるとします。

そのとき、サーバーを立ててWebサーバーをインストールしてredirectの設定をして…等行うと思いますが、それをAWSのサービスでサーバレスに行う方法を紹介します。

これは、

「Webサーバーリクエストを受け付けて、リダイレクト処理 + ちょっとしたロジック」

「API Gatewayでリクエストを受け付けて、Lambdaでリダレクト用のヘッダーを作成 + ちょっとしたロジック。それを、API Gatewayでまた返却」

という形で実現します。

使用するサービスはタイトルにもあるように、

  • API Gateway
  • AWS Lambda

Lambda上のランタイムは、

version
Node.js v6.10

となります。

これら作るのは

あるURLにGETメソッドのクエリパラメータにリダイレクトするという機構です。

さて、実際には以下の作業を行っていきます。

  • Lambdaにロジックを記述
  • API Gatewayの設定をする

Lambdaを先に設定するのは、API Gatewayの設定中、Lambdaを設定する箇所が出てくるからです。

Lambdaにロジックを記述

以下のようにコードを記述します。

index.js
exports.handler = (event, context, callback) => {

    var location = decodeURI(event["params"]["target"])

    context.succeed({"Location": location});
};

event["params"]["target"] : eventオブジェクトのtargetがgetのパラメータと紐付いてます。

API Gatewayの設定をする

  • APIの作成
  • Lambdaの設定
  • 統合リクエストの設定
  • メソッドレスポンスの設定
  • 統合レスポンスの設定

APIの作成/Lambdaの設定

統合リクエスト

[本文マッピングテンプレート]から[Content-Type]として、 application/json を追加します。

スクリーンショット 2017-08-03 16.04.01.png

テンプレートとしては、以下を追加します。

{
  "params": {
    #foreach($param in $input.params().querystring.keySet())
    "$param": "$util.escapeJavaScript($input.params().querystring.get($param))" #if($foreach.hasNext),#end
    #end
  }
}

これにより、GETメソッドのparameterをeventオブジェクトのkey, objectに展開できます。

メソッドレスポンスの設定

デフォルトで、HTTPのステータスコードが200がレスポンスとして設定されているので、それを削除します。

[レスポンスの追加]より、ステータスコード302を追加します。

[302 のレスポンスヘッダー]において、

Locationを追加します。

スクリーンショット 2017-08-03 15.53.11.png

統合レスポンスの設定

[メソッドレスポンスのステータス]が302のものを追加します。ステータスが200のものは消します。

次に
[ヘッダーのマッピング]でメソッドレスポンスより追加した「Location」がレスポンスヘッダーとして表示されているので、それに対応するマッピングの値として、 integrtion.response.body.Location を追加します。

スクリーンショット 2017-08-03 15.55.12.png

これにより、LambdaでのLocationの値が、レスポンスヘッダー「Location」にマッピングされるようになります。

実行してみる

[ステージ名]を test 等にして、APIをDeployすると、

 https://hogehoge.execute-api.ap-northeast-1.amazonaws.com/test

と呼び出し用のURLが発行されます。

https://hogehoge.execute-api.ap-northeast-1.amazonaws.com/test?target=https%3A%2F%2Fwww.google.co.jp

のようにブラウザからアクセスしてみましょう。リダイレクトされるはずです。

参考

続きを読む