[VM継続ビルド編] GCPでVMインスタンスを自動・自律的に構築する仕組み
はじめに
このページは以下の記事シリーズのうち、VMイメージの継続ビルドについて説明しています。
VMイメージ継続ビルド
運用しているシステム向けのVMイメージの継続ビルドにはPackerを用いています。ビルドの仕組みの概観は以下のようになります。
- Github Enterprise Server(GHE)のActionsを起点として動作する
- CIサーバ(Actions self-hosted runner)がPackerのビルド・ジョブを実行
- Packerにより起動されたサーバに対してAnsibleによるプロビジョニングが実行される
- プロビジョニング済のサーバを停止・削除しながらカスタムイメージとして保存する
ファイル構成
この仕組みのファイル構成は以下のようなツリー構造になっています。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を使った自動ビルドについて説明を行いました。次のテーマはサーバ自律構築について取り上げたいと思います。