← Professional Cloud DevOps Engineer: entrega e fiabilidade
23 / 25 · 135 MIN

Fiabilidade: dependências, restauro e prova de recuperação

Prepara recuperação de workloads e dados, calcula precedências e distingue estimativas de evidência para reabrir o serviço.

1. Definir o que significa recuperar

Um restauro concluído é um evento técnico. Para decidir se um serviço pode voltar a ser usado, precisamos de um contrato de recuperação: população abrangida, operações necessárias, estado de dados aceitável e evidência exigida. Num exercício fictício de fundos, a página inicial pode funcionar enquanto o processamento de instruções continua indisponível. Se o critério aprovado inclui consultar uma instrução e verificar o respetivo documento, abrir apenas a página inicial não satisfaz esse critério. Escreve a operação de aceitação antes do ensaio, com resultado esperado e responsável pela avaliação. RTO e RPO medem dimensões diferentes. O primeiro limita o período de indisponibilidade definido no acordo; o segundo limita a janela temporal de perda de dados tolerada. Identifica exatamente início e fim da medição. No nosso exercício, a interrupção começa às 14:00, o comando de restore arranca às 14:08, termina às 14:24 e a validação de negócio conclui às 14:33. Se o acordo termina nessa validação, observámos 33 minutos. Reportar 16 minutos mede apenas o comando. Antes de prometer uma data, distingue objetivo, estimativa e observação. O objetivo orienta o desenho; a estimativa depende de pressupostos; a observação documenta o ensaio executado. Guarda os três valores separadamente. Uma previsão de 30 minutos ainda não demonstra que a equipa recuperou o serviço nesse tempo.

2. Inventariar o conjunto recuperável

Começa pelo percurso de uma operação do utilizador e identifica o que ela precisa para terminar. No exemplo APS, um Pod recebe a instrução, consulta Cloud SQL, lê um documento e grava o resultado. O backup do workload GKE pode proteger manifests e volumes abrangidos pelo serviço, mas não inclui automaticamente o estado do Cloud SQL externo nem as camadas das imagens. O cluster de destino também tem de existir com o agente Backup for GKE ativado. Estas fronteiras obrigam a planear componentes que não aparecem como tarefas do mesmo backup. Cria uma tabela de trabalho com componente, mecanismo de recuperação, identidade necessária, dependência e prova de restauro. Para a imagem, regista o digest e onde o conteúdo continuará disponível. Uma referência intacta num manifest não ajuda se o artefacto foi removido e nenhum nó de destino tem cache. Para o documento externo, identifica quem demonstra a sua recuperação e como se relaciona com a instrução. Para o cluster, verifica configuração de rede e capacidade através do processo que realmente o cria. O objetivo desta tabela é encontrar ausências antes da janela de intervenção. Não preenchas uma célula desconhecida com “incluído no backup” por conveniência. Atribui um responsável à lacuna e ensaia o caminho necessário. O trabalho de projeto inclui estas dependências, mesmo quando pertencem a equipas distintas.

3. Verificar chaves como dependências de recuperação

Uma cópia acessível e uma chave utilizável são evidências distintas. Em Backup for GKE, um volume já protegido por CMEK pode conservar a dependência da chave do disco original, mesmo que o plano use outra chave. Por isso, guardar o backup noutra região não demonstra sozinho independência da região original. No ensaio fictício, a equipa consegue listar a cópia mas o restauro do volume falha ao usar a chave. O ticket deve identificar essa dependência concreta, a identidade que tenta usá-la e a condição observada, sem expor material secreto. Em Cloud KMS, uma versão DISABLED ainda possui material e pode ser reativada mediante autorização. A versão precisa de estar utilizável para a operação criptográfica requerida. Criar uma versão primary diferente não substitui a versão que protege o conteúdo existente. Evita transformar pressão de tempo numa operação de destruição: remover uma chave não reencripta retroativamente os dados. No plano do exercício, “chaves prontas” é uma tarefa com evidência própria. Verifica o caminho autorizado com identidades representativas e conserva o resultado do ensaio. Se só existe confirmação do administrador habitual, ainda falta demonstrar o acesso da identidade que executará a recuperação. A estimativa temporal deve declarar esta condição. Um bloqueio de chave sem prazo conhecido não pode continuar escondido dentro de uma tarefa marcada como três minutos.

