AWSでkubernetesクラスタを構築してmisskey鯖を立てよう


前回のおさらい

k8sクラスタを立ててNodePort経由でnginx Podまで疎通が取れるところまでやりました。今回はその続き。

flannelが通信するためのポートも開放必要

Kuberntes公式にはKubernetes自体が通信に使うポートは載っているものの、cniが通信に使うポートまでは載ってなかった…
上記参考に8472番も許可するセキュリティグループを作成しておく。

cluster.localドメインの名前解決ができない!

前回k8sクラスタを立てたとき、kube-dnsによるcluster.localドメインによる名前解決が動作しなかった。

# svcのIP指定でnginxにアクセスはできる
$ kubectl get svc
NAME         TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)   AGE
kubernetes   ClusterIP   10.96.0.1       <none>        443/TCP   2m31s
nginx        ClusterIP   10.102.226.76   <none>        80/TCP    6s
$ kubectl run alpine -it --rm --image alpine -- ash
/ # wget -O - 10.102.226.76
Connecting to 10.102.226.76 (10.102.226.76:80)
writing to stdout
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
html { color-scheme: light dark; }
body { width: 35em; margin: 0 auto;
font-family: Tahoma, Verdana, Arial, sans-serif; }
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>

<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>

<p><em>Thank you for using nginx.</em></p>
</body>
</html>
-                    100% |***************************************************************************************************************************************************|   615  0:00:00 ETA
written to stdout

# でもcluster.localドメインの名前解決はできない
/ # nslookup nginx
;; connection timed out; no servers could be reached

以下のようにkube-dnsがlistenしているPortを確認し、セキュリティグループによる許可設定も行ったのだが、それでも名前解決ができない。困った。

$ kubectl get svc -n kube-system
NAME       TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)                  AGE
kube-dns   ClusterIP   10.96.0.10   <none>        53/UDP,53/TCP,9153/TCP   7m7s

情報を求めて彷徨っていたところ、興味深いページにたどり着くことができた。

なんとcorednsのdeploymentを一旦削除した後再度applyすることで名前解決ができるようになるらしい。
ものは試しとやってみることにした。

$ kubectl get deployment -n kube-system coredns -o=yaml > coredns_dep.yaml
$ kubectl delete -f coredns_dep.yaml
deployment.apps "coredns" deleted
$ kubectl apply -f coredns_dep.yaml
deployment.apps/coredns created
$ kubectl run alpine -it --rm --image alpine -- ash
/ # nslookup nginx
Server:         10.96.0.10
Address:        10.96.0.10:53


Name:   nginx.default.svc.cluster.local
Address: 10.102.226.76
.
.
.

本当に名前解決ができるようになった。やはり再起動は全てを解決する…

クラスタ構成を考える

概ねインターネットからの疎通ができるk8sクラスタの構築準備が整ったので、misskey鯖を立てるk8sクラスタおよび周辺ネットワークの構成を考えていきたい。
なお構成を考えるにあたりAWSではなく他VPSサービスを利用することも検討はしたものの、ある程度触り慣れたAWSを捨てて移行するほどのメリットを感じられなかった。

サーバスペック

後述する参考資料と利用料金を鑑みて、k8sクラスタに使用するサーバスペックは以下で良いだろう。

  • ec2インスタンス

    • t3.small(CPU:2コア3.1GHz,メモリ2GB) × 3台

    • 1台はcontrol-plane用、残り2台をworker(+ingress)に割り当てる

  • EBS

    • 汎用 SSD (gp3) - ストレージ 30GB

k8s外部との接続方法

インターネットとk8sクラスタの接続方法として、2パターン考えられる。

  • 別途ec2インスタンスを立てnginxをリバースプロキシとして使う

  • ELB(+Ingress)で公開する

前者のメリットは利用料金を安くできること。ELBの代わりにt3.smallのec2インスタンスを立てることでELB利用時に比べて最低1000円/月は安くでき、通信量が増えればその分ELBの利用料が増えるため実際にはもっと安くなると考えられる。

その代わり、以下デメリットがある。

  • nginxでSSL終端することになるため、opensslに脆弱性が見つかった場合に自前で対応が必要

  • 仮にnginxへの(=k8s上で動作させるmisskeyへの)アクセスが増え、慢性的にnginxの処理に大幅な遅延が発生するようになってしまった場合、nginxが動作しているインスタンスのインスタンスタイプの見直し&インスタンス再作成が必要(=サービス断発生が避けられない)

無制限にユーザ登録を開放するつもりはないため2点目のデメリットが顕在化することはほとんど無いと考えられるが、1点目は結構問題である。

