🤑

ChatGPTには不可能だと言われたけど、月8ドルで運用可能な格安KubernetesクラスタをGCPで構築してみた

2023/04/10に公開

とりあえず結論

色々と頑張った結果、約8.09ドル/月で運用可能になりました!
大体の費用の内訳と、一般的な構成との比較です。

リソース 一般的な節約構成 今回の格安構成 一般的な節約構成での料金 今回の格安構成での料金
CDN Cloud CDN Cloudflare
Freeプラン
$0 (+従量課金) $0
ロードバランサー Cloud LoadBalancing e2-micro
GCP無料枠
$18.25 $0
Kubernetesクラスタ Zoneクラスタ
GCP無料枠
Zoneクラスタ
GCP無料枠
$0 $0
Kubernetesノード e2-small x 2 e2-small x 2
Spotインスタンス
$12.23 * 2 = $24.46 $3.67 * 2 = $7.34
NAT Cloud NAT LBと共有 $2 (+従量課金) $0
Database Cloud SQL db-f1-micro + 10GB PGOでKubernetes内で運用 + 1GB * 2 $9.37 $0.1 * 2 = $0.2
DBバックアップ Cloud SQL Cloud Storage
GCP無料枠 5GB
$0.8 $0.3
Dockerイメージ置き場 Artifact Registry 3GB Artifact Registry 3GB $0.25 $0.25
Egress通信量 GCP無料枠1GB GCP無料枠 1GB $0 (+従量課金) $0 (+従量課金)
合計 - - $55.33 (+1GBを超えたEgressの従量課金) $8.09 (+1GBを超えたEgressの従量課金)

IaCのサンプルコードはこちらです。Pulumi(TypeScript)で実装しています。
https://github.com/cheap-k8s/example/blob/main/ops/infra

もし反響があればPulumi/TerraformのPackageなどにまとめて、誰でも数コマンドで構築できるようにできたらなと思っています。むしろ一緒にやってくれるという奇特な方はいらっしゃいませんか。。

詳細

個人開発するとき、どんなインフラ構成にしていますか?

以前の自分は、0円から運用可能なCloudRun+PlanetScaleやGAE(Standard)などを選択していました。でも、細かな制限(WebSocketが難しいとか、起動時間の制限とか)だったり、NoSQLの辛さだったり、各環境に慣れるまでの学習コストだったりが徐々に辛くなり、どうにかKubernetesにまとめられないかと悩んでいました。

Kubernetesの最大の問題は金額です。
一番安そうなvultrでも、最安インスタンス($10) + LoadBalancer($10)で$20必要になります。複数サービスを載せられるにしても$20ドルはな…少なくとも10ドル以内、そしてできれば慣れたGKEかAWSがいいんだよな…と思っていました。

とりあえずChatGPTに聞いてみたところ、以下のようにばっさり不可能と言われて笑ったのですが、諦めず頑張ってみました。なお、この価値を生み出すかわからない挑戦に業務時間を使わせてくれた会社には大変感謝しております。

月8ドルのKubernetesの前に立ちふさがった壁

以降の説明が少しでも伝わりやすくなるように、一般的な構成と、今回の格安構成それぞれの構成図を載せてます。

一般的なGKE構成

今回の格安構成

主な節約のポイントは以下になります。

  • ロードバランサーを無料枠のe2-microインスタンス内で動くCaddyで代替
  • Spotインスタンスを利用 (可用性については後述)
  • データベースはPGOを利用しKubernetesクラスタ内で運用
  • NATも無料枠のe2-microインスタンスをLBと共有して代替

1. 最初の壁 / ロードバランサー

基本的にKubernetesクラスタをAWSやGCP上で運用する場合、各クラウドプロバイダーが提供するマネージドなロードバランサーとセットで扱うことがほぼ前提となってきます。GCPではあればCloud LoadBalancingを利用することになり、これだけで+$18/月程度必要になります。
この$18を回避するために、まずは外部への入口となるIngressControllerのServiceを一般的なtype=LoadBalancerからtype=NodePortに変更してあげれば良いのですが、それだけでは以下の問題が残ります。

  1. 公開するポートは30000-32767の間にしなければいけない(80や443はNG)
  2. ノードインスタンスのIPを把握していないとアクセスすることができない

こちらを回避するために、過去の偉人たちがいくつか方法を編み出されていました。以下、特に参考にさせていただいた情報です。

