Skip to content

Otus-DevOps-2018-09/ozyab09_microservices

Repository files navigation

ozyab09_microservices

ozyab09 microservices repository

Homework 25 (kubernetes-5)

Build Status

Подготовка

У нас должен быть развернуть кластер k8s:

  • минимум 2 ноды g1-small (1,5 ГБ)
  • минимум 1 нода n1-standard-2 (7,5 ГБ)

В настройках:

  • Stackdriver Logging - Отключен
  • Stackdriver Monitoring - Отключен
  • Устаревшие права доступа - Включено

Т.к. сервер был создан заново, повторно установим Tiller: $ kubectl apply -f kubectl apply -f kubernetes/reddit/tiller.yml

Запуск tiller-сервера: $ helm init --service-account tiller

Проверка: $ kubectl get pods -n kube-system --selector app=helm

Из Helm-чарта установим ingress-контроллер nginx: $ helm install stable/nginx-ingress --name nginx

Найдем выданный IP-адрес:

$ kubectl get svc
NAME                                  TYPE           CLUSTER-IP     EXTERNAL-IP     PORT(S)                      AGE
kubernetes                            ClusterIP      10.47.240.1    <none>          443/TCP                      6m
nginx-nginx-ingress-controller        LoadBalancer   10.47.255.40   35.197.107.27   80:30949/TCP,443:31982/TCP   1m
nginx-nginx-ingress-default-backend   ClusterIP      10.47.251.3    <none>          80/TCP                       1m

Добавим в /etc/hosts: # echo 35.197.107.27 reddit reddit-prometheus reddit-grafana reddit-non-prod production reddit-kibana staging prod > /etc/hosts

План

  • Развертывание Prometheus в k8s
  • Настройка Prometheus и Grafana для сбора метрик
  • Настройка EFK для сбора логов

Мониторинг

Будем использовать инструменты:

  • prometheus - сервер сбора и алергинга
  • grafana - сервер визуализации метрик
  • alertmanager - компонент prometheus для алертинга
  • различные экспортеры для метрик prometheus

Prometheus отлично подходит для работы с контейнерами и динамичным размещением сервисов.

Схема работы: Monitoring Pipeline

Установка Prometheus

Prometheus будем ставить с помощью Helm чарта. Загрузим prometheus локально в Charts каталог:

$ cd kubernetes/charts
$ helm fetch —-untar stable/prometheus 

Создадим внутри директории чарта файл custom_values.yml.

Основные отличия от values.yml:

  • отключена часть устанавливаемых сервисов (pushgateway, alertmanager, kube-state-metrics)
  • включено создание Ingress’а для подключения через nginx
  • поправлен endpoint для сбора метрик cadvisor
  • уменьшен интервал сбора метрик (с 1 минуты до 30 секунд)

Запустим Prometheus в k8s:

