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

[サーバ構築編] GCPでVMインスタンスを自律的に構築する仕組み

はじめに

このページは以下の記事シリーズのうち、VMインスタンスを自律的に構築する方法について説明しています。

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

ここでは概要編でも貼った以下の画像の中で赤枠の部分にフォーカスを当ててご紹介します。ここではMySQLサーバの構築を例にとっています。

コンセプト

自律構築の流れ

自律構築は以下のような流れになっています。SREがTerraformを用いて適切なパラメータを付与してVMインスタンスを起動すると、それらのパラメータに従って運用可能な状態まで人手を介さずに自動的にセットアップが実行されます。

  1. TerraformによりMySQLカスタムイメージを指定してVMインスタンスが起動。この際にVMインスタンスのmetadataに後の自動構成時に使用されるパラメータを併せて定義します。
  2. VMインスタンスの初回起動時にcloud-initが実行されるuser-dataを独自に指定することで起動処理をカスタマイズ可能で、user-dataはmetadataを介してサーバに読み込まれます。
  3. user-dataに記述された処理に従って設定データの生成やAnsibleタスクが実行されます。

コンセプト

MySQLベースイメージのビルド

前述1.のMySQLカスタムイメージは事前にPackerによって図のようなビルドが行われるようになっています。
VMイメージ継続ビルド編で記載したようにまずは共通ベースイメージがビルドされ、保存された共通ベースイメージから起動したインスタンスを基にしてMySQLの共通のベースイメージがビルドされ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=-1000usr/lib/systemd/system/mysql.serviceに記述
  • 高負荷に耐えうるMySQLサーバ向けのカーネルパラメーターの変更
    • 変更値を記載した /etc/sysctl.conf の配置
  • mysqld関連のセットアップ
    • mysqld停止・自動起動の無効化(systemctl: is-enabled=disabled)
    • 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 %}

#省略

処理の流れのまとめ

いろんな例示が続いたため、どんな処理をしているかの流れが見えづらくなってしまったので再度流れを列記してまとめたいと思います。

  1. Terraform: MySQLのベースイメージからVMインスタンスが起動される
  2. VM metadataのuser-dataでカスタマイズして記載された内容で初回起動時の処理がcloud-initにより実行(runcmd)される
  3. cloud-init runcmd: metadataサーバからansible_variables.yamlのテキストデータを取得する
  4. cloud-init runcmd: GCP Secret ManagerからMySQLパスワードデータを取得してansible_variables.yamlのパスワードプレースホルダを上書きする
  5. cloud-init runcmd: ansible_varialbes.yamlextra-varsとしてansible playbookを実行する(initial_setup or restore_setup)
  6. Ansible: my.cnfのレンダリングなどのサーバ固有のセットアップ処理や初期化処理ではGRANTの実行を行う
  7. 以上で運用可能な状態のMySQLサーバが構築された状態になる

まとめ: サーバ自律構築編

ここまでお読みいただいてありがとうございます。このページではMySQLサーバを例に自律構築を行う構成について説明をしました。次は最後にテストについて取り上げます。

この記事を書いた人

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