色々な手法の中でも「LBの代わりになるEdgeを自前で立てればいいんじゃね」というのが、最もシンプルに思えました。このエッジを無料枠であるe2-microで立てることができれば、ロードバランサー代を$0に抑えることができます。

そうなると、残る問題は(2)です。Kubernetesの各ノードインスタンスに一定のルールで内部IPを持たせる方法を調べたのですが、IPレンジの範囲は狭くできても「空いているものから昇順に利用する」などを明示する方法はなさそうでした。

そこで今回は、LBとして採用したCaddy用のプラグインを実装し、そのプラグイン内で定期的にKubernetesのノードインスタンスのIPをポーリングする作戦を立てました。実装したプラグインがこちらになります。初めてまともにGoを書いたので、クソコードはお許しください。
https://github.com/cheap-k8s/caddy-k8s-node-upstreams

結果として、一般的なロードバランサを利用する場合と比較して構成の差異が小さく、第3者が見ても理解しやすいシンプルな形になったこともプラスだったと思っています。

2. 可用性の壁 / Spotインスタンスの利用

Spotインスタンスを利用することでVMインスタンスの使用料を60~91%!ほどカットすることができます。料金は需要に合わせて月ごと変わるらしいのですが、最近は70%オフ程度で安定している気がします。

問題は、GCPの状況次第で容赦なくインスタンスが落ちることです。ここ1ヶ月ほどe2-smallのSpotインスタンスx2台体制でkubernetesクラスタを運用していますが、落ちるときは落ちます。1週間まったく落ちた様子がないときもあれば、1日に5回程度落ちる日もありました。

しかし有り難いことに、落ちても直後に別のリソースが付与されます。一時的/恒久的なリソース不足で落とされているというより、リソースの調整か何かに巻き込まれて落ちる、みたいな感じに見えます。e2-smallなのですぐに代わりが見つかるのかもしれません(詳しい人がいれば教えて下さい)。

さらに嬉しいことに、2台が同時に落ちることはありませんでした。つまり片方が落ちたとしても、復活して2台体制に戻るまでの5分程度の間、なんとかサービスが生きている状態を維持できれば、それなりの可用性を担保できそうだということです。

こちらもなかなか厳しい戦いでしたが、以下の調整を施すことで1台のノードインスタンスが落ちた際のダウンタイムを0~10秒ほどに減らすことに成功しています。

Kubernetesクラスタ内での工夫

  • 外部への窓口となるingressController用のPod、本番環境用のアプリケーションのPod、データベース用のPodについてはtopologySpreadConstraintsを使って、各ノードにそれぞれに1Podずつ配置されるようにした。
  • 各PodのstartupProbeをしっかり設定するようにした。特に、リソース節約のためkube-dnsは1Podのみの配置にしているが、kube-dnsが配置されている側のノードが落ちると内部のDNS解決ができなくなる。そのためDNSを必要とするPodではstartupProbeでDNS解決(=DBへの接続など)まで確認できるエンドポイントを用意しkube-dnsが落ちていても問題が発生しないようにした。
  • 当初使っていたKubernetesコミュニティが開発を進めているingress-nginxは、2つあるPodのうち片方が落ちると、謎のダウンタイムが発生したため、nginx社が開発しているnginx-ingressを利用するようにした(公式にも書かれているが名前が似ていてややこしすぎる!)

ロードバランサ側(Caddy)での工夫

  • 振り分け先のうち1台が死ぬことを想定しリトライ回数を設定するようにした。またCaddyにはパッシブヘルスチェック(実際にリクエストを飛ばしてNGならロードバランス先から外す機能)があったので、そちらも設定するようにした。
  • Spotインスタンス単体のStatusがRUNNINGでも、Kubernetesのノードとして機能し始めるまでにはタイムラグがあるため、新規のインスタンスに対しては5分の猶予期間を与えて、その後にロードバランサ先に追加するようにした。

これらの対策により、片方のノードが落ちてもダウンタイムは10秒程度で収まるようになりました。しかし、直後はレスポンスタイムの大幅な悪化が発生します。GCPが用意するsimulate-maintenance-eventというコマンドを使ってSpotインスタンスが落ちた状態を再現するテストをしていた際の外形監視のグラフになります。レスポンスタイムが悪化している2箇所が、Spotインスタンスを落としたタイミングです。

