STORES Product Blog

こだわりを持ったお商売を支える「STORES」のテクノロジー部門のメンバーによるブログです。

Compute Engineで安全・手軽なRails console環境を構築する

こんにちは、エンジニアのokuboです。このブログでは、Rails console用のサーバをCompute Engine上で安全かつ手軽に運用する手法をご紹介します。

モチベーション

現在STORESでは、いくつかのRailsアプリケーションをGoogle CloudのCloud Run上で稼働させています。

Cloud Runはその仕様上SSH接続ができないためrails consoleが利用できません。

開発・運用効率の向上のために何とかrails consoleが使いたいと考えましたが、スタンダートと言えるほどの手法がなく、手探りでいくつかの選択肢を出して検討しました。

  • web consoleを使う
    • NG: 開発環境のみでの利用なら良いが、本番環境ではもう少しオーソドックスで信頼性の高い方式を採用したい
  • GKEを利用する
    • NG: GKEであればコンテナへのSSH接続が可能ですが、せっかくCloud Runでフルマネージドで運用できているのに、開発ツールのためだけにGKEを運用するのは学習コストが高く避けたい
  • 【採用】Compute Engineを利用する
    • OK: VMレベルで管理が必要なのはネックだが、枯れた技術スタックで信頼性が高く、学習コストも低い
    • OK: SSH可能なアカウントの制限をシンプルなIAM設定で実現可能

このブログでは、Compute EngineにRails console用サーバを構築する方法を紹介します。

前提

Cloud RunにデプロイしているDockerイメージは、GitHub Actionsでビルドしています。
今回紹介するRails console用サーバでは、このCloud Run用のビルドチェーンで生成されるイメージをそのまま利用します。

Cloud Run用とCompute Engine用で別々にビルドチェーンを用意せず、同じイメージを使うことで運用の手間を減らすためです。

よって、Compute Engineで稼働させるOSは、Dockerイメージを手軽にデプロイ可能なものが望ましいです。

やったこと

  • Compute Engineの構成を決定
  • セキュアなSSH接続方法の設計・実装
  • デプロイの構築
  • Secret Managerで管理している秘匿情報の取得

Compute Engineの構成を決定

まずCompute Engineで利用するOSを決定します。 今回はContainer-Optimized OS(COS)を利用することにしました。

  • COSを選択する利点
    • Google公式サポートの軽量OSであり、Googleがメンテナンスしているため、セキュリティパッチの適用やアップデートが自動で行われ、運用負荷が低い
    • dockerなどのコンテナランタイムが標準でインストールされており、追加セットアップなしですぐにコンテナを起動できる
    • 不要なパッケージが含まれていないため、攻撃対象領域が小さく、セキュリティリスクを抑えられる

terraformのサンプル

resource "google_compute_instance" "rails_console" {
  name         = "rails-console"
  machine_type = "e2-medium"
  zone         = <ゾーン名>

  boot_disk {
    initialize_params {
      image = "cos-cloud/cos-stable"
    }
  }
  
  # startup-script.sh については後ほど説明します
  metadata_startup_script = file("startup-script.sh")

  service_account {
    email  = "your-service-account@your-project.iam.gserviceaccount.com"
    scopes = ["cloud-platform"]
  }

  tags = ["rails-console"]
}

Compute EngineへのセキュアなSSH接続の実現

Compute EngineへのセキュアなSSH接続を実現するには、IAP(Identity-Aware Proxy)トンネルを利用します。IAP トンネルを使うことで、外部から直接VMへのSSHポートを開放せず、安全にアクセスできます。

1. IAP経由SSH用ファイアウォールルールの作成

