AWS Lambda in VPC

この記事はDMM.com #2 Advent Calendar 2017 – Qiitaの9日目です。

カレンダーはこちら
DMM.com #1 Advent Calendar 2017 – Qiita
DMM.com #2 Advent Calendar 2017 – Qiita

はじめに

こんにちは、@funa1gと申します。
社内で共通で利用されるAPIなどの開発をやっています。
技術選定のために、AWSについても色々と試しています。
今回は、その中でAWS LambdaとVPCをつないだ、アンチパターンについてです。

全体の構成

API Gateway + LambdaでAPIを作成する時、RDSにアクセスしたい場合があります。
しかもRDSとなるとやっぱりVPC内に置きたいです。
この組み合わせがアンチパターンなのは、すでに色々検証記事が出ていますが、
多少遅くても動くなら許せるかなと思って、検証してみました。

全体の構成です
AWS Networking (1).png

動作テスト

雑に動けばいいので、こんなエンドポイントにしました
レスポンスはDB内のデータです

METHOD: GET
URL: /LambdaTest
Response

[
  {
    "id": 1,
    "name": "test1"
  },
  {
    "id": 2,
    "name": "test2"
  }
]

Lambda側のコードも、DBにアクセスして、JSONを返すだけです

const { Client } = require('pg');

exports.handler = (event, context, callback) => {
    // 接続先のPostgresサーバ情報
    const client = new Client()

    client.connect()
    client.query("SELECT * FROM test", (err, res) => {
        if (err) throw err
        client.end()
        callback(null, res.rows)
    })
};

負荷テストにはGatlingを使いました。
利用したコードは以下です。

constantTest.scala
  setUp(
    scn.inject(
      // 50人のアクセスを40秒間継続
      constantUsersPerSec(50) during(40 seconds)
    )
  ).protocols(httpConfig)

これでテストの準備は整いました。
あとは実行結果を待つだけです。

テスト結果

上記の設定でテストした結果が以下になります。

---- Global Information --------------------------------------------------------
> request count                                       2000 (OK=1839   KO=161   )
> min response time                                      0 (OK=144    KO=0     )
> max response time                                  57613 (OK=57613  KO=0     )
> mean response time                                   471 (OK=512    KO=0     )
> std deviation                                       2902 (OK=3023   KO=0     )
> response time 50th percentile                        259 (OK=262    KO=0     )
> response time 75th percentile                        279 (OK=281    KO=0     )
> response time 95th percentile                        454 (OK=479    KO=0     )
> response time 99th percentile                       1304 (OK=1410   KO=0     )
> mean requests/sec                                     20 (OK=18.39  KO=1.61  )
---- Response Time Distribution ------------------------------------------------
> t < 800 ms                                          1802 ( 90%)
> 800 ms < t < 1200 ms                                   1 (  0%)
> t > 1200 ms                                           36 (  2%)
> failed                                               161 (  8%)
---- Errors --------------------------------------------------------------------
> j.u.c.TimeoutException: Request timeout to not-connected after    161 (100.0%)
 60000 ms
================================================================================

いくつか問題がありますね。

  • かなり遅いレスポンスが一部発生している
  • タイムアウトが発生している

何度か試しましたが、1700前後のシナリオを実行したところで、処理ができなくなっていました。
実行側の問題なのか、AWS側の問題なのかまで確認できず。
そこの検証を続けていたんですが、わからないまま、日付が変わりそうなので(12/9 22:00)、一旦の結果を放流です。
寂しい結果になりましたが、検証だとこういうこともありますね。
引き続き調査は続けて、わかったら追記したいと思います。

二ヶ月ほど前に試した際には、300ユーザーのアクセスを60秒間ほどやることで、API GatewayとVPC間のENIを限界(300)まで到達させることができたのですが、そこまでたどり着きませんでした。
http://docs.aws.amazon.com/ja_jp/lambda/latest/dg/vpc.html
実際にそうなると、この記事の一番下のような現象が発生します。
ENIがこれ以上作成できないため、Lambdaの起動ができず、その分が全て失敗します。
しかも、CloudWatchにログが出ないので、かなり厳しいです。
やはりアンチパターンはアンチパターンですね。

最後に

さて、AWS側からアンチパターンということになっているのは、それなりの理由があることがわかりました。
今後もこの方法は取れないかというと、まだわからないかと思っています。

先日のre:InventでAPI Gateway VPC integrationが発表されました。
これはAPI GatewayからVPCのリソースにアクセスする手段を提供したものです。
アーキテクチャの問題上、Lambdaには明示的なエンドポイントがあるわけではないので、現状はアクセスできません。
しかし、VPCへのアクセス手段が用意されたということで、展開に希望が持てないかなと考えています。
今後の発表が楽しみなところです。

続きを読む

`ionic start`の"aws"って何なの? – ionic-angular AWS Mobile Hub Starterを使ってみた

:christmas_tree: オープンストリーム Advent Calendar 2017 の 9 日目です :santa::christmas_tree:

ionic startの”aws”って何なの?

みなさんは ionic start でIonicのプロジェクトを始めるとき、”aws”の文字を見て気になったことはありませんか?:confused:

ionic start aws

そうです、これです。
“ionic-angular AWS Mobile Hub Starter” とありますね。

今回はこの “aws” で生成されるIonicプロジェクト(Starter)について見て・触れてみようと思います。

AWS Mobile Hubとは?

先ほどの通り ionic startの”aws”で生成されるプロジェクトは AWS Mobile Hub に関係するプロジェクトです。

AWS Mobile Hubとは、ブラウザの操作だけでAWSを用いたモバイルアプリ開発がすぐスタートできるサービスになります。

画面は英語表記ですが(2017/12現在)、AWSのサービス名を知らなくとも、
:grinning:「Facebookのアカウント一つでアプリが使えるようにしたい!」
:smiley_cat:「スマートフォンで通知を受け取るアプリを作りたい!」
など、実現したいことに従ってクリックするだけで、AWSのサービスが展開されて機能がすぐに実現できます。

「モバイルアプリケーションを最短の時間で構築する方法」とある通りですね!

AWS Mobile Hubの詳細はこちらになります。

AWS Mobile Hub(モバイルアプリケーションの構築、テスト、モニタリング)| AWS
https://aws.amazon.com/jp/mobile/

なお、Mobile Hubを実際に使った説明やサンプルは他の記事に譲ります。

プロジェクト開始直後を見てみる

blank を選んだときと比べ、プロジェクトのルートには cors-policy.xml, mobile-hub-project.zip が追加されています。

mobile-hub-project.zip は、このプロジェクトで使うAWSのリソースの設定が格納されています。
このプロジェクトのためにAWSをセットアップするために必要となります。

$ ls -l
-rw-r--r--    1 ysd  staff    3622 Dec  7 14:00 README.md
-rw-r--r--    1 ysd  staff    6173 Dec  7 23:02 config.xml
-rw-r--r--    1 ysd  staff     539 Dec  7 14:00 cors-policy.xml
-rw-r--r--    1 ysd  staff     113 Dec  7 23:02 ionic.config.json
-rw-r--r--    1 ysd  staff     775 Dec  7 14:00 mobile-hub-project.zip
drwxr-xr-x  470 ysd  staff   15980 Dec  7 23:02 node_modules
-rw-r--r--    1 ysd  staff  174985 Dec  7 23:02 package-lock.json
-rw-r--r--    1 ysd  staff    1125 Dec  7 23:01 package.json
drwxr-xr-x    7 ysd  staff     238 Dec  7 23:02 resources
drwxr-xr-x   10 ysd  staff     340 Dec  7 23:01 src
-rw-r--r--    1 ysd  staff     576 Dec  7 14:00 tsconfig.json
-rw-r--r--    1 ysd  staff     178 Dec  7 14:00 tslint.json

さらに src/assets の中にあらかじめAWSのJavaScript SDKが入っていました。

$ ls -l ./src/assets
total 10712
-rw-r--r--  1 ysd  staff    43875 Dec  7 14:00 amazon-cognito-identity.min.js
-rw-r--r--  1 ysd  staff   317909 Dec  7 14:00 amazon-cognito-identity.min.js.map
-rw-r--r--  1 ysd  staff   510404 Dec  7 14:00 aws-cognito-sdk.js
-rw-r--r--  1 ysd  staff   259601 Dec  7 14:00 aws-cognito-sdk.min.js
-rw-r--r--  1 ysd  staff   346715 Dec  7 14:00 aws-cognito-sdk.min.js.map
-rw-r--r--  1 ysd  staff  2658078 Dec  7 14:00 aws-sdk.js
-rw-r--r--  1 ysd  staff  1285371 Dec  7 14:00 aws-sdk.min.js
drwxr-xr-x  3 ysd  staff      102 Dec  7 23:01 icon
drwxr-xr-x  3 ysd  staff      102 Dec  7 23:01 imgs
-rw-r--r--  1 ysd  staff    51794 Dec  7 14:00 ionic-aws-logo.png

プロジェクトのルートに配置されるファイルと package.json で導入されるパッケージから、 https://github.com/ionic-team/starters/tree/master/ionic-angular/official/aws の内容が展開されるようです。

AWS Mobile Hub Starterを使ってみる

次に、プロジェクト同梱の README.md に従ってAWS Mobile Hub Starterを使ってみましょう。

