ec2 インスタンスをまとめてaws cli から停止する

忘れやすいのでメモメモ

1. まず確認

指定の名前タグが入っているインスタンスIDと名を取り出す。
タグに入れてあるそれらしい文字列XXXXXXを指定する。

bash
$ aws ec2 describe-instances | jq '.Reservations[].Instances[]
  | select(.Tags[].Key == "Name" and (.Tags[].Value | test("XXXXXX")))
  |  {"InstanceName":(.Tags[] | select(.Key=="Name").Value) ,"instance-id": .InstanceId}'

{
  "InstanceName": "XXXXXX-28",
  "instance-id": "i-0791d3f5e36351883"
}
{
  "InstanceName": "XXXXXX-22",
  "instance-id": "i-03c6f00bd51f7dbcd"
}
{
  "InstanceName": "XXXXXX-23",
  "instance-id": "i-0c5cf083435cb6070"
}
{
  "InstanceName": "XXXXXX-30",
  "instance-id": "i-0151fcf7f29fb0876"
}
{
  "InstanceName": "XXXXXX-25",
  "instance-id": "i-0d0360a9c2b6c8bae"
}

違うインスタンスは含まれていないようです。

2. 停止 インスタンスIDリスト作成

--cli-input-json

を使っても良いのですが、 JSONファイルを作らないと行けないので、 今回は、 --instance-ids で気楽に指定します。なので、1を修正して、
“instance-id1” “instance-id2”
の1行に変換します。

まず、インスタンスIDのみ取り出してみます。

bash
$ aws ec2 describe-instances | jq '.Reservations[].Instances[]
  | select(.Tags[].Key == "Name" and (.Tags[].Value | test("XXXXXX")))
  |  {"InstanceName":(.Tags[] | select(.Key=="Name").Value) ,"instance-id": .InstanceId}'

