ようへいの日々精進XP

よかろうもん

最近ギョームでやったこと (2) 〜 もう 2019 年なのに, 今更だけど EC2 運用の ChatOps にチャレンジしているメモ 〜

tl;dr

タイトルの通りです.

ChatOps という言葉がインターネット上に流布されるようになったのは, 古くは 2003 年!!, そしてグーッと盛り上がってきたのが 2013 年!!ということなので, 今更感がありますが YAMAP にて EC2 の運用に ChatOps を取り入れようとしているので, どんなことをやっているのかをメモっておきます.

YAMAP 社内では EC2 インスタンスでいくつか社内サービスを提供していますが, その中でも, エンジニアチームが検証の為に自由に利用する環境が複数台 EC2 で稼働しています. 今回はこの検証環境用 EC2 (OS は Amazon Linux) を ChatOps で運用するようにしてみた話です.

課題

検証環境用 EC2 には以下のような課題がありました & あります. (現在進行系の課題もあります)

  1. 常時起動する必要は無いので, 会社の始業と共に起動して, 終業と共に停止するようにしたい (自動起動, 自動停止)
  2. 誰が使っているのかをシュッと把握したい (利用者の把握)
  3. サーバーにログインしていなくても, サービスの稼働状態を確認出来るようにしたい
  4. サーバーにログインしていなくても, サービスの起動や停止を制御出来るようにしたい
  5. 必要に応じて, 利用したい人が新しい環境作ることが出来る

そして, 検証したい人が「検証したいだけなのに, サーバーのことを気にするのが辛い」というようなハードルを下げたいのと, 何より, 「俺が楽したい」という思いがあります.

ということで, これらの課題をどのように解決したのか, 解決しようとしているのかをざっくりとで恐縮ですがダンプしていきたいと思います.

施策

概要図

以下のような感じでやっています.

f:id:inokara:20190608152604p:plain

YAMAP における bot 文化

YAMAP は業務に Slack を利用しており, 業務インフラには無くてはならない存在になっています. そして, Slack には Bot (Hubot) が飼われていて, 社内の福利厚生であるシャッフルランチや掃除当番の抽選から Github と連携し CircleCI によるテストやデプロイの指示役等大活躍しております. この bot を仕込んで, 社内文化の一つとして作り上げてくれたのは @morygonzalez さんには本当に感謝です. ちなみに bot の名前は tengu 氏と呼びます.

portalshit.net

ということで, 既に bot 文化は根付いていることから, 少なくとも, エンジニアチームのメンバーは bot を操作することに対するアレルギーは無いという前提で色々と進めたいなーと考えています. 気軽に bot を呼び出して欲しいなあと.

自動起動, 自動停止

これは YAMAP にジョインした時点で既に自動起動, 自動停止の仕組みは構築されていました.

まず, 以下のように tengu 氏に呼びかけると, 指定した EC2 インスタンスが起動します. 停止も同様です. 当然, 以下のコマンドを叩くことで, 任意のタイミングでの起動や停止が可能になっています.

# tag:Name kensho01 を起動する
tengu ec2 start kensho01

# tag:Name kensho01 を停止する
tengu ec2 stop kensho01

自動起動や自動停止は, このコマンドを別の bot (cronbot 氏) に叩かせています. この cronbot 氏は, Slack の slash コマンド経由で呼び出すことで, 通常の crontab と同じ感覚で利用することが可能です.

# crontab -l と同等
/cron list

# 月曜日から金曜日までの 9 時に tengu ec2 start kensho01 を実行
/cron add 0 9 * * 1-5 tengu ec2 start kensho01

cronbot 氏は, 以下の OSS を利用させて頂いています.

github.com

ありがとうございます.

自動起動, 自動停止だけであれば, AWS Lambda を利用するという選択肢がありますが, 任意のタイミングでの起動や停止, 自動起動, 停止の時間をカジュアルに変更し辛いので, Slack のコマンドでちゃちゃっと出来る点は素晴らしいなあと思っています.

