Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ This repository uses Renovate plus a few GitHub Actions workflows to keep depend
Automatically monitors and creates PRs for:
- GitHub Actions updates
- The `TLS-tools/testssl.sh` git submodule
- Hardcoded versions in installer scripts (NGINX and its modules, Ansible, kubectl, minikube, Vagrant) via custom regex managers
- Hardcoded versions in installer scripts (NGINX and its modules, Ansible, Kubernetes, Flannel, K3s, Vagrant) via custom regex managers

**Important:** the custom regex managers match exact variable formats such as
`NGINX_VERSION="1.31.1"` (bash) and `$Script:NGINX_VERSION = '1.31.1'`
Expand Down
112 changes: 91 additions & 21 deletions kubernetes/k3s_installer.sh
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@
# so it works on any systemd-based Linux without a distro package manager.
# Kubernetes role names are used (control plane / worker); K3s' own
# "server / agent" wording is mapped internally.
#
# Control plane: sudo ./<script> --control-plane
# Worker: sudo ./<script> --worker \
# --url https://<control-plane-ip>:6443 --token <token>
#
# This file is kept identical in THectic-NL/Scripts (kubernetes/k3s_installer.sh)
# and Stensel8/DevOps-Security (kubernetes/install-k3s.sh); change both.
# Run as root.
#