{
  "instance-id": "i-0791d3f5e36351883"
}
{
  "instance-id": "i-03c6f00bd51f7dbcd"
}
{
  "instance-id": "i-0c5cf083435cb6070"
}
{
  "instance-id": "i-0151fcf7f29fb0876"
}
{
  "instance-id": "i-0d0360a9c2b6c8bae"

悪くないですね。
これを、sed で IDのみ抽出します。

bash
$ aws ec2 describe-instances | jq '.Reservations[].Instances[]
  | select(.Tags[].Key == "Name" and (.Tags[].Value | test("XXXXXX")))
  |  {"instance-id": .InstanceId}' | sed -n -e 's/.*"instance-id": "(.*)"/1/p'

i-0791d3f5e36351883
i-03c6f00bd51f7dbcd
i-0c5cf083435cb6070
i-0151fcf7f29fb0876
i-0d0360a9c2b6c8bae

改行が入っているので削除してスペースに変換します。

bash
$ aws ec2 describe-instances | jq '.Reservations[].Instances[]
  | select(.Tags[].Key == "Name" and (.Tags[].Value | test("XXXXXX")))
  |  {"instance-id": .InstanceId}' | sed -n -e 's/.*"instance-id": "(.*)"/1/p' | tr 'n' ' '
i-0791d3f5e36351883 i-03c6f00bd51f7dbcd i-0c5cf083435cb6070 i-0151fcf7f29fb0876 i-0d0360a9c2b6c8bae

“instance-id1” “instance-id2” の形式になりました。

3. 停止コマンド発行

では、$ aws ec2 stop-instances を使いましょう。
最初は、Dry-runとか付けておくと良いでしょう。

$ aws ec2 stop-instances --dry-run --instance-ids $(2のコマンドを入れる)

次のようになります。

bash
aws ec2 stop-instances --dry-run --instance-ids $(
aws ec2 describe-instances | jq '.Reservations[].Instances[]
  | select(.Tags[].Key == "Name" and (.Tags[].Value | test("XXXXXX")))
  |  {"instance-id": .InstanceId}' | sed -n -e 's/.*"instance-id": "(.*)"/1/p' | tr 'n' ' ')

こんな感じです。 2の部分を --instance-ids の後ろに $() でBashの展開を利用して指定しています。
上記のコマンドでは、 $() の部分が実行されて実際は2のインスタンスIDのリストになっています。
では、–dry-runを外します。

bash
aws ec2 stop-instances --instance-ids $(aws ec2 describe-instances | jq '.Reservations[].Instances[]
  | select(.Tags[].Key == "Name" and (.Tags[].Value | test("XXXXXX")))
  |  {"instance-id": .InstanceId}' | sed -n -e 's/.*"instance-id": "(.*)"/1/p' | tr 'n' ' ')

{
    "StoppingInstances": [
        {
            "InstanceId": "i-0a011f82414e366bd",
            "CurrentState": {
                "Code": 64,
                "Name": "stopping"
            },
            "PreviousState": {
                "Code": 16,
                "Name": "running"
            }
        },
        {
            "InstanceId": "i-0528abc7952480f4c",
            "CurrentState": {
                "Code": 64,
                "Name": "stopping"
            },
            "PreviousState": {
                "Code": 16,
                "Name": "running"
            }
        },
        {
            "InstanceId": "i-0f049840b8939d681",
            "CurrentState": {
                "Code": 64,
                "Name": "stopping"
            },
            "PreviousState": {
                "Code": 16,
                "Name": "running"
            }
        },
    <<<<<中略>>>>>
    ]
}

良いようですね。コンソールでも確認します。

image.png

OKでした。

続きを読む

CloudfrontでIAMユーザにCache Invalidationのみをさせる方法

実現したいこと

 IAMユーザにキャッシュ失効(Cache Invalidation)を実行させたい。
 Cloudfrontの設定は変更させない

ポリシーJSON


{
    "Statement": [
        {
            "Action": [
                "cloudfront:CreateInvalidation",
                "cloudfront:GetDistribution",
                "cloudfront:GetInvalidation",
                "cloudfront:GetStreamingDistribution",
                "cloudfront:GetDistributionConfig",
                "cloudfront:ListDistributions",
                "cloudfront:ListInvalidations",
                "cloudfront:ListStreamingDistributions"
            ],
            "Effect": "Allow",
            "Resource": "*"
        }
    ]

必要に応じてS3へアクセス権限も組み合わせるとよいです。
特定のDistribution以外を非表示にする方法ないかな。

Amazon Cloudfront Document

続きを読む

AWS X-RayでLambda→Athenaのアクセスを可視化してみた

以前こんなものを作りましたが、これをAWS X-Rayで可視化してみたら、何がわかるのか、実験してみました。

Amazon AthenaをAWS Lambdaから操作できるようにしてみた

AWS X-Ray デーモンの実行

AWS X-Ray SDK は、AWS X-Ray に Trace データを直接送信しないらしいので、送付用のEC2インスタンスを作成します。ユーザデータとして以下を登録してインスタンスを生成するだけなので、簡単です。

#!/bin/bash
curl https://s3.dualstack.us-east-1.amazonaws.com/aws-xray-assets.us-east-1/xray-daemon/aws-xray-daemon-2.x.rpm -o /home/ec2-user/xray.rpm
yum install -y /home/ec2-user/xray.rpm

システムログにxrayのインストールログが出力されていたのでOKでしょう。

Examining /home/ec2-user/xray.rpm: xray-2.0.0-1.x86_64
Marking /home/ec2-user/xray.rpm to be installed
Resolving Dependencies
--> Running transaction check
---> Package xray.x86_64 0:2.0.0-1 will be installed
--> Finished Dependency Resolution

Dependencies Resolved

================================================================================
 Package         Arch              Version               Repository        Size
================================================================================
Installing:
 xray            x86_64            2.0.0-1               /xray            6.6 M

Transaction Summary
================================================================================
Install  1 Package

Total size: 6.6 M
Installed size: 6.6 M
Downloading packages:
Running transaction check
Running transaction test
Transaction test succeeded
Running transaction
  Installing : xray-2.0.0-1.x86_64                                          1/1 
xray start/running, process 2576
  Verifying  : xray-2.0.0-1.x86_64                                          1/1 

Installed:
  xray.x86_64 0:2.0.0-1                                                         

Complete!

Lambdaアプリ側の準備

今回Javaアプリケーションを動かすわけですが、LambdaアプリケーションをX-Rayで監視したい場合は、Lambdaアプリケーションの「設定」タブの中で以下のチェックボックスをONにするだけで良いようです。

スクリーンショット 2017-04-23 22.21.44.png

参考:http://docs.aws.amazon.com/ja_jp/xray/latest/devguide/xray-services.html

またX-Rayを操作するための権限をIAMで設定する必要もあります。今回は試験的な運用だったため「AWSXrayFullAccess」をつけてしまいましたが、実際の運用に合わせてこの辺りは慎重に選びたいですね。

アプリを起動して可視化してみる

ここまでできれば、普通にLambdaアプリを動かしてみてX-Rayでどのように見えるのか確認ができます。今回Lambdaアプリケーションには以下のJSONをインプットとして与えるようにしました。以前の記事でサンプルとしてAthenaのテーブルからデータを取得するようにした際の入力値です。

{
  "region": "us-east-1",
  "s3Path": "s3://ishida-athena-staging-dir/",
  "sql": "SELECT elbname, requestip,  requestport, backendip, backendport, requestprocessingtime, backendprocessingtime, timestamp FROM sampledb.elb_logs order by timestamp desc limit 10",
  "columnListStr": "elbname, requestip,  requestport, backendip, backendport, requestprocessingtime, backendprocessingtime,  timestamp"
}

実行後1分ほど待つと、以下のような表示がX-Rayで確認できました。無事可視化ができたようです。

スクリーンショット 2017-04-23 22.56.40.png

X-Rayの中身を確認してみる

表示されたService Mapの右側のオブジェクトをクリックすると以下のような表示がされました。
スクリーンショット 2017-04-23 22.56.51.png

それぞれの処理にどの程度時間がかかってレスポンスとして何を返しているのかが一覧でわかります。
表示されているIDをクリックすると、そのTraceの詳細が確認できました。

スクリーンショット 2017-04-23 22.56.58.png

これをみる限り、Lambdaアプリの初期化に230ms程度、実際のAthena接続部分に約3秒程度かかっている、という風にみればいいんですかね。この処理全体としては4.6秒かかっているので、実際にAthenaにアクセスするため以外に1.5秒ほどは時間が取られている、と理解すればいいんでしょうか。この辺はもっと勉強が必要だ(^^;

ちなみにエラーが出ている場合は、その例外の中身も確認することができるようです。

まとめ

それぞれの処理がどの程度時間にかかっていて、さらに呼び出し関係までこれほど簡単にセットアップしつつ可視化ができるのは強力ですね。これからMicroservicesなどで分散して処理をさせることが当たり前になることを考えると、必須の技術と言えると思います。Springで言えばZipkinとSleuthをAWS上で実現しているような感じですね。

続きを読む

Angular2をAWSのS3とCloudFrontでホスティングする

Angular2を使い始めたら、すぐAngular4がリリースされましたね。この世界の動きは早いですね:older_man_tone2:

今日はやっとAngular2をAWSでサーバレスでホスティングしてみました。方式も手順も難しくはないですが、すぐ忘れてしまいますので、いつも通りメモしたいと思います。

AWSでホスティング

Angular2とサーバの関係

Angular2はコンパイル後はindex.htmlとcss, javascriptのセットからなる、静的なコンテンツですので、Webサーバを選ばずホスティングできるようです。

AWSで静的コンテンツをホスティングする

AWSの完全ガイドはこちらですが、少々難しいので、端折りながら…

基本的には、

1. S3にコンテンツをアップロードする
2. CloudFrontで、S3のコンテンツを配信する
3. Route 53で、CloudFrontのホスト名に対して公開したいホスト名をエイリアスで付ける

という手順になるようです。

S3にRoute53でエイリアスを付けることも可能ですので、S3だけでもホスティングはできるのですが、S3はHTTPsに対応していないようです。そのため、今回の手順は(HTTPsに対応している)CloudFrontまで入れています。

実際の手順

1. S3での作業

AWSの公式ガイドはこちらです。

1-1. バケットを作成

バケット名は、ホストするサーバ名にします。
※ここではAWSの例通り、『example.com』とします。

1-2. 静的ウェブサイトホスティングを有効にする

バケットのプロパティタブで、上記の項目をクリックし、ラジオボタンで『有効にする』を選択します。

AWS入力欄
- インデックスドキュメント:index.html
- エラードキュメント:error.html

を入力して保存します。

Angular2を ng build コマンドでトランスパイルすると、index.htmlが生成されますので、インデックスドキュメントはそれを参照する形になります。error.htmlはAngular2では用意されませんので、自分でエラーページを作って、名前をerror.htmlにします。

1-3. アクセス許可で、バケットポリシーを作成する

バケットのプロパティタブで、上記の項目をクリックし、『バケットポリシーの編集』ボタンを押して、ダイアログに以下のポリシーJSONを貼り付けます。

ポリシー
{
  "Version":"2012-10-17",
  "Statement":[{
    "Sid":"AddPerm",
        "Effect":"Allow",
      "Principal": "*",
      "Action":["s3:GetObject"],
      "Resource":["arn:aws:s3:::example.com/*"
      ]
    }
  ]
}

コロンの後ろに続く、『example.com』の記載は、ホストするサーバ名(バケット名)になります。

1-4. Angular2ファイルのアップロード

ng buildコマンドを実行し、生成されたコンテンツ(デフォルトでは、distフォルダ)の中の、全てのファイル(index.html, xx.buldle.js, xx.buldle.js.map)を作成したS3バケットにアップロードします。

独自に、favicon, error.htmlを作成している場合は、それも同じS3バケットにアップロードします。

1-5. S3でのホスティングの確認

プロパティタブ>静的ウェブサイトホスティングで表示されるエンドポイントをクリックして、ブラウザに表示されれば成功です。

エンドポイント例)
example.com.s3-website-ap-northeast-1.amazonaws.com

以上でS3での作業は完了です。

2. CloudFrontでの作業

2-1. Distributionを新規作成する

まず、Web distributionを作成するボタンを押し、その後に以下を設定します。

  • Origin Domain Nameに、S3バケットのエンドポイントを設定
  • Viewer Protocol PolicyをRedirect HTTP to HTTPSに設定(任意)
  • Allowed HTTP Methodsに、GET, HEAD, OPTIONSを設定(任意)※1
  • Alternate Domain Names(CNAMEs)にRoute 53で設定するホスト名を設定
  • SSL Certificateを、Custom SSL Certificateを設定(任意)※2
  • Default Root Objectに『index.html』を設定 ※3

※1) CORSをする場合は、プリフライトリクエストに対応するためにOPTIONSを有効にする必要があります。
※2) CloudFrontのHTTPs化については、こちらこちらを参考にしました。
※3) デフォルトルートオブジェクトについては、こちらの説明をご参照ください。

3. Route53の作業

DistoributionのDomain Name(例:dxxxxxxxx.cloudfront.net)を、ホスト名のエイリアスに設定すれば完了です。

Route53入力例)
Name: example.com
Alias Target: dxxxxxxxx.cloudfront.net

