Заметки о развертывании Ruby on Rails Deployment в Google Cloud Kubernetes Engine

http://www.akitaonrails.com/2018/01/09/my-notes-about-a-production-grade-ruby-on-rails-deployment-on-google-cloud-kubernetes-engine
  • Перевод


Я использую Google Cloud с Kubernetes Engine в течение 2 месяцев. На самом деле мне не понадобилось и месяца, чтобы уложить все в голове, но потребовалось еще столько же, чтобы разобраться с некоторыми неприятностями.


TL;DR: Google делает довольно хорошую работу, поэтому AWS не расслабляется. Если вы хорошо знаете AWS, я бы посоветовал протестировать Google Cloud. Возможно, из-за мышечной памяти мне было бы комфортнее с AWS, но я изучил Google Cloud и Kubernetes и уверен в них для большинства моих сценариев.


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


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


Чтобы добиться успеха с чем-то вроде Google Cloud и Kubernetes, у вас должно быть достаточно опыта. Если вы никогда не устанавливали Linux From Scratch, если никогда не проводили оптимизацию сервера, если вам не нравятся железные серверные компоненты, не пытайтесь выполнить реальное производственное развертывание. Ваша самая безопасная ставка по-прежнему будет на Heroku.


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


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


Вот что нужно было мне:


  • Масштабируемый уровень веб-приложений, где я мог бы выполнять как скользящие обновления (для нулевых обновлений простоя), так и автоматическое и ручное горизонтальное масштабирование серверов.
  • Монтируемое постоянное хранилище с автоматическими моментальными снимками / резервными копиями.
  • Управляемая надежная база данных (Postgresql) с автоматическим резервным копированием и простой репликацией в экземпляры только для чтения.
  • Управляемое решение для хранения секретов (таких как поддержка ENV Heroku). Никогда не храните производственную конфигурацию в исходном коде.
  • Поддержка изображений Docker без необходимости создания настраиваемой инфраструктуры для развертывания.
  • Статические внешние IP-адреса для интеграции, требующие фиксированного IP-адреса.
  • SSL-прекращение, чтобы я мог подключиться к CloudFlare (CDN является обязательным, но недостаточным. В 2018 году нам нужна будет защита от DDoS).
  • Достаточно безопасности по умолчанию, то есть теоретически все заблокировано, пока я не решу открыть.
  • Высокая доступность в различных регионах и зонах центров обработки данных.

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


Некоторые проблемы новичков:


  • Документация очень обширна, и вы найдете почти все, если знаете, что ищете. Также имейте в виду, что Azure и AWS реализуют Kubernetes с определенными отличиями, поэтому некоторые документы не применяются к Google Cloud и наоборот.
  • В альфа-, бета- и стабильных стадиях есть много особенностей. Документация, вроде бы, понятна, но большинство учебных пособий, которым всего лишь пару месяцев, могут работать не так, как предполагалось (Kubernetes 1.8.4-gke в их числе).
  • Существует целый набор слов, которые относятся к одному и тому же понятию. Нужно привыкнуть к лексике.
  • Представьте, что вы играете в лего. Детали можно мешать, по-разному сочетать, но очень легко все испортить. Это значит, что вы можете создать свою конфигурацию, которая будет отвечать вашим требованиям. Но нельзя ее просто скопировать.
  • Вы можете сделать почти все через YAML файлы и командную строку, но повторное использование конфигурации (для рабочей и промежуточной среды, например) не тривиальная задача. Существуют сторонние инструменты, которые имеют дело с параметризуемыми и многоразовыми битами YAML, но я бы сделал все это вручную. Никогда, никогда не пробуйте автоматизированные шаблоны в инфраструктуре, не зная точно, что они делают.
  • У вас есть два тяжелых инструмента командной строки: gcloud и kubectl.

Еще раз напомню: это не пошаговое руководство. Я аннотирую несколько шагов.


Масштабируемый веб-уровень (само веб-приложение)


Первое, что вам необходимо, – полное 12-factors app.


