
はじめに
こんにちは!基盤チームの酒井です。
毎年言っている気がしますが、特に今年の夏は暑すぎますね…。
アイスクリームではなく氷菓を食べると内臓が冷えて涼しくなるという情報を聞いてからガリガリ君ばかり食べています。
この間、なんと久しぶりに当たって小学生の頃を思い出して懐かしくなりました。
さて、そんな夏の思い出に浸っている場合ではありませんね。話は変わりますが、VMのパッケージアップデート、皆さんはどのように行っていますか?
Google CloudにはVM Managerのパッチという便利な機能がありますが、今回はCloud Workflowsを組み合わせて、さらに柔軟な自動化を実現してみました。
VM Managerのパッチ機能とは
VM managerのパッチを使えば、簡単にVMのパッケージのアップデートをすることができます。
スケジューリング機能や事前にコマンドを実行したり、diskのsnapshotをとることも可能です。
なぜパッチ機能をCloud Workflowsで実行したのか
VM Managerのパッチ機能は便利ですが、今回はCloud Workflowsを使ってパッチを実行することにしました。
その理由は、定期的に起動をするVMがあり、「起動 → パッケージアップデート → 停止」という一連のフローが必要だったためです。
また、Cloud Workflowsであれば完了後にslackに通知するなど拡張性もあります。
Cloud Workflowsの実行内容
workflow内で行なっている内容としては以下のとおりです。
(ちなみに、この記事を執筆中にVM Managerのパッチでもdiskのスナップショットが取得できることを知りました..。パッチ実行時にオプションを指定するのが良いかもしれません。)
1. 対象VMの起動と待機
停止しているVMを起動してからOS Config エージェントが起動し、
OS Inventoryに最新のパッケージ情報が反映されるまで、目安として1時間待機する設定にしています。
- startVM:
call: googleapis.compute.v1.instances.start
args:
instance: ${instance}
project: ${project_id}
zone: ${zone}
result: startResult
- wait:
call: sys.sleep
args:
seconds: 3600
2. スナップショットの取得と待機
パッチを動かす前に、diskのsnapshotを取得します。
(以前パッチを実行したら、VMのパッケージの整合性が壊れてVMが起動しなくなったことがありました。)
- snapshot_disk:
call: googleapis.compute.v1.disks.createSnapshot
args:
project: ${project_id}
zone: ${zone}
disk: ${instance}
body:
name: ${unique_snapshot_name}
description: ${"Automated snapshot (UTC time" + formatted_datetime_utc + ")"}
result: snapshot_operation
- wait_for_snapshot:
call: googleapis.compute.v1.zoneOperations.wait
args:
project: ${project_id}
zone: ${zone}
operation: ${snapshot_operation.name}
result: snapshot_result
3. アップデート可能なパッケージ一覧の取得
パッチを実行するにあたり、アップデート可能なパッケージ一覧を取得します。
取得した後、パッチのパラメータに渡しやすい形に加工します。
- get_os_inventory_from_osconfig_full:
call: http.get
args:
url: ${"https://osconfig.googleapis.com/v1/projects/" + project_id + "/locations/" + zone + "/instances/" + instance + "/inventory?view=FULL"}
auth:
type: OAuth2
result: inventory_response
- check_api_response: # APIレスポンスコードを確認
switch:
- condition: '${inventory_response.code != 200}'
raise: '${"OS Config APIからのインベントリ取得(FULL view)に失敗しました。HTTPコード " + string(inventory_response.code) + " レスポンスボディ " + string(inventory_response.body)}'
- prepare_items_map: # itemsマップを安全に取得
assign:
- inventory_items_map: ${default(inventory_response.body.items, empty_map_for_default)}
- loop_through_keys:
for:
in: ${keys(inventory_items_map)} # マップ変数を直接指定
value: current_key # ループ変数がキー名になる
index: i
steps:
- check_available_package:
switch:
- condition: '${len(text.find_all_regex(current_key, "^availablePackage-")) > 0}'
next: extract_package_name
next: continue_loop
- extract_package_name:
assign:
# "availablePackage-"プレフィックスを削除
- key_without_prefix: ${text.replace_all(current_key, "availablePackage-", "")}
# 最初の":"までの部分を取得(パッケージ名のみ)
- package_name: ${text.split(key_without_prefix, ":")[0]}
- extracted_package_names: ${list.prepend(extracted_package_names, package_name)}
- continue_loop:
assign:
- dummy: "continue" # 何もしない(ループを続行)
4. パッチジョブの実行と待機
取得したパッケージ一覧を元に、パッチジョブを実行します。
パッチジョブの完了を監視し、終了次第次のstepに進みます。
- execute_patch_job:
call: http.post
args:
url: ${"https://osconfig.googleapis.com/v1/projects/" + project_id + "/patchJobs:execute"}
auth:
type: OAuth2
body:
description: ${patch_description}
instanceFilter:
instanceNamePrefixes:
- ${instance}
displayName: test-patch-job
patchConfig:
yum:
exclusivePackages: ${extracted_package_names}
# dryRun: true
result: patch_job_execution_response
- wait_for_patch_job_completion:
call: sys.sleep
args:
seconds: 30 # パッチジョブの開始を待つ
- monitor_patch_job:
steps:
- get_patch_job_status:
call: http.get
args:
url: ${"https://osconfig.googleapis.com/v1/" + patch_job_execution_response.body.name}
auth:
type: OAuth2
result: patch_job_status_response
- check_patch_job_completion:
switch:
- condition: '${patch_job_status_response.body.state == "SUCCEEDED"}'
next: patch_job_completed
- condition: '${patch_job_status_response.body.state == "COMPLETED_WITH_ERRORS"}'
next: patch_job_completed
- condition: '${patch_job_status_response.body.state == "CANCELED"}'
next: patch_job_completed
- condition: '${patch_job_status_response.body.state == "TIMED_OUT"}'
next: patch_job_completed
next: wait_and_retry
- wait_and_retry:
call: sys.sleep
args:
seconds: 60 # 1分待機してから再確認
next: get_patch_job_status
- patch_job_completed:
assign:
- final_patch_job_status: ${patch_job_status_response.body}
5. VMを終了する
- stopVM:
call: googleapis.compute.v1.instances.stop
args:
instance: ${instance}
project: ${project_id}
zone: ${zone}
result: stopVMResult
ファイル全体
main:
params: [input]
steps:
- init:
assign:
- project_id: ${sys.get_env("GOOGLE_CLOUD_PROJECT_ID")}
- patch_description: "Workflow triggered patch job"
- current_time_utc: ${sys.now()} # 現在時刻をUTCで取得
- zone: us-central1-a
- instance: gce-vm # VM名を変数から取得
- extracted_package_names: [] # 抽出結果を格納するリストを初期化
- empty_map_for_default: {}
- format_snapshot_name:
assign:
- current_time_str: ${string(current_time_utc)}
- year: ${text.substring(current_time_str, 0, 4)}
- month: ${text.substring(current_time_str, 5, 7)}
- day: ${text.substring(current_time_str, 8, 10)}
- hour: ${text.substring(current_time_str, 11, 13)}
- minute: ${text.substring(current_time_str, 14, 16)}
# VM名を YYYYMMDD-HHMM (UTC) 形式に修正
- formatted_datetime_utc: ${year + month + day + "-" + hour + minute}
- snapshot_base_name: bastion-prod-memorystore-proxy-gce
# アンダースコアをハイフンに変更し、命名規則に準拠
- unique_snapshot_name: ${text.to_lower(snapshot_base_name + formatted_datetime_utc)}
- startVM:
call: googleapis.compute.v1.instances.start
args:
instance: ${instance}
project: ${project_id}
zone: ${zone}
result: startResult
- wait:
call: sys.sleep
args:
# VMが起動してからOS Inventryが読み込まれるのを待つ(1時間は目安)
seconds: 3600
- snapshot_disk:
call: googleapis.compute.v1.disks.createSnapshot
args:
project: ${project_id}
zone: ${zone}
disk: ${instance}
body:
name: ${unique_snapshot_name} # 修正されたスナップショット名
description: ${"Automated snapshot (UTC time" + formatted_datetime_utc + ")"}
result: snapshot_operation
- wait_for_snapshot:
call: googleapis.compute.v1.zoneOperations.wait
args:
project: ${project_id}
zone: ${zone}
operation: ${snapshot_operation.name}
result: snapshot_result
- get_os_inventory_from_osconfig_full:
call: http.get
args:
url: ${"https://osconfig.googleapis.com/v1/projects/" + project_id + "/locations/" + zone + "/instances/" + instance + "/inventory?view=FULL"}
auth:
type: OAuth2
result: inventory_response
- check_api_response: # APIレスポンスコードを確認
switch:
- condition: '${inventory_response.code != 200}'
raise: '${"OS Config APIからのインベントリ取得(FULL view)に失敗しました。HTTPコード " + string(inventory_response.code) + " レスポンスボディ " + string(inventory_response.body)}'
- prepare_items_map: # itemsマップを安全に取得
assign:
- inventory_items_map: ${default(inventory_response.body.items, empty_map_for_default)}
- loop_through_keys:
for:
in: ${keys(inventory_items_map)} # マップ変数を直接指定
value: current_key # ループ変数がキー名になる
index: i
steps:
- check_available_package:
switch:
- condition: '${len(text.find_all_regex(current_key, "^availablePackage-")) > 0}'
next: extract_package_name
next: continue_loop
- extract_package_name:
assign:
# "availablePackage-"プレフィックスを削除
- key_without_prefix: ${text.replace_all(current_key, "availablePackage-", "")}
# 最初の":"までの部分を取得(パッケージ名のみ)
- package_name: ${text.split(key_without_prefix, ":")[0]}
- extracted_package_names: ${list.prepend(extracted_package_names, package_name)}
- continue_loop:
assign:
- dummy: "continue" # 何もしない(ループを続行)
- execute_patch_job:
call: http.post
args:
url: ${"https://osconfig.googleapis.com/v1/projects/" + project_id + "/patchJobs:execute"}
auth:
type: OAuth2
body:
description: ${patch_description}
instanceFilter:
instanceNamePrefixes:
- ${instance}
displayName: test-patch-job
patchConfig:
yum:
exclusivePackages: ${extracted_package_names}
# dryRun: true
result: patch_job_execution_response
- wait_for_patch_job_completion:
call: sys.sleep
args:
seconds: 30 # パッチジョブの開始を待つ
- monitor_patch_job:
steps:
- get_patch_job_status:
call: http.get
args:
url: ${"https://osconfig.googleapis.com/v1/" + patch_job_execution_response.body.name}
auth:
type: OAuth2
result: patch_job_status_response
- check_patch_job_completion:
switch:
- condition: '${patch_job_status_response.body.state == "SUCCEEDED"}'
next: patch_job_completed
- condition: '${patch_job_status_response.body.state == "COMPLETED_WITH_ERRORS"}'
next: patch_job_completed
- condition: '${patch_job_status_response.body.state == "CANCELED"}'
next: patch_job_completed
- condition: '${patch_job_status_response.body.state == "TIMED_OUT"}'
next: patch_job_completed
next: wait_and_retry
- wait_and_retry:
call: sys.sleep
args:
seconds: 60 # 1分待機してから再確認
next: get_patch_job_status
- patch_job_completed:
assign:
- final_patch_job_status: ${patch_job_status_response.body}
- stopVM:
call: googleapis.compute.v1.instances.stop
args:
instance: ${instance}
project: ${project_id}
zone: ${zone}
result: stopVMResult
- return_result:
return:
snapshot_name: ${snapshot_result.targetLink}
patch_job_name: ${patch_job_execution_response.body.name}
patch_job_final_status: ${final_patch_job_status.state}
まとめ
今回は定期的にしか起動しないVMのパッケージアップデートを自動化しました。
Cloud Workflowsを使うことで、「VMの起動・停止」「スナップショット取得」「パッチ実行」など、一連の複雑なプロセスを単一のワークフローとして定義できる拡張性の高さを実感しました!
パッチジョブで基本的には十分だと思いますので使うことは少ないかもしれませんが、参考になれば幸いです!
ビザスクではエンジニアの仲間を募集しています! 少しでもビザスク開発組織にご興味を持たれた方は、ぜひ一度カジュアルにお話ししましょう! developer-recruit.visasq.works