288 lines
12 KiB
Bash
Executable File
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'
|