S3 + Cognito + DynamoDBでサーバレスな簡易コメント投稿システム

こんにちは。独りAdventCalendar風ブログ更新の12月5日分の記事です。4日分の投稿が遅かったので間が開かずの更新です。

今回参考にさせていただいたのはQiitaに投稿されているこちらの記事。qiita.com

はてブもたくさんついていて、良い記事だとういことがうかがえます。それにびんj(ryというわけではなく、最近はサーバレスなシステムをどうやって作っていったらいいかと考えていたりとか、触っていないAWSのサービスが多いのでなんとかしていろいろ触ってみたいなと思っていたりすることが重なってこのネタに至ったわけです。

S3バケットの準備

コメントシステムのHTMLを配置するためにStatic Website Hostingを利用します。Bucket名は「matetsu.example.com」とし、PropertiesのStatic Website Hositing(静的ウェブサイトホスティング)メニューから「Enable website hosting」を選択し、必要な項目を埋めて「Save」でひとまず完了。

Static Website Hostingの設定方法については、こちらが参考になるかと思います。

Amazon Web Services実践入門 (WEB+DB PRESS plus)

Amazon Web Services実践入門 (WEB+DB PRESS plus)

Coginitoの設定

Cognitoはモバイルデバイスの認証やデータ同期などに利用されるAWSのサービスの一つです。Cognitoを使ってSTSの一時的セキュリティ認証機能*1を使ってHTMLやJavascriptに固定のIAMの認証情報を埋め込まずとも時限的な認証情報を得ることができるようになります。

Identity Poolを作成します。認証なしで時限的な認証をするために、「Enable access to unauthenticated identities」にはチェックを入れるようにしましょう。

f:id:matetsu:20151205215152p:plain

Cognito用のauthenticated RoleとUnauthenticated Roleが作成されるので、とりあえずはそのまま「Allow」で進む。両者のPolicyはそれぞれ以下の様な感じ。

Authenticated

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "mobileanalytics:PutEvents",
        "cognito-sync:*",
        "cognito-identity:*"
      ],
      "Resource": [
        "*"
      ]
    }
  ]
}

Unauthenticated

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "mobileanalytics:PutEvents",
        "cognito-sync:*"
      ],
      "Resource": [
        "*"
      ]
    }
  ]
}

Identity Poolの作成が完了するとSample Codeの画面になるので、各言語のSample Codeを眺めてみるのもよし。ここで必要なのはIdentity Pool IDの部分。これを控えておきます。

IAM RoleのPolicyを追加

先ほどCognitoのIdentity Poolを作成したところで作ったIAM RoleのPolicyではCognito関連の操作しかできません。S3に配置したHTML+Javascriptかたの操作でDynamoDBに出たを入れられるようにしたいので、DynamoDBの操作権限も追加したいと思います。今回使うのはUnauthenticatedなほうだけなので、そちらにだけ。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "mobileanalytics:PutEvents",
                "cognito-sync:*"
            ],
            "Resource": [
                "*"
            ]
        },
        {
            "Effect": "Allow",
            "Action": [
                "dynamodb:BatchGetItem",
                "dynamodb:BatchWriteItem",
                "dynamodb:DeleteItem",
                "dynamodb:GetItem",
                "dynamodb:PutItem",
                "dynamodb:Query",
                "dynamodb:Scan",
                "dynamodb:UpdateItem"
            ],
            "Resource": [
                "arn:aws:dynamodb:ap-northeast-1:ACCOUNT_NO:table/TABLE_NAME"
            ]
        }
    ]
}

HTML+JSを配置してみる

とりあえず今日はformに投稿された内容をDynamoDBにPutして、Scanで取ってきて表示するというもの。JSはあまり得意ではないので、いろいろなところから拝借した感じになってしまいました。特に表示データを加工もしていないし見た目もあれなので、どうにかしたい。

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>コメント投稿</title>
    <script src="https://sdk.amazonaws.com/js/aws-sdk-2.2.21.min.js"></script>
    <script>
        var $id = function(id) { return document.getElementById(id); };
        AWS.config.region = "ap-northeast-1";
        AWS.config.credentials = new AWS.CognitoIdentityCredentials({IdentityPoolId: "POOL_ID"});
        AWS.config.credentials.get(function(err) {
            if (!err) {
                console.log("Cognito Identify Id: " + AWS.config.credentials.identityId);
            }
        });

        var table = "blog-neta2";
        var dynamodb = new AWS.DynamoDB({params: {TableName: table}});

        function postComment() {
            var now = Math.floor(new Date().getTime() / 1000);
            var itemParams = {
              Item: {
                article: {S: location.pathname},
                timestamp: {N: "" + now},
                data: {S: $id("comment").value}
              }
            };

            dynamodb.putItem(itemParams, function(err){
                if(err){
                    alert("Error: " + err);
                } else {
                    load();
                }
            });
        }

        function load() {
            $id("comment").value = "";
            dynamodb.scan({
                ScanFilter: {
                    article: {
                        ComparisonOperator: 'EQ',
                        AttributeValueList: [
                            { S: location.pathname }
                        ]
                    }
                }
            }, function(err, data) {
                if (err) {
                    alert(err);
                } else {
                    var c = document.getElementById('comment_list');
                    c.innerHTML = '';

                    data.Items.map( function(item) {
                        var p = document.createElement('p');
                        p.textContent = new Date(item.timestamp.N * 1000) + ": " + item.data.S;
                        return p;
                    }).forEach( function(p) {
                        c.appendChild(p);
                    });
                }
            });
        }
    </script>
</head>
<body onload="load();">
    <div class="wrapper">
        <div id="postform">
            <form>
                <table>
                    <tr>
                        <th>Comment</th>
                        <td>
                            <textarea id="comment" cols="40" rows="5" name="comment"></textarea>
                        </td>
                    </tr>
                </table>
                <div id="button_area">
                    <input onClick="postComment();" type="button" value="投稿" id="post_button" />
                </div>
            </form>
        </div>
    </div>
    <div id="comment_list">
    <div>
</body>
</html>

これでテキストエリアに入力して「投稿」ボタンを押していくと、こんな感じになる。

f:id:matetsu:20151206015529p:plain

とりあえず動いてますといった感じ。

おわりに

