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 中),还是仅在容器运行时使用?如果是后者,我会确保将它们放入相应的 Kubernetes 配置文件中。
其次,这里有些人提到将镜像推送到私有容器仓库。这是必须的吗?换句话说,构建镜像中是否包含任何不应发布到 Docker Hub 等公共仓库的机密信息?(我们目前还没有私有容器仓库,我希望避免搭建一个。)
最后,app.yaml 是否有设置可以控制所创建容器的名称?这更多是为了完善细节,但如果有这个功能就太好了
。
提前感谢您的帮助!(抱歉打扰了旧线程。在 Google 上搜索如何在 Kubernetes 上安装 Discourse 时,这是第一个结果。)
@Geoffrey_Challen 你可以使用 Discourse 仓库和插件创建一个镜像,安装 Ruby gems 及其他依赖项,然后将其推送到镜像仓库(如 DockerHub)。该仓库应与环境无关,并且可以是公开的(除非你包含了私有插件或类似内容)。这个基础镜像可用于 staging 和生产环境,甚至可用于不同的项目(如果它们使用相同的插件)。
不过,像预编译资源、数据库迁移和生成 SSL 证书等步骤应在目标机器上执行,以生成最终的镜像。
我不太清楚如何将其集成到 Kubernetes 集群中。我采取了保守的做法,依据 Discourse 团队的官方指南,将其分为两个步骤来操作。
这部分我不太确定是否理解正确。这些操作不会在容器内部按需自动执行吗?我希望只需将其推送到我们的云端,而无需再获取该机器的 shell 访问权限——就像我很少(甚至从未)需要进入 Discourse Docker 容器一样。
您无需显式输入容器。我的意思是,您无法生成预编译的镜像(例如在 CI 流水线中生成)并直接使用它,因为这必须在目标机器上执行,而目标机器上存放着数据库(这可以自动化,但我尚未在 Kubernetes 中实现,尽管我曾用 Ansible 实现过)。
啊,好的。我使用的是“全合一”模板,将数据库包含在容器中。这适合我们的使用场景,即支持由 10 到 1000 名学生组成的班级——至少对于我的班级而言,在这种配置下,“全合一”模板运行良好。因此,数据库位于容器内部。
但无论如何,Discourse 在容器启动时不会运行数据库迁移或其他设置步骤吗?
您确定数据库在容器内部吗?还是指关系型数据库管理系统(本例中为 PostgreSQL)?官方支持的安装方式是将数据库部署在容器外部(这是预期行为),并将容器内的卷映射到外部(即主机)。此外,在容器重建后,容器会被重新创建,导致所有数据丢失。
如果数据库确实位于容器内部,我不确定如何根据官方安装方案进行升级,因为 launcher 脚本在重建过程中似乎会多次创建和销毁容器(并使用 --rm 参数运行),这意味着容器停止后,您将丢失所有数据,包括数据库。
我尚未尝试更改重建方式,但假设您能够修改配置,使所有操作都在容器内运行且无需重新创建容器,那么您应该可以将容器推送到注册表(请确保其为私有,因为其中包含密钥)。不过,出于多种原因(其中一些已在前文提及),我并不推荐这种做法。
标准安装会在容器内包含 nginx、rails、postgres 和 redis。它使用外部卷来存储 postgres 和 redis 的数据。在重建或升级时,这些卷不会被销毁。
是的,只是觉得奇怪,他说数据库在容器内,除非他改变了标准安装的方式,或者他指的是 PostgreSQL,而不是数据库本身。
不会——迁移和资产编译步骤是在 ./launcher bootstrap 阶段进行的,此时插件已解析完毕。完成后,容器可以根据需要多次重启,或者将 Web 进程拆分到多台机器上运行等。
理想情况下,设置应如下所示:
./launcher bootstrap,并将生成的镜像重命名后推送到私有镜像仓库(使用基于时间戳的标签,而非 latest)(此处 local_discourse 并不是一个好名字),然后将部署滚动更新到新的标签。
Postgres 在容器内运行。如果使用标准安装模板,它会将数据保存到容器外部,但本身在容器内运行。Redis 也是如此。我认为混淆在于,当我说“数据库在容器内运行”时,我指的是数据库服务器,即使数据库文件位于容器外部。(但数据库文件本身不会“运行”,所以我原以为我的表述很清晰——但显然还不够清晰
。)
附注:实际上,除非你配置 Docker 将该目录进行绑定挂载,否则数据甚至不一定保存在容器外部。我在初始化过程中跳过了这一步,但这可能不是个好主意,因为此时数据库内容无法在容器重启后保留。
我现在觉得这更有道理了,特别是读了关于 docker-compose、启动脚本等的长篇链接讨论之后。
以下是我希望实现的目标:
./launcher bootstrap,创建一个包含所有依赖项(如 postgres、redis 等)的“胖”Discourse 镜像。./launcher bootstrap 以更新镜像并重新部署,而无需销毁数据(废话)。我的理解是,这个“胖”Discourse 镜像不应依赖任何外部服务。不过,为了让数据在容器升级后得以保留,PostgreSQL 数据库文件必须存储在容器外部。这没问题——我可以为它们创建一个 Kubernetes 持久卷。
现在我预见到唯一的问题。./launcher bootstrap 期间发生的绝大多数操作只涉及容器内的文件。例如,预编译资源。这没问题,因为结果存储在容器内,无需在升级后保留。
这里的大例外是数据库迁移。 该步骤需要能够访问在引导完成后将使用的数据库。因此,对我来说,这似乎是轻松将“胖”Discourse 镜像部署到云端的最大障碍。
我注意到 @sam 多次提到,他们使用大致类似于我上述描述的工作流为客户重新部署 Discourse。但我怀疑,之所以可行,是因为他们的 Discourse 镜像被配置为使用运行在其集群上的数据库服务器(可能还有 Redis)。这对于支持多个部署来说是有道理的,但并非我想要的做法。这意味着引导过程可以修改生产数据库——或者可能只是跳过了数据库迁移步骤,因为数据库升级和迁移是外部处理的。@sam:能否确认一下?
总之,对我来说,这意味着我需要找到一种方法,在容器启动时运行数据库迁移,而不是在 ./launcher bootstrap 期间运行。我想,那时的一种做法可能是:
./launcher bootstrap 在本地构建“胖”Discourse 容器,挂载一个指向空本地数据库的卷,因为该数据库稍后不会被使用。这将使容器内的所有内容都就绪,只是未完成 PostgreSQL 的相关工作。您可能对多站点配置感兴趣。
您目前面临两个主要问题:Discourse 尚未准备好支持 Kubernetes,因此需要自定义代码。此外,您正在涉足 Discourse 团队的主要盈利领域(托管大量论坛),因此您获得的支援水平将会下降。
我的建议是:采用多站点配置,将静态调度部署在虚拟机上,完全独立于您的集群之外。(或者使用 Service Type=ExternalName 指向虚拟机,以保持一致的 Ingress 配置。)
好的……我想到了一个实现方法。虽然我对它还不是百分之百满意,但它确实能行,而且对于那些试图在 Kubernetes 上部署包含 PostgreSQL、Redis 等的单容器“胖”Discourse 镜像的人来说,这可能很有吸引力。
在检查引导(bootstrap)过程后,我清楚地意识到,不幸的是,它将两种不同类型的操作混在一起了:一种只影响底层容器,另一种则“伸出”到周围环境中,主要通过存放 PostgreSQL 数据文件的 /shared 卷挂载点。与其试图将这些步骤拆分开来,不如直接在容器实际部署的环境中运行引导步骤,这样似乎更合理。
不幸的是,launcher bootstrap 想要创建一个容器并使用 Docker。因此,在另一个容器(例如运行在我们云上的容器)中运行 launcher,意味着要么纠缠于 Docker-in-Docker 设置(可行,但不被视为最佳实践),要么暴露底层 Docker 守护进程。我甚至不确定第二种方法是否可行,因为我认为它会将对节点本地文件系统的卷挂载解释为针对节点本地文件系统,而在我们的场景中,我们希望将 /shared 卷挂载到持久的 Kubernetes 卷上。也许 Docker-in-Docker 路线能行得通,但那样的话,你还会面临一个奇怪的三重卷挂载:从嵌套容器内部到外部容器,再从那里到持久的 Kubernetes 卷。听起来……不太明智。
然而,本质上 launcher bootstrap 是通过处理 app.yml 中的 templates 值来生成一个大的 .yml 文件,然后在引导过程结束时将其传递给 Discourse 基础镜像。所以,如果我们能提取出配置文件,就能在任何机器上生成配置,然后只需要弄清楚如何将其传递给我们在云中启动的容器。
因此,作为概述,我们将遵循以下步骤:
launcher 生成引导配置pups),然后启动 Discourse以下是 launcher 所需的更改,以支持一个将合并后的配置写入 STDOUT 的 dump 命令:
run_dump() {
set_template_info
echo "$input"
}
(请注意,此命令可在 我们 fork 的 discourse_docker 中使用。)
因此,第一步是使用上面添加的新 launcher dump 命令创建我们的引导配置:
# 将 app 替换为你自己的容器配置名称
./launcher dump app > bootstrap.yml
接下来,我们需要一个知道在通过 /sbin/boot 启动之前运行 pups 来引导容器的容器。我使用了以下 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。(请注意,你也许也可以通过修改基础 Discourse Docker 镜像的启动命令来实现同样的效果,但我很难让它与 pups 读取配置文件所需的 shell 重定向一起工作。)
现在我们需要 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,配置为持久卷,在 Pod 重启后依然存在。第二个是配置映射,创建方式如下:
kubectl create configmap kotlin-forum-bootstrap --from-file=bootstrap.yml=<path/to/bootstrap.yml>
其中 kotlin-forum-bootstrap 需要与你的 Kubernetes 配置匹配,path/to/bootstrap.yml 是上面使用 launcher dump 创建的 bootstrap.yml 文件的路径。
一旦你的 configmap 就位,你应该能够将部署扩展到 1 个副本,并看到 Discourse 启动并运行与 launcher bootstrap 会执行的相同的引导过程。这需要几分钟时间。完成后,你的 Discourse 安装就会启动。
在使这个(至少目前)完全配置好的过程中,我还遇到了一些其他注意事项:
X-Forwarded 头,包括 X-Forwarded-For、X-Forwarded-Proto 和 X-Forwarded-Port。如果不这样做,在使用 Google 登录和其他登录提供商时会导致奇怪的认证错误。nginx Ingress 控制器必须配置为通过在全局 ConfigMap 中设置 use-forwarded-headers 来传递头。这花了我一些时间才弄对,因为至少有好几次我编辑了错误的 ConfigMap,然后期望我的 Ingress 容器在 ConfigMap 更改时重启。(它们并没有。)要更新已部署的安装,你需要重新生成新的 bootstrap.yml 文件,更新 ConfigMap,然后重启容器(最简单的方法是将副本数缩容到 0,然后再扩容回 1)。
这确实会导致一些停机时间,因为引导过程发生在容器构建之前。但在我看来,在需要更新配置和/或更改基础镜像的情况下,这是不可避免的。launcher rebuild 被文档化为 stop; bootstrap; start,这意味着即使使用 launcher 脚本执行,引导过程仍然会导致停机。
如果 launcher 脚本能更清晰地将 (a) 可离线执行且仅影响容器内文件的引导步骤与 (b) 需要修改或访问容器外数据库或其他状态的引导步骤分离开来,这种胖容器 Discourse 部署模式的支持将会容易得多。上述方法有点令人沮丧,因为你确实看到了各种 JS 混淆、资源压缩以及其他本可以在之前的部署运行时完成的操作……但它们与数据库迁移等其他无法在不访问数据库的情况下完成的操作混在一起。我短暂地想过创建一个只执行 templates/postgres.yml 中步骤的容器,但随后注意到数据库迁移是由 Web 模板执行的,又想到了插件,然后就放弃了
。
如果有更好的分离,胖容器的重新部署可以像这样进行:
这将导致更少的停机时间。仅凭这一点,可能不值得为此付出努力,但我可以想象,这也可能简化涉及共享数据库或其他情况的更复杂的部署场景。
这样更合理。我在将引导阶段分为两步时就是这样做的。第一步可以在隔离环境中运行(例如 CI 流水线),生成一个包含 Discourse 仓库、gem 和插件的基础镜像;第二步则需要在目标机器上运行(或至少能访问生产数据库),以执行数据库迁移并生成资源文件(这一步是在引导过程中完成的,而不是在启动容器时)。
是的,那将非常棒。我已经提出过该请求,但我不确定是否会实施以及何时实施。
要在完全独立的环境中实现这一点会比较困难,因为资源预编译任务需要访问数据库(例如用于自定义 CSS 等)。但如果仅依赖数据库的部分可以单独执行,而其他不依赖数据库的资源文件可以独立预编译,那将非常理想。不过,我不确定在技术上实现这一点的可行性如何。
这基本上就是我在我部署的 Kubernetes 环境中所做的。我无法想象如何在没有独立的数据容器和 Web 容器(或其他类型的外部 PostgreSQL 和 Redis)的情况下使用 k8s——我为客户部署的环境都使用 GCP 资源来实现这一点。
此外,还有一个环境变量 skip_post_migration_updates,要实现真正的零停机升级,你需要了解它。相关内容在 这里 有描述。