$ cd kubernetes/charsts/prometheus
$ helm upgrade prom . -f custom_values.yml --install`

Перейдем по адресу http://reddit-prometheus/ в раздел Targets. reddit-prometheus

Targets

У нас уже присутствует ряд endpoint’ов для сбора метрик

  • метрики API-сервера
  • метрики нод с cadvisor’ов
  • сам prometheus

Targets

Отметим, что можно собирать метрики cadvisor’а (который уже является частью kubelet) через проксирующий запрос в kube-api-server.

Если зайти по ssh на любую из машин кластера и запросить $ curl http://localhost:4194/metrics то получим те же метрики у kubelet напрямую

Но вариант с kube-api предпочтительней, т.к. этот трафик шифруется TLS и требует аутентификации.

Таргеты для сбора метрик найдены с помощью service discovery (SD), настроенного в конфиге prometheus (лежит в custom-values.yml):

 prometheus.yml:
...
 - job_name: 'kubernetes-apiservers' # kubernetes-apiservers (1/1 up)
...
 - job_name: 'kubernetes-nodes' # kubernetes-apiservers (3/3 up)
 kubernetes_sd_configs:         # Настройки Service Discovery (для поиска target'ов)
 - role: node
 scheme: https                  # Настройки подключения к target’ам (для сбора метрик)
 tls_config:
 ca_file: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt
 insecure_skip_verify: true
 bearer_token_file: /var/run/secrets/kubernetes.io/serviceaccount/token
 relabel_configs:             # Настройки различных меток, фильтрация найденных таргетов, их изменение

Использование SD в kubernetes позволяет нам динамично менять кластер (как сами хосты, так и сервисы и приложения) Цели для мониторинга находим c помощью запросов к k8s API:

custom-values.yml

...
 prometheus.yml:
...
scrape_configs:
 - job_name: 'kubernetes-nodes'
 kubernetes_sd_configs:
 - role: node

Role объект, который нужно найти:

  • node
  • endpoints
  • pod
  • service
  • ingress

Prometheus ищет ноды Prometheus service discovery

Т.к. сбор метрик prometheus осуществляется поверх стандартного HTTP-протокола, то могут понадобится дополнительные настройки для безопасного доступа к метрикам.

Ниже приведены настройки для сбора метрик из k8s API

custom-values.yml

...
scheme: https # Схема подключения - http (default) или https
tls_config: # Конфиг TLS - коревой сертификат сервера для проверки достоверности сервера
 ca_file: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt
 insecure_skip_verify: true
bearer_token_file: /var/run/secrets/kubernetes.io/serviceaccount/token # Токен для аутентификации на сервере
custom-values.yml

...
#Kubernetes nodes
relabel_configs:    #  преобразовать все k8s лейблы таргета в лейблы prometheus
- action: labelmap
  regex: __meta_kubernetes_node_label_(.+)
- target_label: __address__   #  Поменять лейбл для адреса сбора метрик
  replacement: kubernetes.default.svc:443
- source_labels: [__meta_kubernetes_node_name]
  regex: (.+)
  target_label: __metrics_path__    #  Поменять лейбл для пути сбора метрик
  replacement: /api/v1/nodes/${1}/proxy/metrics/cadvisor 

Метрики

Все найденные на эндпоинтах метрики сразу же отобразятся в списке (вкладка Graph). Метрики Cadvisor начинаются с container_.

Cardvisor Metics

Cadvisor собирает лишь информацию о потреблении ресурсов и производительности отдельных docker-контейнеров. При этом он ничего не знает о сущностях k8s (деплойменты, репликасеты, ...).

Для сбора этой информации будем использовать сервис kube-state-metrics. Он входит в чарт Prometheus. Включим его.

prometheus/custom_values.yml 

...
kubeStateMetrics:
 ## If false, kube-state-metrics will not be installed
 ##
 enabled: true 

Обновим релиз: $ helm upgrade prom . -f custom_values.yml --install

В Targets Targets After Update

В Graph Graph After Update

По аналогии с kube_state_metrics включим (enabled: true) поды node-exporter в custom_values.yml:

prometheus/custom_values.yml 

...
nodeExporter:
  enabled: true

Обновим релиз: $ helm upgrade prom . -f custom_values.yml --install

Проверим, что метрики начали собираться с них. Node-exporter metrics

Запустим приложение из helm чарта reddit:

$ cd kubernetes/charts
$ helm upgrade reddit-test ./reddit --install
$ helm upgrade production --namespace production ./reddit --install
$ helm upgrade staging --namespace staging ./reddit —-install

Раньше мы "хардкодили" адреса/dns-имена наших приложений для сбора метрик с них.

prometheus.yml

 - job_name: 'ui'
   static_configs:
     - targets:
       - 'ui:9292'

- job_name: 'comment'
  static_configs:
    - targets:
      - 'comment:9292'

Теперь мы можем использовать механизм ServiceDiscovery для обнаружения приложений, запущенных в k8s.

Приложения будем искать так же, как и служебные сервисы k8s. Модернизируем конфиг prometheus:

custom_values.yml

  - job_name: 'reddit-endpoints'
    kubernetes_sd_configs:
      - role: endpoints
    relabel_configs:
      - source_labels: [__meta_kubernetes_service_label_app]
        action: keep  #Используем действие keep, чтобы оставить только эндпоинты сервисов с метками “app=reddit”
        regex: reddit

Обновите релиз prometheus: $ helm upgrade prom . -f custom_values.yml --install SD applying for reddit-endpoints discovery

Мы получили эндпоинты, но что это за поды мы не знаем. Добавим метки k8s. Все лейблы и аннотации k8s изначально отображаются в prometheus в формате:

__meta_kubernetes_service_label_labelname
__meta_kubernetes_service_annotation_annotationname 
custom_values.yml

  relabel_configs:
    - action: labelmap  #Отобразить все совпадения групп из regex в label’ы Prometheus
      regex: __meta_kubernetes_service_label_(.+) 

Обновим релиз prometheus: $ helm upgrade prom . -f custom_values.yml --install

Теперь мы видим лейблы k8s, присвоенные POD’ам: K8s PODs labels

Добавим еще label’ы для prometheus и обновим helm-релиз. Т.к. метки вида _meta* не публикуются, то нужно создать свои, перенеся в них информацию:

custom_values.yml

...
- source_labels: [__meta_kubernetes_namespace]
  target_label: kubernetes_namespace
- source_labels: [__meta_kubernetes_service_name]
  target_label: kubernetes_name
...

Обновим релиз prometheus: Added labels for prometheus

Сейчас мы собираем метрики со всех сервисов reddit’а в 1 группе target-ов. Мы можем отделить target-ы компонент друг от друга (по окружениям, по самим компонентам), а также выключать и включать опцию мониторинга для них с помощью все тех же labelов. Например, добавим в конфиг еще 1 job:

custom_values.yml

...
- 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
...

Обновим релиз prometheus и посмотрим: no information

Метрики будут отображаться для всех инстансов приложений: no information

Разобьем конфигурацию job’а reddit-endpoints так, чтобы было 3 job’а для каждой из компонент приложений (post-endpoints, commenten-dpoints, ui-endpoints), а reddit-endpoints уберем:

custom_values.yml

...
      - job_name: 'post-endpoints'
        kubernetes_sd_configs:
          - role: endpoints
        relabel_configs:
          - source_labels: [__meta_kubernetes_service_label_component,__meta_kubernetes_namespace]
            action: keep
            regex: post;(production|staging)+
          - action: labelmap
            regex: __meta_kubernetes_service_label_(.+)
          - source_labels: [__meta_kubernetes_namespace]
            target_label: kubernetes_namespace
          - source_labels: [__meta_kubernetes_service_name]
            target_label: kubernetes_name

      - job_name: 'ui-endpoints'
        kubernetes_sd_configs:
          - role: endpoints
        relabel_configs:
          - source_labels: [__meta_kubernetes_service_label_component,__meta_kubernetes_namespace]
            action: keep
            regex: ui;(production|staging)+
          - action: labelmap
            regex: __meta_kubernetes_service_label_(.+)
          - 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:
          - source_labels: [__meta_kubernetes_service_label_component,__meta_kubernetes_namespace]
            action: keep
            regex: comment;(production|staging)+
          - action: labelmap
            regex: __meta_kubernetes_service_label_(.+)
          - source_labels: [__meta_kubernetes_namespace]
            target_label: kubernetes_namespace
          - source_labels: [__meta_kubernetes_service_name]
            target_label: kubernetes_name
...

Splitted configuration

Визуализация

Поставим также grafana с помощью helm:

helm upgrade --install grafana stable/grafana --set "adminPassword=admin" \
  --set "service.type=NodePort" \
  --set "ingress.enabled=true" \
  --set "ingress.hosts={reddit-grafana}"

Перейдем на http://reddit-grafana/

Grapafana welcome screen

Добавим prometheus data-source Adding prometheus datasource

Адрес найдем из имени сервиса prometheus сервера

$ kubectl get svc
NAME                                  TYPE          CLUSTER-IP    EXTERNAL-IP   PORT(S)                     AGE
grafana-grafana                       NodePort      10.11.252.216 <none>        80:31886/TCP                22m
kubernetes                            ClusterIP     10.11.240.1   <none>        443/TCP                     22d
nginx-nginx-ingress-controller        LoadBalancer  10.11.243.76  104.154.94.52 80:32293/TCP,443:30193/TCP  7h
nginx-nginx-ingress-default-backend   ClusterIP     10.11.248.132 <none>        80/TCP                      7h
prom-prometheus-server                LoadBalancer  10.11.247.75  35.224.121.85 80:30282/TCP                4d

Добавим самый распространенный dashboard для отслеживания состояния ресурсов k8s. Выберем datasource: Selecting a Prometheus data source

Grafana Dashboard: Kubernetes cluster monitoring

Добавим собственные дашборды, созданные ранее (в ДЗ по мониторингу). Они должны также успешно отобразить данные:

Grafana Dashboard: Docker and system monitoring

Templating

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

Сделать это можно в нашем случае с помощью механизма templating’а Dashboard Templating

У нас появился список со значениями переменной namespace. Пока что он бесполезен. Чтобы их использование имело эффект нужно шаблонизировать запросы к Prometheus.

Dashboard Templating

Теперь мы можем настраивать общие шаблоны графиков и с помощью переменных менять в них нужные нам поля (в нашем случае это namespace). Dashboard Templating: All Namespaces Dashboard Templating: Only Production Namespace

Параметризуем все Dashboard’ы, отражающие параметры работы приложения (созданные в предыдущих ДЗ) reddit для работы с несколькими окружениями (неймспейсами).

Смешанные графики

Импортируем дашбоард: https://grafana.com/dashboards/741.

На этом графике одновременно используются метрики и шаблоны из cAdvisor, и из kube-state-metrics для отображения сводной информации по деплойментам.

Kubernetes Deployment metrics dashboards

Homework 24 (kubernetes-4)

Build Status

Helm

Helm - пакетный менеджер для Kubernetes. С его помощью мы будем:

  1. Стандартизировать поставку приложения в Kubernetes
  2. Декларировать инфраструктуру
  3. Деплоить новые версии приложения

Helm - клиент-серверное приложение. Установим его клиентскую часть - консольный клиент Helm:

$ brew install kubernetes-helm

Helm читает конфигурацию kubectl (~/.kube/config) и определяет текущий контекст (кластер, пользователь, namespace). Для смены кластера: $ kubectl config set-context, либо догрузка helm’у собственного config-файла с флагом --kube-context.

Установим серверную часть Helm’а - Tiller. Tiller - это аддон Kubernetes, т.е. Pod, который общается с API Kubernetes. Для этого понадобится ему выдать ServiceAccount и назначить роли RBAC, необходимые для работы.

Создадим tiller.yml и поместим в него манифест:

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

Запуск tiller-сервера: $ helm init --service-account tiller

Проверка: $ kubectl get pods -n kube-system --selector app=helm

output

NAME                             READY   STATUS    RESTARTS   AGE
tiller-deploy-689d79895f-tmhlp   1/1     Running   0          51s

Charts

Chart - это пакет в Helm.

Создадим директорию Charts в папке kubernetes со следующей структурой директорий:

└─ Charts
   ├─ comment
   ├─ post
   ├─ reddit
   └─ ui

Начнем разработку Chart’а для компоненты ui приложения.

Создадим файл-описание chart’а:

ui/Chart.yaml

---
name: ui
version: 1.0.0
description: OTUS reddit application UI
maintainers:
  - name: Vyacheslav Egorov
    email: [email protected]
appVersion: 1.0 

Значимыми являются поля name и version. От них зависит работа Helm’а с Chart’ом. Остальное - описания.

Templates

Основным содержимым Chart’ов являются шаблоны манифестов Kubernetes.

  1. Создадим директорию ui/templates
  2. Перенесем в неё все манифесты, разработанные ранее для сервиса ui (ui-service, ui-deployment, ui-ingress)
  3. Переименуем манифесты (уберем префикс “ui-“) и поменяем расширение на .yaml) - стилистические правки
└── ui
    ├── Chart.yaml
    └── templates
        ├── deployment.yaml
        ├── ingress.yaml
        └── service.yaml

По-сути, это уже готовый пакет для установки в Kubernetes.

  1. Убедимся, что у нас не развернуты компоненты приложения в kubernetes. Если развернуты - удалим их
kubectl delete service ui -n dev
kubectl delete deploy ui -n dev
kubectl delete ingress ui -n dev
  1. Установим Chart: helm install --name test-ui-1 ui/

здесь test-ui-1 - имя релиза;

ui/ - путь до Chart'а.

  1. Посмотрим, что получилось: helm ls

output

NAME      REVISION  UPDATED                    STATUS   CHART     APP VERSION  NAMESPACE
test-ui-1 1         Wed Jan 30 21:38:50 2019   DEPLOYED ui-1.0.0  1            default

Теперь сделаем так, чтобы можно было использовать 1 Chart для запуска нескольких экземпляров (релизов). Шаблонизируем его:

ui/templates/service.yaml

---
apiVersion: v1
kind: Service
metadata:
  name: {{ .Release.Name }}-{{ .Chart.Name }}
  labels:
    app: reddit
    component: ui
    release: {{ .Release.Name }}
spec:
  type: NodePort
  ports:
  - port: 9292
    protocol: TCP
    targetPort: 9292
  selector:
    app: reddit
    component: ui
    release: {{ .Release.Name }}
ui/templates/service.yaml

---
apiVersion: v1
kind: Service
metadata:
  name: {{ .Release.Name }}-{{ .Chart.Name }}  # уникальное имя запущенного ресурса
  labels:
    app: reddit
    component: ui
    release: {{ .Release.Name }}  # помечаем, что сервис из конкретного релиза
spec:
  type: NodePort
  ports:
  - port: {{ .Values.service.externalPort }}
    protocol: TCP
    targetPort: 9292
  selector:
    app: reddit
    component: ui
    release: {{ .Release.Name }} # Выбираем POD-ы только из этого релиза

name: {{ .Release.Name }}-{{ .Chart.Name }}

Здесь мы используем встроенные переменные:

  • .Release - группа переменных с информацией о релизе (конкретном запуске Chart’а в k8s)
  • .Chart - группа переменных с информацией о Chart’е (содержимое файла Chart.yaml)

Также еще есть группы переменных:

  • .Template - информация о текущем шаблоне (.Name и .BasePath)
  • .Capabilities - информация о Kubernetes (версия, версии API)
  • .Files.Get - получить содержимое файла

Шаблонизируем подобным образом остальные сущности

ui/templates/deployment.yaml

---
apiVersion: apps/v1beta1
kind: Deployment
metadata:
  name: {{ .Release.Name }}-{{ .Chart.Name }}
  labels:
    app: reddit
    component: ui
    release: {{ .Release.Name }}
spec:
...
  selector:
    matchLabels:
      app: reddit       # Важно, чтобы selector deployment’а
      component: ui     # нашел только нужные POD’ы
      release: {{ .Release.Name }}
  template:
    metadata:
      name: ui-pod
      labels:
        app: reddit
        component: ui
        release: {{ .Release.Name }}  
    spec:
      containers:
      - image: ozyab/ui
        name: ui
        ports:
        - containerPort: 9292
          name: ui
          protocol: TCP
        env:
        - name: ENV
          valueFrom:
            fieldRef:
              fieldPath: metadata.namespace
ui/templates/ingress.yaml

---
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: {{ .Release.Name }}-{{ .Chart.Name }}
  annotations:
    kubernetes.io/ingress.class: "gce"
spec:
  rules:
  - http:
      paths:
      - path: /*
        backend:
          serviceName: {{ .Release.Name }}-{{ .Chart.Name }}
          servicePort: 9292

Установим несколько релизов ui:

$ helm install ui --name ui-1 
$ helm install ui --name ui-2
$ helm install ui --name ui-3

Должны появиться 3 ингресса: $ kubectl get ingress

output

NAME      HOSTS   ADDRESS          PORTS   AGE
ui-1-ui   *       35.201.126.86    80      5m
ui-2-ui   *       35.201.67.17     80      1m
ui-3-ui   *       35.227.242.231   80      1m

Мы уже сделали возможность запуска нескольких версий приложений из одного пакета манифестов, используя лишь встроенные переменные. Кастомизируем установку своими переменными (образ и порт).

ui/templates/deployment.yaml

---
apiVersion: apps/v1beta1
kind: Deployment
metadata:
  name: {{ .Release.Name }}-{{ .Chart.Name }}
...
    spec:
      containers:
      - image: "{{ .Values.image.repository }}/ui:{{ .Values.image.tag }}"
        name: ui
        ports:
        - containerPort:  {{ .Values.service.internalPort }} 
ui/templates/service.yaml

---
apiVersion: v1
kind: Service
metadata:
...
spec:
  type: NodePort
  ports:
  - port: {{ .Values.service.externalPort }} 
    protocol: TCP
    targetPort: {{ .Values.service.internalPort }} 
  selector:
    app: reddit
    component: ui
    release: {{ .Release.Name }}
ui/templates/ingress.yaml

---
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: {{ .Release.Name }}-{{ .Chart.Name }}
  annotations:
    kubernetes.io/ingress.class: "gce"
spec:
  rules:
  - http:
      paths:
      - path: /
        backend:
          serviceName: {{ .Release.Name }}-{{ .Chart.Name }}
          servicePort: {{ .Values.service.externalPort }}

Определим значения собственных переменных в файле values.yaml:

ui/values.yaml

---
service:
  internalPort: 9292
  externalPort: 9292
image:
  repository: ozyab/ui
  tag: latest

Можно произвести обновление сервиса:

helm upgrade ui-1 ui/ 
helm upgrade ui-2 ui/ 
helm upgrade ui-3 ui/ 

Мы собрали Chart для развертывания ui-компоненты приложения. Он имеет следующую структуру:

└── ui
    ├── Chart.yaml
    ├── templates
    │   ├── deployment.yaml
    │   ├── ingress.yaml
    │   └── service.yaml
    └── values.yaml 

Соберем пакеты для остальных компонент:

post/templates/service.yaml

---
apiVersion: v1
kind: Service
metadata:
  name:  {{ .Release.Name }}-{{ .Chart.Name }}
  labels:
    app: reddit
    component: post
    release: {{ .Release.Name }} 
spec:
  ports:
  - port:  {{ .Values.service.externalPort }} 
    protocol: TCP
    targetPort: {{ .Values.service.internalPort }} 
  selector:
    app: reddit
    component: post
    release: {{ .Release.Name }}
post/templates/deployment.yaml 

---
apiVersion: apps/v1beta2
kind: Deployment
metadata:
  name: {{ .Release.Name }}-{{ .Chart.Name }}
  labels:
    app: reddit
    component: post
    release: {{ .Release.Name }}
spec:
  replicas: 1
  selector:
    matchLabels:
      app: reddit
      component: post
      release: {{ .Release.Name }}
  template:
    metadata:
      name: post
      labels:
        app: reddit
        component: post
        release: {{ .Release.Name }}
    spec:
      containers:
      - image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
        name: post
        ports:
        - containerPort: {{ .Values.service.internalPort }}
          name: post
          protocol: TCP
        env:
        - name: POST_DATABASE_HOST
          value: postdb

Обратим внимание на адрес БД:

env:
  - name: POST_DATABASE_HOST
    value: postdb

Поскольку адрес БД может меняться в зависимости от условий запуска:

  • бд отдельно от кластера
  • бд запущено в отдельном релизе
  • ... Создадим удобный шаблон для задания адреса БД.
env:
  - name: POST_DATABASE_HOST
   value: {{ .Values.databaseHost }}

Будем задавать бд через переменную databaseHost. Иногда лучше использовать подобный формат переменных вместо структур database.host, так как тогда прийдется определять структуру database, иначе helm выдаст ошибку.

Используем функцию default. Если databaseHost не будет определена или ее значение будет пустым, то используется вывод функции printf (которая просто формирует строку <имярелиза>-mongodb).

value: {{ .Values.databaseHost | default (printf "%s-mongodb" .Release.Name) }} 

В итоге получится:

env:
  - name: POST_DATABASE_HOST
    value: {{ .Values.databaseHost | default (printf "%s-mongodb" .Release.Name) }}

Если databaseHost не задано, то будет использован адрес базы, поднятой внутри релиза.

Документация по шаблонизациям и функциям.

post/values.yaml

---
service:
  internalPort: 5000
  externalPort: 5000

image:
  repository: ozyab/post
  tag: latest

databaseHost:

Шаблонизируем сервис comment

comment/templates/deployment.yaml

---
apiVersion: apps/v1beta2
kind: Deployment
metadata:
  name: {{ .Release.Name }}-{{ .Chart.Name }}
  labels:
    app: reddit
    component: comment
    release: {{ .Release.Name }}
spec:
  replicas: 1
  selector:
    matchLabels:
      app: reddit
      component: comment
      release: {{ .Release.Name }}
  template:
    metadata:
      name: comment
      labels:
        app: reddit
        component: comment
        release: {{ .Release.Name }}
    spec:
      containers:
      - image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
        name: comment
        ports:
        - containerPort: {{ .Values.service.internalPort }}
          name: comment
          protocol: TCP
        env:
        - name: COMMENT_DATABASE_HOST
          value: {{ .Values.databaseHost | default (printf "%s-mongodb" .Release.Name) }}
comment/templates/service.yaml

---
apiVersion: v1
kind: Service
metadata:
  name: {{ .Release.Name }}-{{ .Chart.Name }}
  labels:
    app: reddit
    component: comment
    release: {{ .Release.Name }}
spec:
  type: ClusterIP
  ports:
  - port: {{ .Values.service.externalPort }}
    protocol: TCP
    targetPort: {{ .Values.service.internalPort }}
  selector:
    app: reddit
    component: comment
    release: {{ .Release.Name }}
post/values.yaml

---
service:
  internalPort: 5000
  externalPort: 5000

image:
  repository: ozyab/post
  tag: latest

databaseHost:

Итоговая структура проекта выглядит так:

$ tree
.
├── comment
│   ├── Chart.yaml
│   ├── templates
│   │   ├── deployment.yml
│   │   └── service.yml
│   └── values.yaml
├── post
│   ├── Chart.yaml
│   ├── templates
│   │   ├── deployment.yaml
│   │   └── service.yml
│   └── values.yaml
└── ui
    ├── Chart.yaml
    ├── templates
    │   ├── deployment.yaml
    │   ├── ingress.yaml
    │   └── service.yaml
    └── values.yaml

Также стоит отметить функционал helm по использованию helper’ов и функции templates. Helper - это написанная нами функция. В функция описывается, как правило, сложная логика. Шаблоны этих функция распологаются в файле _helpers.tpl.

Пример функции comment.fullname:

charts/comment/templates/_helpers.tpl

{{- define "comment.fullname" -}}
{{- printf "%s-%s" .Release.Name .Chart.Name }}
{{- end -}}

которая в результате выдаст то же, что и:

{{ .Release.Name }}-{{ .Chart.Name }}

Заменим в соответствующие строчки в файле, чтобы использовать helper:

charts/comment/templates/service.yaml

apiVersion: v1
kind: Service
metadata:
  name: {{ template "comment.fullname" . }}  # было:  {{ .Release.Name }}-{{ .Chart.Name }}

с помощью template вызывается функция comment.fullname, описанная ранее в файле _helpers.tpl.

Структура ипортирующей функции template: {{ template "comment.fullname" . }} где template - функция template

  • comment.fullname - название функции для импорта
  • “.” - область видимости для импорта

“.” - вся область видимости всех переменных (можно передать .Chart, тогда .Values не будут доступны внутри функции)

  • Создадим файл _helpers.tpl в папках templates сервисов ui, post и comment
  • Вставиим функцию “.fullname” в каждый _helpers.tpl файл. заменить на имя чарта соответсвующего сервиса
  • В каждом из шаблонов манифестов вставим следующую функцию там, где это требуется (большинство полей это name:) {{ template "comment.fullname" . }}

Управление зависимостями

Мы создали Chart’ы для каждой компоненты нашего приложения. Каждый из них можно запустить по-отдельности командой $ helm install <chart-path> <release-name>, но они будут запускаться в разных релизах, и не будут видеть друг друга.

С помощью механизма управления зависимостями создадим единый Chart reddit, который объединит наши компоненты.

Структура приложения reddit: Reddit app structure

Создадим файл:

reddit/Chart.yaml

name: reddit
version: 0.1.0
description: OTUS sample reddit application
maintainers:
  - name: Vyacheslav Egorov
    email: [email protected]

Создадим пустой файл reddit/values.yaml.

Файл с зависимотями:

reddit/requirements.yaml

dependencies:
  - name: ui            # Имя и версия должны совпадать
    version: "1.0.0"     # с содеражанием ui/Chart.yml
    repository: "file://../ui"  # Путь относительно расположения самого requiremetns.yml
  - name: post
    version: 1.0.0
    repository: file://../post
  - name: comment
    version: 1.0.0
    repository: file://../comment

Нужно загрузить зависимости (когда Chart’ не упакован в tgz архив):

$ helm dep update 

Будет создан файл requirements.lock с фиксацией зависисмостей. Также будет создана директория с зависимостями в виде архивов.

Струкрута папок:

├── Chart.yaml
├── charts
│   ├── comment-1.0.0.tgz
│   ├── post-1.0.0.tgz
│   └── ui-1.0.0.tgz
├── requirements.lock
├── requirements.yaml
└── values.yaml

Chart для базы данных не будем создавать вручную. Возьмем готовый.

Найдем Chart в общедоступном репозитории: $ helm search mongo

output

NAME                       CHART VERSION   APP VERSION  DESCRIPTION
stable/mongodb             5.3.1           4.0.5        NoSQL document-oriented database that stores JSON-like do...
stable/mongodb-replicaset  3.9.0           3.6          NoSQL document-oriented database that stores JSON-like do...

Добавим в reddit/requirements.yml:

reddit/requirements.yml

dependencies:
...
  - name: comment
    version: 1.0.0
    repository: file://../comment
  - name: mongodb
    version: 0.4.18
    repository: https://kubernetes-charts.storage.googleapis.com

Выгрузим зависимости: helm dep update

Установим приложение: kubernetes/Charts $ helm install reddit --name reddit-test

Найдем IP-адрес приложения: kubectl get ingress

NAME             HOSTS   ADDRESS          PORTS   AGE
reddit-test-ui   *       35.201.126.86    80      1m

Charts-based app runned

Есть проблема с тем, что UI-сервис не знает как правильно ходить в post и comment сервисы. Ведь их имена теперь динамические и зависят от имен чартов.

В Dockerfile UI-сервиса уже заданы переменные окружения. Надо, чтобы они указывали на нужные бекенды:

ENV POST_SERVICE_HOST post
ENV POST_SERVICE_PORT 5000
ENV COMMENT_SERVICE_HOST comment
ENV COMMENT_SERVICE_PORT 9292

Добавим в ui/deployments.yaml:

ui/deployments.yaml

...
spec:
...
    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 }}
# quote - функция для добавления кавычек. Для чисел и булевых значений это важно
...

Добавим в ui/values.yaml (ссылка на gist)

ui/values.yaml
...
postHost:
postPort:
commentHost:
commentPort: 

Тепреь можно задавать переменные для зависимостей прямо в values.yaml самого Chart’а reddit. Они перезаписывают значения переменных из зависимых чартов:

comment: # ссылаемся на переменные чартов из зависимостей
  image:
    repository: ozyab/comment
    tag: latest
  service:
    externalPort: 9292

post:
  image:
    repository: ozyab/post
    tag: latest
    service:
      externalPort: 5000

ui:
  image:
    repository: ozyab/ui
    tag: latest
    service:
      externalPort: 9292

После обновления UI - нужно обновить зависимости чарта reddit: $ helm dep update ./reddit

Обновим релиз, установленный в k8s: $ helm upgrade <release-name> ./reddit

Charts-based app runned

GitLab + Kubernetes

  • Установим Gitlab

Gitlab будем ставить также с помощью Helm Chart’а из пакета Omnibus.

  • Добавим репозиторий Gitlab $ helm repo add gitlab https://charts.gitlab.io
  • Мы будем менять конфигурацию Gitlab, поэтому скачаем Chart
$ helm fetch gitlab/gitlab-omnibus --version 0.1.37 --untar
$ cd gitlab-omnibus
  • Попраим gitlab-omnibus/values.yaml
baseDomain: example.com
legoEmail: [email protected]
  • Добавьте в gitlab-omnibus/templates/gitlab/gitlab-svc.yaml:
...
    - name: web
      port: 80
      targetPort: workhorse
  • Поправить в gitlab-omnibus/templates/gitlab-config.yaml:
...
    heritage: "{{ .Release.Service }}"
data:
  external_scheme: http
  external_hostname: {{ template "fullname" . }}
...
  • Поправим в gitlab-omnibus/templates/ingress/gitlab-ingress.yaml:
apiVersion: extensions/v1beta1
kind: Ingress
...
spec:
  tls:
...
  rules:
  - host: {{ template "fullname" . }}
    http:
      paths:
...

Установим GitLab: $ helm install --name gitlab . -f values.yaml

Найдем выданный IP-адрес ingress-контроллера: $ kubectl get service -n nginx-ingress nginx

Поместим запись в локальный /etc/hosts: # echo "35.184.43.93 gitlab-gitlab staging production" >> /etc/hosts

Идем по адресу http://gitlab-gitlab и ставим собственный пароль.

Запустим проект

Создадим группу ozyab. В настройках группы выберем CI/CD и настроим переменные CI_REGISTRY_USER и CI_REGISTRY_PASSWORD - логин и пароль от dockerhub. Эти учетные данные будут использованы при сборке и релизе docker-образов с помощью Gitlab CI.

В группе создадим новый проект reddit-deploy, а также comment, post и ui.

Локально создадим у себя директорию Gitlab_ci со следующей структурой:

Gitlab_ci
├── comment
├── post
├── reddit-deploy
└── ui

Перенесем исходные коды сервисов из src/ в kubernetes/Gitlab_ci/ui.

В директории Gitlab_ci/ui:

  • Инициализируем локальный git-репозиторий: $ git init
  • Добавим удаленный репозиторий $ git remote add origin http://gitlab-gitlab/ozyab/ui.git
  • Закоммитим и отправим в gitlab:
$ git add .
$ git commit -m "init"
$ git push origin master

Перенесем содержимое директории Charts (папки ui, post, comment, reddit) в Gitlab_ci/reddit-deploy и запушим в reddit-deploy.

Настроим CI

Добавим файл .gitlab-ci.yml, запушим его и проверим, что сборка успешна.

Building project UI in giclab-ci

В текущей конфигурации CI выполняет:

  • Build: Сборку докер-образа с тегом master
  • Test: Фиктивное тестирование
  • Release: Смену тега с master на тег из файла VERSION и пуш docker-образа с новым тегом

Job для выполнения каждой задачи запускается в отдельном Kubernetes POD-е.

Требуемые операции вызываются в блоках script:

 script:
 - setup_docker
 - build 

Описание самих операций производится в виде bash-функций в блоке .auto_devops:

.auto_devops: &auto_devops |
 function setup_docker() {
…
 }
 function release() {
…
 }
 function build() {
…
}

Для Post и Comment также добавим в репозиторий .gitlab-ci.yml.

Дадим возможность разработчику запускать отдельное окружение в Kubernetes по коммиту в feature-бранч.

Немного обновим конфиг ингресса для сервиса UI:

reddit-deploy/ui/templates/ingress.yml

...
  name: {{ template "ui.fullname" . }}
  annotations:
    kubernetes.io/ingress.class: {{ .Values.ingress.class }}
spec:
  rules:
  - host: {{ .Values.ingress.host | default .Release.Name }}
    http:
      paths:
      - path: /  # В качестве контроллера nginx, поэтому правило другое
        backend:
          serviceName: {{ template "ui.fullname" . }}
          servicePort: {{ .Values.service.externalPort }}
reddit-deploy/ui/values.yml

...
ingress:
  class: nginx # Будем использовать nginx-ingress, который был поставлен вместе с gitlab-ом
...            # (так быстрее и правила более гибкие, чем у GCP)
  • Создадим новый бранч в репозитории ui $ git checkout -b feature/3
  • Обновим ui/.gitlab-ci.yml
  • Закоммитим и запушим изменения:
$ git commit -am "Add review feature"
$ git push origin feature/3

Мы добавили стадию review, запускающую приложение в k8s по коммиту в feature-бранчи (не master):

review:
  stage: review
  script:
    - install_dependencies
    - ensure_namespace
    - install_tiller
    - deploy
  variables:
    KUBE_NAMESPACE: review
    host: $CI_PROJECT_PATH_SLUG-$CI_COMMIT_REF_SLUG
  environment:
    name: review/$CI_PROJECT_PATH/$CI_COMMIT_REF_NAME
    url: http://$CI_PROJECT_PATH_SLUG-$CI_COMMIT_REF_SLUG
  only:
    refs:
      - branches
    kubernetes: active
  except:
    - master

Мы добавили функцию deploy, которая загружает Chart из репозитория reddit-deploy и делает релиз в неймспейсе review с образом приложения, собранным на стадии build:

  function deploy() {
  ...
    echo "Clone deploy repository..."
    git clone http://gitlab-gitlab/$CI_PROJECT_NAMESPACE/reddit-deploy.git

    echo "Download helm dependencies..."
    helm dep update reddit-deploy/reddit

    echo "Deploy helm release $name to $KUBE_NAMESPACE"
    helm upgrade --install \
      --wait \
      --set ui.ingress.host="$host" \
      --set $CI_PROJECT_NAME.image.tag=$CI_APPLICATION_TAG \
      --namespace="$KUBE_NAMESPACE" \
      --version="$CI_PIPELINE_ID-$CI_JOB_ID" \
      "$name" \
      reddit-deploy/reddit/
  }

Можем увидеть какие релизы запущены helm ls:

NAME                           REVISION   UPDATED                   STATUS    CHART                  NAMESPACE
gitlab                         1          Sun Feb  3 18:07:41 2019  DEPLOYED  gitlab-omnibus-0.1.37  default
review-ozyab-ui-f-92dwpg       1          Sun Feb  3 19:42:54 2019  DEPLOYED  reddit-0.1.0           review

Созданные для таких целей окружения временны, их требуется “убивать”, когда они больше не нужны. Добавим в .gitlab-ci.yml:

stop_review:
  stage: cleanup
  variables:
    GIT_STRATEGY: none
  script:
    - install_dependencies
    - delete
  environment:
    name: review/$CI_PROJECT_PATH/$CI_COMMIT_REF_NAME
    action: stop
  when: manual
  allow_failure: true
  only:
    refs:
      - branches
    kubernetes: active
  except:
    - master

Добавим функцию удаления окружения:

function delete() {
    track="${1-stable}"
    name="$CI_ENVIRONMENT_SLUG"
    helm delete "$name" --purge || true
  }

Pepiline of feature/3 branch Удалим окружение нажав на кнопку выше. helm ls:

NAME    REVISION   UPDATED                   STATUS    CHART                  NAMESPACE
gitlab  1          Sun Feb  3 18:07:41 2019  DEPLOYED  gitlab-omnibus-0.1.37  default

Скопируем полученный файл .gitlab-ci.yml для ui в репозитории для post и comment.

Теперь создадим staging и production среды для работы приложения в файл reddit-deploy/.gitlab-ci.yml

Запушим в репозиторий reddit-deploy ветку master

Этот файл отличается от предыдущих тем, что:

  • Не собирает docker-образы
  • Деплоит на статичные окружения (staging и production)
  • Не удаляет окружения

Staging environment

Homework 23 (kubernetes-3)

Build Status

Service - абстракции, определяющей конечные узлы доступа (Endpoint’ы) и способ коммуникации с ними (nodePort, LoadBalancer, ClusterIP).

Service - определяет конечные узлы доступа (Endpoint’ы):

  • селекторные сервисы (k8s сам находит POD-ы по label’ам)
  • безселекторные сервисы (мы вручную описываем конкретные endpoint’ы)

и способ коммуникации с ними (тип (type) сервиса):

  • ClusterIP - дойти до сервиса можно только изнутри кластера
  • nodePort - клиент снаружи кластера приходит на опубликованный порт
  • LoadBalancer - клиент приходит на облачный (aws elb, Google gclb) ресурс балансировки
  • ExternalName - внешний ресурс по отношению к кластеру

Ранее был описан service:

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

Это селекторный сервис типа ClusetrIP (тип не указан, т.к. этот тип по-умолчанию).

ClusterIP - это виртуальный (в реальности нет интерфейса, pod’а или машины с таким адресом) IP-адрес из диапазона адресов для работы внутри, скрывающий за собой IP-адреса реальных POD-ов. Сервису любого типа (кроме ExternalName) назначается этот IP-адрес.

$ kubectl get services -n dev

output

NAME         TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)          AGE
comment      ClusterIP   10.11.248.46    <none>        9292/TCP         2d
comment-db   ClusterIP   10.11.252.202   <none>        27017/TCP        2d
mongo        ClusterIP   10.11.243.229   <none>        27017/TCP        2d
post         ClusterIP   10.11.254.105   <none>        5000/TCP         2d
post-db      ClusterIP   10.11.240.87    <none>        27017/TCP        2d
ui           NodePort    10.11.245.7     <none>        9292:32092/TCP   2d

Схема взаимодействия: Service interaction scheme

Service - это лишь абстракция и описание того, как получить доступ к сервису. Но опирается она на реальные механизмы и объекты: DNS-сервер, балансировщики, iptables.

Для того, чтобы дойти до сервиса, нам нужно узнать его адрес по имени. Kubernetes не имеет своего собственного DNS-сервера для разрешения имен. Поэтому используется плагин kube-dns (это тоже Pod).

Его задачи:

  • ходить в API Kubernetes’a и отслеживать Service-объекты
  • заносить DNS-записи о Service’ах в собственную базу
  • предоставлять DNS-сервис для разрешения имен в IP-адреса (как внутренних, так и внешних)

Service interaction scheme with kude-dns

При отключенном kube-dns сервисе связность между компонентами reddit-app пропадет и он перестанет работать.

Проскейлим в 0 сервис, который следит, чтобы dns-kube подов всегда хватало. Аналогично поступим с kube-dns:

$ kubectl scale deployment --replicas 0 -n kube-system kube-dns-autoscaler
$ kubectl scale deployment --replicas 0 -n kube-system kube-dns

Выполним команду:

kubectl exec -ti -n dev post-8ff9c4cb9-h4zpq ping comment

output:

ping: bad address 'comment'
command terminated with exit code 1

Вернем автоскеллер:

kubectl scale deployment --replicas 1 -n kube-system kube-dns-autoscaler

ClusterIP - виртуальный и не принадлежит ни одной реальной физической сущности. Его чтением и дальнейшими действиями с пакетами, принадлежащими ему, занимается в нашем случае iptables, который настраивается утилитой kube-proxy (забирающей инфу с API-сервера).

Сам kube-proxy, можно настроить на прием трафика, но это устаревшее поведение и не рекомендуется его применять.

Service interaction scheme using kube-dns

Независимо от того, на одной ноде находятся поды или на разных - трафик проходит через цепочку, изображенную выше. Kubernetes не имеет в комплекте механизма организации overlay сетей (как у Docker Swarm). Он лишь предоставляет интерфейс для этого. Для создания Overlay-сетей используются отдельные аддоны: Weave, Calico, Flannel, … .

В Google Kontainer Engine (GKE) используется собственный плагин kubenet (он - часть kubelet). Он работает только вместе с платформой GCP и, по-сути занимается тем, что настраивает google-сети для передачи трафика Kubernetes. Поэтому в конфигурации Docker сейчас вы не увидите никаких Overlay-сетей.

Service interaction scheme using kubenet

  • NodePort - похож на сервис типа ClusterIP, только к нему прибавляется прослушивание портов нод (всех нод) для доступа к сервисам снаружи. При этом ClusterIP также назначается этому сервису для доступа к нему изнутри кластера. kube-proxy прослушивается либо заданный порт (nodePort: 32092), либо порт из диапазона 30000-32670. Дальше IPTables решает, на какой Pod попадет трафик.

Сервис UI раньше уже был опубликован наружу с помощью NodePort:

ui-service.yml

---
apiVersion: v1
kind: Service
metadata:
  name: ui
  labels:
    app: reddit
    component: ui
spec:
  type: NodePort
  ports:
  - port: 9292
    nodePort: 32092
    protocol: TCP
    targetPort: 9292
  selector:
    app: reddit
    component: ui

Service interaction scheme using nodePort

  • LoadBalancer

NodePort хоть и предоставляет доступ к сервису снаружи, но открывать все порты наружу или искать IPадреса наших нод (которые вообще динамические) не очень удобно.

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

Service interaction scheme using loadBalancer

Настроим соответствующим образом Service UI:

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 #Порт POD-а
  selector:
    app: reddit
    component: ui

Применим изменения: $ kubectl apply -f ui-service.yml -n dev

Проверим: $ kubectl get service -n dev --selector component=ui

output

NAME   TYPE           CLUSTER-IP    EXTERNAL-IP      PORT(S)        AGE
ui     LoadBalancer   10.11.245.7   35.222.133.XXX   80:32092/TCP   2d

Balancing rule in GCP Network services

Балансировка с помощью Service типа LoadBalancing имеет ряд недостатков:

  • нельзя управлять с помощью http URI (L7-балансировка)
  • используются только облачные балансировщики (AWS, GCP)
  • нет гибких правил работы с трафиком
  • Ingress

Для более удобного управления входящим снаружи трафиком и решения недостатков LoadBalancer можно использовать другой объект Kubernetes - Ingress.

Ingress – это набор правил внутри кластера Kubernetes, предназначенных для того, чтобы входящие подключения могли достичь сервисов (Services).

Сами по себе Ingress’ы это просто правила. Для их применения нужен Ingress Controller.

  • Ingress Conroller

В отличие остальных контроллеров k8s - он не стартует вместе с кластером.

Ingress Controller - это скорее плагин (а значит и отдельный POD), который состоит из 2-х функциональных частей:

  • Приложение, которое отслеживает через k8s API новые объекты Ingress и обновляет конфигурацию балансировщика
  • Балансировщик (Nginx, haproxy, traefik,…), который и занимается управлением сетевым трафиком

Основные задачи, решаемые с помощью Ingress’ов:

  • Организация единой точки входа в приложения снаружи
  • Обеспечение балансировки трафика
  • Терминация SSL
  • Виртуальный хостинг на основе имен и т.д

Посколько у нас web-приложение, нам вполне было бы логично использовать L7-балансировщик вместо Service LoadBalancer.

Google в GKE уже предоставляет возможность использовать их собственные решения балансирощик в качестве Ingress controller-ов.

Убедимся, что встроенный Ingress включен: Service interaction scheme using kubenet

Создадим Ingress для сервиса UI:

ui-ingress.yml

---
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: ui
spec:
  backend:
    serviceName: ui
    servicePort: 80

Это Singe Service Ingress - значит, что весь ingress контроллер будет просто балансировать нагрузку на Node-ы для одного сервиса (очень похоже на Service LoadBalancer).

$ kubectl apply -f ui-ingress.yml -n dev

New load balancer rule

Посмотрим в кластер: kubectl get ingress -n dev

output

NAME   HOSTS   ADDRESS         PORTS   AGE
ui     *       35.201.126.XXX   80      2m

Теперь схема выглядит так:

Service interaction scheme using kubenet

В текущей схеме есть несколько недостатков:

  • у нас 2 балансировщика для 1 сервиса
  • Мы не умеем управлять трафиком на уровне HTTP

Один из балансирощиков можно убрать. Обновим сервис UI:

ui-service.yml

---
apiVersion: v1
kind: Service
metadata:
  ...
spec:
  type: NodePort #заменим на NodePort
  ports:
  - port: 9292
    protocol: TCP
    targetPort: 9292
  selector:
    app: reddit
    component: ui

$ kubectl apply -f … -n dev для применения настроек.

Заставим работать Ingress Controller как классический веб:

ui-ingress.yml

---
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: ui
spec:
  rules:
  - http:
      paths:
      - path: /*
        backend:
          serviceName: ui
          servicePort: 9292

$ kubectl apply -f ui-ingress.yml -n dev для применения

Reddit app started using ingress balabcer based on HTTP proto

  • Secret

Защитим наш сервис с помощью TLS. Найдем наш IP: $ kubectl get ingress -n dev

output

NAME   HOSTS   ADDRESS         PORTS   AGE
ui     *       35.201.126.86   80      1d

Подготовим сертификат используя IP как CN:

$ openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout tls.key -out tls.crt -subj "/CN=35.201.126.86"

Загрузка сертификата в кластер:

$ kubectl create secret tls ui-ingress --key tls.key --cert tls.crt -n dev

Проверка наличия сертификата:

$ kubectl describe secret ui-ingress -n dev

output

Name:         ui-ingress
Namespace:    dev
Labels:       <none>
Annotations:  <none>

Type:  kubernetes.io/tls

Data
====
tls.crt:  989 bytes
tls.key:  1704 bytes

Настроим Ingress на прием только HTTPS трафика:

ui-ingress.yml

---
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: ui
  annotations:
    kubernetes.io/ingress.allow-http: "false" #отключаем проброс http
spec:
  tls:
  - secretName: ui-ingress #подключаем сертификат
  backend:
    serviceName: ui
    servicePort: 9292

$ kubectl apply -f ui-ingress.yml -n dev для применения.

Перейдем на страницу load-balancer'а:

Service interaction scheme using kubenet

Видим, что у нас все еще http load balacer. Вручную удалим и создадим load balancer:

$ kubectl delete ingress ui -n dev
$ kubectl apply -f ui-ingress.yml -n dev
  • Network Policy

Ранее мы приняли следующую схему сетей сервисов:

Service network scheme

В Kubernetes у нас так сделать не получится с помощью отдельных сетей, так как все POD-ы могут достучаться друг до друга по-умолчанию.

NetworkPolicy - инструмент для декларативного описания потоков трафика. Не все сетевые плагины поддерживают политики сети. В частности, у GKE эта функция пока в Beta-тесте и для её работы отдельно будет включен сетевой плагин Calico (вместо Kubenet).

Давайте ее протеструем. Наша задача - ограничить трафик, поступающий на mongodb отовсюду, кроме сервисов post и comment.

Найдем имя кластера:

$ gcloud beta container clusters list

output

NAME                LOCATION       MASTER_VERSION  MASTER_IP     MACHINE_TYPE  NODE_VERSION    NUM_NODES  STATUS
standard-cluster-1  us-central1-a  1.10.11-gke.1   35.202.73.52  g1-small      1.10.9-gke.5 *  2          RUNNING

Включим network-policy для GKE:

gcloud beta container clusters update standard-cluster-1 --zone=us-central1-a --update-addons=NetworkPolicy=ENABLED
gcloud beta container clusters update standard-cluster-1 --zone=us-central1-a  --enable-network-policy

Создадим network policy для mongo:

mongo-network-policy.yml

---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: deny-db-traffic
  labels:
    app: reddit
spec:
  podSelector:    # выбираем объекты политики (POD'ы с mongodb)
    matchLabels:
      app: reddit
      component: mongo
  policyTypes:    # блок запрещающих направлений: Запрещаем все входящие подключения
  - Ingress       # Исходящие разрешены
  ingress:        # блок разрешающих направлений
  - from:         # (белый список)
    - podSelector:    
        matchLabels:
          app: reddit         # Разрешаем все входящие подключения от
          component: comment  # POD-ов с label-ами comment

Применяем политику: $ kubectl apply -f mongo-network-policy.yml -n dev

Для доступа post-сервиса в базу данных добавим:

mongo-network-policy.yml
...
- podSelector:
  matchLabels:
    app: reddit
    component: post
  • Хранилище для баз данных

Основной Stateful сервис в нашем приложении - это база данных MongoDB. В текущий момент она запускается в виде Deployment и хранит данные в стаднартный Docker Volume-ах. Это имеет несколько проблем:

  • при удалении POD-а удаляется и Volume
  • потеря Nod’ы с mongo грозит потерей данных
  • запуск базы на другой ноде запускает новый экземпляр данных
mongo-deployment.yml

---
apiVersion: apps/v1beta1
kind: Deployment
metadata:
  name: mongo
...
    spec:
      containers:
      - image: mongo:3.2
        name: mongo
        volumeMounts:   # подключаем Volume
        - name: mongo-persistent-storage
          mountPath: /data/db
      volumes:
      - name: mongo-persistent-storage # объявляем Volume
        emptyDir: {}

Сейчас используется тип Volume emptyDir. При создании пода с таким типом просто создается пустой docker volume. При остановке POD’a содержимое emtpyDir удалится навсегда. Хотя в общем случае падение POD’a не вызывает удаления Volume’a. Вместо того, чтобы хранить данные локально на ноде, имеет смысл подключить удаленное хранилище. В нашем случае можем использовать Volume gcePersistentDisk, который будет складывать данные в хранилище GCE.

Создадим диск в Google Cloud:

$ gcloud compute disks create --size=25GB --zone=us-central1-a reddit-mongo-disk

Добавим новый Volume POD-у базы:

mongo-deployment.yml

---
apiVersion: apps/v1beta1
kind: Deployment
    ...
    spec:
      containers:
      - image: mongo:3.2
        name: mongo
        volumeMounts:
        - name: mongo-gce-pd-storage
          mountPath: /data/db
      volumes:
      - name: mongo-persistent-storage
        emptyDir: {}
        volumes:
      - name: mongo-gce-pd-storage
        gcePersistentDisk:
          pdName: reddit-mongo-disk # меняем Volume на другой тип
          fsType: ext4

Монтируем выделенный диск к POD'у mongo:

Scheme of mounting dedidcated disk to mongo's pod

kubectl apply -f mongo-deployment.yml -n dev

Дождемся пересоздания POD'a (может занять до 10 минут).

Создадим пост:

New posts in redit app using dedicated disk in mongodb

Пересоздадим mongo-deployment:

$ kubectl delete deploy mongo -n dev
$ kubectl apply -f mongo-deployment.yml -n dev

Посты останутся на месте.

  • PersistentVolume

Используемый механизм Volume-ов можно сделать удобнее. Мы можем использовать не целый выделенный диск для каждого пода, а целый ресурс хранилища, общий для всего кластера. Тогда при запуске Stateful-задач в кластере, мы сможем запросить хранилище в виде такого же ресурса, как CPU или оперативная память. Для этого будем использовать механизм PersistentVolume.

Описание PersistentVolume:

mongo-volume.yml

---
apiVersion: v1
kind: PersistentVolume
metadata:
  name: reddit-mongo-disk # Имя PersistentVolume'а
spec:
  capacity:
    storage: 25Gi
  accessModes:
    - ReadWriteOnce
  persistentVolumeReclaimPolicy: Retain
  gcePersistentDisk:
    fsType: "ext4" 
    pdName: "reddit-mongo-disk" # Имя диска в GCE

Добавим PersistentVolume в кластер: $ kubectl apply -f mongo-volume.yml -n dev

Мы создали PersistentVolume в виде диска в GCP:

Persistent Volume as a disk in GCP

  • PersistentVolumeClaim

Мы создали ресурс дискового хранилища, распространенный на весь кластер, в виде PersistentVolume. Чтобы выделить приложению часть такого ресурса - нужно создать запрос на выдачу - PersistentVolumeClaim. Claim - это именно запрос, а не само хранилище.

С помощью запроса можно выделить место как из конкретного PersistentVolume (тогда параметры accessModes и StorageClass должны соответствовать, а места должно хватать), так и просто создать отдельный PersistentVolume под конкретный запрос.

Создадим описание PersistentVolumeClaim (PVC):

mongo-claim.yml

---
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
  name: mongo-pvc  # Имя PersistentVolumeClame'а
spec:
  accessModes:
    - ReadWriteOnce # accessMode у PVC и у PV должен совпадать
  resources:
    requests:
      storage: 15Gi

Применим: kubectl apply -f mongo-claim.yml -n dev

Мы выделили место в PV по запросу для нашей базы. Одновременно использовать один PV можно только по одному Claim’у.

Persistant Volume Claim inside Persistant Volume

Если Claim не найдет по заданным параметрам PV внутри кластера, либо тот будет занят другим Claim’ом то он сам создаст нужный ему PV воспользовавшись стандартным StorageClass.

$ kubectl describe storageclass standard -n dev 

output

Name:                  standard
IsDefaultClass:        Yes
Annotations:           storageclass.beta.kubernetes.io/is-default-class=true
Provisioner:           kubernetes.io/gce-pd
Parameters:            type=pd-standard
AllowVolumeExpansion:  <unset>
MountOptions:          <none>
ReclaimPolicy:         Delete
VolumeBindingMode:     Immediate
Events:                <none>

В нашем случае это обычный медленный Google Cloud Persistent Drive: Persistant Volume Claim created standart (slow hdd) volume

Подключим PVC к нашим Pod'ам:

mongo-deployment.yml 

---
apiVersion: apps/v1beta1
kind: Deployment
metadata:
  name: mongo
...
    spec:
      containers:
      - image: mongo:3.2
        name: mongo
        volumeMounts:
        - name: mongo-gce-pd-storage
          mountPath: /data/db
      volumes:
      - name: mongo-gce-pd-storage # Имя PersistentVolumeClame'а
        persistentVolumeClaim:
          claimName: mongo-pvc

Применим: $ kubectl apply -f mongo-deployment.yml -n dev

Монтируем выделенное по PVC хранилище к POD’у mongo: Mounting PVC dedicated storage to mongo's POD

  • Динамическое выделение Volume'ов

Создав PersistentVolume мы отделили объект "хранилища" от наших Service'ов и Pod'ов. Теперь мы можем его при необходимости переиспользовать.

Но нам гораздо интереснее создавать хранилища при необходимости и в автоматическом режиме. В этом нам помогут StorageClass’ы. Они описывают где (какой провайдер) и какие хранилища создаются.

Cоздадим StorageClass Fast так, чтобы монтировались SSD-диски для работы нашего хранилища:

storage-fast.yml

---
kind: StorageClass
apiVersion: storage.k8s.io/v1beta1
metadata:
  name: fast # Имя StorageClass'а
provisioner: kubernetes.io/gce-pd # Провайдер хранилища
parameters:
  type: pd-ssd # Тип предоставляемого хранилища

Добавим StorageClass в кластер: $ kubectl apply -f storage-fast.yml -n dev

  • PVC + StorageClass

Создадим описание PersistentVolumeClaim:

mongo-claim-dynamic.yml 

---
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
  name: mongo-pvc-dynamic
spec:
  accessModes:
    - ReadWriteOnce
  storageClassName: fast # Вместо ссылки на созданный диск, теперь 
  resources:               # мы ссылаемся на StorageClass
    requests:
      storage: 10Gi

Добавим StorageClass в кластер:

$ kubectl apply -f mongo-claim-dynamic.yml -n dev

Подключение динамического PVC:

mongo-deployment.yml
---
apiVersion: apps/v1beta1
kind: Deployment
metadata:
  name: mongo
...
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 #Обновим PersistentVolumeClaim

Обновим описание нашего Deployment'а: $ kubectl apply -f mongo-deployment.yml -n dev

Список полученных PersistentVolume'ов:

$ kubectl get persistentvolume -n dev

output

NAME                     CAPACITY  ACCESS MODES  RECLAIM POLICY   STATUS      CLAIM                   STORAGECLASS  AGE
pvc-4aa55cd3-2256-1..a   15Gi      RWO           Delete           Bound       dev/mongo-pvc           standard      23m
pvc-dcb0edd0-2258-1..a   10Gi      RWO           Delete           Bound       dev/mongo-pvc-dynamic   fast          5m
reddit-mongo-disk        25Gi      RWO           Retain           Available                                         28m
  • Status - статус PV по отношению к Pod'ам и Claim'ам (Bound - связанный, Availible - доступный)
  • Claim - к какому Claim'у привязан данный PV
  • StorageClass - StorageClass данного PV

Connecting dynamic PVC

Homework 22 (kubernetes-2)

Build Status

  • Для локальной разработки необходимо:
  • kubectl
  • директория ~/.kube
  • minikube:
brew cask install minikube

или

curl -Lo minikube https://storage.googleapis.com/minikube/releases/v0.27.0/
minikube-darwin-amd64 && chmod +x minikube && sudo mv minikube /usr/local/
bin/

Для OS X понадобится гипервизор xhyve driver, VirtualBox, или VMware Fusion.

  • Запуск Minicube-кластера: minikube start

Если нужна конкретная версия kubernetes, следует указывать флаг --kubernetes-version <version> (v1.8.0)

По-умолчанию используется VirtualBox. Если используется другой гипервизор, то необходим флаг --vm-driver=<hypervisor

  • Minikube-кластер развернут. Автоматически был настроен конфиг kubectl.

Проверим: kubectl get nodes

output:

NAME       STATUS   ROLES    AGE   VERSION
minikube   Ready    master   25s   v1.13.2
  • Манифест kubernetes в формате yml:
~/.kube/config

apiVersion: v1
clusters: ## список кластеров
- cluster:
    certificate-authority: ~/.minikube/ca.crt
    server: https://192.168.99.100:8443
  name: minikube
contexts: ## список контекстов
- context:
    cluster: minikube
    user: minikube
  name: minikube
current-context: minikube
kind: Config
preferences: {}
users: ## список пользователей
- name: minikube
  user:
    client-certificate: ~/.minikube/client.crt
    client-key: ~/.minikube/client.key
  • Обычный порядок конфигурирования kubectl:
  1. Создание cluster'a:
$ kubectl config set-cluster … cluster_name
  1. Создание данных пользователя (credentials):
$ kubectl config set-credentials … user_name
  1. Создание контекста:
$ kubectl config set-context context_name \
  --cluster=cluster_name \
  --user=user_name
  1. Использование контекста:
$ kubectl config use-context context_name

Таким образом, kubectl конфигурируется для подключения к разным кластерам, под разными пользователями.

Текущий контекст: $ kubectl config current-context

output

minikube

Список всех контекстов: $ kubectl config get-contexts

  • Основные объекты - это ресурсы Deployment

Основные задачи Deployment:

  1. Создание ReplicationSet (следит, чтобы число запущенных Pod-ов соответствовало описанному)
  2. Ведение истории версий запущенных Pod-ов (для различных стратегий деплоя, для возможностей отката)
  3. Описание процесса деплоя (стратегия, параметры стратегий)
  • Файл kubernetes/reddit/ui-deployment.yml:
kubernetes/reddit/ui-deployment.yml

---
apiVersion: apps/v1beta2
kind: Deployment
metadata:
  name: ui
  labels:
    app: reddit
    component: ui
spec:
  replicas: 3
  selector:         ## selector описывает, как ему отслеживать POD-ы.
    matchLabels:    ## В данном случае - контроллер будет считать POD-ы с метками:
      app: reddit   ## app=reddit и component=ui
      component: ui
  template:
    metadata:
      name: ui-pod
      labels:       ## Поэтому важно в описании POD-а задать
        app: reddit ## нужные метки (labels) 
        component: ui
    spec:
      containers:
      - image: ozyab/ui
        name: ui
  • Запуск в Minikube ui-компоненты:
$ kubectl apply -f ui-deployment.yml

output

deployment "ui" created

Проверка запущенных deployment'ов:

$ kubectl get deployment 

output

NAME   READY   UP-TO-DATE   AVAILABLE   AGE
ui     3/3     3            3           2m27s

kubectl apply -f <filename> может принимать не только отдельный файл, но и папку с ними. Например:

$ kubectl apply -f ./kubernetes/reddit 
  • Используя selector, найдем POD-ы приложения:
$ kubectl get pods --selector component=ui 

output

NAME                  READY   STATUS    RESTARTS   AGE
ui-84994b4554-5m4cb   1/1     Running   0          3m20s
ui-84994b4554-7gnqf   1/1     Running   0          3m25s
ui-84994b4554-zhfr6   1/1     Running   0          4m48s

Проброс порта на pod:

$ kubectl port-forward <pod-name> 8080:9292

output

Forwarding from 127.0.0.1:8080 -> 9292
Forwarding from [::1]:8080 -> 9292

После этого можно перейти по адресу http://127.0.0.1:8080 Microservices Reddit in ui container

  • Файл kubernetes/reddit/comment-deployment.yml:
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: ozyab/comment # Меняется только имя образа
        name: comment
  • Запрос созданных подов:
$ kubectl get pods --selector component=comment

Выполнив проброс порта в pod, и перейдя по адресу http://127.0.0.1:8080/healthcheck увидим:

Comment component healthcheck

  • Файл kubernetes/reddit/post-deployment.yml:
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-pod
      labels:
        app: reddit
        component: post
    spec:
      containers:
      - image: ozyab/post
        name: post

Применение deployment: kubectl apply -f post-deployment.yml

Post component healthcheck

  • Файл kubernetes/reddit/mongo-deployment.yml:
kubernetes/reddit/mongo-deployment.yml

---
apiVersion: apps/v1beta2
kind: Deployment
metadata:
  name: mongo
  labels:
    app: reddit
    component: mongo
spec:
  replicas: 1
  selector:
    matchLabels:
      app: reddit
      component: mongo
  template:
    metadata:
      name: mongo
      labels:
        app: reddit
        component: mongo
    spec:
      containers:
      - image: mongo:3.2
        name: mongo
        volumeMounts:   #точка монтирования в контейнере (не в POD-е)
        - name: mongo-persistent-storage
          mountPath: /data/db
      volumes:   #Ассоциированные с POD-ом Volume-ы
      - name: mongo-persistent-storage
        emptyDir: {}
  • Для связи компонент между собой и с внешним миром используется объект Service - абстракция, которая определяет набор POD-ов (Endpoints) и способ доступа к ним.

Для связи ui с post и comment нужно создать им по объекту Service.

Файл kubernetes/reddit/comment-service.yml:

kubernetes/reddit/comment-service.yml

---
apiVersion: v1
kind: Service
metadata:
  name: comment  # в DNS появится запись для comment
  labels:
    app: reddit
    component: comment
spec:
  ports:           # При обращении на адрес post:9292 изнутри любого из POD-ов
  - port: 9292      # текущего namespace нас переправит на 9292-ный
    protocol: TCP    # порт одного из POD-ов приложения post,
    targetPort: 9292  # выбранных по label-ам
  selector:
    app: reddit
    component: comment

После применения comment-service.yml найдем по label'ам соответствующие PODы:

$ kubectl describe service comment | grep Endpoints

output

Endpoints:   172.17.0.4:9292,172.17.0.6:9292,172.17.0.9:9292

Выполним команду nslookup comment из контейнера post:

$ kubectl get pods --selector component=post
NAME                    READY   STATUS    RESTARTS   AGE
post-5c45f6d5c8-5dpx7   1/1     Running   0          17m
post-5c45f6d5c8-cb8fv   1/1     Running   0          17m
post-5c45f6d5c8-k9s5h   1/1     Running   0          17m
$ kubectl exec -ti post-5c45f6d5c8-5dpx7 nslookup comment
nslookup: can't resolve '(null)': Name does not resolve
Name:      comment
Address 1: 10.105.95.41 comment.default.svc.cluster.local

Видим, что получен ответ от DNS.

  • Аналогичным образом развернем service для post:
kubernetes/reddit/post-service.yml

---
apiVersion: v1
kind: Service
metadata:
  name: post 
  labels:
    app: reddit
    component: post
spec:
  ports:
  - port: 9292
    protocol: TCP
    targetPort: 9292
  selector:
    app: reddit
    component: post
  • Post и Comment также используют mongodb, следовательно ей тоже нужен объект Service:
kubernetes/reddit/mongodb-service.yml

---
apiVersion: v1
kind: Service
metadata:
  name: mongodb
  labels:
    app: reddit
    component: mongo
spec:
  ports:
  - port: 27017
    protocol: TCP
    targetPort: 27017
  selector:
    app: reddit
    component: mongo

Деплой:

kubectl apply -f mongodb-service.yml
  • Приложение ищет адрес comment_db, а не mongodb. Аналогично и сервис comment ищет post_db.

Эти адреса заданы в их Dockerfile-ах в виде переменных окружения:

post/Dockerfile
…
ENV POST_DATABASE_HOST=post_db
comment/Dockerfile
…
ENV COMMENT_DATABASE_HOST=comment_db

Создадим сервис:

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" # отдельный лейбл для comment-db

Файл mongo-deployment.yml:

kubernetes/reddit/mongo-deployment.yml

---
apiVersion: apps/v1beta2
kind: Deployment
metadata:
  name: mongo
  labels:
  ...
    comment-db: "true"
...
  template:
    metadata:
      name: mongo
      labels:
        ...
        comment-db: "true"

Файл comment-deployment.yml:

kubernetes/reddit/comment-deployment.yml

---
apiVersion: apps/v1beta2
kind: Deployment
metadata:
  name: comment
  labels:
    app: reddit
    component: comment
...
    spec:
      containers:
      - image: ozyab/comment
        name: comment
        env:
        - name: COMMENT_DATABASE_HOST
          value: comment-db

Также необходимо обновить файл mongo-deployment.yml, чтобы новый Service смог найти нужный POD:

kubernetes/reddit/mongo-deployment.yml

---
apiVersion: apps/v1beta2
kind: Deployment
metadata:
  name: mongo
  labels:
    ..
    comment-db: "true"
  template:
    metadata:
      name: mongo
      labels:
        ..
        comment-db: "true"
  • Необходимо обеспечить доступ к ui-сервису снаружи. Для этого нам понадобится Service для UI-компоненты ui-service.yml:
kubernetes/reddit/ui-service.yml
...
  spec:
  - nodePort: 32092 #можно задать свой порт из диапазона 30000-32767
    type: NodePort

В описании service:

  • NodePort - для доступа снаружи кластера
  • port - для доступа к сервису изнутри кластера

Команда minikube service ui откроет в браузере страницу сервиса.

Список всех сервисов с URL: minikube services list

Namespace

Namespace - это "виртуальный" кластер Kubernetes внутри самого Kubernetes. Внутри каждого такого кластера находятся свои объекты (POD-ы, Service-ы, Deployment-ы и т.д.), кроме объектов, общих на все namespace-ы (nodes, ClusterRoles, PersistentVolumes).

В разных namespace-ах могут находится объекты с одинаковым именем, но в рамках одного namespace имена объектов должны быть уникальны.

При старте Kubernetes кластер уже имеет 3 namespace:

  • default - для объектов для которых не определен другой Namespace (в нем мы работали все это время)
  • kube-system - для объектов созданных Kubernetes’ом и для управления им
  • kube-public - для объектов к которым нужен доступ из любой точки кластера Для того, чтобы выбрать конкретное пространство имен, нужно указать флаг -n <namespace> или --namespace <namespace> при запуске kubectl
  • Отделим среду для разработки приложения от всего остального кластера, для чего создадим свой Namespace dev:
dev-namespace.yml: 

---
apiVersion: v1
kind: Namespace
metadata:
  name: dev

Создание namespace dev: $ kubectl apply -f dev-namespace.yml

  • Добавим инфу об окружении внутрь контейнера UI:
kubernetes/reddit/ui-deployment.yml

---
apiVersion: apps/v1beta2
kind: Deployment
...
    spec:
      containers:
      ...
        env:
        - name: ENV #Извлекаем значения из контекста запуска
          valueFrom:
            fieldRef:
              fieldPath: metadata.namespace

После этого: $ kubectl apply -f ui-deployment.yml -n dev

Разворачиваем Kubernetes в GKE (Google Kubernetes Engine)

Переходим на страницу Kubernetes Engine: https://console.cloud.google.com/kubernetes/list?project=${PROJECT_NAME} и создаем кластер.

Компоненты управления кластером запускаются в container engine и управляются Google:

  • kube-apiserver
  • kube-scheduler
  • kube-controller-manager
  • etcd Рабочая нагрузка (собственные POD-ы), аддоны, мониторинг, логирование и т.д. запускаются на рабочих нодах. Рабочие ноды - стандартные ноды Google compute engine. Их можно увидеть в списке запущенных узлов.

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

gcloud container clusters get-credentials standard-cluster-1 --zone us-central1-a --project ${PROJECT_NAME}

В файл ~/.kube/config будут добавлены user, cluster и context для подключения к кластеру в GKE. Также текущий контекст будет выставлен для подключения к этому кластеру.

Проверим текущий контекст: $ kubectl config current-context

output:

gke_keen-${PROJECT_NAME}_us-central1-a_standard-cluster-1

Создадим dev namespace: $ kubectl apply -f ./kubernetes/reddit/dev-namespace.yml

Деплой всех приложений в namespace dev: $ kubectl apply -f ./kubernetes/reddit/ -n dev

Открытие диапазона kubernetes портов в firewall:

     firewall-rules create kubernetes-nodeports \
     --direction=INGRESS \
     --priority=1000 \
     --network=default \
     --action=ALLOW \
     --rules=tcp:30000-32767 \
     --source-ranges=0.0.0.0/0
  • Найдем внешний IP-адрес любой ноды из кластера: $ kubectl get nodes -o wide
NAME                                                STATUS   ROLES    AGE   VERSION         EXTERNAL-IP
gke-standard-cluster-1-default-pool-2dd96181-qt7q   Ready    <none>   11h   v1.10.9-gke.5   XX.XXX.XX.XXX
gke-standard-cluster-1-default-pool-d3cd4782-7j9s   Ready    <none>   11h   v1.10.9-gke.5   XX.XXX.XX.XXX

Порт публикации сервиса ui: $ kubectl describe service ui -n dev | grep NodePort

Type:                     NodePort
NodePort:                 <unset>  32092/TCP

Можно перейти на любой из внешних IP-адресов для открытия страницы http://XX.XXX.XX.XXX:32092

Readdit app deployed in GKE

Homework 21 (kubernetes-1)

Build Status

  • Creted new Deployment manifests in kubernetes/reddit folder:
comment-deployment.yml
mongo-deployment.yml
post-deployment.yml
ui-deployment.yml

This lab assumes you have access to the Google Cloud Platform. This lab we use MacOS.

Prerequisites

  • Install the Google Cloud SDK

Follow the Google Cloud SDK documentation to install and configure the gcloud command line utility.

Verify the Google Cloud SDK version is 218.0.0 or higher: gcloud version

  • Default Compute Region and Zone The easiest way to set default compute region: gcloud init.

Otherwise set a default compute region: gcloud config set compute/region us-west1.

Set a default compute zone: gcloud config set compute/zone us-west1-c.

Installing the Client Tools

  • Install CFSSL

The cfssl and cfssljson command line utilities will be used to provision a PKI Infrastructure and generate TLS certificates.

Installing cfssl and cfssljson using packet manager brew: brew install cfssl.

  • Verification Installing
cfssl version
  • Install kubectl

The kubectl command line utility is used to interact with the Kubernetes API Server.

  • Download and install kubectl from the official release binaries:
curl -o kubectl https://storage.googleapis.com/kubernetes-release/release/v1.12.0/bin/darwin/amd64/kubectl
chmod +x kubectl
sudo mv kubectl /usr/local/bin/
  • Verify kubectl version 1.12.0 or higher is installed:
kubectl version --client

Provisioning Compute Resources

  • Virtual Private Cloud Network Create the kubernetes-the-hard-way custom VPC network:
gcloud compute networks create kubernetes-the-hard-way --subnet-mode custom

A subnet must be provisioned with an IP address range large enough to assign a private IP address to each node in the Kubernetes cluster.

Create the kubernetes subnet in the kubernetes-the-hard-way VPC network:

gcloud compute networks subnets create kubernetes \
  --network kubernetes-the-hard-way \
  --range 10.240.0.0/24

The 10.240.0.0/24 IP address range can host up to 254 compute instances.

  • Firewall

Create a firewall rule that allows internal communication across all protocols:

gcloud compute firewall-rules create kubernetes-the-hard-way-allow-internal \
  --allow tcp,udp,icmp \
  --network kubernetes-the-hard-way \
  --source-ranges 10.240.0.0/24,10.200.0.0/16

Create a firewall rule that allows external SSH, ICMP, and HTTPS:

gcloud compute firewall-rules create kubernetes-the-hard-way-allow-external \
  --allow tcp:22,tcp:6443,icmp \
  --network kubernetes-the-hard-way \
  --source-ranges 0.0.0.0/0

List the firewall rules in the kubernetes-the-hard-way VPC network:

gcloud compute firewall-rules list --filter="network:kubernetes-the-hard-way"

output

NAME                                    NETWORK                  DIRECTION  PRIORITY  ALLOW                 DENY  DISABLED
kubernetes-the-hard-way-allow-external  kubernetes-the-hard-way  INGRESS    1000      tcp:22,tcp:6443,icmp        False
kubernetes-the-hard-way-allow-internal  kubernetes-the-hard-way  INGRESS    1000      tcp,udp,icmp                False
  • Kubernetes Public IP Address

Allocate a static IP address that will be attached to the external load balancer fronting the Kubernetes API Servers:

gcloud compute addresses create kubernetes-the-hard-way \
  --region $(gcloud config get-value compute/region)

Verify the kubernetes-the-hard-way static IP address was created in your default compute region:

gcloud compute addresses list --filter="name=('kubernetes-the-hard-way')"
  • Compute Instances The compute instances in this lab will be provisioned using Ubuntu Server 18.04, which has good support for the containerd container runtime. Each compute instance will be provisioned with a fixed private IP address to simplify the Kubernetes bootstrapping process.

  • Kubernetes Controllers Create three compute instances which will host the Kubernetes control plane:

for i in 0 1 2; do
  gcloud compute instances create controller-${i} \
    --async \
    --boot-disk-size 200GB \
    --can-ip-forward \
    --image-family ubuntu-1804-lts \
    --image-project ubuntu-os-cloud \
    --machine-type n1-standard-1 \
    --private-network-ip 10.240.0.1${i} \
    --scopes compute-rw,storage-ro,service-management,service-control,logging-write,monitoring \
    --subnet kubernetes \
    --tags kubernetes-the-hard-way,controller
done
  • Kubernetes Workers Each worker instance requires a pod subnet allocation from the Kubernetes cluster CIDR range. The pod subnet allocation will be used to configure container networking in a later exercise. The pod-cidr instance metadata will be used to expose pod subnet allocations to compute instances at runtime.

The Kubernetes cluster CIDR range is defined by the Controller Manager's --cluster-cidr flag. In this tutorial the cluster CIDR range will be set to 10.200.0.0/16, which supports 254 subnets.

Create three compute instances which will host the Kubernetes worker nodes:

for i in 0 1 2; do
  gcloud compute instances create worker-${i} \
    --async \
    --boot-disk-size 200GB \
    --can-ip-forward \
    --image-family ubuntu-1804-lts \
    --image-project ubuntu-os-cloud \
    --machine-type n1-standard-1 \
    --metadata pod-cidr=10.200.${i}.0/24 \
    --private-network-ip 10.240.0.2${i} \
    --scopes compute-rw,storage-ro,service-management,service-control,logging-write,monitoring \
    --subnet kubernetes \
    --tags kubernetes-the-hard-way,worker
done
  • Verification List the compute instances in your default compute zone:
gcloud compute instances list

output

NAME          ZONE            MACHINE_TYPE               PREEMPTIBLE  INTERNAL_IP  EXTERNAL_IP     STATUS
controller-0  europe-west4-a  n1-standard-1                           10.240.0.10  X.X.X.X         RUNNING
controller-1  europe-west4-a  n1-standard-1                           10.240.0.11  X.X.X.X         RUNNING
controller-2  europe-west4-a  n1-standard-1                           10.240.0.12  X.X.X.X         RUNNING
worker-0      europe-west4-a  n1-standard-1                           10.240.0.20  X.X.X.X         RUNNING
worker-1      europe-west4-a  n1-standard-1                           10.240.0.21  X.X.X.X         RUNNING
worker-2      europe-west4-a  n1-standard-1                           10.240.0.22  X.X.X.X         RUNNING
  • Configuring SSH Access SSH will be used to configure the controller and worker instances. When connecting to compute instances for the first time SSH keys will be generated for you and stored in the project or instance metadata as describe in the connecting to instances documentation.

Test SSH access to the controller-0 compute instances:

gcloud compute ssh controller-0

If this is your first time connecting to a compute instance SSH keys will be generated for you.

Provisioning a CA and Generating TLS Certificates

  • Certificate Authority

Generate the CA configuration file, certificate, and private key:

cat > ca-config.json <<EOF
{
  "signing": {
    "default": {
      "expiry": "8760h"
    },
    "profiles": {
      "kubernetes": {
        "usages": ["signing", "key encipherment", "server auth", "client auth"],
        "expiry": "8760h"
      }
    }
  }
}
EOF

cat > ca-csr.json <<EOF
{
  "CN": "Kubernetes",
  "key": {
    "algo": "rsa",
    "size": 2048
  },
  "names": [
    {
      "C": "US",
      "L": "Portland",
      "O": "Kubernetes",
      "OU": "CA",
      "ST": "Oregon"
    }
  ]
}
EOF

cfssl gencert -initca ca-csr.json | cfssljson -bare ca
  • Client and Server Certificates

Generate the admin client certificate and private key:

cat > admin-csr.json <<EOF
{
  "CN": "admin",
  "key": {
    "algo": "rsa",
    "size": 2048
  },
  "names": [
    {
      "C": "US",
      "L": "Portland",
      "O": "system:masters",
      "OU": "Kubernetes The Hard Way",
      "ST": "Oregon"
    }
  ]
}
EOF

cfssl gencert \
  -ca=ca.pem \
  -ca-key=ca-key.pem \
  -config=ca-config.json \
  -profile=kubernetes \
  admin-csr.json | cfssljson -bare admin
  • The Kubelet Client Certificates

Kubernetes uses a special-purpose authorization mode called Node Authorizer, that specifically authorizes API requests made by Kubelets.

Generate a certificate and private key for each Kubernetes worker node:

for instance in worker-0 worker-1 worker-2; do
cat > ${instance}-csr.json <<EOF
{
  "CN": "system:node:${instance}",
  "key": {
    "algo": "rsa",
    "size": 2048
  },
  "names": [
    {
      "C": "US",
      "L": "Portland",
      "O": "system:nodes",
      "OU": "Kubernetes The Hard Way",
      "ST": "Oregon"
    }
  ]
}
EOF

EXTERNAL_IP=$(gcloud compute instances describe ${instance} \
  --format 'value(networkInterfaces[0].accessConfigs[0].natIP)')

INTERNAL_IP=$(gcloud compute instances describe ${instance} \
  --format 'value(networkInterfaces[0].networkIP)')

cfssl gencert \
  -ca=ca.pem \
  -ca-key=ca-key.pem \
  -config=ca-config.json \
  -hostname=${instance},${EXTERNAL_IP},${INTERNAL_IP} \
  -profile=kubernetes \
  ${instance}-csr.json | cfssljson -bare ${instance}
done
  • The Controller Manager Client Certificate

Generate the kube-controller-manager client certificate and private key:

cat > kube-controller-manager-csr.json <<EOF
{
  "CN": "system:kube-controller-manager",
  "key": {
    "algo": "rsa",
    "size": 2048
  },
  "names": [
    {
      "C": "US",
      "L": "Portland",
      "O": "system:kube-controller-manager",
      "OU": "Kubernetes The Hard Way",
      "ST": "Oregon"
    }
  ]
}
EOF

cfssl gencert \
  -ca=ca.pem \
  -ca-key=ca-key.pem \
  -config=ca-config.json \
  -profile=kubernetes \
  kube-controller-manager-csr.json | cfssljson -bare kube-controller-manager

  • The Kube Proxy Client Certificate

Generate the kube-proxy client certificate and private key:

cat > kube-proxy-csr.json <<EOF
{
  "CN": "system:kube-proxy",
  "key": {
    "algo": "rsa",
    "size": 2048
  },
  "names": [
    {
      "C": "US",
      "L": "Portland",
      "O": "system:node-proxier",
      "OU": "Kubernetes The Hard Way",
      "ST": "Oregon"
    }
  ]
}
EOF

cfssl gencert \
  -ca=ca.pem \
  -ca-key=ca-key.pem \
  -config=ca-config.json \
  -profile=kubernetes \
  kube-proxy-csr.json | cfssljson -bare kube-proxy
  • The Scheduler Client Certificate

Generate the kube-scheduler client certificate and private key:

cat > kube-scheduler-csr.json <<EOF
{
  "CN": "system:kube-scheduler",
  "key": {
    "algo": "rsa",
    "size": 2048
  },
  "names": [
    {
      "C": "US",
      "L": "Portland",
      "O": "system:kube-scheduler",
      "OU": "Kubernetes The Hard Way",
      "ST": "Oregon"
    }
  ]
}
EOF

cfssl gencert \
  -ca=ca.pem \
  -ca-key=ca-key.pem \
  -config=ca-config.json \
  -profile=kubernetes \
  kube-scheduler-csr.json | cfssljson -bare kube-scheduler
  • The Kubernetes API Server Certificate

The kubernetes-the-hard-way static IP address will be included in the list of subject alternative names for the Kubernetes API Server certificate. This will ensure the certificate can be validated by remote clients.

Generate the Kubernetes API Server certificate and private key:

KUBERNETES_PUBLIC_ADDRESS=$(gcloud compute addresses describe kubernetes-the-hard-way \
  --region $(gcloud config get-value compute/region) \
  --format 'value(address)')

cat > kubernetes-csr.json <<EOF
{
  "CN": "kubernetes",
  "key": {
    "algo": "rsa",
    "size": 2048
  },
  "names": [
    {
      "C": "US",
      "L": "Portland",
      "O": "Kubernetes",
      "OU": "Kubernetes The Hard Way",
      "ST": "Oregon"
    }
  ]
}
EOF

cfssl gencert \
  -ca=ca.pem \
  -ca-key=ca-key.pem \
  -config=ca-config.json \
  -hostname=10.32.0.1,10.240.0.10,10.240.0.11,10.240.0.12,${KUBERNETES_PUBLIC_ADDRESS},127.0.0.1,kubernetes.default \
  -profile=kubernetes \
  kubernetes-csr.json | cfssljson -bare kubernetes
  • The Service Account Key Pair

The Kubernetes Controller Manager leverages a key pair to generate and sign service account tokens as describe in the managing service accounts documentation.

Generate the service-account certificate and private key:

cat > service-account-csr.json <<EOF
{
  "CN": "service-accounts",
  "key": {
    "algo": "rsa",
    "size": 2048
  },
  "names": [
    {
      "C": "US",
      "L": "Portland",
      "O": "Kubernetes",
      "OU": "Kubernetes The Hard Way",
      "ST": "Oregon"
    }
  ]
}
EOF

cfssl gencert \
  -ca=ca.pem \
  -ca-key=ca-key.pem \
  -config=ca-config.json \
  -profile=kubernetes \
  service-account-csr.json | cfssljson -bare service-account
  • Distribute the Client and Server Certificates

Copy the appropriate certificates and private keys to each worker instance:

for instance in worker-0 worker-1 worker-2; do
  gcloud compute scp ca.pem ${instance}-key.pem ${instance}.pem ${instance}:~/
done

Copy the appropriate certificates and private keys to each controller instance:

for instance in controller-0 controller-1 controller-2; do
  gcloud compute scp ca.pem ca-key.pem kubernetes-key.pem kubernetes.pem \
    service-account-key.pem service-account.pem ${instance}:~/
done

Generating Kubernetes Configuration Files for Authentication

In this lab you will generate Kubernetes configuration files, also known as kubeconfigs, which enable Kubernetes clients to locate and authenticate to the Kubernetes API Servers.

In this section you will generate kubeconfig files for the controller manager, kubelet, kube-proxy, and scheduler clients and the admin user.

  • Kubernetes Public IP Address Each kubeconfig requires a Kubernetes API Server to connect to. To support high availability the IP address assigned to the external load balancer fronting the Kubernetes API Servers will be used.

Retrieve the kubernetes-the-hard-way static IP address:

KUBERNETES_PUBLIC_ADDRESS=$(gcloud compute addresses describe kubernetes-the-hard-way \
  --region $(gcloud config get-value compute/region) \
  --format 'value(address)')
  • The kubelet Kubernetes Configuration File

When generating kubeconfig files for Kubelets the client certificate matching the Kubelet's node name must be used. This will ensure Kubelets are properly authorized by the Kubernetes Node Authorizer.

Generate a kubeconfig file for each worker node:

for instance in worker-0 worker-1 worker-2; do
  kubectl config set-cluster kubernetes-the-hard-way \
    --certificate-authority=ca.pem \
    --embed-certs=true \
    --server=https://${KUBERNETES_PUBLIC_ADDRESS}:6443 \
    --kubeconfig=${instance}.kubeconfig

  kubectl config set-credentials system:node:${instance} \
    --client-certificate=${instance}.pem \
    --client-key=${instance}-key.pem \
    --embed-certs=true \
    --kubeconfig=${instance}.kubeconfig

  kubectl config set-context default \
    --cluster=kubernetes-the-hard-way \
    --user=system:node:${instance} \
    --kubeconfig=${instance}.kubeconfig

  kubectl config use-context default --kubeconfig=${instance}.kubeconfig
done
  • The kube-proxy Kubernetes Configuration File

Generate a kubeconfig file for the kube-proxy service:

  kubectl config set-cluster kubernetes-the-hard-way \
    --certificate-authority=ca.pem \
    --embed-certs=true \
    --server=https://${KUBERNETES_PUBLIC_ADDRESS}:6443 \
    --kubeconfig=kube-proxy.kubeconfig

  kubectl config set-credentials system:kube-proxy \
    --client-certificate=kube-proxy.pem \
    --client-key=kube-proxy-key.pem \
    --embed-certs=true \
    --kubeconfig=kube-proxy.kubeconfig

  kubectl config set-context default \
    --cluster=kubernetes-the-hard-way \
    --user=system:kube-proxy \
    --kubeconfig=kube-proxy.kubeconfig

  kubectl config use-context default --kubeconfig=kube-proxy.kubeconfig
  • The kube-controller-manager Kubernetes Configuration File

Generate a kubeconfig file for the kube-controller-manager service:

  kubectl config set-cluster kubernetes-the-hard-way \
    --certificate-authority=ca.pem \
    --embed-certs=true \
    --server=https://127.0.0.1:6443 \
    --kubeconfig=kube-controller-manager.kubeconfig

  kubectl config set-credentials system:kube-controller-manager \
    --client-certificate=kube-controller-manager.pem \
    --client-key=kube-controller-manager-key.pem \
    --embed-certs=true \
    --kubeconfig=kube-controller-manager.kubeconfig

  kubectl config set-context default \
    --cluster=kubernetes-the-hard-way \
    --user=system:kube-controller-manager \
    --kubeconfig=kube-controller-manager.kubeconfig

  kubectl config use-context default --kubeconfig=kube-controller-manager.kubeconfig
  • The kube-scheduler Kubernetes Configuration File

Generate a kubeconfig file for the kube-scheduler service:

  kubectl config set-cluster kubernetes-the-hard-way \
    --certificate-authority=ca.pem \
    --embed-certs=true \
    --server=https://127.0.0.1:6443 \
    --kubeconfig=kube-scheduler.kubeconfig

  kubectl config set-credentials system:kube-scheduler \
    --client-certificate=kube-scheduler.pem \
    --client-key=kube-scheduler-key.pem \
    --embed-certs=true \
    --kubeconfig=kube-scheduler.kubeconfig

  kubectl config set-context default \
    --cluster=kubernetes-the-hard-way \
    --user=system:kube-scheduler \
    --kubeconfig=kube-scheduler.kubeconfig

  kubectl config use-context default --kubeconfig=kube-scheduler.kubeconfig
  • The admin Kubernetes Configuration File

Generate a kubeconfig file for the admin user:

  kubectl config set-cluster kubernetes-the-hard-way \
    --certificate-authority=ca.pem \
    --embed-certs=true \
    --server=https://127.0.0.1:6443 \
    --kubeconfig=admin.kubeconfig

  kubectl config set-credentials admin \
    --client-certificate=admin.pem \
    --client-key=admin-key.pem \
    --embed-certs=true \
    --kubeconfig=admin.kubeconfig

  kubectl config set-context default \
    --cluster=kubernetes-the-hard-way \
    --user=admin \
    --kubeconfig=admin.kubeconfig

  kubectl config use-context default --kubeconfig=admin.kubeconfig
  • Distribute the Kubernetes Configuration Files Copy the appropriate kubelet and kube-proxy kubeconfig files to each worker instance:
for instance in worker-0 worker-1 worker-2; do
  gcloud compute scp ${instance}.kubeconfig kube-proxy.kubeconfig ${instance}:~/
done

Copy the appropriate kube-controller-manager and kube-scheduler kubeconfig files to each controller instance:

for instance in controller-0 controller-1 controller-2; do
  gcloud compute scp admin.kubeconfig kube-controller-manager.kubeconfig kube-scheduler.kubeconfig ${instance}:~/
done

Generating the Data Encryption Config and Key

Kubernetes stores a variety of data including cluster state, application configurations, and secrets. Kubernetes supports the ability to encrypt cluster data at rest.

In this lab you will generate an encryption key and an encryption config suitable for encrypting Kubernetes Secrets.

  • The Encryption Key

Generate an encryption key:

ENCRYPTION_KEY=$(head -c 32 /dev/urandom | base64)
  • The Encryption Config File Create the encryption-config.yaml encryption config file:
cat > encryption-config.yaml <<EOF
kind: EncryptionConfig
apiVersion: v1
resources:
  - resources:
      - secrets
    providers:
      - aescbc:
          keys:
            - name: key1
              secret: ${ENCRYPTION_KEY}
      - identity: {}
EOF

Copy the encryption-config.yaml encryption config file to each controller instance:

for instance in controller-0 controller-1 controller-2; do
  gcloud compute scp encryption-config.yaml ${instance}:~/
done

Bootstrapping the etcd Cluster

Kubernetes components are stateless and store cluster state in etcd. In this lab you will bootstrap a three node etcd cluster and configure it for high availability and secure remote access.

  • Prerequisites

The commands in this lab must be run on each controller instance: controller-0, controller-1, and controller-2. Login to each controller instance using the gcloud command. Example: gcloud compute ssh controller-0

  • Bootstrapping an etcd Cluster Member

Download and Install the etcd Binaries from the coreos/etcd GitHub project:

wget -q --show-progress --https-only --timestamping \
  "https://github.com/coreos/etcd/releases/download/v3.3.9/etcd-v3.3.9-linux-amd64.tar.gz"

Extract and install the etcd server and the etcdctl command line utility:

  tar -xvf etcd-v3.3.9-linux-amd64.tar.gz
  sudo mv etcd-v3.3.9-linux-amd64/etcd* /usr/local/bin/
  • Configure the etcd Server
  sudo mkdir -p /etc/etcd /var/lib/etcd
  sudo cp ca.pem kubernetes-key.pem kubernetes.pem /etc/etcd/

The instance internal IP address will be used to serve client requests and communicate with etcd cluster peers. Retrieve the internal IP address for the current compute instance:

INTERNAL_IP=$(curl -s -H "Metadata-Flavor: Google" \
  http://metadata.google.internal/computeMetadata/v1/instance/network-interfaces/0/ip)

Each etcd member must have a unique name within an etcd cluster. Set the etcd name to match the hostname of the current compute instance:

ETCD_NAME=$(hostname -s)

Create the etcd.service systemd unit file:

cat <<EOF | sudo tee /etc/systemd/system/etcd.service
[Unit]
Description=etcd
Documentation=https://github.com/coreos

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

[Install]
WantedBy=multi-user.target
EOF
  • Start the etcd Server
  sudo systemctl daemon-reload
  sudo systemctl enable etcd
  sudo systemctl start etcd
  • Verification

List the etcd cluster members:

sudo ETCDCTL_API=3 etcdctl member list \
   --endpoints=https://127.0.0.1:2379 \
   --cacert=/etc/etcd/ca.pem \
   --cert=/etc/etcd/kubernetes.pem \
   --key=/etc/etcd/kubernetes-key.pem

output:

3a57933972cb5131, started, controller-2, https://10.240.0.12:2380, https://10.240.0.12:2379
f98dc20bce6225a0, started, controller-0, https://10.240.0.10:2380, https://10.240.0.10:2379
ffed16798470cab5, started, controller-1, https://10.240.0.11:2380, https://10.240.0.11:2379

Bootstrapping the Kubernetes Control Plane

In this lab you will bootstrap the Kubernetes control plane across three compute instances and configure it for high availability. You will also create an external load balancer that exposes the Kubernetes API Servers to remote clients. The following components will be installed on each node: Kubernetes API Server, Scheduler, and Controller Manager.

The commands in this lab must be run on each controller instance: controller-0, controller-1, and controller-2. Login to each controller instance using the gcloud command. Example: gcloud compute ssh controller-0

  • Provision the Kubernetes Control Plane

Create the Kubernetes configuration directory:

sudo mkdir -p /etc/kubernetes/config
  • Download and Install the Kubernetes Controller Binaries

Download the official Kubernetes release binaries:

wget -q --show-progress --https-only --timestamping \
  "https://storage.googleapis.com/kubernetes-release/release/v1.12.0/bin/linux/amd64/kube-apiserver" \
  "https://storage.googleapis.com/kubernetes-release/release/v1.12.0/bin/linux/amd64/kube-controller-manager" \
  "https://storage.googleapis.com/kubernetes-release/release/v1.12.0/bin/linux/amd64/kube-scheduler" \
  "https://storage.googleapis.com/kubernetes-release/release/v1.12.0/bin/linux/amd64/kubectl"
  • Install the Kubernetes binaries:
chmod +x kube-apiserver kube-controller-manager kube-scheduler kubectl
sudo mv kube-apiserver kube-controller-manager kube-scheduler kubectl /usr/local/bin/
  • Configure the Kubernetes API Server
  sudo mkdir -p /var/lib/kubernetes/

  sudo mv ca.pem ca-key.pem kubernetes-key.pem kubernetes.pem \
    service-account-key.pem service-account.pem \
    encryption-config.yaml /var/lib/kubernetes/

The instance internal IP address will be used to advertise the API Server to members of the cluster. Retrieve the internal IP address for the current compute instance:

INTERNAL_IP=$(curl -s -H "Metadata-Flavor: Google" \
  http://metadata.google.internal/computeMetadata/v1/instance/network-interfaces/0/ip)

Create the kube-apiserver.service systemd unit file:

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

[Service]
ExecStart=/usr/local/bin/kube-apiserver \\
  --advertise-address=${INTERNAL_IP} \\
  --allow-privileged=true \\
  --apiserver-count=3 \\
  --audit-log-maxage=30 \\
  --audit-log-maxbackup=3 \\
  --audit-log-maxsize=100 \\
  --audit-log-path=/var/log/audit.log \\
  --authorization-mode=Node,RBAC \\
  --bind-address=0.0.0.0 \\
  --client-ca-file=/var/lib/kubernetes/ca.pem \\
  --enable-admission-plugins=Initializers,NamespaceLifecycle,NodeRestriction,LimitRanger,ServiceAccount,DefaultStorageClass,ResourceQuota \\
  --enable-swagger-ui=true \\
  --etcd-cafile=/var/lib/kubernetes/ca.pem \\
  --etcd-certfile=/var/lib/kubernetes/kubernetes.pem \\
  --etcd-keyfile=/var/lib/kubernetes/kubernetes-key.pem \\
  --etcd-servers=https://10.240.0.10:2379,https://10.240.0.11:2379,https://10.240.0.12:2379 \\
  --event-ttl=1h \\
  --experimental-encryption-provider-config=/var/lib/kubernetes/encryption-config.yaml \\
  --kubelet-certificate-authority=/var/lib/kubernetes/ca.pem \\
  --kubelet-client-certificate=/var/lib/kubernetes/kubernetes.pem \\
  --kubelet-client-key=/var/lib/kubernetes/kubernetes-key.pem \\
  --kubelet-https=true \\
  --runtime-config=api/all \\
  --service-account-key-file=/var/lib/kubernetes/service-account.pem \\
  --service-cluster-ip-range=10.32.0.0/24 \\
  --service-node-port-range=30000-32767 \\
  --tls-cert-file=/var/lib/kubernetes/kubernetes.pem \\
  --tls-private-key-file=/var/lib/kubernetes/kubernetes-key.pem \\
  --v=2
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target
EOF
  • Configure the Kubernetes Controller Manager

Move the kube-controller-manager kubeconfig into place:

sudo mv kube-controller-manager.kubeconfig /var/lib/kubernetes/

Create the kube-controller-manager.service systemd unit file:

cat <<EOF | sudo tee /etc/systemd/system/kube-controller-manager.service
[Unit]
Description=Kubernetes Controller Manager
Documentation=https://github.com/kubernetes/kubernetes

[Service]
ExecStart=/usr/local/bin/kube-controller-manager \\
  --address=0.0.0.0 \\
  --cluster-cidr=10.200.0.0/16 \\
  --cluster-name=kubernetes \\
  --cluster-signing-cert-file=/var/lib/kubernetes/ca.pem \\
  --cluster-signing-key-file=/var/lib/kubernetes/ca-key.pem \\
  --kubeconfig=/var/lib/kubernetes/kube-controller-manager.kubeconfig \\
  --leader-elect=true \\
  --root-ca-file=/var/lib/kubernetes/ca.pem \\
  --service-account-private-key-file=/var/lib/kubernetes/service-account-key.pem \\
  --service-cluster-ip-range=10.32.0.0/24 \\
  --use-service-account-credentials=true \\
  --v=2
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target
EOF
  • Configure the Kubernetes Scheduler

Move the kube-scheduler kubeconfig into place:

sudo mv kube-scheduler.kubeconfig /var/lib/kubernetes/

Create the kube-scheduler.yaml configuration file:

cat <<EOF | sudo tee /etc/kubernetes/config/kube-scheduler.yaml
apiVersion: componentconfig/v1alpha1
kind: KubeSchedulerConfiguration
clientConnection:
  kubeconfig: "/var/lib/kubernetes/kube-scheduler.kubeconfig"
leaderElection:
  leaderElect: true
EOF

Create the kube-scheduler.service systemd unit file:

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

[Service]
ExecStart=/usr/local/bin/kube-scheduler \\
  --config=/etc/kubernetes/config/kube-scheduler.yaml \\
  --v=2
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target
EOF
  • Start the Controller Services
  sudo systemctl daemon-reload
  sudo systemctl enable kube-apiserver kube-controller-manager kube-scheduler
  sudo systemctl start kube-apiserver kube-controller-manager kube-scheduler
  • Enable HTTP Health Checks A Google Network Load Balancer will be used to distribute traffic across the three API servers and allow each API server to terminate TLS connections and validate client certificates. The network load balancer only supports HTTP health checks which means the HTTPS endpoint exposed by the API server cannot be used. As a workaround the nginx webserver can be used to proxy HTTP health checks. In this section nginx will be installed and configured to accept HTTP health checks on port 80 and proxy the connections to the API server on https://127.0.0.1:6443/healthz.

Install a basic web server to handle HTTP health checks:

sudo apt-get install -y nginx
cat > kubernetes.default.svc.cluster.local <<EOF
server {
  listen      80;
  server_name kubernetes.default.svc.cluster.local;

  location /healthz {
     proxy_pass                    https://127.0.0.1:6443/healthz;
     proxy_ssl_trusted_certificate /var/lib/kubernetes/ca.pem;
  }
}
EOF
  sudo mv kubernetes.default.svc.cluster.local \
    /etc/nginx/sites-available/kubernetes.default.svc.cluster.local

  sudo ln -s /etc/nginx/sites-available/kubernetes.default.svc.cluster.local /etc/nginx/sites-enabled/
sudo systemctl restart nginx
sudo systemctl enable nginx
  • Verification
kubectl get componentstatuses --kubeconfig admin.kubeconfig

output:

NAME                 STATUS    MESSAGE             ERROR
controller-manager   Healthy   ok
scheduler            Healthy   ok
etcd-2               Healthy   {"health":"true"}
etcd-0               Healthy   {"health":"true"}
etcd-1               Healthy   {"health":"true"}

Test the nginx HTTP health check proxy:

curl -H "Host: kubernetes.default.svc.cluster.local" -i http://127.0.0.1/healthz

output:

HTTP/1.1 200 OK
Server: nginx/1.14.0 (Ubuntu)
Date: Sun, 20 Jan 2019 19:54:16 GMT
Content-Type: text/plain; charset=utf-8
Content-Length: 2
Connection: keep-alive
  • RBAC for Kubelet Authorization

In this section you will configure RBAC permissions to allow the Kubernetes API Server to access the Kubelet API on each worker node. Access to the Kubelet API is required for retrieving metrics, logs, and executing commands in pods.

Create the system:kube-apiserver-to-kubelet ClusterRole with permissions to access the Kubelet API and perform most common tasks associated with managing pods:

cat <<EOF | kubectl apply --kubeconfig admin.kubeconfig -f -
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRole
metadata:
  annotations:
    rbac.authorization.kubernetes.io/autoupdate: "true"
  labels:
    kubernetes.io/bootstrapping: rbac-defaults
  name: system:kube-apiserver-to-kubelet
rules:
  - apiGroups:
      - ""
    resources:
      - nodes/proxy
      - nodes/stats
      - nodes/log
      - nodes/spec
      - nodes/metrics
    verbs:
      - "*"
EOF

The Kubernetes API Server authenticates to the Kubelet as the kubernetes user using the client certificate as defined by the --kubelet-client-certificate flag.

Bind the system:kube-apiserver-to-kubelet ClusterRole to the kubernetes user:

cat <<EOF | kubectl apply --kubeconfig admin.kubeconfig -f -
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRoleBinding
metadata:
  name: system:kube-apiserver
  namespace: ""
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: system:kube-apiserver-to-kubelet
subjects:
  - apiGroup: rbac.authorization.k8s.io
    kind: User
    name: kubernetes
EOF
  • The Kubernetes Frontend Load Balancer

In this section you will provision an external load balancer to front the Kubernetes API Servers. The kubernetes-the-hard-way static IP address will be attached to the resulting load balancer.

Provision a Network Load Balancer. Create the external load balancer network resources:

  KUBERNETES_PUBLIC_ADDRESS=$(gcloud compute addresses describe kubernetes-the-hard-way \
    --region $(gcloud config get-value compute/region) \
    --format 'value(address)')

  gcloud compute http-health-checks create kubernetes \
    --description "Kubernetes Health Check" \
    --host "kubernetes.default.svc.cluster.local" \
    --request-path "/healthz"

  gcloud compute firewall-rules create kubernetes-the-hard-way-allow-health-check \
    --network kubernetes-the-hard-way \
    --source-ranges 209.85.152.0/22,209.85.204.0/22,35.191.0.0/16 \
    --allow tcp

  gcloud compute target-pools create kubernetes-target-pool \
    --http-health-check kubernetes

  gcloud compute target-pools add-instances kubernetes-target-pool \
   --instances controller-0,controller-1,controller-2

  gcloud compute forwarding-rules create kubernetes-forwarding-rule \
    --address ${KUBERNETES_PUBLIC_ADDRESS} \
    --ports 6443 \
    --region $(gcloud config get-value compute/region) \
    --target-pool kubernetes-target-pool
  • Verification Make a HTTP request for the Kubernetes version info:
curl --cacert ca.pem https://${KUBERNETES_PUBLIC_ADDRESS}:6443/version

output:

{
  "major": "1",
  "minor": "12",
  "gitVersion": "v1.12.0",
  "gitCommit": "0ed33881dc4355495f623c6f22e7dd0b7632b7c0",
  "gitTreeState": "clean",
  "buildDate": "2018-09-27T16:55:41Z",
  "goVersion": "go1.10.4",
  "compiler": "gc",
  "platform": "linux/amd64"
}

Bootstrapping the Kubernetes Worker Nodes

In this lab you will bootstrap three Kubernetes worker nodes. The following components will be installed on each node: runc, gVisor, container networking plugins, containerd, kubelet, and kube-proxy.

  • Provisioning a Kubernetes Worker Node

Install the OS dependencies:

sudo apt-get update
sudo apt-get -y install socat conntrack ipset

Download and Install Worker Binaries

wget -q --show-progress --https-only --timestamping \
  https://github.com/kubernetes-sigs/cri-tools/releases/download/v1.12.0/crictl-v1.12.0-linux-amd64.tar.gz \
  https://storage.googleapis.com/kubernetes-the-hard-way/runsc-50c283b9f56bb7200938d9e207355f05f79f0d17 \
  https://github.com/opencontainers/runc/releases/download/v1.0.0-rc5/runc.amd64 \
  https://github.com/containernetworking/plugins/releases/download/v0.6.0/cni-plugins-amd64-v0.6.0.tgz \
  https://github.com/containerd/containerd/releases/download/v1.2.0-rc.0/containerd-1.2.0-rc.0.linux-amd64.tar.gz \
  https://storage.googleapis.com/kubernetes-release/release/v1.12.0/bin/linux/amd64/kubectl \
  https://storage.googleapis.com/kubernetes-release/release/v1.12.0/bin/linux/amd64/kube-proxy \
  https://storage.googleapis.com/kubernetes-release/release/v1.12.0/bin/linux/amd64/kubelet

Create the installation directories:

sudo mkdir -p \
  /etc/cni/net.d \
  /opt/cni/bin \
  /var/lib/kubelet \
  /var/lib/kube-proxy \
  /var/lib/kubernetes \
  /var/run/kubernetes

Install the worker binaries:

  sudo mv runsc-50c283b9f56bb7200938d9e207355f05f79f0d17 runsc
  sudo mv runc.amd64 runc
  chmod +x kubectl kube-proxy kubelet runc runsc
  sudo mv kubectl kube-proxy kubelet runc runsc /usr/local/bin/
  sudo tar -xvf crictl-v1.12.0-linux-amd64.tar.gz -C /usr/local/bin/
  sudo tar -xvf cni-plugins-amd64-v0.6.0.tgz -C /opt/cni/bin/
  sudo tar -xvf containerd-1.2.0-rc.0.linux-amd64.tar.gz -C /
  • Configure CNI Networking

Retrieve the Pod CIDR range for the current compute instance:

POD_CIDR=$(curl -s -H "Metadata-Flavor: Google" \
  http://metadata.google.internal/computeMetadata/v1/instance/attributes/pod-cidr)

Create the bridge network configuration file:

cat <<EOF | sudo tee /etc/cni/net.d/10-bridge.conf
{
    "cniVersion": "0.3.1",
    "name": "bridge",
    "type": "bridge",
    "bridge": "cnio0",
    "isGateway": true,
    "ipMasq": true,
    "ipam": {
        "type": "host-local",
        "ranges": [
          [{"subnet": "${POD_CIDR}"}]
        ],
        "routes": [{"dst": "0.0.0.0/0"}]
    }
}
EOF

Create the loopback network configuration file:

cat <<EOF | sudo tee /etc/cni/net.d/99-loopback.conf
{
    "cniVersion": "0.3.1",
    "type": "loopback"
}
EOF

Create the containerd configuration file:

sudo mkdir -p /etc/containerd/
cat << EOF | sudo tee /etc/containerd/config.toml
[plugins]
  [plugins.cri.containerd]
    snapshotter = "overlayfs"
    [plugins.cri.containerd.default_runtime]
      runtime_type = "io.containerd.runtime.v1.linux"
      runtime_engine = "/usr/local/bin/runc"
      runtime_root = ""
    [plugins.cri.containerd.untrusted_workload_runtime]
      runtime_type = "io.containerd.runtime.v1.linux"
      runtime_engine = "/usr/local/bin/runsc"
      runtime_root = "/run/containerd/runsc"
    [plugins.cri.containerd.gvisor]
      runtime_type = "io.containerd.runtime.v1.linux"
      runtime_engine = "/usr/local/bin/runsc"
      runtime_root = "/run/containerd/runsc"
EOF

Create the containerd.service systemd unit file:

cat <<EOF | sudo tee /etc/systemd/system/containerd.service
[Unit]
Description=containerd container runtime
Documentation=https://containerd.io
After=network.target

[Service]
ExecStartPre=/sbin/modprobe overlay
ExecStart=/bin/containerd
Restart=always
RestartSec=5
Delegate=yes
KillMode=process
OOMScoreAdjust=-999
LimitNOFILE=1048576
LimitNPROC=infinity
LimitCORE=infinity

[Install]
WantedBy=multi-user.target
EOF
  • Configure the Kubelet
sudo mv ${HOSTNAME}-key.pem ${HOSTNAME}.pem /var/lib/kubelet/
sudo mv ${HOSTNAME}.kubeconfig /var/lib/kubelet/kubeconfig
sudo mv ca.pem /var/lib/kubernetes/

Create the kubelet-config.yaml configuration file:

cat <<EOF | sudo tee /var/lib/kubelet/kubelet-config.yaml
kind: KubeletConfiguration
apiVersion: kubelet.config.k8s.io/v1beta1
authentication:
  anonymous:
    enabled: false
  webhook:
    enabled: true
  x509:
    clientCAFile: "/var/lib/kubernetes/ca.pem"
authorization:
  mode: Webhook
clusterDomain: "cluster.local"
clusterDNS:
  - "10.32.0.10"
podCIDR: "${POD_CIDR}"
resolvConf: "/run/systemd/resolve/resolv.conf"
runtimeRequestTimeout: "15m"
tlsCertFile: "/var/lib/kubelet/${HOSTNAME}.pem"
tlsPrivateKeyFile: "/var/lib/kubelet/${HOSTNAME}-key.pem"
EOF

Create the kubelet.service systemd unit file:

cat <<EOF | sudo tee /etc/systemd/system/kubelet.service
[Unit]
Description=Kubernetes Kubelet
Documentation=https://github.com/kubernetes/kubernetes
After=containerd.service
Requires=containerd.service

[Service]
ExecStart=/usr/local/bin/kubelet \\
  --config=/var/lib/kubelet/kubelet-config.yaml \\
  --container-runtime=remote \\
  --container-runtime-endpoint=unix:///var/run/containerd/containerd.sock \\
  --image-pull-progress-deadline=2m \\
  --kubeconfig=/var/lib/kubelet/kubeconfig \\
  --network-plugin=cni \\
  --register-node=true \\
  --v=2
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target
EOF
  • Configure the Kubernetes Proxy
sudo mv kube-proxy.kubeconfig /var/lib/kube-proxy/kubeconfig

Create the kube-proxy-config.yaml configuration file:

cat <<EOF | sudo tee /var/lib/kube-proxy/kube-proxy-config.yaml
kind: KubeProxyConfiguration
apiVersion: kubeproxy.config.k8s.io/v1alpha1
clientConnection:
  kubeconfig: "/var/lib/kube-proxy/kubeconfig"
mode: "iptables"
clusterCIDR: "10.200.0.0/16"
EOF

Create the kube-proxy.service systemd unit file:

cat <<EOF | sudo tee /etc/systemd/system/kube-proxy.service
[Unit]
Description=Kubernetes Kube Proxy
Documentation=https://github.com/kubernetes/kubernetes

[Service]
ExecStart=/usr/local/bin/kube-proxy \\
  --config=/var/lib/kube-proxy/kube-proxy-config.yaml
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target
EOF
  • Start the Worker Services
  sudo systemctl daemon-reload
  sudo systemctl enable containerd kubelet kube-proxy
  sudo systemctl start containerd kubelet kube-proxy
  • Verification

List the registered Kubernetes nodes:

gcloud compute ssh controller-0 \
  --command "kubectl get nodes --kubeconfig admin.kubeconfig"

output

NAME       STATUS   ROLES    AGE   VERSION
worker-0   Ready    <none>   10m   v1.12.0
worker-1   Ready    <none>   11m   v1.12.0
worker-2   Ready    <none>   10m   v1.12.0

Configuring kubectl for Remote Access

In this lab you will generate a kubeconfig file for the kubectl command line utility based on the admin user credentials.

Each kubeconfig requires a Kubernetes API Server to connect to. To support high availability the IP address assigned to the external load balancer fronting the Kubernetes API Servers will be used.

Generate a kubeconfig file suitable for authenticating as the admin user:

  KUBERNETES_PUBLIC_ADDRESS=$(gcloud compute addresses describe kubernetes-the-hard-way \
    --region $(gcloud config get-value compute/region) \
    --format 'value(address)')

  kubectl config set-cluster kubernetes-the-hard-way \
    --certificate-authority=ca.pem \
    --embed-certs=true \
    --server=https://${KUBERNETES_PUBLIC_ADDRESS}:6443

  kubectl config set-credentials admin \
    --client-certificate=admin.pem \
    --client-key=admin-key.pem

  kubectl config set-context kubernetes-the-hard-way \
    --cluster=kubernetes-the-hard-way \
    --user=admin

  kubectl config use-context kubernetes-the-hard-way
  • Verification

Check the health of the remote Kubernetes cluster:

kubectl get componentstatuses

output:

NAME                 STATUS    MESSAGE             ERROR
controller-manager   Healthy   ok
scheduler            Healthy   ok
etcd-1               Healthy   {"health":"true"}
etcd-2               Healthy   {"health":"true"}
etcd-0               Healthy   {"health":"true"}

List the nodes in the remote Kubernetes cluster:

kubectl get nodes

output:

NAME       STATUS   ROLES    AGE   VERSION
worker-0   Ready    <none>   14m   v1.12.0
worker-1   Ready    <none>   14m   v1.12.0
worker-2   Ready    <none>   14m   v1.12.0

Provisioning Pod Network Routes

Pods scheduled to a node receive an IP address from the node's Pod CIDR range. At this point pods can not communicate with other pods running on different nodes due to missing network routes.

In this lab you will create a route for each worker node that maps the node's Pod CIDR range to the node's internal IP address.

  • The Routing Table

Print the internal IP address and Pod CIDR range for each worker instance:

for instance in worker-0 worker-1 worker-2; do
  gcloud compute instances describe ${instance} \
    --format 'value[separator=" "](networkInterfaces[0].networkIP,metadata.items[0].value)'
done

output:

10.240.0.20 10.200.0.0/24
10.240.0.21 10.200.1.0/24
10.240.0.22 10.200.2.0/24
  • Routes

Create network routes for each worker instance:

for i in 0 1 2; do
  gcloud compute routes create kubernetes-route-10-200-${i}-0-24 \
    --network kubernetes-the-hard-way \
    --next-hop-address 10.240.0.2${i} \
    --destination-range 10.200.${i}.0/24
done

List the routes in the kubernetes-the-hard-way VPC network:

gcloud compute routes list --filter "network: kubernetes-the-hard-way"

output

NAME                            NETWORK                  DEST_RANGE     NEXT_HOP                  PRIORITY
default-route-4efe3fc4aab42a71  kubernetes-the-hard-way  0.0.0.0/0      default-internet-gateway  1000
default-route-b8c3b87a29570c17  kubernetes-the-hard-way  10.240.0.0/24  kubernetes-the-hard-way   1000
kubernetes-route-10-200-0-0-24  kubernetes-the-hard-way  10.200.0.0/24  10.240.0.20               1000
kubernetes-route-10-200-1-0-24  kubernetes-the-hard-way  10.200.1.0/24  10.240.0.21               1000
kubernetes-route-10-200-2-0-24  kubernetes-the-hard-way  10.200.2.0/24  10.240.0.22               1000

Deploying the DNS Cluster Add-on

In this lab you will deploy the DNS add-on which provides DNS based service discovery, backed by CoreDNS, to applications running inside the Kubernetes cluster.

  • The DNS Cluster Add-on

Deploy the coredns cluster add-on:

kubectl apply -f https://storage.googleapis.com/kubernetes-the-hard-way/coredns.yaml

output

serviceaccount/coredns created
clusterrole.rbac.authorization.k8s.io/system:coredns created
clusterrolebinding.rbac.authorization.k8s.io/system:coredns created
configmap/coredns created
deployment.extensions/coredns created
service/kube-dns created

List the pods created by the kube-dns deployment:

kubectl get pods -l k8s-app=kube-dns -n kube-system

output

NAME                       READY   STATUS    RESTARTS   AGE
coredns-699f8ddd77-cpr8j   1/1     Running   0          71s
coredns-699f8ddd77-zcldn   1/1     Running   0          71s
  • Verification

Create a busybox deployment:

kubectl run busybox --image=busybox:1.28 --command -- sleep 3600

List the pod created by the busybox deployment:

kubectl get pods -l run=busybox

output:

NAME                      READY   STATUS    RESTARTS   AGE
busybox-bd8fb7cbd-9bnbk   1/1     Running   0          76s

Retrieve the full name of the busybox pod:

POD_NAME=$(kubectl get pods -l run=busybox -o jsonpath="{.items[0].metadata.name}")

Execute a DNS lookup for the kubernetes service inside the busybox pod:

kubectl exec -ti $POD_NAME -- nslookup kubernetes

output

Server:    10.32.0.10
Address 1: 10.32.0.10 kube-dns.kube-system.svc.cluster.local

Name:      kubernetes
Address 1: 10.32.0.1 kubernetes.default.svc.cluster.local

Smoke Test

In this lab you will complete a series of tasks to ensure your Kubernetes cluster is functioning correctly.

Create a generic secret:

kubectl create secret generic kubernetes-the-hard-way \
  --from-literal="mykey=mydata"

Print a hexdump of the kubernetes-the-hard-way secret stored in etcd:

gcloud compute ssh controller-0 \
  --command "sudo ETCDCTL_API=3 etcdctl get \
  --endpoints=https://127.0.0.1:2379 \
  --cacert=/etc/etcd/ca.pem \
  --cert=/etc/etcd/kubernetes.pem \
  --key=/etc/etcd/kubernetes-key.pem\
  /registry/secrets/default/kubernetes-the-hard-way | hexdump -C"

output

00000000  2f 72 65 67 69 73 74 72  79 2f 73 65 63 72 65 74  |/registry/secret|
00000010  73 2f 64 65 66 61 75 6c  74 2f 6b 75 62 65 72 6e  |s/default/kubern|
00000020  65 74 65 73 2d 74 68 65  2d 68 61 72 64 2d 77 61  |etes-the-hard-wa|
00000030  79 0a 6b 38 73 3a 65 6e  63 3a 61 65 73 63 62 63  |y.k8s:enc:aescbc|
00000040  3a 76 31 3a 6b 65 79 31  3a d7 29 23 06 e7 16 c4  |:v1:key1:.)#....|
00000050  22 bc 75 c2 a3 21 f3 33  fc 4a c4 7e a5 70 83 30  |".u..!.3.J.~.p.0|
00000060  48 13 fe 22 9a 73 0e fc  8c f3 06 01 eb 46 24 15  |H..".s.......F$.|
00000070  59 c5 02 37 8e eb 26 d9  2f 54 1c cd 21 a4 1f 49  |Y..7..&./T..!..I|
00000080  1a cc 9a a6 27 e2 6c 0c  ce 96 da 85 36 21 2a 83  |....'.l.....6!*.|
00000090  cb b3 62 1c d8 c5 18 b0  15 95 48 cf 2c 2f 41 d5  |..b.......H.,/A.|
000000a0  d9 33 10 65 93 4f e3 55  99 3a a2 64 47 83 24 00  |.3.e.O.U.:.dG.$.|
000000b0  96 8b 07 6b 94 f5 62 05  f5 10 12 3f ae 11 97 ca  |...k..b....?....|
000000c0  9e f1 e5 54 c3 43 28 fd  36 15 9b 41 c9 19 08 65  |...T.C(.6..A...e|
000000d0  18 27 16 11 44 b6 24 fc  3f 39 2f 9b 36 3d d1 9e  |.'..D.$.?9/.6=..|
000000e0  c8 da a5 e4 2d 8a 28 bf  2b 0a                    |....-.(.+.|

The etcd key should be prefixed with k8s:enc:aescbc:v1:key1, which indicates the aescbc provider was used to encrypt the data with the key1 encryption key.

  • Deployments

In this section you will verify the ability to create and manage Deployments.

Create a deployment for the nginx web server:

kubectl run nginx --image=nginx

List the pod created by the nginx deployment:

kubectl get pods -l run=nginx

output

NAME                    READY   STATUS    RESTARTS   AGE
nginx-dbddb74b8-9d2ch   1/1     Running   0          22s
  • Port Forwarding

In this section you will verify the ability to access applications remotely using port forwarding.

Retrieve the full name of the nginx pod:

POD_NAME=$(kubectl get pods -l run=nginx -o jsonpath="{.items[0].metadata.name}")

Forward port 8080 on your local machine to port 80 of the nginx pod:

kubectl port-forward $POD_NAME 8080:80

output

Forwarding from 127.0.0.1:8080 -> 80
Forwarding from [::1]:8080 -> 80

In a new terminal make an HTTP request using the forwarding address:

curl --head http://127.0.0.1:8080

output

HTTP/1.1 200 OK
Server: nginx/1.15.8
Date: Sun, 20 Jan 2019 21:48:12 GMT
Content-Type: text/html
Content-Length: 612
Last-Modified: Tue, 25 Dec 2018 09:56:47 GMT
Connection: keep-alive
ETag: "5c21fedf-264"
Accept-Ranges: bytes
  • Logs

In this section you will verify the ability to retrieve container logs.

Print the nginx pod logs:

kubectl logs $POD_NAME

output

127.0.0.1 - - [20/Jan/2019:21:48:12 +0000] "HEAD / HTTP/1.1" 200 0 "-" "curl/7.54.0" "-"

Print the nginx version by executing the nginx -v command in the nginx container:

kubectl exec -ti $POD_NAME -- nginx -v

output

nginx version: nginx/1.15.8
  • Services

In this section you will verify the ability to expose applications using a Service.

Expose the nginx deployment using a NodePort service:

kubectl expose deployment nginx --port 80 --type NodePort

Retrieve the node port assigned to the nginx service:

NODE_PORT=$(kubectl get svc nginx \
  --output=jsonpath='{range .spec.ports[0]}{.nodePort}')

Create a firewall rule that allows remote access to the nginx node port:

gcloud compute firewall-rules create kubernetes-the-hard-way-allow-nginx-service \
  --allow=tcp:${NODE_PORT} \
  --network kubernetes-the-hard-way

Retrieve the external IP address of a worker instance:

EXTERNAL_IP=$(gcloud compute instances describe worker-0 \
  --format 'value(networkInterfaces[0].accessConfigs[0].natIP)')

Make an HTTP request using the external IP address and the nginx node port:

curl -I http://${EXTERNAL_IP}:${NODE_PORT}

output

HTTP/1.1 200 OK
Server: nginx/1.15.8
Date: Sun, 20 Jan 2019 21:56:46 GMT
Content-Type: text/html
Content-Length: 612
Last-Modified: Tue, 25 Dec 2018 09:56:47 GMT
Connection: keep-alive
ETag: "5c21fedf-264"
Accept-Ranges: bytes
  • Untrusted Workloads

This section will verify the ability to run untrusted workloads using gVisor.

Create the untrusted pod:

cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Pod
metadata:
  name: untrusted
  annotations:
    io.kubernetes.cri.untrusted-workload: "true"
spec:
  containers:
    - name: webserver
      image: gcr.io/hightowerlabs/helloworld:2.0.0
EOF
  • Verification

In this section you will verify the untrusted pod is running under gVisor (runsc) by inspecting the assigned worker node.

Verify the untrusted pod is running:

output

NAME                      READY   STATUS    RESTARTS   AGE   IP           NODE       NOMINATED NODE
busybox-bd8fb7cbd-9bnbk   1/1     Running   0          27m   10.200.0.2   worker-0   <none>
nginx-dbddb74b8-9d2ch     1/1     Running   0          15m   10.200.0.3   worker-0   <none>
untrusted                 1/1     Running   0          67s   10.200.1.3   worker-1   <none>

Get the node name where the untrusted pod is running:

INSTANCE_NAME=$(kubectl get pod untrusted --output=jsonpath='{.spec.nodeName}')

SSH into the worker node:

gcloud compute ssh ${INSTANCE_NAME}

List the containers running under gVisor:

sudo runsc --root  /run/containerd/runsc/k8s.io list

output

I0120 22:01:35.289695   18966 x:0] ***************************
I0120 22:01:35.289875   18966 x:0] Args: [runsc --root /run/containerd/runsc/k8s.io list]
I0120 22:01:35.289950   18966 x:0] Git Revision: 50c283b9f56bb7200938d9e207355f05f79f0d17
I0120 22:01:35.290018   18966 x:0] PID: 18966
I0120 22:01:35.290088   18966 x:0] UID: 0, GID: 0
I0120 22:01:35.290158   18966 x:0] Configuration:
I0120 22:01:35.290212   18966 x:0]              RootDir: /run/containerd/runsc/k8s.io
I0120 22:01:35.290475   18966 x:0]              Platform: ptrace
I0120 22:01:35.290627   18966 x:0]              FileAccess: exclusive, overlay: false
I0120 22:01:35.290754   18966 x:0]              Network: sandbox, logging: false
I0120 22:01:35.290877   18966 x:0]              Strace: false, max size: 1024, syscalls: []
I0120 22:01:35.291000   18966 x:0] ***************************
ID                                                                 PID         STATUS      BUNDLE                                          CREATED                OWNER
353e797b41e3e5bdd183605258b66a153a61a3f7ff0eb8b0e0e7d8b6e4b3bc5c   18521       running     /run/containerd/io.containerd.runtime.v1.linux/k8s.io/353e797b41e3e5bdd183605258b66a153a61a3f7ff0eb8b0e0e7d8b6e4b3bc5c   0001-01-01T00:00:00Z
5b1e7bcf2a5cb033888650e49c3978cf429b99d97c4be5c7f5ad14e45b3015a9   18441       running     /run/containerd/io.containerd.runtime.v1.linux/k8s.io/5b1e7bcf2a5cb033888650e49c3978cf429b99d97c4be5c7f5ad14e45b3015a9   0001-01-01T00:00:00Z
I0120 22:01:35.294484   18966 x:0] Exiting with status: 0

Get the ID of the untrusted pod:

POD_ID=$(sudo crictl -r unix:///var/run/containerd/containerd.sock \
  pods --name untrusted -q)

Get the ID of the webserver container running in the untrusted pod:

CONTAINER_ID=$(sudo crictl -r unix:///var/run/containerd/containerd.sock \
  ps -p ${POD_ID} -q)

Use the gVisor runsc command to display the processes running inside the webserver container:

sudo runsc --root /run/containerd/runsc/k8s.io ps ${CONTAINER_ID}

output

I0120 22:04:59.988268   19220 x:0] ***************************
I0120 22:04:59.988443   19220 x:0] Args: [runsc --root /run/containerd/runsc/k8s.io ps 353e797b41e3e5bdd183605258b66a153a61a3f7ff0eb8b0e0e7d8b6e4b3bc5c]
I0120 22:04:59.988521   19220 x:0] Git Revision: 50c283b9f56bb7200938d9e207355f05f79f0d17
I0120 22:04:59.988604   19220 x:0] PID: 19220
I0120 22:04:59.988673   19220 x:0] UID: 0, GID: 0
I0120 22:04:59.988736   19220 x:0] Configuration:
I0120 22:04:59.988789   19220 x:0]              RootDir: /run/containerd/runsc/k8s.io
I0120 22:04:59.988910   19220 x:0]              Platform: ptrace
I0120 22:04:59.989037   19220 x:0]              FileAccess: exclusive, overlay: false
I0120 22:04:59.989160   19220 x:0]              Network: sandbox, logging: false
I0120 22:04:59.989299   19220 x:0]              Strace: false, max size: 1024, syscalls: []
I0120 22:04:59.989431   19220 x:0] ***************************
UID       PID       PPID      C         STIME     TIME      CMD
0         1         0         0         21:58     10ms      app
I0120 22:04:59.990890   19220 x:0] Exiting with status: 0

Cleaning Up

In this lab you will delete the compute resources created during this tutorial.

  • Compute Instances

Delete the controller and worker compute instances:

gcloud -q compute instances delete \
  controller-0 controller-1 controller-2 \
  worker-0 worker-1 worker-2
  • Networking

Delete the external load balancer network resources:

  gcloud -q compute forwarding-rules delete kubernetes-forwarding-rule \
    --region $(gcloud config get-value compute/region)

  gcloud -q compute target-pools delete kubernetes-target-pool

  gcloud -q compute http-health-checks delete kubernetes

  gcloud -q compute addresses delete kubernetes-the-hard-way

Delete the kubernetes-the-hard-way firewall rules:

gcloud -q compute firewall-rules delete \
  kubernetes-the-hard-way-allow-nginx-service \
  kubernetes-the-hard-way-allow-internal \
  kubernetes-the-hard-way-allow-external \
  kubernetes-the-hard-way-allow-health-check

Delete the kubernetes-the-hard-way network VPC:

  gcloud -q compute routes delete \
    kubernetes-route-10-200-0-0-24 \
    kubernetes-route-10-200-1-0-24 \
    kubernetes-route-10-200-2-0-24

  gcloud -q compute networks subnets delete kubernetes

  gcloud -q compute networks delete kubernetes-the-hard-way

Homework 20 (logging-1)

Build Status

  • Создание 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-open-port 5601/tcp \
  --google-open-port 9292/tcp \
  --google-open-port 9411/tcp \
logging 
  • Переключение на созданную docker-machine: eval $(docker-machine env logging)

  • Узнать ip-адрес: docker-machine ip logging

  • Новая версия приложения reddit

  • Сборка образов:

for i in ui post-py comment; do cd src/$i; bash docker_build.sh; cd -; done

или:

/src/ui $ bash docker_build.sh && docker push $USER_NAME/ui
/src/post-py $ bash docker_build.sh && docker push $USER_NAME/post
/src/comment $ bash docker_build.sh && docker push $USER_NAME/comment
  • Отдельный compose-файл для системы логирования:
docker/docker-compose-logging.yml

version: '3.5'
services:
  fluentd:
    image: ${USERNAME}/fluentd
    ports:
      - "24224:24224"
      - "24224:24224/udp"
  elasticsearch:
    image: elasticsearch
    expose:
      - 9200
    ports:
      - "9200:9200"
  kibana:
    image: kibana
    ports:
      - "5601:5601"
  • Fluentd - инструмент для отправки, агрегации и преобразования лог-сообщений:
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 #плагин <i>in_forward</i> для приема логов
  port 24224
  bind 0.0.0.0
</source>
<match *.**>
  @type copy #плагин copy
  <store>
    @type elasticsearch #для перенаправления всех входящих логов в elasticseacrh
    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>
    @type stdout #а также в stdout
  </store>
</match>
  • Сборка образа fluentd: docker build -t $USER_NAME/fluentd .

  • Просмотр логов post сервиса: docker-compose logs -f post

  • Драйвер для логирования для сервиса post внутри compose-файла:

docker/docker-compose.yml

version: '3.5'
services:
  post:
    image: ${USER_NAME}/post
    environment:
      - POST_DATABASE_HOST=post_db
      - POST_DATABASE=posts
    depends_on:
      - post_db
    ports:
      - "5000:5000"
    logging:
      driver: "fluentd"
      options:
        fluentd-address: localhost:24224
        tag: service.post
  • Запуск инфраструктуры централизованной системы логирования и перезапуск сервисов приложения:
docker-compose -f docker-compose-logging.yml up -d
docker-compose down
docker-compose up -d 
  • Kibana будет доступна по адресу http://logging-ip:5061. Необходимо создать индекс паттерн fluentd-*
  • Поле log документа elasticsearch содержит в себе JSON-объект. Необходимо выделить эту информацию в поля, чтобы иметь возможность производить по ним поиск. Это достигается за счет использования фильтров для выделения нужной информации
  • Добавление фильтра для парсинга json-логов, приходящих от post-сервиса, в конфиг fluentd:
logging/fluentd/fluent.conf

<source>
  @type forward
  port 24224
  bind 0.0.0.0
</source>
<filter service.post>
  @type parser
  format json
  key_name log
</filter>
<match *.**>
  @type copy
...
  • Пересборка образа и перезапуск сервиса fluentd:
logging/fluentd $ docker build -t $USER_NAME/fluentd .
docker/ $ docker-compose -f docker-compose-logging.yml up -d fluentd 
  • По аналогии с post-сервисом необходимо для ui-сервиса определить драйвер для логирования fluentd в compose-файле:
docker/docker-compose.yml

...
logging:
  driver: "fluentd"
  options:
    fluentd-address: localhost:24224
    tag: service.post
...
  • Перезапуск ui сервиса:
docker-compose stop ui
docker-compose rm ui
docker-compose up -d 
  • Когда приложение или сервис не пишет структурированные логи, используются регулярные выражения для их парсинга. Выделение информации из лога UI-сервиса в поля:
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>
  • Для облегчения задачи парсинга вместо стандартных регулярок можно использовать grok-шаблоны. Grok - это именованные шаблоны регулярных выражений. Можно использовать готовый regexp, сославшись на него как на функцию:
docker/fluentd/fluent.conf

...
<filter service.ui>
  @type parser
  format grok
  grok_pattern %{RUBY_LOGGER}
  key_name log
</filter> 
...
  • Часть логов нужно еще распарсить. Для этого можно использовать несколько Grok-ов по очереди:
docker/fluentd/fluent.conf

<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>

<filter service.ui>
  @type parser
  format grok
  grok_pattern service=%{WORD:service} \| event=%{WORD:event} \| path=%{GREEDYDATA:path} \| request_id=%{GREEDYDATA:request_id} \| remote_addr=%{IP:remote_addr} \| method= %{WORD:method} \| response_status=%{WORD:response_status}
  key_name message
  reserve_data true
</filter>

Homework 19 (monitoring-2)

Build Status

  • compose-monitoring.yml - для мониторинга приложений Для запуска использовать: docker-compose -f docker-compose-monitoring.yml up -d

  • cAdvisor - для наблюдения за состоянием Docker-контейнеров (использование CPU, памяти, объем сетевого трафика) Сервис помещен в одну сеть с Prometheus, для сбора метрик с cAdvisor'а

  • В Prometheus добавлена информация о новом севрисе:

- job_name: 'cadvisor'
  static_configs:
    - targets:
      - 'cadvisor:8080' 

После внесения изменений необходима пересборка образа:

cd monitoring/prometheus
docker build -t $USER_NAME/prometheus .
  • Запуск сервисов:
docker-compose up -d
docker-compose -f docker-compose-monitoring.yml up -d 
  • Информация из cAdvisor будет доступна по адресу http://docker-machine-host-ip:8080

  • Данные также собираются в Prometheus

  • Для визуализации данных следует использовать Graphana:

services:
...
  grafana:
    image: grafana/grafana:5.0.0
    volumes:
      - grafana_data:/var/lib/grafana
    environment:
      - GF_SECURITY_ADMIN_USER=admin
      - GF_SECURITY_ADMIN_PASSWORD=secret
    depends_on:
      - prometheus
    ports:
      - 3000:3000
volumes:
  grafana_data:
  • Запуск: docker-compose -f docker-compose-monitoring.yml up -d grafana

  • Grapahana доступна по адресу: http://docker-mahine-host-ip:3000

  • Настройка источника данных в Graphana:

Type: Prometheus
URL: http://prometheus:9090
Access: proxy
  • Для сбора информации о post сервисе необходимо добавить информацию в файл prometheus.yml, чтобы Prometheus начал собирать метрики и с него:
scrape_configs:
...
  - job_name: 'post'
  static_configs:
   - targets:
     - 'post:5000'
  • Пересборка образа:
export USER_NAME=username
docker build -t $USER_NAME/prometheus .
  • Пересоздание Docker инфраструктуры мониторинга:
docker-compose -f docker-compose-monitoring.yml down
docker-compose -f docker-compose-monitoring.yml up -d 
  • Загруженные файл дашбоардов расположены в директории monitoring/grafana/dashboards/

  • Alertmanager - дополнительный компонент для системы мониторинга Prometheus

  • Сборка образа для alertmanager'а - файл monitoring/alertmanager/Dockerfile:

FROM prom/alertmanager:v0.14.0
ADD config.yml /etc/alertmanager/ 
  • Содержимое файла config.yml:
global:
  slack_api_url: 'https://hooks.slack.com/services/$token/$token/$token'
route:
  receiver: 'slack-notifications'
receivers:
- name: 'slack-notifications'
  slack_configs:
  - channel: '#userchannel'
  • Сервис алертинга в docker-compose-monitoring.yml:
services:
...
  alertmanager:
    image: ${USER_NAME}/alertmanager
    command:
      - '--config.file=/etc/alertmanager/config.yml'
  ports:
    - 9093:9093 
  • Файл с условиями, при которых должен срабатывать алерт и посылаться Alertmanager'у: monitoring/prometheus/alerts.yml
  • Простой алерт, который будет срабатывать в ситуации, когда одна из наблюдаемых систем (endpoint) недоступна для сбора метрик (в этом случае метрика up с лейблом instance равным имени данного endpoint'а будет равна нулю):
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'
  • Файл alerts.yml также должен быть скопирован в сервис prometheus:
ADD alerts.yml /etc/prometheus/
  • Пересборка образа prometheus:
docker build -t $USER_NAME/prometheus .
  • Пересоздание Docker инфраструктуры мониторинга:
docker-compose down -d
docker-compose -f docker-compose-monitoring.yml down
docker-compose up -d
docker-compose -f docker-compose-monitoring.yml up -d 
  • Пуш всех образов в dockerhub:
docker login
docker push $USER_NAME/ui
docker push $USER_NAME/comment
docker push $USER_NAME/post
docker push $USER_NAME/prometheus
docker push $USER_NAME/alertmanager 

Ссылка на собранные образы на DockerHub

Homework 18 (monitoring-1)

Build Status

  • Правило фаервола для Prometheus и Puma:
gcloud compute firewall-rules create prometheus-default --allow tcp:9090
gcloud compute firewall-rules create puma-default --allow tcp:9292 
  • Создание docker-host:
export GOOGLE_PROJECT=_project_id_

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-host:
eval $(docker-machine env docker-host)
  • Запуск Prometheus из готового образа с DockerHub:
docker run --rm -p 9090:9090 -d --name prometheus  prom/prometheus

Prometheus будет запущен по адресу http://docker-host-ip:9090/ Узнать docker-host-ip можно командой: docker-machine ip docker-host

  • Остановка контейнера: docker stop prometheus

  • Файл monitoring/prometheus/Dockerfile:

FROM prom/prometheus:v2.1.0
ADD prometheus.yml /etc/prometheus/

Файл monitoring/prometheus/prometheus.yml:

---
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'
  • Сборка docker-образа:
export USER_NAME=username
docker build -t $USER_NAME/prometheus .
  • Сборка образов при помощи скриптов docker_build.sh в директории каждого сервиса:
cd src/ui & ./docker_build.sh
cd src/post-py & ./docker_build.sh
cd src/comment & ./docker_build.sh
  • Добавление сервиса Prometheus в docker/Dockerfile:
  prometheus:
    image: ${USERNAME}/prometheus
    ports:
      - '9090:9090'
    volumes:
      - prometheus_data:/prometheus
    command:
      - '--config.file=/etc/prometheus/prometheus.yml'
      - '--storage.tsdb.path=/prometheus'
      - '--storage.tsdb.retention=1d'
    networks:
      - back_net
      - front_net
  • Запуск микросервисов:
docker-compose up -d 
services:
...
  node-exporter:
    image: prom/node-exporter:v0.15.2
    user: root
    volumes:
      - /proc:/host/proc:ro
      - /sys:/host/sys:ro
      - /:/rootfs:ro
    command:
      - '--path.procfs=/host/proc'
      - '--path.sysfs=/host/sys'
      - '--collector.filesystem.ignored-mount-points="^/(sys|proc|dev|host|etc)($$|/)"'

Job для Prometheus (prometheus.yml):

  - job_name: 'node'
    static_configs:
      - targets:
        - 'node-exporter:9100'
  • Пересоздание образов:
cd /monitoring/prometheus && docker build -t $USER_NAME/prometheus .

Пересоздание сервисов:

docker-compose down
docker-compose up -d 
  • Push образов на DockerHub:
docker login
docker push $USER_NAME/ui:1.0
docker push $USER_NAME/comment:1.0
docker push $USER_NAME/post:1.0
docker push $USER_NAME/prometheus

Ссылка на собранные образы на DockerHub

Homework 17 (gitlab-ci-2)

Build Status

  • Создание нового проекта example2

  • Добавление проекта в username_microservices

git checkout -b gitlab-ci-2
git remote add gitlab2 http://vm-ip/homework/example2.git
git push gitlab2 gitlab-ci-2
  • Dev-окружение: Изменение пайплайна таким образом, чтобы job deploy стал определением окружения dev, на которое условно будет выкатываться каждое изменение в коде проекта:
  1. Переименуем deploy stage в review
  2. deploy_job заменим на deploy_dev_job
  3. Добавим environment
name: dev
url: http://dev.example.com

В разделе Operations - Environment появится окружение dev

  • Два новых этапа: stage и production. Stage будет содержать job, имитирующий выкатку на staging окружение, production - на production окружение. Job будут запускаться с кнопки

  • Директива only описывает список условий, которые должны быть истинны, чтобы job мог запуститься. Регулярное выражение /^\d+\.\d+\.\d+/ означает, что должен стоять semver тэг в git, например, 2.4.10

  • Пометка текущего коммита тэгом:

git tag 2.4.10
  • Пуш с тэгами:
git push gitlab2 gitlab-ci-2 --tags
  • Динамические окружения позволяет вам иметь выделенный стенд для каждой feature-ветки в git. Определяются динамические окружения с помощью переменных, доступных в .gitlab-ci.yml. Job определяет динамическое окружение для каждой ветки в репозитории, кроме ветки master
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

Homework 16 (gitlab-ci-1)

Build Status

  • Установка Docker:
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
add-apt-repository "deb https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable"
apt-get update
apt-get install docker-ce docker-compose
  • Подготовка окружения:
mkdir -p /srv/gitlab/config /srv/gitlab/data /srv/gitlab/logs
cd /srv/gitlab/
touch docker-compose.yml

docker-compose.yml:

web:
  image: 'gitlab/gitlab-ce:latest'
  restart: always
  hostname: 'gitlab.example.com'
  environment:
    GITLAB_OMNIBUS_CONFIG: |
      external_url 'http://<VM-IP>'
  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'
  • Запуск Gitlab CI: docker-compose up -d

  • GUI GitLab: Отключение регистрации, создание группы проектов homework, создание проекта example

  • Добавление remote в проект microservices: git remote add gitlab http://<ip>/homework/example.git

  • Push в репозиторий: http://35.204.52.154/homework/example

  • Определение CI/CD Pipeline проекта производится в файле .gitlab-ci.yml:

stages:
  - build
  - test
  - deploy

build_job:
  stage: build
  script:
    - echo 'Building'

test_unit_job:
  stage: test
  script:
    - echo 'Testing 1'

test_integration_job:
  stage: test
  script:
    - echo 'Testing 2'

deploy_job:
  stage: deploy
  script:
    - echo 'Deploy'
  • Установка GitLab Runner:
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
  • Запуск runner'а: docker exec -it gitlab-runner gitlab-runner register

  • Добавление исходного кода в репозиторий:

git clone https://github.com/express42/reddit.git && rm -rf ./reddit/.git
git add reddit/
git commit -m 'Add reddit app'
git push gitlab gitlab-ci-1
  • Изменение описания пайплайна в .gitlab-ci.yml:
image: ruby:2.4.2
stages:
...
variables:
  DATABASE_URL: 'mongodb://mongo/user_posts'
before_script:
  - cd reddit
  - bundle install
...
test_unit_job:
  stage: test
  services:
    - mongo:latest
  script:
    - ruby simpletest.rb
...
  • В пайплайне выше добавлен вызов reddit/simpletest.rb:
require_relative './app'
require 'test/unit'
require 'rack/test'

set :environment, :test

class MyAppTest < Test::Unit::TestCase
  include Rack::Test::Methods

  def app
    Sinatra::Application
  end

  def test_get_request
    get '/'
    assert last_response.ok?
  end
end
  • Добавление библиотеки для тестирования в reddit/Gemfile: gem 'rack-test'

Homework 15 (docker-4)

Build Status

  • Подключение к docker-host: eval $(docker-machine env docker-host)

  • Запуск контейнера joffotron/docker-net-tools с набором сетевых утилит: docker run -ti --rm --network none joffotron/docker-net-tools -c ifconfig

Использован none-driver, вывод работы контейнера:

lo Link encap:Local Loopback
   inet addr:127.0.0.1  Mask:255.0.0.0
   UP LOOPBACK RUNNING  MTU:65536  Metric:1
   RX packets:0 errors:0 dropped:0 overruns:0 frame:0
   TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
   collisions:0 txqueuelen:1000
   RX bytes:0 (0.0 B)  TX bytes:0 (0.0 B)
  • Запуск контейнера в сетевом пространстве docker-хоста: docker run -ti --rm --network host joffotron/docker-net-tools -c ifconfig

Запуск ipconfig на docker-host'е приведет к аналогичному выводу: docker-machine ssh docker-host ifconfig

  • Запуст nginx в фоне в сетевом пространстве docker-host: docker run --network host -d nginx

При повторном выполнении команды получим ошибку:

2018/11/30 19:50:53 [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)

По причине того, что порт 80 уже занят.

  • Для просмотра существующих net-namespaces необходимо выполнить на docker-host: sudo ln -s /var/run/docker/netns /var/run/netns Просмотр: sudo ip netns
  • При запуске контейнера с сетью host net-namespace один - default.
  • При запуске контейнера с сетью none в спсике добавится id net-namespace. Вывод списка net-namespac'ов:
user@docker-host:~$ sudo ip net
88f8a9be77ca
default

Можно выполнить команду в выбранном net-namespace:

user@docker-host:~$ sudo ip netns exec 88f8a9be77ca ifconfig
lo  Link encap:Local Loopback  
  inet addr:127.0.0.1  Mask:255.0.0.0
  UP LOOPBACK RUNNING  MTU:65536  Metric:1
  RX packets:0 errors:0 dropped:0 overruns:0 frame:0
  TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
  collisions:0 txqueuelen:1000 
  RX bytes:0 (0.0 B)  TX bytes:0 (0.0 B)
  • Создание bridge-сети: docker network create reddit --driver bridge

  • Запуск проекта reddit с использованием bridge-сети:

docker run -d --network=reddit mongo:latest
docker run -d --network=reddit ozyab/post:1.0
docker run -d --network=reddit ozyab/comment:1.0
docker run -d --network=reddit -p 9292:9292 ozyab/ui:1.0

В данной конфигурации web-сервис puma не сможет подключиться к БД mongodb.

Сервисы ссылаются друг на друга по dns-именам, прописанным в ENV-переменных Dockerfil'а. Встроенный DNS docker'а ничего не знает об этих именах.

Присвоение контейнерам имен или сетевых алиасов при старте:

--name <name> (max 1 имя)
--network-alias <alias-name> (1 или более)
  • Запуск контейнеров с сетевыми алиасами:
docker run -d --network=reddit --network-alias=post_db --network-alias=comment_db mongo:latest
docker run -d --network=reddit --network-alias=post ozyab/post:1.0
docker run -d --network=reddit --network-alias=comment ozyab/comment:1.0
docker run -d --network=reddit -p 9292:9292 ozyab/ui:1.0
  • Запуск проекта в 2-х bridge сетях. Сервис ui не имеет доступа к базе данных.

Создание docker-сетей:

docker network create back_net --subnet=10.0.2.0/24
docker network create front_net --subnet=10.0.1.0/24

Запуск контейнеров:

docker run -d --network=front_net -p 9292:9292 --name ui ozyab/ui:1.0
docker run -d --network=back_net --name comment ozyab/comment:1.0
docker run -d --network=back_net --name post ozyab/post:1.0
docker run -d --network=back_net --name mongo_db  --network-alias=post_db --network-alias=comment_db mongo:latest 

Docker при инициализации контейнера может подключить к нему только 1 сеть, поэтому контейнеры comment и post не видят контейнер ui из соседних сетей.

Нужно поместить контейнеры post и comment в обе сети. Дополнительные сети подключаются командой: docker network connect <network> <container>:

docker network connect front_net post
docker network connect front_net comment 

Установка пакета bridge-utils:

docker-machine ssh docker-host
sudo apt-get update && sudo apt-get install bridge-utils

Выполнив docker network ls можно увидеть список виртуальных сетей docker'а. ifconfig | grep br покажет список bridge-интерфейсов:

br-45935d0f2bbf Link encap:Ethernet  HWaddr 02:42:6d:5a:8b:7e
br-45bbc0c70de1 Link encap:Ethernet  HWaddr 02:42:94:69:ab:35
br-b6342f9c65f2 Link encap:Ethernet  HWaddr 02:42:9a:b1:73:d9

Можно просмотреть информацию о каждом bridge-интерфейсе командой brctl show <interface>:

docker-user@docker-host:~$brctl show br-45935d0f2bbf
bridge name      bridge id          STP enabled   interfaces
br-45935d0f2bbf  8000.02426d5a8b7e  no            veth05b2946
                                                  veth2f50985
                                                  vetha882d28

veth-интерфейс - часть виртуальной пары интерфейсов, которая лежат в сетевом пространстве хоста и также отображаются в ifconfig. Вторые часть виртуального интерфейса находится внутри контейнеров.

Просмотр iptables: sudo iptables -nL -t nat

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  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
DNAT       tcp  --  0.0.0.0/0            0.0.0.0/0            tcp dpt:9292 to:10.0.1.2:9292

Последняя строка прокидывает порт 9292 внутрь контейнера.

Docker-compose

  • Файлу ./src/docker-compose.yml требуется переменная окружения USERNAME: export USERNAME=ozyab

Можно выполнить: docker-compose up -d

  • Изменить docker-compose под кейс с множеством сетей, сетевых алиасов
  • Файл .env - переменные для docker-compose.yml
  • Базовое имя создается по имени папки, в которой происходит запуск docker-compose.

Для задания базового имени проекта необходимо добавить переменную COMPOSE_PROJECT_NAME=dockermicroservices

Homework 14 (docker-3)

Build Status

Работа в папке src:

  • post-py - сервис отвечающий за написание постов
  • comment - сервис отвечающий за написание комментариев
  • ui - веб-интерфейс, работающий с другими сервисами
  • Сборка образов:
docker build -t ozyab/post:1.0 ./post-py
docker build -t ozyab/comment:1.0 ./comment
docker build -t ozyab/ui:1.0 ./ui
  • Отдельная 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 ozyab/post:1.0
docker run -d --network=reddit --network-alias=comment ozyab/comment:1.0
docker run -d --network=reddit -p 9292:9292 ozyab/ui:1.0
  • Остановка всех контейнеров: docker kill $(docker ps -q)
  • Создание Docker volume: docker volume create reddit_db
  • Запуск контейнеров с docker volume:
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 ozyab/post:1.0
docker run -d --network=reddit --network-alias=comment ozyab/comment:1.0
docker run -d --network=reddit -p 9292:9292 ozyab/ui:2.0
  • После перезапуска информация остается в базе

Homework 13 (docker-2)

Build Status

  • Работа с docker-machine:

docker-machine create <имя> - создание docker-хоста

eval $(docker-machine env <имя>) - перемеключение на docker-хост

eval $(docker-machine env --unset) - переключение на локальный docker

docker-machine rm <имя> - удаление docker-хоста

  • Создание docker-host:
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-host можно, выполнив: docker-machine ls
  • Запустим htop в docker'е: docker run --rm -ti tehbilly/htop

Будет виден только процесс htop.

Если выполнить docker run --rm --pid host -ti tehbilly/htop, то видны будут все процессы на хостовой машине

  • Добавлены:

Dockerfile - текстовое описание нашего образа

mongod.conf - подготовленный конфиг для mongodb

db_config - переменная окружения со ссылкой на mongodb

start.sh - скрипт запуска приложения

  • Сборка образа: docker build -t reddit:latest .
  • Запуск контейнера: docker run --name reddit -d --network=host reddit:latest
  • Создание правило на входящий порт 9292:
gcloud compute firewall-rules create reddit-app \
  --allow tcp:9292 \
  --target-tags=docker-machine \
  --description="Allow PUMA connections" \
  --direction=INGRESS 
  • Команды по работе с образом:

docker tag reddit:latest <login>/otus-reddit:1.0 - добавить тэг образу reddit

docker push <login>/otus-reddit:1.0 - отправка образа в registry

docker logs reddit -f - просмотр логов

docker inspect <login>/otus-reddit:1.0 - просмотр информации об образе

docker inspect <login>/otus-reddit:1.0 -f '{{.ContainerConfig.Cmd}}' - просмотр только определенной информации о контейнере

docker diff reddit - просмотр изменений, произошедних в файловой системе запущенного контейнера

Homework 12 (docker-1)

Build Status

  • Добавлен файл шаблона PR .github/PULL_REQUEST_TEMPLATE
  • Интеграция со slack выполняется командой /github subscribe Otus-DevOps-2018-09/ozyab09_microservices
  • Запуск контейнера: docker run hello-world
  • Список запущенных контейнеров: docker ps
  • Список всех контейнеров: docker ps -a
  • Создание и запуск контейнера: docker run -it ubuntu:16.04 /bin/bash
  • Команда run создает и запускает контейнер из image
  • start запускает остановленный созданный ранее контейнер
  • attach подсоединяет терминал к созданному контейнеру
  • docker system df отображает сколько дискового пространства занято образами, контейнерами и volume'ами
  • Создание образа из контейнера: docker commit <container_id> username/ubuntu-tmp-file