Reduzindo os custos do Cloud Composer na GCP

Utilizando pipelines de CI/CD para destruir e recriar ambientes de maneira segura

Alvaro Leandro Cavalcante Carneiro
Data Hackers
10 min readMay 23, 2024

--

Photo by Jp Valery on Unsplash

O Cloud Composer é um serviço gerenciado da Google Cloud Platform (GCP) que permite a fácil instalação, configuração e escalabilidade de um cluster de orquestração de fluxos de trabalho baseado no Apache Airflow.

Dado a popularidade do Airflow entre diversas empresas como ferramenta de orquestração de tarefas, utilizar um serviço que permita a criação e manutenção de todo o ambiente de maneira simples e com apenas alguns cliques é bastante atrativo. Ainda assim, é preciso se atentar aos custos que um ambiente como esse podem agregar à sua conta da GCP, visto que os mesmo podem crescer rapidamente.

Dito isso, este artigo tem como objetivo demonstrar o passo a passo de como é possível utilizar snapshots e pipelines de CI/CD para “desligar” o ambiente do Composer e reduzir custos desnecessários.

Este artigo foi inspirado no excelente trabalho do Marc Djohossou.

Versões do Cloud Composer

A GCP disponibiliza o Composer em duas versões distintas, o Cloud Composer 1 e o Cloud Composer 2. A principal diferença entre as duas versões é o tipo de cluster Kubernates utilizado para gerenciar as máquinas (pods) que compõe o serviço. De maneira simplificada, o Composer 2 utiliza o conceito de Autopilot (piloto automático) permitindo que a gestão de recursos seja mais simples e otimizada através de autoscaling.

No Composer 1, por outro lado, essa opção não está disponível, sendo necessário criar novos pods manualmente caso sua carga de trabalho fique mais intensa, tornando a gestão da infraestrutura mais complexa. Outra importante desvantagem da versão 1 do serviço é que a mesma está perdendo o suporte oficial do Google.

Segundo a própria documentação oficial, a partir do dia 25 de setembro de 2024 a versão mais recente do Cloud Composer 1 não receberá mais atualizações, incluindo patches de segurança que são essenciais para evitar que o ambiente fique comprometido.

Com isso, é bastante óbvio que o Cloud Composer 2 é a versão adequada para se criar ou manter um ambiente atualmente. Porém, a versão mais recente do serviço possui um detalhe bastante indesejado e que acaba surpreendendo diversos usuários: ele não pode ser desligado.

Essa limitação se dá justamente por conta do cluster Autopilot que foi mencionado anteriormente. Diferentemente da versão 1 em que é possível controlar o pool de pods de maneira bastante granular e manual, o cluster com piloto automático não pode ser reduzido abaixo de sua capacidade mínima, a qual deve ser sempre de pelo menos uma máquina. Em outras palavras, seu ambiente não pode ser totalmente desligado, ficando sempre com um número mínimo de pods ativos gerando custo 24 horas por dia.

Isto não deve ser um problema caso você esteja utilizando o Cloud Composer 2 em um ambiente de produção que precisa orquestrar tarefas 24 horas por dia. Porém, caso você possua um ambiente de desenvolvimento que é utilizado apenas em horário comercial, ou ainda caso o próprio ambiente de produção fique ativo apenas por algumas horas por dia, ser obrigado a pagar por máquinas em tempo integral pode ser bastante desencorajador e criar custos desnecessários.

Baseado nisso, embora você possa reaproveitar as ideias desse tutorial para o Composer 1, o principal objetivo será mostrar uma forma de destruir e recriar automaticamente ambientes baseados no Composer 2, garantindo que você irá pagar apenas pelo tempo que utilizar o serviço!

Snapshots para preservação de estado

A estratégia mais comum no que diz respeito à redução de custos de infraestrutura é o corte de recursos, diminuindo o tamanho do ambiente, reduzindo a quantidade de máquinas e escolhendo instâncias mais simples. Ainda que isso possa ser simples e efetivo, não é a solução definitiva para o Composer 2, visto que seu ambiente não pode ser totalmente desligado, sendo necessário manter ao menos um Worker, um Scheduler e uma instância de Cloud SQL.