4. Escolher e validar um estado coerente

Cloud SQL PostgreSQL PITR cria uma nova instância. Planeia a validação desse destino e a transição das ligações, em vez de assumir que a origem mudou de estado. Confirma também a janela recuperável da instância. Se a janela comprovada termina às 10:42, não há base para prometer 10:47 por esse caminho. Isso limita a recuperação demonstrada; não prova que todos os dados posteriores foram destruídos. Pode haver investigação adicional, mas a comunicação deve separar possibilidade de evidência. A recuperação do serviço exige coerência entre componentes. No nosso caso, a tabela restaurada contém uma referência a um documento criado às 09:58, enquanto os documentos foram recuperados para 09:55. As APIs podem responder normalmente e a referência continuar inválida. Define uma verificação que atravesse essa relação: selecionar instruções do intervalo afetado, resolver os documentos associados e registar exceções. Contagens iguais não estabelecem correspondência entre identidades. Trata também o destino antigo como parte do plano. Num PostgreSQL autogerido após promoção, é necessário impedir que o antigo primário regresse a aceitar escritas como se ainda fosse a autoridade. O exercício não prescreve comandos para um serviço gerido; ensina a procurar o risco de dois históricos. Um job mensal esquecido pode continuar a escrever no destino antigo mesmo depois de a aplicação interativa mudar. Inclui-o na reconciliação e na confirmação de transição.

5. Calcular precedências e margem temporal

O modelo local representa tarefas com identificador, duração em minutos e lista de predecessoras. Assume recursos de execução suficientes, tarefas sem falhas e início de cada tarefa logo que todas as predecessoras terminam. Estes pressupostos permitem calcular um limite de planeamento; não simulam filas de operadores, pedidos de aprovação ou disponibilidade de serviços cloud. Se duas tarefas precisam da mesma pessoa, introduz a restrição no plano ou usa outro modelo. Não declares paralelismo só porque duas linhas aparecem lado a lado. No exemplo, cluster demora 12 minutos, base de dados 18, controlo do escritor antigo cinco e chaves três; podem começar no instante zero. O workload precisa do cluster e das chaves, demorando depois oito minutos. Termina aos 20. A validação precisa do workload, da base de dados e do controlo do escritor; demora seis minutos e termina aos 26. A reabertura demora mais quatro, terminando aos 30. Somar todas as durações conta indevidamente trabalho paralelo. Usar apenas a maior duração individual ignora sucessoras. Agora aumenta a preparação do cluster para 17 minutos. O workload acaba aos 25 e a reabertura aos 35. O alvo de 30 deixa de caber no modelo. Em contrapartida, reduzir a tarefa de chaves de três para um minuto não altera o prazo inicial, porque o cluster continua a dominar essa precedência. Escolhe melhorias com base na cadeia que realmente limita o resultado.

6. Separar condições falsas de condições desconhecidas

