kubernetes でケチケチ節約して複数のサイトを管理する
はじめに
個人で管理するウェブサーバでは、しばしば複数のウェブサイトを管理する際にコストも掛かり気味ですが、kubernetes でもツールを適切に選べば複数サイトをケチケチしながら、うまく管理できるという話をしたいと思います。
複数のサイトに必要な物
複数のサイトを運営するとなれば、こういったものが必要かと思います。
- ロードバランサと複数のドメイン
- HTML で作られた複数の静的サイト
- 複数サイトの証明書
ロードバランサと複数のドメイン
個人で管理するレベルの規模だと、複数のロードバランサをその数分借りるほどコストを掛けたくありません。できれば1つのグローバルアドレスを使い、バーチャルホストとして稼働させたいです。
ドメイン毎に Ingress を構築しても良いのですが、できれば管理コストを減らしたいです。また Ingress をサイト毎に分けると証明書もそれごとに発行しないといけなくなります。kubernetes で証明書を自動発行した事がある方であれば分かると思いますが、案外手間が係るのです。
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: multi-server-ingress
namespace: default
annotations:
cert-manager.io/cluster-issuer: "http01-cluster-issuer"
external-dns.alpha.kubernetes.io/hostname: "example1.jp,example2.jp,example3.jp,cname.example.jp"
external-dns.alpha.kubernetes.io/cloudflare-record-comment: "Managed by external-dns"
external-dns.alpha.kubernetes.io/cloudflare-proxied: "false"
spec:
ingressClassName: traefik
rules:
- host: example1.jp
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: multi-server
port:
number: 80
- host: example2.jp
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: multi-server
port:
number: 80
- host: example3.jp
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: multi-server
port:
number: 80
- host: example4.jp
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: multi-server
port:
number: 80
tls:
- hosts:
- example1.jp
- example2.jp
- example3.jp
- example4.jp
secretName: multi-server-all-tls
例えば cloudflare を使い Ingress/Service に紐づいたグローバル IP アドレスをドメインの A レコードに紐づけるのであれば、external-dns を使うと便利です。external-dns を使うと有名どころの DNS プロバイダの DNS レコードを制御できます。
CNAME レコードで管理するドメイン
これは、ちょっとしたテクニックなのですが、cname.example.jp の様に、CNAME を向けるためのドメインを用意しておくと便利です。
例えば is-a.dev というドメインでは申請するとサブドメイン(例 mattn.is-a.dev) を貰う事ができます。貰ったドメインをこの cname.example.jp に向ける事であとは自分の管理下になるのです。
複数あるドメインを全て cname.example.jp に向けてあっても、Ingress が裁いてくれるので、難しく考える必要もありません。
is-a-dev では、A レコードによる申請と、CNAME による申請ができます。例えば IP アドレスで申請してしまうと、今後ロードバランサを作り直してグローバル IP アドレスが変わる度に、A レコードの更新をしなければなりません。クラウドサービスによってはグローバル IP アドレスを固定する事もできます。
is-a-dev では DNS レコードを申請する際に GitHub の pull-req を作る必要があり、またレユーして貰ってマージされるまでにも時間が掛かるため、若干ながら手間なのです。
A レコードで管理するドメイン
前述の様に kubernetes でリソースを管理しておけば、ロードバランサが作り直され、新しいグローバル IP アドレスが割り振られた際には、external-dns により自動で A レコードが更新されるという訳です。
cloudflare や Azure や Google Cloud、Route 53 等、有名な DNS プロバイダで管理しているドメインであればグローバル IP アドレスの張り替えが自動になります。
external-dns は各 Service にアノテーションを追記するだけで使えるのでとても便利です。
DDNS で管理するドメイン
いっぽうで DDNS の様に、一定期間でドメインのグローバル IP アドレスが変わってしまう様なケースだと定期的に A レコードを更新させなければなりません。
スクリプトから Ingress/Service に割り当てられたグローバル IP アドレスを得て、DDNS サービスに更新する CronJob を実行すると良いでしょう。
CronJob からサービスの情報を得なければならないので、サービスアカウントを作り、ロールをバインドします。
apiVersion: v1
kind: ServiceAccount
metadata:
name: ddns-updater
namespace: traefik
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: ddns-service-reader
namespace: traefik
rules:
- apiGroups: [""]
resources: ["services"]
verbs: ["get"]
resourceNames: ["traefik"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: ddns-service-reader-binding
namespace: traefik
subjects:
- kind: ServiceAccount
name: ddns-updater
namespace: traefik
roleRef:
kind: Role
name: ddns-service-reader
apiGroup: rbac.authorization.k8s.io
僕は ingress-nginx-controller ではなく traefik を使っていますが、ingress-nginx-controller を使いの方は適宜修正ください。実際の CronJob は以下の様になります。
apiVersion: batch/v1
kind: CronJob
metadata:
name: ddns-updater
namespace: traefik
labels:
app: ddns-updater
spec:
timeZone: 'Asia/Tokyo'
schedule: '0 0 * * 0'
successfulJobsHistoryLimit: 1
failedJobsHistoryLimit: 1
suspend: false
concurrencyPolicy: Forbid
startingDeadlineSeconds: 300
jobTemplate:
metadata:
labels:
app: ddns-updater
spec:
parallelism: 1
completions: 1
backoffLimit: 1
activeDeadlineSeconds: 1800
ttlSecondsAfterFinished: 300
template:
metadata:
labels:
cronjob: ddns-updater
spec:
serviceAccountName: ddns-updater
containers:
- name: ddns-updater
image: docker.io/alpine/k8s:1.31.13
command:
- /bin/sh
- -c
- |
LB_IP=$(kubectl get svc -n traefik traefik -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
echo $LB_IP
curl -s "https://example.com/update?domains=myddns.example.net&token=xxxx&ip=$LB_IP"
restartPolicy: Never
HTML で作られた複数の静的サイト
さて前述の Ingress では全て multi-server という Service に向けられているのが分かります。ここでもサービスは1個にケチる事ができます。
このアプリケーションを使うと、ドメイン毎に分けられたディレクトリを使って複数のサイトの静的コンテンツをサーブする事ができます。
+- exmaple1.jp
| +- index.html
|
+- example2.jp
| +- index.html
|
+- example3.jp
+- index.html
ディレクトリは、サーバに対してリクエストされた際に付けられた Host ヘッダにより振り分けられます。
コンテナが起動する前にデータ領域に管理された静的コンテンツを git clone しておくと便利です。サイトを更新したあと、この multi-server を rollout restart すれば新しいコンテンツに更新されます。(実際は mv/rm などを丁寧に処理してサイトが消える時間を減らすべきです)
apiVersion: apps/v1
kind: Deployment
metadata:
name: multi-server
namespace: default
labels:
app: multi-server
spec:
replicas: 1
selector:
matchLabels:
app: multi-server
template:
metadata:
labels:
app: multi-server
spec:
initContainers:
- name: init-websites
image: ghcr.io/mattn/alpine-git-curl
command:
- /bin/sh
- -c
- |
rm -rf /data/example1.jp
git clone https://my-git-repo/example1.jp /data/example1.jp
rm -rf /data/example2.jp
git clone https://my-git-repo/example2.jp /data/example2.jp
rm -rf /data/example3.jp
git clone https://my-git-repo/example3.jp /data/example3.jp
rm -rf /data/error-pages
git clone https://my-git-repo/error-pages /data/error-pages
volumeMounts:
- name: ingress-static-files
mountPath: /data
containers:
- name: multi-server
image: ghcr.io/mattn/multi-server
imagePullPolicy: IfNotPresent
ports:
- containerPort: 8080
volumeMounts:
- name: ingress-static-files
mountPath: /data
volumes:
- name: ingress-static-files
persistentVolumeClaim:
claimName: ingress-static-files-pvc
---
apiVersion: v1
kind: Service
metadata:
name: multi-server
namespace: default
labels:
app: multi-server
spec:
type: ClusterIP
ports:
- port: 80
name: http
targetPort: 8080
selector:
app: multi-server
この例では、単純な git clone を行っていますが、ブログサイトにするのであれば、hugo 等を使って、この PersistentVolume に出力する処理が必要になります。
複数サイトの証明書
証明書はクラウドサービスの証明書を使っても良いですが、そちらにも期限があり、更新の際に都度 kubernetes の中に取り込むのが面倒です。個人サイトなので cert-manager を使い、Let's Encrypt で自動更新してしまうのがお安いです。
前述の様に、証明書の自動生成管理は割と大変です。サブジェクトの代替を使い、1つの証明書を使うのも良いでしょう。
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: multi-server-all-tls
namespace: default
spec:
secretName: multi-server-all-tls
issuerRef:
name: http01-cluster-issuer
kind: ClusterIssuer
dnsNames:
- example1.jp
- example2.jp
- example3.jp
---
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: http01-cluster-issuer
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: me@my-domain.jp
privateKeySecretRef:
name: http01-cluster-issuer-tls-account-key
solvers:
- http01:
ingress:
class: traefik
この方法では traefik が http01 をうまく処理してくれますが、dns01 を使う場合は別途 DNS プロバイダの設定が必要です。
おまけ: エラーページ
Traefik の Middleware を使うと、エラーページを構築する事ができます。これまたお手製の静的サイトサーバを使い、共通のコンテンツのみサーブする様にします。
apiVersion: apps/v1
kind: Deployment
metadata:
name: static
namespace: default
labels:
app: static
spec:
replicas: 1
selector:
matchLabels:
app: static
template:
metadata:
labels:
app: static
spec:
containers:
- name: static
image: ghcr.io/mattn/serve
imagePullPolicy: IfNotPresent
args:
- /go/bin/serve
- -r
- /data/error-pages
ports:
- containerPort: 5000
volumeMounts:
- name: ingress-static-files
mountPath: /data
volumes:
- name: ingress-static-files
persistentVolumeClaim:
claimName: ingress-static-files-pvc
Traefik Middleware は以下の通り。
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: traefik-error-page
spec:
errors:
status:
- "404"
- "500"
- "501"
- "503"
- "505-599"
statusRewrites:
"418": 404
"502-504": 500
query: /{status}.html
service:
name: serve
port: 80,
これで、どのサイトでも共通の 404 ページを作ることができます。
おわりに
これらを使うと、複数のドメインの複数の静的サイトを複数の証明書(実際は1つ)を使って一元管理する事ができます。
今回、Traefik、multi-server、serve と、3つもウェブサーバを起動している様に見えますが、いずれも Go で書かれており、Traefik は alpine、multi-server と serve は scratch をベースイメージにしている為、コンテナイメージのサイズは Traefik で 49.5 MB、multi-server で 2.95 MB、serve で 2.28 MB しかありません。とてもケチケチしてて良いですね。
いかがでしたか。ドケチに節約しまくる事で追加コストをまったく掛けずに複数サイトを管理できる様になります。ぜひお試し下さい。
Discussion