Chapter 2

Operations

Deploying & configuring complex systems.

Subsections of Operations

Alpine Linux

Installation on ESXi

External Link

  • Enable Community repo in file /etc/apk/repositories
  • Install: apk add --update open-vm-tools
  • Enable at boot: rc-service open-vm-tools start rc-update add open-vm-tools

Network Config

External Link In file: /etc/network/interfaces

DHCP: iface eth0 inet dhcp

STATIC:

iface eth0 inet static
        address 192.168.1.150/24
        gateway 192.168.1.1

DNS

In file: /etc/resolv.conf

nameserver 8.8.8.8
nameserver 8.8.4.4

Configue CGROUPS for k8s

External Link rc-update add cgroups

Bugfix:

echo "cgroup /sys/fs/cgroup cgroup defaults 0 0" >> /etc/fstab

cat > /etc/cgconfig.conf <<END
mount {
  cpuacct = /cgroup/cpuacct;
  memory = /cgroup/memory;
  devices = /cgroup/devices;
  freezer = /cgroup/freezer;
  net_cls = /cgroup/net_cls;
  blkio = /cgroup/blkio;
  cpuset = /cgroup/cpuset;
  cpu = /cgroup/cpu;
}
END

sed -i '/^default_kernel_opts/s/=.*$/="quiet rootfstype=ext4 cgroup_enable=cpuset cgroup_memory=1 cgroup_enable=memory"/' /etc/update-extlinux.conf
update-extlinux
apk add --no-cache iptables curl

Ansible

Articles in section

Installation

Do not install Ansible from local OS repo - it is usually older in version there. Use latest Python pip repo to install a fresh version.

sudo apt install python3-distutils
wget http://bootstrap.pypa.io/get-pip.py
sudo python3 get-pip.py
sudo pip3 install ansible
ansible --version
Tip

Ansible will be the latest version supporting current Python in OS. So to get latest Ansible , Python must be updated as well!

Warning

Do not update the default Python in OS - it is used by system services, which may break!

Build Python:

sudo apt install build-essential zlib1g-dev libncurses5-dev libgdbm-dev libnss3-dev libssl-dev libsqlite3-dev libreadline-dev libffi-dev curl libbz2-dev

wget https://www.python.org/ftp/python/3.9.15/Python-3.9.15.tgz

tar -xf Python-3.9.15.tgz

cd Python-3.9.15
./configure --enable-optimizations

make -j 8

Installing Python:

  • Best way is to do checkinstall, create a .deb file to share with the team.
  • Use altinstall parameter to install Python in an alternative path. This is simpler & better than using Python virtualenv.
sudo make altinstall # now Python 3.9x is installed on separate path, while Python 3.7 in OS is unchanged

sudo python3.9 -m pip install --upgrade pip

sudo python3.9 -m pip install ansible

ansible --version # now ansible is the latest version

Architecture

graph LR I[(Inventory)] --> R1(Role01); M(Modules) --> R1; I[(Inventory)] --> R2(Role02); M(Modules) --> R2; I[(Inventory)] --> RN(Role N); M(Modules) --> RN; R1 -->|include role| P(Playbook); R2 -->|include role| P(Playbook); RN -->|include role| P(Playbook); P --> C(Ansible Config); C --> Py(Python); Py-->|SSH|Client01; Py-->|SSH|Client02; Py-->|SSH|Client_N;

Folder Structure

video.yml - main program:

---
- import_playbook: playbooks/db.yml
- import_playbook: playbooks/front.yml
- import_playbook: playbooks/zoneminder.yml
...

front.yml - role example:

---
- name: front-end play
  hosts: all
  gather_facts: yes
  become: yes

  tasks:
    - name: include role apache
      include_role:
        name: apache

    - name: include role php
      include_role:
        name: php
...

apache / tasks / main.yml - tasks example:

---
- name: install apache
  apt:
  name: apache2

- name: Enable service apache2
  ansible.builtin.systemd:
    name: apache2
    enabled: yes
    masked: no

- name: Make sure apache2 is running
  ansible.builtin.systemd:
    state: started
    name: apache2
...

Role structure

Directories inside a role:

- defaults - variable values by default
-- main.yml

- vars - variables defined by role (for other roles)
-- main.yml

- tasks - jobs to be completed
-- main.yml

- handlers - actions to be taken after checks
-- main.yml

- files - static files to be copied into client machine
- templates

Subsections of Ansible

Inventory

Initialization

Create inventory file with IP addresses. Check computers in inventory by doing a ping:

ansible all -i inventory -u vagrant -m ping -k
# -i - <inventory file>
# -u - <username>
# -m - <select module>
# -k - <interactive password prompt>
ansible all -i inventory -u admin -m ping -k -vvv # debug mode with verbosity 3

Inventory examples:

[db]
vm ansible_host=192.168.1.98 ansible_user=aagern

[app]
vm ansible_host=192.168.1.98 ansible_user=aagern

[front]
vm ansible_host=192.168.1.98 ansible_user=aagern

[all:vars]
ansible_ssh_extra_args="-o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no"
ansible_python_interpreter=/usr/bin/python3
web1 ansible_ssh_host=192.168.33.20
db1 ansible_ssh_host=192.168.33.30

[webservers]
web1

[dbservers]
db1

[datacenter:children]
webservers
dbservers

[datacenter:vars]
ansible_ssh_user=vagrant
ansible_ssh_pass=vagrant

Configuration

Order of precedence for the config file:

  1. $ANSIBLE_CONFIG env variable  (first place to be searched)
  2. ./ansible.cfg - local file in the working directory
  3. ~/.ansible.cfg - user home directory
  4. /etc/ansible/ansible.cfg - global config (last place to be searched)

First config file found wins, Ansible stops looking for other config files.

Environment override

Specify: $ANSIBLE_

Override settings on the fly:

$ export ANSIBLE_FORKS=10

[defaults] forks - how many parallel processes does Ansible handle. Default=5, production recommended=20

host_key_checking - check host key before sending commands. Default=True (for production), for dev/test systems =False (easy control)

