スクエニ ITエンジニア ブログ

[テスト編] GCPでVMインスタンスを自動・自律的に構築する仕組み

はじめに

このページは以下の記事シリーズのうち、自律的に構成したVMインスタンスのテストについて説明します。

テストの位置付け

テストと書くと内包される意味は多岐にわたりますが、ここでは構築後のテストを行うということで、特にAnsibleによるプロビジョニングの定義やmetadataを通じて外部から注入された設定値が正しく反映されているかの比較を行うことを目的と定めています。

テストのコンセプト

設定値として注入する値そのものが間違っている…という可能性やtypoなどもテストの範囲に含めたり、正常動作確認をしたい…などテストに様々な要件を載せてしまいたくなるのですが、さまざまな要件を載せてしまうと複雑怪奇な、メンテナンスが困難なものになる可能性があるため、不足気味でも良いのでシンプルなテストを徐々に育てていく方針で実装しています。

仮に運用中に不具合や問題が発生した場合は、テストケースの不足であるかどうかを評価し、テストケースの追加によって対処し次回以降は問題発生しないようにするというループを回せるようにしています。人が頑張って気づけることには限界がありますし、問題の再発防止策にチェックを気をつける/ダブルチェックを頑張ると書くのはツライので、あくまでサーバ構築における問題はテストケースの不足であることに帰着させる構造は重要だと考えています。

テストツールについて

ツールはOSSのgossを使用しています。

gossを選んだ理由は以下のようなものになります。

  • 全体的にシンプル
  • Go言語を元にしたツールであるため1つのバイナリファイルで実行可能であり配布性などが良い
  • text/templateのテンプレートエンジンなのでhelm等と共通した知識でメンテナンスできる

serverspec alternativeと開発者は呼称していますが、serverspecほど機能豊富というわけではありません。しかし、それが故に記述やできることが限られるためにシンプルなテスト記述を維持できるのではないかと考えています。

また、gossはserverspecと違いテスト対象のサーバ上でgossを実行する必要があります。概要編でも掲載した下記の図のフローではテストの際にgossをセットする手間などが発生することになるのですが、自律構築の場合は反対にサーバ上で実行することと相性が良く問題とはなりません。

(再掲)適宜エンジニアが構築作業をするフロー

テスト実行の概要

テストは以下のような図をコンセプトとして行われます。

gossのテストケースはyamlによって記述しますが、テストケースは以下の大きく2つの期待値をもとにサーバーの設定・状態をチェックする設計にしています。

  • metadataを通じて外部から動的に注入される設定値
  • Ansibleによってプロビジョニングされたプロセスやsystemd、サービスなどの状態
テストの概要

テストツールのファイル構成

gossのテストツールは以下のような配置で各サーバにapt installされるようになっています。

test-tool
├── bin
│  ├── goss # gossバイナリファイル
│  └── goss_test.sh # 実行時のwrapperスクリプト(後述)
└── test_files # テストケースや動的な値(vars.yaml)を生成するディレクトリ
   │   # gossfile, varsfile
   ├── goss.yaml
   ├── vars.yaml 
   │   # テストケースのファイル
   ├── mysql-common.yaml
   ├── mysql-master.yaml
   ├── mysql-slave.yaml
   ├── mysql-backup.yaml
   ├── mysql-relay.yaml
   └── bastion.yaml

gossの実行方法

我々の環境では以下のような実行方法になっています。

# goss.yamlに記述したテストファイルを暗黙的に読ませるためにテストファイル・ディレクトリへ移動
cd test_files

# 予めmetadataにlabelsというkeyでサーバの種別を表す文字列を設定しておく
# 例えばmysqlのmasterサーバである場合は"mysql:master"となる
role=$(curl -s -H "Metadata-Flavor: Google" "http://metadata.google.internal/computeMetadata/v1/instance/attributes/labels")

# roleに応じてgoss.yamlに実行するテストケースを出し分ける
if [ "$role" == "mysql:master" ]; then
  echo -n 'gossfile:
    mysql-common.yaml: {}
    mysql-master.yaml: {}
  ' > goss.yaml
elif [ "$role" == "mysql:slave" ]; then
  echo -n 'gossfile:
    mysql-common.yaml: {}
    mysql-slave.yaml: {}
  ' > goss.yaml
fi

# 処理は割愛しますがansbile_vars.yaml(先の自律構築編で取り上げたもの)の内容などから動的な値をvars.yamlに出力する処理
generate_vars 

# gossを実行する。goss.yamlは同一ディレクトリにあれば暗黙的に読み込まれる
# 標準出力にjson形式で結果が出力されるので、結果ファイルを受け取って後続処理で結果の判定処理を行う(後述)
../bin/goss --vars vars.yaml --format json > $result