今日のところはとりあえず簡単なコメントシステムっぽいものができた。これをうまいことちゃんと作れば、WPやMTをStatic Website HostingにHTMLを吐き出してつかった場合にもコメントが出来るようになる(ということをイメージしてみたんだがどうだろうか)。もちろんSEO云々は全く考慮していませんが、WPとかMTを使ってる人の中にはサーバ管理がそんなに得意ではない人もいるのでそのあたりのコストを考えたらコメントの部分くらいは気にしなくてもいいのかなと(といいつつ、そのへんの分野も明るくないのでよくわからないというのが本音)。

とりあえず試してみた感が強いので、もう少し面白みのあることに繋げられるといいなと思う。とくにDynamoDB StreamをつかってさらにLambda Functionを呼び出して云々とかできたらいいなと。

DynamoDBをまともに使ったことがなく、突貫で使ったので正しく使えているのかが不安なので、あとでちゃんとドキュメントを読みます(読んでないのかと)。まずい所があったらコメンとなりSNSなりで教えていただければと思いますm(_ _)m

おわりにのおわりに

5日目もやっぱり日付またいでの更新。。。当日中に更新できる日は来るのだろうか。

Let's EncryptをCloudFront+S3で利用する

こんにちは。12月4日分の投稿を12月5日の夕方に書いています。

今日のネタはPublic BetaになったLet's Encryptを使って無料の証明書を作ってCloudFront+S3で運用されているサイトを独自ドメインHTTPSに対応してみたいと思います。

※CloudFront+S3でのサイトを構築する方法については、こちらを参考にしていただければと思います。(今回はS3バケット自体は公開しないので、ちょっとやり方が違いますが)

Amazon Web Services実践入門 (WEB+DB PRESS plus)

Amazon Web Services実践入門 (WEB+DB PRESS plus)

今回の記事で参考にしたのはこちら。
SSL setup with Let's Encrypt on AWS CloudFront and S3

S3バケットを準備する

S3のバケットを適当なリージョンに適当な名前で作成します。そして公開用コンテンツを配置しておきます。今回は前回の記事で利用したconfig_monitor_botの画像を配置しています。

f:id:matetsu:20151205163735p:plain

CloudFrontの設定をする

CloudFrontのDistributionを作成するときに「Restrict Bucket Access」を「Yes」にして、「Grant Read Permissions on Bucket」を「Yes」にしておくと、CloudFrontからOriginのS3バケットへのアクセス権を自動で設定してくれます(*1)。「Viewer Protocol Policy」は「HTTP and HTTPS」にしておいてください。

f:id:matetsu:20151205164918p:plain

CNAMEsには公開に利用したい独自ドメイン名を入力しましょう。SSL証明書の設定ですが、まずはデフォルトの *.cloudfront.net ドメインでの利用としておいてください。証明書の作成完了後に変更します。

f:id:matetsu:20151205164741p:plain

「Default Root Object」はとりあえずは画像しか無いので、その画像をしておけば良いと思います。あとは「Cretate Distribution」ボタンでDistributionが作成されます。実際にアクセスできるようになるまでは少々時間ががかるので、それまでには別の準備を進めます。

1で述べたとおり、Distributionを作成すると下のようなBucket PolicyがS3バケットに設定されます。

{
	"Version": "2008-10-17",
	"Id": "PolicyForCloudFrontPrivateContent",
	"Statement": [
		{
			"Sid": "1",
			"Effect": "Allow",
			"Principal": {
				"AWS": "arn:aws:iam::cloudfront:user/CloudFront Origin Access Identity XXXXXXXXXXXXXX"
			},
			"Action": "s3:GetObject",
			"Resource": "arn:aws:s3:::BUCKETNAME/*"
		}
	]
}

Route 53でDNSの設定をする