log_path - Ansible logging. Default=Null, produstion recommended - set path all Ansible users can write to.

Target patterns

Patterns to choose hosts/groups:

  • OR pattern: group1:group2

Example:

ansible webservers:dbservers -i inventory -m service -a "name=iptables state=stopped" --sudo
  • NOT pattern: :!group2
  • Wildcard pattern: web.lab.local*
  • REGEX pattern: ~web[0-9]+
  • AND pattern: group1:&group2 - apply to hosts BOTH in group1 and group2 (intersection)
  • Complex patterns

Complex pattern example: webservers:&production:!python3 # apply to web servers in production but not in the Python3 group

Modules

Help on modules

ansible-doc -l # all installed modules list
ansible-doc <module-name> # module man
ansible-doc -s <module-name> # playbook snippets code examples on module

Module examples

Copy module

  • Copies a file from the local box to a remote system - useful for copying config files to remote system
  • can do backups
  • can do remote validation

Fetch module

  • Copy a file from remote system to local box
  • validate file using md5 checksum

Setup module

  • Gather info and facts on remote system
  • Inventory analysis of the system
ansible -i inventory web1 -m setup # gather all available system info
ansible -i inventory web1 -m setup -a "filter=ansible_eth*" # gather info on NICs
ansible -i inventory all -m setup --tree ./setup # form an inventory of files in /setup/ folder with info on targeted systems

Apt module (for Debian systems) / Yum module (RedHat systems)

  • Install, update, delete packages
  • Can update entire system Example for Yum:
ansible webservers -i inventory -m yum -a "name=httpd state=present" -u vagrant --sudo
# name - name of package (Apache)
# present - if package is not there, install. If it is there - do nothing and report "changed: false" (idempotence test)

Service module

  • Start/stop/restart services
  • Set autostart option for services
ansible webservers -i inventory -m service -a "name=httpd state=started enabled=yes" -u vagrant --sudo

# name - name of service (Apache)
# state = started/stopped - make idempotence test and change if necessary
# enabled = yes/no - autostart on system boot

Playbooks

General tasks usage

# Tasks in a playbook are executed top down. Tasks use modules.
tasks:
  - name: Name the task for readability
    module: parameters=go_here

# Example:
  - name: Deploy Apache Configuration File
    copy: src=../ansible/files/configuration/httpd.conf

          dest=/etc/httpd/conf/

Playbook execution: ansible-playbook my_playbook.yml

Playbook example:

---
# -------- Global play declaration
- hosts: webservers    
  ## ----- Variables per play
  vars:
    git_repo: https://github.com/repo.git
    http_port: 8081
    db_name: wordpress

  ## ------------------------
  ### ---- Declare user to run tasks
  sudo: yes
  sudo_user: wordpress_user

  ### ------------------------------
  gather_facts: no # dont't gather facts with SETUP module (default gathers facts - expensive in time)
  remote_user: root
  tasks:

# --------------------------------
  - name: Install Apache
    yum: name=httpd state=present
  - name: Start Apache
    service: name=httpd state=started

Including files

Use “- include” and “- include_vars” directives to include playbook files:

tasks:
- include: wordpress.yaml
  vars:
    sitename: My Awesome Site
    
- include: reverse_proxy.yaml
- include_vars: variables.yaml

Register task output

Use the output of one task for another task:

tasks:
- shell: /usr/bin/whoami
  register: username

- file: path=/home/myfile.txt
  owner: {{ username }}

Debugging tasks with Debug module

Add screen output and print content of variables:

tasks:
  - debug: msg="This host is {{ inventory_hostname }} during execution"

  - shell: /usr/bin/whoami
    register: username
    
  - debug: var=username

Input during playbook execution

Promt user during execution:

- hosts: web1
  vars_prompt:
  - name: "sitename"
    prompt: "What is the new site name?"

  tasks:
    - debug: var=username

Playbook handlers

A handler can be informed to execute a task (restart service) only if state=changed.

  • Run after all tasks
  • Run only once no matter how many times they are called

Handlers syntax is the same as tasks syntax:

  tasks:
  - name: Copy Apache Config files
  - copy: src=../files/httpd.conf
          dest=/etc/httpd/config/

    notify:
      - Apache Restart

  handlers:
  - name: Apache Restart
    service: name=httpd state=restarted

“When” condition

Commence a task if condition is True:

tasks:
- name: Install Httpd Package
  apt: name=httpd state=present
  when: ansible_os_family == "RedHat"

- name: Install Apache2 Package
  yum: name=apache2 state=present
  when: ansible_os_family == "Debian"

Check output of previous task as condition to run next task:

tasks:
- name: Stop iptables now
  service: name=iptables state=stopped
  register: result
  ignore_errors: yes   # supress default stop on error

- debug: msg="Failure!"
  when: result|failed  # Debug message will only be shown if task has failed
                       # other conditions are "result|success", "result|skipped"

Checking variables with WHEN condition

Bring variable check to BOOL check:

- name: "test"
  hosts: local
  vars_prompt:
    - name: "os_type"
      prompt: "What OS? (centos or ubuntu)"
      default: "centos"
      private: no
  vars:
    - is_ubuntu: "{{os_type == 'ubuntu'}}"
    - is_debian: "{{os_type == 'debian'}}"
  tasks:
    - debug: msg="this shows the conditional if variable equals ubuntu"
      when: is_ubuntu|bool
      
    - debug: msg="this shows the conditional if variable equals centos"
      when: is_centos|bool

Grafana Loki

Installation

Create dirs for Loki:

cd ~
mkdir promtail
mkdir loki
mkdir grafana

Create a Docker-Compose file

vim docker-compose.yml

version: "3"

networks:
loki:

services:
loki:
image: grafana/loki:2.4.0
volumes:
- /home/aagern/loki:/etc/loki
ports:
- "3101:3100"
restart: unless-stopped
command: -config.file=/etc/loki/loki-local-config.yaml
networks:
- loki

