初めてのAWS Lambda(どんな環境で動いているのかみてみた)

月曜日から3日間、AWSアーキテクト研修でした。そこではじめてLambdaに接しまして、ひととおり驚いてきたところです。自分なりのまとめです(ご存知の方には釈迦に説法)。

Lambdaとは?

恥ずかしながら私は「サーバーレス」という言葉を聞いてもいまいちピンと来ていませんでした。ですが、これは文字通りなんですよね。プログラムが動作する環境なんてどうだっていいんです。OSがなんだとか、ミドルウェアがなんだとか、メモリがどれくらいでCPUがいくらで、NWがどうで、トポロジーがなんでとか、そんなことはどうでもいいんです。
とにかく、「トリガー」と呼ばれる”きっかけ”を契機に、コード(プログラム)が動くんです。JavaならJVMがおもむろに立ち上がって、アップロードしておいたjarが実行されるんです。「トリガー」はAWSサービスと高度に統合されていて、例えば

  • ファイルストレージサービスであるところのS3(もはや単なるストレージの域を超越していますが)にファイルがアップロードされた
  • メッセージがキューにputされた
  • API Gatewayにリクエストがきた
  • EC2インスタンスが起動した
  • 3時になった
  • おなかがすいた(とAmazon echoに話した)
  • e.t.c.

MDBならぬTDB(Trigger Driven Bean)でしょうか。Beanである必要もないので、TBC(Trigger Driven Code)とでも言ったほうがいいのかもしれません。

うごかしてみる

研修の間、実習時間に余裕があったので、研修端末に入っていたEclipseで簡単なコードを書いて動かしてみました。テストはAWS Consoleからキックできるので、特に「トリガー」を定義しなくても動かすだけなら簡単に試せます。

お作法

基本的にどんなJavaプログラムでも必要なライブラリを組み込んでおけば動きますが、コールするメソッドにはお約束があるようです。それは引数です。第一引数にObjectをもらい、第二引数にContextをもらいます。メソッド名はなんでもいいです。型もなんでもいいです(ただし第一引数と戻り値の型ともにSeriarizableである必要あり。プリミティブ型もOK)。

第一引数に入るのは、具体的には「トリガー」からの情報です。メッセージがキューにputされたことをトリガーとするのであれば、そのメッセージ自体を渡してあげたり。戻り値は同期呼び出しであればほぼそのまんまでしょう。インタフェース要件に従って、Serializeして返してあげればよいだけです。

第二引数のContextですが、これはjavax.naming.Contextではなく、com.amazonaws.services.lambda.runtime.Contextです。というわけで、AWSが提供するjarファイルをビルドパスに追加する必要があります。1

作る

まだ意味のあるコードを書くほどの技量もアイディアもないので、インフラ屋っぽくどんな環境(システムプロパティ、環境変数、渡されたContextオブジェクト)で動いているのかみてみることにしました。

SystemInfo.class
package net.mognet.aws.lambda;

import java.util.Map;
import java.util.Properties;

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

public class SystemInfo {

    public static String printSystemInfo(int i, Context context) {
            StringBuilder sb = new StringBuilder();
            //ヘッダを追加
            sb.append("name,value\n");

            //システムプロパティ取得
            Properties prop = System.getProperties();
            for(Object key : prop.keySet()) {
                String name = (String) key;
                String value  = prop.getProperty(name);
                sb.append(name + "," + value + "\n");
            }
            //環境変数取得
            Map<String, String> env = System.getenv();
            for(String key : env.keySet()) {
                String value = env.get(key);
                sb.append(key + "," + value + "\n");
            }
            //Contextの情報を取得
            sb.append("context" + "," + context.toString());
            return sb.toString();
    }

    public static void main(String[] args) {
            System.out.println(printSystemInfo(1, null));
    }
}

mainはテスト用です。1個目の引数こそ本来は大事なんでしょうけど今回は何もしません。

乗せる

AWSコンソールを開いてLambdaの関数を作ります(関数という単位で動きます。複数の関数をオーケストレーションするサービスもあるようです(詳細未調査))。
スクリーンショット 2018-01-31 21.39.16.png
適当に名前とランタイム(今回はJava8)を選んで「関数の作成」を押します。
標準出力はCloudWatchLogsへ流れるので、事前にCloudWatchLogsへのWrite権限のあるロールを作って必要に応じてここでアタッチしてください。
スクリーンショット 2018-01-31 21.39.50.png
スクリーンショット 2018-01-31 21.39.57.png
本来ならここでトリガーを選んで云々となりますが、とにかくテストしてみたいだけなので、その辺の条件だけいれます。
スクリーンショット 2018-01-31 21.40.05.png
関数コードのところで、「アップロード」からjarファイルをアップロード、大事なのが「ハンドラ」でここに実行するメソッドを入力します。書き方が決まっていて、”.”表記でクラスのフルパスの後ろに”::“をつけてメソッド名です。
今回は”net.mognet.aws.lambda.SystemInfo::printSystemInfo“となります。ついでに環境変数もつけてみました。一旦「保存」すると実際にファイルがアップロードされます。
スクリーンショット 2018-01-31 21.41.03.png
次にテストの準備です。テストケース(入力設定=第一引数設定)です。画面上部の「テストイベントの設定」を選びます。
スクリーンショット 2018-01-31 21.41.19.png
実行するメソッドpublic static String printSystemInfoの第一引数がintなので、1とだけ書いて終わりです。下の方にある「保存」を押します。これでテスト準備完了です。

いざ実行!

おもむろに「テスト」を押します。
スクリーンショット 2018-01-31 21.42.15.png
動きました。今回はログ出力(標準出力)なしなので、ログは見ませんが開始と終了のメッセージが出ていました。String型のメソッドを実行したので、returnした文字列がそのまま画面上に表示されています(改行コードは改行してほしかったけど実行結果表示コンソールとしてはこれが正しいあり方ですね)。

付録

付録で実行結果を載せておきます。

name value
java.runtime.name OpenJDK Runtime Environment
sun.boot.library.path /usr/lib/jvm/java-1.8.0-openjdk-1.8.0.141-1.b16.32.amzn1.x86_64/jre/lib/amd64
java.vm.version 25.141-b16
java.vm.vendor Oracle Corporation
java.vendor.url http://java.oracle.com/
path.separator :
java.vm.name OpenJDK 64-Bit Server VM
file.encoding.pkg sun.io
user.country US
sun.java.launcher SUN_STANDARD
sun.os.patch.level unknown
java.vm.specification.name Java Virtual Machine Specification
user.dir /
java.runtime.version 1.8.0_141-b16
java.awt.graphicsenv sun.awt.X11GraphicsEnvironment
java.endorsed.dirs /usr/lib/jvm/java-1.8.0-openjdk-1.8.0.141-1.b16.32.amzn1.x86_64/jre/lib/endorsed
os.arch amd64
java.io.tmpdir /tmp
line.separator
java.vm.specification.vendor Oracle Corporation
os.name Linux
sun.jnu.encoding UTF-8
java.library.path /lib64:/usr/lib64:/var/runtime:/var/runtime/lib:/var/task:/var/task/lib:/usr/java/packages/lib/amd64:/usr/lib64:/lib64:/lib:/usr/lib
java.specification.name Java Platform API Specification
java.class.version 52.0
sun.management.compiler HotSpot 64-Bit Tiered Compilers
os.version 4.9.77-31.58.amzn1.x86_64
user.home /home/sbx_user1066
user.timezone UTC
java.awt.printerjob sun.print.PSPrinterJob
file.encoding UTF-8
java.specification.version 1.8
java.class.path /var/runtime/lib/LambdaJavaRTEntry-1.0.jar
user.name sbx_user1066
java.vm.specification.version 1.8
sun.java.command /var/runtime/lib/LambdaJavaRTEntry-1.0.jar
java.home /usr/lib/jvm/java-1.8.0-openjdk-1.8.0.141-1.b16.32.amzn1.x86_64/jre
sun.arch.data.model 64
user.language en
java.specification.vendor Oracle Corporation
awt.toolkit sun.awt.X11.XToolkit
java.vm.info mixed mode, sharing
java.version 1.8.0_141
java.ext.dirs /usr/lib/jvm/java-1.8.0-openjdk-1.8.0.141-1.b16.32.amzn1.x86_64/jre/lib/ext:/usr/java/packages/lib/ext
sun.boot.class.path /usr/lib/jvm/java-1.8.0-openjdk-1.8.0.141-1.b16.32.amzn1.x86_64/jre/lib/resources.jar:/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.141-1.b16.32.amzn1.x86_64/jre/lib/rt.jar:/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.141-1.b16.32.amzn1.x86_64/jre/lib/sunrsasign.jar:/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.141-1.b16.32.amzn1.x86_64/jre/lib/jsse.jar:/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.141-1.b16.32.amzn1.x86_64/jre/lib/jce.jar:/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.141-1.b16.32.amzn1.x86_64/jre/lib/charsets.jar:/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.141-1.b16.32.amzn1.x86_64/jre/lib/jfr.jar:/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.141-1.b16.32.amzn1.x86_64/jre/classes
java.vendor Oracle Corporation
file.separator /
java.vendor.url.bug http://bugreport.sun.com/bugreport/
sun.io.unicode.encoding UnicodeLittle
sun.cpu.endian little
sun.cpu.isalist
PATH /usr/local/bin:/usr/bin/:/bin
AWS_XRAY_DAEMONADDRESS 169.254.79.2
LAMBDA_TASK_ROOT /var/task
AWS_LAMBDA_FUNCTION_MEMORY_SIZE 128
TZ :UTC
AWS_SECRET_ACCESS_KEY secret
AWS_EXECUTION_ENV AWS_Lambda_java8
AWS_DEFAULT_REGION ap-northeast-1
AWS_LAMBDA_LOG_GROUP_NAME /aws/lambda/SystemInfo
XFILESEARCHPATH /usr/dt/app-defaults/%L/Dt
_HANDLER net.mognet.aws.lambda.SystemInfo::printSystemInfo
LANG en_US.UTF-8
LAMBDA_RUNTIME_DIR /var/runtime
AWS_SESSION_TOKEN tokenString
AWS_ACCESS_KEY_ID accessKeyId
LD_LIBRARY_PATH /lib64:/usr/lib64:/var/runtime:/var/runtime/lib:/var/task:/var/task/lib
X_AMZN_TRACEID Root=1-5a71b98c-393aaa7b51f5612a348586c0;Parent=3ff8164301e3ccd4;Sampled=0
AWS_SECRET_KEY secretKey
hogehoge gehogeho
AWS_REGION ap-northeast-1
AWS_LAMBDA_LOG_STREAM_NAME 2018/01/31/[$LATEST]29640ec0ac8e426ab2b0a041b3a1b1f4
AWS_XRAY_DAEMON_ADDRESS 169.254.79.2:2000
AWS_XRAY_DAEMONPORT 2000
NLSPATH /usr/dt/lib/nls/msg/%L/%N.cat
AWS_XRAY_CONTEXT_MISSING LOG_ERROR
AWS_LAMBDA_FUNCTION_VERSION $LATEST
AWS_ACCESS_KEY accessKey
AWS_LAMBDA_FUNCTION_NAME SystemInfo
context lambdainternal.api.LambdaContext@604ed9f0

