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

[VM継続ビルド編] GCPでVMインスタンスを自動・自律的に構築する仕組み

はじめに

このページは以下の記事シリーズのうち、VMイメージの継続ビルドについて説明しています。

VMイメージ継続ビルド

運用しているシステム向けのVMイメージの継続ビルドにはPackerを用いています。ビルドの仕組みの概観は以下のようになります。

  1. Github Enterprise Server(GHE)のActionsを起点として動作する
  2. CIサーバ(Actions self-hosted runner)がPackerのビルド・ジョブを実行
  3. Packerにより起動されたサーバに対してAnsibleによるプロビジョニングが実行される
  4. プロビジョニング済のサーバを停止・削除しながらカスタムイメージとして保存する
Packerビルドの概観

ファイル構成

この仕組みのファイル構成は以下のようなツリー構造になっています。AnsibleはPackerからだけ呼び出されるわけではありませんので、それぞれ独立したディレクトリに配置しています。

# project root
.
├── packer
│  ├── config
│  │  └── build_env.yaml
│  ├── script
│  │  └── set_env.sh
│  └── source
│     ├── base.pkr.hcl
│     └── mysql.pkr.hcl
│     └── redis.pkr.hcl
│     └── memcached.pkr.hcl
├── ansible
│  ├── ansible.cfg
│  ├── base.yml
│  ├── group_vars
│  │   ├── base.yml
│  │   ├── mysql
│  │   └── ...
│  ├── inventory
│  └── roles
│     ├── base
│     │  ├── files
│     │  └── tasks
│     │     └── main.yml
│     ├── mysql
│     └── ...

ビルドワークフローの例示

Github Actionsでベースイメージをビルドするワークフローのサンプルを以下に示します。

このワークフロー定義では以下の2種類のトリガーが定義されています。

  • 毎週月曜日の日本時間午前9:15(workflowのTZはUTC)に定期実行
  • もしくはmainブランチへのpushをトリガーに実行

また、on.push.pathsのファイルに該当したファイルが変更されいる場合のみとすることで関係のないファイル更新時でのビルド実行を防止しています。 なお、on.pull_request.branches.mainでPullRequest作成時にテストビルドを行う定義が別に存在しますが、ここでは掲示を割愛します。

name: packer build base image

on:
  # 定期実行のトリガー
  schedule:
    - cron: "15 0 * * 1"
  # 変更時のトリガー
  push:
    branches:
      - main
    # ファイル単位でのトリガー
    paths:
      - .github/workflows/packer_build_base.yaml
      - packer/source/base.pkr.hcl
      - ansible/base.yml
      - ansible/group_vars/**/*
      - ansible/roles/base/**/*

jobs:
  build:
    # self-hosted runnerで実行するタグ(実際は他にもタグがあり実行ホストを選別しています)
    runs-on:
      - self-hosted

    steps:
      - name: Checkout Repository
        uses: actions/checkout@v2
        with:
          fetch-depth: 0

      # 動的なパラメータの生成と環境変数への保存(次のstepへ値を引き回す為)
      - name: set base image configurations to GITHUB_ENV
        run: . ./script/set_env.sh base
        working-directory: packer

      # packerのビルド・ジョブ実行
      - name: Packer build image
        run: packer build base.pkr.hcl
        working-directory: packer/source

base.pkr.hclのサンプル

base.pkr.hclは大きく以下の2つのセクションに分かれます。なお、ここではコンフィグファイルの記述はHCLを使用しています。

  • locals{}, variable{}セクション

    • HCLの変数定義セクション(Terraformを利用されている方はおなじみかと思います)
    • build_env.yamlに共通パラメータを構造データとして定義。configs = yamldecode(file(../config/build_env.yaml))とすることでconfigsにロード
    • variableの値はset_env.shで生成される環境変数の値(後述)を受け取るために定義
  • source {}セクション

    • 一時的に起動するサーバやプロビジョニングが成功した場合のカスタムイメージの名前などを指定
    • 静的な値はbuild_env.yamlにまとめて記述してありlocals.configsに構造データとして読み込まれる
    • variablesで定義されている変数はPKR_VAR_source_imageなどのようにprefixを付与した環境変数からvar.source_imageとして読み込まれる
  • build {}セクション

    • 上記sourceセクションで定義されたサーバに対してansibleを実行してプロビジョニングを行う
    • 実行するAnsible Roleや付随するパラメータを指定
# source/base.pkr.hcl

# プロジェクト名やネットワーク名など複数のビルドタスクで共通の静的な値として指定する値の定義
# 以下で"動的な値"とコメントされていない部分はすべて静的な値でlocal.configs.xxxxと呼び出されています。
locals {
  configs = yamldecode(file("../config/build_env.yaml"))
}

# 環境変数:PKR_VAR_source_imageで指定した値を受け取る定義
variable "source_image" {
  type = string
}

# 環境変数:PKR_VAR_image_nameで指定した値を受け取る定義
variable "image_name" {
  type = string
}

# 環境変数:PKR_VAR_image_familyで指定した値を受け取る定義
variable "image_family" {
  type = string
}

