Нам потребуются в GCP 4 ноды с образом Ubuntu 18.04 LTS
- master - 1 экземпляр (n1-standard-2)
- worker - 3 экземпляра (n1-standard-1)
На каждой машине
- Отключаем swap
sudo -i
swapoff -a
- Включаем маршрутизацию
cat > /etc/sysctl.d/99-kubernetes-cri.conf <<EOF
net.bridge.bridge-nf-call-iptables = 1
net.ipv4.ip_forward = 1
net.bridge.bridge-nf-call-ip6tables = 1
EOF
sysctl --system
- Устанавливаем Docker
apt-get update && apt-get install -y \
apt-transport-https ca-certificates curl software-properties-common gnupg2
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | apt-key add -
add-apt-repository \
"deb [arch=amd64] https://download.docker.com/linux/ubuntu \
$(lsb_release -cs) \
stable"
apt-get update && apt-get install -y \
containerd.io=1.2.13-1 \
docker-ce=5:19.03.8~3-0~ubuntu-$(lsb_release -cs) \
docker-ce-cli=5:19.03.8~3-0~ubuntu-$(lsb_release -cs)
# Setup daemon.
cat > /etc/docker/daemon.json <<EOF
{
"exec-opts": ["native.cgroupdriver=systemd"],
"log-driver": "json-file",
"log-opts": {
"max-size": "100m"
},
"storage-driver": "overlay2"
}
EOF
mkdir -p /etc/systemd/system/docker.service.d
# Restart docker.
systemctl daemon-reload
systemctl restart docker
- Устанавливаем kubeadm, kubelet and kubectl
apt-get update && apt-get install -y apt-transport-https curl
curl -s https://packages.cloud.google.com/apt/doc/apt-key.gpg | apt-key add -
cat <<EOF > /etc/apt/sources.list.d/kubernetes.list
deb https://apt.kubernetes.io/ kubernetes-xenial main
EOF
apt-get update
apt-get install -y kubelet=1.17.4-00 kubeadm=1.17.4-00 kubectl=1.17.4-00
- Создаем кластер
kubeadm init --pod-network-cidr=192.168.0.0/24
В выводе будут:
- команда для копирования конфига kubectl
- сообщение о том, что необходимо установить сетевой плагин
- команда для присоединения worker ноды
Your Kubernetes control-plane has initialized successfully!
To start using your cluster, you need to run the following as a regular user:
mkdir -p $HOME/.kube
sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
sudo chown $(id -u):$(id -g) $HOME/.kube/config
You should now deploy a pod network to the cluster.
Run "kubectl apply -f [podnetwork].yaml" with one of the options listed at:
https://kubernetes.io/docs/concepts/cluster-administration/addons/
Then you can join any number of worker nodes by running the following on each as root:
kubeadm join 10.132.0.52:6443 --token u8gxos.mf3ok30ad4xlgqj4 \
--discovery-token-ca-cert-hash sha256:939bb3eb4be6e159e824b7f0b698a4e53e1b6ee59a7190d462cbc332a1731fec
- Копируем конфиг kubectl
mkdir -p $HOME/.kube
cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
chown $(id -u):$(id -g) $HOME/.kube/config
- Устанавливаем сетевой плагин (Документация)
kubectl apply -f https://docs.projectcalico.org/manifests/calico.yaml
- Присоединяем остальные ноды
kubeadm join 10.132.0.52:6443 --token u8gxos.mf3ok30ad4xlgqj4 \
--discovery-token-ca-cert-hash sha256:939bb3eb4be6e159e824b7f0b698a4e53e1b6ee59a7190d462cbc332a1731fec
Если вывод команды потерялся, токены можно посмотреть командой: kubeadm token list
Получить хеш
openssl x509 -pubkey -in /etc/kubernetes/pki/ca.crt | openssl rsa -pubin -outform der
2>/dev/null | \
openssl dgst -sha256 -hex | sed 's/^.* //'
- Проверяем
kubectl get nodes
NAME STATUS ROLES AGE VERSION
master Ready master 7m40s v1.17.4
worker1 Ready <none> 2m6s v1.17.4
worker2 Ready <none> 86s v1.17.4
worker3 Ready <none> 59s v1.17.4
- Запуск нагрузки
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
spec:
selector:
matchLabels:
app: nginx
replicas: 4
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.17.2
ports:
- containerPort: 80
kubectl apply -f deployment.yaml
deployment.apps/nginx-deployment created
kubectl get pods
NAME READY STATUS RESTARTS AGE
nginx-deployment-c8fd555cc-46jzj 1/1 Running 0 41s
nginx-deployment-c8fd555cc-h8frd 1/1 Running 0 41s
nginx-deployment-c8fd555cc-jnkw4 1/1 Running 0 41s
nginx-deployment-c8fd555cc-m5nf4 1/1 Running 0 41s
- Обновляем кластер
Так как кластер мы разворачивали с помощью kubeadm, то и производить обновление будем с помощью него.
Обновлять ноды будем по очереди.
Допускается, отставание версий worker-нод от master, но не наоборот. Поэтому обновление будем начинать с нее master-нода у нас версии 1.16.8
- Обновляем пакеты
apt-get update && apt-get install -y kubeadm=1.18.0-00 \
kubelet=1.18.0-00 kubectl=1.18.0-00
- Проверка
kubectl get nodes
NAME STATUS ROLES AGE VERSION
master Ready master 14m v1.18.0
worker1 Ready <none> 8m55s v1.17.4
worker2 Ready <none> 8m15s v1.17.4
worker3 Ready <none> 7m48s v1.17.4
kubelet --version
Kubernetes v1.18.0
cat /etc/kubernetes/manifests/kube-apiserver.yaml
image: k8s.gcr.io/kube-apiserver:v1.17.4
- Обновим остальные компоненты кластера
kubeadm upgrade plan
[upgrade/config] Making sure the configuration is correct:
[upgrade/config] Reading configuration from the cluster...
[upgrade/config] FYI: You can look at this config file with 'kubectl -n kube-system get cm kubeadm-config -oyaml'
[preflight] Running pre-flight checks.
[upgrade] Running cluster health checks
[upgrade] Fetching available versions to upgrade to
[upgrade/versions] Cluster version: v1.17.9
[upgrade/versions] kubeadm version: v1.18.0
[upgrade/versions] Latest stable version: v1.18.6
[upgrade/versions] Latest stable version: v1.18.6
[upgrade/versions] Latest version in the v1.17 series: v1.17.9
[upgrade/versions] Latest version in the v1.17 series: v1.17.9
Components that must be upgraded manually after you have upgraded the control plane with 'kubeadm upgrade apply':
COMPONENT CURRENT AVAILABLE
Kubelet 3 x v1.17.4 v1.18.6
1 x v1.18.0 v1.18.6
Upgrade to the latest stable version:
COMPONENT CURRENT AVAILABLE
API Server v1.17.9 v1.18.6
Controller Manager v1.17.9 v1.18.6
Scheduler v1.17.9 v1.18.6
Kube Proxy v1.17.9 v1.18.6
CoreDNS 1.6.5 1.6.7
Etcd 3.4.3 3.4.3-0
You can now apply the upgrade by executing the following command:
kubeadm upgrade apply v1.18.6
Note: Before you can perform this upgrade, you have to update kubeadm to v1.18.6.
_____________________________________________________________________
- Применим изменения
kubeadm upgrade apply v1.18.0
- Проверка
kubeadm version
kubeadm version: &version.Info{Major:"1", Minor:"18", GitVersion:"v1.18.0", GitCommit:"9e991415386e4cf155a24b1da15b
ecaa390438d8", GitTreeState:"clean", BuildDate:"2020-03-25T14:56:30Z", GoVersion:"go1.13.8", Compiler:"gc", Platfor
m:"linux/amd64"}
kubelet --version
Kubernetes v1.18.0
kubectl version
Client Version: version.Info{Major:"1", Minor:"18", GitVersion:"v1.18.0", GitCommit:"9e991415386e4cf155a24b1da15bec
aa390438d8", GitTreeState:"clean", BuildDate:"2020-03-25T14:58:59Z", GoVersion:"go1.13.8", Compiler:"gc", Platform:
"linux/amd64"}
Server Version: version.Info{Major:"1", Minor:"18", GitVersion:"v1.18.0", GitCommit:"9e991415386e4cf155a24b1da15bec
aa390438d8", GitTreeState:"clean", BuildDate:"2020-03-25T14:50:46Z", GoVersion:"go1.13.8", Compiler:"gc", Platform:
"linux/amd64"}
kubectl describe pod kube-apiserver-master -n kube-system
Name: kube-apiserver-master
Namespace: kube-system
Priority: 2000000000
Priority Class Name: system-cluster-critical
Node: master/10.132.0.52
Start Time: Fri, 31 Jul 2020 22:17:25 +0000
Labels: component=kube-apiserver
tier=control-plane
Annotations: kubeadm.kubernetes.io/kube-apiserver.advertise-address.endpoint: 10.132.0.52:6443
kubernetes.io/config.hash: 6f8d44727c25dcb6d73add9fc1b1ea33
kubernetes.io/config.mirror: 6f8d44727c25dcb6d73add9fc1b1ea33
kubernetes.io/config.seen: 2020-07-31T22:33:56.757141182Z
kubernetes.io/config.source: file
Status: Running
IP: 10.132.0.52
IPs:
IP: 10.132.0.52
Controlled By: Node/master
Containers:
kube-apiserver:
Container ID: docker://53f03fb4f66819192bdf6c189a4d584aa386ce4304fa1447b1e3201da4bea860
Image: k8s.gcr.io/kube-apiserver:v1.18.0
Image ID: docker-pullable://k8s.gcr.io/kube-apiserver@sha256:fc4efb55c2a7d4e7b9a858c67e24f00e739df4ef50825
00c2b60ea0903f18248
Port: <none>
Host Port: <none>
Command:
kube-apiserver
--advertise-address=10.132.0.52
--allow-privileged=true
--authorization-mode=Node,RBAC
--client-ca-file=/etc/kubernetes/pki/ca.crt
--enable-admission-plugins=NodeRestriction
--enable-bootstrap-token-auth=true
--etcd-cafile=/etc/kubernetes/pki/etcd/ca.crt
--etcd-certfile=/etc/kubernetes/pki/apiserver-etcd-client.crt
--etcd-keyfile=/etc/kubernetes/pki/apiserver-etcd-client.key
--etcd-servers=https://127.0.0.1:2379
--insecure-port=0
--kubelet-client-certificate=/etc/kubernetes/pki/apiserver-kubelet-client.crt
--kubelet-client-key=/etc/kubernetes/pki/apiserver-kubelet-client.key
--kubelet-preferred-address-types=InternalIP,ExternalIP,Hostname
--proxy-client-cert-file=/etc/kubernetes/pki/front-proxy-client.crt
--proxy-client-key-file=/etc/kubernetes/pki/front-proxy-client.key
--requestheader-allowed-names=front-proxy-client
--requestheader-client-ca-file=/etc/kubernetes/pki/front-proxy-ca.crt
--requestheader-extra-headers-prefix=X-Remote-Extra-
--requestheader-group-headers=X-Remote-Group
--requestheader-username-headers=X-Remote-User
--secure-port=6443
--service-account-key-file=/etc/kubernetes/pki/sa.pub
--service-cluster-ip-range=10.96.0.0/12
--tls-cert-file=/etc/kubernetes/pki/apiserver.crt
--tls-private-key-file=/etc/kubernetes/pki/apiserver.key
State: Running
Started: Fri, 31 Jul 2020 22:33:57 +0000
Ready: True
Restart Count: 0
Requests:
cpu: 250m
Liveness: http-get https://10.132.0.52:6443/healthz delay=15s timeout=15s period=10s #success=1 #failure=8
Environment: <none>
Mounts:
/etc/ca-certificates from etc-ca-certificates (ro)
/etc/kubernetes/pki from k8s-certs (ro)
/etc/ssl/certs from ca-certs (ro)
/usr/local/share/ca-certificates from usr-local-share-ca-certificates (ro)
/usr/share/ca-certificates from usr-share-ca-certificates (ro)
Conditions:
Type Status
Initialized True
Ready True
ContainersReady True
PodScheduled True
Volumes:
ca-certs:
Type: HostPath (bare host directory volume)
Path: /etc/ssl/certs
HostPathType: DirectoryOrCreate
etc-ca-certificates:
Type: HostPath (bare host directory volume)
Path: /etc/ca-certificates
HostPathType: DirectoryOrCreate
k8s-certs:
Type: HostPath (bare host directory volume)
Path: /etc/kubernetes/pki
HostPathType: DirectoryOrCreate
usr-local-share-ca-certificates:
Type: HostPath (bare host directory volume)
Path: /usr/local/share/ca-certificates
HostPathType: DirectoryOrCreate
usr-share-ca-certificates:
Type: HostPath (bare host directory volume)
Path: /usr/share/ca-certificates
HostPathType: DirectoryOrCreate
QoS Class: Burstable
Node-Selectors: <none>
Tolerations: :NoExecute
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Pulled 2m28s kubelet, master Container image "k8s.gcr.io/kube-apiserver:v1.18.0" already present on m
achine
Normal Created 2m28s kubelet, master Created container kube-apiserver
Normal Started 2m28s kubelet, master Started container kube-apiserver
- Вывод worker-нод из планирования
Первым делом, мы сливаем всю нагрузку с ноды и выводим ее из планирования
kubectl drain worker1
node/worker1 cordoned
error: unable to drain node "worker1", aborting command...
There are pending nodes to be drained:
worker1
error: cannot delete DaemonSet-managed Pods (use --ignore-daemonsets to ignore): kube-system/calico-node-26ppd, kub
e-system/kube-proxy-clnf8
kubectl drain убирает всю нагрузку, кроме DaemonSet, поэтому мы явно должны сказать, что уведомлены об этом
kubectl drain worker1 --ignore-daemonsets
node/worker1 already cordoned
WARNING: ignoring DaemonSet-managed Pods: kube-system/calico-node-26ppd, kube-system/kube-proxy-clnf8
evicting pod default/nginx-deployment-c8fd555cc-h8frd
evicting pod default/nginx-deployment-c8fd555cc-m5nf4
pod/nginx-deployment-c8fd555cc-h8frd evicted
pod/nginx-deployment-c8fd555cc-m5nf4 evicted
node/worker1 evicted
- Когда мы вывели ноду на обслуживание, к статусу добавилась строчка SchedulingDisabled
kubectl get nodes -o wide
NAME STATUS ROLES AGE VERSION INTERNAL-IP EXTERNAL-IP OS-IMAGE KERNEL-VERSION CONTAINER-RUNTIME
master Ready master 22m v1.18.0 10.132.0.52 <none> Ubuntu 18.04.4 LTS 5.3.0-1032-gcp docker://19.3.8
worker1 Ready,SchedulingDisabled <none> 17m v1.17.4 10.132.0.53 <none> Ubuntu 18.04.4 LTS 5.3.0-1032-gcp docker://19.3.8
worker2 Ready <none> 16m v1.17.4 10.132.0.54 <none> Ubuntu 18.04.4 LTS 5.3.0-1032-gcp docker://19.3.8
worker3 Ready <none> 15m v1.17.4 10.132.0.55 <none> Ubuntu 18.04.4 LTS 5.3.0-1032-gcp docker://19.3.8
- Обновление worker-нод
apt-get install -y kubelet=1.18.0-00 kubeadm=1.18.0-00
systemctl restart kubelet
- После обновления kubectl показывает новую версию, и статус SchedulingDisabled
kubectl get nodes
NAME STATUS ROLES AGE VERSION
master Ready master 23m v1.18.0
worker1 Ready,SchedulingDisabled <none> 18m v1.18.0
worker2 Ready <none> 17m v1.17.4
worker3 Ready <none> 16m v1.17.4
- Командой kubectl uncordon worker-instance-1 возвращаем ноду обратно в планирование нагрузки
kubectl uncordon worker1
- Обновим остальные ноды и получим
kubectl get nodes
NAME STATUS ROLES AGE VERSION
master Ready master 27m v1.18.0
worker1 Ready <none> 21m v1.18.0
worker2 Ready <none> 21m v1.18.0
worker3 Ready <none> 20m v1.18.0
В данном задании ради демонстрации механики обновления мы вручную развернули и обновили кластер с одной master-нодой.
Но развертывать большие кластера подобным способом не удобно. Поэтому мы рассмотрим инструмент для автоматического развертывания кластеров kubespray.
Kubespray - это Ansible playbook для установки Kubernetes. Для его использования достаточно иметь SSH-доступ на машины, поэтому не важно как они были созданы (Cloud, Bare-metal).
- Установка kubespray
Пре-реквизиты:
- Python и pip на локальной машине
- SSH доступ на все ноды кластера
# получение kubespray
git clone https://github.com/kubernetes-sigs/kubespray.git
# установка зависимостей
sudo pip install -r requirements.txt
# копирование примера конфига в отдельную директорию
cp -rfp inventory/sample inventory/mycluster
Добавьте адреса машин кластера в конфиг kubespray inventory/mycluster/inventory.ini:
# в блоке all мы описывем все машины (master и worker)
# для мастер нод мы указывем переменную etcd_member_name
[all]
node1 ansible_host=192.168.10.1 etcd_member_name=etcd1
node2 ansible_host=192.168.10.2
node3 ansible_host=192.168.10.3
node4 ansible_host=192.168.10.4
# в блоке kube-master мы указывем master-ноды
[kube-master]
node1
# в блоке etcd ноды, где будет установлен etcd
# если мы хотим HA кластер, то etcd устанавливаетcя отдельно от API-server
[etcd]
node1
# в блоке kube-node описываем worker-ноды
[kube-node]
node2
node3
node4
# в блоке k8s-cluster:children соединяем kube-master и kube-node
[k8s-cluster:children]
kube-master
kube-node
- После редактирования конфига можно устанавливать кластер
ansible-playbook -i inventory/mycluster/inventory.ini --become --become-user=root \
--user=${SSH_USERNAME} --key-file=${SSH_PRIVATE_KEY} cluster.yml
Выполним установку кластера с 3 master-нодами, 2 worker нодами и external etcd topology. Используем kubeadm, keekalived, haproxy.
Документация
/etc/keepalived/keepalived.conf
! /etc/keepalived/keepalived.conf
! Configuration File for keepalived
global_defs {
router_id LVS_DEVEL
}
vrrp_script check_apiserver {
script "/etc/keepalived/check_apiserver.sh"
interval 3
weight -2
fall 10
rise 2
}
vrrp_instance VI_1 {
state MASTER
interface ens160
virtual_router_id 51
priority 101
authentication {
auth_type PASS
auth_pass 42
}
virtual_ipaddress {
10.2.1.1
}
track_script {
check_apiserver
}
}
/etc/keepalived/check_apiserver.sh
#!/bin/sh
errorExit() {
echo "*** $*" 1>&2
exit 1
}
curl --silent --max-time 2 --insecure https://localhost:6443/healthz -o /dev/null || errorExit "Error GET https://localhost:6443/healthz"
if ip addr | grep -q 10.2.1.1; then
curl --silent --max-time 2 --insecure https://10.2.1.1:6443/healthz -o /dev/null || errorExit "Error GET https://10.2.1.1:6443/healthz"
fi
/etc/haproxy/haproxy.cfg
# /etc/haproxy/haproxy.cfg
#---------------------------------------------------------------------
# Global settings
#---------------------------------------------------------------------
global
log /dev/log local0
log /dev/log local1 notice
daemon
#---------------------------------------------------------------------
# common defaults that all the 'listen' and 'backend' sections will
# use if not designated in their block
#---------------------------------------------------------------------
defaults
mode http
log global
option httplog
option dontlognull
option http-server-close
option forwardfor except 127.0.0.0/8
option redispatch
retries 1
timeout http-request 10s
timeout queue 20s
timeout connect 5s
timeout client 20s
timeout server 20s
timeout http-keep-alive 10s
timeout check 10s
#---------------------------------------------------------------------
# apiserver frontend which proxys to the masters
#---------------------------------------------------------------------
frontend apiserver
bind *:6443
mode tcp
option tcplog
default_backend apiserver
#---------------------------------------------------------------------
# round robin balancing for apiserver
#---------------------------------------------------------------------
backend apiserver
mode tcp
balance roundrobin
option tcp-check
server dev_master1 10.2.1.5:6443 check
server dev_master2 10.2.1.6:6443 check
server dev_master2 10.2.1.7:6443 check
/etc/keepalived/keepalived.conf
! /etc/keepalived/keepalived.conf
! Configuration File for keepalived
global_defs {
router_id LVS_DEVEL
}
vrrp_script check_apiserver {
script "/etc/keepalived/check_apiserver.sh"
interval 3
weight -2
fall 10
rise 2
}
vrrp_instance VI_1 {
state BACKUP
interface ens160
virtual_router_id 51
priority 100
authentication {
auth_type PASS
auth_pass 42
}
virtual_ipaddress {
10.2.1.1
}
track_script {
check_apiserver
}
}
/etc/keepalived/check_apiserver.sh
#!/bin/sh
errorExit() {
echo "*** $*" 1>&2
exit 1
}
curl --silent --max-time 2 --insecure https://localhost:6443/healthz -o /dev/null || errorExit "Error GET https://localhost:6443/healthz"
if ip addr | grep -q 10.2.1.1; then
curl --silent --max-time 2 --insecure https://10.2.1.1:6443/healthz -o /dev/null || errorExit "Error GET https://10.2.1.1:6443/healthz"
fi
/etc/haproxy/haproxy.cfg
# /etc/haproxy/haproxy.cfg
#---------------------------------------------------------------------
# Global settings
#---------------------------------------------------------------------
global
log /dev/log local0
log /dev/log local1 notice
daemon
#---------------------------------------------------------------------
# common defaults that all the 'listen' and 'backend' sections will
# use if not designated in their block
#---------------------------------------------------------------------
defaults
mode http
log global
option httplog
option dontlognull
option http-server-close
option forwardfor except 127.0.0.0/8
option redispatch
retries 1
timeout http-request 10s
timeout queue 20s
timeout connect 5s
timeout client 20s
timeout server 20s
timeout http-keep-alive 10s
timeout check 10s
#---------------------------------------------------------------------
# apiserver frontend which proxys to the masters
#---------------------------------------------------------------------
frontend apiserver
bind *:6443
mode tcp
option tcplog
default_backend apiserver
#---------------------------------------------------------------------
# round robin balancing for apiserver
#---------------------------------------------------------------------
backend apiserver
mode tcp
balance roundrobin
option tcp-check
server dev_master1 10.2.1.5:6443 check
server dev_master2 10.2.1.6:6443 check
server dev_master2 10.2.1.7:6443 check
Результат
kubectl get nodes
NAME STATUS ROLES AGE VERSION
dev-master1 Ready master 1d v1.18.3
dev-master2 Ready master 1d v1.18.3
dev-master3 Ready master 1d v1.18.3
dev-worker1 Ready <none> 1d v1.18.3
dev-worker2 Ready <none> 1d v1.18.3
Устанавливам в наш кластер kubectl debug
brew install aylei/tap/kubectl-debug
И применяем манифест агента
kubectl apply -f strace/agent_daemonset.yml
daemonset.apps/debug-agent created
Запускаем pod с приложением
kubectl apply -f strace/nginx.yaml
pod/nginx created
Запускаем debug
kubectl-debug nginx --agentless=false --port-forward=true
bash-5.0# strace -p 1
strace: attach: ptrace(PTRACE_SEIZE, 1): Operation not permitted
Видим, что не хватает capability, которые есть в версии v0.1.1.
Меняем image tag и применяем манифест
kubectl apply -f strace/agent_daemonset.yml
daemonset.apps/debug-agent configured
Проверяем, что все в порядке
kubectl-debug nginx --agentless=false --port-forward=true
bash-5.0# strace -c -p1
strace: Process 1 attached
Один из полезных инструментов - это kube-iptables-tailer.
Он предназначен для того, чтобы выводить информацию об отброшенных iptables пакетах в журнал событий Kubernetes (kubectl get events).
Основной кейс - сообщить разработчикам сервисов о проблемах с NetworkPolicy.
Установим netperf-operator
- Это Kubernetes-оператор, который позволяет запускать тесты пропускной способности сети между нодами кластера
- Сам проект - не очень production-grade, но иногда выручает
kubectl apply -f https://raw.githubusercontent.com/piontec/netperf-operator/master/deploy/crd.yaml
kubectl apply -f https://raw.githubusercontent.com/piontec/netperf-operator/master/deploy/rbac.yaml
kubectl apply -f https://raw.githubusercontent.com/piontec/netperf-operator/master/deploy/operator.yaml
kubectl apply -f https://raw.githubusercontent.com/piontec/netperf-operator/master/deploy/cr.yaml
Проверяем
kubectl describe netperf.app.example.com/example
Name: example
Namespace: default
Labels: <none>
Annotations: kubectl.kubernetes.io/last-applied-configuration:
{"apiVersion":"app.example.com/v1alpha1","kind":"Netperf","metadata":{"annotations":{},"name":"example","namespace":"default"}}
API Version: app.example.com/v1alpha1
Kind: Netperf
Metadata:
Creation Timestamp: 2020-07-31T14:39:27Z
Generation: 4
Resource Version: 1162239
Self Link: /apis/app.example.com/v1alpha1/namespaces/default/netperfs/example
UID: 983574f7-60db-4ccb-8a09-d0f7c738dd20
Spec:
Client Node:
Server Node:
Status:
Client Pod: netperf-client-d0f7c738dd20
Server Pod: netperf-server-d0f7c738dd20
Speed Bits Per Sec: 12478.8
Status: Done
Events: <none>
Видим статус Done и скорость: 12478 bps
Добавим сетевую политику для Calicoo, чтобы ограничить доступ к подам Netperf и включить логирование в iptables
kubectl apply -f kit/netperf-calico-policy.yaml
Перезапустим тест удалив cr и создав вновь
kubectl delete -f kit/deploy/cr.yaml
netperf.app.example.com "example" deleted
kubectl apply -f kit/deploy/cr.yaml
netperf.app.example.com/example created
Проверим:
kubectl describe netperf.app.example.com/example
Name: example
Namespace: default
Labels: <none>
Annotations: kubectl.kubernetes.io/last-applied-configuration:
{"apiVersion":"app.example.com/v1alpha1","kind":"Netperf","metadata":{"annotations":{},"name":"example","namespace":"default"}}
API Version: app.example.com/v1alpha1
Kind: Netperf
Metadata:
Creation Timestamp: 2020-07-31T14:47:20Z
Generation: 3
Resource Version: 1164540
Self Link: /apis/app.example.com/v1alpha1/namespaces/default/netperfs/example
UID: 704fc8e5-7f7f-4035-aa2d-60cdcba88958
Spec:
Client Node:
Server Node:
Status:
Client Pod: netperf-client-60cdcba88958
Server Pod: netperf-server-60cdcba88958
Speed Bits Per Sec: 0
Status: Started test
Events: <none>
Видим, что тест висит в состоянии Starting. В нашей сетевой политике есть ошибка.
Проверим, что в логах ноды Kubernetes появились сообщения об отброшенных пакетах
kubectl get pod -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
netperf-client-60cdcba88958 1/1 Running 0 108s 10.32.0.15 gke-cluster-1-default-pool-02a8e156-dr3f <none> <none>
netperf-operator-85569b59dd-hgvnn 1/1 Running 0 9m44s 10.32.2.6 gke-cluster-1-default-pool-3d470404-lm1s <none> <none>
netperf-server-60cdcba88958 1/1 Running 0 110s 10.32.0.14 gke-cluster-1-default-pool-02a8e156-dr3f <none> <none>
Подключимся к ноде по ssh
gcloud compute ssh gke-cluster-1-default-pool-02a8e156-dr3f
iptables --list -nv | grep DROP
19 1140 DROP all -- * * 0.0.0.0/0 0.0.0.0/0 /* cali:He8TRqGPuUw3VGwk */
счетчики дропов ненулевые
iptables --list -nv | grep LOG
21 1260 LOG all -- * * 0.0.0.0/0 0.0.0.0/0 /* cali:B30DykF1ntLW86eD */ LOG flags 0 level 5 prefix "calico-packet: "
счетчики с действием логирования ненулевые
journalctl -k | grep calico
Jul 31 14:52:04 gke-cluster-1-default-pool-02a8e156-dr3f kernel: calico-packet: IN=cali34a48ed36b3 OUT=cali2ff164dea95 MAC=ee:ee:ee:ee:ee:ee:76:35:a4:01:96:bd:08:00 SRC=10.32.0.15 DST=10.32.0.14 LEN=60 TOS=0x00 PREC=0x00 TTL=63 ID=16881 DF PROTO=TCP SPT=48029 DPT=12865 WINDOW=42600 RES=0x00 SYN URGP=0
Jul 31 14:52:12 gke-cluster-1-default-pool-02a8e156-dr3f kernel: calico-packet: IN=cali34a48ed36b3 OUT=cali2ff164dea95 MAC=ee:ee:ee:ee:ee:ee:76:35:a4:01:96:bd:08:00 SRC=10.32.0.15 DST=10.32.0.14 LEN=60 TOS=0x00 PREC=0x00 TTL=63 ID=16882 DF PROTO=TCP SPT=48029 DPT=12865 WINDOW=42600 RES=0x00 SYN URGP=0
Jul 31 14:52:28 gke-cluster-1-default-pool-02a8e156-dr3f kernel: calico-packet: IN=cali34a48ed36b3 OUT=cali2ff164dea95 MAC=ee:ee:ee:ee:ee:ee:76:35:a4:01:96:bd:08:00 SRC=10.32.0.15 DST=10.32.0.14 LEN=60 TOS=0x00 PREC=0x00 TTL=63 ID=16883 DF PROTO=TCP SPT=48029 DPT=12865 WINDOW=42600 RES=0x00 SYN URGP=0
Jul 31 14:53:01 gke-cluster-1-default-pool-02a8e156-dr3f kernel: calico-packet: IN=cali34a48ed36b3 OUT=cali2ff164dea95 MAC=ee:ee:ee:ee:ee:ee:76:35:a4:01:96:bd:08:00 SRC=10.32.0.15 DST=10.32.0.14 LEN=60 TOS=0x00 PREC=0x00 TTL=63 ID=16884 DF PROTO=TCP SPT=48029 DPT=12865 WINDOW=42600 RES=0x00 SYN URGP=0
Установим iptables-tailer
Поправим манифест iptables-tailer.yaml
env:
- name: "JOURNAL_DIRECTORY"
value: "/var/log/journal"
- name: "IPTABLES_LOG_PREFIX"
value: "calico-packet:"
iptables-tailer умеет работать с systemd journal - для этого надо передать ему параметр JOURNAL_DIRECTORY б указав каталог с файлами журнала (по умолчанию, /var/log/journal)
kubectl apply -f kit/deploy/kit-serviceaccount.yaml
kubectl apply -f kit/deploy/kit-clusterrole.yaml
kubectl apply -f kit/deploy/kit-clusterrolebinding.yaml
kubectl apply -f kit/deploy/iptables-tailer.yaml
Перезапустим тест удалив cr и создав вновь
kubectl delete -f kit/deploy/cr.yaml
netperf.app.example.com "example" deleted
kubectl apply -f kit/deploy/cr.yaml
netperf.app.example.com/example created
Проверим
kubectl describe pod --selector=app=netperf-operator
Name: netperf-server-f6b4c7e04d56
Namespace: default
Priority: 0
Node: gke-cluster-1-default-pool-068f038d-4h0p/10.166.0.3
Start Time: Fri, 31 Jul 2020 18:11:25 +0300
Labels: app=netperf-operator
netperf-type=server
Annotations: cni.projectcalico.org/podIP: 10.32.1.4/32
kubernetes.io/limit-ranger: LimitRanger plugin set: cpu request for container netperf-server-f6b4c7e04d56
Status: Running
IP: 10.32.1.4
IPs:
IP: 10.32.1.4
Controlled By: Netperf/example
Containers:
netperf-server-f6b4c7e04d56:
Container ID: docker://47233e57a3a83ef573d8e28be8adbaf2f6cb14b25869e204e4b84c5b13e24a3a
Image: tailoredcloud/netperf:v2.7
Image ID: docker-pullable://tailoredcloud/netperf@sha256:0361f1254cfea87ff17fc1bd8eda95f939f99429856f766db3340c8cdfed1cf1
Port: <none>
Host Port: <none>
State: Running
Started: Fri, 31 Jul 2020 18:11:30 +0300
Ready: True
Restart Count: 0
Requests:
cpu: 100m
Environment: <none>
Mounts:
/var/run/secrets/kubernetes.io/serviceaccount from default-token-66ln5 (ro)
Conditions:
Type Status
Initialized True
Ready True
ContainersReady True
PodScheduled True
Volumes:
default-token-66ln5:
Type: Secret (a volume populated by a Secret)
SecretName: default-token-66ln5
Optional: false
QoS Class: Burstable
Node-Selectors: <none>
Tolerations: node.kubernetes.io/not-ready:NoExecute for 300s
node.kubernetes.io/unreachable:NoExecute for 300s
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Scheduled 9m34s default-scheduler Successfully assigned default/netperf-server-f6b4c7e04d56 to gke-cluster-1-default-pool-068f038d-4h0p
Normal Pulling 9m33s kubelet, gke-cluster-1-default-pool-068f038d-4h0p Pulling image "tailoredcloud/netperf:v2.7"
Normal Pulled 9m30s kubelet, gke-cluster-1-default-pool-068f038d-4h0p Successfully pulled image "tailoredcloud/netperf:v2.7"
Normal Created 9m30s kubelet, gke-cluster-1-default-pool-068f038d-4h0p Created container netperf-server-f6b4c7e04d56
Normal Started 9m29s kubelet, gke-cluster-1-default-pool-068f038d-4h0p Started container netperf-server-f6b4c7e04d56
Warning PacketDrop 9m24s kube-iptables-tailer Packet dropped when receiving traffic from 10.32.2.8
Warning PacketDrop 2m14s (x3 over 7m12s) kube-iptables-tailer Packet dropped when receiving traffic from client (10.32.2.8)
Теперь можно наблюдать в events сообщения о дропах пакетов
- Исправим ошибку в нашей сетевой политике, чтобы Netperf снова начал работать
ingress:
- action: Allow
source:
selector: app == "netperf-operator"
- action: Log
- action: Deny
egress:
- action: Allow
destination:
selector: app == "netperf-operator"
- action: Log
- action: Deny
- Поправим манифест DaemonSet из репозитория, чтобы в логах отображались имена Podов, а не их IP-адреса
Приведем iptables-tailer.yaml к следующему виду
env:
- name: "POD_IDENTIFIER"
value: "name"
# - name: "POD_IDENTIFIER_LABEL"
# value: "netperf-type"
Результат:
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Warning PacketDrop 89s kube-iptables-tailer Packet dropped when receiving traffic from netperf-client-6a28b0921dda (10.32.2.14)
https://github.com/kubernetes-csi/csi-driver-host-path/blob/master/docs/deploy-1.17-and-later.md
Разворачиваем кластер
kind create cluster --config ./cluster/cluster.yaml
Клонируем репозиторий
git clone git@github.com:kubernetes-csi/csi-driver-host-path.git
Применяем CRD
# Change to the latest supported snapshotter version
export SNAPSHOTTER_VERSION=v2.0.1
# Apply VolumeSnapshot CRDs
kubectl apply -f https://raw.githubusercontent.com/kubernetes-csi/external-snapshotter/$SNAPSHOTTER_VERSION/config/crd/snapshot.storage.k8s.io_volumesnapshotclasses.yaml
kubectl apply -f https://raw.githubusercontent.com/kubernetes-csi/external-snapshotter/$SNAPSHOTTER_VERSION/config/crd/snapshot.storage.k8s.io_volumesnapshotcontents.yaml
kubectl apply -f https://raw.githubusercontent.com/kubernetes-csi/external-snapshotter/$SNAPSHOTTER_VERSION/config/crd/snapshot.storage.k8s.io_volumesnapshots.yaml
# Create snapshot controller
kubectl apply -f https://raw.githubusercontent.com/kubernetes-csi/external-snapshotter/$SNAPSHOTTER_VERSION/deploy/kubernetes/snapshot-controller/rbac-snapshot-controller.yaml
kubectl apply -f https://raw.githubusercontent.com/kubernetes-csi/external-snapshotter/$SNAPSHOTTER_VERSION/deploy/kubernetes/snapshot-controller/setup-snapshot-controller.yaml
Устанавливаем драйвер
./csi-driver-host-path/deploy/kubernetes-1.18/deploy.sh
Проверяем поды
kubectl get pods
NAME READY STATUS RESTARTS AGE
csi-hostpath-attacher-0 1/1 Running 0 106s
csi-hostpath-provisioner-0 1/1 Running 0 105s
csi-hostpath-resizer-0 1/1 Running 0 104s
csi-hostpath-snapshotter-0 1/1 Running 0 103s
csi-hostpath-socat-0 1/1 Running 0 103s
csi-hostpathplugin-0 3/3 Running 0 105s
snapshot-controller-0 1/1 Running 0 5m48s
Проверяем storageclass
kubectl get sc
NAME PROVISIONER RECLAIMPOLICY VOLUMEBINDINGMODE ALLOWVOLUMEEXPANSION AGE
standard (default) rancher.io/local-path Delete WaitForFirstConsumer false 12m
Применяем манифесты sc, pvc, app
for i in ./hw/csi-storageclass.yaml ./hw/csi-pvc.yaml ./hw/csi-app.yaml; do kubectl apply -f $i; done
storageclass.storage.k8s.io/csi-hostpath-sc created
persistentvolumeclaim/csi-pvc created
pod/my-csi-app created
kubectl get pv
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE
pvc-220ed5b5-ebd4-4a64-bb9a-c1fa567d47b7 1Gi RWO Delete Bound default/csi-pvc csi-hostpath-sc 7s
kubectl get pvc
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
csi-pvc Bound pvc-220ed5b5-ebd4-4a64-bb9a-c1fa567d47b7 1Gi RWO csi-hostpath-sc 28s
Инспектируем под
kubectl describe pods/my-csi-app
Name: my-csi-app
Namespace: default
Priority: 0
Node: kind-worker/172.18.0.2
Start Time: Tue, 28 Jul 2020 00:46:36 +0300
Labels: <none>
Annotations: kubectl.kubernetes.io/last-applied-configuration:
{"apiVersion":"v1","kind":"Pod","metadata":{"annotations":{},"name":"my-csi-app","namespace":"default"},"spec":{"containers":[{"command":[...
Status: Running
IP: 10.244.1.9
IPs:
IP: 10.244.1.9
Containers:
my-frontend:
Container ID: containerd://487ab4754a47e7f467cb6e27e2c47604881ae02c12e472f084f2e7285c6d5bbd
Image: busybox
Image ID: docker.io/library/busybox@sha256:9ddee63a712cea977267342e8750ecbc60d3aab25f04ceacfa795e6fce341793
Port: <none>
Host Port: <none>
Command:
sleep
1000000
State: Running
Started: Tue, 28 Jul 2020 00:46:45 +0300
Ready: True
Restart Count: 0
Environment: <none>
Mounts:
/data from my-csi-volume (rw)
/var/run/secrets/kubernetes.io/serviceaccount from default-token-p2sd2 (ro)
Conditions:
Type Status
Initialized True
Ready True
ContainersReady True
PodScheduled True
Volumes:
my-csi-volume:
Type: PersistentVolumeClaim (a reference to a PersistentVolumeClaim in the same namespace)
ClaimName: csi-pvc
ReadOnly: false
default-token-p2sd2:
Type: Secret (a volume populated by a Secret)
SecretName: default-token-p2sd2
Optional: false
QoS Class: BestEffort
Node-Selectors: <none>
Tolerations: node.kubernetes.io/not-ready:NoExecute for 300s
node.kubernetes.io/unreachable:NoExecute for 300s
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Scheduled 44s default-scheduler Successfully assigned default/my-csi-app to kind-worker
Normal SuccessfulAttachVolume 44s attachdetach-controller AttachVolume.Attach succeeded for volume "pvc-220ed5b5-ebd4-4a64-bb9a-c1fa567d47b7"
Normal Pulling 40s kubelet, kind-worker Pulling image "busybox"
Normal Pulled 35s kubelet, kind-worker Successfully pulled image "busybox"
Normal Created 35s kubelet, kind-worker Created container my-frontend
Normal Started 35s kubelet, kind-worker Started container my-frontend
Нас интересует следующая информация
Mounts:
/data from my-csi-volume (rw)
Volumes:
my-csi-volume:
Type: PersistentVolumeClaim (a reference to a PersistentVolumeClaim in the same namespace)
ClaimName: csi-pvc
ReadOnly: false
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal SuccessfulAttachVolume 44s attachdetach-controller AttachVolume.Attach succeeded for volume "pvc-220ed5b5-ebd4-4a64-bb9a-c1fa567d47b7"
Подтверждение создания объекта VolumeAttachment
kubectl describe volumeattachment
Name: csi-eb85c21cc2107b883d10fdb1bf15fce62d06166aa889b7528588c1e41e8d3942
Namespace:
Labels: <none>
Annotations: <none>
API Version: storage.k8s.io/v1
Kind: VolumeAttachment
Metadata:
Creation Timestamp: 2020-07-27T21:46:36Z
Managed Fields:
API Version: storage.k8s.io/v1
Fields Type: FieldsV1
fieldsV1:
f:status:
f:attached:
Manager: csi-attacher
Operation: Update
Time: 2020-07-27T21:46:36Z
API Version: storage.k8s.io/v1
Fields Type: FieldsV1
fieldsV1:
f:spec:
f:attacher:
f:nodeName:
f:source:
f:persistentVolumeName:
Manager: kube-controller-manager
Operation: Update
Time: 2020-07-27T21:46:36Z
Resource Version: 1189
Self Link: /apis/storage.k8s.io/v1/volumeattachments/csi-eb85c21cc2107b883d10fdb1bf15fce62d06166aa889b7528588c1e41e8d3942
UID: f9858161-7497-4200-a5ed-0a736d029a52
Spec:
Attacher: hostpath.csi.k8s.io
Node Name: kind-worker
Source:
Persistent Volume Name: pvc-220ed5b5-ebd4-4a64-bb9a-c1fa567d47b7
Status:
Attached: true
Events: <none>
Запишем данные в volume
kubectl exec -it my-csi-app /bin/sh
/ # touch /data/hello-world
/ # ls -la /data
total 8
drwxr-xr-x 2 root root 4096 Jul 27 21:24 .
drwxr-xr-x 1 root root 4096 Jul 27 21:15 ..
-rw-r--r-- 1 root root 0 Jul 27 21:24 hello-world
Создадим snapshot
kubectl apply -f hw/csi-snapshot-v1beta1.yaml
volumesnapshot.snapshot.storage.k8s.io/new-snapshot-demo created
Убедимся, что snapshot создался
kubectl get volumesnapshot
NAME AGE
new-snapshot-demo 94s
kubectl describe volumesnapshot
Name: new-snapshot-demo
Namespace: default
Labels: <none>
Annotations: kubectl.kubernetes.io/last-applied-configuration:
{"apiVersion":"snapshot.storage.k8s.io/v1beta1","kind":"VolumeSnapshot","metadata":{"annotations":{},"name":"new-snapshot-demo","namespace...
API Version: snapshot.storage.k8s.io/v1beta1
Kind: VolumeSnapshot
Metadata:
Creation Timestamp: 2020-07-27T21:49:13Z
Finalizers:
snapshot.storage.kubernetes.io/volumesnapshot-as-source-protection
snapshot.storage.kubernetes.io/volumesnapshot-bound-protection
Generation: 1
Managed Fields:
API Version: snapshot.storage.k8s.io/v1beta1
Fields Type: FieldsV1
fieldsV1:
f:metadata:
f:finalizers:
v:"snapshot.storage.kubernetes.io/volumesnapshot-bound-protection":
Manager: snapshot-controller
Operation: Update
Time: 2020-07-27T21:49:13Z
Resource Version: 1642
Self Link: /apis/snapshot.storage.k8s.io/v1beta1/namespaces/default/volumesnapshots/new-snapshot-demo
UID: 41b224d8-9699-43bf-88f5-c65204b0f99a
Spec:
Source:
Persistent Volume Claim Name: csi-pvc
Volume Snapshot Class Name: csi-hostpath-snapclass
Status:
Bound Volume Snapshot Content Name: snapcontent-41b224d8-9699-43bf-88f5-c65204b0f99a
Ready To Use: false
Events: <none>
kubectl get volumesnapshotcontent
NAME AGE
snapcontent-41b224d8-9699-43bf-88f5-c65204b0f99a 64s
kubectl describe volumesnapshotcontents
Name: snapcontent-41b224d8-9699-43bf-88f5-c65204b0f99a
Namespace:
Labels: <none>
Annotations: <none>
API Version: snapshot.storage.k8s.io/v1beta1
Kind: VolumeSnapshotContent
Metadata:
Creation Timestamp: 2020-07-27T21:49:13Z
Finalizers:
snapshot.storage.kubernetes.io/volumesnapshotcontent-bound-protection
Generation: 1
Managed Fields:
API Version: snapshot.storage.k8s.io/v1beta1
Fields Type: FieldsV1
fieldsV1:
f:metadata:
f:finalizers:
.:
v:"snapshot.storage.kubernetes.io/volumesnapshotcontent-bound-protection":
Manager: snapshot-controller
Operation: Update
Time: 2020-07-27T21:49:13Z
API Version: snapshot.storage.k8s.io/v1beta1
Fields Type: FieldsV1
fieldsV1:
f:status:
.:
f:creationTime:
f:readyToUse:
f:restoreSize:
f:snapshotHandle:
Manager: csi-snapshotter
Operation: Update
Time: 2020-07-27T21:50:00Z
Resource Version: 1777
Self Link: /apis/snapshot.storage.k8s.io/v1beta1/volumesnapshotcontents/snapcontent-41b224d8-9699-43bf-88f5-c65204b0f99a
UID: b120abb0-0470-4550-a900-dc1ac88405db
Spec:
Deletion Policy: Delete
Driver: hostpath.csi.k8s.io
Source:
Volume Handle: a4fc4a93-d052-11ea-8b8f-c22405f4a253
Volume Snapshot Class Name: csi-hostpath-snapclass
Volume Snapshot Ref:
API Version: snapshot.storage.k8s.io/v1beta1
Kind: VolumeSnapshot
Name: new-snapshot-demo
Namespace: default
Resource Version: 1636
UID: 41b224d8-9699-43bf-88f5-c65204b0f99a
Status:
Creation Time: 1595886600444714000
Ready To Use: true
Restore Size: 1073741824
Snapshot Handle: 1eb7cf15-d053-11ea-8b8f-c22405f4a253
Events: <none>
Удалим pod, pvc и pv
kubectl delete -f ./hw/csi-app.yaml
pod "my-csi-app" deleted
kubectl delete -f ./hw/csi-pvc.yaml
persistentvolumeclaim "csi-pvc" deleted
kubectl get pvc
No resources found in default namespace.
kubectl get pv
No resources found in default namespace.
Восстанавливаемся из snapshot
kubectl apply -f ./hw/csi-restore.yaml
persistentvolumeclaim/hpvc-restore created
Применяем манифест пода
kubectl apply -f ./hw/csi-app.yaml
persistentvolumeclaim/hpvc-restore created
kubectl get pv
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE
pvc-25dc08a9-8f0e-4748-92df-0dd39526cba7 1Gi RWO Delete Bound default/csi-pvc csi-hostpath-sc 4s
kubectl get pvc
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
csi-pvc Bound pvc-25dc08a9-8f0e-4748-92df-0dd39526cba7 1Gi RWO csi-hostpath-sc 5s
Проверяем, что данные на месте
kubectl exec my-csi-app -- ls -la /data
total 8
drwxr-xr-x 2 root root 4096 Jul 27 22:00 .
drwxr-xr-x 1 root root 4096 Jul 27 22:00 ..
-rw-r--r-- 1 root root 0 Jul 27 21:48 hello-world
Развертывание k8s-кластер, к которому добавляется хранилище на iSCSI и проверка работы snapshots | Задание со ⭐
Полезные ссылки:
https://github.com/kubernetes/examples/tree/master/volumes/iscsi
https://www.saqwel.ru/articles/linux/nastrojka-linux-iscsi-posredstvom-targetcli/
https://kifarunix.com/how-to-install-and-configure-iscsi-storage-server-on-ubuntu-18-04/
- На vm для iscsi хранилища добавляем дополнительный диск на 200Гб
- Добавляем второй сетевой интерфейс для трафика хранилища
Выключаем SELinux: disabled > /etc/sysconfig/selinux
Если не перезагрузились то: setenforce 0
Выключаем firewall
systemctl disable firewalld
systemctl stop firewalld
Устанавливаем targetcli и targetd (рассмотрим в следующем разделе)
yum install -f targetcli targetd
Сделаем доступным демона (службу) target и запустить его
systemctl enable target
systemctl start target
Создание physical volume
pvcreate /dev/sdb
Physical volume "/dev/sdb" successfully created.
pvdisplay
"/dev/sdb" is a new physical volume of "200.00 GiB"
--- NEW Physical volume ---
PV Name /dev/sdb
VG Name
PV Size 200.00 GiB
Allocatable NO
PE Size 0
Total PE 0
Free PE 0
Allocated PE 0
PV UUID DFvQro-J3FV-xMsW-gRqX-flHE-Gqne-hsdneM
Создаем volume group
vgcreate vg-targetd /dev/sdb
Volume group "vg-targetd" successfully created
vgdisplay
--- Volume group ---
VG Name vg-targetd
System ID
Format lvm2
Metadata Areas 1
Metadata Sequence No 1
VG Access read/write
VG Status resizable
MAX LV 0
Cur LV 0
Open LV 0
Max PV 0
Cur PV 1
Act PV 1
VG Size <200.00 GiB
PE Size 4.00 MiB
Total PE 51199
Alloc PE / Size 0 / 0
Free PE / Size 51199 / <200.00 GiB
VG UUID ZGVJ7J-DQ2N-SlTZ-O8mq-mTlA-6Arh-gQFG3a
vgs
VG #PV #LV #SN Attr VSize VFree
vg-targetd 1 0 0 wz--n- <200.00g <200.00g
Создаем logical volume
lvcreate -L1024 -n lv01 vg-targetd
Logical volume "lv01" created.
lvs
LV VG Attr LSize Pool Origin Data% Meta% Move Log Cpy%Sync Convert
lv01 vg-targetd -wi-a----- 1.00g
Настраиваем iscsi target через targetcli
targetcli
targetcli shell version 2.1.fb49
Copyright 2011-2013 by Datera, Inc and others.
For help on commands, type 'help'.
/> ls
o- / ......................................................................................................................... [...]
o- backstores .............................................................................................................. [...]
| o- block .................................................................................................. [Storage Objects: 0]
| o- fileio ................................................................................................. [Storage Objects: 0]
| o- pscsi .................................................................................................. [Storage Objects: 0]
| o- ramdisk ................................................................................................ [Storage Objects: 0]
o- iscsi ............................................................................................................ [Targets: 0]
o- loopback ......................................................................................................... [Targets: 0]
/> /backstores/block create storage1 /dev/vg-targetd/lv01
Created block storage object storage1 using /dev/vg-targetd/lv01.
/> iscsi/ create iqn.2020-07.local.neclab:dev-storage-iscsi
Created target iqn.2020-07.local.neclab:dev-storage-iscsi.
Created TPG 1.
Global pref auto_add_default_portal=true
Created default portal listening on all IPs (0.0.0.0), port 3260.
/> /iscsi/iqn.2020-07.local.neclab:dev-storage-iscsi/tpg1/luns/ create /backstores/block/storage1
Created LUN 0.
/> /iscsi/iqn.2020-07.local.neclab:dev-storage-iscsi/tpg1/acls create iqn.2020-07.local.neclab:dev-worker1
Created Node ACL for iqn.2020-07.local.neclab:dev-worker1
Created mapped LUN 0.
/> /iscsi/iqn.2020-07.local.neclab:dev-storage-iscsi/tpg1/acls create iqn.2020-07.local.neclab:dev-worker2
Created Node ACL for iqn.2020-07.local.neclab:dev-worker2
Created mapped LUN 0.
/> /iscsi/iqn.2020-07.local.neclab:dev-storage-iscsi/tpg1/acls create iqn.2020-07.local.neclab:dev-worker3
Created Node ACL for iqn.2020-07.local.neclab:dev-worker3
Created mapped LUN 0.
/> /iscsi/iqn.2020-07.local.neclab:dev-storage-iscsi/tpg1/ set parameter AuthMethod=None
Parameter AuthMethod is now 'None'.
/> /iscsi/iqn.2020-07.local.neclab:dev-storage-iscsi/tpg1/ set attribute authentication=0
Parameter authentication is now '0'.
/> ls
o- / ......................................................................................................................... [...]
o- backstores .............................................................................................................. [...]
| o- block .................................................................................................. [Storage Objects: 1]
| | o- storage1 ............................................................. [/dev/vg-targetd/lv01 (1.0GiB) write-thru activated]
| | o- alua ................................................................................................... [ALUA Groups: 1]
| | o- default_tg_pt_gp ....................................................................... [ALUA state: Active/optimized]
| o- fileio ................................................................................................. [Storage Objects: 0]
| o- pscsi .................................................................................................. [Storage Objects: 0]
| o- ramdisk ................................................................................................ [Storage Objects: 0]
o- iscsi ............................................................................................................ [Targets: 1]
| o- iqn.2020-07.local.neclab:dev-storage-iscsi ........................................................................ [TPGs: 1]
| o- tpg1 ............................................................................................... [no-gen-acls, no-auth]
| o- acls .......................................................................................................... [ACLs: 3]
| | o- iqn.2020-07.local.neclab:dev-worker1 ................................................................. [Mapped LUNs: 1]
| | | o- mapped_lun0 .............................................................................. [lun0 block/storage1 (rw)]
| | o- iqn.2020-07.local.neclab:dev-worker2 ................................................................. [Mapped LUNs: 1]
| | | o- mapped_lun0 .............................................................................. [lun0 block/storage1 (rw)]
| | o- iqn.2020-07.local.neclab:dev-worker3 ................................................................. [Mapped LUNs: 1]
| | o- mapped_lun0 .............................................................................. [lun0 block/storage1 (rw)]
| o- luns .......................................................................................................... [LUNs: 1]
| | o- lun0 ....................................................... [block/storage1 (/dev/vg-targetd/lv01) (default_tg_pt_gp)]
| o- portals .................................................................................................... [Portals: 1]
| o- 0.0.0.0:3260 ..................................................................................................... [OK]
o- loopback ......................................................................................................... [Targets: 0]
/>
- Добавляем второй сетевой интерфейс для трафика хранилища на каждую воркер ноду
Устанавливаем iscsi инициатор
apt install -y open-iscsi
Редактируем /etc/iscsi/initiatorname.iscsi
```console
InitiatorName=iqn.2020-07.local.neclab:dev-workerN
где N - порядковый номер ноды
Перезапускаем iscsid и open-iscsi
systemctl restart iscsid open-iscsi
systemctl enable iscsid open-iscsi
Проверяем подключение к iscsi хранилищу
iscsiadm -m discovery -t sendtargets -p 192.2.1.21
192.2.1.21:3260,1 iqn.2020-07.local.neclab:dev-storage-iscsi
iscsiadm -m node --login
Logging in to [iface: default, target: iqn.2020-07.local.neclab:dev-storage-iscsi, portal: 192.2.1.21,3260] (multiple)
Login to [iface: default, target: iqn.2020-07.local.neclab:dev-storage-iscsi, portal: 192.2.1.21,3260] successful.
iscsiadm -m node --logout
Logging out of session [sid: 9, target: iqn.2020-07.local.neclab:dev-storage-iscsi, portal: 192.2.1.21,3260]
Logout of [sid: 9, target: iqn.2020-07.local.neclab:dev-storage-iscsi, portal: 192.2.1.21,3260] successful.
Применяем манифесты создания pv, pvc и pod
kubectl apply -f nginx-pv.yaml -f nginx-pvc.yaml -f nginx.yaml
persistentvolume/iscsi-pv created
persistentvolumeclaim/myclaim created
pod/iscsi-pv-pod1 created
kubectl get pvc
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
myclaim Bound iscsi-pv 1Gi RWO 2s
iscsi kubectl get pv
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE
iscsi-pv 1Gi RWO Retain Bound default/myclaim 2m30s
kubectl get pods -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
iscsi-pv-pod1 1/1 Running 0 21s 192.168.184.72 dev-worker2 <none> <none>
Записываем данные в наш volume
kubectl exec -it iscsi-pv-pod1 /bin/sh
/ # ls -la /var/lib/busybox/
total 24
drwxr-xr-x 3 root root 4096 Jul 26 12:33 .
drwxr-xr-x 3 root root 4096 Jul 26 12:33 ..
drwx------ 2 root root 16384 Jul 26 12:33 lost+found
/ # echo Hello! > /var/lib/busybox/text.txt
/ # ls -la /var/lib/busybox/
total 28
drwxr-xr-x 3 root root 4096 Jul 26 12:34 .
drwxr-xr-x 3 root root 4096 Jul 26 12:33 ..
drwx------ 2 root root 16384 Jul 26 12:33 lost+found
-rw-r--r-- 1 root root 7 Jul 26 12:34 text.txt
Идем на хранилище и создаем snapshot
lvcreate -L 500MB -s -n sn01 /dev/vg-targetd/lv01
Rounding up size to full physical extent 12.00 MiB
Logical volume "sn01" created.
В volume пода удаляем данные
kubectl exec -it iscsi-pv-pod1 /bin/sh
/ # ls -la /var/lib/busybox/
total 28
drwxr-xr-x 3 root root 4096 Jul 26 12:34 .
drwxr-xr-x 3 root root 4096 Jul 26 12:33 ..
drwx------ 2 root root 16384 Jul 26 12:33 lost+found
-rw-r--r-- 1 root root 7 Jul 26 12:34 text.txt
/ # rm /var/lib/busybox/text.txt
/ # ls -la /var/lib/busybox/
total 24
drwxr-xr-x 3 root root 4096 Jul 26 12:36 .
drwxr-xr-x 3 root root 4096 Jul 26 12:33 ..
drwx------ 2 root root 16384 Jul 26 12:33 lost+found
Удаляем наши pv, pvc, pod
kubectl delete pod iscsi-pv-pod1
pod "iscsi-pv-pod1" deleted
kubectl delete pvc myclaim
persistentvolumeclaim "myclaim" deleted
iscsi kubectl delete pv iscsi-pv
persistentvolume "iscsi-pv" deleted
kubectl get pv
No resources found in default namespace.
iscsi kubectl get pvc
No resources found in default namespace.
На хранилище останавливаем сервис target (в противном случаем snapshot не смержить)
systemctl stop target
lvconvert --merge /dev/vg-targetd/sn01
Merging of volume vg-targetd/sn01 started.
vg-targetd/lv01: Merged: 100.00%
Снова стартуем сервис
systemctl start target
Снова применяем манифесты
kubectl apply -f nginx-pv.yaml -f nginx-pvc.yaml -f nginx.yaml
persistentvolume/iscsi-pv created
persistentvolumeclaim/myclaim created
pod/iscsi-pv-pod1 created
kubectl get pv,pvc,pod
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE
persistentvolume/iscsi-pv 1Gi RWO Retain Bound default/myclaim 98s
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
persistentvolumeclaim/myclaim Bound iscsi-pv 1Gi RWO 98s
NAME READY STATUS RESTARTS AGE
pod/iscsi-pv-pod1 1/1 Running 0 97s
Проверяем, что данные из snapshot успешно восстановились
kubectl exec -it iscsi-pv-pod1 /bin/sh
/ # ls -la /var/lib/busybox/
lost+found/ text.txt
/ # ls -la /var/lib/busybox/text.txt
-rw-r--r-- 1 root root 7 Jul 26 13:03 /var/lib/busybox/text.txt
/ # cat /var/lib/busybox/text.txt
Hello!
Полезные ссылки:
https://github.com/kubernetes-incubator/external-storage/tree/master/iscsi/targetd
https://ansilh.com/17-persistent_volumes/02-iscsi-provisioner/
- volume group берем из предыдущего раздела описания
- удаляем старые lv
Настраиваем targetd
/etc/target/targetd.yaml
user: admin
password: password
pool_name: vg-targetd
ssl: false
target_name: iqn.2020-07.local.neclab:dev-storage-iscsi
Перезапускаем сервис
systemctl start targetd
systemctl enable targetd
systemctl status targetd
● targetd.service - targetd storage array API daemon
Loaded: loaded (/usr/lib/systemd/system/targetd.service; enabled; vendor preset: disabled)
Active: active (running) since Sun 2020-07-26 16:44:21 MSK; 22h ago
Main PID: 4924 (targetd)
CGroup: /system.slice/targetd.service
└─4924 targetd
Jul 26 16:44:21 dev-storage-iscsi systemd[1]: Started targetd storage array API daemon.
Jul 26 16:44:27 dev-storage-iscsi targetd[4924]: INFO:root:started server (TLS no)
Создаем ns для провижинера
kubectl create ns iscsi-provisioner
namespace/iscsi-provisioner created
Создаем секрет для подключения к targetd
kubectl create secret generic targetd-account --from-literal=username=admin --from-literal=password=password -n iscsi-provisioner
Применяем манифесты провижинера
kubectl apply -f iscsi-provisioner-d.yaml -n iscsi-provisioner
clusterrole.rbac.authorization.k8s.io/iscsi-provisioner-runner created
clusterrolebinding.rbac.authorization.k8s.io/run-iscsi-provisioner created
serviceaccount/iscsi-provisioner created
deployment.apps/iscsi-provisioner created
kubectl get pods -n iscsi-provisioner
NAME READY STATUS RESTARTS AGE
iscsi-provisioner-6b58bd885-wdqtw 1/1 Running 0 34s
kubectl apply -f iscsi-provisioner-class.yaml -n iscsi-provisioner
storageclass.storage.k8s.io/iscsi-targetd-vg-targetd created
Применяем манифесты нашего приложения
kubectl apply -f nginx-pvc.yaml
persistentvolumeclaim/myclaim created
kubectl apply -f nginx.yaml
pod/iscsi-pv-pod1 created
Проверяем, что pv и pvc в статусе bound
kubectl get pv
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE
pvc-24701fd6-70b3-40b3-9cf4-e2e1f5936cfb 100Mi RWO Delete Bound default/myclaim iscsi-targetd-vg-targetd 25s
kubectl get pvc
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
myclaim Bound pvc-24701fd6-70b3-40b3-9cf4-e2e1f5936cfb 100Mi RWO iscsi-targetd-vg-targetd 62s
kubectl get pods -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
iscsi-pv-pod1 1/1 Running 0 40s 192.168.41.206 dev-worker1 <none> <none>
Идем на vm iscsi хранилища и проверяем там
lvs
LV VG Attr LSize Pool Origin Data% Meta% Move Log Cpy%Sync Convert
pvc-24701fd6-70b3-40b3-9cf4-e2e1f5936cfb vg-targetd -wi-ao---- 100.00m
targetcli
targetcli shell version 2.1.fb49
Copyright 2011-2013 by Datera, Inc and others.
For help on commands, type 'help'.
/> ls
o- / ......................................................................................................................... [...]
o- backstores .............................................................................................................. [...]
| o- block .................................................................................................. [Storage Objects: 1]
| | o- vg-targetd:pvc-24701fd6-70b3-40b3-9cf4-e2e1f5936cfb [/dev/vg-targetd/pvc-24701fd6-70b3-40b3-9cf4-e2e1f5936cfb (100.0MiB) write-thru activated]
| | o- alua ................................................................................................... [ALUA Groups: 1]
| | o- default_tg_pt_gp ....................................................................... [ALUA state: Active/optimized]
| o- fileio ................................................................................................. [Storage Objects: 0]
| o- pscsi .................................................................................................. [Storage Objects: 0]
| o- ramdisk ................................................................................................ [Storage Objects: 0]
o- iscsi ............................................................................................................ [Targets: 1]
| o- iqn.2020-07.local.neclab:dev-storage-iscsi ........................................................................ [TPGs: 1]
| o- tpg1 ............................................................................................... [no-gen-acls, no-auth]
| o- acls .......................................................................................................... [ACLs: 3]
| | o- iqn.2020-07.local.neclab:dev-worker1 ................................................................. [Mapped LUNs: 1]
| | | o- mapped_lun0 ................................... [lun0 block/vg-targetd:pvc-24701fd6-70b3-40b3-9cf4-e2e1f5936cfb (rw)]
| | o- iqn.2020-07.local.neclab:dev-worker2 ................................................................. [Mapped LUNs: 1]
| | | o- mapped_lun0 ................................... [lun0 block/vg-targetd:pvc-24701fd6-70b3-40b3-9cf4-e2e1f5936cfb (rw)]
| | o- iqn.2020-07.local.neclab:dev-worker3 ................................................................. [Mapped LUNs: 1]
| | o- mapped_lun0 ................................... [lun0 block/vg-targetd:pvc-24701fd6-70b3-40b3-9cf4-e2e1f5936cfb (rw)]
| o- luns .......................................................................................................... [LUNs: 1]
| | o- lun0 [block/vg-targetd:pvc-24701fd6-70b3-40b3-9cf4-e2e1f5936cfb (/dev/vg-targetd/pvc-24701fd6-70b3-40b3-9cf4-e2e1f5936cfb) (default_tg_pt_gp)]
| o- portals .................................................................................................... [Portals: 1]
| o- 0.0.0.0:3260 ..................................................................................................... [OK]
o- loopback ......................................................................................................... [Targets: 0]
/>
В качестве хранилища кода и CI-системы в домашнем задании мы будем использовать SaaS GitLab. Зарегистрируемся там и создадим GitLab публичный проект microservicesdemo.
Переместим в проект microservices-demo код из GitHub репозитория:
git clone https://github.com/GoogleCloudPlatform/microservices-demo
cd microservices-demo
git remote add gitlab git@gitlab.com:kovtalex/microservices-demo.git
git remote remove origin
Предварительно необходимо добавить публичный ssh ключ в профиль GitLab, либо использовать https
- Перед началом выполнения домашнего задания необходимо подготовить Helm чарты для каждого микросервиса
- Можно воспользоваться наработками из предыдущих домашних заданий, либо скопировать готовые чарты из демонстрационного репозитория (директория deploy/charts)
- Во всех манифестах, описывающих deployment, обязательно должны быть параметризованы название образа и его тег. Рекомендуется придерживаться следующего формата:
Результат поместим в директорию deploy/charts. Должен получиться следующий вывод:
tree -L 1 deploy/charts
deploy/charts
├── adservice
├── cartservice
├── checkoutservice
├── currencyservice
├── emailservice
├── frontend
├── loadgenerator
├── paymentservice
├── productcatalogservice
├── recommendationservice
└── shippingservice
- Развернем managed Kubernetes кластер в GCP размером 4 ноды типа n1-standard-2
- Установим istio как GKE аддон
gcloud container clusters get-credentials cluster-1 --zone europe-west1-b --project angular-pursuit-275120
Fetching cluster endpoint and auth data.
kubeconfig entry generated for cluster-1.
gcloud container clusters list
NAME LOCATION MASTER_VERSION MASTER_IP MACHINE_TYPE NODE_VERSION NUM_NODES STATUS
cluster-1 europe-west1-b 1.16.9-gke.6 35.189.206.79 n1-standard-2 1.16.9-gke.6 4 RUNNING
gcloud beta container clusters update cluster-1 \
--update-addons=Istio=ENABLED --istio-config=auth=MTLS_PERMISSIVE --region=europe-west1-b
Updating cluster-1...done.
Updated [https://container.googleapis.com/v1beta1/projects/angular-pursuit-275120/zones/europe-west1-b/clusters/cluster-1].
To inspect the contents of your cluster, go to: https://console.cloud.google.com/kubernetes/workload_/gcloud/europe-west1-b/cluster-1?project=angular-pursuit-275120
kubectl get service -n istio-system
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
istio-citadel ClusterIP 10.0.3.16 <none> 8060/TCP,15014/TCP 60s
istio-galley ClusterIP 10.0.13.196 <none> 443/TCP,15014/TCP,9901/TCP 60s
istio-ingressgateway LoadBalancer 10.0.5.79 34.78.81.2 15020:31158/TCP,80:30366/TCP,443:30126/TCP,31400:30279/TCP,15029:31001/TCP,15030:31300/TCP,15031:31993/TCP,15032:31534/TCP,15443:30087/TCP 59s
istio-pilot ClusterIP 10.0.9.184 <none> 15010/TCP,15011/TCP,8080/TCP,15014/TCP 59s
istio-policy ClusterIP 10.0.13.162 <none> 9091/TCP,15004/TCP,15014/TCP 59s
istio-sidecar-injector ClusterIP 10.0.6.145 <none> 443/TCP,15014/TCP 59s
istio-telemetry ClusterIP 10.0.14.197 <none> 9091/TCP,15004/TCP,15014/TCP,42422/TCP 59s
promsd ClusterIP 10.0.2.124 <none> 9090/TCP
kubectl get pods -n istio-system
NAME READY STATUS RESTARTS AGE
istio-citadel-bc69f964d-p9m8p 1/1 Running 0 5m32s
istio-galley-57d74676f6-nk4vr 1/1 Running 0 5m32s
istio-ingressgateway-6855988795-6cv2m 0/1 Running 0 5m32s
istio-pilot-b6866b4d9-c4c9g 2/2 Running 1 5m31s
istio-policy-698f74bc98-xjsn7 2/2 Running 3 5m31s
istio-security-post-install-1.4.6-gke.0-w95m9 0/1 Completed 0 5m29s
istio-sidecar-injector-d5485f495-qxth5 1/1 Running 0 5m30s
istio-telemetry-58769bf595-t5sw6 2/2 Running 2 5m30s
promsd-696bcc5b96-tm44r 2/2 Running 1 5m29s
- Автоматизировано создание Kubernetes кластера
- Кластер разворачиваться после запуска pipeline в GitLab для окружения dev и prod
Cсылка: https://gitlab.com/kovtalex-repo/microservices-demo/base-layer
- Соберем Docker образы для всех микросервиса и поместим данные образы в Docker Hub
- При тегировании образов используем подход semver, например, первому собранному образу логично выставить тег v0.0.1
После выполнения данного шага в Docker Hub должно находиться как минимум по одному образу для каждого микросервиса
Напишем Makefile и исполним:
make
make release
Подготовлен простой pipeline, который состоит из стадии сборки Docker образа для каждого из микросервисов и Push данного образа в Docker Hub.
В качестве тега образа используется tag коммита, инициирующего сборку (переменная CI_COMMIT_TAG в GitLab CI)
Предварительно требуется прописать следующие переменные окружения в Gitlab:
- CI_REGISTRY_USER - Docker ID
- CI_TOKEN - Docker token
https://gitlab.com/kovtalex/microservices-demo
- Добавим официальный репозиторий Flux
helm repo add fluxcd https://charts.fluxcd.io
"fluxcd" has been added to your repositories
- Произведем установку Flux в кластер, в namespace flux
helm upgrade --install flux fluxcd/flux -f flux.values.yaml --namespace flux --create-namespace
Release "flux" does not exist. Installing it now.
NAME: flux
LAST DEPLOYED: Fri Jun 26 02:55:36 2020
NAMESPACE: flux
STATUS: deployed
REVISION: 1
TEST SUITE: None
NOTES:
Get the Git deploy key by either (a) running
kubectl -n flux logs deployment/flux | grep identity.pub | cut -d '"' -f2
or by (b) installing fluxctl through
https://docs.fluxcd.io/en/latest/references/fluxctl#installing-fluxctl
and running:
fluxctl identity --k8s-fwd-ns flux
- Установим Helm operator:
flux.values.yaml
git:
url: git@gitlab.com:kovtalex/microservices-demo.git
path: deploy
ciSkip: true
pollInterval: 1m
registry:
automationInterval: 1m
helm upgrade --install helm-operator fluxcd/helm-operator -f helm-operator.values.yaml --namespace flux
Release "helm-operator" does not exist. Installing it now.
NAME: helm-operator
LAST DEPLOYED: Fri Jun 26 02:57:24 2020
NAMESPACE: flux
STATUS: deployed
REVISION: 1
TEST SUITE: None
NOTES:
Flux Helm Operator docs https://docs.fluxcd.io
Example:
AUTH_VALUES=$(cat <<-END
usePassword: true
password: "redis_pass"
usePasswordFile: true
END
)
kubectl create secret generic redis-auth --from-literal=values.yaml="$AUTH_VALUES"
cat <<EOF | kubectl apply -f -
apiVersion: helm.fluxcd.io/v1
kind: HelmRelease
metadata:
name: redis
namespace: default
spec:
releaseName: redis
chart:
repository: https://kubernetes-charts.storage.googleapis.com
name: redis
version: 10.5.7
valuesFrom:
- secretKeyRef:
name: redis-auth
values:
master:
persistence:
enabled: false
volumePermissions:
enabled: true
metrics:
enabled: true
cluster:
enabled: false
EOF
watch kubectl get hr
Используем values из следующего файла
- Установим fluxctl на локальную машину для управления нашим CD инструментом. Руководство по установке достуно по ссылке
brew install fluxctl
export FLUX_FORWARD_NAMESPACE=flux
- Наконец, добавим в свой профиль GitLab публичный ssh-ключ, при помощи которого flux получит доступ к нашему git-репозиторию
Получить значение ключа можно следующей командой: fluxctl identity --k8s-fwd-ns flux
Пришло время проверить корректность работы Flux. Как мы уже знаем, Flux умеет автоматически синхронизировать состояние кластера и репозитория. Это касается не только сущностей HelmRelease, которыми мы будем оперировать для развертывания приложения, но и обыкновенных манифестов.
Поместим манифест, описывающий namespace microservices-demo в директорию deploy/namespaces и сделаем push в GitLab:
apiVersion: v1
kind: Namespace
metadata:
name: microservices-demo
Если все предыдущие шаги проделаны верно - в кластере через некоторое время будет создан namespace microservices-demo
kubectl get ns
NAME STATUS AGE
default Active 133m
flux Active 16m
istio-system Active 120m
kube-node-lease Active 134m
kube-public Active 134m
kube-system Active 134m
microservices-demo Active 14s
Также в логах pod с flux должна появиться строка, описывающая действия данного инструмента
kubectl logs flux-85bfb4df69-wdwnz -n flux | grep "kubectl apply -f"
ts=2020-06-26T00:11:46.205972915Z caller=sync.go:605 method=Sync cmd="kubectl apply -f -" took=640.185151ms err=null output="namespace/microservices-demo created"
Мы подобрались к сущностям, которыми управляет helm-operator - HelmRelease.
Для описания сущностей такого вида создадим отдельную директорию deploy/releases и поместим туда файл frontend.yaml с описанием конфигурации релиза.
---
apiVersion: helm.fluxcd.io/v1
kind: HelmRelease
metadata:
name: frontend
namespace: microservices-demo
annotations:
fluxcd.io/ignore: "false"
fluxcd.io/automated: "true"
flux.weave.works/tag.chart-image: semver:~v0.0
spec:
releaseName: frontend
helmVersion: v3
chart:
git: git@gitlab.com:kovtalex/microservices-demo.git
ref: master
path: deploy/charts/frontend
values:
image:
repository: kovtalex/frontend
tag: v0.0.1
Не забудем сделать push в GitLab после добавления манифеста
Опишем некоторые части манифеста HelmRelease:
- fluxcd.io/automated: "true" # 1 - Аннотация разрешает автоматическое обновление релиза в Kubernetes кластере в случае изменения версии Docker образа в Registry
- flux.weave.works/tag.chart-image: semver:~v0.0 - Указываем Flux следить за обновлениями конкретных Docker образов в Registry. Новыми считаются только образы, имеющие версию выше текущей и отвечающие маске семантического версионирования ~0.0 (например, 0.0.1, 0.0.72, но не 1.0.0)
- chart: - Helm chart, используемый для развертывания релиза. В нашем случае указываем git-репозиторий, и директорию с чартом внутри него
- values: - Переопределяем переменные Helm chart. В дальнейшем Flux может сам переписывать эти значения и делать commit в git-репозиторий (например, изменять тег Docker образа при его обновлении в Registry)
Более подробное описание доступно по ссылке
Убедимся что HelmRelease для микросервиса frontend появился в кластере:
kubectl get helmrelease -n microservices-demo
NAME RELEASE PHASE STATUS MESSAGE AGE
frontend frontend Succeeded deployed Release was successful for Helm release 'frontend' in 'microservices-demo'. 9h
По статусу мы можем понять, что релиз применился успешно, и frontend запущен. Дополнительно проверим это:
helm list -n microservices-demo
NAME NAMESPACE REVISION UPDATED STATUS CHART APP VERSION
frontend microservices-demo 1 2020-06-26 10:21:59.949005778 +0000 UTC deployed frontend-0.21.0 1.16.0
Командой fluxctl --k8s-fwd-ns flux sync можно инициировать синхронизацию вручную
- Внесем изменения в исходный код микросервиса frontend (не имеет значения, какие) и пересоберем образ, при этом инкрементировав версию тега (до v0.0.2)
- Дождемся автоматического обновления релиза в Kubernetes кластере (для просмотра ревизий релиза можно использовать команду helm history frontend -n microservices-demo)
- Проверим, изменилось ли что-либо в git-репозитории (в частности, в файле deploy/releases/frontend.yaml)
make -n APP_TAG=v0.0.2 build_frontend release_frontend
export USER_NAME=kovtalex && cd microservices-demo/src/frontend && docker build -t kovtalex/frontend:v0.0.2 .
docker push kovtalex/frontend:v0.0.2
make APP_TAG=v0.0.2 build_frontend release_frontend
helm history frontend -n microservices-demo
REVISION UPDATED STATUS CHART APP VERSION DESCRIPTION
1 Fri Jun 26 10:21:59 2020 superseded frontend-0.21.0 1.16.0 Install complete
2 Fri Jun 26 10:38:00 2020 deployed frontend-0.21.0 1.16.0 Upgrade complete
Тег изменился на: v0.0.2:
---
apiVersion: helm.fluxcd.io/v1
kind: HelmRelease
metadata:
name: frontend
namespace: microservices-demo
annotations:
fluxcd.io/ignore: "false"
fluxcd.io/automated: "true"
flux.weave.works/tag.chart-image: semver:~v0.0
spec:
releaseName: frontend
helmVersion: v3
chart:
git: git@gitlab.com:kovtalex/microservices-demo.git
ref: master
path: deploy/charts/frontend
values:
image:
repository: kovtalex/frontend
tag: v0.0.2
- Попробуем внести изменения в Helm chart frontend и поменять имя deployment на frontend-boutique
- Сделаем push измененного Helm chart в GitLab и понаблюдаем за процессом
- Найдем в логах helm-operator строки, указывающие на механизм проверки изменений в Helm chart и определения необходимости обновить релиз. Приложим данные строки к описанию PR.
kubectl logs helm-operator-745586d945-9xfvw -n flux
ts=2020-06-26T10:43:36.731329725Z caller=helm.go:69 component=helm version=v3 info="preparing upgrade for frontend" targetNamespace=microservices-demo release=frontend
ts=2020-06-26T10:43:36.735540145Z caller=helm.go:69 component=helm version=v3 info="resetting values to the chart's original version" targetNamespace=microservices-demo release=frontend
ts=2020-06-26T10:43:37.212661611Z caller=helm.go:69 component=helm version=v3 info="performing update for frontend" targetNamespace=microservices-demo release=frontend
ts=2020-06-26T10:43:37.222554135Z caller=helm.go:69 component=helm version=v3 info="dry run for frontend" targetNamespace=microservices-demo release=frontend
ts=2020-06-26T10:43:37.234971971Z caller=release.go:273 component=release release=frontend targetNamespace=microservices-demo resource=microservices-demo:helmrelease/frontend helmVersion=v3 info="no changes" phase=dry-run-compare
ts=2020-06-26T10:44:28.24689266Z caller=release.go:75 component=release release=frontend targetNamespace=microservices-demo resource=microservices-demo:helmrelease/frontend helmVersion=v3 info="starting sync run"
ts=2020-06-26T10:44:28.525151788Z caller=release.go:289 component=release release=frontend targetNamespace=microservices-demo resource=microservices-demo:helmrelease/frontend helmVersion=v3 info="running upgrade" action=upgrade
ts=2020-06-26T10:44:28.550775188Z caller=helm.go:69 component=helm version=v3 info="preparing upgrade for frontend" targetNamespace=microservices-demo release=frontend
ts=2020-06-26T10:44:28.561625492Z caller=helm.go:69 component=helm version=v3 info="resetting values to the chart's original version" targetNamespace=microservices-demo release=frontend
ts=2020-06-26T10:44:29.039606993Z caller=helm.go:69 component=helm version=v3 info="performing update for frontend" targetNamespace=microservices-demo release=frontend
ts=2020-06-26T10:44:29.052284599Z caller=helm.go:69 component=helm version=v3 info="creating upgraded release for frontend" targetNamespace=microservices-demo release=frontend
ts=2020-06-26T10:44:29.061677757Z caller=helm.go:69 component=helm version=v3 info="checking 2 resources for changes" targetNamespace=microservices-demo release=frontend
ts=2020-06-26T10:44:29.069535953Z caller=helm.go:69 component=helm version=v3 info="Looks like there are no changes for Service \"frontend\"" targetNamespace=microservices-demo release=frontend
ts=2020-06-26T10:44:29.081103201Z caller=helm.go:69 component=helm version=v3 info="Created a new Deployment called \"frontend-boutique\" in microservices-demo\n" targetNamespace=microservices-demo release=frontend
ts=2020-06-26T10:44:29.081154426Z caller=helm.go:69 component=helm version=v3 info="Deleting \"frontend\" in microservices-demo..." targetNamespace=microservices-demo release=frontend
ts=2020-06-26T10:44:29.183386738Z caller=helm.go:69 component=helm version=v3 info="updating status for upgraded release for frontend" targetNamespace=microservices-demo release=frontend
ts=2020-06-26T10:44:29.212776645Z caller=release.go:309 component=release release=frontend targetNamespace=microservices-demo resource=microservices-demo:helmrelease/frontend helmVersion=v3 info="upgrade succeeded" revision=e833d5671a3ec7f53a4c8ae6b34b0fd255c3f7ac phase=upgrade
- Такжн добавим манифесты HelmRelease для всех микросервисов входящих в состав HipsterShop
- Проверим, что все микросервисы успешно развернулись в Kubernetes кластере
kubectl get helmrelease -n microservices-demo
NAME RELEASE PHASE STATUS MESSAGE AGE
adservice adservice Succeeded deployed Release was successful for Helm release 'adservice' in 'microservices-demo'. 93s
cartservice cartservice Succeeded deployed Release was successful for Helm release 'cartservice' in 'microservices-demo'. 93s
checkoutservice checkoutservice Succeeded deployed Release was successful for Helm release 'checkoutservice' in 'microservices-demo'. 93s
currencyservice currencyservice Succeeded deployed Release was successful for Helm release 'currencyservice' in 'microservices-demo'. 93s
emailservice emailservice Succeeded deployed Release was successful for Helm release 'emailservice' in 'microservices-demo'. 93s
frontend frontend Succeeded deployed Release was successful for Helm release 'frontend' in 'microservices-demo'. 10h
loadgenerator loadgenerator Succeeded deployed Release was successful for Helm release 'loadgenerator' in 'microservices-demo'. 93s
paymentservice paymentservice Succeeded deployed Release was successful for Helm release 'paymentservice' in 'microservices-demo'. 93s
productcatalogservice productcatalogservice Succeeded deployed Release was successful for Helm release 'productcatalogservice' in 'microservices-demo'. 31s
recommendationservice recommendationservice Succeeded deployed Release was successful for Helm release 'recommendationservice' in 'microservices-demo'. 93s
shippingservice shippingservice Succeeded deployed Release was successful for Helm release 'shippingservice' in 'microservices-demo'. 93s
kubectl get po -n microservices-demo
NAME READY STATUS RESTARTS AGE
adservice-fc4f76f49-ch7hr 1/1 Running 0 17m
cartservice-5548fd5696-f2bzk 1/1 Running 2 16m
cartservice-redis-master-0 1/1 Running 0 16m
checkoutservice-5f94b5f759-mlqsv 1/1 Running 0 17m
currencyservice-799749bd9f-pm4n8 1/1 Running 0 17m
emailservice-66ddbc4fdc-nwnbk 1/1 Running 0 16m
frontend-boutique-7b4c7858f9-dg665 1/1 Running 0 55m
loadgenerator-59c9dd4bf5-t9tpf 0/1 Init:0/1 0 16m
paymentservice-58cb9655ff-8w6g4 1/1 Running 0 16m
productcatalogservice-7c45d67499-qgx7f 1/1 Running 0 16m
recommendationservice-859ddf86bf-cgbh8 1/1 Running 0 16m
shippingservice-fddb79c47-gtv59 1/1 Running 0 16m
Для доступа к frontend и работы loadgenerator - установим nginx-ingress
helm upgrade --install nginx-ingress stable/nginx-ingress --wait --namespace=nginx-ingress --create-namespace
Release "nginx-ingress" does not exist. Installing it now.
NAME: nginx-ingress
LAST DEPLOYED: Fri Jun 26 14:34:45 2020
NAMESPACE: nginx-ingress
STATUS: deployed
REVISION: 1
TEST SUITE: None
NOTES:
The nginx-ingress controller has been installed.
It may take a few minutes for the LoadBalancer IP to be available.
You can watch the status by running 'kubectl --namespace nginx-ingress get services -o wide -w nginx-ingress-controller'
An example Ingress that makes use of the controller:
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
annotations:
kubernetes.io/ingress.class: nginx
name: example
namespace: foo
spec:
rules:
- host: www.example.com
http:
paths:
- backend:
serviceName: exampleService
servicePort: 80
path: /
# This section is only required if TLS is to be enabled for the Ingress
tls:
- hosts:
- www.example.com
secretName: example-tls
If TLS is enabled for the Ingress, a Secret containing the certificate and key must also be provided:
apiVersion: v1
kind: Secret
metadata:
name: example-tls
namespace: foo
data:
tls.crt: <base64 encoded cert>
tls.key: <base64 encoded key>
type: kubernetes.io/tls
frontend_ingess.yaml
apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
name: frontend
annotations:
kubernetes.io/ingress.class: nginx
spec:
rules:
- host: {{ .Values.ingress.host }}
http:
paths:
- backend:
serviceName: frontend
servicePort: 8080
Также поправим values.yaml для frontend и loadgenerator
frontend:
image:
repository: kovtalex/frontend
tag: latest
ingress:
host: frontend.35.205.32.165.xip.io
loadgenerator:
image:
repository: kovtalex/loadgenerator
tag: latest
ingress:
host: frontend.35.205.32.165.xip.io
Проверяем работу:
kubectl get po -n microservices-demo
NAME READY STATUS RESTARTS AGE
adservice-fc4f76f49-ch7hr 1/1 Running 0 21m
cartservice-5548fd5696-f2bzk 1/1 Running 2 20m
cartservice-redis-master-0 1/1 Running 0 20m
checkoutservice-5f94b5f759-mlqsv 1/1 Running 0 21m
currencyservice-799749bd9f-pm4n8 1/1 Running 0 21m
emailservice-66ddbc4fdc-nwnbk 1/1 Running 0 21m
frontend-boutique-7b4c7858f9-dg665 1/1 Running 0 60m
loadgenerator-7fd875b555-kbxhg 1/1 Running 0 50s
paymentservice-58cb9655ff-8w6g4 1/1 Running 0 21m
productcatalogservice-7c45d67499-qgx7f 1/1 Running 0 20m
recommendationservice-859ddf86bf-cgbh8 1/1 Running 0 21m
shippingservice-fddb79c47-gtv59 1/1 Running 0 21m
- export FLUX_FORWARD_NAMESPACE=flux переменная окружения, указывающая на namespace, в который установлен flux (альтернатива ключу --k8s-fwd-ns <flux installation ns>)
- fluxctl list-workloads -a посмотреть все workloads, которые находятся в зоне видимости flux
- fluxctl list-images -n microservices-demo - посмотреть все Docker образы, используемые в кластере (в namespace microservices-demo)
- fluxctl automate/deautomate - включить/выключить автоматизацию управления workload
- fluxctl policy -w microservices-demo:helmrelease/frontend -- tag-all='semver:~0.1' - установить всем сервисам в workload microservices-demo:helmrelease/frontend политику обновления образов из Registry на базе семантического версионирования c маской 0.1.*
- fluxctl sync - приндительно запустить синхронизацию состояния gitрепозитория с кластером
- fluxctl release --workload=microservicesdemo:helmrelease/frontend --update-all-images - принудительно инициировать сканирование Registry на предмет наличия свежих Docker образов
Flagger - оператор Kubernetes, созданный для автоматизации canary deployments.
Flagger может использовать:
- Istio, Linkerd, App Mesh или nginx для маршрутизации трафика
- Prometheus для анализа канареечного релиза
Istio мы установили в кластер в самом начале ДЗ, но также можно установить istio с помощью istioctl:
[Подробнее] про configuration profile
- Тогда просто установим утилиту: brew install istioctl
- Установим Prometheus и Grafana
wget https://storage.googleapis.com/gke-release/istio/release/1.0.6-gke.1/patches/install-prometheus.yaml
kubectl -n istio-system apply -f install-prometheus.yaml
grafana_vs.yaml
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
name: grafana
namespace: istio-system
spec:
hosts:
- grafana.34.78.81.2.xip.io
gateways:
- frontend.microservices-demo.svc.cluster.local
http:
- route:
- destination:
host: flagger-grafana
helm upgrade -i flagger-grafana flagger/grafana \
--namespace=istio-system \
--set url=http://prometheus:9090 \
--set user=admin \
--set password=admin
kubectl apply -f grafana_vs.yaml
Реализуем установку Istio с помощью istio operator
- Нам потребуется установленный istioctl
- Выполняем установку оператора
istioctl operator init
Using operator Deployment image: docker.io/istio/operator:1.6.3
✔ Istio operator installed
✔ Installation complete
- Создаем namespace
kubectl create ns istio-system
- Создаем IstioOperator ресурс, который увидит istio оператор и развернет необходымые компоненты (к примеру используем профиль demo)
kubectl apply -f - <<EOF
apiVersion: install.istio.io/v1alpha1
kind: IstioOperator
metadata:
namespace: istio-system
name: istio
spec:
profile: demo
EOF
namespace/istio-system created
istiooperator.install.istio.io/istio created
Проверим:
kubectl get po -n istio-system
NAME READY STATUS RESTARTS AGE
grafana-5dc4b4676c-8g2dc 1/1 Running 0 6m27s
istio-egressgateway-5db676495d-znlkg 1/1 Running 0 6m33s
istio-ingressgateway-69bb66cb4b-r7vg9 1/1 Running 0 6m34s
istio-tracing-8584b4d7f9-xkzgn 1/1 Running 0 6m27s
istiod-5cdccfd474-5q5xd 1/1 Running 0 6m47s
kiali-6f457f5964-c8b9z 1/1 Running 0 6m27s
prometheus-d8b7c5949-pnmzp 2/2 Running 0 6m27s
kubectl get svc -n istio-system
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
grafana ClusterIP 10.0.5.19 <none> 3000/TCP 8m18s
istio-egressgateway ClusterIP 10.0.4.48 <none> 80/TCP,443/TCP,15443/TCP 8m24s
istio-ingressgateway LoadBalancer 10.0.12.105 34.78.81.2 15020:32674/TCP,80:31821/TCP,443:32217/TCP,31400:30830/TCP,15443:31513/TCP 8m23s
istiod ClusterIP 10.0.1.215 <none> 15010/TCP,15012/TCP,443/TCP,15014/TCP,53/UDP,853/TCP 8m38s
jaeger-agent ClusterIP None <none> 5775/UDP,6831/UDP,6832/UDP 8m18s
jaeger-collector ClusterIP 10.0.9.107 <none> 14267/TCP,14268/TCP,14250/TCP 8m17s
jaeger-collector-headless ClusterIP None <none> 14250/TCP 8m17s
jaeger-query ClusterIP 10.0.12.241 <none> 16686/TCP 8m17s
kiali ClusterIP 10.0.7.219 <none> 20001/TCP 8m17s
prometheus ClusterIP 10.0.10.80 <none> 9090/TCP 8m17s
tracing ClusterIP 10.0.12.134 <none> 80/TCP 8m17s
zipkin ClusterIP 10.0.7.102 <none> 9411/TCP 8m17s
- Добавление helm-репозитория flagger:
helm repo add flagger https://flagger.app
- Установка CRD для Flagger:
kubectl apply -f https://raw.githubusercontent.com/weaveworks/flagger/master/artifacts/flagger/crd.yaml
customresourcedefinition.apiextensions.k8s.io/canaries.flagger.app created
customresourcedefinition.apiextensions.k8s.io/metrictemplates.flagger.app created
customresourcedefinition.apiextensions.k8s.io/alertproviders.flagger.app created
- Установка flagger с указанием использовать Istio:
helm upgrade --install flagger flagger/flagger \
--namespace=istio-system \
--set crd.create=false \
--set meshProvider=istio \
--set metricsServer=http://prometheus:9090
Release "flagger" does not exist. Installing it now.
NAME: flagger
LAST DEPLOYED: Fri Jun 26 14:49:02 2020
NAMESPACE: istio-system
STATUS: deployed
REVISION: 1
TEST SUITE: None
NOTES:
Flagger installed
Изменим созданное ранее описание namespace microservices-demo:
apiVersion: v1
kind: Namespace
metadata:
name: microservices-demo
labels:
istio-injection: enabled
istio-injection: enabled - указывает на необходимость добавить в каждый pod sidecar контейнер с envoy proxy.
kubectl get ns microservices-demo --show-labels
NAME STATUS AGE LABELS
microservices-demo Active 11h fluxcd.io/sync-gc-mark=sha256.n2cN5YcLlF3ZPaGnbPX9aNnK9f19Wc1XBZ7D8wlSQXk,istio-injection=enabled
Самый простой способ добавить sidecar контейнер в уже запущенные pod - удалить их:
kubectl delete pods --all -n microservices-demo
pod "adservice-fc4f76f49-ch7hr" deleted
pod "cartservice-5548fd5696-f2bzk" deleted
pod "cartservice-redis-master-0" deleted
pod "checkoutservice-5f94b5f759-mlqsv" deleted
pod "currencyservice-799749bd9f-pm4n8" deleted
pod "emailservice-66ddbc4fdc-nwnbk" deleted
pod "frontend-boutique-7b4c7858f9-dg665" deleted
pod "loadgenerator-7fd875b555-kbxhg" deleted
pod "paymentservice-58cb9655ff-8w6g4" deleted
pod "productcatalogservice-7c45d67499-qgx7f" deleted
pod "recommendationservice-859ddf86bf-cgbh8" deleted
pod "shippingservice-fddb79c47-gtv59" deleted
После этого можно проверить, что контейнер с названием istioproxy появился внутри каждого pod:
kubectl describe pod -l app=frontend -n microservices-demo
istio-proxy:
Container ID: docker://8db3ea7ad32f40b40ada842f604171fcb7ae6d1d57a10c6ef47997199dbb92f4
Image: gke.gcr.io/istio/proxyv2:1.4.6-gke.0
Image ID: docker-pullable://gke.gcr.io/istio/proxyv2@sha256:618d0477e88004e0b599e455afd2080360507a7d37eca9c22904b3f6bb0fec36
Port: 15090/TCP
Host Port: 0/TCP
...
State: Running
kubectl get po -n microservices-demo
NAME READY STATUS RESTARTS AGE
adservice-fc4f76f49-zm4hx 2/2 Running 0 2m4s
cartservice-5548fd5696-fdvmm 2/2 Running 3 2m4s
cartservice-redis-master-0 2/2 Running 0 2m2s
checkoutservice-5f94b5f759-lq4nr 2/2 Running 0 2m4s
currencyservice-799749bd9f-pqxll 2/2 Running 0 2m4s
emailservice-66ddbc4fdc-wtkm9 2/2 Running 0 2m4s
frontend-boutique-7b4c7858f9-k4z8z 2/2 Running 0 2m4s
loadgenerator-7fd875b555-brrdj 2/2 Running 1 2m4s
paymentservice-58cb9655ff-tszjf 2/2 Running 0 2m4s
productcatalogservice-7c45d67499-d6zcq 2/2 Running 0 2m4s
recommendationservice-859ddf86bf-qcjpr 2/2 Running 0 2m4s
shippingservice-fddb79c47-67jw5 2/2 Running 0 2m3s
На текущий момент у нас отсутствует ingress и мы не можем получить доступ к frontend снаружи кластера.
В то же время Istio в качестве альтернативы классическому ingress предлагает свой набор абстракций.
Чтобы настроить маршрутизацию трафика к приложению с использованием Istio, нам необходимо добавить ресурсы VirtualService и Gateway.
Создадим директорию deploy/istio и поместим в нее следующие манифесты:
frontend-vs.yaml
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
name: frontend
namespace: microservices-demo
spec:
hosts:
- "*"
gateways:
- frontend
http:
- route:
- destination:
host: frontend
port:
number: 80
frontend-gw.yaml
apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
name: frontend-gateway
namespace: microservices-demo
spec:
selector:
istio: ingressgateway
servers:
- port:
number: 80
name: http
protocol: HTTP
hosts:
- "*"
Созданный Gateway можно увидеть следующим образом:
kubectl get gateway -n microservices-demo
NAME AGE
frontend 43h
Для доступа снаружи нам понадобится EXTERNAL-IP сервиса istioingressgateway:
kubectl get svc istio-ingressgateway -n istio-system
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
istio-ingressgateway LoadBalancer 10.0.5.79 34.78.81.2 15020:31158/TCP,80:30366/TCP,443:30126/TCP,31400:30279/TCP,15029:31001/TCP,15030:31300/TCP,15031:31993/TCP,15032:31534/TCP,15443:30087/TCP 13h
Также удалим наш nginx-ingress (он нам больше не понадобится):
helm uninstall nginx-ingress -n nginx-ingress
kubectl delete ns nginx-ingress
namespace "nginx-ingress" deleted
Теперь мы можем обращаться к frontend так http://EXTERNAL-IP
В нашей ситуации ресурсы Gateway и VirtualService логически являются частью инфраструктурного кода, описывающего окружение микросервиса frontend. Поэтому, оправданно будет перенести манифесты в Helm chart.
Дополним Helm chart frontend манифестами gateway.yaml и virtualService.yaml . Оригинальные манифесты удалим вместе с директорией deploy/istio
virtualService.yaml
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
name: frontend-boutique
spec:
hosts:
- {{ .Values.ingress.host }}
gateways:
- frontend
http:
- route:
- destination:
host: frontend
port:
number: 80
gateway.yaml
apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
name: frontend
spec:
selector:
istio: ingressgateway
servers:
- port:
number: 80
name: http
protocol: HTTP
hosts:
- "*"
values.yaml для frontend
image:
repository: kovtalex/frontend
tag: latest
ingress:
host: frontend.34.78.81.2.xip.io
values.yaml для loadgenerator
image:
repository: kovtalex/loadgenerator
tag: latest
ingress:
host: frontend.34.78.81.2.xip.io
kubectl get gateway -n microservices-demo
NAME AGE
frontend 44s
kubectl get virtualservices -n microservices-demo
NAME GATEWAYS HOSTS AGE
frontend-boutique [frontend] [frontend.34.78.81.2.xip.io] 44h
- Провалидируем на наличие ошибок
istioctl analyze -n microservices-demo
✔ No validation issues found when analyzing namespace: microservices-demo.
```console
```console
kubectl get pods -n microservices-demo
NAME READY STATUS RESTARTS AGE
adservice-fc4f76f49-npmbb 2/2 Running 0 91m
cartservice-5548fd5696-n4snl 2/2 Running 4 91m
cartservice-redis-master-0 2/2 Running 0 90m
checkoutservice-5f94b5f759-tmgk2 2/2 Running 0 91m
currencyservice-799749bd9f-9ds84 2/2 Running 0 91m
emailservice-66ddbc4fdc-mqb62 2/2 Running 0 91m
frontend-boutique-7b4c7858f9-p8czp 2/2 Running 0 91m
loadgenerator-5d6559df8c-qfk2b 2/2 Running 1 91m
paymentservice-58cb9655ff-bmdqg 2/2 Running 0 91m
productcatalogservice-7c45d67499-jlzv8 2/2 Running 0 91m
recommendationservice-859ddf86bf-tnp28 2/2 Running 0 91m
shippingservice-fddb79c47-g87n8 2/2 Running 0 91m
Перейдем непосредственно к настройке канареечных релизов.
Добавим в Helm chart frontend еще один файл - canary.yaml
В нем будем хранить описание стратегии, по которой необходимо обновлять данный микросервис.
Узнать подробнее о Canary Custom Resource можно по ссылке
canary.yaml
apiVersion: flagger.app/v1alpha3
kind: Canary
metadata:
name: frontend-boutique
namespace: microservices-demo
spec:
provider: istio
targetRef:
apiVersion: apps/v1
kind: Deployment
name: frontend-boutique
service:
port: 80
targetPort: 8080
gateways:
- frontend
hosts:
- {{ .Values.ingress.host }}
trafficPolicy:
tls:
mode: DISABLE
analysis:
interval: 30s
threshold: 5
maxWeight: 30
stepWeight: 5
metrics:
- name: request-success-rate
threshold: 99
interval: 30s
Проверим, что Flagger:
- Успешно инициализировал canary ресурс frontend :
kubectl get canaries -n microservices-demo
NAME STATUS WEIGHT LASTTRANSITIONTIME
frontend-boutique Initialized 0 2020-06-26T22:25:40Z
- Обновил pod, добавив ему к названию постфикс primary:
kubectl get pods -n microservices-demo -l app=frontend-boutique-primary
NAME READY STATUS RESTARTS AGE
frontend-boutique-primary-7c964fcff5-rd2md 2/2 Running 0 4m
kubectl get virtualservices -n microservices-demo
NAME GATEWAYS HOSTS AGE
frontend-boutique [frontend] [frontend.34.78.81.2.xip.io frontend-boutique] 17m
Попробуем провести релиз. Соберем новый образ frontend с тегом v0.0.3 и сделаем push в Docker Hub.
make APP_TAG=v0.0.3 build_frontend release_frontend
Через некоторое время в выводе kubectl describe canary frontend -n microservices-demo мы сможет наблюдать следующую картину:
kubectl get canaries -n microservices-demo
NAME STATUS WEIGHT LASTTRANSITIONTIME
frontend-boutique Progressing 30 2020-06-26T22:49:40Z
kubectl get canaries -n microservices-demo
NAME STATUS WEIGHT LASTTRANSITIONTIME
frontend-boutique Succeeded 0 2020-06-26T22:51:09Z
kubectl get pods -n microservices-demo -l app=frontend-boutique-primary
NAME READY STATUS RESTARTS AGE
frontend-boutique-primary-76f7f7c996-vbpjb 2/2 Running 0 4m23s
fluxctl list-images -n microservices-demo
WORKLOAD CONTAINER IMAGE CREATED
microservices-demo:deployment/frontend-boutique-primary server kovtalex/frontend
'-> v0.0.3 26 Jun 20 17:03 UTC
v0.0.2 26 Jun 20 10:37 UTC
v0.0.1 25 Jun 20 23:32 UTC
kubectl describe canary frontend -n microservices-demo
Normal Synced 7m24s (x2 over 18m) flagger New revision detected! Scaling up frontend-boutique.microservices-demo
Normal Synced 5m54s flagger New revision detected! Restarting analysis for frontend-boutique.microservices-demo
Normal Synced 5m24s (x3 over 18m) flagger Advance frontend-boutique.microservices-demo canary weight 5
Normal Synced 5m24s (x3 over 18m) flagger Starting canary analysis for frontend-boutique.microservices-demo
Normal Synced 4m54s (x2 over 6m23s) flagger Advance frontend-boutique.microservices-demo canary weight 10
Normal Synced 4m24s flagger Advance frontend-boutique.microservices-demo canary weight 15
Normal Synced 3m53s flagger Advance frontend-boutique.microservices-demo canary weight 20
Normal Synced 3m24s flagger Advance frontend-boutique.microservices-demo canary weight 25
Normal Synced 2m53s flagger Advance frontend-boutique.microservices-demo canary weight 30
Normal Synced 84s (x3 over 2m24s) flagger (combined from similar events): Promotion completed! Scaling down frontend-boutique.microservices-demo
Реализуеме получение нотификаций о релизах в Slack при помощи данной инструкции
и применим:
helm upgrade --install flagger flagger/flagger \
--namespace=istio-system \
--set crd.create=false \
--set meshProvider=istio \
--set metricsServer=http://prometheus:9090 \
--set slack.url=https://hooks.slack.com/services/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 \
--set slack.channel=devops \
--set slack.user=flagger
Перетегируем и выложим образ на Docker Hub:
docker tag kovtalex/frontend:v0.0.3 kovtalex/frontend:v0.0.4
docker push kovtalex/frontend:v0.0.4
Получаем в Slack уведомления о прогрессе выкатки:
Jaeger можно установить с помощью istio оператора и профиля demo, как указано выше в разделе по установке istio.
-
Из коробки доступны трейсы с istio-proxy sidecar контейнеров
-
Для получения трейсов непосредственно из микросервисов следует указать в темплейтах манифестов деплойментов значение переменной окружения JAEGER_SERVICE_ADDR равное jaeger-collector.istio-system.svc.cluster.local:14268
-
После этого можно выполнить: istioctl dashboard jaeger
-
Перейти в UI jaeger и наблюдать наши трейсы выбрав из списка сервисы с именами:
- <название>.microservices-demo - для трейсов из istio-proxy
- <название> - для тресов непосредственно из микросервисов
https://hub.helm.sh/charts/argo/argo-cd
helm repo add argo https://argoproj.github.io/argo-helm
helm upgrade --install argocd argo/argo-cd -n argocd --create-namespace
Release "argocd" does not exist. Installing it now.
manifest_sorter.go:192: info: skipping unknown hook: "crd-install"
manifest_sorter.go:192: info: skipping unknown hook: "crd-install"
NAME: argocd
LAST DEPLOYED: Wed Jul 1 15:53:14 2020
NAMESPACE: argocd
STATUS: deployed
REVISION: 1
TEST SUITE: None
NOTES:
In order to access the server UI you have the following options:
1. kubectl port-forward service/argocd-server -n argocd 8080:443
and then open the browser on http://localhost:8080 and accept the certificate
2. enable ingress in the values file `service.ingress.enabled` and either
- Add the annotation for ssl passthrough: https://github.com/argoproj/argo-cd/blob/master/docs/operator-manual/ingress.md#option-1-ssl-passthrough
- Add the `--insecure` flag to `server.extraArgs` in the values file and terminate SSL at your ingress: https://github.com/argoproj/argo-cd/blob/master/docs/operator-manual/ingress.md#option-2-multiple-ingress-objects-and-hosts
After reaching the UI the first time you can login with username: admin and the password will be the
name of the server pod. You can get the pod name by running:
kubectl get pods -n argocd -l app.kubernetes.io/name=argocd-server -o name | cut -d'/' -f 2
kubectl label ns microservices-demo istio-injection=enabled
namespace/microservices-demo labeled
В ходе работы мы:
- установим кластер vault в kubernetes
- научимся создавать секреты и политики
- настроим авторизацию в vault через kubernetes sa
- сделаем под с контейнером nginx, в который прокинем секреты из vault через consul-template
Вспомогательные ссылки:
Добавим репозиторий:
helm repo add hashicorp https://helm.releases.hashicorp.com
helm repo update
Установим consul:
helm upgrade --install consul hashicorp/consul
Release "consul" does not exist. Installing it now.
NAME: consul
LAST DEPLOYED: Fri Jun 12 18:38:23 2020
NAMESPACE: default
STATUS: deployed
REVISION: 1
NOTES:
Thank you for installing HashiCorp Consul!
Now that you have deployed Consul, you should look over the docs on using
Consul with Kubernetes available here:
https://www.consul.io/docs/platform/k8s/index.html
Your release is named consul.
To learn more about the release if you are using Helm 2, run:
$ helm status consul
$ helm get consul
To learn more about the release if you are using Helm 3, run:
$ helm status consul
$ helm get all consul
vault.values.yaml:
server:
standalone:
enabled: false
ha:
enabled: true
ui:
enabled: true
helm upgrade --install vault hashicorp/vault -f vault.values.yaml
Release "vault" does not exist. Installing it now.
NAME: vault
LAST DEPLOYED: Fri Jun 12 18:39:19 2020
NAMESPACE: default
STATUS: deployed
REVISION: 1
TEST SUITE: None
NOTES:
Thank you for installing HashiCorp Vault!
Now that you have deployed Vault, you should look over the docs on using
Vault with Kubernetes available here:
https://www.vaultproject.io/docs/
Your release is named vault. To learn more about the release, try:
$ helm status vault
$ helm get vault
kubectl get pods
NAME READY STATUS RESTARTS AGE
consul-consul-29tw4 1/1 Running 0 76s
consul-consul-78t4d 1/1 Running 0 76s
consul-consul-854t7 1/1 Running 0 76s
consul-consul-server-0 1/1 Running 0 76s
consul-consul-server-1 1/1 Running 0 76s
consul-consul-server-2 1/1 Running 0 76s
vault-0 0/1 Running 0 15s
vault-1 0/1 Running 0 15s
vault-2 0/1 Running 0 15s
vault-agent-injector-7898f4df86-64lcr 1/1 Running 0 15s
- Проведем инициализацию черерз любой под vault'а kubectl
kubectl exec -it vault-0 -- vault operator init --key-shares=1 --key-threshold=1
Unseal Key 1: FVvyfk+VawPVOSm+gEAp9YlB17xTctbooy7/v3xmLnQ=
Initial Root Token: s.y71KGil4zgA42MK1VmQEuTs7
- Сохраним ключи, полученные при инициализации
- Unseal Key 1: FVvyfk+VawPVOSm+gEAp9YlB17xTctbooy7/v3xmLnQ=
- Initial Root Token: s.y71KGil4zgA42MK1VmQEuTs7
kubectl exec -it vault-0 -- vault status
Key Value
--- -----
Seal Type shamir
Initialized true
Sealed true
Total Shares 1
Threshold 1
Unseal Progress 0/1
Unseal Nonce n/a
Version 1.4.2
HA Enabled true
- Обратим внимание на переменные окружения в подах
kubectl exec -it vault-0 env | grep VAULT_ADDR
VAULT_ADDR=http://127.0.0.1:8200
- Распечатать нужно каждый под
kubectl exec -it vault-0 -- vault operator unseal 'FVvyfk+VawPVOSm+gEAp9YlB17xTctbooy7/v3xmLnQ='
Key Value
--- -----
Seal Type shamir
Initialized true
Sealed false
Total Shares 1
Threshold 1
Version 1.4.2
Cluster Name vault-cluster-5dddd753
Cluster ID e6a34344-6fd5-0ff9-8d37-b4f702bdc5ea
HA Enabled true
HA Cluster https://vault-1.vault-internal:8201
HA Mode standby
Active Node Address http://10.60.2.6:8200
kubectl exec -it vault-1 -- vault operator unseal 'FVvyfk+VawPVOSm+gEAp9YlB17xTctbooy7/v3xmLnQ='
Key Value
--- -----
Seal Type shamir
Initialized true
Sealed false
Total Shares 1
Threshold 1
Version 1.4.2
Cluster Name vault-cluster-5dddd753
Cluster ID e6a34344-6fd5-0ff9-8d37-b4f702bdc5ea
HA Enabled true
HA Cluster n/a
HA Mode standby
Active Node Address <none>
kubectl exec -it vault-2 -- vault operator unseal 'FVvyfk+VawPVOSm+gEAp9YlB17xTctbooy7/v3xmLnQ='
Key Value
--- -----
Seal Type shamir
Initialized true
Sealed false
Total Shares 1
Threshold 1
Version 1.4.2
Cluster Name vault-cluster-5dddd753
Cluster ID e6a34344-6fd5-0ff9-8d37-b4f702bdc5ea
HA Enabled true
HA Cluster https://vault-1.vault-internal:8201
HA Mode standby
Active Node Address http://10.60.2.6:8200
Получим ошибку:
kubectl exec -it vault-0 -- vault auth list
Error listing enabled authentications: Error making API request.
URL: GET http://127.0.0.1:8200/v1/sys/auth
Code: 400. Errors:
* missing client token
kubectl exec -it vault-0 -- vault login
Token (will be hidden):
Success! You are now authenticated. The token information displayed below
is already stored in the token helper. You do NOT need to run "vault login"
again. Future Vault requests will automatically use this token.
Key Value
--- -----
token s.y71KGil4zgA42MK1VmQEuTs7
token_accessor zs5lY2o23wAMYBjQju47I90R
token_duration ∞
token_renewable false
token_policies ["root"]
identity_policies []
policies ["root"]
- Повторно запросим список авторизаций
kubectl exec -it vault-0 -- vault auth list
Path Type Accessor Description
---- ---- -------- -----------
token/ token auth_token_c82675cb token based credentials
kubectl exec -it vault-0 -- vault secrets enable --path=otus kv
Success! Enabled the kv secrets engine at: otus/
kubectl exec -it vault-0 -- vault secrets list --detailed
Path Plugin Accessor Default TTL Max TTL Force No Cache Replication Seal Wrap External Entropy Access Options Description UUID
---- ------ -------- ----------- ------- -------------- ----------- --------- ----------------------- ------- ----------- ----
cubbyhole/ cubbyhole cubbyhole_571dafbb n/a n/a false local false false map[] per-token private secret storage e1acbd2f-6c75-d39f-e5e0-987f1eb4eb61
identity/ identity identity_1545b700 system system false replicated false false map[] identity store 918fdcbf-e74a-3f5e-d37b-20febd024dbc
otus/ kv kv_5a4a34ef system system false replicated false false map[] n/a 3183b6fe-e4a9-8ed5-37d9-5773e336b730
sys/ system system_d89d7a08 n/a n/a false replicated false false map[] system endpoints used for control, policy and debugging b63b40cf-c24a-0dff-6c26-5c9ce69d5463
kubectl exec -it vault-0 -- vault kv put otus/otus-ro/config username='otus' password='asajkjkahs'
Success! Data written to: otus/otus-ro/config
kubectl exec -it vault-0 -- vault kv put otus/otus-rw/config username='otus' password='asajkjkahs'
Success! Data written to: otus/otus-rw/config
kubectl exec -it vault-0 -- vault read otus/otus-ro/config
Key Value
--- -----
refresh_interval 768h
password asajkjkahs
username otus
kubectl exec -it vault-0 -- vault kv get otus/otus-rw/config
====== Data ======
Key Value
--- -----
password asajkjkahs
username otus
kubectl exec -it vault-0 -- vault auth enable kubernetes
Success! Enabled kubernetes auth method at: kubernetes/
kubectl exec -it vault-0 -- vault auth list
Path Type Accessor Description
---- ---- -------- -----------
kubernetes/ kubernetes auth_kubernetes_2bbb6f48 n/a
token/ token auth_token_c82675cb token based credentials
vault-auth-service-account.yml
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRoleBinding
metadata:
name: role-tokenreview-binding
namespace: default
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: system:auth-delegator
subjects:
- kind: ServiceAccount
name: vault-auth
namespace: default
kubectl create serviceaccount vault-auth
serviceaccount/vault-auth created
kubectl apply -f vault-auth-service-account.yml
clusterrolebinding.rbac.authorization.k8s.io/role-tokenreview-binding created
export VAULT_SA_NAME=$(kubectl get sa vault-auth -o jsonpath="{.secrets[*]['name']}")
export SA_JWT_TOKEN=$(kubectl get secret $VAULT_SA_NAME -o jsonpath="{.data.token}" | base64 --decode; echo)
export SA_CA_CRT=$(kubectl get secret $VAULT_SA_NAME -o jsonpath="{.data['ca\.crt']}" | base64 --decode; echo)
export K8S_HOST=$(more ~/.kube/config | grep server |awk '/http/ {print $NF}')
### alternative way
export K8S_HOST=$(kubectl cluster-info | grep 'Kubernetes master' | awk '/https/ {print $NF}' | sed 's/\x1b\[[0-9;]*m//g' )
kubectl exec -it vault-0 -- vault write auth/kubernetes/config \
token_reviewer_jwt="$SA_JWT_TOKEN" \
kubernetes_host="$K8S_HOST" \
kubernetes_ca_cert="$SA_CA_CRT"
Success! Data written to: auth/kubernetes/config
tee otus-policy.hcl <<EOF
path "otus/otus-ro/*" {
capabilities = ["read", "list"]
}
path "otus/otus-rw/*" {
capabilities = ["read", "create", "list"]
}
EOF
kubectl cp otus-policy.hcl vault-0:/home/vault
kubectl exec -it vault-0 -- vault policy write otus-policy /home/vault/otus-policy.hcl
Success! Uploaded policy: otus-policy
kubectl exec -it vault-0 -- vault write auth/kubernetes/role/otus \
bound_service_account_names=vault-auth \
bound_service_account_namespaces=default policies=otus-policy ttl=24h
Success! Data written to: auth/kubernetes/role/otus
- Создадим под с привязанным сервис аккоунтом и установим туда curl и jq
kubectl run --generator=run-pod/v1 tmp --rm -i --tty --serviceaccount=vault-auth --image alpine:3.7
apk add curl jq
- Залогинимся и получим клиентский токен
VAULT_ADDR=http://vault:8200
KUBE_TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)
curl --request POST --data '{"jwt": "'$KUBE_TOKEN'", "role": "otus"}' $VAULT_ADDR/v1/auth/kubernetes/login | jq
{
"request_id": "2af65405-1324-9b34-cd1a-77f2d8e9badb",
"lease_id": "",
"renewable": false,
"lease_duration": 0,
"data": null,
"wrap_info": null,
"warnings": null,
"auth": {
"client_token": "s.q8YHiRZd6mkfByRn4BcpB6mm",
"accessor": "pB3VsxHkPnISD82QQtbIImCz",
"policies": [
"default",
"otus-policy"
],
"token_policies": [
"default",
"otus-policy"
],
"metadata": {
"role": "otus",
"service_account_name": "vault-auth",
"service_account_namespace": "default",
"service_account_secret_name": "vault-auth-token-rmwxz",
"service_account_uid": "615c6a4f-8885-446c-ac78-c5026d718dca"
},
"lease_duration": 86400,
"renewable": true,
"entity_id": "188b9a87-f440-bcdf-0da7-165865147264",
"token_type": "service",
"orphan": true
}
}
TOKEN=$(curl -k -s --request POST --data '{"jwt": "'$KUBE_TOKEN'", "role": "otus"}' $VAULT_ADDR/v1/auth/kubernetes/login | jq '.auth.client_token' | awk -F\" '{print $2}')
- Используем свой клиентский токен
- Проверим чтение
curl --header "X-Vault-Token:$TOKEN" $VAULT_ADDR/v1/otus/otus-ro/config | jq
{
"request_id": "3a29647c-8e75-4d56-7ed9-d641819c2dda",
"lease_id": "",
"renewable": false,
"lease_duration": 2764800,
"data": {
"password": "asajkjkahs",
"username": "otus"
},
"wrap_info": null,
"warnings": null,
"auth": null
}
curl --header "X-Vault-Token:$TOKEN" $VAULT_ADDR/v1/otus/otus-rw/config | jq
{
"request_id": "48e1eab4-ebd5-b109-4fe7-ac080e7118c3",
"lease_id": "",
"renewable": false,
"lease_duration": 2764800,
"data": {
"password": "asajkjkahs",
"username": "otus"
},
"wrap_info": null,
"warnings": null,
"auth": null
}
- Проверим запись в otus-ro/config
curl --request POST --data '{"bar": "baz"}' --header "X-Vault-Token:s.SCbMdIL61rqmyqrCUldd1ocw" $VAULT_ADDR/v1/otus/otus-ro/config | jq
{
"errors": [
"1 error occurred:\n\t* permission denied\n\n"
]
}
- Проверим запись в otus-ro/config1
curl --request POST --data '{"bar": "baz"}' --header "X-Vault-Token:$TOKEN" $VAULT_ADDR/v1/otus/otus-rw/config1 | jq
curl --header "X-Vault-Token:$TOKEN" $VAULT_ADDR/v1/otus/otus-rw/config1 | jq
{
"request_id": "922fb606-f383-ecf6-6173-06ef2e9c3fcc",
"lease_id": "",
"renewable": false,
"lease_duration": 2764800,
"data": {
"bar": "baz"
},
"wrap_info": null,
"warnings": null,
"auth": null
}
- Проверим запись в otus-ro/config1
curl --request POST --data '{"bar": "baz"}' --header "X-Vault-Token:$TOKEN" $VAULT_ADDR/v1/otus/otus-rw/config | jq
{
"errors": [
"1 error occurred:\n\t* permission denied\n\n"
]
}
Доступ запрещен, так как у нас нет прав на обновление otus/otus-ro/*
Обновим otus-policy.hcl добавив update
{
path "otus/otus-ro/*" {
capabilities = ["read", "list"]
}
path "otus/otus-rw/*" {
capabilities = ["read", "create", "update", "list"]
}
- Применим новые политики
kubectl cp otus-policy.hcl vault-0:/home/vault
kubectl exec -it vault-0 -- vault policy write otus-policy /home/vault/otus-policy.hcl
Success! Uploaded policy: otus-policy
- И попробуем снова записать:
curl --request POST --data '{"bar": "baz"}' --header "X-Vault-Token:$TOKEN" $VAULT_ADDR/v1/otus/otus-rw/config | jq
curl --header "X-Vault-Token:$TOKEN" $VAULT_ADDR/v1/otus/otus-rw/config | jq
{
"request_id": "509645a1-bd6a-704e-c663-2d94ef465176",
"lease_id": "",
"renewable": false,
"lease_duration": 2764800,
"data": {
"bar": "baz"
},
"wrap_info": null,
"warnings": null,
"auth": null
}
Успех!!!
- Авторизуемся через vault-agent и получим клиентский токен
- Через consul-template достанем секрет и положим его в nginx
- Итог - nginx получил секрет из волта, не зная ничего про волт
git clone https://github.com/hashicorp/vault-guides.git
cd vault-guides/identity/vault-agent-k8s-demo
- В каталоге configs-k8s скорректируем конфиги с учетом ранее созданых ролей и секретов
- Проверим и скорректируем конфиг example-k8s-spec.yml
kubectl apply -f configmap.yaml
configmap/example-vault-agent-config created
kubectl get configmap example-vault-agent-config -o yaml
apiVersion: v1
data:
consul-template-config.hcl: |
vault {
renew_token = false
vault_agent_token_file = "/home/vault/.vault-token"
retry {
backoff = "1s"
}
}
template {
destination = "/etc/secrets/index.html"
contents = <<EOT
<html>
<body>
<p>Some secrets:</p>
{{- with secret "otus/otus-ro/config" }}
<ul>
<li><pre>username: {{ .Data.username }}</pre></li>
<li><pre>password: {{ .Data.password }}</pre></li>
</ul>
{{ end }}
</body>
</html>
EOT
}
vault-agent-config.hcl: |
exit_after_auth = true
pid_file = "/home/vault/pidfile"
auto_auth {
method "kubernetes" {
mount_path = "auth/kubernetes"
config = {
role = "otus"
}
}
sink "file" {
config = {
path = "/home/vault/.vault-token"
}
}
}
kind: ConfigMap
metadata:
annotations:
kubectl.kubernetes.io/last-applied-configuration: |
{"apiVersion":"v1","data":{"consul-template-config.hcl":"vault {\n renew_token = false\n vault_agent_token_file = \"/home/vault/.vault-token\"\n retry {\n backoff = \"1s\"\n }\n}\n\ntemplate {\ndestination = \"/etc/secrets/index.html\"\ncontents = \u003c\u003cEOT\n\u003chtml\u003e\n\u003cbody\u003e\n\u003cp\u003eSome secrets:\u003c/p\u003e\n{{- with secret \"otus/otus-ro/config\" }}\n\u003cul\u003e\n\u003cli\u003e\u003cpre\u003eusername: {{ .Data.username }}\u003c/pre\u003e\u003c/li\u003e\n\u003cli\u003e\u003cpre\u003epassword: {{ .Data.password }}\u003c/pre\u003e\u003c/li\u003e\n\u003c/ul\u003e\n{{ end }}\n\u003c/body\u003e\n\u003c/html\u003e\nEOT\n}\n","vault-agent-config.hcl":"exit_after_auth = true\n\npid_file = \"/home/vault/pidfile\"\n\nauto_auth {\n method \"kubernetes\" {\n mount_path = \"auth/kubernetes\"\n config = {\n role = \"otus\"\n }\n }\n\n sink \"file\" {\n config = {\n path = \"/home/vault/.vault-token\"\n }\n }\n}\n"},"kind":"ConfigMap","metadata":{"annotations":{},"name":"example-vault-agent-config","namespace":"default"}}
creationTimestamp: "2020-06-16T16:20:41Z"
name: example-vault-agent-config
namespace: default
resourceVersion: "603374"
selfLink: /api/v1/namespaces/default/configmaps/example-vault-agent-config
uid: 905b0e4b-b142-4d3b-a00c-7f1ef3eca0ba
kubectl apply -f example-k8s-spec.yaml
pod/vault-agent-example created
- Законнектимся к поду nginx и вытащить оттуда index.html
kubectl exec -ti vault-agent-example -c nginx-container -- cat /usr/share/nginx/html/index.html
<html>
<body>
<p>Some secrets:</p>
<ul>
<li><pre>username: otus</pre></li>
<li><pre>password: asajkjkahs</pre></li>
</ul>
</body>
</html>
- Включим pki секретс
kubectl exec -it vault-0 -- vault secrets enable pki
Success! Enabled the pki secrets engine at: pki/
kubectl exec -it vault-0 -- vault secrets tune -max-lease-ttl=87600h pki
Success! Tuned the secrets engine at: pki/\
kubectl exec -it vault-0 -- vault write -field=certificate pki/root/generate/internal common_name="example.ru" ttl=87600h > CA_cert.crt
kubectl exec -it vault-0 -- vault write pki/config/urls issuing_certificates="http://vault:8200/v1/pki/ca" crl_distribution_points="http://vault:8200/v1/pki/crl"
Success! Data written to: pki/config/urls
kubectl exec -it vault-0 -- vault secrets enable --path=pki_int pki
Success! Enabled the pki secrets engine at: pki_int/
kubectl exec -it vault-0 -- vault secrets tune -max-lease-ttl=87600h pki_int
Success! Tuned the secrets engine at: pki_int/
kubectl exec -it vault-0 -- vault write -format=json pki_int/intermediate/generate/internal common_name="example.ru Intermediate Authority" | jq -r '.data.csr' > pki_intermediate.csr
kubectl cp pki_intermediate.csr vault-0:./tmp/
kubectl exec -it vault-0 -- vault write -format=json pki/root/sign-intermediate csr=@/tmp/pki_intermediate.csr format=pem_bundle ttl="43800h" | jq -r '.data.certificate' > intermediate.cert.pem
kubectl cp intermediate.cert.pem vault-0:./tmp/
kubectl exec -it vault-0 -- vault write pki_int/intermediate/set-signed certificate=@/tmp/intermediate.cert.pem
Success! Data written to: pki_int/intermediate/set-signed
- Создадим роль для выдачи сертификатов
kubectl exec -it vault-0 -- vault write pki_int/roles/example-dot-ru \
allowed_domains="example.ru" allow_subdomains=true max_ttl="720h"
Success! Data written to: pki_int/roles/example-dot-ru
- Создадим сертификат
kubectl exec -it vault-0 -- vault write pki_int/issue/example-dot-ru common_name="test.example.ru" ttl="24h"
Key Value
--- -----
ca_chain [-----BEGIN CERTIFICATE-----
MIIDnDCCAoSgAwIBAgIUbs1EMMtsCfGi2jGitWn72jXprjcwDQYJKoZIhvcNAQEL
BQAwFTETMBEGA1UEAxMKZXhtYXBsZS5ydTAeFw0yMDA2MTYxNjQzNDhaFw0yNTA2
MTUxNjQ0MThaMCwxKjAoBgNVBAMTIWV4YW1wbGUucnUgSW50ZXJtZWRpYXRlIEF1
dGhvcml0eTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALhxdC24BpMK
/tkUdItpGgxQsRpp7tTf/6o/RXCblZjHMKsHRhhAdW/Ww57jdCI91Sx30VR4dr6P
0dXLJ/c0VY7PyiH/91j5cmnJGj8fKIKodMei9vNdI+hYKoe4FNc/a1kfKEsUHfGf
QRr2ORwXmEMUyYros+DHUrYoeIMQP+8XJtQQjHCEByWYM6Tgpt4y6pzaRGN97yFs
UlMQmhyh7daRXKX/A4Tx1h/qhbCAjjGTtOKcssaWX6mu+uaY3zNVaWsbPgrL6erD
XEII8Ojh9Mx1StSKjwJfyOCJMVbY5t5xCZfYkOTXGOmuZ4mp3wj5ufgce21NQXwt
vujWMuPrZGECAwEAAaOBzDCByTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUw
AwEB/zAdBgNVHQ4EFgQUWRZvC2QKvzWdmOhtPRKElhpzywkwHwYDVR0jBBgwFoAU
6RO2+EqqFpHyVMR0ya+hy3a7nF4wNwYIKwYBBQUHAQEEKzApMCcGCCsGAQUFBzAC
hhtodHRwOi8vdmF1bHQ6ODIwMC92MS9wa2kvY2EwLQYDVR0fBCYwJDAioCCgHoYc
aHR0cDovL3ZhdWx0OjgyMDAvdjEvcGtpL2NybDANBgkqhkiG9w0BAQsFAAOCAQEA
cXHrM3Cji8lGKIn7O6CnnEnPiiyTxw7QTxJqBZJcGjL5SU/sqDp1cXXupRwHo7Gv
NdqM47HGGpixSekUGqjrlPdKX7/vylFGsh/F8MMOPhIXibBGASL3od+r5fhDVcdk
89Va80qpXx1rNfhphN3YrvNKj6DQSqh4dvlQNkJdj5v+65/vSK52aSilO8h7MIDd
RDc3Hew6dn5URDYueKHzcQ5rn0hg65W+9qeDhdRgcwz9GPJG0adC+RzJGlJE3GKN
aE4fj0ibnC6YJ++1GEPfAmkVuY7YUyUXNK1N3T1B4dzNFqeJ/Mr+pbIssGg9kQ2Z
DjHb8ySUM3JfZp2nQw5/iA==
-----END CERTIFICATE-----]
certificate -----BEGIN CERTIFICATE-----
MIIDYzCCAkugAwIBAgIUU1UayYucW3SW43Etoz1kACntbucwDQYJKoZIhvcNAQEL
BQAwLDEqMCgGA1UEAxMhZXhhbXBsZS5ydSBJbnRlcm1lZGlhdGUgQXV0aG9yaXR5
MB4XDTIwMDYxNjE2NDg1MVoXDTIwMDYxNzE2NDkyMVowGjEYMBYGA1UEAxMPdGVz
dC5leGFtcGxlLnJ1MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAt+GI
RVjssgZgL240kXg+52zE9Yt6ToJI+gXs4wi7BSo68OJHGg8txN2lEOyMNx3Km64B
+Yxs3VGqXorMJgXcaYvbW+wgXFb+zqm6BN3jhVDiAOkmzJaaUtU0E6na/41olOX6
X1wLbkyU2bnV4K4J7KNjVXsGY0e0XZWu6nbfCy1vE5wtcrr8/9I723h6HW26g4cE
MPTXva2sTb/OBoPOozra/aPIYyJsgnPzNbC9SomqrSExXD7XyPzW6QsBOz3xotBZ
lQArT7GxK5BHBo2+Y94E7yGaGUKhfzjKDBVhOziAN3ZczUj/hlmIpB9+ptb140h3
dwuR8sOSQkZNP3XoPQIDAQABo4GOMIGLMA4GA1UdDwEB/wQEAwIDqDAdBgNVHSUE
FjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwHQYDVR0OBBYEFOV6+X88HiFwdYKR6YER
bQWmXSxyMB8GA1UdIwQYMBaAFFkWbwtkCr81nZjobT0ShJYac8sJMBoGA1UdEQQT
MBGCD3Rlc3QuZXhhbXBsZS5ydTANBgkqhkiG9w0BAQsFAAOCAQEAF1E87BGiE67a
8bgbAmAMBZl4tY0XxB9NwO3fqCEVMvgxY4xT3SSQFVJPdN3ZOgOeCxtQ3k88lv0s
I9mZ44uZREDbLx9kMBfWpMwi3JsOjaSjwo/qMbyOUwOkkpSMtoBgolhsXii0PMe2
LRGZbEM4qCTgdgayVDvMUMA1zXUAaIrNRl3GHvWJiNCjtOYDyQFZ+Py+cTuogrPo
pHNAXrvM+ImnsNBHh9iWTN4MJP3rUHEZl4mb7ndy1s7YQjZxd6cCcX6m4fpt0m/e
tbobikyARh248etGSEaoUAjF+9WnMk1fihzRVRhpqV/W/Ix5wiDxfJXTFf7csZub
XZIV134kyg==
-----END CERTIFICATE-----
expiration 1592412561
issuing_ca -----BEGIN CERTIFICATE-----
MIIDnDCCAoSgAwIBAgIUbs1EMMtsCfGi2jGitWn72jXprjcwDQYJKoZIhvcNAQEL
BQAwFTETMBEGA1UEAxMKZXhtYXBsZS5ydTAeFw0yMDA2MTYxNjQzNDhaFw0yNTA2
MTUxNjQ0MThaMCwxKjAoBgNVBAMTIWV4YW1wbGUucnUgSW50ZXJtZWRpYXRlIEF1
dGhvcml0eTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALhxdC24BpMK
/tkUdItpGgxQsRpp7tTf/6o/RXCblZjHMKsHRhhAdW/Ww57jdCI91Sx30VR4dr6P
0dXLJ/c0VY7PyiH/91j5cmnJGj8fKIKodMei9vNdI+hYKoe4FNc/a1kfKEsUHfGf
QRr2ORwXmEMUyYros+DHUrYoeIMQP+8XJtQQjHCEByWYM6Tgpt4y6pzaRGN97yFs
UlMQmhyh7daRXKX/A4Tx1h/qhbCAjjGTtOKcssaWX6mu+uaY3zNVaWsbPgrL6erD
XEII8Ojh9Mx1StSKjwJfyOCJMVbY5t5xCZfYkOTXGOmuZ4mp3wj5ufgce21NQXwt
vujWMuPrZGECAwEAAaOBzDCByTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUw
AwEB/zAdBgNVHQ4EFgQUWRZvC2QKvzWdmOhtPRKElhpzywkwHwYDVR0jBBgwFoAU
6RO2+EqqFpHyVMR0ya+hy3a7nF4wNwYIKwYBBQUHAQEEKzApMCcGCCsGAQUFBzAC
hhtodHRwOi8vdmF1bHQ6ODIwMC92MS9wa2kvY2EwLQYDVR0fBCYwJDAioCCgHoYc
aHR0cDovL3ZhdWx0OjgyMDAvdjEvcGtpL2NybDANBgkqhkiG9w0BAQsFAAOCAQEA
cXHrM3Cji8lGKIn7O6CnnEnPiiyTxw7QTxJqBZJcGjL5SU/sqDp1cXXupRwHo7Gv
NdqM47HGGpixSekUGqjrlPdKX7/vylFGsh/F8MMOPhIXibBGASL3od+r5fhDVcdk
89Va80qpXx1rNfhphN3YrvNKj6DQSqh4dvlQNkJdj5v+65/vSK52aSilO8h7MIDd
RDc3Hew6dn5URDYueKHzcQ5rn0hg65W+9qeDhdRgcwz9GPJG0adC+RzJGlJE3GKN
aE4fj0ibnC6YJ++1GEPfAmkVuY7YUyUXNK1N3T1B4dzNFqeJ/Mr+pbIssGg9kQ2Z
DjHb8ySUM3JfZp2nQw5/iA==
-----END CERTIFICATE-----
private_key -----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEAt+GIRVjssgZgL240kXg+52zE9Yt6ToJI+gXs4wi7BSo68OJH
Gg8txN2lEOyMNx3Km64B+Yxs3VGqXorMJgXcaYvbW+wgXFb+zqm6BN3jhVDiAOkm
zJaaUtU0E6na/41olOX6X1wLbkyU2bnV4K4J7KNjVXsGY0e0XZWu6nbfCy1vE5wt
crr8/9I723h6HW26g4cEMPTXva2sTb/OBoPOozra/aPIYyJsgnPzNbC9SomqrSEx
XD7XyPzW6QsBOz3xotBZlQArT7GxK5BHBo2+Y94E7yGaGUKhfzjKDBVhOziAN3Zc
zUj/hlmIpB9+ptb140h3dwuR8sOSQkZNP3XoPQIDAQABAoIBABXe8mllKUoHbhtW
HVSMG9dE3axi+Zoq7ukmGUXrvOrhWf9auqBD2+rFfiOkLw1DMt0PdlarOCue0gfK
tHt1SQEYzG+Dh1nUUxxbna+EgNJTi34WPTIqW5KsnzQTiOPKevzy2bPL6+QjobS/
4MuHh9CoomEBcAIQ/kf/TL3Ag8j9W8dEe/lFz6n2f7PnbXCWV0bQa7b2mc73F5sC
xWhATeR0QAKJtPUmgPuHNio1+vvVYu62ulEEJgxRrwcqejSAJyRbNi4qPnwzN0nI
x4MEFlzuF7V21oGkovlMPwLybnFSQ+A4FGaiBObEb6IxSRHh2CYwP/t74efiQvzD
gVNwYDUCgYEA4lJDO1C2XOc2ANvrACYEVwXl6+ZVQOwZTN2sMDa4+T/6MQu8ED3j
3LQ4NrQUv5CSsi0ru3g/uIbIZmsVl9KVdWibBiflH3XcGK0Gmuydj26+32sMn1IL
B1cPl4ovlFjDB82D/w8jirdp5bcrAhZhys7uKnaglzaWTxURioG4RqMCgYEAz/6F
zKsr5+2VHWUPx8zsAQbRhNH8aCpBTDjbisuyVDrR6CdhafRUAL8nFs6E/uHKFX7i
3wZdzV6F6G8yON6laO7CdhgyPnH3X6VBeg64NX4OV/E6tyyo+9zZFiinD2Wa1mxg
4/nF1kGblOoZuGVnvCwmVv6EfAo8V0ZYuCKbY58CgYEA3sZWw4Y5W82DjOjss2K4
luiJX+GEPbmpVc5F9deH0GE3ZxvD7MDfEJqdUsuEOOSYYaaxC4HcR0j07kuuH5n7
4GwtXKvcj1E2a1u1yr3qggti+wymihT6IimCgYpWN1H/+ChGK8S9GYhdRu87+HwL
mtSB/25xuSbUYde9QndZ6r0CgYBw1MIApVrMFjYuHATTfncIl4pRYxhp6YJCtxVh
1d6HkuEL71EJCWIIhanO1XmQSyw1PQAVvH5mSkCaNrgn4aAZrGwRZE7dUTXAzQUY
EaQcYKWnT1VqyO9isguR2bvGvnegmmp1Qanw8OZVgikxvtDnY4vo5Wk5YNQbBC5c
tWlWQQKBgQCc77OYAbK6STzcnq3Y5lbGEbPilsZFW9j4mJueOdIJ+zVABagjcdRj
O2ES9nYeeE2H9026Z4KFhCwH3EXEsuDNBDAOiJtt2G9Jyd2uHfsp5J/m7BIKo9Sa
qdWT93qzqGlKeeqeO9M60kupzxJ+piAR4R0TpiS4FnL4qJLnL70now==
-----END RSA PRIVATE KEY-----
private_key_type rsa
serial_number 53:55:1a:c9:8b:9c:5b:74:96:e3:71:2d:a3:3d:64:00:29:ed:6e:e7
- Отзовем сертификат
kubectl exec -it vault-0 -- vault write pki_int/revoke serial_number="53:55:1a:c9:8b:9c:5b:74:96:e3:71:2d:a3:3d:64:00:29:ed:6e:e7"
Key Value
--- -----
revocation_time 1592326196
revocation_time_rfc3339 2020-06-16T16:49:56.117712099Z
Реализуем доступ к vault через https с CA из кубернетес
- Создим CSR
openssl genrsa -out vault_gke.key 4096
vault_gke_csr.cnf
[ req ]
default_bits = 2048
prompt = no
default_md = sha256
distinguished_name = dn
req_extensions = v3_ext
[ dn ]
commonName = localhost
stateOrProvinceName = Moscow
countryName = RU
emailAddress = lucky@perflabs.org
organizationName = Perflabs
organizationalUnitName = Development
[ v3_ext ]
basicConstraints = CA:FALSE
keyUsage = keyEncipherment,dataEncipherment
extendedKeyUsage = serverAuth
subjectAltName = @alt_names
[alt_names]
DNS.0 = localhost
DNS.1 = vault
openssl req -config vault_gke_csr.cnf -new -key vault_gke.key -nodes -out vault.csr
openssl req -config vault_gke_csr.cnf -new -key vault_gke.key -nodes -out vault.csr
vault_csr.yaml
apiVersion: certificates.k8s.io/v1beta1
kind: CertificateSigningRequest
metadata:
name: vaultcsr
spec:
groups:
- system:authenticated
request: $(base64 < "vault.csr" | tr -d '\n')
usages:
- digital signature
- key encipherment
- server auth
- Применим
kubectl apply -f vault_csr.yaml
certificatesigningrequest.certificates.k8s.io/vaultcsr created
kubectl certificate approve vaultcsr
certificatesigningrequest.certificates.k8s.io/vaultcsr approved
kubectl get csr vaultcsr -o jsonpath='{.status.certificate}' | base64 --decode > vault.crt
kubectl create secret tls vault-certs --cert=vault.crt --key=vault_gke.key
Пересоздадим vault с новым vault-tls.values.yaml
helm upgrade --install vault hashicorp/vault -f vault-tls.values.yaml
Release "vault" does not exist. Installing it now.
NAME: vault
LAST DEPLOYED: Tue Jun 16 23:56:52 2020
NAMESPACE: default
STATUS: deployed
REVISION: 1
TEST SUITE: None
NOTES:
Thank you for installing HashiCorp Vault!
Now that you have deployed Vault, you should look over the docs on using
Vault with Kubernetes available here:
https://www.vaultproject.io/docs/
Your release is named vault. To learn more about the release, try:
$ helm status vault
$ helm get vault
Проверяем:
kubectl get secret $(kubectl get sa vault-auth -o jsonpath="{.secrets[*]['name']}") -o jsonpath="{.data['ca\.crt']}" | base64 --decode > ca.crt
kubectl port-forward vault-0 8200:8200
curl --cacert ca.crt -H "X-Vault-Token: s.Q4JOojZtdGgfiwoxJ4L3v75w" -X GET https://localhost:8200/v1/otus/otus-ro/config | jq
{
"request_id": "ecda370b-4696-43b5-d7ad-158736974806",
"lease_id": "",
"renewable": false,
"lease_duration": 2764800,
"data": {
"password": "asajkjkahs",
"username": "otus"
},
"wrap_info": null,
"warnings": null,
"auth": null
}
- Запустим nginx
- Реализуем автообнвление сертификатов для nginx c помощью vault-inject
Подготовим policy:
pki-policy.hcl
path "pki_int/issue/*" {
capabilities = ["create", "read", "update", "list"]
}
Применим:
kubectl cp nginx/nginx-policy.hcl vault-0:/home/vault
kubectl exec -it vault-0 -- vault policy write nginx-policy /home/vault/nginx-policy.hcl
Success! Uploaded policy: pki-policy
kubectl exec -it vault-0 -- vault write auth/kubernetes/role/nginx-role \
bound_service_account_names=vault-auth \
bound_service_account_namespaces=default policies=nginx-policy ttl=24h
Добавим анотации в нашему поду:
annotations:
vault.hashicorp.com/agent-inject: "true"
vault.hashicorp.com/agent-inject-status: "update"
vault.hashicorp.com/role: "nginx-role"
vault.hashicorp.com/agent-inject-secret-server.cert: "pki_int/issue/example-dot-ru"
vault.hashicorp.com/agent-inject-template-server.cert: |
{{- with secret "pki_int/issue/example-dot-ru" "common_name=nginx.example.ru" "ttl=2m" -}}
{{ .Data.certificate }}
{{- end }}
vault.hashicorp.com/agent-inject-secret-server.key: "pki_int/issue/example-dot-ru"
vault.hashicorp.com/agent-inject-template-server.key: |
{{- with secret "pki_int/issue/example-dot-ru" "common_name=nginx.example.ru" "ttl=2m" -}}
{{ .Data.private_key }}
{{- end }}
vault.hashicorp.com/service: "http://vault:8200"
vault.hashicorp.com/agent-inject-command-server.key: "/bin/sh -c 'pkill -HUP nginx || true'"
Описание анотаций
Применим:
kubectl apply -f nginx/nginx-configMap.yaml -f nginx/nginx-service.yaml -f nginx/nginx-deployment.yaml
configmap/nginx-config created
service/nginx created
deployment.apps/nginx created
kubectl get pods -l app=nginx
NAME READY STATUS RESTARTS AGE
nginx-65744668b8-2bppr 2/2 Running 0 49s
Демонстрация рефреша:
Настроим autounseal с помощью GCP Cloud KMS
Воспользуемся документациями: https://www.vaultproject.io/docs/platform/k8s/helm/run#google-kms-auto-unseal и https://www.vaultproject.io/docs/configuration/seal/gcpckms
-
Требования:
- Наличие Cloud IAM Service Account
- Включенного Cloud KMS API и созданных key ring и crypto key
-
Создадим secret из экспортированного json для service account
kubectl create secret generic kms-creds --from-file=/Users/alexey/gcp_sa.json
- Развернем consul
helm upgrade --install consul hashicorp/consul
Release "consul" does not exist. Installing it now.
NAME: consul
LAST DEPLOYED: Sat Jun 20 02:48:12 2020
NAMESPACE: default
STATUS: deployed
REVISION: 1
NOTES:
Thank you for installing HashiCorp Consul!
Now that you have deployed Consul, you should look over the docs on using
Consul with Kubernetes available here:
https://www.consul.io/docs/platform/k8s/index.html
Your release is named consul.
To learn more about the release if you are using Helm 2, run:
$ helm status consul
$ helm get consul
To learn more about the release if you are using Helm 3, run:
$ helm status consul
$ helm get all consul
kubectl get pods
NAME READY STATUS RESTARTS AGE
consul-consul-477ww 1/1 Running 0 54s
consul-consul-7cbdz 1/1 Running 0 54s
consul-consul-nqjhr 1/1 Running 0 54s
consul-consul-server-0 1/1 Running 0 54s
consul-consul-server-1 1/1 Running 0 54s
consul-consul-server-2 1/1 Running 0 54s
- Подготовим vault-kms.vault.yaml
server:
standalone:
enabled: false
extraEnvironmentVars:
GOOGLE_REGION: global
GOOGLE_PROJECT: angular-pursuit-275120
GOOGLE_APPLICATION_CREDENTIALS: /vault/userconfig/kms-creds/gcp_sa.json
extraVolumes:
- type: 'secret'
name: 'kms-creds'
ha:
enabled: true
config: |
ui = true
listener "tcp" {
tls_disable = 1
address = "[::]:8200"
cluster_address = "[::]:8201"
}
seal "gcpckms" {
project = "angular-pursuit-275120"
region = "global"
key_ring = "vault"
crypto_key = "vault"
}
storage "consul" {
path = "vault"
address = "HOST_IP:8500"
}
ui:
enabled: true
- Развернет Vault и убедимся, что он запечатан
helm upgrade --install vault hashicorp/vault -f vault-kms.values.yamlRelease "vault" does not exist. Installing it now.
NAME: vault
LAST DEPLOYED: Sat Jun 20 02:50:12 2020
NAMESPACE: default
STATUS: deployed
REVISION: 1
TEST SUITE: None
NOTES:
Thank you for installing HashiCorp Vault!
Now that you have deployed Vault, you should look over the docs on using
Vault with Kubernetes available here:
https://www.vaultproject.io/docs/
Your release is named vault. To learn more about the release, try:
$ helm status vault
$ helm get vault
kubectl get pods
NAME READY STATUS RESTARTS AGE
consul-consul-477ww 1/1 Running 0 2m22s
consul-consul-7cbdz 1/1 Running 0 2m22s
consul-consul-nqjhr 1/1 Running 0 2m22s
consul-consul-server-0 1/1 Running 0 2m22s
consul-consul-server-1 1/1 Running 0 2m22s
consul-consul-server-2 1/1 Running 0 2m22s
vault-0 0/1 Running 0 22s
vault-1 0/1 Running 0 22s
vault-2 0/1 Running 0 22s
vault-agent-injector-7895bcdb5f-g76dl 1/1 Running 0 22s
kubectl exec -it vault-0 -- vault status
Key Value
--- -----
Recovery Seal Type gcpckms
Initialized false
Sealed true
Total Recovery Shares 0
Threshold 0
Unseal Progress 0/0
Unseal Nonce n/a
Version n/a
HA Enabled true
- Инициализируем
kubectl exec -it vault-0 -- vault operator init
Recovery Key 1: WAH2rssyDCo48EQE0cB/WC5J4YVxF/X085oUrmgtS0eI
Recovery Key 2: iI5HPHKTulARtMmwMr+Q1Y5Bq69cV3R1p+uokmWg8MDT
Recovery Key 3: goWiBRqrKv4jO/kb63VX5EIhhNDg5D9bY0wIvImZ2f7l
Recovery Key 4: +f27e1PSc9ikNLV2Y6sjfAhP09p3W3LrFDHC9oj7Vj/L
Recovery Key 5: n6ADeDlFF1urLGmTIpw+KyM+6og72AhrMVSPTn+Yh1HZ
Initial Root Token: s.GpC6pYApLBC9SOY1RNtfAyHx
Success! Vault is initialized
Recovery key initialized with 5 key shares and a key threshold of 3. Please
securely distribute the key shares printed above.
- Vault автоматом распечатается
kubectl exec -it vault-0 -- vault status
Key Value
--- -----
Recovery Seal Type shamir
Initialized true
Sealed false
Total Recovery Shares 5
Threshold 3
Version 1.4.2
Cluster Name vault-cluster-ec859e31
Cluster ID aa2f86b0-8ced-a80f-0c3a-4127ffb78f78
HA Enabled true
HA Cluster https://vault-0.vault-internal:8201
HA Mode active
kubectl get pods
NAME READY STATUS RESTARTS AGE
consul-consul-477ww 1/1 Running 0 4m23s
consul-consul-7cbdz 1/1 Running 0 4m23s
consul-consul-nqjhr 1/1 Running 0 4m23s
consul-consul-server-0 1/1 Running 0 4m23s
consul-consul-server-1 1/1 Running 0 4m23s
consul-consul-server-2 1/1 Running 0 4m23s
vault-0 1/1 Running 0 2m23s
vault-1 1/1 Running 0 2m23s
vault-2 1/1 Running 0 2m23s
vault-agent-injector-7895bcdb5f-g76dl 1/1 Running 0 2m23s
- Удалим поды
kubectl delete pods -l app.kubernetes.io/name=vault
pod "vault-0" deleted
pod "vault-1" deleted
pod "vault-2" deleted
- После пересоздания подов Vault будет автоматически распечатан
kubectl exec -it vault-0 -- vault status
Key Value
--- -----
Recovery Seal Type shamir
Initialized true
Sealed false
Total Recovery Shares 5
Threshold 3
Version 1.4.2
Cluster Name vault-cluster-ec859e31
Cluster ID aa2f86b0-8ced-a80f-0c3a-4127ffb78f78
HA Enabled true
HA Cluster https://vault-0.vault-internal:8201
HA Mode active
kubectl get pods
NAME READY STATUS RESTARTS AGE
consul-consul-477ww 1/1 Running 0 5m11s
consul-consul-7cbdz 1/1 Running 0 5m11s
consul-consul-nqjhr 1/1 Running 0 5m11s
consul-consul-server-0 1/1 Running 0 5m11s
consul-consul-server-1 1/1 Running 0 5m11s
consul-consul-server-2 1/1 Running 0 5m11s
vault-0 1/1 Running 0 22s
vault-1 1/1 Running 0 22s
vault-2 1/1 Running 0 22s
vault-agent-injector-7895bcdb5f-g76dl 1/1 Running 0 3m11s
Для выполнения домашнего задания нам понадобится managed Kubernetes кластер в GCP:
- Как минимум 1 нода типа n1-standard-2 в default-pool
- Как минимум 3 ноды типа n1-standard-2 в infra-pool
Так как мы будем использовать свои инструменты для логирования, важно отключить Stackdriver, компоненты которого устанавливаются по умолчанию при создании GKE кластера.
Если этого не сделать, существует ненулевая вероятность конфликта между Fluentd, устанавливаемым как часть решения Stackdriver, и Fluent Bit
Как можно догадаться из названия, мы планируем отдать три из четырех нод кластера под инфраструктурные сервисы.
Присвоим этим нодам определенный , чтобы избежать запуска на них случайных pod.
Укажем следующую конфигурацию taint через web-интерфейс GCP: node-role=infra:NoSchedule
В результате должна получиться следующая конфигурация кластера:
kubectl get nodes
NAME STATUS ROLES AGE VERSION
gke-cluster-1-default-pool-77d8d79a-rltz Ready <none> 15m v1.16.9-gke.2
gke-cluster-1-infra-pool-f025c03e-btq7 Ready <none> 7m32s v1.16.9-gke.2
gke-cluster-1-infra-pool-f025c03e-c1t3 Ready <none> 7m33s v1.16.9-gke.2
gke-cluster-1-infra-pool-f025c03e-lbf4 Ready <none> 7m32s v1.16.9-gke.2
Для начала, установим в Kubernetes кластер уже знакомый нам HipsterShop.
Самый простой способ сделать это - применить подготовленный манифест:
kubectl create ns microservices-demo
namespace/microservices-demo created
kubectl apply -f https://raw.githubusercontent.com/express42/otus-platform-snippets/master/Module-02/Logging/microservices-demo-without-resources.yaml -n microservices-demo
deployment.apps/emailservice created
service/emailservice created
deployment.apps/checkoutservice created
service/checkoutservice created
deployment.apps/recommendationservice created
service/recommendationservice created
deployment.apps/frontend created
service/frontend created
service/frontend-external created
deployment.apps/paymentservice created
service/paymentservice created
deployment.apps/productcatalogservice created
service/productcatalogservice created
deployment.apps/cartservice created
service/cartservice created
deployment.apps/loadgenerator created
deployment.apps/currencyservice created
service/currencyservice created
deployment.apps/shippingservice created
service/shippingservice created
deployment.apps/redis-cart created
service/redis-cart created
deployment.apps/adservice created
service/adservice created
Проверим, что все pod развернулись на ноде из default-pool:
kubectl get pods -n microservices-demo -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
adservice-cb695c556-97r7f 1/1 Running 0 5m16s 10.60.0.19 gke-cluster-1-default-pool-77d8d79a-rltz <none> <none>
cartservice-f4677b75f-tbkmj 1/1 Running 2 5m18s 10.60.0.14 gke-cluster-1-default-pool-77d8d79a-rltz <none> <none>
checkoutservice-664f865b9b-gv8pp 1/1 Running 0 5m19s 10.60.0.10 gke-cluster-1-default-pool-77d8d79a-rltz <none> <none>
currencyservice-bb9d998bd-xcjcs 1/1 Running 0 5m17s 10.60.0.15 gke-cluster-1-default-pool-77d8d79a-rltz <none> <none>
emailservice-6756967b6d-znhwj 1/1 Running 0 5m19s 10.60.0.8 gke-cluster-1-default-pool-77d8d79a-rltz <none> <none>
frontend-766587959d-vwfxm 1/1 Running 0 5m19s 10.60.0.11 gke-cluster-1-default-pool-77d8d79a-rltz <none> <none>
loadgenerator-9f854cfc5-brhm8 1/1 Running 4 5m17s 10.60.0.17 gke-cluster-1-default-pool-77d8d79a-rltz <none> <none>
paymentservice-57c87dc78b-zch9w 1/1 Running 0 5m18s 10.60.0.12 gke-cluster-1-default-pool-77d8d79a-rltz <none> <none>
productcatalogservice-9f5d68b54-zrbqt 1/1 Running 0 5m18s 10.60.0.13 gke-cluster-1-default-pool-77d8d79a-rltz <none> <none>
recommendationservice-57c49756fd-lcq6s 1/1 Running 0 5m19s 10.60.0.9 gke-cluster-1-default-pool-77d8d79a-rltz <none> <none>
redis-cart-5f75fbd9c7-9mfck 1/1 Running 0 5m16s 10.60.0.18 gke-cluster-1-default-pool-77d8d79a-rltz <none> <none>
shippingservice-689c6457cd-sbp55 1/1 Running 0 5m17s 10.60.0.16 gke-cluster-1-default-pool-77d8d79a-rltz <none> <none>
В данном домашнем задании мы будет устанавливать и использовать различные решения для логирования различными способами.
Начнем с "классического" набора инструментов (ElasticSearch, Fluent Bit, Kibana) и "классического" способа его установки в Kubernetes кластер (Helm).
Рекомендуемый репозиторий с Helm chart для ElasticSearch и Kibana на текущий момент - https://github.com/elastic/helm-charts.
Добавим его:
helm repo add elastic https://helm.elastic.co
"elastic" has been added to your repositories
И установим нужные нам компоненты, для начала - без какой либо дополнительной настройки:
ElasticSearch:
helm upgrade --install elasticsearch elastic/elasticsearch --namespace observability
Release "elasticsearch" does not exist. Installing it now.
NAME: elasticsearch
LAST DEPLOYED: Fri Jun 5 11:54:41 2020
NAMESPACE: observability
STATUS: deployed
REVISION: 1
NOTES:
1. Watch all cluster members come up.
$ kubectl get pods --namespace=observability -l app=elasticsearch-master -w
2. Test cluster health using Helm test.
$ helm test elasticsearch --cleanup
Kibana
helm upgrade --install kibana elastic/kibana --namespace observability
Release "kibana" does not exist. Installing it now.
NAME: kibana
LAST DEPLOYED: Fri Jun 5 11:55:16 2020
NAMESPACE: observability
STATUS: deployed
REVISION: 1
TEST SUITE: None
Fluent Bit
helm upgrade --install fluent-bit stable/fluent-bit --namespace observability
Release "fluent-bit" does not exist. Installing it now.
NAME: fluent-bit
LAST DEPLOYED: Fri Jun 5 11:57:47 2020
NAMESPACE: observability
STATUS: deployed
REVISION: 1
NOTES:
fluent-bit is now running.
It will forward all container logs to the svc named fluentd on port: 24284
kubectl get pods -n observability -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
elasticsearch-master-0 0/1 Pending 0 3m41s <none> <none> <none> <none>
elasticsearch-master-1 0/1 Pending 0 3m41s <none> <none> <none> <none>
elasticsearch-master-2 0/1 Running 0 3m41s 10.60.0.20 gke-cluster-1-default-pool-77d8d79a-rltz <none> <none>
fluent-bit-j845q 1/1 Running 0 34s 10.60.0.21 gke-cluster-1-default-pool-77d8d79a-rltz <none> <none>
kibana-kibana-869dfc7ff-kvjl9 0/1 Pending 0 3m6s <none> <none> <none> <none>
Если посмотреть, как установленные нами сервисы распределились по нодам, можно догадаться, что что-то пошло не по плану, все сервисы с переменным успехом попытались запуститься только на одной ноде из default-pool.
Попробуем исправить это и запустить каждую реплику ElasticSearch на своей, выделенной ноде из infra-pool.
Создадим в директории kubernetes-logging файл elasticsearch.values.yaml, будем указывать в этом файле нужные нам values.
Для начала, обратимся к файлу values.yaml в репозитории и найдем там ключ tolerations.
Мы помним, что ноды из infra-pool имеют taint node-role=infra:NoSchedule.
Давайте разрешим ElasticSearch запускаться на данных нодах:
tolerations:
- key: node-role
operator: Equal
value: infra
effect: NoSchedule
Обновим установку:
helm upgrade --install elasticsearch elastic/elasticsearch --namespace observability -f elasticsearch.values.yaml
Release "elasticsearch" has been upgraded. Happy Helming!
NAME: elasticsearch
LAST DEPLOYED: Sat Jun 6 11:59:11 2020
NAMESPACE: observability
STATUS: deployed
REVISION: 2
NOTES:
1. Watch all cluster members come up.
$ kubectl get pods --namespace=observability -l app=elasticsearch-master -w
2. Test cluster health using Helm test.
$ helm test elasticsearch --cleanup
Теперь ElasticSearch может запускаться на нодах из infra-pool, но это не означает, что он должен это делать.
Исправим этот момент и добавим в elasticsearch.values.yaml NodeSelector, определяющий, на каких нодах мы можем запускать наши pod.
nodeSelector:
cloud.google.com/gke-nodepool: infra-pool
Другой, и, на самом деле, более гибкий способ осуществить задуманное - nodeAffinity
helm upgrade --install elasticsearch elastic/elasticsearch --namespace observability -f elasticsearch.values.yaml
Release "elasticsearch" has been upgraded. Happy Helming!
NAME: elasticsearch
LAST DEPLOYED: Sat Jun 6 12:02:43 2020
NAMESPACE: observability
STATUS: deployed
REVISION: 3
NOTES:
1. Watch all cluster members come up.
$ kubectl get pods --namespace=observability -l app=elasticsearch-master -w
2. Test cluster health using Helm test.
Если все выполнено корректно, то через некоторое время мы сможем наблюдать следующую картину:
kubectl get pods -n observability -o wide -l chart=elasticsearch
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
elasticsearch-master-0 1/1 Running 0 4m42s 10.60.1.2 gke-cluster-1-infra-pool-f025c03e-btq7 <none> <none>
elasticsearch-master-1 1/1 Running 0 6m5s 10.60.3.3 gke-cluster-1-infra-pool-f025c03e-c1t3 <none> <none>
elasticsearch-master-2 1/1 Running 0 7m34s 10.60.2.3 gke-cluster-1-infra-pool-f025c03e-lbf4 <none> <none>
Пока остановимся на этом и перейдем к следующему компоненту инсталляции.
Для того, чтобы продолжить установку EFK стека и получить доступ к Kibana, предварительно потребуется развернуть ingress-controller.
Установим nginx-ingress, состоящий из трех реплик controller, по одной, на каждую ноду из infra-pool.
Подготовим ingress.values.yaml
controller:
replicaCount: 3
tolerations:
- key: node-role
operator: Equal
value: infra
effect: NoSchedule
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchExpressions:
- key: app
operator: In
values:
- nginx-ingress
topologyKey: kubernetes.io/hostname
nodeSelector:
cloud.google.com/gke-nodepool: infra-pool
Развернем:
helm upgrade --install nginx-ingress stable/nginx-ingress --wait \
--namespace=nginx-ingress \
--version=1.39.0 -f nginx-ingress.values.yaml --create-namespace
Release "nginx-ingress" does not exist. Installing it now.
NAME: nginx-ingress
LAST DEPLOYED: Sat Jun 6 12:14:24 2020
NAMESPACE: nginx-ingress
STATUS: deployed
REVISION: 1
TEST SUITE: None
NOTES:
The nginx-ingress controller has been installed.
It may take a few minutes for the LoadBalancer IP to be available.
You can watch the status by running 'kubectl --namespace nginx-ingress get services -o wide -w nginx-ingress-controller'
An example Ingress that makes use of the controller:
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
annotations:
kubernetes.io/ingress.class: nginx
name: example
namespace: foo
spec:
rules:
- host: www.example.com
http:
paths:
- backend:
serviceName: exampleService
servicePort: 80
path: /
# This section is only required if TLS is to be enabled for the Ingress
tls:
- hosts:
- www.example.com
secretName: example-tls
If TLS is enabled for the Ingress, a Secret containing the certificate and key must also be provided:
apiVersion: v1
kind: Secret
metadata:
name: example-tls
namespace: foo
data:
tls.crt: <base64 encoded cert>
tls.key: <base64 encoded key>
type: kubernetes.io/tls
kubectl get pods -n nginx-ingress
NAME READY STATUS RESTARTS AGE
nginx-ingress-controller-64c77ff77-g7vgr 1/1 Running 0 3m53s
nginx-ingress-controller-64c77ff77-r9v6p 1/1 Running 0 3m53s
nginx-ingress-controller-64c77ff77-z4rpp 1/1 Running 0 3m53s
nginx-ingress-default-backend-5b967cf596-7pckn 1/1 Running 0 3m53s
По традиции создадим файл kibana.values.yaml в директории kubernetes-logging и добавим туда конфигурацию для создания ingress:
ingress:
enabled: true
annotations: {
kubernetes.io/ingress.class: nginx
}
path: /
hosts:
- kibana.35.205.196.95.xip.io
Обновим релиз:
helm upgrade --install kibana elastic/kibana --namespace observability -f kibana.values.yaml
Release "kibana" has been upgraded. Happy Helming!
NAME: kibana
LAST DEPLOYED: Sat Jun 6 12:27:19 2020
NAMESPACE: observability
STATUS: deployed
REVISION: 2
TEST SUITE: None
После прохождения всех предыдущих шагов у вас должен появиться доступ к Kibana по URL kibana.35.205.196.95.xip.io
Попробуем создать index pattern, и увидим, что в ElasticSearch пока что не обнаружено никаких данных.
Посмотрим в логи решения, которое отвечает за отправку логов (Fluent Bit) и увидим следующие строки:
kubectl logs fluent-bit-j845q -n observability --tail 3
[2020/06/06 09:29:24] [ warn] net_tcp_fd_connect: getaddrinfo(host='fluentd'): Name or service not known
[2020/06/06 09:29:24] [error] [out_fw] no upstream connections available
[2020/06/06 09:29:24] [ warn] [engine] failed to flush chunk '1-1591347474.564902547.flb', retry in 1581 seconds: task_id=18, input=tail.0 > output=forward.0
Попробуем исправить проблему. Создадим файл fluentbit.values.yaml и добавим туда:
backend:
type: es
es:
host: elasticsearch-master
Описание того, что мы сделали, можно найти в документации и в оригинальном файле values
helm upgrade --install fluent-bit stable/fluent-bit --namespace observability -f fluent-bit.values.yaml
Release "fluent-bit" has been upgraded. Happy Helming!
NAME: fluent-bit
LAST DEPLOYED: Sat Jun 6 12:31:46 2020
NAMESPACE: observability
STATUS: deployed
REVISION: 2
NOTES:
fluent-bit is now running.
It will forward all container logs to the svc named elasticsearch-master on port: 9200
Попробуем повторно создать index pattern. В этот раз ситуация изменилась, и какие-то индексы в ElasticSearch уже ест.
После установки можно заметить, что в ElasticSearch попадают далеко не все логи нашего приложения.
Причину можно найти в логах pod с Fluent Bit, он пытается обработать JSON, отдаваемый приложением, и находит там дублирующиеся поля time и timestamp
GitHub issue, с более подробным описанием проблемы
Вариантов решения проблемы, озвученной ранее, несколько:
- Полностью отключить парсинг JSON внутри лога ключом filter.mergeJSONLog=false
- Складывать содержимое лога после парсинга в отдельный ключ (в нашем случае для некоторых микросервисов возникнет проблема illegal_argument_exception с полем time)
- Изменить имя ключа (time_key) с датой, добавляемое самим Fluent Bit, на что-то, отличное от @timestamp. Это не решит проблему с тем, что останется поле time, которое также будет помечено дублирующимся
- "Вырезать" из логов поля time и @timestamp
Мы пойдем сложным путем и воспользуемся фильтром, который позволит удалить из логов "лишние" ключи
Пример итогового fluent-bit.values.yaml:
rawConfig: |
@INCLUDE fluent-bit-service.conf
@INCLUDE fluent-bit-input.conf
@INCLUDE fluent-bit-filter.conf
@INCLUDE fluent-bit-output.conf
[FILTER]
Name modify
Match *
Remove time
Remove @timestamp
Обновим:
helm upgrade --install fluent-bit stable/fluent-bit --namespace observability -f fluent-bit.values.yaml
Release "fluent-bit" has been upgraded. Happy Helming!
NAME: fluent-bit
LAST DEPLOYED: Sat Jun 6 12:44:32 2020
NAMESPACE: observability
STATUS: deployed
REVISION: 3
NOTES:
fluent-bit is now running.
It will forward all container logs to the svc named elasticsearch-master on port: 9200
Установка EFK стека | Задание со ⭐
Для решения проблемы с дублирующими полями изменим наш fluent-bit.values.yaml:
- Уберем секцию удаления полей time и timestamp
- Добавим mergeLogKey: "app" - это добавление префикса к полям из json
filter:
mergeLogKey: "app"
Помимо установки ElasticSearch, важно отслеживать его показатели и вовремя понимать, что пора предпринять какие-либо действия. Для мониторинга ElasticSearch будем использовать следующий Prometheus exporter
- Установим prometheus-operator в namespace observability
prometheus:
ingress:
enabled: true
annotations: {
kubernetes.io/ingress.class: nginx
}
path: /
hosts:
- prometheus.35.205.196.95.xip.io
prometheusSpec:
tolerations:
- key: node-role
operator: Equal
value: infra
effect: NoSchedule
serviceMonitorSelectorNilUsesHelmValues: false
grafana:
ingress:
enabled: true
annotations: {
kubernetes.io/ingress.class: nginx
}
path: /
hosts:
- grafana.35.205.196.95.xip.io
helm upgrade --install prometheus-operator stable/prometheus-operator -n observability --create-namespace -f prometheus.values.yaml
Release "prometheus-operator" does not exist. Installing it now.
manifest_sorter.go:192: info: skipping unknown hook: "crd-install"
manifest_sorter.go:192: info: skipping unknown hook: "crd-install"
manifest_sorter.go:192: info: skipping unknown hook: "crd-install"
manifest_sorter.go:192: info: skipping unknown hook: "crd-install"
manifest_sorter.go:192: info: skipping unknown hook: "crd-install"
manifest_sorter.go:192: info: skipping unknown hook: "crd-install"
NAME: prometheus-operator
LAST DEPLOYED: Sat Jun 6 12:47:46 2020
NAMESPACE: observability
STATUS: deployed
REVISION: 1
NOTES:
The Prometheus Operator has been installed. Check its status by running:
kubectl --namespace observability get pods -l "release=prometheus-operator"
Visit https://github.com/coreos/prometheus-operator for instructions on how
to create & configure Alertmanager and Prometheus instances using the Operator.
- Установим prometheus exporter
helm upgrade --install elasticsearch-exporter stable/elasticsearch-exporter --set es.uri=http://elasticsearch-master:9200 --set serviceMonitor.enabled=true --namespace=observability
Release "elasticsearch-exporter" does not exist. Installing it now.
NAME: elasticsearch-exporter
LAST DEPLOYED: Sat Jun 6 12:54:01 2020
NAMESPACE: observability
STATUS: deployed
REVISION: 1
TEST SUITE: None
NOTES:
1. Get the application URL by running these commands:
export POD_NAME=$(kubectl get pods --namespace observability -l "app=elasticsearch-exporter" -o jsonpath="{.items[0].metadata.name}")
echo "Visit http://127.0.0.1:9108/metrics to use your application"
kubectl port-forward $POD_NAME 9108:9108 --namespace observability
Импортируем в Grafana один из популярных Dashboard для ElasticSearch exporter, содержащий визуализацию основных собираемых метрик.
Проверим, что метрики действительно собираются корректно.
Сделаем drain одной из нод infra-pool: kubectl drain <NODE_NAME> --ignore-daemonsets
kubectl get nodes
NAME STATUS ROLES AGE VERSION
gke-cluster-1-default-pool-77d8d79a-rltz Ready <none> 39h v1.16.9-gke.2
gke-cluster-1-infra-pool-f025c03e-btq7 Ready <none> 39h v1.16.9-gke.2
gke-cluster-1-infra-pool-f025c03e-c1t3 Ready <none> 39h v1.16.9-gke.2
gke-cluster-1-infra-pool-f025c03e-lbf4 Ready <none> 39h v1.16.9-gke.2
kubectl drain gke-cluster-1-infra-pool-f025c03e-c1t3 --ignore-daemonsets
node/gke-cluster-1-infra-pool-f025c03e-c1t3 cordoned
WARNING: ignoring DaemonSet-managed Pods: observability/prometheus-operator-prometheus-node-exporter-w4ljx
evicting pod "elasticsearch-master-1"
evicting pod "nginx-ingress-controller-64c77ff77-r9v6p"
pod/elasticsearch-master-1 evicted
pod/nginx-ingress-controller-64c77ff77-r9v6p evicted
node/gke-cluster-1-infra-pool-f025c03e-c1t3 evicted
kubectl get pods -n observability
NAME READY STATUS RESTARTS AGE
alertmanager-prometheus-operator-alertmanager-0 2/2 Running 0 52m
elasticsearch-exporter-65d6647f8b-9gw76 1/1 Running 0 117m
elasticsearch-master-0 1/1 Running 0 165m
elasticsearch-master-1 0/1 Pending 0 59s
elasticsearch-master-2 1/1 Running 0 168m
fluent-bit-9fjhk 1/1 Running 0 126m
kibana-kibana-869dfc7ff-kvjl9 1/1 Running 0 26h
prometheus-operator-grafana-6567c8ff97-46hn8 2/2 Running 0 52m
prometheus-operator-kube-state-metrics-7f979567df-cwpzf 1/1 Running 0 52m
prometheus-operator-operator-874c49dfb-jqkbr 2/2 Running 0 34m
prometheus-operator-prometheus-node-exporter-7mjxz 1/1 Running 0 49m
prometheus-operator-prometheus-node-exporter-gmpfw 1/1 Running 0 49m
prometheus-operator-prometheus-node-exporter-r68t4 1/1 Running 0 49m
prometheus-operator-prometheus-node-exporter-w4ljx 1/1 Running 0 49m
prometheus-prometheus-operator-prometheus-0 3/3 Running 1 18m
Статус Cluster Health остался зеленым, но количество нод в кластере уменьшилось до двух штук. При этом, кластер сохранил полную работоспособность.
Попробуем сделать drain второй ноды из infra-pool, и увидим что не дает этого сделать.
kubectl drain gke-cluster-1-infra-pool-f025c03e-lbf4 --ignore-daemonsets
node/gke-cluster-1-infra-pool-f025c03e-lbf4 cordoned
error: unable to drain node "gke-cluster-1-infra-pool-f025c03e-lbf4", aborting command...
There are pending nodes to be drained:
gke-cluster-1-infra-pool-f025c03e-lbf4
error: cannot delete Pods with local storage (use --delete-local-data to override): observability/prometheus-prometheus-operator-prometheus-0
Вручную удалим pod, находящийся на ноде, для которой мы пытались сделать drain:
kubectl delete pod elasticsearch-master-2 -n observability
pod "elasticsearch-master-2" deleted
kubectl get pods -n observability -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
alertmanager-prometheus-operator-alertmanager-0 2/2 Running 0 58m 10.60.0.44 gke-cluster-1-default-pool-77d8d79a-rltz <none> <none>
elasticsearch-exporter-65d6647f8b-9gw76 1/1 Running 0 123m 10.60.0.33 gke-cluster-1-default-pool-77d8d79a-rltz <none> <none>
elasticsearch-master-0 1/1 Running 0 172m 10.60.1.2 gke-cluster-1-infra-pool-f025c03e-btq7 <none> <none>
elasticsearch-master-1 0/1 Pending 0 7m12s <none> <none> <none> <none>
elasticsearch-master-2 0/1 Pending 0 33s <none> <none> <none> <none>
fluent-bit-9fjhk 1/1 Running 0 133m 10.60.0.26 gke-cluster-1-default-pool-77d8d79a-rltz <none> <none>
kibana-kibana-869dfc7ff-kvjl9 1/1 Running 0 27h 10.60.0.23 gke-cluster-1-default-pool-77d8d79a-rltz <none> <none>
prometheus-operator-grafana-6567c8ff97-46hn8 2/2 Running 0 58m 10.60.0.42 gke-cluster-1-default-pool-77d8d79a-rltz <none> <none>
prometheus-operator-kube-state-metrics-7f979567df-cwpzf 1/1 Running 0 58m 10.60.0.41 gke-cluster-1-default-pool-77d8d79a-rltz <none> <none>
prometheus-operator-operator-874c49dfb-jqkbr 2/2 Running 0 40m 10.60.0.56 gke-cluster-1-default-pool-77d8d79a-rltz <none> <none>
prometheus-operator-prometheus-node-exporter-7mjxz 1/1 Running 0 55m 10.132.0.9 gke-cluster-1-infra-pool-f025c03e-btq7 <none> <none>
prometheus-operator-prometheus-node-exporter-gmpfw 1/1 Running 0 55m 10.132.0.8 gke-cluster-1-default-pool-77d8d79a-rltz <none> <none>
prometheus-operator-prometheus-node-exporter-r68t4 1/1 Running 0 55m 10.132.0.10 gke-cluster-1-infra-pool-f025c03e-lbf4 <none> <none>
prometheus-operator-prometheus-node-exporter-w4ljx 1/1 Running 0 55m 10.132.0.11 gke-cluster-1-infra-pool-f025c03e-c1t3 <none> <none>
prometheus-prometheus-operator-prometheus-0 3/3 Running 1 25m 10.60.2.6 gke-cluster-1-infra-pool-f025c03e-lbf4 <none> <none>
kubectl get nodes
NAME STATUS ROLES AGE VERSION
gke-cluster-1-default-pool-77d8d79a-rltz Ready <none> 39h v1.16.9-gke.2
gke-cluster-1-infra-pool-f025c03e-btq7 Ready <none> 39h v1.16.9-gke.2
gke-cluster-1-infra-pool-f025c03e-c1t3 Ready,SchedulingDisabled <none> 39h v1.16.9-gke.2
gke-cluster-1-infra-pool-f025c03e-lbf4 Ready,SchedulingDisabled <none> 39h v1.16.9-gke.2\
После данного действия можно заметить следующее:
- Оставшийся pod с ElasticSearch перешел в статус "Not Ready"
- Kibana потеряла подключение к кластеру
- Метрики Prometheus перестали собираться, так как у сервиса, к которому подключается exporter, пропали все endpoint
Сделаем вывод - узнавать о проблемах с ElasticSearch в нашем сценарии (replication factor = 1: 1 shard + 1 replica на индекс) желательно на этапе выхода из строя первой ноды в кластере.
В определении проблем с участниками кластера ElasticSearch может помочь следующий Prometheus alert источник.
ALERT ElasticsearchTooFewNodesRunning
IF elasticsearch_cluster_health_number_of_nodes < 3
FOR 5m
LABELS {severity="critical"}
ANNOTATIONS {description="There are only {{$value}} < 3 ElasticSearch nodes running", summary="ElasticSearch running on less than 3 nodes"}
Ввернем ноды в строй (kubectl uncordon <NODE_NAME>)
kubectl uncordon gke-cluster-1-infra-pool-f025c03e-c1t3
node/gke-cluster-1-infra-pool-f025c03e-c1t3 uncordoned
kubectl uncordon gke-cluster-1-infra-pool-f025c03e-lbf4
node/gke-cluster-1-infra-pool-f025c03e-lbf4 uncordoned
kubectl get nodes
NAME STATUS ROLES AGE VERSION
gke-cluster-1-default-pool-77d8d79a-rltz Ready <none> 39h v1.16.9-gke.2
gke-cluster-1-infra-pool-f025c03e-btq7 Ready <none> 39h v1.16.9-gke.2
gke-cluster-1-infra-pool-f025c03e-c1t3 Ready <none> 39h v1.16.9-gke.2
gke-cluster-1-infra-pool-f025c03e-lbf4 Ready <none> 39h v1.16.9-gke.2
Рассмотрим некоторое количество ключевых метрик, которые рекомендуется отслеживать при эксплуатации ElasticSearch:
- unassigned_shards - количество shard, для которых не нашлось подходящей ноды, их наличие сигнализирует о проблемах
- jvm_memory_usage - высокая загрузка (в процентах от выделенной памяти) может привести к замедлению работы кластера
- number_of_pending_tasks - количество задач, ожидающих выполнения. Значение метрики, отличное от нуля, может сигнализировать о наличии проблем внутри кластера
Больше метрик с их описанием можно найти здесь
Для того, чтобы логи nginx-ingress отобразились в Kibana разрешим запуск fluent-bit на infra нодах в fluent-bit.values.yaml:
tolerations:
- key: node-role
operator: Equal
value: infra
effect: NoSchedule
helm upgrade --install fluent-bit stable/fluent-bit --namespace observability -f fluent-bit.values.yaml
Release "fluent-bit" has been upgraded. Happy Helming!
NAME: fluent-bit
LAST DEPLOYED: Sat Jun 6 15:57:57 2020
NAMESPACE: observability
STATUS: deployed
REVISION: 4
NOTES:
fluent-bit is now running.
После появления логов nginx у нас возникнет следующая проблема:
"_source": {
"log": "10.44.2.1 - - [11/Feb/2020:15:26:56 +0000] \"POST
/elasticsearch/kubernetes_cluster-*/_search?
rest_total_hits_as_int=true&ignore_unavailable=true&ignore_throttled=true&preference=158
1433758087&timeout=30000ms HTTP/1.1\" 499 0
\"http://kibana.35.184.239.20.xip.io/app/kibana\" \"Mozilla/5.0 (Macintosh; Intel Mac OS
X 10_15_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.87 Safari/537.36\"
1535 0.303 [observability-kibana-kibana-5601] [] 10.44.0.70:5601 0 0.303 -
8de867470c348bd5598e554b5b36f8d9\n",
Сейчас лог представляет из себя строку, с которой сложно работать.
Мы можем использовать полнотекстовый поиск, но лишены возможности:
- Задействовать функции KQL
- Полноценно проводить аналитику
- Создавать Dashboard по логам
Вспомним что у nginx есть возможность выбора формата и отформатируем лог в JSON
Отвечает за это ключ log-format-escape-json, добавим его в nginx-ingress.values.yaml
Также не забудем про опцию log-format-upstream
nginx-ingress.values.yaml
config:
log-format-escape-json: "true"
log-format-upstream: '{"remote_addr": "$proxy_protocol_addr", "x-forward-for": "$proxy_add_x_forwarded_for", "request_id": "$req_id", "remote_user": "$remote_user", "bytes_sent": $bytes_sent, "request_time": $request_time, "status":$status, "vhost": "$host", "request_proto": "$server_protocol", "path": "$uri", "request_query": "$args", "request_length": $request_length, "duration": $request_time,"method": "$request_method", "http_referrer": "$http_referer", "http_user_agent": "$http_user_agent" }'
kubectl get configmaps nginx-ingress-controller -n nginx-ingress -o yaml
apiVersion: v1
data:
log-format-escape-json: "true"
kind: ConfigMap
metadata:
annotations:
meta.helm.sh/release-name: nginx-ingress
meta.helm.sh/release-namespace: nginx-ingress
creationTimestamp: "2020-06-06T13:11:23Z"
labels:
app: nginx-ingress
app.kubernetes.io/managed-by: Helm
chart: nginx-ingress-1.39.0
component: controller
heritage: Helm
release: nginx-ingress
name: nginx-ingress-controller
namespace: nginx-ingress
resourceVersion: "661825"
selfLink: /api/v1/namespaces/nginx-ingress/configmaps/nginx-ingress-controller
uid: ec1b4699-6af3-4814-8f8d-e21b906457df
Если все сделано корректно, формат логов должен измениться на следующий:
"_source": {
x-forward-for": "10.128.0.35",
"request_id": "bfcee33afe75c099f7887d7e70b1ab00",
"bytes_sent": 19087,
"request_time": 1.168,
"status": 200,
Теперь, когда мы научились собирать логи с nginx-ingress и смогли их структурировать, можно опробовать возможности Kibana для визуализации.
Перейдем на вкладку Visualize и создадим новую визуализацию с типом TSVB.
Для начала, создадим визуализацию, показывающую общее количество запросов к nginx-ingress. Для этого нам понадобится применить следующий KQL фильтр: kubernetes.labels.app : nginx-ingress
Добавим данный фильтр в Panel options нашей визуализации.
Также создадим визуализацию для отображения запросов к nginx-ingress со статусами:
- 200-299
- 300-399
- 400-499
- 500+
Пришло время объединить созданные нами визуализации в Dashboard.
- Создадим Dashboard (соответствующая вкладка в меню) и добавим на него наши визуализации.
- Экспортируем получившиеся визуализации и Dashboard и добавим файл export.ndjson в директорию kubernetes-logging
Перейдем к еще одному решению для сбора и визуализации логов Kubernetes кластера
- Установим Loki в namespace observability, используя любой из доступных способов. Должны быть установлены непосредственно Loki и Promtail
- Модифицируем конфигурацию prometheus-operator таким образом, чтобы datasource Loki создавался сразу после установки оператора
- Итоговый файл prometheus-operator.values.yaml выложим в репозиторий в директорию kubernetes-logging
- Включим метрики для nginx-ingress
loki.values.yaml
promtail:
tolerations:
- key: node-role
operator: Equal
value: infra
effect: NoSchedule
prometheus.values.yaml
additionalDataSources:
- name: Loki
type: loki
url: http://loki:3100/
access: proxy
nginx-ingress.values.yaml
metrics:
enabled: true
serviceMonitor:
enabled: true
namespace: observability
helm repo add loki https://grafana.github.io/loki/charts
"loki" has been added to your repositories
helm repo update
helm upgrade --install loki loki/loki-stack -n observability -f loki.values.yaml
Release "loki" does not exist. Installing it now.
NAME: loki
LAST DEPLOYED: Sat Jun 6 17:00:38 2020
NAMESPACE: observability
STATUS: deployed
REVISION: 1
NOTES:
The Loki stack has been deployed to your cluster. Loki can now be added as a datasource in Grafana.
See http://docs.grafana.org/features/datasources/loki/ for more detail.
В итоге должно получиться следующее:
kubectl get pods -n observability
NAME READY STATUS RESTARTS AGE
alertmanager-prometheus-operator-alertmanager-0 2/2 Running 0 3h3m
elasticsearch-exporter-65d6647f8b-9gw76 1/1 Running 0 4h8m
elasticsearch-master-0 1/1 Running 0 4h56m
elasticsearch-master-1 1/1 Running 0 131m
elasticsearch-master-2 1/1 Running 0 125m
fluent-bit-8nl7n 1/1 Running 0 64m
fluent-bit-hb58v 1/1 Running 0 64m
fluent-bit-m2v2q 1/1 Running 0 64m
fluent-bit-t99hv 1/1 Running 0 64m
kibana-kibana-869dfc7ff-kvjl9 1/1 Running 0 29h
loki-0 1/1 Running 0 106s
loki-promtail-8m62g 1/1 Running 0 22s
loki-promtail-bpn8d 1/1 Running 0 22s
loki-promtail-dg2rl 1/1 Running 0 22s
loki-promtail-jw5xg 1/1 Running 0 22s
prometheus-operator-grafana-6567c8ff97-46hn8 2/2 Running 0 3h3m
prometheus-operator-kube-state-metrics-7f979567df-cwpzf 1/1 Running 0 3h3m
prometheus-operator-operator-874c49dfb-jqkbr 2/2 Running 0 164m
prometheus-operator-prometheus-node-exporter-7mjxz 1/1 Running 0 179m
prometheus-operator-prometheus-node-exporter-gmpfw 1/1 Running 0 179m
prometheus-operator-prometheus-node-exporter-r68t4 1/1 Running 0 179m
prometheus-operator-prometheus-node-exporter-w4ljx 1/1 Running 0 179m
prometheus-prometheus-operator-prometheus-0 3/3 Running 1 149m
До недавнего времени, единственным способом визуализации логов в Loki была функция Explore. Например, логи nginx-ingress с ее помощью можно отобразить следующим образом: {app="nginx-ingress"}
Loki, аналогично ElasticSearch умеет разбирать JSON лог по ключам, но, к сожалению, фильтрация по данным ключам на текущий момент не работает.
Создадим Dashboard, на котором одновременно выведем метрики nginx-ingress и его логи
-
Убедимся, что вместе с nginx-ingress устанавливается serviceMonitor, и Prometheus "видит" его (ответ выше в nginx-ingress.values.yaml)
-
Создадим в Grafana новый Dashboard
-
Добавим для него следующие переменные (взяты из официального Dashoboard для nginx-ingress):
-
namespace
-
controller_class
-
controller
-
ingress
-
Создадим новую панель и добим туда следующую [query] (взято из официального Dashboard для nginx-ingress):
sum(rate(nginx_ingress_controller_requests{controller_pod=~"$controller",controller_class=~"$controller_class",namespace=~"$namespace",ingress=~"$ingress", status!~"[4-5].*"} [1m])) by (ingress) /
sum(rate(nginx_ingress_controller_requests{controller_pod=~"$controller",controller_class=~"$controller_class",namespace=~"$namespace",ingress=~"$ingress"}[1m])) by (ingress)
- Аналогичным образом добавим панель, позволяющую оценить количество запросов к nginx-ingress в секунду
- Добавим панель с логами и укажем для нее следующие настройки Query: {app="nginx-ingress"}
- Выгрузим из Grafana JSON с финальным Dashboard и поместим его в файл kubernetes-logging/nginx-ingress.json
Также можно обратить внимание на еще одну небольшую, но очень полезную утилиту, позволяющую получить и сохранить event'ы Kubernetes в выбранном решении для логирования
Например, event, говорящий нам о том, что liveness probe у prometheus-operator выполнилась неуспешно
Развернем:
helm upgrade --install k8s-event-logger k8s-event-logger -n observability --create-namespace
Release "k8s-event-logger" does not exist. Installing it now.
NAME: k8s-event-logger
LAST DEPLOYED: Sun Jun 7 22:19:18 2020
NAMESPACE: observability
STATUS: deployed
REVISION: 1
TEST SUITE: None
NOTES:
To verify that the k8s-event-logger pod has started, run:
kubectl --namespace=observability get pods -l "app.kubernetes.io/name=k8s-event-logger,app.kubernetes.io/instance=k8s-event-logger"
kubectl get pods -n observability
NAME READY STATUS RESTARTS AGE
k8s-event-logger-6db47fdcfd-s26z4 1/1 Running 0 2m35s
Еще один важный тип логов, который рекомендуется собирать и хранить - логи аудита
Получить их в GKE сложнее, чем в self-hosted кластерах из-за того, что доступ к master нодам, на которых запущен kubeapiserver, отсутствует.
Поэтому развернем self-hosted кластер и настроим сбор аудит логов:
Наши ноды:
kubectl get nodes
NAME STATUS ROLES AGE VERSION
master1 Ready master 2d23h v1.18.3
master2 Ready master 2d20h v1.18.3
worker1 Ready <none> 2d20h v1.18.3
worker2 Ready <none> 2d20h v1.18.3
worker3 Ready <none> 2d20h v1.18.3
- Развернем Elasticsearch и Kibana
- Создадим fluent-bit.audit.values.yaml:
- Запускающий Fluent-bit на всех нодах
- Включим опцию аудита в параметрах
- Развернем Fluent-bit
backend:
type: es
es:
host: elasticsearch-master
audit:
enable: true
input:
memBufLimit: 35MB
parser: docker
tag: audit.*
path: /var/log/kube-audit/audit.log
bufferChunkSize: 2MB
bufferMaxSize: 10MB
skipLongLines: On
key: kubernetes-audit
rawConfig: |
@INCLUDE fluent-bit-service.conf
@INCLUDE fluent-bit-input.conf
@INCLUDE fluent-bit-filter.conf
@INCLUDE fluent-bit-output.conf
tolerations:
- operator: Exists
- Создадим необходимые для работы директории
sudo mkdir -p /etc/kubernetes/policies
sudo mkdir -p /var/log/kube-audit
Создадим /etc/kubernetes/policies/audit-policy.yaml со следующим содержимым:
apiVersion: audit.k8s.io/v1
kind: Policy
rules:
- level: Metadata
Используем простейшую политику аудита - Metadata - логирование метаданных запроса: пользователя, время запроса, целевой ресурс (pod, namespace и т.п.), тип действия (verb)
Добавим в /etc/kubernetes/manifests/kube-apiserver.yaml
- Включим аудит, нам нужно передать контейнеру в api-server три обязательных параметра:
- --audit-policy-file=/etc/kubernetes/policies/audit-policy.yaml
- --audit-log-path=/var/log/kube-audit/audit.log
- --audit-log-format=json
- audit-policy-file — путь до YAML-файла с описанием политики (policy) аудита:
volumeMounts:
- mountPath: /etc/kubernetes/policies
name: policies
readOnly: true
volumes:
- hostPath:
path: /etc/kubernetes/policies
type: DirectoryOrCreate
name: policies
- audit-log-path — путь до файла лога:
volumeMounts:
- mountPath: /var/log/kube-audit
name: logs
readOnly: false
volumes:
- hostPath:
path: /var/log/kube-audit
type: DirectoryOrCreate
name: logs
Перезапустим api-server:
sudo docker stop $(sudo docker ps | grep k8s_kube-apiserver | awk '{print $1}')
Теперь перейдем в Kibana, где мы сможем наблюдать следующие логи аудита:
_source:
kind:Event @timestamp:Jun 8, 2020 @ 01:12:18.763 apiVersion:audit.k8s.io/v1 level:Metadata auditID:83490317-c0c2-4a30-9726-77ca7b37c47b stage:RequestReceived requestURI:/apis/coordination.k8s.io/v1/namespaces/kube-system/leases/kube-scheduler?timeout=10s verb:update user.username:system:kube-scheduler user.groups:system:authenticated sourceIPs:10.2.1.2 userAgent:kube-scheduler/v1.18.3 (linux/amd64) kubernetes/2e7996e/leader-election objectRef.resource:leases objectRef.namespace:kube-system objectRef.name:kube-
- На текущий момент мы лишены возможности централизованного просмотра логов с виртуальных машин, на которых запущен Kubernetes
- Модернизируем конфигурацию fluent-bit таким образом, чтобы данные логи отображались в ElasticSearch
Добавим в наш fluent-bit.audit.values.yaml:
[INPUT]
Name Tail
Path /var/log/syslog
Path_Key log_file
DB /run/fluent-bit-messages.state
Parser syslog-rfc3164
[INPUT]
Name Tail
Path /var/log/kern.log
Path_Key log_file
DB /run/fluent-bit-kern.state
Parser syslog-rfc3164
[INPUT]
Name Tail
Path /var/log/auth.log
Path_Key log_file
DB /run/fluent-bit-auth.state
Parser syslog-rfc3164
[INPUT]
Name Tail
Path /var/log/docker.log
Path_Key log_file
DB /run/fluent-bit-docker.state
Parser docker-daemon
Применим и сможем наблюдать в Kibana логи с хостов (к примеру syslog)
_source:
log_file:/var/log/syslog @timestamp:Jun 8, 2020 @ 15:13:32.648 log:Jun 8 13:13:32 master2 kubelet[1111]: E0608 13:13:32.646400 1111 file.go:187] Can't process manifest file "/etc/kubernetes/manifests/kube-apiserver.yaml": invalid pod: [spec.containers[0].volumeMounts[5].mountPath: Invalid value: "/usr/share/ca-certificates": must be unique] _id:8doQlHIBJSoEJCXikBpI _type:flb_type _index:kubernetes_cluster-2020.06.08 _score: -
Развертывать prometheus-operator будет через Helm3:
kubectl create ns monitoring
helm upgrade --install prometheus-operator stable/prometheus-operator -n monitoring
Проверяем:
kubectl get pods -n monitoring
NAME READY STATUS RESTARTS AGE
alertmanager-prometheus-operator-alertmanager-0 2/2 Running 0 82s
prometheus-operator-grafana-64bcbf975f-7dx9k 2/2 Running 0 88s
prometheus-operator-kube-state-metrics-5fdcd78bc-gnqv4 1/1 Running 0 88s
prometheus-operator-operator-778d5d7b98-6c6pj 2/2 Running 0 88s
prometheus-operator-prometheus-node-exporter-9grbk 1/1 Running 0 88s
prometheus-prometheus-operator-prometheus-0 3/3 Running 1 72s
Подготовим манифесты для:
- Deployment с nginx и sidecar nginx-exporter контейнерами со следующими характеристиками
- nginx работающий на 80 порту с поддержкой nginx-статуса доступного на порту 8080 по адресу http://127.0.0.1:8080/basic_status
- nginx-exporter отдающий метрики в формате prometheus на порту 9113
- три реплики
- ConfigMap с конфигурацией nginx
- Service для доступа к нашим pods по необходимым портам
- CR ServiceMonitor для мониторинга наших сервисов
nginx-configMap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: nginx-config
data:
nginx.conf: |
user nginx;
worker_processes 1;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
sendfile on;
keepalive_timeout 65;
server {
listen 80;
server_name localhost;
location / {
root /usr/share/nginx/html;
index index.html index.htm;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}
server {
listen 8080;
server_name localhost;
location = /basic_status {
stub_status;
allow 127.0.0.1;
deny all;
}
}
}
nginx-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx
labels:
app: nginx
spec:
replicas: 3
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.19.0-alpine
ports:
- containerPort: 80
volumeMounts:
- name: nginx-config
mountPath: /etc/nginx/nginx.conf
subPath: nginx.conf
- name: nginx-exporter
image: nginx/nginx-prometheus-exporter:0.7.0
env:
- name: SCRAPE_URI
value: "http://127.0.0.1:8080/basic_status"
- name: NGINX_RETRIES
value: "10"
ports:
- containerPort: 9113
volumes:
- name: nginx-config
configMap:
name: nginx-config
nginx-nginx-service.yaml
apiVersion: v1
kind: Service
metadata:
name: nginx
labels:
app: nginx
spec:
selector:
app: nginx
type: ClusterIP
ports:
- name: http
port: 80
protocol: TCP
targetPort: 80
- name: nginx-exporter
port: 9113
protocol: TCP
targetPort: 9113
nginx-serviceMonitor.yaml
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: nginx-sm
namespace: default
labels:
release: prometheus-operator
spec:
namespaceSelector:
matchNames:
- default
selector:
matchLabels:
app: nginx
endpoints:
- port: nginx-exporter
Развернем:
kubectl apply -f nginx-configMap.yaml
kubectl apply -f nginx-deployment.yaml
kubectl apply -f nginx-nginx-service.yaml
kubectl apply -f nginx-serviceMonitor.yaml
И проверим работу nginx-exporter:
kubectl get pods
NAME READY STATUS RESTARTS AGE
nginx-869f7cf565-hk2xd 2/2 Running 0 11h
nginx-869f7cf565-kjdk8 2/2 Running 0 11h
nginx-869f7cf565-qd7wx 2/2 Running 0 11h
kubectl port-forward service/nginx 9113:9113
curl http://127.0.0.1:9113/metrics
# HELP nginx_connections_accepted Accepted client connections
# TYPE nginx_connections_accepted counter
nginx_connections_accepted 1024
# HELP nginx_connections_active Active client connections
# TYPE nginx_connections_active gauge
nginx_connections_active 1
# HELP nginx_connections_handled Handled client connections
# TYPE nginx_connections_handled counter
nginx_connections_handled 1024
# HELP nginx_connections_reading Connections where NGINX is reading the request header
# TYPE nginx_connections_reading gauge
nginx_connections_reading 0
# HELP nginx_connections_waiting Idle client connections
# TYPE nginx_connections_waiting gauge
nginx_connections_waiting 0
# HELP nginx_connections_writing Connections where NGINX is writing the response back to the client
# TYPE nginx_connections_writing gauge
nginx_connections_writing 1
# HELP nginx_http_requests_total Total http requests
# TYPE nginx_http_requests_total counter
nginx_http_requests_total 3355
# HELP nginx_up Status of the last metric scrape
# TYPE nginx_up gauge
nginx_up 1
# HELP nginxexporter_build_info Exporter build information
# TYPE nginxexporter_build_info gauge
nginxexporter_build_info{gitCommit="a2910f1",version="0.7.0"} 1
Выполним:
kubectl port-forward service/prometheus-operator-prometheus -n monitoring 9090:9090
Далее заходим по адресу http://127.0.0.1:9090/targets и наблюдаем наши endpoints в default/nginx-sm в количестве трех реплик.
Выполним:
kubectl port-forward service/prometheus-operator-grafana -n monitoring 8000:80
Далее зайдем в Grafana по адресу http://127.0.0.1:8000/ используя логин/пароль admin/prom-operator и импортируем dashboard.
Теперь мы можем наблюдать статусы наших nginx и необходимые нам метрики:
❗ minikube addons disable default-storageclass
❗ pip install kopf jinja2 kubernetes
- В ходе работы мы:
- Напишем CustomResource и CustomResourceDefinition для mysql оператора
- 🐍 Напишем часть логики mysql оператора при помощи python KOPF
- Сделаем соберем образ и сделаем деплой оператора.
Для создания pod с MySQL оператору понадобится знать:
- Какой образ с MySQL использовать
- Какую db создать
- Какой пароль задать для доступа к MySQL
То есть мы бы хотели, чтобы описание MySQL выглядело примерно так:
apiVersion: otus.homework/v1
kind: MySQL
metadata:
name: mysql-instance
spec:
image: mysql:5.7
database: otus-database
password: otuspassword # Так делать не нужно, следует использовать secret
storage_size: 1Gi
Cоздадим CustomResource deploy/cr.yml со следующим содержимым:
apiVersion: otus.homework/v1
kind: MySQL
metadata:
name: mysql-instance
spec:
image: mysql:5.7
database: otus-database
password: otuspassword # Так делать не нужно, следует использовать secret
storage_size: 1Gi
usless_data: "useless info"
Пробуем применить его:
kubectl apply -f deploy/cr.yml
error: unable to recognize "deploy/cr.yml": no matches for kind "MySQL" in version "otus.homework/v1"
Ошибка связана с отсутсвием объектов типа MySQL в API kubernetes. Исправим это недоразумение.
CustomResourceDefinition - это ресурс для определения других ресурсов (далее CRD)
Создадим CRD deploy/crd.yml:
apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
metadata:
name: mysqls.otus.homework # name состоит из <plural>.<group>
spec:
scope: Namespaced # Длибо Namespaced, либо Cluster
group: otus.homework # REST API: /apis/<group>/<version>
versions: # Список версий
- name: v1
served: true # каждую версию можно включать и выключать
storage: true # но только одну можно хранить
names: # различные форматы имени объекта CR
kind: MySQL # CamelCased в единственном числе
plural: mysqls # URL: /apis/<group>/<version>/<plural>
singular: mysql # для командной строки
shortNames:
- ms # сокращенная версия для командной строки
preserveUnknownFields: false
Создадим CRD:
kubectl apply -f deploy/crd.yml
customresourcedefinition.apiextensions.k8s.io/mysqls.otus.homework created
Cоздаем CR:
kubectl apply -f deploy/cr.yml
mysql.otus.homework/mysql-instance created
C созданными объектами можно взаимодействовать через kubectl:
kubectl get crd
NAME CREATED AT
mysqls.otus.homework 2020-06-01T16:16:05Z
kubectl get mysqls.otus.homework
NAME AGE
mysql-instance 44s
kubectl describe mysqls.otus.homework mysql-instance
Name: mysql-instance
Namespace: default
Labels: <none>
Annotations: kubectl.kubernetes.io/last-applied-configuration:
{"apiVersion":"otus.homework/v1","kind":"MySQL","metadata":{"annotations":{},"name":"mysql-instance","namespace":"default"},"spec":{"datab...
API Version: otus.homework/v1
Kind: MySQL
Metadata:
Creation Timestamp: 2020-06-01T16:16:20Z
Generation: 1
Managed Fields:
API Version: otus.homework/v1
Fields Type: FieldsV1
fieldsV1:
f:metadata:
f:annotations:
.:
f:kubectl.kubernetes.io/last-applied-configuration:
f:spec:
.:
f:database:
f:image:
f:password:
f:storage_size:
f:usless_data:
Manager: kubectl
Operation: Update
Time: 2020-06-01T16:16:20Z
Resource Version: 539535
Self Link: /apis/otus.homework/v1/namespaces/default/mysqls/mysql-instance
UID: 564e0916-7192-4e4c-8a98-755387dfe410
Spec:
Database: otus-database
Image: mysql:5.7
Password: otuspassword
storage_size: 1Gi
usless_data: useless info
Events: <none>
На данный момент мы никак не описали схему нашего CustomResource. Объекты типа mysql могут иметь абсолютно произвольные поля, нам бы хотелось этого избежать, для этого будем использовать validation. Для начала удалим CR mysql-instance:
kubectl delete mysqls.otus.homework mysql-instance
mysql.otus.homework "mysql-instance" deleted
Добавим в спецификацию CRD ( spec ) параметры validation:
validation:
openAPIV3Schema:
type: object
properties:
apiVersion:
type: string # Тип данных поля ApiVersion
kind:
type: string # Тип данных поля kind
metadata:
type: object # Тип поля metadata
properties: # Доступные параметры и их тип данных поля metadata (словарь)
name:
type: string
spec:
type: object
properties:
image:
type: string
database:
type: string
password:
type: string
storage_size:
type: string
Пробуем применить CRD и CR:
kubectl apply -f deploy/crd.yml
customresourcedefinition.apiextensions.k8s.io/mysqls.otus.homework configured
kubectl apply -f deploy/cr.yml
error: error validating "deploy/cr.yml": error validating data: ValidationError(MySQL): unknown field "usless_data" in homework.otus.v1.MySQL; if you choose to ignore these errors, turn validation off with --validate=false
Убираем из cr.yml: usless_data: "useless info"
Применяем:
kubectl apply -f deploy/cr.yml
mysql.otus.homework/mysql-instance created
Ошибки больше нет
Если сейчас из описания mysql убрать строчку из спецификации, то манифест будет принят API сервером. Для того, чтобы этого избежать, добавим описание обязательный полей в CustomResourceDefinition:
required: ["spec"]
required: ["image", "database", "password", "storage_size"]
kubectl apply -f deploy/crd.yml
customresourcedefinition.apiextensions.k8s.io/mysqls.otus.homework configured
kubectl apply -f deploy/cr.yml
mysql.otus.homework/mysql-instance created
Если удалить из cr.yml поле storage_size, то получим следующую ошибку:
kubectl apply -f deploy/cr.yml
error: error validating "deploy/cr.yml": error validating data: ValidationError(MySQL.spec): missing required field "storage_size" in homework.otus.v1.MySQL.spec; if you choose to ignore these errors, turn validation off with --validate=false
- Оператор включает в себя CustomResourceDefinition и сustom сontroller
- CRD содержит описание объектов CR
- Контроллер следит за объектами определенного типа, и осуществляет всю логику работы оператора
- CRD мы уже создали далее будем писать свой контроллер (все задания по написанию контроллера дополнительными)
- Далее развернем custom controller
Используемый/написанный нами контроллер будет обрабатывать два типа событий:
- При создании объекта типа ( kind: mySQL ), он будет:
- Cоздавать PersistentVolume, PersistentVolumeClaim, Deployment, Service для mysql
- Создавать PersistentVolume, PersistentVolumeClaim для бэкапов базы данных, если их еще нет.
- Пытаться восстановиться из бэкапа
- При удалении объекта типа ( kind: mySQL ), он будет:
- Удалять все успешно завершенные backup-job и restore-job
- Удалять PersistentVolume, PersistentVolumeClaim, Deployment, Service для mysql
Создадим файл mysqloperator.py. Для написания контроллера будем использовать kopf.
# Добавим импорт необходимых библиотек:
import kopf
import yaml
import kubernetes
import time
from jinja2 import Environment, FileSystemLoader
В дирректории kubernetes-operators/build/templates создадим шаблоны:
- mysql-deployment.yml.j2
- mysql-service.yml.j2
- mysql-pv.yml.j2
- mysql-pvc.yml.j2
- backup-pv.yml.j2
- backup-pvc.yml.j2
- backup-job.yml.j2
- restore-job.yml.j2
#Добавим функцию, для обработки Jinja шаблонов и преобразования YAML в JSON:
def render_template(filename, vars_dict):
env = Environment(loader=FileSystemLoader('./templates'))
template = env.get_template(filename)
yaml_manifest = template.render(vars_dict)
json_manifest = yaml.load(yaml_manifest)
return json_manifest
Ниже добавим декоратор:
Функция mysql_on_create будет запускаться при создании объектов типа MySQL.
@kopf.on.create('otus.homework', 'v1', 'mysqls')
# Функция, которая будет запускаться при создании объектов тип MySQL:
def mysql_on_create(body, spec, **kwargs):
name = body['metadata']['name']
image = body['spec']['image']
password = body['spec']['password']
database = body['spec']['database']
storage_size = body['spec']['storage_size']
Добавим в декоратор рендер шаблонов:
# Генерируем JSON манифесты для деплоя
persistent_volume = render_template('mysql-pv.yml.j2',
{'name': name,
'storage_size': storage_size})
persistent_volume_claim = render_template('mysql-pvc.yml.j2',
{'name': name,
'storage_size': storage_size})
service = render_template('mysql-service.yml.j2', {'name': name})
deployment = render_template('mysql-deployment.yml.j2', {
'name': name,
'image': image,
'password': password,
'database': database})
Для создания объектов пользуемся библиотекой kubernetes:
api = kubernetes.client.CoreV1Api()
# Создаем mysql PV:
api.create_persistent_volume(persistent_volume)
# Создаем mysql PVC:
api.create_namespaced_persistent_volume_claim('default', persistent_volume_claim)
# Создаем mysql SVC:
api.create_namespaced_service('default', service)
# Создаем mysql Deployment:
api = kubernetes.client.AppsV1Api()
api.create_namespaced_deployment('default', deployment)
Сейчас должно получиться, что-то похожее на:
import kopf
import yaml
import kubernetes
import time
from jinja2 import Environment, FileSystemLoader
def render_template(filename, vars_dict):
env = Environment(loader=FileSystemLoader('./templates'))
template = env.get_template(filename)
yaml_manifest = template.render(vars_dict)
json_manifest = yaml.load(yaml_manifest)
return json_manifest
@kopf.on.create('otus.homework', 'v1', 'mysqls')
# Функция, которая будет запускаться при создании объектов тип MySQL:
def mysql_on_create(body, spec, **kwargs):
name = body['metadata']['name']
image = body['spec']['image']
password = body['spec']['password']
database = body['spec']['database']
storage_size = body['spec']['storage_size']
# Генерируем JSON манифесты для деплоя
persistent_volume = render_template('mysql-pv.yml.j2',
{'name': name,
'storage_size': storage_size})
persistent_volume_claim = render_template('mysql-pvc.yml.j2',
{'name': name,
'storage_size': storage_size})
service = render_template('mysql-service.yml.j2', {'name': name})
deployment = render_template('mysql-deployment.yml.j2', {
'name': name,
'image': image,
'password': password,
'database': database})
api = kubernetes.client.CoreV1Api()
# Создаем mysql PV:
api.create_persistent_volume(persistent_volume)
# Создаем mysql PVC:
api.create_namespaced_persistent_volume_claim('default', persistent_volume_claim)
# Создаем mysql SVC:
api.create_namespaced_service('default', service)
# Создаем mysql Deployment:
api = kubernetes.client.AppsV1Api()
api.create_namespaced_deployment('default', deployment)
С такой конфигурацие уже должны обрабатываться события при создании cr.yml, проверим, для этого из папки build.
Если cr.yml был до этого применен, то увидим:
kopf run mysql-operator.py
[2020-06-02 00:04:35,625] kopf.objects [INFO ] [default/mysql-instance] Handler 'mysql_on_create' succeeded.
[2020-06-02 00:04:35,625] kopf.objects [INFO ] [default/mysql-instance] All handlers succeeded for creation.
Объект создался до того, как запустили контролле потому что реализован level triggered механизм - опрос изменений во времени.
Если сделать kubectl delete mysqls.otus.homework mysqlinstance , то CustomResource будет удален, но наш контроллер ничего не сделает т. к обработки событий на удаление у нас нет.
Удалим все ресурсы, созданные контроллером:
kubectl delete mysqls.otus.homework mysql-instance
kubectl delete deployments.apps mysql-instance
kubectl delete pvc mysql-instance-pvc
kubectl delete pv mysql-instance-pv
kubectl delete svc mysql-instance
mysql.otus.homework "mysql-instance" deleted
deployment.apps "mysql-instance" deleted
persistentvolumeclaim "mysql-instance-pvc" deleted
persistentvolume "mysql-instance-pv" deleted
service "mysql-instance" deleted
Для того, чтобы обработать событие удаления ресурса используется другой декоратор, в нем можно описать удаление ресурсов, аналогично тому, как мы их создавали, но есть более удобный метод.
Для удаления ресурсов, сделаем deployment,svc,pv,pvc дочерними ресурсами к mysql, для этого в тело функции mysql_on_create , после генерации json манифестов добавим:
# Определяем, что созданные ресурсы являются дочерними к управляемому CustomResource:
kopf.append_owner_reference(persistent_volume, owner=body)
kopf.append_owner_reference(persistent_volume_claim, owner=body) # addopt
kopf.append_owner_reference(service, owner=body)
kopf.append_owner_reference(deployment, owner=body)
# ^ Таким образом при удалении CR удалятся все, связанные с ним pv,pvc,svc, deployments
В конец файла добавим обработку события удаления ресурса mysql:
# Добавим обработку события удаления ресурса mysql:
@kopf.on.delete('otus.homework', 'v1', 'mysqls')
def delete_object_make_backup(body, **kwargs):
return {'message': "mysql and its children resources deleted"}
Перезапустим контроллер, создадим и удалим mysql-instance, проверим, что все pv, pvc, svc и deployments удалились.
kubectl apply -f cr.yml
mysql.otus.homework/mysql-instance created
kopf run mysql-operator.py
[2020-06-02 00:20:00,980] kopf.objects [INFO ] [default/mysql-instance] Handler 'mysql_on_create' succeeded.
[2020-06-02 00:20:00,981] kopf.objects [INFO ] [default/mysql-instance] All handlers succeeded for creation.
kubectl delete -f cr.yml
mysql.otus.homework "mysql-instance" deleted
kopf run mysql-operator.py
[2020-06-02 00:21:47,967] kopf.objects [INFO ] [default/mysql-instance] Handler 'delete_object_make_backup' succeeded.
[2020-06-02 00:21:47,968] kopf.objects [INFO ] [default/mysql-instance] All handlers succeeded for deletion.
kubectl get deployments
No resources found in default namespace.
kubectl get pv
No resources found in default namespace.
kubectl get pvc
No resources found in default namespace.
Теперь добавим создание pv, pvc для backup и restore job. Для этого после создания deployment добавим следующий код:
# Cоздаем PVC и PV для бэкапов:
try:
backup_pv = render_template('backup-pv.yml.j2', {'name': name})
api = kubernetes.client.CoreV1Api()
print(api.create_persistent_volume(backup_pv))
api.create_persistent_volume(backup_pv)
except kubernetes.client.rest.ApiException:
pass
try:
backup_pvc = render_template('backup-pvc.yml.j2', {'name': name})
api = kubernetes.client.CoreV1Api()
api.create_namespaced_persistent_volume_claim('default', backup_pvc)
except kubernetes.client.rest.ApiException:
pass
Конструкция try, except - это обработка исключений, в данном случае, нужна, чтобы наш контроллер не пытался бесконечно пересоздать pv и pvc для бэкапов, т к их жизненный цикл отличен от жизненного цикла mysql.
Далее нам необходимо реализовать создание бэкапов и восстановление из них. Для этого будут использоваться Job. Поскольку при запуске Job, повторно ее запустить нельзя, нам нужно реализовать логику удаления успешно законченных jobs c определенным именем.
Для этого выше всех обработчиков событий (под функций render_template) добавим следующую функцию:
def delete_success_jobs(mysql_instance_name):
print("start deletion")
api = kubernetes.client.BatchV1Api()
jobs = api.list_namespaced_job('default')
for job in jobs.items:
jobname = job.metadata.name
if (jobname == f"backup-{mysql_instance_name}-job") or \
(jobname == f"restore-{mysql_instance_name}-job"):
if job.status.succeeded == 1:
api.delete_namespaced_job(jobname,
'default',
propagation_policy='Background')
Также нам понадобится функция, для ожидания пока наша backup job завершится, чтобы дождаться пока backup выполнится перед удалением mysql deployment, svc, pv, pvc.
Опишем ее:
def wait_until_job_end(jobname):
api = kubernetes.client.BatchV1Api()
job_finished = False
jobs = api.list_namespaced_job('default')
while (not job_finished) and \
any(job.metadata.name == jobname for job in jobs.items):
time.sleep(1)
jobs = api.list_namespaced_job('default')
for job in jobs.items:
if job.metadata.name == jobname:
print(f"job with { jobname } found,wait untill end")
if job.status.succeeded == 1:
print(f"job with { jobname } success")
job_finished = True
Добавим запуск backup-job и удаление выполненных jobs в функцию delete_object_make_backup:
name = body['metadata']['name']
image = body['spec']['image']
password = body['spec']['password']
database = body['spec']['database']
delete_success_jobs(name)
# Cоздаем backup job:
api = kubernetes.client.BatchV1Api()
backup_job = render_template('backup-job.yml.j2', {
'name': name,
'image': image,
'password': password,
'database': database})
api.create_namespaced_job('default', backup_job)
wait_until_job_end(f"backup-{name}-job")
Добавим генерацию json из шаблона для restore-job:
restore_job = render_template('restore-job.yml.j2', {
'name': name,
'image': image,
'password': password,
'database': database})
Добавим попытку восстановиться из бэкапов после deployment mysql:
try:
api = kubernetes.client.BatchV1Api()
api.create_namespaced_job('default', restore_job)
except kubernetes.client.rest.ApiException:
pass
Добавим зависимость restore-job от объектов mysql (возле других owner_reference):
kopf.append_owner_reference(restore_job, owner=body)
Теперь должно выглядеть так:
import kopf
import yaml
import kubernetes
import time
from jinja2 import Environment, FileSystemLoader
def wait_until_job_end(jobname):
api = kubernetes.client.BatchV1Api()
job_finished = False
jobs = api.list_namespaced_job('default')
while (not job_finished) and \
any(job.metadata.name == jobname for job in jobs.items):
time.sleep(1)
jobs = api.list_namespaced_job('default')
for job in jobs.items:
if job.metadata.name == jobname:
print(f"job with { jobname } found,wait untill end")
if job.status.succeeded == 1:
print(f"job with { jobname } success")
job_finished = True
def render_template(filename, vars_dict):
env = Environment(loader=FileSystemLoader('./templates'))
template = env.get_template(filename)
yaml_manifest = template.render(vars_dict)
json_manifest = yaml.load(yaml_manifest)
return json_manifest
def delete_success_jobs(mysql_instance_name):
print("start deletion")
api = kubernetes.client.BatchV1Api()
jobs = api.list_namespaced_job('default')
for job in jobs.items:
jobname = job.metadata.name
if (jobname == f"backup-{mysql_instance_name}-job") or \
(jobname == f"restore-{mysql_instance_name}-job"):
if job.status.succeeded == 1:
api.delete_namespaced_job(jobname,
'default',
propagation_policy='Background')
@kopf.on.create('otus.homework', 'v1', 'mysqls')
# Функция, которая будет запускаться при создании объектов тип MySQL:
def mysql_on_create(body, spec, **kwargs):
name = body['metadata']['name']
image = body['spec']['image']
password = body['spec']['password']
database = body['spec']['database']
storage_size = body['spec']['storage_size']
# Генерируем JSON манифесты для деплоя
persistent_volume = render_template('mysql-pv.yml.j2',
{'name': name,
'storage_size': storage_size})
persistent_volume_claim = render_template('mysql-pvc.yml.j2',
{'name': name,
'storage_size': storage_size})
service = render_template('mysql-service.yml.j2', {'name': name})
deployment = render_template('mysql-deployment.yml.j2', {
'name': name,
'image': image,
'password': password,
'database': database})
restore_job = render_template('restore-job.yml.j2', {
'name': name,
'image': image,
'password': password,
'database': database})
# Определяем, что созданные ресурсы являются дочерними к управляемому CustomResource:
kopf.append_owner_reference(persistent_volume, owner=body)
kopf.append_owner_reference(persistent_volume_claim, owner=body) # addopt
kopf.append_owner_reference(service, owner=body)
kopf.append_owner_reference(deployment, owner=body)
kopf.append_owner_reference(restore_job, owner=body)
# ^ Таким образом при удалении CR удалятся все, связанные с ним pv,pvc,svc, deployments
api = kubernetes.client.CoreV1Api()
# Создаем mysql PV:
api.create_persistent_volume(persistent_volume)
# Создаем mysql PVC:
api.create_namespaced_persistent_volume_claim('default', persistent_volume_claim)
# Создаем mysql SVC:
api.create_namespaced_service('default', service)
# Создаем mysql Deployment:
api = kubernetes.client.AppsV1Api()
api.create_namespaced_deployment('default', deployment)
# Пытаемся восстановиться из backup
try:
api = kubernetes.client.BatchV1Api()
api.create_namespaced_job('default', restore_job)
except kubernetes.client.rest.ApiException:
pass
# Cоздаем PVC и PV для бэкапов:
try:
backup_pv = render_template('backup-pv.yml.j2', {'name': name})
api = kubernetes.client.CoreV1Api()
print(api.create_persistent_volume(backup_pv))
api.create_persistent_volume(backup_pv)
except kubernetes.client.rest.ApiException:
pass
try:
backup_pvc = render_template('backup-pvc.yml.j2', {'name': name})
api = kubernetes.client.CoreV1Api()
api.create_namespaced_persistent_volume_claim('default', backup_pvc)
except kubernetes.client.rest.ApiException:
pass
@kopf.on.delete('otus.homework', 'v1', 'mysqls')
def delete_object_make_backup(body, **kwargs):
name = body['metadata']['name']
image = body['spec']['image']
password = body['spec']['password']
database = body['spec']['database']
delete_success_jobs(name)
# Cоздаем backup job:
api = kubernetes.client.BatchV1Api()
backup_job = render_template('backup-job.yml.j2', {
'name': name,
'image': image,
'password': password,
'database': database})
api.create_namespaced_job('default', backup_job)
wait_until_job_end(f"backup-{name}-job")
return {'message': "mysql and its children resources deleted"}
Вот и готово. Запускаем оператор (из директории build):
kopf run mysql-operator.py
Создаем CR:
kubectl apply -f deploy/cr.yml
kopf run mysql-operator.py
json_manifest = yaml.load(yaml_manifest)
{'api_version': 'v1',
'kind': 'PersistentVolume',
'metadata': {'annotations': None,
'cluster_name': None,
'creation_timestamp': datetime.datetime(2020, 6, 2, 11, 16, 39, tzinfo=tzutc()),
'deletion_grace_period_seconds': None,
'deletion_timestamp': None,
'finalizers': ['kubernetes.io/pv-protection'],
'generate_name': None,
'generation': None,
'initializers': None,
'labels': {'pv-usage': 'backup-mysql-instance'},
'managed_fields': [{'api_version': 'v1',
'fields': None,
'manager': 'OpenAPI-Generator',
'operation': 'Update',
'time': datetime.datetime(2020, 6, 2, 11, 16, 39, tzinfo=tzutc())}],
'name': 'backup-mysql-instance-pv',
'namespace': None,
'owner_references': None,
'resource_version': '4593',
'self_link': '/api/v1/persistentvolumes/backup-mysql-instance-pv',
'uid': '42e3cb0b-4b5f-463d-9171-3c618b059505'},
'spec': {'access_modes': ['ReadWriteOnce'],
'aws_elastic_block_store': None,
'azure_disk': None,
'azure_file': None,
'capacity': {'storage': '1Gi'},
'cephfs': None,
'cinder': None,
'claim_ref': None,
'csi': None,
'fc': None,
'flex_volume': None,
'flocker': None,
'gce_persistent_disk': None,
'glusterfs': None,
'host_path': {'path': '/data/pv-backup/', 'type': ''},
'iscsi': None,
'local': None,
'mount_options': None,
'nfs': None,
'node_affinity': None,
'persistent_volume_reclaim_policy': 'Retain',
'photon_persistent_disk': None,
'portworx_volume': None,
'quobyte': None,
'rbd': None,
'scale_io': None,
'storage_class_name': None,
'storageos': None,
'volume_mode': 'Filesystem',
'vsphere_volume': None},
'status': {'message': None, 'phase': 'Pending', 'reason': None}}
[2020-06-02 14:16:39,502] kopf.objects [INFO ] [default/mysql-instance] Handler 'mysql_on_create' succeeded.
[2020-06-02 14:16:39,502] kopf.objects [INFO ] [default/mysql-instance] All handlers succeeded for creation.```
Проверяем что появились pvc:
kubectl get pvc
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
backup-mysql-instance-pvc Bound backup-mysql-instance-pv 1Gi RWO 5s
mysql-instance-pvc Bound mysql-instance-pv 1Gi RWO 5s
kubectl get pv
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE
backup-mysql-instance-pv 1Gi RWO Retain Bound default/backup-mysql-instance-pvc 7s
mysql-instance-pv 1Gi RWO Retain Bound default/mysql-instance-pvc 7s
Проверим, что все работает, для этого заполним базу созданного mysqlinstance:
export MYSQLPOD=$(kubectl get pods -l app=mysql-instance -o jsonpath="{.items[*].metadata.name}")
kubectl exec -it $MYSQLPOD -- mysql -u root -potuspassword -e "CREATE TABLE test (id smallint unsigned not null auto_increment, name varchar(20) not null, constraint pk_example primary key (id) );" otus-database
mysql: [Warning] Using a password on the command line interface can be insecure.
kubectl exec -it $MYSQLPOD -- mysql -potuspassword -e "INSERT INTO test ( id, name) VALUES ( null, 'some data' );" otus-database
mysql: [Warning] Using a password on the command line interface can be insecure.
kubectl exec -it $MYSQLPOD -- mysql -potuspassword -e "INSERT INTO test ( id, name) VALUES ( null, 'some data-2' );" otus-database
kubectl exec -it $MYSQLPOD -- mysql -potuspassword -e "INSERT INTO test ( id, name) VALUES ( null, 'some data-2' );" otus-database
mysql: [Warning] Using a password on the command line interface can be insecure.
kubectl exec -it $MYSQLPOD -- mysql -potuspassword -e "select * from test;" otus-database
mysql: [Warning] Using a password on the command line interface can be insecure.
+----+-------------+
| id | name |
+----+-------------+
| 1 | some data |
| 2 | some data-2 |
+----+-------------+
Удалим mysql-instance:
kubectl delete mysqls.otus.homework mysql-instance
mysql.otus.homework "mysql-instance" deleted
Теперь kubectl get pv показывает, что PV для mysql больше нет, а kubectl get jobs.batch показывает:
kubectl get pv
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE
backup-mysql-instance-pv 1Gi RWO Retain Bound default/backup-mysql-instance-pvc 5m49s
mysql-instance-pv 1Gi RWO Retain Terminating default/mysql-instance-pvc 5m49s
kubectl get jobs.batch
NAME COMPLETIONS DURATION AGE
backup-mysql-instance-job 1/1 1s 36s
Если Job не выполнилась или выполнилась с ошибкой, то ее нужно удалять в ручную, т к иногда полезно посмотреть логи
Создадим заново mysql-instance:
kubectl apply -f cr.yml
mysql.otus.homework/mysql-instance created
Немного подождем и:
export MYSQLPOD=$(kubectl get pods -l app=mysql-instance -o jsonpath="{.items[*].metadata.name}")
kubectl exec -it $MYSQLPOD -- mysql -potuspassword -e "select * from test;" otus-database
mysql: [Warning] Using a password on the command line interface can be insecure.
+----+-------------+
| id | name |
+----+-------------+
| 1 | some data |
| 2 | some data-2 |
+----+-------------+
Мы убедились, что наш контроллер работает, теперь нужно его остановить и собрать Docker образ с ним. В директории build создадим Dockerfile:
FROM python:3.7
COPY templates ./templates
COPY mysql-operator.py ./mysql-operator.py
RUN pip install kopf kubernetes pyyaml jinja2
CMD kopf run /mysql-operator.py
Соберем и сделаем push в dockerhub наш образ с оператором:
docker build -t kovtalex/mysql-operator:0.1 .
docker push kovtalex/mysql-operator:0.1
Создадим и применим манифесты в папке kubernetes-operator/deploy:
- service-account.yml
- role.yml
- role-binding.yml
- deploy-operator.yml
kubectl apply -f role.yml -f service-account.yml -f role-binding.yml -f deploy-operator.yml
clusterrole.rbac.authorization.k8s.io/mysql-operator created
serviceaccount/mysql-operator created
clusterrolebinding.rbac.authorization.k8s.io/workshop-operator created
deployment.apps/mysql-operator created
Создаем CR (если еще не создан):
kubectl apply -f deploy/cr.yml
Проверяем что появились pvc:
kubectl get pvc
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
backup-mysql-instance-pvc Bound backup-mysql-instance-pv 1Gi RWO 19m
mysql-instance-pvc Bound mysql-instance-pv 1Gi RWO 8m14s
Заполним базу созданного mysql-instance:
export MYSQLPOD=$(kubectl get pods -l app=mysql-instance -o jsonpath="{.items[*].metadata.name}")
kubectl exec -it $MYSQLPOD -- mysql -potuspassword -e "select * from test;" otus-database
mysql: [Warning] Using a password on the command line interface can be insecure.
+----+-------------+
| id | name |
+----+-------------+
| 1 | some data |
| 2 | some data-2 |
+----+-------------+
Удалим mysql-instance:
kubectl delete mysqls.otus.homework mysql-instance
mysql.otus.homework "mysql-instance" deleted
Теперь kubectl get pv показывает, что PV для mysql больше нет, а kubectl get jobs.batch показывает:
kubectl get pv
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE
backup-mysql-instance-pv 1Gi RWO Retain Bound default/backup-mysql-instance-pvc 22m
kubectl get jobs.batch
NAME COMPLETIONS DURATION AGE
backup-mysql-instance-job 1/1 1s 28s
Если Job не выполнилась или выполнилась с ошибкой, то ее нужно удалять в ручную, т к иногда полезно посмотреть логи
Создадим заново mysql-instance:
kubectl apply -f deploy/cr.yml
Немного подождем и:
export MYSQLPOD=$(kubectl get pods -l app=mysql-instance -o jsonpath="{.items[*].metadata.name}")
kubectl exec -it $MYSQLPOD -- mysql -potuspassword -e "select * from test;" otus-database
mysql: [Warning] Using a password on the command line interface can be insecure.
+----+-------------+
| id | name |
+----+-------------+
| 1 | some data |
| 2 | some data-2 |
+----+-------------+
Содержимое папки kubernetes-operators:
tree kubernetes-operators
kubernetes-operators
├── build
│ ├── Dockerfile
│ ├── mysql-operator.py
│ └── templates
│ ├── backup-job.yml.j2
│ ├── backup-pv.yml.j2
│ ├── backup-pvc.yml.j2
│ ├── mysql-deployment.yml.j2
│ ├── mysql-pv.yml.j2
│ ├── mysql-pvc.yml.j2
│ ├── mysql-service.yml.j2
│ └── restore-job.yml.j2
└── deploy
├── cr.yml
├── crd.yml
├── deploy-operator.yml
├── role-binding.yml
├── role.yml
└── service-account.yml
Результаты:
kubectl get jobs
NAME COMPLETIONS DURATION AGE
backup-mysql-instance-job 1/1 1s 4m53s
restore-mysql-instance-job 1/1 38s 3m58s
export MYSQLPOD=$(kubectl get pods -l app=mysql-instance -o jsonpath="{.items[*].metadata.name}")
kubectl exec -it $MYSQLPOD -- mysql -potuspassword -e "select * from test;" otus-database
mysql: [Warning] Using a password on the command line interface can be insecure.
+----+-------------+
| id | name |
+----+-------------+
| 1 | some data |
| 2 | some data-2 |
+----+-------------+
Домашнее задание выполняем в GKE кластере.
Для доступа к Google Cloud Platform нужно активировать триальный аккаунт в GCP.
gcloud container clusters get-credentials cluster-1 --zone europe-west1-b --project angular-pursuit-275120
etching cluster endpoint and auth data.
kubeconfig entry generated for cluster1.
Попробуем установить Helm charts созданные сообществом. С их помощью создадим и настроим инфраструктурные сервисы, необходимые для работы нашего кластера.
Для установки будем использовать Helm 3.
Сегодня будем работать со следующими сервисами:
- nginx-ingress - сервис, обеспечивающий доступ к публичным ресурсам кластера
- cert-manager - сервис, позволяющий динамически генерировать Let's Encrypt сертификаты для ingress ресурсов
- chartmuseum - специализированный репозиторий для хранения helm charts
- harbor - хранилище артефактов общего назначения (Docker Registry), поддерживающее helm charts
Для начала нам необходимо установить Helm 3 на локальную машину.
Инструкции по установке можно найти по ссылке.
brew install helm
Критерий успешности установки - после выполнения команды вывод:
helm version
version.BuildInfo{Version:"v3.2.1", GitCommit:"fe51cd1e31e6a202cba7dead9552a6d418ded79a", GitTreeState:"clean", GoVersion:"go1.13.10"}
Создание release:
helm install <chart_name> --name=<release_name> --namespace=<namespace>
kubectl get secrets -n <namespace> | grep <release_name>
Обновление release:
helm upgrade <release_name> <chart_name> --namespace=<namespace>
kubectl get secrets -n <namespace> | grep <release_name>
Создание или обновление release:
helm upgrade --install <release_name> <chart_name> --namespace=<namespace>
kubectl get secrets -n <namespace> | grep <release_name>
Добавим репозиторий stable
По умолчанию в Helm 3 не установлен репозиторий stable
helm repo add stable https://kubernetes-charts.storage.googleapis.com
"stable" has been added to your repositories
helm repo list
NAME URL
stable https://kubernetes-charts.storage.googleapis.com
Создадим namespace и release nginx-ingress
kubectl create ns nginx-ingress
namespace/nginx-ingress created
helm upgrade --install nginx-ingress stable/nginx-ingress --wait \
--namespace=nginx-ingress \
--version=1.39.0
Release "nginx-ingress" does not exist. Installing it now.
NAME: nginx-ingress
LAST DEPLOYED: Wed May 27 22:15:54 2020
NAMESPACE: nginx-ingress
STATUS: deployed
REVISION: 1
TEST SUITE: None
NOTES:
The nginx-ingress controller has been installed.
It may take a few minutes for the LoadBalancer IP to be available.
You can watch the status by running 'kubectl --namespace nginx-ingress get services -o wide -w nginx-ingress-controller'
An example Ingress that makes use of the controller:
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
annotations:
kubernetes.io/ingress.class: nginx
name: example
namespace: foo
spec:
rules:
- host: www.example.com
http:
paths:
- backend:
serviceName: exampleService
servicePort: 80
path: /
# This section is only required if TLS is to be enabled for the Ingress
tls:
- hosts:
- www.example.com
secretName: example-tls
If TLS is enabled for the Ingress, a Secret containing the certificate and key must also be provided:
apiVersion: v1
kind: Secret
metadata:
name: example-tls
namespace: foo
data:
tls.crt: <base64 encoded cert>
tls.key: <base64 encod
Разберем используемые ключи:
- --wait - ожидать успешного окончания установки (подробности)
- --timeout - считать установку неуспешной по истечении указанного времени
- --namespace - установить chart в определенный namespace (если не существует, необходимо создать)
- --version - установить определенную версию chart
Добавим репозиторий, в котором хранится актуальный helm chart cert-manager:
helm repo add jetstack https://charts.jetstack.io
"jetstack" has been added to your repositories
helm repo update
Hang tight while we grab the latest from your chart repositories...
...Successfully got an update from the "jetstack" chart repository
...Successfully got an update from the "stable" chart repository
Update Complete. ⎈ Happy Helming!⎈
Создадим namespace
kubectl create namespace cert-manager
namespace/cert-manager created
Также для установки cert-manager предварительно потребуется создать в кластере некоторые CRD:
kubectl apply --validate=false -f https://github.com/jetstack/cert-manager/releases/download/v0.15.0/cert-manager.crds.yaml
customresourcedefinition.apiextensions.k8s.io/certificaterequests.cert-manager.io created
customresourcedefinition.apiextensions.k8s.io/certificates.cert-manager.io created
customresourcedefinition.apiextensions.k8s.io/challenges.acme.cert-manager.io created
customresourcedefinition.apiextensions.k8s.io/clusterissuers.cert-manager.io created
customresourcedefinition.apiextensions.k8s.io/issuers.cert-manager.io created
customresourcedefinition.apiextensions.k8s.io/orders.acme.cert-manager.io created
Установим cert-manager:
helm install cert-manager jetstack/cert-manager --namespace cert-manager --version v0.15.1
NAME: cert-manager
LAST DEPLOYED: Wed May 27 22:19:16 2020
NAMESPACE: cert-manager
STATUS: deployed
REVISION: 1
TEST SUITE: None
NOTES:
cert-manager has been deployed successfully!
In order to begin issuing certificates, you will need to set up a ClusterIssuer
or Issuer resource (for example, by creating a 'letsencrypt-staging' issuer).
More information on the different types of issuers and how to configure them
can be found in our documentation:
https://cert-manager.io/docs/configuration/
For information on how to configure cert-manager to automatically provision
Certificates for Ingress resources, take a look at the `ingress-shim`
documentation:
https://cert-manager.io/docs/usage/ingress/
Проверим, что cert-manager успешно развернут и работает:
kubectl get pods --namespace cert-manager
NAME READY STATUS RESTARTS AGE
cert-manager-766d5c494b-d9mkq 1/1 Running 0 14s
cert-manager-cainjector-6649bbb695-cpk46 1/1 Running 0 14s
cert-manager-webhook-68d464c8b-9mfpv 1/1 Running 0 14s
cat <<EOF > test-resources.yaml
apiVersion: v1
kind: Namespace
metadata:
name: cert-manager-test
---
apiVersion: cert-manager.io/v1alpha2
kind: Issuer
metadata:
name: test-selfsigned
namespace: cert-manager-test
spec:
selfSigned: {}
---
apiVersion: cert-manager.io/v1alpha2
kind: Certificate
metadata:
name: selfsigned-cert
namespace: cert-manager-test
spec:
dnsNames:
- example.com
secretName: selfsigned-cert-tls
issuerRef:
kubectl apply -f test-resources.yaml
namespace/cert-manager-test created
issuer.cert-manager.io/test-selfsigned created
certificate.cert-manager.io/selfsigned-cert created
kubectl describe certificate -n cert-manager-test
Name: selfsigned-cert
Namespace: cert-manager-test
Labels: <none>
Annotations: kubectl.kubernetes.io/last-applied-configuration:
{"apiVersion":"cert-manager.io/v1alpha2","kind":"Certificate","metadata":{"annotations":{},"name":"selfsigned-cert","namespace":"cert-mana...
API Version: cert-manager.io/v1alpha3
Kind: Certificate
Metadata:
Creation Timestamp: 2020-05-27T19:20:40Z
Generation: 1
Resource Version: 3879
Self Link: /apis/cert-manager.io/v1alpha3/namespaces/cert-manager-test/certificates/selfsigned-cert
UID: 2166b2f3-faf7-4fe5-a9e4-410f4540effc
Spec:
Dns Names:
example.com
Issuer Ref:
Name: test-selfsigned
Secret Name: selfsigned-cert-tls
Status:
Conditions:
Last Transition Time: 2020-05-27T19:20:41Z
Message: Certificate is up to date and has not expired
Reason: Ready
Status: True
Type: Ready
Not After: 2020-08-25T19:20:41Z
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal GeneratedKey 11s cert-manager Generated a new private key
Normal Requested 11s cert-manager Created new CertificateRequest resource "selfsigned-cert-504566127"
Normal Issued 11s cert-manager Certificate issued successfully
kubectl delete -f test-resources.yaml
namespace "cert-manager-test" deleted
issuer.cert-manager.io "test-selfsigned" deleted
Для выпуска сертификатов нам потребуются ClusterIssuers. Создадим их для staging и production окружений.
cluster-issuer-prod.yaml:
apiVersion: cert-manager.io/v1alpha2
kind: ClusterIssuer
metadata:
name: letsencrypt-production
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: kovtalex@gmail.com
privateKeySecretRef:
name: letsencrypt-production
solvers:
- http01:
ingress:
class: nginx
cluster-issuer-stage.yaml:
apiVersion: cert-manager.io/v1alpha2
kind: ClusterIssuer
metadata:
name: letsencrypt-staging
spec:
acme:
server: https://acme-staging-v02.api.letsencrypt.org/directory
email: kovtalex@gmail.com
privateKeySecretRef:
name: letsencrypt-staging
solvers:
- http01:
ingress:
class: nginx
Проверим статус:
kubectl describe clusterissuers -n cert-manager
Name: letsencrypt-production
Namespace:
Labels: app=raw
app.kubernetes.io/managed-by=Helm
chart=raw-0.2.3
heritage=Helm
release=cert-manager-issuers
Annotations: meta.helm.sh/release-name: cert-manager-issuers
meta.helm.sh/release-namespace: cert-manager
API Version: cert-manager.io/v1alpha3
Kind: ClusterIssuer
Metadata:
Creation Timestamp: 2020-05-30T17:09:38Z
Generation: 1
Resource Version: 1274031
Self Link: /apis/cert-manager.io/v1alpha3/clusterissuers/letsencrypt-production
UID: 6491edb5-382c-48a9-bc92-a000342dc334
Spec:
Acme:
Email: kovtalex@gmail.com
Private Key Secret Ref:
Name: letsencrypt-production
Server: https://acme-v02.api.letsencrypt.org/directory
Solvers:
http01:
Ingress:
Class: nginx
Status:
Acme:
Last Registered Email: kovtalex@gmail.com
Uri: https://acme-v02.api.letsencrypt.org/acme/acct/87525196
Conditions:
Last Transition Time: 2020-05-30T17:09:47Z
Message: The ACME account was registered with the ACME server
Reason: ACMEAccountRegistered
Status: True
Type: Ready
Events: <none>
Name: letsencrypt-staging
Namespace:
Labels: app=raw
app.kubernetes.io/managed-by=Helm
chart=raw-0.2.3
heritage=Helm
release=cert-manager-issuers
Annotations: meta.helm.sh/release-name: cert-manager-issuers
meta.helm.sh/release-namespace: cert-manager
API Version: cert-manager.io/v1alpha3
Kind: ClusterIssuer
Metadata:
Creation Timestamp: 2020-05-30T17:09:38Z
Generation: 1
Resource Version: 1274029
Self Link: /apis/cert-manager.io/v1alpha3/clusterissuers/letsencrypt-staging
UID: 29366949-a45a-458f-bea1-95d7cd74773a
Spec:
Acme:
Email: kovtalex@gmail.com
Private Key Secret Ref:
Name: letsencrypt-staging
Server: https://acme-staging-v02.api.letsencrypt.org/directory
Solvers:
http01:
Ingress:
Class: nginx
Status:
Acme:
Last Registered Email: kovtalex@gmail.com
Uri: https://acme-staging-v02.api.letsencrypt.org/acme/acct/13939356
Conditions:
Last Transition Time: 2020-05-30T17:09:47Z
Message: The ACME account was registered with the ACME server
Reason: ACMEAccountRegistered
Status: True
Type: Ready
Events: <none>
Кастомизируем установку chartmuseum
- Создадим директорию kubernetes-templating/chartmuseum/ и поместим туда файл values.yaml
- Изучим содержимое оригинальный файла values.yaml
- Включим:
- Создание ingress ресурса с корректным hosts.name (должен использоваться nginx-ingress)
- Автоматическую генерацию Let's Encrypt сертификата
https://github.com/helm/charts/tree/master/stable/chartmuseum
Файл values.yaml для chartmuseum будет выглядеть следующим образом:
ingress:
enabled: true
annotations:
kubernetes.io/ingress.class: "nginx"
cert-manager.io/cluster-issuer: "letsencrypt-production"
hosts:
- name: chartmuseum.35.189.202.237.nip.io
path: /
tls: true
tlsSecret: chartmuseum.35.189.202.237.nip.io
securityContext: {}
env:
open:
DISABLE_API: false
Установим chartmuseum:
kubectl create ns chartmuseum
namespace/chartmuseum created
helm upgrade --install chartmuseum stable/chartmuseum --wait --version=2.13.0 --namespace=chartmuseum -f kubernetes-templating/chartmuseum/values.yaml
Release "chartmuseum" has been upgraded. Happy Helming!
NAME: chartmuseum
LAST DEPLOYED: Thu May 28 14:24:55 2020
NAMESPACE: chartmuseum
STATUS: deployed
REVISION: 3
TEST SUITE: None
NOTES:
** Please be patient while the chart is being deployed **
Get the ChartMuseum URL by running:
export POD_NAME=$(kubectl get pods --namespace chartmuseum -l "app=chartmuseum" -l "release=chartmuseum" -o jsonpath="{.items[0].metadata.name}")
echo http://127.0.0.1:8080/
kubectl port-forward $POD_NAME 8080:8080 --namespace chartmuseum
Проверим, что release chartmuseum установился:
helm ls -n chartmuseum
NAME NAMESPACE REVISION UPDATED STATUS CHART APP VERSION
chartmuseum chartmuseum 1 2020-05-27 22:23:39.234698 +0300 MSK deployed chartmuseum-2.13.0 0.12.0
- helm 2 хранил информацию о релизе в configMap'ах (kubectl get configmaps -n kube-system)
- Helm 3 хранит информацию в secrets (kubectl get secrets - n chartmuseum)
kubectl get secrets -n chartmuseum
NAME TYPE DATA AGE
chartmuseum-chartmuseum Opaque 0 9m17s
chartmuseum.35.189.202.237.nip.io kubernetes.io/tls 3 9m16s
default-token-cxhw9 kubernetes.io/service-account-token 3 9m25s
sh.helm.release.v1.chartmuseum.v1 helm.sh/release.v1 1 9m17s
Проверяем: https://chartmuseum.35.240.96.124.nip.io
Научимся работать с chartmuseum и зальем в наш репозиторий - примеру frontend
- Добавяем наш репозитарий
helm repo add chartmuseum https://chartmuseum.35.189.202.237.nip.io/
"chartmuseum" has been added to your repositories
- Проверяем линтером
helm lint
==> Linting .
[INFO] Chart.yaml: icon is recommended
1 chart(s) linted, 0 chart(s) failed
- Пакуем
helm package .
Successfully packaged chart and saved it to: /Users/alexey/kovtalex_platform/kubernetes-templating/frontend/frontend-0.1.0.tgz
- Заливаем
curl -L --data-binary "@frontend-0.1.0.tgz" https://chartmuseum.35.189.202.237.nip.io/api/charts
{"saved":true}
- Обновляем список repo
helm repo update
Hang tight while we grab the latest from your chart repositories...
...Successfully got an update from the "harbor" chart repository
...Successfully got an update from the "chartmuseum" chart repository
...Successfully got an update from the "templating" chart repository
...Successfully got an update from the "jetstack" chart repository
...Successfully got an update from the "stable" chart repository
Update Complete. ⎈ Happy Helming!⎈
- Ищем наш frontend в репозитории
helm search repo -l chartmuseum/
NAME CHART VERSION APP VERSION DESCRIPTION
chartmuseum/frontend 0.1.0 1.16.0 A Helm chart for Kubernetes
- И выкатываем
helm upgrade --install frontend chartmuseum/frontend --namespace hipster-shop
Release "frontend" does not exist. Installing it now.
NAME: frontend
LAST DEPLOYED: Sat May 30 01:59:17 2020
NAMESPACE: hipster-shop
STATUS: deployed
REVISION: 1
TEST SUITE: None
Установим Harbor
- Пишем values.yaml
expose:
type: ingress
tls:
enables: true
secretName: harbor.35.189.202.237.nip.io
notarySecretName: notary.35.189.202.237.nip.io
ingress:
hosts:
core: harbor.35.189.202.237.nip.io
annotations:
kubernetes.io/ingress.class: "nginx"
cert-manager.io/cluster-issuer: "letsencrypt-production"
notary:
enabled: false
- Добавляем repo
helm repo add harbor https://helm.goharbor.io
"harbor" has been added to your repositories
- Обновляем repo
helm repo update
Hang tight while we grab the latest from your chart repositories...
...Successfully got an update from the "harbor" chart repository
...Successfully got an update from the "jetstack" chart repository
...Successfully got an update from the
- Создаем ns
kubectl create ns harbor
namespace/harbor created
- Выкатывем
helm upgrade --install harbor harbor/harbor --wait --namespace=harbor-system --version=1.3.2 -f kubernetes-templating/harbor/values.yaml
Release "harbor" does not exist. Installing it now.
NAME: harbor
LAST DEPLOYED: Thu May 28 23:35:35 2020
NAMESPACE: harbor-system
STATUS: deployed
REVISION: 1
TEST SUITE: None
NOTES:
Please wait for several minutes for Harbor deployment to complete.
Then you should be able to visit the Harbor portal at https://core.harbor.domain.
For more details, please visit https://github.com/goharbor/harbor.
- Формат описания переменных в файле values.yaml для chartmuseum и harbor отличается
- Helm3 не создает namespace в который будет установлен release
- Проще выключить сервис notary, он нам не понадобится
- Реквизиты по умолчанию - admin/Harbor12345
- nip.io может оказаться забанен в cert-manager. Если у вас есть собственный домен - лучше использовать его, либо попробовать xip.io, либо переключиться на staging ClusterIssuer
- Обратим внимание, как helm3 хранит информацию о release: kubectl get secrets -n harbor -l owner=helm
Проверяем: https://harbor.35.189.202.237.nip.io/
Опишем установку nginx-ingress, cert-manager и harbor в helmfile
- Установим helmfile
brew install helmfile
Для применения манифестов ClusterIssuers воспользуемся incubator/raw - A place for all the Kubernetes resources which don't already have a home.
Пилим helmfile.yaml
repositories:
- name: stable
url: https://kubernetes-charts.storage.googleapis.com
- name: jetstack
url: https://charts.jetstack.io
- name: harbor
url: https://helm.goharbor.io
- name: incubator
url: https://kubernetes-charts-incubator.storage.googleapis.com
helmDefaults:
wait: true
releases:
- name: cert-manager
namespace: cert-manager
chart: jetstack/cert-manager
version: v0.15.1
set:
- name: installCRDs
value: true
- name: cert-manager-issuers
needs:
- cert-manager/cert-manager
namespace: cert-manager
chart: incubator/raw
version: 0.2.3
values:
- ./cert-manager/values.yaml
- name: harbor
needs:
- cert-manager/cert-manager
namespace: harbor
chart: harbor/harbor
version: 1.3.2
values:
- ./harbor/values.yaml
- name: chartmuseum
needs:
- cert-manager/cert-manager
namespace: chartmuseum
chart: stable/chartmuseum
version: 2.13.0
values:
- ./chartmuseum/values.yaml
- Проверим отсутствие ns наших сервисов
kubectl get ns
NAME STATUS AGE
default Active 2d21h
kube-node-lease Active 2d21h
kube-public Active 2d21h
kube-system Active 2d21h
- Линтим
helmfile lint
Fetching incubator/raw
Fetching jetstack/cert-manager
Fetching harbor/harbor
Fetching stable/chartmuseum
Adding repo stable https://kubernetes-charts.storage.googleapis.com
"stable" has been added to your repositories
Adding repo jetstack https://charts.jetstack.io
"jetstack" has been added to your repositories
Adding repo harbor https://helm.goharbor.io
"harbor" has been added to your repositories
Adding repo incubator https://kubernetes-charts-incubator.storage.googleapis.com
"incubator" has been added to your repositories
Updating repo
Hang tight while we grab the latest from your chart repositories...
...Successfully got an update from the "jetstack" chart repository
...Successfully got an update from the "incubator" chart repository
...Successfully got an update from the "harbor" chart repository
...Successfully got an update from the "stable" chart repository
Update Complete. ⎈ Happy Helming!⎈
Building dependency release=cert-manager, chart=/var/folders/kq/byg7brhj5g39gnj_66sp02z00000gn/T/077869428/cert-manager/v0.15.1/jetstack/cert-manager/cert-manager
Building dependency release=cert-manager-issuers, chart=/var/folders/kq/byg7brhj5g39gnj_66sp02z00000gn/T/077869428/cert-manager-issuers/0.2.3/incubator/raw/raw
Building dependency release=harbor, chart=/var/folders/kq/byg7brhj5g39gnj_66sp02z00000gn/T/077869428/harbor/1.3.2/harbor/harbor/harbor
Building dependency release=chartmuseum, chart=/var/folders/kq/byg7brhj5g39gnj_66sp02z00000gn/T/077869428/chartmuseum/2.13.0/stable/chartmuseum/chartmuseum
Linting release=cert-manager, chart=/var/folders/kq/byg7brhj5g39gnj_66sp02z00000gn/T/077869428/cert-manager/v0.15.1/jetstack/cert-manager/cert-manager
==> Linting /var/folders/kq/byg7brhj5g39gnj_66sp02z00000gn/T/077869428/cert-manager/v0.15.1/jetstack/cert-manager/cert-manager
1 chart(s) linted, 0 chart(s) failed
Linting release=cert-manager-issuers, chart=/var/folders/kq/byg7brhj5g39gnj_66sp02z00000gn/T/077869428/cert-manager-issuers/0.2.3/incubator/raw/raw
==> Linting /var/folders/kq/byg7brhj5g39gnj_66sp02z00000gn/T/077869428/cert-manager-issuers/0.2.3/incubator/raw/raw
[INFO] Chart.yaml: icon is recommended
1 chart(s) linted, 0 chart(s) failed
Linting release=harbor, chart=/var/folders/kq/byg7brhj5g39gnj_66sp02z00000gn/T/077869428/harbor/1.3.2/harbor/harbor/harbor
==> Linting /var/folders/kq/byg7brhj5g39gnj_66sp02z00000gn/T/077869428/harbor/1.3.2/harbor/harbor/harbor
1 chart(s) linted, 0 chart(s) failed
Linting release=chartmuseum, chart=/var/folders/kq/byg7brhj5g39gnj_66sp02z00000gn/T/077869428/chartmuseum/2.13.0/stable/chartmuseum/chartmuseum
==> Linting /var/folders/kq/byg7brhj5g39gnj_66sp02z00000gn/T/077869428/chartmuseum/2.13.0/stable/chartmuseum/chartmuseum
1 chart(s) linted, 0 chart(s) failed
- Устанавлием nginx-ingress, cert-manager и harbor
helmfile sync
Adding repo stable https://kubernetes-charts.storage.googleapis.com
"stable" has been added to your repositories
Adding repo jetstack https://charts.jetstack.io
"jetstack" has been added to your repositories
Adding repo harbor https://helm.goharbor.io
"harbor" has been added to your repositories
Adding repo incubator https://kubernetes-charts-incubator.storage.googleapis.com
"incubator" has been added to your repositories
Updating repo
Hang tight while we grab the latest from your chart repositories...
...Successfully got an update from the "jetstack" chart repository
...Successfully got an update from the "incubator" chart repository
...Successfully got an update from the "harbor" chart repository
...Successfully got an update from the "stable" chart repository
Update Complete. ⎈ Happy Helming!⎈
Affected releases are:
cert-manager (jetstack/cert-manager) UPDATED
cert-manager-issuers (incubator/raw) UPDATED
chartmuseum (stable/chartmuseum) UPDATED
harbor (harbor/harbor) UPDATED
Upgrading release=cert-manager, chart=jetstack/cert-manager
Release "cert-manager" has been upgraded. Happy Helming!
NAME: cert-manager
LAST DEPLOYED: Sat May 30 20:11:38 2020
NAMESPACE: cert-manager
STATUS: deployed
REVISION: 4
TEST SUITE: None
NOTES:
cert-manager has been deployed successfully!
In order to begin issuing certificates, you will need to set up a ClusterIssuer
or Issuer resource (for example, by creating a 'letsencrypt-staging' issuer).
More information on the different types of issuers and how to configure them
can be found in our documentation:
https://cert-manager.io/docs/configuration/
For information on how to configure cert-manager to automatically provision
Certificates for Ingress resources, take a look at the `ingress-shim`
documentation:
https://cert-manager.io/docs/usage/ingress/
Listing releases matching ^cert-manager$
cert-manager cert-manager 4 2020-05-30 20:11:38.415808 +0300 MSK deployed cert-manager-v0.15.1 v0.15.1
Upgrading release=cert-manager-issuers, chart=incubator/raw
Upgrading release=chartmuseum, chart=stable/chartmuseum
Upgrading release=harbor, chart=harbor/harbor
Release "cert-manager-issuers" has been upgraded. Happy Helming!
NAME: cert-manager-issuers
LAST DEPLOYED: Sat May 30 20:11:53 2020
NAMESPACE: cert-manager
STATUS: deployed
REVISION: 4
TEST SUITE: None
Listing releases matching ^cert-manager-issuers$
cert-manager-issuers cert-manager 4 2020-05-30 20:11:53.680654 +0300 MSK deployed raw-0.2.3 0.2.3
Release "chartmuseum" does not exist. Installing it now.
NAME: chartmuseum
LAST DEPLOYED: Sat May 30 20:11:54 2020
NAMESPACE: chartmuseum
STATUS: deployed
REVISION: 1
TEST SUITE: None
NOTES:
** Please be patient while the chart is being deployed **
Get the ChartMuseum URL by running:
export POD_NAME=$(kubectl get pods --namespace chartmuseum -l "app=chartmuseum" -l "release=chartmuseum" -o jsonpath="{.items[0].metadata.name}")
echo http://127.0.0.1:8080/
kubectl port-forward $POD_NAME 8080:8080 --namespace chartmuseum
Listing releases matching ^chartmuseum$
chartmuseum chartmuseum 1 2020-05-30 20:11:54.450653 +0300 MSK deployed chartmuseum-2.13.0 0.12.0
Release "harbor" does not exist. Installing it now.
NAME: harbor
LAST DEPLOYED: Sat May 30 20:11:53 2020
NAMESPACE: harbor
STATUS: deployed
REVISION: 1
TEST SUITE: None
NOTES:
Please wait for several minutes for Harbor deployment to complete.
Then you should be able to visit the Harbor portal at https://core.harbor.domain.
For more details, please visit https://github.com/goharbor/harbor.
Listing releases matching ^harbor$
harbor harbor 1 2020-05-30 20:11:53.628585 +0300 MSK deployed harbor-1.3.2 1.10.2
UPDATED RELEASES:
NAME CHART VERSION
cert-manager jetstack/cert-manager v0.15.1
cert-manager-issuers incubator/raw 0.2.3
chartmuseum stable/chartmuseum 2.13.0
harbor harbor/harbor 1.3.2
- Проверяем:
kubectl get certificate --all-namespaces
NAMESPACE NAME READY SECRET AGE
chartmuseum chartmuseum.35.189.202.237.nip.io True chartmuseum.35.189.202.237.nip.io 2m14s
harbor harbor.35.189.202.237.nip.io True harbor.35.189.202.237.nip.io 2m11s
kubectl get deployments --all-namespaces
NAMESPACE NAME READY UP-TO-DATE AVAILABLE AGE
cert-manager cert-manager 1/1 1 1 3m14s
cert-manager cert-manager-cainjector 1/1 1 1 3m14s
cert-manager cert-manager-webhook 1/1 1 1 3m14s
chartmuseum chartmuseum-chartmuseum 1/1 1 1 3m14s
harbor harbor-harbor-chartmuseum 1/1 1 1 3m11s
harbor harbor-harbor-clair 1/1 1 1 3m11s
harbor harbor-harbor-core 1/1 1 1 3m11s
harbor harbor-harbor-jobservice 1/1 1 1 3m11s
harbor harbor-harbor-notary-server 1/1 1 1 3m11s
harbor harbor-harbor-notary-signer 1/1 1 1 3m11s
harbor harbor-harbor-portal 1/1 1 1 3m11s
harbor harbor-harbor-registry 1/1 1 1 3m11s
Типичная жизненная ситуация:
- У вас есть приложение, которое готово к запуску в Kubernetes
- У вас есть манифесты для этого приложения, но вам надо запускать его на разных окружениях с разными параметрами
Возможные варианты решения:
- Написать разные манифесты для разных окружений
- Использовать "костыли" - sed, envsubst, etc...
- Использовать полноценное решение для шаблонизации (helm, etc...)
Мы рассмотрим третий вариант. Возьмем готовые манифесты и подготовим их к релизу на разные окружения.
Использовать будем демо-приложение hipster-shop, представляющее собой типичный набор микросервисов.
Стандартными средствами helm инициализируем структуру директории с содержимым будущего helm chart
helm create kubernetes-templating/hipster-shop
Изучим созданный в качестве примера файл values.yaml и шаблоны в директории templates, примерно так выглядит стандартный helm chart.
Мы будем создавать chart для приложения с нуля, поэтому удалим values.yaml и содержимое templates.
После этого перенесем файл all-hipster-shop.yaml в директорию templates.
В целом, helm chart уже готов, попробуем установить его:
kubectl create ns hipster-shop
namespace/hipster-shop created
helm upgrade --install hipster-shop kubernetes-templating/hipster-shop --namespace hipster-shop
Release "hipster-shop" does not exist. Installing it now.
NAME: hipster-shop
LAST DEPLOYED: Fri May 29 00:39:12 2020
NAMESPACE: hipster-shop
STATUS: deployed
REVISION: 1
TEST SUITE: None
После этого можно зайти в UI используя сервис типа NodePort (создается из манифестов) и проверить, что приложение заработало.
kubectl get svc -n hipster-shop -l app=frontend
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
frontend NodePort 10.0.11.161 <none> 80:32500/TCP 12h
Добавим правило FW разрешающее доступ по порту 32500 на все worker хосты GKE.
Сейчас наш helm chart hipster-shop совсем не похож на настоящий. При этом, все микросервисы устанавливаются из одного файла all-hipster-shop.yaml
Давайте исправим это и первым делом займемся микросервисом frontend. Скорее всего он разрабатывается отдельной командой, а исходный код хранится в отдельном репозитории.
Поэтому, было бы логично вынести все что связано с frontend в отдельный helm chart.
Создадим заготовку:
helm create kubernetes-templating/frontend
Аналогично чарту hipster-shop удалим файл values.yaml и файлы в директории templates, создаваемые по умолчанию.
Выделим из файла all-hipster-shop.yaml манифесты для установки микросервиса frontend.
В директории templates чарта frontend создадим файлы:
- deployment.yaml - должен содержать соответствующую часть из файла all-hipster-shop.yaml
- service.yaml - должен содержать соответствующую часть из файла all-hipster-shop.yaml
- ingress.yaml - должен разворачивать ingress с доменным именем shop.<IP-адрес>.nip.io
После того, как вынесем описание deployment и service для frontend из файла all-hipster-shop.yaml переустановим chart hipster-shop и проверим, что доступ к UI пропал и таких ресурсов больше нет.
helm upgrade --install hipster-shop kubernetes-templating/hipster-shop --namespace hipster-shop
Release "hipster-shop" has been upgraded. Happy Helming!
NAME: hipster-shop
LAST DEPLOYED: Fri May 29 13:33:57 2020
NAMESPACE: hipster-shop
STATUS: deployed
REVISION: 2
TEST
SUITE: None
```console
Установим chart **frontend** в namespace **hipster-shop** и проверим, что доступ к UI вновь появился:
```console
helm upgrade --install frontend kubernetes-templating/frontend --namespace hipster-shop
Release "frontend" does not exist. Installing it now.
NAME: frontend
LAST DEPLOYED: Fri May 29 13:36:06 2020
NAMESPACE: hipster-shop
STATUS: deployed
REVISION: 1
TEST SUITE: None
Пришло время минимально шаблонизировать наш chart frontend
Для начала продумаем структуру файла values.yaml
- Docker образ из которого выкатывается frontend может пересобираться, поэтому логично вынести его тег в переменную frontend.image.tag
В values.yaml это будет выглядеть следующим образом:
image:
tag: v0.1.3
❗Это значение по умолчанию и может (и должно быть) быть переопределено в CI/CD pipeline
Теперь в манифесте deployment.yaml надо указать, что мы хотим использовать это переменную.
Было:
image: gcr.io/google-samples/microservices-demo/frontend:v0.1.3
Стало:
image: gcr.io/google-samples/microservices-demo/frontend:{{ .Values.image.tag }}
Аналогичным образом шаблонизируем следующие параметры frontend chart
- Количество реплик в deployment
- Port, targetPort и NodePort в service
- Опционально - тип сервиса. Ключ NodePort должен появиться в манифесте только если тип сервиса - NodePort
- Другие параметры, которые на наш взгляд стоит шаблонизировать
❗Не забываем указывать в файле values.yaml значения по умолчанию
Как должен выглядеть минимальный итоговый файл values.yaml:
image:
tag: v0.1.3
replicas: 1
service:
type: NodePort
port: 80
targetPort: 8080
NodePort: 32500
service.yaml:
spec:
type: {{ .Values.service.type }}
selector:
app: frontend
ports:
- name: http
port: {{ .Values.service.port }}
targetPort: {{ .Values.service.targetPort }}
nodePort: {{ .Values.service.NodePort }}
Теперь наш frontend стал немного похож на настоящий helm chart. Не стоит забывать, что он все еще является частью одного большого микросервисного приложения hipster-shop.
Поэтому было бы неплохо включить его в зависимости этого приложения.
Для начала, удалим release frontend из кластера:
helm delete frontend -n hipster-shop
release "frontend" uninstalled
В Helm 2 файл requirements.yaml содержал список зависимостей helm chart (другие chart).
В Helm 3 список зависимостей рекомендуют объявлять в файле Chart.yaml.
При указании зависимостей в старом формате, все будет работать, единственное выдаст предупреждение. Подробнее
Добавим chart frontend как зависимость
dependencies:
- name: frontend
version: 0.1.0
repository: "file://../frontend"
Обновим зависимости:
helm dep update kubernetes-templating/hipster-shop
Hang tight while we grab the latest from your chart repositories...
...Successfully got an update from the "harbor" chart repository
...Successfully got an update from the "jetstack" chart repository
...Successfully got an update from the "stable" chart repository
Update Complete. ⎈Happy Helming!⎈
Saving 1 charts
Deleting outdated charts
В директории kubernetes-templating/hipster-shop/charts появился архив frontend-0.1.0.tgz содержащий chart frontend определенной версии и добавленный в chart hipster-shop как зависимость.
Обновим release hipster-shop и убедимся, что ресурсы frontend вновь созданы.
helm upgrade --install hipster-shop kubernetes-templating/hipster-shop --namespace hipster-shop
Release "hipster-shop" has been upgraded. Happy Helming!
NAME: hipster-shop
LAST DEPLOYED: Fri May 29 14:36:18 2020
NAMESPACE: hipster-shop
STATUS: deployed
REVISION: 3
TEST SUITE: None
Осталось понять, как из CI-системы мы можем менять параметры helm chart, описанные в values.yaml.
Для этого существует специальный ключ --set
Изменим NodePort для frontend в release, не меняя его в самом chart:
helm upgrade --install hipster-shop kubernetes-templating/hipster-shop --namespace hipster-shop --set frontend.service.NodePort=31234
Так как как мы меняем значение переменной для зависимости - перед названием переменной указываем имя (название chart) этой зависимости.
Если бы мы устанавливали chart frontend напрямую, то команда выглядела бы как --set service.NodePort=31234
helm upgrade --install hipster-shop kubernetes-templating/hipster-shop --namespace hipster-shop --set frontend.service.NodePort=31234
Release "hipster-shop" has been upgraded. Happy Helming!
NAME: hipster-shop
LAST DEPLOYED: Fri May 29 15:39:44 2020
NAMESPACE: hipster-shop
STATUS: deployed
REVISION: 4
TEST SUITE: None
Выберем сервис, который можно установить как зависимость, используя community chart's. Например, это может быть Redis.
- Удалим из all-hipster-shop.yaml часть манифеста касательно redis
- Добавим repo с redis
helm repo add bitnami https://charts.bitnami.com/bitnami
"bitnami" has been added to your repositories
- дополняем наш Charts.yaml
dependencies:
- name: redis
version: 10.6.17
repository: https://charts.bitnami.com/bitnami
- меняем значение переменной окружения REDIS_ADDR с redis-cart на redis-cart-master для cartservice Deployment
- обновляем dep для hipster-shop: helm dep update kubernetes-templating/hipster-shop
- выкатываем:
helm upgrade --install hipster-shop kubernetes-templating/hipster-shop --namespace hipster-shop
Release "hipster-shop" has been upgraded. Happy Helming!
NAME: hipster-shop
LAST DEPLOYED: Fri May 29 16:20:22 2020
NAMESPACE: hipster-shop
STATUS: deployed
REVISION: 5
TEST SUITE: None
- Проверяем создание pod
kubectl get pods -n hipster-shop
NAME READY STATUS RESTARTS AGE
adservice-55f9757757-jnr72 1/1 Running 0 3h37m
cartservice-78577c6fbf-ldm7k 1/1 Running 15 88m
checkoutservice-6fcc84467f-27wrn 1/1 Running 0 3h37m
currencyservice-8b7b8c647-mqsmj 1/1 Running 0 3h37m
emailservice-6c46854ccc-bxxsv 1/1 Running 0 3h37m
frontend-745f64f8b6-rfmvx 1/1 Running 0 3h18m
paymentservice-76654769f7-xvlg8 1/1 Running 0 3h37m
productcatalogservice-d564bdf4c-2cr74 1/1 Running 0 3h37m
recommendationservice-76598d5889-jsq6m 1/1 Running 0 3h37m
redis-cart-master-0 1/1 Running 0 1m
shippingservice-b6db65f7f-qshbv 1/1 Running 0 3h37m
- Проверяем что UI работает
Разберемся как работает плагин helm-secrets. Для этого добавим в Helm chart секрет и научимся хранить его в зашифрованном виде.
Начнем с того, что установим плагин и необходимые для него зависимости (здесь и далее инструкции приведены для MacOS):
brew install sops
brew install gnupg2
brew install gnu-getopt
helm plugin install https://github.com/futuresimple/helm-secrets --version 2.0.2
sops is already installed:
sops 3.5.0 (latest)
Installed plugin: secrets
В домашней работы мы будем использовать PGP, но также можно воспользоваться KMS.
Сгенерируем новый PGP ключ:
gpg --full-generate-key
После этого командой gpg -k можно проверить, что ключ появился:
gpg -k
gpg: проверка таблицы доверия
gpg: marginals needed: 3 completes needed: 1 trust model: pgp
gpg: глубина: 0 достоверных: 1 подписанных: 0 доверие: 0-, 0q, 0n, 0m, 0f, 1u
/Users/alexey/.gnupg/pubring.kbx
--------------------------------
pub rsa2048 2020-05-29 [SC]
1EFA58CC515C2A87D5834A86C12D0C3E96B08842
uid [ абсолютно ] alexey <kovtalex@hmail.com>
sub rsa2048 2020-05-29 [E]
Создадим новый файл secrets.yaml в директории kubernetestemplating/frontend со следующим содержимым:
visibleKey: hiddenValue
И попробуем зашифровать его: sops -e -i --pgp <$ID> secrets.yaml
Примечание - вместо ID подставим длинный хеш, в выводе на предыдущей странице это 1EFA58CC515C2A87D5834A86C12D0C3E96B08842
sops -e -i --pgp 1EFA58CC515C2A87D5834A86C12D0C3E96B08842 kubernetes-templating/frontend/secrets.yaml
Проверим, что файл secrets.yaml изменился. Сейчас его содержание должно выглядеть примерно так:
visibleKey: ENC[AES256_GCM,data:fOgyl1H2cmc5kYo=,iv:Cv54qLIkjV1/S3MEyyMPlBmBUp3sIER+Fw8duXgRpR8=,tag:A01Z0Vlrtncn3sTk4JyCtQ==,type:str]
sops:
kms: []
gcp_kms: []
azure_kv: []
lastmodified: '2020-05-29T15:44:09Z'
mac: ENC[AES256_GCM,data:rPXR9oZwlZ0eayWmXa47lBAnpsVjijkFikLX1xs42+sprwyu3Iq2UgP5rstYNNwE6J0ZzjRivQXRoCTM+Y2GI6JkZTO6B95E9xfBblFlqjb1L+63btovUnI6z+IyMCl+eAZO6dK7jYgGu21T6PfS3pk2RrdYSJ6tDIkZOqlZMIw=,iv:nnNKQ0cfGR+0FCsXv/CtFli628WZjNq8sn6Hv9uni/0=,tag:324Mv9MZxIMyLycZZsDALQ==,type:str]
pgp:
- created_at: '2020-05-29T15:44:05Z'
enc: |
-----BEGIN PGP MESSAGE-----
hQEMA+tUwpJ4avm2AQf/fnDnm99OZrxgtmyeSE1KsHH4/ux0NLbmffT4yjKMg+yW
rwAhueBVt/utNCFyVSH6BAcMFP9Wy3EYvbKzZJx9/ypjXaJ+aZZ7MSzKRnNgauF3
pSB9noScMWquShGubvgsXr++Jc7aXd/qSCrIbrgkDfHTSYri4QO9CjkmZIXXFXdJ
ijx+BpgsJtWsCM/pzBMi31q0AU4G0SulH5CsVeg5TvrZMuUPzihVrqD4yOxZU3xx
w+QHtiDHzXxYowQTV9UVBiwaBEf1+pl2CYY3E5gizn7CuQuRc1bttvuGdht34YDH
TjRRmoFs0hPGEJFzbGfUMe8j8pb8ZApD2y5YZpzmSNJcAWumbdpN5agLapOCs0AJ
iZOW83tRSg4C6uc/eSZ9Z+IsoAq9fYDXrWVitXRuZ7AkTSB9J7Jm6YUrB8r39bx5
uux6k7/6QjN9OlZIbHIZHA7aPzmAZVPNDws2e9Q=
=742r
-----END PGP MESSAGE-----
fp: 1EFA58CC515C2A87D5834A86C12D0C3E96B08842
unencrypted_suffix: _unencrypted
version: 3.5.0
Заметим, что структура файла осталась прежней. Мы видим ключ visibleKey, но его значение зашифровано
В таком виде файл уже можно коммитить в Git, но для начала - научимся расшифровывать его.
Можно использовать любой из инструментов:
# helm secrets
helm secrets view secrets.yaml
# sops
sops -d secrets.yaml
helm secrets view kubernetes-templating/frontend/secrets.yaml
visibleKey: hiddenValue
sops -d kubernetes-templating/frontend/secrets.yaml
visibleKey: hiddenValue
Теперь осталось понять, как добавить значение нашего секрета в настоящий секрет kubernetes и устанавливать его вместе с основным helm chart.
Создадим в директории kubernetestemplating/frontend/templates еще один файл secret.yaml.
Несмотря на похожее название его предназначение будет отличаться.
Поместим туда следующий шаблон:
apiVersion: v1
kind: Secret
metadata:
name: secret
type: Opaque
data:
visibleKey: {{ .Values.visibleKey | b64enc | quote }}
Теперь, если мы передадим в helm файл secrets.yaml как values файл - плагин helm-secrets поймет, что его надо расшифровать, а значение ключа visibleKey подставить в соответствующий шаблон секрета.
Запустим установку:
helm secrets upgrade --install frontend kubernetes-templating/frontend --namespace hipster-shop \
-f kubernetes-templating/frontend/values.yaml \
-f kubernetes-templating/frontend/secrets.yaml
Release "frontend" does not exist. Installing it now.
NAME: frontend
LAST DEPLOYED: Fri May 29 18:50:00 2020
NAMESPACE: hipster-shop
STATUS: deployed
REVISION: 1
TEST SUITE: None
kubernetes-templating/frontend/secrets.yaml.dec
В процессе установки helm-secrets расшифрует наш секретный файл в другой временный файл secrets.yaml.dec, а после выполнения установки - удалит его
- Проверим, что секрет создан и его содержимое соответсвуем нашим ожиданиям
kubectl get secrets secret -n hipster-shop -o yaml
apiVersion: v1
data:
visibleKey: aGlkZGVuVmFsdWU=
kind: Secret
metadata:
annotations:
meta.helm.sh/release-name: frontend
meta.helm.sh/release-namespace: hipster-shop
creationTimestamp: "2020-05-29T15:50:00Z"
labels:
app.kubernetes.io/managed-by: Helm
name: secret
namespace: hipster-shop
resourceVersion: "811436"
selfLink: /api/v1/namespaces/hipster-shop/secrets/secret
uid: b22f6a8d-6c8e-49a7-bcc2-3b86ce59a8ce
type: Opaque
kubectl get secrets secret -n hipster-shop -o "jsonpath={.data.visibleKey}" | base64 -D
hiddenValue%
- В CI/CD плагин helm-secrets можно использовать для подготовки авторизации на различных сервисах
- Как обезопасить себя от коммита файлов с секретами - https://github.com/zendesk/helm-secrets#important-tips
Поместим все получившиеся helm chart's в наш установленный harbor в публичный проект.
Установим helm-push
helm plugin install https://github.com/chartmuseum/helm-push.git
Создадим файл kubernetes-templating/repo.sh со следующим содержанием:
#!/bin/bash
helm repo add templating https://harbor.35.189.202.237.nip.io/chartrepo/library
helm push --username admin --password Harbor12345 frontend/ templating
helm push --username admin --password Harbor12345 hipster-shop/ templating
./repo.sh
"templating" has been added to your repositories
Pushing frontend-0.1.0.tgz to templating...
Done.
Pushing hipster-shop-0.1.0.tgz to templating...
Done.
Проверим:
Представим, что одна из команд разрабатывающих сразу несколько микросервисов нашего продукта решила, что helm не подходит для ее нужд и попробовала использовать решение на основе jsonnet - kubecfg.
Посмотрим на возможности этой утилиты. Работать будем с сервисами paymentservice и shippingservice. Для начала - вынесем манифесты описывающие service и deployment для этих микросервисов из файла all-hipstershop.yaml в директорию kubernetes-templating/kubecfg
Проверим:
helm search repo -l templating
NAME CHART VERSION APP VERSION DESCRIPTION
templating/frontend 0.1.0 1.16.0 A Helm chart for Kubernetes
templating/hipster-shop 0.1.0 1.16.0 A Helm chart for Kubernetes
И развернем:
helm upgrade --install hipster-shop templating/hipster-shop --namespace hipster-shop
helm upgrade --install frontend templating/frontend --namespace hipster-shop
Представим, что одна из команд разрабатывающих сразу несколько микросервисов нашего продукта решила, что helm не подходит для ее нужд и попробовала использовать решение на основе jsonnet - kubecfg.
Посмотрим на возможности этой утилиты. Работать будем с сервисами paymentservice и shippingservice.
Для начала - вынесем манифесты описывающие service и deployment для этих микросервисов из файла all-hipstershop.yaml в директорию kubernetes-templating/kubecfg
В итоге должно получиться четыре файла:
tree -L 1 kubecfg
kubecfg
├── paymentservice-deployment.yaml
├── paymentservice-service.yaml
├── shippingservice-deployment.yaml
└── shippingservice-service.yaml
Можно заметить, что манифесты двух микросервисов очень похожи друг на друга и может иметь смысл генерировать их из какого-то шаблона.
Попробуем сделать это.
Обновим release hipster-shop, проверим, что микросервисы paymentservice и shippingservice исчезли из установки и магазин стал работать некорректно (при нажатии на кнопку Add to Cart).
helm upgrade --install hipster-shop kubernetes-templating/hipster-shop --namespace hipster-shopRelease "hipster-shop" has been upgraded. Happy Helming!
NAME: hipster-shop
LAST DEPLOYED: Fri May 29 23:26:43 2020
NAMESPACE: hipster-shop
STATUS: deployed
REVISION: 2
TEST SUITE: None
Установим kubecfg (доступна в виде сборок по MacOS и Linux и в Homebrew)
brew install kubecfg
kubecfg version
kubecfg version: v0.16.0
jsonnet version: v0.15.0
client-go version: v0.0.0-master+604c38a2
Kubecfg предполагает хранение манифестов в файлах формата .jsonnet и их генерацию перед установкой. Пример такого файла можно найти в официальном репозитории
Напишем по аналогии свой .jsonnet файл - services.jsonnet.
Для начала в файле мы должны указать libsonnet библиотеку, которую будем использовать для генерации манифестов. В домашней работе воспользуемся готовой от bitnami
wget https://github.com/bitnami-labs/kube-libsonnet/raw/52ba963ca44f7a4960aeae9ee0fbee44726e481f/kube.libsonnet
❗ В kube.libsonnet исправим версию api для Deploymens и Service на apps/v1
Импортируем ее:
local kube = import "kube.libsonnet";
Перейдем к основной части
Общая логика происходящего следующая:
- Пишем общий для сервисов шаблон, включающий описание service и deployment
- Наследуемся от него, указывая параметры для конкретных
services.jsonnet:
local kube = import "kube.libsonnet";
local common(name) = {
service: kube.Service(name) {
target_pod:: $.deployment.spec.template,
},
deployment: kube.Deployment(name) {
spec+: {
template+: {
spec+: {
containers_: {
common: kube.Container("common") {
env: [{name: "PORT", value: "50051"}],
ports: [{containerPort: 50051}],
securityContext: {
readOnlyRootFilesystem: true,
runAsNonRoot: true,
runAsUser: 10001,
},
readinessProbe: {
initialDelaySeconds: 20,
periodSeconds: 15,
exec: {
command: [
"/bin/grpc_health_probe",
"-addr=:50051",
],
},
},
livenessProbe: {
initialDelaySeconds: 20,
periodSeconds: 15,
exec: {
command: [
"/bin/grpc_health_probe",
"-addr=:50051",
],
},
},
},
},
},
},
},
},
};
{
catalogue: common("paymentservice") {
deployment+: {
spec+: {
template+: {
spec+: {
containers_+: {
common+: {
name: "server",
image: "gcr.io/google-samples/microservices-demo/paymentservice:v0.1.3",
},
},
},
},
},
},
},
payment: common("shippingservice") {
deployment+: {
spec+: {
template+: {
spec+: {
containers_+: {
common+: {
name: "server",
image: "gcr.io/google-samples/microservices-demo/shippingservice:v0.1.3",
},
},
},
},
},
},
},
}
Проверим, что манифесты генерируются корректно:
kubecfg show services.jsonnet
---
apiVersion: apps/v1
kind: Deployment
metadata:
annotations: {}
labels:
name: paymentservice
name: paymentservice
spec:
minReadySeconds: 30
replicas: 1
selector:
matchLabels:
name: paymentservice
strategy:
rollingUpdate:
maxSurge: 25%
maxUnavailable: 25%
type: RollingUpdate
template:
metadata:
annotations: {}
labels:
name: paymentservice
spec:
containers:
- args: []
env:
- name: PORT
value: "50051"
image: gcr.io/google-samples/microservices-demo/paymentservice:v0.1.3
imagePullPolicy: IfNotPresent
livenessProbe:
exec:
command:
- /bin/grpc_health_probe
- -addr=:50051
initialDelaySeconds: 20
periodSeconds: 15
name: server
ports:
- containerPort: 50051
readinessProbe:
exec:
command:
- /bin/grpc_health_probe
- -addr=:50051
initialDelaySeconds: 20
periodSeconds: 15
securityContext:
readOnlyRootFilesystem: true
runAsNonRoot: true
runAsUser: 10001
stdin: false
tty: false
volumeMounts: []
imagePullSecrets: []
initContainers: []
terminationGracePeriodSeconds: 30
volumes: []
---
apiVersion: v1
kind: Service
metadata:
annotations: {}
labels:
name: paymentservice
name: paymentservice
spec:
ports:
- port: 50051
targetPort: 50051
selector:
name: paymentservice
type: ClusterIP
---
apiVersion: apps/v1
kind: Deployment
metadata:
annotations: {}
labels:
name: shippingservice
name: shippingservice
spec:
minReadySeconds: 30
replicas: 1
selector:
matchLabels:
name: shippingservice
strategy:
rollingUpdate:
maxSurge: 25%
maxUnavailable: 25%
type: RollingUpdate
template:
metadata:
annotations: {}
labels:
name: shippingservice
spec:
containers:
- args: []
env:
- name: PORT
value: "50051"
image: gcr.io/google-samples/microservices-demo/shippingservice:v0.1.3
imagePullPolicy: IfNotPresent
livenessProbe:
exec:
command:
- /bin/grpc_health_probe
- -addr=:50051
initialDelaySeconds: 20
periodSeconds: 15
name: server
ports:
- containerPort: 50051
readinessProbe:
exec:
command:
- /bin/grpc_health_probe
- -addr=:50051
initialDelaySeconds: 20
periodSeconds: 15
securityContext:
readOnlyRootFilesystem: true
runAsNonRoot: true
runAsUser: 10001
stdin: false
tty: false
volumeMounts: []
imagePullSecrets: []
initContainers: []
terminationGracePeriodSeconds: 30
volumes: []
---
apiVersion: v1
kind: Service
metadata:
annotations: {}
labels:
name: shippingservice
name: shippingservice
spec:
ports:
- port: 50051
targetPort: 50051
selector:
name: shippingservice
type: ClusterIP
И установим их:
kubecfg update services.jsonnet --namespace hipster-shop
INFO Validating deployments paymentservice
INFO validate object "apps/v1, Kind=Deployment"
INFO Validating services paymentservice
INFO validate object "/v1, Kind=Service"
INFO Validating services shippingservice
INFO validate object "/v1, Kind=Service"
INFO Validating deployments shippingservice
INFO validate object "apps/v1, Kind=Deployment"
INFO Fetching schemas for 4 resources
INFO Creating services paymentservice
INFO Creating services shippingservice
INFO Creating deployments paymentservice
INFO Creating deployments shippingservice
Через какое-то время магазин снова должен заработать и товары можно добавить в корзину
Выберем еще один микросервис (recommendationservice)из состава hipster-shop и попробуем использовать другое решение на основе jsonnet, например qbec
Также можно использовать Kapitan
Приложим артефакты их использования в директорию kubernetes-templating/jsonnet и опишем проделанную работу и порядок установки.
- Установим qbec
brew tap splunk/tap
brew install qbec
- Подготовим новую структуру для приложения
qbec init recommendationservice
using server URL "https://34.78.75.22" and default namespace "default" for the default environment
wrote recommendationservice/params.libsonnet
wrote recommendationservice/environments/base.libsonnet
wrote recommendationservice/environments/default.libsonnet
wrote recommendationservice/qbec.yaml
- Конвертируем манифест Deployment и Service (recommendationservice) из yaml в json и поместим результат в компонент components/recommendationservice.jsonnet
- Также добавим в components/recommendationservice.jsonnet блок:
local env = {
name: std.extVar('qbec.io/env'),
namespace: std.extVar('qbec.io/defaultNs'),
};
local p = import '../params.libsonnet';
local params = p.components.recommendationservice;
- Шаблонизируем необходимые нам параметры в components/recommendationservice.jsonnet:
"resources": {
"requests": {
"cpu": params.cpu_requests,
"memory": params.memory_requests
},
"limits": {
"cpu": params.cpu_limits,
"memory": params.memory_limits
}
}
- Определим их значения по умолчанию для всех окружений в environments/base.libsonnet:
{
components: {
recommendationservice: {
name: "recommendationservice",
image: "gcr.io/google-samples/microservices-demo/recommendationservice:v0.1.3",
replicas: 1,
cpu_requests: "100m",
memory_requests: "220Mi",
cpu_limits: "200m",
memory_limits: "450Mi",
containterPort: 8080,
servicePort: 8080,
},
},
}
- Создадим environments/dev.libsonnet содержаший значения переменных для окружение dev:
local base = import './base.libsonnet';
base {
components +: {
recommendationservice +: {
name: "dev-recommendationservice",
replicas: 3,
},
},
}
- Добавим наше новое окружение dev в params.libsonnet:
dev: import './environments/dev.libsonnet',
- И в qbec.yaml:
dev:
defaultNamespace: hipster-shop
server: https://34.78.75.22
namespaceTagSuffix: true
namespaceTagSuffix: true - позволит добавлять суффикс к namespace, указывая в командной строке: --app-tag=<suffix>
- Провалидируем окружение по умолчанию
qbec validate default
setting cluster to gke_angular-pursuit-275120_europe-west1-b_cluster-1
setting context to gke_angular-pursuit-275120_europe-west1-b_cluster-1
cluster metadata load took 334ms
1 components evaluated in 4ms
✔ deployments recommendationservice -n hipster-shop (source recommendationservice) is valid
✔ services recommendationservice -n hipster-shop (source recommendationservice) is valid
---
stats:
valid: 2
command took 540ms
- Просмотрим:
qbec show default
1 components evaluated in 5ms
---
apiVersion: apps/v1
kind: Deployment
metadata:
annotations:
qbec.io/component: recommendationservice
labels:
qbec.io/application: recommendationservice
qbec.io/environment: default
name: recommendationservice
spec:
replicas: 1
selector:
matchLabels:
app: recommendationservice
template:
metadata:
labels:
app: recommendationservice
spec:
containers:
- env:
- name: PORT
value: "8080"
- name: PRODUCT_CATALOG_SERVICE_ADDR
value: productcatalogservice:3550
- name: ENABLE_PROFILER
value: "0"
image: gcr.io/google-samples/microservices-demo/recommendationservice:v0.1.3
livenessProbe:
exec:
command:
- /bin/grpc_health_probe
- -addr=:8080
periodSeconds: 5
name: server
ports:
- containerPort: 8080
readinessProbe:
exec:
command:
- /bin/grpc_health_probe
- -addr=:8080
periodSeconds: 5
resources:
limits:
cpu: 200m
memory: 450Mi
requests:
cpu: 100m
memory: 220Mi
terminationGracePeriodSeconds: 5
---
apiVersion: v1
kind: Service
metadata:
annotations:
qbec.io/component: recommendationservice
labels:
qbec.io/application: recommendationservice
qbec.io/environment: default
name: recommendationservice
spec:
ports:
- name: grpc
port: 8080
targetPort: 8080
selector:
app: recommendationservice
type: ClusterIP
- И выкатим:
qbec apply default
setting cluster to gke_angular-pursuit-275120_europe-west1-b_cluster-1
setting context to gke_angular-pursuit-275120_europe-west1-b_cluster-1
cluster metadata load took 279ms
1 components evaluated in 2ms
will synchronize 2 object(s)
Do you want to continue [y/n]: y
1 components evaluated in 1ms
create deployments recommendationservice -n hipster-shop (source recommendationservice)
create services recommendationservice -n hipster-shop (source recommendationservice)
waiting for deletion list to be returned
server objects load took 669ms
---
stats:
created:
- deployments recommendationservice -n hipster-shop (source recommendationservice)
- services recommendationservice -n hipster-shop (source recommendationservice)
command took 9.51s
- Проверяем
kubectl get deployments -n hipster-shop
NAME READY UP-TO-DATE AVAILABLE AGE
adservice 1/1 1 1 83m
cartservice 1/1 1 1 57m
checkoutservice 1/1 1 1 83m
currencyservice 1/1 1 1 83m
emailservice 1/1 1 1 83m
frontend 1/1 1 1 83m
paymentservice 1/1 1 1 82m
productcatalogservice 1/1 1 1 83m
recommendationservice 1/1 1 1 2m30s
shippingservice 1/1 1 1 82m
- Валидания для dev окружения
qbec validate dev
setting cluster to gke_angular-pursuit-275120_europe-west1-b_cluster-1
setting context to gke_angular-pursuit-275120_europe-west1-b_cluster-1
cluster metadata load took 263ms
1 components evaluated in 5ms
✔ deployments dev-recommendationservice -n hipster-shop (source recommendationservice) is valid
✔ services dev-recommendationservice -n hipster-shop (source recommendationservice) is valid
---
stats:
valid: 2
command took 500ms
- Просмотрим:
qbec show dev --app-tag dev
1 components evaluated in 3ms
---
apiVersion: apps/v1
kind: Deployment
metadata:
annotations:
qbec.io/component: recommendationservice
labels:
qbec.io/application: recommendationservice
qbec.io/environment: dev
qbec.io/tag: dev
name: dev-recommendationservice
spec:
replicas: 3
selector:
matchLabels:
app: dev-recommendationservice
template:
metadata:
labels:
app: dev-recommendationservice
spec:
containers:
- env:
- name: PORT
value: "8080"
- name: PRODUCT_CATALOG_SERVICE_ADDR
value: productcatalogservice:3550
- name: ENABLE_PROFILER
value: "0"
image: gcr.io/google-samples/microservices-demo/recommendationservice:v0.1.3
livenessProbe:
exec:
command:
- /bin/grpc_health_probe
- -addr=:8080
periodSeconds: 5
name: server
ports:
- containerPort: 8080
readinessProbe:
exec:
command:
- /bin/grpc_health_probe
- -addr=:8080
periodSeconds: 5
resources:
limits:
cpu: 200m
memory: 450Mi
requests:
cpu: 100m
memory: 220Mi
terminationGracePeriodSeconds: 5
---
apiVersion: v1
kind: Service
metadata:
annotations:
qbec.io/component: recommendationservice
labels:
qbec.io/application: recommendationservice
qbec.io/environment: dev
qbec.io/tag: dev
name: dev-recommendationservice
spec:
ports:
- name: grpc
port: 8080
targetPort: 8080
selector:
app: dev-recommendationservice
type: ClusterIP
Увеличилость количество реплик и изменились значения для name
- Выкатим с dry-run
qbec apply dev --app-tag dev --dry-run
---
stats:
created:
- deployments dev-recommendationservice -n hipster-shop-dev (source recommendationservice)
- services dev-recommendationservice -n hipster-shop-dev (source recommendationservice)
- И наконец выкатим dev в namespace hipster-shop-dev
qbec apply dev --app-tag dev
setting cluster to gke_angular-pursuit-275120_europe-west1-b_cluster-1
setting context to gke_angular-pursuit-275120_europe-west1-b_cluster-1
cluster metadata load took 264ms
1 components evaluated in 4ms
will synchronize 2 object(s)
Do you want to continue [y/n]: y
1 components evaluated in 2ms
create deployments dev-recommendationservice -n hipster-shop-dev (source recommendationservice)
create services dev-recommendationservice -n hipster-shop-dev (source recommendationservice)
waiting for deletion list to be returned
server objects load took 856ms
---
stats:
created:
- deployments dev-recommendationservice -n hipster-shop-dev (source recommendationservice)
- services dev-recommendationservice -n hipster-shop-dev (source recommendationservice)
command took 3.07s
Проверяем:
kubectl describe deployments -n hipster-shop-dev | grep replicas
NewReplicaSet: dev-recommendationservice-6b6bd45b99 (3/3 replicas created)
Отпилим еще один (cartservice) микросервис из all-hipstershop.yaml.yaml и займемся его kustomизацией.
В минимальном варианте реализуем установку на три окружения - hipster-shop (namespace hipster-shop), hipster-shop-prod (namespace hipster-shop-prod) и hipster-shop-dev (namespace hipster-shop-dev) из одних манифестов deployment и service.
Окружения должны отличаться:
- Набором labels во всех манифестах
- Префиксом названий ресурсов
- Для dev окружения значением переменной окружения REDIS_ADDR
Установим kustomize:
brew install kustomize
Для namespace hipster-shop:
kustomize build .
apiVersion: v1
kind: Service
metadata:
name: cartservice
namespace: hipster-shop
spec:
ports:
- name: grpc
port: 7070
targetPort: 7070
selector:
app: cartservice
type: ClusterIP
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: cartservice
namespace: hipster-shop
spec:
selector:
matchLabels:
app: cartservice
template:
metadata:
labels:
app: cartservice
spec:
containers:
- env:
- name: REDIS_ADDR
value: redis-cart-master:6379
- name: PORT
value: "7070"
- name: LISTEN_ADDR
value: 0.0.0.0
image: gcr.io/google-samples/microservices-demo/cartservice:v0.1.3
livenessProbe:
exec:
command:
- /bin/grpc_health_probe
- -addr=:7070
- -rpc-timeout=5s
initialDelaySeconds: 15
periodSeconds: 10
name: server
ports:
- containerPort: 7070
readinessProbe:
exec:
command:
- /bin/grpc_health_probe
- -addr=:7070
- -rpc-timeout=5s
initialDelaySeconds: 15
resources:
limits:
cpu: 300m
memory: 128Mi
requests:
cpu: 200m
memory: 64Mi
Для namespace hipster-shop-dev:
kustomize build .
apiVersion: v1
kind: Service
metadata:
labels:
environment: dev
name: dev-cartservice
namespace: hipster-shop-dev
spec:
ports:
- name: grpc
port: 7070
targetPort: 7070
selector:
app: cartservice
environment: dev
type: ClusterIP
---
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
environment: dev
name: dev-cartservice
namespace: hipster-shop-dev
spec:
selector:
matchLabels:
app: cartservice
environment: dev
template:
metadata:
labels:
app: cartservice
environment: dev
spec:
containers:
- env:
- name: REDIS_ADDR
value: redis-cart:6379
- name: PORT
value: "7070"
- name: LISTEN_ADDR
value: 0.0.0.0
image: gcr.io/google-samples/microservices-demo/cartservice:v0.1.3
livenessProbe:
exec:
command:
- /bin/grpc_health_probe
- -addr=:7070
- -rpc-timeout=5s
initialDelaySeconds: 15
periodSeconds: 10
name: server
ports:
- containerPort: 7070
readinessProbe:
exec:
command:
- /bin/grpc_health_probe
- -addr=:7070
- -rpc-timeout=5s
initialDelaySeconds: 15
resources:
limits:
cpu: 300m
memory: 128Mi
requests:
cpu: 200m
memory: 64Mi
Задеплоим и проверим работу UI:
kustomize build . | kubectl apply -f -
Warning: kubectl apply should be used on resource created by either kubectl create --save-config or kubectl apply
service/cartservice created
deployment.apps/cartservice created
kind - инструмент для запуска Kuberenetes при помощи Docker контейнеров.
Запуск: kind create cluster
В этом ДЗ мы развернем StatefulSet c MinIO - локальным S3 хранилищем.
Конфигурация StatefulSet.
minio-statefulset.yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
# This name uniquely identifies the StatefulSet
name: minio
spec:
serviceName: minio
replicas: 1
selector:
matchLabels:
app: minio # has to match .spec.template.metadata.labels
template:
metadata:
labels:
app: minio # has to match .spec.selector.matchLabels
spec:
containers:
- name: minio
env:
- name: MINIO_ACCESS_KEY
value: "minio"
- name: MINIO_SECRET_KEY
value: "minio123"
image: minio/minio:RELEASE.2019-07-10T00-34-56Z
args:
- server
- /data
ports:
- containerPort: 9000
# These volume mounts are persistent. Each pod in the PetSet
# gets a volume mounted based on this field.
volumeMounts:
- name: data
mountPath: /data
# Liveness probe detects situations where MinIO server instance
# is not working properly and needs restart. Kubernetes automatically
# restarts the pods if liveness checks fail.
livenessProbe:
httpGet:
path: /minio/health/live
port: 9000
initialDelaySeconds: 120
periodSeconds: 20
# These are converted to volume claims by the controller
# and mounted at the paths mentioned above.
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 10Gi
Для того, чтобы наш StatefulSet был доступен изнутри кластера, создадим Headless Service.
minio-headless-service.yaml
apiVersion: v1
kind: Service
metadata:
name: minio
labels:
app: minio
spec:
clusterIP: None
ports:
- port: 9000
name: minio
selector:
app: minio
В результате применения конфигурации должно произойти следующее:
- Запуститься под с MinIO
- Создаться PVC
- Динамически создаться PV на этом PVC с помощью дефолотного StorageClass
kubectl apply -f minio-statefulset.yaml
statefulset.apps/minio created
kubectl apply -f minio-headless-service.yaml
service/minio created
Создадим сервис LB:
minio-svc-lb.yaml
apiVersion: v1
kind: Service
metadata:
name: minio-svc-lb
spec:
selector:
app: minio
type: LoadBalancer
ports:
- protocol: TCP
port: 9000
targetPort: 9000
- Проверить работу Minio можно с помощью консольного клиента mc
mc config host add minio http://172.17.255.1:9000 minio minio123
Added `minio` successfully.
- Проверить работу Minio можно с помощью браузера: http://172.17.255.1:9000/minio/
Также для проверки ресурсов k8s помогут команды:
kubectl get statefulsets
kubectl get pods
kubectl get pvc
kubectl get pv
kubectl describe <resource> <resource_name>
kubectl get statefulsets
NAME READY AGE
minio 1/1 10h
kubectl get pods
NAME READY STATUS RESTARTS AGE
minio-0 1/1 Running 0 10h
kubectl get pvc
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
data-minio-0 Bound pvc-905bcd19-8e21-41a3-902b-a75a80b2c4dc 10Gi RWO standard 10h
kubectl get pv
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE
pvc-30f9d611-009d-4b70-90c1-4eee8b707cfe 10Gi RWO Delete Bound default/data-minio-0 standard 7h7m
В конфигурации нашего StatefulSet данные указаны в открытом виде, что не безопасно.
Поместим данные в secrets и настроим конфигурацию на их использование.
Конвертируем username и password в base64:
echo -n 'minio' | base64
bWluaW8=
echo -n 'minio123' | base64
bWluaW8xMjM=
Подготовим манифест с Secret:
apiVersion: v1
kind: Secret
metadata:
name: minio
type: Opaque
data:
username: bWluaW8=
password: bWluaW8xMjM=
Изменим minio-headless-service.yaml для использования нашего Secret:
env:
- name: MINIO_ACCESS_KEY
valueFrom:
secretKeyRef:
name: minio
key: username
- name: MINIO_SECRET_KEY
valueFrom:
secretKeyRef:
name: minio
key: password
Применим изменения:
kubectl apply -f minio-statefulset.yaml
statefulset.apps/minio configured
secret/minio created
Посмотрим на Secret:
kubectl get secret minio -o yaml
apiVersion: v1
data:
password: bWluaW8xMjM=
username: bWluaW8=
kind: Secret
metadata:
annotations:
kubectl.kubernetes.io/last-applied-configuration: |
{"apiVersion":"v1","data":{"password":"bWluaW8xMjM=","username":"bWluaW8="},"kind":"Secret","metadata":{"annotations":{},"name":"minio","namespace":"default"},"type":"Opaque"}
creationTimestamp: "2020-05-23T12:33:55Z"
managedFields:
- apiVersion: v1
fieldsType: FieldsV1
fieldsV1:
f:data:
.: {}
f:password: {}
f:username: {}
f:metadata:
f:annotations:
.: {}
f:kubectl.kubernetes.io/last-applied-configuration: {}
f:type: {}
manager: kubectl
operation: Update
time: "2020-05-23T12:33:55Z"
name: minio
namespace: default
resourceVersion: "19218"
selfLink: /api/v1/namespaces/default/secrets/minio
uid: 87e39a34-4a5b-40c0-a4b9-5181b0a3cb34
type: Opaque
kubectl describe statefulsets minio
Name: minio
Namespace: default
CreationTimestamp: Fri, 22 May 2020 23:38:47 +0300
Selector: app=minio
Labels: <none>
Annotations: kubectl.kubernetes.io/last-applied-configuration:
{"apiVersion":"apps/v1","kind":"StatefulSet","metadata":{"annotations":{},"name":"minio","namespace":"default"},"spec":{"replicas":1,"sele...
Replicas: 1 desired | 1 total
Update Strategy: RollingUpdate
Partition: 824642936636
Pods Status: 1 Running / 0 Waiting / 0 Succeeded / 0 Failed
Pod Template:
Labels: app=minio
Containers:
minio:
Image: minio/minio:RELEASE.2019-07-10T00-34-56Z
Port: 9000/TCP
Host Port: 0/TCP
Args:
server
/data
Liveness: http-get http://:9000/minio/health/live delay=120s timeout=1s period=20s #success=1 #failure=3
Environment:
MINIO_ACCESS_KEY: minio
MINIO_SECRET_KEY: minio123
Mounts:
/data from data (rw)
Volumes: <none>
Volume Claims:
Name: data
StorageClass:
Labels: <none>
Annotations: <none>
Capacity: 10Gi
Access Modes: [ReadWriteOnce]
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal SuccessfulCreate 11h statefulset-controller create Claim data-minio-0 Pod minio-0 in StatefulSet minio success
Normal SuccessfulCreate 11h statefulset-controller create Pod minio-0 in StatefulSet minio successful
Удалить кластер можно командой: kind delete cluster
- Откроем файл с описанием Pod из предыдущего ДЗ kubernetes-intro/web-pod.yml
- Добавим в описание пода readinessProbe
readinessProbe:
httpGet:
path: /index.html
port: 80
- Запустим наш под командой kubectl apply -f webpod.yml
kubectl apply -f web-pod.yaml
pod/web created
- Теперь выполним команду kubectl get pod/web и убедимся, что под перешел в состояние Running
kubectl get pod/web
NAME READY STATUS RESTARTS AGE
web 0/1 Running 0 45s
Теперь сделаем команду kubectl describe pod/web (вывод объемный, но в нем много интересного)
- Посмотрим в конце листинга на список Conditions:
kubectl describe pod/web
Conditions:
Type Status
Initialized True
Ready False
ContainersReady False
PodScheduled True
Также посмотрим на список событий, связанных с Pod:
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Warning Unhealthy 3s (x2 over 13s) kubelet, minikube Readiness probe failed: Get http://172.18.0.4:80/index.html: dial tcp 172.18.0.4:80: connect: connection refused
Из листинга выше видно, что проверка готовности контейнера завершается неудачно. Это неудивительно - вебсервер в контейнере слушает порт 8000 (по условиям первого ДЗ).
Пока мы не будем исправлять эту ошибку, а добавим другой вид проверок: livenessProbe.
- Добавим в манифест проверку состояния веб-сервера:
livenessProbe:
tcpSocket: { port: 8000 }
- Запустим Pod с новой конфигурацией:
kubectl apply -f web-pod.yaml
pod/web created
kubectl get pod/web
NAME READY STATUS RESTARTS AGE
web 0/1 Running 0 17s
Вопрос для самопроверки:
- Почему следующая конфигурация валидна, но не имеет смысла?
livenessProbe:
exec:
command:
- 'sh'
- '-c'
- 'ps aux | grep my_web_server_process'
Данная конфигурация не имеет смысла, так как не означает, что работающий веб сервер без ошибок отдает веб страницы.
- Бывают ли ситуации, когда она все-таки имеет смысл?
Возможно, когда требуется проверка работы сервиса без доступа к нему из вне.
В процессе изменения конфигурации Pod, мы столкнулись с неудобством обновления конфигурации пода через kubectl (и уже нашли ключик --force ).
В любом случае, для управления несколькими однотипными подами такой способ не очень подходит.
Создадим Deployment, который упростит обновление конфигурации пода и управление группами подов.
- Для начала, создадим новую папку kubernetes-networks в нашем репозитории
- В этой папке создадим новый файл web-deploy.yaml
Начнем заполнять наш файл-манифест для Deployment:
apiVersion: apps/v1
kind: Deployment
metadata:
name: web # Название нашего объекта Deployment
spec:
replicas: 1 # Начнем с одного пода
selector: # Укажем, какие поды относятся к нашему Deployment:
matchLabels: # - это поды с меткой
app: web # app и ее значением web
template: # Теперь зададим шаблон конфигурации пода
metadata:
name: web # Название Pod
labels: # Метки в формате key: value
app: web
spec: # Описание Pod
containers: # Описание контейнеров внутри Pod
- name: web # Название контейнера
image: kovtalex/simple-web:0.1 # Образ из которого создается контейнер
readinessProbe:
httpGet:
path: /index.html
port: 80
livenessProbe:
tcpSocket: { port: 8000 }
volumeMounts:
- name: app
mountPath: /app
initContainers:
- name: init-web
image: busybox:1.31.1
command: ['sh', '-c', 'wget -O- https://tinyurl.com/otus-k8s-intro | sh']
volumeMounts:
- name: app
mountPath: /app
volumes:
- name: app
emptyDir: {}
- Для начала удалим старый под из кластера:
kubectl delete pod/web --grace-period=0 --force
pod "web" deleted
- И приступим к деплою:
kubectl apply -f web-deploy.yaml
deployment.apps/web created
- Посмотрим, что получилось:
kubectl describe deployment web
Conditions:
Type Status Reason
---- ------ ------
Available False MinimumReplicasUnavailable
Progressing True ReplicaSetUpdated
OldReplicaSets: <none>
NewReplicaSet: web-dbfcc8c76 (1/1 replicas created)
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal ScalingReplicaSet 57s deployment-controller Scaled up replica set web-dbfcc8c76 to 1
- Поскольку мы не исправили ReadinessProbe , то поды, входящие в наш Deployment, не переходят в состояние Ready из-за неуспешной проверки
- Это влияет На состояние всего Deployment (строчка Available в блоке Conditions)
- Теперь самое время исправить ошибку! Поменяем в файле web-deploy.yaml следующие параметры:
- Увеличим число реплик до 3 ( replicas: 3 )
- Исправим порт в readinessProbe на порт 8000
apiVersion: apps/v1
kind: Deployment
metadata:
name: web # Название нашего объекта Deployment
spec:
replicas: 3
selector: # Укажем, какие поды относятся к нашему Deployment:
matchLabels: # - это поды с меткой
app: web # app и ее значением web
template: # Теперь зададим шаблон конфигурации пода
metadata:
name: web # Название Pod
labels: # Метки в формате key: value
app: web
spec: # Описание Pod
containers: # Описание контейнеров внутри Pod
- name: web # Название контейнера
image: kovtalex/simple-web:0.1 # Образ из которого создается контейнер
readinessProbe:
httpGet:
path: /index.html
port: 8000
livenessProbe:
tcpSocket: { port: 8000 }
volumeMounts:
- name: app
mountPath: /app
initContainers:
- name: init-web
image: busybox:1.31.1
command: ['sh', '-c', 'wget -O- https://tinyurl.com/otus-k8s-intro | sh']
volumeMounts:
- name: app
mountPath: /app
volumes:
- name: app
emptyDir: {}
- Применим изменения командой kubectl apply -f webdeploy.yaml
kubectl apply -f web-deploy.yaml
deployment.apps/web configured
- Теперь проверим состояние нашего Deployment командой kubectl describe deploy/web и убедимся, что условия (Conditions) Available и Progressing выполняются (в столбце Status значение true)
kubectl describe deployment web
Conditions:
Type Status Reason
---- ------ ------
Available True MinimumReplicasAvailable
Progressing True NewReplicaSetAvailable
- Добавим в манифест ( web-deploy.yaml ) блок strategy (можно сразу перед шаблоном пода)
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 0
maxSurge: 100%
- Применим изменения
kubectl apply -f web-deploy.yaml
deployment.apps/web configured
ROLLOUT STATUS:
- [Current rollout | Revision 8] [MODIFIED] default/web-6596d967d4
⌛ Waiting for ReplicaSet to attain minimum available Pods (0 available of a 3 minimum)
- [ContainersNotInitialized] web-6596d967d4-gvmlp containers with incomplete status: [init-web]
- [ContainersNotReady] web-6596d967d4-gvmlp containers with unready status: [web]
- [ContainersNotInitialized] web-6596d967d4-rz68n containers with incomplete status: [init-web]
- [ContainersNotReady] web-6596d967d4-rz68n containers with unready status: [web]
- [ContainersNotInitialized] web-6596d967d4-lzjlf containers with incomplete status: [init-web]
- [ContainersNotReady] web-6596d967d4-lzjlf containers with unready status: [web]
- [Previous ReplicaSet | Revision 7] [MODIFIED] default/web-54c8466885
⌛ Waiting for ReplicaSet to scale to 0 Pods (3 currently exist)
- [Ready] web-54c8466885-rmwnb
- [Ready] web-54c8466885-hf7bh
- [Ready] web-54c8466885-jxqgk
добавляются сразу 3 новых пода
- Попробуем разные варианты деплоя с крайними значениями maxSurge и maxUnavailable (оба 0, оба 100%, 0 и 100%)
- За процессом можно понаблюдать с помощью kubectl get events --watch или установить kubespy и использовать его kubespy trace deploy
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 0
maxSurge: 0
kubectl apply -f web-deploy.yaml
The Deployment "web" is invalid: spec.strategy.rollingUpdate.maxUnavailable: Invalid value: intstr.IntOrString{Type:0, IntVal:0, StrVal:""}: may not be 0 when `maxSurge` is 0
оба значения не могут быть одновременно равны 0
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 100%
maxSurge: 0
ROLLOUT STATUS:
- [Current rollout | Revision 7] [MODIFIED] default/web-54c8466885
⌛ Waiting for ReplicaSet to attain minimum available Pods (0 available of a 3 minimum)
- [ContainersNotReady] web-54c8466885-hf7bh containers with unready status: [web]
- [ContainersNotReady] web-54c8466885-jxqgk containers with unready status: [web]
- [ContainersNotReady] web-54c8466885-rmwnb containers with unready status: [web]
удаление 3 старых подов и затем создание трех новых
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 100%
maxSurge: 100%
kubectl get events -w
0s Normal Scheduled pod/web-54c8466885-b8lvv Successfully assigned default/web-54c8466885-b8lvv to minikube
0s Normal Killing pod/web-6596d967d4-chgtq Stopping container web
0s Normal Scheduled pod/web-54c8466885-v2229 Successfully assigned default/web-54c8466885-v2229 to minikube
0s Normal Scheduled pod/web-54c8466885-xjtgn Successfully assigned default/web-54c8466885-xjtgn to minikube
0s Normal Killing pod/web-6596d967d4-qcgkv Stopping container web
0s Normal Killing pod/web-6596d967d4-678v7 Stopping container web
0s Normal Pulled pod/web-54c8466885-b8lvv Container image "busybox:1.31.1" already present on machine
0s Normal Created pod/web-54c8466885-b8lvv Created container init-web
0s Normal Pulled pod/web-54c8466885-v2229 Container image "busybox:1.31.1" already present on machine
0s Normal Pulled pod/web-54c8466885-xjtgn Container image "busybox:1.31.1" already present on machine
0s Normal Created pod/web-54c8466885-v2229 Created container init-web
0s Normal Created pod/web-54c8466885-xjtgn Created container init-web
0s Normal Started pod/web-54c8466885-b8lvv Started container init-web
0s Normal Started pod/web-54c8466885-v2229 Started container init-web
0s Normal Started pod/web-54c8466885-xjtgn Started container init-web
0s Normal Pulled pod/web-54c8466885-xjtgn Container image "kovtalex/simple-web:0.2" already present on machine
0s Normal Pulled pod/web-54c8466885-v2229 Container image "kovtalex/simple-web:0.2" already present on machine
0s Normal Pulled pod/web-54c8466885-b8lvv Container image "kovtalex/simple-web:0.2" already present on machine
0s Normal Created pod/web-54c8466885-xjtgn Created container web
0s Normal Created pod/web-54c8466885-v2229 Created container web
0s Normal Created pod/web-54c8466885-b8lvv Created container web
0s Normal Started pod/web-54c8466885-xjtgn Started container web
0s Normal Started pod/web-54c8466885-v2229 Started container web
0s Normal Started pod/web-54c8466885-b8lvv Started container web
Одновременное удаление трех старых и создание трех новых подов
Для того, чтобы наше приложение было доступно внутри кластера (а тем более - снаружи), нам потребуется объект типа Service . Начнем с самого распространенного типа сервисов - ClusterIP.
- ClusterIP выделяет для каждого сервиса IP-адрес из особого диапазона (этот адрес виртуален и даже не настраивается на сетевых интерфейсах)
- Когда под внутри кластера пытается подключиться к виртуальному IP-адресу сервиса, то нода, где запущен под меняет адрес получателя в сетевых пакетах на настоящий адрес пода.
- Нигде в сети, за пределами ноды, виртуальный ClusterIP не встречается.
ClusterIP удобны в тех случаях, когда:
- Нам не надо подключаться к конкретному поду сервиса
- Нас устраивается случайное расределение подключений между подами
- Нам нужна стабильная точка подключения к сервису, независимая от подов, нод и DNS-имен
Например:
- Подключения клиентов к кластеру БД (multi-read) или хранилищу
- Простейшая (не совсем, use IPVS, Luke) балансировка нагрузки внутри кластера
Итак, создадим манифест для нашего сервиса в папке kubernetes-networks.
- Файл web-svc-cip.yaml:
apiVersion: v1
kind: Service
metadata:
name: web-svc-cip
spec:
selector:
app: web
type: ClusterIP
ports:
- protocol: TCP
port: 80
targetPort: 8000
- Применим изменения: kubectl apply -f web-svc-cip.yaml
kubectl apply -f web-svc-cip.yaml
service/web-svc-cip created
- Проверим результат (отметим назначенный CLUSTER-IP):
kubectl get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 41m
web-svc-cip ClusterIP 10.97.60.103 <none> 80/TCP 13s
Подключимся к ВМ Minikube (команда minikube ssh и затем sudo -i ):
- Сделаем curl http://10.97.60.103/index.html - работает!
sudo -i
curl http://10.97.60.103/index.html
- Сделаем ping 10.97.60.103 - пинга нет
ping 10.97.60.103
PING 10.97.60.103 (10.97.60.103) 56(84) bytes of data.
- Сделаем arp -an , ip addr show - нигде нет ClusterIP
- Сделаем iptables --list -nv -t nat - вот где наш кластерный IP!
iptables --list -nv -t nat
Chain PREROUTING (policy ACCEPT 1 packets, 60 bytes)
pkts bytes target prot opt in out source destination
39 2627 KUBE-SERVICES all -- * * 0.0.0.0/0 0.0.0.0/0 /* kubernetes service portals */
7 420 DOCKER all -- * * 0.0.0.0/0 0.0.0.0/0 ADDRTYPE match dst-type LOCAL
Chain INPUT (policy ACCEPT 1 packets, 60 bytes)
pkts bytes target prot opt in out source destination
Chain OUTPUT (policy ACCEPT 62 packets, 3720 bytes)
pkts bytes target prot opt in out source destination
2551 154K KUBE-SERVICES all -- * * 0.0.0.0/0 0.0.0.0/0 /* kubernetes service portals */
349 20940 DOCKER all -- * * 0.0.0.0/0 !127.0.0.0/8 ADDRTYPE match dst-type LOCAL
Chain POSTROUTING (policy ACCEPT 18 packets, 1080 bytes)
pkts bytes target prot opt in out source destination
2728 165K KUBE-POSTROUTING all -- * * 0.0.0.0/0 0.0.0.0/0 /* kubernetes postrouting rules */
10 657 MASQUERADE all -- * !docker0 172.18.0.0/16 0.0.0.0/0
1424 86064 KIND-MASQ-AGENT all -- * * 0.0.0.0/0 0.0.0.0/0 ADDRTYPE match dst-type !LOCAL /* kind-masq-agent: ensure nat POSTROUTING directs all non-LOCAL destination traffic to our custom KIND-MASQ-AGENT chain */
Chain DOCKER (2 references)
pkts bytes target prot opt in out source destination
0 0 RETURN all -- docker0 * 0.0.0.0/0 0.0.0.0/0
Chain KIND-MASQ-AGENT (1 references)
pkts bytes target prot opt in out source destination
0 0 RETURN all -- * * 0.0.0.0/0 10.244.0.0/16 /* kind-masq-agent: local traffic is not subject to MASQUERADE */
1424 86064 MASQUERADE all -- * * 0.0.0.0/0 0.0.0.0/0 /* kind-masq-agent: outbound traffic is subject to MASQUERADE (must be last in chain) */
Chain KUBE-KUBELET-CANARY (0 references)
pkts bytes target prot opt in out source destination
Chain KUBE-MARK-DROP (0 references)
pkts bytes target prot opt in out source destination
0 0 MARK all -- * * 0.0.0.0/0 0.0.0.0/0 MARK or 0x8000
Chain KUBE-MARK-MASQ (15 references)
pkts bytes target prot opt in out source destination
0 0 MARK all -- * * 0.0.0.0/0 0.0.0.0/0 MARK or 0x4000
Chain KUBE-NODEPORTS (1 references)
pkts bytes target prot opt in out source destination
Chain KUBE-POSTROUTING (1 references)
pkts bytes target prot opt in out source destination
0 0 MASQUERADE all -- * * 0.0.0.0/0 0.0.0.0/0 /* kubernetes service traffic requiring SNAT */ mark match 0x4000/0x4000 random-fully
Chain KUBE-PROXY-CANARY (0 references)
pkts bytes target prot opt in out source destination
Chain KUBE-SEP-5EZNUB76DNDU3ZTK (1 references)
pkts bytes target prot opt in out source destination
0 0 KUBE-MARK-MASQ all -- * * 172.18.0.3 0.0.0.0/0 /* kube-system/kube-dns:dns */
0 0 DNAT udp -- * * 0.0.0.0/0 0.0.0.0/0 /* kube-system/kube-dns:dns */ udp to:172.18.0.3:53
Chain KUBE-SEP-FBR7GW7VHBPLDP7B (1 references)
pkts bytes target prot opt in out source destination
0 0 KUBE-MARK-MASQ all -- * * 172.18.0.3 0.0.0.0/0 /* kube-system/kube-dns:metrics */
0 0 DNAT tcp -- * * 0.0.0.0/0 0.0.0.0/0 /* kube-system/kube-dns:metrics */ tcp to:172.18.0.3:9153
Chain KUBE-SEP-KLMOTHZKN3LNJ7NB (1 references)
pkts bytes target prot opt in out source destination
0 0 KUBE-MARK-MASQ all -- * * 172.18.0.2 0.0.0.0/0 /* kube-system/kube-dns:dns */
0 0 DNAT udp -- * * 0.0.0.0/0 0.0.0.0/0 /* kube-system/kube-dns:dns */ udp to:172.18.0.2:53
Chain KUBE-SEP-PATXOTJBHFPU4CNS (1 references)
pkts bytes target prot opt in out source destination
0 0 KUBE-MARK-MASQ all -- * * 172.17.0.2 0.0.0.0/0 /* default/kubernetes:https */
0 0 DNAT tcp -- * * 0.0.0.0/0 0.0.0.0/0 /* default/kubernetes:https */ tcp to:172.17.0.2:8443
Chain KUBE-SEP-PXI5DVWAQX37NI6K (1 references)
pkts bytes target prot opt in out source destination
0 0 KUBE-MARK-MASQ all -- * * 172.18.0.5 0.0.0.0/0 /* default/web-svc-cip: */
0 0 DNAT tcp -- * * 0.0.0.0/0 0.0.0.0/0 /* default/web-svc-cip: */ tcp to:172.18.0.5:8000
Chain KUBE-SEP-QBJZSVSYALF66SO6 (1 references)
pkts bytes target prot opt in out source destination
0 0 KUBE-MARK-MASQ all -- * * 172.18.0.4 0.0.0.0/0 /* default/web-svc-cip: */
0 0 DNAT tcp -- * * 0.0.0.0/0 0.0.0.0/0 /* default/web-svc-cip: */ tcp to:172.18.0.4:8000
Chain KUBE-SEP-RYTAWN2VNC6HJFUQ (1 references)
pkts bytes target prot opt in out source destination
0 0 KUBE-MARK-MASQ all -- * * 172.18.0.3 0.0.0.0/0 /* kube-system/kube-dns:dns-tcp */
0 0 DNAT tcp -- * * 0.0.0.0/0 0.0.0.0/0 /* kube-system/kube-dns:dns-tcp */ tcp to:172.18.0.3:53
Chain KUBE-SEP-S77W6PMQVTFQMRF2 (1 references)
pkts bytes target prot opt in out source destination
0 0 KUBE-MARK-MASQ all -- * * 172.18.0.2 0.0.0.0/0 /* kube-system/kube-dns:dns-tcp */
0 0 DNAT tcp -- * * 0.0.0.0/0 0.0.0.0/0 /* kube-system/kube-dns:dns-tcp */ tcp to:172.18.0.2:53
Chain KUBE-SEP-SZWO3ZNWGEEQBN7C (1 references)
pkts bytes target prot opt in out source destination
0 0 KUBE-MARK-MASQ all -- * * 172.18.0.6 0.0.0.0/0 /* default/web-svc-cip: */
0 0 DNAT tcp -- * * 0.0.0.0/0 0.0.0.0/0 /* default/web-svc-cip: */ tcp to:172.18.0.6:8000
Chain KUBE-SEP-Z2QZYSORHBODDMUQ (1 references)
pkts bytes target prot opt in out source destination
0 0 KUBE-MARK-MASQ all -- * * 172.18.0.2 0.0.0.0/0 /* kube-system/kube-dns:metrics */
0 0 DNAT tcp -- * * 0.0.0.0/0 0.0.0.0/0 /* kube-system/kube-dns:metrics */ tcp to:172.18.0.2:9153
Chain KUBE-SERVICES (2 references)
pkts bytes target prot opt in out source destination
0 0 KUBE-MARK-MASQ tcp -- * * !10.244.0.0/16 10.96.0.1 /* default/kubernetes:https cluster IP */ tcp dpt:443
0 0 KUBE-SVC-NPX46M4PTMTKRN6Y tcp -- * * 0.0.0.0/0 10.96.0.1 /* default/kubernetes:https cluster IP */ tcp dpt:443
0 0 KUBE-MARK-MASQ udp -- * * !10.244.0.0/16 10.96.0.10 /* kube-system/kube-dns:dns cluster IP */ udp dpt:53
0 0 KUBE-SVC-TCOU7JCQXEZGVUNU udp -- * * 0.0.0.0/0 10.96.0.10 /* kube-system/kube-dns:dns cluster IP */ udp dpt:53
0 0 KUBE-MARK-MASQ tcp -- * * !10.244.0.0/16 10.96.0.10 /* kube-system/kube-dns:dns-tcp cluster IP */ tcp dpt:53
0 0 KUBE-SVC-ERIFXISQEP7F7OF4 tcp -- * * 0.0.0.0/0 10.96.0.10 /* kube-system/kube-dns:dns-tcp cluster IP */ tcp dpt:53
0 0 KUBE-MARK-MASQ tcp -- * * !10.244.0.0/16 10.96.0.10 /* kube-system/kube-dns:metrics cluster IP */ tcp dpt:9153
0 0 KUBE-SVC-JD5MR3NA4I4DYORP tcp -- * * 0.0.0.0/0 10.96.0.10 /* kube-system/kube-dns:metrics cluster IP */ tcp dpt:9153
0 0 KUBE-MARK-MASQ tcp -- * * !10.244.0.0/16 10.97.60.103 /* default/web-svc-cip: cluster IP */ tcp dpt:80
0 0 KUBE-SVC-WKCOG6KH24K26XRJ tcp -- * * 0.0.0.0/0 10.97.60.103 /* default/web-svc-cip: cluster IP */ tcp dpt:80
127 7620 KUBE-NODEPORTS all -- * * 0.0.0.0/0 0.0.0.0/0 /* kubernetes service nodeports; NOTE: this must be the last rule in this chain */ ADDRTYPE match dst-type LOCAL
Chain KUBE-SVC-ERIFXISQEP7F7OF4 (1 references)
pkts bytes target prot opt in out source destination
0 0 KUBE-SEP-S77W6PMQVTFQMRF2 all -- * * 0.0.0.0/0 0.0.0.0/0 /* kube-system/kube-dns:dns-tcp */ statistic mode random probability 0.50000000000
0 0 KUBE-SEP-RYTAWN2VNC6HJFUQ all -- * * 0.0.0.0/0 0.0.0.0/0 /* kube-system/kube-dns:dns-tcp */
Chain KUBE-SVC-JD5MR3NA4I4DYORP (1 references)
pkts bytes target prot opt in out source destination
0 0 KUBE-SEP-Z2QZYSORHBODDMUQ all -- * * 0.0.0.0/0 0.0.0.0/0 /* kube-system/kube-dns:metrics */ statistic mode random probability 0.50000000000
0 0 KUBE-SEP-FBR7GW7VHBPLDP7B all -- * * 0.0.0.0/0 0.0.0.0/0 /* kube-system/kube-dns:metrics */
Chain KUBE-SVC-NPX46M4PTMTKRN6Y (1 references)
pkts bytes target prot opt in out source destination
0 0 KUBE-SEP-PATXOTJBHFPU4CNS all -- * * 0.0.0.0/0 0.0.0.0/0 /* default/kubernetes:https */
Chain KUBE-SVC-TCOU7JCQXEZGVUNU (1 references)
pkts bytes target prot opt in out source destination
0 0 KUBE-SEP-KLMOTHZKN3LNJ7NB all -- * * 0.0.0.0/0 0.0.0.0/0 /* kube-system/kube-dns:dns */ statistic mode random probability 0.50000000000
0 0 KUBE-SEP-5EZNUB76DNDU3ZTK all -- * * 0.0.0.0/0 0.0.0.0/0 /* kube-system/kube-dns:dns */
Chain KUBE-SVC-WKCOG6KH24K26XRJ (1 references)
pkts bytes target prot opt in out source destination
0 0 KUBE-SEP-QBJZSVSYALF66SO6 all -- * * 0.0.0.0/0 0.0.0.0/0 /* default/web-svc-cip: */ statistic mode random probability 0.33333333349
0 0 KUBE-SEP-PXI5DVWAQX37NI6K all -- * * 0.0.0.0/0 0.0.0.0/0 /* default/web-svc-cip: */ statistic mode random probability 0.50000000000
0 0 KUBE-SEP-SZWO3ZNWGEEQBN7C all -- * * 0.0.0.0/0 0.0.0.0/0 /* default/web-svc-cip: */
- Нужное правило находится в цепочке KUBE-SERVICES
Chain KUBE-SERVICES (2 references)
pkts bytes target prot opt in out source destination
0 0 KUBE-MARK-MASQ tcp -- * * !10.244.0.0/16 10.97.60.103 /* default/web-svc-cip: cluster IP */ tcp dpt:80
0 0 KUBE-SVC-WKCOG6KH24K26XRJ tcp -- * * 0.0.0.0/0 10.97.60.103 /* default/web-svc-cip: cluster IP */ tcp dpt:80
- Затем мы переходим в цепочку KUBE-SVC-..... - здесь находятся правила "балансировки" между цепочками KUBESEP-..... (SVC - очевидно Service)
Chain KUBE-SVC-WKCOG6KH24K26XRJ (1 references)
pkts bytes target prot opt in out source destination
0 0 KUBE-SEP-QBJZSVSYALF66SO6 all -- * * 0.0.0.0/0 0.0.0.0/0 /* default/web-svc-cip: */ statistic mode random probability 0.33333333349
0 0 KUBE-SEP-PXI5DVWAQX37NI6K all -- * * 0.0.0.0/0 0.0.0.0/0 /* default/web-svc-cip: */ statistic mode random probability 0.50000000000
0 0 KUBE-SEP-SZWO3ZNWGEEQBN7C all -- * * 0.0.0.0/0 0.0.0.0/0 /* default/web-svc-cip: */
- В цепочках KUBE-SEP-..... находятся конкретные правила перенаправления трафика (через DNAT) (SEP - Service Endpoint)
Chain KUBE-SEP-PXI5DVWAQX37NI6K (1 references)
pkts bytes target prot opt in out source destination
0 0 KUBE-MARK-MASQ all -- * * 172.18.0.5 0.0.0.0/0 /* default/web-svc-cip: */
0 0 DNAT tcp -- * * 0.0.0.0/0 0.0.0.0/0 /* default/web-svc-cip: */ tcp to:172.18.0.5:8000
Chain KUBE-SEP-QBJZSVSYALF66SO6 (1 references)
pkts bytes target prot opt in out source destination
0 0 KUBE-MARK-MASQ all -- * * 172.18.0.4 0.0.0.0/0 /* default/web-svc-cip: */
0 0 DNAT tcp -- * * 0.0.0.0/0 0.0.0.0/0 /* default/web-svc-cip: */ tcp to:172.18.0.4:8000
Chain KUBE-SEP-SZWO3ZNWGEEQBN7C (1 references)
pkts bytes target prot opt in out source destination
0 0 KUBE-MARK-MASQ all -- * * 172.18.0.6 0.0.0.0/0 /* default/web-svc-cip: */
0 0 DNAT tcp -- * * 0.0.0.0/0 0.0.0.0/0 /* default/web-svc-cip: */ tcp to:172.18.0.6:8000
Подробное описание можно почитать тут
Итак, с версии 1.0.0 Minikube поддерживает работу kubeproxy в режиме IPVS. Попробуем включить его "наживую".
При запуске нового инстанса Minikube лучше использовать ключ --extra-config и сразу указать, что мы хотим IPVS: minikube start --extra-config=kube-proxy.mode="ipvs"
- Включим IPVS для kube-proxy, исправив ConfigMap (конфигурация Pod, хранящаяся в кластере)
- Выполним команду kubectl --namespace kube-system edit configmap/kube-proxy
- Или minikube dashboard (далее надо выбрать namespace kube-system, Configs and Storage/Config Maps)
- Теперь найдем в файле конфигурации kube-proxy строку mode: ""
- Изменим значение mode с пустого на ipvs и добавим параметр strictARP: true и сохраним изменения
ipvs:
strictARP: true
mode: "ipvs"
- Теперь удалим Pod с kube-proxy, чтобы применить новую конфигурацию (он входит в DaemonSet и будет запущен автоматически)
kubectl --namespace kube-system delete pod --selector='k8s-app=kube-proxy'
pod "kube-proxy-7cwgh" deleted
Описание работы и настройки IPVS в K8S
Причины включения strictARP описаны тут
- После успешного рестарта kube-proxy выполним команду minikube ssh и проверим, что получилось
- Выполним команду iptables --list -nv -t nat в ВМ Minikube
- Что-то поменялось, но старые цепочки на месте (хотя у них теперь 0 references) �
- kube-proxy настроил все по-новому, но не удалил мусор
- Запуск kube-proxy --cleanup в нужном поде - тоже не помогает
kubectl --namespace kube-system exec kube-proxy-<POD> kube-proxy --cleanup
W0520 09:57:48.045293 606 server.go:225] WARNING: all flags other than --config, --write-config-to, and --cleanup are deprecated. Please begin using a config file ASAP.
Полностью очистим все правила iptables:
- Создадим в ВМ с Minikube файл /tmp/iptables.cleanup
*nat
-A POSTROUTING -s 172.17.0.0/16 ! -o docker0 -j MASQUERADE
COMMIT
*filter
COMMIT
*mangle
COMMIT
- Применим конфигурацию: iptables-restore /tmp/iptables.cleanup
iptables-restore /tmp/iptables.cleanup
- Теперь надо подождать (примерно 30 секунд), пока kube-proxy восстановит правила для сервисов
- Проверим результат iptables --list -nv -t nat
iptables --list -nv -t nat
Chain PREROUTING (policy ACCEPT 0 packets, 0 bytes)
pkts bytes target prot opt in out source destination
0 0 KUBE-SERVICES all -- * * 0.0.0.0/0 0.0.0.0/0 /* kubernetes service portals */
Chain INPUT (policy ACCEPT 0 packets, 0 bytes)
pkts bytes target prot opt in out source destination
Chain OUTPUT (policy ACCEPT 9 packets, 540 bytes)
pkts bytes target prot opt in out source destination
62 3720 KUBE-SERVICES all -- * * 0.0.0.0/0 0.0.0.0/0 /* kubernetes service portals */
Chain POSTROUTING (policy ACCEPT 2 packets, 120 bytes)
pkts bytes target prot opt in out source destination
61 3660 KUBE-POSTROUTING all -- * * 0.0.0.0/0 0.0.0.0/0 /* kubernetes postrouting rules */
7 420 MASQUERADE all -- * !docker0 172.17.0.0/16 0.0.0.0/0
66 3960 KIND-MASQ-AGENT all -- * * 0.0.0.0/0 0.0.0.0/0 ADDRTYPE match dst-type !LOCAL /* kind-masq-agent: ensure nat POSTROUTING directs all non-LOCAL destination traffic to our custom KIND-MASQ-AGENT chain */
Chain KIND-MASQ-AGENT (1 references)
pkts bytes target prot opt in out source destination
0 0 RETURN all -- * * 0.0.0.0/0 10.244.0.0/16 /* kind-masq-agent: local traffic is not subject to MASQUERADE */
66 3960 MASQUERADE all -- * * 0.0.0.0/0 0.0.0.0/0 /* kind-masq-agent: outbound traffic is subject to MASQUERADE (must be last in chain) */
Chain KUBE-FIREWALL (0 references)
pkts bytes target prot opt in out source destination
0 0 KUBE-MARK-DROP all -- * * 0.0.0.0/0 0.0.0.0/0
Chain KUBE-KUBELET-CANARY (0 references)
pkts bytes target prot opt in out source destination
Chain KUBE-LOAD-BALANCER (0 references)
pkts bytes target prot opt in out source destination
0 0 KUBE-MARK-MASQ all -- * * 0.0.0.0/0 0.0.0.0/0
Chain KUBE-MARK-DROP (1 references)
pkts bytes target prot opt in out source destination
Chain KUBE-MARK-MASQ (2 references)
pkts bytes target prot opt in out source destination
0 0 MARK all -- * * 0.0.0.0/0 0.0.0.0/0 MARK or 0x4000
Chain KUBE-NODE-PORT (1 references)
pkts bytes target prot opt in out source destination
Chain KUBE-POSTROUTING (1 references)
pkts bytes target prot opt in out source destination
0 0 MASQUERADE all -- * * 0.0.0.0/0 0.0.0.0/0 /* kubernetes service traffic requiring SNAT */ mark match 0x4000/0x4000 random-fully
0 0 MASQUERADE all -- * * 0.0.0.0/0 0.0.0.0/0 /* Kubernetes endpoints dst ip:port, source ip for solving hairpin purpose */ match-set KUBE-LOOP-BACK dst,dst,src
Chain KUBE-SERVICES (2 references)
pkts bytes target prot opt in out source destination
0 0 KUBE-MARK-MASQ all -- * * !10.244.0.0/16 0.0.0.0/0 /* Kubernetes service cluster ip + port for masquerade purpose */ match-set KUBE-CLUSTER-IP dst,dst
6 360 KUBE-NODE-PORT all -- * * 0.0.0.0/0 0.0.0.0/0 ADDRTYPE match dst-type LOCAL
0 0 ACCEPT all -- * * 0.0.0.0/0 0.0.0.0/0 match-set KUBE-CLUSTER-IP dst,dst
- Итак, лишние правила удалены и мы видим только актуальную конфигурацию
- kube-proxy периодически делает полную синхронизацию правил в своих цепочках)
- Как посмотреть конфигурацию IPVS? Ведь в ВМ нет утилиты ipvsadm ?
- В ВМ выполним команду toolbox - в результате мы окажется в контейнере с Fedora
- Теперь установим ipvsadm: dnf install -y ipvsadm && dnf clean all
Выполним ipvsadm --list -n и среди прочих сервисов найдем наш:
ipvsadm --list -n
IP Virtual Server version 1.2.1 (size=4096)
Prot LocalAddress:Port Scheduler Flags
-> RemoteAddress:Port Forward Weight ActiveConn InActConn
TCP 10.97.60.103:80 rr
-> 172.18.0.4:8000 Masq 1 0 0
-> 172.18.0.5:8000 Masq 1 0 0
-> 172.18.0.6:8000 Masq 1 0 0
- Теперь выйдем из контейнера toolbox и сделаем ping кластерного IP:
ping 10.97.60.103
PING 10.97.60.103 (10.97.60.103) 56(84) bytes of data.
64 bytes from 10.97.60.103: icmp_seq=1 ttl=64 time=0.030 ms
64 bytes from 10.97.60.103: icmp_seq=2 ttl=64 time=0.077 ms
64 bytes from 10.97.60.103: icmp_seq=3 ttl=64 time=0.038 ms
64 bytes from 10.97.60.103: icmp_seq=4 ttl=64 time=0.064 ms
Итак, все работает. Но почему пингуется виртуальный IP?
Все просто - он уже не такой виртуальный. Этот IP теперь есть на интерфейсе kube-ipvs0:
ip addr show kube-ipvs0
17: kube-ipvs0: <BROADCAST,NOARP> mtu 1500 qdisc noop state DOWN group default
link/ether a6:de:ae:75:df:04 brd ff:ff:ff:ff:ff:ff
inet 10.97.60.103/32 brd 10.97.60.103 scope global kube-ipvs0
valid_lft forever preferred_lft forever
Также, правила в iptables построены по-другому. Вместо цепочки правил для каждого сервиса, теперь используются хэш-таблицы (ipset). Можем посмотреть их, установив утилиту ipset в toolbox .
ipset list
Name: KUBE-LOOP-BACK
Type: hash:ip,port,ip
Revision: 5
Header: family inet hashsize 1024 maxelem 65536
Size in memory: 816
References: 1
Number of entries: 9
Members:
172.18.0.4,6:8000,172.18.0.4
172.18.0.5,6:8000,172.18.0.5
172.18.0.6,6:8000,172.18.0.6
Name: KUBE-CLUSTER-IP
Type: hash:ip,port
Revision: 5
Header: family inet hashsize 1024 maxelem 65536
Size in memory: 408
References: 2
Number of entries: 5
Members:
10.97.60.103,6:80
MetalLB позволяет запустить внутри кластера L4-балансировщик, который будет принимать извне запросы к сервисам и раскидывать их между подами. Установка его проста:
kubectl apply -f https://raw.githubusercontent.com/metallb/metallb/v0.9.3/manifests/namespace.yaml
kubectl apply -f https://raw.githubusercontent.com/metallb/metallb/v0.9.3/manifests/metallb.yaml
kubectl create secret generic -n metallb-system memberlist --from-literal=secretkey="$(openssl rand -base64 128)"
❗ В продуктиве так делать не надо. Сначала стоит скачать файл и разобраться, что там внутри
Проверим, что были созданы нужные объекты:
kubectl --namespace metallb-system get all
NAME READY STATUS RESTARTS AGE
pod/controller-5468756d88-nxh2f 1/1 Running 0 19s
pod/speaker-rkb5s 1/1 Running 0 19s
NAME DESIRED CURRENT READY UP-TO-DATE AVAILABLE NODE SELECTOR AGE
daemonset.apps/speaker 1 1 1 1 1 beta.kubernetes.io/os=linux 19s
NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/controller 1/1 1 1 19s
NAME DESIRED CURRENT READY AGE
replicaset.apps/controller-5468756d88 1 1 1 19s
Теперь настроим балансировщик с помощью ConfigMap
- Создадмс манифест metallb-config.yaml в папке kubernetes-networks:
apiVersion: v1
kind: ConfigMap
metadata:
namespace: metallb-system
name: config
data:
config: |
address-pools:
- name: default
protocol: layer2
addresses:
- "172.17.255.1-172.17.255.255"
- В конфигурации мы настраиваем:
- Режим L2 (анонс адресов балансировщиков с помощью ARP)
- Создаем пул адресов 172.17.255.1-172.17.255.255 - они будут назначаться сервисам с типом LoadBalancer
- Теперь можно применить наш манифест: kubectl apply -f metallb-config.yaml
- Контроллер подхватит изменения автоматически
kubectl apply -f metallb-config.yaml
configmap/config created
Сделаем копию файла web-svc-cip.yaml в web-svclb.yaml и откроем его в редакторе:
apiVersion: v1
kind: Service
metadata:
name: web-svc-lb
spec:
selector:
app: web
type: LoadBalancer
ports:
- protocol: TCP
port: 80
targetPort: 8000
- Применим манифест
kubectl apply -f web-svc-lb.yaml
service/web-svc-lb created
- Теперь посмотрим логи пода-контроллера MetalLB
kubectl --namespace metallb-system logs pod/controller-5468756d88-flqbf
{"caller":"service.go:98","event":"ipAllocated","ip":"172.17.255.1","msg":"IP address assigned by controller","service":"default/web-svc-lb","ts":"2020-05-21T19:38:21.161120726Z"}
Обратим внимание на назначенный IP-адрес (или посмотрим его в выводе kubectl describe svc websvc-lb)
kubectl describe svc web-svc-lb
Name: web-svc-lb
Namespace: default
Labels: <none>
Annotations: kubectl.kubernetes.io/last-applied-configuration:
{"apiVersion":"v1","kind":"Service","metadata":{"annotations":{},"name":"web-svc-lb","namespace":"default"},"spec":{"ports":[{"port":80,"p...
Selector: app=web
Type: LoadBalancer
IP: 10.103.160.42
LoadBalancer Ingress: 172.17.255.1
Port: <unset> 80/TCP
TargetPort: 8000/TCP
NodePort: <unset> 32615/TCP
Endpoints: 172.17.0.5:8000,172.17.0.6:8000,172.17.0.7:8000
Session Affinity: None
External Traffic Policy: Cluster
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal IPAllocated 106s metallb-controller Assigned IP "172.17.255.1"
-
Если мы попробуем открыть URL http://172.17.255.1/index.html, то... ничего не выйдет.
-
Это потому, что сеть кластера изолирована от нашей основной ОС (а ОС не знает ничего о подсети для балансировщиков)
-
Чтобы это поправить, добавим статический маршрут:
- В реальном окружении это решается добавлением нужной подсети на интерфейс сетевого оборудования
- Или использованием L3-режима (что потребует усилий от сетевиков, но более предпочтительно)
-
Найдем IP-адрес виртуалки с Minikube. Например так:
minikube ssh
ip addr show eth0
42: eth0@if43: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
link/ether 02:42:ac:11:00:02 brd ff:ff:ff:ff:ff:ff link-netnsid 0
inet 172.17.0.2/16 brd 172.17.255.255 scope global eth0
valid_lft forever preferred_lft forever
- Добавим маршрут в вашей ОС на IP-адрес Minikube:
sudo route add 172.17.255.0/24 172.17.0.2
DISCLAIMER:
Добавление маршрута может иметь другой синтаксис (например, ip route add 172.17.255.0/24 via 192.168.64.4 в ОС Linux) или вообще не сработать (в зависимости от VM Driver в Minkube).
В этом случае, не надо расстраиваться - работу наших сервисов и манифестов можно проверить из консоли Minikube, просто будет не так эффектно.
P.S. - Самый простой способ найти IP виртуалки с minikube - minikube ip
Все получилось, можно открыть в браузере URL с IP-адресом нашего балансировщика и посмотреть, как космические корабли бороздят просторы вселенной.
Если пообновлять страничку с помощью Ctrl-F5 (т.е. игнорируя кэш), то будет видно, что каждый наш запрос приходит на другой под. Причем, порядок смены подов - всегда один и тот же.
Так работает IPVS - по умолчанию он использует rr (Round-Robin) балансировку.
К сожалению, выбрать алгоритм на уровне манифеста сервиса нельзя. Но когда-нибудь, эта полезная фича появится.
Доступные алгоритмы балансировки описаны здесь и появится здесь.
- Сделаем сервис LoadBalancer, который откроет доступ к CoreDNS снаружи кластера (позволит получать записи через внешний IP). Например, nslookup web.default.cluster.local 172.17.255.10.
- Поскольку DNS работает по TCP и UDP протоколам - учтем это в конфигурации. Оба протокола должны работать по одному и тому же IP-адресу балансировщика.
- Полученные манифесты положим в подкаталог ./coredns
😉 Hint
Для выполнения задания создадим манифест с двумя сервисами типа LB включающие размещение на общем IP:
- аннотацию metallb.universe.tf/allow-shared-ip равную для обоих сервисов
- spec.loadBalancerIP равный для обоих сервисов
coredns-svc-lb.yaml
apiVersion: v1
kind: Service
metadata:
name: coredns-svc-lb-tcp
annotations:
metallb.universe.tf/allow-shared-ip: coredns
spec:
loadBalancerIP: 172.17.255.2
selector:
k8s-app: kube-dns
type: LoadBalancer
ports:
- protocol: TCP
port: 53
targetPort: 53
---
apiVersion: v1
kind: Service
metadata:
name: coredns-svc-lb-udp
annotations:
metallb.universe.tf/allow-shared-ip: coredns
spec:
loadBalancerIP: 172.17.255.2
selector:
k8s-app: kube-dns
type: LoadBalancer
ports:
- protocol: UDP
port: 53
targetPort: 53
Применим манифест:
kubectl apply -f coredns-svc-lb.yaml -n kube-system
service/coredns-svc-lb-tcp created
service/coredns-svc-lb-udp created
Проверим, что сервисы создались:
kubectl get svc -n kube-system | grep coredns-svc
coredns-svc-lb-tcp LoadBalancer 10.111.58.253 172.17.255.2 53:31250/TCP 90s
coredns-svc-lb-udp LoadBalancer 10.96.243.226 172.17.255.2 53:32442/UDP 89s
Обратимся к DNS:
nslookup web-svc-cip.default.svc.cluster.local 172.17.255.2
Server: 172.17.255.2
Address: 172.17.255.2#53
Name: web-svc-cip.default.svc.cluster.local
Address: 10.104.155.78
Теперь, когда у нас есть балансировщик, можно заняться Ingress-контроллером и прокси:
- неудобно, когда на каждый Web-сервис надо выделять свой IP-адрес
- а еще хочется балансировку по HTTP-заголовкам (sticky sessions)
Для нашего домашнего задания возьмем почти "коробочный" ingress-nginx от проекта Kubernetes. Это "достаточно хороший" Ingress для умеренных нагрузок, основанный на OpenResty и пачке Lua-скриптов.
- Установка начинается с основного манифеста:
kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingressnginx/master/deploy/static/provider/cloud/deploy.yaml
namespace/ingress-nginx created
configmap/nginx-configuration created
configmap/tcp-services created
configmap/udp-services created
serviceaccount/nginx-ingress-serviceaccount created
clusterrole.rbac.authorization.k8s.io/nginx-ingress-clusterrole created
role.rbac.authorization.k8s.io/nginx-ingress-role created
rolebinding.rbac.authorization.k8s.io/nginx-ingress-role-nisa-binding created
clusterrolebinding.rbac.authorization.k8s.io/nginx-ingress-clusterrole-nisa-binding created
deployment.apps/nginx-ingress-controller created
limitrange/ingress-nginx created
- После установки основных компонентов, в инструкции рекомендуется применить манифест, который создаст NodePort -сервис. Но у нас есть MetalLB, мы можем сделать круче.
Можно сделать просто minikube addons enable ingress , но мы не ищем легких путей
Проверим, что контроллер запустился:
kubectl get pods -n ingress-nginx
NAME READY STATUS RESTARTS AGE
nginx-ingress-controller-5bb8fb4bb6-rvkz5 1/1 Running 0 2m2s
Создадим файл nginx-lb.yaml c конфигурацией LoadBalancer - сервиса (работаем в каталоге kubernetes-networks):
kind: Service
apiVersion: v1
metadata:
name: ingress-nginx
namespace: ingress-nginx
labels:
app.kubernetes.io/name: ingress-nginx
app.kubernetes.io/part-of: ingress-nginx
spec:
externalTrafficPolicy: Local
type: LoadBalancer
selector:
app.kubernetes.io/name: ingress-nginx
app.kubernetes.io/part-of: ingress-nginx
ports:
- name: http
port: 80
targetPort: http
- name: https
port: 443
targetPort: https
- Теперь применим созданный манифест и посмотрим на IP-адрес, назначенный ему MetalLB
kubectl get svc -n ingress-nginx
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
ingress-nginx LoadBalancer 10.109.249.5 172.17.255.3 80:30552/TCP,443:30032/TCP 5m13s
- Теперь можно сделать пинг на этот IP-адрес и даже curl
curl 172.17.255.3
<html>
<head><title>404 Not Found</title></head>
<body>
<center><h1>404 Not Found</h1></center>
<hr><center>nginx/1.17.8</center>
</body>
</html>
Видим страничку 404 от Nginx - значит работает!
- Наш Ingress-контроллер не требует ClusterIP для балансировки трафика
- Список узлов для балансировки заполняется из ресурса Endpoints нужного сервиса (это нужно для "интеллектуальной" балансировки, привязки сессий и т.п.)
- Поэтому мы можем использовать headless-сервис для нашего вебприложения.
- Скопируем web-svc-cip.yaml в web-svc-headless.yaml
- Изменим имя сервиса на web-svc
- Добавим параметр clusterIP: None
apiVersion: v1
kind: Service
metadata:
name: web-svc
spec:
selector:
app: web
type: ClusterIP
clusterIP: None
ports:
- protocol: TCP
port: 80
targetPort: 8000
- Теперь применим полученный манифест и проверим, что ClusterIP для сервиса web-svc действительно не назначен
kubectl apply -f web-svc-headless.yaml
service/web-svc created
kubectl get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
web-svc ClusterIP None <none> 80/TCP 32s
Теперь настроим наш ingress-прокси, создав манифест с ресурсом Ingress (файл назовем web-ingress.yaml):
apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
name: web
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
spec:
rules:
- http:
paths:
- path: /web
backend:
serviceName: web-svc
servicePort: 8000
Применим манифест и проверим, что корректно заполнены Address и Backends:
kubectl apply -f web-ingress.yaml
ingress.networking.k8s.io/web created
kubectl describe ingress/web
Name: web
Namespace: default
Address: 172.17.255.3
Default backend: default-http-backend:80 (<none>)
Rules:
Host Path Backends
---- ---- --------
*
/web web-svc:8000 (172.17.0.5:8000,172.17.0.6:8000,172.17.0.7:8000)
Annotations:
kubectl.kubernetes.io/last-applied-configuration: {"apiVersion":"networking.k8s.io/v1beta1","kind":"Ingress","metadata":{"annotations":{"nginx.ingress.kubernetes.io/rewrite-target":"/"},"name":"web","namespace":"default"},"spec":{"rules":[{"http":{"paths":[{"backend":{"serviceName":"web-svc","servicePort":8000},"path":"/web"}]}}]}}
nginx.ingress.kubernetes.io/rewrite-target: /
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal CREATE 24s nginx-ingress-controller Ingress default/web
Normal UPDATE 4s nginx-ingress-controller Ingress default/web
- Теперь можно проверить, что страничка доступна в браузере (http://172.17.255.3/web/index.html)
- Обратим внимание, что обращения к странице тоже балансируются между Podами. Только сейчас это происходит средствами nginx, а не IPVS
Добавим доступ к kubernetes-dashboard через наш Ingress-прокси:
- Cервис должен быть доступен через префикс /dashboard.
- Kubernetes Dashboard должен быть развернут из официального манифеста. Актуальная ссылка в репозитории проекта.
- Написанные манифесты положим в подкаталог ./dashboard
kubectl apply -f https://raw.githubusercontent.com/kubernetes/dashboard/v2.0.1/aio/deploy/recommended.yaml
namespace/kubernetes-dashboard created
serviceaccount/kubernetes-dashboard created
service/kubernetes-dashboard created
secret/kubernetes-dashboard-certs created
secret/kubernetes-dashboard-csrf created
secret/kubernetes-dashboard-key-holder created
configmap/kubernetes-dashboard-settings created
role.rbac.authorization.k8s.io/kubernetes-dashboard created
clusterrole.rbac.authorization.k8s.io/kubernetes-dashboard created
rolebinding.rbac.authorization.k8s.io/kubernetes-dashboard created
clusterrolebinding.rbac.authorization.k8s.io/kubernetes-dashboard created
deployment.apps/kubernetes-dashboard created
service/dashboard-metrics-scraper created
deployment.apps/dashboard-metrics-scraper created
dashboard-ingress.yaml
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: dashboard
annotations:
nginx.ingress.kubernetes.io/backend-protocol: "HTTPS"
nginx.ingress.kubernetes.io/rewrite-target: /$2
namespace: kubernetes-dashboard
spec:
rules:
- http:
paths:
- path: /dashboard(/|$)(.*)
backend:
serviceName: kubernetes-dashboard
servicePort: 443
Аннотация nginx.ingress.kubernetes.io/rewrite-target перезаписывает URL-адрес перед отправкой запроса на бэкэнд подов.
В /dashboard(/|$)(.*) для пути (. *) хранится динамический URL, который генерируется при доступе к Kubernetes Dashboard.
Аннотация nginx.ingress.kubernetes.io/rewrite-target заменяет захваченные данные в URL-адресе перед отправкой запроса в сервис kubernetes-dashboard
Применим наш манифест:
kubectl apply -f dashboard-ingress.yaml
ingress.extensions/dashboard configured
kubectl get ingress -n kubernetes-dashboard
NAME CLASS HOSTS ADDRESS PORTS AGE
dashboard <none> * 172.17.255.3 80 12h
Проверим работоспособность по ссылке: https://172.17.255.3/dashboard/
Реализуем канареечное развертывание с помощью ingress-nginx:
- Перенаправление части трафика на выделенную группу подов должно происходить по HTTP-заголовку.
- Документация тут
- Естественно, что нам понадобятся 1-2 "канареечных" пода. Написанные манифесты положим в подкаталог ./canary
Пишем манифесты для:
- namespace canary-ns.yaml
- deployment canary-deploy.yaml
- service canary-svc-headless.yaml
- ingress canary-ingress.yml
apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
name: web
namespace: canary
annotations:
kubernetes.io/ingress.class: nginx
nginx.ingress.kubernetes.io/rewrite-target: /
nginx.ingress.kubernetes.io/canary: "true"
nginx.ingress.kubernetes.io/canary-by-header: "canary"
spec:
rules:
- host: app.local
http:
paths:
- path: /web
backend:
serviceName: web-svc
servicePort: 8000
И применяем:
kubectl apply -f .
namespace/canary unchanged
deployment.apps/web created
service/web-svc created
ingress.networking.k8s.io/web created
Так же придется впиcать host: app.local (к примеру) в манифест web-ingress.yaml
иначе на ingress контроллере валится ошибка: cannot merge alternative backend canary-web-svc-8000 into hostname that does not exist
Запоминаем названия pods:
kubectl get pods
NAME READY STATUS RESTARTS AGE
web-6596d967d4-fw9px 1/1 Running 0 3h4m
web-6596d967d4-pd65t 1/1 Running 0 3h4m
web-6596d967d4-znkmv 1/1 Running 0 3h4m
kubectl get pods -n canary
NAME READY STATUS RESTARTS AGE
web-54c8466885-f8nn6 1/1 Running 0 93m
web-54c8466885-gtk6x 1/1 Running 0 93m
И проверяем работу:
curl -s -H "Host: app.local" http://172.17.255.2/web/index.html | grep "HOSTNAME"
export HOSTNAME='web-6596d967d4-fw9px'
curl -s -H "Host: app.local" -H "canary: always" http://172.17.255.2/web/index.html | grep "HOSTNAME"
export HOSTNAME='web-54c8466885-f8nn6'
- Создадим Service Account bob и дади ему роль admin в рамках всего кластера
01-serviceAccount.yaml:
apiVersion: v1
kind: ServiceAccount
metadata:
name: bob
02-clusterRoleBinding.yaml:
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: bob
roleRef:
kind: ClusterRole
name: admin
apiGroup: rbac.authorization.k8s.io
subjects:
- kind: ServiceAccount
name: bob
namespace: default
- Создадим Service Account dave без доступа к кластеру
03-serviceAccount.yaml:
apiVersion: v1
kind: ServiceAccount
metadata:
name: dave
- Создадим Namespace prometheus
01-namespace.yaml:
apiVersion: v1
kind: Namespace
metadata:
name: prometheus
- Создадим Service Account carol в этом Namespace
02-serviceAccount.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: carol
namespace: prometheus
- Дадим всем Service Account в Namespace prometheus возможность делать get, list, watch в отношении Pods всего кластера
03-clusterRole.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: prometheus
rules:
- apiGroups: [""]
verbs: ["get", "list", "watch"]
resources: ["pods"]
04-clusterRoleBinding.yaml
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: prometheus
roleRef:
kind: ClusterRole
name: prometheus
apiGroup: rbac.authorization.k8s.io
subjects:
- kind: Group
name: system:serviceaccounts:prometheus
apiGroup: rbac.authorization.k8s.io
- Создадим Namespace dev
01-namespace.yaml
apiVersion: v1
kind: Namespace
metadata:
name: dev
- Создадим Service Account jane в Namespace dev
02-serviceAccount.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: jane
namespace: dev
- Дадим jane роль admin в рамках Namespace dev
03-RoleBinding.yaml
kind: RoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: jane
namespace: dev
roleRef:
kind: ClusterRole
name: admin
apiGroup: rbac.authorization.k8s.io
subjects:
- kind: ServiceAccount
name: jane
namespace: dev
- Создади Service Account ken в Namespace dev
04-serviceAccount.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: ken
namespace: dev
- Дадим ken роль view в рамках Namespace dev
05-RoleBinding.yaml
kind: RoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: ken
namespace: dev
roleRef:
kind: ClusterRole
name: view
apiGroup: rbac.authorization.k8s.io
subjects:
- kind: ServiceAccount
name: ken
namespace: dev
Для начала установим Kind и создадим кластер. Инструкция по быстрому старту.
brew install kind
Будем использовать следующую конфигурацию нашего локального кластера kind-config.yml
kind: Cluster
apiVersion: kind.sigs.k8s.io/v1alpha3
nodes:
- role: control-plane
- role: control-plane
- role: control-plane
- role: worker
- role: worker
- role: worker
Запустим создание кластера kind:
kind create cluster --config kind-config.yaml
Creating cluster "kind" ...
✓ Ensuring node image (kindest/node:v1.18.2) 🖼
✓ Preparing nodes 📦 📦 📦 📦 📦 📦
✓ Configuring the external load balancer ⚖️
✓ Writing configuration 📜
✓ Starting control-plane 🕹️
✓ Installing CNI 🔌
✓ Installing StorageClass 💾
✓ Joining more control-plane nodes 🎮
✓ Joining worker nodes 🚜
Set kubectl context to "kind-kind"
You can now use your cluster with:
kubectl cluster-info --context kind-kind
После появления отчета об успешном создании убедимся, что развернуто три master ноды и три worker ноды:
kubectl get nodes
NAME STATUS ROLES AGE VERSION
kind-control-plane Ready master 3m13s v1.18.2
kind-control-plane2 Ready master 2m34s v1.18.2
kind-control-plane3 Ready master 98s v1.18.2
kind-worker Ready <none> 70s v1.18.2
kind-worker2 Ready <none> 71s v1.18.2
kind-worker3 Ready <none> 71s v1.18.2
В предыдущем домашнем задании мы запускали standalone pod с микросервисом frontend. Пришло время доверить управление pod'ами данного микросервиса одному из контроллеров Kubernetes.
Начнем с ReplicaSet и запустим одну реплику микросервиса frontend.
Создадим и применим манифест frontend-replicaset.yaml
apiVersion: apps/v1
kind: ReplicaSet
metadata:
name: frontend
labels:
app: frontend
spec:
replicas: 1
selector:
matchLabels:
app: frontend
template:
metadata:
labels:
app: frontend
spec:
containers:
- name: server
image: kovtalex/hipster-frontend:v0.0.2
env:
- name: PRODUCT_CATALOG_SERVICE_ADDR
value: "productcatalogservice:3550"
- name: CURRENCY_SERVICE_ADDR
value: "currencyservice:7000"
- name: CART_SERVICE_ADDR
value: "cartservice:7070"
- name: RECOMMENDATION_SERVICE_ADDR
value: "recommendationservice:8080"
- name: SHIPPING_SERVICE_ADDR
value: "shippingservice:50051"
- name: CHECKOUT_SERVICE_ADDR
value: "checkoutservice:5050"
- name: AD_SERVICE_ADDR
value: "adservice:9555"
kubectl apply -f frontend-replicaset.yaml
В результате вывод команды kubectl get pods -l app=frontend должен показывать, что запущена одна реплика микросервиса frontend:
kubectl get pods -l app=frontend
NAME READY STATUS RESTARTS AGE
frontend-zl2wj 1/1 Running 0 14s
Одна работающая реплика - это уже неплохо, но в реальной жизни, как правило, требуется создание нескольких инстансов одного и того же сервиса для:
- Повышения отказоустойчивости
- Распределения нагрузки между репликами
Давайте попробуем увеличить количество реплик сервиса ad-hoc командой:
kubectl scale replicaset frontend --replicas=3
Проверить, что ReplicaSet контроллер теперь управляет тремя репликами, и они готовы к работе, можно следующим образом:
kubectl get rs frontend
NAME DESIRED CURRENT READY AGE
frontend 3 3 3 5m25s
Проверим, что благодаря контроллеру pod'ы действительно восстанавливаются после их ручного удаления:
kubectl delete pods -l app=frontend | kubectl get pods -l app=frontend -w
NAME READY STATUS RESTARTS AGE
frontend-9xvfj 1/1 Running 0 108s
frontend-rgfsf 1/1 Running 0 108s
frontend-zl2wj 1/1 Running 0 3m6s
frontend-9xvfj 1/1 Terminating 0 108s
frontend-rgfsf 1/1 Terminating 0 109s
frontend-zl2wj 1/1 Terminating 0 3m7s
frontend-ptk2f 0/1 Pending 0 0s
frontend-5m7kl 0/1 Pending 0 0s
frontend-ptk2f 0/1 Pending 0 0s
frontend-5m7kl 0/1 Pending 0 0s
frontend-ptk2f 0/1 ContainerCreating 0 0s
frontend-5m7kl 0/1 ContainerCreating 0 0s
frontend-7nzld 0/1 Pending 0 0s
frontend-7nzld 0/1 Pending 0 0s
frontend-7nzld 0/1 ContainerCreating 0 0s
frontend-zl2wj 0/1 Terminating 0 3m7s
frontend-rgfsf 0/1 Terminating 0 110s
frontend-9xvfj 0/1 Terminating 0 110s
frontend-zl2wj 0/1 Terminating 0 3m8s
frontend-zl2wj 0/1 Terminating 0 3m8s
frontend-7nzld 1/1 Running 0 2s
frontend-5m7kl 1/1 Running 0 2s
frontend-9xvfj 0/1 Terminating 0 111s
frontend-9xvfj 0/1 Terminating 0 111s
frontend-ptk2f 1/1 Running 0 2s
frontend-rgfsf 0/1 Terminating 0 2m
frontend-rgfsf 0/1 Terminating 0 2m
- Повторно применим манифест frontend-replicaset.yaml
- Убедимся, что количество реплик вновь уменьшилось до одной
kubectl apply -f frontend-replicaset.yaml
kubectl get rs frontend
NAME DESIRED CURRENT READY AGE
frontend 1 1 1 8m55s
- Изменим манифест таким образом, чтобы из манифеста сразу разворачивалось три реплики сервиса, вновь применим его
kubectl apply -f frontend-replicaset.yaml
kubectl get rs frontend
NAME DESIRED CURRENT READY AGE
frontend 3 3 3 9m44s
Давайте представим, что мы обновили исходный код и хотим выкатить новую версию микросервиса
- Добавим на DockerHub версию образа с новым тегом (v0.0.2, можно просто перетегировать старый образ)
docker build -t kovtalex/hipster-frontend:v0.0.2 .
docker push kovtalex/hipster-frontend:v0.0.2
- Обновим в манифесте версию образа
- Применим новый манифест, параллельно запустим отслеживание происходящего:
kubectl apply -f frontend-replicaset.yaml | kubectl get pods -l app=frontend -w
NAME READY STATUS RESTARTS AGE
frontend-2g8vl 1/1 Running 0 3m19s
frontend-7nzld 1/1 Running 0 6m11s
frontend-hgsx4 1/1 Running 0 3m19s
Давайте проверим образ, указанный в ReplicaSet:
kubectl get replicaset frontend -o=jsonpath='{.spec.template.spec.containers[0].image}'
kovtalex/hipster-frontend:v0.0.2%
И образ из которого сейчас запущены pod, управляемые контроллером:
kubectl get pods -l app=frontend -o=jsonpath='{.items[0:3].spec.containers[0].image}'
kovtalex/hipster-frontend:v0.0.1 kovtalex/hipster-frontend:v0.0.1 kovtalex/hipster-frontend:v0.0.1%
Обратим внимание на использование ключа -o jsonpath для форматирования вывода. Подробнее с данным функционалом kubectl можно ознакомиться по ссылке.
- Удалим все запущенные pod и после их пересоздания еще раз проверим, из какого образа они развернулись
kubectl get pods -l app=frontend -o=jsonpath='{.items[0:3].spec.containers[0].image}'
kovtalex/hipster-frontend:v0.0.2 kovtalex/hipster-frontend:v0.0.2 kovtalex/hipster-frontend:v0.0.2%
Обновление ReplicaSet не повлекло обновление запущенных pod по причине того, что ReplicaSet не умеет рестартовать запущенные поды при обновлении шаблона
Для начала - воспроизведем действия, проделанные с микросервисом frontend для микросервиса paymentService.
Результат:
- Собранный и помещенный в Docker Hub образ с двумя тегами v0.0.1 и v0.0.2
- Валидный манифест paymentservice-replicaset.yaml с тремя репликами, разворачивающими из образа версии v0.0.1
docker build -t kovtalex/hipster-paymentservice:v0.0.1 .
docker build -t kovtalex/hipster-paymentservice:v0.0.2 .
docker push kovtalex/hipster-paymentservice:v0.0.1
docker push kovtalex/hipster-paymentservice:v0.0.2
Приступим к написанию Deployment манифеста для сервиса payment
- Скопируем содержимое файла paymentservicereplicaset.yaml в файл paymentservice-deployment.yaml
- Изменим поле kind с ReplicaSet на Deployment
- Манифест готов 😉 Применим его и убедимся, что в кластере Kubernetes действительно запустилось три реплики сервиса payment и каждая из них находится в состоянии Ready
- Обратим внимание, что помимо Deployment (kubectl get deployments) и трех pod, у нас появился новый ReplicaSet (kubectl get rs)
apiVersion: apps/v1
kind: Deployment
metadata:
name: frontend
labels:
app: frontend
spec:
replicas: 3
selector:
matchLabels:
app: frontend
template:
metadata:
labels:
app: frontend
spec:
containers:
- name: server
image: kovtalex/hipster-frontend:v0.0.1
env:
- name: PRODUCT_CATALOG_SERVICE_ADDR
value: "productcatalogservice:3550"
- name: CURRENCY_SERVICE_ADDR
value: "currencyservice:7000"
- name: CART_SERVICE_ADDR
value: "cartservice:7070"
- name: RECOMMENDATION_SERVICE_ADDR
value: "recommendationservice:8080"
- name: SHIPPING_SERVICE_ADDR
value: "shippingservice:50051"
- name: CHECKOUT_SERVICE_ADDR
value: "checkoutservice:5050"
- name: AD_SERVICE_ADDR
value: "adservice:9555"
kubectl apply -f paymentservice-replicaset.yaml
kubectl get rs paymentservice
NAME DESIRED CURRENT READY AGE
paymentservice 3 3 3 22s
kubectl apply -f paymentservice-deployment.yaml
kubectl get rs -l app=paymentservice
NAME DESIRED CURRENT READY AGE
paymentservice-84f44df66 3 3 3 60s
kubectl get deployments
NAME READY UP-TO-DATE AVAILABLE AGE
paymentservice 3/3 3 3 112s
Давайте попробуем обновить наш Deployment на версию образа v0.0.2
Обратим внимание на последовательность обновления pod. По умолчанию применяется стратегия Rolling Update:
- Создание одного нового pod с версией образа v0.0.2
- Удаление одного из старых pod
- Создание еще одного нового pod
- ...
kubectl apply -f paymentservice-deployment.yaml | kubectl get pods -l app=paymentservice -w
NAME READY STATUS RESTARTS AGE
paymentservice-84f44df66-h2hxp 1/1 Running 0 3m14s
paymentservice-84f44df66-jwrsj 1/1 Running 0 3m14s
paymentservice-84f44df66-kzrkr 1/1 Running 0 3m14s
paymentservice-7845cdfff9-tmxgp 0/1 Pending 0 0s
paymentservice-7845cdfff9-tmxgp 0/1 Pending 0 0s
paymentservice-7845cdfff9-tmxgp 0/1 ContainerCreating 0 0s
paymentservice-7845cdfff9-tmxgp 1/1 Running 0 3s
paymentservice-84f44df66-h2hxp 1/1 Terminating 0 3m18s
paymentservice-7845cdfff9-5c7q8 0/1 Pending 0 0s
paymentservice-7845cdfff9-5c7q8 0/1 Pending 0 1s
paymentservice-7845cdfff9-5c7q8 0/1 ContainerCreating 0 1s
paymentservice-7845cdfff9-5c7q8 1/1 Running 0 4s
paymentservice-84f44df66-jwrsj 1/1 Terminating 0 3m22s
paymentservice-7845cdfff9-bs59h 0/1 Pending 0 0s
paymentservice-7845cdfff9-bs59h 0/1 Pending 0 0s
paymentservice-7845cdfff9-bs59h 0/1 ContainerCreating 0 1s
paymentservice-7845cdfff9-bs59h 1/1 Running 0 5s
paymentservice-84f44df66-kzrkr 1/1 Terminating 0 3m28s
Убедимся что:
- Все новые pod развернуты из образа v0.0.2
- Создано два ReplicaSet:
- Один (новый) управляет тремя репликами pod с образом v0.0.2
- Второй (старый) управляет нулем реплик pod с образом v0.0.1
Также мы можем посмотреть на историю версий нашего Deployment:
kubectl get pods -l app=paymentservice -o=jsonpath='{.items[0:3].spec.containers[0].image}'
kovtalex/hipster-paymentservice:v0.0.2 kovtalex/hipster-paymentservice:v0.0.2 kovtalex/hipster-paymentservice:v0.0.2%
kubectl get rs
NAME DESIRED CURRENT READY AGE
frontend 3 3 3 36m
paymentservice-7845cdfff9 3 3 3 4m47s
paymentservice-84f44df66 0 0 0 8m2s
kubectl rollout history deployment paymentservice
deployment.apps/paymentservice
REVISION CHANGE-CAUSE
1 <none>
2 <none>
Представим, что обновление по каким-то причинам произошло неудачно и нам необходимо сделать откат. Kubernetes предоставляет такую возможность:
kubectl rollout undo deployment paymentservice --to-revision=1 | kubectl get rs -l app=paymentservice -w
NAME DESIRED CURRENT READY AGE
paymentservice-7845cdfff9 3 3 3 7m12s
paymentservice-84f44df66 0 0 0 10m
paymentservice-84f44df66 0 0 0 10m
paymentservice-84f44df66 1 0 0 10m
paymentservice-84f44df66 1 0 0 10m
paymentservice-84f44df66 1 1 0 10m
paymentservice-84f44df66 1 1 1 10m
paymentservice-7845cdfff9 2 3 3 7m15s
paymentservice-84f44df66 2 1 1 10m
paymentservice-7845cdfff9 2 3 3 7m15s
paymentservice-84f44df66 2 1 1 10m
paymentservice-7845cdfff9 2 2 2 7m15s
paymentservice-84f44df66 2 2 1 10m
paymentservice-84f44df66 2 2 2 10m
paymentservice-7845cdfff9 1 2 2 7m17s
paymentservice-84f44df66 3 2 2 10m
paymentservice-7845cdfff9 1 2 2 7m17s
paymentservice-7845cdfff9 1 1 1 7m17s
paymentservice-84f44df66 3 2 2 10m
paymentservice-84f44df66 3 3 2 10m
paymentservice-84f44df66 3 3 3 10m
paymentservice-7845cdfff9 0 1 1 7m19s
paymentservice-7845cdfff9 0 1 1 7m20s
paymentservice-7845cdfff9 0 0 0 7m20s
В выводе мы можем наблюдать, как происходит постепенное масштабирование вниз "нового" ReplicaSet, и масштабирование вверх "старого".
С использованием параметров maxSurge и maxUnavailable самостоятельно реализуем два следующих сценария развертывания:
- Аналог blue-green:
- Развертывание трех новых pod
- Удаление трех старых pod
- Reverse Rolling Update:
- Удаление одного старого pod
- Создание одного нового pod
Документация с описанием стратегий развертывания для Deployment.
maxSurge - определяет количество реплик, которое можно создать с превышением значения replicas
Можно задавать как абсолютное число, так и процент. Default: 25%
maxUnavailable - определяет количество реплик от общего числа, которое можно "уронить"
Аналогично, задается в процентах или числом. Default: 25%
В результате должно получиться два манифеста:
- paymentservice-deployment-bg.yaml
Для реализации аналога blue-green развертывания устанавливаем значения:
- maxSurge равным 3 для превышения количества требуемых pods
- maxUnavailable равным 0 для ограничения минимального количества недоступных pods
apiVersion: apps/v1
kind: Deployment
metadata:
name: paymentservice
labels:
app: paymentservice
spec:
replicas: 3
strategy:
type: RollingUpdate
rollingUpdate:
# Количество реплик, которое можно создать с превышением значения replicas
# Можно задавать как абсолютное число, так и процент. Default: 25%
maxSurge: 3
# Количество реплик от общего числа, которое можно "уронить"
# Аналогично, задается в процентах или числом. Default: 25%
maxUnavailable: 0
selector:
matchLabels:
app: paymentservice
template:
metadata:
labels:
app: paymentservice
spec:
containers:
- name: server
image: kovtalex/hipster-paymentservice:v0.0.1
Применим манифест:
kubectl apply -f paymentservice-deployment-bg.yaml
deployment.apps/paymentservice created
kubectl get pods
NAME READY STATUS RESTARTS AGE
paymentservice-84f44df66-kr62g 1/1 Running 0 30s
paymentservice-84f44df66-ltsx8 1/1 Running 0 30s
paymentservice-84f44df66-nn8ml 1/1 Running 0 30s
В манифесте paymentservice-deployment-bg.yaml меняем версию образа на v0.0.2 и применяем:
kubectl apply -f paymentservice-deployment-bg.yaml
deployment.apps/paymentservice configured
kubectl get pods -w
NAME READY STATUS RESTARTS AGE
paymentservice-84f44df66-kr62g 1/1 Running 0 109s
paymentservice-84f44df66-ltsx8 1/1 Running 0 109s
paymentservice-84f44df66-nn8ml 1/1 Running 0 109s
paymentservice-7845cdfff9-bgr7k 0/1 Pending 0 0s
paymentservice-7845cdfff9-n6nqw 0/1 Pending 0 0s
paymentservice-7845cdfff9-snjpm 0/1 Pending 0 0s
paymentservice-7845cdfff9-snjpm 0/1 Pending 0 0s
paymentservice-7845cdfff9-bgr7k 0/1 Pending 0 0s
paymentservice-7845cdfff9-n6nqw 0/1 Pending 0 0s
paymentservice-7845cdfff9-n6nqw 0/1 ContainerCreating 0 0s
paymentservice-7845cdfff9-bgr7k 0/1 ContainerCreating 0 0s
paymentservice-7845cdfff9-snjpm 0/1 ContainerCreating 0 0s
paymentservice-7845cdfff9-snjpm 1/1 Running 0 2s
paymentservice-7845cdfff9-n6nqw 1/1 Running 0 2s
paymentservice-7845cdfff9-bgr7k 1/1 Running 0 3s
paymentservice-84f44df66-nn8ml 1/1 Terminating 0 2m1s
paymentservice-84f44df66-ltsx8 1/1 Terminating 0 2m2s
paymentservice-84f44df66-kr62g 1/1 Terminating 0 2m2
Как видно выше, сначала создаются три новых пода, а затем удаляются три старых.
- paymentservice-deployment-reverse.yaml
Для реализации Reverse Rolling Update устанавливаем значения:
- maxSurge равным 1 для превышения количества требуемых pods
- maxUnavailable равным 1 для ограничения минимального количества недоступных pods
apiVersion: apps/v1
kind: Deployment
metadata:
name: paymentservice
labels:
app: paymentservice
spec:
replicas: 3
strategy:
type: RollingUpdate
rollingUpdate:
# Количество реплик, которое можно создать с превышением значения replicas
# Можно задавать как абсолютное число, так и процент. Default: 25%
maxSurge: 1
# Количество реплик от общего числа, которое можно "уронить"
# Аналогично, задается в процентах или числом. Default: 25%
maxUnavailable: 1
selector:
matchLabels:
app: paymentservice
template:
metadata:
labels:
app: paymentservice
spec:
containers:
- name: server
image: kovtalex/hipster-paymentservice:v0.0.1
Проверяем результат:
kubectl apply -f paymentservice-deployment-reverse.yaml | kubectl get pods -w
NAME READY STATUS RESTARTS AGE
paymentservice-7845cdfff9-2hgrh 1/1 Running 0 92s
paymentservice-7845cdfff9-ffg84 1/1 Running 0 91s
paymentservice-7845cdfff9-vqjm2 1/1 Running 0 91s
paymentservice-84f44df66-t7rph 0/1 Pending 0 0s
paymentservice-7845cdfff9-ffg84 1/1 Terminating 0 91s
paymentservice-84f44df66-t7rph 0/1 Pending 0 1s
paymentservice-84f44df66-jhw47 0/1 Pending 0 0s
paymentservice-84f44df66-t7rph 0/1 ContainerCreating 0 1s
paymentservice-84f44df66-jhw47 0/1 Pending 0 0s
paymentservice-84f44df66-jhw47 0/1 ContainerCreating 0 0s
paymentservice-84f44df66-t7rph 1/1 Running 0 2s
paymentservice-7845cdfff9-2hgrh 1/1 Terminating 0 94s
paymentservice-84f44df66-jhw47 1/1 Running 0 1s
paymentservice-84f44df66-sjllv 0/1 Pending 0 0s
paymentservice-84f44df66-sjllv 0/1 Pending 0 1s
paymentservice-84f44df66-sjllv 0/1 ContainerCreating 0 1s
paymentservice-7845cdfff9-vqjm2 1/1 Terminating 0 94s
paymentservice-84f44df66-sjllv 1/1 Running 0 3s
paymentservice-7845cdfff9-ffg84 0/1 Terminating 0 2m3s
paymentservice-7845cdfff9-2hgrh 0/1 Terminating 0 2m5s
paymentservice-7845cdfff9-vqjm2 0/1 Terminating 0 2m5s
paymentservice-7845cdfff9-vqjm2 0/1 Terminating 0 2m6s
paymentservice-7845cdfff9-vqjm2 0/1 Terminating 0 2m6s
paymentservice-7845cdfff9-2hgrh 0/1 Terminating 0 2m12s
paymentservice-7845cdfff9-2hgrh 0/1 Terminating 0 2m12s
paymentservice-7845cdfff9-ffg84 0/1 Terminating 0 2m11s
paymentservice-7845cdfff9-ffg84 0/1 Terminating 0 2m12s
Мы научились разворачивать и обновлять наши микросервисы, но можем ли быть уверены, что они корректно работают после выкатки? Один из механизмов Kubernetes, позволяющий нам проверить это - Probes.
Давайте на примере микросервиса frontend посмотрим на то, как probes влияют на процесс развертывания.
- Создадим манифест frontend-deployment.yaml из которого можно развернуть три реплики pod с тегом образа v0.0.1
- Добавим туда описание readinessProbe. Описание можно взять из манифеста по ссылке.
Применим манифест с readinessProbe. Если все сделано правильно, то мы вновь увидим три запущенных pod в описании которых (kubectl describe pod) будет указание на наличие readinessProbe и ее параметры.
Давайте попробуем сымитировать некорректную работу приложения и посмотрим, как будет вести себя обновление:
- Заменим в описании пробы URL /_healthz на /_health
- Развернем версию v0.0.2
kubectl apply -f frontend-deployment.yaml
Если посмотреть на текущее состояние нашего микросервиса, мы увидим, что был создан один pod новой версии, но его статус готовности 0/1:
Команда kubectl describe pod поможет нам понять причину:
kubectl describe pod frontend-78c57b6df6-vvvbt
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Warning Unhealthy 4s (x2 over 14s) kubelet, kind-worker3 Readiness probe failed: HTTP probe failed with statuscode: 404
Как можно было заметить, пока readinessProbe для нового pod не станет успешной - Deployment не будет пытаться продолжить обновление.
На данном этапе может возникнуть вопрос - как автоматически отследить успешность выполнения Deployment (например для запуска в CI/CD).
В этом нам может помочь следующая команда:
kubectl rollout status deployment/frontend
Таким образом описание pipeline, включающее в себя шаг развертывания и шаг отката, в самом простом случае может выглядеть так (синтаксис GitLab CI):
deploy_job:
stage: deploy
script:
- kubectl apply -f frontend-deployment.yaml
- kubectl rollout status deployment/frontend --timeout=60s
rollback_deploy_job:
stage: rollback
script:
- kubectl rollout undo deployment/frontend
when: on_failure
Рассмотрим еще один контроллер Kubernetes. Отличительная особенность DaemonSet в том, что при его применении на каждом физическом хосте создается по одному экземпляру pod, описанного в спецификации.
Типичные кейсы использования DaemonSet:
- Сетевые плагины
- Утилиты для сбора и отправки логов (Fluent Bit, Fluentd, etc...)
- Различные утилиты для мониторинга (Node Exporter, etc...)
- ...
Опробуем DaemonSet на примере Node Exporter
- Найдем в интернете манифест node-exporter-daemonset.yaml для развертывания DaemonSet с Node Exporter
- После применения данного DaemonSet и выполнения команды: kubectl port-forward <имя любого pod в DaemonSet> 9100:9100 доступны на localhost: curl localhost:9100/metrics
Подготовим манифесты и развернем Node Exporter как DaemonSet:
kubectl create ns monitoring
namespace/monitoring created
kubectl apply -f node-exporter-serviceAccount.yaml
serviceaccount/node-exporter created
kubectl apply -f node-exporter-clusterRole.yaml
clusterrole.rbac.authorization.k8s.io/node-exporter created
kubectl apply -f node-exporter-clusterRoleBinding.yaml
clusterrolebinding.rbac.authorization.k8s.io/node-exporter created
kubectl apply -f node-exporter-daemonset.yaml
daemonset.apps/node-exporter created
kubectl apply -f node-exporter-service.yaml
service/node-exporter created
Проверим созданные pods:
kubectl get pods -n monitoring
NAME READY STATUS RESTARTS AGE
node-exporter-j657t 2/2 Running 0 110s
node-exporter-k6nwd 2/2 Running 0 105s
node-exporter-vsrzp 2/2 Running 0 119s
В соседнем терминале запустим проброс порта:
kubectl port-forward node-exporter-j657t 9100:9100 -n monitoring
Forwarding from 127.0.0.1:9100 -> 9100
Forwarding from [::1]:9100 -> 9100
И убедимся, что мы можем получать метрики:
curl localhost:9100/metrics
# TYPE go_gc_duration_seconds summary
go_gc_duration_seconds{quantile="0"} 0
go_gc_duration_seconds{quantile="0.25"} 0
go_gc_duration_seconds{quantile="0.5"} 0
go_gc_duration_seconds{quantile="0.75"} 0
go_gc_duration_seconds{quantile="1"} 0
go_gc_duration_seconds_sum 0
go_gc_duration_seconds_count 0
# HELP go_goroutines Number of goroutines that currently exist.
# TYPE go_goroutines gauge
go_goroutines 6
# HELP go_info Information about the Go environment.
# TYPE go_info gauge
go_info{version="go1.12.5"} 1
# HELP go_memstats_alloc_bytes Number of bytes allocated and still in use.
# TYPE go_memstats_alloc_bytes gauge
go_memstats_alloc_bytes 2.300448e+06
# HELP go_memstats_alloc_bytes_total Total number of bytes allocated, even if freed.
# TYPE go_memstats_alloc_bytes_total counter
go_memstats_alloc_bytes_total 2.300448e+06
# HELP go_memstats_buck_hash_sys_bytes Number of bytes used by the profiling bucket hash table.
# TYPE go_memstats_buck_hash_sys_bytes gauge
go_memstats_buck_hash_sys_bytes 1.444017e+06
# HELP go_memstats_frees_total Total number of frees.
...
- Как правило, мониторинг требуется не только для worker, но и для master нод. При этом, по умолчанию, pod управляемые DaemonSet на master нодах не разворачиваются
- Найдем способ модернизировать свой DaemonSet таким образом, чтобы Node Exporter был развернут как на master, так и на worker нодах (конфигурацию самих нод изменять нельзя)
- Отразим изменения в манифесте
Материал по теме: Taint and Toleration.
Решение: для развертывания DaemonSet на master нодах нам необходимо выдать допуск поду.
Правим наш node-exporter-daemonset.yaml:
tolerations:
- operator: Exists
Применяем манифест и проверяем, что DaemonSet развернулся на master нодах.
kubectl apply -f node-exporter-daemonset.yaml
daemonset.apps/node-exporter configured
kubectl get pods -n monitoring -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
node-exporter-25d4s 2/2 Running 0 45m 172.18.0.6 kind-worker3 <none> <none>
node-exporter-8dp28 2/2 Running 0 45m 172.18.0.4 kind-control-plane <none> <none>
node-exporter-bb76j 2/2 Running 0 45m 172.18.0.7 kind-control-plane2 <none> <none>
node-exporter-dzmm9 2/2 Running 0 45m 172.18.0.5 kind-control-plane3 <none> <none>
node-exporter-p9sn4 2/2 Running 0 45m 172.18.0.3 kind-worker2 <none> <none>
node-exporter-s8dh7 2/2 Running 0 45m 172.18.0.8 kind-worker <none> <none>
kubectl - консольная утилита для управления кластерами Kubernetes.
Установим последнюю доступную версию kubectl на локальную машину. Инструкции по установке доступны по ссылке.
brew install kubectl
И автодополнение для shell.
ZSH:
source <(kubectl completion zsh) # setup autocomplete in zsh into the current shell
echo "[[ $commands[kubectl] ]] && source <(kubectl completion zsh)" >> ~/.zshrc # add autocomplete permanently to your zsh shell
Minikube - наиболее универсальный вариант для развертывания локального окружения.
Установим последнюю доступную версию Minikube на локальную машину. Инструкции по установке доступны по ссылке.
brew install minikube
После установки запустим виртуальную машину с кластером Kubernetes командой minikube start.
minikube start --vm-driver=docker
😄 minikube v1.9.2 on Darwin 10.15.4
✨ Using the docker driver based on user configuration
👍 Starting control plane node m01 in cluster minikube
🚜 Pulling base image ...
💾 Downloading Kubernetes v1.18.0 preload ...
> preloaded-images-k8s-v2-v1.18.0-docker-overlay2-amd64.tar.lz4: 542.91 MiB
🔥 Creating Kubernetes in docker container with (CPUs=2) (4 available), Memory=1989MB (1989MB available) ...
🐳 Preparing Kubernetes v1.18.0 on Docker 19.03.2 ...
▪ kubeadm.pod-network-cidr=10.244.0.0/16
🌟 Enabling addons: default-storageclass, storage-provisioner
🏄 Done! kubectl is now configured to use "minikube"
❗ /usr/local/bin/kubectl is v1.15.5, which may be incompatible with Kubernetes v1.18.0.
💡 You can also use 'minikube kubectl -- get pods' to invoke a matching version
После запуска, Minikube должен автоматически настроить kubectl и создать контекст minikube.
Посмотреть текущую
конфигурацию kubectl можно командой kubectl config view.
Проверим, что подключение к кластеру работает корректно:
kubectl cluster-info
Kubernetes master is running at https://127.0.0.1:32768
KubeDNS is running at https://127.0.0.1:32768/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy
To further debug and diagnose cluster problems, use 'kubectl cluster-info dump'.
Также можно подключить один из наиболее часто устанавливаемых аддонов для Kubernetes - Dashboard. Но делать мы этого не будем.
Удобный способ визуализации консольной работы с кластером - k9s.
При установке кластера с использованием Minikube будет создан контейнер docker в котором будут работать все системные компоненты кластера Kubernetes.
Можем убедиться в этом, зайдем на ВМ по SSH и посмотрим запущенные Docker контейнеры:
minikube ssh
docker@minikube:~$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
85937caf79d3 67da37a9a360 "/coredns -conf /etc…" 43 minutes ago Up 43 minutes k8s_coredns_coredns-66bff467f8-p7swh_kube-system_e6bf2e9d-2e19-499d-8a73-8e991702b87e_0
e538bf422acc 67da37a9a360 "/coredns -conf /etc…" 43 minutes ago Up 43 minutes k8s_coredns_coredns-66bff467f8-q4z6m_kube-system_cf396204-7fbb-4d33-b4ec-9388db14d8ee_0
a38019c3596a 4689081edb10 "/storage-provisioner" 43 minutes ago Up 43 minutes k8s_storage-provisioner_storage-provisioner_kube-system_6fac89db-3152-41d5-ac3f-0e0bc3538ae6_0
f2a5bf383bb3 43940c34f24f "/usr/local/bin/kube…" 43 minutes ago Up 43 minutes k8s_kube-proxy_kube-proxy-c485n_kube-system_c024f4ec-4738-4eb1-906f-106492f0b975_0
e43a4a8f5529 k8s.gcr.io/pause:3.2 "/pause" 43 minutes ago Up 43 minutes k8s_POD_coredns-66bff467f8-p7swh_kube-system_e6bf2e9d-2e19-499d-8a73-8e991702b87e_0
8742b307d2e0 k8s.gcr.io/pause:3.2 "/pause" 43 minutes ago Up 43 minutes k8s_POD_coredns-66bff467f8-q4z6m_kube-system_cf396204-7fbb-4d33-b4ec-9388db14d8ee_0
0d69fb536fcb aa67fec7d7ef "/bin/kindnetd" 43 minutes ago Up 43 minutes k8s_kindnet-cni_kindnet-rgt42_kube-system_68104c7f-a787-4fcb-a65c-91d51372c1de_0
700e5e2226c7 k8s.gcr.io/pause:3.2 "/pause" 43 minutes ago Up 43 minutes k8s_POD_storage-provisioner_kube-system_6fac89db-3152-41d5-ac3f-0e0bc3538ae6_0
b44bbd21388a k8s.gcr.io/pause:3.2 "/pause" 43 minutes ago Up 43 minutes k8s_POD_kube-proxy-c485n_kube-system_c024f4ec-4738-4eb1-906f-106492f0b975_0
f29c9a711b8f k8s.gcr.io/pause:3.2 "/pause" 43 minutes ago Up 43 minutes k8s_POD_kindnet-rgt42_kube-system_68104c7f-a787-4fcb-a65c-91d51372c1de_0
9cd68dda5f76 a31f78c7c8ce "kube-scheduler --au…" 43 minutes ago Up 43 minutes k8s_kube-scheduler_kube-scheduler-minikube_kube-system_5795d0c442cb997ff93c49feeb9f6386_0
0695ca78733b 303ce5db0e90 "etcd --advertise-cl…" 43 minutes ago Up 43 minutes k8s_etcd_etcd-minikube_kube-system_ca02679f24a416493e1c288b16539a55_0
733c2fef50cf 74060cea7f70 "kube-apiserver --ad…" 43 minutes ago Up 43 minutes k8s_kube-apiserver_kube-apiserver-minikube_kube-system_45e2432c538c36239dfecde67cb91065_0
bed099c2898a d3e55153f52f "kube-controller-man…" 43 minutes ago Up 43 minutes k8s_kube-controller-manager_kube-controller-manager-minikube_kube-system_c92479a2ea69d7c331c16a5105dd1b8c_0
ef65a858de32 k8s.gcr.io/pause:3.2 "/pause" 43 minutes ago Up 43 minutes k8s_POD_etcd-minikube_kube-system_ca02679f24a416493e1c288b16539a55_0
92cefb8f5af9 k8s.gcr.io/pause:3.2 "/pause" 43 minutes ago Up 43 minutes k8s_POD_kube-scheduler-minikube_kube-system_5795d0c442cb997ff93c49feeb9f6386_0
27fb23d3eceb k8s.gcr.io/pause:3.2 "/pause" 43 minutes ago Up 43 minutes k8s_POD_kube-controller-manager-minikube_kube-system_c92479a2ea69d7c331c16a5105dd1b8c_0
4212378acdea k8s.gcr.io/pause:3.2 "/pause" 43 minutes ago Up 43 minutes k8s_POD_kube-apiserver-minikube_kube-system_45e2432c538c36239dfecde67cb91065_0
Проверим, что Kubernetes обладает некоторой устойчивостью к отказам, удалим все контейнеры:
docker rm -f $(docker ps -a -q)
Эти же компоненты, но уже в виде pod можно увидеть в namespace kube-system:
kubectl get pods -n kube-system
NAME READY STATUS RESTARTS AGE
coredns-66bff467f8-p7swh 1/1 Running 0 46m
coredns-66bff467f8-q4z6m 1/1 Running 0 46m
etcd-minikube 1/1 Running 0 47m
kindnet-rgt42 1/1 Running 0 46m
kube-apiserver-minikube 1/1 Running 0 47m
kube-controller-manager-minikube 1/1 Running 0 47m
kube-proxy-c485n 1/1 Running 0 46m
kube-scheduler-minikube 1/1 Running 1 47m
storage-provisioner 1/1 Running 0 47m
Расшифруем: данной командой мы запросили у API вывести список (get) всех pod (pods) в namespace (-n, сокращенное от --namespace) kube-system.
Можно устроить еще одну проверку на прочность и удалить все pod с системными компонентами:
kubectl delete pod --all -n kube-system
pod "coredns-66bff467f8-p7swh" deleted
pod "coredns-66bff467f8-q4z6m" deleted
pod "etcd-minikube" deleted
pod "kindnet-rgt42" deleted
pod "kube-apiserver-minikube" deleted
pod "kube-controller-manager-minikube" deleted
pod "kube-proxy-c485n" deleted
pod "kube-scheduler-minikube" deleted
pod "storage-provisioner" deleted
Проверим, что кластер находится в рабочем состоянии, команды kubectl get cs или kubectl get componentstatuses.
выведут состояние системных компонентов:
kubectl get componentstatuses
NAME STATUS MESSAGE ERROR
scheduler Healthy ok
controller-manager Healthy ok
etcd-0 Healthy {"health":"true"}
Для выполнения домашней работы создадим Dockerfile, в котором будет описан образ:
- Запускающий web-сервер на порту 8000
- Отдающий содержимое директории /app внутри контейнера (например, если в директории /app лежит файл homework.html, то при запуске контейнера данный файл должен быть доступен по URL [http://localhost:8000/homework.html])
- Работающий с UID 1001
FROM nginx:1.18.0-alpine
RUN apk add --no-cache shadow
RUN usermod -u 1001 nginx \
&& groupmod -g 1001 nginx
WORKDIR /app
COPY ./app .
COPY ./default.conf /etc/nginx/conf.d/default.conf
COPY ./nginx.conf /etc/nginx/nginx.conf
EXPOSE 8000
USER 1001
После того, как Dockerfile будет готов:
- В корне репозитория создадим директорию kubernetesintro/web и поместим туда готовый Dockerfile
- Соберем из Dockerfile образ контейнера и поместим его в публичный Container Registry (например, Docker Hub)
docker build -t kovtalex/simple-web:0.1 .
docker push kovtalex/simple-web:0.1
Напишем манифест web-pod.yaml для создания pod web c меткой app со значением web, содержащего один контейнер с названием web. Необходимо использовать ранее собранный образ с Docker Hub.
apiVersion: v1 # Версия API
kind: Pod # Объект, который создаем
metadata:
name: web # Название Pod
labels: # Метки в формате key: value
app: web
spec: # Описание Pod
containers: # Описание контейнеров внутри Pod
- name: web # Название контейнера
image: kovtalex/simple-web:0.1 # Образ из которого создается контейнер
Поместим манифест web-pod.yaml в директорию kubernetesintro и применим его:
kubectl apply -f web-pod.yaml
pod/web created
После этого в кластере в namespace default должен появиться запущенный pod web:
kubectl get pods
NAME READY STATUS RESTARTS AGE
web 1/1 Running 0 46s
В Kubernetes есть возможность получить манифест уже запущенного в кластере pod.
В подобном манифесте помимо описания pod будутфигурировать служебные поля (например, различные статусы) и значения, подставленные по умолчанию:
kubectl get pod web -o yaml
Другой способ посмотреть описание pod - использовать ключ describe. Команда позволяет отследить текущее состояние объекта, а также события, которые с ним происходили:
kubectl describe pod web
Успешный старт pod в kubectl describe выглядит следующим образом:
- scheduler определил, на какой ноде запускать pod
- kubelet скачал необходимый образ и запустил контейнер
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Scheduled 3m23s default-scheduler Successfully assigned default/web to minikube
Normal Pulled 3m22s kubelet, minikube Container image "kovtalex/simple-web:0.1" already present on machine
Normal Created 3m22s kubelet, minikube Created container web
Normal Started 3m22s kubelet, minikube Started container web
При этом kubectl describe - хороший старт для поиска причин проблем с запуском pod.
Укажем в манифесте несуществующий тег образа web и применим его заново (kubectl apply -f web-pod.yaml).
Статус pod (kubectl get pods) должен измениться на ErrImagePull/ImagePullBackOff, а команда kubectl describe pod web поможет понять причину такого поведения:
Events:
Warning Failed 12s kubelet, minikube Failed to pull image "kovtalex/simple-web:0.2": rpc error: code = Unknown desc = Error response from daemon: manifest for kovtalex/simple-web:0.2 not found: manifest unknown: manifest unknown
Warning Failed 12s kubelet, minikube Error: ErrImagePull
Normal BackOff 12s kubelet, minikube Back-off pulling image "kovtalex/simple-web:0.2"
Warning Failed 12s kubelet, minikube Error: ImagePullBackOff
Normal Pulling 0s (x2 over 14s) kubelet, minikube Pulling image "kovtalex/simple-web:0.2"
Вывод kubectl describe pod web если мы забыли, что Container Registry могут быть приватными:
Events:
Warning Failed 2s kubelet, minikube Failed to pull image "quay.io/example/web:1.0": rpc error: code = Unknown desc =Error response from daemon: unauthorized: access to the requested resource is not authorized
Добавим в наш pod init контейнер, генерирующий страницу index.html.
Init контейнеры описываются аналогично обычным контейнерам в pod. Добавим в манифест web-pod.yaml описание init контейнера, соответствующее следующим требованиям:
- image init контейнера должен содержать wget (например, можно использовать busybox:1.31.0 или любой другой busybox актуальной версии)
- command init контейнера (аналог ENTRYPOINT в Dockerfile) укажем следующую:
['sh', '-c', 'wget -O- https://tinyurl.com/otus-k8s-intro | sh']
Для того, чтобы файлы, созданные в init контейнере, были доступны основному контейнеру в pod нам понадобится использовать volume типа emptyDir.
У контейнера и у init контейнера должны быть описаны volumeMounts следующего вида:
volumeMounts:
- name: app
mountPath: /app
web-pod.yaml
apiVersion: v1 # Версия API
kind: Pod # Объект, который создаем
metadata:
name: web # Название Pod
labels: # Метки в формате key: value
app: web
spec: # Описание Pod
containers: # Описание контейнеров внутри Pod
- name: web # Название контейнера
image: kovtalex/simple-web:0.1 # Образ из которого создается контейнер
volumeMounts:
- name: app
mountPath: /app
initContainers:
- name: init-web
image: busybox:1.31.1
command: ['sh', '-c', 'wget -O- https://tinyurl.com/otus-k8s-intro | sh']
volumeMounts:
- name: app
mountPath: /app
volumes:
- name: app
emptyDir: {}
Удалим запущенный pod web из кластера kubectl delete pod web и применим обновленный манифест web-pod.yaml
Отслеживать происходящее можно с использованием команды kubectl get pods -w
Должен получиться аналогичный вывод:
kubectl get pods -w
NAME READY STATUS RESTARTS AGE
web 0/1 Init:0/1 0 2s
web 0/1 Init:0/1 0 2s
web 0/1 PodInitializing 0 3s
web 1/1 Running 0 4s
Проверим работоспособность web сервера. Существует несколько способов получить доступ к pod, запущенным внутри кластера.
Мы воспользуемся командой kubectl port-forward
kubectl port-forward --address 0.0.0.0 pod/web 8000:8000
Если все выполнено правильно, на локальном компьютере по ссылке http://localhost:8000/index.html должна открыться страница.
В качестве альтернативы kubectl port-forward можно использовать удобную обертку kube-forwarder. Она отлично подходит для доступа к pod внутри кластера с локальной машины во время разработки продукта.
Давайте познакомимся с приложением поближе и попробуем запустить внутри нашего кластера его компоненты.
Начнем с микросервиса frontend. Его исходный код доступен по адресу.
- Склонируем репозиторий и соберем собственный образ для frontend (используем готовый Dockerfile)
- Поместим собранный образ на Docker Hub
git clone https://github.com/GoogleCloudPlatform/microservices-demo.git
docker build -t kovtalex/hipster-frontend:v0.0.1 .
docker push kovtalex/hipster-frontend:v0.0.1
Рассмотрим альтернативный способ запуска pod в нашем Kubernetes кластере.
Мы уже умеем работать с манифестами (и это наиболее корректный подход к развертыванию ресурсов в Kubernetes), но иногда бывает удобно использовать ad-hoc режим и возможности Kubectl для создания ресурсов.
Разберем пример для запуска frontend pod:
kubectl run frontend --image kovtalex/hipster-frontend:v0.0.1 --restart=Never
- kubectl run - запустить ресурс
- frontend - с именем frontend
- --image - из образа kovtalex/hipster-frontend:v0.0.1 (подставим свой образ)
- --restart=Never указываем на то, что в качестве ресурса запускаем pod. Подробности
Один из распространенных кейсов использования ad-hoc режима - генерация манифестов средствами kubectl:
kubectl run frontend --image kovtalex/hipster-frontend:v0.0.1 --restart=Never --dryrun -o yaml > frontend-pod.yaml
Рассмотрим дополнительные ключи:
- --dry-run - вывод информации о ресурсе без его реального создания
- -o yaml - форматирование вывода в YAML
- > frontend-pod.yaml - перенаправление вывода в файл
- Выясним причину, по которой pod frontend находится в статусе Error
- Создадим новый манифест frontend-pod-healthy.yaml. При его применении ошибка должна исчезнуть. Подсказки можно найти:
- В логах - kubectl logs frontend
- В манифесте по ссылке
- В результате, после применения исправленного манифеста pod frontend должен находиться в статусе Running
- Поместим исправленный манифест frontend-pod-healthy.yaml в директорию kubernetes-intro
- Проверив лог pod можно заметить, что не заданы переменные окружения. Добавим их.
- Так же можно свериться со списком необходимых переменных окружения из готового манифеста.
- Добавим отсутствующие переменные окружения в наш yaml файл и пересоздадим pod.
- name: PRODUCT_CATALOG_SERVICE_ADDR
value: "productcatalogservice:3550"
- name: CURRENCY_SERVICE_ADDR
value: "currencyservice:7000"
- name: CART_SERVICE_ADDR
value: "cartservice:7070"
- name: RECOMMENDATION_SERVICE_ADDR
value: "recommendationservice:8080"
- name: SHIPPING_SERVICE_ADDR
value: "shippingservice:50051"
- name: CHECKOUT_SERVICE_ADDR
value: "checkoutservice:5050"
- name: AD_SERVICE_ADDR
value: "adservice:9555"
frontend в статусе Running.
kubectl get pods
NAME READY STATUS RESTARTS AGE
frontend 1/1 Running 0 10s