公開に使うドメインをCloudFrontに向けるように設定をします。Route 53以外のDNSサーバではCNAMEレコードとしてさきほど作成したDistributionの「Domain Name」の欄にあるドメイン名を指定しますが、Route 53ではAレコードALIASとして指定します。Route 53でのDNS設定についてもうえで紹介した本が(ry

f:id:matetsu:20151205171343p:plain

設定が完了したら、digコマンドやhostコマンドを使って正しく設定できていることを確認して、さらにcurlなどでアクセスできることを確認してみてください。

サーバ証明書を作成する

それでは本題です。letsencrytコマンドはMac OS Xはvery experimentalとのことだし、必要なパッケージが勝手にインストールされたりするとのことで、Dockerでやることにしました(といいつつ、参考にしたサイトでもそうなっている)。

$ mkdir -p ~/tmp/le/{etc,lib}
$ docker run -it --rm --name letsencrypt \
  -v "/Users/matetsu/tmp/le/etc:/etc/letsencrypt" \
  -v "/Users/matetsu/tmp/le/lib:/var/lib/letsencrypt" \
  quay.io/letsencrypt/letsencrypt:latest \
  --agree-dev-preview \
  --server  https://acme-v01.api.letsencrypt.org/directory \
  -a manual \
  auth

なにやらTUIの画面が。emailアドレスを入れて、OK。

f:id:matetsu:20151205190955p:plain

次の画面では規約に同意するかとのことなので、規約を読んでOK。

次は証明書を発行する対象となるドメイン名をコンマ区切りか空白区切りで入力する。

f:id:matetsu:20151205191156p:plain

お主のマシンのIPアドレスを登録するけど良いかと聞かれるので、Yesとする。

次にTUIが終了して、ターミナル上にドメイン所有確認を行うための手順が記されるのでそれに従う。「Press ENTER to continue」と出ているが、準備が整うまではEnterを押さないようにしましょう。手順というのは、よくある対象ドメインの指定されたパスに指定された内容のファイルを置いて公開しておけというものです。今回は支持に従ってS3にファイルをアップロードして閲覧できるようにしておきました。

Enterを押すと、「できたでー」といった内容が表示されるので、早速確認して見る。dockerにマウントさせておいたディレクトリを確認してみましょう。

$ ls -1 /Users/matetsu/tmp/le/etc/live/DOMAIN_NAME/
.
..
cert.pem
chain.pem
fullchain.pem
privkey.pem

何やらできている感じですね。CloudFrontに設定してみましょう。まずは作成された証明書などをCloudFrontで利用できるようにアップロードします。

$ aws iam upload-server-certificate \
 --server-certificate-name CERTIFICATE_NAME \
 --certificate-body file:///Users/matetsu/tmp/le/etc/live/DOMAIN_NAME/cert.pem \
 --private-key file:///Users/matetsu/tmp/le/etc/live/DOMAIN_NAME/privkey.pem \
 --certificate-chain file:///Users/matetsu/tmp/le/etc/live/DOMAIN_NAME/chain.pem \
 --path /cloudfront/ 

そして、SNIでHTTPSが使えるようにDistributionの設定変更をします。

f:id:matetsu:20151205194353p:plain

これで設定変更を完了して、しばらく待ちましょう。

確認して見る

さて、CloudFrontのStatusがIn ProgressからDeployedになったらブラウザから確認して見ましょう(In Progressでもアクセスしているエッジによっては確認できます)。

f:id:matetsu:20151205195706p:plain

キタ━━━━(゚∀゚)━━━━!!きたきたきたー!問題なさそうですね。

おわりに

無料で簡単にSSL/TLS証明書を手に入れることができるようになってきていますね。現在有料で提供されているドメイン認証型の証明書を無料化するという話も出ているようですす、ますます総HTTPS化が捗りそうですね。あとはSNI非対応なブラウザが完全に滅びていただければ。。。

ふぅ

4日目も無事に(!?)終えることができましたので、急いで5日目に取り掛かりたいと思います。

AWS Configの通知をSNS、Lambdaを通してSlackへ 〜実行編〜

3日目ですね。

今日は昨日の続きで実際にリソースに変更を加えて、その結果をSlackに通知してみましょう。

変更内容通知の構造を調査

まずは、昨日作ったConfigで監視対象としたリソースに変更を加えて、Lambda(というよりCloudWatch Logs)に変更内容を出力してみます。こうすることで、変更通知がどのような構造になっているかを確認することができます。

RouteTable

とあるRouteTableに10.0.1.0/24宛のトラフィックはInternetGateway経由で送るという設定を追加してみましょう。変更を通知したいだけなので、意味のない設定を追加しているということは気にしないでくださいm(_ _)m

f:id:matetsu:20151203232318p:plain

これに対するConfigからのSNS通知は、下記の様になります。

{
    "configurationItemDiff": {
        "changedProperties": {
            "Configuration.Routes.0": {
                "previousValue": null,
                "updatedValue": {
                    "destinationCidrBlock": "10.0.1.0/24",
                    "destinationPrefixListId": null,
                    "gatewayId": "igw-AAAAAAAA",
                    "instanceId": null,
                    "instanceOwnerId": null,
                    "networkInterfaceId": null,
                    "vpcPeeringConnectionId": null,
                    "state": "active",
                    "origin": "CreateRoute"
                },
                "changeType": "CREATE"
            }
        },
        "changeType": "UPDATE"
    },
    "configurationItem": {
        "configurationItemVersion": "1.1",
        "configurationItemCaptureTime": "2015-12-03T13:59:59.958Z",
        "configurationStateId": 5,
        "relatedEvents": [
            "527b06c5-3dfc-482b-84df-0f92185ed838"
        ],
        "awsAccountId": "ACCOUNT_NO",
        "configurationItemStatus": "OK",
        "resourceId": "rtb-XXXXXXXX",
        "resourceName": null,
        "ARN": "arn:aws:ec2:ap-northeast-1:ACCOUNT_NO:route-table/rtb-XXXXXXXX",
        "awsRegion": "ap-northeast-1",
        "availabilityZone": "Not Applicable",
        "configurationStateMd5Hash": "STATE_HASH",
        "resourceType": "AWS::EC2::RouteTable",
        "resourceCreationTime": null,
        "tags": {},
        "relationships": [
        (略)
        ],
        "configuration": {
            "routeTableId": "rtb-281ba241",
            "vpcId": "vpc-5b1ba232",
            "routes": [
            (略)
            ],
            "associations": [
            (略)
            ],
            "tags": [],
            "propagatingVgws": []
        }
    },
    "notificationCreationTime": "2015-12-03T14:00:00.821Z",
    "messageType": "ConfigurationItemChangeNotification",
    "recordVersion": "1.2"
}

ConfigurationDiffとConfigurationItemのresourceID、resourceTypeあたりを取っておけば、簡単な内容通知としては使えそうですね。

NetworkACL

とあるNetworkACLのInboundに0.0.0.0/0からのすべてのICMPを許可する設定を追加してみましょう。

f:id:matetsu:20151203234048p:plain

これに対する通知はこんな感じ。

{
    "configurationItemDiff": {
        "changedProperties": {
            "Configuration.Entries.0": {
                "previousValue": null,
                "updatedValue": {
                    "ruleNumber": 10,
                    "protocol": "1",
                    "ruleAction": "allow",
                    "egress": false,
                    "cidrBlock": "0.0.0.0/0",
                    "icmpTypeCode": {
                        "type": -1,
                        "code": -1
                    },
                    "portRange": null
                },
                "changeType": "CREATE"
            }
        },
        "changeType": "UPDATE"
    },
    "configurationItem": {
        "configurationItemVersion": "1.1",
        "configurationItemCaptureTime": "2015-12-03T14:09:57.945Z",
        "configurationStateId": 5,
        "relatedEvents": [
            "eabe4ad9-fb26-42d2-982a-73fe08d961a6"
        ],
        "awsAccountId": "ACCOUNT_NO",
        "configurationItemStatus": "OK",
        "resourceId": "acl-XXXXXXXX",
        "resourceName": null,
        "ARN": "arn:aws:ec2:ap-northeast-1:ACCOUNT_NO:network-acl/acl-XXXXXXXX",
        "awsRegion": "ap-northeast-1",
        "availabilityZone": "Multiple Availability Zones",
        "configurationStateMd5Hash": "c91cc8f80553b988016a6ec69fc57b16",
        "resourceType": "AWS::EC2::NetworkAcl",
        "resourceCreationTime": null,
        "tags": {},
        "relationships": [
        (略)
        ],
        "configuration": {
            "networkAclId": "acl-XXXXXXXX",
            "vpcId": "vpc-XXXXXXXX",
            "isDefault": true,
            "entries": [
            (略)
            ],
            "associations": [
            (略)
            ],
            "tags": []
        }
    },
    "notificationCreationTime": "2015-12-03T14:09:58.933Z",
    "messageType": "ConfigurationItemChangeNotification",
    "recordVersion": "1.2"
}

こちらも同様にconfigurationItemDiffとconfigurationItemのresourceId、resourceTypeを使えば良さそうですね。

SecurityGroup

とあるSecurityGroupのInboundに0.0.0.0/0から8443/TCPへのアクセスを許可してみます。

f:id:matetsu:20151203235115p:plain

これに対する通知内容は、

{
    "configurationItemDiff": {
        "changedProperties": {
            "Configuration.IpPermissions.0": {
                "previousValue": null,
                "updatedValue": {
                    "ipProtocol": "tcp",
                    "fromPort": 8443,
                    "toPort": 8443,
                    "userIdGroupPairs": [],
                    "ipRanges": [
                        "0.0.0.0/0"
                    ],
                    "prefixListIds": []
                },
                "changeType": "CREATE"
            }
        },
        "changeType": "UPDATE"
    },
    "configurationItem": {
        "configurationItemVersion": "1.1",
        "configurationItemCaptureTime": "2015-12-02T22:16:50.445Z",
        "configurationStateId": 2,
        "relatedEvents": [],
        "awsAccountId": "ACCOUNT_NO",
        "configurationItemStatus": "OK",
        "resourceId": "sg-XXXXXXXX",
        "resourceName": null,
        "ARN": "arn:aws:ec2:ap-northeast-1:ACCOUNT_NO:security-group/sg-XXXXXXXX",
        "awsRegion": "ap-northeast-1",
        "availabilityZone": "Not Applicable",
        "configurationStateMd5Hash": "c25b5d90c889ae1ef76851b707046790",
        "resourceType": "AWS::EC2::SecurityGroup",
        "resourceCreationTime": null,
        "tags": {},
        "relationships": [
        (略)
        ],
        "configuration": {
            "ownerId": "ACCOUNT_NO",
            "groupName": "GROUP_NAME",
            "groupId": "sg-XXXXXXXX",
            "description": "DESCRIPTION",
            "ipPermissions": [
            (略)
            ],
            "ipPermissionsEgress": [
            (略)
            ],
            "vpcId": "vpc-XXXXXXXX",
            "tags": []
        }
    },
    "notificationCreationTime": "2015-12-02T22:16:53.032Z",
    "messageType": "ConfigurationItemChangeNotification",
    "recordVersion": "1.2"
}

これまた同様(ry

Lambda Functionの修正

SNSから通知されるJSONの構造と必要な要素がわかったところで、とりあえずで作っておいたLambda Functionを修正してSlackにPostできるようにします。SlackへのPostには外部ライブラリがあったほうがらくなので、一旦ローカルでの開発に移ります。

コードはこんな感じになります。(Diff部分が手抜きですみません。いろいろと対応するよりはいいかなと思って・・・)

# coding: utf-8
import json
import slackweb

def lambda_handler(event, context):
  message = json.loads(event['Records'][0]['Sns']['Message'])
  configuration_item_diff = message['configurationItemDiff']
  resource_id = message['configurationItem']['resourceId']
  resource_type = message['configurationItem']['resourceType']

  text = u"リソースの変更を検知しました。\n\n変更されたリソース: %s %s\n変更内容: \n```\n%s\n```\n" % (resource_type, resource_id, json.dumps(configuration_item_diff,  indent=2))

  hook_url = "https://hooks.slack.com/services/xxxxxxxxx/yyyyyyyyy/zzzzzzzzzzzzzzzzzzzzzzzz"
  username = "config_monitor_bot"
  channel = "#config_monitor"
  icon_emoji = ":cop:"

  slack = slackweb.Slack(url=hook_url)
  slack.notify(text=text,
               channel=channel,
               username=username,
               icon_emoji=icon_emoji)

billing_bot の時と同じように、下記のようにzipファイルを作成します。

$mkdir config_bot
$ vi lambda_function.py
(上のPythonコード)
$ vi requirements.txt
slackweb
$ pip -r requirements.txt -t ./
$ zip -r ~/func.zip * -x *.pyc event.json
(python-lambda-localでのテストで作成した/されたuploadには不要なファイルは除いておく)

LambdaのConsoleから今度はUpload .ZIP file を選んでsave。テストをするには、先ほどのCloudWatch Logsに出力されたJSONSNSが付与している部分も付け足して、下記のようなeventを登録する。これを登録したうえTestを実行すればよい。

{
  "Records": [
    {
      "Sns": {
        "Message": "(Logに出力したjsonを文字列にしたもの)"
    }
  ]
}

リソースを変更してみる

さあ、準備ができましたので、実際にリソースの変更をしてみましょう。とりあえずはRouteTableへの変更を実施してみましょう。(SecurityGroupへの変更が通知されるのにはなぜが時間がかかる模様)

しばらくすると、Slackにこんな感じで出力されます。

f:id:matetsu:20151204010008p:plain

CloudTrailでも操作のログを取ることができますが、Configを使うことでより視覚的に捉えることができます。また、サーバレスで通知の仕組みも簡単に作れるというところがいいですね。

これにて3日目終了です。実際の動作があるから昨日よりは実用的な感じに見えますかねw

AWS Configの通知をSNS、Lambdaを通してSlackへ 〜準備編〜

2日目も遅れているからってめげたりしません。日付が変わってからネタを決めているこの体たらくを笑って許して。まずは続けることに意義がある、そう思ってやっています。

本日のお題はAWS Config + SNS + Lambda + Slack で行きたいと思います。最近はLambdaをつかってSlackと戯れるのが好きなので許してやってください。今回も突貫ゆえ大したことはできませんが、生暖かく流し読みでもしていただければと思います。

AWS ConfigはAWSのリソースの状況を可視化してくれたり変更を検知して通知してくれ、いわゆる監査的なことに使えるサービスです。詳細はこちら。まあ、細かい部分の説明はブログの会社さんなり、プレミアコンサルティングパートナーの会社さんが書いてくださっているはずですので割愛いたします。

作業開始

SNSの枠を準備

まずはSNSのコンソールから、config-topicというTopicを作成しておきます。Subscriptionは特に設定しなくても良いです。

f:id:matetsu:20151203015337p:plain

Lambda Functionの作成

次にLambda Functionの設定を行います。簡易手順は下記の通り。

  1. Blue print
    • sns-message-pythonを選択(下のキャプチャ参照)
  2. Congirure event source
    • Event source type: SNS
    • SNS Topic: config-topic
  3. Configure function
    • Name: config-bot
    • Description: (そのまま)
    • Runtime: Python2.7
  4. Lambda function code
    • とりあえず通知されるメッセージの中身が見たいので「Edit code inline」を選択して、サンプルをそのまま利用
  5. Lambda function handler and role
    • Handler: (そのまま)
    • role: lambda_exec_role (LambdaからCreate roleするときにデフォルトで作ることができるrole)
  6. Advanced settings
    • Memory: 128MB
    • Timeout: 10sec (なんとなく)
  7. Event source
    • Enable now ※こまけえことは(ry
  8. 「Create function」!!!!

f:id:matetsu:20151203014846p:plain

SNSにLambdaの情報を反映

ふたたびSNSに戻ってきて、subscriptionの設定をします。

  1. Create Subscription
    • TopicARN: (そのまま)
    • Protocol: AWS Lambda
    • Endpoint: 先ほど作成した config-bot のARNを指定
    • Version or ALIAS: default
  2. Create subscription

本日のメインAWS Config

それでは、本題の Config の設定です*1。今回は、Network ACL, Security Group, Route Tableの勝手に触られると困ることの多い3つのリソースに関しての記録を取りたいと思います。

f:id:matetsu:20151203020241p:plain

  1. (はじめてなので)「Get Started Now」
  2. Resource Type: 「EC2: NetworkAcl」「EC2: SecurityGroup」「EC2: RouteTable」
  3. Amazon S3 Bucket
    • Create a new bucketを選択
    • Bucket Name: matetsu-config-record-test (デフォルトのまま or 各自好きな名前をつけてください)
    • Prefix: (今回はなし)
  4. Amazon SNS Topic
    • Enable configuration... にチェックが入っているのでそのまま
    • Choose a topic from your accountを選択
    • Topic Name: config-topic (先に作成しておいたLambdaにEvent通知できるもの)
  5. Continue
  6. (IAMのコンソールになるのでなされるがままに)「許可」
    • 下に示すPolicyで config-role という IAM Roleが作成されます。Policyを編集したい場合は適宜どうぞ。

最初Policyの権限が足りないようなエラーメッセージがでて、最初に戻されたが、もう一度やったら完了した。S3との何か順番的な何かがあったかな?

ちなみに、config-roleはこんなPolicyになってます。

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Action": [
        "appstream:Get*",
        "autoscaling:Describe*",
        "cloudformation:DescribeStacks",
        "cloudformation:DescribeStackEvents",
        "cloudformation:DescribeStackResource",
        "cloudformation:DescribeStackResources",
        "cloudformation:GetTemplate",
        "cloudformation:List*",
        "cloudfront:Get*",
        "cloudfront:List*",
        "cloudtrail:DescribeTrails",
        "cloudtrail:GetTrailStatus",
        "cloudwatch:Describe*",
        "cloudwatch:Get*",
        "cloudwatch:List*",
        "config:Put*",
        "directconnect:Describe*",
        "dynamodb:GetItem",
        "dynamodb:BatchGetItem",
        "dynamodb:Query",
        "dynamodb:Scan",
        "dynamodb:DescribeTable",
        "dynamodb:ListTables",
        "ec2:Describe*",
        "elasticache:Describe*",
        "elasticbeanstalk:Check*",
        "elasticbeanstalk:Describe*",
        "elasticbeanstalk:List*",
        "elasticbeanstalk:RequestEnvironmentInfo",
        "elasticbeanstalk:RetrieveEnvironmentInfo",
        "elasticloadbalancing:Describe*",
        "elastictranscoder:Read*",
        "elastictranscoder:List*",
        "iam:List*",
        "iam:Get*",
        "kinesis:Describe*",
        "kinesis:Get*",
        "kinesis:List*",
        "opsworks:Describe*",
        "opsworks:Get*",
        "route53:Get*",
        "route53:List*",
        "redshift:Describe*",
        "redshift:ViewQueriesInConsole",
        "rds:Describe*",
        "rds:ListTagsForResource",
        "s3:Get*",
        "s3:List*",
        "sdb:GetAttributes",
        "sdb:List*",
        "sdb:Select*",
        "ses:Get*",
        "ses:List*",
        "sns:Get*",
        "sns:List*",
        "sqs:GetQueueAttributes",
        "sqs:ListQueues",
        "sqs:ReceiveMessage",
        "storagegateway:List*",
        "storagegateway:Describe*",
        "trustedadvisor:Describe*"
      ],
      "Effect": "Allow",
      "Resource": "*"
    },
    {
      "Effect": "Allow",
      "Action": [
        "s3:PutObject*"
      ],
      "Resource": [
        "arn:aws:s3:::matetsu-config-record-test/AWSLogs/[ACCOUNT_NO]/*"
      ],
      "Condition": {
        "StringLike": {
          "s3:x-amz-acl": "bucket-owner-full-control"
        }
      }
    },
    {
      "Effect": "Allow",
      "Action": [
        "s3:GetBucketAcl"
      ],
      "Resource": "arn:aws:s3:::matetsu-config-record-test"
    },
    {
      "Effect": "Allow",
      "Action": "sns:Publish",
      "Resource": "arn:aws:sns:ap-northeast-1:[ACCOUNT_NO]:config-topic"
    }
  ]
}

