Criando uma imagem Docker com o banco de dados pré-carregado
Imagine que você possua o seguinte dump de uma base de dados Postgres.
Uma base simples com uma tabela Clients que contém 2 registros.
Se desejarmos iniciar um container Docker Postgres com essa base já pré-carregada para poder distribuir para as pessoas do time, podemos colocar esse arquivo sql na pasta /docker-entrypoint-initdb.d/ dentro do container, como informado na documentação da página da imagem no DockerHub:
Initialization scripts
If you would like to do additional initialization in an image derived from this one, add one or more *.sql, *.sql.gz, or *.sh scripts under /docker-entrypoint-initdb.d (creating the directory if necessary). After the entrypoint calls initdb to create the default postgres user and database, it will run any *.sql files, run any executable *.sh scripts, and source any non-executable *.sh scripts found in that directory to do further initialization before starting the service.
O seguinte Dockerfile usa a imagem postgres:11-alpine como base e realiza a cópia do arquivo test_dump.sql para a pasta indicada:
Se realizarmos o build desse arquivo:
$ docker image build . -t preloaded_db:latest
E inicializarmos um container da imagem gerada:
$ docker container run -d --rm -p 5432:5432 -e POSTGRES_USER=postgres --name test_preloaded_db preloaded_db:latest
Podemos verificar no client do Postgres que o banco foi criado e inicializado:
$ psql -h localhost -U postgrespostgres=# \c my_db
psql (11.3, server 11.5)
You are now connected to database “my_db” as user “postgres”.
my_db=# SELECT * FROM clients;
id | name
— — + — — — — —
1 | Client 1
2 | Client 2
(2 rows)
Se analisarmos o log desse container:
$ docker container logs test_preloaded_db
Vemos vários comandos do Postgres sendo executados. O que nos indica que o dump está sendo processado ao criar o container.
Ok. Nosso banco está sendo carregado, porém o dump está sendo executado toda vez que o container é criado. Se destruirmos o container e criarmos outro, o arquivo .sql vai ser executado novamente. Essa abordagem é ok, porém se tivermos um dump muito grande, a inicialização do container é mais lenta, pois a execução do dump pode demorar para ser completada. Para resolvermos isso, podemos gerar uma imagem com a base de dados já pré carregada na imagem
Antes de continuarmos, vamos parar o container que criamos:
$ docker container rm -f test_preloaded_db
Pré-carregando o banco na imagem
Para pré-carregar a imagem, precisamos fazer nosso Dockerfile executar o mesmo entrypoint da imagem original do Postgres, para ele realizar a inicialização do banco no processo de build. Vamos utilizar Multi-Stage build para dividirmos o build em duas etapas. A primeira irá executar o entrypoint com o arquivo de dump, e a segunda irá copiar a pasta de dados do Postgres para a imagem final
No primeira etapa temos as seguintes instruções:
- FROM postgres:11-alpine as dumper: Definimos a imagem base que nossa etapa irá usar. Nesse caso, imagem `postgres` com a tag `11-alpine`.
- COPY test_dump.sql /docker-entrypoint-initdb.d/: Copiamos o arquivo `test_dump.sql`, que contém o dump do nosso banco de dados para pasta `/docker-entrypoint-initdb.d/`
- RUN ["sed", "-i", "s/exec \"$@\"/echo "skipping…\"/", "/usr/local/bin/docker-entrypoint.sh"]: Nessa linha, executamos o sed para trocar o conteúdo exec "$@" para skipping… do arquivo /usr/local/bin/docker-entrypoint.sh. Fazendo isso, quando executarmos o entrypoint, ele irá carregar o script sql da pasta porém não irá iniciar o daemon do Postgres, pois não precisamos. Só queremos que ele faça a pré-carga dos dados para podermos copiar para a etapa posterior.
- ENV PG_USER=postgres ; ENV PGDATA=/data: Nessas duas linhas configuramos duas variáveis de ambientes. Uma para definir que o usuário será o `postgres` e outra para informar que a pasta de dados do postgres será `/data`.
- RUN ["/usr/local/bin/docker-entrypoint.sh", "postgres"]: Aqui executamos o entrypoint em si. Ele irá executar o dump e carregar os dados na pasta `/data`. E como executamos o`sed` para remover o `$@`, ele não irá executar o daemon do postgres.
Todas essas alterações de arquivos e variáveis de ambientes só ficam disponíveis nessa etapa de build. Na próxima, como veremos, a etapa vai se basear novamente da imagem base do Postgres, sem nenhuma configuração modificada que fizemos nessa etapa.
Na segunda etapa temos apenas a seguinte instrução:
- COPY — from=dumper /data $PGDATA: Essa instrução copia todos os arquivos da pasta `/data` da etapa `dumper` para a pasta `$PGDATA` dessa etapa, fazendo nossos dados do dump serem já carregados na imagem.
Se realizarmos um build desse Dockerfile:
$ docker image build . -t preloaded_db:new
Vemos no output que ele executa o script sql e gera a imagem.
Se criarmos um container com essa imagem:
$ docker container run -d --rm -p 5432:5432 -e POSTGRES_USER=postgres --name test_preloaded_db preloaded_db:new
Vemos que nosso banco de dados está carregado:
$ psql -h localhost -U postgrespsql (11.3, server 11.5)
Type “help” for help.postgres=# \c my_db
psql (11.3, server 11.5)
You are now connected to database “my_db” as user “postgres”.
my_db=# SELECT * FROM clients;
id | name
— — + — — — — —
1 | Client 1
2 | Client 2
(2 rows)
E ao analisar os logs, vemos que o dump não é executado ao criar o container:
$ docker container logs test_preloaded_db2019–09–16 01:42:22.458 UTC [1] LOG: listening on IPv4 address “0.0.0.0”, port 5432
2019–09–16 01:42:22.458 UTC [1] LOG: listening on IPv6 address “::”, port 5432
2019–09–16 01:42:22.460 UTC [1] LOG: listening on Unix socket “/var/run/postgresql/.s.PGSQL.5432”
2019–09–16 01:42:22.470 UTC [18] LOG: database system was shut down at 2019–09–16 01:41:02 UTC
2019–09–16 01:42:22.473 UTC [1] LOG: database system is ready to accept connections
Vemos que apenas a inicialização do Postgres é feita. Não há nenhum comando da execução do dump em si, isso foi realizado no build da imagem.
Criando um Makefile para facilitar o processo
Para facilitar o processo de dump e criação da imagem customizada, eu gosto de criar um Makefile que contém uma série de passos para criar a imagem e tagear ela pela data, me possibilitando ter dumps diários no meu registry para poder baixar.
E eu consigo executar o seguinte comando para gerar a imagem com o dump presente (apenas trocando as variáveis para a configuração certa)
make DB_ENDPOINT=127.0.0.1 DB_USER=postgres DB_PASSWORD=password DB_NAME=my_db TARGET_IMAGE=myapp DESTINATION_REPOSITORY=gcr.io/my_project
Esse comando é integrado em um Cron para ser executado diariamente, então eu tenho em meu repositório imagens com dump de Dia - 1.
Outro ponto interessante é você colocar mais um script SQL para ser executado que irá realizar a ofuscação dos dados dos usuários. Esse artigo pode ser interessante para saber mais sobre.
Thanks ☕️