【注意】Amazon Aurora(MySQL)でZero Downtime Patch実行時に不具合が発生するケースがある【AWS】

はじめに

先日、Amazon Aurora(MySQL)の必須アップデートの案内が来たのでアップデートの検証作業を実施しておりました。
Zero Downtime Patch(以下、ZDP)が実行され、ダウンタイム無しでアップデートが行われるはずが、不具合が発生してしまいました。

ググっても関連する情報が見つからなかったので、取り急ぎ分かっている情報について記録しておきます。

なお、対象のアップデートは下記のものになります。(Cluster Version 1.14)

Database Engine Updates 2017-08-07
http://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/Aurora.DatabaseEngineUpdates.20170807.html

ポイント(TL;DR)

重要なポイントは下記のとおりです。

Aurora(MySQL)に対し、DB Connection Poolを利用し、且つPrepared Statementを利用していると、Zero Downtime Patch実行時に不具合が発生する可能性が高い。
Aurora(MySQL)のアップデートを行う前に、アプリの構成・実装を調べた方が良い。

Zero Downtime Patch(ZDP)とは何か

Auroraを無停止(正確には5秒程度の遅延)でアップデートできる画期的な仕組みです。
アップデート処理中にも、クライアントからのDB接続は維持されます。

今回、Cluster Version 1.14へのアップデートとなりますが、本アップデートに関するドキュメントにも説明があります。
http://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/Aurora.DatabaseEngineUpdates.20170807.html

AuroraのCluster Version 1.10で初めて導入されたようです。
ZDPの内容について、下記ドキュメントにも記載があります。

Database Engine Updates 2016-12-14
http://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/Aurora.DatabaseEngineUpdates.20161214.html

こちらの記事も情報がまとまっていて参考になります。

Amazon Aurora のアップグレード
https://qiita.com/tonishy/items/542f7dd10cc43fd299ab

事象の内容

Aurora(MySQL)でClusterのアップデートが必要の旨の案内が来たため、AWS Management Consoleからアップデートを行いました。
AuroraのClusterアップデートは無事に完了しましたが、当該Auroraインスタンスを利用しているJavaアプリが軒並みエラーを吐き出し始めました。
その後Javaアプリを再起動し、復旧しました。

AWS Management Console上、WriterとなっているインスタンスのEventに下記のものが表示されました。
従って、Zero Downtime Patchが行われていたことは間違いありません。

The database was successfully patched with a zero downtime patch.

Javaアプリのログ上には、「Unknown prepared statement handler」といったエラーが多数出力されておりました。

事象の原因

当該JavaアプリはJDBC Connection Poolを利用し、DBとの接続を維持する構成になっています。
また、Prepared Statementを利用する実装になっております。

ZDP実行時、MySQLのクライアント(i.e. Javaアプリ)からの接続は維持されるようですが、Prepared Statementは維持されないことが原因のようでした。
時系列的に整理すると、下記のような状況になっていたようです。

  1. Javaアプリ起動時、Auroraに対して幾つかJDBC接続を確立する
  2. リクエストに応じて、JavaアプリからAuroraにクエリを発行する。この際、Prepareを発行してPrepared Statementを作る。
    Javaアプリは、同一のリクエストに対してはPrepared Statementを再利用する。
  3. AuroraのアップデートをZDPで行う。この際、JavaアプリからのJDBC接続は維持されるが、Prepared Statementは全てクリアされる。
  4. Javaアプリに対し、項番2と同様のリクエストを行う。Javaアプリは、コンパイル済みのPrepared Statementが存在しているという認識のまま、これを再利用しようとし、エラーとなる。

同一のアプリに対し、Prepared Statementを利用するもの/しないものを用意してAuroraのアップデートを実施しました。
結果、前者はエラーが発生し、後者はエラーが発生しませんでした。

なお上記の挙動は、独自に調査・検証したものとなります。
現在、AWSの公式見解・ドキュメント等の情報は見つけられておりません。
# 一応、Database Engine Updates 2016-12-14には以下の記載がありますが、これが該当するかどうかは不明です。
# Note that temporary data like that in the performance schema is reset during the upgrade process.