利用者の把握

前述の通り, 検証環境用 EC2 はエンジニアであれば誰 (エンジニアに限ってはいませんが) でも利用が可能です. ただ, 誰が利用しているかは, Slack のエンジニアチームチャンネルに問い合わせる必要がありました.

@here kensho01 を使いたいんですが, どなたか使っていますかー?

これでは非効率なので, EC2 のタグに利用者を登録する仕組みを追加しました. まずは, 誰 ( or どのチーム) が利用しているのかを確認する為に, 以下のように tengu 氏に呼びかけます.

tengu feature reservations?

下図のように出力されます.

f:id:inokara:20190608101527p:plain

Reserved By が空欄になっている為, 誰にも利用されていないということにしています. ここで, インスタンス kensho01 を利用したい場合には, 以下のように呼びかけます.

tengu kensho reserve 01 kappa

下図のように出力されます.

f:id:inokara:20190608102129p:plain

利用状態を解除したい場合には, 以下のように呼びかけます.

tengu kensho release 01

これらの仕組みは, 先述の通り, EC2 タグを利用していて, reserve を呼ぶと EC2 タグ reserved_by に第四引数 (上記の例だと kappa) を登録しているだけです. 同様に release を呼ぶと EC2 タグ reserved_by の値をクリアしています.

サービスの起動, 停止, 稼働状況の確認

AWS Systems Manager

ここから少し込み入った話になります.

検証環境用 EC2 上では素の Docker エンジンが動いていて, 各種サービスは docker-compose で管理されたコンテナで起動しています. これらのコンテナが起動しているか, また, コンテナ上のサービスが利用可能であるかインスタンスにログインせずに確認出来れば嬉しいのではと考えました. また, 確認だけでなく, コンテナの起動や停止も行えれば, 更に利便性も上げられそうです.

ということで, これらの要件を AWS Systems Manager を利用して実装してみました.

docs.aws.amazon.com

AWS Systems Manager とは, EC2 インスタンスだけではなく, オンプレミスサーバーや他の AWS リソースの設定を管理する機能を集めたもので, その中から AWS Systems Manager Run Command (以後, Run Command) を利用しました.

docs.aws.amazon.com

Run Command は簡単に言うと, EC2 やオンプレミスサーバーにインストールされている SSM エージェントを介して, ドキュメントと呼ばれる, 予め用意された (用意している) コマンドを実行してくれるサービスです.

Run Command (1)

検証環境用 EC2 インスタンスには, docker-compose up -d やコンテナ上で稼働するサービスの動作を確認する為のコマンドをまとめたスクリプトを用意しておきます. EC2 インスタンス再起動時に実行出来るように, init スクリプトとして用意しました.

#!/bin/sh
# chkconfig: 2345 99 10
# description: control containers for kensho environment

USERNAME=xxxxxx

function start_containers() {
  /sbin/runuser -l ${USERNAME} -s /bin/bash -c "
    eval $(aws ecr get-login --no-include-email --region ap-northeast-1)
    docker-compose pull
    docker-compose stop
    docker-compose up -d
  "
}

function get_kensho_service_status() {

... 略 ...

}

case "$1" in
  start)
    start_containers
    ;;
  stop)
    stop_containers
    stop_ssmweb
    ;;
  status)
    get_kensho_service_status
    ;;
  *) break ;;
esac

EC2 インスタンス上では, 以下のように実行されることを想定しています.

$ sudo service kensho-service status
===== Certificate Status
notBefore=Jun  4 02:46:05 2019 GMT
notAfter=Sep  2 02:46:05 2019 GMT

===== Containers Status
NAMES               CREATED             STATUS              IMAGE
container1       4 minutes ago       Up 4 minutes      image1
container2       4 minutes ago       Up 4 minutes     image2
conteiner3       4 minutes ago       Up 4 minutes     image3