Configの設定が完了すると Resource Inventory の画面になります。ここでは、各種リソースがいつ変更されたかとかを見ることができます。初めて見たけど、これ、いいですね!Inventoryからリソースの詳細に移動すると、こんな感じの画面でいろいろ確認ができます。

f:id:matetsu:20151203013710p:plain

そして、Configが作成されるとまず、作成イベントとしてSNSに指定した記録対象Resourceの通知が来ていることが確認できます。確認はCloudWatch LogsからLambda Functionのログを見てみてください。

f:id:matetsu:20151203014012p:plain

ズラーっと出てますね。ここから、どんな形式の通知が来ているかが確認できるので、これを利用してSlackに通知をします。

次回予告

次回は実際に構成変更を検知させて、Slackに通知させる部分をやりたいと思います。

すみません、エネルギー切れです。。。そして、微妙なキャプチャを挟んでなんとなくコンテンツがある風にみせかけててすみません。。。とりあえず触ってみた的な内容なので、(マサカリではなく)ツッコミ歓迎です。

では、また明日。

*1:「Configの設定」って日本語にすると不思議な感じですね

Scheduled LabmdaでDaily費用レポート

初日から間に合わない事になってしまって反省してます。一人アドベントカレンダー的更新第1日目行ってみたいと思います。どこかの誰かのネタとかぶっている可能性もありますが、最近やったことを書いていきます。

