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 へ何らかの方法で注入されるなど)に使用されるのでしょうか、それともコンテナの実行時のみでしょうか?後者であれば、適切な k8s 設定ファイルに含めるようにします。
次に、ここにはプライベートなコンテナレジストリにイメージをプッシュするという話が出ていましたが、これは必須でしょうか。言い換えれば、ビルドされたイメージには、Docker Hub のようなパブリックなリポジトリに公開すべきでない機密情報が含まれているのでしょうか?(私たちはまだプライベートなコンテナレジストリを持っていませんし、設置したくありません。)
最後に、作成されるコンテナの名前を制御する app.yaml の設定はありますか?これは単なる仕上げの問題ですが、あればうれしいです
。
ご協力いただき、ありがとうございます!(古いスレッドを掘り起こしてすみません。Kubernetes 上で Discourse をインストールする方法で Google 検索した際、これが最初のヒットでした。)
@Geoffrey_Challen Discourse リポジトリとプラグインを使用してイメージを作成し、Ruby Gems やその他の依存関係をインストールして、レジストリ(DockerHub など)にプッシュすることができます。このリポジトリは環境に依存しないものであり、公開することも可能です(プライベートなプラグインなどを含まない限り)。このベースイメージは、ステージング環境や本番環境、さらには同じプラグインを使用する異なるプロジェクトでも利用できます。
ただし、アセットのプリコンパイル、データベースマイグレーション、SSL 証明書の生成などの手順は、最終的なイメージを生成するために、ターゲットマシン上で実行する必要があります。
これを Kubernetes クラスタにどう組み込むかはっきりとはわかりません。私は保守的なアプローチを選び、Discourse チームの公式ガイドに基づいて使用し、2 つのステップに分けています。
この部分は少し理解できません。これらは必要に応じてコンテナ内で自動的に実行されないのでしょうか?私はこれをクラウドにプッシュするだけで、マシンへのシェルアクセスを取得する必要がまったくないことを願っています。Discourse の Docker コンテナにログインする必要がほとんど(あるいは全く)ないのと同じように。
コンテナを明示的に指定する必要はありません。私が言いたいのは、CI パイプラインなどで生成されたプリコンパイル済みのイメージをそのまま使用することはできないということです。なぜなら、これはデータベースが存在するターゲットマシンで実行する必要があるからです(これは自動化可能ですが、k8s ではまだ実装していません。Ansible では実装しました)。
ああ、なるほど。私はデータベースをコンテナに含めるために「オールインワン」テンプレートを使用しています。これは、10〜1000人の学生で構成されるクラスをサポートするという私たちのユースケースに適しています。少なくとも、私のクラスではこの設定でオールインワンは問題なく機能しています。つまり、データベースはコンテナ内部にあります。
しかし、いずれにせよ、Discourseはコンテナの起動時にデータベースのマイグレーションやその他のセットアップステップを実行しないのでしょうか?
データベースが本当にコンテナ内にあると確信していますか?それとも、RDBMS(この場合は PostgreSQL)のことでしょうか?サポートされているインストール方法では、データベースはコンテナ外(これが期待される動作です)にあり、ボリュームをコンテナ内からホスト(外部)にマッピングしています。さらに、コンテナを再構築するとコンテナが再作成され、すべてのデータが失われます。
もし本当にコンテナ内にある場合、公式のインストールに基づいてアップグレードする方法は正確にはわかりません。launcher スクリプトは、再構築時にコンテナを複数回作成・破棄するようであり(--rm フラグで実行されるため、コンテナが停止するとデータベースを含むすべてのデータが失われます)。
再構築の方法を変更する試みは行っていませんが、コンテナを再作成することなく、すべてをコンテナ内で実行するように変更できると仮定すると、そのコンテナをレジストリにプッシュできるはずです(シークレットが含まれるため、必ずプライベートにしてください)。ただし、このアプローチはいくつかの理由(前述のものも含む)から推奨しません。
標準インストールでは、コンテナ内に nginx、Rails、Postgres、Redis が含まれます。Postgres と Redis のデータには外部ボリュームが使用されます。再構築やアップグレード時に破棄されることはありません。
はい、彼がデータベースがコンテナ内にあると言っていたのが奇妙だと気づきました。標準的なインストール方法が変更されたか、データベース自体ではなく PostgreSQL を意味しているのでしょうか。
いいえ、マイグレーションとアセットコンパイルの手順は、プラグインの解決後に ./launcher bootstrap フェーズで実行されます。その後、コンテナは必要に応じて何度でも再起動できますし、Web プロセスを複数のマシンに分散することも可能です。
イメージとして、セットアップは以下のようなものになるはずです:
./launcher bootstrap を実行する Job を実行し、生成されたイメージを名前を変更してプライベートレジストリにプッシュします(latest ではなく、タイムスタンプベースのラベルを使用します)(local_discourse はここでの名前として適切ではありません)。その後、デプロイメントを新しいラベルにロールアウトします。
PostgreSQL はコンテナ内で動作します。標準のインストールテンプレートを使用する場合、データはコンテナ外に保存されますが、データベースサーバー自体はコンテナ内で動作します。Redis も同様です。私が「データベースがコンテナ内で動作する」と言うとき、データベースファイルがコンテナ外に存在していても、データベースサーバーのことを指しています。(データベースファイルは「動作」しないため、私の表現は明確だと考えていましたが、明らかに十分ではなかったようです
。)
PS:実際には、Docker でそのディレクトリをバインドマウントするように設定しない限り、データが必ずしもコンテナ外に保存されるわけではありません。私はブートストラップ時にこれをスキップできましたが、おそらく良い考えではありません。なぜなら、その場合、コンテナの再起動後にデータベースの内容が失われてしまうからです。
今では、特に docker-compose、ランチャー・スクリプトなどに関する長いリンク先の会話を読んだ後、これがより理解できるようになりました。
私が実現したいことは以下の通りです:
./launcher bootstrap を実行し、Postgres、Redis などのすべての依存関係を含む「太った」Discourse イメージを作成する./launcher bootstrap を再実行してイメージを更新し、データを破壊せずに再デプロイする(当たり前ですが)私の理解では、この「太った」Discourse イメージは外部サービスの依存関係を必要としないはずです。ただし、コンテナのアップグレード後にデータが生存するためには、Postgres のデータベースファイルはコンテナの外に存在する必要があります。それは問題ありません。それらのために k8s の永続ボリュームを作成できます。
ここで予想される唯一の問題があります。./launcher bootstrap の間に起こる大部分の処理は、コンテナ内に存在するファイルのみを操作します。例えば、アセットのプリコンパイルなどです。これは問題ありません。なぜなら、その結果はコンテナ内にあり、アップグレード後に生存する必要がないからです。
大きな例外はデータベースのマイグレーションです。 このステップは、ブートストラップ完了後に使用されるデータベースにアクセスする必要があります。そのため、私にはこれが、太った Discourse イメージをクラウドに簡単にデプロイする際の主要な障壁のように思えます。
@sam 氏は、お客様向けに Discourse を再デプロイする際、私が上記で説明したようなワークフローとほぼ似た方法を採用していると何度も言及しています。しかし、これが機能する理由は、彼らの Discourse イメージがクラスター上で実行されるデータベースサーバー(おそらく Redis も同様)を使用するように設定されているためではないかと推測します。これは複数のデプロイメントをサポートする上で理にかなっていますが、私がやりたいこととは少し異なります。つまり、ブートストラップ処理が生産データベースを変更できる、あるいは単にデータベースのアップグレードとマイグレーションが外部で処理されるため、データベースのマイグレーションステップが完全にスキップされるのかもしれません。@sam 氏:確認していただけますか?
とにかく、私にとっての結論は、データベースのマイグレーションを ./launcher bootstrap 中にではなく、コンテナ起動時に実行する方法を見つける必要があるということです。その場合、一つの方法は以下のようになるでしょう:
./launcher bootstrap を使用して太った Discourse コンテナを構築する。この際、後で使用されない空のローカルデータベースを指すボリュームマウントを使用する。これによりコンテナ内の設定はすべて整うが、Postgres の作業は完了しない。マルチサイト構成に興味があるかもしれません。
現在、2 つの大きな問題に直面しています。Discourse は Kubernetes に対応していないため、カスタムコードが必要です。また、Discourse チームが収益を得ている分野(多数のフォーラムのホスティング)に踏み込みつつあるため、サポートレベルは低下するでしょう。
私のアドバイスは、VM への静的スケジューリングによるマルチサイト構成を、クラスターの外側で完全に実施することです。(または、同じ Ingress を維持するために VM を指す Service Type=ExternalName を使用します。)
OK… I managed to figure out one way of doing this. I’m not 100% happy with it, but it does work and may be appealing to others that are trying a simple single-container fat (includes postgres, redis, etc.) Discourse image deployment to Kubernetes.
After examining the bootstrap process it became clear to me that unfortunately it mixes in two different kinds of operations—ones that only affect the underlying container, and others that poke out into the surrounding environment, mainly through the /shared volume mount where the postgres data files live. Rather than trying to tease these steps apart, it seems more sane to just run the bootstrapping steps in the environment where the container is actually going to be deployed.
Unfortunately, launcher bootstrap wants to create a container and use Docker. So running launcher inside another container (for example, in a container running on our cloud) means either tangling with a Docker-in-Docker setup (doable, but not considered best practices) or exposing the underlying Docker daemon. I’m not even sure that that second approach would work, since I think that it would interpret a volume mount against the node’s local filesystem, whereas in our scenario we want to volume mount /shared to a persistent Kubernetes volume. Maybe the Docker-in-Docker route would work, but then you’d also have a weird triple volume mount from inside the nested container into the outer container and from there to the persistent Kubernetes volume. That sounds… unwise.
However, essentially launcher bootstrap creates one large .yml file by processing the templates value in the app.yml and then passes that to the Discourse base image when finishes the bootstrap process. So if we can extract the configuration file we can generate the configuration on any machine and then we only need to figure out how to pass it to a container we start in the cloud.
So as an overview, here are the steps we are going to follow:
launcherpups) and then start DiscourseHere’s the required change to launcher to support a dump command that writes the merged configuration to STDOUT:
run_dump() {
set_template_info
echo "$input"
}
(Note that this command is available in our fork of discourse_docker.)
So the first step is to use the new launcher dump command added above to create our bootstrap configuration:
# Substitute whatever your container configuration is called for app
./launcher dump app > bootstrap.yml
Next we need a container that knows to run pups to bootstrap the container before booting via /sbin/boot. I used the following Dockerfile to make a tiny change to the base discourse image:
FROM discourse/base:2.0.20191219-2109
COPY scripts/bootstrap.sh /
CMD bash bootstrap.sh
Where scripts/bootstrap.sh contains:
cd /pups/ && /pups/bin/pups --stdin < /bootstrap/bootstrap.yml && /sbin/boot
I published this as geoffreychallen:discourse_base:2.0.20191219-2109. (Note that you could probably also accomplish the same thing by modifying the boot command of the base Discourse docker image, but I was having a hard time getting that to work with the shell redirection required to get pups to read the configuration file.)
Now we need our Kubernetes configuration. Mine looks like this:
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
Yours will look different. Note that I’m terminating HTTPS upstream, hence the modifications to the Ingress configuration. I also like to put everything in one file, delete pieces that don’t work as I iterate, and then let Kubernetes skip duplicates on the next kubectl create -f. Also note that I set replicas: 0 so that the deployment doesn’t start as soon as its configured. That’s because we have one bit of additional configuration to finish.
I copied the list of environment variables from what I saw being passed to the container by launcher start. I don’t know if all of these are necessary and others may be missing depending on your configuration. YMMV.
Note that we have two volume maps pointing into the container: the first is for postgres, configured as a persistent volume that will survive pod restarts. The second is a configuration mapping created like this:
kubectl create configmap kotlin-forum-bootstrap --from-file=bootstrap.yml=<path/to/bootstrap.yml>
Where kotlin-forum-bootstrap needs to match your Kubernetes configuration and path/to/bootstrap.yml is the path to the bootstrap.yml file we created using launcher dump above.
Once your configmap is in place, you should be able to scale your deployment to one replica and see Discourse booting and running the same bootstrap process that launcher bootstrap would have performed. That takes a few minutes. When that is done, your Discourse installation will boot.
A few other notes that I ran on the way to getting this (at least for now) fully configured:
X-Forwarded headers, including both X-Fowarded-For, X-Forwarded-Proto, X-Forwarded-Port. Not doing so will result in strange authentication errors when trying to use Google login and probably other login providers.nginx ingress controller must be configured to pass headers by setting use-forwarded-headers in the global config map. This took me a while to get right, since at least several times I edited the wrong configuration map, and then expected my ingress containers to restart when the configuration map changed. (They didn’t.)To update the deployed installation, you regenerate the new bootstrap.yml file, update the config map, and then restart the container (easiest by scaling to 0 and then back to 1 replica).
This does incur a bit of downtime since the bootstrapping happens before the container is built. But this seems inevitable to me in cases where you need to update the configuration and/or change the base image. launcher rebuild is documented as stop; bootstrap; start, meaning that the bootstrap process will still cause downtime even if performed using the launcher script.
This fat container Discourse deployment pattern would be much easier to support if the launcher script would more cleanly separate (a) bootstrap steps that could be performed offline and only affect the files in the container and (b) bootstrap steps that modify or need access to the database or other state outside the container. The approach described above is a bit frustrating because you do see all kinds of JS uglification, asset minification, and other things that could be done with the previous deployment running… but they are just too mixed in with other things (like database migrations) that can’t be done without access to the database. I briefly thought about creating a container that would only perform the steps in templates/postgres.yml, but then noticed that database migrations were being done by the web template, and thought about plugins, and then just gave up
.
With better separation redeployment for fat containers could work something like this:
That would result in a bit less downtime. It’s probably not worth the effort for that reason alone, but I can imagine that this might also simplify more complex deployment scenarios involving shared databases or whatever.
なるほど、その方が理にかなっています。ブートストラップ段階を 2 つのステップに分割する際、私も同じように対応しました。最初のステップは孤立した環境(CI パイプラインなど)で実行し、Discourse リポジトリ、gems、プラグインがインストールされたベースイメージを生成します。2 番目のステップは、ターゲットマシンで実行するか(少なくとも本番データベースへのアクセスが必要)、データベースのマイグレーションとアセットの生成を行います(これはコンテナ起動時ではなく、ブートストラップ処理で行われます)。
はい、それは素晴らしいことです。すでにリクエストしていますが、それが実現されるかどうか、いつ実現されるかはわかりません。
それを完全に別環境で実装するのは難しいでしょう。アセットの事前コンパイルタスクにはデータベースへのアクセスが必要(カスタム CSS などのため)ですが、データベースに依存する部分のみを別ステップで行えれば素晴らしいですね(データベースに依存しない他のアセットも別で事前コンパイルできるなら)。ただし、技術的にそれが実現可能かどうかはわかりません。
私がこれまで行ってきた Kubernetes インストールでは、ほぼ同じように対応しています。データ用と Web 用のコンテナを分離しない、あるいは外部の PostgreSQL や Redis を使わない(私がクライアント向けに実施したインストールでは、それらには GCP リソースを使用しています)状態で k8s を使う方法や理由を想像することはできません。
また、真のゼロダウンタイムアップグレードを実現するためには、skip_post_migration_updates という環境変数の理解が不可欠です。詳細は こちら で説明されています。