← Professional Cloud DevOps Engineer: entrega e fiabilidade
17 / 20 · 135 MIN

Entregas recuperáveis: hooks, retries e decisões de RUN

Reconstrói uma entrega parcial, coordena a recuperação e evita repetir efeitos externos quando uma confirmação se perde.

1. Reconstruir a entrega antes de intervir

Às 22:10, uma equipa entrega à APS a mensagem «a release falhou». Essa frase não identifica o que ficou instalado nem o que ainda está em execução. Começa por construir uma sequência com release, destino, rollout, fase, job e job run. Acrescenta a hora e o identificador da evidência. Um job pode ter várias tentativas, e uma tentativa pode ter produzido um efeito externo antes de perder a confirmação. Regista separadamente o estado observado e a decisão pretendida. «Verify falhou por falta de acesso ao endpoint» descreve uma observação; «repetir após corrigir a rota» é uma decisão que precisa de responsável e de condição de entrada. No exercício desta aula, prod-west e prod-east partilham a equipa de suporte, mas conservam históricos de entrega diferentes. Uma release bem-sucedida em east não demonstra que west a recebeu. O handover deve permitir a outra equipa reconstruir a situação sem depender da memória de quem executou a mudança. Inclui o que ainda está desconhecido: confirmação externa perdida, tráfego em convergência ou job em execução. Não preenches essas lacunas com o último estado verde do dashboard. Esta disciplina permite escolher entre acompanhar, repetir, interromper e recuperar sem transformar uma hipótese num facto.

2. Escolher uma recuperação por destino

Um rollback em Cloud Deploy gera um rollout novo a partir de uma release anterior. A escolha por omissão considera o último sucesso no destino indicado; uma release específica pode ser escolhida. No nosso exemplo, r18 passou em prod-west, r19 passou apenas em staging e r20 falhou em prod-west. A referência por omissão de west é r18. Antes de executar, a equipa confirma se essa release continua elegível e compatível com o ambiente atual. O histórico de sucesso é uma referência operacional, não uma garantia de que dependências, dados e políticas continuam iguais. Conserva o rollout novo e liga-o ao incidente que motivou a recuperação. Não substituas os resultados antigos pelos novos. O objetivo de aceitação deste exercício é demonstrar que o fluxo crítico voltou a funcionar no destino afetado, com reconciliação dos efeitos produzidos durante a falha. Se um sistema externo recebeu instruções enquanto r20 estava ativo, voltar a r18 não apaga essas instruções. O plano deve indicar quem consulta o estado externo e quem pode autorizar uma compensação. A decisão de rollback e a decisão sobre dados podem ter responsáveis e tempos diferentes, que precisam de ser coordenados.

3. Separar abandono, cancelamento e terminação

Abandonar uma release impede novas entregas dessa release e é permanente. Rollouts já criados, incluindo os que estão em fila, não são cancelados por esse abandono. Por isso, se o incidente exige contenção, consulta também os rollouts existentes. Um recurso abandonado não pode ser reativado para servir como opção de recuperação. Revê a lista de releases elegíveis do runbook quando retirar uma versão; caso contrário, a equipa de noite pode descobrir a indisponibilidade do único candidato durante o incidente. Cancelar um rollout pode deixá-lo em CANCELLING enquanto os job runs pendentes de conclusão continuam. Terminar um job run ativo deixa o respetivo job em falha. Nenhuma destas indicações demonstra que efeitos externos foram desfeitos. Num predeploy que cria uma reserva, a reserva pode continuar a existir após a interrupção. Antes de uma nova tentativa, consulta o resultado pela identidade lógica da operação. No relatório à gestão, escreve «cancelamento pedido; job X ainda ativo; reserva Y por reconciliar» se for essa a evidência. A frase permite atribuir trabalho concreto. «Tudo revertido» encerraria prematuramente tarefas que continuam necessárias.

4. Coordenar retries e a intervenção manual

