Yes
Whatever ENV you specify when you finally do you docker run on the image … will take.
Yes
Whatever ENV you specify when you finally do you docker run on the image … will take.
And doing those things at every boot of the container is reasonable?
Some people like this pattern, I do not, that is why rails introduces some super fancy locking around migrations to ensure they never run concurrently when a cluster of 20 app try to run migrations at the same time.
Some people just boot the same image and run the migrations in some out-of-band process.
But each container should still rake assets:precompile at least once?
Depends on how do you host those.
If you have a NFS share of some sort (like AWS EFS) or if you upload those to some object storage service that can only be done on bootstrap.
Спасибо всем за полезные ответы. У меня есть ещё несколько вопросов о том, как правильно это сделать.
Я пытаюсь подготовить минимальный файл app.yaml для начальной настройки, который содержит только информацию, необходимую для сборки контейнера. Часть информации в нём явно предназначена для времени выполнения контейнера — например, монтирование томов и сопоставление портов. Но я не уверен насчёт переменных окружения. Думаю, я просто попробую, но используются ли эти переменные окружения во время сборки контейнера (как-то внедряются в Dockerfile) или только при его запуске? Если последнее, я просто ensured, что они попадут в соответствующий файл конфигурации k8s.
Во-вторых, некоторые люди здесь говорили о загрузке образа в приватный реестр контейнеров. Это обязательно? Иными словами — содержит ли образ сборки какие-либо секретные данные, которые не следует публиковать в публичном репозитории, таком как Docker Hub? (У нас пока нет приватного реестра контейнеров, и я хотел бы избежать его настройки.)
Наконец, есть ли в app.yaml настройка для управления именем создаваемого контейнера? Это скорее вопрос завершения и аккуратности, но было бы приятно
.
Заранее спасибо за помощь! (Извините, что поднял старую тему. Это первая ссылка в Google при поиске о том, как установить Discourse на Kubernetes.)
@Geoffrey_Challen Вы можете создать образ с использованием репозитория Discourse и плагинов, установить Ruby-гемы и другие зависимости, а затем загрузить его в реестр (например, DockerHub). Этот репозиторий будет независимым от окружения и может быть публичным (если только вы не включите приватный плагин или что-то подобное). Этот базовый образ можно использовать в средах тестирования и продакшена, а также в разных проектах (если они используют одни и те же плагины).
Такие шаги, как предварительная компиляция ассетов, миграция базы данных и генерация SSL-сертификата, должны выполняться на целевой машине для создания финального образа.
Я точно не знаю, как включить это в кластер Kubernetes. Я выбрал консервативный подход и использую его на основе официального руководства команды Discourse, разделив процесс на два шага.
Этот момент я не совсем понимаю. Разве всё это не произойдёт автоматически внутри контейнера по мере необходимости? Я надеюсь, что смогу просто загрузить это в наше облако и никогда не понадобится получать доступ к оболочке машины — так же, как я редко (если вообще когда-либо) нуждаюсь входить в контейнер Docker с Discourse.
Вводить контейнер явно не нужно. Я имею в виду, что вы не можете сгенерировать предварительно скомпилированный образ с их помощью (например, в конвейере CI) и использовать его как есть, так как это должно выполняться на целевой машине, где расположена база данных (это можно автоматизировать, но я не делал этого в k8s, хотя делал с Ansible).
А, понятно. Я использую шаблон «всё в одном», чтобы включить базу данных в контейнер. Это подходит для нашего случая, когда классы состоят из 10–1000 студентов — по крайней мере, шаблон «всё в одном» отлично работал в моей группе в такой конфигурации. Таким образом, база данных находится внутри контейнера.
Но в любом случае, разве Discourse не выполняет миграции базы данных или другие шаги настройки при запуске контейнера?
Вы уверены, что база данных находится внутри контейнера? Или имеется в виду СУБД (в данном случае PostgreSQL)? Поддерживаемая установка предполагает, что база данных находится вне контейнера (что и ожидается), при этом том внутри контейнера сопоставляется с хостом. Более того, после пересборки контейнер воссоздаётся, и вы потеряете все данные.
Если база данных действительно находится внутри контейнера, я точно не знаю, как вы сможете выполнить обновление на основе официальной установки, поскольку скрипт launcher создаёт и уничтожает контейнер несколько раз при пересборке (и запускается с флагом --rm, что означает потерю всех данных, включая базу данных, после остановки контейнера).
Я не пробовал изменять способ пересборки, но если предположить, что вы можете изменить его так, чтобы всё выполнялось внутри контейнера без его пересоздания, то вы сможете выгрузить образ контейнера в реестр (убедитесь, что он приватный, так как там будут храниться секреты). Тем не менее, я не рекомендую такой подход по нескольким причинам (некоторые из них уже упоминались ранее).
Стандартная установка включает в себя nginx, Rails, PostgreSQL и Redis внутри контейнера. Для данных PostgreSQL и Redis используются внешние тома. Они не уничтожаются при пересборке или обновлении.
Да, просто странно, что он сказал, что база данных находится внутри контейнера, если только он не изменил способ работы стандартной установки или не имел в виду PostgreSQL, а не саму базу данных.
Нет — миграции и компиляция ассетов выполняются на этапе ./launcher bootstrap после разрешения плагинов. После этого контейнер можно перезапускать столько раз, сколько необходимо, или распределять веб-процессы между несколькими машинами и т. д.
В идеале настройка должна выглядеть примерно так:
./launcher bootstrap с использованием вложенного Docker на узле на базе виртуальной машины (без доступа к сокету Docker), переименует и отправит полученный образ в частный реестр (с меткой на основе временной метки, а не latest; local_discourse здесь не подходит) и обновит развёртывание до новой метки.
Postgres работает внутри контейнера. Он сохраняет данные за пределами контейнера, но сам сервер базы данных запускается внутри контейнера, если вы используете стандартный набор шаблонов установки. То же самое касается Redis. Я думаю, что путаница возникает из-за того, что, когда я говорю «база данных работает внутри контейнера», я имею в виду сервер базы данных, даже если файлы базы данных хранятся за пределами контейнера. (Но файлы базы данных не «работают», поэтому я считаю свою формулировку понятной — но, очевидно, недостаточно ясной
.)
P.S.: на самом деле данные не обязательно сохраняются за пределами контейнера, если вы не настроите Docker для монтирования этой директории. Мне удавалось пропустить этот шаг при начальной настройке, хотя это, вероятно, не лучшая идея, поскольку в этом случае содержимое базы данных не сохранится после перезапуска контейнера.
Теперь мне это кажется более понятным, особенно после прочтения длинной связанной дискуссии о docker-compose, скрипте запуска и т. д.
Вот что я хотел бы иметь возможность делать:
./launcher bootstrap локально для создания «толстого» образа Discourse, включающего все зависимости: postgres, redis и т. д../launcher bootstrap для обновления образа и повторного развертывания без уничтожения данных (это очевидно).Мое понимание таково, что «толстый» образ Discourse не должен требовать каких-либо внешних зависимостей сервисов. Однако, чтобы данные сохранялись при обновлении контейнеров, файлы базы данных postgres должны находиться вне контейнера. Это нормально — я могу создать постоянный том (persistent volume) в k8s для них.
Теперь единственная проблема, которую я предвижу. Большинство действий, выполняемых во время ./launcher bootstrap, затрагивают только файлы внутри контейнера. Например, предварительная компиляция ресурсов. Это нормально, так как результаты находятся внутри контейнера и не должны сохраняться при обновлениях.
Главное исключение здесь — миграция базы данных. Этот шаг должен иметь доступ к базе данных, которая будет использоваться после завершения этапа bootstrap. Поэтому, на мой взгляд, это основное препятствие для простого развертывания «толстых» образов Discourse в облаке.
Я заметил, что @sam неоднократно упоминал, что развертывает Discourse для своих клиентов, используя рабочий процесс, примерно аналогичный описанному мной выше. Но я подозреваю, что это работает, потому что их образы Discourse настроены на использование сервера базы данных (и, вероятно, Redis), который работает в их кластере — что логично для поддержки нескольких развертываний, но не совсем то, что хочу я. Это означает, что процесс bootstrap может изменять продакшн-базу данных — или, возможно, шаг миграции базы данных просто пропускается, так как обновления и миграции базы данных обрабатываются внешним образом. @sam: не могли бы вы подтвердить?
В любом случае, итог для меня таков: мне нужно найти способ выполнять миграции базы данных при запуске контейнера, а не во время ./launcher bootstrap. Я полагаю, что в таком случае один из способов сделать это следующий:
./launcher bootstrap, используя монтирование тома, указывающего на пустую локальную базу данных, поскольку эта база данных не будет использоваться позже. Это настроит всё внутри контейнера правильно, но не завершит работу с postgres.Возможно, вас заинтересует конфигурация с несколькими сайтами.
У вас возникло две основные проблемы: Discourse пока не готов для Kubernetes, поэтому требуется кастомный код. Кроме того, вы затрагиваете область, где команда Discourse зарабатывает деньги (хостинг большого количества форумов), поэтому уровень поддержки, который вы получаете, снизится.
Мой совет? Настройте конфигурацию с несколькими сайтами со статическим планированием на виртуальные машины, полностью вне вашего кластера. (Или используйте Service Type=ExternalName, указывающий на виртуальную машину, чтобы сохранить тот же Ingress.)
OK… Мне удалось найти один способ сделать это. Я не на 100% доволен этим решением, но оно работает и может показаться привлекательным другим, кто пытается развернуть простое одноконтейнерное «толстое» изображение Discourse (включающее PostgreSQL, Redis и т. д.) в Kubernetes.
После изучения процесса загрузки стало ясно, что, к сожалению, он смешивает два разных типа операций: те, которые влияют только на сам контейнер, и те, которые выходят за его пределы, в основном через точку монтирования тома /shared, где хранятся файлы данных PostgreSQL. Вместо того чтобы пытаться разделить эти шаги, кажется более разумным просто выполнить шаги инициализации в той среде, где контейнер будет фактически развернут.
К сожалению, команда launcher bootstrap хочет создать контейнер и использовать Docker. Поэтому запуск launcher внутри другого контейнера (например, в контейнере, работающем в нашем облаке) означает либо возню с настройкой Docker-in-Docker (это возможно, но не считается лучшей практикой), либо предоставление доступа к демону Docker на нижнем уровне. Я даже не уверен, что второй подход сработает, так как, по моему мнению, он интерпретирует монтирование тома относительно локальной файловой системы узла, тогда как в нашем сценарии мы хотим смонтировать /shared на постоянный том Kubernetes. Возможно, вариант с Docker-in-Docker сработал бы, но тогда у вас также возникла бы странная тройная цепочка монтирования томов: из вложенного контейнера во внешний контейнер, а оттуда — на постоянный том Kubernetes. Это звучит… неразумно.
Однако по сути launcher bootstrap создает один большой файл .yml, обрабатывая значение templates в app.yml, а затем передает его базовому образу Discourse после завершения процесса инициализации. Таким образом, если мы сможем извлечь файл конфигурации, мы сможем генерировать конфигурацию на любой машине, а затем нам останется только понять, как передать её контейнеру, который мы запускаем в облаке.
Итак, в качестве обзора, вот шаги, которые мы будем выполнять:
launcher.pups), а затем запустит Discourse.Вот необходимое изменение в launcher для поддержки команды dump, которая записывает объединенную конфигурацию в STDOUT:
run_dump() {
set_template_info
echo "$input"
}
(Обратите внимание, что эта команда доступна в нашем форке discourse_docker.)
Итак, первый шаг — использовать новую команду launcher dump, добавленную выше, для создания нашей конфигурации инициализации:
# Замените app на имя вашей конфигурации контейнера
./launcher dump app > bootstrap.yml
Далее нам нужен контейнер, который знает, что нужно запустить pups для инициализации контейнера перед запуском через /sbin/boot. Я использовал следующий Dockerfile, чтобы внести крошечное изменение в базовый образ Discourse:
FROM discourse/base:2.0.20191219-2109
COPY scripts/bootstrap.sh /
CMD bash bootstrap.sh
Где scripts/bootstrap.sh содержит:
cd /pups/ && /pups/bin/pups --stdin < /bootstrap/bootstrap.yml && /sbin/boot
Я опубликовал это как geoffreychallen:discourse_base:2.0.20191219-2109. (Обратите внимание, что вы, вероятно, могли бы добиться того же, изменив команду запуска базового Docker-образа Discourse, но мне было трудно заставить это работать с перенаправлением оболочки, необходимым для того, чтобы pups прочитал файл конфигурации.)
Теперь нам нужна наша конфигурация Kubernetes. Моя выглядит так:
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
name: kotlin-forum-pvc
namespace: ikp
spec:
storageClassName: rook-ceph-block
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 64Gi
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: kotlin-forum-deployment
namespace: ikp
spec:
replicas: 0
selector:
matchLabels:
app: kotlin-forum
template:
metadata:
labels:
app: kotlin-forum
spec:
volumes:
- name: kotlin-forum
persistentVolumeClaim:
claimName: kotlin-forum-pvc
- name: bootstrap
configMap:
name: kotlin-forum-bootstrap
containers:
- name: kotlin-forum
image: geoffreychallen/discourse_base:2.0.20191219-2109
imagePullPolicy: Always
volumeMounts:
- name: kotlin-forum
mountPath: /shared/
- name: bootstrap
mountPath: /bootstrap/
ports:
- containerPort: 80
env:
- name: TZ
value: "America/Chicago"
- name: LANG
value: en_US.UTF-8
- name: RAILS_ENV
value: production
- name: UNICORN_WORKERS
value: "3"
- name: UNICORN_SIDEKIQS
value: "1"
- name: RUBY_GLOBAL_METHOD_CACHE_SIZE
value: "131072"
- name: RUBY_GC_HEAP_GROWTH_MAX_SLOTS
value: "40000"
- name: RUBY_GC_HEAP_INIT_SLOTS
value: "400000"
- name: RUBY_GC_HEAP_OLDOBJECT_LIMIT_FACTOR
value: "1.5"
- name: DISCOURSE_DB_SOCKET
value: /var/run/postgresql
- name: DISCOURSE_DEFAULT_LOCALE
value: en
- name: DISCOURSE_HOSTNAME
value: kotlin-forum.cs.illinois.edu
- name: DISCOURSE_DEVELOPER_EMAILS
value: challen@illinois.edu
- name: DISCOURSE_SMTP_ADDRESS
value: outbound-relays.techservices.illinois.edu
- name: DISCOURSE_SMTP_PORT
value: "25"
---
apiVersion: v1
kind: Service
metadata:
name: kotlin-forum
namespace: ikp
spec:
type: NodePort
ports:
- name: http
port: 80
targetPort: 80
selector:
app: kotlin-forum
---
apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
namespace: ikp
annotations:
nginx.ingress.kubernetes.io/ssl-redirect: "true"
name: kotlin-forum-ingress
spec:
rules:
- host: kotlin-forum.cs.illinois.edu
http:
paths:
- backend:
serviceName: kotlin-forum
servicePort: 80
Ваша конфигурация будет отличаться. Обратите внимание, что я завершаю HTTPS на верхнем уровне, отсюда и изменения в конфигурации Ingress. Мне также нравится помещать всё в один файл, удалять части, которые не работают в процессе итераций, а затем позволять Kubernetes пропускать дубликаты при следующем запуске kubectl create -f. Также обратите внимание, что я установил replicas: 0, чтобы развертывание не запускалось сразу после настройки. Это связано с тем, что нам нужно выполнить одну дополнительную настройку.
Я скопировал список переменных окружения из того, что видел при передаче в контейнер через launcher start. Я не знаю, все ли они необходимы, и в зависимости от вашей конфигурации могут отсутствовать другие. Всё зависит от вашей ситуации (YMMV).
Обратите внимание, что у нас есть две точки монтирования томов в контейнер: первая — для PostgreSQL, настроенная как постоянный том, который сохранится после перезапуска пода. Вторая — карта конфигурации, созданная следующим образом:
kubectl create configmap kotlin-forum-bootstrap --from-file=bootstrap.yml=<path/to/bootstrap.yml>
Где kotlin-forum-bootstrap должен совпадать с вашей конфигурацией Kubernetes, а path/to/bootstrap.yml — это путь к файлу bootstrap.yml, который мы создали с помощью launcher dump выше.
Как только ваш configmap будет создан, вы сможете масштабировать развертывание до одного реплики и увидеть, как Discourse загружается и выполняет тот же процесс инициализации, который выполнил бы launcher bootstrap. Это занимает несколько минут. Когда это будет сделано, ваша установка Discourse запустится.
Несколько других замечаний, с которыми я столкнулся на пути к полной настройке (по крайней мере, на данный момент):
X-Forwarded, включая X-Forwarded-For, X-Forwarded-Proto и X-Forwarded-Port. Если этого не сделать, возникнут странные ошибки аутентификации при попытке использовать вход через Google и, вероятно, других провайдеров входа.nginx должен быть настроен на передачу заголовков путем установки use-forwarded-headers в глобальной карте конфигурации. Мне потребовалось некоторое время, чтобы настроить это правильно, поскольку несколько раз я редактировал неправильную карту конфигурации, а затем ожидал, что мои контейнеры ingress перезапустятся при изменении карты конфигурации. (Они этого не сделали.)Чтобы обновить развернутую установку, вам нужно сгенерировать новый файл bootstrap.yml, обновить карту конфигурации, а затем перезапустить контейнер (самый простой способ — масштабировать до 0, а затем снова до 1 реплики).
Это влечет за собой небольшое время простоя, поскольку инициализация происходит до сборки контейнера. Но в случаях, когда необходимо обновить конфигурацию и/или изменить базовый образ, это кажется неизбежным. launcher rebuild документирован как stop; bootstrap; start, что означает, что процесс инициализации все равно вызовет простой, даже если он выполняется с помощью скрипта launcher.
Этот паттерн развертывания «толстого» контейнера Discourse был бы гораздо проще в поддержке, если бы скрипт launcher более четко разделял (а) шаги инициализации, которые можно выполнить офлайн и которые влияют только на файлы внутри контейнера, и (б) шаги инициализации, которые изменяют или требуют доступа к базе данных или другому состоянию за пределами контейнера. Описанный выше подход немного расстраивает, потому что вы видите всякую уродливую минификацию JS, сжатие ассетов и другие вещи, которые можно было бы выполнить с предыдущим развертыванием… но они слишком смешаны с другими вещами (например, миграциями базы данных), которые нельзя выполнить без доступа к базе данных. Я кратко подумал о создании контейнера, который выполнял бы только шаги из templates/postgres.yml, но затем заметил, что миграции базы данных выполняются веб-шаблоном, подумал о плагинах и просто сдался
.
При лучшем разделении повторное развертывание для «толстых» контейнеров могло бы работать примерно так:
Это привело бы к меньшему времени простоя. Возможно, это не стоит усилий только по этой причине, но я могу представить, что это также могло бы упростить более сложные сценарии развертывания, включающие общие базы данных или что-то в этом роде.
Это имеет больше смысла. Именно так я поступил при разделении этапа начальной настройки на два шага. Первый может выполняться в изолированной среде (например, в конвейере CI), создавая базовый образ с репозиторием Discourse, установленными библиотеками gems и плагинами. Второй шаг должен выполняться на целевой машине (или, по крайней мере, иметь доступ к производственной базе данных) для выполнения миграции базы данных и генерации ассетов (это делается в процессе начальной настройки, а не при запуске контейнера).
Да, это было бы отлично. Я уже подавал соответствующую заявку, но не знаю, будет ли это реализовано и когда.
Полностью реализовать это в отдельной среде будет сложно, поскольку задача предварительной компиляции ассетов требует доступа к базе данных (например, для пользовательских CSS). Однако было бы здорово, если бы то, что зависит от базы данных, можно было выполнить отдельным шагом (а все остальные ассеты, не зависящие от базы данных, компилировать отдельно). Но я не знаю, насколько технически жизнеспособна такая реализация.
Вот что я обычно делаю при установке Kubernetes. Не представляю, как или зачем использовать k8s без отдельных контейнеров для данных и веб-части (или какого-либо другого внешнего PostgreSQL и Redis — в установках, которые я делал для клиентов, для этого используются ресурсы GCP).
Также существует переменная окружения skip_post_migration_updates, которую необходимо понимать для обеспечения истинных обновлений без простоя. Она описана здесь.