Go + MySQL
Docker-compose em ação — Artigo 5
Postman collection, código do artigo, diff com o artigo 4.
Sumário: Desenvolvimento de APIs em Go
Próximo Artigo: Go + GORM — O melhor ORM de Go
Artigo Anterior: Go + Repository + Command-Query Separation
Introdução
Existem diversos tipos de bancos de dados, inclusive muitos tipos de bancos de dados relacionais. O MySQL é apenas mais um desses bancos relacionais. Novamente, não vamos nos concentrar nas razões pelas quais você deve usar ou não um banco de dados relacional, nem mesmo nos motivos pelos quais você deve optar pelo MySQL como banco de dados relacional. O objetivo deste artigo é demonstrar como realizar a integração de uma API Go com o MySQL via docker e automatizar os procedimentos de execução com docker-compose.
Alterações Gerais
Anteriormente, o código da API estava localizado na raiz do repositório. No entanto, como estamos agora adicionando um banco de dados, optei por mover o código da API para uma nova pasta chamada (productapi) e criar outra pasta chamada (productdb) para o banco de dados.
Dentro de (productapi), criei uma nova pasta na raiz chamada (configs), seguindo o padrão apresentado anteriormente do project-layout. Nessa pasta, serão armazenados os arquivos de configuração da nossa API. Até o momento, as únicas configurações da nossa API serão as variáveis de acesso ao banco de dados MySQL.
Até o momento, temos dois arquivos de configuração: um para executar a API localmente (local.yml) e outro para executar a API dentro do docker (docker.yml). Esses arquivos são semelhantes, com exceção da URL do banco de dados. Isso acontece porque, ao executar a API localmente e o MySQL no docker, é possível acessar o banco de dados através do localhost. No entanto, quando a API estiver sendo executada dentro de um container, não será possível acessar o banco de dados usando localhost, uma vez que a API e o banco de dados serão containers separados. Nesse cenário, a URL vira o nome do container do banco de dados dentro do contexto do docker-compose.
Foram realizadas algumas alterações no arquivo main. Basicamente, adicionamos um procedimento para ler os arquivos de configuração localizados em (productapi/configs). Utilizamos essas configurações para estabelecer a conexão com o banco de dados e, em seguida, organizamos o código da API de acordo com o padrão de injeção de dependência. No entanto, deixaremos o assunto da injeção de dependência para ser abordado em um episódio posterior.
A função utilizada para ler a configurações de ambiente (config.Load) é simples e vem a seguir.
Foi criado um objeto chamado (Config) que é responsável por permitir a serialização do arquivo YAML em uma struct Go. Dentro da função (Load), primeiro obtemos o ambiente em que o código será executado através de uma variável de ambiente chamada (env). Em seguida, com base no ambiente, carregamos o arquivo correto. No cenário em que a API é executada localmente, essa variável (env) não existe. Nesse caso, o código define que irá ler o arquivo (local.yml). Para permitir a diferenciação no docker, fiz uma pequena modificação no Dockerfile da API. Na última linha, foi incluído o argumento (env=docker). Com essa alteração, quando o código da API estiver sendo executado dentro do docker, a variável (env) existirá e o arquivo carregado será o (docker.yml).
O código utilizado para conectar nossa API com o banco MySQL ocupa somente essas 12 linhas. Nesse código é basicamente construído uma string de conexão com o banco de dados e é realizado a conexão. Vamos entrar em mais detalhes sobre essa conexão no próximo episódio.
As alterações dentro do repository foram basicamente remover a utilização de nosso banco em memória e passar a utilizar o objeto do GORM (ORM de Go). Como esse episódio já está se estendendo bastante, vamos deixar para falar do GORM no próximo episódio.
Criando o Banco MySQL
Dentro de (productdb), criei uma pasta chamada (create-scripts) e adicionei o arquivo (create_db.sql), que será usado para criar o banco de dados. Nesse arquivo, está definido o código para criar a tabela que será utilizada, que é basicamente uma cópia da nossa entidade de domínio (product). Estabelecemos que a chave primária será o ID e também definimos que o conjunto de caracteres a ser utilizado será o UTF8, permitindo assim o trabalho com caracteres especiais sem problemas. Por fim, inserimos as mesmas entradas que tínhamos no banco de dados em memória.
Assim como definimos um Dockerfile para API, temos que definir um Dockerfile para nosso MySQL.
O Dockerfile nesse caso é simples. Definimos que vamos utilizar a imagem mysql:8.0 e copiamos o conteúdo da pasta (create-scripts) para a pasta (docker-entrypoint-initdb.d). Esse nome extenso não foi definido por mim, é basicamente um padrão da imagem docker do MySQL. Quando o container baseado nessa imagem é iniciado, todo o código definido nessa pasta é executado. Isso significa que, quando o nosso banco de dados for executado dentro do container, as tabelas e entradas previamente definidas já estarão prontas para uso.
Rodando o MySQL no Docker
Para construir a imagem docker do banco de dados, utilizamos o mesmo comando usado para construir a imagem da API, porém com uma tag diferente (product-db). Para executar a imagem, precisamos fornecer alguns parâmetros adicionais. Primeiro, é necessário definir o volume que o container utilizará. Em seguida, devemos especificar os seguintes parâmetros do banco de dados: MYSQL_ROOT_PASSWORD, MYSQL_DATABASE, MYSQL_USER e MYSQL_PASSWORD. O primeiro parâmetro é uma senha root que concede acesso global, e os outros parâmetros serão utilizados para estabelecer a conexão com o banco de dados a partir da nossa API.
Após executar o container de nosso banco de dados, podemos acessá-lo através do container_id utilizando o comando docker exec -it $containerID bash. Esse comando irá nos jogar dentro do container que está rodando o banco, e com o comando mysql -uroot -p, podemos acessar o banco de dados com a senha definida em MYSQL_ROOT_PASSWORD.
Dessa forma conseguimos rodar o banco MySQL dentro de um container docker e confirmar que tudo foi configurado do jeito correto. Porém, falta um último passo para automatizar o processo de inicialização do banco de dados e da nossa API. Para isso, vamos utilizar o docker-compose.
Rodando o MySQL e a API com docker-compose
Docker-compose é uma ferramenta que permite definir e executar aplicativos docker compostos por vários containers. Ele é usado para orquestrar o processo de criação, configuração e execução de aplicativos multi-containers.
Com o docker-compose, você pode descrever os serviços do seu aplicativo em um arquivo YAML, especificando as imagens docker, as variáveis de ambiente, as portas expostas e as dependências entre os containers. Em seguida, você pode usar o comando docker-compose up
para iniciar e executar todos os containers do aplicativo em conjunto.
Para conseguir orquestrar os procedimentos necessários para subir nossa API com o banco MySQL, escrevemos o seguinte docker-compose.yml.
No nível de serviços no arquivo YAML, definimos todas as aplicações que iremos utilizar, que neste caso são: product-db e product-api. Dentro de cada serviço, são definidos alguns atributos que são relevantes para o contexto de cada um. A seguir, segue uma explicação dos atributos definidos para o serviço (product-db):
- image: aqui estamos definindo que será utilizada a imagem (product-db) que foi construída anteriormente.
- container_name: aqui é definido o nome do container dentro da execução via docker-compose.
- expose: esse atributo é utilizado para expor uma porta de acesso para o banco de dados, que nesse caso será 3306 (porta padrão do MySQL).
- ports: define que será roteado a porta externa do container 3306, para a porta interna 3306.
- environment: aqui são redefinidos todos aquele atributos que utilizamos para rodar o container do MySQL manualmente.
- volumes: aqui é definido o volume utilizado pelo banco de dados para guardar os dados, nesse caso é realizado uma referencia ao volumes na raiz do arquivo.
- healthcheck: aqui, definimos que o retorno de um healthcheck será considerado bem-sucedido quando o banco de dados tiver concluído sua inicialização.
Ao analisarmos os atributos definidos no serviço (product-api), notamos que a maioria deles é semelhante ao serviço (product-db), com a única diferença sendo o atributo (depends_on). Esse atributo é utilizado para definir a sequência em que o docker-compose inicia as aplicações. O uso de (depends_on) garante que nossa API só será iniciada após o banco de dados confirmar que está em execução (com base no healthcheck). Com isso, garantimos que a API não encontrará problemas de conexão ao tentar se comunicar com o banco de dados.
Por fim, ao executar docker-compose up, temos a seguinte saída no terminal, indicando que tudo está executando dentro dos conformes.
Conclusões e Próximas Etapas
Neste artigo, foi apresentado uma maneira de integrar uma API escrita em Go com o banco de dados MySQL, utilizando o docker. Foram demonstradas as etapas para executar a API e o banco de dados MySQL manualmente, utilizando comandos do docker no terminal. E por fim, foi mostrado como automatizar esse processo utilizando o docker-compose. Além da integração, também foi adicionado um tratamento que permite executar a API tanto localmente quanto em um container.
Apesar de já termos adicionado o código que integra com o MySQL por meio da implementação do GORM, não entramos em detalhes sobre o funcionamento dessa integração. No próximo episódio, dedicaremos tempo para explorar o uso desse ORM e aprofundar nossos conhecimentos sobre ele.