# 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](https://2167929857-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FjISexK2k1gRWKUt6SseU%2Fuploads%2Fgit-blob-18e46f1089bad332743314e24a6a21eccf0a98ad%2Fmlflow1.png?alt=media\&token=9ec658b8-6a39-4fd8-9a71-c016f1a4f49d)

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](https://aurimrv.gitbook.io/pratica-devops-com-docker-para-machine-learning/id-5-infraestrutura-como-codigo-e-orquestracao/5-2-docker-compose). 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](https://2167929857-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FjISexK2k1gRWKUt6SseU%2Fuploads%2Fgit-blob-7f38fe6b5fdc6d6f67002a137d5c77d3108bdd6d%2Fmlflow2.png?alt=media)

## 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.