2017/9/26にAWSサポートへの問い合わせを行っておりますが、2017/10/19現在、回答待ちの状況です。

確認した環境・構成

上記事象は、下記構成のJavaアプリで確認しました。

  • Java 8 (Oracle JDK)
  • spring-boot-1.4.0.RELEASE
  • mybatis-spring-boot-starter 1.1.1
  • mariadb-java-client 1.5.5

併せてMyBatisGeneratorも利用しています。
特別な理由がない限り、JavaアプリからはMyBatisGeneratorで生成されたMapperとClassを利用しています。

デフォルトでは、MyBatisGeneratorではPrepared Statementを利用するMapperを生成するようです。

回避策

回避策は概ね以下の3通りになると思います。

1.Prepared Statementを利用しないよう、アプリを変更する。

ソースコード/ORマッパーの改修・設定変更を行い、Prepared Statementを利用しないようにする方法です。
おそらく、この対応は難しいケースがほとんどだと思います。

2.MySQL接続ドライバの設定で、Prepared Statementを利用しないよう変更する。 

今回のケースでは、MariaDB Connector/Jを利用しており、オプションを指定することでPrepared Statementを利用しないように変更することができました。

useServerPrepStmtsオプションをfalseにすることで実現可能です。
ちなみに、1.6.0以降のバージョンはデフォルトでfalseになっているようですので、これ以降のバージョンを利用してる場合は、本件の影響を受けません。

About MariaDB Connector/J -> Optional URL parameters -> Essential options
https://mariadb.com/kb/en/library/about-mariadb-connector-j/

3.ZDPではない、通常のアップデートを実施する

Auroraインスタンスの再起動を伴う、通常のアップデートを実行することで、本件事象を回避することが可能と考えられます(※未検証)
20~30秒程度のダウンタイムが発生します。

下記に、ZDPにならないケースについて記載がありますので、このどれかを満たせば通常のアップデートになると推測できます(※未検証)

Database Engine Updates 2017-08-07 -> Zero-Downtime Patching
http://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/Aurora.DatabaseEngineUpdates.20170807.html

  • Long-running queries are in progress
  • Open long-running transactions exist
  • Binary logging is enabled
  • Binary log replication is running
  • Pending parameter changes exist
  • Temporary tables are in use
  • Table locks are in use
  • Open SSL connections exist

まとめ

DBへのConnection Poolを利用し、且つPrepared Statementを利用するようアプリを実装していると、不具合が発生する可能性が高いです。

経験的な感覚ですが、JavaアプリはConnection Poolを利用しているケースが多いかと思います。
Aurora(MySQL)を利用している場合は、チェックをした方が良いかと思います。

検証作業について

古いClusterバージョンのAuroraインスタンスを作ることはできません。AWSサポートにも聞いてみましたが無理なようでした。
従って、運良く過去に構築した古いAuroraインスタンスが存在していれば検証は可能ですが、それ以外のケースでは検証手段がありません。

続きを読む

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で完結するのが、アプリ屋にとっては本当うれしいです。

続きを読む

CodeBuildのbuildspec.ymlを別の名前にしたいときの手順

TL;DR

CodeBuildを使っていると、ビルドの成果物が違うからビルドスペックのファイル(buildspec.ymlというもの)の単位で成果物を分けたいということがあります。調べてみると、ドキュメントに1にやり方が載っていたのでやってみました。方向性としては、AWS CLIで変更操作をする手順です。2

(2017-10-13 22:00 追記。 今さっき見てみたら、Consoleでファイル名を指定できるようになっていました。ちょろっと直すならConsoleでいいですね。)

概要

nantoka-buildというプロジェクトのビルドスペックのファイルをnantoka-kantoka-buildspec.ymlに変えたいとします。windowsの例ですが、mac でも linuxでも概ね同じでしょう。

プロジェクトの名前を確認する

Consoleから確認することもできますが、CLIと同じものが見えているか確認する意味も込めてやってみましょう。