今日のネタはAWS LambdaのScheduled Eventをつかって日次の費用レポートをSlackにPostするというものです。

まずはLambda Functionを実装します。CloudWatchからBilling NamespaceのEstimatedChargesメトリクスを前日の6:00〜本日の6:00までの費用計とさらに1日前の費用計とで取得して差分を取るという簡易的な日次費用取得となっています。end_time/start_time周りの時刻がアレな感じなのは、一番それっぽい値が取れたというあまり根拠の無いend_timeなどを6時にしているのと、一回で2日分まとめて取得していないのは2日前の0時からから本日の0時までだとうまくいかなかったからというネガティブな理由です。。。あくまでも費用感を知るためのものとしておいてください。

この例ではConsolidatedBillingの親アカウントから複数アカウント分の費用を取得するようにしています。

2015.12.02 9:45 追記: 月初日の費用取得が 0USD になってしまうバグがありました。後で修正します。
2015.12.02 11:22 追記: 月初日の費用取得が 0USD になってしまうバグを修正しました。値の取り方も変えてよりトリッキーな感じに。。。なんだか汚い感じがしますが許してください><

# -*- coding: utf-8 -*-
import sys, traceback
import boto3
import json
import datetime, pytz

import slackweb

def billing_info(cloudwatch, account_no, start_time, end_time):
    billing_info = cloudwatch.get_metric_statistics(
                Namespace = 'AWS/Billing',
                MetricName = 'EstimatedCharges',
                Dimensions = [{'Name': 'LinkedAccount', 'Value': account_no},{'Name': 'Currency', 'Value': 'USD'},],
                StartTime = start_time,
                EndTime = end_time,
                Period = 3600,
                Statistics = ["Maximum"],
                )
    return billing_info