promtail:
image: grafana/promtail:2.4.0
volumes:
- /var/log:/var/log
- /home/aagern/promtail:/etc/promtail
restart: unless-stopped
command: -config.file=/etc/loki/promtail-local-config.yaml
networks:
- loki

grafana:
image: grafana/grafana:latest
user: "1000"
volumes:
- /home/aagern/grafana:/var/lib/grafana
ports:
- "3000:3000"
restart: unless-stopped
networks:
- loki
Note

The config files MUST be in the inner container directory!

Loki Configs

wget https://raw.githubusercontent.com/grafana/loki/master/cmd/loki/loki-local-config.yaml
 
wget https://raw.githubusercontent.com/grafana/loki/main/clients/cmd/promtail/promtail-local-config.yaml

Promtail config

server:
http_listen_port: 9080
grpc_listen_port: 0
 
positions:
filename: /tmp/positions.yaml
 
clients:
- url: http://loki:3101/loki/api/v1/push
 
# Local machine logs
scrape_configs:
- job_name: local
static_configs:
- targets:
- localhost
labels:
job: varlogs
__path__: /var/log/*log
 
#scrape_configs:
#- job_name: docker
# pipeline_stages:
# - docker: {}
# static_configs:
# - labels:
# job: docker
# path: /var/lib/docker/containers/*/*-json.log

Create the containers

cd ~
sudo docker-compose up -d --force-recreate
sudo docker ps

Data Sources

Go to Grafana :3000 First login/password = admin/admin

Add New Data Source… → Loki
http://loki:3100

Explore {job=varlogs} |= “Network”

Docker logs

