Network Endpoint でもヘルスチェックに失敗したらインスタンスを自動で再起動させる


この記事はGoogle Cloud Platform Advent Calendar 2020の4日目の記事です。
4日目の担当の方がなかなか投稿しないので雑に埋めにきました。
元々はCloud SQLについての記事が投稿されそうでしたが、筆者はRDBMSに関心がないので好きなことを書くことにします。

Network Endpoint Group(NEG)、使っていますか?
筆者はこれができてからUnmanaged Instance Groupではなく、NEGを利用してVMインスタンスをBackendService経由で利用するようになりました。
Unmanaged Instance Groupでも同じことはできますが、それでもNEGを利用しているのは、NEGはVMインスタンスだけではなく他のServerlessなサービスでも利用できて応用が利くからです。

一方で、NEGを利用することでVMインスタンスをGoogle Cloud Load Balancing(GCLB)から利用できますが、Managed Instance Group(MIG)とは異なり、ヘルスチェックに失敗した際にインスタンスを自動で再起動することができません。
この記事では、NEGをもっとproduction ready(?)に使っていくための工夫を紹介します。

Uptime checks, Notification channels, PubSub, Cloud Functions 辺りの単語でやることがわかった方は読む必要がありません。

全てのイベントはPubSubに通ずべし

基本的な考え方として、イベントをPubSubにPublishできたら何をやるにしてもほぼ勝利が確定します。3割くらい嘘かもしれません。
ここでは、Cloud MonitoringのUptime checksを利用してインスタンスを監視し、ヘルスチェックに失敗したらイベントをPubSubに通知し、Cloud Functionsで拾って該当のインスタンスをRESETすることを目指します。

下準備

VMインスタンスにアプリケーションを用意する

Container Optimized OS(COS)に単一のコンテナをデプロイするだけでは、何も工夫しなくても勝手に再起動してくれやがるので、ここではWordpressとMySQLの2つのコンテナをdocker composeで起動することにします。
使用するcomposeの定義は以下のもので試します。
簡便のため、データの永続化は無視します。
めんどくさい人は適当なOSにWEBサーバでもインストールしてsystemctl enableでもしてください。

wordpress.yaml
version: "3"

services:
  wordpress:
    hostname: wordpress
    image: wordpress:5-php7.2-apache
    ports:
      - 80:80
    environment:
      WORDPRESS_DB_HOST: mysql
      WORDPRESS_DB_USER: wordpress
      WORDPRESS_DB_PASSWORD: password
      WORDPRESS_DB_NAME: wordpress
      WORDPRESS_TABLE_PREFIX: wp_
  mysql:
    hostname: mysql
    image: mysql:5
    environment:
      MYSQL_DATABASE: wordpress
      MYSQL_USER: wordpress
      MYSQL_PASSWORD: password
      MYSQL_RANDOM_ROOT_PASSWORD: "yes"

このファイルを、COSのホストOSでwritableな領域に保存します。
ここでは /var/wordpress.yaml に保存します。

続いて、COSでdocker composeを利用するために、live restoreを無効にしてdockerを再起動します。

$ sudo sed -i 's/true/false/g' /etc/docker/daemon.json

$ sudo systemctl restart docker 

あとはいつも通りです。

$ docker swarm init
...

$ docker stack deploy -c /var/wordpress.yaml wordpress
Creating network wordpress_default
Creating service wordpress_wordpress
Creating service wordpress_mysql

$ docker stack services wordpress
ID                  NAME                  MODE                REPLICAS            IMAGE                       PORTS
h4tnwbuvamru        wordpress_mysql       replicated          1/1                 mysql:5
ugg0mpp6sqs5        wordpress_wordpress   replicated          1/1                 wordpress:5-php7.2-apache   *:80->80/tcp

