Construindo a imagem sem tocar no banco de dados

Olá a todos.

Tenho uma instância do Discourse bastante pequena em execução (na verdade, há anos, com praticamente zero problemas): https://discuss.cubeisland.de/.
Tenho usado o processo de implantação padrão baseado no launcher em uma VM dedicada (em meu próprio hardware em um data center). A única coisa que mudei ao longo dos anos foi migrar para um banco de dados PostgreSQL externo compartilhado.

Recentemente, comecei a migrar aplicativos de VMs dedicadas para um swarm Docker como passo preparatório para eventualmente migrar para um cluster Kubernetes, principalmente para economizar recursos e tornar partes da infraestrutura mais “elásticas”.

Hoje foi o dia em que olhei para essa pequena instância do Discourse, uma das poucas VMs de aplicativo dedicadas restantes. “Já está rodando no Docker, quão difícil pode ser implantá-la em um swarm”, pensei. E, pelo que li, realmente seria. Posso simplesmente pegar a imagem da instância em execução atualmente, enviá-la ao nosso registro interno e executá-la no swarm, e tudo funcionaria perfeitamente, o que é ótimo.

Analisei os arquivos do launcher, especialmente os templates e exemplos, e percebi que provavelmente seria uma boa ideia ter o Redis separado em tal implantação. Talvez eu pudesse configurar um job de CI para construir novas imagens quando adicionar plugins ou quiser atualizar. Então, verifiquei o discourse_docker localmente, copiei minha definição de container app.yml existente para a cópia e tentei executar ./launcher bootstrap app para construir uma imagem que eu pudesse então enviar ao meu registro, sem implantá-la imediatamente.

Para minha surpresa, o script tentou conectar ao servidor PostgreSQL de “produção” para migrar o banco de dados, o que, felizmente, ele não tinha acesso a partir da minha estação de trabalho local.

Procurei por aqui e aparentemente é assim que funciona, o que me faz questionar:

  1. Como construiria um container para uma nova instância, onde ainda não tenho um banco de dados? Preciso configurar o banco de dados de produção antes de poder construir a imagem?
  2. Suponho que essa seja a única vez que db:migrate é executado. Então, se eu tiver várias instâncias semelhantes (por exemplo, produção e teste), precisaria atualizar uma das instâncias para construir a nova imagem e, em seguida, não poderia usar a mesma imagem para a segunda instância, mesmo que a imagem fosse idêntica.
  3. Como proceder para construir imagens para instâncias onde o servidor de banco de dados não é acessível a partir do sistema que está construindo a imagem (o que não deve ser tão incomum).

Depois de ler alguns posts (obviamente incluindo este), estou perfeitamente ciente das razões para o processo de construção como ele está atualmente e vejo o valor dele para os mencionados 99% das pessoas que implantam o Discourse casualmente em suas VMs padrão completas. E estou muito acostumado com modelos de container “tudo em um” e não me oponho a isso. Afinal, o valor principal do Docker vem do fato de que o fornecedor do software pode pré-configurar configurações altamente otimizadas e agrupá-las em um ambiente de execução reproduzível, eliminando a necessidade de muito conhecimento específico do aplicativo no lado das operações. Então, estou totalmente alinhado com o uso das ferramentas fornecidas por vocês. Por que eu esperaria que alguém construísse containers melhores do que o próprio fornecedor do software? Por que eu gostaria de separar o nginx e o aplicativo Ruby, quando não há nenhum benefício a ser ganho, apenas para tornar a implantação mais “pura” (o que quer que isso signifique)?

No entanto, é estranho ver um container que está mutando o estado de execução enquanto está longe de estar em execução. Já executo vários aplicativos em containers e containerizei vários eu mesmo, alguns dos quais nunca foram destinados a rodar em containers.

O principal exemplo que me vem à mente, de um aplicativo que lida com requisitos/problemas semelhantes de maneira similar ao Discourse, é o GitLab. Embora eles agora forneçam um elegante Helm chart para uma implantação Kubernetes totalmente decomposta “como deveria ser”, estou supondo (sem olhar números) que uma porcentagem semelhante de 99% de suas implantações de pequeno a médio porte estão usando a imagem Docker omnibus do GitLab (ou o pacote do SO, que é praticamente a mesma coisa). Eles têm um processo de inicialização semelhante, mas baseado no chef dentro do container, que é executado a cada inicialização e faz as coisas usuais, como migrações de banco de dados e compilação de ativos.