pip, AWS CLIをインストールする

この後のステップでAWS CLIが必要になります。ターミナルを開いて次のコマンドを実行します。

$ pip install --upgrade pip
$ pip install awscli

Mobile Hub を始める

このサンプルで使うAWSのリソースをセットアップするために AWS Mobile Hub を使います。Mobile Hubでプロジェクトを作成し、このプロジェクトで使う/作成するAWSのリソースをまとめます。

Mobile Hubのコンソールを開くとプロジェクトの一覧が表示されます。ここで目につく Create や + Create Project を押したくなりますが、その隣の Import ボタンをクリックします。

Mobile Hub top

Importボタンを押すと、次のようにimportするプロジェクトの設定画面が表示されます。
ここで mobile-hub-project.zip を使います!

“Import your Mobile Hub project zip file” にある “You can also drag and drop your file here” にこの mobile-hub-project.zip をドラッグします。

Mobile Hub Import

AWS Mobile Hub Starterで使うAWSのリソースが mobile-hub-project.zip にあらかじめ記述されているため、このファイルからプロジェクトをインポートすることでAWSのリソースをセットアップできます。

その他の項目は次の通りになります。

  • Enter a name for your Mobile Hub project: Mobile Hubのプロジェクト名になります。 ionic start で設定したプロジェクト名でも可能です
  • Allow AWS Mobile Hub to administer resources on my behalf: Mobile HubでAWSのリソースをセットアップできるようにチェックを入れます
    • 既にMobile Hubを使っている場合は表示されないことがあります
  • Resources for your project will be created: リージョン名をクリックすると、Mobile HubでセットアップされるAWSのリソースのリージョンを指定することができます

設定例としてこのようになります :bulb:

Mobile Hub import sample.png

設定値を入力したら Import project ボタンをクリックします。
クリックするとAWSのリソースが一括で作成されます。1-2分ほどかかります。

完了するとMobile Hubのプロジェクトが開きます。

ここで作成されたAWSのリソースを見ると、
Amazon Pinpoint のプロジェクトが作成され…………(プッシュ通知などに使います)

Amazon Cognitoのユーザープールが作成され…………(ユーザーの認証などに使います)

Amazon Cognitoのフェデレーティッドアイデンティティが作成され…………(ユーザーの認証などに使います)

Amazon S3のバケットが3つ作成され…………(アプリの配布やユーザーデータの保存などに使います)

Amazon CloudFrontのディストリビューションが作成され…………(アプリの配布などに使います)

Amazon DynamoDBのテーブルが作成され…………

IAMロールが4つ作成されました。

通常はこのMobile Hubの画面で各項目を一つずつクリックして設定するところ、このAWS Mobile Hub Starterを使うことで一度に設定できます。

恐ろし… 一通りそろって便利ですね! ※この段階では無料利用枠に収まります。

料金 – AWS Mobile Hub | AWS
https://aws.amazon.com/jp/mobile/pricing/

AWS Mobile Hub Starterでセットアップされないリソース

先ほどの方法でMobile Hubがセットアップしないリソースは次の2つです。

  • Cloud Logic: Amazon API Gateway + AWS Lambda
  • Conversational Bots: Amazon Lex

Cloud Logicはモバイルアプリから送信されたデータを、サービス側で処理する……構造が実現できます。
一方のConversational Botsは、自動で会話できるチャットボットを作成するサービスになります。2017/12現在で米国英語のみの対応になりますが、アプリと会話して注文を受け付ける機能 :shopping_bags: などが実現できます。

Mobile Hubから aws-config.js をダウンロードする

しかし、設定はまだ終わっていません。 先ほど作成されたS3バケットが次の設定で重要になります。


(再掲)

hosting-mobilehub を含むS3バケットの名前を記録します。
そして、Ionicのプロジェクトに戻り、 BUCKET_NAME を先ほどのS3バケットの名前に置き換えて aws-config.js をダウンロードします。

aws s3 cp s3://BUCKET_NAME/aws-config.js src/assets

この例では次の通りになります。

$ aws s3 cp s3://myawsionic-hosting-mobilehub-1404708309/aws-config.js src/assets
download: s3://myawsionic-hosting-mobilehub-1404708309/aws-config.js to src/assets/aws-config.js

この、 aws-config.js はMobile Hubで作成されたAWSリソース達のARNが記録されています。
「モバイルアプリがアップロードしたファイルをどこのS3バケットに配置すべきか」などが決まります。

ユーザーがS3バケットにファイルをアップロードできるようにする

AWS Mobile Hub Starterの初期状態は、アプリを使っているユーザーがS3バケットにファイルをアップロードできる機能がありますが、この機能を使うためにはアップロード先のS3バケットで設定が必要になります。

userfiles を含むS3バケットを開きます。
次に、アクセス権限 -> CORSポリシー を開き、 README.md で説明された内容 もしくはプロジェクトルートにある cors-policy.xml の内容に差し替えて「保存」ボタンをクリックします。

userfiles S3 CORS setting

実行する

プロジェクトのルートで ionic serve を実行するとブラウザで動きを確認することができます。

ユーザーアカウントの登録

ionic serve 実行後に開くブラウザではログイン画面が表示されますが、AWSやIonic Proのアカウントを入力するわけではなく、このアプリ用のユーザーアカウントを登録します。

Login screen

“Create one.”のリンクをクリックし必要な項目を入力するとアカウントの登録ができますが、次のステップで確認コードがメールで送られてそれを入力する必要があるためメールアドレスが適当すぎると詰みます。
(詰んだ場合はブラウザをリロードするとログイン画面に戻ります)

email confirm screen

ユーザー登録完了後は特に何も表示されることなくログイン画面に遷移します。

ここで Amazon Cognitoのユーザープールを見るとユーザーが作成されていることや、確認コード入力の時点で詰んだことがわかります。

Cognito User Pool

タスクを追加する

ログインできると”Tasks”の画面が表示されます。

tasks screen

右上にある 「+」 ボタンをクリックして項目を追加できます。

input tasks screen

inputed tasks screen

項目を追加した後にAmazon DynamoDBを見ると、データベースに先ほどの内容が記録されていることが分かります。

DynamoDB Table Contents

ユーザーの写真を更新する

Settings -> Account を開き,「CHANGE PHOTO」をクリックするとユーザーの写真を更新することができます。

account edit screen

正しく更新されると即時反映されます。反映されない場合はS3バケットのCORSポリシーの設定を再度確認します。

ここでは userfiles を含むS3バケットを見るとユーザーの写真がアップロードされていることが分かります。 protected のディレクトリにあります。

結論(ionic startの”aws”って何だったの?)

AWSのサービスを用いたToDoアプリができるひな形になります。

Settings -> About this app を確認すると、実際に次のサービスが使われている旨が表示されます。

  • Cognito
  • DynamoDB
  • S3

言い換えると、AWS Mobile Hub StarterからIonicのプロジェクトをスタートすると、この3つのサービスがアプリ内ですぐに利用できます。

AWS Mobile Hub Starterを動かすだけで長くなりましたが、Ionicを使うと少し手を動かすだけで

  • ユーザー登録時に確認コードでメールアドレスの所在が保証でき
  • DynamoDBでクラウドに内容が保存できる
  • ToDoアプリ

が作れました! :smiley:

おわりに

……と言いたいのですが、動かしただけではAWS Mobile Hub Starterからどのようにアプリにしていくのか分かりにくいですね?

そこで、この先は AWS Mobile Hub Starterで用意されたAmazon PinpointとFirebaseを組み合わせてAndroidのプッシュ通知を受け取るサンプル を作ろうとしましたが、

AWSブログにて紹介されている方法でFirebaseの送信者IDを入れると、トークン取得の段階で次のエラーが出て正しく動作させることができませんでした。

console.error: Error with Push pluginError: AUTHENTICATION_FAILED

Push Notifications with Ionic and Amazon Pinpoint | AWS Mobile Blog
https://aws.amazon.com/jp/blogs/mobile/push-notifications-with-ionic-and-amazon-pinpoint/

その他の方法を調べましたが、Androidのビルド環境の問題なのかビルドができませんでした :confused:

Mobile HubでセットアップされるAWSの各サービスのドキュメントを読んでも、先ほどのAWSブログ以外にIonicを用いた実装例が見つかりませんでした。
そのため、「せっかく用意したPinpointでプッシュ通知を使ってみたい」「メールアドレスではなくFacebookとCognitoとIonicでユーザー登録を実現したい」など、IonicのAWS Mobile Hub Starterのコードで用意された範囲を超えるのは非常に難しい印象でした(個人の感想です)

紹介できなかったCloud Logicについても試してみたかったのですが、次回の機会に……

翌日 10 日目は @granoeste さんです!

セットアップしたAWSリソースのクリーンアップ

プロジェクトが不要な場合は、Mobile Hubコンソールのトップ画面から操作します :bulb:

削除したいプロジェクトの右上に「…」がありますので、カーソルを合わせて「Delete」をクリックします。

Mobile HubでセットアップされたAWSリソースが削除されアクセス拒否される旨の確認ダイアログが表示されるので、「Delete project」ボタンをクリックするとMobile Hubのプロジェクトが削除されます。

MobileHub project delete confirm dialog

しかし、完全にAWSリソースが削除されないため注意が必要です。