def latest_bill(billing_info):
    bill = 0
    billing_dict = {}
    for info in billing_info['Datapoints']:
        timestamp = int(time.mktime(info['Timestamp'].timetuple()))
        billing_dict[timestamp] = info['Maximum']

    x = 0
    former_bill = 0
    billing_dict = sorted(billing_dict.items(), key=lambda x: x[0])
    for t, b in billing_dict:
        if x != 0:
            bill += b if former_bill > b else (b - former_bill)
        former_bill = b
        x += 1

    return bill

def lambda_handler(event, context):
    hook_url = "https://hooks.slack.com/services/xxxxxxxxx/yyyyyyyyy/zzzzzzzzzzzzzzzzzzzzzzzz"
    username = "billing_bot"
    channel = "#report"
    icon_emoji = ":money_with_wings:"

    try:
        tokyo_tz = pytz.timezone('Asia/Tokyo')
        d = datetime.datetime.now()
        todays_end_time = datetime.datetime(d.year, d.month, d.day, 6, 0, 0, 0, tokyo_tz)
        if d.day > 2:
          todays_start_time = todays_end_time - datetime.timedelta(days=1)
        else:
          todays_start_time = todays_end_time - datetime.timedelta(days=1, hours=3)
        yesterday = yesterdays_end_time.strftime("%Y-%m-%d")

        cloudwatch = boto3.client('cloudwatch', region_name='us-east-1')

        post_message = ""
        for account_name, account_no in {'Account1':'000000000000', 'Account2':'111111111111'}.items():
            todays_billing_info = billing_info(cloudwatch, account_no, todays_start_time, todays_end_time)

            todays_bill = latest_bill(todays_billing_info)

            if post_message != "":
                post_message += "\n"
            post_message += "%s: %f USD" % (account_name, todays_bill)

        post_message = "昨日(%s)1日分のAWS費用は以下のとおりでした。\n```\n%s\n```\n今日もコスト意識を持って行きましょう。" % (yesterday, post_message)

        slack = slackweb.Slack(url=hook_url)
        slack.notify(text=post_message, 
                     channel=channel,
                     username=username,
                     icon_emoji=icon_emoji)
    except:
        print traceback.format_exc()
    else:
        print('finished.')

これをこんな感じでZIPファイルにする。

$mkdir billing_bot
$ vi lambda_function.py
(上のPythonコード)
$ vi requirements.txt
slackweb
pytz
$ pip -r requirements.txt -t ./
$ zip -r ~/myLambdaFunction.zip *

Management ConsoleからLambdaのメニューに進んで、次のように設定していく。(例は朝9:10 JSTにスケジュールしています)

  1. Create a Lambda Dunction
  2. lambda-canary
  3. Configure Event Source
    • Event source type: Scheduled Event
    • Name: billing_bot
    • Description: daily billing report
    • Schedule Expression: cron(10 0 * * ? *)
  4. Configure function
    • Name: billing_bot
    • Runtime: Python2.7
  5. Lambda function code
    • Upload a .ZIP file: 先ほど作成したZIPファイルを選択
  6. Lambda function handler and role
    • lambda_function.lambda_handler (lambda functionのメインファイル名.handler関数名)
    • Role: lambda_billing_bot (※roleのPolicyは後述)
  7. Advanced settings
    • Memory: 128MB
    • Timeout: 1min
  8. Enable nowをチェック
  9. Create function

Lambda functionに設定するRoleのPolicyはこんな感じ。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "logs:CreateLogGroup",
                "logs:CreateLogStream",
                "logs:PutLogEvents"
            ],
            "Resource": "arn:aws:logs:*:*:*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "cloudwatch:GetMetricStatistics"
            ],
            "Resource": "*"
        }
    ]
}

Lambda Functionが作成できたら、「Test」ボタンを押してテスト実行をしてみます。するとSlackにこんな感じでPostされます。

f:id:matetsu:20151202004539p:plain

はい、うまくいきましたね。

かなりはしょった感は否めませんが、なんとなく雰囲気は伝わったと思います。突っ込みどころは多くあると思いますので、優しく教えていただければと思います。

