GCP: 自動スケーリング時における Google Cloud SQL ホワイトリストの管理

次のような条件でRuby on Railsアプリケーションを運用することにした。

セットアップの過程で少し困ったのが、PostgreSQL インスタンスに接続できるIPアドレスのリスト(ホワイトリスト)をどう管理するか、という点である。

なぜ困るか。次の3点に集約される。

  1. マネージドインスタンスグループに属する VM インスタンスPostgreSQL インスタンスは別のネットワークにある。
  2. 自動スケーリングによって起動される VM インスタンスの IP アドレスは事前にわからない。
  3. 自動スケーリングによって起動された VM インスタンスに割り当てられた IP アドレスは、インスタンス停止後に他のインスタンスにより再利用される。

アプリケーションサーバーと同じネットワーク内に PostgreSQL サーバーを自前で立てて動かす場合、そのネットワーク(例えば、192.168.0.0/24)を pg_hba.conf に記載してやればいいのだが、その方法は使えない。

VM インスタンスの起動時にその IP アドレスをホワイトリストに登録し、停止時に抹消する必要がある。

この状況を打開する方法はふたつある。ひとつは、VM gcloud コマンドで VM インスタンスの起動時にその IP アドレスをホワイトリストに登録し、停止時に抹消するという方法、もうひとつは、Cloud SQL Proxyを利用する方法である。

今回は、前者の方法について解説しよう。


前提条件

対象となる GCP プロジェクトに関して、以下の準備作業を済ませる。

  • GCP コンソールの[APIとサービス]→[ライブラリ]から「Cloud SQL Admin API」が有効化してある。
  • 「Cloud SQL Admin」権限を持つ GCP のサービスアカウントを作成してある。

以下の条件を満たす VM インスタンスを用意する。

  • サーバー OS は Ubuntu 18.04
  • Ruby 2.4 以上がインストール済み
  • 上記サービスアカウントの資格情報(JSONデータ)を /root/gcp_sa_credentials.json に保存してある。

VM インスタンスの起動・停止時に実行する Ruby スクリプトの作成

/root/bin/ ディレクトリに次のような内容のファイル vm-instance-startup.rb を作成する。ただし、5 行目の pg-instance-0 の部分は、自分の Google Cloud SQL インスタンスの名前(id)で置き換えること。

#!/usr/bin/env ruby

require "json"

SQL_INSTANCE_ID = "pg-instalnce-0"
GCLOUD_COMMAND = "/snap/bin/gcloud sql instances"
KEY_FILE_PATH = "/root/gcp_sa_credentials.json"

system("/snap/bin/gcloud auth activate-service-account --key-file=#{KEY_FILE_PATH}")

api_url = "http://metadata.google.internal/computeMetadata/v1" +
  "/instance/network-interfaces/0/access-configs/0/external-ip"

external_ip = `/usr/bin/curl -s #{api_url} -H "Metadata-Flavor: Google"`

sql_instance_description = `#{GCLOUD_COMMAND} describe #{SQL_INSTANCE_ID} --format=json`
description_hash = JSON.parse(sql_instance_description)

networks = description_hash["settings"]["ipConfiguration"]["authorizedNetworks"].map do |e|
  e["value"]
end

unless networks.include?(external_ip)
  networks << external_ip
  joined = networks.join(",")

  system("#{GCLOUD_COMMAND} patch #{SQL_INSTANCE_ID} --quiet --authorized-networks=#{joined}")
end

また、同じディレクトリに次のような内容のファイル vm-instance-shutdown.rb を作成する。

#!/usr/bin/env ruby

require "json"

SQL_INSTANCE_ID = "pg-00"
GCLOUD_COMMAND = "/snap/bin/gcloud sql instances"
KEY_FILE_PATH = "/root/gcp_sa_credentials.json"

system("/snap/bin/gcloud auth activate-service-account --key-file=#{KEY_FILE_PATH}")

api_url = "http://metadata.google.internal/computeMetadata/v1" +
  "/instance/network-interfaces/0/access-configs/0/external-ip"

external_ip = `/usr/bin/curl -s #{api_url} -H "Metadata-Flavor: Google"`

sql_instance_description = `#{GCLOUD_COMMAND} describe #{SQL_INSTANCE_ID} --format=json`
description_hash = JSON.parse(sql_instance_description)

networks = description_hash["settings"]["ipConfiguration"]["authorizedNetworks"].map do |e|
  e["value"]
end

if networks.include?(external_ip)
  networks.delete(external_ip)
  joined = networks.join(",")

  system("#{GCLOUD_COMMAND} patch #{SQL_INSTANCE_ID} --quiet --authorized-networks=#{joined}")
end

ふたつの Ruby スクリプトの内容はほぼ同じである。末尾の6行だけが異なっている。

そして、これらの Ruby ファイルの実行可能フラグを立てておく。

$ chmod u+x /root/bin/*.rb

Systemdの設定ファイルを作成

ディレクト/etc/systemd/system/whitelist-registry.service という名前のファイルを次のような内容で作成する(名前は何でもよい)。ただし、このファイルには実行可能フラグを立てないこと。

[Unit]
Description=Google Cloud SQL Whitelist Registry

[Service]
Type=oneshot
ExecStart=/root/bin/vm-instance-startup.rb
ExecStop=/root/bin/vm-instance-shutdown.rb
RemainAfterExit=yes

[Install]
WantedBy=multi-user.target

そして、このサービスを有効化する。

$ sudo systemctl enable whitelist-registry.service

動作確認

サービスの起動を試す。

$ sudo systemctl start whitelist-registry.service

GCP コンソールで Cloud SQL インスタンスの「承認」タブで、この VM インスタンスの IP アドレスが「承認済みネットワーク」に加わっていれば OK である。

サービスの停止を試す。

$ sudo systemctl stop whitelist-registry.service

GCP コンソールで Cloud SQL インスタンスの「承認」タブで、この VM インスタンスの IP アドレスが「承認済みネットワーク」から消えていれば OK である。

最後に VM インスタンス自体を再起動したり、停止したりして、動作確認する。

考察

今回解説した方法の懸念点は、Systemd の停止スクリプトが実行されないまま VM インスタンスが異常終了したときに、その IP アドレスがホワイトリストに残ってしまう、ということである。データベース自体にパスワードが設定されていればそれほど危険ではないものの、気持ち悪さは残る。

Cloud SQL Proxy を使えばこの懸念は解消されるだろうが、正直に言えば、私自身がまだ試せていない。近いうちにレポートを書きたいと思う。