Script Python para Limpar Imagens Docker Antigas Automaticamente

12 min de leitura Automação
Script Python para Limpar Imagens Docker Antigas Automaticamente

O Desafio do Armazenamento em Ambientes de Containerização

A adoção massiva de docker e orquestradores como Kubernetes transformou a maneira como o software é desenvolvido, testado e implantado. No entanto, essa agilidade traz consigo um custo oculto significativo: o consumo descontrolado de espaço em disco. Por padrão, o daemon do Docker mantém todas as camadas de imagem, volumes não vinculados e dados de containeres parados no disco local para facilitar a recuperação rápida e o versionamento.

Em ambientes de produção ou de desenvolvimento intensivo (CI/CD), onde imagens são construídas e destruídas dezenas de vezes ao dia, esse acúmulo pode saturar rapidamente o /var/lib/docker. Isso resulta em falhas na construção de novas imagens, erros de escrita no sistema de arquivos e, consequentemente, interrupções nos pipelines de entrega contínua. A solução nativa do Docker, o comando docker system prune, é útil, mas muitas vezes agressiva ou insuficiente para cenários que exigem retenção estratégica de versões específicas.

Neste tutorial, vamos criar um script em Python robusto e automatizado para gerenciar essas imagens antigas. Diferente de scripts bash complexos e frágeis, uma solução em Python oferece melhor legibilidade, tratamento de erros mais claro e facilidade de integração com outras ferramentas de DevOps. O objetivo é liberar espaço em disco removendo imagens que não estão sendo usadas por containeres ativos e que estejam obsoletas, mantendo o ambiente limpo sem interromper serviços críticos.

Planejamento da Lógica do Script

Antes de escrever uma única linha de código, é fundamental definir as regras de limpeza. Um script de automação não pode ser "cego"; ele deve seguir políticas claras para evitar a remoção acidental de imagens que ainda estão em uso ou que são necessárias para rollbacks.

A lógica central do nosso script seguirá os seguintes passos:

  1. Leitura do Estado Atual: Consultar o daemon do Docker para obter uma lista de todas as imagens locais e seus IMAGE_IDs.
  2. Identificação de Containeres Ativos: Determinar quais imagens estão sendo executadas por containeres em estado "running" ou "created". Essas imagens são protegidas.
  3. Filtragem de Imagens: Identificar imagens que não possuem nenhum container vinculado (dangling) ou que correspondem a critérios de antiguidade/etiquetas específicas.
  4. Ação de Limpeza: Remover as imagens selecionadas e, opcionalmente, os dados residuais (volumes e cache).

Essa abordagem garante que a automação atue apenas no que é seguro remover, preservando a integridade do ambiente de gerenciamento de containeres.

Pré-requisitos e Ambiente

Para executar este tutorial, você precisará dos seguintes componentes instalados na sua máquina ou servidor:

  • Sistema Operacional: Linux (Ubuntu, CentOS, Debian) ou macOS. O script pode rodar no Windows com o Docker Desktop, mas os caminhos de integração podem variar.
  • Docker Engine: Versão 18.09 ou superior recomendada para melhor suporte à API.
  • Python 3: Versão 3.6 ou superior. Utilizaremos a biblioteca padrão subprocess e json, evitando dependências externas pesadas para facilitar o deploy em ambientes minimalistas.
  • Permissões: O usuário que rodar o script precisa ter permissão de sudo ou fazer parte do grupo docker para acessar a API do Docker sem erros de permissão.

Desenvolvimento do Script Python

Vamos estruturar o código em uma função principal que orquestra a limpeza. O script utilizará o comando docker images com saída JSON para parsear os dados de forma segura, evitando problemas de parsing de texto que ocorrem ao usar saídas formatadas em tabela.

Crie um arquivo chamado clean_docker_images.py e abra-o em seu editor de código preferido.

A primeira etapa é importar as bibliotecas necessárias:

import subprocess
import json
import sys
import argparse
import datetime

Agora, definiremos uma função auxiliar para executar comandos do Docker e capturar sua saída JSON. Isso abstrai a complexidade de lidar com erros de subprocesso.

