Serverless FrameworkでChatwork通知用APIを作成する方法

こんにちは。串上です。
前回ReactとAWS API Gatewayの連携方法を書いた者です。

前回の記事を読んでいただいた方は思ったかもしれませんが、API Gatewayって手動で設定するの面倒ですよね。

そこで今回はそのあたりの設定をコマンド一発で自動化してくれる素敵なフレームワークであるServerless Framework(v1.1)について書きたいと思います。 https://serverless.com/

Serverless Frameworkを利用するにあたって、 AWS CLIAWSコマンドラインからごにょごにょするツール)をインストールする必要があるので、まずはそちらから。

pipをインストール

curl "https://bootstrap.pypa.io/get-pip.py" -o "get-pip.py"
sudo python get-pip.py

え、AWS CLIちゃうん?って思った方、そうあせらないでください。
AWS CLIをインストールするためにはPythonのパッケージ管理ツールpipが必要なんです。
とりあえず入れて下さい。

こんな感じになればインストール完了です。

Collecting pip
  Downloading pip-9.0.1-py2.py3-none-any.whl (1.3MB)
    100% |████████████████████████████████| 1.3MB 816kB/s
Collecting wheel
  Downloading wheel-0.29.0-py2.py3-none-any.whl (66kB)
    100% |████████████████████████████████| 71kB 5.9MB/s

次に行きます。

AWS CLIをインストール

sudo pip install awscli

で、OKと思ったらsixがどうちゃら言われてエラーが出たら、以下の魔法版で実行して下さい。

sudo pip install awscli --upgrade --ignore-installed six

するとこんな感じにインストールが完了します。

Collecting awscli
  Downloading awscli-1.11.15-py2.py3-none-any.whl (1.0MB)
    100% |████████████████████████████████| 1.0MB 804kB/s
Collecting six
  Downloading six-1.10.0-py2.py3-none-any.whl
...
Successfully installed awscli-1.11.15 botocore-1.4.72 colorama-0.3.7 docutils-0.12 futures-3.0.5 jmespath-0.9.0 pyasn1-0.1.9 python-dateutil-2.6.0 rsa-3.4.2 s3transfer-0.1.9 six-1.10.0

AWS CLIのインストールは完了しましたが、このままではまだ「だれのAWSに接続するん?」って話なんで、そのあたりも設定していきます。

AWS CLIの設定

aws configure

ってコマンドを打つと、以下のように質問してくるので答えます。

AWS Access Key ID [None]: xxxxxxxxxx
AWS Secret Access Key [None]: yyyyyyyyyy
Default region name [None]: ap-northeast-1
Default output format [None]: json

region nameとoutput formatは好きに変えて下さい。
自分はjson好きの日本在住なので上記の設定にしています。

xxxxxxxxxxとyyyyyyyyyyはAWSのユーザーを作成したときの一回だけしかダウンロードまたは表示ができないアレです。
一回だけだなんて厳しいですよね。
しかもユーザー作成したときなんて何にもわかってないときやのに、ダウンロードってなんやねんって感じですよね。
でもダウンロードはしといてください。
そして安全な場所に保管しておいて下さい。
確かファイル名はcredentialsだったような。

これが完了すると、~/.aws/credentialsっていうファイルが出来ます。 中身はこんな感じ。

[default]
aws_access_key_id = xxxxxxxxxx
aws_secret_access_key = yyyyyyyyyy

これでAWS CLIが使えるようになったので、ついにServerless Frameworkをインストールします。

※Serverless Frameworkをインストールするにはnpmが必要です。
npmって何?ってひとはこちらを参考にしてください。

Serverless Frameworkをインストール

npm install -g serverless

こんな感じになればインストール完了です。

└─┬ serverless@1.1.0 
  ├── agent-base@2.0.1 
  ├── ansi-regex@2.0.0 
  ├── ansi-styles@2.2.1 
  ├─┬ archiver@1.1.0 
  │ └── async@2.1.1 
...

これでserverlessコマンドが使えるようになったんですが、いちいちserverlessとか長いしタイポしそうなんで、
slsって短縮コマンドも用意されています。

早速プロジェクトを作成しましょう!

Serverless Frameworkでプロジェクトを作成

mkdir serverless-chatwork
cd serverless-chatwork
sls create --template=aws-nodejs

するとこんな感じにカッコイイロゴが表示されれば、プロジェクトの作成は完了です。

Serverless: Generating boilerplate…
 _______                             __
