fluentdをsystemdで管理する

6日目が少々反則的な内容となってしまいましたが、きにせず7日目行きたいと思います。今日は仕事からの帰りが遅かったので書き始めから日付を超えているというミラクル。もう、時間を気にせず書けます。

と、前置きはこれくらいにして本題に入りたいと思います。いままでのガッツリな感じ(?)とは異なり、時間の関係でサラッとしたネタです。参考にさせていただいたのはRHELやSystemdやDockerの説明では知らない人はいないであろう中井さんの下記サイトです。

enakai00.hatenablog.com


ログ収集をする場合にはデファクトスタンダードとなっているであろうfluentdをCentOS7などのSystemdなディストリビューションで起動/停止の管理をしたい場合の設定方法です。といっても、ひとまず動かしてみたレベルなので、問題もあるかもしれません。

通常はtd-agentをRPMからインストールしますし、まだCentOS7でもCentOS6の時のようにinitスクリプトで管理することもできますし、パッケージに付属しているのもinitスクリプトとなっています。ただ、私、結構変わり者でして、gem install fluentdとして使うのが好きだったりします(ツッコミたい気持ちは抑えてください)。

というわけで、rbenvでシステム全体にインストールしたRubyを使って、fluentdをSystemdの管理下で動かしてみたいと思います。

Fluentdのインストールと初期設定

jemallocは入れなくてもいいとも言われていますが、とりあえず入れる場合の手順です。入れない場合は、関連する部分を削っていただければ。jemallocはepelリポジトリが必要なので、先にepel-releaseをインストールしてリポジトリ情報を追加してから。
※パス周りがいろいろあるので、rootで作業しちゃいます。あしからず。

インストールと起動直前まで設定はこんな感じ。

# yum install epel-release -y
# yum install jemalloc -y 
# gem install fluentd --no-document
# useradd -M -s /bin/false fluentd
# mkdir /var/log/fluentd
# chown fluentd. /var/log/fluentd
# vim /etc/fluent/fluent.conf
# vim /etc/sysconfig/fluentd

/etc/fluent/fluent.conf

<source>
  type forward
</source>