scrape_configs:
- job_name: docker
 pipeline_stages:
  - docker: {}
  static_configs:
   - labels:
   job: docker
   path: /var/lib/docker/containers/*/*-json.log

Install a Docker Driver https://grafana.com/docs/loki/latest/clients/docker-driver/

docker plugin install grafana/loki-docker-driver:latest --alias loki --grant-all-permissions
sudo docker plugin ls

Check config: https://grafana.com/docs/loki/latest/clients/docker-driver/configuration/

{
"debug" : true,
"log-driver": "loki",
"log-opts": {
"loki-url": "http://localhost:3100/loki/api/v1/push",
"loki-batch-size": "400"
}
}
  • Config: vim /etc/docker/daemon.json
  • Restart Docker: sudo systemctl restart docker

Kubernetes

Статьи в разделе

Master Node

Мастер нода представляет службы управления (Control Plane)

  • Кластер Etcd ведёт запись всех нод, размещённых на них контейнерах, запись иных данных - это СУБД для Kubernetes, заточенная на согласованности данных и их доступности;
  • Kube-scheduler: команды создания и переноса контейнеров на worker nodes. Считает число ресурсов на нодах и подбирает размещение pods на нодах в соответствии с профилем потребляемых ресурсов;
  • Kube API Server: служба обмена сообщениями в кластере k8s. Аутентификация отправителя сообщения, валидирует отправителя сообщений, регистрирует сообщения по интерфейсу API в базу Etcd; Это единственный компонент, который общается напрямую с Etcd;
  • Kube Controller Manager: содержит службы контроля Node Controller (следит за доступностью нод), Replication Controller (отслеживание распространения копий контейнеров в рамках группы репликации по нодам).

Worker Nodes

Ноды-работники размещают у себя контейнеры через Container Runtime Interface (CRI) с поддержкой containerd (через него Docker, с версии k8s 1.24+) и Rocket:

  • Для приёма команд и передачи статистики по рабочей ноде используется kubelet, служба управления нодой;
Note

Kubeadm не устанавливает автоматически Kubelet-ы. Они всегда ставятся вручную на worker nodes.

  • Для связи с нодой применяется служба Kube-proxy. Создаёт правила проброса потоков данных от служб к pods, на которых они размещены. Один из способов - создание правил iptables;

crictl

Проверка и решение проблем с рабочими нодами. В отличие от утилит Docker, crictl понимает pods.

crictl images # список образов
circtl ps -a # список контейнеров
crictl exec -i -t 288023742....aaabb392849 ls # запуск команды в контейнере
crictl logs 288023742....aaabb392849 # посмотреть лог контейнера
crictl pods 

IDE

Для написания YAML-файлов хорошо подходит редактор с плагином, понимающим k8s. Пример: VSCode + Red Hat YAML plugin

В свойствах плагина найти пункт Yaml: Schemas -> Edit in settings.json Добавить в конфиг:

{
    "yaml.schemas": {
        
    "kubernetes": "*.yaml"
    },
    "redhat.telemetry.enabled": true
}

Это позволит все файлы YAML редактировать с учётом полей, принятых для k8s.

Subsections of Kubernetes

Controller Manager

Node Controller

Отслеживает состояние нод (через kube-apiserver):

  • Отслеживает статус;
  • Реагирует на проблемы;
  • Node Monitor Period = 5 секунд на опрос всех нод через kube-apiserver;
  • Node Monitor Grace Period = 40 секунд время ожидания, если нода не отвечает - отметить её как Unreachable;
  • POD Eviction Timeout = 5 минут на ожидание возврата ноды, иначе перенос pod с этой ноды на другие ноды.

Replication Controller

Отслеживает множества replica sets, и что нужное число pods всегда доступны в каждом replica set.

Другие подсистемы

  • Deployment Controller
  • Namespace Controller
  • Job Controller
  • PV-Protection Controller
  • Endpoint Controller
  • CronJob
  • Service Account COntroller
  • Stateful-Set
  • Replica set
  • и т.д.

ETCD

Особенности

  • Служба Etcd слушает на TCP2379;
  • Клиент - etcdctl;
  •  По умолчанию, etcdctl использует API v2. Переключать на API v3 нужно явно. Команды в API v2 и v3 отличаются:
# ETCD API v2.x
./etcdctl set key1 value1
./etcdctl get key1
./etcdctl --version # важно увидеть версию API (2), от этого команды зависят

# ETCD API v3.x
./etcdctl version # в API 3.x параметр version => команда, набирать без "--"
./etcdctl put key1 value1
./etcdctl get key1
./etcdctl get / --prefix -keys-only # вывести ключи в БД, без значений 
export ETCDCTL_API=3 # поменять версию API на 3.х

Для аутентификации клиента ETCDCTL на ETCD API Server нужно указывать также сертификат:

--cacert /etc/kubernetes/pki/etcd/ca.crt     
--cert /etc/kubernetes/pki/etcd/server.crt     
--key /etc/kubernetes/pki/etcd/server.key

Отсюда полноценная команда-запрос клиента к ETCD будет выглядет вот так:

kubectl exec etcd-master -n kube-system -- sh -c "ETCDCTL_API=3 etcdctl get / --prefix --keys-only --limit=10 --cacert /etc/kubernetes/pki/etcd/ca.crt --cert /etc/kubernetes/pki/etcd/server.crt  --key /etc/kubernetes/pki/etcd/server.key"

Ingress

Ingress Controller

Объект служит в роли прокси и балансировщика L7.

Создание минимального ingress:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata: 
  name: minimal-ingress-controller
spec:
  replicas: 1
  selector:
    matchLabels:
      name: nginx-ingress
  template:
    metadata:
      labels:
        name: nginx-ingress
    spec: 
      containers:
        - name: nginx-ingress-controller
          image: quay.io/kubernetes-ingress-controller/nginx-ingress-controller:0.21.0
          args: 
            - /nginx-ingress-controller
            - --configmap=$ (POD_NAMESPACE)/nginx_configuraruin
          env:            # nginx требует 2 переменные для конфигурации
            - name: POD_NAME
              valueFrom: 
                fieldRef:
                  fieldPath: metadata.name
            - name: POD_NAMESPACE
              valueFrom: 
                fieldRef:
                  fieldPath: metadata.namespace
          ports:
            - name: http
              containerPort: 80
            - name: https
              containerPort: 443

Для конфигурации ingress на nginx также нужен ConfigMap. В него закладывается конфигурация nginx, которая в обычном варианте nginx как reverse-proxy вписывалась в config самого nginx:

apiVersion: v1
kind: ConfigMap
metadata:
  name: nginx-configuration

Также необходимо создать Service, который публикует ingress вовне:

apiVersion: v1
kind: Service
metadata: 
  name: nginx-ingress
spec: 
  type: NodePort
  ports:
  - port: 80
    targetPort: 80
    protocol: TCP
    name: http
  - port: 443
    targetPort: 443
    protocol: TCP
    name: https
  selector:
    name: nginx-ingress

И нужен Service Account для аутентификации:

apiVersion: v1
kind: ServiceAccount
metadata:
  name: nginx-ingress-serviceaccounts

Ingress Resource

Набор правил ingress называются Ingress Resource.

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: ingress-rules
spec:
  backend:
    serviceName: app-service
    servicePort: 80

Определение нахождения правил ingress resource:

kubectl get ingress -A # найти Ingress Resource среди Namespaces 

Редактирование правил ingress resource:

kubectl edit ingress <имя ingress resource> -n <namespace>

Nginx rewrite rules

Настройка rewrite-target нужна, чтобы правильно транслировать сетевой путь.

# Без rewrite-target:
http://<ingress-service>:<ingress-port>/watch --> http://<watch-service>:<port>/path

# С rewrite-target типа replace("/path","/"):
http://<ingress-service>:<ingress-port>/watch --> http://<watch-service>:<port>/

Для включения правил rewrite, нужно добавить в манифест annotations:

1. apiVersion: extensions/v1beta1
2. kind: Ingress
3. metadata:
4.   name: test-ingress
5.   namespace: critical-space
6.   annotations:               # применение rewrite правил
7.     nginx.ingress.kubernetes.io/rewrite-target: /
8. spec:
9.   rules:
10.   - http:
11.       paths:
12.       - path: /pay
13.         backend:
14.           serviceName: pay-service
15.           servicePort: 8282

Jobs & CronJobs

Jobs

Это объект для выполнения однократных служебных задач.

apiVersion: batch/v1
kind: Job
metadata:
  name: my-job
spec:
  completions: 3 # сколько Pod-ов запускать под задачу
  parallelism: 3 # запускать Pod-ы не последовательно, а сразу пачками по 3
  # если 1 из 3 Pod завершится с ошибкой, k8s будет 1 оставшийся перезапускать,    # пока тот не закончит работу корректно
  template:
    spec:
      containers:
      - name: job-container
        image: busybox
        command: [ "/run/job" ]
      restartPolicy: Never

Команды для работы с Jobs:

kubectl create -f <имя job.yaml>
kubectl get jobs
kubectl logs <имя Pod с Job> # вывод результата
kubectl delete job <имя Pod>

CronJobs

Объект для создания периодической задачи:

apiVersion: batch/v1
kind: CronJob
metadata:
  name: my-cronjob
spec:
  schedule: "*/1 * * * *" # работает как Cron в Linux, см ниже Cron Parameters
  jobTemplate: # ниже описание spec обычного Job
    spec:
      completions: 3
      parallelism: 3 
      template:
        spec:
          containers:
          - name: job-container
            image: busybox
            command: [ "/run/job" ]
          restartPolicy: Never

Cron parameters:

# ┌───────────── minute (0 - 59)
# │ ┌───────────── hour (0 - 23)
# │ │ ┌───────────── day of the month (1 - 31)
# │ │ │ ┌───────────── month (1 - 12)
# │ │ │ │ ┌───────────── day of the week (0 - 6) (Sunday to Saturday;
# │ │ │ │ │                                   7 is also Sunday on some systems)
# │ │ │ │ │
# │ │ │ │ │
# * * * * * <command to execute>

Kube-apiserver

Описание kube-apiserver

  • При работе с кластером k8s через инстумент kubectl, по сути работа идёт именно с kube-apiserver;
  • Kube-apiserver забирает данные из БД Etcd и возвращает их пользователю kubectl.