参考)S3に対してRoute 53でホスト名を割り振る場合

HTTPsが必要ない場合には、CloudFrontまで使わずにS3だけでホスティングすることができます。

このとき、Route 53でエイリアスとして指定するのは、S3のwebホスティングを示すドメイン名部分です。面白いことに『example.com』のような固有値が必要ありません。

Route53入力例)
Name: example.com
Alias Target: s3-website-ap-northeast-1.amazonaws.com

私も最初は『example.com』を付けたFQDN(example.com.s3-website-ap-northeast-1.amazonaws.com)を設定してハマりまして、こちらを参考にさせて頂いて上記の設定に気が付きました。

Route53でTargetが表示されない場合は、こちらのページをご参考にしてみたら良いかもしれません。

以上です:grinning:

続きを読む

RDSのスロークエリログをmysqldumpslowに突っ込む

JSON形式で取得されるRDS(MySQL)のスロークエリログをcatして絶望したときに、心を落ち着けてmysqldumpslowに突っ込めるようにします。

用意するもの

  • awscli
  • jq
  • mysqldumpslow 1

スロークエリログのダウンロード

スロークエリログのリストを取得

$ aws rds describe-db-log-files \
  --db-instance-identifier hoge-instance \
  | jq '.DescribeDBLogFiles[].LogFileName' -r  \
  | grep slow
slowquery/mysql-slowquery.log
slowquery/mysql-slowquery.log.0
...

スロークエリログのダウンロード

describe-db-log-files取得されたファイル名には/が含まれていて邪魔なので、ファイルに保存するときに取っています。

for log in $(cat slowlog-list.txt); do \
  aws rds download-db-log-file-portion \
  --db-instance-identifier hoge-instance \
  --log-file-name $log \
  > $(echo $log | cut -d / -f 2); done

スロークエリログの読み込み

取得されたスロークエリログの中身はJSONデータの奥に格納されたテキスト文字列なので、jqで抽出してからmysqldumpslowに食わせます。

$ cat mysql-slowquery.log \
  | jq -rs .[].LogFileData \
  | mysqldumpslow -
# 遅い順にソートされたクエリたち...

  1. Ubuntuではmysql-clientをインストールするとついてきます。 

続きを読む

Terraformでstate lockを触ってみた。

はじめに

初めてTerraformを触る折に
「v0.9以上だとstate lockっていう機能があるから使ったほうが良いよ」
というアドバイスをもらいました。
(そもそも、stateってなんですか?から始まるわけですが・・・)

初めてTerraformを触った時に、公式ドキュメントだけでは???な状態だったので
痕跡として残しておきます。

結果として、まだ使いこなせてません。というか僕の使い方は果たしてあっているのだろうか
なので情報としては不足しているかもしれませんが、とりあえず備忘録的に残します。
またわかったらUpdateします。

そもそもState Lockとは

完全に理解しないで書いてますが
Terraformは「自分が知っているリソース(EC2やS3)しか関与しないよ」というポリシーで
どのリソースを自分が扱ったか、というのをtfstateというJSONファイルで管理しているようです。

このtfstateファイルは、一人でTerraformを動かす場合は問題無いのですが
複数人でTerraformをいじる必要が出てきた時に問題で、それは「backend」モジュールを使うことで回避してきたようですが
同じタイミングでterraformを実施した場合、その部分までは制御しきれてなかったようです。

で、v0.9以上?から、「Plan/Applyをしているタイミングではロックする」機能が実装されたようで。
せっかくなので導入してみました。

公式サイト:https://www.terraform.io/docs/state/locking.html

準備

手動で準備が必要なものは
・terraformを実行するユーザのCredential情報
→ めんどくさかったので test-terraformとかいうユーザを別途作成しました。
・S3 Bucketの作成
→ terraform-s3-state とかいう名前で作りましょう
・DynamoDBの作成
→ 名前はなんでも良いです。terraform-state-lock とかで作りましょう。
  プライマリキーを「LockID」としてください。それ以外では動きません。
作り終えたら、読み込み・書き込み容量ユニットは最低の1にしておいたほうが良いと思います。

※ S3とDynamoDBをterraformで管理はしないほうが良さげです。
どのみち、初回実行時に「そんなリソース無いんだけど!」ってTerraformが怒ります。

動くまで

今回はState Lockの話がメインなので作るリソースはなんでも良いです。
リージョンが関係無いRoute53を題材にします。

route53.tf
# 適当に1ゾーン作ります。

resource "aws_route53_zone" "test_zone" {
    name    =   "test.lockstate.com"
}
settings.tf
# ネーミングよくないですが、providerとbackendの設定します

provider "aws" {
    access_key = "ACCESS_KEY"
    private_key = "PRIVATE_KEY"
}

terraform {
    backend "s3" {
        bucket     = "terraform-s3-state"
        key        = "terraform.tfstate"
        region     = "s3とdynamoがいるregion"
        lock_table = "terraform-state-lock"
    }
}

これで、「該当のディレクトリに移動して」$ terraform plan してください。(怒られます。)

Backend reinitialization required. Please run "terraform init".
Reason: Initial configuration of the requested backend "s3"

The "backend" is the interface that Terraform uses to store state,
perform operations, etc. If this message is showing up, it means that the
Terraform configuration you're using is using a custom configuration for
the Terraform backend.

とりあえず terraform init しろよと言われるので言われるがままに。

$ terraform init

ただし、以下の通り、認証情報がねーんだけど!って怒られる。なんやねん。

Initializing the backend...

Error configuring the backend "s3": No valid credential sources found for AWS Provider.
  Please see https://terraform.io/docs/providers/aws/index.html for more information on
  providing credentials for the AWS Provider

Please update the configuration in your Terraform files to fix this error
then run this command again.

ここらへんちゃんとよくわかってないですが、この問題の対処として2つありました。
A. settings.tfのbackendにaccess_key/secret_keyの2つを渡してCredential情報を平文で書く。
-> 変数でいいじゃん!と思ったんですが、変数で渡すと、Terraformがよしなにやる「前に」
  Backendが立ち上がるために違う方法で渡してくれ、と怒られました。
以下のようなメッセージ

% terraform init 
Initializing the backend...
Error loading backend config: 1 error(s) occurred:

* terraform.backend: configuration cannot contain interpolations

The backend configuration is loaded by Terraform extremely early, before
the core of Terraform can be initialized. This is necessary because the backend
dictates the behavior of that core. The core is what handles interpolation
processing. Because of this, interpolations cannot be used in backend
configuration.

If you'd like to parameterize backend configuration, we recommend using
partial configuration with the "-backend-config" flag to "terraform init".

B. 環境変数にaccess_key/secret_keyの2つを食わせる
  → 今はこちらを採用中。

init or plan実行時に、状況に応じて「ローカルのStateをS3にコピーするか / S3からStateファイルをローカルにコピーするか」聞かれるかもしれません。
初回(もしくはテスト)であればYes、すでに何回か実行しているような状況ではnoでいいんじゃないでしょうか。

これで、terraform plan (or apply)を実行すると
DynamoDBのLockDBに対して誰が使用しているか、のロック情報が書き込まれ
終わったタイミングでリリースされます。