Expand Down Expand Up @@ -99,16 +106,20 @@ Invoke-Cmd() {
# Usage
# ============================================================================

# Name as invoked, so help and join hints match however the file was saved.
SCRIPT_NAME=$(basename "$0")

Show-Usage() {
cat <<'EOF'
Usage: k3s_installer.sh <role> [options]
cat <<EOF
Usage: ${SCRIPT_NAME} <role> [options]

Roles:
--control-plane Install the first K3s node (control plane)
--worker Join this node to an existing cluster as a worker

Options (worker):
--url URL Control-plane API URL, or a bare IP/host (-> https://IP:6443)
--url HOST[:PORT] Control-plane API endpoint; port defaults to 6443
(an https:// prefix is accepted)
--token VALUE Join value from the control plane
(/var/lib/rancher/k3s/server/node-token)

Expand All @@ -119,8 +130,8 @@ After a --control-plane install the summary prints the join value and the
exact --worker command to run on the other nodes.

Examples:
sudo ./k3s_installer.sh --control-plane
sudo ./k3s_installer.sh --worker --url https://10.0.0.1:6443 --token K10abc...
sudo ./${SCRIPT_NAME} --control-plane
sudo ./${SCRIPT_NAME} --worker --url https://10.0.0.1:6443 --token K10abc...
EOF
}

Expand All @@ -136,20 +147,33 @@ K3S_VERSION="${K3S_VERSION:-v1.37.0+k3s1}"
Install-ControlPlane() {
Write-Log INFO "Installing K3s ${K3S_VERSION} (control plane)"

# The kubeconfig is root-only by default, so 'kubectl' fails for the sudo
# user. Hand it to that user's group (0640) instead of making it world-
# readable; the flags end up in the unit, so they survive k3s restarts.
local kube_user="${SUDO_USER:-}" kube_group=""
local -a server_args=(server)
if [[ -n "$kube_user" && "$kube_user" != root ]]; then
kube_group=$(id -gn "$kube_user")
server_args+=(--write-kubeconfig-mode 0640 --write-kubeconfig-group "$kube_group")
fi

# get.k3s.io is Rancher's official install path; it is a remote script.
Write-Log WARN "Fetching and running the official installer from https://get.k3s.io"
curl -sfL https://get.k3s.io | \
INSTALL_K3S_VERSION="$K3S_VERSION" sh -s - server >> "$LOG_FILE" 2>&1 || \
INSTALL_K3S_VERSION="$K3S_VERSION" sh -s - "${server_args[@]}" >> "$LOG_FILE" 2>&1 || \
Stop-Script "K3s install failed. Check log: $LOG_FILE"
Invoke-Cmd systemctl enable k3s

Write-Log INFO "Waiting for the node to become Ready..."
local _
for _ in $(seq 1 45); do
k3s kubectl get node 2>/dev/null | grep -q ' Ready ' && break
local _ ready=false
for _ in $(seq 1 60); do
if k3s kubectl get node --no-headers 2>/dev/null | grep -q ' Ready '; then
ready=true; break
fi
sleep 2
done
k3s kubectl get node || Write-Log WARN "Node not Ready yet; re-check with 'k3s kubectl get node'."
k3s kubectl get node || true
$ready || Stop-Script "Node not Ready after 120s. Inspect: journalctl -u k3s -n 50. Join value, once fixed: /var/lib/rancher/k3s/server/node-token"

local node_ip node_join k3s_ver
node_ip=$(hostname -I | awk '{print $1}')
Expand All @@ -160,23 +184,52 @@ Install-ControlPlane() {
Write-Log SUCCESS "K3s control plane ready!"
echo -e "${GREEN}==============================================================${NC}\n"
echo -e "${BLUE}K3s:${NC} ${GREEN}${k3s_ver}${NC}"
echo -e "${BLUE}kubeconfig:${NC} ${GREEN}/etc/rancher/k3s/k3s.yaml${NC}"
echo -e "${BLUE}API URL:${NC} ${GREEN}https://${node_ip}:6443${NC}"
echo -e "${BLUE}kubeconfig:${NC} ${GREEN}/etc/rancher/k3s/k3s.yaml${NC}${kube_group:+ (readable by group ${kube_group})}"
echo -e "${BLUE}Log:${NC} ${GREEN}${LOG_FILE}${NC}"
echo ""
echo -e "${BLUE}Join a worker:${NC}"
echo -e " sudo ./k3s_installer.sh --worker --url https://${node_ip}:6443 --token ${node_join:-(see /var/lib/rancher/k3s/server/node-token)}"
echo -e "${BOLD}Join a worker (run on each worker node)${NC}"
echo -e " sudo ./${SCRIPT_NAME} --worker \\"
echo -e " --url https://${node_ip}:6443 --token ${node_join:-(see /var/lib/rancher/k3s/server/node-token)}"
echo ""
echo -e "${BLUE}Use kubectl:${NC} export KUBECONFIG=/etc/rancher/k3s/k3s.yaml # or: k3s kubectl ..."
# The installer links kubectl to k3s, which reads /etc/rancher/k3s/k3s.yaml
# by default. It skips the link when another kubectl already exists; that
# one looks in ~/.kube/config, so it needs KUBECONFIG.
local kubectl_prefix=""
if [[ "$(readlink -f "$(command -v kubectl 2>/dev/null)" 2>/dev/null)" != "$(readlink -f "$(command -v k3s)")" ]]; then
kubectl_prefix="KUBECONFIG=/etc/rancher/k3s/k3s.yaml "
fi
if [[ -n "$kube_group" ]]; then
echo -e "${BOLD}Use kubectl (as ${kube_user}, no sudo needed)${NC}"
echo -e " ${kubectl_prefix}kubectl get nodes"
else
echo -e "${BOLD}Use kubectl${NC}"
echo -e " sudo ${kubectl_prefix}kubectl get nodes"
fi
echo ""
echo -e "${YELLOW}Cloud/firewall:${NC} nodes must reach each other on TCP 6443, TCP 10250 and"
echo -e " UDP 8472 (on AWS: a self-referencing 'All traffic' security group rule),"
echo -e " plus inbound on any NodePort you expose (30000-32767)."
}

# Usage: Install-Worker <server-url-or-ip> <join-value>
Install-Worker() {
local url=$1 join=$2

[[ -n "$url" ]] || Stop-Script "Worker needs --url https://<control-plane-ip>:6443"
[[ -n "$join" ]] || Stop-Script "Worker needs --token <value from the control plane>"
[[ "$url" == https://* ]] || url="https://${url}:6443"
local endpoint=$1 join=$2 url

[[ -n "$endpoint" ]] || Stop-Script "Worker needs --url <control-plane-ip>:6443"
[[ -n "$join" ]] || Stop-Script "Worker needs --token <value from the control plane>"
endpoint=${endpoint#https://}
endpoint=${endpoint%/}
[[ "$endpoint" == *:* ]] || endpoint="${endpoint}:6443"
[[ "$endpoint" =~ ^[A-Za-z0-9.-]+:[0-9]+$ ]] || Stop-Script "The --url value should look like 10.0.0.1:6443."
[[ "$join" =~ ^[A-Za-z0-9:._-]+$ ]] || Stop-Script "The --token value has unexpected characters."
url="https://${endpoint}"

# Fail fast when the API port is unreachable (on AWS: security group).
# /ping is served unauthenticated by the K3s supervisor and returns "pong".
Write-Log INFO "Checking that ${url} is reachable..."
curl -ksf --max-time 5 "${url}/ping" 2>/dev/null | grep -q pong || \
Stop-Script "Cannot reach ${url}/ping. Check the URL, that K3s runs on the control plane, and that the security group allows TCP 6443 from this node."

Write-Log INFO "Installing K3s ${K3S_VERSION} (worker), joining ${url}"

Expand All @@ -188,6 +241,19 @@ Install-Worker() {
Stop-Script "K3s agent install failed. Check log: $LOG_FILE"
Invoke-Cmd systemctl enable k3s-agent

# The installer returns as soon as the service starts, not when the join
# succeeds. The kubelet client cert is only issued after the server accepts
# the token, so its presence means the node registered.
Write-Log INFO "Waiting for the control plane to accept this node..."
local _ joined=false
for _ in $(seq 1 30); do
if [[ -s /var/lib/rancher/k3s/agent/client-kubelet.crt ]]; then
joined=true; break
fi
sleep 2
done
$joined || Stop-Script "Agent did not join within 60s. Inspect: journalctl -u k3s-agent -n 50 (wrong token, or UDP 8472 / TCP 10250 blocked?)"

local k3s_ver
k3s_ver=$(k3s --version 2>/dev/null | head -n1) || k3s_ver="N/A"

Expand All @@ -198,7 +264,11 @@ Install-Worker() {
echo -e "${BLUE}Joined:${NC} ${GREEN}${url}${NC}"
echo -e "${BLUE}Log:${NC} ${GREEN}${LOG_FILE}${NC}"
echo ""
echo -e "${BLUE}Verify:${NC} k3s kubectl get node # run on the control plane"
echo -e "${BOLD}Verify on the control plane${NC}"
echo -e " kubectl get nodes # this node should be Ready within ~30s"
echo ""
echo -e "${YELLOW}Note:${NC} kubectl does not work on a worker (no kubeconfig here, so it"
echo -e " falls back to localhost:8080). Manage the cluster from the control plane."
}

# ============================================================================
Expand Down Expand Up @@ -237,7 +307,7 @@ Test-Root

if command -v k3s &>/dev/null; then
Write-Log WARN "K3s is already installed: $(k3s --version | head -n1)"
Write-Log INFO "Remove it with k3s-uninstall.sh or k3s-agent-uninstall.sh first."
Write-Log INFO "Remove it first: k3s-uninstall.sh (control plane) or k3s-agent-uninstall.sh (worker)."
exit 0
fi

Expand Down
Loading
Loading