Работа kube-apiserver по созданию pod:

  1. Аутентификация пользователя;
  2. Валидация запроса о создании pod;
  3. Получение данных;
  4. Обновление Etcd;
  5. kube-scheduler мониторит kube-apiserver, узнаёт о появлении pod без привязки к worker-node. Он находит node, куда положить pod и сообщает в kube-apiserver;
  6. kube-apiserver обновляет данные в Etcd;
  7. kube-apiserver передаёт данные на kubelet выбранного worker-node;
  8. Kubelet создаёт на своей worker-node нужный pod и раскатывает там образ контейнера через CRE.

Настройки

  • Если k8s ставился через kubeadm, то они находятся в /etc/kubernetes/manifests/kube-apiserver.yaml
  • Если k8s ставился вручную, то они находятся в /etc/systemd/system/kube-apiserver.service

Pod Commands & Configs

Commands & Arguments

Команды и аргументы команд, которые срабатывают при запуске контейнера.

apiVersion: v1
kind: Pod
metadata: 
  name: myapp
  labels:
    app: test_app
    env: productinon
spec:
  containers:
    - name: nginx-container
      image: nginx
      command: [ "python3" ]
      args: [ "app-test.py" ]

## Вариант 2
      command: 
        - "python3"
        - "app-test.py"

## Вариант 3
      command: [ "python3", "app-test.py" ]

Заменять команды, аргументы, метки и т.д. нельзя. Однако, можно вызвать ошибку, потом пересоздать Pod на лету из сохранённого промежуточного файла:

$ kubectl edit pod nginx-container # отредактировал поле command

error: pods "nginx-container" is invalid
A copy of your changes has been stored to "/tmp/kubectl-edit-1395347318.yaml"
error: Edit cancelled, no valid changes were saved.

$ kubectl replace --force -f /tmp/kubectl-edit-1395347318.yaml

Environmental Variables

Переменные среды задаются как список, похожим образом с командами.

apiVersion: v1
kind: Pod
metadata: 
  name: myapp
  labels:
    app: test_app
    env: productinon
spec:
  containers:
    - name: nginx-container
      image: nginx
      env:
        - name: APP_COLOR 
          value: green

ConfigMap

Отдельный объект, который содержит переменные среды. Можно получить их список через kubectl get configmaps

apiVersion: v1
kind: ConfigMap
metadata: 
  name: mydb
data:
  APP_COLOR: blue
  APP_MODE: testdev

Императивный способ создания ConfigMap

kubectl create configmap \ 
        <имя конфига> --from-literal=<ключ>=<значение> \
                      --from-literal=APP_USER=testuser

kubectl create configmap \
        <имя конфига> --from-file=<путь до файла>
                    # --from-file=app_config.properties

Ссылка на ConfigMap в описании Pod

Ссылка производится по именам ConfigMap в виде списка:

apiVersion: v1
kind: Pod
metadata: 
  name: myapp
  labels:
    app: test_app
    env: productinon
spec:
  containers:
    - name: nginx-container
      image: nginx
      envFrom:
        - configMapRef: 
            name: mydb
            
# Вариант взять только конкретную переменную: 
      env: 
        - name: APP_COLOR 
          valueFrom: 
            configMapKeyRef:
              name: mydb
              key: APP_COLOR

Secrets

Секреты - это ConfigMap, значения которых кодируются по base64. Можно получить их список через:

kubectl get secrets # список секретов
kubectl describe secret <имя секрета> # не отображает значения
kubectl get secret <имя секрета> -o yaml # отображает значения в файле 

Декларативное описание

apiVersion: v1
kind: Secret
metadata: 
  name: mydb
data:
  APP_PWD: dmVyeXNlY3JldA== # base64 Encode
  APP_TOKEN: dGVzdGRldg==

Императивный способ создания Secret

kubectl create secret generic \ 
        <имя конфига> --from-literal=<ключ>=<значение> \
                      --from-literal=APP_USER=testuser

kubectl create secret generic \
        <имя конфига> --from-file=<путь до файла>
                    # --from-file=app_config.properties

Ссылка на Secret в описании Pod

apiVersion: v1
kind: Pod
metadata: 
  name: myapp
  labels:
    app: test_app
    env: productinon
spec:
  containers:
    - name: nginx-container
      image: nginx
      envFrom:
        - secretRef: 
            name: mydb

# Вариант взять только конкретное значение: 
      env: 
        - name: APP_COLOR 
          valueFrom: 
            secretKeyRef:
              name: mydb
              key: APP_PWD
              
# Вариант смонтировать как файлы (каждый пароль - отдельный файл)
      volumes:
      - name: app-secret-volume
        secret: 
          secretName: app-secret

Service Accounts

Специальные учётные записи для доступа к k8s. При создании вместе с ними создаётся объект secret.

  • С версии k8s 1.22 объект Secret имеет время жизни;
  • С версии k8s 1.24 секрет не создаётся на автомате, нужно его отдельно создать:
kubectl create serviceaccount dashboard-sa
kubectl create token dashboard-sa # с k8s 1.24+ необходимо создать токен, у которого время жизни (по умолчанию) =1 час с момента создания

Pods

Articles

PODs

Pod - наименьшая сущность в k8s. Обычно, pod = контейнер по принципу 1:1. Однако, можно несколько контейнеров разместить в 1 pod, при условии, что они функционально разные. Обычно это главный контейнер приложения и вспомогательные контейнеры, которые с ним связаны.

В обычном Docker, если развернуть множество копий “контейнер приложения” + “вспомогательный контейнер”, то нужно будет иметь карту взаимосвязей между ними всеми. Более того, в случае выхода из строя контейнера с приложением, нужно будет вручную удалять сопутствующий вспомогательный контейнер. От этого всего избавляют pod-ы, в рамках которых всё размещается, обеспечивается внутренняя связность, и далее k8s размножает готовые копии pod-ов в рамках кластера.

Pod-ы добавляют функционал к контейнерам:

  • Labels and annotations
  • Restart policies
  • Probes (startup probes, readiness probes, liveness probes, and potentially more)
  • Affinity and anti-affinity rules
  • Termination control
  • Security policies
  • Resource requests and limits