これが業務で立てるサービスであれば所属会社のセキュリティ部門などが定期的に行っている(であろう)脆弱性チェック結果を元に対応を行うだけなのだが、個人で立てるサービスではチェックから自分でやらなければならない。常日頃からセキュリティ関連のニュースに目を光らせ、脆弱性報告が挙がってないかを探しに行くのを個人でやるのは中々ハードルが高い。

一方ELBを用いる場合はELBでSSL終端できるため、前述のようなセキュリティ面の対応はAWS側にお任せできる。
また、仮にk8s上で動作させるmisskeyへのアクセスが増えてスペック増強が必要になった場合でも、ELB自体を止める必要が無くworker nodeおよびIngress周りのPodのreplica数を増やすことで対応可能なためサービス断が発生しない。

現状そこまで通信量は増えない見込みであることを鑑みると、ELB(+Ingress)で公開するメリットの方が大きそうである。

CDNどうする

AWSが提供しているCloudFrontを利用するのが良さそうに思われる。現在想定している使い方なら無料枠に収まりそう。

misskeyのドメインとサーバ証明書どうする

misskey-naosuki.netというドメインをRoute53で作成・登録する。
ELBを使用することでACMが無料で利用可能になるため、ACMで自動更新ありのサーバ証明書を発行して使う。 

misskeyのメールサーバどうする

Cloudflare Email Routing + Gmailを用いて独自ドメインからmisskey関連のメール送受信を行いたい。そのためCloudflareのアカウント作成が別途必要。

いざ構築

ec2インスタンス作成 ~ master/worker nodeセットアップについては本記事では割愛。
なお、改めてk8sクラスタを構築しようとしたところなぜかkubeadmのapt-get installがコケるようになってしまった…
ので、以下ページを参考にkubernetes-cniをforce overrideして乗り切った。

また、dockerインストール時にcontainerdがインストールされなくなったので別途インストールするようplaybookの修正も行った。

NFSサーバセットアップ

上記を参考にセットアップする。
worker含む全nodeにnfs-commonをinstallするのを忘れないように。
なお、マウント先パスは /k8s/misskey/ にしておくと後々都合がいい。

misskeyセットアップ

上記を参考にセットアップする。
なお、t3.smallタイプだと misskey.yamlのDeploymentで指定している
`spec.template.spec.container.resource.limits` を外さないとPodがスケジューリングされなかった。

構築確認

UI表示確認

ubuntu@kube-controlplane:~$ kubectl get svc -n misskey
NAME    TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)          AGE
db      ClusterIP   10.105.227.12   <none>        5432/TCP         44m
redis   ClusterIP   10.111.88.70    <none>        6379/TCP         44m
web     NodePort    10.109.209.13   <none>        3000:30100/TCP   5m
ubuntu@kube-controlplane:~$ kubectl get svc -n misskey
NAME    TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)          AGE
db      ClusterIP   10.105.227.12   <none>        5432/TCP         44m
redis   ClusterIP   10.111.88.70    <none>        6379/TCP         44m
web     NodePort    10.109.209.13   <none>        3000:30100/TCP   5m

worker1ないしworker2のパブリックIPで30100ポートを指定すれば取り急ぎUIは開ける。

取り急ぎ管理者アカウントなどを作成

この辺を見ながら設定していく。
基本的にお一人様鯖、招待するにしても知り合いのみにするため、「誰でも新規登録できるようにする」は切っておく。

外部公開設定

ドメイン作成・登録

AWSのRoute53でドメインを作成。

サーバ証明書作成

上記を参考にサーバ証明書を発行。
ベースドメインを含むワイルドカード証明書を発行した。

Cloudflareアカウント作成

Freeプランで作成。Botプロテクションに使うTurnstileの作成をして、いざメール転送設定を…というところで、どうやらmisskey-naosuki.netのままメール転送設定しようと思ったらRoute53で登録したDNSレコードを移植しないといけないらしい。

どうせ個人鯖だし、不特定多数のアカウント登録を想定しているわけでもないので、普段利用している個人メールアドレスをそのまま使う方針で妥協することにした。

ELB作成

この辺りを参考に、Webコンソールからポチポチっと作成。
HTTPベースでのルーティングは不要だったのと、nginx-ingress-controllerの公式インストールガイドで紹介されているのがNLB向けの手順だったこともあり、ALBではなくNLBを採用することにした。

疎通確認のためmisskey-webサーバのデプロイ先をworker1に固定し、LBの80番ポートに来たHTTP通信をworker1の30110番ポートに転送してみたところ無事NLB経由でmisskeyのページが表示された。
試してはいないが、ターゲットグループの設定で転送先ノードを増やせばworker2にデプロイされてても問題なく表示されそうである。
これなら無理にingressを立てる必要もなさそう。

