Como usar uma única imagem para múltiplos ambientes com Docker

joão narciso
Jota

--

Cenário:

Estamos desenvolvendo um SPA com uma arquitetura robusta e queremos disponibilizá-lo em três ambientes diferentes: Desenvolvimento, Homologação (QA) e Produção (podemos também ter dois ambientes de produção). Cada versão do App deve consumir as APIS de seus respectivos ambientes.

Problema:

O empacotamento não permite alterar as variáveis de ambiente em tempo de execução.

Solução:

Existem algumas soluções disponíveis, mas aqui trataremos de uma bem simples de ser aplicada. Nesse caso em específico estaremos realizando as alterações em um projeto iniciado a partir do “create-react-app”, mas nada impede da solução ser utilizada com outras tecnologias, como Vue.

1 — Organização de e armazenamento das variáveis de ambiente

Vamos começar criando 3 arquivos (um para cada ambiente) na pasta pública:

Dentro dos arquivos vamos salvar os valores dentro do objeto window, que representa a janela do navegador. Nossas variáveis serão salvas dentro do atributo ENV (o nome aqui não faz diferença, poderia ser window.VARIAVEIS, window.API etc.)

Nossos arquivos (config-dev, config-qa e config- prod) devem ficar, com suas respectivas rotas, assim:

Agora precisamos criar um config.js na raiz da pasta pública, que é onde nosso app irá consumir as rotas. O Arquivo pode ficar vazio por enquanto, pois vamos gerar seu conteúdo automaticamente a partir dos arquivos que criamos anteriormente.

No fim nossa pasta pública ficou assim:

2 — Declarar config.js antes do React ser executado

Precisamos chamar nosso config.js diretamente no index.html para salvar nossas variáveis no navegador antes que os serviços sejam executados (o que resultaria em undefined):

3 — Configurar serviços

Agora precisamos fazer com que nossos serviços consumam as rotas salvas no objeto window. Por organização vamos criar uma pasta envinroments dentro do nosso projeto com um único arquivo index.js e apenas uma linha de código:

Agora com um simples import ENV from '@enviroments'podemos ter acesso as rotas e consumi-lás em nossos serviços.

4 — Configurar ambiente de desenvolvimento local

Precisamos configurar nosso ambiente de desenvolvimento local para que seja possível utilizar o comando npm run {env} e gerar automaticamente o conteúdo do arquivo config.js que criamos anteriormente.

Vamos executar npm install cpy-clipara insalar a lib cpy-cli, que por sua vez nos permite copiar e renomear arquivos. Agora vamos configurar os scripts em package.json:

"scripts": {"dev": "npm run env:dev && node scripts/start.js","qa": "npm run env:qa && node scripts/start.js","prod": "npm run env:prod && node scripts/start.js","env:dev": "cpy ./public/config/config-dev.js ./public/ --rename config.js","env:qa": "cpy ./public/config/config-qa.js ./public/ --rename config.js","env:prod": "cpy ./public/config/config-prod.js ./public/ --rename config.js","start": "npm run env:dev && node scripts/start.js","build": "node scripts/build.js","test": "node scripts/test.js"},

Explicando a configuração:

Quando executarmos um npm run {env}nosso script irá copiar o arquivo config-{env} para a raíz da pasta pública e renomear para config.js, substituindo o atual arquivo (que neste caso está vazio).

5 — Shell Script e Docker

Chegamos na última parte.

Criaremos um shell script (por questões de organização, o mesmo poderia ser passado diretamente no dockerfile):

#!/usr/bin/env bashARG1="$1"echo "Configurando ambiente de ${ARG1}"cp "/opt/bitnami/nginx/html/config/config-${ARG1}.js" /opt/bitnami/nginx/html/config.jsset -o errexitset -o nounsetset -o pipefail# set -o xtrace # Remova o comentário desta linha para debug# Carregar bibliotecas. /opt/bitnami/scripts/libbitnami.sh. /opt/bitnami/scripts/libnginx.sh# Carregar variáveis de ambiente NGINX. /opt/bitnami/scripts/nginx-env.shprint_welcome_pageinfo "** Starting NGINX setup **"/opt/bitnami/scripts/nginx/setup.shinfo "** NGINX setup finished! **"/opt/bitnami/scripts/nginx/run.shecho ""exec "nginx -g daemon off;"

e vamos salvar como environment-tag.sh dentro da nossa pasta config do diretório público.

Explicando a configuração:

Na hora de executar o container, docker run, vamos passar um parâmetro (neste caso dev, qa ou prod), nosso arquivo shell vai receber esse parâmetro, atribuir a variável ARG1 e a partir disso copiar o respectivo arquivo(config-dev, config-qa ou config-prod) para a raíz da pasta pública (que definitivamente será consumida).

Agora precisamos configurar nosso dockerfile para executar o shell script:

FROM node:12.14.0 as build-stageWORKDIR /usr/COPY package*.json public config scripts src ./  ./RUN npm install && \npm run-script buildFROM bitnami/nginx:1.17COPY --from=build-stage /usr/build/ /opt/bitnami/nginx/htmlCOPY nginx.conf /opt/bitnami/nginx/conf/nginx.confUSER 0:0RUN ["/bin/bash", "-c", "chmod +x /opt/bitnami/nginx/html/config/environment-tag.sh"]ENTRYPOINT ["/opt/bitnami/nginx/html/config/environment-tag.sh"]CMD ["dev"]

Explicando a configuração:

  • “USER” executa chmod como root.
  • “ENTRYPOINT” executa o shell script.
  • “CMD” define um parâmetro padrão, que pode ser alterado, por linha de comando, na hora de executar o container: docker run [nome-da-imagem] {arg}.

Pronto. Agora podemos economizar tempo gerando apenas uma imagem e fazendo quantos deploys quisermos em quantos ambientes quisermos:

docker build --tag projeto:1.0 . // build
docker run -ti projeto {env} // substituir {env} por dev, qa ou prod

--

--