# 10.3 Configuração do MLflow

O MLflow precisa de um *back-end* para salvar os dados dos experimentos, tais como as métricas, parâmetros, artefatos e modelos. Na seção anterior, executamos os tutoriais básicos, que utilizam o sistema de arquivos como *back-end*, mas essa abordagem é muito limitada, pois exige que os comandos sejam executados sempre na mesma máquina. Para um único usuário interessado em controlar seus experimentos e modelos, pode servir, mas para uma equipe que deseja um mínimo de consistência e colaboração, é desejável a configuração de um local centralizado.

Existem dois tipos de *storage* que precisam ser configurados para o funcionamento completo do MLflow:

* Um serviço de *storage* simples, como Amazon S3 ou Google File storage, para armazenar artefatos e parâmetros/métrica;
* Um serviço de banco de dados, como MySQL ou PostgreSQL, para armazenar dados para registro de modelos.

Faremos aqui uma configuração completa usando Docker, que possibilita que o MLflow seja utilizado com todos seus recursos. O exemplo foi adaptado [deste repositório aqui](https://github.com/Toumash/mlflow-docker) e da [documentação oficial do MLFlow](https://mlflow.org/docs/latest/tracking/tutorials/remote-server.html).

## 10.3.1 Serviço de storage MinIO

O primeiro serviço que iremos configurar é o armazenamento de arquivos. O [MinIO](https://min.io/) é uma ferramenta open source compatível com o Amazon S3, portanto é uma excelente opção para demonstrar como esse tipo de armazenamento pode ser utilizado. O leitor pode optar por configurar sua própria instância do MinIO ou utilizar o Amazon S3, com as mesmas configurações do lado do MLflow.

Para configurar o MinIO, utilizaremos uma imagem Docker pronta. Faremos também uso do `docker compose` como facilitador na configuração do ambiente.

Crie uma pasta em um local qualquer, chamada `mlflow-server`. Crie um arquivo chamado `compose.yml`:

```yaml
services:
  minio:
    image: minio/minio
    restart: unless-stopped
    ports:
      - "9000:9000"
      - "9001:9001"
    expose:
      - 53
    environment:
      - MINIO_ROOT_USER=${AWS_ACCESS_KEY_ID}
      - MINIO_ROOT_PASSWORD=${AWS_SECRET_ACCESS_KEY}
    healthcheck:
      test: timeout 5s bash -c ':> /dev/tcp/127.0.0.1/9000' || exit 1
      interval: 1s
      timeout: 10s
      retries: 5
    command: server /data --console-address ":9001"
    networks:
      - internal
      - public
    volumes:
      - minio_volume:/data
networks:
  internal:
  public:
    driver: bridge
volumes:
  minio_volume:
```

Esse arquivo faz a configuração de um contêiner com base na imagem oficial do MinIO. Há duas portas sendo abertas, 9000 (para a interface principal) e 9001 (para os comandos internos). A porta 53 está sendo exposta para uso interno, pois será necessária para um serviço que criaremos a seguir. A configuração do usuário e senha também é feita nesse arquivo, com base em duas variáveis de ambiente que serão definidas a seguir. O comando que executa o servidor é o seguinte:

`server /data --console-address ":9001"`

* `/data` é a pasta onde serão salvos os arquivos e metadados
* `console-address` define a porta para onde os comandos devem ser enviados

O arquivo do `docker compose` também define duas redes onde esse serviço estará disponível. A rede `internal` será compartilhada com outros serviços que definiremos mais adiante, e a rede `public` é, como o nome sugere, a rede por onde acessaremos o MinIO a partir da máquina local e de outras máquinas, caso quisermos.

Outra configuração importante desse arquivo é o trecho `healthcheck`. Esse trecho define um script responsável verificar a saúde do serviço, ou seja, se ele está disponível a ponto de poder ser utilizado por outros serviços. No caso, verifica se a porta 9000 está disponível por meio do protocolo TCP, o que indica que o serviço está rodando. Isso será necessário mais adiante. Mais informações sobre essa configuração do Docker podem ser encontradas na [documentação oficial do Docker](https://docs.docker.com/reference/compose-file/services/#healthcheck).

Por último, o arquivo define que existirá um volume, chamado `minio_volume` que ficará mapeado para a pasta `/data` do contêiner (a mesma indicada no comando que executa o servidor).

Para poder rodar esse serviço, é necessário definir as configurações de usuário e senha. Faremos isso em um arquivo chamado `.env`:

```
AWS_ACCESS_KEY_ID=admin
AWS_SECRET_ACCESS_KEY=senhasenha
```

Fique à vontade para trocar os valores, caso desejar.

Agora já podemos rodar. Execute:

```sh
docker compose up
```

Assim que o contêiner for iniciado, podemos testar, abrindo o navegador no endereço `http://localhost:9000`.

![Interface principal do MinIO](/files/P01MIyQLBJysAYTK1H2I)

Experimente a interface, crie novos *buckets*, faça o envio de arquivos para ver como funciona.

O MLflow irá armazenar seus artefatos em um *bucket*. Um *bucket* é uma unidade de armazenamento no Amazon S3, [confira a documentação para saber mais](https://docs.aws.amazon.com/pt_br/AmazonS3/latest/userguide/creating-buckets-s3.html). Podemos criá-lo manualmente, pela interface, e depois informar ao MLflow o nome do *bucket*. Mas podemos também automatizar essa etapa. Assim, seguindo a filosofia de infraestrutura como código, podemos deixar tudo programado de modo a gerar menos erros.

Vamos fazer uso do [CLI do MinIO](https://docs.min.io/minio/baremetal/reference/minio-mc.html), uma ferramenta em linha de comando que permite a execução de diversos comandos. Por exemplo, para criar um novo *bucket* chamado "umbucketqualquer" podemos executar os comandos (é possível rodar diretamente de dentro do container, ou instalar a CLI em algum local):

```sh
.\mc alias set minio http://localhost:9000 admin senhasenha
.\mc mb minio/umbucketqualquer
.\mc rb minio/umbucketqualquer  
```

O primeiro comando configura a ferramenta CLI para acessar o servidor local, e o segundo solicita a criação de um novo *bucket*. O terceiro o remove.

Portanto, vamos configurar um serviço no arquivo `compose` para automaticamente criar um *bucket*, caso não exista, assim que o serviço for executado. Modifique o arquivo `compose.yml`:

```diff
services:
  minio:
    image: minio/minio
    restart: unless-stopped
    ports:
      - "9000:9000"
      - "9001:9001"
    expose:
      - 53      
    environment:
      - MINIO_ROOT_USER=${AWS_ACCESS_KEY_ID}
      - MINIO_ROOT_PASSWORD=${AWS_SECRET_ACCESS_KEY}
    healthcheck:
      test: timeout 5s bash -c ':> /dev/tcp/127.0.0.1/9000' || exit 1
      interval: 1s
      timeout: 10s
      retries: 5
    command: server /data --console-address ":9001"
    networks:
      - internal
      - public
    volumes:
      - minio_volume:/data
+  create_s3_buckets:
+    image: minio/mc
+    depends_on:
+      minio:
+        condition: service_healthy
+    entrypoint: >
+      bash -c "
+      mc alias set minio http://minio:9000 '${AWS_ACCESS_KEY_ID}' '${AWS_SECRET_ACCESS_KEY}' &&
+      mc mb minio/${AWS_BUCKET_NAME}
+      "
+    networks:
+      - internal
networks:
  internal:
  public:
    driver: bridge
volumes:
  minio_volume:
```

O novo serviço utiliza uma imagem que tem apenas o cliente do MinIO instalado, pronto para usar. Então podemos apenas executar o comando que quisermos. Mas não podemos executar nada antes de termos certeza que o serviço do MinIO terminou de rodar. No arquivo, existe uma dependência deste novo serviço para o serviço do MinIO (`depends_on`), mas isso não garante que ele execute apenas após o outro estar pronto, como já discutimos antes, na [Seção 5.2](/pratica-devops-com-docker-para-machine-learning/id-5-infraestrutura-como-codigo-e-orquestracao/5-2-docker-compose.md). Por isso, o comando definido nesse serviço acrescenta uma condição `service_healthy`, que espera até que o outro container esteja passando por sua própria condição de `healthcheck`, como configurado acima. Apenas quando essa tentativa é bem sucedida que o comando segue para a criação do *bucket*. O nome do *bucket* deve ser definido no arquivo `.env`:

```diff
AWS_ACCESS_KEY_ID=admin
AWS_SECRET_ACCESS_KEY=senhasenha
+AWS_BUCKET_NAME=mlflow
```

É importante entender que o comando primeiro verifica se o bucket já existe. Se ele já existir, não será criado. Como estamos fazendo uso de um volume, os dados ficarão persistentes mesmo entre as execuções. Para testar, interrompa e execute o `docker compose up` novamente. Após a execução, acesse a interface novamente e veja como o *bucket* foi criado.

![Bucket criado com sucesso](/files/majLLe1fdyUQ0OCGciu9)

## 10.3.2 Serviço de banco de dados MySQL

O próximo serviço a ser executado é o banco de dados. Faremos uso do MySQL, que também já tem uma imagem Docker pronta para ser utilizada. Vamos modificar o arquivo `compose.yml`:

```diff
services:
  minio:
    image: minio/minio
    restart: unless-stopped
    ports:
      - "9000:9000"
      - "9001:9001"
    expose:
      - 53
    environment:
      - MINIO_ROOT_USER=${AWS_ACCESS_KEY_ID}
      - MINIO_ROOT_PASSWORD=${AWS_SECRET_ACCESS_KEY}
    healthcheck:
      test: timeout 5s bash -c ':> /dev/tcp/127.0.0.1/9000' || exit 1
      interval: 1s
      timeout: 10s
      retries: 5
    command: server /data --console-address ":9001"
    networks:
      - internal
      - public
    volumes:
      - minio_volume:/data
  create_s3_buckets:
    image: minio/mc
    depends_on:
      minio:
        condition: service_healthy
    entrypoint: >
      bash -c "
      mc alias set minio http://minio:9000 '${AWS_ACCESS_KEY_ID}' '${AWS_SECRET_ACCESS_KEY}' &&
      mc mb minio/${AWS_BUCKET_NAME}
      "
    networks:
      - internal
+  db:
+    image: mysql
+    restart: unless-stopped
+    container_name: mlflow_db
+    expose:
+      - "3306"
+    environment:
+      - MYSQL_DATABASE=${MYSQL_DATABASE}
+      - MYSQL_USER=${MYSQL_USER}
+      - MYSQL_PASSWORD=${MYSQL_PASSWORD}
+      - MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD}
+    volumes:
+      - db_volume:/var/lib/mysql
+    networks:
+      - internal
+    healthcheck:
+      test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "{MYSQL_USER}", "-p{MYSQL_PASSWORD}"]
+      interval: 30s
+      timeout: 10s
+      retries: 5
networks:
  internal:
  public:
    driver: bridge
volumes:
+  db_volume:
  minio_volume:
```

Não há muita novidade aqui. Estamos expondo a porta 3306, que é a porta padrão do MySQL, configurando dados do banco de dados, como base de dados, usuário e senha, um volume para armazenamento persistente e uso da rede `internal` (somente o mlflow precisará acessar esse serviço). Também definimos uma condição de `healthcheck` que usa o comando `mysqladmin` para testar se o serviço já está respondendo.

Modifique também o arquivo `.env` para adicionar as informações adicionadas:

```diff
AWS_ACCESS_KEY_ID=admin
AWS_SECRET_ACCESS_KEY=senhasenha
AWS_BUCKET_NAME=mlflow
+MYSQL_DATABASE=mlflow
+MYSQL_USER=mlflow_user
+MYSQL_PASSWORD=mlflow_password
+MYSQL_ROOT_PASSWORD=senhasenha
```

Já podemos subir os serviços e ver o resultado. Na verdade não há muito o que ver, exceto as mensagens no terminal informando que o MySQL está de fato rodando.

## 10.3.3 Serviço do MLflow

O terceiro e último serviço que iremos rodar é o MLflow. Também não é difícil configurá-lo no `docker compose`, sendo basicamente configurações e endereços dos demais serviços para a orquestração. Porém, não existe uma imagem oficial do mlflow para simplesmente utilizarmos no Docker compose. No entanto, sua instalação para efeitos de demonstração é trivial, de modo que faremos isso usando um arquivo `Dockerfile`, com o seguinte conteúdo:

```
FROM python

RUN pip install mlflow boto3 pymysql cryptography

ADD . /app
WORKDIR /app
```

Essa imagem é baseada em uma imagem padrão `python`, e terá instalado o MLflow e demais dependências necessárias para conexão aos demais serviços.

Agora vamos modificar o `compose.yml`:

```diff
services:
  minio:
    image: minio/minio
    restart: unless-stopped
    ports:
      - "9000:9000"
      - "9001:9001"
    expose:
      - 53
    environment:
      - MINIO_ROOT_USER=${AWS_ACCESS_KEY_ID}
      - MINIO_ROOT_PASSWORD=${AWS_SECRET_ACCESS_KEY}
    healthcheck:
      test: timeout 5s bash -c ':> /dev/tcp/127.0.0.1/9000' || exit 1
      interval: 1s
      timeout: 10s
      retries: 5
    command: server /data --console-address ":9001"
    networks:
      - internal
      - public
    volumes:
      - minio_volume:/data
  create_s3_buckets:
    image: minio/mc
    depends_on:
      minio:
        condition: service_healthy
    entrypoint: >
      bash -c "
      mc alias set minio http://minio:9000 '${AWS_ACCESS_KEY_ID}' '${AWS_SECRET_ACCESS_KEY}' &&
      mc mb minio/${AWS_BUCKET_NAME}
      "
    networks:
      - internal
  db:
    image: mysql
    restart: unless-stopped
    container_name: mlflow_db
    expose:
      - "3306"
    environment:
      - MYSQL_DATABASE=${MYSQL_DATABASE}
      - MYSQL_USER=${MYSQL_USER}
      - MYSQL_PASSWORD=${MYSQL_PASSWORD}
      - MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD}
    volumes:
      - db_volume:/var/lib/mysql
    networks:
      - internal
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "{MYSQL_USER}", "-p{MYSQL_PASSWORD}"]
      interval: 30s
      timeout: 10s
      retries: 5
+  mlflow:
+    container_name: mlflow-server-container
+    image: mlflow-server
+    restart: unless-stopped
+    depends_on:
+      db:
+        condition: service_healthy
+    build:
+      context: .
+      dockerfile: Dockerfile
+    ports:
+      - "5000:5000"
+    environment:
+      - AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID}
+      - AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY}
+      - AWS_DEFAULT_REGION=${AWS_REGION}
+      - MLFLOW_S3_ENDPOINT_URL=http://minio:9000
+    networks:
+      - public
+      - internal
+    entrypoint: mlflow server --backend-store-uri mysql+pymysql://${MYSQL_USER}:${MYSQL_PASSWORD}@db:3306/${MYSQL_DATABASE} --artifacts-destination s3://${AWS_BUCKET_NAME}/ --serve-artifacts -h 0.0.0.0
networks:
  internal:
  public:
    driver: bridge
volumes:
  db_volume:
  minio_volume:
```

Ainda falta configurar uma última coisa no `.env`:

```diff
AWS_ACCESS_KEY_ID=admin
AWS_SECRET_ACCESS_KEY=senhasenha
AWS_BUCKET_NAME=mlflow
+AWS_REGION=us-east-1
MYSQL_DATABASE=mlflow
MYSQL_USER=mlflow_user
MYSQL_PASSWORD=mlflow_password
MYSQL_ROOT_PASSWORD=senhasenha
```

Esta variável define em qual região da AWS o *bucket* está armazenado. Como o MinIO roda localmente, este valor não é nenhum valor real, mas é necessário para que a configuração fique completa para quando isso for migrado para a AWS realmente.

O principal a ser analisado aqui é o *entrypoint*:

`mlflow server --backend-store-uri mysql+pymysql://${MYSQL_USER}:${MYSQL_PASSWORD}@db:3306/${MYSQL_DATABASE} --artifacts-destination s3://${AWS_BUCKET_NAME}/ --serve-artifacts -h 0.0.0.0`

* `mlflow server`: comando que inicia o MLflow
* `--backend-store-uri XXX`: indica ao MLflow onde armazenar os metadados referentes aos modelos (no caso, o MySQL)
* `--artifacts-destination s3://${AWS_BUCKET_NAME}/ --serve-artifacts`: indica ao MLflow onde armazenar os artefatos (no caso, o MinIO, ou poderia ser Amazon S3). O mlflow server estará funcionando como um proxy para armazenar e recuperar os artefatos do local especificado
* `-h 0.0.0.0`: diz ao MLflow para aceitar requisições de fora da máquina local

Pronto, basta executar o comando `docker compose up` e teremos tudo funcionando. Estamos com uma instância do MLflow completamente configurada e acessível no endereço `http://localhost:5000`. Na próxima seção mostraremos como utilizar essa instância para aproveitar das funcionalidades dos componentes do MLflow de forma centralizada.


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://aurimrv.gitbook.io/pratica-devops-com-docker-para-machine-learning/id-10-mlops-com-mlflow/10-3-configuracao-do-mlflow.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
