Перейти к содержанию

Kubernetes hard way

Любой человек, активно работающий с k8s, рано или поздно приходит к пониманию необходимости пройти дорогу самостоятельного разворачивания кластера.

Скорее всего в этот момент быстрый поиск приведет вас к инструментам на подобии kubeadm, kubespray, rke. Как правило, развернув кластер с их помощью природная любознательность утоляется и мы можем вернуться к своим рутинным задачам, но иногда нам хочется узнать как же глубока кроличья нора.

Вот именно для людей, чей интерес невозможно утолить распатроненными средствами автоматизации, посвящена эта статья.

Под катом вас ждет хардкорный гайд, который поможет вам собрать свой кластер k8s используя только linux и пресловутые 5 бинарей.

Предупреждение

Данное руководство нельзя воспринимать как руководство подготовки промышленного кластера. Относитесь к нему как к лабораторной работе.

Что нам понадобится

Чеклист

  • Две виртуальные машины с Ubuntu 22.04
  • Настроенный ntp на этих машинах
  • У машин должен быть доступ к сети интернет
  • Настроенный hostname и fqdn

Системные требования для машин

Параметр Минимальная конфигурация
CPU (vCPU) 4
RAM (GB) 8
Storage (GB) 30

Подготовка машин

Nota bene

В данной заметки мы будем использовать 2 виртуальные машины

10.0.138.179 с хостнеймом k8shw-1 (воркер)

10.0.138.180 с хостнеймом k8shw-0 (контролплейн)

Обновление и установка пакетов

На каждой машине выполняем

Bash
apt update && apt upgrade -y && sudo swapoff -a

После чего вытягиваем kubectl на машины

Bash
curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"
sudo install -o root -g root -m 0755 kubectl /usr/local/bin/kubectl

Проверяем что у нас есть настроенный ntp

Bash
timedatectl timesync-status 

Добавляем машины в хосты

На каждой машине необходимо добавить в файл /etc/hosts информацию о наших ВМ

Bash
10.0.138.179 k8shw-1

10.0.138.180 k8shw-0

Служебные утилиты

Для работы kubelet нам потребуются зависимости, установим их из официального репозитория.

Для начала добавим этот репозиторий:

Bash
sudo mkdir -p /etc/apt/keyrings
curl -fsSL https://pkgs.k8s.io/core:/stable:/${KUBE_LATEST}/deb/Release.key | sudo gpg --dearmor -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg
echo "deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://pkgs.k8s.io/core:/stable:/${KUBE_LATEST}/deb/ /" | sudo tee /etc/apt/sources.list.d/kubernetes.list
Далее обновим кеш пакетного менеджера и запустим установку
Bash
sudo apt update
sudo apt install -y containerd kubernetes-cni kubectl ipvsadm ipset
После чего нам нужно обновить конфигурацию containerd:

Bash
sudo mkdir -p /etc/containerd
containerd config default | sed 's/SystemdCgroup = false/SystemdCgroup = true/' | sudo tee /etc/containerd/config.toml
Активируем автостарт containerd и запускаем его
Bash
sudo systemctl enable containerd
sudo systemctl restart containerd

Подготавливаем переменные окружения

Предупреждение

Обратите внимание, эти переменные окружения активно используются практически всеми командами ниже. Всегда проверяйте что переменные корректно экспортированы на сервере где вы работаете.

Bash
export MASTER_1=$(dig +short k8shw-0)

export WORKER_1=$(dig +short k8shw-1)

export SERVICE_CIDR=172.21.0.0/16

export POD_CIDR=172.20.0.0/16

export API_SERVICE=$(echo $SERVICE_CIDR | awk 'BEGIN {FS="."} ; { printf("%s.%s.%s.1", $1, $2, $3) }')