===== API Status
HTTP/1.1 200 OK

===== Web Status
HTTP/1.1 200 OK

===== Batch Status
HTTP/1.1 200 OK

start では, コンテナの起動 docker-compose -d や, サービスを動かす為の各種初期設定が実行されます. stop では, 単純に docker-compose down が実行されます.

Run Command (2)

上記の init スクリプトを Run Command で実行します. 以下のようなシンプルなドキュメントを用意しました.

{
  "parameters": {
    "action": {
      "type": "String",
      "description": "(Required) Specify a action(start|stop|status).",
      "displayType": "textarea"
    }
  },
  "schemaVersion": "2.2",
  "description": "update kensho environment",
  "mainSteps": [
    {
      "action": "aws:runShellScript",
      "name": "control_kensho_env",
      "inputs": {
        "runCommand": [
          "service kensho-env {{ action }}"
        ]
      }
    }
  ]
}

あとは, hubot から Run Command でコマンドを実行する為の API を叩くだけ (厳密に言うと, IAM ポリシーを付与したりする必要はあります) です. API を叩く部分の実装は以下のような感じです.

// 略 ....
  const sendCommand = async (name, action) => {
    const targetInstance = await detectInstance(name)
    let result
    try {
      result = await ssm.sendCommand({ DocumentName: 'control_kensho_env', InstanceIds: [targetInstance.InstanceId], Parameters: { 'action': [ action ]} }).promise()
    } catch(e) {
      result = e
    }
    return result
  }

  const checkIfOperationFinished = async (name, commandId, channel) => {
    const targetInstance = await detectInstance(name);
    let message;
    let count = 0;
    while(true){
      result = await ssm.listCommandInvocations({ CommandId: commandId, InstanceId: targetInstance.InstanceId, Details: true }).promise()
      if (result.CommandInvocations.length != 0 || count > 20){
          if (result.CommandInvocations[0].StatusDetails != 'InProgress') {
            break;
          }
      }
      count++;
      sleep.sleep(3);
    }
    message = result.CommandInvocations[0].CommandPlugins[0].Output;
    robot.messageRoom(channel, "コマンドの実行結果です.\n```\n" + message + "\n```")
  }
// 略 ....

見よう見まねの実装なので, すごく汚いかもしれませんが, sendCommand してから listCommandInvocations をループでコールしてコマンドの終了を待ったりする部分は苦労しました.

あとは

以下のように tengu 氏に問い合わせます.

# kensho01 のサービス稼働状況を取得します
tengu kensho service status 01

# kensho01 のサービスを停止します
tengu kensho service stop 01

# kensho01 のサービスを起動します
tengu kensho service start 01

以下, 実際に tengu kensho service status 01 を実行している様子です.

f:id:inokara:20190608144423p:plain

startstop もこんな感じの出力になります.

これから

EC2 の起動や停止, サービスの起動, 停止や稼働確認はやりたいことが出来た気がしています. 次のステップとして, 必要に応じて, 検証環境用 EC2 インスタンスを chatops で手軽に増やしていけるような仕組みを考えたいと思います.

これは EC2 じゃなくても良いかなと. ECS だとクラスタを増やしていけば良いのかな. ECS や EKS 等, 既にベンダからは様々なソリューションが提供されています. これらをうまく組み合わせて, よりエンジニアの皆さんの利便性が上がるように, そして自分が楽になれるような仕組みを考えていければなあと考えています.

まとまっていないまとめ

ということで, YAMAP に入社してから半年が経とうとしていますが, こんな感じのことを色々とやっていますので, 今後共よろしくお願いいたします.

冒頭に書きましたが, chatops という言葉が夜に放たれてから 10 年以上の時を経て, 今更感は否めませんが, 業務効率向上の為, そして, 何よりもインフラ担当者が少しでも楽になるような仕組みはドンドコ導入していきたいし, 手を動かして行ければ (手を動かさなくても回るのがベスト) なあと考えています.