アクセスキー等の情報も環境変数に乗っていましたのでそこはマスクしてます。そういう仕様だということは理解しておくべきかもしれません。この辺のキーを使ってAWS API呼び出したりするのかな?あと、ちゃんと設定した環境変数も出て来てます(あたりまえですが)。
OpenJDK on Amazon Linuxで動かしているみたいですね。こればっかりは実際に本稼働したときにどうなるかわかりませんけれども。あくまでこのテスト実行時はこうでした、というだけです。なんといってもサーバーレスですので、繰り返しになりますが実行環境(HW、OS、MW等々)はどうでもいいです。というか、どうでもいい前提でコードを書いてください、というのがLambda的な使い方と認識しました。

参考

Lambda 関数ハンドラー(Java) – AWS Lambda


  1. EclipseにはAWSのツールキットプラグインがあるので、この環境をセットアップしておくだけでも可です。 

続きを読む

Alexaのスキル開発中、Warning: Application ID is not set

alexa.APP_ID を alexa.appIdに修正すれば解決

結論から言うと仕様が変わったようで、文字列をほんの少し修正するとWarningが消えました。
以下経緯と解説です。

スキルのデバッグ中にWarningを発見

Amazon EchoのAlexaのスキルの処理をAWS Lambdaに書きながらデバッグしていたところ、AWS ClowdWatch Management Consoleにいつの間にか以下のエラーが出ており気になったので調査しました。

2018-01-28T03:58:29.297Z    xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx    Warning: Application ID is not set

アプリケーションIDがセットされていないとのことです。
今回AWS Lambdaの関数作成時に選択できる設計図であるalexa-skill-kit-sdk-factskillを選択して開発を始めましたが、どうも仕様に変更があったらしくこのままだとWarningになるようです。(スキルは一応ちゃんと動くので気づきにくい!)

どこがWarningの原因となっていたのか

alexa-skill-kit-sdk-factskillを選択して生成されるコードは以下のようになっていました。

index.js
// 一部不要なコメント等を省略しています
// 以下10行目〜15行目付近
'use strict';
const Alexa = require('alexa-sdk');
// 以下の行にスキルの`アプリケーション ID`を指定する必要があります(最初はundefinedですね)
const APP_ID = undefined;  // TODO replace with your app ID (OPTIONAL).

// 〜
// (中略)
// 〜

// 以下140目付近〜
exports.handler = function (event, context) {
    const alexa = Alexa.handler(event, context);
    // 以下の行に上の方で設定したアプリケーションIDが入るようですね
    alexa.APP_ID = APP_ID;
    // To enable string internationalization (i18n) features, set a resources object.
    alexa.resources = languageStrings;
    alexa.registerHandlers(handlers);
    alexa.execute();
};

alexa.APP_IDが現在は使われていないようです。
以下のようにalexa.appIdに変更するとWarningが消えます。

- alexa.APP_ID = APP_ID; // こちらは現在Warningとなる
+ alexa.appId = APP_ID; // 正しくはこちら

以上でClowdWatch上でWarningが消えるのを確認しました。

補足:アプリケーションIDを環境変数で指定する

ファイル内にスキルのアプリケーションIDを直書きするのもあれなのでAWS Lambdaにて環境変数を指定して記述するほうが良いかなと思います。

スキルのアプリケーションIDは開発者コンソールのAlexaスキルを管理するページで確認できます。

スクリーンショット 2018-01-31 20.46.28.png

AWS Lambdaの環境変数部分に以下のように記述しました。
環境変数名をALEXA_APP_ID、値に先程のアプリケーションIDを指定します。

スクリーンショット 2018-01-31 21.08.24.png

(TZはタイムゾーンの設定です)

Alexa Skillのindex.jsのAPP_ID部分に環境変数で指定します。

index.js
const APP_ID = process.env.ALEXA_APP_ID;

これでアプリケーションIDを直書きしなくて済みました。

続きを読む

コピペで使えるELBのアクセスログ解析による事象分析 (ShellScript, Athena)

アクセスログ解析

ELBのアクセスログの事象分析について、ShellScriptとAthenaを用いた実行例についてまとめます。

ShellScript

CLB

No.1 : レスポンスが正常に受け取れていないELBのレスポンスコード毎のカウント

$ awk '$10 == "-"' * | awk '{print $9}' | sort | uniq -c

No.2 : ELBのレスポンスコード毎の数集計

$ awk '{print $8}' *.log | sort | uniq -c

No.3 : 504のレコード一覧

$ awk '$8 == 504'

No.4 : 504がどのELBノードから多く出力されているか

$ grep ' 504 ' *.log | awk '{print $3}' | sed 's/:.*//' | sort | uniq -c

No.5 : バックエンドから正常に応答が受け取れていない時

$ awk '{if (! int($5) < 0) {print $0}}' * | egrep '2018-01-2[45]'

No.6 : target_processing_time の3つの統計値(最小値、最大値、平均)と -1 の値を取った回数を表示する

$ awk '{ print $4,$8,$9,$6 }' * | sort | sed -e 's/ /!!/' -e 's/ /!!/' | awk '{if(count[$1]==0) min[$1]=100; count[$1]+=1; if(max[$1]<$2&&$2!=-1) max[$1]=$2; if(min[$1]>$2&&$2!=-1) min[$1]=$2; if($2!=-1)sum[$1]+=$2; else minus[$1]+=1;} END{for(k in count)print k,", count:",count[k],", max:",max[k],", min:",min[k],", avg:",sum[k]/count[k],", -1:",minus[k];}' | sort -k4nr

No.7 : response_processing_time の3つの統計値(最小値、最大値、平均)と -1 の値を取った回数を表示する

$ awk '{ print $4,$8,$9,$7 }' * | sort | sed -e 's/ /!!/' -e 's/ /!!/' | awk '{if(count[$1]==0) min[$1]=100; count[$1]+=1; if(max[$1]<$2&&$2!=-1) max[$1]=$2; if(min[$1]>$2&&$2!=-1) min[$1]=$2; if($2!=-1)sum[$1]+=$2; else minus[$1]+=1;} END{for(k in count)print k,", count:",count[k],", max:",max[k],", min:",min[k],", avg:",sum[k]/count[k],", -1:",minus[k];}' | sort -k4nr

No.8 : 最も多いリクエスト元のELBノードIPアドレスのリクエスト数

$ awk '{print $3}' * | awk -F ":" '{print $1}' | sort | uniq -c | sort -r| head -n 10 

No.9 : 時間毎のリクエスト数

grep中の二重引用符内は適宜日付等を入れて絞り込み

grep -r "" . | cut -d [ -f2 | cut -d] -f1 | awk -F: '{print $2":00"}' | sort -n | uniq -c

No.10 : 分単位でのリクエスト数

grep中の二重引用符内は適宜日付等を入れて絞り込み

$ grep "" * | cut -d [ -f2 | cut -d ] -f1 | awk -F: '{print $2":"$3}' | sort -nk1 -nk2 | uniq -c | awk '{ if ($1 > 10) print $0}'

No.11 : ユーザーエージェント毎のランキング

$ awk '{split($0, array, """); agent=array[4]; print agent}' * | sort | uniq -c | sort -nr | head

No.12 : TLSでクライアントが最も使った暗号スイートのランキング

$ awk '{split($0, array, """); afterUserAgent=array[5]; print afterUserAgent}' * | awk '{print $1}' | sort | uniq -c | sort -nr | head -5

No.13 : TLSでクライアントが最も使ったTLSバージョンのランキング

$ awk '{split($0, array, """); afterUserAgent=array[5]; print afterUserAgent}' * | awk '{print $2}' | sort | uniq -c | sort -nr | head

No.14 : TLSでクライアントが最も使ったプロトコルと暗号スイートのランキング

$ awk '{split($0, array, """); proto=array[1]; afterUserAgent=array[5]; print proto afterUserAgent}' * | awk '{print $1 " " $13}' | sort | uniq -c | sort -nr | head

ALB

No.1 : target_processing_time の3つの統計値(最小値、最大値、平均)と -1 の値を取った回数を表示する

 $ awk '{ print $5,$9,$10,$7 }' * | sort | sed -e 's/ /!!/' -e 's/ /!!/' | awk '{if(count[$1]==0) min[$1]=100; count[$1]+=1; if(max[$1]<$2&&$2!=-1) max[$1]=$2; if(min[$1]>$2&&$2!=-1) min[$1]=$2; if($2!=-1)sum[$1]+=$2; else minus[$1]+=1;} END{for(k in count)print k,", count:",count[k],", max:",max[k],", min:",min[k],", avg:",sum[k]/count[k],", -1:",minus[k];}' | sort -k4nr

No.2 : response_processing_time の3つの統計値(最小値、最大値、平均)と -1 の値を取った回数を表示する