>aws codebuild list-projects
{
    "projects": [
        "nantoka-build",
        "kantoka-build-project",
        "test-test-project"
    ]
}

>

各環境とかリージョンとかで色々あると思いますが、Console 
https://ap-northeast-1.console.aws.amazon.com/codebuild/home?region=ap-northeast-1#/projects
で、見えるプロジェクトとおなじビルドプロジェクトが見えていればOKです。nantoka-build以外のプロジェクトも、存在するなら見えていることでしょう。

プロジェクト名を指定して、プロジェクト構造のJSONファイルを取得する

更新の操作はプロジェクト構造のJSONファイルをアップロードする形になります。アップロードするファイルを作るため、現状のファイルをダウンロードします。

>aws codebuild batch-get-projects --names nantoka-build
{
    "projectsNotFound": [],
    "projects": [
        {
            "name": "nantoka-build",
            "serviceRole": "arn:aws:iam::1234123412341234:role/service-role/nantokarole",
            "tags": [],
            "artifacts": {
                "packaging": "NONE",
                "type": "CODEPIPELINE",
                "name": "nantoka-artifact"
            },
            "lastModified": 1512341234.783,
            "timeoutInMinutes": 60,
            "created": 1512341234.68,
            "environment": {
                "computeType": "BUILD_GENERAL1_SMALL",
                "privilegedMode": false,
                "image": "aws/codebuild/java:openjdk-8",
                "type": "LINUX_CONTAINER",
                "environmentVariables": []
            },
            "source": {
                "type": "CODEPIPELINE"
            },
            "encryptionKey": "arn:aws:kms:ap-northeast-1:1234123412341234:alias/aws/s3",
            "arn": "arn:aws:codebuild:ap-northeast-1:1234123412341234:project/nantokanantoka"
        }
    ]
}

>

(arnなどは内容を適当にいじっています。表示される項目はあなたのプロジェクトの内容と同じはずです。)

プロジェクト構造のJSONファイルを

コマンドプロンプトに主力されたJSONをファイルに落として修正を入れます。

  • JSONのルートの"projects"の配下を取り出す。
  • "buildspec": "nantoka-kantoka-buildspec.yml"という1行を入れる。JSONなのでカンマ忘れずに。
  • "lastModified" "created" "arn" は消す。JSONの構造が違うというエラーになるので。無くても多分実害ないと想像する次第。本当は正しい書き方あると思いますが未確認です。
update.buildproject.json
{
    "name": "nantoka-build",
    "serviceRole": "arn:aws:iam::1234123412341234:role/service-role/nantokarole",
    "tags": [],
    "artifacts": {
        "packaging": "NONE",
        "type": "CODEPIPELINE",
        "name": "nantoka-artifact"
    },
    "timeoutInMinutes": 60,
    "environment": {
        "computeType": "BUILD_GENERAL1_SMALL",
        "privilegedMode": false,
        "image": "aws/codebuild/java:openjdk-8",
        "type": "LINUX_CONTAINER",
        "environmentVariables": []
    },
    "source": {
        "type": "CODEPIPELINE",
        "buildspec": "nantoka-kantoka-buildspec.yml"
    },
    "encryptionKey": "arn:aws:kms:ap-northeast-1:1234123412341234:alias/aws/s3"
}

修正をアップロード

update.buildproject.jsonをカレントディレクトリに置いて、下記コマンドを実行する。

>aws codebuild update-project --cli-input-json file://update.buildproject.json
{
    "project": {
        "name": "nantoka-build",
        "serviceRole": "arn:aws:iam::1234123412341234:role/service-role/nantokarole",
        "tags": [],
        "artifacts": {
            "packaging": "NONE",
            "type": "CODEPIPELINE",
            "name": "nantoka-artifact"
        },
        "timeoutInMinutes": 60,
        "environment": {
            "computeType": "BUILD_GENERAL1_SMALL",
            "privilegedMode": false,
            "image": "aws/codebuild/java:openjdk-8",
            "type": "LINUX_CONTAINER",
            "environmentVariables": []
        },
        "source": {
            "type": "CODEPIPELINE",
            "buildspec": "nantoka-kantoka-buildspec.yml"
        },
        "encryptionKey": "arn:aws:kms:ap-northeast-1:1234123412341234:alias/aws/s3"
    }
}