ターミナルやシェルを複数立ち上げ、同じぐらいのタイミングで
terraform plan( or apply )を実行すると、ロックが成功できた奴以外はエラーで落とされます。
ただし、ロックがリリースされる前の実行中などにCtrl-Cなどで強制終了させるとロックが残り続けるので注意。
$ terraform force-unlock ID情報
で強制ロック解除できます。

今僕が解決できてない問題点

公式ドキュメントを見る限り、「terraform remote コマンドは terraform initコマンドに置き換えたよ!」と言っています。
また、backendを使用する、backendを書き換えた、などのタイミングに起因して terraform init を求められます。

が、terraform initは、「実行されたディレクトリに対して .terraform/terraform.tfstate」を吐き出します。
そしてそれが無いと怒ります。
まだ試せてはいませんが、以下のようなtfの配置をしていたりした場合
root配下でinitを実行しても、tfファイルがありませんと怒られる状況で
tfファイルがあるディレクトリまで移動しないとinitができない状態です。

$ terraform init ./route53/tf
とするとroot直下にtfファイルがコピーされる状況です。
なので、リソースごとに区切る、とか環境ごとに区切る、とかはうまくできていません。
別の見方をすれば、環境ごとに一つのディレクトリでリソース類は全部その中で管理しろよ!
というHashiCorpからのお達しなのか・・・?
これは、新しく実装されたEnvironmentを試せ、ということなんでしょうか。。。

と思ったらBackendConfigなるものがあるらしいのでそれを試してみよう。

root
├── settings
│   └── tf
│   └── settings.tf
├── route53
│   └── tf
│   └── route53.tf
└── ec2
└── tf
└── ec2.tf

Terraformとの付き合い方がよくわからない

続きを読む

簡単に仮想のS3を作成してAWSLambdaとS3サービスの連携をローカル環境でテストする

事前準備

実行する前にEclise用のAWSツールキットを導入しておいてください。導入手順は以下のリンクにご参考をお願いします。

ー>AWS Toolkit導入手順

導入完成したらAWSプロジェクトがプロジェクト新規画面で出てくるはずです。

mavenは導入済みの前提です。

Screenshot from 2017-04-20 17-13-55.png

Lambdaファンクションを書く

  1. まずはAWS Lambda Java ファンクションのプロジェクト作成する.

Screenshot from 2017-04-20 17-19-41.png
- プロジェクト名 :S3EventTutorial
- パッケージ名:com.amazonaws.lambda.s3tutorial
以上のように必須な情報をいれておきまましょう。「完了」を押したらプロジェクトは作成されて一般的なプロジェクトフォルダは以下のようにみれます。
Screenshot from 2017-04-20 17-25-12.png

  1. S3をモックするように「s3mock_2.11」というライブラリをMavenで導入する。pomファイルに依存ライブラリを定義するだけでいいですので下のpomファイルを参考にして自分が作成したプロジェクトのpomを作成してみてください。
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.amazonaws.lambda</groupId>
    <artifactId>s3tutorial</artifactId>
    <version>4.0.0</version>
    <dependencies>
        <dependency>
            <groupId>com.amazonaws</groupId>
            <artifactId>aws-lambda-java-core</artifactId>
            <version>1.1.0</version>
            <scope>compile</scope>
        </dependency>
        <dependency>
            <groupId>com.amazonaws</groupId>
            <artifactId>aws-lambda-java-events</artifactId>
            <version>1.3.0</version>
            <scope>compile</scope>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.11</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>com.amazonaws</groupId>
            <artifactId>aws-java-sdk</artifactId>
            <version>1.11.119</version>
            <scope>compile</scope>
        </dependency>

        <!-- https://mvnrepository.com/artifact/com.typesafe.akka/akka-http-experimental_2.11 -->
        <dependency>
            <groupId>com.typesafe.akka</groupId>
            <artifactId>akka-http-experimental_2.11</artifactId>
            <version>2.4.11.1</version>
        </dependency>

        <!-- https://mvnrepository.com/artifact/com.typesafe.scala-logging/scala-logging_2.11 -->
        <dependency>
            <groupId>com.typesafe.scala-logging</groupId>
            <artifactId>scala-logging_2.11</artifactId>
            <version>3.5.0</version>
        </dependency>

        <!-- https://mvnrepository.com/artifact/io.findify/s3mock_2.11 -->
        <dependency>
            <groupId>io.findify</groupId>
            <artifactId>s3mock_2.11</artifactId>
            <version>0.1.10</version>
            <scope>test</scope>
        </dependency>
        <!-- https://mvnrepository.com/artifact/org.mockito/mockito-core -->
        <dependency>
            <groupId>org.mockito</groupId>
            <artifactId>mockito-core</artifactId>
            <version>2.7.22</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/com.github.tomakehurst/wiremock -->
        <dependency>
            <groupId>com.github.tomakehurst</groupId>
            <artifactId>wiremock</artifactId>
            <version>2.6.0</version>
        </dependency>


    </dependencies>
</project>

ロカールにおいてあるmavenのリポジトリーにない依存ライブラリがあるかもしれないので一応プロジェクトのrootフォルダで「mvn package」をコマンドラインとして実行してみましう。そしてmavenはpomに定義されていたdependencyをダウンロードしてくれます。

  1. Lambdaファンクションのロジック
    作成してもらったLambdaFunctionHandler.javaを開いてロジックをかいてみましょう。アイデアは凄っく簡単です。

S3からファイルがアップロードされたというイベントがこられたら、イベントの内容を見てアップロードされたファイルをゲットしてコンソールでそのファイルを書き出すという作業です。コードみてみたらすぐ分かると思いますので説明しないですむ。


public class LambdaFunctionHandler implements RequestHandler<S3Event, Object> {

    private AmazonS3 s3Client;

    public LambdaFunctionHandler(AmazonS3 s3Client){
        this.s3Client = s3Client;
    }
    public LambdaFunctionHandler(){
        this.s3Client =  new AmazonS3Client(new ProfileCredentialsProvider());
    }

    private static void storeObject(InputStream input) throws IOException {
        // Read one text line at a time and display.
        BufferedReader reader = new BufferedReader(new InputStreamReader(input));
        while (true) {
            String line = reader.readLine();
            if (line == null)
                break;
            System.out.println("    " + line);
        }
        System.out.println();
    }

