🐕

VM インスタンス作成中にコンテナをデプロイするオプション廃止に伴う Container-Optimized OS での対応

に公開

はじめに

これまで、VMインスタンス作成時にコンテナを直接デプロイする機能(gce-container-declaration メタデータを使用する方法)がありましたが、このコンテナ起動エージェントを利用した機能が非推奨となり、2026 年 7 月 31 日に廃止されることがアナウンスされました。
これを受け、起動スクリプトを使用してコンテナをデプロイする方法へと移行作業を行いました。利用していたVMインスタンスのイメージは、Container-Optimized OS (以下、COS)です。
本記事では、実際に行った具体的な手順やポイントを、公式ドキュメントを基にご紹介します。

なぜ変更が必要なのか?

従来の方法は、VM作成時にgce-container-declarationというメタデータを読み込み、コンテナ起動エージェントがコンテナをデプロイする仕組みでした。
この方法に紐づくgcloudコマンドのオプションや Terraform モジュール(gce-container)も非推奨となり、2026 年 7 月 31 日に廃止されます。
今後は、より柔軟で汎用的な起動スクリプトcloud-initを用いる方法が推奨されます。これにより、特定のOSイメージへの依存を減らし、VMの構成管理をより標準的なアプローチで行えるようになります。

移行対象のVMを確認する方法

まず、プロジェクトで影響を受けるVMインスタンスが存在するかを確認しましょう。以下のgcloudコマンドを実行することで、非推奨のメタデータキー gce-container-declaration を持つインスタンスを一覧表示できます。

gcloud compute instances list --filter="metadata.items.key:gce-container-declaration"

このコマンドでインスタンスが表示された場合は、移行の対象となります。

移行プロセス:起動スクリプトを使ったコンテナデプロイ

移行の核心部分は、既存のコンテナ設定(メタデータ)をdocker runコマンドを含む起動スクリプトに変換することです。

ステップ1:コンテナメタデータをdocker runコマンドへマッピングする

最初に、既存のVMに設定されているgce-container-declarationの内容をdocker runコマンドの引数に置き換える必要があります。

例えば、以下のようなメタデータが設定されていたとします。

既存のメタデータ (YAML形式):

spec:
  containers:
    - image: 'docker.io/library/busybox'
      command:
        - 'echo'
      args:
        - '"hello world!"'
      restartPolicy: Always

この設定は、以下のdocker runコマンドに相当します。

変換後のdocker runコマンド:

docker run --restart=always busybox:latest echo "hello world!"

公式ドキュメントには、--container-imageフラグやrestartPolicyなどの各メタデータキーとdocker runコマンドの引数の対応表が記載されていますので、ご自身の構成に合わせて変換してください。

ステップ2:起動スクリプトを作成する

次に、VM起動時にコンテナを実行するためのシェルスクリプトを作成します。
以下は、COS上でシンプルなコンテナを実行する起動スクリプトの例です。

startup_script.sh:

#!/bin/bash

# コンテナ名を定義
CONTAINER_NAME="my-app-container"

# 既存の同名コンテナがあれば停止・削除
docker stop $CONTAINER_NAME || true
docker rm $CONTAINER_NAME || true

# Docker Hubから最新のイメージをプル
docker pull busybox:latest

# イメージからコンテナを実行
docker run --name $CONTAINER_NAME busybox:latest echo "hello world!"

ポイント:

  • 冪等性(べきとうせい): スクリプト冒頭でdocker stopdocker rmを実行することで、VMが再起動された際にエラーが発生することなく、スクリプトが再実行されるようにしています
  • イメージの指定: docker runコマンドで、ステップ1でマッピングしたイメージ名やコマンド、引数を指定します

ステップ3:起動スクリプトを使って新しいVMを作成する

作成した起動スクリプトを使用して、新しいVMインスタンスを立ち上げます。以下はTerraformを使用した例です。

main.tf:

resource "google_compute_instance" "cos_vm_instance" {
  project      = "your-project-id"
  zone         = "us-central1-a"
  name         = "new-container-vm"
  machine_type = "e2-medium"

  boot_disk {
    initialize_params {
      # Container-Optimized OSイメージを指定
      image = "cos-cloud/cos-stable"
    }
  }

  network_interface {
    network = "default"
  }

  // 起動スクリプトファイルを指定
  metadata = {
    startup-script = file("startup_script.sh")
  }
}

metadataブロック内でstartup-scriptキーにスクリプトファイルを指定するのがポイントです。

ステップ4:動作確認と旧VMの削除

新しいVMが起動し、コンテナが意図通りに動作していることを確認します。 コンテナのログはdocker logs CONTAINER_NAMEコマンドで、起動スクリプト自体のログはsudo journalctl | grep "startup script"で確認できます。

アプリケーションが正常に動作することを確認できたら、古いデプロイ方法を使用していたVMを安全に削除してください。