$ awk '{ print $5,$9,$10,$8 }' * | sort | sed -e 's/ /!!/' -e 's/ /!!/' | awk '{if(count[$1]==0) min[$1]=100; count[$1]+=1; if(max[$1]<$2&&$2!=-1) max[$1]=$2; if(min[$1]>$2&&$2!=-1) min[$1]=$2; if($2!=-1)sum[$1]+=$2; else minus[$1]+=1;} END{for(k in count)print k,", count:",count[k],", max:",max[k],", min:",min[k],", avg:",sum[k]/count[k],", -1:",minus[k];}' | sort -k4nr

Athena

以下、全て CLB を前提とします。
また、以下のような、デフォルトで生成されている sampledb データベースの elb_logs テーブルを使用します。

CREATE EXTERNAL TABLE `elb_logs`(
  `request_timestamp` string COMMENT '', 
  `elb_name` string COMMENT '', 
  `request_ip` string COMMENT '', 
  `request_port` int COMMENT '', 
  `backend_ip` string COMMENT '', 
  `backend_port` int COMMENT '', 
  `request_processing_time` double COMMENT '', 
  `backend_processing_time` double COMMENT '', 
  `client_response_time` double COMMENT '', 
  `elb_response_code` string COMMENT '', 
  `backend_response_code` string COMMENT '', 
  `received_bytes` bigint COMMENT '', 
  `sent_bytes` bigint COMMENT '', 
  `request_verb` string COMMENT '', 
  `url` string COMMENT '', 
  `protocol` string COMMENT '', 
  `user_agent` string COMMENT '', 
  `ssl_cipher` string COMMENT '', 
  `ssl_protocol` string COMMENT '')
ROW FORMAT SERDE 
  'org.apache.hadoop.hive.serde2.RegexSerDe' 
WITH SERDEPROPERTIES ( 
  'input.regex'='([^ ]*) ([^ ]*) ([^ ]*):([0-9]*) ([^ ]*):([0-9]*) ([.0-9]*) ([.0-9]*) ([.0-9]*) (-|[0-9]*) (-|[0-9]*) ([-0-9]*) ([-0-9]*) \"([^ ]*) ([^ ]*) (- |[^ ]*)\" ("[^"]*") ([A-Z0-9-]+) ([A-Za-z0-9.-]*)$') 
STORED AS INPUTFORMAT 
  'org.apache.hadoop.mapred.TextInputFormat' 
OUTPUTFORMAT 
  'org.apache.hadoop.hive.ql.io.HiveIgnoreKeyTextOutputFormat'
LOCATION
  's3://athena-examples-us-west-2/elb/plaintext'
TBLPROPERTIES (
  'transient_lastDdlTime'='1480278335');

HTTPステータスコードが200のレコード一覧

SELECT * 
FROM elb_logs
WHERE elb_response_code <> '200'
ORDER BY request_timestamp;

ELB毎のリクエスト数

SELECT elb_name,
         count(*) AS request_count
FROM elb_logs
GROUP BY elb_name
ORDER BY request_count DESC;

ELB毎のリクエスト数(期間指定)

SELECT elb_name,
         count(*) AS request_count
FROM elb_logs
WHERE request_timestamp >= '2014-01-01T00:00:00Z'
        AND request_timestamp < '2016-01-01T00:00:00Z'
GROUP BY elb_name
ORDER BY request_count DESC;

ELB毎のリクエスト数(期間+ELB 指定)

SELECT elb_name,
         count(*) AS request_count
FROM elb_logs
WHERE elb_name LIKE 'elb_demo_008'
        AND request_timestamp >= '2014-01-01T00:00:00Z'
        AND request_timestamp < '2016-01-01T00:00:00Z'
GROUP BY elb_name
ORDER BY request_count DESC;

ELB毎の5XXエラーのリクエスト数

SELECT elb_name,
         backend_response_code,
         count(*) AS request_count
FROM elb_logs
WHERE backend_response_code >= '500'
GROUP BY backend_response_code, elb_name
ORDER BY backend_response_code, elb_name;

ELB毎の5XXエラーのリクエスト数(ELB指定)

SELECT elb_name,
         backend_response_code,
         count(*) AS request_count
FROM elb_logs
WHERE elb_name LIKE 'elb_demo_008'
        AND backend_response_code >= '500'
GROUP BY backend_response_code, elb_name
ORDER BY backend_response_code, elb_name;

ELB毎の5XXエラーのリクエスト数(期間+ELB 指定)

SELECT elb_name,
         backend_response_code,
         count(*) AS request_count
FROM elb_logs
WHERE elb_name LIKE 'elb_demo_008'
        AND backend_response_code >= '500'
        AND request_timestamp >= '2014-01-01T00:00:00Z'
        AND request_timestamp < '2016-01-01T00:00:00Z'
GROUP BY backend_response_code, elb_name
ORDER BY backend_response_code, elb_name;

ELB毎の5XXエラーのリクエスト数(期間+ELB+URL 指定)

SELECT count(*) AS request_count,
         elb_name,
         url,
         elb_response_code,
         backend_response_code
FROM elb_logs
WHERE elb_name LIKE 'elb_demo_008'
        AND backend_response_code >= '500'
        AND url LIKE 'http://www.example.com/jobs/%'
        AND request_timestamp >= '2014-01-01T00:00:00Z'
        AND request_timestamp < '2016-01-01T00:00:00Z'
GROUP BY elb_name,url,elb_response_code,backend_response_code
ORDER BY request_count DESC limit 10;

ELB毎の5XXエラーのリクエスト数(期間+ELB+URL+UserAgent 指定)

SELECT count(*) AS request_count,
         elb_name,
         url,
         elb_response_code,
         backend_response_code,
         user_agent
FROM elb_logs
WHERE elb_name LIKE 'elb_demo_008'
        AND backend_response_code >= '500'
        AND url LIKE 'http://www.example.com/jobs/%'
        AND user_agent LIKE '%Mozilla/5.0%'
        AND request_timestamp >= '2014-01-01T00:00:00Z'
        AND request_timestamp < '2016-01-01T00:00:00Z'
GROUP BY elb_name,url,elb_response_code,backend_response_code,user_agent
ORDER BY request_count DESC limit 10;

送信元IPのリクエスト数ランキング

SELECT request_ip,
         url,
         count(*) AS request_count
FROM elb_logs
WHERE elb_name LIKE 'elb_demo_008'
        AND request_timestamp >= '2014-01-01T00:00:00Z'
        AND request_timestamp < '2016-01-01T00:00:00Z'
GROUP BY request_ip,url
ORDER BY request_count DESC limit 5;

日付ごとのリクエスト数

SELECT date(from_iso8601_timestamp(request_timestamp)),
         count(*)
FROM elb_logs
WHERE url LIKE '%/jobs/%'
        AND date(from_iso8601_timestamp(request_timestamp)) >= date('2014-12-01')
GROUP BY  1
ORDER BY  1;

直近1年の500エラー発生のリクエスト数

SELECT elb_response_code,
         count(*)
FROM elb_logs
WHERE from_iso8601_timestamp(request_timestamp) >= date_add('day', -365 * 1, now())
        AND elb_response_code >= '500'
GROUP BY  1
ORDER BY  1;

レスポンスに1.0s以上時間がかかっているリクエスト

SELECT url,
         count(*) AS count,
         backend_processing_time
FROM elb_logs
WHERE backend_processing_time >= 1.0
GROUP BY  url, backend_processing_time
ORDER BY backend_processing_time DESC;

任意のエントリ取得(期間+リクエスト元IP 指定)

SELECT *
FROM elb_logs
WHERE request_ip = '245.85.197.169'
        AND request_timestamp >= '2014-01-01T00:00:00Z'
        AND request_timestamp <= '2016-01-01T00:00:00Z';

あるページからの遷移先ページ傾向

SELECT d.*
FROM 
    (SELECT b.request_ip,
         min(b.request_timestamp) AS request_timestamp
    FROM 
        (SELECT *
        FROM elb_logs
        WHERE url LIKE '%/jobs/%') a
        JOIN elb_logs b
            ON a.request_timestamp < b.request_timestamp
        GROUP BY  1 ) c
    JOIN elb_logs d
    ON c.request_ip = d.request_ip
        AND c.request_timestamp = d.request_timestamp
ORDER BY  d.request_timestamp;

参考

ELB アクセスログ

Classic Load Balancer のアクセスログ – Elastic Load Balancing
https://docs.aws.amazon.com/ja_jp/elasticloadbalancing/latest/classic/access-log-collection.html#access-log-entry-syntax
Application Load Balancer のアクセスログ – Elastic Load Balancing
https://docs.aws.amazon.com/ja_jp/elasticloadbalancing/latest/application/load-balancer-access-logs.html#access-log-entry-syntax

Athena

Querying Classic Load Balancer Logs – Amazon Athena
https://docs.aws.amazon.com/ja_jp/athena/latest/ug/elasticloadbalancer-classic-logs.html
Querying Application Load Balancer Logs – Amazon Athena
https://docs.aws.amazon.com/athena/latest/ug/application-load-balancer-logs.html

Amazon AthenaでELBのログを調査するときに使ったSQL
https://dev.classmethod.jp/cloud/amazon-athena-sql-for-elb/
Amazon AthenaでELBログをSQLで解析する #reinvent
https://dev.classmethod.jp/cloud/aws/amazon-athena-sql-elb-log-reinvent/
Amazon Athenaではじめるログ分析入門
https://qiita.com/miyasakura_/items/174dc73f706e8951dbdd

続きを読む

AWS AppSyncとReactでToDoアプリを作ってみよう(3) Reactアプリの作成

はじめに

前回の記事まででは、AWS AppSyncを使ってGraphQL APIを作成しました。
今回は、そのGraphQL APIと連携するクライアント側Reactで作成してきたいと思います。
AWS AppSyncでは、React向けのGraphQLクライアントのApolloに対応した、aws-appsync-react (バインディングライブラリ)が用意されているので、今回はこれを使って、AWS AppSyncのデータとコンポーネントの紐付けを行っていきます。