Работа с pod-ами ведётся с помощью API или инструмента kubectl:

kubectl run nginx --image nginx # образ nginx будет скачан с DockerHub
kubectl get pods # список всех pod-ов и их статусов
kubectl get pods --selector app=App1 # отфильтровать вывод по заданному label

Создание Pod через файл YAML

Создадим pod-definition.yml:

apiVersion: v1
kind: Pod       
metadata: 
  name: myapp-pod
  labels:
    app: myapp
    type: front-end
spec:
  containers:
    - name: nginx-container
      image: nginx

Далее создаём pod командой:

kubectl create -f pod-definition.yml
kubectl get pods

Посмотреть доступные поля, подробную информацию о поле у pod:

kubectl explain pods --recursive
kubectl explain pod.spec.restartPolicy

Посмотреть конкретное поле у всех pod, например, образ image, из которого он сделан:

kubectl get pods -o jsonpath={.items[*].spec.containers[*].image}

Можно у работающего Pod получить спецификацию в YAML, из которой он сделан:

kubectl get pod <имя pod> -o yaml > pod-definition.yaml

Удалить Pod

kubectl delete pod <имя Pod> --now 

Обновить Pod

В конфигурацию pod можно добавить период обновления (например, 30 секунд) и установить “imagePullPolicy: “Always”. Удалить Pod с помощью kubectl delete pod pod_name. Новый контейнер будет создан на последней версии образа, после чего старый контейнер будет удалён.

spec:
  terminationGracePeriodSeconds: 30
  containers:
  - name: my_container
    image: my_image:latest
    imagePullPolicy: "Always"

Есть вариант “дёргать” за Deployment, вызывая тем самым обновление:

kubectl patch deployment <имя deployment> -p \
  '{"spec":{"template":{"spec":{"terminationGracePeriodSeconds":31}}}}'

Выполнение задач в Pod

Если необходимо, чтобы Pod поработал и выключился, без перезапуска, то необходимо поменять его restartPolicy, которая по умолчанию стоит в Always - то есть перезапуск всегда по завершении работы.

spec:
  containers:
  - name: my_container
    image: my_image:latest
  restartPolicy: Never # ещё вариант OnFailure

Императивные команды

В отличие от декларативных, такие команды позволяют быстро решить однократную задачу.

kubectl run nginx --image=nginx --dry-run=client -o yaml # --dry-run=client - не создаёт объект, сообщает о возможности его создания

kubectl run httpd --image=httpd:alpine --port=80 --expose=true # создать Pod из образа httpd:alpine и к нему сразу создать ClusterIP с публикацией порта

Multi-Container PODs

Несколько контейнеров в 1 POD делят один адрес IP (между собой они общаются через адаптер localhost), хранилище. Есть несколько типовых сценариев:

  • Sidecar pattern - самый популярный случай, один контейнер отрабатывает задачу (например, выгрузки данных на веб-сайт), а другой решает вспомогательную задачу (например, синхронизация данных для последующей выгрузки);
  • Init pattern - перед запуском контейнера с основным ПО сначала стартует вспомогательный контейнер, который производит настройку окружения;
  • Adapter pattern - ПО в основном контейнере обрабатывает данные, а вспомогательный контейнер передаёт эти данные в другое приложение в понятном ему формате. Например, система SIEM не понимает формат логов приложения, и вспомогательный модуль парсит и транслирует логи в понятный для SIEM формат;
  • Ambassador pattern - ПО в основном контейнере отрабатывает задачи, а вспомогательный контейнер вызывает через API внешние системы, чтобы собрать с них данные для обработки, либо передать данные в эти системы.

PODы стартуют атомарно - только после успешного старта всех контейнеров POD считается запущенным. Частичный запуск не допускается. POD целиком всеми контейнерами размещается на одной ноде worker.

apiVersion: v1
kind: Pod        
metadata: 
  name: myapp-pod
  labels:
    app: myapp
spec:
  containers:
    - name: nginx-container
      image: nginx
      ports:
        - containerPort: 8080
    - name: log-agent
      image: log-agent

InitContainer - не живёт постоянно, а выполняется ДО загрузки остальных контейнеров в Pod, поэтому его инициализация - в отдельной секции:

apiVersion: v1
kind: Pod        
metadata: 
  name: myapp-pod
  labels:
    app: myapp
spec:
  containers:
    - name: myapp-container
      image: nginx
      ports:
        - containerPort: 8080
  initContainers:
    - name: init-service
      image: busybox
      command: [ 'sh', '-c', 'git clone <some repo to be used by app>' ]

Если таких InitContainer несколько, они будут выполняться последовательно один за другим. Если любой InitContainer не сможет выполниться успешно, k8s будет перезапускать Pod, пока InitContainer успешно не завершится.

Subsections of Pods

Readiness Probes

Readiness Probes

Определение, что ПО в контейнер действительно запустилось успешно и готово принимать данные пользователей, можно провести по-разному, добавив в манифест раздел spec -> containers поле readinessProbe.

  • Для проверки HTTP сервера:

    readinessProbe:
      httpGet:
        path: /api/ready
        port: 8080
      initialDelaySeconds: 10 # предусматриваем 10 сек задержку на старте
      periodSeconds: 5 # повторяем проверку спустя 5 секунд
      failureThreshold: 8 # повторяем проверку 8 раз (по умолчанию 3)
    
  • Для проверки открытого порта (например, у СУБД):

    readinessProbe:
      tcpSocket:
        port: 3306
    
  • Для проверки с помощью своей команды:

    readinessProbe:
      exec:
        command: 
          - cat
          - /app/is_ready
    

Liveness Probes