HTTPS化する場合はLBの443番ポートに来たTLS通信をworker1の30110番に転送するようにし、作成したサーバ証明書をSSL/TLS証明書に指定すればOK。

エイリアスレコードを設定

NLBに割り当てられたパブリックDNSでアクセスできるようになったので、取得した独自ドメインでもアクセスできるようにする。

上記参考にRoute53で取得したドメインをNLBのパブリックドメインのエイリアスレコードとして指定する。
ちなみに後々CloudFront噛ませる際に設定変更することになる。

メールサーバ設定

こちらを参考に、Gmailのメールサーバを利用するよう設定を行った。

オブジェクトストレージにAmazon S3を指定した場合に画像が表示されないのを修正する

この辺参考に設定を行った。

CloudFrontを噛ませる

この辺り読みながら設定を行った。
流れとしては

  1. バージニアリージョンのACMでCloudFront用のサーバ証明書を発行

  2. Route53で取得したドメインのサブドメインをLBのパブリックドメインのエイリアスレコードとして登録

  3. CloudFrontのディストリビューションを作成

    1. LBをオリジンに設定。LBを選択するのではなく上述のサブドメインを指定すること。

    2. キャッシュビヘイビアはCloudFront の WAFを設定する #CloudFront - Qiita を参考に指定 追記した内容を参照。

    3. 料金クラスは北米、欧州、アジア、中東、アフリカを使用

    4. 代替ドメイン名を misskey-naosuki.net に指定

  4. Route53で取得したドメインをCloudFrontドメインのエイリアスレコードとして登録

といった感じ。

(2024/2/11)追記
設定によってはCloudfrontを噛ませた瞬間に予期せぬ挙動を引き起こすことが判明した。具体的には以下。

  • 画像投稿ができない

    • 投稿しようとすると403エラーや504エラーが発生する

  • リモートのフォロワーの投稿がタイムラインに追加されない

  • 自動承認されるはずのフォローが承認されない

  • アバターやサムネが全て同じ画像になる

以下のビヘイビア設定を行うことで、上記挙動が発生しなくなることを確認した。

  • 優先順位0

    • パスパターン: /api/*

    • オリジンとオリジンパスグループ: LBを指定

    • オブジェクトを自動的に圧縮: No

    • ビューワープロトコルポリシー: Redirect HTTP to HTTPS

    • 許可された HTTP メソッド: GET, HEAD, OPTIONS, PUT, POST, PATCH, DELETE

    • ビューワーのアクセスを制限する: No

    • キャッシュキーとオリジンリクエスト

      • Cache policy and origin request policy (recommended) にチェック

      • キャッシュポリシー: CachingDisabled

      • オリジンリクエストポリシー: AllViewer

  • 優先順位1

    • パスパターン: デフォルト(*)

    • オリジンとオリジンパスグループ: LBを指定

    • オブジェクトを自動的に圧縮: No

    • ビューワープロトコルポリシー: Redirect HTTP to HTTPS

    • 許可された HTTP メソッド: GET, HEAD, OPTIONS, PUT, POST, PATCH, DELETE

    • ビューワーのアクセスを制限する: No

    • キャッシュキーとオリジンリクエスト

      • Cache policy and origin request policy (recommended) にチェック

      • キャッシュポリシー: Amplify

      • オリジンリクエストポリシー: AllViewer

また、WAFを有効にすることで追加されるルールの内、以下を許可することで403エラーの発生を防ぐことができる。こちらについてはアップロードする画像によるため、DENYのままではどうしても困るといった場合に設定変更するとよい。

  • AWS-AWSManagedRulesCommonRuleSet

    • SizeRestrictions_BODY

お一人様鯖を立ててみた感想

Discordの鯖立てと違って結構かかってしまったなという感想。
AWSとk8sを触ることが主目的であったのでまあ致し方なしか。

これまでEC2はよく触っていたものの、ELBやCloudFront、ACM、Route53といったサービスは今回初めて触ることになった。
外部のドメイン登録サービスや証明書発行サービスを頼ることなく一気通貫でwebサービスを構築できるのはやはりAWS強し…といったところか。

基本的にお一人鯖として運用することになるとは思われるが、k8s上で動くサービスとして構築したおかげで割とスケールアウトは簡単に行えるようになっている。もし利用者が増えるようなことがあっても柔軟に対応できる余地があるのは良いことだ。

この記事が気に入ったらサポートをしてみませんか?