プロジェクトのセットアップ

雛形の作成

Create React Appを使って、雛形を作成します。
(今回使用したnodeのバージョンは9.2.0です。)

$ mkdir aws-appsync-todo-app
$ npx create-react-app .

追加で必要なパッケージをインストールします。

$ yarn add graphql-tag react-apollo aws-appsync aws-appsync-react uuid
  • graphql-tag

    • GraphQLのスキーマをJavaScriptのコード内に定義するために使用
  • react-apollo
    • GraphQLクライアント
  • aws-*のパッケージ
    • AWS AppSyncとApolloを連携するために使用
  • uuid
    • Todo個々のアイテムにクライアント側でユニークなIDをつけるために使用

設定ファイルの取得とインポート

コンソール画面から、「AWS AppSync > 作成したプロジェクト > Top画面」を開き、「Getting Started」の一番下にある、「Download the AWS AppSync.js config file」からAppSync.jsをダウンロードします。

スクリーンショット 2018-01-25 22.17.20.png

ダウンロードしたファイルを、作成したプロジェクトのsrc以下に配置し、他のパッケージと合わせてApp.jsにインポート

App.js
import { ApolloProvider } from 'react-apollo';
import AWSAppSyncClient from "aws-appsync";
import { Rehydrated } from "aws-appsync-react";
import appSyncConfig from "./AppSync";

AppSyncClientを初期化

設定ファイルから読み込んだ、AWS認証情報・API情報を引数に、AWS AppSyncClientを初期化します。
authパラメータで認証方式を選択できますが、今回はAPI Keyによる認証を使用します。

App.js
// AWS AppSync Client
const client = new AWSAppSyncClient({
  url: appSyncConfig.graphqlEndpoint,
  region: appSyncConfig.region,
  auth: {
    type: appSyncConfig.authenticationType,
    apiKey: appSyncConfig.apiKey,
  }
});

AppSyncのデータと連携するコンポーネントをApolloProviderRehydratedに含めApolloProviderに対して、AppSyncClientを渡しています。

App.js
class App extends Component {
  render() {
    return (
      <ApolloProvider client={client}>
        <Rehydrated>
          <div className="App">
            <header className="App-header">
              <h1 className="App-title">AWS AppSync Todo</h1>
            </header>
            <TodoListWithData />
          </div>
        </Rehydrated>
      </ApolloProvider>
    );
  }
}

GraphQLクエリの作成

graphql-tagのgqlメソッドを使って、クエリを定義します。今回は、src/GraphQL以下に作成しました。

Todo全件取得のQuery

QueryGetTodos.js
import gql from "graphql-tag";

export default gql(`
query {
  getTodos {
    id
    title
    description
    completed
  }
}`);

Todo作成のMutation

MutationAddTodo.js
import gql from "graphql-tag";

export default gql(`
mutation addTodo($id: ID!, $title: String, $description: String, $completed: Boolean) {
  addTodo(
    id: $id
    title: $title
    description: $description
    completed: $completed
  ) {
    id
    title
    description
    completed
  }
}`);

Todo更新のMutation

MutationUpdateTodo.js
import gql from "graphql-tag";

export default gql(`
mutation updateTodo($id: ID!, $title: String, $description: String, $completed: Boolean) {
  updateTodo(
    id: $id
    title: $title
    description: $description
    completed: $completed
  ) {
    id
    title
    description
    completed
  }
}`);

Todo削除のMutation

MutationDeleteTodo.js
import gql from "graphql-tag";

export default gql(`
mutation deleteTodo($id: ID!) {
  deleteTodo(id: $id) {
    id
    title
    description
    completed
  }
}`);

ToDoList Componentの実装

今回は、ざっくり1つのComponentにTodoリストの全機能をまとめてしまいます。
src/Components以下に、TodoList.jsを作成しました。

AWS AppSyncとComponentの連携

AWS AppSyncのGraphQL APIからとReact Componentを連携するために、react-apolloを使用します。

react-apolloのgraphqlメソッドの引数にComponentを渡すと、ComponentのpropsでGraphQL APIから取得したデータを取得できるComponentを受け取ることができます。

graphql
const TodoListWithData = graphql(QueryGetTodos)(TodoList);

また、複数のQuery、MutationとComponentを連携する場合には、react-apolloのcomposeメソッドを使用します。
実際には、それぞれにオプションなどを指定するため、あくまでイメージです。

compose
const TodoListWithData = compose(
  graphql(QueryGetTodos),
  graphql(MutationAddTodo),
  graphql(MutationUpdateTodo),
  graphql(MutationDeleteTodo),
)(TodoList);

実際に、それぞれのQuery、Mutationを紐付ける部分は次の通りです。

TodoList.js
export default compose(
  // 全件取得Query
  graphql(QueryGetTodos, {  // あらかじめ定義したGraphQLクエリを使用
    options: {
      fetchPolicy: 'cache-and-network'
    },
    props: (props) => ({
      todos: props.data.getTodos
    })
  }),
  // 追加Mutation
  graphql(AddTodoMutation, {  // あらかじめ定義したGraphQLクエリを使用
    props: (props) => ({
      onAdd: (todo) => {
        props.mutate({
          variables: { ...todo },
          // APIからのレスポンスが返ってくるまえにpropsに反映する値を設定
          optimisticResponse: () => ({ addTodo: { ...todo, __typename: 'Todo' } })
        })
      }
    }),
    options: {
      // 追加の後に全件リストを更新するアクション
      refetchQueries: [{ query: QueryGetTodos }],
      update: (proxy, { data: { addTodo } }) => {
        const query = QueryGetTodos;
        const data = proxy.readQuery({ query });

        data.getTodos.push(addTodo);

        proxy.writeQuery({ query, data });
      }
    }
  }),
  // 状態更新(チェック)Mutation
  graphql(UpdateTodoMutation, {  // あらかじめ定義したGraphQLクエリを使用
    props: (props) => ({
      onCheck: (todo) => {
        props.mutate({
          variables: { id: todo.id, title: todo.title, description: todo.description, completed: !todo.completed },
          // APIからのレスポンスが返ってくるまえにpropsに反映する値を設定
          optimisticResponse: () => ({ updateTodo: { id: todo.id, title: todo.title, description: todo.description, completed: !todo.completed, __typename: 'Todo' } })
        })
      }
    }),
    options: {
      // 更新の後に全件リストを更新するアクション
      refetchQueries: [{ query: QueryGetTodos }],
      update: (proxy, { data: { updateTodo } }) => {
        const query = QueryGetTodos;
        const data = proxy.readQuery({ query });

        data.getTodos = data.getTodos.map(todo => todo.id !== updateTodo.id ? todo : { ...updateTodo });

        proxy.writeQuery({ query, data });
      }
    }
  }),
  // 削除Mutation
  graphql(DeleteTodoMutation, {  // あらかじめ定義したGraphQLクエリを使用
    props: (props) => ({
      onDelete: (todo) => props.mutate({
        variables: { id: todo.id },
        // APIからのレスポンスが返ってくるまえにpropsに反映する値を設定
        optimisticResponse: () => ({ deleteTodo: { ...todo, __typename: 'Todo' } }),
      })
    }),
    options: {
      // 削除の後に全件リストを更新するアクション
      refetchQueries: [{ query: QueryGetTodos }],
      update: (proxy, { data: { deleteTodo: { id } } }) => {
        const query = QueryGetTodos;
        const data = proxy.readQuery({ query });

        data.getTodos = data.getTodos.filter(todo => todo.id !== id);

        proxy.writeQuery({ query, data });
      }
    }
  })
)(TodoList);

それ以外の部分は次の通りです。

TodoList.js
class TodoList extends Component {

  constructor(props) {
    super(props);
    this.state = {
      todo: {
        title: '',
        description: ''
      },
    };
  }

  // propsの初期値を設定
  static defaultProps = {
      todos: [],
      onAdd: () => null,
      onDelete: () => null,
      onUpdate: () => null,
  }

  todoForm = () => (
    <div>
      <span><input type="text" placeholder="タイトル" value={this.state.todo.title} onChange={this.handleChange.bind(this, 'title')} /></span>
      <span><input type="text" placeholder="説明" value={this.state.todo.description} onChange={this.handleChange.bind(this, 'description')} /></span>
      <button onClick={this.handleOnAdd}>追加</button>
    </div>
  );

  renderTodo = (todo) => (
    <li key={todo.id}>
      <input type="checkbox"checked={todo.completed} onChange={this.handleCheck.bind(this, todo)} />
      {!todo.completed && todo.title}
      {todo.completed && (<s>{todo.title}</s>)}
      <button onClick={this.handleOnDelete.bind(this, todo)}>削除</button>
    </li>
    );

  handleChange = (field, { target: { value }}) => {
    const { todo } = this.state;
    todo[field] = value;
    this.setState({ todo });
  }

  handleOnAdd = () => {
    if (!this.state.todo.title || !this.state.todo.description) {
      return;
    }
    const uuid = uuidv4();
    const newTodo = {
      id: uuid,
      title: this.state.todo.title,
      description: this.state.todo.description,
      completed: false
    }
    this.props.onAdd(newTodo);

    const { todo } = this.state;
    todo.title = '';
    todo.description = '';
    this.setState({ todo });
  }

  handleCheck = (todo) => {
    this.props.onCheck(todo);
  }

  handleOnDelete = (todo) => {
    this.props.onDelete(todo);
  }

  render() {
    const { todos } = this.props;
    return (
      <Fragment>
        {this.todoForm()}
        <ul>
          {todos.map(this.renderTodo)}
        </ul>
      </Fragment>
    );
  }
}

参考

Building a ReactJS Client App -AWS AppSync
ReactとApolloを使ってGithub GraphQL APIを試してみる -Qiita
APOLLO CLIENT -Apollo

まとめ