削除されるAWSリソース

  • Amazon Pinpoint のプロジェクト
  • Amazon Cognitoのフェデレーティッドアイデンティティ
  • Amazon S3のバケット(userdata, deployments)
  • プロジェクトに関わるIAMロール

削除されないAWSリソース

  • Amazon Cognitoのユーザープール
  • Amazon CloudFrontのディストリビューション
  • Amazon DynamoDBのテーブル
  • Amazon S3のバケット(hosting)
  • Mobile Hubのサービスが使うIAMロール

動作環境

今回の記事で使った環境はこちらになります :computer:

$ sw_vers
ProductName:    Mac OS X
ProductVersion: 10.12.6
BuildVersion:   16G29

$ node -v
v8.1.4

$ npm -v
5.4.2

$ ionic --version
3.19.0

$ cordova --version
7.1.0

続きを読む

AWS Vulnerability / Penetration Testing Request Form(日本語訳)

AWSの環境に対して脆弱性診断を実施する際には、事前に「侵入テスト申請」が必要となります。
この申請フォームの内容が、2017年9月頃のアップデートの影響で大幅に変更されたのですが、日本語訳がなかったので翻訳してみました(2017年12月9日時点の情報です)。

AWSの脆弱性/侵入テストリクエストフォーム

AWS クラウド内の任意のリソースに起因する脆弱性および侵入テストの実施を許可するには、次の情報が必要です。要求側当事者は、セキュリティ評価ツールとサービスの使用に関する利用規約とAWS の方針に同意する必要があります。情報の受領と確認の後、テストプランを承認する承認メールが下記のアドレスに送信されます。テストは、要求側当事者がその承認を受け取るまで許可されません。アスタリスク(*)は、必要な情報を示します。

連絡先

このフォームにログインする際に使用した AWS アカウント所有者のメールアドレスと関連する名前を入力してください。このフォームにログインする際に使用されたアカウントの AWS アカウント ID 番号が、送信時に送信されます。別のアカウントのテストをリクエストしたい場合は、ログアウトして、テストするアカウントで再度ログインしてください。

顧客情報

  • あなたの名前(*)
  • 会社名(*)
  • 電子メールアドレス
  • 追加のメールアドレス
  • 追加のメールアドレス
  • 追加のメールアドレス
  • あなたは AWS と NDA (秘密保持契約)を締結していますか?

ターゲットデータ

  • AWS ターゲット

    • EC2 Resources
    • Cloudfront Distribution
    • API Gateway
    • Lambda
    • RDS Resources
    • ELB Name
  • 非 AWS ターゲット
    • IP アドレス

DNSZoneウォーキング

  • ネームサーバのドメイン名とIPアドレス
  • ターゲットにはアクティビティが通知されていますか?
  • スキャンされた TLD(トップレベルドメイン)

ソースデータ

  • IP アドレス
  • 上記の IP アドレスはあなたのオフィスのものですか?
  • 誰が IP アドレスを所有していますか?
  • テストチームの電話連絡先
  • テスト会社は AWS と NDA (秘密保持契約)を締結していますか?

テストの詳細

  • 予想される帯域幅のピーク(Gbps)(*)
  • 予想される1秒あたりのピーク要求数(RPS)(*)
  • DNSゾーンウォーキングで予想されるピーク秒数(OPS)
  • 開始日時(YYYY-MM-DDHHMM)(*)
  • 終了日時(YYYY-MM-DDHHMM)(*)
  • 追加のテストの詳細とこのテストが必要な理由
  • このテストの成功を確実にするために、どのような基準を監視しますか?
  • あなたが問題を発見した場合、すぐにトラフィックを停止する方法がありますか?
  • 2つの緊急連絡先(電子メールと電話)を提供してください(*)

利用規約

侵入テスト(以下「テスト」)