# Packerのビルド・ジョブの定義
source "googlecompute" "base" {
  # 基礎情報(プロジェクト名、ネットワーク、zoneなど)
  project_id       = local.configs.project_id
  network          = local.configs.network
  subnetwork       = local.configs.subnet
  zone             = local.configs.zone

  # ベースとなるイメージ名(環境変数で動的な値を指定/後述)
  source_image     = var.source_image

  # Packerが一時的に起動するインスタンスの情報
  disk_size        = local.configs.disk_size
  preemptible      = true
  omit_external_ip = true
  use_internal_ip  = true
  ssh_username     = "packer"
  tags             = local.configs.tags # GCPのネットワークタグでActions self-hosted-runnerからSSHを可能とするように定義します。
  scopes           = ["https://www.googleapis.com/auth/cloud-platform"]

  # カスタムイメージのイメージ名・ファミリー名(環境変数で動的な値を指定/後述)
  image_name       = var.image_name
  image_family     = var.image_family

  # 構築時に生成されるキーペアの公開鍵を削除する
  ssh_clear_authorized_keys = true

  service_account_email = "xxxx@dummy-project-name.iam.gserviceaccount.com"
}

# sourceセクションで定義されたサーバに対してansibleを実行してプロビジョニングを行う定義
build {
  sources = ["source.googlecompute.base"]

  provisioner "ansible" {
    groups        = ["base"] # ansible/group_vars/base.ymlの変数を利用
    playbook_file = "../../ansible/base.yml"
    inventory_directory = "../../ansible/inventory"
    user          = "packer"
  }
}

動的な値の生成(set_env.sh)

set_env.shでは以下を実行しています。なおスクリプト全体は冗長になるため、生成された結果の一部だけ抜粋して掲載しています。

  • source_image
    • Packerが一時的に起動するサーバーのbootイメージとなるイメージ名
  • image_name
    • プロビジョニングが成功しカスタムイメージとして保存する場合のイメージ名
  • image_family
    • カスタムイメージのファミリー名グループ名のようなののでTerraformで利用する際に使用(後述)
# packer/bin/set_env.sh
#!/bin/bash

# source iamge名: 例では執筆時点でのGCP公式のubuntu20.04イメージ
echo PKR_VAR_source_image=ubuntu-2004-focal-v20220308 >> $GITHUB_ENV
# カスタムイメージ名
echo PKR_VAR_image_name=custom-ubuntu2004-base-vYYYYMMDD-01 >> $GITHUB_ENV
# family名(グループ名のようなもの)
echo PKR_VAR_image_family=custom-ubuntu2004-base >> $GITHUB_ENV

コード中に示されている$GITHUB_ENVはGithub Actionsの環境ファイルを指します。

image_nameの動的な取得

source imageは以下のようにgcloudコマンドで動的に取得しています。まだ現状ではARMアーキテクチャのVMを利用するケースがない為、INTELアーキテクチャのイメージのみでビルドするためにアーキテクチャのfilterを追加しています。

source_image=$(gcloud compute images list \
  --project $project \
  --format="value(NAME)" \
  --filter='family = ubuntu-minimal-2004 AND architecture = X86_64'
)

Ansibleによるプロビジョニング

Ansibleについては細かな説明をすると非常に長くなってしまうため、ここでは割愛をしますが、以下のようなファイルを元にPackerによりビルドが行われています。

# ansible/base.yaml
- hosts: base
  gather_facts: false
  remote_user: packer
  become: yes
  roles:
    - base

ansible/roles/base/tasks/main.ymlで定義されているタスクでは代表的に以下のような初期セットアップを実行しています。

  • unattended-upgradesの無効化
  • TimeZone等の環境定義
  • cronやシステム・アプリケーションの実行用ユーザの作成
  • パッケージマネージャー(ここではapt)に関する定義
  • システムアップデート(最新状態の適用)
  • 共通で使用される基礎的なツールパッケージのインストール
  • Ansibleの導入(次稿のテーマである自律セットアップに関係します)
  • ops-agent,logrotatedやprometheus系のexporterなど運用系ツールの設定

自動ビルドと最新イメージでのVM起動

このシステムによって変更時や定期的なトリガーでカスタムイメージが最新の状態にビルドされるようになります。Ansibleのタスクやパラメータを修正した場合にビルドされるのはもちろんのこと、schedule(cron)の設定も行われるため、ほぼ最新のパッチが適用されたイメージが作成されている状態になります。

我々の環境には起動するVMサーバの種別毎にカスタムイメージを作成しています。また、これらのサーバはベースイメージから再帰的に作成されています。

  • Github Actions self-hosted runnerサーバ
  • MySQLサーバ
  • Redisサーバ

これらのカスタムイメージを起動する場合、Terraform側ではComputeイメージのデータリソースを使用することで最新のイメージで起動することが可能になります。

# 起動するイメージファミリー名を定義
locals {
  image_families = [
    "custom-ubuntu2004-base",
    "custom-ubuntu2004-self-hosted-runner"
    "custom-ubuntu2004-mysql"
  ]
}

# イメージファミリ名で検索し、カスタムイメージのデータリソースを作成
data "google_compute_image" "main" {
  for_each = toset(local.image_families)

  family  = each.value
  project = data.google_client_config.current.project
}

resource "google_compute_instance" "main" {
  #省略
  boot_disk {
    initialize_params {
      # custom-ubuntu2004-base-v20230308のような形式で最新のイメージ名が代入される
      image = data.google_compute_image.main["custom-ubuntu2004-base"].self_link
    }
  }
  #省略
}

まとめ: VMイメージ継続ビルド編

ここまでお読みいただいてありがとうございます。このページではPackerとAnsibleを使った自動ビルドについて説明を行いました。次のテーマはサーバ自律構築について取り上げたいと思います。

この記事を書いた人

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