export KUBE_LATEST=$(curl -L -s https://dl.k8s.io/release/stable.txt | awk 'BEGIN { FS="." } { printf "%s.%s", $1, $2 }')

export KUBE_VERSION=$(curl -L -s https://dl.k8s.io/release/stable.txt)

export CLUSTER_DNS=$(echo $SERVICE_CIDR | awk 'BEGIN {FS="."} ; { printf("%s.%s.%s.10", $1, $2, $3) }')

export ETCD_VERSION="v3.5.10"

export ENCRYPTION_KEY=$(head -c 32 /dev/urandom | base64)

Работа с сертификатами

В этой главе мы создадим pki инфраструктуру для нашего будущего кластера, создадим сертификаты для всех нужных компонентов и административный сертификат.

В рамках этой статьи мы будем использовать openssl, однако вы можете использовать например утилиту cfssl

Nota bene

Действия этой главы мы выполняем на k8shw-0 который будет нашим controlplane

CA

Для того чтобы иметь возможность выпускать сертификаты, нам необходимо сгенерировать так называемый Certificate Authority (CA), для этого на машине k8shw-0 выполним следующие команды:

Bash
openssl genrsa -out ca.key 2048
openssl req -new -key ca.key -subj "/CN=KUBERNETES-CA/O=Kubernetes" -out ca.csr
openssl x509 -req -in ca.csr -signkey ca.key -CAcreateserial  -out ca.crt -days 1000
в результате мы получим 3 файла:
Bash
ca.crt
ca.csr
ca.key

Для работы нам будут нужны ca.crt и ca.key

Предупреждение

Утечка ca.key является критической проблемой безопасности, будьте осторожны.

Далее мы можем приступить к генерации остальных сертификатов

Административный сертификат

Этот сертификат будет использоваться для задач администрирования кластера, процесс генерации выполняется с помощью следующих команд:

Bash
openssl genrsa -out admin.key 2048
openssl req -new -key admin.key -subj "/CN=admin/O=system:masters" -out admin.csr
openssl x509 -req -in admin.csr -CA ca.crt -CAkey ca.key -CAcreateserial  -out admin.crt -days 1000
Теперь мы можем приступить к генерации сертификатов компонентов k8s

Сертификаты компонентов кластера

Для некоторых компонентов нужны сертификаты содержащие в себе большое количество полей, для этих компонентов мы будем создавать специальный конфигурационный файл, например как тут

Controller Manager

Bash
openssl genrsa -out kube-controller-manager.key 2048
openssl req -new -key kube-controller-manager.key -subj "/CN=system:kube-controller-manager/O=system:kube-controller-manager" -out kube-controller-manager.csr
openssl x509 -req -in kube-controller-manager.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out kube-controller-manager.crt -days 1000

Kube Proxy

Bash
  openssl genrsa -out kube-proxy.key 2048
  openssl req -new -key kube-proxy.key -subj "/CN=system:kube-proxy/O=system:node-proxier" -out kube-proxy.csr
  openssl x509 -req -in kube-proxy.csr -CA ca.crt -CAkey ca.key -CAcreateserial  -out kube-proxy.crt -days 1000

Scheduler Client

Bash
openssl genrsa -out kube-scheduler.key 2048
openssl req -new -key kube-scheduler.key -subj "/CN=system:kube-scheduler/O=system:kube-scheduler" -out kube-scheduler.csr
openssl x509 -req -in kube-scheduler.csr -CA ca.crt -CAkey ca.key -CAcreateserial  -out kube-scheduler.crt -days 1000

Kubernetes API Server

Bash
cat > kubeapi.cnf <<EOF
[req]
req_extensions = v3_req
distinguished_name = req_distinguished_name
[req_distinguished_name]
[v3_req]
basicConstraints = critical, CA:FALSE
keyUsage = critical, nonRepudiation, digitalSignature, keyEncipherment
extendedKeyUsage = serverAuth
subjectAltName = @alt_names
[alt_names]
DNS.1 = kubernetes
DNS.2 = kubernetes.default
DNS.3 = kubernetes.default.svc
DNS.4 = kubernetes.default.svc.cluster
DNS.5 = kubernetes.default.svc.cluster.local
IP.1 = ${API_SERVICE}
IP.2 = ${MASTER_1}
IP.5 = 127.0.0.1
EOF
Bash
  openssl genrsa -out kube-apiserver.key 2048
  openssl req -new -key kube-apiserver.key -subj "/CN=kube-apiserver/O=Kubernetes" -out kube-apiserver.csr -config kubeapi.cnf
  openssl x509 -req -in kube-apiserver.csr -CA ca.crt -CAkey ca.key -CAcreateserial  -out kube-apiserver.crt -extensions v3_req -extfile kubeapi.cnf -days 1000

Kubelet Certificate

Bash
cat > openssl-kubelet.cnf <<EOF
[req]
req_extensions = v3_req
distinguished_name = req_distinguished_name
[req_distinguished_name]
[v3_req]
basicConstraints = critical, CA:FALSE
keyUsage = critical, nonRepudiation, digitalSignature, keyEncipherment
extendedKeyUsage = clientAuth
EOF
Bash
openssl genrsa -out apiserver-kubelet-client.key 2048
openssl req -new -key apiserver-kubelet-client.key -subj "/CN=kube-apiserver-kubelet-client/O=system:masters" -out apiserver-kubelet-client.csr -config openssl-kubelet.cnf
openssl x509 -req -in apiserver-kubelet-client.csr -CA ca.crt -CAkey ca.key -CAcreateserial  -out apiserver-kubelet-client.crt -extensions v3_req -extfile openssl-kubelet.cnf -days 1000

ETCD Server

Bash
cat > openssl-etcd.cnf <<EOF
[req]
req_extensions = v3_req
distinguished_name = req_distinguished_name
[req_distinguished_name]
[ v3_req ]
basicConstraints = CA:FALSE
keyUsage = nonRepudiation, digitalSignature, keyEncipherment
subjectAltName = @alt_names
[alt_names]
IP.1 = ${MASTER_1}
IP.3 = 127.0.0.1
EOF
Bash
  openssl genrsa -out etcd-server.key 2048
  openssl req -new -key etcd-server.key -subj "/CN=etcd-server/O=Kubernetes" -out etcd-server.csr -config openssl-etcd.cnf
  openssl x509 -req -in etcd-server.csr -CA ca.crt -CAkey ca.key -CAcreateserial  -out etcd-server.crt -extensions v3_req -extfile openssl-etcd.cnf -days 1000

Service Account

Bash
  openssl genrsa -out service-account.key 2048
  openssl req -new -key service-account.key -subj "/CN=service-accounts/O=Kubernetes" -out service-account.csr
  openssl x509 -req -in service-account.csr -CA ca.crt -CAkey ca.key -CAcreateserial  -out service-account.crt -days 1000

Kubeconfig

В целом операции довольно типичны, мы не будем подробно на них подробно останавливаться. Основной концепт связан с тем что с помощью kubectl мы создаём новые kubeconfig

admin

Bash
kubectl config set-cluster kubernetes-the-hard-way \
    --certificate-authority=ca.crt \
    --embed-certs=true \
    --server=https://${MASTER_1}:6443 \
    --kubeconfig=admin.kubeconfig
kubectl config set-credentials admin \
    --client-certificate=admin.crt \
    --client-key=admin.key \
    --embed-certs=true \
    --kubeconfig=admin.kubeconfig
kubectl config set-context default \
    --cluster=kubernetes-the-hard-way \
    --user=admin \
    --kubeconfig=admin.kubeconfig
kubectl config use-context default --kubeconfig=admin.kubeconfig

kube-proxy

Bash
kubectl config set-cluster kubernetes-the-hard-way \
    --certificate-authority=/var/lib/kubernetes/pki/ca.crt \
    --server=https://${MASTER_1}:6443 \
    --kubeconfig=kube-proxy.kubeconfig
kubectl config set-credentials system:kube-proxy \
  --client-certificate=/var/lib/kubernetes/pki/kube-proxy.crt \
  --client-key=/var/lib/kubernetes/pki/kube-proxy.key \
  --kubeconfig=kube-proxy.kubeconfig
kubectl config set-context default \
  --cluster=kubernetes-the-hard-way \
  --user=system:kube-proxy \
  --kubeconfig=kube-proxy.kubeconfig
kubectl config use-context default --kubeconfig=kube-proxy.kubeconfig

kube-controller-manager

Bash
kubectl config set-cluster kubernetes-the-hard-way \
  --certificate-authority=/var/lib/kubernetes/pki/ca.crt \
  --server=https://127.0.0.1:6443 \
  --kubeconfig=kube-controller-manager.kubeconfig
kubectl config set-credentials system:kube-controller-manager \
  --client-certificate=/var/lib/kubernetes/pki/kube-controller-manager.crt \
  --client-key=/var/lib/kubernetes/pki/kube-controller-manager.key \
  --kubeconfig=kube-controller-manager.kubeconfig
kubectl config set-context default \
  --cluster=kubernetes-the-hard-way \
  --user=system:kube-controller-manager \
  --kubeconfig=kube-controller-manager.kubeconfig
kubectl config use-context default --kubeconfig=kube-controller-manager.kubeconfig

kube-scheduler

Bash
kubectl config set-cluster kubernetes-the-hard-way \
    --certificate-authority=/var/lib/kubernetes/pki/ca.crt \
    --server=https://127.0.0.1:6443 \
    --kubeconfig=kube-scheduler.kubeconfig
kubectl config set-credentials system:kube-scheduler \
    --client-certificate=/var/lib/kubernetes/pki/kube-scheduler.crt \
    --client-key=/var/lib/kubernetes/pki/kube-scheduler.key \
    --kubeconfig=kube-scheduler.kubeconfig
kubectl config set-context default \
    --cluster=kubernetes-the-hard-way \
    --user=system:kube-scheduler \
    --kubeconfig=kube-scheduler.kubeconfig
kubectl config use-context default --kubeconfig=kube-scheduler.kubeconfig

Запуск компонентов Controlplane

ETCD

Шифрование

Для того чтобы etcd шифровал секреты, необходимо подготовить специальный конфигурационный файл:

Bash
cat > encryption-config.yaml <<EOF
kind: EncryptionConfig
apiVersion: v1
resources:
  - resources:
      - secrets
    providers:
      - aescbc:
          keys:
            - name: key1
              secret: ${ENCRYPTION_KEY}
      - identity: {}
EOF

Установка ETCD

Качаем бинарник etcd:

Bash
wget -q --show-progress --https-only --timestamping \
  "https://github.com/coreos/etcd/releases/download/${ETCD_VERSION}/etcd-${ETCD_VERSION}-linux-amd64.tar.gz"
Распаковываем архив:
Bash
tar -xvf etcd-${ETCD_VERSION}-linux-amd64.tar.gz
Bash
sudo mv etcd-${ETCD_VERSION}-linux-amd64/etcd* /usr/local/bin/
Далее нам нужно подготовить сервер к работе etcd, создать нужные директории, скопировать сертификаты, расставить права:
Bash
sudo mkdir -p /etc/etcd /var/lib/etcd /var/lib/kubernetes/pki
sudo cp etcd-server.key etcd-server.crt /etc/etcd/
sudo cp ca.crt /var/lib/kubernetes/pki/
sudo chown root:root /etc/etcd/*
sudo chmod 600 /etc/etcd/*
sudo chown root:root /var/lib/kubernetes/pki/*
sudo chmod 600 /var/lib/kubernetes/pki/*
sudo ln -s /var/lib/kubernetes/pki/ca.crt /etc/etcd/ca.crt
После этого переходим к созданию systemd unit:
Bash
ETCD_NAME=$(hostname -s)
cat <<EOF | sudo tee /etc/systemd/system/etcd.service
[Unit]
Description=etcd
Documentation=https://github.com/coreos

[Service]
ExecStart=/usr/local/bin/etcd \\
  --name ${ETCD_NAME} \\
  --cert-file=/etc/etcd/etcd-server.crt \\
  --key-file=/etc/etcd/etcd-server.key \\
  --peer-cert-file=/etc/etcd/etcd-server.crt \\
  --peer-key-file=/etc/etcd/etcd-server.key \\
  --trusted-ca-file=/etc/etcd/ca.crt \\
  --peer-trusted-ca-file=/etc/etcd/ca.crt \\
  --peer-client-cert-auth \\
  --client-cert-auth \\
  --initial-advertise-peer-urls https://${MASTER_1}:2380 \\
  --listen-peer-urls https://${MASTER_1}:2380 \\
  --listen-client-urls https://${MASTER_1}:2379,https://127.0.0.1:2379 \\
  --advertise-client-urls https://${MASTER_1}:2379 \\
  --initial-cluster-token etcd-cluster-0 \\
  --initial-cluster ${ETCD_NAME}=https://${MASTER_1}:2380 \\
  --initial-cluster-state new \\
  --data-dir=/var/lib/etcd
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target
EOF
Нам осталось только запустить etcd и настроить его автостарт:
Bash
sudo systemctl daemon-reload
sudo systemctl enable etcd
sudo systemctl start etcd

Nota bene

Для проверки работоспособности etcd можно посмотреть логи, для этого выполните следующую команду:

Bash
  sudo journalctl -u etcd -f

Control plane

Для начала нам необходимо получить бинарники компонентов:

Bash
wget -q --show-progress --https-only --timestamping \
  "https://dl.k8s.io/release/${KUBE_VERSION}/bin/linux/amd64/kube-apiserver" \
  "https://dl.k8s.io/release/${KUBE_VERSION}/bin/linux/amd64/kube-controller-manager" \
  "https://dl.k8s.io/release/${KUBE_VERSION}/bin/linux/amd64/kube-scheduler" \
Далее подготавливаем сервер к работе:
Bash
sudo chmod +x kube-apiserver kube-controller-manager kube-scheduler 
sudo mv kube-apiserver kube-controller-manager kube-scheduler /usr/local/bin/
sudo cp encryption-config.yaml /var/lib/kubernetes/
sudo mkdir -p /var/lib/kubernetes/pki
sudo cp ca.crt ca.key /var/lib/kubernetes/pki
Копируем ключи и сертификаты:

Bash
for c in kube-apiserver service-account apiserver-kubelet-client etcd-server kube-scheduler kube-controller-manager
 do
   sudo mv "$c.crt" "$c.key" /var/lib/kubernetes/pki/
 done
Выставляем права:
Bash
sudo chown root:root /var/lib/kubernetes/pki/*
sudo chmod 600 /var/lib/kubernetes/pki/*
на этом общая подготовка компонентов завершена, переходим к настройке конкретных компонентов

Kube-apiserver

Создаём systemd unit:

Bash
cat <<EOF | sudo tee /etc/systemd/system/kube-apiserver.service
[Unit]
Description=Kubernetes API Server
Documentation=https://github.com/kubernetes/kubernetes

[Service]
ExecStart=/usr/local/bin/kube-apiserver \\
  --advertise-address=${MASTER_1} \\
  --allow-privileged=true \\
  --apiserver-count=1 \\
  --audit-log-maxage=30 \\
  --audit-log-maxbackup=3 \\
  --audit-log-maxsize=100 \\
  --audit-log-path=/var/log/audit.log \\
  --authorization-mode=Node,RBAC \\
  --bind-address=0.0.0.0 \\
  --client-ca-file=/var/lib/kubernetes/pki/ca.crt \\
  --enable-admission-plugins=NodeRestriction,ServiceAccount \\
  --enable-bootstrap-token-auth=true \\
  --etcd-cafile=/var/lib/kubernetes/pki/ca.crt \\
  --etcd-certfile=/var/lib/kubernetes/pki/etcd-server.crt \\
  --etcd-keyfile=/var/lib/kubernetes/pki/etcd-server.key \\
  --etcd-servers=https://${MASTER_1}:2379 \\
  --event-ttl=1h \\
  --encryption-provider-config=/var/lib/kubernetes/encryption-config.yaml \\
  --kubelet-certificate-authority=/var/lib/kubernetes/pki/ca.crt \\
  --kubelet-client-certificate=/var/lib/kubernetes/pki/apiserver-kubelet-client.crt \\
  --kubelet-client-key=/var/lib/kubernetes/pki/apiserver-kubelet-client.key \\
  --runtime-config=api/all=true \\
  --service-account-key-file=/var/lib/kubernetes/pki/service-account.crt \\
  --service-account-signing-key-file=/var/lib/kubernetes/pki/service-account.key \\
  --service-account-issuer=https://${MASTER_1}:6443 \\
  --service-cluster-ip-range=${SERVICE_CIDR} \\
  --service-node-port-range=30000-32767 \\
  --tls-cert-file=/var/lib/kubernetes/pki/kube-apiserver.crt \\
  --tls-private-key-file=/var/lib/kubernetes/pki/kube-apiserver.key \\
  --v=2
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target
EOF

Controller Manager

Копируем kubeconfig для controller manager:

Bash
sudo mv kube-controller-manager.kubeconfig /var/lib/kubernetes/
Создаём systemd unit:
Bash
cat <<EOF | sudo tee /etc/systemd/system/kube-controller-manager.service
[Unit]
Description=Kubernetes Controller Manager
Documentation=https://github.com/kubernetes/kubernetes

[Service]
ExecStart=/usr/local/bin/kube-controller-manager \\
  --allocate-node-cidrs=true \\
  --authentication-kubeconfig=/var/lib/kubernetes/kube-controller-manager.kubeconfig \\
  --authorization-kubeconfig=/var/lib/kubernetes/kube-controller-manager.kubeconfig \\
  --bind-address=127.0.0.1 \\
  --client-ca-file=/var/lib/kubernetes/pki/ca.crt \\
  --cluster-cidr=${POD_CIDR} \\
  --cluster-name=kubernetes \\
  --cluster-signing-cert-file=/var/lib/kubernetes/pki/ca.crt \\
  --cluster-signing-key-file=/var/lib/kubernetes/pki/ca.key \\
  --controllers=*,bootstrapsigner,tokencleaner \\
  --kubeconfig=/var/lib/kubernetes/kube-controller-manager.kubeconfig \\
  --leader-elect=true \\
  --node-cidr-mask-size=24 \\
  --requestheader-client-ca-file=/var/lib/kubernetes/pki/ca.crt \\
  --root-ca-file=/var/lib/kubernetes/pki/ca.crt \\
  --service-account-private-key-file=/var/lib/kubernetes/pki/service-account.key \\
  --service-cluster-ip-range=${SERVICE_CIDR} \\
  --use-service-account-credentials=true \\
  --v=2
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target
EOF

Scheduler

Копируем kubeconfig для scheduler:

Bash
sudo mv kube-scheduler.kubeconfig /var/lib/kubernetes/
Создаём systemd unit:
Bash
cat <<EOF | sudo tee /etc/systemd/system/kube-scheduler.service
[Unit]
Description=Kubernetes Scheduler
Documentation=https://github.com/kubernetes/kubernetes

[Service]
ExecStart=/usr/local/bin/kube-scheduler \\
  --kubeconfig=/var/lib/kubernetes/kube-scheduler.kubeconfig \\
  --leader-elect=true \\
  --v=2
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target
EOF

Запуск компонентов

Перед запуском выставим верные права на все kubeconfig

Bash
sudo chmod 600 /var/lib/kubernetes/*.kubeconfig
Далее настраиваем автостарт и запускаем все необходимые компоненты:
Bash
sudo systemctl daemon-reload
sudo systemctl enable kube-apiserver kube-controller-manager kube-scheduler
sudo systemctl start kube-apiserver kube-controller-manager kube-scheduler

Nota bene

Для проверки работоспособности control plane можно посмотреть логи этих компонентов, дополнительно можно запросить статус у самого cp:

Bash
  kubectl get cs --kubeconfig admin.kubeconfig

Добавление узла в кластер

Предупреждение

Действия в этом разделе выполняются как на master узле так и на worker, будьте внимательны

После того как мы закончили с master узлом, нам нужно добавить в кластер машинки на которых будут крутиться наши нагрузки.

Создание конфигурации

Внимание

Дальнейшие действия производятся на master узле

Выпускаем сертификат для регистрации kubelet, для этого сначала создадим конфигурацию:

Bash
cat > openssl-k8shw-1.cnf <<EOF
[req]
req_extensions = v3_req
distinguished_name = req_distinguished_name
[req_distinguished_name]
[ v3_req ]
basicConstraints = CA:FALSE
keyUsage = nonRepudiation, digitalSignature, keyEncipherment
subjectAltName = @alt_names
[alt_names]
DNS.1 = k8shw-1
IP.1 = ${WORKER_1}
EOF

После этого выпускаем сертификат и ключ

Bash
openssl genrsa -out k8shw-1.key 2048
openssl req -new -key k8shw-1.key -subj "/CN=system:node:k8shw-1/O=system:nodes" -out k8shw-1.csr -config openssl-k8shw-1.cnf
openssl x509 -req -in k8shw-1.csr -CA ca.crt -CAkey ca.key -CAcreateserial  -out k8shw-1.crt -extensions v3_req -extfile openssl-k8shw-1.cnf -days 1000
Создаём kubeconfig

Bash
  kubectl config set-cluster kubernetes-the-hard-way \
    --certificate-authority=/var/lib/kubernetes/pki/ca.crt \
    --server=https://${MASTER_1}:6443 \
    --kubeconfig=k8shw-1.kubeconfig
  kubectl config set-credentials system:node:k8shw-1 \
    --client-certificate=/var/lib/kubernetes/pki/k8shw-1.crt \
    --client-key=/var/lib/kubernetes/pki/k8shw-1.key \
    --kubeconfig=k8shw-1.kubeconfig
  kubectl config set-context default \
    --cluster=kubernetes-the-hard-way \
    --user=system:node:k8shw-1 \
    --kubeconfig=k8shw-1.kubeconfig
  kubectl config use-context default --kubeconfig=k8shw-1.kubeconfig

Переносим все необходимые файлы с мастера на worker

Bash
scp ca.crt k8shw-1.crt k8shw-1.key k8shw-1.kubeconfig kube-proxy.kubeconfig k8shw-1:~/

Регистрация рабочего узла

Внимание

Дальнейшие действия производятся на worker узле

Загружаем необходимые компоненты:

Bash
wget -q --show-progress --https-only --timestamping \
  https://dl.k8s.io/release/${KUBE_VERSION}/bin/linux/amd64/kube-proxy \
  https://dl.k8s.io/release/${KUBE_VERSION}/bin/linux/amd64/kubelet 
Копируем их в нужные места и выставляем корректные права:
Bash
  chmod +x kube-proxy kubelet
  sudo mv kube-proxy kubelet /usr/local/bin/
После этого нам нужно подготовить все директории для работы:
Bash
sudo mkdir -p \
  /var/lib/kubelet \
  /var/lib/kube-proxy \
  /var/lib/kubernetes/pki \
  /var/run/kubernetes
Копируем файлы и выставляем необходимые права:
Bash
  sudo mv ${HOSTNAME}.key ${HOSTNAME}.crt /var/lib/kubernetes/pki/
  sudo mv ${HOSTNAME}.kubeconfig /var/lib/kubelet/kubelet.kubeconfig
  sudo mv ca.crt /var/lib/kubernetes/pki/
  sudo mv kube-proxy.crt kube-proxy.key /var/lib/kubernetes/pki/
  sudo chown root:root /var/lib/kubernetes/pki/*
  sudo chown root:root /var/lib/kubelet/*
Создаём systemd unit для kubelet:
Bash
cat <<EOF | sudo tee /var/lib/kubelet/kubelet-config.yaml
kind: KubeletConfiguration
apiVersion: kubelet.config.k8s.io/v1beta1
authentication:
  anonymous:
    enabled: false
  webhook:
    enabled: true
  x509:
    clientCAFile: /var/lib/kubernetes/pki/ca.crt
authorization:
  mode: Webhook
containerRuntimeEndpoint: unix:///var/run/containerd/containerd.sock
clusterDomain: cluster.local
clusterDNS:
  - ${CLUSTER_DNS}
cgroupDriver: systemd
resolvConf: /run/systemd/resolve/resolv.conf
runtimeRequestTimeout: "15m"
tlsCertFile: /var/lib/kubernetes/pki/${HOSTNAME}.crt
tlsPrivateKeyFile: /var/lib/kubernetes/pki/${HOSTNAME}.key
registerNode: true
EOF
Переходим к kube-proxy, копируем kubeconfig
Bash
sudo cp kube-proxy.kubeconfig /var/lib/kube-proxy/
Создаём конфигурационный файл для прокси:
Bash
cat <<EOF | sudo tee /var/lib/kube-proxy/kube-proxy-config.yaml
kind: KubeProxyConfiguration
apiVersion: kubeproxy.config.k8s.io/v1alpha1
clientConnection:
  kubeconfig: /var/lib/kube-proxy/kube-proxy.kubeconfig
mode: ipvs
clusterCIDR: ${POD_CIDR}
EOF
и systemd unit
Bash
cat <<EOF | sudo tee /etc/systemd/system/kube-proxy.service
[Unit]
Description=Kubernetes Kube Proxy
Documentation=https://github.com/kubernetes/kubernetes
[Service]
ExecStart=/usr/local/bin/kube-proxy \\
  --config=/var/lib/kube-proxy/kube-proxy-config.yaml
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
EOF

На этом настройки завершены, можно настроить автостарт и запустить компоненты:

Bash
  sudo systemctl daemon-reload
  sudo systemctl enable kubelet kube-proxy
  sudo systemctl start kubelet kube-proxy

Nota bene

Для проверки работоспособности proxy\kubelet можно посмотреть логи этих компонентов:

Bash
  sudo journalctl -u kubelet -f
  sudo journalctl -u kube-proxy -f

Регистрация узла control plane

Nota bene

Это опциональный шаг, его можно пропустить.

By design, если не зарегистрировать узел в кластере, он не будет отображаться в

Bash
kubectl get nodes

Соответственно если мы хотим взаимодействовать с узлами control plane из кластера, нам необходимо пройтись по вот этой инструкции

Та инструкция разделена на 2 этапа:

  1. Действия которые необходимо выполнить на master
  2. Действия которые необходимо выполнить на worker

В нашем случае worker и master это один и тот-же узел, соответственно все действия из этого пункта мы выполняем на master узле.

Наполнение кластера

К этому моменту наш кластер запущен, но все еще не готов к рабочим нагрузкам, нам нужно еще как минимум два важных компонента, это cni flannel и coredns

Внимание

Дальнейшие действия производятся на master узле

CNI Flannel

Flannel — это простой и легкий способ настройки сетевой структуры третьего уровня, предназначенной для Kubernetes. (с) Flannel

Flannel один из самых простых представителей сетевого стека для k8s, он не поддерживает сетевые политики, но в рамках этой статьи они нам и не нужны.

Для установки нам понадобится манифест , в котором необходимо исправить параметр Network.

Готовый манифест который можно сразу применить в кластере:

flannel.yaml
YAML
apiVersion: v1
kind: Namespace
metadata:
  labels:
    k8s-app: flannel
    pod-security.kubernetes.io/enforce: privileged
  name: kube-flannel
---
apiVersion: v1
kind: ServiceAccount
metadata:
  labels:
    k8s-app: flannel
  name: flannel
  namespace: kube-flannel
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  labels:
    k8s-app: flannel
  name: flannel
rules:
- apiGroups:
  - ""
  resources:
  - pods
  verbs:
  - get
- apiGroups:
  - ""
  resources:
  - nodes
  verbs:
  - get
  - list
  - watch
- apiGroups:
  - ""
  resources:
  - nodes/status
  verbs:
  - patch
- apiGroups:
  - networking.k8s.io
  resources:
  - clustercidrs
  verbs:
  - list
  - watch
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  labels:
    k8s-app: flannel
  name: flannel
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: flannel
subjects:
- kind: ServiceAccount
  name: flannel
  namespace: kube-flannel
---
apiVersion: v1
data:
  cni-conf.json: |
    {
      "name": "cbr0",
      "cniVersion": "0.3.1",
      "plugins": [
        {
          "type": "flannel",
          "delegate": {
            "hairpinMode": true,
            "isDefaultGateway": true
          }
        },
        {
          "type": "portmap",
          "capabilities": {
            "portMappings": true
          }
        }
      ]
    }
  net-conf.json: |
    {
      "Network": "172.20.0.0/16",
      "Backend": {
        "Type": "vxlan"
      }
    }
kind: ConfigMap
metadata:
  labels:
    app: flannel
    k8s-app: flannel
    tier: node
  name: kube-flannel-cfg
  namespace: kube-flannel
---
apiVersion: apps/v1
kind: DaemonSet
metadata:
  labels:
    app: flannel
    k8s-app: flannel
    tier: node
  name: kube-flannel-ds
  namespace: kube-flannel
spec:
  selector:
    matchLabels:
      app: flannel
      k8s-app: flannel
  template:
    metadata:
      labels:
        app: flannel
        k8s-app: flannel
        tier: node
    spec:
      affinity:
        nodeAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
            nodeSelectorTerms:
            - matchExpressions:
              - key: kubernetes.io/os
                operator: In
                values:
                - linux
      containers:
      - args:
        - --ip-masq
        - --kube-subnet-mgr
        command:
        - /opt/bin/flanneld
        env:
        - name: POD_NAME
          valueFrom:
            fieldRef:
              fieldPath: metadata.name
        - name: POD_NAMESPACE
          valueFrom:
            fieldRef:
              fieldPath: metadata.namespace
        - name: EVENT_QUEUE_DEPTH
          value: "5000"
        image: docker.io/flannel/flannel:v0.23.0
        name: kube-flannel
        resources:
          requests:
            cpu: 100m
            memory: 50Mi
        securityContext:
          capabilities:
            add:
            - NET_ADMIN
            - NET_RAW
          privileged: false
        volumeMounts:
        - mountPath: /run/flannel
          name: run
        - mountPath: /etc/kube-flannel/
          name: flannel-cfg
        - mountPath: /run/xtables.lock
          name: xtables-lock
      hostNetwork: true
      initContainers:
      - args:
        - -f
        - /flannel
        - /opt/cni/bin/flannel
        command:
        - cp
        image: docker.io/flannel/flannel-cni-plugin:v1.2.0
        name: install-cni-plugin
        volumeMounts:
        - mountPath: /opt/cni/bin
          name: cni-plugin
      - args:
        - -f
        - /etc/kube-flannel/cni-conf.json
        - /etc/cni/net.d/10-flannel.conflist
        command:
        - cp
        image: docker.io/flannel/flannel:v0.23.0
        name: install-cni
        volumeMounts:
        - mountPath: /etc/cni/net.d
          name: cni
        - mountPath: /etc/kube-flannel/
          name: flannel-cfg
      priorityClassName: system-node-critical
      serviceAccountName: flannel
      tolerations:
      - effect: NoSchedule
        operator: Exists
      volumes:
      - hostPath:
          path: /run/flannel
        name: run
      - hostPath:
          path: /opt/cni/bin
        name: cni-plugin
      - hostPath:
          path: /etc/cni/net.d
        name: cni
      - configMap:
          name: kube-flannel-cfg
        name: flannel-cfg
      - hostPath:
          path: /run/xtables.lock
          type: FileOrCreate
        name: xtables-lock
Bash
kubectl apply -f flannel.yaml --kubeconfig admin.kubeconfig

DNS

После установки кластера у нас отсутствует DNS сервер, что в свою очередь не позволяет работать внутренним резолверам. Например не получиться организовать взаимодействие между подами по имени сервиса. Для того чтобы исправить эту ситуацию нам понадобится CoreDNS.

Готовый манифест который можно сразу применить в кластере:

coredns.yaml
YAML
apiVersion: v1
kind: ServiceAccount
metadata:
  name: coredns
  namespace: kube-system
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  labels:
    kubernetes.io/bootstrapping: rbac-defaults
  name: system:coredns
rules:
  - apiGroups:
    - ""
    resources:
    - endpoints
    - services
    - pods
    - namespaces
    verbs:
    - list
    - watch
  - apiGroups:
    - discovery.k8s.io
    resources:
    - endpointslices
    verbs:
    - list
    - watch
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  annotations:
    rbac.authorization.kubernetes.io/autoupdate: "true"
  labels:
    kubernetes.io/bootstrapping: rbac-defaults
  name: system:coredns
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: system:coredns
subjects:
- kind: ServiceAccount
  name: coredns
  namespace: kube-system
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: coredns
  namespace: kube-system
data:
  Corefile: |
    .:53 {
        errors
        health {
          lameduck 5s
        }
        ready
        kubernetes cluster.local in-addr.arpa ip6.arpa {
           pods insecure
          fallthrough in-addr.arpa ip6.arpa
        }
        prometheus :9153
        forward . /etc/resolv.conf {
          max_concurrent 1000
        }
        cache 30
        loop
        reload
        loadbalance
    }
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: coredns
  namespace: kube-system
  labels:
    k8s-app: kube-dns
    kubernetes.io/name: "CoreDNS"
spec:
  replicas: 2
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxUnavailable: 1
  selector:
    matchLabels:
      k8s-app: kube-dns
  template:
    metadata:
      labels:
        k8s-app: kube-dns
    spec:
      priorityClassName: system-cluster-critical
      serviceAccountName: coredns
      tolerations:
        - key: "CriticalAddonsOnly"
          operator: "Exists"
      nodeSelector:
        kubernetes.io/os: linux
      affinity:
         podAntiAffinity:
           requiredDuringSchedulingIgnoredDuringExecution:
           - labelSelector:
               matchExpressions:
               - key: k8s-app
                 operator: In
                 values: ["kube-dns"]
             topologyKey: kubernetes.io/hostname
      containers:
      - name: coredns
        image: coredns/coredns:1.9.4
        imagePullPolicy: IfNotPresent
        resources:
          limits:
            memory: 170Mi
          requests:
            cpu: 100m
            memory: 70Mi
        args: [ "-conf", "/etc/coredns/Corefile" ]
        volumeMounts:
        - name: config-volume
          mountPath: /etc/coredns
          readOnly: true
        ports:
        - containerPort: 53
          name: dns
          protocol: UDP
        - containerPort: 53
          name: dns-tcp
          protocol: TCP
        - containerPort: 9153
          name: metrics
          protocol: TCP
        securityContext:
          allowPrivilegeEscalation: false
          capabilities:
            add:
            - NET_BIND_SERVICE
            drop:
            - all
          readOnlyRootFilesystem: true
        livenessProbe:
          httpGet:
            path: /health
            port: 8080
            scheme: HTTP
          initialDelaySeconds: 60
          timeoutSeconds: 5
          successThreshold: 1
          failureThreshold: 5
        readinessProbe:
          httpGet:
            path: /ready
            port: 8181
            scheme: HTTP
      dnsPolicy: Default
      volumes:
        - name: config-volume
          configMap:
            name: coredns
            items:
            - key: Corefile
              path: Corefile
---
apiVersion: v1
kind: Service
metadata:
  name: kube-dns
  namespace: kube-system
  annotations:
    prometheus.io/port: "9153"
    prometheus.io/scrape: "true"
  labels:
    k8s-app: kube-dns
    kubernetes.io/cluster-service: "true"
    kubernetes.io/name: "CoreDNS"
spec:
  selector:
    k8s-app: kube-dns
  clusterIP: 172.21.0.10
  ports:
  - name: dns
    port: 53
    protocol: UDP
  - name: dns-tcp
    port: 53
    protocol: TCP
  - name: metrics
    port: 9153
    protocol: TCP
Bash
kubectl apply -f cdns.yaml --kubeconfig admin.kubeconfig

Проверка кластера

Мы выполним пару проверок: - Проверка статуса нод - Проверка DNS сервера - Проверка готовности кластера запускать нагрузки

Внимание

Дальнейшие действия производятся на master узле

Статус узлов

Для проверки статуса узлов, достаточно выполнить следующую команду:

Bash
kubectl --kubeconfig admin.kubeconfig exec get nodes -o wide
Если узлы в поле STATUS имеют строку Ready, значит все узлы готовы к работе.

DNS

Для проверки днс сервера, сначало нам нужно создать POD в котором мы будем работать:

Bash
kubectl --kubeconfig admin.kubeconfig run busybox -n default --image=busybox:1.28 --restart Never --command -- sleep 150
Далее выполняем в этом поде команду nslookup kubernetes, таким образом мы запросим внутренний адрес kube-api:
Bash
kubectl --kubeconfig admin.kubeconfig exec -ti -n default busybox -- nslookup kubernetes
Если запрос выполнился удачно - dns сервер функционирует правильно.

Запуск рабочей нагрузки

Этим пунктом мы проверим все критически важные компоненты в k8s для начала запустим pod с nginx

Bash
kubectl create deployment nginx --image=nginx:1.23.1
Далее, откроем доступ к этому деплойменту из вне, для этого создадим service с типом NodePort

Bash
kubectl expose deploy nginx --type=NodePort --port 80
Во время создания такого сервиса, будет использован рандомный реальный порт с worker узла, чтобы узнать какой именно - выполним комманду:
Bash
kubectl get svc -l app=nginx -o jsonpath="{.items[0].spec.ports[0].nodePort}"
Теперь у нас есть все чтобы проверить наш деплоймент, открываем браузер и переходим по адресу
Text Only
http://ip-address-worker:node-port
в моём же случае итоговая ссылка выглядит так
Text Only
http://10.0.138.179:30010

Если в браузере мы видим стандартную страничку nginx - все работает отлично, поздравляю!

Post Scriptum

После прохождения этого материалы мы смогли развернуть кластер куба без использования средств автоматизации, я надеюсь что теперь вы чуть лучше понимаете как устроен куб и как его компоненты взаимодействуют друг с другом.

Хотелось бы отдельно отметить работу Kelsey Hightower, без неё этой статьи бы не было.