AWS LambdaでサーバーレスにEC2メンテナンスをslackに通知する 〜その4〜
4回に分けて連載しましたが、本記事で最後となります。
ここでは、Lambda関数の実装解説と、実践で困ったことを紹介します。
インストール
ご紹介するコードを含めたツールは、実行が可能な状態でnpmかGitHubからインストールできます。
前回までの手順を元にLambda関数をAWSに設定しておくことで、毎回AWSコンソールから実行することなくローカルでも実行できるようにしているのが特徴です。
実行する前に、aws-cliをグローバルにインストール・configureした後、AWSに設定した関数名を下記に渡してください。
- .envのFUNCTION_NAME
- zipDeploy.shに渡す第一引数
コード解説
実際のコードを少しずつ解説します。
require('dotenv').config(); const AWS = require('aws-sdk'); AWS.config.update({ region: process.env.REGION, }); const ec2 = new AWS.EC2({});
ローカルで実行する場合は、.envに必要な環境変数を指定して、それをdotenvで読み込んでいます。
尚、aws-sdkはAWS上で実行する場合は不要ですが、ローカルで実行するためにdependenciesに指定しています。
exports.handler = () => {
Lambda 関数を作成するときに、ハンドラーを指定しましたが、これはサービス内でコードを実行する際に AWS Lambda が呼び出すことができる関数です。
コールバックパラメーターは省略可能で、呼び出し元に情報を返す場合には使用しますが、今回はslackにPOSTするだけなのでcallbackは呼びません。
const params = { IncludeAllInstances: true, }; … exports.handler = () => { ec2.describeInstanceStatus(params, (err, res) => {
DescribeInstanceStatus - Amazon Elastic Compute Cloud
こちらのAWSの公式APIを使ってイベント情報を取得しています。
前回の記事でも触れましたが、"IncludeAllInstances: true"にすれば、Configurationでサブネットに指定したサブネット以外の全インスタンスのステータスが取れました。
console.log(`Instance num: ${res.InstanceStatuses.length}`);
console.log() ステートメントは、受信イベントデータの一部を CloudWatch Logsに記録します。
function report(text) { slack.setWebhook(process.env.WEBHOOK_URI); slack.webhook({ channel: process.env.SLACK_CHANNEL, username: process.env.SLACK_USERNAME, icon_emoji: process.env.SLACK_ICON_EMOJI, text: toMentionText(text), }, (err, res) => { console.log(err, res); }); }
最後に取得した全イベントをslackに通知します。
困ったこと
if (!err) { … } ↓ if (err !== null) { … }
普通のJavaScriptでは、変数の存在判定でnullかどうかも判別できます。
しかし、Lambda上ではできなかったので仕方なくerrがnullかどうかを指定しました。
if (status.Events.length > 0) { events.push({ status.InstanceId, // Syntaxエラー status.InstanceState, // Syntaxエラー status.Events, // Syntaxエラー }); } ↓ if (status.Events.length > 0) { events.push({ InstanceId: status.InstanceId, InstanceState: status.InstanceState, Events: status.Events, }); }
PropertyShorthandも使えませんでした。
最も困ったのは、対応済のイベントもAPIで取得してしまうことでした。
Webコンソールだと、下記のようなリクエストパラメータで、対応完了したイベントは取得しません。
"継続中と予定"を示す、"eventsStatusFilter=in-progress-and-scheduled-events"であれば、対応済のイベントは表示されませんが、"all-statuses"だと表示されてしまいます。
APIのFilterパラメータ仕様を見ても、対応済のイベントを取得しない方法が見つからなかったので、下記のようにDecriptionの"[Completed]"で判断する運びとなりました。
const events = _ .chain(res.InstanceStatuses) .filter(status => status.Events.length > 0) .map(status => { return { InstanceId: status.InstanceId, InstanceState: status.InstanceState, Events: status.Events, }; }) .filter(instance => { // for exclude completed event let filtered = _.filter(instance.Events, (event) => { return !event.Description.startsWith('[Completed]'); }); return filtered.length > 0; }) .value();
おわりに
Labmdaはサーバーレスのさきがけとして、Alexa SkillをAmazon Echoに追加できたりと重要性が高まっているため、これ以外にも日常業務で使えそうなツールを開発していきたいです。
AWS LambdaでサーバーレスにEC2メンテナンスをslackに通知する 〜その3〜
登場人物おさらい
この記事では4を行っていきます。
- Lambdaを実行するIAMにアタッチするポリシー
- スケジュール実行に必要なCloudWatch Events設定
- SlackのIncoming Webhooks設定/Webhook URL取得
- Lambda Functionの作成・実装
Lambda前提知識
- メモリはデフォルト128MBで、64MBごとに最大1.5GBまで指定可能
- タイムアウト時間はデフォルト3秒で、最大5分まで指定可能
- ステートレスに実装する必要あり。永続化したい場合はDynamoDBやS3などを利用する。
- ディスクIOは/tmp領域のみ読み書き可能
- リクエスト数、遅延、可用性とエラー率のメトリクスはCloudWatchに、実行時のログはCloudWatch Logsに保存され、後から参照可能
AWSコンソールからLambda Functionの作成
サービス一覧から Lambda を選択します。
Create a Lambda function を選択します。
Select blueprint ではLambdaのワークフローにおいて、様々なテンプレートが用意されています。
今回は関連するサービスを一から選択するので、Blank Function を選択します。
Configure triggers では下記の通り選択していきます。
トリガーにはプルダウンから CloudWatch Events - Schedule を選択します。
- Rule name:イベントのルール名を決めます。
- Rule description:イベントルールの詳細を入力します。
- Schedule expression:実行間隔を指定します。
- Enable trigger:実装が完了するまでは無効にしておかないと、途中状態のコードで実行されてしまうので注意してください。
作成したら関数 → トリガー一覧に、下記のようなトリガーが作成されます。
Lambda Function作成
細かい設定を行っていきます。
- 名前:関数名を指定します。
- 説明:この関数の説明を記述します。
- ランタイム:Lambdaの実行環境です。ここではNode.js 6.10を指定します。
ランタイムは2017/04/12時点で他に、下記の環境を指定することができます。
コードエントリタイプについて
コンソール上の説明に記述されたとおり、aws-sdk以外のカスタムライブラリ以外が必要ない場合は、インライン編集で十分かと思います。
- コードをインラインで編集
- .ZIP ファイルをアップロード:ライブラリ毎固めてzipファイルでアップロードします。
- Amazon S3 からのファイルアップロード:選択すると、S3 リンクのURLが指定できるようになります。
今回はlodashやslack-nodeなどの外部モジュールを使いたかったので、
node_modules込みでzip形式でアップロードしました。
環境変数
コードの再利用性を考えて、環境変数にアクセス先などを指定しています。
- SLACK_CHANNEL:通知したいslackのチャンネル名です。
- SLACK_ICON_EMOJI:slackに通知したときのアイコンです。ここではoctodexに投稿された画像をアイコンとして使っています。
- SLACK_USERNAME:slackに通知したときのユーザ名です。
- REGION:LambdaやEC2を用意しているリージョンです。
- WEBHOOK_URI:前回の記事の3.で取得したSlackのIncoming Webhooks URLです。
Lambda 関数ハンドラおよびロール
- ハンドラ:index.handler
- ロール:既存のロールを選択
- 既存のロール:lambda_basic_execution
ハンドラはindex.js内のexports.handlerを呼び出すのでこのままでよいです。
ロールは前回の記事の1.で作成したロールを指定します。
タイムアウトはデフォルトの3秒から10秒に変更します。
このぐらいの処理であれば3秒のままでも正常終了はしますが、エラーが表示されたためです。
実行結果
以上、1〜4 まで全ての設定を行うことで、このように朝10時にチェックが走り、その結果を通知するようになりました。
尚、メンテナンスイベントが何もない場合は通知しないようにしています。
InstanceStateはそれぞれ、
- 0 (pending)
- 16 (running)
- 32 (shutting-down)
- 48 (terminated)
- 64 (stopping)
- 80 (stopped)
を示しています。
この例でいくと、instance-stop イベントなので、稼働中のインスタンスにおいて再起動が必要です。
予定された日時に停止してかまわなければこのままでよいですが、対処しないと同じ通知が飛ぶので、基本的には手動で停止 → 起動を行ったほうがよさそうです。
尚、AWSコンソールからも同じ内容が確認できました。
大変長い記事となってしまいましたが、
最後に次回の記事で、実際のコードと解説・及び困ったことを紹介していきます。
次回の記事はこちら。
kitakitabauer.hatenablog.com
AWS LambdaでサーバーレスにEC2メンテナンスをslackに通知する 〜その2〜
登場人物おさらい
4は少し長くなりそうなので、この記事では1, 2, 3を行います。
- Lambdaを実行するIAMにアタッチするポリシー
- スケジュール実行に必要なCloudWatch Events設定
- SlackのIncoming Webhooks設定/Webhook URL取得
- Lambda Functionの作成・実装
尚、設定用のIAM Userは事前に作成済で、ログインした上で操作している前提とします。
1. Lambda実行用の独自ポリシー作成
ポリシー要件は下記の通りです。
- CloudWatch Logsへのログ出力
- VPC内で実行可能
- EC2メンテナンスの情報が取得可能
AWSコンソールにて "IAM" を選択し、ロールの設定画面を開きます。
ポリシーをアタッチするIAMロールは、Lambda関数を作成すると自動的に作成されるデフォルトの"lambda_basic_execution"にします。
もちろん先にLambda関数を作成せずに、同名・別名で新規作成してもかまいません。
ポリシーの作成は "ロール" の下の "ポリシー" → "ポリシーの作成" から行います。
既存のポリシーからコピーしたり、ジェネレータを使うやり方もありますが、
ここでは "独自のポリシーを作成" を選択します。
選択後表示された入力欄をそれぞれ埋めていきます。
ポリシー名:EC2FullAccess
説明:EC2 フルアクセス
ポリシードキュメントはこちらです。
{ "Version": "2012-10-17", "Statement": [ { "Sid": "Stmt14811776XXXXX", "Effect": "Allow", "Action": [ "logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents", "ec2:*" ], "Resource": "arn:aws:logs:*:*:*" } ] }
インスタンスの状態を確認するためには "ec2:DescribeInstanceStatus" が許可されている必要があります。
Lambda作成時にVPCを指定して作成されるロール "AWSLambdaVPCAccessExecutionRole" には上記が付かないため、別のロールにアタッチしています。
ここでは"ec2:*"としてしまっていますが、もちろん今回必要と思われる下記アクションを一つ一つ指定してもかまいません。
{ "Version": "2012-10-17", "Statement": [ { "Sid": "Stmt14811776XXXXX", "Effect": "Allow", "Action": [ "logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents", "ec2:CreateNetworkInterface", "ec2:DescribeNetworkInterfaces", "ec2:DeleteNetworkInterface", "ec2:DescribeInstanceStatus" ], "Resource": "arn:aws:logs:*:*:*" } ] }
作成したポリシーを先程のロールにアタッチしたら完了です。
2. CloudWatch Eventsにルールを作成
CloudWatch Eventとは、AWSのシステムイベントのほぼリアルタイムなストリームを、Lambda関数や、Amazon SNS のトピック、Amazon Kinesis Streamsに振り分けることが可能なサービスです。
Cron 式に下記を記述します。
0 1 ? * MON-FRI *
この設定により、1日1回、月〜金の10時ごろに実行されます。
グリニッジ標準時で記述するので、日本標準時(UTC+0900)だと +9時間となります。
実際は、次回作成するLambda関数を準備してから、CloudWatch Eventのターゲットに指定します。
関数作成後にこの作業を行うことだけ覚えておいてください。
3. SlackのIncoming Webhooks設定/Webhook URL取得
Incoming Webhooksとは、外部ソースからのメッセージをSlackに投稿するWebhookです。
設定・URL取得方法は下記がわかりやすかったので参考にしてください。
docs.hatenablog.jp
ここで取得したURLを、Lambda関数のslack通知ロジックに指定することになります。
今回はここまでです。
次回の記事はこちら。
kitakitabauer.hatenablog.com
AWS LambdaでサーバーレスにEC2メンテナンスをslackに通知する 〜その1〜
はじめに
直近の業務で下記案件に対応する機会がありました。
EC2メンテナンスイベントが、英語のメールで通知されるだけじゃ見落としがちなのでslackに通知したい
色々な方法が考えられますが、せっかくなので興味があったAWS Lambdaを使ってみました。
手順が多かったので、数回に分けて書いていきます。
Lambdaとは?
AWSのサービスの一つで、サーバーのプロビジョニングや管理なしでコードを実行することができるサービスです。
aws.typepad.com
サーバーレスというのはあくまで我々サービス利用側で、仕組み的には、必要に応じてコンピュートリソース(EC2インスタンス)を起動し管理されていますが、それを意識する必要はありません。
EC2メンテナンスイベントってなに?
AWS は、インスタンスの基盤となるホストコンピュータをメンテナンスする必要があるとき、インスタンスのメンテナンスを予定します。
とあるように、EC2では、まれにインスタンスの再起動が行われます。
これは前述したメールやマネジメントコンソールのEC2のトップページに通知されますが、
見落としがちなのと、普段からコンソールにログインするとは限らないので、今回slackに通知する運びとなりました。
要件
今回の要件をまとめると、下記2点になります。
- EC2のメンテナンス通知は、AWSから英語メールが届くものの見落としがち
- EC2ではオーバースペックなこともあり、Lambdaでサーバレスに通知してみたい
尚、今回はAWS内部でEventが走り、slackからOutgoingで呼び出しもないため、API Gatewayの準備は不要です。
具体的にはこの流れです。
- 1日1回のスケジュールイベントでLambda Functionを叩く
- メンテナンスイベントから対象のEC2インスタンスイベントを抜きだす
- slackに通知する
登場人物
- Lambdaを実行するIAMにアタッチするポリシー
- スケジュール実行に必要なCloudWatch Events設定
- SlackのIncoming Webhooks設定/Webhook URL取得
- Lambda Functionの作成・実装
今回はここまでです。
次回以降、これら登場人物の詳細を説明していきます。
次回の記事はこちら。
kitakitabauer.hatenablog.com
GoでSet型を実現する場合の選択肢
はじめに
Go言語には標準で Set 型がありません。
ここでは2通りの実現方法を検討してみます。
1. map ✕ structで実現する
mapのフィールドに空のstructを使ってsetを定義します。
structは何もフィールドを持たない場合、サイズは0になるのでコストがかかりません。
A struct{} takes up no space.
※golang-nutsにて説明されています。
package main import . "fmt" func main() { list := []string{ "test1", "test2", "test3", } set := make(map[string]struct{}) for _, v := range list { set[v] = struct{}{} } Printf("%#v\n", set) }
map[string]struct {}{ "test1":struct {}{}, "test2":struct {}{}, "test3":struct {}{} }
2. ライブラリを使う
golang-setというライブラリがいけてそうなので紹介します。
2017/02/03にもコミットがあり、わりと最近も更新されているようです。
github.com
GoDocはこちら
関数はかなり充実していました。
package main import ( . "fmt" "github.com/deckarep/golang-set" ) func main() { s1 := mapset.NewSet() s1.Add("1") s1.Add("2") s1.Add(3) s1.Add(4) print(s1) // Set{4, "1", "2", 3} // slice → set slice := []interface{}{"1", "2", 3, "5"} s2 := mapset.NewSetFromSlice(slice) print(s2) // Set{"5", "1", "2", 3} // set同士の結合 all := s1.Union(s2) print(all) // Set{3, 4, "1", "2", "5"} print(all.Difference(s1)) // Set{"5"} // 要素数確認 Println(s1.Cardinality()) // 4 Println(s2.Cardinality()) // 4 Println(all.Cardinality()) // 5 // 存在チェック Println(s1.Contains(1)) // false Println(s1.Contains("1")) // true // 要素削除 s1.Remove(4) print(s1) // Set{"1", "2", 3} // 要素全削除 s1.Clear() print(s1) // Set{} } func print(s mapset.Set) { Printf("%#v\n", s) }
まとめ
簡易的でよければ 1 で、
凝った使い方をしたければ 2 という選択がよさそうです。
おしまい。
GitHubの2段階認証におけるプロトコル最適解を考える
なに?
cloneしたときに選択したプロトコルが、ローカルGit設定の remote url にそのまま設定されるのですが、
そもそもプロトコルは何を選べばいいのか、ベストプラクティスは何かを検討します。
前提
現場では2段階認証が必須となっているので、前提項目とします。
2段階認証にしておけば、パスワードが盗まれても安全なので推奨します。
プロトコルの種類と速さ
まずプロトコルの種類は下記の3つです。
そしてcloneの速さは下記の順番です。
git > https >> ssh
これだけ見れば "gitプロトコルでいいのでは?" となるのですが、そうもいかない事情があります。
git プロトコル
gitプロトコルは read-only のため、そのままではpushできません。
また、社内proxyを経由していて gitプロトコルで使う9418ポートがFWなどで制限されている場合は使えないので、その場合は他のプロトコルに変更する必要が出てきます。
尚、gitプロトコルは現在GitHubのヘルプ上では言及されていませんが、利用可能です。
結論
下記手順を踏むことによって、
cloneは最速のgitプロトコルで clone し、
pushではsshプロトコルによってパスフレーズを求められることなくpushできます。
指定したurlに対して他のプロトコルを使うようにgitconfigに定義します。
[url "git@github.com:"] pushInsteadOf = git://github.com/ pushInsteadOf = https://github.com/ [url "git://github.com/"] insteadOf = https://github.com/
この設定によって、"https:" や "git:" を使っていても git push のときには ssh 経由になります。
git fetch や git pull の時は "https:" の代わりに "git:" を使用します。
尚、git configでは"pushInsteadOf"の部分が一つずつしか設定できないため、直接ファイルに追記しました。
また、ssh/configに下記設定を追加すれば、sshのcloneが高速化するので、
間違ってsshでcloneしたときのために設定しておくとよいかと思います。
Host github.com Compression yes Ciphers arcfour128,arcfour,aes128-cbc,3des-cbc,blowfish-cbc,cast128-cbc,aes192-cbc,aes256-cbc
Ciphersで利用する暗号方式を指定します。左から順に利用を試みます。
検証
それでは設定を検証してみます。
まずgitプロトコルでcloneします。
% git clone git://github.com/kitakitabauer/clone-sample.git Cloning into 'clone-sample'... remote: Counting objects: 51, done. remote: Compressing objects: 100% (3/3), done. remote: Total 51 (delta 0), reused 0 (delta 0), pack-reused 48 Receiving objects: 100% (51/51), 10.82 KiB | 0 bytes/s, done. Resolving deltas: 100% (6/6), done.
リモートURLを確認すると、"fetch" ではgitプロトコルが、
"push" ではsshプロトコルが設定されていることがわかります。
% git remote -v origin git://github.com/kitakitabauer/clone-sample.git (fetch) origin git@github.com:kitakitabauer/clone-sample.git (push)
問題なくpull/pushできます。
% git pull -v Looking up github.com ... done. Connecting to github.com (port 9418) ... 192.XX.XXX.XXX done. From git://github.com/kitakitabauer/clone-sample = [up to date] master -> origin/master Already up-to-date. % git push -v Pushing to git@github.com:kitakitabauer/clone-sample.git To github.com:kitakitabauer/clone-sample.git = [up to date] master -> master updating local tracking ref 'refs/remotes/origin/master' Everything up-to-date
リポジトリ内の .git/config にも反映されています。
[remote "origin"] url = https://kitahara-yuki@github.com/kitakitabauer/go-sample.git fetch = +refs/heads/*:refs/remotes/origin/*
おしまい
golang製のREPL内でUnixタイムスタンプから日付フォーマットを取得する
忘れがちだけど、よくある動作なので書き留めておきます。
準備
goreをインストールしておきます。
github.com
$ go get -u github.com/motemen/gore
"gore" とは、コード補完もしてくれるREPL(Read-eval-print loop)で、文字通り読んで評価して表示して繰り返すツールのことです。
仕組み的には、裏ではそれまでの入力を順番に実行するソースコードを生成して "go run" しています。
goreをより使いこなしたい場合は下記もインストールしておくとよいです。
# 入力補完 $ go get -u github.com/nsf/gocode # プリティプリント $ go get -u github.com/k0kubun/pp # ドキュメント $ go get -u golang.org/x/tools/cmd/godoc
実践
$ gore gore > :import time # 第一引数に日付フォーマット変換したいUnixタイムスタンプ gore > t := time.Unix(1488697427, 0) time.Time{sec:63624294227, nsec:0, loc:(*time.Location)(0x1070a0)} gore > t.Format(time.RFC3339) "2017-03-05T16:03:47+09:00"
フォーマットの種類は公式のformat.goのdocを参照してください。
https://golang.org/src/time/format.go
逆に、指定した日付のタイムスタンプを取得する場合
UTCでの、2016年1月1日0時00分の定点がほしい場合
$ gore gore > :import time gore > day := time.Date(2016, 1, 1, 0, 0, 0, 0, time.UTC) time.Time{sec:63587203200, nsec:0, loc:(*time.Location)(nil)} gore> day.String() "2016-01-01 00:00:00 +0000 UTC" gore> day.Unix() 1451606400
東京での、2016年1月1日0時00分の定点がほしい場合
# ロケーション名とUTCとの時差で、ロケーション構造体を作成 gore> jst := time.FixedZone("Asia/Tokyo", 9 * 60 * 60) &time.Location{name:"Asia/Tokyo", zone:[]time.zone{time.zone{name:"Asia/Tokyo", offset:32400, isDST:false}}, tx:[]time.zoneTrans{time.zoneTrans{when:-9223372036854775808, index:0x0, isstd:false, isutc:false}}, cacheStart:-9223372036854775808, cacheEnd:9223372036854775807, cacheZone:(*time.zone)(0xc42000a3e0)} gore> day := time.Date(2016, 1, 1, 0, 0, 0, 0, jst) time.Time{sec:63587170800, nsec:0, loc:(*time.Location)(0xc42006e120)} gore> day.String() "2016-01-01 00:00:00 +0900 Asia/Tokyo" gore> day.Unix() 1451574000
おしまい