$ curl -I localhost/readme.html
HTTP/1.1 200 OK
Date: Wed, 09 Dec 2020 14:26:06 GMT
Server: Apache/2.4.38 (Debian)
Last-Modified: Fri, 26 Jun 2020 13:58:02 GMT
ETag: "1c6e-5a8fd18ee0e80"
Accept-Ranges: bytes
Content-Length: 7278
Vary: Accept-Encoding
Content-Type: text/html

ところで、 /etc/docker/daemon.json はインスタンスが再起動するたびに元の状態に戻ってしまうので、インスタンスのスタートアップスクリプトで書き換えるようにしてしまいましょう。
具体的にはインスタンスのメタデータ startup-scriptに以下のように書き込んでください。


sed -i 's/true/false/g' /etc/docker/daemon.json &&\
docker stack deploy -c /var/wordpress.yaml wordpress

これでインスタンスの準備完了です。

Network Endpoint Groupを作成してインスタンスを紐付ける

ここからはコマンドラインでいきます。
ネットワークなどは適宜置き換えてください。
zoneはインスタンスのzoneと同じものを指定してください。ここではasia-northeast1-bとします。

NEGを作成する

$ gcloud compute network-endpoint-groups create wordpress \
  --zone=asia-northeast1-b \
  --subnet=default \
  --network-endpoint-type=GCE_VM_IP_PORT \
  --default-port=80

Network Endpointを作成する

$ gcloud compute network-endpoint-groups update wordpress \
  --zone=asia-northeast1-b \
  --add-endpoint=instance=<インスタンス名>,ip=<インスタンス内部IP>,port=80

GCLBを起動する

コマンドラインでやるとすごく長いのでやめます。
ポイントは以下の点です。
* Zonal Network Endpoint Group wordpressをBackend Serviceとして利用する
* ヘルスチェックのプロトコルをHTTP, Pathを/readme.html にする。

ヘルスチェックに成功し、GCLBのIPアドレスでWordpressの初期設定画面が出ることを確認してください。

PubSub Topic, Uptime checks, Notification channelを作成する

インスタンスのヘルスチェックを行うためのUptime checks
Uptime checksに失敗したときに通知を飛ばすNotification channel
通知先となるPubSub Topicを作成していきます。

PubSub Topic

$ gcloud pubsub topics create notification

Notification Channel

上で作成したtopicをフルネームで指定します。

Uptime Checks

インスタンスに対してHTTPリクエストでチェックします。

必要なIAMを設定する

MonitoringのService AccountがPubSub Topic notification へメッセージをPublishするために、役割を設定します。
ドキュメントによると

最初の Pub/Sub チャネルを作成すると、Cloud Monitoring は、チャネルが作成されたプロジェクトの Monitoring Notification Service Agent 用のサービス アカウントを作成します。

とあるのですが、一向に出来上がらないです。
しかし、出来上がるのを見たことはあるので、この辺がまだβなのかもしれません。
production readyとは一体...ウゴゴゴ。

Cloud Functionsで自動再起動する

かなりやっつけですがnodejsでコードを書いてきました。
PubSubのメッセージから該当インスタンスのzoneなどを取得してresetをします。
resetされたインスタンスは、startup-scriptによって必要なコンテナを起動します。

index.js
const Compute = require('@google-cloud/compute');
const compute = new Compute();

exports.resetInstance = async (event, context, callback) => {
  const data = JSON.parse(Buffer.from(event.data, 'base64').toString());
  const vm   = await getVm(data);

  try {
    await vm.reset();
  } catch (err) {
    console.error(err);
    callback(err);
  }
}

async function getVm(data) {
  const zone       = compute.zone(data.incident.resource.labels.zone);
  const vmname     = data.incident.resource_display_name;
  const vminstance = zone.vm(vmname);

  return vminstance;
}
package.json
{
  "name": "instance-auto-restarter",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "dependencies": {
    "@google-cloud/compute": "^2.4.0"
  }
}

あとはこいつをデプロイして、コンテナを止めてUptime checksに失敗させたかったのですが、
自動で作成されるはずのサービスアカウントが作成されず、動作を試せません。

サービスアカウントが作成され次第、覚えていたら追記したいと思います。