Por conta disso, a única forma de realmente evitar o desperdício de recursos é destruindo o ambiente por completo, fazendo com que nenhum custo adicional seja gerado pelo serviço. Ainda assim, essa solução não é tão simples quanto parece, visto que destruir o ambiente faz com que todas as configurações, estados e logs sejam perdidos.

Dessa forma, é essencial utilizar a funcionalidade de snapshots. Um snapshot é basicamente uma imagem estática ou “fotografia” do ambiente do Composer, permitindo que as configurações possam ser salvas e utilizadas para restaurar o ambiente ao estado em que o snapshot foi criado.

Deste modo, através do uso de snapshots, é possível simular a funcionalidade de ligar e desligar ambientes, visto que podemos destruí-los e recriá-los sem perder o seu estado.

É importante lembrar, entretanto, que os snapshots não salvam os logs das tasks que foram executadas nas Dags do Airflow, sendo necessário uma etapa adicional para garantir o seu salvamento.

Assim, a pipeline de CI/CD será dividida em duas, sendo uma responsável pela criação do ambiente e carregamento do snapshot e outra pelo salvamento do snapshot para o próximo dia e destruição do ambiente.

Criando ambientes do Composer através de Snapshots

A primeira etapa que iremos desenvolver será a criação de um ambiente do Cloud Composer de maneira automatizada, bem como o carregamento de um snapshot e dos tasks logs (que não são salvos automaticamente junto com os snapshots).

Para isso, vamos precisar criar um novo bucket no Cloud Storage que será responsável por armazenar os arquivos de backup. Isso pode ser feito através do comando abaixo:

gsutil mb gs://<PROJECT_ID>-nome-do-seu-bucket

Para executar o comando acima é preciso instalar e configurar o Google Cloud SDK.

Feito isso, é preciso criar uma conta de serviço para ser utilizada no ambiente do Composer. A conta de serviço possui todas as roles necessárias para permissionar as ações que serão executadas no Airflow. Caso você já tenha um ambiente do Composer (ainda que seja a versão 1) basta utilizar a mesma conta nesta etapa. É possível verificar a conta de serviço que está sendo utilizada através do seguinte comando:

gcloud composer environments describe composer-env-name --location=us-east1

Criar uma nova conta, por sua vez, pode ser feito utilizando o comando abaixo:

# Habilitando o serviço do Composer
gcloud services enable composer.googleapis.com

# Criando a conta de serviço do ambiente, chamada "srv-composer"
gcloud iam service-accounts create srv-composer

# Adicione a role "Composer Worker" à conta de serviço
gcloud projects add-iam-policy-binding <PROJECT_ID> \
--member serviceAccount:srv-composer@<PROJECT_ID>.iam.gserviceaccount.com \
--role roles/composer.worker

O comando em questão é apenas um exemplo, visto que você deverá adicionar as demais roles que serão necessárias no seu ambiente. Lembre-se de que a boa prática é adicionar roles de maneira granular, aumentando os privilégios da conta de acordo com o necessário.

Por fim, vamos criar uma Trigger utilizando o serviço de CI/CD da GCP, chamado Cloud Build. Para isso, basta acessar o menu de “Triggers” do Cloud Build no console da GCP:

Acessando o menu de Triggers do Cloud Build. Fonte: Autor

Depois, basta clicar no botão “Create Trigger” e inserir as informações conforme mostrado abaixo:

Criando uma nova trigger do Cloud Build. Fonte: Autor

Por fim, clique no botão “OPEN EDITOR”, mostrado acima, e insira o seguinte yaml:

steps:

- name: gcr.io/cloud-builders/gcloud
entrypoint: /bin/bash
id: 'Create environment'
args:
- -c
- |
set -e
# Este é um project_id e env_name de exemplo. Use o seu próprio
project_id=my-gcp-project
env_name=my-basic-environment
gcloud composer environments create ${env_name} \
--location europe-west1 \
--project ${project_id} \
--image-version=composer-2.1.10-airflow-2.4.3 \
--service-account srv-composer@${project_id}.iam.gserviceaccount.com \
--storage-bucket europe-west1-${env_name}-19a29c3d-bucket