では、1日坊主にならないことを祈って、初日はこの辺で。

『Amazon Web Services実践入門』に共著者として参加しました

JAWS-UG高尾山のビアで楽しい記事も書いていない今日このごろ(私だけですね、はい)、皆様いかがお過ごしでしょうか。秋めいた気候になったり冬のように寒くなったりと体調に気をつけなければいけない日々をお過ごしのことと思います。

そんな中、来る11月10日に『Amazon Web Services実践入門』という書籍が技術評論社様より発売になります。僭越ながら私も共著者として本書籍の執筆に参加させていただきました。

  • 3次元の本

Amazon Web Services実践入門 (WEB+DB PRESS plus)

Amazon Web Services実践入門 (WEB+DB PRESS plus)

  • 2次元の本

gihyo.jp


どんな本

これからAWSを使ってみようと思っている方から、使っているけどマネージメントコンソールでポチポチとEC2インスタンスを起動して仮想サーバとして使っているだけという方にはちょうどよい導入書となっているのではないでしょうか。

書籍内で扱っているサービスとしてはEC2、Route53、VPC、RDS、S3/CloudFront、ELB、CloudWatch/AutoScaling、IAM、Billingと基本的な部分をおさえています。マネージメントコンソールでの操作だけでなくaws-cliでの操作もカバーしているので、脱初心者を目指す方やコマンドラインを使った運用の自動化をしたいと考えている方など、結構幅広い方にお楽しみいただける内容になっていると思います。

なぜ私が共著に?

仕事を依頼したことがきっかけでJAWS-UGなどのコミュニティで親しくさせていただいていたサーバーワークスの @ さんと @ さんに誘っていただいたのがきっかけでした(後でわかったこととして、実は裏にはボスとして @ さんがいたという)。仕事でのつながりだったり、コミュニティでのつながりからこんな素晴らしい機会を与えていただけて、大変感謝しています。

感想

書籍中の2章分(Route 53とS3/CloudFront)を担当させていただいただけですが、1冊の本を完成させることの大変さを実際に体験することができたのは、自分にとってすごくいい経験になりました。普段仕事では関わることのできない人たちと一つのものを作り上げるということもなかなかできないことですので、本当に(辛いこともありましたが)楽しかったです。

なにより、他の方は文章を書き慣れているようで、シンプルでわかりやすい文章を書かれていて素晴らしいなというのが率直な感想です。私ももっと読みやすい文章をかけるように精進したいと思います。

最後に

いやはや、執筆中にアップデートがあったり、最大の敵マネージメントコンソールの日本語化があったりと大変なこともありましたが、ようやく発売です!(本当は1年位前という噂も。。。)

皆様、書店などでお見かけになった際には是非お手にとって、そのままレジまで行っちゃってください!!そして、是非フィードバックをいただけますと幸いです。

謝辞的な

@ さんをはじめ、 @ さん、 @ さん、 @ さん、 @ さん、そして技術評論社の春原さん、ご迷惑をお掛けすることも多々あったかと思いますが、素晴らしい経験をさせていただけたこと本当に感謝しています。

(また機会があったらよろしくお願いします!)

その他書籍に関するブログ記事

okochang.hatenablog.jp
qphoney.hatenadiary.com

JAWS-UG京王線 第2回 レッツラーニングを開催しました

5月30日、JAWS-UG 京王線の第2回勉強会「レッツラーニング」を開催しました。
今回も前回と同じく電気通信大学での開催でしたが、ベンチャー支援部門の共催という形で100名近く入れる会場をお借りすることができました。電通大まじ電通大ですね。広い会場を借りられて、たくさんの参加者の方にも来ていただけて、いろいろな人に協力して頂いて、本当にいい会になったと思います。

今回は本編の発表4本+LT4本+ハイパー宣伝タイム3本という内容でお送りしました。

CloudTrailとの賢いつきあいかた

目黒方面の雲の中の密林でサポートをしているマル秘ゲスト こと 半田さんによる発表。

speakerdeck.com

AWSを利用していく上で欠かせない、API利用ログのCloudTrailの基本から検索エンジンを使った活用までを紹介していただきました。

CloudTrail単体でも簡易的にはAPIアクセスのログを見ることはできますが、より柔軟にログの調査をしたい場合や利用傾向を見たい場合には公式に提供されているものだけだとちょっと物足りないので、CloudSearchやElasticsearch+kibanaを利用して、より便利に使いましょうというお話。

CloudTrailのログだけ取得していて活用できていなかった場合には、emblukを使って、「今」のログではないけれど、とりあえずたまっているログを検索エンジンに入れてみて利用イメージを掴んでみるといいということも教えてくれたりと、さすが中の人。

皆さん、まずはCloudTrailをOnにしましょう。

AWS認定ソリューションアーキテクト受験顛末記

ハートビーツで働く学生、阿部さんによるリアルな受験記。

なんと発表の1週間前に受験することを決めて、発表の前日が試験の日というhbさんのスパルタぶり学生でも社会人でも隔て無く育てる愛情を感じつつ、それでもやってのける阿部さんのポテンシャル高さと優秀さが伝わってくる発表でした。

ITインフラの運用に関わりつつも、AWS自体はそれほど触っていないという中から、試験に受かるところまでが生々しく語られていました。

ハートビーツさんで学生の頃から働くことで、学生時代ではなかなか触れることのない運用という部分に触れて、しっかり教育もしてもらえるという本当にいい環境にいる学生さんたちが羨ましく思えました。(ハートビーツさんで立派に育って弊社に来てもいいんだよ、と思ったことは内緒です)

OSSなPrime Cloud Controllerの話

某ホワイト企業で働く浅野さんによるPrimeCloud Controlerの紹介。

AWSに限らず様々なCloudサービスの管理を統合できるOSSツール。EBSやらsunapshotの氾濫を防ぐためにも有用なツールとなるし、Puppetを駆使してインスタンスの追加と監視ツール(Zabbix)への追加とを同時に出来る大変ありがち機能も。

CloudTrailでAPIログを取ることもできるが、PrimeCloud Controllerのようなツールを使うことで、自前でも操作ログを取ることができるので、様々なサービスの操作ログを集約することもできる。UIもキレイに作りこまれているので、簡素なUIと比べて使おうという気にもなりやすいと思う。