Будь то Ruby on Rails, Django, Laravel, Node.js (что угодно), это должно быть приложение, которое не имеет общего доступа и не зависит от записи чего-либо в локальной файловой системе. Приложение, которое вы можете легко отключать и запускать экземпляры независимо. Не должно быть сессий в локальной памяти или в локальных файлах (я предпочитаю избегать близости сессии). Отсутствует загрузка файлов в локальную файловую систему (если необходимо, вам нужно смонтировать внешнее постоянное хранилище), всегда предпочитайте отправлять двоичные потоки в управляемые службы хранения.
У вас должен быть надлежащий конвейер, который выводит кеширование через фингерпринтные активы (нравится вам это или нет, Rails по-прежнему имеет лучшее из готовых решений в Asset Pipeline).
Проинструментируйте приложение, добавьте New Relic RPM и Rollbar.
2018 год, вы не хотите разворачивать собственный код с помощью SQL-инъекции (или любой другой входной), неконтролируемый eval вокруг вашего кода, нет места для CSRF или XSS и т. д. Двигайтесь вперед, купите лицензию на Brakeman Pro и добавьте его в конвейер CI. Я могу подождать…
Поскольку это не учебник, предполагаю, что вы можете зарегистрироваться в Google Cloud и настроить проект, свой регион и зону.
Мне потребовалось некоторое время, чтобы понять первичную структуру в Google Cloud:


  • Вы начинаете с проекта, который служит прикрытием для всего необходимого для вашего приложения.
  • Затем вы создаете «кластеры». Например, у вас может быть производственный, промежуточный кластер, веб-кластер или разделенный кластер служб для несетевых материалов.
  • У кластера есть «кластер-мастер», который является контроллером всего остального (команды gcloud и kubectl взаимодействуют API-интерфейсами).
  • У кластера множество «экземпляров узлов», правильных «машин» (или, точнее, экземпляров VM).
  • Каждый кластер также имеет по крайней мере один «пул узлов» («пул по умолчанию»), который представляет собой набор экземпляров узлов с одинаковой конфигурацией и тем же «машинным типом».
  • Наконец, каждый экземпляр узла запускает один или несколько «контейнеров», которые представляют собой простые контейнеры, такие как LXC. Вот где ваше приложение на самом деле.
    Пример создания кластера:

gcloud container clusters create my-web-production \
--enable-cloud-logging \
--enable-cloud-monitoring \
--machine-type n1-standard-4 \
--enable-autoupgrade \
--enable-autoscaling --max-nodes=5 --min-nodes=2 \
--num-nodes 2

Как я уже упоминал, кластер создает default-pool с машинным типом n1-standard-4. Выберите комбинацию CPU/RAM, которая вам понадобится для приложения. Тип, который я выбрал, имеет 4 vCPU и 15 ГБ оперативной памяти.


По умолчанию он начинается с 3 узлов, поэтому я сначала выбрал 2, но автоматически масштабировался до 5 (вы можете обновить его позже, если нужно, но убедитесь, что есть место для первоначального роста). Вы можете продолжать добавлять дополнительные пулы для экземпляров узлов разного размера, скажем, для рабочих Sidekiq, чтобы выполнять интенсивную обработку фона. Затем создайте отдельный пул узлов с другим машинным типом для своего набора экземпляров, например:


gcloud container node-pools create large-pool \
--cluster=my-web-production \
--node-labels=pool=large \
--machine-type=n1-highcpu-8 \
--num-nodes 1

Этот другой пул управляет 1 узлом типа n1-highcpu-8, который имеет 8 vCPU с 7,2 ГБ ОЗУ. Больше процессоров, меньше памяти. У вас есть категория highmem, которая меньше CPU с гораздо большим объемом памяти. Опять же, нужно знать, чего вы хотите.


Важный момент здесь – --node-labels – как я буду сопоставлять развертывание для выбора между пулами узлов (в данном случае между пулом по умолчанию и большим пулом).


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


gcloud container clusters get-credentials my-web-production


Команда задает kubectl. Если у вас более одного кластера (my-web-production
и my-web-staging), нужно быть осторожными сget-credentials для правильного кластера, иначе вы можете запустить промежуточную развертывание на производственном кластере.


Поскольку это сбивает с толку, я изменил свой ZSH PROMPT, чтобы всегда видеть, с каким кластером сталкиваюсь. Я адаптировался из zsh-kubectl-prompt:



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


Как вы разворачиваете приложение в подах в экземплярах узлов?