O exercício pede cinco campos de evidência: dados coerentes, escritor antigo controlado, chaves acessíveis, verificação funcional e aceitação de negócio. Cada campo admite true, false ou null. Estes nomes e esta lista são uma convenção pedagógica do exercício, não um padrão obrigatório Google Cloud ou um processo interno BNP Paribas. Numa implementação real, os critérios teriam de corresponder ao serviço, ao risco e às responsabilidades aprovadas. Preencher true é uma afirmação fornecida pelo utilizador; o programa não inspeciona a evidência que a sustenta. Uma condição false representa uma verificação que falhou. null indica que falta confirmação. Ambos impedem que a ficha fique pronta para decisão, mas exigem ações diferentes: corrigir uma falha conhecida ou recolher evidência ainda ausente. A idade do ponto recuperado também pode ser desconhecida. Não a substituas pela idade do backup mais recente quando a coerência dos dados ainda está em investigação. O resultado readyForDecision só aparece quando a estimativa cabe no RTO, a idade reportada cabe no RPO e os cinco campos são verdadeiros. Mesmo assim, productionAuthorized continua false. O modelo não tem autoridade operacional, não verifica assinaturas nem comprova a situação atual. Serve para organizar uma discussão verificável. A pessoa responsável precisa de consultar evidências, conhecer exceções e decidir segundo o processo aplicável ao serviço.

7. Comunicar recuperação parcial e aprender com o ensaio

Uma atualização de incidente deve permitir perceber o impacto restante e a próxima decisão. No cenário, a sonda interna passa mas os parceiros continuam a falhar por um caminho ainda antigo. Comunica recuperação parcial, população afetada, ações em curso e próximo momento de atualização. Evita tanto declarar sucesso total como descartar a evidência útil da sonda. O seu resultado é válido dentro do caminho que efetivamente exercitou. Regista limites de observação junto da conclusão para que a equipa seguinte não a interprete como cobertura universal. Na passagem de coordenação, confirma que alguém aceitou a responsabilidade, conhece as ações em execução e entende as condições de reabertura pendentes. Uma lista de links sem contexto não mostra quais decisões já foram tomadas. No nosso exercício, o restauro está em curso e a chave é um bloqueio conhecido; o sucessor deve saber quem investiga a chave, que operações não podem avançar e quando rever a previsão. A comunicação ao negócio pode ocorrer em inglês, com horários e âmbitos explícitos. Depois do ensaio, liga as falhas a ações acompanháveis. Para a dependência de chave, propõe responsável, prazo e um novo ensaio em que essa dependência esteja indisponível. O resultado esperado deve ser demonstrável. Uma apresentação concluída ou um pedido genérico de maior atenção não comprova a correção do caminho de recuperação.

8. Executar e interpretar o exercício local

Executa python3 run.py num diretório local com Python 3.13 ou compatível. O programa usa apenas a biblioteca padrão e não chama APIs, não restaura bases de dados nem altera ficheiros. O JSON apresentado é um relatório do modelo com dados fictícios. Começa pelo caso parallel-plan: o resultado previsto é 30 minutos e a ficha está pronta para decisão com os valores fornecidos. Compara depois rto-missed, rpo-missed e unknown-recovery-point. Um tempo que cabe no plano não compensa perda de dados fora do limite nem um ponto coerente ainda desconhecido. No caso cleanup-outside-service, há uma tarefa de limpeza que termina depois da reabertura. O programa lista-a em outsideServicePath, mantendo 30 minutos para a tarefa explicitamente escolhida como fim do serviço. Isto não decide se é correto excluir essa limpeza do contrato real. Se a limpeza for condição da aceitação, modifica as dependências e volta a calcular. O utilizador é responsável pelo significado do grafo. O código verifica oito casos nomeados, 512 combinações de durações e 243 combinações dos campos de evidência. Rejeita 14 entradas inválidas, incluindo ciclos, tarefas repetidas e referências inexistentes. A validação também confirma que os dados de entrada não foram modificados. Estes testes demonstram propriedades deste modelo pequeno. Não demonstram tempos cloud, consistência de backups, permissões efetivas ou capacidade real de failover. Usa as perguntas seguintes para explicar decisões e limitações, além de repetir os números.

"""Original recovery planning worksheet. No scheduler, database or cloud execution."""
from copy import deepcopy
from graphlib import TopologicalSorter, CycleError
import hashlib
import itertools
import json
from pathlib import Path

GATES = ('data_consistent', 'old_writer_fenced', 'keys_accessible',
         'functional_check', 'business_acceptance')