クライアント画の実装に関しては、react-apolloに依存する部分が多く、AWS AppSyncとReactの組み合わせをガッツリ使っていこうとすると、react-apolloのヘルパーメソッドやキャッシュなどのオプション周りを詳しく見ていく必要があるかと思います。
(逆にそれ以外の部分は、aws-appsync-reactがよしなにやってくれるので、気にする必要はない)
今回は、GraphQLクエリにQueryとMutationのみを使用しましたが、機会があれば、Subscriptionを使ってリアルタイムでサーバー側と通信を行うパターンも試してみたいと思います。

続きを読む

UnityからAWS Cognito Identity Providerで認証機能を実装する(User_Auth_Flow)

前置き:AWSSDK for Unityでは現状,公式にCognito Identity Providerの機能をサポートしていません.紹介した方法は自己責任で参考にしてください.

UnityからCognito User Poolを使って認証する

この記事ではAWSのCognito User Poolを使用してログイン機能をUnityのアプリケーションに実装し,AWSのリソースにアクセスするためのトークンを取得する方法を紹介します.

Cognito User Poolとは

https://docs.aws.amazon.com/ja_jp/cognito/latest/developerguide/cognito-user-identity-pools.html

自分でサーバー側の実装を書くことなくユーザー認証機能を実装できます.

手順

環境

AWSの設定

User Poolの作成と設定

UserPool.png

Cognitoからユーザーを管理するためのUser Poolを作成し,以下のように設定します.

  • プール名

    • わかりやすい任意の名前を設定します.
  • 属性
    • 標準属性のチェックを全て外し,ユーザー名とパスワードだけでログインできるように設定します.
  • アプリクライアントの追加

    • “アプリクライアントの追加”をクリックします.
    • アプリクライアント名を設定します.
    • “クライアントシークレットを生成する”をオフにします.

設定し終わったら”確認タブ”からプール作成を終了し,

  • プールIDとプール名 (リージョン名と固有なプール名の組み合わせ)

    • 例) ap-northeast-1_Hoge0Hoge
      を確認します.

さらにアプリの統合>アプリクライアントの設定タブから,

  • “有効なIDプロバイダ”のチェック欄に表示されている”Cognito User Pool”にチェックを入れます.
  • アプリクライアントID (25桁のAlphanumericな値)を確認します.

Identity Poolの作成と設定

IdentityPool.png

Cognitoから”フェデレーテッドアイデンティティ”のページに移動し,”新しいIDプールの作成”をクリックするとIDプール作成ウィザードが開始します.

  • IDプール名を入力します.
  • “認証されていないIDに対してアクセスを有効にする”をオンにするとゲストユーザーとしてアプリを使用することが可能です.ここでは必要ないのでオフのままで大丈夫です.
  • 認証プロバイダーのセクションで”Cognito”タブをクリックし,先ほど確認したユーザープールのIDとアプリクライアントIDを入力します.

Createpolicy.png

“プールの作成”をクリックすると,”AWSの諸々にアクセスするための権限の設定をする必要がある”ということで,ゲストユーザーと認証ユーザーの二つのユーザーについてIAMロールを設定するように言われます.

ここではどちらも”新しいIAMロールの作成”を選択し,そのまま右下の許可を押します.ここで作成した二つのIAMロール(Cognito_*Hoge*Auth_RoleCognito_*Hoge*Unauth_Role)はあとで編集するので名前を覚えておきます.

IAMロールの編集

先ほど作成したロールの権限を編集し,Cognito User Poolに対して認証リクエストを送る権限を付与します.

CogitoIdentityProvider.dll for Unityのビルド

AWS Mobile SDK for UnityではCognito User Poolがサポートされておらず,自分でCognitoIdentityProvider.dllをビルドする必要があります.dotNet版のコードジェネレーターの設定をいじることでUnityに対応したdllを得ることができます.

GitHubのissueに具体的な方法が書かれており,それを翻訳すると以下のようになります.

  1. AWS SDK dot netのリポジトリをクローンします.

  2. aws-sdk-netgeneratorServiceModelscognito-idpmetadata.jsonを編集し,"platforms": ["Unity"],をjsonのrootに追加します.

  3. aws-sdk-netgeneratorServiceClientGeneratorLib/Generators/ProjectFiles/UnityProjectFile.csの265行目でUnityEngine.dllのパスを修正します. UnityEngine.dllの場所はMacOSとWindowsで異なるので注意.ここではWindowsでビルドします.修正後のパスはC:/Program Files/Unity/Editor/Data/Managed/UnityEngine.dllとなります.

  4. aws-sdk-netgeneratorAWSSDKGenerator.slnをVisual Studioで開きます.実行ボタン(上の緑の三角ボタン)を押すとビルドされたコード生成プログラムが実行され,上手くいけばaws-sdk-netsdksrcServicesCognitoIdentityProvider内部にAWSSDK.CognitoIdentityProvider.Unity.csprojというファイルが生成されます.

  5. 次にaws-sdk-netsdkAWSSDK.Unity.slnを開き,Build TypeをReleaseに設定してからビルド(ctrl + shift + B)します.すると新しくaws-sdk-netsdksrcServicesCognitoIdentityProviderbinReleaseunityフォルダが作られ,中にCognitoIdentityProvider.dllが生成されます.

  6. UnityプロジェクトのAssets/下任意の場所にdllを置きます.

metadata.json
{
  "platforms": ["Unity"],
  "active": true, 
  "synopsis": "You can create a user pool in Amazon Cognito Identity to manage directories and users. You can authenticate a user to obtain tokens related to user identity and access policies. This API reference provides information about user pools in Amazon Cognito Identity, which is a new capability that is available as a beta."
}
UnityProjectFile.cs
...
 this.Write(this.ToStringHelper.ToStringWithCulture(Path.Combine((string)this.Session["UnityPath"], "Editor", "Data", "Managed", "UnityEngine.dll")));
...

Unityで認証フローを実装する

AWS Mobile SDK for Unityを公式サイトからダウンロードし,AWSSDK.IdentityManagement.unitypackageをプロジェクトにインポートします.

以下,ブログエントリーを参考にして認証フローと暗号化処理を実装します.

AdminInitiateAuth関数を使った方法が紹介されることが多いですが,パスワードが平文で送信されるのでモバイルアプリで用いるにはセキュリティ的に非常に危険です.今回はSecure Remote Password (SRP)プロトコルをつかった認証フローであるUSER_SRP_AUTHに従います.

暗号化の実装はブログエントリーが詳しく,また依存するHkdfクラスの実装もGistに公開されているものが完動するのでコピペします.Bouncy Castle C#のライブラリが必要なのでBouncy Castle C#のGitHubリポジトリから最新リリースをダウンロードし,.net2.0版のdllをプロジェクトにコピーします.

注意すべき点として, 作成したばかりのユーザーのステータスはFORCE_CHANGE_PASSWORDと設定されており,User Poolからのレスポンス(チャレンジ)として新しいパスワードを要求してきます.下のプログラムではまだ実装しておらず,パスワード変更済みのユーザーに対してログイン処理をかけています.

認証フローが成功すると
* IdToken
* AccessToken
* RefreshToken
の三つが手に入ります.

得られたIdトークンを使ってCredential.AddLogins(IdentityProviderName , IdToken)することで,GetCredentialForIdentityAsync関数を実行した時にAWSのAPIを叩く上で必要なトークンを得ることができます.このトークンを使って認可されるアクション,アクセスできるリソースはIAMで設定したCognito_HogeAuth_Roleに従います.

リフレッシュトークンはPlayerPrefなどに保存し,アプリを起動した際にIdトークンを更新することで自動ログイン処理を行います.

CognitoUserPoolClient.cs
namespace CognitoLogInSample
{
    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    using UnityEngine.UI;
    using System;
    using System.Globalization;
    using Amazon;
    using Amazon.Runtime;
    using Amazon.CognitoIdentity;
    using Amazon.CognitoIdentity.Model;
    using Amazon.CognitoIdentityProvider;
    using Amazon.CognitoIdentityProvider.Model;

    public class CognitoUserPoolClient : MonoBehaviour
    {
        #region CognitoCredentials
        public string IdentityPoolId;            // IDプールのID
        public string CognitoIdentityRegion;     // リージョン名 例)ap-northeast-1

        private RegionEndpoint _CognitoIdentityRegion
        {
            get { return RegionEndpoint.GetBySystemName(CognitoIdentityRegion); }
        }
        private CognitoAWSCredentials _credentials;

        private CognitoAWSCredentials Credentials
        {
            get
            {
                if (_credentials == null)
                {
                    _credentials = new CognitoAWSCredentials(IdentityPoolId, _CognitoIdentityRegion);
                    _credentials.IdentityChangedEvent += Credentials_IdentityChangedEvent;
                }
                return _credentials;
            }
        }
        #endregion

        #region CognitoIdP
        public string CognitoIdPRegion;          // User Poolのリージョン 例)ap-northeast-1

        private RegionEndpoint _CognitoIdPRegion
        {
            get { return RegionEndpoint.GetBySystemName(CognitoIdPRegion); }
        }

        [SerializeField]
        string userPoolName;                     // User Poolの固有名 アンダーバーで区切ったうちの後半
        [SerializeField]
        string clientId;                         // User PoolのクライアントID

        string UserPoolId
        {
            get
            {
                return string.Format("{0}_{1}", _CognitoIdPRegion, userPoolName);
            }
        }

        public string CognitoIdentityProviderName
        {
            get
            {
                return string.Format("cognito-idp.{0}.amazonaws.com/{1}", _CognitoIdentityRegion.SystemName, UserPoolId);
            }
        }
        #endregion

        AmazonCognitoIdentityProviderClient idpClient;
        AmazonCognitoIdentityClient cognitoIdentityClient;

        public InputField IdInputField;
        public InputField passwordInputField;
        public Toggle clearCredentialToggle;

        TokenCacheManager tokenCacheManager;
        public bool cleanPlayerPrefSetting;

        string currentSession;
        string currentUserName;

        private void Start()
        {
            tokenCacheManager = new TokenCacheManager();
            if (cleanPlayerPrefSetting)
            {
                tokenCacheManager.DeleteCachedToken();
            }
            //AdminAuthenticateWithRefreshToken();
            SignInWithRefreshToken();
        }

