Files

288 lines
12 KiB
Bash
Executable File

#!/usr/bin/env bash
set -Eeuo pipefail
readonly ROOT="$(cd -- "$(dirname -- "$BASH_SOURCE")/../.." && pwd -P)"
readonly HOST_SOURCE="$ROOT/infrastructure/networking/private-dns/host"
readonly K8S_SOURCE="$ROOT/infrastructure/networking/private-dns/kubernetes"
readonly LAN_IP="192.168.0.107"
readonly TAIL_IP="100.92.240.34"
readonly TARGET_NODE="donghyeon-system-product-name"
readonly BUSYBOX_IMAGE="docker.io/library/busybox:1.37.0@sha256:7a3ebe5bfd1a4a19797d20b0c0bb39d44393e9a03fd852c0865b0f540d868df0"
readonly -a PRIVATE_HOSTS=(git.learn.hyeonworks.com id.learn.hyeonworks.com storage-admin.learn.hyeonworks.com db-admin.learn.hyeonworks.com grafana.learn.hyeonworks.com)
readonly -a PUBLIC_PRIVATE_HOSTS=(storage-admin.learn.hyeonworks.com db-admin.learn.hyeonworks.com grafana.learn.hyeonworks.com)
execute=false
mutation=false
rollback_armed=false
temp=""
lan_was_active=false
tail_was_active=false
lan_was_enabled=false
tail_was_enabled=false
fail() {
printf 'ERROR: %s\n' "$*" >&2
exit 1
}
usage() {
cat <<'USAGE'
사용법:
bash scripts/bootstrap/apply-private-dns.sh
bash scripts/bootstrap/apply-private-dns.sh --execute
인자 없이 실행하면 설정, 주소, 공개 DNS, manifest hash만 검사합니다.
--execute는 두 dnsmasq 인스턴스와 coredns-custom을 적용합니다.
공유기 DHCP DNS와 Tailscale 관리 화면은 변경하지 않습니다.
USAGE
}
if (( $# == 0 )); then
:
elif (( $# == 1 )) && [[ "$1" == "-h" || "$1" == "--help" ]]; then
usage
exit 0
elif (( $# == 1 )) && [[ "$1" == "--execute" ]]; then
execute=true
else
usage >&2
exit 2
fi
for cmd in awk dig find ip install journalctl kubectl rg sha256sum sleep ss stat systemctl systemd-analyze; do
command -v "$cmd" >/dev/null 2>&1 || fail "$cmd 명령이 필요합니다"
done
[[ -x /usr/sbin/dnsmasq ]] || fail "/usr/sbin/dnsmasq가 없습니다"
[[ "$(pwd -P)" == "$ROOT" ]] || fail "$ROOT에서 실행하세요"
for file in dnsmasq-lan.conf dnsmasq-tailscale.conf \
hyeonworks-dnsmasq-lan.service hyeonworks-dnsmasq-tailscale.service; do
[[ -f "$HOST_SOURCE/$file" && ! -L "$HOST_SOURCE/$file" ]] || fail "후보 파일이 없습니다: $file"
done
/usr/sbin/dnsmasq --test --conf-file="$HOST_SOURCE/dnsmasq-lan.conf"
/usr/sbin/dnsmasq --test --conf-file="$HOST_SOURCE/dnsmasq-tailscale.conf"
systemd-analyze verify "$HOST_SOURCE/hyeonworks-dnsmasq-lan.service" \
"$HOST_SOURCE/hyeonworks-dnsmasq-tailscale.service"
kubectl kustomize "$K8S_SOURCE" >"/tmp/private-dns-render.$$"
trap 'rm -f -- "/tmp/private-dns-render.$$"' EXIT
manifest_sha="$(sha256sum "/tmp/private-dns-render.$$" | awk '{print $1}')"
ip -4 address show | rg -q -F "$LAN_IP/" || fail "LAN 주소 $LAN_IP가 호스트에 없습니다"
ip -4 address show | rg -q -F "$TAIL_IP/" || fail "Tailscale 주소 $TAIL_IP가 호스트에 없습니다"
[[ "$(kubectl get node "$TARGET_NODE" -o jsonpath='{.status.conditions[?(@.type=="Ready")].status}')" == True ]] || \
fail "대상 노드가 Ready가 아닙니다"
for name in "${PUBLIC_PRIVATE_HOSTS[@]}"; do
public_a="$(dig +short @1.1.1.1 A "$name" | tr -d '[:space:]')"
public_aaaa="$(dig +short @1.1.1.1 AAAA "$name" | tr -d '[:space:]')"
[[ -z "$public_a" && -z "$public_aaaa" ]] || fail "$name 공개 A/AAAA가 존재합니다"
done
printf 'Current context: %s\n' "$(kubectl config current-context)"
printf 'LAN listener: %s:53\n' "$LAN_IP"
printf 'Tail listener: %s:53\n' "$TAIL_IP"
printf 'CoreDNS SHA-256: %s\n' "$manifest_sha"
printf '공개 private-service A/AAAA: 없음\n'
if [[ "$execute" == false ]]; then
printf 'DRY RUN PASS: --execute를 지정하지 않아 변경하지 않았습니다.\n'
exit 0
fi
[[ -t 0 ]] || fail "--execute는 대화형 터미널이 필요합니다"
context="$(kubectl config current-context)"
api="$(kubectl config view --minify -o jsonpath='{.clusters[0].cluster.server}')"
printf 'Type APPLY %s to install private DNS: ' "$context"
read -r answer
[[ "$answer" == "APPLY $context" ]] || fail "취소했습니다"
[[ "$(kubectl config current-context)" == "$context" ]] || fail "context가 바뀌었습니다"
[[ "$(kubectl config view --minify -o jsonpath='{.clusters[0].cluster.server}')" == "$api" ]] || \
fail "API server가 바뀌었습니다"
sudo -v
managed_listeners_are_exact() {
local lan_pid tail_pid line protocol local_address
local lan_tcp=0 lan_udp=0 tail_tcp=0 tail_udp=0
local -a listener_lines=()
sudo systemctl is-active --quiet hyeonworks-dnsmasq-lan.service || return 1
sudo systemctl is-active --quiet hyeonworks-dnsmasq-tailscale.service || return 1
lan_pid="$(sudo systemctl show hyeonworks-dnsmasq-lan.service --property MainPID --value)" || return 1
tail_pid="$(sudo systemctl show hyeonworks-dnsmasq-tailscale.service --property MainPID --value)" || return 1
[[ "$lan_pid" =~ ^[1-9][0-9]*$ && "$tail_pid" =~ ^[1-9][0-9]*$ ]] || return 1
sudo cmp -s "$HOST_SOURCE/dnsmasq-lan.conf" /etc/dnsmasq-hyeonworks/lan.conf || return 1
sudo cmp -s "$HOST_SOURCE/dnsmasq-tailscale.conf" /etc/dnsmasq-hyeonworks/tailscale.conf || return 1
sudo cmp -s "$HOST_SOURCE/hyeonworks-dnsmasq-lan.service" \
/etc/systemd/system/hyeonworks-dnsmasq-lan.service || return 1
sudo cmp -s "$HOST_SOURCE/hyeonworks-dnsmasq-tailscale.service" \
/etc/systemd/system/hyeonworks-dnsmasq-tailscale.service || return 1
mapfile -t listener_lines < <(sudo ss -H -lnupt '( sport = :53 )') || return 1
for line in "${listener_lines[@]}"; do
read -r protocol _ _ _ local_address _ <<<"$line"
case "$local_address" in
"$LAN_IP:53")
[[ "$line" == *"pid=$lan_pid,"* ]] || return 1
[[ "$protocol" == tcp ]] && lan_tcp=$((lan_tcp + 1))
[[ "$protocol" == udp ]] && lan_udp=$((lan_udp + 1))
;;
"$TAIL_IP:53")
[[ "$line" == *"pid=$tail_pid,"* ]] || return 1
[[ "$protocol" == tcp ]] && tail_tcp=$((tail_tcp + 1))
[[ "$protocol" == udp ]] && tail_udp=$((tail_udp + 1))
;;
esac
done
(( lan_tcp == 1 && lan_udp == 1 && tail_tcp == 1 && tail_udp == 1 ))
}
if sudo ss -H -lntu '( sport = :53 )' | awk -v lan="$LAN_IP:53" -v tail="$TAIL_IP:53" \
'$5 == lan || $5 == tail {found=1} END {exit found ? 0 : 1}'; then
if managed_listeners_are_exact; then
printf '기존 exact managed DNS listener를 안전한 재적용 대상으로 확인했습니다.\n'
else
fail "대상 LAN/Tailscale 주소의 53번 포트를 exact managed listener가 아닌 프로세스가 사용 중입니다"
fi
fi
umask 077
temp="$(mktemp -d /tmp/platform-private-dns.XXXXXX)"
cm_existed=false
sudo systemctl is-active --quiet hyeonworks-dnsmasq-lan.service && lan_was_active=true
sudo systemctl is-active --quiet hyeonworks-dnsmasq-tailscale.service && tail_was_active=true
[[ "$(sudo systemctl is-enabled hyeonworks-dnsmasq-lan.service 2>/dev/null || true)" == enabled ]] && lan_was_enabled=true
[[ "$(sudo systemctl is-enabled hyeonworks-dnsmasq-tailscale.service 2>/dev/null || true)" == enabled ]] && tail_was_enabled=true
[[ -e /etc/dnsmasq-hyeonworks ]] && sudo cp -a /etc/dnsmasq-hyeonworks "$temp/etc-dnsmasq"
[[ -e /etc/systemd/system/hyeonworks-dnsmasq-lan.service ]] && \
sudo cp -a /etc/systemd/system/hyeonworks-dnsmasq-lan.service "$temp/lan.service"
[[ -e /etc/systemd/system/hyeonworks-dnsmasq-tailscale.service ]] && \
sudo cp -a /etc/systemd/system/hyeonworks-dnsmasq-tailscale.service "$temp/tailscale.service"
if kubectl -n kube-system get configmap coredns-custom -o yaml >"$temp/coredns-custom.yaml" 2>/dev/null; then
cm_existed=true
fi
restore_service_state() {
local unit=$1 was_active=$2 was_enabled=$3
if [[ "$was_enabled" == true ]]; then
sudo systemctl enable "$unit" >/dev/null 2>&1
else
sudo systemctl disable "$unit" >/dev/null 2>&1 || true
fi
if [[ "$was_active" == true ]]; then
sudo systemctl start "$unit" >/dev/null 2>&1
else
sudo systemctl stop "$unit" >/dev/null 2>&1 || true
fi
}
rollback() {
set +e
printf '\nROLLBACK: private DNS 이전 상태를 복원합니다.\n' >&2
sudo systemctl disable --now hyeonworks-dnsmasq-lan.service \
hyeonworks-dnsmasq-tailscale.service >/dev/null 2>&1
if [[ -d "$temp/etc-dnsmasq" ]]; then
sudo rm -rf -- /etc/dnsmasq-hyeonworks
sudo cp -a "$temp/etc-dnsmasq" /etc/dnsmasq-hyeonworks
else
sudo rm -rf -- /etc/dnsmasq-hyeonworks
fi
for unit in lan tailscale; do
target="/etc/systemd/system/hyeonworks-dnsmasq-$unit.service"
if [[ -f "$temp/$unit.service" ]]; then
sudo install -o root -g root -m 0644 "$temp/$unit.service" "$target"
else
sudo rm -f -- "$target"
fi
done
sudo systemctl daemon-reload
restore_service_state hyeonworks-dnsmasq-lan.service "$lan_was_active" "$lan_was_enabled"
restore_service_state hyeonworks-dnsmasq-tailscale.service "$tail_was_active" "$tail_was_enabled"
if [[ "$cm_existed" == true ]]; then
kubectl apply -f "$temp/coredns-custom.yaml" >/dev/null
else
kubectl -n kube-system delete configmap coredns-custom --ignore-not-found >/dev/null
fi
kubectl -n kube-system rollout restart deployment/coredns >/dev/null
kubectl -n kube-system rollout status deployment/coredns --timeout=90s >/dev/null
rollback_armed=false
printf 'ROLLBACK complete.\n' >&2
}
finish() {
rc=$?
trap - EXIT INT TERM
if (( rc != 0 )) && [[ "$rollback_armed" == true ]]; then
rollback
fi
if [[ -n "$temp" ]]; then
case "$temp" in
/tmp/platform-private-dns.*) rm -rf -- "$temp" ;;
esac
fi
rm -f -- "/tmp/private-dns-render.$$"
exit "$rc"
}
trap finish EXIT
trap 'exit 130' INT
trap 'exit 143' TERM
rollback_armed=true
mutation=true
sudo install -d -o root -g root -m 0755 /etc/dnsmasq-hyeonworks
sudo install -o root -g root -m 0644 "$HOST_SOURCE/dnsmasq-lan.conf" /etc/dnsmasq-hyeonworks/lan.conf
sudo install -o root -g root -m 0644 "$HOST_SOURCE/dnsmasq-tailscale.conf" /etc/dnsmasq-hyeonworks/tailscale.conf
sudo install -o root -g root -m 0644 "$HOST_SOURCE/hyeonworks-dnsmasq-lan.service" \
/etc/systemd/system/hyeonworks-dnsmasq-lan.service
sudo install -o root -g root -m 0644 "$HOST_SOURCE/hyeonworks-dnsmasq-tailscale.service" \
/etc/systemd/system/hyeonworks-dnsmasq-tailscale.service
sudo systemctl daemon-reload
sudo systemctl enable --now hyeonworks-dnsmasq-lan.service hyeonworks-dnsmasq-tailscale.service
stable=0
for (( attempt=1; attempt<=20; attempt++ )); do
if sudo systemctl is-active --quiet hyeonworks-dnsmasq-lan.service \
hyeonworks-dnsmasq-tailscale.service \
&& [[ "$(dig +time=1 +tries=1 +short "@$LAN_IP" git.learn.hyeonworks.com A | tail -n1)" == "$LAN_IP" ]] \
&& [[ "$(dig +time=1 +tries=1 +short "@$TAIL_IP" git.learn.hyeonworks.com A | tail -n1)" == "$TAIL_IP" ]]; then
stable=$((stable + 1))
if (( stable >= 3 )); then
break
fi
else
stable=0
fi
sleep 0.5
done
if (( stable < 3 )); then
for unit in hyeonworks-dnsmasq-lan.service hyeonworks-dnsmasq-tailscale.service; do
sudo systemctl status "$unit" --no-pager -n 20 >&2 || true
sudo journalctl -u "$unit" --no-pager -n 20 >&2 || true
done
fail "private DNS listener가 안정화되지 않았습니다"
fi
kubectl apply -f "/tmp/private-dns-render.$$"
kubectl -n kube-system rollout restart deployment/coredns
kubectl -n kube-system rollout status deployment/coredns --timeout=90s
for resolver in "$LAN_IP" "$TAIL_IP"; do
for name in "${PRIVATE_HOSTS[@]}"; do
[[ "$(dig +short "@$resolver" A "$name" | tail -n1)" == "$resolver" ]] ||
fail "$name private DNS 검증 실패"
done
done
pod="private-dns-smoke-$(date +%H%M%S)"
kubectl -n default run "$pod" --restart=Never --image="$BUSYBOX_IMAGE" \
--labels=platform.hyeonworks.com/transient=true \
--command -- sh -c 'nslookup git.learn.hyeonworks.com >/dev/null && nslookup id.learn.hyeonworks.com >/dev/null && nslookup storage-admin.learn.hyeonworks.com >/dev/null && nslookup db-admin.learn.hyeonworks.com >/dev/null && nslookup grafana.learn.hyeonworks.com >/dev/null'
kubectl -n default wait --for=jsonpath='{.status.phase}'=Succeeded "pod/$pod" --timeout=60s
kubectl -n default delete "pod/$pod" --wait=true >/dev/null
rollback_armed=false
printf 'PRIVATE DNS APPLY SUCCESS\n'
printf '공유기 DHCP DNS와 Tailscale split DNS는 문서에 따라 별도로 등록하세요.\n'