def minutes(value, name):
    if type(value) is not int or value < 0:
        raise ValueError(name + ' must be a nonnegative integer')
    return value


def assess(tasks, service_task, rto_minutes, recovery_age_minutes, rpo_minutes, checks):
    """All tasks start as soon as dependencies finish, with unlimited workers.

    Durations are fictional estimates from interruption time zero. Checks are
    supplied assertions, not verified evidence. None means unknown. Tasks outside
    the service-task ancestor graph are explicitly listed. RPO uses an externally
    established coherent recovery point; a timestamp alone does not establish one.
    """
    for name, value in [('rto', rto_minutes), ('rpo', rpo_minutes)]:
        minutes(value, name)
    if recovery_age_minutes is not None:
        minutes(recovery_age_minutes, 'recovery age')
    if not isinstance(tasks, list) or not tasks:
        raise ValueError('tasks must be a nonempty list')
    if not isinstance(checks, dict) or set(checks) != set(GATES):
        raise ValueError('provide exactly the five evidence fields')
    if any(value is not None and type(value) is not bool for value in checks.values()):
        raise ValueError('checks must be true, false or null')
    by_id = {}
    for task in tasks:
        if not isinstance(task, dict) or set(task) != {'id', 'minutes', 'after'}:
            raise ValueError('invalid task fields')
        name = task['id']
        if not isinstance(name, str) or not name.strip() or name in by_id:
            raise ValueError('task IDs must be nonempty and unique')
        minutes(task['minutes'], name)
        deps = task['after']
        if not isinstance(deps, list) or any(not isinstance(x, str) for x in deps):
            raise ValueError('dependencies must be strings in a list')
        if len(set(deps)) != len(deps):
            raise ValueError('repeated dependency')
        by_id[name] = task
    if not isinstance(service_task, str) or service_task not in by_id:
        raise ValueError('service task not defined')
    graph = {name: task['after'][:] for name, task in by_id.items()}
    if any(dep not in by_id for deps in graph.values() for dep in deps):
        raise ValueError('dependency not defined')
    try:
        order = list(TopologicalSorter(graph).static_order())
    except CycleError as error:
        raise ValueError('cyclic recovery plan') from error
    finish, ancestors = {}, {}
    for name in order:
        finish[name] = max((finish[d] for d in graph[name]), default=0) + by_id[name]['minutes']
        ancestors[name] = {name}.union(*(ancestors[d] for d in graph[name]))
    total = finish[service_task]
    rpo_met = None if recovery_age_minutes is None else recovery_age_minutes <= rpo_minutes
    return {
        'estimatedServiceMinutes': total,
        'finishMinutes': dict(sorted(finish.items())),
        'outsideServicePath': sorted(set(graph) - ancestors[service_task]),
        'estimatedRtoMet': total <= rto_minutes,
        'reportedRpoMet': rpo_met,
        'failedChecks': sorted(k for k, v in checks.items() if v is False),
        'unknownChecks': sorted(k for k, v in checks.items() if v is None),
        'readyForDecision': total <= rto_minutes and rpo_met is True and all(v is True for v in checks.values()),
        'productionAuthorized': False,
    }


def task(name, duration, *after):
    return {'id': name, 'minutes': duration, 'after': list(after)}