Sim, a inicialização do GitLab pode levar vários minutos devido a isso, mas isso nunca foi um problema para as implantações que vi (algumas em empresas maiores). Especialmente com sistemas modernos de orquestração como Docker Swarm e Kubernetes e outros, que podem executar atualizações contínuas para você, onde a instância antiga é desligada apenas se a nova instância estiver em execução e passar com sucesso nos testes de saúde e prontidão, um processo de implantação demorado pode não ser realmente um problema. Mas mesmo sem atualizações contínuas sofisticadas, que podem ou não funcionar, você também pode lidar com bastante tempo de inatividade em muitas situações.

Então: É possível configurar o launcher para ignorar as operações dependentes do banco de dados durante a construção da imagem e, em vez disso, realizar essas operações durante a inicialização do container?

Estou definitivamente disposto a investir um pouco de tempo nisso eu mesmo, mas meu tempo à noite é limitado, então qualquer orientação seria muito bem-vinda.

Também estou aberto a processos completamente diferentes se vocês acharem que isso é estúpido ou nem mesmo possível, etc.

Obrigado por qualquer feedback!

5 curtidas

Eu queria fazer o mesmo que você — nós rodamos o Discourse no Amazon ECS, então precisávamos ser capazes de construir apenas a imagem da web e enviá-la para um registro. Não quis mexer no processo de build do Discourse porque queremos manter o máximo possível da instalação suportada.

Em vez disso, usamos o script launcher normal para construir uma configuração de dois contêineres em uma máquina local, mas ignoramos o contêiner de dados e enviamos o contêiner da web para o registro. Em tempo de execução, sobrescrevemos os detalhes de conexão do Postgres e do Redis por meio de variáveis de ambiente.

A implantação da nova imagem é um processo de 3 etapas:

  1. Execute as pré-migrações seguras. Faça o ECS executar este comando (com a nova imagem):

     SKIP_POST_DEPLOYMENT_MIGRATIONS=1 rake db:migrate
    
  2. Implante a nova imagem. Atualize o serviço do ECS.

  3. Execute as pós-migrações. Faça o ECS executar este comando:

     SKIP_POST_DEPLOYMENT_MIGRATIONS=0 rake db:migrate
    

Ter um contêiner de dados local rodando enquanto construímos a imagem provavelmente é desperdício, mas isso significa que podemos usar o web.template.yml padrão sem nos preocupar com quais partes tentam se comunicar com o banco de dados ou o Redis.

8 curtidas

Obrigado por isso! Também pensei que poderia simplesmente levantar um PostgreSQL durante a construção da imagem e descartá-lo assim que a construção real for concluída.

2 curtidas

Finalmente consegui dedicar um tempo para implementar isso!

Implementei a construção da imagem usando um pipeline do GitLab CI que executa o PostgreSQL e o Redis como serviços durante a construção e os descarta em seguida:

Agora, só preciso automatizar a implantação com as migrações do banco de dados.

2 curtidas

Este sistema está funcionando há mais de um ano sem que eu o toque nunca, nem mesmo para a versão 2.8.

2 curtidas

Movi a construção da imagem para o GitHub: GitHub - pschichtel/discourse-docker: A reusable Discourse container built using the launcher tool.

A imagem está publicada em pschichtel/discourse:stable-web_only

parece que isso finalmente quebrou. ao atualizar de 3.0.6 para 3.1.0, nenhuma migração de banco de dados foi realizada. No entanto, executar o bundle exec rake db:migrate final dentro do contêiner em execução funcionou, embora apenas após outra reinicialização do contêiner.

Você terá que migrar novamente quando a nova imagem for iniciada sem essa variável de ambiente definida. Existe uma tarefa rake que fará isso, mas não consigo me lembrar dela nem encontrá-la pelo celular. Algo como ensure_post_migrations.

Para que valha, não notei nenhuma falha. Eu sigo principalmente o branch de lançamento beta e, até onde sei, as migrações foram executadas corretamente em todas as etapas da série 3.1.0.beta…