У вас должен быть файл Docker в репозитории проектов приложений для создания образа Docker. Это один из примеров приложения Ruby on Rails:


FROM ruby:2.4.3
ENV RAILS_ENV production
ENV SECRET_KEY_BASE xpto
RUN curl -sL https://deb.nodesource.com/setup_8.x | bash -
RUN apt-get update && apt-get install -y nodejs postgresql-client cron htop vim
ADD Gemfile* /app/
WORKDIR /app
RUN gem update bundler --pre
RUN bundle install --without development test
RUN npm install
ADD . /app
RUN cp config/database.yml.prod.example config/database.yml && cp config/application.yml.example config/application.yml
RUN RAILS_GROUPS=assets bundle exec rake assets:precompile

В Google Cloud Web Console вы найдете «Container Registry», который является Private Docker Registry.


Добавьте удаленный URL-адрес в локальную конфигурацию следующим образом:


git remote add gcloud https://source.developers.google.com/p/my-project/r/my-app


Теперь вы можете git push gcloud master. Я рекомендую добавить триггеры, чтобы помечать изображения. Я добавляю 2 триггера: один, чтобы пометить его latest, а другой – пометить его случайным номером версии. Вам это понадобится позже.


После добавления репозитория реестра в качестве удаленного в вашей конфигурации git (git remote add) нажмите на него. Он должен начать создавать образ Docker с соответствующими тегами, которые вы настроили с помощью триггеров.


Убедитесь, что ваше приложение Ruby on Rails не имеет ничего в инициализаторах, которым требуется подключение к базе данных, поскольку оно недоступно. Вы можете застрять, когда сборка Docker завершится с ошибкой из-за того, что assets:precompile — задача загрузила инициализатор, который случайно вызывает модель, а это вызывает триггеры ActiveRecord :: Base для попытки подключиться.


Убедитесь, что версия Ruby в файле Dockerfile совпадает с версией в Gemfile, иначе она также потерпит неудачу.


Вы заметили странную config/application.yml. Это от фигаро. Рекомендую упростить настройку переменной ENV в системе. Мне не нравятся секреты Rails, и это не совсем дружественно по отношению к системам развертывания после того, как Heroku сделал ENV vars повсеместным. Придерживайтесь ENV vars. Kubernetes поблагодарит вас за это.


Теперь вы можете переопределить любую переменную окружения из файла развертывания Kubernetes и yaml. Сейчас самое время показать пример. Вы можете назвать его deploy / web.yml или как-то, как вам подходит. И, конечно же, проверьте его в репозитории исходного кода.


kind: Deployment
apiVersion: apps/v1beta1
metadata:
  name: web
spec:
  strategy:
    type: RollingUpdate
    rollingUpdate:
    maxSurge: 1
    maxUnavailable: 1
  minReadySeconds: 10
  replicas: 2
  template:
    metadata:
    labels:
        app: web
    spec:
    containers:
        - image: gcr.io/my-project/my-app:latest
        name: my-app
          imagePullPolicy: Always
        ports:
        - containerPort: 4001
        command: ["passenger", "start", "-p", "4001", "-e", "production",
          "--max-pool-size", "2", "--min-instances", "2", "--no-friendly-error-pages"
          "--max-request-queue-time", "10", "--max-request-time", "10",
          "--pool-idle-time", "0", "--memory-limit", "300"]
        env:
            - name: "RAILS_LOG_TO_STDOUT"
            value: "true"
            - name: "RAILS_ENV"
            value: "production"
            # ... obviously reduced the many ENV vars for brevity
            - name: "REDIS_URL"
              valueFrom:
                secretKeyRef:
                  name: my-env
                key: REDIS_URL
            - name: "SMTP_USERNAME"
              valueFrom:
                secretKeyRef:
                  name: my-env
                key: SMTP_USERNAME
            - name: "SMTP_PASSWORD"
              valueFrom:
                secretKeyRef:
                name: my-env
                key: SMTP_PASSWORD
            # ... this part below is mandatory for Cloud SQL
            - name: DB_HOST
            value: 127.0.0.1
            - name: DB_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: cloudsql-db-credentials
                key: password
            - name: DB_USER
              valueFrom:
                secretKeyRef:
                  name: cloudsql-db-credentials
                key: username

        - image: gcr.io/cloudsql-docker/gce-proxy:latest
        name: cloudsql-proxy
        command: ["/cloud_sql_proxy", "--dir=/cloudsql",
                    "-instances=my-project:us-west1:my-db=tcp:5432",
                    "-credential_file=/secrets/cloudsql/credentials.json"]
          volumeMounts:
            - name: cloudsql-instance-credentials
              mountPath: /secrets/cloudsql
              readOnly: true
            - name: ssl-certs
              mountPath: /etc/ssl/certs
            - name: cloudsql
              mountPath: /cloudsql
    volumes:
        - name: cloudsql-instance-credentials
        secret:
            secretName: cloudsql-instance-credentials
        - name: ssl-certs
        hostPath:
            path: /etc/ssl/certs
        - name: cloudsql
        emptyDir:

