Definir o serviço que será entregue ao RUN
Uma equipa APS de um banco fictício reverte uma release do serviço de fundos durante uma janela de manutenção. O comité quer saber quando pode devolver a responsabilidade operacional ao RUN. Este caso não descreve procedimentos internos BNP Paribas. A decisão exige mais do que observar um comando aceite: deve identificar a revisão recuperada, o destino, os testes funcionais e a capacidade de detetar o próximo incidente. Em Cloud Deploy, o rollback cria um novo rollout a partir de uma release anterior. A existência desse rollout mostra que há uma execução a acompanhar; o sucesso de um rollout antigo não demonstra o resultado da execução atual. Regista os dois identificadores para evitar que um dashboard apresente o resultado histórico como se fosse evidência nova. Liga cada teste à revisão e ao target realmente usados no ensaio. Se a aplicação depender de uma mudança de dados incompatível com a versão anterior, a mera seleção dessa release não resolve a compatibilidade. Essa verificação é uma inferência operacional do caso, não uma garantia fornecida pelo comando. Como exercício guiado, escreve três condições de aceitação antes de executar o rollback: o serviço processa uma operação representativa, os controlos aplicáveis continuam ativos e o RUN recebe o sinal de falha acordado. Para cada condição, escolhe quem observa, qual recurso é observado e quando a evidência deixa de ser suficientemente recente. Estes prazos pertencem ao contrato fictício do exercício. Não são valores por omissão do fornecedor nem autorização para executar alterações reais.
Recuperar com a configuração de execução correta
Uma release conserva a instância da pipeline e dos targets existente quando foi criada. A equipa altera entretanto a configuração atual do target e assume que o rollback usará automaticamente essa alteração. Quando surge o aviso de divergência, confirmar sem ler a diferença pode executar um plano diferente do esperado. Compara a configuração associada à release com a configuração atual e identifica quais os campos que afetam o destino, as permissões e a sequência de execução. Outro grupo suspendeu a pipeline para impedir novas mudanças durante o incidente. Essa medida também impede rollback, redeploy e outras operações documentadas. O plano de recuperação tem de incluir a decisão sobre a suspensão, os responsáveis e o momento de voltar a limitar alterações, se necessário. Não assumes que a palavra «emergência» cria uma exceção automática. O controlo usado para travar a propagação do problema pode também bloquear o mecanismo escolhido para recuperar. No cenário guiado, o responsável de produção apresenta um target com configuração nova, uma release antiga e uma pipeline suspensa. Pede ao formando uma sequência de análise: identificar a release pretendida, rever a divergência, obter a decisão operacional sobre a suspensão e acompanhar o novo rollout. Não forneças uma receita universal de comandos para ultrapassar controlos. A ordem concreta depende da autorização e do desenho do serviço. O resultado esperado é uma decisão explicada, com evidência do que será executado e um ponto claro para parar se o destino não corresponder ao aprovado.
Preservar os artefactos que o rollback exige
O serviço pode ter um procedimento de rollback correto e, ainda assim, perder a imagem necessária por uma política de limpeza. O inventário de recuperação deve ligar cada release ao artefacto que precisa de continuar disponível. Num repositório Artifact Registry, uma keep policy que corresponde à mesma versão que uma delete policy preserva essa versão. A ordem visual das regras não deve ser usada para concluir o contrário. O ensaio de cleanup também precisa de evidência própria. Configurar dry run e não observar eventos imediatamente não demonstra que a política seja inofensiva. O processamento é periódico e os logs de Data Write do serviço precisam de estar configurados para analisar os resultados. Antes de ativar eliminações, verifica os candidatos previstos, os artefactos de recuperação e a identidade do repositório. Mantém a decisão de remover imagens separada da decisão de libertar espaço num prazo prometido. Se o repositório tiver immutable tags, os artefactos tagged não podem ser eliminados nessa configuração. Uma estimativa de capacidade que os conta como removíveis pode, por isso, ser incorreta. Para o exercício, apresenta três versões: uma abrangida apenas por delete, outra também por keep e uma tagged sob a restrição de imutabilidade. Pede o resultado esperado e a evidência necessária para o confirmar. Depois pergunta quais dessas versões suportam o plano de rollback. Conservar uma imagem não prova que esteja aprovada, compatível ou segura para voltar a produção; demonstra apenas uma das dependências de recuperação.
Interpretar a manutenção por etapa e por VM
Um patch job termina a sua janela às duas da manhã, mas a equipa tem de planear o acompanhamento de processos que podem continuar, incluindo downloads e reinícios. A janela não deve ser apresentada ao negócio como garantia de interrupção instantânea de toda a atividade. Define quem acompanha cada VM ainda em transição e como será comunicada a diferença entre «não iniciar trabalho novo» e «trabalho anterior concluído». Os scripts pre-patch e post-patch permitem preparar e validar a aplicação. Quando é necessário reiniciar antes de começar o patching, o pre-patch script corre antes desse reinício. Um passo que drena ligações deve, portanto, ser analisado nesse ponto do ciclo. Para scripts num objeto Cloud Storage, a referência inclui a geração do objeto, permitindo fixar a versão usada pelo job. Um nome de ficheiro isolado não demonstra que o código executado seja o mesmo que foi revisto. A interpretação do resultado também depende do contrato. ExecStepConfig permite allowedSuccessCodes; se a lista aprovada incluir zero e doze, um resultado doze não é falha apenas por ser não zero. Contudo, aceitar esse código não demonstra sucesso do job inteiro nem saúde funcional. No exercício, o script comunica «drenagem já concluída» através de um código aceite. O formando deve manter separados o sucesso desse passo, a instalação dos patches, os reinícios e o teste da aplicação. Alargar a lista para esconder um erro real não é uma correção do problema operacional.
Distinguir silêncio de observabilidade recuperada
Depois do rollback, o dashboard deixa de apresentar alertas. Antes de interpretar esse silêncio, confirma se existem medições atuais e como a condição trata dados ausentes. Numa condição MetricThreshold com duração não zero, EVALUATION_MISSING_DATA_INACTIVE pode fazer deixar de satisfazer a condição quando os dados param. Isso não constitui uma medição saudável. O responsável de produção deve pedir uma observação funcional e uma observação do percurso de telemetria. Uma metric-absence policy também não resolve automaticamente todos os arranques sem dados. O comportamento exige uma medição bem-sucedida antes da ausência monitorizada; um emissor que nunca produziu qualquer ponto pode não satisfazer a condição esperada. Inclui no ensaio a confirmação de que a série existe e de que os labels correspondem ao recurso recuperado. Criar a definição de uma métrica ou o nome de um dashboard não produz essa evidência. As janelas temporais podem ainda explicar um resultado inesperado. A janela de reteste de uma condição de limiar reinicia quando uma medição alinhada deixa de violar. Três minutos de violação, uma medição normal e mais dois minutos não constituem cinco minutos contínuos. Ao reativar uma política antes desativada, a avaliação pode usar a janela recente, incluindo dados anteriores à reativação. Regista alinhamento, duração e momento de reativação ao investigar um alerta aparentemente imediato. O objetivo é explicar o sinal efetivo, evitando alterar o limiar apenas para fazer o painel ficar verde.
Limitar supressões e correlacionar o recurso certo
Durante uma manutenção, um snooze da política inteira pode fechar os alertas abertos e impedir novos alertas ou notificações enquanto estiver ativo. O fecho administrativo não resolve a aplicação. O plano deve registar o âmbito e o fim da supressão, a observação alternativa durante a janela e quem confirma o retorno ao funcionamento esperado. Não uses a contagem de alertas fechados como medida de incidentes resolvidos sem identificar a causa de encerramento. Quando a intenção é limitar o snooze à instância de recuperação, verifica a capacidade efetivamente suportada. Na API, um snooze com filtro aplica-se a uma política e combina múltiplos labels através de AND. Um nome descritivo não restringe o âmbito. O formando deve comparar a seleção autorizada com o conjunto selecionado pelo filtro antes de aceitar a mudança. Uma regra demasiado ampla pode retirar visibilidade a recursos que não estão em manutenção. A correlação entre condições também exige atenção. AND pode combinar condições satisfeitas em recursos diferentes. Se o requisito for observar as duas condições na mesma VM, avalia AND_WITH_MATCHING_RESOURCE e conserva os labels de recurso necessários após agregação. No exemplo, a VM de reconciliação tem atraso e a VM de distribuição tem falhas de autenticação; juntar estes factos não demonstra ambas as falhas numa única VM. Desenha uma matriz por recurso e condição antes de escolher o combinador. No handover, confirma ainda a receção do sinal pelo destinatário acordado: uma condição corretamente calculada não prova, sozinha, entrega da notificação.
Preservar logs com resultado e cobertura explícitos
A investigação precisa de preservar uma janela de logs antes de perder a retenção na origem. A operação de cópia retroativa do Cloud Logging pode encaminhar entradas existentes para Cloud Storage, mas não recupera entradas cuja retenção já expirou. Identifica o bucket de origem, a localização, o filtro temporal, o destino e a razão pela qual essa janela cobre o incidente. A existência de um destino vazio não demonstra preservação de evidência. O pedido gera uma operação que deve ser acompanhada até ao resultado. Um estado terminal com error não equivale a sucesso. Se a escrita falhar, reconcilia o conteúdo disponível no destino e as lacunas antes de declarar o arquivo completo. A contagem copiada, o âmbito do filtro e a leitura dos objetos resultantes respondem a perguntas diferentes. Uma operação bem-sucedida para um filtro errado continua a ser insuficiente para o âmbito da investigação. Cancelar também não desfaz a cópia anterior. Os dados já copiados permanecem e processos em curso podem terminar antes de o cancelamento ficar concluído. No exercício, uma equipa cancela por ter escolhido o intervalo errado. Pede ao formando que proponha uma reconciliação: identificar os objetos produzidos, separar o âmbito correto do incorreto e definir a próxima operação autorizada. Evita apagar automaticamente o destino para o fazer parecer vazio. Preservação, acesso, retenção e eventual remoção precisam de decisões próprias. O relatório deve permitir que outro analista reconstrua o que foi pedido, o que terminou e o que ainda não está demonstrado.
Exercício de evidência recente para o handover
Executa python3 run.py sem credenciais. O programa usa segundos inteiros fictícios, recursos declarados e três verificações de exemplo: saúde, notificação e rollback. Não simula Cloud Monitoring. Para cada recurso e verificação, seleciona a observação mais recente, exige a revisão esperada e confirma que a observação ocorreu depois ou no momento da recuperação e dentro da idade máxima acordada. Uma falha recente substitui um sucesso antigo; não se escolhe o último resultado favorável. Se duas observações mais recentes tiverem o mesmo instante mas resultados ou revisões contraditórios, o exercício exige reconciliação. Uma observação de outro recurso não satisfaz a verificação em falta. A supressão usa um intervalo fechado no início e aberto no fim, definido apenas para o modelo: no instante end já não está ativa. O código distingue verificações satisfeitas de inventário completo e mantém falsas as afirmações de autenticidade, notificação real e autorização de handover. Antes de executar, prevê os resultados para revisão errada, evidência antiga, ausência de teste, resultado desconhecido e supressão ativa. Depois compara reasons e evidenceIds com a previsão. Os testes verificam limites temporais, combinações de resultados, ordem de entrada e rejeição de campos inválidos. A consistência dos dados fornecidos não prova que os testes reais ocorreram. Para terminar, escreve uma nota ao RUN com a revisão, o âmbito, a hora das observações, os responsáveis e as lacunas. Resolve o caso final explicando que evidência permitiria aceitar a entrega e qual condição ainda impede essa decisão.
"""Original offline handover worksheet, not a Cloud Monitoring emulator.
All timestamps are synthetic integer seconds. Freshness and required checks are
fictional acceptance rules, not vendor defaults. Evidence is not authenticated.
"""
from copy import deepcopy
from itertools import permutations, product
from hashlib import sha256
from pathlib import Path
import json
def label(x):
return isinstance(x,str) and bool(x.strip()) and x==x.strip()
def integer(x):
return type(x)is int and x>=0
def unique(xs):
return isinstance(xs,list) and bool(xs) and all(label(x)for x in xs) and len(xs)==len(set(xs))
def validate(m):
if not isinstance(m,dict) or set(m)!={'now','maxAge','inventoryComplete','resources','evidence','suppressions'}:
raise ValueError('Expected exact worksheet fields')
if not integer(m['now']) or not integer(m['maxAge']) or type(m['inventoryComplete'])is not bool:
raise ValueError('Integer clock, age and boolean inventory flag required')
if not isinstance(m['resources'],list) or not m['resources'] or not isinstance(m['evidence'],list) or not isinstance(m['suppressions'],list):
raise ValueError('Lists and nonempty resource scope required')
resources={}
for r in m['resources']:
if not isinstance(r,dict)or set(r)!={'id','revision','recoveryAt','checks'}:
raise ValueError('Expected exact resource fields')
if not label(r['id'])or r['id']in resources or not label(r['revision'])or not unique(r['checks'])or not integer(r['recoveryAt'])or r['recoveryAt']>m['now']:
raise ValueError('Invalid resource scope')
resources[r['id']]=r
ids=set()
for e in m['evidence']:
if not isinstance(e,dict)or set(e)!={'id','resource','revision','check','observedAt','result'}:
raise ValueError('Expected exact evidence fields')
if not label(e['id'])or e['id']in ids or not label(e['revision'])or not label(e['resource'])or not label(e['check'])or e['resource']not in resources or e['check']not in resources[e['resource']]['checks']:
raise ValueError('Invalid or duplicate evidence identity')
if not integer(e['observedAt'])or e['observedAt']>m['now']or e['result']not in ['pass','fail','unknown']:
raise ValueError('Invalid evidence observation')
ids.add(e['id'])
ids=set()
for s in m['suppressions']:
if not isinstance(s,dict)or set(s)!={'id','resource','check','start','end'}:
raise ValueError('Expected exact suppression fields')
if not label(s['id'])or s['id']in ids or not label(s['resource'])or not label(s['check'])or s['resource']not in resources or s['check']not in resources[s['resource']]['checks']:
raise ValueError('Invalid suppression scope')
if not integer(s['start'])or not integer(s['end'])or s['start']>=s['end']:
raise ValueError('Invalid half-open suppression interval')
ids.add(s['id'])
return resources
def analyze(m):
resources=validate(m);out=[]
for id,r in sorted(resources.items()):
checks=[]
for check in sorted(r['checks']):
events=[e for e in m['evidence']if e['resource']==id and e['check']==check]
latest=max((e['observedAt']for e in events),default=None)
selected=[e for e in events if e['observedAt']==latest]
reasons=[]
if not selected:reasons.append('missing')
else:
# No arbitrary tie-break: conflicting newest observations need reconciliation.
if len({(e['revision'],e['result'])for e in selected})>1:reasons.append('conflicting-latest')
if any(e['revision']!=r['revision']for e in selected):reasons.append('revision-mismatch')
if latest<r['recoveryAt']:reasons.append('before-recovery')
if m['now']-latest>m['maxAge']:reasons.append('stale')
if any(e['result']=='fail'for e in selected):reasons.append('failed')
if any(e['result']=='unknown'for e in selected):reasons.append('unknown')
suppressed=sorted(s['id']for s in m['suppressions']if s['resource']==id and s['check']==check and s['start']<=m['now']<s['end'])
if suppressed:reasons.append('suppressed')
checks.append({'check':check,'latestObservedAt':latest,'evidenceIds':sorted(e['id']for e in selected),
'suppressionIds':suppressed,'reasons':sorted(reasons),'meetsDeclaredCheck':not reasons})
consistent=all(c['meetsDeclaredCheck']for c in checks)
out.append({'resource':id,'revision':r['revision'],'checks':checks,'allDeclaredChecksMet':consistent,
'inventoryCoverageUnproven':not m['inventoryComplete'],
'supportedUnderDeclaredEvidence':consistent and m['inventoryComplete'],
'evidenceAuthenticityVerified':False,'cloudPolicyBehaviorSimulated':False,
'realNotificationDelivered':False,'productionHandoverAuthorized':False})
return out
def model():
return {'now':1000,'maxAge':100,'inventoryComplete':True,
'resources':[{'id':'funds','revision':'r8','recoveryAt':850,'checks':['health','notification','rollback']}],
'evidence':[{'id':'e'+str(i),'resource':'funds','revision':'r8','check':c,'observedAt':950,'result':'pass'}for i,c in enumerate(['health','notification','rollback'])],
'suppressions':[]}
def evidence():
fixtures=[]
def record(id,m):
old=deepcopy(m);r=analyze(m);assert old==m
fixtures.append({'id':id,'results':r});return r
def reasons(r,check='health',resource=0):return next(c['reasons']for c in r[resource]['checks']if c['check']==check)
assert record('current-complete-evidence',model())[0]['supportedUnderDeclaredEvidence']
m=model();m['evidence'].pop(0);assert reasons(record('missing-check',m))==['missing']
m=model();m['evidence'][0]['observedAt']=899;assert reasons(record('stale-observation',m))==['stale']
m['evidence'][0]['observedAt']=900;assert record('exact-freshness-boundary',m)[0]['supportedUnderDeclaredEvidence']
m=model();m['maxAge']=200;m['evidence'][0]['observedAt']=849;assert reasons(record('before-recovery',m))==['before-recovery']
m['evidence'][0]['observedAt']=850;assert record('at-recovery-boundary',m)[0]['supportedUnderDeclaredEvidence']
m=model();e=deepcopy(m['evidence'][0]);e.update(id='new',observedAt=960,result='fail');m['evidence'].append(e);assert reasons(record('new-failure-supersedes-pass',m))==['failed']
m=model();m['evidence'][0]['result']='unknown';assert reasons(record('unknown-result',m))==['unknown']
m=model();m['evidence'][0]['revision']='r7';assert reasons(record('wrong-revision',m))==['revision-mismatch']
m=model();e=deepcopy(m['evidence'][0]);e.update(id='conflict',result='fail');m['evidence'].append(e);assert reasons(record('conflicting-latest',m))==['conflicting-latest','failed']
m=model();e=deepcopy(m['evidence'][0]);e['id']='corroborating';m['evidence'].append(e);assert record('consistent-latest-observations',m)[0]['supportedUnderDeclaredEvidence']
m=model();m['suppressions']=[dict(id='maintenance',resource='funds',check='notification',start=990,end=1010)];assert reasons(record('active-suppression',m),'notification')==['suppressed']
m['suppressions'][0]['end']=1000;assert record('at-suppression-end',m)[0]['supportedUnderDeclaredEvidence']
m=model();m['resources'].append(dict(id='payments',revision='r8',recoveryAt=850,checks=['health']));r=record('cannot-borrow-another-resource',m);assert r[0]['supportedUnderDeclaredEvidence']and not r[1]['supportedUnderDeclaredEvidence']
m=model();m['inventoryComplete']=False;r=record('incomplete-inventory',m)[0];assert r['allDeclaredChecksMet']and not r['supportedUnderDeclaredEvidence']
m=model();e=deepcopy(m['evidence'][0]);e.update(id='late-old-revision',revision='r7',observedAt=960);m['evidence'].append(e);assert reasons(record('newer-wrong-revision',m))==['revision-mismatch']
combinations=0
for results in product(['pass','fail','unknown'],repeat=3):
m=model()
for e,r in zip(m['evidence'],results):e['result']=r
assert analyze(m)[0]['supportedUnderDeclaredEvidence']==all(r=='pass'for r in results);combinations+=1
timing=0
for at,age,suppressed in product([899,900,1000],[0,100],[False,True]):
m=model();m['maxAge']=age
for e in m['evidence']:e['observedAt']=at
if suppressed:m['suppressions']=[dict(id='maint',resource='funds',check='notification',start=999,end=1001)]
assert analyze(m)[0]['supportedUnderDeclaredEvidence']==(1000-at<=age and not suppressed);timing+=1
m=model();expected=analyze(m);orders=0
for order in permutations(m['evidence']):
v=deepcopy(m);v['evidence']=list(order);assert analyze(v)==expected;orders+=1
bad=[]
def altered(fn):
m=model();fn(m);bad.append(m)
altered(lambda m:m.update(now=True))
altered(lambda m:m.update(maxAge=-1))
altered(lambda m:m.update(inventoryComplete='yes'))
altered(lambda m:m.update(resources=[]))
altered(lambda m:m['resources'].append(deepcopy(m['resources'][0])))
altered(lambda m:m['resources'][0].update(checks=[]))
altered(lambda m:m['resources'][0].update(checks=['health','health']))
altered(lambda m:m['resources'][0].update(recoveryAt=1001))
altered(lambda m:m['resources'][0].update(revision=''))
altered(lambda m:m['evidence'].append(deepcopy(m['evidence'][0])))
altered(lambda m:m['evidence'][0].update(observedAt=1001))
altered(lambda m:m['evidence'][0].update(observedAt=-1))
altered(lambda m:m['evidence'][0].update(result='green'))
altered(lambda m:m['evidence'][0].update(resource='unknown'))
altered(lambda m:m['evidence'][0].update(resource=[]))
altered(lambda m:m['evidence'][0].update(check={}))
altered(lambda m:m['evidence'][0].update(check='unknown'))
altered(lambda m:m['evidence'][0].update(extra=True))
altered(lambda m:m.update(extra=True))
for start,end,resource,check in [(1000,1000,'funds','health'),(1001,1000,'funds','health'),(0,2,'unknown','health'),(0,2,'funds','unknown')]:
m=model();m['suppressions']=[dict(id='s',resource=resource,check=check,start=start,end=end)];bad.append(m)
bad.extend([None,[]])
for m in bad:
try:analyze(m)
except ValueError:pass
else:raise AssertionError('Invalid worksheet accepted')
return dict(scriptSha256=sha256(Path(__file__).read_bytes()).hexdigest(),fixtures=fixtures,
resultCombinations=combinations,timingCombinations=timing,inputPermutations=orders,invalidInputs=len(bad),
inputPreserved=True,orderIndependent=True,network=False,cloudExecuted=False,persistentWrites=False)
if __name__=='__main__':print(json.dumps(evidence(),ensure_ascii=False,indent=2))
O rollout atual existe, mas o teste pertence à revisão anterior e o snooze fechou os alertas. A entrega ao RUN continua sem evidência suficiente.
Armadilhas comuns
Contar alertas fechados como incidentes resolvidos; escolher o último teste favorável; assumir que suspensão permite rollback; confundir operação terminada com sucesso.
Tópicos relacionados: Recuperar dados, chaves e controlos de IA · Failover com capacidade e confiança preservadas · Automação segura e deteção operacional
A entrega ao RUN precisa de observações atuais da revisão e do recurso corretos, incluindo a capacidade de detetar e comunicar a próxima falha.
Referência: Roll back a target · Current linked guide; edition date unconfirmed (2026-09-30 inspection)