Encontrei db:ensure_post_migrations via rake -AT.

Qual é a diferença entre db:migrate com SKIP_POST_DEPLOYMENT_MIGRATIONS=0 e db:ensure_post_migrations?

Ok, depois de dar uma olhada no código, entendi o que db:ensure_post_migrations faz. Ele deve ser usado na mesma execução do rake antes do db:migrate para garantir que SKIP_POST_DEPLOYMENT_MIGRATIONS seja definido como 0. Meu script já garante isso:

o .gitlab-ci.yml:

./migrate.sh pre || echo "Redis não está rodando durante as pré-migrações, pulando..."
docker stack deploy --prune --resolve-image always -c "$STACK.yml" "$STACK"
./docker-stack-wait.sh -t 180 "$STACK"
./migrate.sh post

o migrate.sh:

#!/usr/bin/env sh

if [ "$(docker ps -q --filter "label=com.docker.stack.namespace=${STACK}" --filter "label=com.docker.swarm.service.name=${STACK}_${DISCOURSE_REDIS_HOST}" | wc -l)" = "0" ]
then
    echo "Nenhum container redis encontrado, impossível executar migrações!"
    exit 1
fi

if [ "$1" = "pre" ]
then
    skip_post=1
else
    skip_post=0
fi


docker run \
    --rm \
    --name "discourse-migration-${DISCOURSE_DB_HOST}-${DISCOURSE_DB_NAME}" \
    --network "${STACK}_discourse" \
    --workdir /var/www/discourse \
    -u discourse \
    -e SKIP_POST_DEPLOYMENT_MIGRATIONS="$skip_post" \
    -e LANG="${LANG}" \
    -e DISCOURSE_DEFAULT_LOCALE="${DISCOURSE_DEFAULT_LOCALE}" \
    -e DISCOURSE_HOSTNAME="${DISCOURSE_HOSTNAME}" \
    -e DISCOURSE_DEVELOPER_EMAILS="${DISCOURSE_DEVELOPER_EMAILS}" \
    -e DISCOURSE_SMTP_ADDRESS="${DISCOURSE_SMTP_ADDRESS}" \
    -e DISCOURSE_SMTP_PORT="${DISCOURSE_SMTP_PORT}" \
    -e DISCOURSE_DB_USERNAME="${DISCOURSE_DB_USERNAME}" \
    -e DISCOURSE_DB_PASSWORD="${DISCOURSE_DB_PASSWORD}" \
    -e DISCOURSE_DB_HOST="${DISCOURSE_DB_HOST}" \
    -e DISCOURSE_DB_NAME="${DISCOURSE_DB_NAME}" \
    -e DISCOURSE_REDIS_HOST="${DISCOURSE_REDIS_HOST}" \
    "$DISCOURSE_IMAGE" \
    bundle exec rake db:migrate

Ele executa db:migrate com SKIP_POST_DEPLOYMENT_MIGRATIONS=1 na nova imagem no docker swarm enquanto o discourse ainda está rodando a versão antiga. Em seguida, ele implanta a nova imagem no swarm e espera que ela converja. No final, ele executa db:migrate novamente, mas com SKIP_POST_DEPLOYMENT_MIGRATIONS=0.

Isso funcionou de forma confiável para todas as versões por mais de 2 anos agora. Dado que funcionou para você, @simonk, você fez algo fundamentalmente diferente em comparação com o meu script?

1 curtida

Não, eu ainda estou seguindo o mesmo processo que descrevi aqui em cima, que, até onde sei, é muito parecido com o seu. Eu uso um rake db:migrate simples em vez de bundle exec rake db:migrate, mas não consigo imaginar que isso faria muita diferença.

Eu nunca usei docker stack ou swarm. Há alguma chance de um bug em algum lugar nos seus scripts que possa fazer com que o script migrate.sh use a imagem antiga em vez da nova?

Eu não verifiquei isso explicitamente, vou investigar. O swarm definitivamente usará a imagem mais recente, mas talvez o script de CI por algum motivo não tenha usado a mais recente.

Investiguei isso agora com a atualização 3.1.1. De fato, o script de CI estava usando uma versão mais antiga do contêiner.