goss.yamlは上記の例示のとおりですが、metadata.labelsmysql:masterという設定がされていれば以下のような記述になっています。 このファイル名のテストケースを順に実行する指示となるため、mysql-common.yaml, mysql-master.yamlの2つのファイルに記述されたテストが実行されることになります。

# test_files/goss.yaml
echo -n 'gossfile:
  mysql-common.yaml: {}
  mysql-master.yaml: {}

vars.yamlは上記のgenerate_varsの処理によって例として以下のような内容が動的に生成されます。

mysqlUsers:
  - name: backup
    pass: xxxxxxx
  - name: monitoring
    pass: xxxxxxx
  #省略

mysqlParams:
  - name: rpl_semi_sync_master_enabled
    value: 1
  - name: rpl_semi_sync_master_timeout
    value: 70000
  - name: rpl_semi_sync_slave_enabled
    value: 1
  #省略

テストケースの具体的な例

テストケースの一部を例として説明します。以下のような定義はAnsibleでプロビジョニングされた際にすべてのMySQLサーバで同様の設定が期待されるため、静的なテストケースが記述されています。内容としては 外部ディスクのマウントやファイルパーミッション、サービスプロセスの起動などをチェックするものになります。

# test_files/mysql-common.yaml
mount:
  /var/lib/mysql:
    exists: true
    source: /dev/sdb
    filesystem: xfs

file:
  /var/lib/mysql:
    exists: true
    mode: "0755"
    owner: mysql
    group: mysql
    filetype: directory

service:
  mysql:
    enabled: true
    running: true
  mysqld-exporter:
    enabled: true
    running: true
  node-exporter:
    enabled: true
    running: true

ユーザ定義とパスワードが正しいかのチェック

次にvars.yamlに動的に生成されたパラメータを使ったテストケースについては以下のような例になります。先に示したvars.yamlを用いたものとして例示します。

mysqlユーザのユーザ名とパスワードの対がvars.mysqlUsersに定義されているので、ループでmysql clinetコマンドでログインを試行しています。ログインに失敗するとコマンドのexit-statusがゼロ以外となり、exit-status: 0の結果と反するためテストが失敗します。

  {{- range $name, $pass := .Vars.mysqlUsers}}
  check_mysql_user_pass_valid_{{.name}}:
    exit-status: 0
    exec: |
      {{- if eq .name "monitoring" }}
      mysql -u{{.name}} -p{{.pass}} -h127.0.0.1 -e "quit"
      {{- else if eq .name "xxxxx" }}
      mysql -h$(dig $(hostname) +short | grep 10\.) -u{{.name}} -p{{.pass}} -e "quit"
      {{- else }}
      mysql -u{{.name}} -p{{.pass}} -e "quit"
      {{- end }}      
  {{end}}

mysqlのオンライン・パラメーターが正しいかどうかのチェック

以下はmy.cnfで定義された値がmysqlデーモンに正しく設定値として反映されているかをチェックする例です。

  {{- range $name, $value := .Vars.mysqlParams}}
  check_mysql_param_{{.name}}:
    exit-status: 0
    stdout:
    - {{.value}}
    exec: |
            mysql -usre -p$MYSQL_PASS_SRE --silent --skip-column-names -e "select @@{{.name}}" 2> /dev/null
  {{end}}

例えばvars.yamlの以下の値のような構造なので、.name = rpl_semi_sync_master_enabledのvariableの結果が .value = 1になっているかをチェックします。1以外の値であればテストが失敗します。

mysqlParams:
  - name: rpl_semi_sync_master_enabled
    value: 1

テストについて

上記が構築したMySQLサーバのテストの例になります。上記のようにyamlベースのgossのDSL記法によりテストケースを記述し、起動時や起動後の設定変更時などに実行をして正常性の確認意図した定義が反映されているかの確認を行うようにしています。サーバの定義はどんどん複雑・多様化していきますので、複数の人間が全てを理解して正しく運用することは困難になってきます。このため、運用においては「設定したらテストを実行する→そこでエラーになったら原因を調査する」という単純で認知負荷の低いサイクルを回せるようにすることが重要だと考えています。

まとめ: テスト編

以上でGCPでVMインスタンスを自動・自律的に構築する仕組みについて一通りの説明を終えました。サーバ運用については継続的な改善が行われているため、ここに記載した内容もいずれ置き換わっていく可能性もありあくまで一例という内容になってしまいます。しかしながら、大量のサーバを運用者の負担を軽減する手段としての考え方・工夫のエッセンスは大きく変わらないと考えています。これらを参考にみなさんの運用負荷の軽減のお役に立てると幸いです。最後までお読みいただいてありがとうございました。

この記事を書いた人

記事一覧
SQUARE ENIXでは一緒に働く仲間を募集しています!
興味をお持ちいただけたら、ぜひ採用情報ページもご覧下さい!