(a)AWS の脆弱性/侵入テストリクエストフォームに指定されている送信元および宛先のIPアドレス、ネットワーク帯域幅、インスタンスレベルのリソース(CPU、メモリ、入出力など)、および上記で提供された電子メールアドレスに送信される承認メール。
(b)t2.nano、m1.small または t1.micro インスタンスは含まれません(AWS の Web サイト http://aws.amazon.com に記載されています)。
(c)AWS と当社(http://aws.amazon.com/agreement /で利用可能)(以下「本契約」)との間の Amazon Web Services 顧客契約の条件に従います。
(d)セキュリティ評価ツールおよびサービスの使用に関する AWS の方針(下記を含む)を遵守します。

なお、 AWS が情報を検証し、認証番号を含む要求側の当事者に認証メールを送信するまで、テストは承認されません。承認には最大 48 時間かかることがあります。

AWS の直接的な結果である脆弱性やその他の問題の発見は、テストが完了してから 24 時間以内にaws-security@amazon.comに伝達されなければなりません。テストの結果は、本契約第 9 条に基づく AWS 機密情報とみなされます。

  • 利用規約に同意しますか?(*)

セキュリティ評価ツールおよびサービスの使用に関するAWSの方針

セキュリティ評価ツールおよびサービスの使用に関する AWS の方針は、 AWS 資産のセキュリティ評価を実行する際に大幅な柔軟性を提供し、他の AWS 顧客を保護し、 AWS 全体のサービス品質を保証します。

AWS は、 AWS 資産のセキュリティ評価を行うために選択できる公的、私的、商用、および/またはオープンソースのさまざまなツールとサービスがあることを理解しています。

「セキュリティ評価」という用語は、 AWS 資産間のセキュリティ管理の有効性または存在を判断する目的で従事するすべての活動を指します。(例えば、 AWS 資産間、 AWS 資産間、または仮想化内でローカルに実行される、ポートスキャン、脆弱性スキャン/チェック、侵入テスト、開発、 Web アプリケーションスキャン、注入、偽造、 資産そのもの。)

AWS 資産のセキュリティ評価を行うためのツールやサービスの選択に制限はありません。 ただし、サービス拒否( DoS )攻撃や、 AWS の資産、お客様または他のものに対するそのようなもののシミュレーションを実行する方法で、ツールまたはサービスを利用することは禁止されています。 禁止されている活動には、以下が含まれますが、これに限定されません。

  • プロトコルフラッディング(例えば、 SYN フラッディング、 ICMP フラッディング、 UDP フラッディング)
  • リソースリクエストフラッディング(例えば、 HTTP リクエストフラッディング、ログインリクエストフラッディング、 API リクエストフラッディング)

DoS に脆弱であることが知られているバージョンのリストと比較する目的で、バナーをつかむなど、 AWS 資産のリモートクエリを単独で実行してソフトウェア名とバージョンを判断するセキュリティツールは、このポリシーに違反していません。

さらに、セキュリティ評価の一環として、リモートまたはローカルの搾取のために必要に応じて、 AWS 資産の実行中のプロセスを一時的にクラッシュするセキュリティツールまたはサービスは、このポリシーに違反していません。 ただし、このツールは、前述のように、プロトコルのフラッディングやリソース要求のフラッディングに関与しない場合があります。

DoS 状態を作成、決定、または実際にまたはシミュレートされた他の方法で DoS 状態を示すセキュリティツールまたはサービスは、明示的に禁止されています。

一部のツールまたはサービスには、実際に DoS 機能が含まれています。不注意に使用された場合、またはツールまたはサービスの明示的なテスト/チェックまたは機能として、静かに/本質的に使用されます。このような DoS 機能を持つセキュリティツールまたはサービスは、その DoS 機能を無効にするか、無効にするか、そうでなければ HARMLESS を表示する明示的な能力を備えていなければなりません。さもなければ、そのツールまたはサービスは、セキュリティ評価のどの側面にも採用することはできません。

セキュリティ評価を実施するために使用されるツールとサービスが適切に構成され、 DoS 攻撃やそのようなシミュレーションを実行しない方法で正常に動作することを保証することは、 AWS のお客様の唯一の責任です。使用するツールまたはサービスが、 AWS 資産のセキュリティ評価に先立って、 DoS 攻撃またはそのシミュレーションを実行しないことを独立して検証するのは、 AWS のお客様の唯一の責任です。この AWS の顧客責任には、契約している第三者がこのポリシーに違反しない方法でセキュリティ評価を実施することが含まれます。

さらに、侵入テスト活動によって引き起こされた AWS または他の AWS 顧客に対する損害賠償責任はお客様にあります。

  • セキュリティ評価ツールとサービスの利用に関する AWS の方針に同意しますか?(*)

参考

続きを読む

AWS Lambda で Angular アプリを Server Side Rendering してみる

AWS Lambda Advent Calendar 2017 の8日目です。

前書き

Server Side Rendering (SSR) は、いわゆる SPA (Single Page Application) において、ブラウザー上(クライアントサイド)で動的に生成される DOM と同等の内容を持つ HTML をサーバーサイドで出力するための仕組みです。

React、Vue 等のモダンなフロントエンドフレームワークに軒並み搭載されつつあるこの機能ですが、 Angular にもバージョン2以降 Universal という SSR の仕組みがあります。

この仕組みを Lambda の上で動かして、 API Gateway 経由で見れるようにしてみる記事です。

SSR すると何がうれしいの?

以下のようなメリットがあるとされています

  • Web クローラーへの対応(SEO)

    • 検索エンジンのクローラーは HTML の内容を解釈してその内容をインデックスしますが、クライアントサイドで動的に変更された状態までは再現できません。そのため SSR に対応していない SPA では、コンテンツをクローラーに読み込ませるのが困難です。検索エンジンではないですが、 OGPTwitter Card もクローラーで読み込んだ情報を元に表示していますね
  • モバイル、低スペックデバイスでのパフォーマンス改善
    • いくつかのデバイスは JavaScript の実行パフォーマンスが非常に低かったり、そもそも実行できなかったりします。 SSR された HTML があれば、そのようなデバイスでもコンテンツを全く見られないという事態を避けられます
  • 最初のページを素早く表示する

どうやって Lambda で SSR するか

Angular Universal は各 Web サーバーフレームワーク用のミドルウェアとして実装されており、現時点では Express, Hapi, ASP.NET 用のエンジンがリリースされています。

他方 Lambda には AWS が開発した、既存の Express アプリを Lambda 上で動かすための aws-serverless-express があります。

今回は Angular Express Engineaws-serverless-express を組み合わせて Lambda 上で Angular アプリを SSR してみます。

やってみる

公式の Universal チュートリアルをなぞりつつ、 Lambda 対応に必要な部分をフォローしていきます。

Router を使っていないと面白くないので、公式の Angular チュートリアルである Tour of Heroes の完成段階 をいじって SSR 対応にしてみましょう。

チュートリアルのコードをまず動かす

コードを DL して展開、 yarn で依存物をインストールします。

ついでに git init して、 .gitignore も追加しておきましょう。

curl -LO https://angular.io/generated/zips/toh-pt6/toh-pt6.zip
unzip toh-pt6 -d toh-pt6-ssr-lambda
cd toh-pt6-ssr-lambda
yarn

ng serve で動くことを確認しておきます。

yarn run ng serve --open

SSR に必要なものをインストール

yarn add @angular/platform-server @nguniversal/express-engine @nguniversal/module-map-ngfactory-loader express
yarn add --dev @types/express

ルートモジュールを SSR 用に改変

src/app/app.module.ts
import { NgModule, Inject, PLATFORM_ID, APP_ID } from '@angular/core';
import { isPlatformBrowser } from '@angular/common';

// ...

@NgModule({
  imports: [
    BrowserModule.withServerTransition({appId: 'toh-pt6-ssr-lambda'}),

// ...

export class AppModule {
  constructor(
    @Inject(PLATFORM_ID) private platformId: Object,
    @Inject(APP_ID) private appId: string,
  ) {
    const platform = isPlatformBrowser(platformId) ? 'browser' : 'server';

    console.log({platform, appId});
  }
}

サーバー用ルートモジュールを追加

yarn run ng generate module app-server --flat true
src/app/app-server.module.ts
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ServerModule } from '@angular/platform-server';
import { ModuleMapLoaderModule } from '@nguniversal/module-map-ngfactory-loader';

import { AppModule } from './app.module';
import { AppComponent } from './app.component';

@NgModule({
  imports: [
    CommonModule,
    AppModule,
    ServerModule,
    ModuleMapLoaderModule,
  ],
  declarations: [],
  bootstrap: [AppComponent],
})
export class AppServerModule {}

サーバー用ブートストラップローダーを追加

src/main.server.ts
export { AppServerModule } from './app/app.server.module';

サーバーのコードを実装

server/index.ts
import 'zone.js/dist/zone-node';
import 'reflect-metadata';

import {enableProdMode} from '@angular/core';

import * as express from 'express';
import {join} from 'path';

// Faster server renders w/ Prod mode (dev mode never needed)
enableProdMode();

// Express server
export const app = express();

// * NOTE :: leave this as require() since this file is built Dynamically from webpack
const {AppServerModuleNgFactory, LAZY_MODULE_MAP} = require('../dist/serverApp/main.bundle');

// Express Engine
import {ngExpressEngine} from '@nguniversal/express-engine';
// Import module map for lazy loading
import {provideModuleMap} from '@nguniversal/module-map-ngfactory-loader';

app.engine('html', ngExpressEngine({
  bootstrap: AppServerModuleNgFactory,

  providers: [
    provideModuleMap(LAZY_MODULE_MAP)
  ],
}));

app.set('view engine', 'html');
app.set('views', join(process.cwd(), 'dist', 'browserApp'));

// Server static files from /browser
app.get('*.*', express.static(join(process.cwd(), 'dist', 'browserApp')));

// All regular routes use the Universal engine
app.get('*', (req, res) => {
  res.render(join(process.cwd(), 'dist', 'browserApp', 'index.html'), {req});
});
server/start.ts
import {app} from '.';

const PORT = process.env.PORT || 4000;

app.listen(PORT, () => {
  console.log(`Node server listening on http://localhost:${PORT}`);
});

チュートリアルからの変更点: あとで Lambda で使い回すのでこのコードでは app.listen() せずに単に export しておき、スタート用のファイルは別に用意します。ファイルの置き場所も若干変更しています。

サーバー用のビルド設定を追加

.angular-cli.json
// ...
  "apps": [
    {
      "platform": "browser",
      "root": "src",
      "outDir": "dist/browser",
// ...
    {
      "platform": "server",
      "root": "src",
      "outDir": "dist/server",
      "assets": [
        "assets",
        "favicon.ico"
      ],
      "index": "index.html",
      "main": "main.server.ts",
      "test": "test.ts",
      "tsconfig": "tsconfig.server.json",
      "testTsconfig": "tsconfig.spec.json",
      "prefix": "app",
      "styles": [
        "styles.css"
      ],
      "scripts": [],
      "environmentSource": "environments/environment.ts",
      "environments": {
        "dev": "environments/environment.ts",
        "prod": "environments/environment.prod.ts"
      }
    }
src/tsconfig.server.json
{
  "extends": "../tsconfig.json",
  "compilerOptions": {
    "outDir": "../out-tsc/app",
    "baseUrl": "./",
    "module": "commonjs",
    "types": []
  },
  "exclude": [
    "test.ts",
    "**/*.spec.ts"
  ],
  "angularCompilerOptions": {
    "entryModule": "app/app-server.module#AppServerModule"
  }
}

.angular-cli.json にはクライアント用のビルド設定しか書かれていないので、ここにサーバーサイド用アプリのビルド設定を追加します。 outDir が被らないように変えておきます。

ng build では「サーバーサイド用の Angular アプリのビルド」までは面倒を見てくれますが、サーバー自体のコードのビルドは自力でやる必要があるので、もろもろ追加します。

yarn add --dev awesome-typescript-loader webpack copy-webpack-plugin concurrently
server/webpack.config.js
const {join} = require('path');
const {ContextReplacementPlugin} = require('webpack');
const CopyWebpackPlugin = require("copy-webpack-plugin");

module.exports = {
  entry: {
    server: join(__dirname, 'index.ts'),
    start: join(__dirname, 'start.ts'),
  },

  resolve: { extensions: ['.js', '.ts'] },
  target: 'node',
  externals: [/(node_modules|main\..*\.js)/],
  output: {
    path: join(__dirname, '..', 'dist', 'server'),
    filename: '[name].js'
  },
  module: {
    rules: [{ test: /\.ts$/, loader: 'awesome-typescript-loader' }]
  },
  plugins: [
    new CopyWebpackPlugin([
      {from: "dist/browserApp/**/*"},
      {from: "dist/serverApp/**/*"},
    ]),

    // Temporary Fix for issue: https://github.com/angular/angular/issues/11580
    // for 'WARNING Critical dependency: the request of a dependency is an expression'
    new ContextReplacementPlugin(
      /(.+)?angular(\\|\/)core(.+)?/,
      join(__dirname, '..', 'src'), // location of your src
      {} // a map of your routes
    ),
    new ContextReplacementPlugin(
      /(.+)?express(\\|\/)(.+)?/,
      join(__dirname, '..', 'src'),
      {}
    )
  ]
};

サーバーの実装上の都合で (クライアント用の Angular ビルド + サーバー用の Angular ビルド) → サーバーのビルド という手順を踏む必要があるので、一連のビルド用スクリプトを追加します。

package.json
    "build:prod": "concurrently --names 'browser,server' 'ng build --prod --progress false --base-href /prod/ --app 0' 'ng build --prod --progress false --base-href /prod/ --app 1 --output-hashing false'",
    "prebuild:server": "npm run build:prod",
    "build:server": "webpack --config server/webpack.config.js",
    "start:server": "node dist/server/start.js"

--base-href を指定しているのは、後で API Gateway の仕様との整合性をとるためです。

ローカルでサーバーを動かしてみる

yarn run build:server
yarn run start:server

http://localhost:4000/prod/detail/14 あたりにアクセスして、 SSR されているか確認してみます。

Tour_of_Heroes.png

最初のリクエストに対するレスポンスで、 Angular が生成した HTML が返ってきていることがわかります。チュートリアル第1章で pipe を使って大文字にしたところもちゃんと再現されていますね。

普通に ng serve した場合はこんな感じのはずです。

Tour_of_Heroes.png

Lambda にデプロイする

おなじみの Serverless Framework を使います。いつもありがとうございます。

ここで aws-serverless-express も追加しましょう。

yarn add aws-serverless-express
yarn add --dev @types/aws-serverless-express serverless serverless-webpack

Lambda 用のエントリーポイントを追加します。

lambda/index.ts

import {createServer, proxy} from 'aws-serverless-express';

import {app} from '../server';

export default (event, context) => proxy(createServer(app), event, context);

Lambda (serverless-webpack) 用のビルド設定を追加します。さっきのやつとほぼ同じですが、 libraryTarget: "commonjs" がないと動かないようです。

lambda/webpack.config.js
const {join} = require('path');
const {ContextReplacementPlugin} = require('webpack');
const CopyWebpackPlugin = require("copy-webpack-plugin");
const slsw = require('serverless-webpack');

module.exports = {
  entry: slsw.lib.entries,
  resolve: { extensions: ['.js', '.ts'] },
  target: 'node',
  externals: [/(node_modules|main\..*\.js)/],
  output: {
    libraryTarget: "commonjs",
    path: join(__dirname, '..', 'dist', 'lambda'),
    filename: '[name].js'
  },
  module: {
    rules: [{ test: /\.ts$/, loader: 'awesome-typescript-loader' }]
  },
  plugins: [
    new CopyWebpackPlugin([
      {from: "dist/browserApp/**/*"},
      {from: "dist/serverApp/**/*"},
    ]),

    // Temporary Fix for issue: https://github.com/angular/angular/issues/11580
    // for 'WARNING Critical dependency: the request of a dependency is an expression'
    new ContextReplacementPlugin(
      /(.+)?angular(\\|\/)core(.+)?/,
      join(__dirname, '..', 'src'), // location of your src
      {} // a map of your routes
    ),
    new ContextReplacementPlugin(
      /(.+)?express(\\|\/)(.+)?/,
      join(__dirname, '..', 'src'),
      {}
    )
  ]
};

Serverless の設定ファイルを追加します。あらゆるパスへの GET を Lambda にルーティングするように設定します。

serverless.yml
service: toh-pt6-ssr-lambda
provider:
  name: aws
  runtime: nodejs6.10
  region: ${env:AWS_REGION}
  memorySize: 128
plugins:
- serverless-webpack
custom:
  webpack: lambda/webpack.config.js
  webpackIncludeModules: true
functions:
  main:
    handler: lambda/index.default
    events:
    - http:
        path: /
        method: get
    - http:
        path: "{proxy+}"
        method: get

Serverless をインストールした状態でビルドしようとすると以下のようなエラーが出るので、とりあえず tsconfig.json を変更して回避しておきます。

ERROR in [at-loader] ./node_modules/@types/graphql/subscription/subscribe.d.ts:17:4
    TS2304: Cannot find name 'AsyncIterator'.

ERROR in [at-loader] ./node_modules/@types/graphql/subscription/subscribe.d.ts:29:4
    TS2304: Cannot find name 'AsyncIterable'.
tsconfig.json
    "lib": [
      "es2017",
      "dom",
      "esnext.asynciterable"
    ]

Lambda 用のデプロイスクリプトを追加します。

package.json
    "predeploy": "npm run build:prod",
    "deploy": "serverless deploy"

で、ようやく Lambda のデプロイです。

yarn run deploy

うまくいけば Serverless によって各種 AWS リソースが作られ、 API Gateway のエンドポイントが出力されるはずです。

API Gateway にアクセスして SSR されているか見てみる

先程と同様に確認してみます。

Tour_of_Heroes.png

よさげ。

今後の課題

Lambda + API Gateway で SSR することができました。しかし、いろいろと課題はまだありそうです。

HTTP ステータスコード問題

現状のコードでは固定的に 200 を返しているため、ありえないパスにリクエストが来てもクローラー的には OK と解釈されてしまいます。本来なら 404 などを適切に返すべきです。

HTML 以外をどうやって動的に返すか

一般的なサイトでは sitemap.xml など動的な内容かつ HTML ではないものも返す必要があります。これを Angular でできるかどうか、調べる必要がありそうです。

Express なくせるんじゃね?

今回は Angular -> Universal + Express Engine -> Express -> aws-serverless-express -> Lambda (API Gateway Proxy Integration) という繋ぎ方をしました。

これを Angular -> Universal + [何か] -> Lambda (API Gateway Proxy Integration) にできたら構成要素が減らせていい感じですよね。

renderModuleFactory などを自力で叩く実装ができれば、これが実現できるのかもしれません。

今回作ったコード

参考

続きを読む

api-gateway+lambdaからlambda・AWS batchを非同期でキックしてみた記事

最近はやりのchatopsをrubyで書いていたのですが、botが死ぬたびに原因調査を行うのがめんどくさくて、slackでメッセージを受け取るところと内部の処理を分離しました。
分離した処理はjenkinsのapiを使って処理していたのですが、せっかくなのでAWSに全てあげてしまうことにしました。
そこでlambdaからLambda・AWS Batchを非同期でキックした時のことを書こうと思います。

はじめに

本稿はslackにbotを多数動かす想定で設計しているので、個人でちゃちゃっと作りたい人はbotで完結したほうがいいと思います。
またlambdaの非同期でキックするところを中心に書くのでbot部分や周辺は割愛します。
(litaをECS上で動作 passなどはkmsで管理)

想定ユーザー

  • データ基盤の機能などの一部を社内ユーザーに解放したい
  • いくつかのbotを動かしたい
  • terraformで環境構築している
  • サーバーレスが好き

全体的な構成

以下構成図になります
– メッセージは全てlita一台で受け取ってApi-GatewayにPOST
– LambdaからLambdaかAWS batchを非同期で起動

スクリーンショット 2017-12-05 12.42.21.png

post.json
{
    "method":{
         "channel_name" : "****",
         "req_usr" : "***",
         "magic_words" : "特定の情報を渡す場所(ddl取る場合はスキーマとテーブル名など)"
    }
}

api-gatewayの部分

以下の記事にlambdaをキックするところまでを書いたので割愛します。
API Gatewayを叩いてLambdaからRedshiftにSQLを投げる(ついでにslackにsnippet通知)

lambda(kicker)の部分

  • post内容から呼び出したい関数名を取り出す
  • 関数名のlambdaがあればそれをキック
  • 関数名に対応するAWS batchがあればそれをキック
kicker.py
import simplejson as json
import boto3
import logging


logger = logging.getLogger()
logger.setLevel(logging.INFO)

def handler(event, context):
    event_dict = json.loads(event["body"])
    function_name = list(event_dict.keys())[0]
    if function_name == "kicker":
        status_code = 404
        body_text = "Kicker loop error : " + function_name
        logger.error(body_text)
    elif kick_lambda(function_name, event):
        status_code = 201
        body_text = "kick lambda : " + function_name
        logger.info(body_text)
    elif kick_batch(function_name, event):
        status_code = 201
        body_text = "kick batch : " + function_name
        logger.info(body_text)
    else:
        status_code = 404
        body_text = "No Method Error : " + function_name
        logger.error(body_text)

    responseObj = {
        "statusCode": status_code,
        "body": body_text
    }
    return responseObj


def kick_lambda(function_name, event):
    try:
        clientLambda = boto3.client("lambda")
        res = clientLambda.invoke(
            FunctionName=function_name,
            InvocationType="Event",
            Payload=json.dumps(event)
        )
        return True
    except:
        return False


def kick_batch(function_name, event):
    try:
        clientBatch = boto3.client("batch")
        res = clientBatch.submit_job(
            jobName=function_name,
            jobQueue=function_name + "_queue",
            jobDefinition=function_name + "_job_definition",
            parameters={
                'event' : json.dumps(event)
            }
        )
        return True
    except:
        return False
    return False


if __name__ == '__main__':
    handler('', '')

非同期で呼び出されたlambda(右側)の部分

eventをそのまま渡しているので以下の記事のように色々作ればOK
API Gatewayを叩いてLambdaからRedshiftにSQLを投げる(ついでにslackにsnippet通知)

非同期で呼び出されたAWS batch(右側)の部分

kickerで定義された名前に対応するものを作成しておけばOK
例のものであれば
– aws_batch_job_queueのname属性が{function_name}_queue
– aws_batch_job_definitionのname属性が{function_name}_job_definition

kicker.pyの一部
def kick_batch(function_name, event):
    try:
        clientBatch = boto3.client("batch")
        res = clientBatch.submit_job(
            jobName=function_name,
            jobQueue=function_name + "_queue",
            jobDefinition=function_name + "_job_definition",
            parameters={
                'event' : json.dumps(event)
            }
        )
        return True
    except:
        return False
    return False

終わりに

全体的にスッキリして機能追加などがかなり簡単になりました。
あとはterraform・lambdaのzipファイル・AWS batchのコンテナイメージをどうやって運用するかが考え所な感じがしました

続きを読む

ゼロからはじめるServerless Java Container

日頃AWSやその他クラウドサービスを使ってインテグレーションしていく中で、 https://github.com/awslabs を定期的にウォッチしているのですが、その中で Serverless Java Container が気になったので試してみました。

https://github.com/awslabs/aws-serverless-java-container

Serverless Java Container is 何?

簡単に言うと、API GatewayとLambdaを使ったサーバレスアプリケーションを Jersey, Spark, Spring Frameworkといったフレームワークを使って作るためのライブラリです。

このライブラリを利用することで、「つなぎ」となる必要最低限のコードを書いてあげさえすれば、あとはいつも通り、フレームワークの流儀に沿ってアプリケーションを実装していくだけで、Lambda上で動くハンドラができあがる、というシロモノです。
絶対不可欠なライブラリではありませんが、あると便利なので、一考の価値はあると思います。


で、このAdvent Calendarにエントリした時には気づいていなかったのですが、AWSの中の人がこのライブラリについて詳しく紹介しているスライド・動画があることに気づきました :neutral_face:
「AWS Dev Day Tokyo 2017」で登壇された時のものみたいですね。

というわけで、このライブラリについての詳細な解説については上記を参考にしてもらうとして、今回のエントリでは、以下のような違いを出しつつ、このライブラリを使ってアプリケーションを作ってみることにします。

  • SAMやCloudFormationなどを使わずに、ゼロから構築してみる
  • よくあるPetStoreアプリケーションではなく、Hello, worldアプリケーションを作る
  • ビルドにはMavenではなくGradleを使う

試してみる

アプリケーションの雛形を作る

Spring Initialzrから新規にGradleプロジェクトを作っていきます。
必要な入力項目は以下のとおり。

項目 入力値
Group com.example
Artifact demo
Dependencies DevTools

「Generate Project」を押すと、プロジェクトの雛形がZipファイルで作られるので、展開後のディレクトリをワークスペースとします。

依存関係にServerless Java Containerを追加

今回の主役となるライブラリを追加します。執筆時点での最新バージョンは0.8のようでした。

build.gradle
    compile('com.amazonaws.serverless:aws-serverless-java-container-spring:0.8')

READMEにしたがってConfigとLambdaHandlerを作成

Serverless Java ContainerリポジトリのREADMEの「Spring support」のセクションを参考にして、アプリケーションとLambdaの「つなぎ」となる部分のコードを作っていきます。

まずはコンフィグ。

com.example.demo.AppConfig.java
@Configuration
@ComponentScan("com.example.demo")
public class AppConfig {
}

続いてハンドラ。

com.example.demo.LambdaHandler.java
public class LambdaHandler implements RequestHandler<AwsProxyRequest, AwsProxyResponse> {

    private static class Singleton {

        static SpringLambdaContainerHandler<AwsProxyRequest, AwsProxyResponse> handler = instance();

        static SpringLambdaContainerHandler<AwsProxyRequest, AwsProxyResponse> instance() {
            try {
                return SpringLambdaContainerHandler.getAwsProxyHandler(AppConfig.class);
            } catch (final ContainerInitializationException e) {
                throw new RuntimeException("Cannot get Spring Lambda Handler", e);
            }
        }
    }

    @Override
    public AwsProxyResponse handleRequest(AwsProxyRequest awsProxyRequest, Context context) {
        return Singleton.handler.proxy(awsProxyRequest, context);
    }
}

LambdaHandler の方は、READMEの通りに実装するとコンパイルエラーになってしまうので、少し修正しました。
SpringLambdaContainerHandler.getAwsProxyHandler がチェック例外 ContainerInitializationException を投げるので、そのままフィールドとして初期化できないんですよね…。

コントローラを作る

準備が終わったので、アプリケーション本体を作っていきます。
とは言え、今回は簡単なHello, Worldアプリケーションなので、これだけです。

com.example.demo.controller.DemoController.java
@RestController
public class DemoController {

    @GetMapping("/hello")
    public String hello(@RequestParam(required = false) Optional<String> message) {
        return "Hello, " + message.orElse("world") + "!";
    }
}

普通のSpringアプリケーションのコードですね。

Lambda用のパッケージの作成

下記ドキュメントを参考に、Lambda用のパッケージを作ります。
http://docs.aws.amazon.com/ja_jp/lambda/latest/dg/create-deployment-pkg-zip-java.html

実は、普段Lambda関数を作る時はランタイムとしてNode.jsを使うことが多く、Javaランタイムを使うのは始めてでした。
build.gradle に以下を追加すればOKです。

build.gradle
task buildZip(type: Zip) {
    from compileJava
    from processResources
    into('lib') {
        from configurations.runtime
    }
}

build.dependsOn buildZip

Lambda関数を作ってパッケージをアップロード

Lambda関数を作って、パッケージをアップロードします。

ここで関数をテストする場合、イベントテンプレートとして「API Gateway AWS Proxy」を選択すればよいです。
選択して出てくるテンプレート中で、実装したアプリケーションに合わせて

  • "queryStringParameters""message": "好きな文字列"
  • "httpMethod""GET"
  • "path""/hello"

としてそれぞれ変更してください(下図)。

lambda.png

API Gatewayと連携させる

仕上げに、APIを作り、デプロイします。

今回は、ルートの直下にプロキシリソースを作ってしまいます(プロキシリソースの呼び出し先のLambda関数は、上記で作成したLambda関数を指定してください)

apigw2.png

テストの際は、クエリ文字列として message=好きな文字列(今回はServerless) を指定し、GETメソッドを呼び出すと、Springのコントローラが発火し、

Hello, Serverless!!

がレスポンスとして返ってくることが確認できます。

また、APIをデプロイした後は、curl コマンドなどでアクセスしても、きちんとレスポンスが取得できることが確認します。

$ curl "https://xxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/dev/hello?message=Serverless"

まとめ

繰り返しになってしまいますが、Serverless Java Containerライブラリ、簡単に言うと、API GatewayのLambdaプロキシ利用時に、普通のSpringアプリケーションとしてハンドラを実装できるようにするためのラッパー、という感じだと思います。
API Gatewayと組み合わせて力を発揮するライブラリですね。
最初は、Lambda上でSpringを使う時の足回りを面倒見てくれるライブラリかな?と思ったのですが、ちょっとイメージしていたものとはズレていました…。

現実的には、RDBに依存したアプリケーションの場合のコネクションの話1など、既存のWebアプリケーションがそのまま載せ替えられるか?というと検討ポイントはありそうですが、使い慣れたフレームワークをサーバレスアプリケーション化する場合には、こういったライブラリの活用もよいのかなーと思いました。

おまけ:起動時間など

LambdaのランタイムとしてJavaを使っている場合、起動時間も少し気になるところだと思うので、メモしておきます。

ちなみに、メモリの設定は512MBです。

正確なベンチマークは取得できていませんが、今回のアプリケーションで確認した範囲においては、10ミリ秒前後の処理時間で済むみたいです(コンテナが起動して、ApplicationContextが初期化済みになっている場合ですが)。

続きを読む

re:invent2017で発表されたAWS Cloud9でサーバーレスなAPIを作ってみた

AbemaTV Advent Calendar 2017 3日目の記事です。
テーマフリーなので多様な記事が並ぶと思いますよ。

今日は2017年2月にジョインしたiOSエンジニアの服部が担当します。

はじめに

re:Invent 2017で多数のサービスが発表されました。

外部参考エントリ:
【速報】AWS re:Invent 2017 Keynote 1日目で発表された新サービスまとめ #reinvent
【速報】AWS re:Invent 2017 Keynote 2日目で発表された新サービスまとめ #reinvent
クラスメソッドさんの怒涛の速報。早過ぎる。

普段iOSエンジニアの私ですが AWS Cloud9 を試してみたので気軽に見てみてください。
おまけで Amazon Rekognition Videoもブラウザから動かしてみました。

AWS Cloud9 画面例: ブラウザ上で開発が完結

AWS Cloud9

AWS Cloud9 – クラウド開発環境

クラウド上のIDEです。
ブラウザで開発が完結します。これがあるべき未来か。
近いうちにElectronでアプリが出る予感。

Lambda Functionのローカル実行、API Gatewayでのデプロイ、デプロイしたAPIの実行が画面切り替えなしにできるのが神です。

ハッカソンのちょっとしたAPI作成や趣味で作るアプリのバックエンド作成など、高速にできそうですね。
ガチサービスのバックエンド開発でも使えるはず。

まだTokyoリージョンでは使えません。

今回は簡易的なサーバーレスAPIモニタリング機能を作ってみました。

ざっくり手順を。

Step 1. Lambda Function 関連権限付与

Admin権限を持つユーザで進める場合、Step 2. Environment作成へスキップして良いです。

Working with AWS Lambda Functions in the AWS Cloud9 Integrated Development Environment (IDE)

1-1. IAMで開発用ユーザorグループに以下の権限をAttach

Adminユーザで、開発用グループにIAMのページから以下を付与しました。
– AWSLambdaFullAccess
– AmazonAPIGatewayAdministrator
– AmazonAPIGatewayInvokeFullAccess

1-2. AWS CloudFormationでStack作成

Adminユーザで、CloudFormationからStackを作成。

Choose a template > Specify an Amazon S3 template URL に https://s3.amazonaws.com/cloud9-cfn-templates/Cloud9LambdaAccessGroup.yaml をペースト:

Stack nameには AWSCloud9LambdaAccessStack、GroupNameには開発用グループ名:

Optionsは変更なし:

Createをクリックし数秒でStack作成完了:

Step 2. Envioronment作成

Create environmentクリック

“Sample0001″等入れてNext Step。(この名前は後から変えられるのだろうか)

EC2インスタンス上に作ります。VPC作っていなかったら作成してください。Next Step。

Create environment

数十秒待つと…

environmentが出来ます!

3. Lambda Function作成

environmentのWelcome画面からCreate Lambda Function…

Function名入れてNext。

今回はnode.jsで。

Function TriggerにAPI Gateway設定。Resource Pathに/check

Next。

Finish!

以下を実装。
Javascriptは初心者ですみませぬ。
今回はgithubユーザ情報取得APIステータス200を確認しています。

'use strict';

var https = require('https');
var url = require('url');

var targetURLPaths = {
    "githubUsers":"https://api.github.com/users/[githubユーザ名]"
    //,"someAPI": "API URL"
};

var results = {};

function getAPI(key) {
    var urlObj = url.parse(targetURLPaths[key]);
    var options = {
        host: urlObj.host,
        port: 443,
        path: urlObj.pathname,
        headers: {
            "Content-type": "application/json; charset=UTF-8",
            "User-Agent": ""
        },
        method: 'GET'
    };
    return new Promise((resolve, reject) => {
        setTimeout(function () {
            var req = https.request(options, function(res) {
                console.log(res.statusCode);
                if (res.statusCode != 200) {
                    results[key] = "Fail: " + res.statusCode;
                    resolve(key);
                    return;
                }
                var body = '';
                res.on('data', (chunk) => body += chunk);
                res.on('end', () => {
                    //console.log(body);
                    results[key] = "Success";
                    resolve(key);
                });
                res.on('error', function(e) {
                    results[key] = "Fail: " + e;
                    resolve(key);
                    return;
                });
            });
            req.end();
        }, 300);
    });
}

exports.handler = (event, context, callback) => {
    var promises = [];
    Object.keys(targetURLPaths).forEach(function (key) {
        promises.push(getAPI(key));
    });
    Promise.all(promises)
        .then(() => {
            var response = {
                statusCode: 200,
                headers: {
                    "Content-type": "application/json; charset=UTF-8"
                },
                body: JSON.stringify(results, null, 2)
            };
            callback(null, response);
        })
        .catch(callback);
};

4. Deploy

Lambda (local)で動作確認した後…

左から4番目の上向き矢印でデプロイ!簡単だぜ

API Gateway (remote)で動作確認!動いとる

ブラウザからAPI叩いてみても良し。

Lambdaは定期実行できるので、内部APIの生存確認を回してSlack通知などしたいところ。

おまけ: Amazon Rekognition Image / Amazon Rekognition Video

画像解析APIです。
2016年に発表されたAmazon Rekognition Imageは静止画を解析します。

まずはこちらを試してみます。

(シェア可能な画像を使用。問題あったら言ってください…)
スクリーンショット 2017-12-02 3.16.59.png

うむ!
セレブリティとしてばっちり認識されています。

デモなのでブラウザから動かしていますが、APIとしても利用可能です。

続いて先日発表されたAmazon Rekognition Videoで動画を解析してみます。
動画の長さに制限があり1分まで。

研究/調査目的のmp4動画を流し込んでみます。

S3の保存先を生成。

5分程待つと結果が出ました!

稲垣氏が見事パットを決めた名場面ですが Objects and activities はかなり情景を捉えている気がします。

雑感

IDEがクラウド上にありサービスとつながっているのは想像以上に便利。
AWSのアカウントさえあれば、どこでも開発の続きができるという安心感。
Lambdaのローカルテスト、1クリックでのデプロイ、API Gatewayとの連携は嬉しい機能。
手順が手に馴染めば超高速開発ができるのでは。

Amazon Rekognition Imageのイメージ解析の速度と精度はかなりのもの。
Videoは1分の動画解析に5分程掛かるのは仕方ないか。
取り出すべき場面を見つけるシーケンスとそこから解析するシーケンスに分けているようだ。
返却されるJSONは15MBあった。
これはユースケースをまず考えるべき。

AWS楽しいすね。

続きを読む

AWS Cloud9でLambdaの作成、テスト、デプロイまでの手順まとめ

はじめに

2017/11/30にリリースされたAWSCloud9環境で
Lambdaに関する以下手順をまとめました。

  • AWS Cloud9にLambda作成
  • AWS Cloud9でローカルテスト
  • AWS Lambdaへデプロイ

AWS Cloud9の環境作成方法は以下記事で行っています。
AWS Cloud9を動かしてみた

AWS Cloud9にLambda作成

  1. AWS Cloud9の右側にAWS Resourcesタブがあるのでクリック
  2. 上にあるλ+マークをクリック。
    1.PNG

  3. ファンクション名、テンプレートを決めてNext

2.PNG
3.PNG

  1. トリガーは今回はnoneを選びました。

4.PNG

  1. メモリとロールを選択。

5.PNG

  1. FinishをクリックしてLambda functionの完成です

6.PNG

  1. 各種ファイルが出来ました。

    • 左にAWS Cloud9上の環境
    • 真ん中にLambdaのソース
    • 右上にAWS Cloud9のローカルにできたファイル
    • 右下にAWS Lambda上にできたファイル

7.PNG

  1. AWS Lambdaを確認。ローカル作成と同時に初回デプロイは完了していました。

8.PNG

AWS Cloud9でローカルテスト

AWS Cloud9上でハロワを書いて実行してみます。
テスト用のJSONファイルを作成し、実行。

  • 左側環境に「lambda-payloads.json」が自動で保存されました。
  • また、Function Logsに実行結果ログが表示されました。

※2017/12/02現在はLambda+pythonの場合ブレークポイントデバッグはできません。
公式サイトではNode.jsはデバッグ出来てるようです。

Debug the Local Version of a Lambda Function or Its Related API Gateway API

You can debug local Lambda function code or its related API Gateway API in your environment using common debugging aids such as breakpoints, stepping through code, and setting watch expressions.

Note
You cannot debug the remote version of a Lambda function or its related API Gateway API in your environment. You can only invoke it.
You cannot debug local Lambda function code that uses Python.

9.PNG

AWS Lambdaへデプロイ

右側「↑」をクリック。1アクションでAWS Lambdaへデプロイ完了。

12.PNG

AWS Lambdaのダッシュボードから編集内容が反映されていることを確認。

10.PNG

まとめ

  • AWS Cloud9を使いブラウザ上でLambda開発、テスト、デプロイができました。
  • クライアント端末の環境構築がいらない分Lambda開発のハードルが下がったと思います。

続きを読む

Node-REDをAWS API Gateway + lambda + S3で動かす方法

はじめに

2017年 Node-RED Advent Calendarの3日目の記事です。

2015年 Node-RED Advent Calendarで@stomiaさんが Node-REDをAWS Lambdaで動かす話 という記事を書かれています。
この方式はS3やSESなどのトリガーで動かせるというメリットがありますが、HTTP inノードの利用を諦める+専用ノードを使ってフローを作る必要がありました。

今回の記事は、HTTP inノードが利用できる状態でNode-REDをlambdaで動かす方法です。
これによってHTTP inノードを使ったフローを修正なしでlamdbaで動かせるようになります。
(デメリットとしてAPI Gateway以外のトリガーでは動かせません。)

また、フロー更新のたびに毎回flow.jsonを含んだNode-REDをlambdaにデプロイする構成では、デプロイ時間がネックで修正サイクルがリズムよく回せません。
これでは開発効率が悪いので、flow.json ファイルはS3に置いてフロー更新はS3のファイルを更新すれば済む構成にします。

構成図

今回の構成は下図のとおりです。この記事では青点線範囲の設定方法を説明します。
node-red-on-lambda-with-api-gw-overview.png

準備するもの

  • AWSアカウント
  • AWS CLI が使える端末(AWSアクセスキー他が設定済みであること)

    $ export AWS_ACCESS_KEY_ID=AKIAXXXXXXXXXXXXXXXX
    $ export AWS_SECRET_ACCESS_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
    $ export AWS_DEFAULT_REGION=us-west-2
    
  • ブラウザ

設定方法

  • 所要時間参考:約10分
  1. GitHubからaws-serverless-node-red リポジトリをクローンします

    $ git clone https://github.com/sakazuki/aws-serverless-node-red.git
    $ cd aws-serverless-node-red
    
  2. 自分のAWS環境に合わせた設定ファイルを準備します

    $ AWS_ACCOUNT_ID=123456789012
    $ S3_BUCKET=nodered12
    $ AWS_REGION=us-west-2
    $ AWS_FUNCNAME=api12
    $ AWS_STACK_NAME=Node-RED
    $ npm run config -- --account-id="${AWS_ACCOUNT_ID}" 
    --bucket-name="${S3_BUCKET}" 
    --region="${AWS_REGION}" 
    --function-name="${AWS_FUNCNAME}" 
    --stack-name="${AWS_STACK_NAME}"
    

    ※これにより次のファイルが自分のAWS環境用に更新されます。

    package.json
    simple-proxy-api.yaml
    cloudformation.yaml
    settings.js
    
  3. Node-REDと必要パッケージをインストールして、lambdaにデプロイします。

    $ npm run setup
    
  4. PC上でNode-REDを起動してフローを作成します

    $ node node_modules/.bin/node-red -s ./settings.js
    

    http://localhost:1880 にブラウザでアクセスしてNode-REDエディタでフローを作成します。
    HTTP inノードが使えることを確認するためフォームのサンプルフローを作ります。
    node-red-on-lambda-flow.png

    フローデータはこちら

    コピーして、右上のドロップメニューから[読み込み]-[クリップボード]でインポートできます

    
    [{"id":"e164ca79.d4bba8","type":"http in","z":"d540fb70.fb4658","name":"","url":"/hello","method":"get","upload":false,"swaggerDoc":"","x":170,"y":100,"wires":[["f5cc40d7.836fd"]]},{"id":"4818807c.9a996","type":"http response","z":"d540fb70.fb4658","name":"","statusCode":"","headers":{},"x":510,"y":100,"wires":[]},{"id":"f5cc40d7.836fd","type":"template","z":"d540fb70.fb4658","name":"form","field":"payload","fieldType":"msg","format":"handlebars","syntax":"mustache","template":"<form method="POST">n    Input your name<input type="text" name="key1">n    <input type="submit">n</form>","output":"str","x":350,"y":100,"wires":[["4818807c.9a996"]]},{"id":"5c77ba58.b744e4","type":"http in","z":"d540fb70.fb4658","name":"","url":"/hello","method":"post","upload":false,"swaggerDoc":"","x":180,"y":180,"wires":[["4c91fba9.11ed84"]]},{"id":"4c91fba9.11ed84","type":"template","z":"d540fb70.fb4658","name":"view","field":"payload","fieldType":"msg","format":"handlebars","syntax":"mustache","template":"<h1>Hello {{payload.key1}}</h1>","output":"str","x":350,"y":180,"wires":[["4818807c.9a996"]]}]
    

  5. 動作確認(ローカル)
    デプロイしたら、次のURLにアクセスしてローカルで動作確認してください。

    http://localhost:1880/hello
    
  6. 動作確認(API Gateway)
    正常動作が確認できたら、API Gateway経由でも確認します。
    次のコマンドでAPI GatewayのApiUrlを確認します。

    $ aws cloudformation describe-stacks --stack-name ${AWS_STACK_NAME} 
    --output json --query "Stacks[*].Outputs"
    [
    [
        {
            "Description": "Invoke URL for your API. Clicking this link will perform a GET request on the root resource of your API.",
            "OutputKey": "ApiUrl",
            "OutputValue": "https://1xxx2y5zzz.execute-api.us-west-2.amazonaws.com/prod/"
        },
        {
            "Description": "Console URL for the API Gateway API's Stage.",
            "OutputKey": "ApiGatewayApiConsoleUrl",
            "OutputValue": "https://us-west-2.console.aws.amazon.com/apigateway/home?region=us-west-2#/apis/1xxx2y5zzz/stages/prod"
        },
        {
            "Description": "Console URL for the Lambda Function.",
            "OutputKey": "LambdaFunctionConsoleUrl",
            "OutputValue": "https://us-west-2.console.aws.amazon.com/lambda/home?region=us-west-2#/functions/api12"
        }
    ]
    ]
    $
    

    ブラウザで ApiUrlにアクセスして動作確認します。

    https://1xxx2y5zzz.execute-api.us-west-2.amazonaws.com/prod/hello
    

    node-red-on-lambda-with-api-gw (6).png
    動作が確認できました。

  7. 後片付け(オプション)
    この手順で作成されたものを削除する場合には以下のコマンドを実行します。

    $ npm run delete-stack
    

まとめ

  • lambdaでNode-REDのフローを動かしました
  • Node-RED on lambdaでHTTP inノードが使えるようになりました
  • フローファイル(flow.json)をS3から読み込ませました。
    • これでNode-RED本体の再デプロイをすることなく、フローファイルだけを更新できるようになり、開発や修正が簡単になりました。

Node-REDで作ったAPIアプリは、lambdaに簡単に載せられることがわかりました。
皆さんも、試してみてはどうでしょうか。

明日は

@taiponrockさんのNode-RED on IBM Cloud with Watson APIの記事です。

参考

続きを読む

Xamarin.FormsでAWSのAPIGatewayを叩いてみた

Xamarin.FormsでAWSのAPIGatewayを叩いてみた

本記事は、[初心者さん・学生さん大歓迎!] Xamarin その1 Advent Calendar 2017 の2日目となります。

やること

Xamarin.Forms製のアプリでWebAPIを叩き、結果を受け取って表示させてみます。
WebAPI(クラウド環境)はAWSのAPI Gatewayを使用します。

やること概要

開発環境

クラウド

  • Amazon Web Service

    • API Gateway
    • Lambda

ローカル

  • Windows 10
  • Visual Studio 2017 Community
  • Nexus 5X (Android 8.0.0)

NuGetパッケージ

  • Newtonsoft.Json

AWS

API GatewayとLambdaの準備

API GatewayLambda の2つのサービスを使用します。
環境構築などは以下を参照してください。

Lambdaのコード

API GatewayLambdaを作成したら、Lambdaのコードを以下にします。(付け焼き刃の)Pythonです。
Param1とParam2を合計しているだけです。

import json

print('Loading function')

def lambda_handler(event, context):

    body = json.loads(event['body'])

    print(body)

    response = []

    for param in body:
        data = {}
        data['Id'] = param['Id']
        data['Sum'] = param['Param1'] + param['Param2']
        response.append(data)


    return {
        'statusCode': '200',
        'headers': {
            'Content-Type': 'application/json',
        },
        'body': json.dumps(response),
    }

以上でAWSの準備は完了です。

Xamarin.Forms

Xamarin.Formsの新規プロジェクトを作成します。

NuGetパッケージのインストール

Newtonsoft.Json を検索して追加します。

Xamarin.Formsのコード

パラメータを(今回は固定値ですが)表示し、ボタン押すとAPIGatewayを叩き、レスポンスの結果を表示します。

まずはXAMLからいきましょう。

MainPage.xaml
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:local="clr-namespace:TestAPIGateway"
             x:Class="TestAPIGateway.MainPage">
    <ContentPage.Content>
        <StackLayout Margin="20">
            <Label Text="'Id': 1, 'Param1': 1, 'Param2': 3"></Label>
            <Label Text="'Id': 2, 'Param1': 4, 'Param2': 7"></Label>
            <Label Text="'Id': 3, 'Param1': 2, 'Param2': 5"></Label>
            <Button Text="Click Me!" Clicked="Button_OnClicked"></Button>
            <Label x:Name="ResultStatus" Text="???"></Label>
            <Label x:Name="Id1Text" Text="???"></Label>
            <Label x:Name="Id2Text" Text="???"></Label>
            <Label x:Name="Id3Text" Text="???"></Label>
        </StackLayout>
    </ContentPage.Content>
</ContentPage>

次です。WebAPIの要求/応答パラメータのクラスを作成します。

public class RequestItem
{
    public int Id { get; set; }
    public int Param1 { get; set; }
    public int Param2 { get; set; }
}
public class ResponseItem
{
    public int Id { get; set; }
    public int Sum { get; set; }
}

そして最後に、ボタンを押した際の処理を作成します。

MainPage.xaml.cs
public partial class MainPage : ContentPage
{
    public MainPage()
    {
        InitializeComponent();
    }

    private async void Button_OnClicked(object sender, EventArgs e)
    {
        var httpClient = new HttpClient
        {
            Timeout = TimeSpan.FromSeconds(30)
        };

        /* 送信用データを作成 */
        var item = new List<RequestItem>
        {
            new RequestItem
            {
                Id = 1,
                Param1 = 1,
                Param2 = 3,
            },
            new RequestItem
            {
                Id = 2,
                Param1 = 4,
                Param2 = 7,
            },
            new RequestItem
            {
                Id = 3,
                Param1 = 2,
                Param2 = 5,
            }
        };

        var json = JsonConvert.SerializeObject(item, Formatting.Indented);

        var content = new StringContent(json, System.Text.Encoding.UTF8, "application/json");


        /* 送信(URIは適宜変更してください) */
        var response = await httpClient.PostAsync("https://xxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/prod/TestAPIGateway", content);


        if (response.IsSuccessStatusCode)
        {
            var result = await response.Content.ReadAsStringAsync();

            System.Diagnostics.Debug.WriteLine("Success Status");
            System.Diagnostics.Debug.WriteLine(result);


            var resultJson = JsonConvert.DeserializeObject<List<ResponseItem>>(result);

            ResultStatus.Text = "Success";
            Id1Text.Text = $"'Id': {resultJson[0].Id}, 'Sum': {resultJson[0].Sum}";
            Id2Text.Text = $"'Id': {resultJson[1].Id}, 'Sum': {resultJson[1].Sum}";
            Id3Text.Text = $"'Id': {resultJson[2].Id}, 'Sum': {resultJson[2].Sum}";
        }
        else
        {
            System.Diagnostics.Debug.WriteLine("Not Success Status: " + response.StatusCode.ToString() + "  -  Reason: " + response.ReasonPhrase);

            ResultStatus.Text = "Fail";
        }
    }
}

動作結果

こうなります。

ボタンを押す前

ボタンを押す前

ボタンを押した後

ボタンを押した後

簡単ですね!

ソースコード

Githubに置いています。

まとめ

Xamarinと見せかけて、AWSとC#のお話になっていますが、これを応用すれば、

  • アプリ内データをクラウドにバックアップする
  • クラウドを経由した端末間のデータ移行

などが簡単に実現できます。

本例題アプリとは別のXamarin.Forms製アプリに組み込んで(API叩くだけですが)うまいこと動いています。リリース後、私以外は1度も使われてないですが……。
今回はAWSを使用しましたが、Azureでも同様の事ができるはずです。

Xamarinはいいぞ。これからももっと勉強していきます!

明日はKenshiro_Fukudaさんです。よろしくお願いします。

続きを読む