Периодическое определение, работает ли ПО в контейнере - для случаев, когда падение ПО не приводит к его вылету и закрытию контейнера.

  • Для проверки HTTP сервера:

    livenessProbe:
      httpGet:
        path: /api/health_status
        port: 8080
      initialDelaySeconds: 10 # предусматриваем 10 сек задержку на старте
      periodSeconds: 5 # повторяем проверку спустя 5 секунд
      failureThreshold: 8 # повторяем проверку 8 раз (по умолчанию 3)
    
  • Для проверки открытого порта (например, у СУБД):

    livenessProbe:
      tcpSocket:
        port: 3306
    
  • Для проверки с помощью своей команды:

    livenessProbe:
      exec:
        command: [ "cat", "/app/is_working" ]
    

Container Logging

Для получения логов с Pod:

kubectl logs {-f} <pod name> <container name> # -f = tail

kubectl logs -f myapp logger # пример, в случае нескольких контейнеров в Pod выбран контейнер logger 

Resources

Requests

Запрос контейнеров на гарантированные ресурсы.

apiVersion: v1
kind: Pod
metadata:
  name: myapp
  labels:
    name: myapp
spec:
  containers:
  - name: mywebapp
    image: nginx
    resources:
      requests:
        memory: "4Gi" # 4 гибибайта
        cpu: 2 # минималка 0.1 CPU

Limits

Указание ограничений. При переходе лимита по CPU скорость для Pod замедляется (throttling). При переходе лимита по RAM происходит убийство Pod с ошибкой OOM Error (Out-Of-Memory).

apiVersion: v1
kind: Pod
metadata:
  name: myapp
  labels:
    name: myapp
spec:
  containers:
  - name: mywebapp
    image: nginx
    resources:
      requests:
        memory: "2Gi"
        cpu: 2
      limits:
        memory: "4Gi"
        cpu: 2

Security Contexts

Security Contexts

В описании Pod можно указать ID пользователя, который запускает контейнеры, а также описать его возможности (capabilities).

  • Если контекст безопасности определён на уровне Pod, он действует для всех входящих в него контейнеров;
  • Если контекст безопасности определён на уровне Pod и на уровне контейнера, то настройки контейнера приоритетны перед настройками Pod.

Уровень Pod:

apiVersion: v1
kind: Pod
metadata:
  name: web-pod
spec:
  securityContext:
    runAsUser: 1001
  containers:
    - name: ubuntu
      image: ubuntu

Уровень контейнера:

apiVersion: v1
kind: Pod
metadata:
  name: web-pod
spec:
  containers:
    - name: ubuntu
      image: ubuntu
      securityContext:
        runAsUser: 1001
        capabilities:   
          add: ["MAC_ADMIN"]
# возможности можно определить ТОЛЬКО на уровне контейнера

Selectors and Affinity

Node Selectors

Добавить пометки к node можно командой:

kubectl label nodes <node-name> <label-key>=<label-value>

kubectl label nodes node-01 size=Large # пример

После этого в описании Pod можно указать Node Selector:

apiVersion: v1
kind: Pod
metadata: 
  name: myapp
spec:
  containers:
  - name: data-processor
    image:: data-processor
  nodeSelector:
    size: Large

Node Selectors работают по принципу 1:1 совпадения метки Node и Pod. Для более сложных сценариев применяют Node Affinity.

Node Affinity

Для создание свойств Node Affinity нужно поменять свойства в манифесте Pod:

apiVersion: v1
kind: Pod
metadata: 
  name: myapp
spec:
  containers:
  - name: data-processor
    image:: data-processor
  affinity:
    nodeAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
        nodeSelectorTerms:
        - matchExpressions:
          - key: size
            operator: In # условие, может быть наоборот NotIn, или Exists - есть ли вообще такой label, необязательно имеющий значение?
            values:
            - Large # условие действует на любое из значений списка
            - Medium

В случае, если нужные labels отсутствуют на Nodes кластера, есть 2 типа поведения, которое задаётся свойством Pod:

  • requiredDuringSchedulingIgnoredDuringExecution - если Nodes с нужными labels нет, вообще не размещать данный Pod на кластере;
  • preferredDuringSchedulingIgnoredDuringExecution - если Nodes с нужными labels нет, всё равно разместить данный Pod где-нибудь на кластере.

Если Pod уже запущен на Node в момент, когда добавили label, то в версии 1.27 ничего не произойдёт в обоих случаях. В плане добавить третий тип поведения:

  • requiredDuringSchedulingRequiredDuringExecution - если во время работы Pod произойдёт изменение affinity - удалить Pod с Node.

Taints & Tolerations

Для распределения Pods по Nodes применяется сочетание покраски (taint) и восприимчивости (toleration) к ней.

Taints

Покраска Node говорит kube-scheduler, что есть 1 из 3 эффектов:

  • NoSchedule - не назначать сюда Pods без toleration;
  • PreferNo Schedule - назначать Pods без toleration с наименьшим приоритетом, если больше некуда;
  • NoExecute - не назначать сюда Pods без toleration, уже имеющиеся тут Pods удалить и перенести куда-то ещё.

Покраска node:

kubectl taint nodes <имя node> key=value:effect

kubectl taint nodes node01 app=myapp:NoSchedule # пример
kubectl taint nodes node01 app=myapp:NoSchedule- # минус в конце снимает покрас

Tolerations

Поменять восприимчивость Pod к покраске:

apiVersion: v1
kind: Pod
metadata:
  name: myapp-pod
spec:
  containers:
  - name: nginx-controller
    image: nginx
  tolerations:
  - key: "app"
    operator: "Equal"
    value: "blue"
    effect: "NoSchedule"

Replicasets & Deployments

ReplicaSets

ReplicaSet следит за тем, чтобы количество Pod всегда было заданным числом штук (параметр replicas) - не больше и не меньше. ReplicaSet Controller является более современным аналогом ReplicationController:

  • ReplicationController apiVersion = v1
  • ReplicaSetController apiVersion = apps/v1

Отличие ReplicaSet в том, что в нём обязательным параметром есть selectors, который отслеживает контейнеры, в том числе созданные ДО создания самого ReplicaSet. Отслеживание можно производить по их меткам labels. Для совпадающих меток работает алгоритм приведения к нужному количеству.

