[サーバ構築編] GCPでVMインスタンスを自律的に構築する仕組み
はじめに
このページは以下の記事シリーズのうち、VMインスタンスを自律的に構築する方法について説明しています。
- 概要編
- VMイメージ継続ビルド編
- サーバ自律構築編(このページ)
- テスト編
GCPでVMインスタンスを自動・自律的に構築する仕組み
ここでは概要編でも貼った以下の画像の中で赤枠の部分にフォーカスを当ててご紹介します。ここではMySQLサーバの構築を例にとっています。
自律構築の流れ
自律構築は以下のような流れになっています。SREがTerraformを用いて適切なパラメータを付与してVMインスタンスを起動すると、それらのパラメータに従って運用可能な状態まで人手を介さずに自動的にセットアップが実行されます。
- TerraformによりMySQLカスタムイメージを指定してVMインスタンスが起動。この際にVMインスタンスのmetadataに後の自動構成時に使用されるパラメータを併せて定義します。
- VMインスタンスの初回起動時にcloud-initが実行される。user-dataを独自に指定することで起動処理をカスタマイズ可能で、user-dataはmetadataを介してサーバに読み込まれます。
- user-dataに記述された処理に従って設定データの生成やAnsibleタスクが実行されます。
MySQLベースイメージのビルド
前述1.のMySQLカスタムイメージは事前にPackerによって図のようなビルドが行われるようになっています。
VMイメージ継続ビルド編で記載したようにまずは共通ベースイメージがビルドされ、保存された共通ベースイメージから起動したインスタンスを基にしてMySQLの共通のベースイメージがビルドされMySQLベースイメージとして保存されます。
github actions workflowでの呼び出し方は以下のようにmysqlに関わる定義の部分のみが違う形になっています。
jobs:
build:
#省略
steps:
#省略
- name: set base image configurations to GITHUB_ENV
- run: . ./script/set_env.sh base
+ run: . ./script/set_env.sh mysql
working-directory: packer
- name: Packer build image
- run: packer build base.pkr.hcl
+ run: packer build mysql80.pkr.hcl
working-directory: packer/source
また、set_env.shにより動的に生成されるイメージ名も以下の部分が差し替わった上でpacker buildが実行されるようになっています。
#!/bin/bash
# source iamge名: base.pkr.hclで継続的にビルドされている共通ベースイメージの保存時のイメージ名がセットされる
# base.pkr.hclでは"ubuntu-2004-focal-v20220308"がセットされていた箇所
echo PKR_VAR_source_image=custom-ubuntu2004-base-vYYYYMMDD-01 >> $GITHUB_ENV
# カスタムイメージ名: MySQLのベースイメージ名として保存する名前をセット
echo PKR_VAR_image_name=custom-ubuntu2004-mysql-vYYYYMMDD-01 >> $GITHUB_ENV
# family名: MySQLのベースイメージのFamily名をセット
echo PKR_VAR_image_family=custom-ubuntu2004-mysql >> $GITHUB_ENV
MySQLベースイメージのプロビジョニング
MySQLのプロビジョニングはmysql.pkr.hcl
中で以下のようにMySQLベースイメージをプロビジョニングするAnsibleタスクが実行されることで行われます。
# packer/source/mysql.pkr.hcl
locals {} # 共通ベースイメージのビルドと同じなので省略
source "googlecompute" "mysql" {
# 共通ベースイメージのビルドと同じなので省略
# ここの定義により共通ベースイメージをsource_imageとしてVMが起動する
}
build {
sources = ["source.googlecompute.mysql"] # 共通ベースイメージで起動した上記computeインスタンスを対象に指定する
provisioner "ansible" {
groups = ["mysql_base"]
playbook_file = "../../ansible/mysql.yml" # mysql baseイメージを実行するplaybookを呼び出す
inventory_directory = "../../ansible/inventory"
user = "sre"
}
}
MySQLのベースイメージのセットアップ内容
上記の../../ansible/mysql.yml
のansible playbookでは以下のようなことを実行しています。
- mysql client, server, librariesのインストールとバージョンのhold
- 運用中のapt install等の実行で意図しないmysqlのupdateが発生しないようにholdしています
- mysqldのOOMスコアを下げる定義
OOMScoreAdjust=-1000
をusr/lib/systemd/system/mysql.service
に記述
- 高負荷に耐えうるMySQLサーバ向けのカーネルパラメーターの変更
- 変更値を記載した /etc/sysctl.conf の配置
- mysqld関連のセットアップ
- mysqld停止・自動起動の無効化(
systemctl: is-enabled=disabled
) - mysqldの初期化処理やディレクトリ作成など
- mysqld停止・自動起動の無効化(
- 運用ツール関連の配備
- python運用スクリプトが使用するモジュールのpip install
Terraformによるインスタンス起動
上記まででMySQLベースイメージをカスタムイメージとしてビルドしました。これを用いてTerraformによってサーバを起動します(図中の①)
。前置きが長くなりましたので、全体の流れについては改めて最後にまとめを掲載します。
自律構築部分の詳細
先述の画像の①〜③をより細かくしたものが次の図になります。定義ファイルも交えて少し細かな動作を説明します。
①Terraformのインスタンス定義
起動時のTerraformのHCLは以下のような記述になります。
module "gce" {
# yamlに記述されたサーバ情報をもとにインスタンス定義を展開
for_each = { for v in local.configs.mysqls : v.name => v }
source = "../../../modules/compute/gce"
name = each.value.name
machine_type = each.value.machine_type
#省略 project, network, tag等
# 上記のdataリソースの定義によって、each.value.imageがcustom-ubuntu2004-mysqlと定義されていれば
# 該当familyのイメージのself_linkが取得される(後述)
image = data.google_compute_image.main[each.value.image].self_link
# この仕組みのキーポイント
metadata = merge(
local.default_metadata, # 共通のmetadata定義(内容は省略)
each.value.metadata, # サーバ固有で指定されたmetadata(内容は省略)
# cloud-initを上書きするuser-data.yamlを生成する(後述)
{
user-data = templatefile("../../../templates/mysql/user-data.yaml.tpl",
{
mysql_is_restored = each.value.mysql.is_restored,
mysql_is_innodb_cluster = each.value.mysql.is_innodb_cluster
},
),
},
# Ansible実行時の各種パラメータを注入するためのyamlファイルを生成する(後述)
{
ansible_variables = templatefile("templates/mysql/ansible_variables.yaml.tpl",
{
mysql_env = each.value.mysql.env,
mysql_repl = each.value.mysql.repl,
mysql_sync_binlog = each.value.mysql.sync_binlog,
mysql_innodb_flush_log_at_trx_commit = each.value.mysql.innodb_flush_log_at_trx_commit,
mysql_semi_sync = lookup(each.value.mysql, "semi_sync", null),
mysql_innodb_buffer_pool_size_percent = each.value.mysql.innodb_buffer_pool_size_percent,
mysql_innodb_buffer_pool_size = lookup(each.value.mysql, "innodb_buffer_pool_size", null),
mysql_innodb_buffer_pool_instances = each.value.mysql.innodb_buffer_pool_instances,
#省略
}
)
},
)
# ディスク定義等
attached_disks = each.value.attached_disks
boot_disk_size = lookup(each.value, "boot_disk_size", null)
deletion_protection = lookup(each.value, "deletion_protection", true)
allow_stopping_for_update = lookup(each.value, "allow_stopping_for_update", false)
}
構築するMySQLサーバは以下のようなyaml形式で定義されています。
なお、このyamlファイルのメンテナンスについても解説が必要なのですが、内容が多いため今回の投稿からは割愛しました。後日Cloud Operator Days Tokyo 2023にて本件とともに取り上げる予定ですので、その際にはご確認いただけると幸いです。
# configs/dev/mysqls.yaml
- name: dev-mysql-xxxx
machine_type: n2-standard-8
image: custom-ubuntu2004-mysql
#省略
- name: dev-mysql-yyyy
machine_type: n2-standard-8
...
localsで以下のように定義を行い、先に記載したTerraform定義のmodule "gce"
においてfor_each = { for v in local.configs.mysqls : v.name => v }
の記述で利用されています。
locals {
configs = {
mysqls = yamldecode(file("../../../configs/dev/mysqls.yaml"))
}
}
先に記載したmysqls.yaml
ではiamge: custom-ubuntu2004-mysql
という起動時のイメージを指定するフィールドがあります。これはgoogle_compute_image
データリソースを以下のように定義することで、data.google_compute_image.main["custom-ubuntu2004-mysql"]
というイメージリソースを定義・利用することができるようになります。
locals = {
images = [
"costom-ubuntu2004-mysql",
]
}
# 該当するイメージfamily名の最新イメージがデータリソースとして定義される
data "google_compute_image" "main" {
for_each = toset(local.images)
family = each.value
project = "xxxx" # カスタムイメージを共有しているプロジェクト名を指定
}
②サーバ起動後のcloud-initの初期化処理
①のTerraform定義のようにuser-data
というkeyに対してTerraformのbuilt-in関数であるtemplatefile(filename)
によってレンダリングを行いながらmetadataにセットされます。
metadata = {
user-data = templatefile("../../../templates/mysql/user-data.yaml.tpl",
{
mysql_is_restored = each.value.mysql.is_restored,
mysql_is_innodb_cluster = each.value.mysql.is_innodb_cluster
},
)
}
上記でレンダリング対象に指定されているファイル(user-data.yaml.tpl
は以下のような記述がされています。 %{ ... }
の部分がtemplatefile
関数によって値がレンダリングされ、yamlファイルのデータとしてmetadataサーバ上に保管されます。
# templates/dev/user-data.yaml.tpl
#cloud-config
timezone: Asia/Tokyo
locale: C.UTF-8
package_update: false
package_upgrade: false
# 実行時に実行される(contentの内容が指定のpathに書き込まれる)
write_files:
- path: /var/lib/toolbox/scripts/get_metadata.sh
permissions: "0770"
owner: "sre:sre"
content: |
#!/bin/bash
attr="http://metadata.google.internal/computeMetadata/v1/instance/attributes"
curl -s -f -H "Metadata-Flavor: Google" $attr/$1
# 以下に記載のコマンドが逐次実行される
runcmd:
# 自作の運用ツール・スクリプトをdeb package化したものをインストール
# 以下で呼び出されている/var/lib/toolbox/scripts/xxxの最新のものが都度installされる
- apt install -y our-mysql-utils
# IaCリポジトリをshallow cloneするスクリプトを実行する
- /bin/bash /var/lib/toolbox/scripts/clone_from_git.sh our_iac_repo
# metadataからTerraformによりmetadataとして注入されたansible_variablesを取得する
- /var/lib/toolbox/scripts/get_metadata.sh ansible_variables > /var/lib/toolbox/ansible_variables.yaml
# ansible_variablesのusers.xxx.passwordに記載されている置換文字列に該当するパスワードを
# GCP Secret Managerから取得して置換するpython scriptを実行する
- python /var/lib/toolbox/scripts/get_mysql_password.py
# 以下はtemplatefile関数を通じてレンダリングされた状態でmetadataにセットされる
# リストアフラグの場合はリストア用のansible playbookを実行する
%{~ if mysql_is_restored ~}
- ansible-playbook /var/lib/toolbox/our_iac_repo/ansible/mysql_restore_setup.yml --extra-vars="@/var/lib/toolbox/ansible_variables.yaml"
# 通常(新規構築時)は初期セットアップを行うansible playbookを実行する
%{~ else ~}
- ansible-playbook /var/lib/toolbox/our_iac_repo/ansible/mysql_initial_setup.yml --extra-vars="@/var/lib/toolbox/ansible_variables.yaml"
%{~ endif }
③Ansible定義のgitリポジトリをcloneする
Ansibleの定義データを配置しているgitリポジトリをshallow cloneしています。本テーマからは外れるため詳細は割愛しますが、GithubサーバのOAuth認証用のcredential情報をSecretManagerから取得し認証トークンを生成、そのトークンを用いてgit cloneを行うようにしています。
④ansible_variables.yamlの取得
起動されたMySQLサーバ上で以下のようにmetadataサーバに対してリクエストを行うと、metadataのkeyにansible_variables
を指定してセットしたテキストファイルのデータがレスポンスとして取得することができます。
curl -H "Metadata-Flavor: Google" \
"http://metadata.google.internal/computeMetadata/v1/instance/attributes/ansible_variables" > /var/lib/toolbox/ansible_variables.yaml
metadataサーバにセットされる前のテンプレートファイルの状態のansible_variablesのテキストデータを以下に示します。${ ... }
や%{ ... }
の部分はTerraformのtemplatefile関数
でレンダリングされた状態でmetadataにセットされます。これらの値を用いてansibleがmysqlの定義ファイルであるmy.cnf
を生成・更新したりGRANT
の定義を行ったりします。
# templates/mysql/ansible_variables.yaml.tpl
mysql:
env: ${ mysql_env }
repl: ${ mysql_repl }
sync_binlog: ${ mysql_sync_binlog }
innodb_flush_log_at_trx_commit: ${ mysql_innodb_flush_log_at_trx_commit }
%{~ if mysql_semi_sync != null ~}
semi_sync: ${ mysql_semi_sync }
%{~ endif ~}
innodb_buffer_pool_size_percent: ${ mysql_innodb_buffer_pool_size_percent }
%{ if mysql_innodb_buffer_pool_size != null }
innodb_buffer_pool_size: ${ mysql_innodb_buffer_pool_size }
%{ endif }
innodb_buffer_pool_instances: ${ mysql_innodb_buffer_pool_instances }
#省略
# rootや一般ユーザをansible.mysql_userプラグインによって設定するための定義
# __MYSQL_XXX__ という部分はget_mysql_password.pyという運用ツールにより置換するための指示子(後述)
root_users:
- host: "localhost"
user: "root"
priv:
"*.*": "ALL,GRANT"
encrypted: no
password: __MYSQL_ROOT_PASSWORD__
users:
# 運用系のものを例示でピックアップして表示
# アプリケーション接続等のGRANT定義もここに含まれますが割愛しています
- host: "%"
user: "slave"
priv:
"*.*": "REPLICATION SLAVE"
encrypted: no
password: __MYSQL_SLAVE_PASSWORD__
- host: "127.0.0.1"
user: "monitoring"
priv:
"*.*": "PROCESS,REPLICATION CLIENT,SELECT"
encrypted: no
password: __MYSQL_MONITORING_PASSWORD__
- host: "localhost"
user: "backup"
priv:
"*.*": "SUPER,REPLICATION CLIENT,SELECT"
encrypted: no
password: __MYSQL_BACKUP_PASSWORD__
- host: "%"
user: "sre_ro"
priv:
"*.*": "SELECT,PROCESS,REPLICATION CLIENT,SHOW DATABASES,SHOW VIEW"
encrypted: no
password: __MYSQL_SRE_RO_PASSWORD__
④ansbile_variables.yaml上でMySQLパスワードの置換処理
上記のansible_variables.yamlで__MYSQL_XX__
となっている場所は適切なパスワードに置換するためのプレースホルダになっています。
我々の環境ではSecretServerとしてGCP Secret Managerにパスワード情報を保管しています。secret managerから取得して該当するプレースホルダを置換するスクリプトを実行しています。
⑤ansible playbookの実行
最後にansible-playbookコマンドを以下のように実行して初期セットアップのタスクを実行します。
ansible-playbook \
/var/lib/toolbox/our_iac_repo/ansible/mysql80_idbc_initial_setup.yml \
--extra-vars="@/var/lib/toolbox/ansible_variables.yaml"
なお、サーバ毎にis_restored
というフィールドを持たせており、Terraformでレンダリングを行う際にis_restored = True
の場合はリストアを行うmysql_restore_setup.yml
というplaybookが実行され、False
の場合はmysql_initial_setup.yml
というplaybookが実行されることで、リストアと初期化設定が分岐実行されるようになっています。
初期化設定(initial_setup)とリストア(restore_setup)の違いはMySQLデータディレクトリ(/var/lib/mysql)の扱いが異なります。初期セットアップは新規外部ディスクを初期化・マウントしinitialize処理とユーザ設定等を行います。リストア処理ではsnapshotイメージを外部ディスクとして接続されているため、当該ディスクをマウントするだけの動作になっています。
# templates/mysql/user-data.yaml.tpl
%{~ if mysql_is_restored ~}
- ansible-playbook /var/lib/toolbox/our_iac_repo/ansible/mysql_restore_setup.yml --extra-vars="@/var/lib/toolbox/ansible_variables.yaml"
%{~ else ~}
- ansible-playbook /var/lib/toolbox/our_iac_repo/ansible/mysql_initial_setup.yml --extra-vars="@/var/lib/toolbox/ansible_variables.yaml"
%{~ endif }
mysqlサーバのタイプによる設定の分岐
例として以下のようなmy.cnf
のjinja templateファイルでは、ansible_variables.yaml
内のmysql.repl
の値によって分岐してレンダリングされるようになっています。
# ansible/roles/mysql_initial_setup/files/my.cnf.j2
[client]
port=3306
socket=/var/run/mysqld/mysqld.sock
[mysqld]
server-id={{ mysql_server_id }}
user=mysql
port=3306
socket=/var/run/mysqld/mysqld.sock
datadir=/var/lib/mysql
#省略
# mysql.replの値によってレンダリングを分岐させる記述
{% if mysql.repl == "master" %}
log-bin=/var/lib/mysql/mysql-bin
sync_binlog={{ mysql.sync_binlog }}
#省略
{% elif mysql.repl in ["relay", "slave", "dr"] %}
log-bin=/var/lib/mysql/mysql-bin
sync_binlog={{ mysql.sync_binlog }}
log-slave-updates
read_only
#省略
{% elif mysql.repl in ["backup", "xxx"] %}
read_only
slave_net_timeout=60
#省略
{% endif %}
#省略
処理の流れのまとめ
いろんな例示が続いたため、どんな処理をしているかの流れが見えづらくなってしまったので再度流れを列記してまとめたいと思います。
- Terraform: MySQLのベースイメージからVMインスタンスが起動される
- VM metadataの
user-data
でカスタマイズして記載された内容で初回起動時の処理がcloud-initにより実行(runcmd)される - cloud-init runcmd: metadataサーバから
ansible_variables.yaml
のテキストデータを取得する - cloud-init runcmd: GCP Secret ManagerからMySQLパスワードデータを取得してansible_variables.yamlのパスワードプレースホルダを上書きする
- cloud-init runcmd:
ansible_varialbes.yaml
をextra-vars
としてansible playbookを実行する(initial_setup or restore_setup) - Ansible:
my.cnf
のレンダリングなどのサーバ固有のセットアップ処理や初期化処理ではGRANT
の実行を行う - 以上で運用可能な状態のMySQLサーバが構築された状態になる
まとめ: サーバ自律構築編
ここまでお読みいただいてありがとうございます。このページではMySQLサーバを例に自律構築を行う構成について説明をしました。次は最後にテストについて取り上げます。
- 概要編
- VMイメージ継続ビルド編
- サーバ自律構築編(このページ)
- テスト編