Kubernetesサービスへのウェブアクセス自動実装 - おうちでGitOpsシリーズ投稿5/6
こちらはおうちラボでGitOpsシリーズの5つ目のポストです。
前々回までで、Kubernetesクラスタ上で立ち上げたサービスへのクラスタ外・実ネットワークからのアクセスをできるようにし、前回は更に手動でリバースプロキシとDNSサーバをコンフィグして、httpsアクセスができるようにしました。
リバースプロキシ経由でサービスへhttpsアクセスできるようにするまでの手順は割りと簡単でしたが、今後このGitOps環境で遊んでいってもっとたくさんのサービスを立ち上げていくとなると、その都度手動でリバースプロキシとDNSサーバの設定をしなければいけなくなるのが手間になってくると思います。
今回はその部分の自動化をカバーしていきます。前回Weave GitOps Dashboard用にDNSとリバースプロキシに変更を加えたところは巻き戻しておいてください。具体的にはUnbound DNSに追加したレコードの削除と、Nginxに追加したWeave用のリバースプロキシサーバ設定の削除です。今回のセットアップでまた自動的に追加されます。
本シリーズのお約束
-
Kubernetes
Cluster on 3 nodes (2 raspberry pi4 - arm64, and 1 amd64 fanless mini PC)-
flannel
for networking -
flux
as a GitOps tool - metallb for
LoadBalancer
service type implementation
-
- 1 or more machine running
Docker
- serving
GitLab
(Used for GitOps central repository) - serving
Nginx
(Reverse proxy to provide access to GitLab and other web services to be created on Kubernetes Cluster) - serving
Unbound
(DNS resolver for machines on LAN)
- serving
- public DNS domain + SSL/TLS certification (for example, Let's Encrypt) recommended
- they are all
mydomain.net
in the series - replace
mydomain.net
with your own DNS domain to follow through
- they are all
More on Docker - Series Top: Dockerで作るおうちLAN遊び場 シリーズ1/7
Interested in getting your own DNS domain?
自動化タスクの流れ
DNSとリバースプロキシどちらでも同じ流れですが、簡単に並べると次の通りです。
- Kubernetesクラスタのcontrol-planeノードにて
- Kubernetesクラスタ上のservice情報確認
- コンフィグファイルを生成
- DNS・リバースプロキシサーバへ送る
- DockerでDNS・リバースプロキシサーバを走らせているノードにて
- 受け取ったコンフィグに変更があればサービスをリロード
DNS分だけですが、自動化タスクの流れに関して絵を用意しました。
Service情報の整理
全ネームスペース上のサービスをkubectl get svc -A
で確認すると以下のような出力になると思います。
ここで関係あるのは、第一にタイプがLoadBalancerであることです。Kubernetesクラスタ外からアクセスできるようServiceを作成し、metallbによってEXTERNAL-IP、実ネットワークのIPアドレスが振られているサービスが今回扱いたいものとなります。
そしてリバースプロキシやDNSサーバを設定するに際し関係あるデータとして、web-appやweaveといったNAMEフィールドにある各サービスの名称、EXTERNAL-IPフィールドにあるIPアドレス、そしてPORTフィールドにある、サービスアクセス時の宛先となるポート番号です。
❯ kubectl get svc -A
NAMESPACE NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
default kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 21d
default web-app LoadBalancer 10.99.119.177 192.168.1.201 80:31224/TCP 20h
flux-system notification-controller ClusterIP 10.98.188.167 <none> 80/TCP 21d
flux-system source-controller ClusterIP 10.98.116.158 <none> 80/TCP 21d
flux-system weave LoadBalancer 10.103.134.70 192.168.1.200 9001:32377/TCP 20d
flux-system webhook-receiver ClusterIP 10.103.219.65 <none> 80/TCP 21d
flux-system ww-gitops-weave-gitops ClusterIP 10.97.101.48 <none> 9001/TCP 20d
kube-system kube-dns ClusterIP 10.96.0.10 <none> 53/UDP,53/TCP,9153/TCP 21d
metallb-system webhook-service ClusterIP 10.97.230.223 <none> 443/TCP 20d
DNSレコード更新の自動化
それでは早速DNSに関してセットアップしていきましょう。Kubernetesクラスタ側とunbound DNS側両方でのセットアップが必要になります。クラスタ側では、kubectl get svc -A
で確認できた情報からunbound DNS用のコンフィグファイルを用意し、unbound DNSを実行しているサーバへscpで渡します。インタラクティブな操作なしに自動でやらせたいのでKubernetesクラスタのcontrol-planeとDockerでUnboundやNginxを実行している端末は鍵認証でssh/scpできるようにしておいてください。
Kubernetesクラスタcontrol-plane側での準備
まず自動化を実施するあれこれを配置するディレクトリとして~/svc_automation
を用意しましょう。
mkdir ~/svc_automation
cd ~/svc_automation
そして肝心のスクリプトです。dump_svc.sh
という名前で用意しました。
-
~/svc_automation/ddns.txt, ddns.conf, ddns.log
ファイルを用意(touch) -
ddns.txt
ファイルにはまずはダミーレコードとしての行を追加 - 次に
kubectl get svc -A
で確認できるLoadBalancerタイプの各サービスに関して- local-data: "{svc_name}.cluster.mydomain.net. IN A {EXTERNAL_IP}"という行を
ddns.txt
に追記 - local-data: "{svc_name}.mydomain.net. IN A {IP Address of Reverse Proxy/192.168.1.55}"という行を
ddns.txt
に追記
- local-data: "{svc_name}.cluster.mydomain.net. IN A {EXTERNAL_IP}"という行を
- 生成したunbound DNS用のコンフィグファイル
ddns.txt
のハッシュ値を確認 - (前回スクリプト実行時に作成されたデータとしての)
ddns.conf
のハッシュ値も確認 - 前回から変更があるかチェック
- もし変更があれば
-
ddns.txt
をddns.conf
としてコピー作成 (次回の比較用) - また
ddns.conf
をunbound DNSを実行しているサーバ内、config/a-records.conf
と同じディレクトリに当たる場所にscp
でコピーを渡す- なおDNSサーバを2台用意している場合はそちらへもscpするよう行を追加すること
-
- 変更がなければ何もしない
- もし変更があれば
#!/bin/bash
touch ddns.txt
touch ddns.conf
touch ddns.log
# get the list of service names and IP addresses
echo 'local-data: "dummydata.mydomain.net. IN A 127.0.0.1"' > ddns.txt
kubectl get svc -A | awk '/LoadBalancer/ {print "local-data: \""$2".cluster.mydomain.net. IN A " $5 "\""}' >> ddns.txt
kubectl get svc -A | awk '/LoadBalancer/ {print "local-data: \""$2".mydomain.net. IN A 192.168.1.55\""}' >> ddns.txt
live_data=$(echo `cat ddns.txt` | sha1sum | cut -c1-32)
conf_data=$(echo `cat ddns.conf` | sha1sum | cut -c1-32)
logger "live data is `echo $live_data`"
logger "conf data is `echo $conf_data`"
# if the ddns.conf needs to be updated
if [[ "$live_data" != "$conf_data" ]]; then
cp ddns.txt ddns.conf
scp ddns.conf 192.168.1.55:/{path_to_unbound_docker_compose_dir}/config/ddns.new
echo "ddns.conf copied to dns server - `date`" >> ddns.log
logger "Change detected and conf file scp'd"
else
logger "No change detected"
fi
そしてsystemdでこのスクリプトを定期実行されるようserviceファイルとtimerファイルを用意します。/etc/systemd/system/ddns_k8s_svc.service
と/etc/systemd/system/ddns_k8s_svc.timer
というファイルにしました。
ファイル名、タイマーの実行間隔などは自由に決めてください。serviceファイルのUser
、Group
に関しては鍵認証でのscpが通るユーザにしてください。WorkingDirectory
とExecStart
にあるパス情報も環境に合わせて入力してください。/home/myusername/svc_automation
などになります。
これらのファイルが準備できたら、sudo systemctl daemon-reload
とsudo systemctl enable ddns_k8s_svc.timer --now
で定期的に実行されるようになります。
[Unit]
Description=scp k8s svc unbound record data to local DNS servers
[Service]
WorkingDirectory={home_directory}/svc_automation
ExecStart={home_directory}/svc_automation/dump_svc.sh
User={username}
Group={user's group name}
[Install]
WantedBy=default.target
[Unit]
Description=Timer for k8s svc to unbound script - every 1 min
[Timer]
Unit=ddns_k8s_svc.service
OnCalendar=*-*-* *:*:00
[Install]
WantedBy=timers.target
Unbound DNSを実行しているノードでの準備
クラスタのcontrol-planeからddns.new
というファイルをUnbound DNS用docker composeディレクトリにscpされてきています。
❯ tree
.
|-config
| |-a-records.conf
| |-ddns.new # scp from k8s control-plane
|-docker-compose.yml
受け取ったconfig/ddns.new
の中身は以下のように、config/a-records.conf
と変わらない感じで行があると思います。
例えばweave.mydomain.net
はクライアントがアクセスする際にリバースプロキシへ向けるためのレコードで、weave.cluster.mydomain.net
はリバースプロキシが通信流す先として使用するレコードとなっています。
❯ cat config/ddns.new
local-data: "dummydata.mydomain.net. IN A 127.0.0.1"
local-data: "web-app.cluster.mydomain.net. IN A 192.168.1.201"
local-data: "weave.cluster.mydomain.net. IN A 192.168.1.200"
local-data: "web-app.mydomain.net. IN A 192.168.1.55"
local-data: "weave.mydomain.net. IN A 192.168.1.55"
それでは、最初にconfig/a-records.conf
を更新していきます。以下の2行を追記しましょう。
# dynamic
include: /opt/unbound/etc/unbound/ddns.conf
次にdocker-compose.yml
ファイルの更新です。元はconfig/a-records.conf
だけコンテナ内に渡していましたが、config/ddns.conf
ファイルも渡すようにしましょう。そして渡す先のパスは、すぐ上でconfig/a-records.conf
に変更を加えたように、追加参照するファイルのパスとなります。
version: '3'
services:
unbound:
image: mvance/unbound:1.16.2
restart: unless-stopped
container_name: landns
hostname: 'landns'
ports:
- '53:53/udp'
- '53:53'
volumes:
- './config/a-records.conf:/opt/unbound/etc/unbound/a-records.conf:ro'
- './config/ddns.conf:/opt/unbound/etc/unbound/ddns.conf:ro'
そして次は定期的に新たなconfig/ddns.new
ファイルが来ていないか確認し、config/ddns.conf
にリネームしてサービスリスタートさせるスクリプトddns_update.sh
です。
#!/bin/bash
if [ -f ./config/ddns.new ]; then
mv ./config/ddns.new ./config/ddns.conf
docker compose restart
fi
あとはこれを定期実行されるようsystemdのserviceとtimerを用意します。
ファイル内のパス{path_to_dns_docker_compose_dir}
や実行間隔は適当に変えてください。
reload_lan_dns.service
とreload_lan_dns.timer
ファイルとして/etc/systemd/system
以下に配置し、sudo systemctl daemon-reload && sudo systemctl enable reload_lan_dns.timer --now
で回し始めます。
[Unit]
Description=Reload lan-dns docker compose when config/ddns.new is received
[Service]
WorkingDirectory={path_to_dns_docker_compose_dir}
ExecStart={path_to_dns_docker_compose_dir}/ddns_update.sh
[Install]
WantedBy=default.target
[Unit]
Description=Reload timer for lan-dns
[Timer]
Unit=reload_lan_dns.service
OnCalendar=*-*-* *:*:00
[Install]
WantedBy=timers.target
リバースプロキシ設定更新の自動化
手間がかかりますがリバースプロキシに関しても同じ流れで自動化をセットアップしていきます。
つまりこういった流れです。
- クラスタ上でservice情報を確認し
- nginxのコンフィグファイルを生成し
- それらをリバースプロキシサーバを実行している端末にscpで渡し
- 以上3点を実行するスクリプトをsystemdで回し
- リバースプロキシを実行しているノードでは受け取ったコンフィグファイルに変更があればサービスをリスタートする、といったスクリプトをsystemdで以下同
Kubernetesクラスタcontrol-plane側での準備
まずは~/svc_automation
にディレクトリrp
とrp_template
を追加しましょう。
mkdir ~/svc_automation/rp ~/svc_atuomation/rp_template
NginxのコンフィグファイルはUnboundより長いので、テンプレートファイルを一つ用意し、それを各サービスの値で差し替えたファイルを出力するというやり方で用意していきます。
テンプレートはrp_template/template.conf
として用意しましょう。次の通りです。シリーズ前回手動で作業した通り、サービスが増えても差し替える箇所はserver_nameやupstream辺りのみです。
# ${svc_name}.mydomain.net server
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
server {
listen 443 ssl http2;
server_name ${svc_name}.mydomain.net;
# docker resolver - optional for docker nginx upstream/proxy
resolver 127.0.0.11 valid=30s;
# upload size
client_max_body_size 10000M;
# ssl
include /etc/nginx/cert/ssl-base.conf;
include /etc/nginx/cert/ssl-wild.conf;
location / {
proxy_http_version 1.1;
set $upstream_svc ${svc_name}.cluster.mydomain.net:${svc_port};
proxy_pass http://$upstream_svc;
proxy_set_header Host $host;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
}
}
スクリプトは次の通りです。~/svc_automation/dynamic_rp.sh
としてあります。
-
kubectl get svc -A
でサービス情報を確認 - 各サービスの値でテンプレートの特定箇所を差し替える形で、
rp/*.conf
としてコンフィグファイルを作っていく - 生成したコンフィグファイルからハッシュ値を出して控えておく
- 前回スクリプト実行時に控えておいたハッシュ値から変更があれば、リバースプロキシを実行しているノードにscpでコンフィグファイルを渡す
#!/bin/bash
# get service name and port
K8SSVCDATA=`kubectl get svc -A | awk '/LoadBalancer/ {print $2, $6}' | cut -d ":" -f 1`
logger "k8s svc data: ${K8SSVCDATA}"
# use the retrieved values to generate nginx conf files using the base template
while read -r svc_name svc_port; do
export svc_name=$svc_name
export svc_port=$svc_port
envsubst '${svc_name},${svc_port}'< rp_template/template.conf > rp/$svc_name-ve.conf
done <<< "$K8SSVCDATA"
echo `cat rp/*.conf` | sha1sum | cut -c1-32 > rp_template/live.data
live_data=`cat rp_template/live.data`
touch rp_template/conf.data
conf_data=`cat rp_template/conf.data`
# if the generated nginx conf is different
if [[ "$live_data" != "$conf_data" ]]; then
# copy generated conf files to the reverse proxy server
scp rp/*.conf 192.168.1.55:{nginx_docker_compose_dir}/conf/k8s/.
logger "k8s svc nginx conf files copied to rp remote server"
# cp live data as conf
cp rp_template/live.data rp_template/conf.data
else
logger "No change observed in k8s service rp conf files"
fi
なおこのスクリプトではscpでコンフィグファイルを渡す先がリバースプロキシのdocker compose
を実行しているディレクトリのconf/k8s
になります。スクリプトを実行する前に受け側でディレクトリを作っておきましょう。
そしてこのスクリプトを定期実行するよう、systemdのserviceファイルとtimerファイルを例えばdynamic_rp.service
、dynamic_rp.timer
といったファイル名で用意し、daemon-reload
し、enable {.timer} --now
しましょう。DNS分で用意したものと内容はほぼ同じです。ファイル名や実行するスクリプト名に注意して用意しましょう。
[Unit]
Description=scp k8s svc nginx conf files to reverse proxy server
[Service]
WorkingDirectory={home_directory}/svc_automation
ExecStart={home_directory}/svc_automation/dynamic_rp.sh
User={username}
Group={user's group name}
[Install]
WantedBy=default.target
[Unit]
Description=Timer for k8s svc to unbound script - every 1 min
[Timer]
Unit=dynamic_rp.service
OnCalendar=*-*-* *:*:00
[Install]
WantedBy=timers.target
リバースプロキシを実行しているノード側での準備
リバースプロキシのdocker compose
実行ディレクトリは以下のような感じで残っていると思いますが、コンフィグファイルをscpで渡される先としてconf/k8s
というディレクトリを新たに作成していると思います。
以下のような形になっていると思いますが、もしスクリプトをすでに実行していたらconf/k8s
内にコンフィグファイルもいくつか入っているでしょう。
❯ tree
.
|-docker-compose.yml
|-conf
| |-k8s # NEW - dir w/ scp'd conf files
| |-nginx
| | |-gitlab.conf
| |-cert
| | |-fullchain.pem
| | |-privkey.pem
| | |-ssl-base.conf
| | |-ssl-dhparams.pem
| | |-ssl-wild.conf
| |-nginx.conf
まずはconf/ngixn.conf
ファイルの更新です。
行の最後の方に/etc/nginx/conf.d/*.conf;
をincludeしているところがありますが、同様にk8s.d
ディレクトリの*.conf
ファイルもincludeするよう追記しましょう。
include /etc/nginx/conf.d/*.conf;
include /etc/nginx/k8s.d/*.conf;
そしてdocker-compose.yml
ファイルでも、conf/k8s
をコンテナ内で/etc/nginx/k8s.d
としてマウントさせるよう更新します。
services:
nginx:
image: nginx:1.24.0
container_name: nginx
ports:
- "443:443"
volumes:
- ./conf/nginx/:/etc/nginx/conf.d
- ./conf/k8s:/etc/nginx/k8s.d
- type: bind
source: ./conf/cert
target: /etc/nginx/cert
read_only: true
- type: bind
source: ./conf/nginx.conf
target: /etc/nginx/nginx.conf
read_only: true
そして、定期的に受け取ったコンフィグファイルの変更を確認しサービスのリスタートを実行するスクリプト、タイマーを用意すれば完成です。
スクリプトはrp_k8s_reload.sh
として用意しました。内容は以下です。
ちなみにDNSの方も同様ですが、コンテナを再起動しようという時に、コンフィグファイルに悪いところがあって失敗した場合のことを考えていないところが穴です。
#!/bin/bash
# This script depends on the other script running on k8s control plane
# which generates nginx conf files for the services
# running with LoadBalancer ExternalIP
#
# The nginx *.conf files are scp'd to conf/k8s directory.
#
# This script monitors the change to the *.conf files
# and execute nginx -t and nginx -s reload when change was detected.
# touch files
touch conf/k8s/monitor.data
touch conf/k8s/change.data
# generate the hash data
echo "`cat conf/k8s/*.conf`" | sha1sum | cut -c1-32 > conf/k8s/change.data
if [[ `cat conf/k8s/monitor.data` != `cat conf/k8s/change.data` ]]; then
logger "Change detected in k8s nginx conf files"
cp conf/k8s/change.data conf/k8s/monitor.data
docker exec nginx nginx -t && docker exec nginx nginx -s reload
logger "Reloaded nginx"
else
logger "No change detected"
fi
serviceとtimerファイルは例によってパスや実行間隔を適当に変更してください。sudo systemctl daemon-reload
とsudo systemctl enable rp_k8s_reload.timer --now
でセットアップ完了です。
[Unit]
Description=Reload nginx in response to k8s nginx conf change
[Service]
WorkingDirectory={path to nginx docker compose dir}
ExecStart={path to nginx docker compose dir}/rp_k8s_reload.sh
User={username}
Group={user's group name}
[Install]
WantedBy=default.target
[Unit]
Description=Timer for nginx k8s svc reload
[Timer]
Unit=rp_k8s_reload.service
OnCalendar=*:0/3
[Install]
WantedBy=timers.target
できあがり!!
weave
という名前でLoadBalancerタイプのserviceを前回作成したWeave GitOps Dashboardについては、以上の作業をする中で自動化スクリプトが走り、weave.mydomain.net
で名前解決できるようになり、https://weave.mydomain.net
でアクセスもできるようになります。
今回は特に用意するファイルも多く大変でしたが、GitLab + Kubernetes + FluxのGitOps環境でKubernetesクラスタ上のワークロードなどの立ち上げが容易になり、Metallb + シェルスクリプト + systemd + Nginx + Unboundでサービスへのアクセスのセットアップも自動化できました。
せっかく簡単にいろいろなサービスをKubernetes上でも動かしやすくなったところですが、まだデータの保持ができていません。そういうわけで次回はNFS、PVCについて触れたいと思います。
Discussion