🌟

Kubernetesサービスへのウェブアクセス自動実装 - おうちでGitOpsシリーズ投稿5/6

2023/06/28に公開

こちらはおうちラボで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)
  • 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

More on Docker - Series Top: Dockerで作るおうちLAN遊び場 シリーズ1/7

Interested in getting your own DNS domain?

自動化タスクの流れ

DNSとリバースプロキシどちらでも同じ流れですが、簡単に並べると次の通りです。

  1. Kubernetesクラスタのcontrol-planeノードにて
    1. Kubernetesクラスタ上のservice情報確認
    2. コンフィグファイルを生成
    3. DNS・リバースプロキシサーバへ送る
  2. DockerでDNS・リバースプロキシサーバを走らせているノードにて
    1. 受け取ったコンフィグに変更があればサービスをリロード

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に追記
  • 生成したunbound DNS用のコンフィグファイルddns.txtのハッシュ値を確認
  • (前回スクリプト実行時に作成されたデータとしての)ddns.confのハッシュ値も確認
  • 前回から変更があるかチェック
    • もし変更があれば
      • ddns.txtddns.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ファイルのUserGroup
に関しては鍵認証でのscpが通るユーザにしてください。WorkingDirectoryExecStartにあるパス情報も環境に合わせて入力してください。/home/myusername/svc_automationなどになります。

これらのファイルが準備できたら、sudo systemctl daemon-reloadsudo 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.servicereload_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にディレクトリrprp_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.servicedynamic_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-reloadsudo 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