>

Consoleで確認する。

Consoleからプロジェクトを選択し、プロジェクト詳細 -> ビルド仕様の表示 とクリックして進むと、nantoka-kantoka-buildspec.yml と表示されます。


  1. http://docs.aws.amazon.com/codebuild/latest/userguide/build-spec-ref.html#build-spec-ref-name-storage 今のところ日本語にはなってないようです。 

  2. ぶっちゃけた話、Consoleがすぐにでも対応しそうな気もしますが、今時点での手順を残します。 

続きを読む

Serverless FrameworkでAWS Lamda関数を作成する

概要

Serverless Frameworkとは、Lambda、API Gateway、DynamoDBなどを作成、管理、デプロイできるツールです。
Frameworkと付いていますが、ツールです。
この記事では、python3でLambda関数を作成します。

環境

  • CentOS7.2
  • serverless 1.23.0
  • node v6.11.3
  • npm 3.10.10
  • OpenSSL 1.0.2k-fips 26 Jan 2017

npmのインストール

以下の記事を参照
npmのインストール手順

Serverless Frameworkのインストール

slsというディレクトリを作成し、そこで作業を行います。

$ mkdir sls
$ cd sls
$ npm init
$ npm install --save serverless

serverlessコマンドのパスを通します

$ npm bin serverless
$ echo 'export PATH="$HOME/sls/node_modules/.bin/:$PATH"' >> ~/.bash_profile
$ source ~/.bash_profile

インストール確認

$ serverless -v
1.23.0

aws credential登録

以下のコマンドで、AWSのキーを登録します。

$ serverless config credentials --provider aws --key XXXXXXXXXXXXEXAMPLE --secret XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXEXAMPLEKEY
Serverless: Setting up AWS...
Serverless: Saving your AWS profile in "~/.aws/credentials"...
Serverless: Success! Your AWS access keys were stored under the "default" profile.

Lambda関数の作成

以下のコマンドで、Lambda関数を作成します。

$ serverless create -t aws-python3 -p sample-app

Serverless: Generating boilerplate...
Serverless: Generating boilerplate in "/home/vagrant/sls/sample-app"
 _______                             __
|   _   .-----.----.--.--.-----.----|  .-----.-----.-----.
|   |___|  -__|   _|  |  |  -__|   _|  |  -__|__ --|__ --|
|____   |_____|__|  ___/|_____|__| |__|_____|_____|_____|
|   |   |             The Serverless Application Framework
|       |                           serverless.com, v1.23.0
 -------'

Serverless: Successfully generated boilerplate for template: "aws-python3"

オプションの説明ですが、
-pは、Lambda関数名のprefixとなります。

また、-tで実装する言語を選びます。
以下のいずれかを選択します。

  • -t

    • aws-nodejs
    • aws-python
    • aws-python3
    • aws-java-maven
    • aws-java-gradle
    • aws-scala-sbt
    • aws-csharp
    • openwhisk-nodejs

すると、以下のファイルが生成されます。
handler.pyは、Lambda関数のテンプレート、serverless.ymlは設定ファイルになります。

$ ll sample-app/
total 8
-rw-rw-r--. 1 vagrant vagrant  497 Oct 10 04:42 handler.py
-rw-rw-r--. 1 vagrant vagrant 2758 Oct 10 04:42 serverless.yml

関数の情報を設定

serverless.ymlに関数の設定情報が書かれているので、環境合わせて編集します。

serverless.yml
provider:
  name: aws
  runtime: python3.6

# you can overwrite defaults here
- #  stage: dev
-#  region: us-east-1
+  stage: production
+  region: ap-northeast-1

# *snip*

# 関数名などの定義
functions:
-  hello:
-    handler: handler.hello
+  sample-func:
+    handler: handler.main

serverless.ymlで、関数のメソッド名を変更したので、handler.pyも以下のように編集します。