Поясню пример:


  • kind и apiVersion – важны, следите за документацией, она может измениться. Это то, что называется Deployment. Раньше был контроллер репликации (вы найдете его в старых учебниках), но он больше не используется. Рекомендация: изпользуйте ReplicaSet.
  • Называйте вещи своими именами, у вас есть metadata:name с web. Обратите внимание на spec:template:metadata:labels, где я помещаю каждый блок с меткой app: web. Она понадобится, чтобы иметь возможность выбирать элементы позже в разделе «Сервис».
  • У меня есть spec:strategy, в которой мы настраиваем Rolling Update.
  • spec:replicas объявляет, сколько подов я хочу. Вам придется вручную вычислить машинный тип пула узлов, а затем разделить ресурсы сервера для каждого приложения.
  • Изображение Docker, которое мы создали, с «последним» тегом. Вы ссылаетесь на него в spec: template: spec: container: image.
  • Я использую Passenger с конфигурацией производства (ознакомьтесь с документацией Phusion)
  • В spec: template: spec: container: env section я могу переопределить VAR ENV с настоящими производственными секретами. Я могу кодировать значения или использовать это приспособление:

- name: "SMTP_USERNAME"
  valueFrom:
    secretKeyRef:
      name: my-env
      key: SMTP_USERNAME

Это ссылка на «секретное» хранилище, которое я назвал my-env. И вот как вы создаете свой собственный:


kubectl create secret generic my-env \
--from-literal=REDIS_URL=redis://foo.com:18821 \
--from-literal=SMTP_USERNAME=foobar

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


Как я уже говорил, я бы предпочел использовать управляемую услугу для базы данных. Вы можете загрузить свой образ Docker, но я не рекомендую. То же самое касается других сервисов, подобных базам данных, таких как Redis, Mongo. Если вы используете AWS, то имейте в виду: Google Cloud SQL похож на RDS.


После создания экземпляра PostgreSQL вы не сможете получить к нему доступ непосредственно из веб-приложения. У вас есть шаблон для второго изображения Docker, «CloudSQL Proxy».


Для этого необходимо сначала создать учетную запись службы:


gcloud sql users create proxyuser host --instance=my-db --password=abcd1234

После создания экземпляра PostgreSQL вам будет предложено загрузить учетные данные JSON. Сохраните их где-нибудь. Надеюсь, я не должен напоминать о необходимости надежного пароля. Нужно создать дополнительные секреты:


kubectl create secret generic cloudsql-instance-credentials \
--from-file=credentials.json=/home/myself/downloads/my-db-12345.json

kubectl create secret generic cloudsql-db-credentials \
--from-literal=username=proxyuser --from-literal=password=abcd1234

Они упоминаются в этой части развертывания:


- image: gcr.io/cloudsql-docker/gce-proxy:latest
  name: cloudsql-proxy
  command: ["/cloud_sql_proxy", "--dir=/cloudsql",
            "-instances=my-project:us-west1:my-db=tcp:5432",
            "-credential_file=/secrets/cloudsql/credentials.json"]
  volumeMounts:
    - name: cloudsql-instance-credentials
      mountPath: /secrets/cloudsql
      readOnly: true
    - name: ssl-certs
      mountPath: /etc/ssl/certs
    - name: cloudsql
      mountPath: /cloudsql
volumes:
- name: cloudsql-instance-credentials
  secret:
    secretName: cloudsql-instance-credentials
