ChatOpsとは「Chat」と「Ops」を掛けあわせた造語で、Chatをベースにシステム運用(Ops)を行うことを指します。日々のチームコミュニケーションで利用しているSlackのSlashコマンドを利用して、AWSのEC2インスタンスの起動・停止を行えるようにしたので、その手順を記載していきます。
使ったサービスは、AWS Lambda、Amazon API Gateway、そしてSlackです。
課題意識
弊チームでは、社内の検証用/分析用途で、AWS上にEC2インスタンスでClouderaのCDH(Cloudera Distribution Including Apache Hadoop)を構築しています。それなりにハイスペックなインスタンスを使っているため、利用していない時は停止してコストを抑える運用をしています。
最近では下記のような課題が出てきました。
かかわる人が増えてきた
Hadoop環境を触れる人が増えてきました。その結果、「誰が立ち上げたのかわからない。使用中なのかわからない」自体が発生するようになりました。誰が使っているのか可視化したい、また新しいメンバーも簡単に操作できた方が楽だなと思いました。
オペレーションミスを防ぐ
MasterやWorker、Gatewayノード、CDHの場合がCloudera ManagerやCloudera Atlas Directorといった関連EC2インスタンスが多く存在しました。間違って他のインスタンスを立ち上げてしまったり、一部のインスタンスを停止し忘れることがでてきました。
AWSのマネジメントコンソールへのアクセス面倒
そもそも、AWSのマネジメントコンソールに都度アクセスして、6つか7つのEC2インスタンスをチェックして起動する、という操作は面倒でした。
最終ゴール
Slackのチャネル上で、つぎのSlash Command (スラッシュコマンド) を実行し、Hadoopクラスタ関連の複数インスタンスを一度に操作(起動・停止)できるようにします。Slash Commandは、Slack のメッセージ入力欄に / からはじまるコマンドを入力することでAPIを実行できます。私の場合では、以下のようにコマンドを入力することで、EC2インスタンスを操作できるようにします。
/cdh-dev start
/cdh-dev stop
全体構成
今回はAWSの次のサービスを利用します。
AWS Lambdaは、イベントが発生した際にスクリプトを実行できる、サーバレスコンピューティングサービスです。リクエストが飛んできたときだけ、EC2インスタンスを起動/停止するスクリプトを実行してくれます。
Amazon API Gatewayは、AWS上で簡単にAPIの作成ができるサービスです。SlackからGETリクエストを取得して、それをLambdaに渡す役割を果たします。
参考
今回私は、次のブログを参考にさせて頂きました。
Slack の Slash Command で AWS の EC2 と RDS の起動と停止を実現してみた (1) 導入
ロールの作成
まず最初に、EC2の起動・停止ができるポリシーとロールを作成します。AWSマネジメントコンソールより、IAMにアクセスし、ロールの画面に飛びます。
ロールの作成をクリックし、新しいロールを作成していきます。 ロールの作成画面では、
をクリックし、次のステップをクリックします
ポリシーを選択する画面になりますので、ここではEC2Controlという新しいポリシーを作成しましょう。ポリシーを作成をクリックします
ビジュアルエディタで、次の設定をしてください。
- サービス1
- サービス:EC2
- アクション
- 書き込み:StartInstances, StartVpcEndpointServicePrivateDnsVerification, StopInstances
- リソース:すべてのリソース
- サービス2
- サービス:CloudWatch Logs
- アクション
- 書き込み:すべて
- リソース:すべてのリソース
設定が完了したら、ポリシーの確認をクリックします。
ポリシーの名前をEC2Controlと入力しポリシーの作成をクリックします。これでポリシーが作成できました。
先程のロールの作成画面に戻り、ポリシー名を検索すると、今作成したポリシーが確認できます。EC2Controlにチェックを入れて、次のステップに進みます。
ロールの作成の確認画面に進みますので、ロール名をLambdaEC2Controlとしてロールの作成をクリックして完了です。
AWS Lambdaの設定
次にLambdaの設定を行っていきます。ここでは、イベント発生時に実行するスクリプトを登録します。 AWS マネジメントコンソールより、AWS Lambdaにアクセスし、右上の関数の作成をクリックします。
関数の作成では、一から作成をクリックし、関数名、ランタイム、アクセス権限を指定していきます。
- 関数名:slack-cdh-dev(※私の場合、Dev環境にあるCDHのEC2インスタンスをSlackから操作するため、このような名前にしました)
- ランタイム:Node.js 12.x
- アクセス権限
- 既存のロールを使用するをクリックし、先程作成したLambdaEC2Controlを指定します。
設定が完了したら、右下の関数の作成をクリックします
作成したLambda関数の具体的な設定を行っていきます
関数コード
実行するスクリプトを登録します。私の用途としては、Hadoopクラスタに関連するインスタンスを一度に起動・停止するユースケースを想定していたため、インスタンスIDリストで全てハードコードしました。この部分は、実際は、のちに説明する環境変数に登録した方が良い情報かもしれません。
またスクリプト内では、イベント発生時にevent.tokenとevent.textをAPI Gatewayから受け取り、処理を行います。event.tokenはSlackから受け取ったtoken情報です。event.textはSlashコマンドの後ろに続くテキスト情報であり、私の場合が/cdh-dev [start/stop]のstartやstopなどの文字列を表しています。その文字列によって、Node.jsスクリプト内の異なる関数を呼び出し、各APIを実行しています。
'use strict'; const AWS = require('aws-sdk'); // 操作したいEC2インスタンスIDを入力する。※各自が入力する const instance_list = ["i-xxxxxxxxxxxxxxxxx", "i-xxxxxxxxxxxxxxxxx"] // EC2 インスタンスを起動する function startEC2Instance(region, instanceId) { const ec2 = new AWS.EC2({ region: region }); const params = { InstanceIds: instance_list, DryRun: false, }; return new Promise((resolve, reject) => { ec2.startInstances(params, (err, data) => { if (err) reject(err); else resolve(data); }); }); } // EC2 インスタンスを停止する function stopEC2Instance(region, instanceId) { const ec2 = new AWS.EC2({ region: region }); const params = { InstanceIds: instance_list, DryRun: false, }; return new Promise((resolve, reject) => { ec2.stopInstances(params, (err, data) => { if (err) reject(err); else resolve(data); }); }); } // EC2 インスタンスのステータスを確認する function describeStatusEC2Instance(region, instanceId) { const ec2 = new AWS.EC2({ region: region }); const params = { InstanceIds: instance_list, DryRun: false, }; return new Promise((resolve, reject) => { ec2.describeInstanceStatus(params, (err, data) => { if (err) reject(err); else resolve(data); }); }); } // 関数指定してインスタンスを制御します。 function executeControl(ec2Function) { const result = { EC2: null}; const a = ec2Function(process.env.EC2_REGION, process.env.EC2_INSTANCE_ID) .then(data => { result.EC2 = { result: 'OK', data: data }; }).catch(err => { result.EC2 = { result: 'NG', data: err }; }); const b = null return Promise.all([a, b]).then(() => result ); } function getSuccessfulResponse(message, result) { return { "response_type": "in_channel", "attachments": [ { "color": "#32cd32", "title": 'Success', "text": message, }, { "title": 'Result', "text": '```' + JSON.stringify(result, null, 2) + '```', }, ], }; } function getErrorResponse(message) { return { "response_type": "ephemeral", "attachments": [ { "color": "#ff0000", "title": 'Error', "text": message, }, ], }; } exports.handler = (event, context, callback) => { if (!event.token || event.token !== process.env.SLASH_COMMAND_TOKEN) callback(null, getErrorResponse('Invalid token')); if (!event.text) callback(null, getErrorResponse('Parameter missing')); if (event.text.match(/start/)) { executeControl(startEC2Instance) .then(result => { callback(null, getSuccessfulResponse('Starting...', result)); }); } else if (event.text.match(/stop/)) { executeControl(stopEC2Instance) .then(result => { callback(null, getSuccessfulResponse('Stopping...', result)); }); } else if (event.text.match(/status/)) { executeControl(describeStatusEC2Instance) .then(result => { callback(null, getSuccessfulResponse('Checking statuses...', result)); }); } else { callback(null, getErrorResponse('Unknown parameters')); } };
コードの作成が完了したらDeployボタンをクリックして、スクリプトを保存します。
環境変数
次の環境変数を入力し保存してください。各環境変数は、上のスクリプト内で利用します。なお、SLASH_COMMAND_TOKENについては、後々Slack側でSlash Commandの設定をする際に発行されるので、現在がひとまずhogehogeなどと入力しておきます。後ほど更新します。
キー | 値 |
---|---|
EC2_REGION | ap-northeast-1 |
SLASH_COMMAND_TOKEN | hogehoge |
API Gatewayの設定
次に、AWSのマネジメントコンソールから、Amazon API Gatewayの画面に進んでください。このサービスでは簡単にAPIを作成することができます。ここでのAPI Gatewayの役割は、Slackからコマンドを受け取り、GETメソッドでLambda関数にリクエストをイベントとして引き渡すことです。APIを作成ボタンをクリックしてください。
REST APIを構築する選択に進むと、新しいAPIの設定が行えます。
- API名:slack-cdh-dev ※Lambda関数と同じ
- エンドポイントタイプ:リージョン
とし、APIの作成をクリックします。s
まだ、何もAPIメソッドが登録されていないので、GETメソッドを作成します。アクション >> メソッドの作成をクリックし、GETを選択します。右側の✔ボタンをクリックすると、GETメソッドのセットアップに進みます。
Lambda関数には、先程作成した関数名を入力してください。私の場合はslack-cdh-devです。その他の設定がそのままで保存します。Lambda 関数に権限を追加するというダイアログが出ますので、そのままOKをクリックします。
次に、メソッドリクエストと統合リクエストの設定を行っていきます。
メソッドリクエスト
画面のメソッドリクエストをクリックしてください。ここでは、Slackから受信可能なパラメータの情報を指定します。
まず、リクエストの検証をクリックし、クエリ文字列パラメータおよびヘッダーの検証を選択してください。次に、URL クエリ文字列パラメータのタブを開くと、Slackから受け取るパラメータを指定できます。クエリ文字列の追加をクリックし、以下の情報を追加してください。
名前 | 必須 |
---|---|
text | 必須✔ |
token | 必須✔ |
textは、Slackで/cdh-dev startと入力した際のstart部分の情報が受信できます。tokenは、Slackから受け取るトークン情報です。
統合リクエスト
次に一画面前に戻り、統合リクエストをクリックします。ここでは、メソッドリクエストで取得したtextとtokenの情報をlambda関数(のevent引数)に渡す役目を果たします。一番下のマッピングテンプレートをクリックします。
リクエスト本文のパススルーの設定では、 テンプレートが定義されていない場合 (推奨)を選択し、Content Typeとしてapplication/jsonと入力します。表示されたテンプレート部分に、次のコードを入力して保存してください。この指定で、URL パラメーターの text と token がそれぞれ Lambda 関数側にevent引数のtextとtokenとして渡されます。
{ "token": "$input.params('token')", "text": "$input.params('text')" }
これでAPIの設定は完了です。
APIのデプロイ
作成したAPIをデプロイします。アクション >> APIのデプロイを選択します。
APIのデプロイダイアログが表示されます。ここでは、ステージの設定を行えますので、dev環境へのデプロイを行いましょう。
- デプロイされるステージ:[新しいステージ]
- ステージ名:dev
そのままデプロイをクリックしてください。
次の画面のように、APIのエンドポイントURLが発行されます。これでAPI Gatewayの設定は完了です。
Slackの設定
Slack側で、Slash Commandの登録を行っていきます。Slackの管理画面より、その他管理項目 >> アプリを管理するに進みます。検索窓でSlash Commandと検索してもらうと、Slash Commandのアプリが表示されるはずです。Slackに追加をクリックしてください。
コマンドを選択するの項目では、実際に利用するSlash Commandを入力します。執筆者の場合には、/cdh-devと入力します。Slash Commandは、その名のとおり、スラッシュ/から始まるコマンドです。入力が終わったらスラッシュコマンドインテグレーションを追加するを押下してください。
最後にコマンドの各設定を行っていきます。
一番下のインテグレーションの保存をクリックすれば、Slash Commandは完成です。
lambda関数のバージョンの発行
最後に、残っていることが2つあります。1つはlambda関数の環境変数SLASH_COMMAND_TOKENの編集です。現在hogehoeという値になっていますので、Slash Commandの設定で取得したトークン情報に書き換えてください。
2つ目に、Lambda関数の発行を行います。Lambda関数の設定画面上段のアクション >> 新しいバージョンを発行をクリックしてください。これで、最新のlambda関数が利用できるようになりました。
Slackから試してみる
任意のチャネルで、/cdh-dev start
と入力してください。(※このコマンドは私が指定したコマンドです)。レスポンスとして、Successというメッセージと、各インスタンスの起動状態がjson形式で返ってきます。/cdh-dev stop
と入力してあげれば、同様にインスタンスを停止してあげることができます。