    @Override
    public Object handleRequest(S3Event input, Context context) {
        context.getLogger().log("Input: " + input);

        // Simply return the name of the bucket in request
        LambdaLogger lambdaLogger = context.getLogger();
        S3EventNotificationRecord record = input.getRecords().get(0);
        lambdaLogger.log(record.getEventName()); // イベント名

        String bucketName = record.getS3().getBucket().getName();
        String key = record.getS3().getObject().getKey();
        /*
         * Get file to do further operation
         */
        try {
            lambdaLogger.log("Downloading an object");

            S3Object s3object = s3Client.getObject(new GetObjectRequest(bucketName, key));

            lambdaLogger.log("Content-Type: " + s3object.getObjectMetadata().getContentType());

            storeObject(s3object.getObjectContent());

            // Get a range of bytes from an object.

            GetObjectRequest rangeObjectRequest = new GetObjectRequest(bucketName, key);
            rangeObjectRequest.setRange(0, 10);
            S3Object objectPortion = s3Client.getObject(rangeObjectRequest);

            System.out.println("Printing bytes retrieved.");
            storeObject(objectPortion.getObjectContent());

        } catch (AmazonServiceException ase) {
            System.out.println("Caught an AmazonServiceException, which" + " means your request made it "
                    + "to Amazon S3, but was rejected with an error response" + " for some reason.");
            System.out.println("Error Message:    " + ase.getMessage());
            System.out.println("HTTP Status Code: " + ase.getStatusCode());
            System.out.println("AWS Error Code:   " + ase.getErrorCode());
            System.out.println("Error Type:       " + ase.getErrorType());
            System.out.println("Request ID:       " + ase.getRequestId());
        } catch (AmazonClientException ace) {
            System.out.println("Caught an AmazonClientException, which means" + " the client encountered "
                    + "an internal error while trying to " + "communicate with S3, "
                    + "such as not being able to access the network.");
            System.out.println("Error Message: " + ace.getMessage());
        }catch (IOException ioe){
            System.out.println("Caught an IOException, which means" + " the client encountered "
                    + "an internal error while trying to " + "save S3 object, "
                    + "such as not being able to access the network.");
            System.out.println("Error Message: " + ioe.getMessage());
        }
        return record.getS3().getObject().getKey();
    }

}


書いたコードに対してのテストケースを作成しましょう

今回は実装したLambdaコードを注目しますのでLambdaFunctionHandlerTestを開いてテストケース作成します。まずはテストケースのコードに目を通してみましょう。


    private static S3Event input;
    private static AmazonS3Client client;

    @BeforeClass
    public static void createInput() throws IOException {
        input = TestUtils.parse("s3-event.put.json", S3Event.class);

        S3Mock api = S3Mock.create(8999, "/tmp/s3");
        api.start();

        client = new AmazonS3Client(new AnonymousAWSCredentials());
        client.setRegion(Region.getRegion(Regions.AP_NORTHEAST_1));

        // use IP endpoint to override DNS-based bucket addressing
        client.setEndpoint("http://127.0.0.1:8999");

    }

    private Context createContext() {
        TestContext ctx = new TestContext();

        // TODO: customize your context here if needed.
        ctx.setFunctionName("Your Function Name");

        return ctx;
    }

    @Test
    public void testLambdaFunctionHandlerShouldReturnObjectKey() {

        client.createBucket(new CreateBucketRequest("newbucket", "ap-northeast-1"));
        ClassLoader classLoader = this.getClass().getClassLoader();
        File file = new File(classLoader.getResource("file/test.xml").getFile());
        client.putObject(new PutObjectRequest(
                                 "newbucket", "file/name", file));

        LambdaFunctionHandler handler = new LambdaFunctionHandler(client);
        Context ctx = createContext();

        Object output = handler.handleRequest(input, ctx);

        if (output != null) {
            assertEquals("file/name", output.toString());
            System.out.println(output.toString());
        }
    }

テストのため、createInput関数でS3Mockのインスタンスを作成して起動します。このインスタンスはローカル環境の8999番ポートにバイドしてリクエストを待ちます。それに「/temp/s3」というフォルダを作成しておいてS3サービスのストレージを真似する。

一番大事なのはtestLambdaFunctionHandlerShouldReturnObjectKeyという関数の内容です。見るの通り、以下の作業を実装します。
– 「testbucket」を作成する。注意:Regionを指定するのは必須です(Regionの内容は別になでもいいですがなかったらjava.lang.NoSuchMethodError: com.amazonaws.regions.RegionUtils.getRegionByEndpoint(Ljava/lang/String;)Lcom/amazonaws/regions/Region;というErrorが出てきます。これはAWSのバグです)
– プロジェクトのしたにあるresourceフォルダに作成したfile/test.xmlを仮ストレージにアップロードする
– アップロードしたファイルを仮S3からダウンロードして内容をチェックする。

トリガーは「s3-event.put.json」で定義されているイベントの内容なので「s3-event.put.json」の内容にアップロードされたファイルの情報を反映しなければなりません


{
  "Records": [
    {
      "eventVersion": "2.0",
      "eventSource": "aws:s3",
      "awsRegion": "us-east-1",
      "eventTime": "1970-01-01T00:00:00.000Z",
      "eventName": "ObjectCreated:Put",
      "userIdentity": {
        "principalId": "EXAMPLE"
      },
      "requestParameters": {
        "sourceIPAddress": "127.0.0.1"
      },
      "responseElements": {
        "x-amz-request-id": "C3D13FE58DE4C810",
        "x-amz-id-2": "FMyUVURIY8/IgAtTv8xRjskZQpcIZ9KG4V5Wp6S7S/JRWeUWerMUE5JgHvANOjpD"
      },
      "s3": {
        "s3SchemaVersion": "1.0",
        "configurationId": "testConfigRule",
        "bucket": {
          "name": "testbucket",
          "ownerIdentity": {
            "principalId": "EXAMPLE"
          },
          "arn": "arn:aws:s3:::mybucket"
        },
        "object": {
          "key": "file/name",
          "size": 1024,
          "eTag": "d41d8cd98f00b204e9800998ecf8427e"
        }
      }
    }
  ]
}

注意:bucket名とobjectのキーは一番大事です。見た内容の通りファイルはtestbuckにfile/nameというキーでアップロードされましたので応じてjsonの内容はそ言うことを表現される。

#終わり

ドラフトに説明しましたが不明なところがありましたらご相談をお願いします

続きを読む

CloudFormerのちょっとした落とし穴

はじめに

同じ内容で2回もハマってしまったのでメモ。

CloudFormerで既存のAWS構成をテンプレートに落とした時、CloudFormationの構文エラーになるJSONが吐かれる事があるという話です。

本題

文字列の配列でなければならないプロパティーが、単一の文字列になってしまいます。

CloudFormerの出力
{
    "s3policyxxxbucket": {
      "Type": "AWS::S3::BucketPolicy",
      "Properties": {
        "Bucket": {
          "Ref": "s3xxxbucket"
        },
        "PolicyDocument": {
          "Version": "2012-10-17",
          "Id": "Policy-xxx",
          "Statement": [
            {
              "Sid": "xxxxxxxxxxxx",
              "Effect": "Allow",
              "Principal": "*",
              "Action": "s3:GetObject",
              "Resource": "<省略>",
              "Condition": {
                "IpAddress": {
                  "aws:SourceIp": "0.0.0.0/0"
                }
              }
            }
          ]
        }
      }
    }
}

上記の"Action": "s3:GetObject"が問題で、正しくは"Action": [ "s3:GetObject" ]です。

最後に

CloudFormerで既存のAWS構成をテンプレートに落とすことが出来るのは便利ですが、それを使う時にはちょっと注意が必要ですね。

それと、CloudFormationのエラー出力がもう少し情報を出してくれれば..と思います。今回もInvalid Resource ...ぐらいしか言ってくれなかったので、他の箇所を疑ってしまい、解決するまでに時間を浪費しました。

とは言え、CloudFormerの出力はそのまま使うなと公式にも注意書きしてあった気がするので、よく確認しなかった自分が悪いのですが。

続きを読む

[AWS] Terraform で EFS 作って、EC2 起動時にマウントさせておく

Terraform を使うと、EFS を作成して EC2 にマウントさせておくなんてことが簡単にできます。
Autoscaling 環境で Web ドキュメントルートを共有したい時とかに便利なんで、みんな使えばいいと思うよ。
なお、この記事の想定読者は AWS はダッシュボードからポチポチしてインスタンス立てたりしてるけど、そろそろインフラをコードで管理したいな。Terraform とか便利そうだねー使ってみたいねーって人です。
てわけで、単純に EC2 立ち上げても面白くないので EFS をマウントさせてみました。