def run_docker_command(args):
    """Executa um comando docker e retorna o resultado em JSON."""
    try:
        # Constrói o comando completo
        cmd = ['docker'] + args
        
        # Executa e captura a saída
        result = subprocess.run(
            cmd, 
            stdout=subprocess.PIPE, 
            stderr=subprocess.PIPE, 
            check=True, 
            text=True
        )
        
        # Tenta parsear o JSON retornado
        return json.loads(result.stdout)
    except subprocess.CalledProcessError as e:
        print(f"Erro ao executar comando docker: {e.stderr}")
        sys.exit(1)
    except json.JSONDecodeError:
        # Se a saída não for JSON (ex: docker images sem --format), tenta converter manualmente ou retorna lista vazia
        return []

A função central, get_dangling_images, será responsável por identificar as imagens que podem ser removidas. Utilizaremos o filtro :none do Docker para encontrar imagens "fantasmas" (sem tag) e imagens que não estão associadas a nenhum container.

def get_dangling_images():
    """Retorna uma lista de IDs de imagens pendentes (dangling)."""
    # O filtro ':none' captura imagens sem tag, que geralmente são camadas intermediárias
    images = run_docker_command(['images', '--filter', 'dangling=true', '-q'])
    
    # A flag -q retorna apenas os IDs. Se o retorno for string, convertemos para lista
    if isinstance(images, str):
        return [img.strip() for img in images.split('\n') if img.strip()]
    
    return images

No entanto, a limpeza de imagens "dangling" nem sempre é suficiente. Muitas vezes, queremos limpar imagens antigas que têm tag, mas não estão em uso. Para isso, precisamos cruzar dados com os containeres parados.

def get_stopped_container_images():
    """Retorna uma lista de IDs de imagens usadas por containeres parados."""
    # Obtém todos os containeres (incluindo parados) e suas imagens
    containers = run_docker_command(['ps', '-a', '--format', '{{.Image}}'])
    
    if isinstance(containers, str):
        return [img.strip() for img in containers.split('\n') if img.strip()]
    
    return []

Agora, implementamos a lógica de decisão. O script deve comparar as imagens locais com as que estão em uso.

def clean_old_images(keep_last_n=0):
    """
    Remove imagens antigas.
    keep_last_n: Se > 0, mantém as N últimas versões de cada repositório.
    """
    
    # 1. Listar todas as imagens locais
    all_images = run_docker_command(['images', '--format', '{{.ID}} {{.Repository}} {{.Tag}}'])
    
    # Parse manual da saída formatada para extrair ID, Repo e Tag
    image_list = []
    for line in all_images.split('\n'):
        if not line.strip():
            continue
        parts = line.split()
        if len(parts) >= 2:
            img_id = parts[0]
            repo = parts[1]
            tag = parts[2] if len(parts) > 2 else "latest"
            image_list.append({'id': img_id, 'repo': repo, 'tag': tag})

    # 2. Identificar imagens em uso por containeres ativos
    running_images = run_docker_command(['ps', '--format', '{{.Image}}'])
    if isinstance(running_images, str):
        running_list = [img.strip() for img in running_images.split('\n') if img.strip()]
    else:
        running_list = []

    # 3. Filtrar imagens para remoção
    images_to_remove = []
    
    for img in image_list:
        # Se a imagem está em uso por um container ativo, PULA
        if img['repo'] + ':' + img['tag'] in running_list or img['id'] in [c.split(':')[1] for c in running_list]:
            continue
            
        # Lógica de retenção baseada na contagem (opcional)
        # Se keep_last_n for 0, removemos todas as não ativas (exceto dangling que já tratamos antes se desejado)
        
        images_to_remove.append(img['id'])

    return images_to_remove

Finalmente, a função de execução que itera sobre os IDs coletados e executa o docker rmi.

def remove_images(image_ids):
    """Remove as imagens listadas."""
    if not image_ids:
        print("Nenhuma imagem para remover.")
        return

    print(f"Iniciando remoção de {len(image_ids)} imagens...")
    
    # Executa a remoção em lote ou individualmente
    # O Docker permite passar múltiplos IDs, mas é mais seguro tratar erros um por um ou em grupos pequenos
    for img_id in image_ids:
        try:
            print(f"Removendo imagem {img_id}...")
            run_docker_command(['rmi', img_id])
        except Exception as e:
            # Pode falhar se outra tarefa estiver usando a imagem ou se houver dependências
            print(f"Aviso: Falha ao remover {img_id}: {e}")

    print("Limpeza concluída.")

O bloco if __name__ == "__main__": permitirá que o script seja executado via linha de comando, aceitando argumentos para personalização.

