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:
- Leitura do Estado Atual: Consultar o daemon do Docker para obter uma lista de todas as imagens locais e seus
IMAGE_IDs. - Identificação de Containeres Ativos: Determinar quais imagens estão sendo executadas por containeres em estado "running" ou "created". Essas imagens são protegidas.
- Filtragem de Imagens: Identificar imagens que não possuem nenhum container vinculado (dangling) ou que correspondem a critérios de antiguidade/etiquetas específicas.
- 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
subprocessejson, 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
dockerpara 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.