とはいえ…

当然SpotインスタンスにはSLAの設定がありません。2台が同時に落ちることや、そもそもGCP上のリソースに余剰がなくリソースが付与されない可能性も十分にありえます。あくまで無料で提供されるような個人開発プロダクトでの利用に限定されると思います。

3. 情報なさすぎな壁 / PGO

GCPで一番安いマネージドなRDBは、CloudSQLのdb-f1-microインスタンス+10GBのストレージです。これだけで月$9.37になってしまうためCloudSQLは採用できません。選択肢としては以下があると思います。

  1. PlanetScaleやNeonなど無料枠のあるDBaaS使う
  2. Kubernetesクラスタ内で運用する

(1)でも良かったのですが、(2)ができるならすごく嬉しいので挑戦してみました。利用したのはPGOと呼ばれる、Kubernetes内でPostgreSQLのクラスタを管理してくれるoperatorです。特に、内部でpgBackRestを利用しての定期的なバックアップとリストア機能を備えているので、感覚的にはCloudSQLと変わらない使用感かもしれません(全然使い倒していないので希望的観測ですが)。

cybozuさんが開発されているMySQL用のoperatordeであるMOCOもとても実績がありそうで有力だったのですがCloudStorageへのバックアップに対応しておらず断念しました。少し前にPRが出て対応済みのようです!すごい!

とにかくPGOは情報がなく、特にCloudStorageへのバックアップを許可する権限をWorkloadIdentity経由でPGOのバックアップJobに渡す部分の実装が辛かったです。あと、DBへの接続情報を持ったSecretを生成してくれるのですが生成するNamespaceが指定できないのも地味に辛いポイントでした。

4. セキュリティの壁 / NATインスタンス

Kubernetesクラスタはプライベートクラスタにしています。そのため各ノードインスタンスはグローバルIPを持たず、Web上の外部リソースへのアクセスにはNATを必要とします。通常はGCPが用意するCloudNATを利用するのですが、こちらもインスタンス1台に対して$1+1GBの通信ごとに$0.045という無視できない金額が必要になります。

プライベートクラスタを諦めて、各ノードインスタンスにグローバルIPを持たせてしまえとも思ったのですが、GCPでは利用中であってもグローバルIPには利用料がかかり、なんと1IPにつき月$1.4ほど必要になります。(なお、無料枠であるe2-mircoインスタンスに付与するグローバルIPは無料です!ありがとうGCP!)

ここで月8ドルを諦めかけたのですが、ロードバランサとして利用しているe2-microインスタンスをNATとしても利用する方法を思いつきました。具体的には、外部からのリクエストはCaddyがロードバランサーとして捌き、内部からのリクエストはiptablesでIPマスカレードしてNATとして動作させるという方法です。具体的には、以下のコマンドを設定しただけです。

sudo iptables -t nat -A POSTROUTING -o eth0 -s 10.0.0.0/8 -j MASQUERADE

正直、この使い方にセキュリティ面で穴が全く無いのか100%の自信がありません。ただ問題があるにしても、もう少し丁寧な設定をすれば回避できるのではと思っています。詳しい方はご指摘いただけると嬉しいです!

まとめ

いかがだったでしょうか。個人開発をする際はどうしても運用コストに目が行きます。しかし月8ドルであればKubernetesの勉強も兼ねて使ってもいいかも…と思う方が少しでもいらっしゃれば幸いです。

今回、8ドルという金額に拘ったことで様々な知識を深めることができました。特に、安さだけでなく、まあ個人プロダクトくらいなら運用してもいいかな程度の可用性が確保できたのが一番の収穫でした。

この試みには個人開発での開発体験と社内開発での開発体験をもっと近づけたいという背景があります。その為に、取っ付きにくさがあるKubernetesをGitOps(Flux2)で身近にする試みも、この格安Kubernetesクラスタ上で試みています。こちらも機会があれば別記事でまとめようとおもいます。

もし興味がある方、説明が足りない部分について聞きたい方がいれば、ぜひぜひ質問など投げていただけると嬉しいです。1人でPulumiやTerraformのパッケージにする元気はなさそうなので、一緒にやってくれるという奇特な方がいたら、声をかけてくださるとめっちゃ嬉しいです。

それではよい個人開発ライフを!

Discussion