weisdd microservices repository
В данной работе мы:
- установили docker, docker-compose, docker-machine;
- рассмотрели жизненные цикл контейнера на примере hello-world и nginx;
- рассмотрели отличия image и container.
$ docker info - информация о dockerd (включая количество containers, images и т.п.).
$ docker version
$ docker images - список всех images.
$ docker ps - список запущенных на текущий момент контейнеров.
$ docker ps -a - список всех контейнеров, в т.ч. остановленных.
$ docker system df - информация о дисковом пространстве (контейнеры, образы и т.д.).
$ docker inspect <id> - подробная информация об объекте docker.
$ docker run hello-world - запуск контейнера hello-world. Может служить тестом на проверку работоспособности docker.
$ docker run -it ubuntu:16.04 /bin/bash - пример того, как можно создать и запустить контейнер с последующим переходом в терминал.
-i - запуск контейнера в foreground-режиме (docker attach).
-d - запуск контейнера в background-режиме.
-t - создание TTY.
Пример:
* docker run -it ubuntu:16.04 bash
* docker run -dt nginx:latest
Важные моменты:
- если не указывать флаг --rm, то после остановки контейнер останется на диске;
- docker run каждый раз запускает новый контейнер;
- docker run = docker create + docker start;
$ docker start <u_container_id> - запуск остановленного контейнера.
$ docker attach <u_container_id> - подключение к терминалу уже созданного контейнера.
$ docker exec <u_container_id> <command> - запуск нового процесса внутри контейнера.
Пример:
docker exec -it <u_container_id> bash
$ docker commit <u_container_id> <name> - создание image из контейнера.
$ docker kill <u_container_id> - отправка SIGKILL.
$ docker stop <u_container_id> - отправка SIGTERM, затем (через 10 секунд) SIGKILL.
Пример:
docker kill $(docker ps -q) - уничтожение всех запущенных контейнеров.
$ docker rm <u_container_id> - удаление контейнер (должен быть остановлен).
-f - позволяет удалить работающий контейнер (предварительно посылается SIGKILL).
Пример:
$ docker rm $(docker ps -a -q) - удаление всех незапущенных контейнеров.
$ docker rmi - удаление image, если от него не зависят запущенные контейнеры.
В данной работе мы:
- создали docker host;
- описали Dockerfile;
- опубликовали Dockerfile на Docker Hub;
- подготовили прототип автоматизации деплоя приложения в связке Packer + Ansible Terraform.
Создание хоста с docker в GCP при помощи docker-machine:
$ docker-machine create --driver google --google-machine-image https://www.googleapis.com/compute/v1/projects/ubuntu-os-cloud/global/images/family/ubuntu-1604-lts --google-machine-type n1-standard-1 --google-zone europe-west1-b docker-host
Переключение на удалённый docker (все команды docker будут выполняться на удалённом хосте):
eval $(docker-machine env <имя>)
Переключение на локальный докер:
eval $(docker-machine env --unset)
Удаление:
docker-machine rm <имя>
Для полного описания контейнера нам потребуются следующие файлы:
- Dockerfile
- mongod.conf
- db_config
- start.sh
FROM ubuntu:16.04
RUN apt-get update
RUN apt-get install -y mongodb-server ruby-full ruby-dev build-essential git
RUN gem install bundler
RUN git clone -b monolith https://github.com/express42/reddit.git
COPY mongod.conf /etc/mongod.conf
COPY db_config /reddit/db_config
COPY start.sh /start.sh
RUN cd /reddit && bundle install
RUN chmod 0777 /start.sh
CMD ["/start.sh"]
Создание image из Dockerfile:
$ docker build -t reddit:latest .
-t - тэг для собранного образа;
Запуск контейнера на основе нашего образа:
$ docker run --name reddit -d --network=host reddit:latest
d6a3b85c02f45a830ae33bedd8e3eb9c40fe92eca36f3739818c73aaea903172
$ docker-machine ls
NAME ACTIVE DRIVER STATE URL SWARM DOCKER ERRORS
docker-host - google Running tcp://35.195.29.23:2376 v18.09.6
После регистрации на docker hub выполняем аутентификацию: $ docker login
Публикация образа на docker hub:
$ docker tag reddit:latest weisdd/otus-reddit:1.0
$ docker push weisdd/otus-reddit:1.0
The push refers to repository [docker.io/weisdd/otus-reddit]
0bf5968c99fc: Pushed
b1c92e529423: Pushed
28c2df747281: Pushed
34a6ce7c331c: Pushed
25c23944e2ff: Pushed
95a0f83dbe7b: Pushed
ed18b4cc6d66: Pushed
8990d0339489: Pushed
d1bdff2328b9: Pushed
4c54072a5034: Mounted from library/ubuntu
49652298c779: Mounted from library/ubuntu
e15278fcccca: Mounted from library/ubuntu
739482a9723d: Mounted from library/ubuntu
1.0: digest: sha256:89a2160aa4440081157a1534b63bb987e69db67006d58b98e960ddbc31a98da7 size: 3034
Проверка, с какой командой будет запущен контейнер:
$ docker inspect weisdd/otus-reddit:1.0 -f '{{.ContainerConfig.Cmd}}'
Список изменений в ФС с момента запуска контейнера:
$ docker diff reddit
Задание: Теперь, когда есть готовый образ с приложением, можно автоматизировать поднятие нескольких инстансов в GCP, установку на них докера и запуск там образа /otus-reddit:1.0 Нужно реализовать в виде прототипа в директории /docker-monolith/infra/
- Поднятие инстансов с помощью Terraform, их количество задается переменной;
- Несколько плейбуков Ansible с использованием динамического инвентори для установки докера и запуска там образа приложения;
- Шаблон пакера, который делает образ с уже установленным Docker.
Решение: Решение базировалось на том, что мы описывали в infra-репозитории, - это в последствии сыграет нам на руку.
Здесь всё предельно схоже с предыдущим кодом: main.tf:
terraform {
# Версия terraform
required_version = ">=0.11,<0.12"
}
provider "google" {
# Версия провайдера
version = "2.0.0"
# ID проекта
project = "${var.project}"
region = "${var.region}"
}
resource "google_compute_instance" "app" {
count = "${var.number_of_instances}"
name = "docker-host-${count.index}"
machine_type = "g1-small"
zone = "${var.zone}"
boot_disk {
initialize_params {
# Здесь можно передать либо имя семейства, либо полное имя
image = "${var.app_disk_image}"
}
}
metadata {
# путь до публичного ключа
# file считывает файл и вставляет в конфигурационный файл
ssh-keys = "appuser:${file(var.public_key_path)}"
}
tags = ["docker-machine"]
labels {
ansible_group = "docker-host"
env = "${var.label_env}"
}
network_interface {
# сеть, к которой присоединить данный интерфейс
network = "default"
# использовать ephemeral IP для доступа в Интернет
access_config {
nat_ip = "${element(google_compute_address.app_ip.*.address, count.index)}"
}
}
# Параметры подключения провижионеров
connection {
type = "ssh"
user = "appuser"
agent = false
private_key = "${file(var.private_key_path)}"
}
}
resource "google_compute_address" "app_ip" {
count = "${var.number_of_instances}"
name = "reddit-app-ip-${count.index}"
region = "${var.region}"
}
outputs.tf
output "app_external_ip" {
value = "${google_compute_instance.app.*.network_interface.0.access_config.0.nat_ip}"
}
variables.tf
variable "project" {
description = "Project ID"
}
variable "region" {
description = "Region"
default = "europe-west-1"
}
variable "public_key_path" {
description = "Path to the public key used for ssh access"
}
variable "app_disk_image" {
description = "Disk image for reddit app"
default = "docker-host"
}
variable "private_key_path" {
description = "Path to the private key used for ssh provisioners"
}
variable "zone" {
description = "Zone"
default = "europe-west1-b"
}
variable "number_of_instances" {
description = "Number of reddit-app instances (count)"
default = 1
}
variable "location" {
description = "Bucket location"
default = "europe-west1"
}
variable "label_env" {
description = "GCP label 'env' associating an instance with an environment in which it's being run (e.g. stage, prod)"
default = "stage"
}
terraform.tfvars
project = "docker-12345"
public_key_path = "~/.ssh/appuser.pub"
app_disk_image = "docker-host"
region = "europe-west1"
private_key_path = "~/.ssh/appuser"
number_of_instances = 1
location = "europe-west1"
label_env = "stage"
ansible.cfg
[defaults]
inventory = ./environments/stage/inventory_gcp.yml
remote_user = appuser
private_key_file = ~/.ssh/appuser
host_key_checking = False
retry_files_enabled = False
roles_path = ./roles
vault_password_file = ~/devops/vault.key
[inventory]
enable_plugins = gcp_compute, host_list, script, yaml, ini, auto
[diff]
# Включим обязательный вывод diff при наличии изменений и вывод 5 строк контекста
always = True
context = 5
Dynamic inventory будет основан на gcp_compute (он как раз и был активирован выше в секции inventory). inventory.gcp
---
#http://docs.testing.ansible.com/ansible/latest/plugins/inventory/gcp_compute.html
#Uses a YAML configuration file that ends with gcp_compute.(yml|yaml) or gcp.(yml|yaml).
plugin: gcp_compute
zones:
- europe-west1-b
projects:
- docker-240120
scopes:
- https://www.googleapis.com/auth/compute
service_account_file: ~/devops/service_account_docker.json
auth_kind: serviceaccount
filters:
- labels.env = stage
keyed_groups:
# <prefix><separator><key>
- prefix: ""
separator: ""
key: labels.ansible_group
hostnames:
# List hosts by name instead of the default public ip
- name
compose:
# Set an inventory parameter to use the Public IP address to connect to the host
# For Private ip use "networkInterfaces[0].networkIP"
ansible_host: networkInterfaces[0].accessConfigs[0].natIP
Плейбуки для удобства разбиты на роли docker_host и otus_reddit. ansible/roles/docker_host/tasks/main.yml
---
- include: install_docker.yml
ansible/roles/docker_host/tasks/install_docker.yml
---
- name: Install packages to allow apt to use a repository over HTTPS
apt:
name: "{{ packages }}"
update_cache: yes
vars:
packages:
- apt-transport-https
- ca-certificates
- curl
- gnupg-agent
- software-properties-common
- name: Add APT key
apt_key:
url: https://download.docker.com/linux/ubuntu/gpg
state: present
- name: Add APT repository
apt_repository:
repo: "deb [arch=amd64] https://download.docker.com/linux/ubuntu {{ ansible_distribution_release }} stable"
state: present
- name: Install Docker CE & python-docker
apt:
name: "{{ packages }}"
update_cache: yes
vars:
packages:
- docker-ce
- docker-ce-cli
- containerd.io
- python-docker
- поскольку далее мы планируем использовать Ansible, нам необходимо установить python-docker. Все остальные шаги - "перевод" официальной документации Docker (список shell-команд) в yaml.
ansible/roles/otus_reddit/tasks/main.yml
---
- include: deploy_container.yml
ansible/roles/otus_reddit/tasks/deploy_container.yml
---
- name: Create a container with the otus-reddit app
docker_container:
name: reddit
image: "weisdd/otus-reddit:1.0"
ports:
- "9292:9292"
Плейбуки, в которых используются описанные выше роли: site.yml
---
- import_playbook: base.yml
- import_playbook: docker_host.yml
- import_playbook: otus_reddit.yml
base.yml
---
- name: Check && install python
hosts: all
become: true
gather_facts: false
tasks:
- name: Install python for Ansible
raw: test -e /usr/bin/python || (apt -y update && apt install -y python-minimal)
changed_when: false
docker_host.yml
---
- name: Install docker
hosts: docker_host
become: true
roles:
- docker_host
otus_reddit.yml
---
- name: Deploy app
hosts: docker_host
become: true
roles:
- otus_reddit
Плейбук для Packer:
---
- import_playbook: base.yml
- name: Install Docker CE
hosts: all
become: true
roles:
- docker_host
docker_host.json
{
"variables": {
"project_id": null,
"source_image_family": null,
"machine_type": "f1-micro",
"image_description": "no description",
"disk_size": "10",
"network": "default",
"tags": "docker_host"
},
"builders": [
{
"type": "googlecompute",
"project_id": "{{ user `project_id` }}",
"image_name": "docker-host-{{timestamp}}",
"image_family": "docker-host",
"source_image_family": "{{ user `source_image_family` }}",
"zone": "europe-west1-b",
"ssh_username": "appuser",
"machine_type": "{{ user `machine_type` }}",
"image_description": "{{ user `image_description` }}",
"disk_size": "{{ user `disk_size` }}",
"network": "{{ user `network` }}",
"tags": "{{ user `tags` }}"
}
],
"provisioners": [
{
"type": "ansible",
"playbook_file": "ansible/playbooks/packer_docker_host.yml",
"ansible_env_vars": ["ANSIBLE_ROLES_PATH={{ pwd }}/ansible/roles"]
}
]
}
variables.json.example
{
"project_id": "docker-12345",
"source_image_family": "ubuntu-1804-lts",
"machine_type": "f1-micro",
"image_description": "base image for docker host",
"disk_size": "10",
"network": "default",
"tags": "docker-host"
}
Создаём образ в GCP при помощи packer:
weisdd_microservices/docker-monolith/infra$ packer validate -var-file=packer/variables.json packer/docker_host.json
Template validated successfully.
weisdd_microservices/docker-monolith/infra$ packer build -var-file=packer/variables.json packer/docker_host.json
googlecompute output will be in this color.
[...]
==> googlecompute: Provisioning with Ansible...
[...]
googlecompute: PLAY RECAP *********************************************************************
googlecompute: default : ok=6 changed=4 unreachable=0 failed=0
[...]
==> Builds finished. The artifacts of successful builds are:
--> googlecompute: A disk image was created: docker-host-1559416667
Создаём instance с этим образом:
$ terraform apply
Деплоим контейнер:
weisdd_microservices/docker-monolith/infra/ansible$ ansible-playbook playbooks/otus_reddit.yml
PLAY [Deploy app] ******************************************************************************************************************************
TASK [Gathering Facts] *************************************************************************************************************************
ok: [docker-host-0]
TASK [otus_reddit : Create a container with the otus-reddit app] *******************************************************************************
changed: [docker-host-0]
PLAY RECAP *************************************************************************************************************************************
docker-host-0 : ok=2 changed=1 unreachable=0 failed=0
Теперь приложение доступно по адресу: 35.240.8.240:9292
В данной работе мы:
- научились описывать и собирать Docker-образы для сервисного приложения;
- научились оптимизировать работу с Docker-образами;
- опробовали запуск и работу приложения на основе Docker-образов;
- оценили удобство запуска контейнеров при помощи docker run;
- переопределили ENV через docker run;
- оптимизировали размер контейнера (образ на базе Alpine).
Работа велась в каталоге src, где под каждый сервис существует отдельная директория (comment, post-py, ui). Для MongoDB использовался образ из Docker Hub.
В соответствии с рекомендациями hadolint было внесены изменения:
RUN apt-get update -qq && apt-get install -y build-essential
=>
RUN apt-get update -qq && apt-get install -y build-essential --no-install-recommends
&& apt-get clean && rm -rf /var/lib/apt/lists/*
ADD Gemfile* $APP_HOME/ => COPY Gemfile* $APP_HOME/
ADD . $APP_HOME => COPY . $APP_HOME
RUN apt-get update -qq && apt-get install -y build-essential
=>
RUN apt-get update -qq && apt-get install -y build-essential --no-install-recommends
&& apt-get clean && rm -rf /var/lib/apt/lists/*
ADD Gemfile* $APP_HOME/ => COPY Gemfile* $APP_HOME/
ADD . $APP_HOME => COPY . $APP_HOME
ADD . /app => COPY . /app
В Dockerfile со слайдов было обнаружено ряд проблем:
- image для post-py не собирался, т.к. отсутствовал build-base. Обновлённый dockerfile выглядит следующим образом:
FROM python:3.6.0-alpine
WORKDIR /app
COPY . /app
RUN apk update && apk add --no-cache build-base \
&& pip install -r /app/requirements.txt \
&& apk del build-base
ENV POST_DATABASE_HOST post_db
ENV POST_DATABASE posts
ENTRYPOINT ["python3", "post_app.py"]
- образы для comment и ui не собирались из-за отсутствия одной записи в apt list:
W: Failed to fetch http://deb.debian.org/debian/dists/jessie-updates/InRelease Unable to find expected entry 'main/binary-amd64/Packages' in Release file (Wrong sources.list entry or malformed file)
E: Some index files failed to download. They have been ignored, or old ones used instead.
Поэтому пришлось использовать другую версию контейнера: FROM ruby:2.2 => FROM ruby:2.3
После этого все образы успешно собрались:
$ docker build -t weisdd/post:1.0 post-py/
$ docker build -t weisdd/comment:1.0 ./comment
$ docker build -t weisdd/ui:1.0 ./ui
Также мы скачали image для mongo:
$ docker pull mongo:latest
Для удобства, в наших контейнерах использовались сетевые алиасы (отсылка к ним есть в ENV). Поскольку в сети по умолчанию алиасы недоступны, потребовалось создать отдельную bridge-сеть.
$ docker network create reddit
$ docker run -d --network=reddit --network-alias=post_db --network-alias=comment_db mongo:latest
$ docker run -d --network=reddit --network-alias=post weisdd/post:1.0
$ docker run -d --network=reddit --network-alias=comment weisdd/comment:1.0
$ docker run -d --network=reddit -p 9292:9292 weisdd/ui:1.0
Задание: Остановите контейнеры: docker kill $(docker ps -q) Запустите контейнеры с другими сетевыми алиасами. Адреса для взаимодействия контейнеров задаются через ENV-переменные внутри Dockerfile'ов. При запуске контейнеров (docker run) задайте им переменные окружения соответствующие новым сетевым алиасам, не пересоздавая образ. Проверьте работоспособность сервиса
Решение: Переопределить ENV мы можем при помощи флага -e:
$ docker run -d --network=reddit --network-alias=post_db2 --network-alias=comment_db2 mongo:latest
$ docker run -d --network=reddit --network-alias=post2 -e POST_DATABASE_HOST=post_db2 weisdd/post:1.0
$ docker run -d --network=reddit --network-alias=comment2 -e COMMENT_DATABASE_HOST=comment_db2 weisdd/comment:1.0
$ docker run -d --network=reddit -p 9292:9292 -e POST_SERVICE_HOST=post2 -e COMMENT_SERVICE_HOST=comment2 weisdd/ui:1.0
После внесения изменений в Dockerfile и пересборки образа:
$ docker build -t weisdd/ui:2.0 ./ui
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
weisdd/ui 2.0 0bab01157f90 About a minute ago 402MB
weisdd/ui 1.0 29a58be9410f 27 minutes ago 995MB
Задание: Попробуйте собрать образ на основе Alpine Linux. Придумайте еще способы уменьшить размер образа. Можете реализовать как только для UI сервиса, так и для остальных (post, comment). Все оптимизации проводите в Dockerfile сервиса. Дополнительные варианты решения уменьшения размера образов можете оформить в виде файла Dockerfile.<цифра> в папке сервиса.
Решение:
После перехода на Alpine образ уменьшился вдвое:
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
weisdd/ui 2.0 0bab01157f90 About an hour ago 402MB
weisdd/ui 1.0 29a58be9410f 2 hours ago 995MB
FROM alpine:3.9.4
RUN apk update && apk add --no-cache build-base ruby-full ruby-dev ruby-bundler \
&& gem install bundler --no-ri --no-rdoc
ENV APP_HOME /app
RUN mkdir $APP_HOME
WORKDIR $APP_HOME
COPY Gemfile* $APP_HOME/
RUN bundle install
COPY . $APP_HOME
ENV POST_SERVICE_HOST post
ENV POST_SERVICE_PORT 5000
ENV COMMENT_SERVICE_HOST comment
ENV COMMENT_SERVICE_PORT 9292
CMD ["puma"]
Более полная оптимизация:
- ruby вместо ruby-full (соответственно, нужно ставить отдельные компоненты вроде ruby-json);
- комбинирование всех команд, связанных с установкой приложения, в одну инструкцию RUN, что позволяет удалить build-base и ruby-dev после сборки приложения.
FROM alpine:3.9.4
ENV APP_HOME /app
RUN mkdir $APP_HOME
WORKDIR $APP_HOME
COPY . $APP_HOME
RUN apk update && apk add --no-cache build-base ruby ruby-json ruby-dev ruby-bundler \
&& gem install bundler --no-ri --no-rdoc \
&& bundle install \
&& apk del build-base ruby-dev
ENV POST_SERVICE_HOST post
ENV POST_SERVICE_PORT 5000
ENV COMMENT_SERVICE_HOST comment
ENV COMMENT_SERVICE_PORT 9292
CMD ["puma"]
Таким образом, мы уменьшили размер образа с 995MB до 67.9MB:
$ docker build -t weisdd/ui:2.3 ./ui
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
weisdd/ui 2.3 580d3a9549ee 45 seconds ago 67.9MB
В целом, миграция с ruby-full на ruby+отдельные компоненты не даёт большого выйгрыша в дисковом пространстве. При этом поддержка образа усложняется, поскольку при включении в приложение дополнительного компонента (в процессе разработки) придётся выполнять пересборку. Но для эксперимента сгодится.
Версия с ruby-full (69.2MB):
FROM alpine:3.9.4
ENV APP_HOME /app
RUN mkdir $APP_HOME
WORKDIR $APP_HOME
COPY . $APP_HOME
RUN apk update && apk add --no-cache build-base ruby-full ruby-dev ruby-bundler \
&& gem install bundler --no-ri --no-rdoc \
&& bundle install \
&& apk del build-base ruby-dev
ENV COMMENT_DATABASE_HOST comment_db
ENV COMMENT_DATABASE comments
CMD ["puma"]
Версия с ruby + отдельными компонентами (65.1MB):
FROM alpine:3.9.4
ENV APP_HOME /app
RUN mkdir $APP_HOME
WORKDIR $APP_HOME
COPY . $APP_HOME
RUN apk update && apk add --no-cache build-base ruby ruby-json ruby-bigdecimal ruby-dev ruby-bundler \
&& gem install bundler --no-ri --no-rdoc \
&& bundle install \
&& apk del build-base ruby-dev
ENV COMMENT_DATABASE_HOST comment_db
ENV COMMENT_DATABASE comments
CMD ["puma"]
После внесённых изменений, приложение можно запустить набором из следующих команд:
$ docker run -d --network=reddit --network-alias=post_db --network-alias=comment_db mongo:latest
$ docker run -d --network=reddit --network-alias=post weisdd/post:1.0
$ docker run -d --network=reddit --network-alias=comment weisdd/comment:2.1
$ docker run -d --rm --network=reddit -p 9292:9292 weisdd/ui:2.3
$ docker volume create reddit_db
reddit_db
$ docker run -d --network=reddit --network-alias=post_db --network-alias=comment_db -v reddit_db:/data/db mongo:latest
$ docker run -d --network=reddit --network-alias=post weisdd/post:1.0
$ docker run -d --network=reddit --network-alias=comment weisdd/comment:2.1
$ docker run -d --rm --network=reddit -p 9292:9292 weisdd/ui:2.3
В данной работе мы:
- изучили особенности работы с сетями в Docker;
- опробовали docker-compose;
- параметризировали docker-compose.yml через .env файл;
- переопределили базовое имя проекта;
- при помощи docker-compose.override.yml добавили следующие возможности (*):
- изменять код каждого из приложений, не выполняя сборку образа;
- запускать puma для руби приложений в дебаг-режиме с двумя воркерами (флаги --debug и -w 2)
$ docker run -ti --rm --network none joffotron/docker-net-tools -c ifconfig
В данном случае в контейнере будет только интерфейс lo.
docker run --network host -d nginx
В данном случае будет использоваться namespace хоста.
На стр. 11 было предложено выполнить вышеуказанную команду несколько раз и изучить результат. - В итоге оказался запущенным только первый контейнер, поскольку каждый последующий пытался использовать уже занятый порт:
$ docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
7570632fd416 nginx "nginx -g 'daemon of…" 14 seconds ago Exited (1) 11 seconds ago goofy_mclean
e02b2bd6889a nginx "nginx -g 'daemon of…" 23 seconds ago Exited (1) 20 seconds ago clever_mahavira
4f7d9625ffa0 nginx "nginx -g 'daemon of…" 34 seconds ago Up 32 seconds priceless_goldstine
$ docker logs e02b2bd6889a
2019/06/03 21:19:21 [emerg] 1#1: bind() to 0.0.0.0:80 failed (98: Address already in use)
nginx: [emerg] bind() to 0.0.0.0:80 failed (98: Address already in use)
2019/06/03 21:19:21 [emerg] 1#1: bind() to 0.0.0.0:80 failed (98: Address already in use)
nginx: [emerg] bind() to 0.0.0.0:80 failed (98: Address already in use)
2019/06/03 21:19:21 [emerg] 1#1: bind() to 0.0.0.0:80 failed (98: Address already in use)
nginx: [emerg] bind() to 0.0.0.0:80 failed (98: Address already in use)
2019/06/03 21:19:21 [emerg] 1#1: bind() to 0.0.0.0:80 failed (98: Address already in use)
nginx: [emerg] bind() to 0.0.0.0:80 failed (98: Address already in use)
2019/06/03 21:19:21 [emerg] 1#1: bind() to 0.0.0.0:80 failed (98: Address already in use)
nginx: [emerg] bind() to 0.0.0.0:80 failed (98: Address already in use)
2019/06/03 21:19:21 [emerg] 1#1: still could not bind()
nginx: [emerg] still could not bind()
docker network create reddit --driver bridge
--driver bridge можно не указывать, т.к. это - опция по умолчанию.
Интересной особенностью является тот факт, что при инициализации контейнера к нему можно подключить только одну сеть, тогда как нам необходимо использовать несколько:
$ docker run -d --network=front_net -p 9292:9292 --name ui weisdd/ui:1.0
$ docker run -d --network=back_net --name comment weisdd/comment:1.0
$ docker run -d --network=back_net --name post weisdd/post:1.0
$ docker run -d --network=back_net --name mongo_db --network-alias=post_db --network-alias=comment_db mongo:latest
Дополнительные сети подключаются отдельной командой:
$ docker network connect front_net post
$ docker network connect front_net comment
Список всех сетей можно вывести следующим образом:
docker-user@docker-host:~$ sudo docker network ls
NETWORK ID NAME DRIVER SCOPE
b9b5b1434f86 back_net bridge local
b85ca48c0c17 bridge bridge local
ff21730503d4 front_net bridge local
67dc6e20baac host host local
01e8073f021f none null local
e5f24ec1dd5d reddit bridge local
Каждой bridge-сети будет соответствовать отдельный интерфейс:
docker-user@docker-host:~$ ifconfig | grep br
br-b9b5b1434f86 Link encap:Ethernet HWaddr 02:42:22:16:32:a2
br-e5f24ec1dd5d Link encap:Ethernet HWaddr 02:42:23:6b:b7:a6
br-ff21730503d4 Link encap:Ethernet HWaddr 02:42:aa:ab:90:a3
docker-user@docker-host:~$ brctl show br-b9b5b1434f86
bridge name bridge id STP enabled interfaces
br-b9b5b1434f86 8000.0242221632a2 no veth0b4f90a
veth6340764
vethb631212
docker-user@docker-host:~$ brctl show br-ff21730503d4
bridge name bridge id STP enabled interfaces
br-ff21730503d4 8000.0242aaab90a3 no veth7902c28
veth7e78ec6
veth8f619b0
В iptables существуют соответствующие правила:
$ sudo iptables -nL -t nat
Chain PREROUTING (policy ACCEPT)
target prot opt source destination
DOCKER all -- 0.0.0.0/0 0.0.0.0/0 ADDRTYPE match dst-type LOCAL
Chain INPUT (policy ACCEPT)
target prot opt source destination
Chain OUTPUT (policy ACCEPT)
target prot opt source destination
DOCKER all -- 0.0.0.0/0 !127.0.0.0/8 ADDRTYPE match dst-type LOCAL
Chain POSTROUTING (policy ACCEPT)
target prot opt source destination
MASQUERADE all -- 10.0.1.0/24 0.0.0.0/0
MASQUERADE all -- 10.0.2.0/24 0.0.0.0/0
MASQUERADE all -- 172.17.0.0/16 0.0.0.0/0
MASQUERADE all -- 172.18.0.0/16 0.0.0.0/0
MASQUERADE tcp -- 10.0.1.2 10.0.1.2 tcp dpt:9292
Chain DOCKER (2 references)
target prot opt source destination
RETURN all -- 0.0.0.0/0 0.0.0.0/0
RETURN all -- 0.0.0.0/0 0.0.0.0/0
RETURN all -- 0.0.0.0/0 0.0.0.0/0
RETURN all -- 0.0.0.0/0 0.0.0.0/0
DNAT tcp -- 0.0.0.0/0 0.0.0.0/0 tcp dpt:9292 to:10.0.1.2:9292
- в цепочке POSTROUTING мы видим правила NAT'а для каждой подсети + проброс порта.
Для выставленных наружу портов (в данном случае - 9292) мы видим процесс docker-proxy:
docker-user@docker-host:~$ ps ax | grep docker-proxy
7705 ? Sl 0:00 /usr/bin/docker-proxy -proto tcp -host-ip 0.0.0.0 -host-port 9292 -container-ip 10.0.1.2 -container-port 9292
10982 pts/0 S+ 0:00 grep --color=auto docker-proxy
docker-compose поддерживает интерполяцию переменных окружения, при этом можно использовать как export, так и специальный файл .env (об этом позже):
$ export USERNAME=weisdd
$ weisdd_microservices/src$ docker-compose up -d
Creating network "src_reddit" with the default driver
Creating volume "src_post_db" with default driver
Pulling post_db (mongo:3.2)...
[...]
Status: Downloaded newer image for mongo:3.2
Creating src_ui_1 ... done
Creating src_post_db_1 ... done
Creating src_post_1 ... done
Creating src_comment_1 ... done
ibeliako@dev:~/devops/git/weisdd_microservices/src$ docker-compose ps
Name Command State Ports
----------------------------------------------------------------------------
src_comment_1 puma Up
src_post_1 python3 post_app.py Up
src_post_db_1 docker-entrypoint.sh mongod Up 27017/tcp
src_ui_1 puma Up 0.0.0.0:9292->9292/tcp
Задача 1: Изменить docker-compose под кейс с множеством сетей, сетевых алиасов (стр 18)
Решение:
version: '3.3'
services:
post_db:
image: mongo:3.2
volumes:
- post_db:/data/db
networks:
back_net:
aliases:
- comment_db
ui:
build: ./ui
image: ${USERNAME}/ui:1.0
ports:
- 9292:9292/tcp
networks:
- front_net
post:
build: ./post-py
image: ${USERNAME}/post:1.0
networks:
- back_net
- front_net
comment:
build: ./comment
image: ${USERNAME}/comment:1.0
networks:
- back_net
- front_net
volumes:
post_db:
networks:
back_net:
driver: bridge
ipam:
driver: default
config:
- subnet: 10.0.2.0/24
front_net:
driver: bridge
ipam:
driver: default
config:
- subnet: 10.0.1.0/24
ВАЖНО: На слайдах была ошибка в описании docker-compose - отсутствует алиас для comment_db, поэтому работает всё, кроме комментариев. В решении выше, эта ошибка исправлена.
Задача 2: Параметризуйте с помощью переменных окружений:
- порт публикации сервиса ui
- версии сервисов
- возможно что-либо еще на ваше усмотрение Параметризованные параметры запишите в отдельный файл c расширением .env Без использования команд source и export docker-compose должен подхватить переменные из этого файла.
Решение: docker-compose.yml
version: '3.3'
services:
post_db:
image: mongo:3.2
volumes:
- post_db:/data/db
networks:
back_net:
aliases:
- comment_db
ui:
build: ./ui
image: ${USERNAME}/ui:${UI_VERSION}
ports:
- ${UI_PORT}:9292/tcp
networks:
- front_net
post:
build: ./post-py
image: ${USERNAME}/post:${POST_VERSION}
networks:
- back_net
- front_net
comment:
build: ./comment
image: ${USERNAME}/comment:${COMMENT_VERSION}
networks:
- back_net
- front_net
volumes:
post_db:
networks:
back_net:
driver: bridge
ipam:
driver: default
config:
- subnet: 10.0.2.0/24
front_net:
driver: bridge
ipam:
driver: default
config:
- subnet: 10.0.1.0/24
https://docs.docker.com/compose/environment-variables/#the-env-file .env
weisdd_microservices/src$ cat env
USERNAME=weisdd
UI_PORT=9292
UI_VERSION=1.0
POST_VERSION=1.0
COMMENT_VERSION=1.0
Задание: Узнайте как образуется базовое имя проекта.
Решение: Структура имени: project_service_index_slug, где slug - случайно сгенерированное шестнадцатиричное число.
Задание: Можно ли его задать? Если можно то как?
Решение: Имя можно переопределить при помощи специальной переменной окружения COMPOSE_PROJECT_NAME (её можно также внести в файл .env). Пример:
export COMPOSE_PROJECT_NAME=test
weisdd_microservices/src$ docker-compose up -d
Creating network "test_back_net" with driver "bridge"
Creating network "test_front_net" with driver "bridge"
Creating volume "test_post_db" with default driver
Creating test_post_1 ... done
Creating test_post_db_1 ... done
Creating test_comment_1 ... done
Creating test_ui_1 ... done
Задание: Создайте docker-compose.override.yml для reddit проекта, который позволит:
- Изменять код каждого из приложений, не выполняя сборку образа;
- Запускать puma для руби приложений в дебаг режиме с двумя воркерами (флаги --debug и -w 2).
Решение: https://docs.docker.com/compose/extends/
weisdd_microservices/src/docker-compose.override.yml
version: '3.3'
services:
ui:
command: puma --debug -w 2
volumes:
- app_ui:/app
post:
volumes:
- app_post:/app
comment:
command: puma --debug -w 2
volumes:
- app_comment:/app
volumes:
app_ui:
app_comment:
app_post:
- инструкция command переопределяет CMD, указанное в Dockerfile;
- для каждого каталога с приложением создан отдельный volume.
Проверка:
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
b3479d2afabe mongo:3.2 "docker-entrypoint.s…" 47 seconds ago Up 44 seconds 27017/tcp src_post_db_1
c63dba23b2fb weisdd/post:1.0 "python3 post_app.py" 47 seconds ago Up 43 seconds src_post_1
fe962f5e8ff7 weisdd/ui:1.0 "puma --debug -w 2" 47 seconds ago Up 44 seconds 0.0.0.0:9292->9292/tcp src_ui_1
cd046c798596 weisdd/comment:1.0 "puma --debug -w 2" 47 seconds ago Up 43 seconds src_comment_1
ib
$ docker volume ls | egrep 'DRIVER|src'
DRIVER VOLUME NAME
local src_app_comment
local src_app_post
local src_app_ui
local src_post_db
В данной работе мы:
- подготовили инсталляцию Gitlab CI;
- подготовили репозиторий с кодом приложения;
- описали этапы пайплайна;
- определили окружения.
gitlab-ci/docker-compose.yml
web:
image: 'gitlab/gitlab-ce:latest'
restart: always
hostname: 'gitlab.example.com'
environment:
GITLAB_OMNIBUS_CONFIG: |
external_url 'http://35.195.71.152/'
ports:
- '80:80'
- '443:443'
- '2222:22'
volumes:
- '/srv/gitlab/config:/etc/gitlab'
- '/srv/gitlab/logs:/var/log/gitlab'
- '/srv/gitlab/data:/var/opt/gitlab'
# docker-compose up -d
Подключение удалённого репозитория:
$ git checkout -b gitlab-ci-1
$ git remote add gitlab http://35.195.71.152/homework/example.git
$ git push gitlab gitlab-ci-1
Запуск:
# docker run -d --name gitlab-runner --restart always -v /srv/gitlab-runner/config:/etc/gitlab-runner -v /var/run/docker.sock:/var/run/docker.sock gitlab/gitlab-runner:latest
Регистрация:
root@gitlab-ci:/srv/gitlab# docker exec -it gitlab-runner gitlab-runner register --run-untagged --locked=false
Runtime platform arch=amd64 os=linux pid=11 revision=ac2a293c version=11.11.2
Running in system-mode.
Please enter the gitlab-ci coordinator URL (e.g. https://gitlab.com/):
http://35.195.71.152/
Please enter the gitlab-ci token for this runner:
FBVa-1qHjxLsKHDDbjd2
Please enter the gitlab-ci description for this runner:
[4bbf73c19c89]: my-runner
Please enter the gitlab-ci tags for this runner (comma separated):
linux,xenial,ubuntu,docker
Registering runner... succeeded runner=FBVa-1qH
Please enter the executor: ssh, docker-ssh+machine, kubernetes, docker, docker-ssh, parallels, shell, virtualbox, docker+machine:
docker
Please enter the default Docker image (e.g. ruby:2.1):
alpine:latest
Runner registered successfully. Feel free to start it, but if it's running already the config should be automatically reloaded!
Как вариант, можно регистрировать Runner в неинтерактивном режиме:
sudo docker exec gitlab-runner gitlab-runner register --run-untagged --locked=false --non-interactive --executor "docker" --docker-image alpine:latest --url "http://35.195.71.152/" --registration-token "FBVa-1qHjxLsKHDDbjd2" --description "docker-runner" --tag-list "docker,linux" --run-untagged="true"
image: ruby:2.4.2
stages:
- build
- test
- review
- stage
- production
variables:
DATABASE_URL: 'mongodb://mongo/user_posts'
before_script:
- cd reddit
- bundle install
build_job:
stage: build
script:
- echo 'Building'
test_unit_job:
stage: test
services:
- mongo:latest
script:
- ruby simpletest.rb
test_integration_job:
stage: test
script:
- echo 'Testing 2'
deploy_dev_job:
stage: review
script:
- echo 'Deploy'
environment:
name: dev
url: http://dev.example.com
branch review:
stage: review
script: echo "Deploy to $CI_ENVIRONMENT_SLUG"
environment:
name: branch/$CI_COMMIT_REF_NAME
url: http://$CI_ENVIRONMENT_SLUG.example.com
only:
- branches
except:
- master
staging:
stage: stage
when: manual
only:
- /^\d+\.\d+\.\d+/
script:
- echo 'Deploy'
environment:
name: stage
url: https://beta.example.com
production:
stage: production
when: manual
only:
- /^\d+\.\d+\.\d+/
script:
- echo 'Deploy'
environment:
name: production
url: https://example.com
Здесь у нас есть несколько этапов:
- build;
- test;
- review;
- stage;
- production. При этом, два последних будут предложены только в том случае, если к коммиту добавлен тэг с версией (ограничение указывается в секции only):
git commit -a -m ‘#4 add logout button to profile page’
git tag 2.4.10
git push gitlab gitlab-ci-1 --tags
Само выполнение этих этапов - ручное (when: manual). В секции branch review у нас настроено динамическое создание окружений для всех веток кроме master:
environment:
name: branch/$CI_COMMIT_REF_NAME
url: http://$CI_ENVIRONMENT_SLUG.example.com
only:
- branches
except:
- master
ВАЖНО: В слайдах было неправильно написано - environment появляется не в CI/CD, а в Operations -> Environments
В данной работе мы:
- познакомились с Prometheus;
- настроили мониторинг состояния микросервисов;
- подключили node exporter;
- подключили mongodb и blackbox exporters (*).
monitoring/prometheus/Dockerfile
FROM prom/prometheus:v2.1.0
ADD prometheus.yml /etc/prometheus/
monitoring/prometheus/prometheus.yaml
---
global:
scrape_interval: '5s'
scrape_configs:
- job_name: 'prometheus'
static_configs:
- targets:
- 'localhost:9090'
- job_name: 'ui'
static_configs:
- targets:
- 'ui:9292'
- job_name: 'comment'
static_configs:
- targets:
- 'comment:9292'
$ export USER_NAME=weisdd
$ docker build -t $USER_NAME/prometheus .
Обновленный docker/docker-compose.yml:
version: '3.3'
services:
post_db:
image: mongo:3.2
volumes:
- post_db:/data/db
networks:
back_net:
aliases:
- comment_db
ui:
# build: ./ui
image: ${USERNAME}/ui:${UI_VERSION}
ports:
- ${UI_PORT}:9292/tcp
networks:
- front_net
post:
# build: ./post-py
image: ${USERNAME}/post:${POST_VERSION}
networks:
- back_net
- front_net
comment:
# build: ./comment
image: ${USERNAME}/comment:${COMMENT_VERSION}
networks:
- back_net
- front_net
prometheus:
image: ${USERNAME}/prometheus
ports:
- '9090:9090'
volumes:
- prometheus_data:/prometheus
networks:
- back_net
- front_net
command:
- '--config.file=/etc/prometheus/prometheus.yml'
- '--storage.tsdb.path=/prometheus'
- '--storage.tsdb.retention=1d'
volumes:
post_db:
prometheus_data:
networks:
back_net:
driver: bridge
ipam:
driver: default
config:
- subnet: 10.0.2.0/24
front_net:
driver: bridge
ipam:
driver: default
config:
- subnet: 10.0.1.0/24
Для сбора метрик c docker-host необходимо использовать node-exporter. Настраивается следующим образом:
docker/docker-compose.yml:
services
node-exporter:
image: prom/node-exporter:v0.15.2
user: root
volumes:
- /proc:/host/proc:ro
- /sys:/host/sys:ro
- /:/rootfs:ro
networks:
- back_net
command:
- '--path.procfs=/host/proc'
- '--path.sysfs=/host/sys'
- '--collector.filesystem.ignored-mount-points="^/(sys|proc|dev|host|etc)($$|/)"'
monitoring/prometheus/prometheus.yaml
- job_name: 'node'
static_configs:
- targets:
- 'node-exporter:9100'
После этого необходимо пересобрать образ и перезапустить контейнеры через docker-compose.
Важно: На слайде 45 была некорректная конфигурация для node-exporter - он не был подключен ни к какой сети, соответственно, не резолвился через docker dns. Фикс: networks: - back_net Также, команда, которая была указана для пересборки образа prometheus не учитывала, что поскольку менялось только содержимое yml-файла, соответствующий слой всегда будет браться из кэша. Поэтому необходимо при сборке добавлять --no-cache
monitoring/prometheus$ docker build --no-cache -t $USER_NAME/prometheus .
https://hub.docker.com/u/weisdd
Задание: Добавьте в Prometheus мониторинг MongoDB с использованием необходимого экспортера.
- Версию образа экспортера нужно фиксировать на последнюю стабильную
- Если будете добавлять для него Dockerfile, он должен быть в директории monitoring, а не в корне репозитория. P.S. Проект dcu/mongodb_exporter не самый лучший вариант, т.к. у него есть проблемы с поддержкой (не обновляется)
Решение: Раз уж dcu/mongodb_exporter было рекомендовано не использовать, эксперимента ради выбрал форк: https://github.com/percona/mongodb_exporter/ Для снижения размера образа использовал multi-stage сборку:
FROM golang:1.12.5 AS builder
ENV APPPATH $GOPATH/src/github.com/percona/mongodb_exporter
WORKDIR $APPPATH
RUN git clone -b v0.7.0 "https://github.com/percona/mongodb_exporter" "$APPPATH" \
&& go get -d && CGO_ENABLED=0 GOOS=linux go build -o /bin/mongodb_exporter \
&& rm -rf "$GOPATH"
FROM alpine:3.9.4
ENV MONGODB_URI mongodb://post_db:27017
#EXPOSE 9216
COPY --from=builder /bin/mongodb_exporter /bin/mongodb_exporter
ENTRYPOINT [ "/bin/mongodb_exporter" ]
ВАЖНО: Для Alpine необходимо использовать статическую компиляцию Go, иначе mongodb_exporter будет вылетать с ошибкой.
CGO_ENABLED=0 GOOS=linux go build -o /bin/mongodb_exporter
Немного подробнее: gin-gonic/gin#1178
Собираем образ:
monitoring/mongodb_exporter$ docker build -t weisdd/mongodb_exporter . --no-cache
В наш docker-compose добавляем следующее содержимое docker/docker-compose.yml:
services
mongodb_exporter:
image: ${USERNAME}/mongodb_exporter:${MONGODB_EXPORTER_VERSION}
environment:
- MONGODB_URI=${MONGODB_URI}
ports:
- '9216:9216'
networks:
- back_net
docker/env
MONGODB_EXPORTER_VERSION=latest
MONGODB_URI=mongodb://post_db:27017
ВАЖНО: Как выяснилось, этот exporter не умеет парсить кавычки/апострофы. Соответственно, если указать MONGODB_URI='mongodb://post_db:27017' / MONGODB_URI="mongodb://post_db:27017" / MONGODB_URI="${MONGODB_URI}", то подключиться он не сможет.
Дополняем конфигурацию prometheus и пересобираем образ: monitoring/prometheus/prometheus.yaml
- job_name: 'mongodb'
static_configs:
- targets:
- 'mongodb_exporter:9216'
monitoring/prometheus$ docker build --no-cache -t $USER_NAME/prometheus .
Задание: Добавьте в Prometheus мониторинг сервисов comment, post, ui с помощью blackbox экспортера. Blackbox exporter позволяет реализовать для Prometheus мониторинг по принципу черного ящика. Т.е. например мы можем проверить отвечает ли сервис по http, или принимает ли соединения порт.
- Версию образа экспортера нужно фиксировать на последнюю стабильную
- Если будете добавлять для него Dockerfile, он должен быть в директории monitoring, а не в корне репозитория. Вместо blackbox_exporter можете попробовать использовать Cloudprober от Google.
Решение: Для blackbox использовался образ из docker hub: docker/docker-compose.yml:
services
blackbox-exporter:
image: prom/blackbox-exporter:${BLACKBOX_EXPORTER_VERSION}
networks:
- front_net
- back_net
docker/env
BLACKBOX_EXPORTER_VERSION=v0.14.0
Дополняем конфигурацию prometheus и пересобираем образ: monitoring/prometheus/prometheus.yaml
- job_name: 'blackbox'
metrics_path: /probe
params:
module: [http_2xx]
static_configs:
- targets:
- http://comment:9292/metrics
- http://post:9292/metrics
- http://ui:9292/metrics
relabel_configs:
- source_labels: [__address__]
target_label: __param_target
- source_labels: [__param_target]
target_label: instance
- target_label: __address__
replacement: blackbox-exporter:9115
monitoring/prometheus$ docker build --no-cache -t $USER_NAME/prometheus .
Результаты у нас следующие:
probe_http_status_code{instance="http://comment:9292/metrics",job="blackbox"} 200
probe_http_status_code{instance="http://post:9292/metrics",job="blackbox"} 0
probe_http_status_code{instance="http://ui:9292/metrics",job="blackbox"} 200
т.к. у post нет /metrics, да и вообще не прослушивается порт 9292, статус 0. Если для comment не указывать /metrics, то получим 404 (Not Found).
В данной работе мы:
- реализовали мониторинг docker-контейнеров;
- визуализировали метрики;
- настроили уведомления в slack;
- сравнили набор метрик для Prometheus в cAdvisor, Docker, Telegraf (*).
- выполнили автоматический provisioning конфигурации (datasources, dashboards) в Grafana (**).
Нам потребовалось вынести конфигурацию docker-compose из docker-compose.yml в docker-compose-monitoring.yml. ВАЖНО: поскольку определение volumes и networks учитывается лишь в пределах файла, потребовалось добавить соответствующую часть конфигурации:
networks:
back_net:
front_net:
Здесь, в общем-то, ничего примечательного - запустили, привязали к Prometheus, создали dashboard в Grafana.
Задание: Используйте для первого графика (UI http requests) функцию rate аналогично второму графику (Rate of UI HTTP Requests with Error)
Решение: rate(ui_request_count[1m])
monitoring/alertmanager/Dockerfile
FROM prom/alertmanager:v0.14.0
ADD config.yml /etc/alertmanager/
monitoring/alertmanager/config.yml
global:
slack_api_url: 'https://hooks.slack.com/services/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'
route:
receiver: 'slack-notifications'
receivers:
- name: 'slack-notifications'
slack_configs:
- channel: '#igor_beliakov'
title: "{{ range .Alerts }}{{ .Annotations.summary }}\n{{ end }}"
text: "{{ range .Alerts }}{{ .Annotations.description }}\n{{ end }}"
ВАЖНО: в презентации отсутствовала конфигурация для title и text, в следствие чего в Slack приходило сообщение без особо ценной информации.
monitoring/prometheus/Dockerfile
FROM prom/prometheus:v2.1.0
ADD prometheus.yml /etc/prometheus/
ADD alerts.yml /etc/prometheus/
monitoring/prometheus/alerts.yml
groups:
- name: alert.rules
rules:
- alert: InstanceDown
expr: up == 0
for: 1m
labels:
severity: page
annotations:
description: "{{ $labels.instance }} of job {{ $labels.job }} has been down for more than 1 minute"
summary: "Instance {{ $labels.instance }} down"
https://docs.docker.com/config/thirdparty/prometheus/ Задание:
- В Docker в экспериментальном режиме реализована отдача метрик в формате Prometheus. Добавьте сбор этих метрик в Prometheus. Сравните количество метрик с Cadvisor. Выберите готовый дашборд или создайте свой для этого источника данных. Выгрузите его в monitoring/grafana/dashboards;
Решение: /etc/docker/daemon.json
{
"metrics-addr" : "10.0.2.1:9323",
"experimental" : true
}
monitoring/prometheus/prometheus.yml
scrape_configs:
- job_name: 'docker'
static_configs:
- targets:
- '10.0.2.1:9323'
ВАЖНО: в официальной документации предлагалось указать metrics-addr равным 127.0.0.1:9323, но учитывая, что контейнеры у нас запускаются не в сети host, потребовалось изменить адрес на 10.0.2.1 (соответствует bridge-интерфейсу)
Dashboard: monitoring/grafana/dashboards/Docker_Engine_Metrics.json
В cAdvisor было 1482 метрики, в docker export - 499. В последнем отсутствует, как минимум, информация по каждому контейнеру в отдельности.
Задание: Для сбора метрик с Docker демона также можно использовать Telegraf от InfluxDB. Добавьте сбор этих метрик в Prometheus. Сравните количество метрик с Cadvisor. Выберите готовый дашборд или создайте свой для этого источника данных. Выгрузите его в monitoring/grafana/dashboards;
Решение: monitoring/telegraf/telegraf.conf
[[inputs.docker]]
endpoint = "unix:///var/run/docker.sock"
[[outputs.prometheus_client]]
listen = ":9273"
monitoring/telegraf/Dockerfile
FROM telegraf:1.9.5-alpine
COPY telegraf.conf /etc/telegraf/telegraf.conf
docker/docker-compose-monitoring.yml
services:
telegraf:
image: ${USER_NAME}/telegraf
volumes:
- /var/run/docker.sock:/var/run/docker.sock
networks:
- back_net
monitoring/prometheus/prometheus.yml
---
scrape_configs:
- job_name: 'telegraf'
static_configs:
- targets:
- 'telegraf:9273'
Dashboard: monitoring/grafana/dashboards/Telegraf.json
987 метрик (vs. 1482 в cAdvisor)
Задание:
- Придумайте и реализуйте другие алерты, например на 95 процентиль времени ответа UI, который рассмотрен выше; Настройте интеграцию Alertmanager с e-mail помимо слака;
Решение: monitoring/prometheus/alerts.yml
groups:
- name: alert.rules
rules:
- alert: LackOfSpace
expr: node_filesystem_free{mountpoint="/"} / node_filesystem_size * 100 < 20
labels:
severity: moderate
annotations:
summary: "Instance {{ $labels.instance }} is low on disk space"
description: "On {{ $labels.instance }}, / has only {{ $value | humanize }}% of disk space left"
Пример уведомления: AlertManagerAPP [6:31 PM] Instance node-exporter:9100 is low on disk space On node-exporter:9100, / has only 11.27% of disk space left
Интеграцию с e-mail не настраивал.
Выполнено частично.
Задание: В Grafana 5.0 была добавлена возможность описать в конфигурационных файлах источники данных и дашборды. Реализуйте автоматическое добавление источника данных и созданных в данном ДЗ дашбордов в графану;
Решение: Потребовалось создать отдельный Dockerfile:
monitoring/grafana/Dockerfile
FROM grafana/grafana:5.0.0
COPY dashboards-providers/providers.yml /etc/grafana/provisioning/dashboards/
COPY datasources/datasources.yml /etc/grafana/provisioning/datasources/
COPY dashboards/* /var/lib/grafana/dashboards/
monitoring/grafana/dashboards-providers/providers.yml
---
apiVersion: 1
providers:
# <string> provider name
- name: 'default'
# <string, required> provider type. Required
type: file
# <bool> disable dashboard deletion
disableDeletion: false
# <bool> enable dashboard editing
editable: true
# <int> how often Grafana will scan for changed dashboards
updateIntervalSeconds: 10
options:
# <string, required> path to dashboard files on disk. Required
path: /var/lib/grafana/dashboards
monitoring/grafana/datasources/datasources.yml
---
# config file version
apiVersion: 1
datasources:
# <string, required> name of the datasource. Required
- name: Prometheus Server
# <string, required> datasource type. Required
type: prometheus
# <string, required> access mode. proxy or direct (Server or Browser in the UI). Required
access: proxy
# <string> url
url: http://prometheus:9090/
# <string> Deprecated, use secureJsonData.password
isDefault: true
version: 2
# <bool> allow users to edit datasources from the UI.
editable: true
ВАЖНО: с экспортированными через web-интерфейс grafana dashboards была обнаружена интересная особенность:
grafana_1 | t=2019-06-13T12:28:11+0000 lvl=eror msg="failed to save dashboard" logger=provisioning.dashboard type=file name=default error="Invalid alert data. Cannot save dashboard"
grafana_1 | t=2019-06-13T12:28:11+0000 lvl=info msg="Initializing Alerting" logger=alerting.engine
grafana_1 | t=2019-06-13T12:28:11+0000 lvl=info msg="Initializing CleanUpService" logger=cleanup
grafana_1 | t=2019-06-13T12:28:14+0000 lvl=eror msg="failed to save dashboard" logger=provisioning.dashboard type=file name=default error="Invalid alert data. Cannot save dashboard"
Как выяснилось, в json-файлах фигурировали переменные ${DS_PROMETHEUS} и ${DS_PROMETHEUS_SERVER} в параметре datasource. Потребовалось изменить их значения на "Prometheus Server" (соответствует содержимому monitoring/grafana/datasources/datasources.yml).
В данной работе мы:
- познакомились с особенностями сбора структурированных и неструктурированных логов (EFK);
- рассмотрели распределенную трасировку (zipkin).
В презентации была отсылка к ветке logging, которая больше не используется, а сам код находится в неработоспопсобном состоянии. В качестве основной используется ветка microservices, с которой мы уже работали ранее. Чтобы контейнер с ElasticSearch не падал, необходимо подкрутить sysctl на docker host:
$ docker-compose -f docker-compose-logging.yml logs elasticsearch
[...]
elasticsearch_1 | [1]: max virtual memory areas vm.max_map_count [65530] is too low, increase to at least [262144]
[...]
$ sudo sysctl -w vm.max_map_count=262144
Используемые инструменты:
- ElasticSearch (TSDB + поисковый движок для хранения данных);
- fluentd (агрегация и трансформация данных);
- Kibana (визуализация).
kibana: 35.225.135.235:5601
docker/docker-compose-logging.yml
version: '3.3'
services:
fluentd:
image: ${USER_NAME}/fluentd
ports:
- "24224:24224"
- "24224:24224/udp"
elasticsearch:
image: elasticsearch:6.8.0
expose:
- 9200
ports:
- "9200:9200"
kibana:
image: kibana:6.8.0
ports:
- "5601:5601"
logging/fluentd/Dockerfile:
FROM fluent/fluentd:v0.12
RUN gem install fluent-plugin-elasticsearch --no-rdoc --no-ri --version 1.9.5
RUN gem install fluent-plugin-grok-parser --no-rdoc --no-ri --version 1.0.0
ADD fluent.conf /fluentd/etc
Базовая конфигурация fluentd: logging/fluentd/fluent.conf
<source>
@type forward #получение логов
port 24224
bind 0.0.0.0
</source>
<match *.**>
@type copy # отправка логов в ElasticSearch
<store>
@type elasticsearch
host elasticsearch
port 9200
logstash_format true
logstash_prefix fluentd
logstash_dateformat %Y%m%d
include_tag_key true
type_name access_log
tag_key @log_name
flush_interval 1s
</store>
<store> # вывод логов в stdout
@type stdout
</store>
</match>
По умолчанию, docker использует драйвер json для хранения логов, нам же необходимо использовать fluentd. Поэтому, для сервисов ui и post мы переопределяем секцию logging: docker/docker-compose-logging.yml
services:
post:
logging:
driver: "fluentd"
options:
fluentd-address: localhost:24224
tag: service.post
ui:
logging:
driver: "fluentd"
options:
fluentd-address: localhost:24224
tag: service.ui
Парсинг json-логов (=структурированных) от сервиса post: logging/fluentd/fluent.conf
<filter service.post>
@type parser
format json
key_name log
</filter>
Сервис ui отправляет неструктурированные логи в нескольких форматах. Для парсинга мы можем воспользоваться либо регулярными выражениями, либо готовым grok-шаблоном (именованный шаблон регулярных выражений). Последнее - гораздо удобнее. logging/fluentd/fluent.conf
<filter service.ui>
@type parser
format /\[(?<time>[^\]]*)\] (?<level>\S+) (?<user>\S+)[\W]*service=(?<service>\S+)[\W]*event=(?<event>\S+)[\W]*(?:path=(?<path>\S+)[\W]*)?request_id=(?<request_id>\S+)[\W]*(?:remote_addr=(?<remote_addr>\S+)[\W]*)?(?:method= (?<method>\S+)[\W]*)?(?:response_status=(?<response_status>\S+)[\W]*)?(?:message='(?<message>[^\']*)[\W]*)?/
key_name log
</filter>
logging/fluentd/fluent.conf
<filter service.ui>
@type parser
key_name log
format grok
grok_pattern %{RUBY_LOGGER}
</filter>
В данной конфигурации у нас распарсятся на ключ-значение все поля, вот только message так и останется большим блоком данных. Поэтому следует применить два grok'а по очереди: logging/fluentd/fluent.conf
<filter service.ui>
@type parser
key_name log
format grok
grok_pattern %{RUBY_LOGGER}
</filter>
<filter service.ui>
@type parser
format grok
grok_pattern service=%{WORD:service} \| event=%{WORD:event} \| request_id=%{GREEDYDATA:request_id} \| message='%{GREEDYDATA:message}'
key_name message
reserve_data true
</filter>
Задание: UI-сервис шлет логи в нескольких форматах. (пример дан на слайде) Такой лог остался неразобранным. Составьте конфигурацию fluentd так, чтобы разбирались оба формата логов UI-сервиса (тот, что сделали до этого и текущий) одновременно.
Решение: Получается, что различие форматах появляется только для поля message. Соответственно, нужно, чтобы сработал лишь тот шаблон, что совпадёт первым. В процессе решения опирался на:https://github.com/fluent/fluent-plugin-grok-parser ("If you want to try multiple grok patterns and use the first matched one, you can use the following syntax:"[...])
<filter service.ui>
@type parser
format grok
<grok>
pattern service=%{WORD:service} \| event=%{WORD:event} \| request_id=%{GREEDYDATA:request_id} \| message='%{GREEDYDATA:message}'
</grok>
<grok>
pattern service=%{WORD:service} \| event=%{WORD:event} \| path=%{URIPATH:path} \| request_id=%{GREEDYDATA:request_id} \| remote_addr=%{IP:remote_addr} \| method= %{WORD:method} \| response_status=%{NUMBER:response_status}
</grok>
key_name message
reserve_data true
</filter>
- Здесь первый шаблон - то, что было описано на слайдах, второй - под задание со *.
docker/docker-compose-logging.yml
services:
zipkin:
image: openzipkin/zipkin
ports:
- "9411:9411"
networks:
- front_net
- back_net
networks:
back_net:
front_net:
Для активации трейсов необходимо проинструктировать приложение через специальную переменную окружения: docker/docker-compose.yml
ui:
environment:
- ZIPKIN_ENABLED=${ZIPKIN_ENABLED}
post:
environment:
- ZIPKIN_ENABLED=${ZIPKIN_ENABLED}
comment:
environment:
- ZIPKIN_ENABLED=${ZIPKIN_ENABLED}
docker/.env
ZIPKIN_ENABLED=true
Задание: С нашим приложением происходит что-то странное. Пользователи жалуются, что при нажатии на пост они вынуждены долго ждать, пока у них загрузится страница с постом. Жалоб на загрузку других страниц не поступало. Нужно выяснить, в чем проблема, используя Zipkin. Репозиторий со сломанным кодом приложения: https://github.com/Artemmkin/bugged-code
Решение: Исходники приложения размещены в src/bugged-code. "Из коробки" оно не собиралось (у образа ruby:2.2 проблемы с запросом отдельных списков в apt) + отсутствовали необходимые переменные окружения в Dockerfile (видимо, предполагалось, что будут задаваться через секцию environment в docker-compose.yml). Исправленные Dockerfile выглядят следующим образом: src/bugged-code/ui/Dockerfile
FROM ruby:2.3
RUN apt-get update -qq && apt-get install -y build-essential
ENV APP_HOME /app
RUN mkdir $APP_HOME
WORKDIR $APP_HOME
ADD Gemfile* $APP_HOME/
RUN bundle install
ADD . $APP_HOME
ENV POST_SERVICE_HOST post
ENV POST_SERVICE_PORT 5000
ENV COMMENT_SERVICE_HOST comment
ENV COMMENT_SERVICE_PORT 9292
CMD ["puma"]
src/bugged-code/comment/Dockerfile
FROM ruby:2.3
RUN apt-get update -qq && apt-get install -y build-essential
ENV APP_HOME /app
RUN mkdir $APP_HOME
WORKDIR $APP_HOME
ADD Gemfile* $APP_HOME/
RUN bundle install
ADD . $APP_HOME
ENV COMMENT_DATABASE_HOST comment_db
ENV COMMENT_DATABASE comments
CMD ["puma"]
src/bugged-code/comment/Dockerfile
# FROM python:3.6.0-alpine
FROM python:2.7
WORKDIR /app
ADD requirements.txt /app
RUN pip install -r requirements.txt
ADD . /app
EXPOSE 5000
ENV POST_DATABASE_HOST post_db
ENV POST_DATABASE posts
ENTRYPOINT ["python", "post_app.py"]
Чтобы не смешивать образы из веток microservices и bugged-code, подправил docker-build.sh файлы для каждого сервиса - добавил тэг bug. E.g.: src/bugged-code/ui/docker_build.sh
#!/bin/bash
echo `git show --format="%h" HEAD | head -1` > build_info.txt
echo `git rev-parse --abbrev-ref HEAD` >> build_info.txt
docker build -t $USER_NAME/ui:bug .
docker/.env
UI_VERSION=bug
POST_VERSION=bug
COMMENT_VERSION=bug
Теперь можно приступить к трейсингу. Пример трейса при открытии любого поста:
zipkin: 35.225.135.235:9411
post./post/<id>: 3.052s
×
Services: post,ui_app
Date Time Relative Time Annotation Address
6/17/2019, 4:57:43 PM 2.463ms Client Start 10.0.1.5:9292 (ui_app)
6/17/2019, 4:57:43 PM 5.044ms Server Start 10.0.2.5:5000 (post)
6/17/2019, 4:57:46 PM 3.039s Server Finish 10.0.2.5:5000 (post)
6/17/2019, 4:57:46 PM 3.054s Client Finish 10.0.1.5:9292 (ui_app)
Key Value
http.path /post/5d07877fa9cc96000e30efba
http.status 200
Server Address 10.0.1.4:5000 (post)
- Здесь мы видим, что span post выполняется за 3 секунды. Он соответствует функции find_post(id) в src/bugged-code/post-py/post_app.py:
# Retrieve information about a post
@zipkin_span(service_name='post', span_name='db_find_single_post')
def find_post(id):
start_time = time.time()
try:
post = app.db.find_one({'_id': ObjectId(id)})
except Exception as e:
log_event('error', 'post_find',
"Failed to find the post. Reason: {}".format(str(e)),
request.values)
abort(500)
else:
stop_time = time.time() # + 0.3
resp_time = stop_time - start_time
app.post_read_db_seconds.observe(resp_time)
time.sleep(3)
log_event('info', 'post_find',
'Successfully found the post information',
{'post_id': id})
return dumps(post)
Блок else выполняется, если в функции не возникло никаких исключений. За задержку в 3 секунды ответственна строка:
time.sleep(3)
В данной работе мы:
- развернули kubernetes, опираясь на Kubernetes The Hard Way;
- ознакомились с описанием основных примитивов нашего приложения и его дальнейшим запуском в Kubernetes.
В целом, нет смысла дублировать описание процесса установки, поскольку я не отступал ни на шаг от https://github.com/kelseyhightower/kubernetes-the-hard-way Все файлы (в т.ч. сертификаты уже удалённого кластера) расположены в kubernetes/the_hard_way
Все yaml файлы размещены в kubernetes/reddit.
weisdd_microservices/kubernetes/reddit$ kubectl apply -f comment-deployment.yml
deployment.apps/comment-deployment created
weisdd_microservices/kubernetes/reddit$ kubectl apply -f mongo-deployment.yml
deployment.apps/mongo-deployment created
weisdd_microservices/kubernetes/reddit$ kubectl apply -f post-deployment.yml
deployment.apps/post-deployment created
weisdd_microservices/kubernetes/reddit$ kubectl apply -f ui-deployment.yml
deployment.apps/ui-deployment created
weisdd_microservices/kubernetes/reddit$ kubectl get pods
NAME READY STATUS RESTARTS AGE
busybox-bd8fb7cbd-lkp8n 1/1 Running 0 10m
comment-deployment-56bf895499-f9zl9 1/1 Running 0 2m21s
mongo-deployment-67f58fb89-4nwd8 1/1 Running 0 2m13s
nginx-dbddb74b8-clcqd 1/1 Running 0 9m14s
post-deployment-675449bbf7-h6r5t 1/1 Running 0 2m6s
ui-deployment-846bd8c8b5-lqqqh 1/1 Running 0 2m1s
untrusted 1/1 Running 0 5m37s
Пробрасываем порт и пробуем подключиться к ui:
$ kubectl port-forward ui-deployment-846bd8c8b5-lqqqh 8080:9292
$ curl -I http://127.0.0.1:8080
HTTP/1.1 200 OK
Content-Type: text/html;charset=utf-8
X-XSS-Protection: 1; mode=block
X-Content-Type-Options: nosniff
X-Frame-Options: SAMEORIGIN
Set-Cookie: rack.session=BAh7CEkiD3Nlc3Npb25faWQGOgZFVEkiRWI4MGViOWRjNTRiMzc4YmU0NmU2%0AMTU4ZGZmZWRkN2YxNTRmNmY0MWI5MDRiYmM2NjA2OWIxMTMzNDk1ZGUxOWEG%0AOwBGSSIJY3NyZgY7AEZJIjF4Mkk0bHZoZDlJNElxazNoRWZ5Um5qVzZGZGJl%0AUUI4YnZBNnNIWE1oOHZvPQY7AEZJIg10cmFja2luZwY7AEZ7B0kiFEhUVFBf%0AVVNFUl9BR0VOVAY7AFRJIi02ODg1ZWIzZjc0M2UzZjI4YzJlNWQxYjlkMTUx%0AMWNlYzY1MDBmN2M3BjsARkkiGUhUVFBfQUNDRVBUX0xBTkdVQUdFBjsAVEki%0ALWRhMzlhM2VlNWU2YjRiMGQzMjU1YmZlZjk1NjAxODkwYWZkODA3MDkGOwBG%0A--59d0a97564f4049ff21508e11f1501a76ea468de; path=/; HttpOnly
Content-Length: 1852
В данной работе мы:
- развернули локальное окружение для работы с Kubernetes;
- развернули Kubernetes в GKE;
- запустили reddit в Kubernetes;
- подготовили конфигурацию terraform для поднятия Kubernetes-кластера в GKE.
Предварительно необходимо установить kubectl и Virtualbox.
$ minikube start
Манифесты с приложением: kubernetes/reddit/ui-deployment.yml
---
apiVersion: apps/v1beta2
kind: Deployment
metadata:
name: ui
labels:
app: reddit
component: ui
spec:
replicas: 3
selector:
matchLabels:
app: reddit
component: ui
template:
metadata:
name: ui-pod
labels:
app: reddit
component: ui
spec:
containers:
- image: weisdd/ui
name: ui
env:
- name: ENV
valueFrom:
fieldRef:
fieldPath: metadata.namespace
kubernetes/reddit/post-deployment.yml
---
apiVersion: apps/v1beta2
kind: Deployment
metadata:
name: post
labels:
app: reddit
component: post
spec:
replicas: 3
selector:
matchLabels:
app: reddit
component: post
template:
metadata:
name: post
labels:
app: reddit
component: post
spec:
containers:
- image: weisdd/post
name: post
env:
- name: POST_DATABASE_HOST
value: post-db
kubernetes/reddit/comment-deployment.yml
---
apiVersion: apps/v1beta2
kind: Deployment
metadata:
name: comment
labels:
app: reddit
component: comment
spec:
replicas: 3
selector:
matchLabels:
app: reddit
component: comment
template:
metadata:
name: comment
labels:
app: reddit
component: comment
spec:
containers:
- image: weisdd/comment
name: comment
env:
- name: COMMENT_DATABASE_HOST
value: comment-db
kubernetes/reddit/mongo-deployment.yml
---
apiVersion: apps/v1beta2
kind: Deployment
metadata:
name: mongo
labels:
app: reddit
component: mongo
comment-db: "true"
post-db: "true"
spec:
replicas: 1
selector:
matchLabels:
app: reddit
component: mongo
template:
metadata:
name: mongo
labels:
app: reddit
component: mongo
comment-db: "true"
post-db: "true"
spec:
containers:
- image: mongo:3.2
name: mongo
volumeMounts:
- name: mongo-persistent-storage
mountPath: /data/db
volumes:
- name: mongo-persistent-storage
emptyDir: {}
Запускаем приложение:
$ kubectl apply -f ./kubernetes/reddit
В Kubernetes взаимодействие с другими контейнерами и с внешним миром происходит через т.н. services (определяет набор POD'ов и способ доступа к ним). До того как они определены, извне мы можем обратиться к конкретному контейнеру через port-forwarding. К примеру, для ui:
$ kubectl get pods --selector component=ui
$ kubectl port-forward <pod-name> 8080:9292
Соответственно, станет возможным зайти на веб-интерфейс по адресу http://localhost:8080
kubernetes/reddit/ui-service.yml
---
apiVersion: v1
kind: Service
metadata:
name: ui
labels:
app: reddit
component: ui
spec:
type: NodePort
ports:
- nodePort: 32092
port: 9292
protocol: TCP
targetPort: 9292
selector:
app: reddit
component: ui
kubernetes/reddit/post-service.yml
---
apiVersion: v1
kind: Service
metadata:
name: post
labels:
app: reddit
component: post
spec:
ports:
- port: 5000
protocol: TCP
targetPort: 5000
selector:
app: reddit
component: post
kubernetes/reddit/comment-service.yml
---
apiVersion: v1
kind: Service
metadata:
name: comment
labels:
app: reddit
component: comment
spec:
ports:
- port: 9292
protocol: TCP
targetPort: 9292
selector:
app: reddit
component: comment
После применения конфигурации в coreDNS появятся записи вида ..svc.cluster.local (e.g. comment.default.svc.cluster.local).
$ kubectl exec -ti <pod-name> nslookup comment
C MongoDB ситуация осложняется тем, что в Kubernetes отсутствуют сетевые алиасы, соответственно, под каждое имя, используемое приложением (comment_db, post_db - передавались в ENV в соответствующих Dockerfile; в k8s в name нельзя использовать символ подчёркивания), необходимо создать отдельный сервис:
kubernetes/reddit/comment-mongodb-service.yml
---
apiVersion: v1
kind: Service
metadata:
name: comment-db
labels:
app: reddit
component: mongo
comment-db: "true"
spec:
ports:
- port: 27017
protocol: TCP
targetPort: 27017
selector:
app: reddit
component: mongo
comment-db: "true"
kubernetes/reddit/post-mongodb-service.yml
---
apiVersion: v1
kind: Service
metadata:
name: post-db
labels:
app: reddit
component: mongo
post-db: "true"
spec:
ports:
- port: 27017
protocol: TCP
targetPort: 27017
selector:
app: reddit
component: mongo
post-db: "true"
Необходимые переменные окружения уже были описаны в deployment'ах. E.g.:
env:
- name: COMMENT_DATABASE_HOST
value: comment-db
Вернёмся к конфигурации ui-service: kubernetes/reddit/ui-service.yml
---
apiVersion: v1
kind: Service
metadata:
name: ui
labels:
app: reddit
component: ui
spec:
type: NodePort
ports:
- nodePort: 32092
port: 9292
protocol: TCP
targetPort: 9292
selector:
app: reddit
component: ui
По умолчанию, все сервисы имеют тип ClusterIP - располагаются во внутреннем диапазоне IP-адресов. Для публикации сервиса наружу стоит использовать т.н. NodePort. В нашем случае, сервис извне будет доступен на :32092.
weisdd_microservices/kubernetes/reddit$ minikube service list
|-------------|------------|-----------------------------|
| NAMESPACE | NAME | URL |
|-------------|------------|-----------------------------|
| default | comment | No node port |
| default | comment-db | No node port |
| default | kubernetes | No node port |
| default | post | No node port |
| default | post-db | No node port |
| default | ui | http://192.168.99.100:32092 |
| kube-system | kube-dns | No node port |
|-------------|------------|-----------------------------|
Активация dashboard в minikube:
weisdd_microservices/kubernetes/reddit$ minikube dashboard
🔌 Enabling dashboard ...
🤔 Verifying dashboard health ...
🚀 Launching proxy ...
🤔 Verifying proxy health ...
🎉 Opening http://127.0.0.1:41989/api/v1/namespaces/kube-system/services/http:kubernetes-dashboard:/proxy/ in your default browser...
ibeliako@dev:~/devops/git/weisdd_microservices/kubernetes/reddit$ kubectl get services -n kube-system
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kube-dns ClusterIP 10.96.0.10 <none> 53/UDP,53/TCP,9153/TCP 3h25m
kubernetes-dashboard ClusterIP 10.110.131.76 <none> 80/TCP 72s
ibeliako@dev:~/devops/git/weisdd_microservices/kubernetes/reddit$ minikube service list
|-------------|----------------------|-----------------------------|
| NAMESPACE | NAME | URL |
|-------------|----------------------|-----------------------------|
| default | comment | No node port |
| default | comment-db | No node port |
| default | kubernetes | No node port |
| default | post | No node port |
| default | post-db | No node port |
| default | ui | http://192.168.99.100:32092 |
| kube-system | kube-dns | No node port |
| kube-system | kubernetes-dashboard | No node port |
|-------------|----------------------|-----------------------------|
Мы можем запустить наше приложение в отдельном namespace. Для начала, создадим namespace dev: kubernetes/reddit/dev-namespace.yml
---
apiVersion: v1
kind: Namespace
metadata:
name: dev
$ kubectl apply -f dev-namespace.yml
$ kubectl apply -n dev -f .
Открываем веб-страницу:
$ minikube service ui -n dev
Note: вероятно из-за того, что minikube запускался в VirtualBox внутри VMware, постоянно происходили странные ошибки. К примеру, когда было по 3 реплики, docker для части контейнеров не мог подгрузить образы (ImagePullBackoff), даже в рамках одного компонента. После перезапуска начал крэшиться coreDNS:
$ kubectl get pods -n kube-system
NAME READY STATUS RESTARTS AGE
coredns-fb8b8dccf-55b6k 0/1 CrashLoopBackOff 1 17s
coredns-fb8b8dccf-94lbj 0/1 CrashLoopBackOff 16 61m
etcd-minikube 1/1 Running 0 60m
kube-addon-manager-minikube 1/1 Running 0 60m
kube-apiserver-minikube 1/1 Running 0 60m
kube-controller-manager-minikube 1/1 Running 1 60m
kube-proxy-smhbc 1/1 Running 0 61m
kube-scheduler-minikube 1/1 Running 1 60m
kubernetes-dashboard-d7c9687c7-5mlvh 0/1 CrashLoopBackOff 16 60m
storage-provisioner 1/1 Running 0 60m
Пришлось снизить количество реплик и несколько раз перезапустить minikube.
Большого смысла описывать действия в веб-интерфейсе нет, поэтому сразу перейду к конфигурации terraform. Стоит только отметить, что в firewall мы открыли порты tcp:30000-32767 для all instances.
Задание: Разверните Kubenetes-кластер в GKE с помощью Terraform модуля (https://www.terraform.io/docs/providers/google/r/container_cluster.html)
Решение: Здесь, собственно, ничего необычного: kubernetes/terraform/main.tf
terraform {
required_version = ">=0.11,<0.12"
}
provider "google" {
version = "2.0.0"
project = "${var.project}"
region = "${var.region}"
}
resource "google_container_cluster" "cluster" {
name = "${var.cluster_name}"
zone = "${var.zone}"
initial_node_count = "${var.initial_node_count}"
addons_config {
kubernetes_dashboard {
disabled = false
}
}
}
kubernetes/terraform/variables.tf
variable "project" {
description = "Project ID"
}
variable "region" {
description = "Region"
default = "europe-west-1"
}
variable "cluster_name" {
default = "default-cluster-1"
}
variable "zone" {
description = "Zone"
default = "europe-west1-b"
}
variable "initial_node_count" {
default = 2
}
variable "disk_size" {
default = 20
}
variable "machine_type" {
default = "small"
}
kubernetes/terraform/terraform.tfvars
project = "docker-1234"
region = "europe-west-1"
cluster_name = "default-cluster-1"
zone = "europe-west1-b"
initial_node_count = 3
disk_size = 20
machine_type = "small"
kubernetes/terraform/outputs.tf
output "gcloud connect command" {
value = "gcloud container clusters get-credentials ${var.cluster_name} --zone ${var.zone} --project ${var.project}"
}
output "enable access to dashboard" {
value = "kubectl create clusterrolebinding kubernetes-dashboard --clusterrole=cluster-admin --serviceaccount=kube-system:kubernetes-dashboard"
}
Задание: Создайте YAML-манифесты для описания созданных сущностей для включения dashboard.
Решение: kubernetes/terraform/extra/role-binding.yml
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRoleBinding
metadata:
name: kubernetes-dashboard
selfLink: /apis/rbac.authorization.k8s.io/v1beta1/clusterrolebindings/kubernetes-dashboard
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: cluster-admin
subjects:
- kind: ServiceAccount
name: kubernetes-dashboard
namespace: kube-system
Теперь конфигурация готова, можем сделать deploy: Поднимаем кластер:
kubernetes/terraform$ terraform init
kubernetes/terraform$ terraform apply
Настраиваем kubectl:
$ gcloud container clusters get-credentials default-cluster-1 --zone europe-west1-b --project docker-1234
Деплоим приложение:
kubernetes/reddit$ kubectl apply -f .
deployment.apps/comment created
service/comment-db created
service/comment created
namespace/dev unchanged
deployment.apps/mongo created
service/mongodb created
deployment.apps/post created
service/post-db created
service/post created
deployment.apps/ui created
service/ui created
Отыскиваем external-IP любой nod'ы:
kubernetes/reddit$ kubectl get nodes -o wide
NAME STATUS ROLES AGE VERSION INTERNAL-IP EXTERNAL-IP OS-IMAGE KERNEL-VERSION CONTAINER-RUNTIME
gke-default-cluster-1-default-pool-108b7cdc-9xdm Ready <none> 2m49s v1.12.8-gke.10 10.132.0.22 35.195.41.120 Container-Optimized OS from Google 4.14.127+ docker://17.3.2
gke-default-cluster-1-default-pool-108b7cdc-b2cf Ready <none> 2m47s v1.12.8-gke.10 10.132.0.20 35.187.169.66 Container-Optimized OS from Google 4.14.127+ docker://17.3.2
gke-default-cluster-1-default-pool-108b7cdc-ddl6 Ready <none> 2m47s v1.12.8-gke.10 10.132.0.21 104.199.86.87 Container-Optimized OS from Google 4.14.127+ docker://17.3.2
NodePort мы жестко задали в конфигурации (32092), поэтому искать его нет нужды, но при необходимости это всегда можно сделать через: kubectl get services -o wide.
Проверяем, что все pods поднялись:
$ kubectl get pods -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE
comment-849fdc7ddd-c2r8r 1/1 Running 0 5m45s 10.4.2.5 gke-default-cluster-1-default-pool-108b7cdc-b2cf <none>
comment-849fdc7ddd-klfs7 1/1 Running 0 5m45s 10.4.1.4 gke-default-cluster-1-default-pool-108b7cdc-ddl6 <none>
comment-849fdc7ddd-wpr86 1/1 Running 0 5m45s 10.4.2.6 gke-default-cluster-1-default-pool-108b7cdc-b2cf <none>
mongo-55f849fdbc-txctd 1/1 Running 0 5m42s 10.4.2.7 gke-default-cluster-1-default-pool-108b7cdc-b2cf <none>
post-86ddb7dc7d-bc4l8 1/1 Running 0 5m41s 10.4.2.8 gke-default-cluster-1-default-pool-108b7cdc-b2cf <none>
post-86ddb7dc7d-bjjz6 1/1 Running 0 5m41s 10.4.0.8 gke-default-cluster-1-default-pool-108b7cdc-9xdm <none>
post-86ddb7dc7d-rx56k 1/1 Running 0 5m41s 10.4.1.5 gke-default-cluster-1-default-pool-108b7cdc-ddl6 <none>
ui-576c455f4-f79fm 1/1 Running 0 5m39s 10.4.0.9 gke-default-cluster-1-default-pool-108b7cdc-9xdm <none>
ui-576c455f4-l94gg 1/1 Running 0 5m39s 10.4.2.9 gke-default-cluster-1-default-pool-108b7cdc-b2cf <none>
ui-576c455f4-rwtzh 1/1 Running 0 5m39s 10.4.1.6 gke-default-cluster-1-default-pool-108b7cdc-ddl6 <none>
Теперь мы можем подключиться к приложению по любому из адресов: 35.195.41.120:32092, 35.187.169.66:32092, 104.199.86.87:32092.
Осталось получить доступ к dashboard:
kubernetes/terraform/extra$ kubectl apply -f role-binding.yaml
clusterrolebinding.rbac.authorization.k8s.io/kubernetes-dashboard created
$ kubectl proxy
Starting to serve on 127.0.0.1:8001
В процессе выяснилось, что 2 worker nodes на машинах типа small недостаточно, чтобы поднять приложение:
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
comment-849fdc7ddd-4qdqn 1/1 Running 0 96s
comment-849fdc7ddd-9mbzt 1/1 Running 0 96s
comment-849fdc7ddd-kxnlf 1/1 Running 0 96s
mongo-55f849fdbc-knvhp 1/1 Running 0 94s
post-86ddb7dc7d-9fxnk 0/1 Pending 0 93s
post-86ddb7dc7d-rn2h9 1/1 Running 0 93s
post-86ddb7dc7d-vqkt7 1/1 Running 0 93s
ui-576c455f4-68r7z 0/1 Pending 0 91s
ui-576c455f4-94zmd 0/1 Pending 0 91s
ui-576c455f4-f4rh7 0/1 Pending 0 91s
$ kubectl describe pods ui-576c455f4-68r7z
Name: ui-576c455f4-68r7z
Namespace: default
Priority: 0
PriorityClassName: <none>
Node: <none>
Labels: app=reddit
component=ui
pod-template-hash=576c455f4
Annotations: kubernetes.io/limit-ranger: LimitRanger plugin set: cpu request for container ui
Status: Pending
IP:
Controlled By: ReplicaSet/ui-576c455f4
Containers:
ui:
Image: weisdd/ui
Port: <none>
Host Port: <none>
Requests:
cpu: 100m
Environment:
ENV: default (v1:metadata.namespace)
Mounts:
/var/run/secrets/kubernetes.io/serviceaccount from default-token-qkvjv (ro)
Conditions:
Type Status
PodScheduled False
Volumes:
default-token-qkvjv:
Type: Secret (a volume populated by a Secret)
SecretName: default-token-qkvjv
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
---- ------ ---- ---- -------
Warning FailedScheduling 39s (x5 over 3m6s) default-scheduler 0/2 nodes are available: 2 Insufficient cpu.
После добавления ещё одной nod'ы всё поднялось.
В данной работе мы рассмотрели:
- Ingress Controller;
- Ingress;
- Secret;
- TLS;
- LoadBalancer Service;
- Network Policies;
- PersistentVolumes;
- PersistentVolumeClaims.
- создали Kubernetes-манифест для TLS-сертификатов (*)
kubernetes/reddit/ui-service.yml
---
apiVersion: v1
kind: Service
metadata:
name: ui
labels:
app: reddit
component: ui
spec:
type: LoadBalancer
ports:
- port: 80
nodePort: 32092
protocol: TCP
targetPort: 9292
selector:
app: reddit
component: ui
Недостатки:
- нет поддержки правил HTTP URI;
- используются только облачные балансировщики.
В конечном итоге мы хотим получить Ingress для сервиса UI с терминацией TLS:
$ kubectl get ingress -n dev
NAME HOSTS ADDRESS PORTS AGE
ui * 34.98.108.33 80 13m
$ openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout tls.key -out tls.crt -subj "/CN=34.98.108.33"
$ kubectl create secret tls ui-ingress --key tls.key --cert tls.crt -n dev
$ kubectl describe secret ui-ingress -n dev
Name: ui-ingress
Namespace: dev
Labels: <none>
Annotations:
Type: kubernetes.io/tls
Data
====
tls.crt: 1107 bytes
tls.key: 1704 bytes
kubernetes/reddit/ui-service.yml
---
apiVersion: v1
kind: Service
metadata:
name: ui
labels:
app: reddit
component: ui
spec:
type: NodePort
ports:
- port: 9292
protocol: TCP
targetPort: 9292
selector:
app: reddit
component: ui
kubernetes/reddit/ui-ingress.yml
---
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: ui
annotations:
kubernetes.io/ingress.allow-http: "false"
spec:
tls:
- secretName: ui-ingress
rules:
- http:
paths:
- path: /*
backend:
serviceName: ui
servicePort: 9292
Задание: Опишите создаваемый объект Secret в виде Kubernetes-манифеста
Решение: kubernetes/reddit/secret.yml
---
apiVersion: v1
data:
tls.crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURCRENDQWV5Z0F3SUJBZ0lKQUxTcWp3TmFueVFaTUEwR0NTcUdTSWIzRFFFQkN3VUFNQmN4RlRBVEJnTlYKQkFNTURETTBMams0TGpFd09DNHpNekFlRncweE9UQTJNalF3T1RJME5EaGFGdzB5TURBMk1qTXdPVEkwTkRoYQpNQmN4RlRBVEJnTlZCQU1NRERNMExqazRMakV3T0M0ek16Q0NBU0l3RFFZSktvWklodmNOQVFFQkJRQURnZ0VQCkFEQ0NBUW9DZ2dFQkFMTTNldVFRSzJubm55ZWNTbWgycG1ob1NwOGFram1rbkVab2pud0tTWDVHZklZS1BkZTIKaXZybFVxd0dTQ3p5MVZaMXhaSlVGRGd1NUFTdU9CKzM2WFZ5alJVN1pOc3hwUnZQK3FyYUxQV1kwc2FuMzhtbAp3cmczYytXanprMWZLOXNlSE9DZHcyYzRhWmlBdXh2ZVNjMEQ4QlBhZHZHTjNUVXluVmFzSUV3aDFsUUx6MGYxCllZVDhHeHp5RnFxcXF6bFoxZytCUC85UjladVRyRTBHYzZWYkltOHNaVWROTlhLaG0rbXdRWTZwZkpMaERjNzcKZUFhQVk3RkVwVEQyNFJOMFptYkwwaHU2TlpjM2pxSU9jaXk0d0JrSW8rUXpKQTNRN0RVUGhOUk5NR1NObllDbApaRTI1QzYwSGtlRk1TSmVTeHJVMjJMSDdMd01FdXZTWWNaa0NBd0VBQWFOVE1GRXdIUVlEVlIwT0JCWUVGTmZBCnhFbFFJMCtOMFY0K1MxQXRObERiNnE2NU1COEdBMVVkSXdRWU1CYUFGTmZBeEVsUUkwK04wVjQrUzFBdE5sRGIKNnE2NU1BOEdBMVVkRXdFQi93UUZNQU1CQWY4d0RRWUpLb1pJaHZjTkFRRUxCUUFEZ2dFQkFIR0krb3J5Z0tGZAovMVJEUVh1UjJzYXIvWkdBdmpPUUFWbG5kWU5YRDhVaXhOOU5HYWxGQUR3UGIzd0RIUUkwOXZ5UnRVUkQ1NTNyCi9hd1ZDaFdBRmpGTndTMVRNOXhFb2ZNNFhoZ2sybDhLWlRUdWZmck5FQ1NkYm11eFF2SitscHR6czlkTTdxc2YKcFRKaEJXY2dzWG42SkQ3c01pY3pjNW1kUk0rd3AzVVdnYWs5UW5xODFscHhDVmhWNlZUREZkaUxQcXhUL3BvTQpSSDBycnZqWGVyRUNxTTNtak81ZDMxK24vTE9FbDVDL21VVlhGTU9hODhrZkNzeENUbUxLd0kxbndhM1NxdTZLCkh5Z1NPTlF3MTJpaVA5clVzcGsvSUxiRmt6d0FaZnZPN3RtRjVrRElIOUNDeDc5WVlpeElwWERoQTFpbnc2RUQKVUxicG1xTi8waGs9Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K
tls.key: LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JSUV2Z0lCQURBTkJna3Foa2lHOXcwQkFRRUZBQVNDQktnd2dnU2tBZ0VBQW9JQkFRQ3pOM3JrRUN0cDU1OG4KbkVwb2RxWm9hRXFmR3BJNXBKeEdhSTU4Q2tsK1JueUdDajNYdG9yNjVWS3NCa2dzOHRWV2RjV1NWQlE0THVRRQpyamdmdCtsMWNvMFZPMlRiTWFVYnovcXEyaXoxbU5MR3A5L0pwY0s0TjNQbG84NU5YeXZiSGh6Z25jTm5PR21ZCmdMc2Iza25OQS9BVDJuYnhqZDAxTXAxV3JDQk1JZFpVQzg5SDlXR0UvQnNjOGhhcXFxczVXZFlQZ1QvL1VmV2IKazZ4TkJuT2xXeUp2TEdWSFRUVnlvWnZwc0VHT3FYeVM0UTNPKzNnR2dHT3hSS1V3OXVFVGRHWm15OUlidWpXWApONDZpRG5Jc3VNQVpDS1BrTXlRTjBPdzFENFRVVFRCa2paMkFwV1JOdVF1dEI1SGhURWlYa3NhMU50aXgreThECkJMcjBtSEdaQWdNQkFBRUNnZ0VCQUxGTFYzVzdVVmVKM25kUE5JVTNxOVJxS0RoU0R3cWJnRVpmTldZY3Z5di8KQ3oySGNEcXd5YmovbU5PNXV6bE1XaVVZaVlZRFM2VnhMZjQ3WkNac3lMWjV4bnlVcjJmeEt3ZmlybmJMTStNYgpkcGswaGoxMGtMZXhsQ1EvOHQyMEl5eTRFNVZhUFc4bDdjRUN5UmYreGovZGYyb0VtOXJIbjJqN1U0L2s1R2FyCkFwZ0hHTk9yZWRhb2F5UWtCVjJ4TWRObW4vTCtrOW1pTmtZU1p3anRwSHhIRGlKb1dPaTFRYjY5TGV4Z2p0LzkKencvMEUxcTlnS0JmTVcwaWZrUnNlYTQrSkFMcEorYjlod1BiSHFxcDg5d0VkS3FxaDNtQmNRSjVFY0xlOEwrUQphMFdlR0pqeU9SMm5WSllmbzFiWGFyY3pkbFFKSWpHRG1oR1lmSGZ6TVVrQ2dZRUE0SnJPQlpoSk5HN3BlN1lXCmVIb1R4dzZzOVhqamloQTU0ZDVxaUNBLzZXMlNWUW1hTTJzUlJLbGxrd1AwODBUenY0ekFLbXpOUUZkSTlnWXkKM2M3ZzhOUGRrNmZoSDh4eTZOOWJsdCtmYUdUQWk3cVZHTitDVE9WRXd2MXZWdVRSV1B5MjFyV01qRFRTYWxGcgpRbk5MbmFxQlRFejMvaEhSSm5yWjgxTG84QXNDZ1lFQXpFU0dDUEpneXR5V3UrQmhzbG9xVnZycmVPS2ZuRXlXClpEV0trajFERWtzQitjUEdDTEpNeUlUUmdCa0xpUXBGRnhGSVZrQmFtZ2NFWDk3b0FXNVkwWXFxNFg1T1dzY2EKYzd0MXRORVVsVFpZaitwZUJkMkdocHUvL1JxNE5qZXdyWFRFM1hZM1EzTm9YdVBqeWZtUW5odE1FWXN4T1FSYQoreXhXYVRaVmQyc0NnWUFDNHJwRzVCLzhwbmdsZUw5dEtOUmNPRm9NaFZNWEVRSk5YcGdyeTQ1R3AzdTd6L1hGCk4xanZjdVcxTDlYVDBrejNadzJJL2dEZEZNSUJOeThzSGw1UmVjZGRBRWtxS1d4SWhhWWRtQjNxTDNFeDJtSFUKQWZzZ2xjbCtYOWN3WEp3NzhuTSs2dkkxVXZCcElVYXBnZVcwenQzK1p2YkFzdVhuQXFrT01VQ0E1UUtCZ1FETQpCeFdxRWc3RUVaa3RoMjJONjZtbXYzSTdpSURNYVdNK0xJeUFqVmhWQ1Q2cjd2UDY4N3psL0dRVmd1NldydjliCmp4T2xzeG5NQjM1REZDN0FYeXV3QU1XMS9hYmQzVWw2bDU5MlAvQkRLY2xEMW1DYmt5MHZmcHBCZ1ZMR05OSHcKTDFZRHJuUjhsdVdGQWpNZ0pucGdzZnRFK3JicnNJSC9wOFdYeVlHTWlRS0JnRHhVZUtRSXJaS0p6TENZRHZXMQpKU3FmckJwOGxXTnlnaDdsYjVKNEc5alJYby91ejBHWHBma0YvTHJRdWxaMTYxb3Zlakx2ZnovRnUxbG5SUHc4CmY0ZzdSOVFnLzB3UExTaGIxLzlaaHBaUlFzL1V6a1BkQ2pXSlpoV0xsZ0h2L3FaQzQ0K3VwcGdVd2RvbVo2WTEKQXluUnJYVTZFZS94eGw2eDc5YzJOWHVaCi0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0K
kind: Secret
metadata:
name: ui-ingress
namespace: dev
selfLink: /api/v1/namespaces/dev/secrets/ui-ingress
type: kubernetes.io/tls
После применения конфигурации в веб-интерфейсе GCP мы видим следующее:
k8s-um-dev-ui--502ac9c2c73eb555
Frontend
Protocol IP:Port Certificate Network Tier
HTTPS 34.98.77.119:443 k8s-ssl-2d85053accdfb0f1-970a0856fd7c2de7--502ac9c2c73eb555 Premium
Теперь приложение доступно по адресу: https://34.98.77.119/
Вопреки тому, что описано на слайдах otus, на машинах типа g1-small NetworkPolicy не поддерживается (судя по всему, никто работоспособность не проверял):
https://cloud.google.com/kubernetes-engine/docs/how-to/network-policy
Network policy is not supported for clusters whose nodes are f1-micro or g1-small instances, as the resource requirements are too high for instances of that size.
Note: If you enable or disable network policy for an existing cluster, GKE is required to re-create all of your cluster's node pools to ensure that the nodes are configured to run the network policy process.
После изменения конфигурации в Terraform и пересоздания GKE-кластера с изначально включенной поддержкой NetworkPolicy, ограничения заработали.
Note: Есть пара команд включающих поддержку NetworkPolicy для уже созданных кластеров ("gcloud beta container clusters update..."), но, как мне показалось, они не производят должного эффекта даже на правильном типе машин. Быть может, конечно, слишком мало ждал.
kubernetes/terraform/main.tf:
terraform {
required_version = ">=0.11,<0.12"
}
provider "google" {
version = "2.0.0"
project = "${var.project}"
region = "${var.region}"
}
resource "google_container_cluster" "cluster" {
name = "${var.cluster_name}"
zone = "${var.zone}"
initial_node_count = "${var.initial_node_count}"
node_config {
preemptible = "${var.is_preemptible}"
machine_type = "${var.machine_type}"
}
addons_config {
kubernetes_dashboard {
disabled = false
}
network_policy_config {
disabled = false
}
}
network_policy {
enabled = true
}
}
kubernetes/terraform/variables.tf:
variable "project" {
description = "Project ID"
}
variable "region" {
description = "Region"
default = "europe-west-1"
}
variable "cluster_name" {
default = "default-cluster-1"
}
variable "zone" {
description = "Zone"
default = "europe-west1-b"
}
variable "initial_node_count" {
default = 2
}
variable "disk_size" {
default = 20
}
variable "machine_type" {
default = "n1-standard-1"
}
variable "is_preemptible" {
default = "true"
}
kubernetes/terraform/terraform.tfvars:
project = "docker-1234"
region = "europe-west-1"
cluster_name = "default-cluster-1"
zone = "europe-west1-b"
initial_node_count = 3
disk_size = 20
machine_type = "n1-standard-1"
is_preemptible = true
kubernetes/reddit/mongo-network-policy.yml
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: deny-db-traffic
labels:
app: reddit
spec:
podSelector:
matchLabels:
app: reddit
component: mongo
policyTypes:
- Ingress
ingress:
- from:
- podSelector:
matchLabels:
component: comment
- podSelector:
matchLabels:
component: post
- приведённая конфигурация учитывает задание "Обновите mongo-network-policy.yml так, чтобы post-сервис дошел до базы данных." (стр. 53)
Пример логов при недоступности comment / post:
E, [2019-06-24T11:59:32.204277 #1] ERROR -- : service=ui | event=show_post | request_id=17cde18e-7f8a-4762-b698-6d1d685125da | message='Counldn't show the comments. Reason: 765: unexpected token at ''' | params: {"id":"5d10baa44549b1000edb5d2c"}
E, [2019-06-24T11:18:02.689629 #1] ERROR -- : service=ui | event=show_all_posts | request_id=b1019a24-209f-4736-8355-c98e2bb0997c | message='Failed to read from Post service. Reason: Net::ReadTimeout' | params: "{}"
Создаём диск:
$ gcloud compute disks create --size=25GB --zone=europe-west1-b reddit-mongo-disk
Подключаем к машине: kubernetes/reddit/mongo-deployment.yml
[...]
spec:
containers:
- image: mongo:3.2
name: mongo
volumeMounts:
- name: mongo-gce-pd-storage
mountPath: /data/db
volumes:
- name: mongo-gce-pd-storage
gcePersistentDisk:
pdName: reddit-mongo-disk
fsType: ext4
Данная схема позволяет выделять ресурс не целиком (весь диск), а лишь его часть, в соответствии с запросом. Здесь мы используем тот же самый диск из п.1.
Добавляем PersistentVolume в кластер: kuberentes/reddit/mongo-volume.yml
---
apiVersion: v1
kind: PersistentVolume
metadata:
name: reddit-mongo-disk
spec:
capacity:
storage: 25Gi
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Retain
gcePersistentDisk:
fsType: "ext4"
pdName: "reddit-mongo-disk"
$ kubectl apply -f mongo-volume.yml -n dev
Далее описываем PersistentVolumeClaim (PVC):
kuberentes/reddit/mongo-claim.yml
---
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
name: mongo-pvc
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 15Gi
$ kubectl apply -f mongo-claim.yml -n dev
ВАЖНО: в единый момент времени, один PV может использоваться только для одного Claim'а. Если PV с заданными параметрами не будет найдет, то Claim его создаст, воспользовавшись стандартным StorageClass ($ kubectl describe storageclass standard -n dev)
kuberentes/reddit/mongo-deployment.yml
[...]
spec:
containers:
- image: mongo:3.2
name: mongo
volumeMounts:
- name: mongo-gce-pd-storage
mountPath: /data/db
volumes:
- name: mongo-gce-pd-storage
persistentVolumeClaim:
claimName: mongo-pvc
StorageClass позволяет описывать, где и какое хранилище будет создаваться. Пример для SSD:
kubernetes/reddit/storage-fast.yml
---
kind: StorageClass
apiVersion: storage.k8s.io/v1beta1
metadata:
name: fast
provisioner: kubernetes.io/gce-pd
parameters:
type: pd-ssd
$ kubectl apply -f storage-fast.yml -n dev
kuberentes/reddit/mongo-claim-dynamic.yml
---
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
name: mongo-pvc-dynamic
spec:
accessModes:
- ReadWriteOnce
storageClassName: fast
resources:
requests:
storage: 10Gi
$ kubectl apply -f mongo-claim-dynamic.yml -n dev
Вносим соответствующие изменения в deployment: kuberentes/reddit/mongo-deployment.yml
spec:
containers:
- image: mongo:3.2
name: mongo
volumeMounts:
- name: mongo-gce-pd-storage
mountPath: /data/db
volumes:
- name: mongo-gce-pd-storage
persistentVolumeClaim:
claimName: mongo-pvc-dynamic
Статус PV можно проверить следующим образом:
$ kubectl get persistentvolume -n dev
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE
pvc-c01885d8-9683-11e9-9cd4-42010a84020f 15Gi RWO Delete Bound dev/mongo-pvc standard 9m20s
pvc-f20ff404-9684-11e9-9cd4-42010a84020f 10Gi RWO Delete Bound dev/mongo-pvc-dynamic fast 47s
reddit-mongo-disk 25Gi RWO Retain Available 18m
В данной работе мы:
- познакомились с Helm;
- развернули Gitlab CI Omnibus в Kubernetes;
- настроили Pipeline для автоматического разворачивания приложения в Kubernetes.
Подготавливаем Tiller (серверная часть Helm, разворачивается в kubernetes) kubernetes/tiller.yml
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: tiller
namespace: kube-system
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRoleBinding
metadata:
name: tiller
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: cluster-admin
subjects:
- kind: ServiceAccount
name: tiller
namespace: kube-system
$ kubectl apply -f tiller.yml
$ helm init --service-account tiller
В kubernetes/Charts подготовлены шаблоны как для поднятия отдельных компонентов приложения (comment, ui, post), так и приложения целиком (reddit). Шаблоны параметризированы, значения переменных хранятся в соответствующих values.yaml. При этом, reddit/values.yaml позволяет переопределять значения, заданные в конкретных компонентах. Helper'ы хранятся в Charts//templates/_helpers.tpl
Подготовка пакета:
kubernetes/Charts$ helm dep update ./reddit
Выкатывание релиза (как для отдельного компонента, так и для всего приложения):
kubernetes/Charts$ helm install --name <release-name> <path>
kubernetes/Charts$ helm install --name test-ui-1 ui/
Обновление релиза после внесения изменений:
kubernetes/Charts$ helm upgrade <release-name> <path>
Полное удаление релиза:
$ helm del --purge <release-name>
В helm есть некий репозиторий с заранее подготовленными Charts. Пример поиска:
$ helm search mongo
NAME CHART VERSION APP VERSION DESCRIPTION
stable/mongodb 5.20.0 4.0.10 NoSQL document-oriented database that stores JSON-like do...
[...]
Соответственно, в kubernetes/Charts/reddit/requirements.yaml мы можем сослаться на найденное в репозитории.
Важно: Учитывая, что на слайдах была отсылка к старому релизу Chart (0.4.18), я решил поставить версию посвежее - 5.20.0:
---
dependencies:
[...]
- name: mongodb
version: 5.20.0
repository: https://kubernetes-charts.storage.googleapis.com
Увы в данной версии, похоже, есть какие-то отличия в плане авторизации, поэтому post начал падать с ошибкой:
"/usr/local/lib/python3.6/site-packages/pymongo/helpers.py\", line 210, in _check_command_response\n raise OperationFailure(msg % errmsg, code, response)\npymongo.errors.OperationFailure: command find requires authentication\n"}
Пришлось вернуть на:
---
dependencies:
[...]
- name: mongodb
version: 0.4.18
repository: https://kubernetes-charts.storage.googleapis.com
Поскольку имена сервисов у нас теперь с префиксами, включающими namespace, необходимо переопределить переменные окружения в ui: /kubernetes/Charts/ui/templates/deployment.yaml
env:
- name: POST_SERVICE_HOST
value: {{ .Values.postHost | default (printf "%s-post" .Release.Name) }}
- name: POST_SERVICE_PORT
value: {{ .Values.postPort | default "5000" | quote }}
- name: COMMENT_SERVICE_HOST
value: {{ .Values.commentHost | default (printf "%s-comment" .Release.Name) }}
- name: COMMENT_SERVICE_PORT
value: {{ .Values.commentPort | default "9292" | quote }}
Для Gitlab CI потребовалось сменить тип машин на более мощный (-> n1-standard-2) и активировать устаревшие права доступа (enable_legacy_abac). Попутно через дополнительные переменные отключил network policy за ненадобностью (disable_network_policy_addon, enable_network_policy), размер кластера сократил до двух (в процессе работы один из кластеров падал, pod автоматически пересоздался - всё отлично отработало). Всё это отражено в конфигурации terraform: kubernetes/terraform/.
kubernetes/Charts/$ helm repo add gitlab https://charts.gitlab.io
$ helm fetch gitlab/gitlab-omnibus --version 0.1.37 --untar
$ cd gitlab-omnibus
Далее подменяем файлы (см. репозиторий):
- gitlab-omnibus/values.yaml
- gitlab-omnibus/templates/gitlab/gitlab- svc.yaml
- gitlab-omnibus/templates/gitlab- config.yaml
- gitlab-omnibus/templates/ingress/gitlab-ingress.yaml
Делаем пробный деплой:
kubernetes/Charts/gitlab-omnibus$ helm install --name gitlab . -f values.yaml
И тут же упираемся в ошибку:
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
gitlab-gitlab-65fc944597-57rzr 0/1 Pending 0 9m6s
gitlab-gitlab-postgresql-784bcc4487-jcvm7 1/1 Running 0 9m6s
gitlab-gitlab-redis-55b589c99c-jl6dj 1/1 Running 0 9m6s
gitlab-gitlab-runner-5f8575bb9c-w9p54 1/1 Running 6 9m6s
$ kubectl describe pods gitlab-gitlab-65fc944597-57rzr
Name: gitlab-gitlab-65fc944597-57rzr
Namespace: default
Priority: 0
PriorityClassName: <none>
[...]
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Warning FailedScheduling 5s (x26 over 9m21s) default-scheduler pod has unbound immediate PersistentVolumeClaims (repeated 2 times)
$ kubectl describe pvc
[...]
Name: gitlab-gitlab-registry-storage
Namespace: default
StorageClass: gitlab-gitlab-fast
Status: Pending
Volume:
Labels: app=gitlab-gitlab
chart=gitlab-omnibus-0.1.37
heritage=Tiller
release=gitlab
Annotations: volume.beta.kubernetes.io/storage-class: gitlab-gitlab-fast
volume.beta.kubernetes.io/storage-provisioner: kubernetes.io/gce-pd
Finalizers: [kubernetes.io/pvc-protection]
Capacity:
Access Modes:
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Warning ProvisioningFailed 88s (x16 over 20m) persistentvolume-controller Failed to provision volume with StorageClass "gitlab-gitlab-fast": googleapi: Error 403: QUOTA_EXCEEDED - Quota 'SSD_TOTAL_GB' exceeded. Limit: 100.0 in region europe-west1.
Mounted By: gitlab-gitlab-65fc944597-57rzr
Поскольку Gitlab CI Omnibus в GKE по умолчанию использует SSD, можете мы упёрлись в лимит в 100 Гб для одного из POD'ов. В процессе исследования вопроса обнаружил, что после задания kubernetes-3 у нас остался ненужный SSD диск. После его удаления всё ок. В качестве альтернативы можно было в конфигурации Gitlab CI явно задать storage class, соответствующий обычным дискам.
Отыскиваем IP-адрес ingress'а:
$ kubectl get service -n nginx-ingress nginx
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
nginx LoadBalancer 10.7.252.35 34.77.124.149 80:32616/TCP,443:30608/TCP,22:31145/TCP 2m57s
Прописываем его в /etc/hosts:
34.77.124.149 gitlab-gitlab staging production
Теперь проект у нас есть доступ к http://gitlab-gitlab/.
Создаём группу weisdd, в Settings->CI/CD определяем две переменные, которые будут использоваться в нашем pipeline: CI_REGISTRY_USER, CI_REGISTRY_PASSWORD - они соответствуют реквизитам доступа к Docker Hub.
ВАЖНО: переменная CI_REGISTRY_PASSWORD не должна иметь статус protected, иначе стадия build будет вываливаться с ошибкой на этапе аутентификации ($ docker login ...)
Далее в группе создаём проекты comment, post, ui, reddit-deploy, закидываем в них соответсвующие файлы из src/ (для comment, post, ui) и kubernetes/Charts (для reddit-deploy).
Пример:
$ git init
$ git remote add origin http://gitlab-gitlab/weisdd/ui.git
$ git add .
$ git commit -m “init”
$ git push origin master
Итоговые пайплайны:
- reddit-deploy: kubernetes/Charts/.gitlab-ci.yml
- comment: src/comment/.gitlab-ci.yml
- post: src/post/.gitlab-ci.yml
- ui: src/ui/.gitlab-ci.yml
ВАЖНО: пример пайплайна из презентации вываливался с ошибкой:
$HELM_HOME has been configured at /root/.helm.
Error: error when upgrading: current Tiller version is newer, use --force-upgrade to downgrade
ERROR: Job failed: error executing remote command: command terminated with non-zero exit code: Error executing in Docker Container: 1
Поэтому в function install_tiller() в строке "helm init --upgrade" убирал ключ "--upgrade".
Теперь у нас по коммиту в feature-ветки автоматически выкатывается окружение для review, удалять его необходимо вручную. В master-ветке пайплайн отличается:
- docker-образы не собираются;
- деплой идёт на статичные окружения (staging, production);
- окружения не удаляются (в т.ч. staging).
Для каждой ветки в /etc/hosts должна быть отдельная запись:
34.77.124.149 gitlab-gitlab staging production
34.77.124.149 weisdd-ui-feature-3
34.77.124.149 weisdd-comment-feature-4
34.77.124.149 weisdd-post-feature-3
$ helm ls
NAME REVISION UPDATED STATUS CHART APP VERSION NAMESPACE
gitlab 1 Wed Jun 26 01:34:49 2019 DEPLOYED gitlab-omnibus-0.1.37 default
production 1 Wed Jun 26 03:28:48 2019 DEPLOYED reddit-0.1.0 production
review-weisdd-com-79uloj 1 Wed Jun 26 03:16:05 2019 DEPLOYED reddit-0.1.0 review
staging 1 Wed Jun 26 03:26:39 2019 DEPLOYED reddit-0.1.0 staging
В данной работе мы:
- развернули Prometheus в k8s;
- настроили Prometheus и Grafana для сбора метрик;
- настроили EFK для сбора логов;
- подготовили helm chart для разворачивания EFK (*).
Для кластера теперь используем:
- 2 x g1-small;
- 1 x n1-standard-2.
Дополнительные параметры:
- Stackdriver Logging - Отключен (tf: logging_service = "none")
- Stackdriver Monitoring - Отключен (tf: monitoring_service = "none")
- Устаревшие права доступа - Включено (tf: enable_legacy_abac = true)
Конфигурация kubernetes/terraform адаптирована под поддержку кластера из разных нод, а также на использование указанных выше параметров.
kubernetes/terraform$ terraform apply
kubernetes/terraform$ gcloud container clusters get-credentials default-cluster-1 --zone europe-west1-b --project docker-1234
kubernetes$ kubectl apply -f tiller.yml
kubernetes$ helm init --service-account tiller
Устанавливаем ingress-контроллер nginx:
$ helm install stable/nginx-ingress --name nginx
[...]
$ kubectl get services
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kubernetes ClusterIP 10.7.240.1 <none> 443/TCP 13m
nginx-nginx-ingress-controller LoadBalancer 10.7.252.12 34.76.156.20 80:31484/TCP,443:31490/TCP 51s
nginx-nginx-ingress-default-backend ClusterIP 10.7.240.172 <none> 80/TCP 51s
Добавляем запись в /etc/hosts:
$ tail -n 2 /etc/hosts
34.76.156.20 reddit reddit-prometheus reddit-grafana reddit-non-prod production reddit-kibana staging prod
Запускаем приложение в нескольких экземплярах:
kubernetes/Charts$ helm upgrade reddit-test ./reddit --install
kubernetes/Charts$ helm upgrade production --namespace production ./reddit --install
kubernetes/Charts$ helm upgrade staging --namespace staging ./reddit --install
Устанавливаем Prometheus:
kubernetes/Charts$ helm fetch --untar stable/prometheus
Добавляем файл с конфигурацией custom_values.yaml (https://gist.githubusercontent.com/chromko/2bd290f7becdf707cde836ba1ea6ec5c/raw/c17372866867607cf4a0445eb519f9c2c377a0ba/gistfile1.txt), активируем в ней kubeStateMetrics и nodeExporter:
kubeStateMetrics:
enabled: true
nodeExporter:
enabled: true
Запускаем Prometheus:
Charts/prometheus$ helm upgrade prom . -f custom_values.yaml --install
Release "prom" does not exist. Installing it now.
Конфигурация меток для каждого из компонентов приложения
- job_name: 'ui-endpoints'
kubernetes_sd_configs:
- role: endpoints
relabel_configs:
- action: labelmap
regex: __meta_kubernetes_service_label_(.+)
- source_labels: [__meta_kubernetes_service_label_app]
action: keep
regex: ui
- source_labels: [__meta_kubernetes_namespace]
target_label: kubernetes_namespace
- source_labels: [__meta_kubernetes_service_name]
target_label: kubernetes_name
- job_name: 'comment-endpoints'
kubernetes_sd_configs:
- role: endpoints
relabel_configs:
- action: labelmap
regex: __meta_kubernetes_service_label_(.+)
- source_labels: [__meta_kubernetes_service_label_app]
action: keep
regex: comment
- source_labels: [__meta_kubernetes_namespace]
target_label: kubernetes_namespace
- source_labels: [__meta_kubernetes_service_name]
target_label: kubernetes_name
- job_name: 'post-endpoints'
kubernetes_sd_configs:
- role: endpoints
relabel_configs:
- action: labelmap
regex: __meta_kubernetes_service_label_(.+)
- source_labels: [__meta_kubernetes_service_label_app]
action: keep
regex: post
- source_labels: [__meta_kubernetes_namespace]
target_label: kubernetes_namespace
- source_labels: [__meta_kubernetes_service_name]
target_label: kubernetes_name
Поддержка разделения target'ов по окружениям:
- job_name: 'reddit-production'
kubernetes_sd_configs:
- role: endpoints
relabel_configs:
- action: labelmap
regex: __meta_kubernetes_service_label_(.+)
- source_labels: [__meta_kubernetes_service_label_app, __meta_kubernetes_namespace]
action: keep
regex: reddit;(production|staging)+
- source_labels: [__meta_kubernetes_namespace]
target_label: kubernetes_namespace
- source_labels: [__meta_kubernetes_service_name]
target_label: kubernetes_name
ВАЖНО: конфигурация, предложенная на слайдах, не учитывает существование namespace отличных от production и staging, поэтому остальные target'ы выпадают. В качестве эксперимента решил добавить отображение остальных окружений в отдельной группе reddit-test:
- job_name: 'reddit-test'
kubernetes_sd_configs:
- role: endpoints
relabel_configs:
- action: labelmap
regex: __meta_kubernetes_service_label_(.+)
- source_labels: [__meta_kubernetes_namespace]
action: drop
regex: (production|staging)+
- source_labels: [__meta_kubernetes_service_label_app, __meta_kubernetes_namespace]
action: keep
regex: reddit;.+
- source_labels: [__meta_kubernetes_namespace]
target_label: kubernetes_namespace
- source_labels: [__meta_kubernetes_service_name]
target_label: kubernetes_name
- Не разобрался как сделать негативное совпадение (стандартный для этих целей синтаксис regex не сработал), поэтому пришлось попросту отбросить метки production и staging из __meta_kubernetes_namespace. Пример отображения:
reddit-test (6/6 up)
Endpoint State Labels Last Scrape Error
http://10.4.3.11:9292/metrics
up app="reddit" component="comment" instance="10.4.3.11:9292" kubernetes_name="reddit-test-comment" kubernetes_namespace="default" release="reddit-test" 11.408s ago
http://10.4.3.12:5000/metrics
up app="reddit" component="post" instance="10.4.3.12:5000" kubernetes_name="reddit-test-post" kubernetes_namespace="default" release="reddit-test" 40.539s ago
http://10.4.3.13:9292/metrics
up app="reddit" component="ui" instance="10.4.3.13:9292" kubernetes_name="reddit-test-ui" kubernetes_namespace="default" release="reddit-test" 14.671s ago
http://10.4.3.18:9292/metrics
up app="reddit" component="ui" instance="10.4.3.18:9292" kubernetes_name="test2-ui" kubernetes_namespace="test2" release="test2" 11.346s ago
http://10.4.3.19:5000/metrics
up app="reddit" component="post" instance="10.4.3.19:5000" kubernetes_name="test2-post" kubernetes_namespace="test2" release="test2" 19.908s ago
http://10.4.3.20:9292/metrics
up app="reddit" component="comment" instance="10.4.3.20:9292" kubernetes_name="test2-comment" kubernetes_namespace="test2" release="test2" 4.169s ago
helm upgrade --install grafana stable/grafana --set "adminPassword=admin" --set "service.type=NodePort" --set "ingress.enabled=true" --set "ingress.hosts={reddit-grafana}"
Импортировал 3 дэшборда из предыдущего дз:
- Docker and system monitoring;
- Business_Logic_Monitoring;
- UI Service Monitoring.
Изначально графики Business_Logic_Monitoring и UI Service Monitoring не поддерживали разделение на окружения, эта поддержка была добавлена в рамках д/з при помощи т.н. механизма Templating, обновленные файлы положил в monitoring/grafana-k8s/dashboards.
ВАЖНО: на слайде на стр. 44 была ошибка - у нас нет метки namespace, для templating использовать kubernetes_namespace. e.g.
rate(ui_request_count{kubernetes_namespace=~"$namespace"}[1m])
Т.к. в нашей конфигурации ElasticSearch жестко задано, на ноду с какой меткой должна происходить установка elastic, поэтому её необходимо проставить на ноде с n1-standard-2:
$ kubectl label node gke-default-cluster-1-node-pool-2-7607162c-pg28 elastichost=true
node/gke-default-cluster-1-node-pool-2-7607162c-pg28 labeled
Заранее подготовленную конфигурацию помещаем в kubernetes/efk/, запускаем ELK:
kubernetes/Charts$ kubectl apply -f ./efk
persistentvolumeclaim/elasticsearch-logging-claim created
service/elasticsearch-logging created
statefulset.apps/elasticsearch-logging created
service/elasticsearch-logging unchanged
daemonset.apps/fluentd-es-v2.0.2 created
$ helm upgrade --install kibana stable/kibana --set "ingress.enabled=true" --set "ingress.hosts={reddit-kibana}" --set "env.ELASTICSEARCH_URL=http://elasticsearch-logging:9200" --version 0.1.1
Задание: Создайте Helm-чарт для установки стека EFK и поместите в директорию charts
Решение: kubernetes/Charts/efk2:
kubernetes/Charts$ tree efk2
efk2
├── charts
│ ├── elasticsearch-1.0.0.tgz
│ ├── fluentd-1.0.0.tgz
│ └── kibana-0.1.1.tgz
├── Chart.yaml
├── requirements.lock
├── requirements.yaml
└── values.yaml
1 directory, 7 files
kubernetes/Charts$ tree elasticsearch/
elasticsearch/
├── Chart.yaml
├── templates
│ ├── pvc.yaml
│ ├── service.yaml
│ └── statefulset.yaml
└── values.yaml
1 directory, 5 files
kubernetes/Charts$ tree fluentd/
fluentd/
├── Chart.yaml
├── templates
│ ├── configmap.yaml
│ └── deamonset.yaml
└── values.yaml
1 directory, 4 files
В целом, мне было интересно лишь попробовать собрать всё в один пакет, поэтому параметризировать конфигурацию не стал. Добавил только необходимые параметры: kubernetes/Charts/efk2/values.yaml
---
kibana:
ingress:
enabled: true
hosts: {reddit-kibana}
env:
ELASTICSEARCH_URL: http://elasticsearch-logging:9200
Деплой можно выполнить следующим образом:
kubernetes/Charts/efk2$ helm install --name efk .