そもそも、Terraform ってなんだ?って人は、以下のページとか参考になると思います。
Terraform簡易チュートリアル on AWS

実際の設定

Terraform は、特定のディレクトリ下にある拡張子が .tf なファイルを全部読み込んでいい感じにリソースを起動してくれます。なので、機能別に .tf 作成していってみましょう。

メイン設定

まず、メインの設定を作成。
プロバイダーとか、設定ファイル内で使用する変数とか設定していってみましょうか。

main.tf
# 今回のプロジェクト名
variable "project" {}
variable "domain" {}

# AWS リソースを起動するリージョンとかの情報
variable "region" { default = "us-west-2" }
variable "azs" {
    default {
        "a" = "us-west-2a"
        "b" = "us-west-2b"
        "c" = "us-west-2c"
    }
}

# AMI ID (Amazon Linux)
variable "ami" { 
    default {
        "us-west-2" = "ami-8ca83fec"
    }
}

# EC2 接続用の SSH 鍵の公開鍵
variable "ssh_public_key" {}

provider "aws" {
    region = "${var.region}"
}

variable で設定した値は tf ファイル内で ${var.region} のようにして参照可能です。
また、terraform の各種コマンドを実行する際に以下のようにパラメータとして変数を渡して上書きすることもできます。

$ terraform plan \
  -var 'project=example' \
  -var 'domain=example.com'

同じディレクトリ内に terraform.tfvars というファイルがあれば、それを読み込んで値が上書きされたりします。この辺の詳細は以下を参照してください。
Input Variables – Terraform by HashiCorp

provider "aws" は、aws を使いますよって宣言です。
以下のように アクセスキーを書いておくこともできますが、それやるとうっかり github とかに公開した時とかに切ない目にあうのでやめたほうが吉でしょう。

provider "aws" {
    access_key = "__ACCESS_KEY__"
    secret_key = "__SECRET_KEY__"
    region = "us-west-2"
}

環境変数 AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY を読み込んでいい感じでやってくれるので、僕は direnv 使って作業ディレクトリ内で環境変数を変更することで対応してます。
(もちろん、この場合でも .gitignore.envrc を含めておいて間違って公開しないようにしないと切ない目にあうので注意)

VPC の作成

こんな感じの .tf ファイルで VPC と subnet が作成できます。

vpc.tf
## VPC
resource "aws_vpc" "app" {
    cidr_block           = "172.31.0.0/16"
    enable_dns_hostnames = true
    enable_dns_support   = true
    instance_tenancy     = "default"

    tags {
        "Name" = "${var.project}"
    }
}

## Subnet
resource "aws_subnet" "a" {
    vpc_id                  = "${aws_vpc.app.id}"
    cidr_block              = "172.31.0.0/20"
    availability_zone       = "${lookup(var.azs,"a")}"
    map_public_ip_on_launch = true

    tags {
        "Name" = "${var.project}-subnet-a"
    }
}

resource "aws_subnet" "b" {
    vpc_id                  = "${aws_vpc.app.id}"
    cidr_block              = "172.31.16.0/20"
    availability_zone       = "${lookup(var.azs,"b")}"
    map_public_ip_on_launch = true

    tags {
        "Name" = "${var.project}-subnet-b"
    }
}

resource "aws_subnet" "c" {
    vpc_id                  = "${aws_vpc.app.id}"
    cidr_block              = "172.31.32.0/20"
    availability_zone       = "${lookup(var.azs,"c")}"
    map_public_ip_on_launch = true

    tags {
        "Name" = "${var.project}-subnet-c"
    }
}

resource "aws_subnet" の中に ${aws_vpc.app.id} ってのが出てきましたね。
Terraform の中では、管理下にあるリソースの情報を他のリソースの設定でも参照することが可能です。
各リソースで使用できる値が異なってくるので、その辺は公式ドキュメント読みましょう。
例えば aws_vpc で使用できる値は aws_vpc を参照すればわかります。

また、${lookup(var.azs,"a")} ってのも出てきましたね。
これは Terraform の組み込み関数です、lookup は配列の中からキーをもとに値を探す関数です。
詳しくは Built-in Functions を読んでください。

ついでに Internet Gateway と Route Table も設定しておきましょう。

route-table.tf
## Internet Gateway
resource "aws_internet_gateway" "igw" {
    vpc_id = "${aws_vpc.app.id}"
}

## Route Table
resource "aws_route_table" "rtb" {
    vpc_id     = "${aws_vpc.app.id}"
    route {
        cidr_block = "0.0.0.0/0"
        gateway_id = "${aws_internet_gateway.igw.id}"
    }
}

resource "aws_route_table_association" "route_a" {
    subnet_id = "${aws_subnet.a.id}"
    route_table_id = "${aws_route_table.rtb.id}"
}

resource "aws_route_table_association" "route_b" {
    subnet_id = "${aws_subnet.b.id}"
    route_table_id = "${aws_route_table.rtb.id}"
}

resource "aws_route_table_association" "route_c" {
    subnet_id = "${aws_subnet.c.id}"
    route_table_id = "${aws_route_table.rtb.id}"
}

IAM ロールの作成

次に EC2 に割り当てるための IAM ロールを作ってみましょう。
ポリシーは、AWS が用意している AmazonEC2RoleforDataPipelineRole と、EC2 から CloudwatchLogs にログを送信するためのカスタムポリシーを作ってアタッチしてみます。

iam-role.tf
## For EC2 instance Role
resource "aws_iam_role" "instance_role" {
    name               = "instance_role"
    path               = "/"
    assume_role_policy = <<POLICY
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": "ec2.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}
POLICY
}

## AmazonEC2RoleforDataPipelineRole
resource "aws_iam_role_policy_attachment" "data-pipeline" {
    role       = "${aws_iam_role.instance_role.name}"
    policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonEC2RoleforDataPipelineRole"
}

## PutCloudwatchLogs
resource "aws_iam_policy" "put-cloudwatch-logs" {
    name        = "AmazonEC2PutCloudwatchLogs"
    description = ""
    policy      = <<POLICY
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Action": [
                "logs:CreateLogGroup",
                "logs:CreateLogStream",
                "logs:PutLogEvents"
            ],
            "Effect": "Allow",
            "Resource": "*"
        }
    ]
}
POLICY
}

resource "aws_iam_role_policy_attachment" "put-cloudwatch-logs" {
    role       = "${aws_iam_role.instance_role.name}"
    policy_arn = "${aws_iam_policy.put-cloudwatch-logs.arn}"
}

aws_iam_roleassume_role_policy のところと、aws_iam_policypolicy のところでヒアドキュメントが出てきましたね。
こんな風に複数行にわたるインラインポリシーはヒアドキュメントで記述することが可能です。
また、以下のように別ファイルにしておいて読み込ませることも可能です。
管理しやすい方でやってください。

iam-role.tf
resource "aws_iam_role" "instance_role" {
    name               = "instance_role"
    path               = "/"
    assume_role_policy = "${file("data/instance_role_assume_policy.json")}"
}
data/instance_role_assume_policy.json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": "ec2.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}

セキュリティグループの作成

EC2 から EFS へのアクセスは 2049 番ポートを介して行われるので、EFS が所属するセキュリティグループに穴を開けないといけません。
EC2 は 80, 443, 22 を解放してみます。