- name: ssl-certs
  hostPath:
    path: /etc/ssl/certs
- name: cloudsql
  emptyDir:

Вы должны добавить имя базы данных (my-db в нашем случае) в -instance в команде.


Кстати, gce-proxy:latest относится к версии 1.09, когда уже существовала версия 1.11. Новая версия создала головную боль обрывом соединения и продлением времени ожидания. Поэтому я вернулся к более поздней версии – 1.09, и все наладилось. Так что не все новое хорошо. В инфраструктуре лучше придерживаться стабильности.


You may also want the option to load a separated CloudSQL instance instead of having it in each pod, so the pods could connect to just one proxy. You may want to read this thread on the subject.


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


Кажется, ничто не подвергается воздействию. Поэтому нужно разоблачить поды через Node Port Service. Давайте создадим файл deploy/web-svc.yamlfile.


apiVersion: v1
kind: Service
metadata:
  name: web-svc
spec:
  sessionAffinity: None
  ports:
  - port: 80
    targetPort: 4001
    protocol: TCP
  type: NodePort
  selector:
    app: web

Вот почему я подчеркнул важность spec:template:metadata:labels. Мы будем использовать его в spec:selector, чтобы выбрать правильные контейнеры.


Теперь мы можем развернуть эти 2 пода следующим образом:


kubectl create -f deploy/web.yml
kubectl create -f deploy/web-svc.yml

Вы видите, что поды созданы с помощью kubectl get pods --watch.


Балансировка нагрузки


Во многих учебниках поды предоставляются через другой сервис, который называется Load Balancer. Я не уверен, насколько хорошо он ведет себя под давлением, есть ли у него завершение SSL и т. д. Я решил включиться на полную мощность с Ingress Load Balancer при помощи контроллера NGINX.


Прежде всего я решил создать для него отдельный пул узлов, например:


gcloud container node-pools create web-load-balancer \
--cluster=my-web-production \
--node-labels=role=load-balancer \
--machine-type=g1-small \
--num-nodes 1 \
--max-nodes 3 --min-nodes=1 \
--enable-autoscaling

Как только создан пример large-pool, позаботьтесь о добавлении --node-labels, чтобы установить контроллер вместо default-pool. Нужно знать имя экземпляра узла, и мы можем сделать это следующим образом:


$ gcloud compute instances list
NAME                                             ZONE        MACHINE_TYPE   PREEMPTIBLE  INTERNAL_IP  EXTERNAL_IP      STATUS
gke-my-web-production-default-pool-123-123       us-west1-a  n1-standard-4               10.128.0.1   123.123.123.12   RUNNING
gke-my-web-production-large-pool-123-123         us-west1-a  n1-highcpu-8                10.128.0.2   50.50.50.50      RUNNING
gke-my-web-production-web-load-balancer-123-123  us-west1-a  g1-small                    10.128.0.3   70.70.70.70      RUNNING

Сохраним его:


export LB_INSTANCE_NAME=gke-my-web-production-web-load-balancer-123-123


Вы можете вручную зарезервировать внешний IP-адрес и присвоить ему имя:


gcloud compute addresses create ip-web-production \
        --ip-version=IPV4 \
        --global

Предположим, что он сгенерировал зарезервированный IP-адрес "111.111.111.111". Сохраним его:


export LB_ADDRESS_IP=$(gcloud compute addresses list | grep "ip-web-production" | awk '{print $3}')

Cвяжем адрес с экземпляром узла балансировки нагрузки:


export LB_INSTANCE_NAT=$(gcloud compute instances describe $LB_INSTANCE_NAME | grep -A3 networkInterfaces: | tail -n1 | awk -F': ' '{print $2}')
gcloud compute instances delete-access-config $LB_INSTANCE_NAME \
    --access-config-name "$LB_INSTANCE_NAT"
gcloud compute instances add-access-config $LB_INSTANCE_NAME \
    --access-config-name "$LB_INSTANCE_NAT" --address $LB_ADDRESS_IP

Добавим остальную конфигурацию Ingress Deployment. Это может занять много времени, но в основном мы будем идти по шаблону. Начнем с определения другого веб-приложения, называющегося default-http-backend. Оно будет использоваться для ответа на запросы HTTP, если веб-контейнеры по какой-либо причине недоступны. Назовем его deploy / default-web.yml:


apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: default-http-backend
spec:
  replicas: 1
  template:
    metadata:
      labels:
        app: default-http-backend
    spec:
      terminationGracePeriodSeconds: 60
      containers:
      - name: default-http-backend
        # Any image is permissable as long as:
        # 1. It serves a 404 page at /
        # 2. It serves 200 on a /healthz endpoint
        image: gcr.io/google_containers/defaultbackend:1.0
        livenessProbe:
          httpGet:
            path: /healthz
            port: 8080
            scheme: HTTP
          initialDelaySeconds: 30
          timeoutSeconds: 5
        ports:
        - containerPort: 8080
        resources:
          limits:
            cpu: 10m
            memory: 20Mi
          requests:
            cpu: 10m
            memory: 20Mi

Не надо ничего менять. Теперь вы знакомы с шаблоном развертывания. Нужно разоблачить шаблон через NodePort, поэтому давайте добавим deploy/default-web-svc.yml:


kind: Service
apiVersion: v1
metadata:
  name: default-http-backend
spec:
  selector:
    app: default-http-backend
  ports:
    - protocol: TCP
      port: 80
      targetPort: 8080
  type: NodePort

Снова ничего не меняем. Следующие 3 файла являются важными частями. Во-первых, мы создадим балансировщик нагрузки NGINX, назовем его deploy / nginx.yml:


apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: nginx-ingress-controller
spec:
  replicas: 1
  template:
    metadata:
      labels:
        k8s-app: nginx-ingress-lb
    spec:
      # hostNetwork makes it possible to use ipv6 and to preserve the source IP correctly regardless of docker configuration
      # however, it is not a hard dependency of the nginx-ingress-controller itself and it may cause issues if port 10254 already is taken on the host
      # that said, since hostPort is broken on CNI (https://github.com/kubernetes/kubernetes/issues/31307) we have to use hostNetwork where CNI is used
      hostNetwork: true
      terminationGracePeriodSeconds: 60
      nodeSelector:
        role: load-balancer
      containers:
        - args:
            - /nginx-ingress-controller
            - "--default-backend-service=$(POD_NAMESPACE)/default-http-backend"
            - "--default-ssl-certificate=$(POD_NAMESPACE)/cloudflare-secret"
          env:
            - name: POD_NAME
              valueFrom:
                fieldRef:
                  fieldPath: metadata.name
            - name: POD_NAMESPACE
              valueFrom:
                fieldRef:
                  fieldPath: metadata.namespace
          image: "gcr.io/google_containers/nginx-ingress-controller:0.9.0-beta.5"
          imagePullPolicy: Always
          livenessProbe:
            httpGet:
              path: /healthz
              port: 10254
              scheme: HTTP
            initialDelaySeconds: 10
            timeoutSeconds: 5
          name: nginx-ingress-controller
          ports:
            - containerPort: 80
              name: http
              protocol: TCP
            - containerPort: 443
              name: https
              protocol: TCP
          volumeMounts:
            - mountPath: /etc/nginx-ssl/dhparam
              name: tls-dhparam-vol
      volumes:
        - name: tls-dhparam-vol
          secret:
            secretName: tls-dhparam

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


Возможно, вы захотите поработать с метками, количеством реплик. Важно заметить, что здесь монтируется том, который я назвал tls-dhparam-vol. Это Diffie Hellman Ephemeral Parameters. Мы генерируем его:


sudo openssl dhparam -out ~/documents/dhparam.pem 2048

kubectl create secret generic tls-dhparam --from-file=/home/myself/documents/dhparam.pem

kubectl create secret generic tls-dhparam --from-file=/home/myself/documents/dhparam.pem

Обратите внимание, что я использую версию 0.9.0-beta_5 для изображения контроллера. Он работает хорошо, никаких проблем до сих пор. Но следите за комментариями к выпуску и для новых версий и проводите собственное тестирование.


Опять же, давайте разоблачим этот контроллер Ingress через службу балансировки нагрузки. Назовем его deploy / nginx-svc.yml:


apiVersion: v1
kind: Service
metadata:
  name: nginx-ingress