        public void OnButtonClick()
        {
            if (clearCredentialToggle.isOn)
            {
                Credentials.Clear();
            }
            SignIn(IdInputField.text, passwordInputField.text);
        }

        #region InitiateAuthFlow

        /// <summary>
        /// Signs In with refresh token (USER_AUTH_FLOW).
        /// </summary>
        void SignInWithRefreshToken()
        {
            tokenCacheManager.GetCachedTokens((getCachedTokensResult) =>
            {
                if (getCachedTokensResult.IsCacheAvailable)
                {
                    Debug.Log(getCachedTokensResult.Token.ToString());
                    // RefreshToken
                    idpClient = new AmazonCognitoIdentityProviderClient(Credentials, _CognitoIdentityRegion);
                    InitiateAuthRequest initiateAuthRequest = new InitiateAuthRequest()
                    {
                        ClientId = clientId,
                        AuthFlow = AuthFlowType.REFRESH_TOKEN_AUTH,
                    };
                    initiateAuthRequest.AuthParameters.Add("REFRESH_TOKEN", getCachedTokensResult.Token.refreshToken);

                    idpClient.InitiateAuthAsync(initiateAuthRequest, (initiateAuthResponse) =>
                    {
                        if (initiateAuthResponse.Exception != null) return;
                        CognitoIdentityProviderToken cognitoIdentityProviderToken = new CognitoIdentityProviderToken
                        {
                            accessToken = initiateAuthResponse.Response.AuthenticationResult.AccessToken,
                            idToken = initiateAuthResponse.Response.AuthenticationResult.IdToken ?? getCachedTokensResult.Token.idToken,
                            refreshToken = initiateAuthResponse.Response.AuthenticationResult.RefreshToken ?? getCachedTokensResult.Token.refreshToken,
                            expireTime = initiateAuthResponse.Response.AuthenticationResult.ExpiresIn
                        };
                        tokenCacheManager.CacheTokens(cognitoIdentityProviderToken);

                        Credentials.AddLogin(CognitoIdentityProviderName, initiateAuthResponse.Response.AuthenticationResult.IdToken);
                        Credentials.GetIdentityIdAsync(responce =>
                        {
                            Debug.Log("Logged In with refreshed IdToken : " + responce.Response);
                        });
                    });
                    idpClient.Dispose();
                }
                else
                {
                    Credentials.Clear();
                    // Redirect to LogIn Dialog
                    Debug.Log("RefreshToken is not available");
                }
            });
        }


        /// <summary>
        /// Sign In.
        /// </summary>
        /// <param name="userName">User name.</param>
        /// <param name="password">Password.</param>
        void SignIn(string userName, string password)
        {
            Debug.Log("Initiate Authentication Flow");

            var cred = new AnonymousAWSCredentials();

            idpClient = new AmazonCognitoIdentityProviderClient(cred, _CognitoIdentityRegion);
            var TupleAa = AuthenticationHelper.CreateAaTuple();

            var initiateAuthRequest = new InitiateAuthRequest
            {
                AuthFlow = AuthFlowType.USER_SRP_AUTH,
                ClientId = clientId,
                AuthParameters = new Dictionary<string, string>(){
                            {"USERNAME", userName},
                            {"SRP_A", TupleAa.Item1.ToString(16)},
                        }
            };

            Debug.Log(initiateAuthRequest.AuthParameters["SRP_A"]);

            idpClient.InitiateAuthAsync(initiateAuthRequest, (initiateAuthResponse) =>
            {
                var challengeName = initiateAuthResponse.Response.ChallengeName;
                if (challengeName == ChallengeNameType.NEW_PASSWORD_REQUIRED)
                {
                    // newPasswordRequired
                    idpClient.Dispose();
                }
                else if (challengeName == ChallengeNameType.PASSWORD_VERIFIER)
                {
                    DateTime timestamp = TimeZoneInfo.ConvertTimeToUtc(DateTime.Now);
                    var usCulture = new CultureInfo("en-US");
                    string timeStr = timestamp.ToString("ddd MMM d HH:mm:ss "UTC" yyyy", usCulture);

                    byte[] claim = AuthenticationHelper.authenticateUser(initiateAuthResponse.Response.ChallengeParameters["USERNAME"],
                                                                         password,
                                                                         userPoolName,
                                                                         TupleAa,
                                                                         initiateAuthResponse.Response.ChallengeParameters["SALT"],
                                                                         initiateAuthResponse.Response.ChallengeParameters["SRP_B"],
                                                                         initiateAuthResponse.Response.ChallengeParameters["SECRET_BLOCK"],
                                                                         timeStr
                                                                        );
                    string claimBase64 = Convert.ToBase64String(claim);

                    var respondToAuthChallengeRequest = new RespondToAuthChallengeRequest()
                    {
                        ChallengeName = initiateAuthResponse.Response.ChallengeName,
                        ClientId = clientId,
                        ChallengeResponses = new Dictionary<string, string>(){
                                    { "PASSWORD_CLAIM_SECRET_BLOCK", initiateAuthResponse.Response.ChallengeParameters["SECRET_BLOCK"]},
                                    { "PASSWORD_CLAIM_SIGNATURE", claimBase64 },
                                    { "USERNAME", userName },
                                    { "TIMESTAMP", timeStr }
                        }
                    };

                    Debug.Log(timeStr);

                    idpClient.RespondToAuthChallengeAsync(respondToAuthChallengeRequest, respondToAuthChallengeResponse =>
                   {
                       try
                       {
                           Debug.LogFormat("User was verified in SRP Auth Flow : {0}", respondToAuthChallengeResponse.Response.AuthenticationResult.IdToken);
                           Credentials.AddLogin(CognitoIdentityProviderName, respondToAuthChallengeResponse.Response.AuthenticationResult.IdToken);

                           tokenCacheManager.CacheTokens(new CognitoIdentityProviderToken()
                           {
                               accessToken = respondToAuthChallengeResponse.Response.AuthenticationResult.AccessToken,
                               idToken = respondToAuthChallengeResponse.Response.AuthenticationResult.IdToken,
                               refreshToken = respondToAuthChallengeResponse.Response.AuthenticationResult.RefreshToken,
                               expireTime = respondToAuthChallengeResponse.Response.AuthenticationResult.ExpiresIn,
                           });
                       }
                       catch (Exception e)
                       {
                           Debug.LogErrorFormat("Encountered exception: {0}", e);
                       }
                       finally
                       {
                           idpClient.Dispose();
                       }
                   });
                }
            });

            currentUserName = userName;
        }
        #endregion

        private void Credentials_IdentityChangedEvent(object sender, CognitoAWSCredentials.IdentityChangedArgs e)
        {
            Debug.Log(string.Format("Identity has changed from {0} to {1}", e.OldIdentityId, e.NewIdentityId));
        }
    }
}

参考資料

続きを読む

AWS AppSyncとReactでToDoアプリを作ってみよう (1)GraphQLスキーマの定義

はじめに

AWS AppSyncでは、APIクエリにGraphQLを利用するため、GraphQLのスキーマ定義が必須です。
今回は、 schema.graphqlというファイルを作成し、ここにGraphQLスキーマを定義していきます。

Todoスキーマの定義

Query

まずは、Todoのtypeを定義します。
それぞれの項目に指定された、IDStringIntはGraphQLで用意されているデフォルトの型です。
また、!は必須項目であることを表します。

type Todo {
  id: ID!
  title: String
  description: String
  completed: Boolean 
}

次にこのtypeに対するqueryを定義します。
Queryという名前のルート型を追加して、Todoオブジェクトを含むリストを返すのgetTodosフィールドを追加します。

type Query {
  getTodos: [Todo]
}

schema に登録します。

schema {
  query:Query
}

ここまでの結果

schema.graphql
schema {
  query:Query
}

type Query {
  getTodos: [Todo]
}

type Todo {
  id: ID!
  title: String
  description: String
  completed: Boolean 
}

Mutation

queryでのデータ取得のクエリ型定義を行ったので、mutationでデータの追加、更新、削除する場合のクエリ型を定義していきます。

まずは、追加・更新の際のmutationを定義します。
addTodoフィールドは引数を取り、結果としてTodoオブジェクトを返すメソッドのようなイメージです。

type Mutation {
  addTodo(id: ID!, title: String, description: String, , completed: Boolean): Todo
}

同様に編集・削除の際のmutationを定義します。

type Mutation {
  addTodo(id: ID!, title: String, description: String, , completed: Boolean): Todo
  updateTodo(id: ID!, title: String, description: String, , completed: Boolean): Todo
  deleteTodo(id: ID!): Todo
}

また、今回は使用しませんでしたが、enum型なども使用できるようです。

ここまでの結果

schema.graphql
schema {
  query: Query
  mutation: Mutation
}

type Query {
  getTodos: [Todo]
}

type Mutation {
  addTodo(id: ID!, title: String, description: String, , completed: Boolean): Todo
  updateTodo(id: ID!, title: String, description: String, , completed: Boolean): Todo
  deleteTodo(id: ID!): Todo
}

type Todo {
  id: ID!
  title: String
  description: String
  completed: Boolean 
}

Subscription

今回の例ではリアルタイム性を求めないため、Subscriptionは利用しません。
また機会があったら、試してみようと思います。

GraphQLスキーマの登録

前の手順で作成したGraphQLスキーマをAppSyncにアップロードします。

まずは、コンソール画面から、「AWS AppSync > 作成したプロジェクト > Schema」を選択し、Schema編集画面を開きます。

スクリーンショット 2018-01-21 19.59.31.png

Schemaの項目に、作成したGraphQLスキーマの内容をコピー&ペーストして、右下の「Save」ボタンをクリックし保存します。
ボタンをクリックすると、内容が検証され、スキーマの内容に構文エラーがあると、エラーメッセージが表示されます。

スクリーンショット 2018-01-21 20.05.13.png

エラーが有った場合には、対象の箇所を修正して再度保存してください。
正常に保存されると、「Data Types」の項目に、schema.graphqlに定義した型の一覧が表示されます。