Uma reparação automática precisa de um orçamento de tempo e de uma regra sobre quem assume a condução se alguém intervier. No modelo usado aqui, três retries esperam 2, 4 e 8 minutos, e cada tentativa demora 1 minuto. São 17 minutos até terminar a terceira tentativa, sem filas ou latência de controlo. Se restarem 12 minutos na janela, o plano já não cabe nas hipóteses declaradas. Reduzir a espera só para cumprir a janela pode agravar uma dependência saturada. Discute antes a mitigação, a extensão autorizada ou a interrupção segura com os responsáveis. O retry manual de um job aborta a execução de repairRolloutRule em curso. Não deixes duas equipas assumir que a outra continua a seguir a sequência automática original. Regista a intervenção, o estado observado e quem decide o passo seguinte. A opção disableRollbackIfRolloutPending evita iniciar esse rollback quando existe outro rollout pendente no destino. Isso resolve uma condição de concorrência definida, mas não prova que a entrega pendente é segura. É preciso avaliar o candidato e coordenar o destino. No exercício, a autorização de recuperação pertence ao responsável identificado no incidente; não é inferida do simples facto de existir uma automação.

5. Construir hooks que reconheçam a mesma intenção

O autor do hook é responsável pela idempotência. Identifica primeiro o efeito de negócio que deve ocorrer uma vez: reservar capacidade para prod-west e r70, por exemplo. Um ID de job run muda entre tentativas e não serve, sozinho, para reconhecer essa intenção. O exercício Python usa uma chave estável, payload e recibo. Se a primeira chamada guarda o efeito e perde o ACK, a repetição com a mesma chave e payload devolve o recibo existente. Uma chave diferente cria outra reserva, mostrando por que motivo falha observada e ausência de efeito não são equivalentes. O mesmo identificador com payload diferente é tratado como conflito. A passagem de 500 para 700 unidades exige uma decisão nova; não deve ser disfarçada de retry nem devolver sucesso silencioso com o valor antigo. O modelo é sequencial e só mantém dados em memória. Não demonstra exclusão concorrente, durabilidade após crash, autenticação ou atomicidade com um serviço externo. Numa integração real, essas propriedades precisam de ser desenhadas e ensaiadas com o fornecedor. Usa o exercício para explicar a identidade da intenção e reconhecer duplicações; não o uses como biblioteca pronta para executar operações de produção.

6. Verificar ordem, configuração e fronteiras

Se o teste precisa de dados sintéticos, esses dados têm de existir antes de verify. Colocá-los num postdeploy cria uma dependência invertida, pois verify precede postdeploy quando configurado. No canary automático, predeploy corre na primeira fase e postdeploy na última; não são tarefas repetidas em cada percentagem. Uma lista de tasks é sequencial e termina na primeira falha. Se reserve passou e seed falhou a meio, announce não correu; os efeitos de reserve e os dados parciais continuam a precisar de reconciliação. A configuração por destino também merece uma revisão explícita. As labels de matchTargetLabels combinam-se por AND: region=west e class=critical requerem ambas. Dois valores diferentes do mesmo parâmetro, sem seleção que os distinga, não são automaticamente distribuídos pela ordem dos destinos. No exercício, a equipa deve mostrar o manifesto renderizado de cada filho e confirmar o número de réplicas esperado. Liga essa evidência ao destino e à release. Uma aprovação de uma tabela de intenções não demonstra que o ficheiro efetivamente entregue contém os valores certos. Se houver divergência, suspende a promoção normal e corrige a seleção antes de produzir nova evidência.

7. Aplicar freezes e observar tráfego real