spec:
  type: LoadBalancer
  loadBalancerIP: 111.111.111.111
  ports:
  - name: http
    port: 80
    targetPort: http
  - name: https
    port: 443
    targetPort: https
  selector:
    k8s-app: nginx-ingress-lb

Помните статический внешний IP, который мы зарезервировали выше и сохранили в LB_INGRESS_IP ENV var? Нужно включить его в раздел spec: loadBalancerIP.Это также IP-адрес, который вы добавите в качестве «записи» в своей службе DNS (скажем, сопоставление вашего www.my-app.com.br c CloudFlare).


Теперь мы можем создать собственную конфигурацию Ingress, давайте создадим deploy / ingress.yml следующим образом:


apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: ingress
  annotations:
    kubernetes.io/ingress.class: "nginx"
    nginx.org/ssl-services: "web-svc"
    kubernetes.io/ingress.global-static-ip-name: ip-web-production
    ingress.kubernetes.io/ssl-redirect: "true"
    ingress.kubernetes.io/rewrite-target: /
spec:
  tls:
    - hosts:
      - www.my-app.com.br
      secretName: cloudflare-secret
  rules:
    - host: www.my-app.com.br
      http:
        paths:
        - path: /
          backend:
            serviceName: web-svc
            servicePort: 80

Таким образом мы связали службу NodePort, созданную для веб-модулей, с контроллером входа nginx и добавили завершение SSL через spec: tls: secretName. Как это создать? Во-первых, вы должны приобрести сертификат SSL, используя CloudFlare в качестве примера.


При покупке провайдер должен предоставить вам секретные файлы для загрузки (сохраните их в безопасности! общедоступная папка с Dropbox небезопасна!). Затем вы должны добавить его в инфраструктуру следующим образом:


kubectl create secret tls cloudflare-secret \
--key ~/downloads/private.pem \
--cert ~/downloads/fullchain.pem

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


kubectl create -f deploy/default-web.yml
kubectl create -f deploy/default-web-svc.yml
kubectl create -f deploy/nginx.yml
kubectl create -f deploy/nginx-svc.yml
kubectl create -f deploy/ingress.yml

Эта конфигурация NGINX Ingress основана на сообщении блога Zihao Zhang. Также есть примеры в репозитории инкубаторов кубернетов. Можете проверить это.


Если вы все сделали правильно, https://www.my-app-com.br должно загрузить ваше веб-приложение. Можно проверить время на первый байт (TTFB) через CloudFlare следующим образом:


curl -vso /dev/null -w "Connect: %{time_connect} \n TTFB: %{time_starttransfer} \n Total time: %{time_total} \n" https://www.my-app.com.br

Если у вас медленный TTFB:


curl --resolve www.my-app.com.br:443:111.111.111.111 https://www.my-app.com.br -svo /dev/null -k -w "Connect: %{time_connect} \n TTFB: %{time_starttransfer} \n Total time: %{time_total} \n"

TTFB должен быть в районе 1 секунды или меньше. Иначе это означает, что в приложении есть ошибка. Необходимо проверить типы экземпляров машинных узлов, количество рабочих, загруженных на один модуль, версию прокси-сервера CloudSQL, версию контроллера NGINX и т. д. Это процедура проб и ошибок. Подпишитесь на Loader или Web Page Test для понимания.


Роллинг-обновление


Теперь, когда все работает, как выполнить обновление Rolling Update, о котором я упоминал в начале? Сначала выполните git push на репозиторий реестра Container и дождитесь создания образа Docker.


Помните, я сделал так, чтобы триггер помещал изображение со случайным номером версии? Давайте использовать (его можно увидеть в списке Build History в Контейнерном реестре консоли Google Cloud):


kubectl set image deployment web my-app=gcr.io/my-project/my-app:1238471234g123f534f543541gf5 --record

Необходимо использовать то же имя и образ, который объявлен в deploy/web.yml. Начнется роллинг-обновление, добавление нового модуля, а затем завершение блока, пока все они не будут обновлены без простоя для пользователей.


Роллинг-обновления должны выполняться тщательно. Например, если для развертывания требуется миграция базы данных, вы должны добавить окно обслуживания (нужно делать это среди ночи, когда объем трафика небольшой ). Таким образом, вы можете запустить команду миграции следующим образом:


kubectl get pods # to get a pod name

kubectl exec -it my-web-12324-121312 /app/bin/rails db:migrate

# you can also bash to a pod like this, but remember that this is an ephemeral container, so file you edit and write there disappear on the next restart:

kubectl exec -it my-web-12324-121312 bash

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


kubectl delete -f deploy/web.yml && kubectl apply -f deploy/web.yml

Бонус: Автоснимки


В моем списке «Я хочу» был пункт – иметь постоянное монтируемое хранилище с автоматическими резервными копиями/моментальными снимками. Google Cloud предоставляет половину этого. Для подключения к модулям вы можете создавать постоянные диски, но без функции автоматического резервного копирования. Однако у хранилища есть API для создания снимка вручную.


Создадим новый SSD-диск и отформатируем его:


gcloud compute disks create --size 500GB my-data --type pd-ssd

gcloud compute instances list

Последняя команда позволяет скопировать имя экземпляра узла. Предположим, что это gke-my-web-app-default-pool-123-123. Мы прикрепим диск my-data к нему:


gcloud compute instances attach-disk gke-my-web-app-default-pool-123-123 --disk my-data --device-name my-data

gcloud compute ssh gke-my-web-app-default-pool-123-123

Последняя команда ssh в экземпляре. Мы можем перечислить прикрепленные диски с помощью sudo lsblk, и вы увидите диск 500 ГБ, возможно, как / dev / sdb. Убедитесь, что это нужный диск, мы будем его форматировать!


sudo mkfs.ext4 -m 0 -F -E lazy_itable_init=0,lazy_journal_init=0,discard /dev/sdb

Теперь мы можем выйти из сеанса SSH и отсоединить диск:


gcloud compute instances detach-disk gke-my-web-app-default-pool-123-123 --disk my-data

Вы можете установить этот диск в свои модули, добавив к развертыванию yaml следующее:


spec:
  containers:
    - image: ...
      name: my-app
      volumeMounts:
        - name: my-data
          mountPath: /data
          # readOnly: true
   # ...
   volumes:
     - name: my-data
       gcePersistentDisk:
         pdName: my-data
         fsType: ext4

Теперь создадим файл развертывания CronJob как deploy / auto-snapshot.yml:


apiVersion: batch/v1beta1
kind: CronJob
metadata:
  name: auto-snapshot
spec:
  schedule: "0 4 * * *"
  concurrencyPolicy: Forbid
  jobTemplate:
    spec:
      template:
        spec:
          restartPolicy: OnFailure
          containers:
          - name: auto-snapshot
            image: grugnog/google-cloud-auto-snapshot
            command: ["/opt/entrypoint.sh"]
            env:
            - name: "GOOGLE_CLOUD_PROJECT"
              value: "my-project"
            - name: "GOOGLE_APPLICATION_CREDENTIALS"
              value: "/credential/credential.json"
            volumeMounts:
              - mountPath: /credential
                name: editor-credential
          volumes:
            - name: editor-credential
              secret:
                secretName: editor-credential

Как мы уже делали ранее, нужно будет создать еще одну учетную запись службы с разрешениями редактора в разделе IAM & admin консоли Google Cloud, затем загрузить учетные данные JSON и, наконец, загрузить ее следующим образом:


kubectl create secret generic editor-credential \
--from-file=credential.json=/home/myself/download/my-project-1212121.json

Также обратите внимание: вы можете поменять задание cron. В примере «0 4 *» означает, что он будет запускать моментальный снимок каждый день в 4 часа утра.


Более подробно ознакомьтесь с исходным репозиторием этого решения.


Как я уже сказал, это не полная процедура, просто основные моменты некоторых важных частей. Если вы новичок в Kubernetes, прочитайте о Deployment, Service, Ingress, но у вас есть ReplicaSet, DaemonSet и многое другое.


Потребуется много времени, чтобы добавить настройку multi-region High Availability, поэтому оставим ее.


Оригинал: My Notes about a Production-grade Ruby on Rails Deployment on Google Cloud Kubernetes Engine

Southbridge 108,41
Обеспечиваем стабильную работу серверов
Поделиться публикацией
Похожие публикации
Комментарии 0

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

Самое читаемое