security-group.tf
## For EC2
resource "aws_security_group" "ec2" {
    name        = "${var.project}-EC2"
    description = "for ${var.project} EC2"
    vpc_id      = "${aws_vpc.app.id}"

    ingress = [
        {
            from_port       = 80
            to_port         = 80
            protocol        = "tcp"
            cidr_blocks     = ["0.0.0.0/0"]
        },
        {
            from_port       = 443
            to_port         = 443
            protocol        = "tcp"
            cidr_blocks     = ["0.0.0.0/0"]
        },
        {
            from_port       = 22
            to_port         = 22
            protocol        = "tcp"
            cidr_blocks     = ["0.0.0.0/0"]
        }
    ]

    egress {
        from_port       = 0
        to_port         = 0
        protocol        = "-1"
        cidr_blocks     = ["0.0.0.0/0"]
    }
}

## For EFS
resource "aws_security_group" "efs" {
    name        = "${var.project}-EFS"
    description = "for ${var.project} EFS"
    vpc_id      = "${aws_vpc.app.id}"

    ingress {
        from_port       = 2049
        to_port         = 2049
        protocol        = "tcp"
        security_groups = ["${aws_security_group.ec2.id}"]
    }

    egress {
        from_port       = 0
        to_port         = 0
        protocol        = "-1"
        cidr_blocks     = ["0.0.0.0/0"]
    }
}

EFS の作成

こんな感じで EFS が作成できます。
各サブネットごとにマウントターゲットを作成して、そいつをセキュリティグループに所属させる形ですね。

efs.tf
resource "aws_efs_file_system" "app" {
  tags {
        "Name" = "${var.domain}"
  }
}

resource "aws_efs_mount_target" "app-a" {
  file_system_id  = "${aws_efs_file_system.app.id}"
  subnet_id       = "${aws_subnet.a.id}"
  security_groups = ["${aws_security_group.efs.id}"]
}

resource "aws_efs_mount_target" "app-b" {
  file_system_id = "${aws_efs_file_system.app.id}"
  subnet_id      = "${aws_subnet.b.id}"
  security_groups = ["${aws_security_group.efs.id}"]
}

resource "aws_efs_mount_target" "app-c" {
  file_system_id = "${aws_efs_file_system.app.id}"
  subnet_id      = "${aws_subnet.c.id}"
  security_groups = ["${aws_security_group.efs.id}"]
}

EC2 の作成

さて、いよいよ EC2 です。
ここでは、user-data を使って、初回ローンチ時に EFS をマウントさせてしまいます。
さらにマウントした EFS 内に html/ ってディレクトリを作成して、そいつを /var/www/html にシンボリックリンクしてみましょうか。
と言っても、こんな感じで大丈夫です。

ec2.tf
## IAM Instance Profile
resource "aws_iam_instance_profile" "instance_role" {
    name = "instance_role"
    role = "${aws_iam_role.instance_role.name}"
}

## SSH Key
resource "aws_key_pair" "deployer" {
  key_name   = "${var.project}"
  public_key = "${var.ssh_public_key}"
}

## EC2
resource "aws_instance" "app" {
    ami                         = "${lookup(var.ami,var.region)}"
    availability_zone           = "${aws_subnet.a.availability_zone}"
    ebs_optimized               = false
    instance_type               = "t2.micro"
    monitoring                  = true
    key_name                    = "${aws_key_pair.deployer.key_name}"
    subnet_id                   = "${aws_subnet.a.id}"
    vpc_security_group_ids      = ["${aws_security_group.ec2.id}"]
    associate_public_ip_address = true
    source_dest_check           = true
    iam_instance_profile        = "${aws_iam_instance_profile.instance_role.id}"
    disable_api_termination     = false

    user_data                   = <<USERDATA
#!/bin/bash
az="${aws_subnet.a.availability_zone}"
efs_region="${var.region}"
efs_id="${aws_efs_file_system.app.id}"
efs_mount_target="${aws_efs_mount_target.app-a.dns_name}:/"
efs_mount_point="/mnt/efs/$${efs_id}/$${az}"
web_doc_root="/var/www/html"

# EFS Mount
/usr/bin/yum -y install nfs-utils || /usr/bin/yum -y update nfs-utils
if [ ! -d $${efs_mount_point} ]; then
  mkdir -p $${efs_mount_point}
fi
cp -pi /etc/fstab /etc/fstab.$(date "+%Y%m%d")
echo "$${efs_mount_target}    $${efs_mount_point}   nfs4    defaults" | tee -a /etc/fstab
mount $${efs_mount_point}

# create Web document root
if [ -d $${web_doc_root} ]; then
  rm -rf $${web_doc_root}
fi
if [ ! -d $${efs_mount_point}/html ]; then
  mkdir $${efs_mount_point}/html
  chown ec2-user:ec2-user $${efs_mount_point}/html
fi
ln -s $${efs_mount_point}/html $${web_doc_root}
chown -h ec2-user:ec2-user $${web_doc_root}
USERDATA

    root_block_device {
        volume_type           = "gp2"
        volume_size           = 10
        delete_on_termination = true
    }

    tags {
        "Name"          = "${var.domain}"
    }
}

user_data は長めのシェルスクリプトなので、可読性が悪いから ${file("data/user_data.sh")} とかってやって別ファイルで管理したいですよね。
でも待ってください、ヒアドキュメントでやってるのは理由があるのです。

ヒアドキュメントで書くと、user_data 用のシェルスクリプトの中で Terraform の変数が使えます。
マウントするには EFS の ID とか、マウントターゲットの dns_name とか必要になってきますが、それを作成前に知らなくてもこのように書いておけるのです。便利ですね。
その代わり、user_data 用のシェルスクリプト内でローカルな環境変数を使いたい場合は $${efs_mount_point} のように書いてあげてくださいね。

ざっと、こんな感じです。
慣れちゃえば、tf ファイルを使い回しできるので便利ですよ。
また、すでに作成済みの AWS リソースを Terraform 管理下に置きたい場合は

$ terraform import aws_instance.app ${instance_id}

のようにして管理下に置くことができます。
管理されているリソースは terraform.tfstate というファイルに書き込まれます。
さらに別プロダクトになるのですが Terraforming と言うツールを使用すると、既存の AWS リソースから Terraform 用の tf ファイルを作成したり、terraform.tfstate を作成したりもできるので便利です。
Terraforming については、Terraforming で既存のインフラを Terraform 管理下におく を参考にしてください。

実際にリソース作成してみる

tf ファイル書いたら

$ terraform plan

で、設定ファイルに誤りがないか?既存のリソースへの影響はどの程度あるのかが確認できます。
実際に反映させたい時は

$ terraform apply

で、おっけ。

では、良い Terraform を!

続きを読む

AWSIoT ラージデータアップロードパターン実装

AWSIoT ラージデータアップロードパターン実装

このBlackbeltセミナー の「ラージデータアップデートパターン」を実装しようと思ったら、未熟ゆえに結構色々詰まったので、やりかたをまとめときます。

image

AWSIoT Thing作成

このへんは普通に作るだけなので、適当に流します。