handler.py
-def hello(event, context):
+def main(event, context):

deploy

以下のコマンドでデプロイをします。

$ cd sample-app
$ serverless deploy

Serverless: Packaging service...
Serverless: Excluding development dependencies...
Serverless: Creating Stack...
Serverless: Checking Stack create progress...
.....
Serverless: Stack create finished...
Serverless: Uploading CloudFormation file to S3...
Serverless: Uploading artifacts...
Serverless: Uploading service .zip file to S3 (389 B)...
Serverless: Validating template...
Serverless: Updating Stack...
Serverless: Checking Stack update progress...
...............
Serverless: Stack update finished...
Service Information
service: sample-app
stage: production
region: ap-northeast-1
stack: sample-app-production
api keys:
  None
endpoints:
  None
functions:
  sample-func: sample-app-production-sample-func

すると、sample-app-production-sample-funcという名前のLambda関数が作成されます。

Lambda関数の実行

deployが出来たら、以下のコマンドで、Lambda関数を実行することができます。

$ serverless invoke -f sample-func

{
    "statusCode": 200,
    "body": "{"message": "Go Serverless v1.0! Your function executed successfully!", "input": {}}"
}

パラメータを付ける場合は、-dオプションで指定します。
戻り値にinputの項目が増えているのが確認できます。

$ serverless invoke -f sample-func -d '{"key":"value"}'

{
    "statusCode": 200,
    "body": "{"message": "Go Serverless v1.0! Your function executed successfully!", "input": {"key": "value"}}"
}

以下のようなjsonファイルを作成して、パラメータを渡すこともできます。

event.json
{
  "key" : "value"
}

-pオプションでjsonファイルを指定して実行

$ serverless invoke -f sample-func -p event.json
{
    "statusCode": 200,
    "body": "{"message": "Go Serverless v1.0! Your function executed successfully!", "input": {"key": "value"}}"
}

Lambda関数の削除

関数の削除は以下のコマンドで行います。
関連するS3のファイルもすべて消してくれます。
AWS Console上で手動でLambda関数を削除すると、S3のファイルなどが残ってしまいます。

関数のあるディレクトリに移動でして実行します。

$ cd sample-function
$ serverless remove -v

-sオプションで、特定のstageのみを削除することもできます。

$ serverless remove -v -s dev

その他

以下のモジュールで、擬似的にローカルでApi Gateway、DynamoDBを使うことができます。

$ npm install aws-sdk
# 擬似的Api Gateway
$ npm install --save-dev serverless-offline
# 擬似的DynamoDB
$ npm install --save-dev serverless-dynamodb-local

参考

続きを読む

何となくEC2(Amazone Linux)にMariaDBを入れてSpiderで水平Shardingしてみた

雑記と言うか備忘録と言うか、そんな感じでEC2にMariaDBの10.0系をyum経由で導入し、水平Sharding環境を構築しました

#RDS使えばいいんですけど、高いんですよね。それなりにお試しでならEC2を3台構成にすれば水平Shardingは遊べますし。

※構成例

MariaDB:USER / PASS = root / Spider
SERVER:三台 Spiderサーバ:1台 DATANODE:2台
※ IPは仮として
SpiderNode:192.168.10.10
Datanode1:192.168.10.11
Datanode2:192.168.10.12
としますので読み替えてご活用下さい
DB:example_db
table:books
使うエンジン:Spider&Innodb

下準備

yum -y update && shutdown -r now

1.MariaDBインストール
●MariaDBのレポジトリ追加

echo '# MariaDB 10.0 CentOS repository list - created 2014-04-02 07:21 UTC' >> /etc/yum.repos.d/mariadb.repo
echo '# http://mariadb.org/mariadb/repositories/' >> /etc/yum.repos.d/mariadb.repo
echo '[mariadb]' >> /etc/yum.repos.d/mariadb.repo
echo 'name = MariaDB' >> /etc/yum.repos.d/mariadb.repo
echo 'baseurl = http://yum.mariadb.org/10.0/centos6-amd64' >> /etc/yum.repos.d/mariadb.repo
echo 'gpgkey=https://yum.mariadb.org/RPM-GPG-KEY-MariaDB' >> /etc/yum.repos.d/mariadb.repo
echo 'gpgcheck=1' >> /etc/yum.repos.d/mariadb.repo
echo 'enable=1' >> /etc/yum.repos.d/mariadb.repo