|   _   .-----.----.--.--.-----.----|  .-----.-----.-----.
|   |___|  -__|   _|  |  |  -__|   _|  |  -__|__ --|__ --|
|____   |_____|__|  \___/|_____|__| |__|_____|_____|_____|
|   |   |             The Serverless Application Framework
|       |                           serverless.com, v1.1.0
 -------'

Serverless: Successfully generated boilerplate for template: "aws-nodejs"
Serverless: NOTE: Please update the "service" property in serverless.yml with your service name

nodejs?僕パイソニスタなんだけど。って人はpython版で作成してください。 templateのaws-nodejsをaws-pythonに変更すればOKです。

ではプロジェクトの設定をしていきましょう。

Serverless Frameworkプロジェクトの設定

serverless.ymlというファイルがあるので以下のように編集します。

service: serverless-chatwork # プロジェクト名です。好きな値をどうぞ。

provider:
  name: aws
  runtime: nodejs4.3
#  stage: dev # デプロイする環境。デフォルトではdevになってる。開発環境なんていらんよ。って人はproduction等に変更しましょう。
  profile: default # ~/.aws/credentialsの中の[]の中の文字です。defaultの場合はこの行削除してもいいです。
  region: ap-northeast-1 # AWSのリージョンです。東京に変更したい場合はこんな感じ。

functions:
  push: # 作成するLambdaの名前
    handler: handler.push # handler.js内の関数名
    events:
      - http:
          path: v1/push # API Gatewayのエンドポイント v1とかバージョンを作っとくと大規模な改修の際に便利かも?
          method: post # get, post, put, delete等
          cors: true # これ設定するとCORSが有効になって外部からも叩けるよ

plugins:
  - serverless-run-function-plugin # あとで説明するけど、これ入れとくとローカルでテストできます。

上の設定で出て来るserverless-run-function-pluginですが、デプロイする前にローカルでAPIのテスト出来るようになるので入れておきましょう。

serverless-run-function-pluginのインストール

npm install -g serverless-run-function-plugin

早速ローカルでテストを実行する前に、handler.jsも修正しましょう。 handler.jsはLambdaの中身になります。

handler.jsとevent.jsonの設定

今回は以下のように修正して下さい。 簡単に補足するとevent.jsonはコマンドから実行するときにhandler.jsのeventに任意の値を設定するためのものです。

handler.js

'use strict'; // 厳しいモードでいきましょう。

const request = require('request');

const CHATWORK_TOKEN = 'チャットワークAPIを利用するためのトークン';

module.exports.push = (event, context, callback) => {
  var response = {
    statusCode: 200,
    // CORSを有効にする場合は設定必要。*は全てなので許可する範囲を設定したい人は修正してください。
    headers: {
      "Access-Control-Allow-Origin" : "*"
    },
  };

  // postでjson文字列を送信した場合、受け取ったデータは全てevent.bodyに入っています
  const post = JSON.parse(event.body);

  // room_idは必須にする
  if(typeof post.room_id == 'undefined'){
    response.statusCode = 400;
    response.body = JSON.stringify({ message: 'room_idを指定して下さい。' });
    return callback(null, response);
  }

  // textは必須にする
  if(typeof post.text == 'undefined'){
    response.statusCode = 400;
    response.body = JSON.stringify({ message: 'textを指定して下さい。' });
    return callback(null, response);
  }

  // チャットワークにチャット送信
  pushToChatwork(post.room_id, post.text);

  response.body = JSON.stringify({ message: '送信完了' });
  callback(null, response);

};

// チャットワークにチャット送信
const pushToChatwork = (room_id, text) => {
  var options = {
    url: 'https://api.chatwork.com/v1/rooms/' + room_id + '/messages',
    headers: {
      'X-ChatWorkToken': CHATWORK_TOKEN
    },
    form: { body: text },
    json: true
  };
  request.post(options);
}

event.json

{
  "body": "{\"room_id\": \"チャットワークのルームID\", \"text\": \"投稿したい文字列\"}"
}

また、今回requestパッケージを使用したので、npmの下準備をしてインストールしましょう。

npm init

Press ^C at any time to quit.
name: (serverless-chatwork) 
version: (1.0.0) 
description: Chatwork API with Serverless Framework
entry point: (handler.js) 
test command: 
git repository: 
keywords: 
author: 
license: (ISC) 
About to write to /Users/shunkushigami/tmp/serverless-chatwork/package.json:

{
  "name": "serverless-chatwork",
  "version": "1.0.0",
  "description": "Chatwork API with Serverless Framework",
  "main": "handler.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC"
}

requestのインストール

npm install request --save

これでnode_modulesというディレクトリが出来て、デプロイ時に一緒にアップしてくれます。

ローカルでテストを実行

sls run -f push