デバイス名はdevice00, 以下のようなPolicyを作成し、attachしておきます。

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": "iot:Connect",
      "Resource": [
        "arn:aws:iot:ap-northeast-1:[YOUR_ACCOUNT_ID]:client/device00"
      ]
    },
    {
      "Effect": "Allow",
      "Action": "iot:Publish",
      "Resource": "arn:aws:iot:ap-northeast-1:[YOUR_ACCOUNT_ID]:topic/device00/token/req"
    },
    {
      "Effect": "Allow",
      "Action": "iot:Subscribe",
      "Resource": "arn:aws:iot:ap-northeast-1:[YOUR_ACCOUNT_ID]:topicfilter/device00/token/res"
    },
    {
      "Effect": "Allow",
      "Action": "iot:Receive",
      "Resource": "arn:aws:iot:ap-northeast-1:[YOUR_ACCOUNT_ID]:topic/device00/token/res"
    }
  ]
}

また、証明書のダウンロードもおこなっておきます。

IAMロールの設定

この後、LambdaでToken発行処理とデバイスからのS3へのアップロード処理を作成するのですが、
関数作成時に該当処理の実行権限をもつIAMロールが必要になるので、先に作っておきます。

S3アップロード処理ロール

デバイスに渡すSTSに許可する権限を付与したロールを”s3-role”として定義します。
ここでは、任意バケット以下にPutObjectのみをおこなえる権限を与えておきます。

実際には、バケット以下のデバイス名のフォルダにのみアップロード可能にする予定ですが、それは
STS発行時にロール権限を制限する形で加えるので、ここではバケット以下への書き込み権限を与えます。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "",
            "Effect": "Allow",
            "Action": [
                "s3:PutObject"
            ],
            "Resource": [
                "arn:aws:s3:::[YOUR_BACKET_NAME]/*"
            ]
        }
    ]
}

STS発行処理ロール

“s3-role”のロール権限を付与したSTSを作成するLambda関数に与えるロール権限を
“create-token”ロールとして作成します。

Lambda関数を実行するので、既存ポリシーのアタッチで”AWSLambdaBasicExecutionRole”を付与します。

また、作成したSTSをAWSIoTの機能でPublishするので、そのための権限も付与します。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "",
            "Effect": "Allow",
            "Action": [
                "iot:Publish"
            ],
            "Resource": [
                "arn:aws:iot:ap-northeast-1:[YOUR_ACCOUNT_ID]:topic/device00/token/res"
            ]
        }
    ]
}

そして、STSにs3-roleの権限を付与するための権限も追加します。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": "sts:AssumeRole",
            "Resource": "arn:aws:iam::[YOUR_ACCOUNT_ID]:role/s3-role"
        }
    ]
}

さらに、s3-role側でcreate-tokenロールを信頼しAssumeRoleを許可する旨の設定をおこなう必要があります。

s3-roleの設定画面で「信頼関係」タブを選択し、「信頼関係の編集」をおこなってエディタで
JSONの”Statement”要素の配列に以下を追加します。

{
  "Effect": "Allow",
  "Principal": {
    "AWS": "arn:aws:iam::[YOUR_ACCOUNT_ID]:role/create-token"
  },
  "Action": "sts:AssumeRole"
}

STS作成とデバイスへのトークン送信処理

STSを発行し、AWSIoT経由で指定のTopicにTokenをpublishするLambda処理を作成します。

今回はスライドの構成にしたがってTopic経由のpublishにしましたが、STSを使った通信が頻繁に
発生することが想定される場合は、Lambdaをスケジュール起動にしてDeviceShadowを用い定期的に
Pushで通知するほうがいいかもしれません。

import boto3
import json

def lambda_handler(event, context):

    sts = boto3.client('sts')
    iotData = boto3.client('iot-data')

    thingName=event['name']

    token = sts.assume_role(
        RoleArn="arn:aws:iam::[YOUR_ACCOUNT_ID]:role/s3-role",
        RoleSessionName=thingName,
        Policy=json.dumps({
            "Version": "2012-10-17",
            "Statement": [
                {
                    "Sid": "",
                    "Effect": "Allow",
                    "Action": [
                        "s3:PutObject"
                    ],
                    "Resource": [
                        "arn:aws:s3:::large-data-upload-pattern/"+thingName+"/*"
                    ]
                }
            ]
        })
    )

    iotData.publish(
        topic=thingName+'/token/res',
        qos=1,
        payload=json.dumps({
            "AccessKeyId": token['Credentials']['AccessKeyId'],
            "SecretAccessKey": token['Credentials']['SecretAccessKey'],
            "SessionToken": token['Credentials']['SessionToken']
        })
    )

eventオブジェクトのメンバー’name’からデバイス名を取得していますが、ここにデバイス名をセットするための
設定は次項でおこないます。

AWSIoTでSTS発行リクエストを受ける

AWSIoTで”*/token/req”のTopicにトークン発行リクエストがpublishされたら、上記のLambdaを起動する
Ruleを作成します。

“Rule query statement” が以下になるように設定し、ActionにはLambda起動を選択して先述の
関数を指定してください。

SELECT topic(1) AS name FROM '+/token/req'

ここまでの設定で、AWSIoTコンソールの”Test”から、”device00/token/req”にメッセージ(空でよい)を
送信するとTokenが”device00/token/res”にpublishされることが確認できます。

あとは動作確認です。

STSを取得してみる

下記サンプルスクリプトを実行すると、”device00/token/req”にトークン発行リクエストを送信し、
“device00/token/res”からトークンを取得して標準出力に表示します。

from AWSIoTPythonSDK.MQTTLib import AWSIoTMQTTClient
import json
import time

def cb(client, userdata, message):
    print("got message")
    print(message.payload)

host="[IOT_ENDPOINT_DOMAIN]"
rootCAPath = "[ROOT_CA_FILE_PATH]"
certificatePath = "[CERTIFICATE_FILE_PATH]"
privateKeyPath = "[PRIVATE_KEY_FILE_PATH]"

myAWSIoTMQTTClient = None
myAWSIoTMQTTClient = AWSIoTMQTTClient("device00")
myAWSIoTMQTTClient.configureEndpoint(host, 8883)
myAWSIoTMQTTClient.configureCredentials(rootCAPath, privateKeyPath, certificatePath)

myAWSIoTMQTTClient.configureAutoReconnectBackoffTime(1, 32, 20)
myAWSIoTMQTTClient.configureOfflinePublishQueueing(-1)
myAWSIoTMQTTClient.configureDrainingFrequency(2)
myAWSIoTMQTTClient.configureConnectDisconnectTimeout(10)
myAWSIoTMQTTClient.configureMQTTOperationTimeout(5)

myAWSIoTMQTTClient.connect()
myAWSIoTMQTTClient.subscribe("device00/token/res", 1, cb)

time.sleep(2)

myAWSIoTMQTTClient.publish("device00/token/req", json.dumps({}), 1)

while True:
    time.sleep(5)

S3へのアップロード

取得したトークンを用いて、下記サンプルスクリプトで適当にS3にアップロードしてみます。

import boto3

AWS_S3_BUCKET_NAME = '[YOUR_BACKET_NAME]'

s3 = boto3.resource(
    's3',
    aws_access_key_id="[ACCESS_KEY]",
    aws_secret_access_key="[SECRET_KEY]",
    aws_session_token="[SESSION_TOKEN]"
)
bucket = s3.Bucket(AWS_S3_BUCKET_NAME)

obj = bucket.Object("device00/hogehoge")

response = obj.put(
    Body="hogehogehogehoge".encode('utf-8'),
    ContentEncoding='utf-8',
    ContentType='text/plane'
)

まとめ

まとめてみると大したことではないのですが、AssumeRoleとか権限まわりで色々詰まってよくわかんなくなるので大変でした。

続きを読む