Contributorも募集しているとのこと。気になる方は
primecloud-controller-org/primecloud-controller · GitHubこちらをチェック。

ML入門(+ Amazon MLちょっと触ってみた)

構造計画研究所滝さんによるガチなML(Machine Learning/機械学習)のお話。

speakerdeck.com

はい、出直してきます。。。
この御方、ツールを使ってMLかじりましたではなく、Ph.Dとってるガチな方でした。。本気でついていけなくて、ツイートが少なくなってしまってすみませんでした。。。ただ、本気でMLとはという部分から活用するところまでをやろうと思ったら、これくらいは理科しておかないといけないと、気を引き締めるいい機会をいただけたように思えます。

滝さん曰く、「今のMLaaSなら、自分のほうが上」的なことを言っていて、まだまだサービスとしてもこれからな分野なんだろうなと。

やはり、どこでも言われていますが、こういったツールなどを用いて分析をする場合、前処理が重要になってくるので、そこはまだまだ人手で何とかする必要があるようです。が、それ以降の処理についてはAmazon MLを使えば簡単に扱うことができるので、良いとのことでした。

弊社の画像処理研究チームと話が合いそうなので、是非勉強会というか交流会をしましょう的な感じで繋げられたのも良かった。

LTs

Amazon ML with API

三浦さん(1)によるAmazon MLのAPIのお話。

www.slideshare.net

こんな感じで簡単に使うことができますよというところをご紹介頂きました。

映像制作、学生とビジネスブリッジ系

三浦さん(2)による学生さんとビジネスとを結びつけた事例の紹介。

AWSクラウド、ITと言った感じではなく、企業と学生のコラボ的な企画の紹介と言った感じ。企業主体で学生とコラボ(協力)するというアプローチはそれなりにあるので、学生(アカデミック)から企業へのアプローチしたり、研究成果をいかに実用化ビジネス化をしていくかというったところで協力・苦労した話だったら、(JAWS-UG京王線のテーマとして)もう少し将来的な広がりが感じられてより良かったかなと(三浦さんの発表が云々でなく、そういったところをテーマとしても扱いたいなという意味で)。

あなたのそのWebアプリ守られてますか?

私まてつによるLTです。本当はECSでなにかやってみた的なことをしたかったけど、間に合わず開始時刻の数時間前にネタ切り替え。

www.slideshare.net

AWS上で動作するWAF,Imperva SecureSohereを使った環境を構築してみた話。CloudFormationのテンプレートや構築手順がImperva社から公式に提供されているが、少々クセがあるために一筋縄ではいかないということを紹介したかったというのが1つ。セキュリティの専門家ではないので偉そうなことは言えないですが、アプリケーションの作りとして既知の攻撃や脆弱性への対応はしておく必要はあるけど、その前段階でブロックしてもらえるのであれば専用のツールでブロックしておいたほうがいいよねと言ったことを伝えたかった。

あとは、こういったツールを導入するのはいいけど、重要なのはそこで出てきたログに対してどうするかという運用がより大切だよという感じでまとめてみました。

メガネさん!

JAWS Daysなどの大きなイベントのLTでも有名なメガネさんによるLT。Daysのあの興奮を再び!かと思いましたが、Daysの内容はチラ見せだけでしたw

これだけJAWSのLT界隈(?)では有名なメガネさんだが、JAWS-UGの勉強会はこれが初めてだったとのこと。まさかの初めてに選んでいただけて光栄です!

もともとはメガネさんもドアウェイな感じから始まった超有名LTerへの道。LTをやることでコミュニティや界隈への一歩を踏み出しやすく(逆に言えば周りから声を書けてもらいやすくなってまじりやすくなる)といったことを話してくださった。自分も仕事で初めて発表してからは、少しずつコミュニティに入っていけるようになったと思うので、最初の一歩は大事だなと思います。

ただ、メガネさん、6/3(勉強会の4日後)のSummitのJAWS-UGのLTで何を話すかまだ決まっていなとのことでしたw 懇親会でいろいろな方と話してネタ集めをしていたので、3日が楽しみでしょうがないです。

ハイパー宣伝タイム

BATTLE HACK

SendGridエバンジェリスト中井さんによるBATTLE HACKの紹介。

2015.battlehack.org

豪華賞品/賞金がもらえる世界的なハッカソン。SendGridもAPI提供しています。

クラウド女子会&JAWS FESTA青森物産展

飯田橋のぎょリアロバーツことぎょりさんによる宣伝。

7/25の「JAWS-UGクラウド女子会~コミュニティの式年遷宮!?初めてさんもお馴染みさんもいっしゃーい~」と8/29「JAWS FESTA Tohoku 青森物産展」の宣伝。女子会の方は式年遷宮と銘打って、コミュニティの世代交代があるとかないとか。

July Tech Festa

ハートビーツ藤崎さんによるJuly Tech Festaの紹介。

2015.techfesta.jp

今年もやりますJTF。インフラエンジニアの祭典と言われているのに、未だに一度も参加したことがなかったので、今年は参加するためにチケットも購入しました!豪華発表者が予定されているので、初めての方も、そうでない方も。

もう終わっちゃってますが、5月中に申し込むとチケットが80% OFFでした。

まとめ

第1回とは比べ物にならない規模で開催することができて、藤崎さんをはじめハートビーツの方々、電気通信大学ベンチャー支援部門の方々、スタッフのみなさん、参加者の皆様には本当に感謝しています。

はやりのMLというだけで飛びついてみようと思っていた気持ちに対し、気軽なものではないんだという感じで目を覚まさせてくれた滝さんのガチっぷりには本当にいい刺激を受けましたw

相変わらずしゃべりベタで、まだまだ司会に返り咲くことは難しそうですが、LTなどの発表を通していつの日か司会に返り咲くことができるよう精進してまいる所存にございます。

今回発表してくださったハートビーツの阿部さんに刺激を受けて、ハートビーツで働く別の学生 伊藤さんも次回にはLTで発表してみたいとの意思表明をいただいたので、こんな感じでもっともっと学生さんが前に出てきてくれるようになることを願いつつ、3ヶ月後くらいの次回もより良い会になるように企画できたらいいなと。


さあ、6/3はウルトラクイズの日だ(それがメインイベントではない)。今年もラスベガスにけるように頑張りますよ!