-fの後に実行する関数名を指定すると実行してくれます。

こんな感じに表示されて、

Serverless: -----------------
Serverless: Success! - This Response Was Returned:
Serverless: {
    "statusCode": 200,
    "headers": {
        "Access-Control-Allow-Origin": "*"
    },
    "body": "{\"message\":\"送信完了\"}"
}

指定のルームにチャットが届けば成功です!

今のままではローカルでしか実行できないので、いよいよデプロイします。

Serverless FrameworkでAWSにデプロイする

sls deploy

たったのこれだけです。
Serverless Frameworkやばいですね。 これだけでCloudFormationを作成してくれて、それらをS3にアップしてくれて、Lambda作ってくれて、API Gateway作ってくれます。

こんな感じでエンドポイントが表示されればデプロイ完了です。

Serverless: Creating Stack…
Serverless: Checking Stack create progress…
..
Serverless: Stack create finished…
Serverless: Deprecation Notice: Starting with the next update, we will drop support for Lambda to implicitly create LogGroups. Please remove your log groups and set "provider.cfLogs: true", for CloudFormation to explicitly create them for you.
Serverless: Packaging service…
Serverless: Uploading CloudFormation file to S3…
Serverless: Uploading service .zip file to S3…
Serverless: Updating Stack…
Serverless: Checking Stack update progress…
.................
Serverless: Stack update finished…

Service Information
service: serverless-chatwork
stage: dev
region: ap-northeast-1
api keys:
  None
endpoints:
  POST - https://xxxxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/dev/v1/push
functions:
  serverless-chatwork-dev-push: arn:aws:lambda:ap-northeast-1:xxxxxxxxxxxxxxxx:function:serverless-chatwork-dev-push

では、AWSにアップされたようなのでコマンドから実際にエンドポイントが効いているか試してみましょう。

Serverless FrameworkでAPIをコールする

sls invoke -f push -p event.json

このコマンドで実際にリモートのAPIをevent.jsonの内容でコールしてくれます。
-pで使用するevent用jsonを指定します。

以下のように表示されて、実際にチャットが届けば完了です!

{
    "statusCode": 200,
    "headers": {
        "Access-Control-Allow-Origin": "*"
    },
    "body": "{\"message\":\"送信完了\"}"
}

作ったはいいけど、他の人に試してもらうためには以下のように教えてあげましょう。

jQueryから試す

jQueryマイスターには以下で試してもらいましょう。

var data = {
    room_id: "チャットワークのルームID",
    text: "投稿したい文字列"
}
$.ajax({
    url: "生成したエンドポイント",
    type: "POST",
    dataType: "JSON",
    data: JSON.stringify(data)
});

Curlから試す

コマンドライン大好きな人には以下で試してもらいましょう。

curl 生成したエンドポイント -X POST -d "{\"room_id\": "チャットワークのルームID", \"text\": \"投稿したい文字列\"}"

まとめ

以上、長々と書きましたが、Serverless Frameworkを実践的に使用する方法を解説しました。
Serverless Frameworkとかサーバーレス自体に興味ある人はぜひ試してみて下さい。

次回以降はDynamoDBとの連携方法や、Lambdaの定期実行等も書いていきたいと思いますので、乞うご期待!

お疲れ様でした。

テストで保護してリファクタリングする方法

こんにちは。エンジニアの中井です。

コードが複雑化したり規模が大きくなってくると、少しの変更でも、その変更の影響がどこまで及ぶかわからないということが起こると思います。このとき、コードの複雑さを解消するためにリファクタリングを行うのが一つの解決策です。

ただ、リファクタリングによってコードをむやみに変更した場合、依存する処理を壊してしまうリスクもあります。かといってそのまま放置しているとメンテナンスにかかる手間が大きくなる一方なので、どんどんと修正を行いづらい・ビジネスの要求に応えづらいコードベースになっていきます。

依存する処理を壊さずにリファクタリングするには、コードをテストコードで保護しておくと安心です。
このエントリでは、安心して気軽にリファクタリングするために、コードをテストコードで保護してからリファクタリングする流れについて、例を挙げてご紹介します。
記事内のコードはPHPを、テストフレームワークにはPHPUnitを使用します。

1. リファクタリング対象の設定

ここに「日付を渡すと午前か午後かを判定する (午前の場合は true を返す)」仕様のプログラムがあるとします。
日付は "Y-m-d H:i:s" 形式の文字列で渡されるものとし、現在これに対応するテストコードはないとします。

<?php

