こんにちは、エンジニアの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
環境を実現できます。
今後も運用を通じてより良い仕組みにアップデートしていく予定です。