スクリーンショット 2018-01-21 20.16.49.png

ここまでが、GraphQLスキーマの定義と、AppSyncへの登録の手順です。

参考

Designing Your Schema -AWS AppSync
Introduction to GraphQL -GraphQL

続きを読む

[バッドノウハウ]webプログラミング初学者がCloud9にPython3×djangoを使う

バッドノウハウというタイトルをつけましたが内容自体は使えなくもないと思います。
筆者にとって不要な手順を省略して後日記事を書き直す予定です。

この記事を書こうと思ったきっかけ

ローカルに環境を用意しなくても開発が始められるという統合開発環境(IDE)Cloud9を始めてみようと思いましたが、ネットで調べてもAWS版のcloud9がリリースされる前の情報が多く、いろいろと戸惑ったこともありましたので記事を書くことにしました。

これまでのあらすじ

Cloud9でpython3を標準で使えるまでできました。
webプログラミング初学者がAWS Cloud9でPython3を使う
webプログラミング初学者がAWS Cloud9を使い始める

pipでdjangoをインストールする→エラー

ec2-user:~/environment $ sudo pip install django
Collecting django
  Downloading Django-2.0.tar.gz (8.0MB)
    100% |████████████████████████████████| 8.0MB 140kB/s 
    Complete output from command python setup.py egg_info:
    Traceback (most recent call last):
      File "<string>", line 1, in <module>
      File "/tmp/pip-build-IzDxWp/django/setup.py", line 32, in <module>
        version = __import__('django').get_version()
      File "django/__init__.py", line 1, in <module>
        from django.utils.version import get_version
      File "django/utils/version.py", line 61, in <module>
        @functools.lru_cache()
    AttributeError: 'module' object has no attribute 'lru_cache'

    ----------------------------------------
Command "python setup.py egg_info" failed with error code 1 in /tmp/pip-build-IzDxWp/django/

前回python -m pip -v の結果がこうだったのでいけるかなと思ったのですが。

ec2-user:~/environment $ python -m pip -V
pip 9.0.1 from /usr/lib/python3.6/dist-packages (python 3.6)

pip3を探す

pip3のありかを確認

ec2-user:~/environment $ ls -l /usr/bin |grep pip
-rwxr-xr-x   1 root root       2804 Sep 10  2014 lesspipe.sh
lrwxrwxrwx   1 root root         21 Jan  4 02:19 pip -> /etc/alternatives/pip
-rwxr-xr-x   1 root root        370 Aug 25 18:59 pip-2.7
-rwxr-xr-x   1 root root        370 Aug 25 18:59 pip-3.6

pip3.6にリンクを張る

alternativesとかよくわからないのでとりあえず

ec2-user:~/environment $ sudo ln -s /usr/bin/pip-3.6 /usr/bin/pip3

preferenceにPYTHONPATHの項目を発見

よく見るとpreferenceのPYTHONPATHに3.6が入っていないようなので先頭に追加

PYTHONPATH
/usr/local/lib/python3.6/dist-packages:/usr/local/lib/python2.7/dist-packages:/usr/local/lib/python3.4/dist-packages:/usr/local/lib/python3.5/dist-packages

pip3でdjangoインストール

pip3でdjangoをインストール

ec2-user:~/environment $ sudo pip3 install django                                                            
Collecting django
  Downloading Django-2.0.1-py3-none-any.whl (7.1MB)
    100% |████████████████████████████████| 7.1MB 200kB/s 
Collecting pytz (from django)
  Downloading pytz-2017.3-py2.py3-none-any.whl (511kB)
    100% |████████████████████████████████| 512kB 2.5MB/s 
Installing collected packages: pytz, django
Successfully installed django-2.0.1 pytz-2017.3

インストールできた!

ここまできて

“pip3″でやるんだったら、”python3″でいいじゃん。
前回の記事、べつにいらなくね?

なのでもう一回下記の手順でやり直して記事を書き直します。

  • cloud9でenvironment作成
  • cloud9の設定でPythonバージョン、PYTHONPATH設定
  • pip3のリンク作成
  • djangoインストール

続きを読む

mapを使った読みやすいterraform variablesの書き方

スクリーンショット 2018-01-19 0.51.28.png

ディレクトリ構成

以下のように、terraform/provider/aws/env/stgとしました。
変数ファイルであるvariables.tfもメイン処理をするec2.tfも同じディレクトリに置きます。

環境ごとに完全にファイルを分断するイメージで、tfstateファイルも環境ごとに別にします。(保管場所はS3です)
なお、module化はしません。

 %tree 
.
└── provider
    └── aws
        └── env
            └── stg
                ├── backend.tf
                ├── ec2.tf
                └── variables.tf

変数の書き方

Before

これまでterraformの変数ファイルはこんな感じで書いてました。

before_variables.tf
variable "ami" {
  default = "ami-4af5022c"
}

variable "instance_type" {
  default = "t2.micro"
}

variable "instance_key" {
  default = "id_rsa"
}

実際のEC2のパラメータはこれだけではありません。もっとたくさんある上に、AWSのリソースは他にもあるとなるとかなり長い変数ファイルとなります。

特徴としては、variableが毎回並んでしまって行数が増える。読みにくい見にくい探しづらい。

これを解決するのにmapを使いました。

A map value is a lookup table from string keys to string values. This is useful for selecting a value based on some other provided value.
A common use of maps is to create a table of machine images per region, as follows:

sample.tf
variable "images" {
  type    = "map"
  default = {
    "us-east-1" = "image-1234"
    "us-west-2" = "image-4567"
  }
}

After

一つのvariableに対してリソースの値をまとめます。
map型の宣言は省略可能みたいです。

after_variables.tf
variable "ec2_config" {
  type = "map" #省略化
  default = {
    ami = "ami-4af5022c" 
    instance_type = "t2.micro" 
    instance_key = "id_rsa" 
  }
}

こうすることで、リソースごとに値がまとまるので読みやすくなりました。
ではメイン処理をするec2.tfを見ていきましょう。

map型でのec2.tfの書き方

Before

これまでのリソース定義の書き方はこちらです。

before_ec2.tf
resource "aws_instance" "vtryo-web01" {
  ami              = "${var.ami}"
  instance_type    = "${var.instance_type}"
  instance_key     = "${var.instance_key}"

    tags {
    Name = "vtryo-web01"
  }
}

そしてafter版に対するvariableに対応するリソース定義の仕方です。
map型で変数を格納したので、lookup関数を使って値を参照させます。

beforeのときよりやや長くなったように見えますが、ルールを覚えれば(後述)そこまで複雑ではない上、柔軟性は良くなっています。