if __name__ == "__main__":
    parser = argparse.ArgumentParser(description='Script para limpar imagens Docker antigas.')
    parser.add_argument('--dry-run', action='store_true', help='Simula a remoção sem excluir nada.')
    parser.add_argument('--verbose', action='store_true', help='Mostra detalhes do processo.')
    
    args = parser.parse_args()
    
    # Execução principal
    dangling_ids = get_dangling_images()
    old_image_ids = clean_old_images()
    
    # Combina listas (evitando duplicatas)
    all_to_remove = list(set(dangling_ids + old_image_ids))
    
    if args.dry_run:
        print("Modo Simulação (--dry-run):")
        for id in all_to_remove:
            print(f"  [SERIA REMOVIDO] {id}")
    else:
        remove_images(all_to_remove)

Como Utilizar o Script na Prática

Com o script salvo e as permissões de execução definidas (chmod +x clean_docker_images.py), você pode integrá-lo ao seu fluxo de trabalho de várias maneiras.

1. Execução Manual com Simulação

Sempre que for aplicar mudanças na lógica de limpeza, comece com o modo --dry-run. Isso permite visualizar quais IDs serão afetados antes de comprometer o disco.

./clean_docker_images.py --dry-run

2. Integração com Crontab (Linux/macOS)

A verdadeira força da automação reside na execução recorrente. Edite o crontab do seu usuário:

cron -e

Adicione a seguinte linha para rodar a limpeza diariamente às 3 da manhã, quando o tráfego de containeres tende a ser menor:

0 3 * * * /usr/bin/python3 /opt/scripts/clean_docker_images.py >> /var/log/docker_cleanup.log 2>&1

Isso garante que o espaço em disco seja mantido sob controle automaticamente, sem intervenção manual do administrador.

3. Uso em Pipelines CI/CD (GitHub Actions, GitLab CI)

Em pipelines de integração contínua, o acúmulo de imagens é comum. Você pode adicionar uma etapa de limpeza no final do job ou antes de um build pesado:

jobs:
  cleanup:
    runs-on: ubuntu-latest
    steps:
      - name: Clean Docker Images
        run: |
          python3 clean_docker_images.py --verbose

Melhores Práticas e Considerações de Segurança

Ao implementar scripts de gerenciamento de containeres, é crucial observar alguns pontos de atenção para evitar incidentes operacionais.

1. Volumes e Dados: O script acima foca em imagens. Lembre-se que volumes órfãos (dados de banco de dados, logs, arquivos de configuração) também consomem espaço. Para limpar volumes, o comando docker volume prune é recomendado, mas tome cuidado com dados não versionados.

2. Imagens Base: Em ambientes multi-projeto, várias equipes podem estar usando a mesma imagem base (ex: python:3.9-slim). Se você remover essa imagem porque "está antiga", mas ela é usada por um container em produção de outra equipe, o sistema quebrará. O script atual protege imagens ativas, mas verifique sempre se há containeres no estado "created" (parados mas não removidos) que ainda referenciam aquela imagem.

3. Monitoramento: Após a implementação do script, monitore o uso de disco em /var/lib/docker. Ferramentas como Prometheus e Grafana podem alertar se o crescimento do diretório exceder limites críticos antes que a limpeza automática seja executada.

4. Logs de Auditoria: O script imprime no stdout o que está sendo removido. Certifique-se de redirecionar essas saídas para arquivos de log rotativos (usando logrotate) para manter um histórico de auditoria de quais imagens foram excluídas e quando.

Conclusão

A manutenção proativa do ambiente Docker é essencial para a saúde da infraestrutura de TI. Scripts manuais ou comandos repetitivos consomem tempo valioso dos engenheiros e estão sujeitos a erro humano. Ao utilizar Python para criar uma lógica de limpeza estruturada, como demonstrado neste tutorial, você ganha controle fino sobre o que é removido, aumenta a confiabilidade do ambiente e otimiza o uso de recursos de armazenamento.

A combinação de docker, automação via scripts e práticas de DevOps permite que equipes escalarem suas operações de containerização sem que o custo operacional (neste caso, espaço em disco) se torne um gargalo. Implemente este script, ajuste-o às políticas de retenção da sua organização e mantenha seus servidores limpos e eficientes.

Compartilhar: Link copiado!
Esse tutorial foi útil?

Comentários (0)

Seja o primeiro a comentar.

Deixe seu comentário

Seu comentário será analisado antes de ser publicado.

0/2000