●MariaDBの起動

yum install -y MariaDB-server MariaDB-client

●MariaDB起動

chkconfig mysql on
/etc/rc.d/init.d/mysql start

2.MariaDBの初期設定
●設定適用(対象:全サーバ)


/usr/bin/mysqladmin -u root password 'Spider'
mysql -u 'root' --password='Spider' -e 'CREATE DATABASE example_db;GRANT ALL PRIVILEGES ON *.* TO "root"@"192.168.10.%" IDENTIFIED BY "Spider";FLUSH PRIVILEGES;'

●設定適用(対象:Spiderサーバのみ)
▼MariaDBへのspiderエンジンのインストール

mysql -u 'root' --password='Spider' -e 'source /usr/share/mysql/install_spider.sql'

確認は mysql -u 'root' --password='Spider' -e 'show engines;' にて

●データノードのspiderサーバへの登録(対象:Spiderサーバのみ)

mysql -u 'root' --password='Spider' -e 'CREATE SERVER db1 FOREIGN DATA WRAPPER mysql OPTIONS (USER "root", PASSWORD "Spider", HOST "192.168.10.11", PORT 3306);'
mysql -u 'root' --password='Spider' -e 'CREATE SERVER db2 FOREIGN DATA WRAPPER mysql OPTIONS (USER "root", PASSWORD "Spider", HOST "192.168.10.12", PORT 3306);'

こちらも確認は mysql -u 'root' --password='Spider' -e 'select * from servers;' mysql にて

●スキーマの登録(対象:SpiderNodeのみ)

    CREATE TABLE books
    (
        id int AUTO_INCREMENT NOT NULL,
        name VARCHAR(255) NOT NULL,
        price int(11) NOT NULL default 0,
        created_at DATETIME NOT NULL,
        updated_at DATETIME,
        lock_version int(11) NOT NULL default 0,
        PRIMARY KEY (id)
    ) ENGINE = SPIDER DEFAULT CHARSET=utf8
    PARTITION BY HASH(id) (
      PARTITION p1 comment 'server "db1", table "books"',
      PARTITION p2 comment 'server "db2", table "books"'
    );

●スキーマの登録(対象:db1,db2)

    CREATE TABLE books
    (
        id int AUTO_INCREMENT NOT NULL,
        name VARCHAR(255) NOT NULL,
        price int(11) NOT NULL default 0,
        created_at DATETIME NOT NULL,
        updated_at DATETIME,
        lock_version int(11) NOT NULL default 0,
        PRIMARY KEY (id)
    ) ENGINE = SPIDER DEFAULT CHARSET=utf8
    ;

●Spiderサーバの「my.cnf」設定変更(対象:Spiderサーバのみ)

echo '[mysqld]' >> /etc/my.cnf
echo 'spider_internal_sql_log_off = ON' >> /etc/my.cnf
echo 'spider_remote_sql_log_off   = 1' >> /etc/my.cnf

●Spiderサーバの「my.cnf」設定変更(対象:Spiderサーバのみ)

/etc/rc.d/init.d/mysql restart

お仕舞。

テスト:以下10行のクエリを発行する
※クエリの発行はSpiderサーバのexample_dbで実施

    INSERT INTO books(name, price, created_at) VALUES ('3日で分かるJava', 2500, NOW());
    INSERT INTO books(name, price, created_at) VALUES ('3日で分かるRuby', 2300, NOW());
    INSERT INTO books(name, price, created_at) VALUES ('独習仮想化',      5000, NOW());
    INSERT INTO books(name, price, created_at) VALUES ('Java入門',        2000, NOW());
    INSERT INTO books(name, price, created_at) VALUES ('入門Ruby',        2800, NOW());
    INSERT INTO books(name, price, created_at) VALUES ('Effective Ruby',  4200, NOW());
    INSERT INTO books(name, price, created_at) VALUES ('すごいRuby',      5800, NOW());
    INSERT INTO books(name, price, created_at) VALUES ('Ruby徹底入門',    3000, NOW());
    INSERT INTO books(name, price, created_at) VALUES ('RubyからJavaへ',  1800, NOW());
    INSERT INTO books(name, price, created_at) VALUES ('クラウド大全',    6000, NOW());