after_ec2.tf
resource "aws_instance" "vtryo-web" {
  ami              = "${lookup(var.ec2_config, "ami")}" 
  instance_type    = "${lookup(var.ec2_config, "instance_type")}" 
  key_name         = "${lookup(var.ec2_config, "instance_key")}" 

  tags {
    Name = "vtryo-${format("web%02d", count.index + 1)}" 
  }

注意点

resource定義内のami, instance_type, key_nameはterraform側で名前が決まっています。variables.tf内の変数名は任意ですが、こちらは自由には決められないので注意しましょう。
aws_instance

lookup

上記の書き方は、lookup関数でKeyを直接指定する方法を取っています。
こちらを参考にしています。
Terraformのoutputでmapを利用する方法

たとえば以下であれば
ami = "${lookup(var.ec2_config, "ami")}"

variables.tf内のec2_configamiの値を参照する」という意味になります。

format

最終行の"vtryo-${format("web%02d", count.index + 1)}"についてですが、変数格納後の値はvtryo-web01になります。
ちなみに"web%02d"を、"web%01d"にするとvtryo-web1になってしまうので注意です。

terraform init

さて実行です。

% terraform init

Initializing provider plugins...
- Checking for available provider plugins on https://releases.hashicorp.com...
- Downloading plugin for provider "aws" (1.7.0)...

The following providers do not have any version constraints in configuration,
so the latest version was installed.

To prevent automatic upgrades to new major versions that may contain breaking
changes, it is recommended to add version = "..." constraints to the
corresponding provider blocks in configuration, with the constraint strings
suggested below.

* provider.aws: version = "~> 1.7"

Terraform has been successfully initialized!

You may now begin working with Terraform. Try running "terraform plan" to see
any changes that are required for your infrastructure. All Terraform commands
should now work.

If you ever set or change modules or backend configuration for Terraform,
rerun this command to reinitialize your working directory. If you forget, other
commands will detect it and remind you to do so if necessary.

terraform plan

 % terraform plan
Refreshing Terraform state in-memory prior to plan...
The refreshed state will be used to calculate this plan, but will not be
persisted to local or remote state storage.


------------------------------------------------------------------------

An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  + aws_instance.vtryo-web
      id:                           <computed>
      ami:                          "ami-4af5022c"
      associate_public_ip_address:  <computed>
      availability_zone:            <computed>
      ebs_block_device.#:           <computed>
      ephemeral_block_device.#:     <computed>
      instance_state:               <computed>
      instance_type:                "t2.micro"
      ipv6_address_count:           <computed>
      ipv6_addresses.#:             <computed>
      key_name:                     "id_rsa"
      network_interface.#:          <computed>
      network_interface_id:         <computed>
      placement_group:              <computed>
      primary_network_interface_id: <computed>
      private_dns:                  <computed>
      private_ip:                   <computed>
      public_dns:                   <computed>
      public_ip:                    <computed>
      root_block_device.#:          <computed>
      security_groups.#:            <computed>
      source_dest_check:            "true"
      subnet_id:                    <computed>
      tags.%:                       "1"
      tags.Name:                    "vtryo-web01"
      tenancy:                      <computed>
      volume_tags.%:                <computed>
      vpc_security_group_ids.#:     <computed>


Plan: 1 to add, 0 to change, 0 to destroy.

------------------------------------------------------------------------

Note: You didn't specify an "-out" parameter to save this plan, so Terraform
can't guarantee that exactly these actions will be performed if
"terraform apply" is subsequently run.

変数が格納されました!

ちょっと応用

さきほどのec2.tfでNameには"vtryo-${format("web%02d", count.index + 1)}"を書きました。
この出力結果はvtryo-web01です。
これをstg-vtryo-web01のように、環境を先頭に入れる方法を書いておきます。

variable “env”

variables.tfにvariable "env { }"を書きます。

variables.tf
variable "env" { }

variable "ec2_config" {
  default = {
    ami           = "ami-4af5022c"
    instance_type = "t2.micro"
    instance_key  = "id_rsa"
  }
}

{ }の中にdefaultを入れてもよいです。その場合は以下のように書きます。

variables.tf
variable "env" { 
  default = "text message..."
 }

${var.env}

一方でec2.tfには以下を書きます。

resource "aws_instance" "vtryo-web" {
  ami                      = "${lookup(var.ec2_config, "ami")}"
  instance_type            = "${lookup(var.ec2_config, "instance_type")}"
  key_name                 = "${lookup(var.ec2_config, "instance_key")}"
  tags {
    Name = "${var.env}-vtryo-${format("web%02d", count.index + 1)}"
  }
}

${var.env}をつけることで、terraform planしたときに入力を求められます。
入力した内容が、${var.env}に格納されるという仕組みです。

terraform plan

Enter a valueを求められるので今回はstgにしました。
すると、stgという文字列が格納されます。

% terraform plan
var.env
  Enter a value: stg

Refreshing Terraform state in-memory prior to plan...
The refreshed state will be used to calculate this plan, but will not be
persisted to local or remote state storage.


------------------------------------------------------------------------

An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  + aws_instance.vtryo-web
      id:                           <computed>
      ami:                          "ami-4af5022c"
      associate_public_ip_address:  <computed>
      availability_zone:            <computed>
      ebs_block_device.#:           <computed>
      ephemeral_block_device.#:     <computed>
      instance_state:               <computed>
      instance_type:                "t2.micro"
      ipv6_address_count:           <computed>
      ipv6_addresses.#:             <computed>
      key_name:                     "id_rsa"
      network_interface.#:          <computed>
      network_interface_id:         <computed>
      placement_group:              <computed>
      primary_network_interface_id: <computed>
      private_dns:                  <computed>
      private_ip:                   <computed>
      public_dns:                   <computed>
      public_ip:                    <computed>
      root_block_device.#:          <computed>
      security_groups.#:            <computed>
      source_dest_check:            "true"
      subnet_id:                    <computed>
      tags.%:                       "1"
      tags.Name:                    "stg-vtryo-web01"
      tenancy:                      <computed>
      volume_tags.%:                <computed>
      vpc_security_group_ids.#:     <computed>


Plan: 1 to add, 0 to change, 0 to destroy.

------------------------------------------------------------------------

Note: You didn't specify an "-out" parameter to save this plan, so Terraform
can't guarantee that exactly these actions will be performed if
"terraform apply" is subsequently run.

ちなみにterraform plan -var "env=stg"とすることで入力なしで格納することが可能です。

今回の補足というか余談

「terraformのbest-practice、わかりづらくない?」

開発者が推奨している構成だとはわかっていますが、素直にそんな感想がありました。
terraform best-practice
(もちろんQiitaにかかれている記事も読んでいます
Terraform Best Practices in 2017

ファイルの中に何度も宣言をしないといけないようですし、どうにも読みづらい印象があります。
また、「Environmentで環境を分ける」構成になっていますが、公式曰く

Workspaces can be used to manage small differences between development, staging, and production, but they should not be treated as the only isolation mechanism.

とあります。我らがGoogle翻訳を使うとこんなことを言ってます。

ワークスペースは、開発、ステージング、およびプロダクションの小さな違いを管理するために使用できますが、唯一の分離メカニズムとして扱うべきではありません。

初見で解読するには骨がいる内容です。何度もterraformを使い込んだらわかるんでしょうか。
ま、単純に私の技術力が追いついていない可能性の方が高いですが(爆)

とはいえ誰でも彼でもterraformの技術力が高いわけではないので、より読みやすくメンテナンスしやすい書き方をしても良いと思っています。

terraformは一歩間違えるとインフラそのものが削除されることがあるので、それを踏まえて確実な方が良いだろうというきもちです。

今回はQiitaの記事にだいぶ助けられたこともあったので、Qiitaに書くことにしました。

シンプルに書きたい欲

terraformにはmodule化が出来る機能があって、それがやや難易度を上げているように思います。

変数をmodule化できることでメリットもありますが、利用ができなければ意味がない。terraformにはmoduleのレジストリがありますが、仕組みを理解しないとやっぱり使うのは大変です。

学習コストばかりかかるくらいなら、いっそ使わずにシンプルにコードを書いて行こうということでした。

best-practiceに固執しない

重要なのは可読性ととっつきやすさだと思ったので、あえて固執せずにディレクトリを構成し、terraformを見やすく書く方法を考えました。
誰かの参考になれば幸いです。

参考

Terraformのoutputでmapを利用する方法 – Qiita
Terraform Best Practices in 2017 – Qiita
TerraformでのAWS環境構築の設定を分ける – Qiita
terraform
さらに一歩楽しむterraform. moduleでIAM UserとPolicy管理を簡素化しよう
本記事はこちらのクローンです。

続きを読む

terraform_remote_stateで値を取得する際にはoutputの位置が大事

やりたかったこと

VPCとそれに乗るサービスを別々のTerraformで管理、VPCのIDをoutputで出力してそれをterraform_remote_stateで呼び出して利用したかった。 これはできた

workspaceとしてvpc-dev, db-devを用意、db-devをデプロイする際にvpc-devのoutputを利用したかったが、まだworkspaceをまたいでのoutputの利用はうまくできていない…ヘルプミー

とりあえずうまく行った部分だけ共有!

だめな例

├── vpc
│   ├── main.tf
│   ├── output.tf
│   └── variables.tf
└── vpc.tf
output.tf
output "vpc_id" {
  value = "${aws_vpc.vpc.id}"
}

main.tfvariable.tfは省略

vpc.tf
module "vpc" {
  source = "./vpc"
}

上記だとmoduleのvpcのoutputになるが、以下の記事からterraformのremote_stateはルートレベルのものしか許容していないためremote_stateは利用できない

https://www.terraform.io/docs/providers/terraform/d/remote_state.html#root-outputs-only

ルートレベル出力のみがアクセス可能です

tfstateを覗いて見るとルートにはoutputがなく、vpcモジュールにoutputがある

terraform.tfstate
~~~~~~~~~略~~~~~~~~~~~~
    "modules": [
            {
            "path": [
                "root"
            ],
            "outputs": {},
~~~~~~~~~略~~~~~~~~~~~~
            "path": [
                "root",
                "vpc"
            ],
            "outputs": {
                "vpc_id": {
                    "sensitive": false,
                    "type": "string",
                    "value": "vpc-*******"
                }
            },
~~~~~~~~~略~~~~~~~~~~~~

良い例

モジュールを呼び出した所で再度outputしてあげればOK

vpc.tf
module "vpc" {
  source = "./vpc"
}

output "vpc_id" {
  value = "${module.vpc.vpc_id}"
}

tfstateを見てみるとルートに値があることが確認できる

terraform.tfstate
~~~~~~~~~略~~~~~~~~~~~~
    "modules": [
            {
            "path": [
                "root"
            ],
            "outputs": {
                "vpc_id": {
                    "sensitive": false,
                    "type": "string",
                    "value": "vpc-********"
                }
            },
~~~~~~~~~略~~~~~~~~~~~~
            "path": [
                "root",
                "vpc"
            ],
            "outputs": {
                "vpc_id": {
                    "sensitive": false,
                    "type": "string",
                    "value": "vpc-*******"
                }
            },
~~~~~~~~~略~~~~~~~~~~~~

これによってremote_stateからoutputを取得できるようになりました

workspaceをまたいだremote_stateの利用ができるのかがまだわかっていないので知ってる人いたら教えてほしいです:bow:

参考リンク

続きを読む

AWS Lambda�× Goを試す

はじめに

AWS LambdaでGoがサポートされたので、早速、試してみました。

この記事では、

  • AWS Lambdaの簡単な使い方
  • AWS LambdaでGoを動かす方法

を紹介しています。

ザクザク行けば、10分くらいでLambdaの実行までいけると思います

Lambdaを作成する

AWS Lambdaのトップから、関数の作成を選択
Lambda_Management_Console.png

名前やロールなどを設定し、関数の作成を選択
ランタイムはGo 1.Xとします
ロールは、使用されるリソースに応じてお好みで。
aws_lambda_sample.png

Lambdaで動かすGoを作る

公式ライブラリを使用する

公式のGoのライブラリgo getします。

go get -u github.com/aws/aws-lambda-go/lambda

コードを書く

※公式のサンプルのまま書いています。

main.go
package main

import (
    "github.com/aws/aws-lambda-go/lambda"
)

func hello() (string, error) {
    return "Hello ƛ!", nil
}

func main() {
    // Make the handler available for Remote Procedure Call by AWS Lambda
    lambda.Start(hello)
}

ビルドする

ビルドのターゲットは以下を指定

GOOS=linux GOARCH=amd64 go build -o hello

これで、 helloという名前のバイナリがカレントディレクトリに生成されます。

$ ls
hello          main.go

zipに圧縮する

手動アップロードの場合、Lambdaに上げる時はzipで上げなければいけないため、固めます。

$ zip hello.zip hello 
  adding: hello (deflated 66%)
$ ls
hello              hello.zip          main.go

アップロード

アップロードから、先程作成したhello.zipを選択してアップロードを実行します。
アップロード後は、右上のテストボタンを選択します。
Lambda_Management_Console.png

テスト

テストを作成します。作成後、右上の保存を選択してください。
Lambda_Management_Console2.png

テストを実行すると…
ちゃんと、 Hello ƛ!と出力されています :tada:
Lambda_Management_Console3.png

終わりに :airplane:

読んで下さりありがとうございます。
今回はサンプルだけなので、次回は実際に活用した記事を書く予定です。

続きを読む