Server-Side Swift VaporをCloud Runにデプロイし、PostgreSQLに証明書認証で接続する
この記事は、Swift Advent Calendar 2024の9日目の記事になります。
概要
私は、個人開発でServer-Side Swift Vaporのプロジェクトをよく作ります。しかし、デプロイ先環境を毎回毎回作るのは面倒ですし、お金がかけられません。そこで、この記事ではデプロイ先をお手軽かつお安く用意する方法を紹介します。
個人開発のデプロイ先事情
私の個人開発のサーバー側には以下の特徴があります。
- Swift製
- お金をかけられない
- たいていRDBを使用
- 小さなプロジェクトが多く存在
- 自分のみ、または知人のみが使用するためアクセス数が少ない
そのため、毎度デプロイ先環境を作るのは時間がかかりますし、パブリッククラウドを利用するとお金がかかります。
Cloud Runについて
パブリッククラウドを利用するとお金がかかると言いましたが、使わないとデプロイ先環境を用意するのに非常に時間がかかりますし、TLS化やドメインを設定するのに手間がかかります。
そこで、Swiftを動かす環境にはGoogle CloudのCloud Runを使用します。
Cloud RunはフルマネージドなDockerコンテナ実行環境です。設定によってHTTPリクエストがあったときのみお金がかかるようにできるため、数人しか使わないアプリケーションではほとんどお金がかかりません。また無料枠が毎月存在し、およそ50時間程度までのコンピューティングでは料金が発生しないため、大抵の場合はお金がかかりません。TLS化、コンテナビルド、ドメインも自動で用意してくれます。
VPS上のPostgreSQL
パブリッククラウドで、データベースを使用すると、最低スペックでも1台につき月額3000円はかかります。もちろん
- 自動スケールアウトできる
- 自動スケールアップできる
- データの耐久性が高い
- バックアップが容易
- 可用性が高い
- セキュアである
- アクセスが早い
- ネットワーク帯域が多い
- ログ・アラートが見やすい
- 設定が容易
- レプリケーションがしやすい
などの絶対的メリットがあるため、商用などであればフルマネージドなRDBを使用すべきではありますが、個人開発では特にアクセス数がスケールアップするわけでもなく、可用性が必要なこともなく、人件費もタダなので、過剰スペックです。最悪データが吹き飛んでもちょっと困るかもで済みます。
そこで、月額定額制のVPS上にPostgreSQLを設定し、Cloud Runからアクセスするようにします。
VPSでRDBを作成すると、月額500〜2000円程度でいくつものプロジェクトでデータベースを使用可能です。本番環境以外でのみVPS利用とすると開発環境をいくつもお金を気にせずに作れて良いかもしれません。
開発環境
- MacBook Pro M1 Pro
- macOS 15.1.1
- Xcode 16.1
- Swift 6.0.2
- vapor toolbox 18.7.5
- Docker 27.3.1
PostgreSQLを準備する
ではまず、自前で用意する以上、可能な限りセキュアに、かつデータベースの追加が楽にを目標に作成します。そのため以下の要件でPostgreSQLを用意します
- Docker Composeで定義
- 証明書認証を使用
証明書認証について
PostgreSQLでは複数の認証方式が提供されていますが、証明書認証が比較的セキュアと言われており、またデータベースの追加が容易だと考えています。
証明書認証では、認証局(CA)が信用できると仮定した上で、PostgreSQLとそのクライアントがお互いにお互いが認証局(CA)によって信用できることを保証されていることを確認して認証を行います。
用語
秘密鍵(.key)
認証局(CA), PostgreSQL, クライアントがそれぞれ各自で保持、秘匿する鍵。
秘密鍵では任意のメッセージに対する署名を作成することができる。
署名は公開鍵によって検証することができ、メッセージが改竄されていないことが確認できる。
公開鍵
秘密鍵に対になっていて、秘密鍵で作成した署名を検証できる。
秘密鍵から公開鍵を作成できる。
証明書(.crt)
認証局(CA)の秘密鍵で署名された情報。主体者の識別情報(名前、ドメイン、URI等)や主体者の公開鍵などを含んでいる。
証明書作成には、認証局の秘密鍵と主体者の情報や公開鍵が必要。
登場人物
クライアント: 以下のものを持っている
- クライアント秘密鍵
- クライアント証明書
- 認証局(CA)証明書
PostgreSQL: 以下のものを持っている
- PostgreSQL秘密鍵
- PostgreSQL証明書
- 認証局(CA)証明書
認証局(CA): 以下のものを持っている
- 認証局(CA)秘密鍵
- 認証局(CA)証明書
認証手順
- クライアントがPostgreSQLに接続を試みる
- クライアントがPostgreSQL証明書を認証局(CA)公開鍵で検証して認証する
- PostgreSQLがクライアント証明書を認証局(CA)公開鍵で検証して認証する
証明書の作成
登場人物の全てを用意します。
認証局(CA)
まず、認証局(CA)を用意します。
認証局(CA): 以下のものを持っている
- 認証局(CA)秘密鍵
- 認証局(CA)証明書
認証局(CA)の秘密鍵
認証局の秘密鍵をED25519
のアルゴリズムで作成します。以下のコマンドで、root.key
というファイルを作成します
openssl genpkey -algorithm ED25519 -out root.key
中身は以下のようなものになります。
-----BEGIN PRIVATE KEY-----
MC4CAQAwBQYDK2VwBCIEIHFli9TfCcxELs/Iu5u372Ea03xnVwiBxkTESyKLEzmy
-----END PRIVATE KEY-----
認証局(CA)証明書
認証局の公開鍵を署名付きで利用するため、認証局の証明書を作成します。先ほど作ったroot.keyを用いますが、これは秘密鍵から公開鍵を生成するために必要です。
openssl req -new -x509 -days 3650 -key root.key -out root.crt -subj "/C=JP/ST=Tokyo/L=Shibuyaku/O=My CA/OU=CA Dept./CN=Postgres Root CA"
以下のようなファイルが生成されます。
-----BEGIN CERTIFICATE-----
MIIB8zCCAaWgAwIBAgIUdJp0JspKwj/1LPi4pkiItTgplkkwBQYDK2VwMG8xCzAJ
BgNVBAYTAkpQMQ4wDAYDVQQIDAVUb2t5bzESMBAGA1UEBwwJU2hpYnV5YWt1MQ4w
DAYDVQQKDAVNeSBDQTERMA8GA1UECwwIQ0EgRGVwdC4xGTAXBgNVBAMMEFBvc3Rn
cmVzIFJvb3QgQ0EwHhcNMjQxMjA4MDgzMjI4WhcNMzQxMjA2MDgzMjI4WjBvMQsw
CQYDVQQGEwJKUDEOMAwGA1UECAwFVG9reW8xEjAQBgNVBAcMCVNoaWJ1eWFrdTEO
MAwGA1UECgwFTXkgQ0ExETAPBgNVBAsMCENBIERlcHQuMRkwFwYDVQQDDBBQb3N0
Z3JlcyBSb290IENBMCowBQYDK2VwAyEAMcs4KU5+hpPBNnhscYSYWNc+hV6UU0DU
GEjCmq8vZOajUzBRMB0GA1UdDgQWBBTkvs5W8nqsOYc4SbM8UX/P54CAFDAfBgNV
HSMEGDAWgBTkvs5W8nqsOYc4SbM8UX/P54CAFDAPBgNVHRMBAf8EBTADAQH/MAUG
AytlcANBAEOELhPCKYPynn7CsJz5zFm6QU5iKMDP2OkQ8Xx+o8FdgkvwoK06WETC
nhRRhKf2E1xS0zbJs/2AznpkGySDYw8=
-----END CERTIFICATE-----
PostgreSQL
次に、PostgreSQLの秘密鍵と証明書を作成します。
PostgreSQL: 以下のものを持っている
- PostgreSQL秘密鍵
- PostgreSQL証明書
- 認証局(CA)証明書
PostgreSQL秘密鍵
秘密鍵は認証局(CA)とほぼ同じ作成方法です。
openssl genpkey -algorithm ED25519 -out server.key
-----BEGIN PRIVATE KEY-----
MC4CAQAwBQYDK2VwBCIEINW2ZLloTsfh+3+DOshIeKvWPgy+6n3RGt2QybiEiXWv
-----END PRIVATE KEY-----
PostgreSQL証明書
PostgreSQLの証明書は、PostgreSQLの秘密鍵と認証局の秘密鍵から作成可能ですが、一旦証明書要求
というファイルを作成するステップを挟みます。このステップは不要ではありますが、もし認証局(CA)を変更する場合(例えば自前の認証局ではなくLet's Encryptに変更する場合など)に再利用することができます。
以下のコマンドで証明書要求を作成します。
openssl req -new -key server.key -out server.csr -subj "/C=JP/ST=Tokyo/L=Shibuyaku/O=DB Server/OU=Web Dept./CN=<PostgreSQLのドメイン>"
この<PostgreSQLのドメイン>の部分を置き換えてください。クライアントはこの接続先ドメインがこのCN(Common Name)と一致しているかを検証して認証を行います。ローカルの場合にはその検証を外して利用するため、CNは適当なもの、例えば以下のコマンドで大丈夫です。
openssl req -new -key server.key -out server.csr -subj "/C=JP/ST=Tokyo/L=Shibuyaku/O=DB Server/OU=Web Dept./CN=127.0.0.1"
以下のような証明書要求ファイルが作成されます。
-----BEGIN CERTIFICATE REQUEST-----
MIHtMIGgAgEAMG0xCzAJBgNVBAYTAkpQMQ4wDAYDVQQIDAVUb2t5bzESMBAGA1UE
BwwJU2hpYnV5YWt1MRIwEAYDVQQKDAlEQiBTZXJ2ZXIxEjAQBgNVBAsMCVdlYiBE
ZXB0LjESMBAGA1UEAwwJMTI3LjAuMC4xMCowBQYDK2VwAyEA0eI2VxkKz4Zf1y3O
EWlQxBtFO/3eydVkwb4aGHWfjWmgADAFBgMrZXADQQCkP/M1gnDJ32oHnTEcslbt
UixgI9Lc/3IRwqY4pFiGnIDc9CGYfDpsPC4GQ0Ecg8ZVd9VRBThVrYekejMIwlAE
-----END CERTIFICATE REQUEST-----
このサーバー証明書要求に対して、認証局(CA)の秘密鍵で署名を行い、PostgreSQLサーバー証明書を作成します。
openssl x509 -req -days 365 -in server.csr -CA root.crt -CAkey root.key -CAcreateserial -out server.crt
以下のようなPostgreSQLサーバー証明書が作成されます
-----BEGIN CERTIFICATE-----
MIIB4DCCAZKgAwIBAgIUNaZRAMhZuVRskGtJI/X4j1iYvCgwBQYDK2VwMG8xCzAJ
BgNVBAYTAkpQMQ4wDAYDVQQIDAVUb2t5bzESMBAGA1UEBwwJU2hpYnV5YWt1MQ4w
DAYDVQQKDAVNeSBDQTERMA8GA1UECwwIQ0EgRGVwdC4xGTAXBgNVBAMMEFBvc3Rn
cmVzIFJvb3QgQ0EwHhcNMjQxMjA4MDg0NDI4WhcNMjUxMjA4MDg0NDI4WjBtMQsw
CQYDVQQGEwJKUDEOMAwGA1UECAwFVG9reW8xEjAQBgNVBAcMCVNoaWJ1eWFrdTES
MBAGA1UECgwJREIgU2VydmVyMRIwEAYDVQQLDAlXZWIgRGVwdC4xEjAQBgNVBAMM
CTEyNy4wLjAuMTAqMAUGAytlcAMhANHiNlcZCs+GX9ctzhFpUMQbRTv93snVZMG+
Ghh1n41po0IwQDAdBgNVHQ4EFgQUyfAH1R00IjtySlwJ+pkwT6E9AIswHwYDVR0j
BBgwFoAU5L7OVvJ6rDmHOEmzPFF/z+eAgBQwBQYDK2VwA0EAkFF0hfk9hpY2kh65
WyinULE11ga8P/Rjs/Pe232AjYIlLqRggrbFHxcwOYpKC3zea60Y7biAJvX4WG0g
cQg3Bg==
-----END CERTIFICATE-----
クライアント
最後に、クライアントの秘密鍵と証明書を作成します。
クライアント: 以下のものを持っている
- クライアント秘密鍵
- クライアント証明書
- 認証局(CA)証明書
ほぼPostgreSQLサーバー証明書と作り方は同じです。
クライアント秘密鍵
openssl genpkey -algorithm ED25519 -out client.key
-----BEGIN PRIVATE KEY-----
MC4CAQAwBQYDK2VwBCIEIB+cZxeQwCvQFxWBDO4zTVC2gg3KVcrKW07jK3OW4dR7
-----END PRIVATE KEY-----
クライアント証明書
まず証明書要求を作成します。この際に、CN(CommonName)データベース名にします。PostgreSQLは、このCNが接続で要求されたデータベース名と合致しているかを検証して認証します。
openssl req -new -key client.key -out client.csr -subj "/C=JP/ST=Tokyo/L=Shibuyaku/O=Sample Client/OU=Client Dept./CN=<データベース名>"
とりあえずでpostgres
データベースへの接続用の証明書を作成します。他のデータベースを使用する場合にはその都度証明書を発行し直してください。
openssl req -new -key client.key -out client.csr -subj "/C=JP/ST=Tokyo/L=Shibuyaku/O=Sample Client/OU=Client Dept./CN=postgres"
-----BEGIN CERTIFICATE REQUEST-----
MIHzMIGmAgEAMHMxCzAJBgNVBAYTAkpQMQ4wDAYDVQQIDAVUb2t5bzESMBAGA1UE
BwwJU2hpYnV5YWt1MRYwFAYDVQQKDA1TYW1wbGUgQ2xpZW50MRUwEwYDVQQLDAxD
bGllbnQgRGVwdC4xETAPBgNVBAMMCHBvc3RncmVzMCowBQYDK2VwAyEAOYXz7EFX
QvHY5w4EIZRdtTqKUZi1zYm3pZB2RTTtJcagADAFBgMrZXADQQBK6FVS7TqCld8G
Bvu5U1Z9lG/Zl/TPrZjaySoVwkrZh8iRfB+7ASp3GqTvQIRjlYbwhY6YzldWPWWG
2VB71UcP
-----END CERTIFICATE REQUEST-----
証明書要求を認証局の秘密鍵を使って署名し、証明書を発行します。
openssl x509 -req -days 365 -in client.csr -CA root.crt -CAkey root.key -CAcreateserial -out client.crt
-----BEGIN CERTIFICATE-----
MIIB5jCCAZigAwIBAgIUNaZRAMhZuVRskGtJI/X4j1iYvCkwBQYDK2VwMG8xCzAJ
BgNVBAYTAkpQMQ4wDAYDVQQIDAVUb2t5bzESMBAGA1UEBwwJU2hpYnV5YWt1MQ4w
DAYDVQQKDAVNeSBDQTERMA8GA1UECwwIQ0EgRGVwdC4xGTAXBgNVBAMMEFBvc3Rn
cmVzIFJvb3QgQ0EwHhcNMjQxMjA4MDg1MzAwWhcNMjUxMjA4MDg1MzAwWjBzMQsw
CQYDVQQGEwJKUDEOMAwGA1UECAwFVG9reW8xEjAQBgNVBAcMCVNoaWJ1eWFrdTEW
MBQGA1UECgwNU2FtcGxlIENsaWVudDEVMBMGA1UECwwMQ2xpZW50IERlcHQuMREw
DwYDVQQDDAhwb3N0Z3JlczAqMAUGAytlcAMhADmF8+xBV0Lx2OcOBCGUXbU6ilGY
tc2Jt6WQdkU07SXGo0IwQDAdBgNVHQ4EFgQUWoEf4fbs/lsIgMU97Rt083ze1Mkw
HwYDVR0jBBgwFoAU5L7OVvJ6rDmHOEmzPFF/z+eAgBQwBQYDK2VwA0EACnccbFL8
uDFyQtQ58FZEAEuRWQ5BsUE4s9XsvuoN/uB360OdLvd+Zu6KtmxupOGsydmRykYE
GQTWr1fRlrHNAQ==
-----END CERTIFICATE-----
DockerでPostgreSQLを立ち上げる
以下の設定ファイル、compose.yaml
ファイルを用意し、docker compose up
でサーバーを立ち上げます。
VPSでも同様にして立ち上げます。
.
├── compose.yaml
├── pg_hba.conf
├── postgresql.conf
├── root.crt
├── server.crt
└── server.key
volumes:
db_data:
services:
db:
image: postgres:17
restart: always
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
volumes:
- ./postgresql.conf:/etc/postgresql/postgresql.conf
- ./pg_hba.conf:/etc/postgresql/pg_hba.conf
- ./root.crt:/var/lib/postgresql/root.crt
- ./server.key:/var/lib/postgresql/server.key
- ./server.crt:/var/lib/postgresql/server.crt
- db_data:/var/lib/postgresql/data
command: -c 'config_file=/etc/postgresql/postgresql.conf' -c log_destination=stderr -c log_statement=all -c log_connections=on -c log_disconnections=on -c log_min_messages=debug1
ports:
- "5432:5432"
postgresql.conf
pg_hba.conf
VaporをCloud Runにデプロイする
あらかじめ用意しておいた以下のリポジトリをデプロイします。
プロジェクトを作成する
Google Cloudのコンソールに入り、新しいプロジェクトを作成します
Cloud Runを追加する
Cloud Runのサービスを追加します。
GitHubと接続する
CLOUD BUILDの設定ボタンを押し、対象リポジトリとDockerfileの位置を指定します。
サービスの設定
認証
エンドポイントを公開するため、未認証の呼び出しを許可を選択します
CPUの割り当てと料金
お安くするため、リクエストの処理中のみCPUを割り当てるを選択します。代償としてしばらくアクセスしていない際に初回アクセス時のレスポンスが遅くなります
サービスのスケーリング
リクエストがないときに料金が発生しないよう、最小インスタンス数を0にします
コンテナの設定
ポート
VaporもCloud Runもデフォルトポートが8080なのでそのままで大丈夫です。
またVaporが生成するDockerfileにはENTRYPOINTもCMDも定義済みなので、特に設定する場所はありません。
シークレット
環境変数として、データベースのHOST, USERNAME, PORT, 証明書等を設定する必要があります。シークレットマネージャーを利用する場合には、シークレットマネージャーアクセサーの権限をIAMで付与しておくことを忘れないようにしてください。
作成ボタンを押して完了です。
デプロイ後のURL
以下がサンプルとしてデプロイしたURLです。
Vapor Fluentで証明書認証接続するときの注意点
証明書は環境変数で渡す
証明書はdocker volume mountではなく、環境変数で渡すのがCloud Runではおすすめです。理由としてはmount設定が面倒だからです。
証明書を環境変数で渡す際にはbase64エンコードする
証明書はbase64エンコードして環境変数で渡し、使用時にSwiftでデコードすることがおすすめです。理由は改行を含む証明書の内容は環境変数で渡すことが難しく、改行が入らなかったり、タブが入ったりしてしまうと動かなくなってしまうためです。
base64 -i root.crt
base64 -i client.key
base64 -i client.crt
でbase64エンコードしたものを環境変数に入れてください。
ローカル環境ではホスト名の認証を外す
ローカル環境(127.0.0.1)で立ち上げたPostgreSQLではドメインがないため接続時に証明書のドメインと異なり、認証が失敗します。そのため、ローカル環境ではホスト名の認証を外しておきます。
ED25519を有効化する
SwiftのNIOSSLでは、RSAはデフォルトで使用可能になっていますが、今回はED25519を使用したため明示的に利用可能にする必要があります。
まとめ
Server-Side Swiftを個人開発レベルで複数プロジェクトお手軽にお安くデプロイする方法の1つを紹介しました!
- PostgreSQLの証明書認証の利用方法
- Cloud Runのデプロイ方法
- Vapor/Fluent/NIOSSL特有の詰まりどころ
などが伝われば嬉しいです。
参考文献
- WEB+DB PRESS plusシリーズ SSL/TLS実践入門 ──Webの安全性を支える暗号化技術の設計思想
市原 創、板倉 広明
Discussion