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 |
次に、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は、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のデプロイダイアログが表示されます。ここでは、ステージの設定を行えますので、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
と入力してあげれば、同様にインスタンスを停止してあげることができます。