より高度な設定:起動スクリプトの応用

基本的なコンテナ起動に加えて、起動スクリプト内でより高度な設定を行うことも可能です。以下はその一例です。

Artifact Registry イメージへのアクセス

gcr.iopkg.dev といったArtifact Registryからプライベートイメージをプルする場合、認証設定が必要です。

当初、COSにプリインストールされているdocker-credential-gcrツールの利用を検討しました。しかし、起動スクリプトはrootユーザーで実行されるため、ツールの書き込み操作がCOSのセキュリティ機構によって阻まれる問題がありました。

そこで、メタデータサーバーから直接アクセストークンを取得してDockerにログインする方法で対応しました。この方法であれば、ファイルシステムへの書き込みなしに認証を完了できます。

起動スクリプトのdocker pulldocker runの前に、以下のコマンドを追加してください。

# apt-get(パッケージマネージャ)が利用可能な場合、jqをインストール
# Container-Optimized OSではjqはプリインストールされています
if command -v apt-get &> /dev/null; then
    apt-get update
    apt-get install -y jq
fi

# メタデータサーバーからアクセストークンを取得
ACCESS_TOKEN=$(curl "http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token" -H "Metadata-Flavor: Google" | jq -r .access_token)

# 取得したアクセストークンを使ってdocker login
# リージョン部分(asia-northeast1)はご自身の環境に合わせて変更してください
docker login -u oauth2accesstoken -p "${ACCESS_TOKEN}" https://asia-northeast1-docker.pkg.dev

内部ファイアウォールを構成する

COSは、デフォルトで受信トラフィックを拒否するホストファイアウォール(iptables)が設定されています。コンテナのポートを外部に公開するには、Google CloudのVPCファイアウォールルールに加えて、ホストOSのファイアウォールも許可する必要があります。

起動スクリプトに以下のiptablesコマンドを追加して、特定のポートへのトラフィックを許可します。

# HTTP (ポート80) と HTTPS (ポート443) の受信トラフィックを許可する例
iptables -A INPUT -p tcp --dport 80 -j ACCEPT
iptables -A INPUT -p tcp --dport 443 -j ACCEPT
iptables -A FORWARD -p tcp --dport 80 -j ACCEPT
iptables -A FORWARD -p tcp --dport 443 -j ACCEPT

ホストディレクトリをマウントする

VMインスタンス上のディレクトリをコンテナ内にマウント(共有)したい場合、docker runコマンドで--mount引数を使用します。

例えば、ホストの/var/www/htmlディレクトリを、コンテナ内の/usr/share/nginx/htmlに読み取り専用でマウントするには、以下のように記述します。

docker run -d --name $CONTAINER_NAME \
  --mount type=bind,source=/var/www/html,target=/usr/share/nginx/html,readonly \
  nginx:latest

補足: docker runのネットワークモードについて

通常、コンテナは「bridge」モードで動作し、ホストOSとは別の隔離されたネットワーク空間を持ちます。しかし、「host」モードで動作するコンテナは、そのネットワーク分離を無効にします。
つまり、コンテナはホストOS(VMインスタンス)のネットワーク環境をそのまま共有します。

このモードの主な特徴は以下の通りです。

  • ネットワーク分離がない:
    • コンテナは自分専用のIPアドレスを持ちません
  • ホストのIPアドレスを直接使用:
    • コンテナはホストのIPアドレスを使い、ホストのポートを直接リッスンします
  • ポートマッピングが不要:
    • -pや-Pオプションは効果がありません。コンテナがポート80でサービスを起動すると、それはホストのポート80で直接公開されます

どのような時に使うか:

「host」モードは特殊なケースで利用されます。これによりパフォーマンスは向上しますが、Dockerの大きな利点である「隔離性」は失われます。そのため、必要に応じて適用を判断します。

  • パフォーマンス:
    • ポートマッピングによるネットワークのオーバーヘッドをなくしたい場合。大量のトラフィックを扱う場合や、レイテンシーを極限まで削減したい場合に選択されることがあります
  • ネットワーク管理:
    • コンテナがホストのネットワークインターフェースを直接監視・管理する必要がある場合(例:ネットワーク監視ツールなど)

まとめ

今回の仕様変更は、より標準的で管理しやすいVM構成への移行を促すものです。起動スクリプトを使用する方法は、コンテナの起動だけでなく、ディスクのマウントやロギング設定など、より高度なカスタマイズにも対応できる柔軟なアプローチです。
また、公式ドキュメントではcloud-initを用いる方法も紹介されており、こちらはより宣言的にVMの構成管理を行いたい場合に適しています。

期限まではまだ時間がありますが、計画的な移行を強く推奨します。この記事が、皆さんのスムーズな移行の一助となれば幸いです。

参考資料:

レスキューナウテックブログ

Discussion