Создание ReplicaSet:

apiVersion: apps/v1
kind: ReplicaSet
metadata:
  name: myreplicaset
  labels:
    name: myapp
spec:
  selector:
    matchLabels:
      env: production ### метка должна совпадать
  replicas: 3
  template:
    metadata:  ### шаблон контейнера берётся из описания Pod
      name: myapp
      labels:
        env: production ### метка должна совпадать
    spec:
      containers:
        - name: nginx-container
          image: nginx
    

Шаблон Pod всегда должен быть описан в части template, чтобы ReplicaSet знал, какие Pod создавать.

Команды для работы с ReplicaSets

kubectl create -f <имя файла с описанием replicaset>
kubectl get rs # вывести все ReplicaSet в кластере
kubectl describe rs <имя replicaset> # подробности о ReplicaSet
kubectl delete rs <имя replicaset> # удаляет все Pods и сам ReplicaSet
kubectl edit rs <имя replicaset> # отредактировать описание Replicaset
kubectl scale rs <имя replicaset> --replicas=<новое количество копий> 
kubectl replace -f <имя файла с описанием replicaset> # заменить ReplicaSet 

Deployments

Deployment - это надстройка над ReplicaSet, добавляет логику обновления Pod с одной версии на другую. При обновлении Deployment имеет 2 стратегии:

  • Rolling Update - (по умолчанию) при обновлении Pods, делает это поштучно: один Pod старой версии кладёт (Terminated), сразу поднимает заместо него новый Pod;
  • Recreate - при обновлении сначала удаляются все Pods, после чего взамен поднимаются новые. Сопровождается падением/отключением приложения для потребителей.
kubectl apply -f <имя Deployment> # запустить процесс rollout после внесения изменений в манифест
kubectl rollout status <имя Deployment> # узнать о статусе выкатывания

kubectl rollout history <имя Deployment> # узнать о всех ревизиях и причинах их перехода
kubectl rollout history <deployment> --revision=1 # узнать статус конкретной версии

kubectl rollout undo <имя Deployment> # откатить назад обновление Deployment
kubectl rollout undo <имя Deployment> --to-revision=1 # откатить до версии 1
apiVersion: apps/v1
kind: Deployment
metadata:
  name: mydeployment
  labels:
    name: myapp
spec:
  selector:
    matchLabels:
      env: production ### метка должна совпадать
  replicas: 3
  strategy: 
    type: RollingUpdate ### стратегия замены Pods
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 0
  template:
    metadata:  ### шаблон контейнера берётся из описания Pod
      name: myapp
      labels:
        env: production ### метка должна совпадать
    spec:
      containers:
        - name: nginx-container
          image: nginx
    

Команды для работы с Deployments

kubectl create -f <имя файла с описанием Deployment>
kubectl get deploy # вывести все Deployment в кластере
kubectl describe deploy <имя Deployment> # подробности о Deployment
kubectl delete rs <имя Deployment> # удаляет все Pods и сам Deployment
kubectl edit rs <имя Deployment> # отредактировать описание Deployment и произвести его обновление
kubectl edit rs <имя Deployment> --record # отредактировать описание, вызвав обновление и записать команду как причину обновления в список ревизий
kubectl set image deploy <имя Deployment> nginx=nginx:1.18 # пример обновления без редактирования YAML

Императивные команды

В отличие от декларативных, такие команды позволяют быстро решить однократную задачу.

kubectl create deploy nginx --image=nginx --replicas=4
kubectl scale deploy nginx --replicas=4

kubectl create deployment nginx --image=nginx --dry-run=client -o yaml > nginx-deployment.yaml # вывести манифест YAML для последующего редактирования 

Services

Services

Service - логический объект в k8s, который позволяет внешним клиентам подключаться к сетевым портам на контейнерах внутри кластеров Pod.

Service делятся на 3 вида:

  • NodePort - публикация порта с Pod наружу на уровень Worker Node (причём, если Pods размазаны лишь по части от всех нод кластера k8s, то всё равно они доступны при обращении К ЛЮБОЙ ноде кластера);
  • ClusterIP - по сути внутренний балансировщик для обращения, например, части frontend приложения к множеству Pods, реализующих backend;
  • LoadBalancer - NodePort с заданием стратегии балансировки (в NodePort - случайный алгоритм балансировки), поддерживается лишь на ряде облачных площадок (AWS, GCM и т.д.). В остальных площадках ведёт себя как NodePort.

NodePort

apiVersion: v1
kind: Service
metadata:
  name: myservice
spec: 
  type: NodePort
  ports:
    - port: 80
      targetPort: 80
      nodePort: 30004 # порты ограничены диапазоном 30000-32767
  selector:
    app: myapp

ClusterIP

apiVersion: v1
kind: Service
metadata:
  name: myservice
spec: 
  type: ClusterIP
  ports:
    - port: 80
      targetPort: 80
  selector:
    app: myapp

LoadBalancer

apiVersion: v1
kind: Service
metadata:
  name: myservice
spec: 
  type: LoadBalancer # only works with a supported platform
  ports:
    - port: 80
      targetPort: 80
  selector:
    app: myapp

Команды работы с Service

kubectl create -f <имя файла с описанием Service>
kubectl get svc # вывести все Service в кластере
kubectl describe svc <имя service> # подробности о service
kubectl delete svc <имя service> # удаляет все объект service
kubectl edit svc <имя service> # отредактировать описание service и произвести его обновление

Императивные команды

В отличие от декларативных, такие команды позволяют быстро решить однократную задачу.

kubectl create service clusterip redis --tcp=6379:6379 --dry-run=client -o yaml # нельзя подавать selectors в команду, следует вывести YAML и отредактировать, что небыстро

kubectl expose pod redis --port=6379 --name=redis-service --dry-run=client -o yaml # такой вариант CLusterIP использует labels самого pod как selectors, что намного эффективнее

kubectl expose pod nginx --port=80 --name=nginx-service --type=NodePort --dry-run=client -o yaml # такой вариант NodePort использует labels самого pod как selectors