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.
Obrigado a todos pelas respostas úteis. Tenho mais algumas perguntas sobre como fazer isso corretamente.
Estou tentando preparar um app.yaml mínimo para inicialização que contenha apenas as informações necessárias para a construção da imagem do contêiner. Algumas das informações ali são claramente destinadas à execução do contêiner — como os mounts de volume e as mapeamentos de portas. Mas não tenho certeza sobre as variáveis de ambiente. Acho que vou apenas tentar, mas essas variáveis de ambiente são usadas durante a construção do contêiner (injetadas no Dockerfile de alguma forma) ou apenas na execução do contêiner? Se for o último, vou garantir que elas acabem no arquivo de configuração do k8s apropriado.
Em segundo lugar, algumas pessoas aqui falaram sobre enviar a imagem para um repositório de contêiner privado. Isso é obrigatório? Por outras palavras — a imagem de construção contém alguma informação secreta que não deveria ser publicada em um repositório público como o Docker Hub? (Ainda não temos um repositório de contêiner privado e gostaria de evitar configurá-lo.)
Por fim, há uma configuração no app.yaml para controlar o nome do contêiner criado? É mais uma questão de acabamento, mas seria bom
.
Obrigado antecipadamente pela ajuda! (Desculpe por reativar um tópico antigo. Esta é a primeira pesquisa no Google ao buscar como instalar o Discourse no Kubernetes.)
@Geoffrey_Challen Você pode criar uma imagem com o repositório do Discourse e os plugins, instalar as gems do Ruby e outras dependências e enviá-la para um registro (como o DockerHub). Esse repositório seria agnóstico ao ambiente e poderia ser público (a menos que você inclua um plugin privado ou algo do tipo). Essa imagem base poderia ser usada em ambientes de staging e produção e até mesmo em projetos diferentes (se eles usarem os mesmos plugins).
Passos como a pré-compilação dos assets, a migração do banco de dados e a geração do certificado SSL devem ser executados na máquina de destino, no entanto, para gerar a imagem final.
Não sei exatamente como incluir isso em um cluster k8s, embora. Optei pela abordagem conservadora e a utilizei com base no guia oficial da equipe do Discourse, apenas separando em 2 etapas.
Não tenho certeza se entendi essa parte. Esses processos não acontecerão automaticamente dentro do contêiner, conforme necessário? Espero poder apenas fazer o push disso para nossa nuvem e nunca precisar acessar o shell da máquina — de forma semelhante ao fato de eu raramente (se é que alguma vez) precisar entrar no contêiner Docker do Discourse.
Você não precisa entrar explicitamente no container. O que quero dizer é que você não pode gerar uma imagem pré-compilada com eles (gerada em um pipeline de CI, por exemplo) e usá-la diretamente, pois isso deve ser executado na máquina de destino, onde o banco de dados está localizado (isso poderia ser automatizado, mas eu não fiz isso no k8s, embora tenha feito com Ansible).
Ah, OK. Estou usando os modelos all-in-one para incluir o banco de dados no container. Isso é adequado para nosso caso de uso, que envolve classes com 10 a 1000 alunos — pelo menos o all-in-one tem funcionado bem para minha turma nessa configuração. Então, o banco de dados está dentro do container.
Mas, de qualquer forma, o Discourse não executa as migrações do banco de dados ou outras etapas de configuração quando o container é iniciado?
Tem certeza de que o banco de dados está dentro do contêiner? Ou é o SGBD (neste caso, PostgreSQL)? A instalação suportada usa o banco de dados fora do contêiner (o que é esperado), mapeando um volume de dentro para fora (do host). Além disso, após uma reconstrução do contêiner, ele é recriado e você perderia todos os dados.
Se realmente estiver dentro do contêiner, não sei exatamente como você conseguiria fazer uma atualização com base na instalação oficial, pois o script launcher parece criar e destruir o contêiner várias vezes durante a reconstrução (e executa com --rm, o que significa que você perderia todos os seus dados, incluindo o banco de dados, após o contêiner parar).
Não tentei alterar a forma como a reconstrução é feita, mas, supondo que você consiga modificá-la para executar tudo dentro do contêiner, sem recriá-lo, então você deverá conseguir enviar o contêiner para um registro (certifique-se de que seja privado, pois as senhas estariam lá). Dito isso, não recomendo essa abordagem por vários motivos (alguns deles mencionados anteriormente).
A instalação padrão inclui nginx, rails, postgres e redis dentro do container. Ela usa volumes externos para os dados do postgres e redis. Eles não são destruídos em uma reconstrução/atualização.
É, achei estranho ele ter dito que o banco de dados está dentro do contêiner, a menos que ele tenha mudado a forma como a instalação padrão funciona, ou esteja se referindo ao PostgreSQL, e não ao banco de dados em si.
Não — as etapas de migração e compilação de ativos ocorrem durante a fase ./launcher bootstrap, após a resolução dos plugins. Depois disso, o contêiner pode ser reiniciado quantas vezes for necessário, ou os processos da web podem ser divididos entre várias máquinas, etc.
Em termos de engenharia, a configuração deve ser algo assim:
./launcher bootstrap usando o Docker aninhado em um nó baseado em VM (sem acesso ao socket do Docker), renomeie e envie a imagem resultante para o registro privado (com uma etiqueta baseada em timestamp, não latest) (local_discourse não é um bom nome aqui) e realize o rollout da implantação para a nova etiqueta
O PostgreSQL roda dentro do container. Ele salva os dados fora do container, mas roda dentro dele se você usar o conjunto padrão de modelos de instalação. O mesmo vale para o Redis. Acho que a confusão surge quando digo “o banco de dados roda dentro do container”: estou me referindo ao servidor de banco de dados, mesmo que os arquivos do banco de dados estejam fora do container. (Mas arquivos de banco de dados não “rodam”, por isso considero minha formulação clara — mas claramente não o suficiente
.)
PS: na verdade, ele nem sempre salva os dados fora do container, a menos que você configure o Docker para fazer o mount da ligação desse diretório. Consegui pular isso durante o bootstrap, embora provavelmente não seja uma boa ideia, pois, nesse caso, o conteúdo do banco de dados não sobreviverá a reinicializações do container.
Acho que isso está fazendo mais sentido para mim agora, especialmente após ler a longa conversa vinculada sobre docker-compose, o script do launcher, etc.
Aqui está o que gostaria de poder fazer:
./launcher bootstrap localmente para criar uma imagem “completa” do Discourse que inclua todas as dependências: postgres, redis, etc../launcher bootstrap para atualizar a imagem e redesplegar sem destruir os dados (óbvio)Minha compreensão é que a imagem completa do Discourse não deve exigir nenhuma dependência de serviço externo. No entanto, para que os dados sobrevivam às atualizações dos contêineres, os arquivos do banco de dados postgres precisam ficar fora do contêiner. Isso não há problema — posso criar um volume persistente do k8s para eles.
Agora, aqui está o único problema que antecipo. A maior parte do que acontece durante ./launcher bootstrap apenas toca em arquivos que vivem dentro do contêiner. Por exemplo, a pré-compilação de ativos. Isso está tudo bem, já que os resultados ficam dentro do contêiner e não precisam sobreviver às atualizações.
A grande exceção aqui é a migração do banco de dados. Essa etapa precisa ter acesso ao banco de dados que será usado após a conclusão do bootstrap. Então, para mim, isso parece ser o principal obstáculo para implantar facilmente imagens completas do Discourse na nuvem.
Percebi que @sam mencionou várias vezes que eles redesplegam o Discourse para seus clientes usando um fluxo de trabalho aproximadamente semelhante ao que descrevi acima. Mas suspeito que a razão pela qual isso funciona é que as imagens do Discourse deles são configuradas para usar um servidor de banco de dados (e provavelmente o Redis também) que roda em seu cluster — o que faria sentido para suportar várias implantações, mas não é exatamente o que quero fazer. Isso significa que o processo de bootstrap pode modificar o banco de dados de produção — ou talvez apenas que a etapa de migração do banco de dados seja pulada completamente, já que atualizações e migrações do banco de dados são tratadas externamente. @sam: você poderia confirmar?
De qualquer forma, a conclusão disso para mim é que preciso encontrar uma maneira de executar as migrações do banco de dados quando o contêiner iniciar, e não durante o ./launcher bootstrap. Acho que, nesse ponto, uma maneira de fazer isso seria:
./launcher bootstrap, usando um ponto de montagem de volume apontando para um banco de dados local vazio, já que esse banco de dados não será usado posteriormente. Isso colocaria tudo certo dentro do contêiner, apenas não finalizaria o trabalho do postgres.Você pode estar interessado em uma configuração multi-site.
Existem dois grandes problemas que você está enfrentando: o Discourse não está pronto para o Kubernetes, então é necessário código personalizado. E você está entrando no território do que a equipe do Discourse faz para ganhar dinheiro (hospedar um grande número de fóruns), então o nível de suporte que você recebe diminuirá.
Meu conselho? Faça uma configuração multi-site com agendamento estático em VMs, totalmente fora do seu cluster. (Ou um Service do tipo ExternalName apontando para a VM para manter o mesmo Ingress.)
OK… Consegui descobrir uma maneira de fazer isso. Não estou 100% satisfeito com a solução, mas ela funciona e pode ser atraente para outros que estão tentando implantar uma imagem Discourse “gorda” (que inclui PostgreSQL, Redis, etc.) em um único contêiner no Kubernetes.
Após examinar o processo de inicialização (bootstrap), ficou claro para mim que, infelizmente, ele mistura dois tipos diferentes de operações: aquelas que afetam apenas o contêiner subjacente e outras que interagem com o ambiente externo, principalmente através da montagem de volume /shared, onde os arquivos de dados do PostgreSQL residem. Em vez de tentar separar esses passos, parece mais sensato executar os passos de inicialização diretamente no ambiente onde o contêiner será realmente implantado.
Infelizmente, o launcher bootstrap deseja criar um contêiner e usar o Docker. Portanto, executar o launcher dentro de outro contêiner (por exemplo, em um contêiner rodando em nossa nuvem) significa lidar com uma configuração Docker-in-Docker (viável, mas não considerada uma prática recomendada) ou expor o daemon Docker subjacente. Nem mesmo tenho certeza se essa segunda abordagem funcionaria, pois acredito que ela interpretaria uma montagem de volume contra o sistema de arquivos local do nó, enquanto, em nosso cenário, queremos montar o volume /shared em um volume persistente do Kubernetes. Talvez a rota Docker-in-D funcionasse, mas então você teria uma estranha montagem tripla de volumes: de dentro do contêiner aninhado para o contêiner externo e, dali, para o volume persistente do Kubernetes. Isso soa… pouco sábio.
No entanto, essencialmente, o launcher bootstrap cria um único arquivo .yml grande processando o valor templates no app.yml e, em seguida, passa isso para a imagem base do Discourse quando termina o processo de inicialização. Então, se pudermos extrair o arquivo de configuração, podemos gerar a configuração em qualquer máquina e, em seguida, precisamos apenas descobrir como passá-la para um contêiner que iniciamos na nuvem.
Portanto, como uma visão geral, aqui estão os passos que vamos seguir:
launcher modificado.pups) e, em seguida, iniciará o Discourse.Aqui está a alteração necessária no launcher para suportar um comando dump que escreve a configuração mesclada na saída padrão (STDOUT):
run_dump() {
set_template_info
echo "$input"
}
(Observe que este comando está disponível em nosso fork do discourse_docker.)
Então, o primeiro passo é usar o novo comando launcher dump adicionado acima para criar nossa configuração de inicialização:
# Substitua pelo nome da configuração do seu contêiner para o app
./launcher dump app > bootstrap.yml
Em seguida, precisamos de um contêiner que saiba executar pups para inicializar o contêiner antes de iniciar via /sbin/boot. Usei o seguinte Dockerfile para fazer uma pequena alteração na imagem base do Discourse:
FROM discourse/base:2.0.20191219-2109
COPY scripts/bootstrap.sh /
CMD bash bootstrap.sh
Onde scripts/bootstrap.sh contém:
cd /pups/ && /pups/bin/pups --stdin < /bootstrap/bootstrap.yml && /sbin/boot
Publiquei isso como geoffreychallen:discourse_base:2.0.20191219-2109. (Observe que você provavelmente também poderia accomplir a mesma coisa modificando o comando de inicialização da imagem Docker base do Discourse, mas tive dificuldade em fazer isso funcionar com a redirecionamento de shell necessário para que o pups lesse o arquivo de configuração.)
Agora precisamos da nossa configuração do Kubernetes. A minha se parece com isto:
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
A sua será diferente. Observe que estou encerrando o HTTPS a montante (upstream), daí as modificações na configuração do Ingress. Também gosto de colocar tudo em um único arquivo, excluir partes que não funcionam enquanto iterio e, em seguida, deixar o Kubernetes pular duplicatas no próximo kubectl create -f. Note também que defini replicas: 0 para que a implantação não inicie assim que for configurada. Isso ocorre porque temos uma parte adicional de configuração para finalizar.
Copiei a lista de variáveis de ambiente do que vi sendo passada para o contêiner pelo launcher start. Não sei se todas essas são necessárias e outras podem estar faltando dependendo da sua configuração. YMMV (Seu Milha Pode Variar).
Observe que temos dois mapeamentos de volume apontando para o contêiner: o primeiro é para o PostgreSQL, configurado como um volume persistente que sobreviverá a reinicializações de pod. O segundo é um mapeamento de configuração criado assim:
kubectl create configmap kotlin-forum-bootstrap --from-file=bootstrap.yml=<path/to/bootstrap.yml>
Onde kotlin-forum-bootstrap precisa corresponder à sua configuração do Kubernetes e path/to/bootstrap.yml é o caminho para o arquivo bootstrap.yml que criamos usando o launcher dump acima.
Uma vez que seu configmap esteja no lugar, você deverá ser capaz de escalar sua implantação para uma réplica e ver o Discourse iniciando e executando o mesmo processo de inicialização que o launcher bootstrap teria realizado. Isso leva alguns minutos. Quando isso for concluído, sua instalação do Discourse iniciará.
Algumas outras notas que encontrei no caminho para obter isso (pelo menos por enquanto) totalmente configurado:
X-Forwarded, incluindo tanto X-Forwarded-For, X-Forwarded-Proto e X-Forwarded-Port. Não fazer isso resultará em erros de autenticação estranhos ao tentar usar o login do Google e provavelmente outros provedores de login.nginx deve ser configurado para passar cabeçalhos definindo use-forwarded-headers no configmap global. Isso me levou um tempo para acertar, pois, pelo menos várias vezes, editei o configmap errado e, em seguida, esperei que meus contêineres de entrada reiniciassem quando o configmap mudasse. (Eles não reiniciaram.)Para atualizar a instalação implantada, você gera o novo arquivo bootstrap.yml, atualiza o configmap e, em seguida, reinicia o contêiner (o mais fácil é escalar para 0 e depois de volta para 1 réplica).
Isso incorre em um pouco de tempo de inatividade, pois a inicialização ocorre antes que o contêiner seja construído. Mas isso me parece inevitável em casos onde você precisa atualizar a configuração e/ou alterar a imagem base. O launcher rebuild é documentado como stop; bootstrap; start, o que significa que o processo de inicialização ainda causará tempo de inatividade, mesmo que seja realizado usando o script do launcher.
Esse padrão de implantação de Discourse com contêiner “gordo” seria muito mais fácil de suportar se o script launcher separasse mais limpiamente (a) os passos de inicialização que poderiam ser realizados offline e afetarem apenas os arquivos no contêiner e (b) os passos de inicialização que modificam ou precisam de acesso ao banco de dados ou outro estado fora do contêiner. A abordagem descrita acima é um pouco frustrante porque você vê todo tipo de ofuscação de JS, minificação de ativos e outras coisas que poderiam ser feitas com a implantação anterior em execução… mas elas estão simplesmente misturadas demais com outras coisas (como migrações de banco de dados) que não podem ser feitas sem acesso ao banco de dados. Pensei brevemente em criar um contêiner que realizasse apenas os passos em templates/postgres.yml, mas então notei que as migrações de banco de dados estavam sendo feitas pelo template da web, pensei em plugins e então desisti
.
Com uma melhor separação, a reimplantação para contêineres gordos poderia funcionar mais ou menos assim:
Isso resultaria em um pouco menos de tempo de inatividade. Provavelmente não vale a pena o esforço por esse motivo apenas, mas posso imaginar que isso também poderia simplificar cenários de implantação mais complexos envolvendo bancos de dados compartilhados ou o que quer que seja.
Isso faz mais sentido. Foi o que fiz ao separar em duas etapas o estágio de inicialização (bootstrap). A primeira pode rodar em um ambiente isolado (como um pipeline de CI), gerando uma imagem base com o repositório do Discourse, gems e plugins instalados. A segunda etapa precisa rodar na máquina de destino (ou ter, pelo menos, acesso ao banco de dados de produção) para realizar a migração do banco de dados e gerar os assets (isso é feito no processo de bootstrap, embora não ao iniciar o contêiner).
Sim, isso seria incrível. Já solicitei isso, mas não sei se e quando isso será feito.
Isso seria difícil de implementar completamente em um ambiente separado, pois a tarefa de pré-compilação de assets precisa de acesso ao banco de dados (para coisas como CSS personalizado). No entanto, seria ótimo se apenas o que depende do banco de dados pudesse ser feito em uma etapa separada (e todos os outros assets, que não dependem do banco de dados, pudessem ser pré-compilados separadamente), mas não sei quão viável seria implementá-lo tecnicamente.
Isso é praticamente o que faço nas instalações do Kubernetes que realizei. Não consigo imaginar como ou por que usar o k8s sem contêineres separados para dados e web (ou algum outro tipo de PostgreSQL e Redis externos — as instalações que fiz para clientes usam recursos do GCP para isso).
Além disso, há uma variável de ambiente chamada skip_post_migration_updates que você precisa entender para upgrades verdadeiramente sem tempo de inatividade. Ela é descrita aqui.