@include conf.d/*.conf

<match fluent.**>
  type file
  path /var/log/fluentd/fluentd_internal.log
</match>

<match **>
  type file
  path /var/log/fluentd/else
  compress gz
</match>

/etc/sysconfig/fluentd

LD_PRELOAD=/usr/lib64/libjemalloc.so.1
FLUENTD_USER=fluentd
FLUENTD_GROUP=fluentd
FLUENTD_LOGFILE=/var/log/fluentd/fluentd.log

これが今日のメインともいうべきファイル。SystemdのUnitファイルと言うんですかね、設定ファイルはRPMでインストールしたものは /usr/lib/systemd/system/hoge.service に配置され、それを上書きをしたり自前で作る場合は /etc/systemd/system/hoge.service に配置します。

/etc/systemd/system/fluentd.service

[Unit]
Description=Fluentd daemon
After=network.service

[Service]
EnvironmentFile=/etc/sysconfig/fluentd
LimitNOFILE=65536
ExecStart=/usr/local/rbenv/versions/2.2.3/bin/fluentd --user ${FLUENTD_USER} --group ${FLUENTD_GROUP} --log ${FLUENTD_LOGFILE}
ExecStop=/bin/kill -INT ${MAINPID}
ExecReload=/bin/kill -HUP ${MAINPID}
Restart=always

[Install]
WantedBy=multi-user.target

それぞれの説明

  • Unit
    • Description: Unitの説明
    • After: このサービスの後に起動されるように
  • Service
    • EnvironmentFile: 環境変数を定義したファイル
    • LimitNOFILE: ulimit -fで指定するようにmax open fileを指定
    • ExecStart: 起動コマンド
    • ExecStop: 停止コマンド($MAINPIDでsystemdが管理しているpidを取得できる)
    • ExecReload: Reloadコマンド(同上)
    • Restart: プロセスの再起動条件(respawnと同じような感じ)
  • Install (enable/disableに関連する設定)
    • WantedBy: enable時にこのUnitのtargetディレクトリ(multi-user.target.wants)にsymlinkが作成される
      • multi-user.targetはこれまでの run-level 3に相当します。(run-level 5はgraphcal.target)

これにて起動してみましょう。

# systemctl enable fluentd
ln -s '/etc/systemd/system/fluentd.service' '/etc/systemd/system/multi-user.target.wants/fluentd.service'
# systemctl start fluentd
 systemctl status fluentd
fluentd.service - Fluentd daemon
   Loaded: loaded (/etc/systemd/system/fluentd.service; enabled)
   Active: active (running) since Mon 2015-12-07 16:30:42 UTC; 5s ago
 Main PID: 28406 (fluentd)
   CGroup: /system.slice/fluentd.service
           ├─28406 /usr/local/rbenv/versions/2.2.3/bin/ruby /usr/local/rbenv/versions/2.2.3/bin/fluentd --user fluentd --group fluentd --log /var/log/fluentd/fluentd.log
           └─28408 /usr/local/rbenv/versions/2.2.3/bin/ruby /usr/local/rbenv/versions/2.2.3/bin/fluentd --user fluentd --group fluentd --log /var/log/fluentd/fluentd.log

Dec 07 16:30:42 ip-10-11-0-199.localdomain systemd[1]: Starting Fluentd daemon...
Dec 07 16:30:42 ip-10-11-0-199.localdomain systemd[1]: Started Fluentd daemon.

無事起動しました。それでは、ちょっと設定ファイルを変えて、reloadしてみましょう。

# systemctl reload fluentd
# cat /vat/log/fluentd/fluentd.log
(一部抜粋)
2015-12-07 16:37:59 +0000 [info]: restarting
2015-12-07 16:37:59 +0000 [info]: reading config file path="/etc/fluent/fluent.conf"
2015-12-07 16:37:59 +0000 [info]: shutting down fluentd
2015-12-07 16:37:59 +0000 [info]: shutting down input type="forward" plugin_id="object:3fb66434acd8"
2015-12-07 16:37:59 +0000 [info]: shutting down input type="debug_agent" plugin_id="object:3fb66434a10c"
2015-12-07 16:37:59 +0000 [info]: shutting down output type="file" plugin_id="object:3fb66628a300"
2015-12-07 16:37:59 +0000 [info]: shutting down output type="stdout" plugin_id="object:3fb664310cb8"
2015-12-07 16:37:59 +0000 [info]: shutting down output type="file" plugin_id="object:3fb6662a89f4"
2015-12-07 16:37:59 +0000 [info]: process finished code=0
2015-12-07 16:37:59 +0000 [error]: fluentd main process died unexpectedly. restarting.
2015-12-07 16:37:59 +0000 [info]: starting fluentd-0.12.17
2015-12-07 16:37:59 +0000 [info]: gem 'fluentd' version '0.12.17'
2015-12-07 16:37:59 +0000 [info]: adding match pattern="debug.**" type="stdout"
2015-12-07 16:37:59 +0000 [info]: adding match pattern="fluent.**" type="file"
2015-12-07 16:37:59 +0000 [info]: adding match pattern="**" type="file"
2015-12-07 16:37:59 +0000 [info]: adding source type="forward"
2015-12-07 16:37:59 +0000 [info]: adding source type="debug_agent"
2015-12-07 16:37:59 +0000 [info]: using configuration file: <ROOT>
  <source>
    type forward
  </source>
  <source>
    type debug_agent
    port 24230
  </source>
  <match debug.**>
    type stdout
  </match>
  <match fluent.**>
    type file
    path /var/log/fluentd/fluentd_internal.log
    buffer_path /var/log/fluentd/fluentd_internal.log.*
  </match>
  <match **>
    type file
    path /var/log/fluentd/else
    compress gz
    buffer_path /var/log/fluentd/else.*
  </match>
</ROOT>
2015-12-07 16:37:59 +0000 [info]: listening fluent socket on 0.0.0.0:24224
2015-12-07 16:37:59 +0000 [info]: listening dRuby uri="druby://0.0.0.0:24230" object="Engine"
# systemctl status fluentd
(一部抜粋)
  Process: 28701 ExecReload=/bin/kill -HUP ${MAINPID} (code=exited, status=0/SUCCESS)

よさ気ですね。次に強制停止してみましょう。

# kill -9 28406
# systemctl status fluentd
fluentd.service - Fluentd daemon
   Loaded: loaded (/etc/systemd/system/fluentd.service; enabled)
   Active: active (running) since Mon 2015-12-07 16:45:32 UTC; 1s ago
  Process: 11982 ExecStop=/bin/kill -INT ${MAINPID} (code=exited, status=1/FAILURE)
 Main PID: 11986 (fluentd)
   CGroup: /system.slice/fluentd.service
           ├─11986 /usr/local/rbenv/versions/2.2.3/bin/ruby /usr/local/rbenv/versions/2.2.3/bin/fluentd --user fluentd --group fluentd --log /var/log/fluentd/fluentd.log
           └─11991 /usr/local/rbenv/versions/2.2.3/bin/ruby /usr/local/rbenv/versions/2.2.3/bin/fluentd --user fluentd --group fluentd --log /var/log/fluentd/fluentd.log

Dec 07 16:45:32 ip-10-11-0-199.localdomain systemd[1]: fluentd.service holdoff time over, scheduling restart.
Dec 07 16:45:32 ip-10-11-0-199.localdomain systemd[1]: Stopping Fluentd daemon...
Dec 07 16:45:32 ip-10-11-0-199.localdomain systemd[1]: Starting Fluentd daemon...
Dec 07 16:45:32 ip-10-11-0-199.localdomain systemd[1]: Started Fluentd daemon.

ちゃんと再起動されています。最後に正しく停止してみましょう。

# systemctl stop fluentd
# systemctl status fluentd
fluentd.service - Fluentd daemon
   Loaded: loaded (/etc/systemd/system/fluentd.service; enabled)
   Active: inactive (dead) since Mon 2015-12-07 16:48:50 UTC; 2s ago
  Process: 12004 ExecStop=/bin/kill -INT ${MAINPID} (code=exited, status=0/SUCCESS)
  Process: 11986 ExecStart=/usr/local/rbenv/versions/2.2.3/bin/fluentd --user ${FLUENTD_USER} --group ${FLUENTD_GROUP} --log ${FLUENTD_LOGFILE} (code=exited, status=0/SUCCESS)
 Main PID: 11986 (code=exited, status=0/SUCCESS)

Dec 07 16:45:32 ip-10-11-0-199.localdomain systemd[1]: fluentd.service holdoff time over, scheduling restart.
Dec 07 16:45:32 ip-10-11-0-199.localdomain systemd[1]: Stopping Fluentd daemon...
Dec 07 16:45:32 ip-10-11-0-199.localdomain systemd[1]: Starting Fluentd daemon...
Dec 07 16:45:32 ip-10-11-0-199.localdomain systemd[1]: Started Fluentd daemon.
Dec 07 16:48:49 ip-10-11-0-199.localdomain systemd[1]: Stopping Fluentd daemon...
Dec 07 16:48:50 ip-10-11-0-199.localdomain systemd[1]: Stopped Fluentd daemon.
# ps aux|grep [f]luentd
#

fluentdのログも見てましょう。

# cat /var/log/fluentd/fluentd.log
2015-12-07 16:48:49 +0000 [info]: shutting down fluentd
2015-12-07 16:48:49 +0000 [info]: shutting down input type="forward" plugin_id="object:3f92e82725a8"
2015-12-07 16:48:49 +0000 [info]: shutting down input type="debug_agent" plugin_id="object:3f92e634d4d4"
2015-12-07 16:48:50 +0000 [info]: shutting down output type="file" plugin_id="object:3f92e6312b7c"
2015-12-07 16:48:50 +0000 [info]: shutting down output type="file" plugin_id="object:3f92e63385fc"
2015-12-07 16:48:50 +0000 [info]: shutting down output type="stdout" plugin_id="object:3f92e63081b8"
2015-12-07 16:48:50 +0000 [info]: process finished code=0

はい、ちゃんと停止されましたね。めでたしめでたし。

おわりに

まだ設定をして軽く動作を確認して見た程度ですが、うまく行っていそうな感じではあります。以前からも今後も長くお世話になるfluentd、これから仲良くしていかなければならないSystemd、どちらもちゃんと使えるようになりたいですね。

これにて7日目の更新は終了です。それでは、また明日!

JAWS-UG京王線 攻めと守りのセキュリティ&監視を開催しました

前回の単独開催からは結構期間が空いてしまいましたが、12月6日にJAWS-UG京王線の勉強会を開催しました。今回も会場は電通大のリサージュ。ちょっとしたトラブルもありましたが、みんなで協力してなんとかなりました。(写真を見るとどことなく違和感があることに気づけるはず)

f:id:matetsu:20151206143931j:plain

今回のテーマはセキュリティ(と監視)。東京リージョンではまだ提供されていないInspectorの紹介をはじめ、さまざまな観点でのセキュリティ関連の話題がてんこ盛りでした。なんだかJAWS-UG支部の中でもマイナーな方の京王線にこんなに豪華なメンツが揃ってしまっていいのかというくらい豪華でした。

話し下手な私は、今回も表立って話すことはせずにつぶやき担当として裏方を支えていました(自画自賛)。

AWSJ松本さんによるInspectorの紹介

まだ東京リージョンでは使えないですが、オレゴンリージョンでPreviewとして提供されているAmazon Inspectorについての紹介を、AWSの中の人である松本さんにしていただきました。

AWSのサービスとしては珍しい、利用者の環境の中にアクションを起こすサービスであるInspector。これまで専門の業者を選定して様々なコストをかけて実施されてきた脆弱性診断をAWSの中の機能として実施することができるのはかなり革新的なサービスではないかと思います。現状ではAmazon LinuxUbuntuだけがサポートされていますが、そのうち様々なディストロやOSに対応してくれると思いますので、気長に待ちましょう。気長でなくても大丈夫な気がしますし、東京リージョンに来るときにはよく使われているディストリやOSには対応してくれていると信じています。

それにしても、もともとがセキュリティコンサルとして様々な立場の人に説明していたということもあってか、すごくわかりやすくて聴きやすい発表でした。内部的にもあまり情報が出回っていない中、ありがとうございました。

サーバーワークス柳瀬さんによる「モンハンで学ぶIAM」

本質のネタ枠、、、ではないです。久々に柳瀬さんの発表を聞きましたね。「モンハンで学ぶ」という感じでサーバワークスらしいネタ感も出していますが、内容としてはすごく丁寧にIAMの説明がされている発表でした。

IAM User、IAM Group、IAM Roleとある中で、やっぱりIAM Roleの説明の難しさは痛いほどよくわかりました(質問された時に回答に困りやすい)。また、githubなどの公開リポジトリACCESS_KEYID/SECRET_ACCESS_KEYを上げてしまうことの怖さについても説明していただいたので、どこを自分たちで守らないといけないのかといった共有責任モデルの部分に関しても勉強になる発表がったと思います。

自分のブログでもちょっとだけ出てきた(その時はCognitoを使いましたが)STSの時限式アクセス権についても説明がありました。自分としても勉強になったのは、一時的なアクセス権の付与にはIAM UserにConditionで期間設定するよりも、時限式アクセス権を使ったほうが安全で簡潔に済ませらるということですね。次回同じようなことが必要になる機会があったら絶対にSTSでやりますね。

モンハン経験者が少ないアウェー感のある中お疲れ様でした。

休憩

なんと、予定よりも20分早くすすんでしまい、ちょっとばかり時間の活用方法にこまる事態に。。。

坪さんによる「AWS + セキュリティ」

同時間帯に開催されていたSECCONのオンライン予選に参加しつつ、この勉強会でも発表してくださるというバイタリティあふれる方。内容もセキュリティ界隈のトレンドからAWS WAFまで幅広い内容でした。(自分がそういうリクエストをしたというのもありますがw)

最近のWebセキュリティではどんなことを気をつけて対策をするかということで、対策をどこかのポイントで1つ行うだけでは不十分で多段防御を行う事が重要であることや、いろいろ起きている昨今ではクライアント側の対策がかなり重要になっているということについても説明いただき、今すぐにでも実践しないといけないという気持ちが強くなりました。これまではセキュリティは門外漢だからという感じでいましたが、全員が当事者としてあたっていくということが大事なんだなと思える発表でした。

また、次の河野さんの発表でも出てきますが、ロギングの重要性もしていただき、とっていることに安心せず常にチェックをしていくことが非常に大切であることを改めて感じさせられました。

無茶振りに近い感じでの発表依頼となってしまいましたが受けていただけて、さらに素晴らしい発表も聞けたので大満足です。

河野さんによる「クラウドで実現する理想のID管理とトータルセキュリティ」

セキュリティ界隈では知らない人がいないくらい有名な河野さん、これまたギリギリに発表依頼をしたにもかかわらず快諾いただきました。内容は、ID管理とログ管理の統合がどれだけ重要であるか、ログ管理はリアルタイムで見なければ意味がなくなってきているということなど、真に迫る内容とわかりやす話し方で非常に勉強になる発表でした。

ID管理とログ管理を統合してリアルタイムで監視をし、普段とは違った挙動をしていることを検出するといったことを自前で持つことは大変なコストがかかるだけでなく、専門の人材もひつようとなるので、すべての企業が実施することは現実的では無い。そこで必要とされるのが Security as a Services。自分たちでやるべきことはすべてを自前で持つことではなく、共有IDなどはせず(識別)、確認機構を正しく設定し(認証)、権限管理を正しく行う(認可)といったところ。その下にあるログ管理を正しく行う(説明責任)部分はそれができるサービス(たとえばAzure AD)を使うことが求められるようになっている。当たり前のようでなかなかできていない部分ではあるので、気を引き締めなければいけないなと思いました。

河野さんの意図していないことを書いてしまいそうで怖いので、詳しくはこちらの資料を見ていただいたほうが良いと思います。

自分の初速している会社でもID管理の統合や監査のためのログ管理などすごくタイムリーな話題だっただけに、響くものが多かったです。すごく濃い内容を40分くらいで話していただきましたが、60分くらいの枠で話していただいても良かったなと思いました。次回は是非もっと長い枠でお願いしたいなと淡い期待を込めて。

LT

高度論文試験の対策本の紹介LT、攻めのITに向けてのお話を書かれた本の紹介LT、HPCに関する紹介LT、筋肉についてAWS WAFの紹介LT、社内のセキュリティ対策はちゃんとできてますかと問われるLT、KMSに関するLTと、セキュリティに関する内容でバラエティに富んだ発表がたくさんでした。

感想と反省

専門外だからと敬遠しがちだったセキュリティ周り。もともと重要だという認識はあったがなかなか自分から手を出そうとは思わないようにしていたのが少し恥ずかしくなるくらいにためになる発表ばかりでした。何をするにも当事者意識を持って臨まないと物事は進まないという気持ちを強く持たされるすごく気持ちの引き締まる勉強会となりました。

ただ、時間配分など運営側の至らない点が多かったことは、今回は反省すべき点だと思っていますので、次回以降はこの反省を活かしてより良い勉強会にしていきたいと思います。

発表者の皆様、参加してくださった皆様、スタッフの皆様、本当にありがとうございました。そして休日開催の勉強会への参加、お疲れ様でした。次回開催は未定ですが、楽しみに待っていていただければと思います。

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日坊主にならないことを祈って、初日はこの辺で。