ブログ基盤をk8sに移行した
Table of Contents
ブログ基盤をk8sに移行するにあたって、やったことのメモ
経緯
これまでの公開サーバはRaspberry Pi 3B単体で運用していた(参考記事)が、 以前ラズパイクラスタを作ったことだし、ついでにブログも丸々移行することにした。
構成
以下のような構成にした。
- L7LB & SSL終端
- NGINX Ingress Controller
- 証明書の自動更新
- cert-manager
- ベアメタルLB
- metalLB(L2 mode)
- 記事を置くためのPV
- NAS(NFS)←この記事で買ったやつ
手順
それでは実際の手順を紹介する。
Ingress経由でサービスを公開する
Nginx Ingress Controllerの導入
NginxをL7ロードバランサとして動かすために、helmでingress-nginxを導入する。
helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
helm repo update
kubectl create ns ingress-system
helm install ingress-nginx ingress-nginx/ingress-nginx -n ingress-system
kubectl get svc ingress-nginx-controller # 確認
その後、下記マニフェストをapplyしてIngressの設定を行う。
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
namespace: nginx
name: ingress-nginx
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
nginx.ingress.kubernetes.io/ssl-redirect: "false"
spec:
ingressClassName: nginx #必須
rules:
- http:
paths:
- path: /
pathType: Prefix #必須
backend:
service:
name: service-np-blog
port:
number: 8080
metalLBを導入している場合、kubectl get svc -n ingress-system
したときにNginx Ingress ControllerがLoadBalancer typeのサービスとして動いていることが確認できる。
過去のk8sバージョンの情報がネット上に多いが、
apiVersion: extensions/v1beta1
とか
serviceName: service-lb-blog
servicePort: 8080
等は古い記法なので動かない。
一概にNginx Ingress Controllerといっても、コミュニティ版とNingx版がある。今回入れているのはコミュニティ版。
一旦hostPathで公開
hostPathを利用してhugoのブログを公開してみる
apiVersion: v1
kind: Namespace
metadata:
name: nginx
---
apiVersion: v1
kind: Service
metadata:
name: service-np-blog
namespace: nginx
spec:
type: NodePort
ports:
- name: http
protocol: TCP
port: 8080 # ServiceのIPでlistenするポート
nodePort: 30080 # nodeのIPでlistenするポート(30000-32767)
targetPort: 80 # 転送先(コンテナ)でlistenしているPort番号のポート
externalTrafficPolicy: Local
selector:
app: blog # 転送先の Pod のラベル
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: deployment-blog
namespace: nginx
spec:
selector:
matchLabels:
app: blog
replicas: 1
template:
metadata:
name: blog
namespace: nginx
labels:
app: blog
spec:
containers:
- name: nginx-container
image: nginx
env:
- name: nginx-container
ports:
- containerPort: 80
volumeMounts:
- name: document-root
mountPath: /usr/share/nginx/html
volumes:
- name: document-root
hostPath:
path: /var/www/html/hugo
ここまでで、metalLB・Ingressの設定が適切に済んでいれば、IngressのExternal-IPにアクセスすることで外部からコンテンツが閲覧できる。
$ kubectl get svc -n ingress-system
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
ingress-nginx-controller LoadBalancer 10.109.60.51 192.168.1.208 80:27914/TCP,443:23986/TCP 13d
ingress-nginx-controller-admission ClusterIP 10.104.185.55 <none> 443/TCP 13d
# ingress-nginx-controllerのexternal IPはmetalLBのアドレスプールから自動で振られる
NFSサーバをPVとして利用
hostPathだと別ノードで動作しているPodがそれぞれ別のデータを参照するので(今回の例ではそれでも問題ないが)、こちらの記事で設定したNASをNFSサーバとして内部ネットワークに公開し、k8sから利用することにした。
NASでの設定
QNAP公式の手順を参考に実施。Squashオプションはユーザなしのスカッシュを選択する。
ローカルでNFSとしてマウントできるかテストを行う場合は以下のようにすれば可能
mkdir ./nfs
sudo mount -t nfs {NASのIP}:/k8s ./nfs
-t
オプションでfstypeを指定する。
NFSv4
を利用する場合は、fstype
をnfs4
にする必要がある。
nfs-commonのインストール
PVとしてNFSを利用する際に必要なパッケージnfs-common
をクラスタにインストールする。(入っていなければ)
sudo apt install nfs-common
pv/pvc作成
apiVersion: v1
kind: Namespace
metadata:
name: nginx
---
apiVersion: v1
kind: PersistentVolume
metadata:
name: blog-pv
spec:
capacity:
storage: 10Gi
persistentVolumeReclaimPolicy: Retain
accessModes:
- ReadOnlyMany
nfs:
server: {NASのIP}
path: "/k8s/blog"
---
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
name: blog-pvc
namespace: nginx
annotations:
volume.beta.kubernetes.io/persistent-volume: blog-pv
spec:
accessModes:
- ReadOnlyMany
resources:
requests:
storage: 10Gi
pathは先程NAS側で行った設定に従ってよしなに。
blogサービス公開
apiVersion: v1
kind: Service
metadata:
name: service-np-blog
namespace: nginx
spec:
type: NodePort
ports:
- name: http
protocol: TCP
port: 8080 # ServiceのIPでlistenするポート
nodePort: 30080 # nodeのIPでlistenするポート(30000-32767)
targetPort: 80 # 転送先(コンテナ)でlistenしているPort番号のポート
externalTrafficPolicy: Local
selector:
app: blog
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: deployment-blog
namespace: nginx
spec:
selector:
matchLabels:
app: blog
replicas: 1
template:
metadata:
name: blog
namespace: nginx
labels:
app: blog
spec:
containers:
- name: nginx-container
image: nginx
env:
- name: nginx-container
ports:
- containerPort: 80
volumeMounts:
- name: document-root
mountPath: /usr/share/nginx/html
volumes:
- name: document-root
persistentVolumeClaim:
claimName: blog-pvc
これで各Podが参照するストレージをNASに統一することができた。
ブログの自動デプロイ設定
今まではremote push時にGitHub Actionsで自宅のラズパイ公開サーバにrsyncしていたが、 NASは外部に公開したくないので、違う手法をとる必要があった。
そのため、masterへのpushにフックしてビルドとNFSサーバへのデプロイをするようにした。
やりかた
hugoのプロジェクトディレクトリの.git/hooks/pre-commit
に以下を記載して、実行権限をつける
hugo --minify
PASSWORD="xxx"
ROOT=`git rev-parse --show-toplevel`
mkdir -p ${ROOT}/nfs
echo $PASSWORD | sudo -S mount -t nfs -o rw -O user=${NFS_USERNAME},password=${NFS_PASSWORD} {NASのIP}:/k8s/blog ${ROOT}/nfs
echo $PASSWORD | sudo -S rsync -lOrtcv --delete --progress ${ROOT}/public/* ${ROOT}/nfs
echo $PASSWORD | sudo -S umount ${ROOT}/nfs
rm -rf ${ROOT}/nfs
こうしておくと、remoteにpushしたとき、nfsサーバへのrsyncが走るようになる。
参考: How to backup files from a Linux OS to NAS using Rsync? - QNAP
今回の方法以外にも、iSCSIでPVとして利用する方法がある。
こちらはブロックストレージなので、速度はNFSに比べて速いが、 今回の用途ではNFSのほうが使い勝手が良かったため採用しなかった。
https対応
今まではcertbotを利用してcronで自動化していた証明書更新だったが、k8s移行にあたってその代替手法を検討した。
調べてみると、IngressをSSL終端として、k8sクラスタ上で証明書更新をいい感じにやってくれるcert-managerというツールがあるらしい。ので、これを使ってみることにした。
cert-managerインストール
ingress同様helm経由で。現時点での最新版v1.11.0をインストール。
helm install \
cert-manager jetstack/cert-manager \
--namespace cert-manager \
--create-namespace \
--version v1.11.0 \
--set installCRDs=true
Issuerデプロイ
cert-managerで証明書を発行するリソースであるIssuerを作成する。
IssuerにはIssuer
とClusterIssuer
の2種類があり、
Issuer
はnamespaceで閉じているが、ClusterIssuer
はすべてのnamespaceから利用可能。
チャレンジの種類は一番お手軽なhttp-01で(参考: チャレンジの種類)
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-issuer
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory # Production用URL
email: 'メアド'
privateKeySecretRef:
name: acme-client-letsencrypt
solvers:
- http01:
ingress:
class: nginx
Ingress更新
変更点は以下
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
namespace: nginx
name: ingress-nginx
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
nginx.ingress.kubernetes.io/ssl-redirect: "false"
cert-manager.io/cluster-issuer: "letsencrypt-issuer"
spec:
ingressClassName: nginx #必須
tls:
- hosts:
- 4nm1tsu.com
secretName: letsencrypt-cert
rules:
- host: 4nm1tsu.com
http:
paths:
- path: /
pathType: Prefix #必須
backend:
service:
name: service-np-blog
port:
number: 8080
ハマったポイント
ingressを更新したあと、どうしてもセルフチェックが通らなかった。具体的には以下のエラーが出た。
kubectl get challenges -A -o wide
> NAMESPACE NAME STATE DOMAIN REASON AGE
> nginx letsencrypt-cert-sp27q-3208533145-1674215623 pending 4nm1tsu.com Waiting for HTTP-01 challenge propagation: failed to perform self check GET request 'http://4nm1tsu.com/.well-known/acme-challenge/なんか長いhash': Get "http://4nm1tsu.com/.well-known/acme-challenge/なんか長いhash": dial tcp {おうちのグローバルIP}:80: connect: connection refused 10m
原因はドメイン名が内部アドレスに解決できないことだった。俗に言うヘアピンNAT。
これは、coreDNSに以下の書き換えルールを適用することで回避した。 (参考リンク)
ただ、あくまでこれは暫定対応に過ぎない(サーバの再起動やk8s自体のアップデートで設定が戻る)ので、恒久対応としてヘアピンNAT対応ルータをポチった。
...
apiVersion: v1
data:
Corefile: |
.:53 {
errors
health {
lameduck 5s
}
rewrite name 4nm1tsu.com ingress-nginx-controller.ingress-system.svc.cluster.local
ready
kubernetes cluster.local in-addr.arpa ip6.arpa {
pods insecure
fallthrough in-addr.arpa ip6.arpa
ttl 30
}
prometheus :9153
forward . /etc/resolv.conf {
max_concurrent 1000
}
cache 30
loop
reload
loadbalance
}
kind: ConfigMap
...
これで先程のエラーは解消されたが、その後も以下のエラーが出てしまった。
Waiting for HTTP-01 challenge propagation: did not get expected response when querying endpoint, expected "なんか長いhash" but got:
この原因はingress.yaml
に先程のエラーのトラブルシューティングで記載していた以下の設定が残っていたことだった。
acme.cert-manager.io/http01-edit-in-place: "true"
これに気づくのに相当な時間を要してしまった。
cert-manager関連のトラブルシューティングは、とりあえず以下を試しておけば、どこでコケているか分かる。
kubectl get cert,cr,order -A
kubectl describe challenge -A
kubectl get challenges -A -o wide
公式ページもとてもわかりやすいので、参考にしたい。
おわりに
今回の作業は、GWにk8sのお勉強がてらKubernetes Up & Running 3rd EditionというO’reillyの本を片手に行ったのだが、やはり手を動かしながらだと理解が進むのが早くて良い。
英語の本ではあるが、DeepLとかで翻訳すれば母国語並みのスピード感で読めるので、まったく良い時代になったなと思う。