def evidence():
    tasks = [task('cluster', 12), task('keys', 3), task('database', 18),
             task('fence', 5), task('workload', 8, 'cluster', 'keys'),
             task('validate', 6, 'database', 'workload', 'fence'),
             task('service', 4, 'validate')]
    good = dict.fromkeys(GATES, True)
    base = [tasks, 'service', 30, 4, 5, good]
    original = deepcopy(base)
    fixtures = []
    for name, change in [
        ('parallel-plan', {}), ('rto-missed', {2: 29}),
        ('rpo-missed', {3: 6}), ('unknown-recovery-point', {3: None}),
        ('unknown-acceptance', {5: {**good, 'business_acceptance': None}}),
        ('old-writer-active', {5: {**good, 'old_writer_fenced': False}}),
        ('cleanup-outside-service', {0: tasks + [task('cleanup', 60, 'service')]}),
        ('slow-cluster', {0: [{**t, 'minutes': 17} if t['id'] == 'cluster' else t for t in tasks]}),
    ]:
        args = deepcopy(base)
        for index, value in change.items():
            args[index] = value
        fixtures.append({'id': name, **assess(*args)})
    assert fixtures[0]['estimatedServiceMinutes'] == 30
    assert fixtures[0]['readyForDecision'] is True
    assert all(not f['productionAuthorized'] for f in fixtures)
    assert all(not f['readyForDecision'] for f in fixtures[1:6])
    assert fixtures[6]['estimatedServiceMinutes'] == 30
    assert fixtures[6]['outsideServicePath'] == ['cleanup']
    assert fixtures[7]['estimatedServiceMinutes'] == 35
    duration_checks = 0
    for a, b, c in itertools.product(range(8), repeat=3):
        model = [task('a', a), task('b', b), task('c', c, 'a', 'b')]
        result = assess(model, 'c', 100, 0, 0, good)
        assert result['estimatedServiceMinutes'] == max(a, b) + c
        assert assess(list(reversed(model)), 'c', 100, 0, 0, good) == result
        duration_checks += 1
    gate_checks = 0
    for values in itertools.product((True, False, None), repeat=5):
        checks = dict(zip(GATES, values))
        result = assess(tasks, 'service', 30, 4, 5, checks)
        assert result['readyForDecision'] == all(v is True for v in values)
        assert result['unknownChecks'] == sorted(k for k,v in checks.items() if v is None)
        assert result['failedChecks'] == sorted(k for k,v in checks.items() if v is False)
        gate_checks += 1
    invalid = [
        {0: []}, {0: tasks + [tasks[0]]}, {0: [task('a', -1)]},
        {0: [task('a', True)]}, {0: [task('service', 1, 'missing')]},
        {0: [task('service', 1, 'x'), task('x', 1, 'service')]},
        {0: [task('service', 1, 'service')]}, {1: 'absent'},
        {2: True}, {3: -1}, {4: 1.2}, {5: {}},
        {5: {**good, 'functional_check': 1}},
        {0: [task('a', 1), task('service', 2, 'a', 'a')]},
    ]
    for change in invalid:
        args = deepcopy(base)
        for index, value in change.items():
            args[index] = value
        try:
            assess(*args)
        except ValueError:
            pass
        else:
            raise AssertionError('invalid input accepted')
    assert base == original
    return {'fixtures': fixtures, 'durationCombinations': duration_checks,
            'gateCombinations': gate_checks, 'invalidInputs': len(invalid),
            'inputPreserved': True, 'cloudExecuted': False, 'databaseRestored': False,
            'network': False, 'persistentWrites': False, 'independentVerification': False,
            'scriptSha256': hashlib.sha256(Path(__file__).read_bytes()).hexdigest()}


if __name__ == '__main__':
    print(json.dumps(evidence(), sort_keys=True, indent=2))
NA PRÁTICA

Cluster 12 min, workload 8 min, validação 6 min e reabertura 4 min formam o caminho de 30 minutos, com base de dados em paralelo.

Armadilhas comuns

Backup acessível como prova de restauro; timestamps como coerência; estimativa como medição; null como aprovação; sonda interna como cobertura de todos os clientes.

Tópicos relacionados: Continuidade de processamento e reconciliação · Identidade e gestão de chaves · Comunicação de incidentes e passagem de turno

Leva esta ideia contigo

Uma recuperação defensável liga dependências executáveis, dados coerentes, controlo de escrita e aceitação demonstrada, com limites de evidência explícitos.

Criar conta

Referência: Disaster recovery planning guide · 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.