IAPが利用するIPレンジ(35.235.240.0/20からのSSH(TCP:22)通信のみ許可するファイアウォールルールを作成します。

resource "google_compute_firewall" "iap_ssh" {
  name    = "allow-iap-ssh"
  network = "default"

  allow {
    protocol = "tcp"
    ports    = ["22"]
  }

  source_ranges = ["35.235.240.0/20"] # IAPのIPレンジ
  target_tags   = ["rails-console"]
}
  • target_tags には対象VMのネットワークタグを指定してください。

2. SSH接続コマンド

必要な権限が付与されたアカウントで、以下のコマンドを実行します。

gcloud compute ssh rails-console \
  --project=your-project-id \
  --zone=<ゾーン名> \
  --tunnel-through-iap
  • --tunnel-through-iap オプションにより、IAP経由でSSH接続します
  • ファイアウォールでIAPのIPレンジのみ許可しているため、外部からの直接アクセスは遮断されます

デプロイフローの構築手順

1. GitHub Actionsでイメージをビルド&Artifact Registryへpush

本番同様にCloud Run用のDockerイメージをビルドし、Google Cloud Artifact Registryにpushします。

2. Compute Engineの再起動でデプロイをトリガー

gcloud compute instancesコマンドでインスタンスを停止・起動し、startup scriptを実行します。

gcloud compute instances stop rails-console- --zone=<ゾーン名> --project=your-project-id
gcloud compute instances start rails-console- --zone=<ゾーン名>  --project=your-project-id

3. startup scriptでイメージをpull&コンテナ起動

startup script内でArtifact Registryからイメージをpullし、コンテナを起動します。

#!/bin/bash

echo "=== Dockerリソースのクリーンアップ ==="
docker image prune -a -f
docker container prune -f

echo "=== Artifact Registry認証 ==="
HOME=/tmp docker-credential-gcr configure-docker --registries asia-northeast1-docker.pkg.dev

echo "=== Rails Consoleコンテナ起動 ==="
HOME=/tmp docker run \
  --name=rails-console \
  --network=host \
  --env-file=/tmp/.env \
  "asia-northeast1-docker.pkg.dev/your-project/your-repo/your-image:latest"

Secret Managerでの秘匿情報取得

Secret Managerを利用している場合は、startup script内でSecret Manager APIでシークレットを取得し、環境変数ファイル(.env)として保存します。

こちらのブログを参考にさせていただきました。 qiita.com

  • jq base64 を利用しますが、COSイメージにはデフォルトでインストールされています
echo "=== Secret Managerから環境変数ファイルを生成します ==="

# メタデータサーバからプロジェクトIDとアクセストークンを取得
function meta() {
  curl -s -H 'Metadata-Flavor: Google' "http://metadata.google.internal/computeMetadata${1}"
}

PROJECT_ID=$(meta '/v1/project/project-id')
ACCESS_TOKEN=$(meta '/v1/instance/service-accounts/default/token' | jq -r '.access_token')
ENV_FILE="/tmp/.env"

# シークレット名を配列で定義
SECRETS=(DB_PASSWORD ANOTHER_SECRET)

for SECRET_NAME in "${SECRETS[@]}"
do
  echo "# ${SECRET_NAME} を取得中..."
  SECRET_RESPONSE=$(curl -s -H "Authorization: Bearer $ACCESS_TOKEN" \
    "https://secretmanager.googleapis.com/v1/projects/${PROJECT_ID}/secrets/${SECRET_NAME}/versions/latest:access")
  SECRET_VALUE=$(echo "$SECRET_RESPONSE" | jq -r '.payload.data' | base64 --decode)
  echo "${SECRET_NAME}=${SECRET_VALUE}" >> "$ENV_FILE"
done

echo "=== .envファイルの生成が完了しました ==="

課題

  • 新しいイメージをデプロイするためにVMの再起動を伴うので、mainブランチへのマージをトリガーに継続的にデプロイする運用は実現できません
    • 作業中にVMがシャットダウンされてしまうためです
    • 私たちはRails console用サーバは日次での更新とし、必要に応じて手動でも更新可能とする運用としています

まとめ

本記事では、Cloud Run環境下でもrails consoleを安全かつ手軽に利用するため、Compute Engine上に専用サーバを構築する方法を紹介しました。

  • Container-Optimized OSの採用で、セキュリティと運用負荷の低減を両立
  • IAPトンネルとファイアウォール設定により、外部からの不要なアクセスを遮断しつつ、必要なメンバーだけがSSH接続可能
  • startup scriptを活用し、最新のDockerイメージを反映する仕組みを構築
  • Secret Manager を利用して秘匿情報を安全に取得する

現時点での課題もありますが、この構成により運用効率とセキュリティを両立したrails console環境を実現できます。
今後も運用を通じてより良い仕組みにアップデートしていく予定です。