class Example
{
    public static function isAM($dateString)
    {
        preg_match('/^\d{4}-\d{2}-\d{2} (\d{2}):\d{2}:\d{2}$/', $dateString, $matches);

        if ($matches[1] >= 0) {
            if ($matches[1] < 12) {
                return true;
            } else {
                return false;
            }
        } else {
            return false;
        }
    }
}

このコードを簡単にリファクタリングしてみます。方針として、以下2点を行ってみます。

  • 正規表現でのチェックを DateTime クラスを使うようにする
  • if 文のネストを浅くする

なお、記載をシンプルにするため、日付文字列以外が渡された場合・2016-01-32など不適切な日付が渡された場合などの異常系は考慮しないものとします。

2. 現状のコードにテストコードを書く

まずは上記のコードに対応するテストコードを書きます。

<?php
use PHPUnit\Framework\TestCase;

class ExampleTest extends TestCase
{
    public function testAM()
    {
        $actual = Example::isAM('2016-11-01 00:00:00');
        $this->assertTrue($actual);
    }

    public function testPM()
    {
        $actual = Example::isAM('2016-11-01 13:00:00');
        $this->assertFalse($actual);
    }
}

このテストが通ることを確認します。

$ phpunit ExampleTest.php
..                                             2 / 2 (100%)

Time: 35 ms, Memory: 3.00MB

OK (2 tests, 2 assertions)

3. リファクタリングする

<?php

class Example
{
    public static function isAM($dateString)
    {
        $dateTime = new \DateTime($dateString);
        $hour = $dateTime->format('G');

        return ($hour < 12);
    }
}

1.の方針どおり DateTime クラスを使うようにし、午前か午後かを判定する if 文を単純にしました。

これでテストが落ちないことを確認します。

$ phpunit ExampleTest.php
..                                              2 / 2 (100%)

Time: 35 ms, Memory: 3.00MB

OK (2 tests, 2 assertions)

おわりに

テストコードで保護した上で、コードをリファクタリングする流れについて簡単にご紹介しました。
実際のプロダクションコードではビジネスロジックが複雑でテストが書きづらいケースなどがあり、もう少し複雑なアプローチが必要かもしれません。「レガシーコード改善ガイド」などで様々なテクニックが書かれているので、参考にしてみると良いでしょう。

Aurora リーダーエンドポイントによる負荷分散

今日は最近発表された、Amazon Aurora リーダーエンドポイント を利用して 比較的簡易にデータベースの負荷分散を行ってみたいと思います。

概要

  • 書き込みも読み込みにも対応する本体(Writer) 1台、読み込み専用Auroraインスタンス(Reader(リードレプリカ)) 2台の構成を作成します。
  • 任意の読み込みクエリーを、Writerではなく、Readerに投げるようにし、かつ均等に分散させるようにします。 f:id:i-plug-develop:20161109095915p:plain

まず既存のAurora1台の状況からリードレプリカ2台を増やす手順

  • 現在の、Auroraインスタンスを選択します。
  • インスタンスの操作」「Aurora レプリカの作成」と進めて、
  • インスタンスタイプを選び、作成します。
  • パラメーターグループがデフォルトになってしまうので、 もしカスタマイズしたものを使用していた場合は、そちらに変更して再起動が必要となります。この場合、
    • インスタンスの操作」「変更」「データベースの設定」「DBパラメータグループ」と進めて、
      • 「パラメータグループ」でカスタマイズしたパラメーターグループを選択し、 「すぐに適用」にチェックを入れます。
    • そのリードレプリカを再起動します。
  • これを繰り返して本体である Writer 1台、Reader(リードレプリカ) 2台の構成にします。

アプリケーション側(FuelPHPでの一例)で必要な設定

  • アプリケーションの設定ファイルに接続すべきデータベースとして、上で作成したリードリプリカを登録します。
    この時にホスト名として Aurora リーダーエンドポイントを「一つ」指定することで、存在するReaderインスタンスの中から均等にクエリーを投げるようになります。
    • APPPATH/config/production/db.php
     'readreplica' => array(
         'connection'  => array(
             'dsn'        => 'mysql:host=<リーダーエンドポイント>;dbname='.$dbname,
             'username'   => $username,
             'password'   => $password,
         )
     ),
  • APPPATH/config/db.php
     'readreplica' => array(
              'type'        => 'pdo',
              'connection'  => array(
                      'persistent' => false,
              ),
              'identifier'   => '`',
              'table_prefix' => '',
              'charset'      => 'utf8',
              'enable_cache' => true,
              'profiling'    => true,
      ),
  • プログラム中で接続先を上記設定ファイルで指定した「readreplica」とすることで、複数存在するリードレプリカへ分散してクエリーを投げることになります。