●Spiderノード上で全件Select

  MariaDB [example_db]> select * from books order by id;
  +----+----------------------+-------+---------------------+------------+--------------+
  | id | name                 | price | created_at          | updated_at | lock_version |
  +----+----------------------+-------+---------------------+------------+--------------+
  |  1 | 3日で分かるJava      |  2500 | 2017-09-13 00:06:51 | NULL       |            0 |
  |  2 | 3日で分かるRuby      |  2300 | 2017-09-13 00:06:51 | NULL       |            0 |
  |  3 | 独習仮想化           |  5000 | 2017-09-13 00:06:51 | NULL       |            0 |
  |  4 | Java入門             |  2000 | 2017-09-13 00:06:51 | NULL       |            0 |
  |  5 | 入門Ruby             |  2800 | 2017-09-13 00:06:51 | NULL       |            0 |
  |  6 | Effective Ruby       |  4200 | 2017-09-13 00:06:51 | NULL       |            0 |
  |  7 | すごいRuby           |  5800 | 2017-09-13 00:06:51 | NULL       |            0 |
  |  8 | Ruby徹底入門         |  3000 | 2017-09-13 00:06:51 | NULL       |            0 |
  |  9 | RubyからJavaへ       |  1800 | 2017-09-13 00:06:51 | NULL       |            0 |
  | 10 | クラウド大全         |  6000 | 2017-09-13 00:06:51 | NULL       |            0 |
  +----+----------------------+-------+---------------------+------------+--------------+
  10 rows in set (0.01 sec

●db1ノード上で全件Select

  MariaDB [example_db]> select * from books order by id;
  +----+----------------------+-------+---------------------+------------+--------------+
  | id | name                 | price | created_at          | updated_at | lock_version |
  +----+----------------------+-------+---------------------+------------+--------------+
  |  2 | 3日で分かるRuby      |  2300 | 2017-09-13 00:06:51 | NULL       |            0 |
  |  4 | Java入門             |  2000 | 2017-09-13 00:06:51 | NULL       |            0 |
  |  6 | Effective Ruby       |  4200 | 2017-09-13 00:06:51 | NULL       |            0 |
  |  8 | Ruby徹底入門         |  3000 | 2017-09-13 00:06:51 | NULL       |            0 |
  | 10 | クラウド大全         |  6000 | 2017-09-13 00:06:51 | NULL       |            0 |
  +----+----------------------+-------+---------------------+------------+--------------+
  5 rows in set (0.00 sec)

●db2ノード上で全件Select

  MariaDB [example_db]> select * from books order by id;
  +----+----------------------+-------+---------------------+------------+--------------+
  | id | name                 | price | created_at          | updated_at | lock_version |
  +----+----------------------+-------+---------------------+------------+--------------+
  |  1 | 3日で分かるJava      |  2500 | 2017-09-13 00:06:51 | NULL       |            0 |
  |  3 | 独習仮想化           |  5000 | 2017-09-13 00:06:51 | NULL       |            0 |
  |  5 | 入門Ruby             |  2800 | 2017-09-13 00:06:51 | NULL       |            0 |
  |  7 | すごいRuby           |  5800 | 2017-09-13 00:06:51 | NULL       |            0 |
  |  9 | RubyからJavaへ       |  1800 | 2017-09-13 00:06:51 | NULL       |            0 |
  +----+----------------------+-------+---------------------+------------+--------------+
  5 rows in set (0.00 sec)

出来てますね

続きを読む

いまからはじめる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を投入しています。今現在はβで実績はこれからだとおもいますが、今後はそちらも選択肢に入ってきそうです。

続きを読む