Uma política de entrega pode restringir ações como CREATE ou ADVANCE na janela configurada. A release pode existir sem que seja permitido criar o rollout. A política atual é avaliada quando se tenta a ação; criar a release antes do freeze não concede uma exceção. Dentro de um selector, as condições combinam-se por AND; entre selectors, por OR. Confirma também o invocador e o fuso horário. Poder ultrapassar uma política não substitui a permissão IAM da operação. No exercício, qualquer exceção precisa da autorização definida pela equipa, além da capacidade técnica de a executar. Depois de uma alteração de tráfego Cloud Run, observa a convergência e os efeitos dos pedidos em execução. Uma resposta tardia da revisão anterior durante a transição não demonstra, por si só, que o comando falhou. E uma revisão com zero na divisão de tráfego pode continuar acessível por uma tag para testes autorizados. Para um ensaio sem efeitos de negócio, controla os dados, endpoints e identidades usados. Escrever zero no diagrama não isola uma base de dados partilhada. O critério de recuperação deve medir o fluxo relevante e acompanhar resultados externos, não apenas o estado desejado da distribuição.

8. Praticar, explicar e passar ao RUN

Executa o modelo Python local e compara lost-ack-stable-key com lost-ack-fresh-key. A primeira sequência conserva uma reserva de 500; a segunda termina com duas, totalizando 1000. Em changed-intent, o pedido de 700 com a chave anterior é rejeitado e o total continua 500. Antes de ler as respostas, escreve a previsão de cada caso e identifica a evidência que te faria mudar de decisão. Depois modifica o número de repetições sem alterar a intenção. O resultado esperado continua a ser um único efeito. O exercício também verifica entradas inválidas e a cópia do payload para evitar alteração acidental do registo guardado. Termina preparando uma nota de handover em inglês: o que foi pedido, o que foi observado, que efeito externo está confirmado, quem conduz a recuperação e que critério permite encerrar. Associa os identificadores corretos e explicita o que ainda falta verificar. Num ensaio de mesa, outra pessoa deve conseguir decidir o próximo passo apenas com essa nota e as referências. O desafio final é justificar por que motivo abandonar, cancelar, terminar e fazer rollback resolvem problemas diferentes. A avaliação usa situações fictícias e regras explícitas; não descreve procedimentos internos de um banco real nem substitui a validação técnica de uma integração de produção.

"""Original sequential teaching model: not a Cloud Deploy or payment emulator.

No network or persistent writes. A production implementation needs durable,
atomic idempotency records, authorization and reconciliation with its provider.
"""
from copy import deepcopy
from hashlib import sha256
import json
from pathlib import Path


def reserve(ledger, key, payload, *, lose_ack=False):
    if not isinstance(key, str) or not key.strip():
        raise ValueError('nonempty operation key required')
    if not isinstance(payload, dict) or set(payload) != {'target', 'units'}:
        raise ValueError('target and units required')
    if not isinstance(payload['target'], str) or not payload['target'].strip():
        raise ValueError('nonempty target required')
    if type(payload['units']) is not int or payload['units'] <= 0:
        raise ValueError('positive integer units required')
    if type(lose_ack) is not bool:
        raise ValueError('boolean acknowledgement flag required')
    if key in ledger:
        if ledger[key]['payload'] != payload:
            return {'status': 'conflict', 'created': False, 'receipt': None}
        return {'status': 'replayed', 'created': False,
                'receipt': ledger[key]['receipt']}
    receipt = f"reservation-{len(ledger) + 1}"
    ledger[key] = {'payload': deepcopy(payload), 'receipt': receipt}
    return {'status': 'unknown' if lose_ack else 'created', 'created': True,
            'receipt': None if lose_ack else receipt}


def totals(ledger):
    return sum(row['payload']['units'] for row in ledger.values())


def fixture(name, calls):
    ledger = {}
    results = [reserve(ledger, key, payload, lose_ack=lost)
               for key, payload, lost in calls]
    return {'id': name, 'results': results, 'reservations': len(ledger),
            'reservedUnits': totals(ledger)}