- name: gcr.io/cloud-builders/gcloud
entrypoint: /bin/bash
id: 'Load Snapshot'
args:
- -c
- |
set -e
# Este é um project_id e env_name de exemplo. Use o seu próprio
project_id=my-gcp-project
env_name=my-basic-environment
if gsutil ls gs://${project_id}-europe-west1-backup/snapshots/* ; then
snap_folder=$(gsutil ls gs://${project_id}-europe-west1-backup/snapshots)
gcloud composer environments snapshots load ${env_name} --project ${project_id} \
--location europe-west1 \
--snapshot-path ${snap_folder}
else
echo "There is no snapshot to load"
fi

- name: gcr.io/cloud-builders/gcloud
entrypoint: /bin/bash
id: 'Restore Tasks Logs'
args:
- -c
- |
set -e
# Este é um project_id e env_name de exemplo. Use o seu próprio
project_id=my-gcp-project
env_name=my-basic-environment
if gsutil ls gs://${project_id}-europe-west1-backup/tasks-logs/* ; then
dags_folder=$(gcloud composer environments describe ${env_name} --project ${project_id} \
--location europe-west1 --format="get(config.dagGcsPrefix)")
logs_folder=$(echo $dags_folder | cut -d / -f-3)/logs
gsutil -m cp -r gs://${project_id}-europe-west1-backup/tasks-logs/* ${logs_folder}/
else
echo "There is no task logs to restore"
fi

O comando acima define todos os passos que serão executados mediante ao acionamento da nossa trigger. O primeiro deles, Create environment, realiza a criação de um novo ambiente do Cloud Composer. É importante lembrar que é possível especificar outras opções na criação do ambiente, como a quantidade de workers, uso de CPU, memória, subrede e entre outros, conforme visto na documentação. O opção storage-bucket, por exemplo, é bastante recomendável para evitar que um novo bucket seja criado toda vez que executarmos a pipeline.

As demais etapas, por sua vez, são voltadas para recuperação do estado do ambiente, carregando o snapshot e os tasks logs. Nesse primeiro momento, entretanto, essas etapas serão ignoradas, visto que ainda não realizamos o salvamento de tais arquivos no bucket GCS que criamos anteriormente.

Depois de adequar o script com as suas informações, basta criar a nova trigger. Finalmente, clique no botão “RUN” para executar a trigger:

Executando a Trigger de criação do ambiente. Fonte: Autor.

Se tudo tiver funcionado conforme esperado, a trigger será executada com sucesso e o novo ambiente do Composer terá sido criado. Vale lembrar que o processo de criação do ambiente deve levar entre 20 a 30 minutos.

Verificando os logs e status da trigger do Composer. Fonte: Autor

Destruindo o ambiente do Composer

De maneira muito similar ao que foi feito anteriormente, vamos criar uma nova Trigger, porém, essa será responsável por destruir o ambiente do Composer a fim de evitar custos desnecessário e economizar dinheiro. Dito isso, insira o seguinte script YAML na nova trigger:

steps:

- name: gcr.io/cloud-builders/gcloud
entrypoint: /bin/bash
id: 'Save snapshot'
args:
- -c
- |
set -e
# Este é um project_id e env_name de exemplo. Use o seu próprio
project_id=my-gcp-project
env_name=my-basic-environment
# Lembre-se de alterar o bucket da GCS para utilizar o que foi criado anteriomente
snap_folder=$(gsutil ls gs://${project_id}-europe-west1-backup/snapshots) || snap_folder=empty
gcloud composer environments snapshots save ${env_name} \
--location europe-west1 --project ${project_id} \
--snapshot-location gs://${project_id}-europe-west1-backup/snapshots
if [[ $snap_folder != empty ]]
then
gsutil -m rm -r $snap_folder
fi

- name: gcr.io/cloud-builders/gcloud
entrypoint: /bin/bash
id: 'Save Tasks Logs'
args:
- -c
- |
set -e
# Este é um project_id e env_name de exemplo. Use o seu próprio
project_id=my-gcp-project
env_name=my-basic-environment
dags_folder=$(gcloud composer environments describe ${env_name} --project ${project_id} \
--location europe-west1 --format="get(config.dagGcsPrefix)")
logs_folder=$(echo $dags_folder | cut -d / -f-3)/logs
gsutil -m cp -r ${logs_folder}/* gs://${project_id}-europe-west1-backup/tasks-logs/

- name: gcr.io/cloud-builders/gcloud
entrypoint: /bin/bash
id: 'Delete Composer Environment'
args:
- -c
- |
set -e
# Este é um project_id e env_name de exemplo. Use o seu próprio
project_id=my-gcp-project
env_name=my-basic-environment
dags_folder=$(gcloud composer environments describe ${env_name} --project ${project_id} \
--location europe-west1 --format="get(config.dagGcsPrefix)")
gcloud composer environments delete --project ${project_id} --quiet \
${env_name} --location europe-west1
dags_bucket=$(echo $dags_folder | cut -d / -f-3)
gsutil -m rm -r $dags_bucket

Conforme discutimos anteriormente, antes de destruir o ambiente, realizamos o salvamento do snapshot e dos tasks logs no nosso bucket GCS. Após criar a trigger com o código acima, basta executá-la e aguardar o “desligamento” do seu ambiente do Cloud Composer.

Eu recomendo fortemente que você destrua um ambiente de teste para garantir que o salvamento e carregamento do backup está funcionando corretamente. Caso contrário, você pode acabar perdendo o seu ambiente “oficial” do Composer.

Se tudo tiver corrido conforme esperado, além da destruição do ambiente, você também deverá ver os snapshots e tasks logs criados no seu bucket, conforme mostrado abaixo:

Verificando os snapshots e tasks logs que foram criados. Fonte: Autor

Tente executar a trigger de criação de ambiente novamente e verifique se, dessa vez, o snapshot e os tasks logs serão carregados durante a criação.

Agendando a execução das pipelines

Agora que você possui uma pipeline para criar e destruir o ambiente do Composer, é preciso automatizar a execução das triggers em horários específicos, garantindo que o ambiente estará pronto assim que você iniciar seu trabalho, e será destruindo ao final do expediente.

Para isso, iremos utilizar o serviço de agendamento de execução de tarefas da GCP, o Cloud Scheduler. A maneira mais fácil de criar um novo agendamento para as nossas triggers é através do atalho “Run on Schedule”, conforme mostrado abaixo:

Criando um novo schedule para executar a trigger. Fonte: Autor

Depois, basta adicionar os parâmetros para configurar o agendamento da execução da trigger da forma que achar melhor:

Configurando o agendamento da execução da Trigger. Fonte: Autor

Na configuração acima, por exemplo, utilizamos uma cron expression para agendar a execução para as 7:30 da manhã, de segunda a sexta feira. Também é importante lembrar de adicionar a conta de serviço que será utilizada para realizar o disparo da execução da trigger. Você pode utilizar a mesma conta que foi criada anteriormente para o Cloud Composer.

A criação do ambiente leva entre 20 a 30 minutos. Leve esse tempo em consideração ao criar o seu agendamento.

Conclusão

Utilizar serviços gerenciados por provedores de nuvem costuma ser muito vantajoso, pois garante facilidade, segurança e escalabilidade da infraestrutura. O Cloud Composer é um ótimo exemplo disso, permitindo a criação de um ambiente robusto de Airflow com apenas alguns cliques.

Ainda assim, o custo não pode ser ignorado, podendo ser um dos principais ofensores desse tipo de abordagem. Dito isso, uma pipeline de CI/CD para criar e destruir ambientes de maneira automatizada é a melhor forma de reduzir o desperdício de recursos, especialmente na versão 2 do Composer.

Além disso, graças ao salvamento dos snapshots e dos tasks logs, garantimos que o estado do nosso ambiente seja preservado, evitando a perda de configurações importantes para a execução do mesmo.

Por fim, uma outra abordagem que pode ser muito útil para reduzir ainda mais os custos de ambientes de desenvolvimento do Airflow é o uso do Composer localmente através de uma imagem Docker. Irei abordar essa estratégia em um artigo futuro.

--

--

Alvaro Leandro Cavalcante Carneiro
Data Hackers

MSc. Computer Science | Data Engineer. I write about artificial intelligence, deep learning and programming.