def main():
    base = {'target': 'prod-west', 'units': 500}
    fixtures = [
        fixture('acknowledged', [('reserve:prod-west:r70', base, False)]),
        fixture('lost-ack-stable-key', [('reserve:prod-west:r70', base, True),
                                      ('reserve:prod-west:r70', base, False)]),
        fixture('lost-ack-fresh-key', [('run-1', base, True), ('run-2', base, False)]),
        fixture('changed-intent', [('op-1', base, True),
                 ('op-1', {'target': 'prod-west', 'units': 700}, False)]),
        fixture('wrong-target', [('op-1', base, True),
                 ('op-1', {'target': 'prod-east', 'units': 500}, False)]),
        fixture('two-authorized-operations', [('op-1', base, False),
                                            ('op-2', base, False)]),
    ]
    by_id = {row['id']: row for row in fixtures}
    assert by_id['lost-ack-stable-key']['reservedUnits'] == 500
    assert by_id['lost-ack-stable-key']['results'][1]['status'] == 'replayed'
    assert by_id['lost-ack-fresh-key']['reservedUnits'] == 1000
    for name in ['changed-intent', 'wrong-target']:
        assert by_id[name]['results'][1]['status'] == 'conflict'
        assert by_id[name]['reservedUnits'] == 500
    assert by_id['two-authorized-operations']['reservations'] == 2
    replay_checks = 0
    for units in range(1, 41):
        for retries in range(1, 21):
            ledger = {}
            payload = {'target': 'prod-west', 'units': units}
            reserve(ledger, 'stable', payload, lose_ack=True)
            for _ in range(retries):
                result = reserve(ledger, 'stable', payload)
                assert result['status'] == 'replayed' and not result['created']
            assert totals(ledger) == units and len(ledger) == 1
            replay_checks += 1
    conflict_checks = 0
    for changed_units in range(1, 41):
        ledger = {}
        reserve(ledger, 'stable', base)
        before = deepcopy(ledger)
        assert reserve(ledger, 'stable', {'target': 'prod-west',
                        'units': changed_units})['status'] == 'conflict'
        assert ledger == before
        conflict_checks += 1
    invalid = [('', base), ('x', None), ('x', {}),
               ('x', {'target': '', 'units': 5}),
               *[('x', {'target': 'prod', 'units': n})
                 for n in [0, -1, True, 1.5, '5']]]
    for key, payload in invalid:
        ledger = {}
        try:
            reserve(ledger, key, payload)
        except ValueError:
            assert not ledger
        else:
            raise AssertionError('invalid input accepted')
    payload = deepcopy(base); ledger = {}
    reserve(ledger, 'stable', payload)
    payload['units'] = 900
    assert totals(ledger) == 500
    print(json.dumps({'fixtures': fixtures, 'replayCombinations': replay_checks,
        'conflictChecks': conflict_checks, 'invalidInputs': len(invalid),
        'payloadCopyCheck': True, 'network': False, 'persistentWrites': False,
        'vendorExecution': False, 'concurrencyProof': False,
        'independentVerification': False,
        'scriptSha256': sha256(Path(__file__).read_bytes()).hexdigest()}, indent=2))


if __name__ == '__main__':
    main()
NA PRÁTICA

r70 reservou 500 unidades antes de perder o ACK. Repetir com a mesma chave e payload devolve o recibo existente; mudar o ID cria outra reserva. Mudar para 700 com a chave antiga exige resolver um conflito de intenção.

Armadilhas comuns

Confundir abandono com cancelamento; assumir que um ACK perdido significa ausência de efeito; usar uma chave nova em cada retry; ignorar um freeze por a release ser antiga; dar como concluída uma recuperação ainda em convergência.

Tópicos relacionados: Verificação e promoção de pipelines · Incidentes e passagem ao RUN · Identidade e autorização da entrega

Leva esta ideia contigo

Decide a partir do estado do destino e dos efeitos confirmados. Preserva a identidade da intenção entre retries e explicita quem conduz a recuperação.

Criar conta

Referência: Roll back a target · Current linked guide; edition date unconfirmed (2026-09-30 inspection)

Google Cloud é uma marca comercial de Google LLC. A dr.pt é uma plataforma de preparação independente e não está afiliada, associada, patrocinada, autorizada nem aprovada por Google. Os conteúdos e as perguntas são originais, não são perguntas oficiais de exame, e concluir os nossos testes não atribui nem garante qualquer certificação. Os nomes são usados apenas para identificar o tema. Todas as outras marcas pertencem aos respetivos titulares.