1. Definir o que a mudança tem de demonstrar
Uma aplicação fictícia de valorização de fundos vai mudar de segmento de rede. O batch precisa de consultar preços em TCP/8443, a equipa de administração precisa de um percurso separado e uma origem não autorizada deve continuar bloqueada. Este cenário não descreve procedimentos internos do BNP Paribas. O exercício começa antes da ferramenta: escreve a origem, o destino, o protocolo, a porta, a revisão da mudança e o resultado esperado para cada percurso. “A rede funciona” é demasiado amplo para orientar uma decisão. Distingue três perguntas. A configuração prevê um percurso compatível? Uma observação recente mostra entrega nas condições testadas? A aplicação conclui a operação necessária dentro do prazo? Guarda respostas separadas porque uma delas pode ser favorável enquanto outra falha. Por exemplo, a API pode não ter um processo em escuta mesmo quando as regras de rede permitem a ligação. A ausência de uma observação deve continuar visível como lacuna, com responsável e próximo passo. No comité, o PM apresenta o impacto para o serviço, a evidência disponível e o tempo necessário para resolver as lacunas. APS confirma os fluxos essenciais; rede identifica os controlos; desenvolvimento verifica a operação funcional. Define também um teste negativo: uma origem não autorizada tenta o mesmo destino e deve ser bloqueada. A aceitação exige verificar o comportamento pretendido, incluindo restrições, e não maximizar a quantidade de ligações permitidas. Antes de avançar, explica que falha concreta cada teste conseguiria detetar.
2. Usar diagnósticos dentro do seu âmbito
Connectivity Tests ajuda a investigar o percurso previsto pela configuração. Em cenários suportados pode acrescentar sondas no plano de dados. O relatório deve identificar qual destes mecanismos produziu cada resultado. Uma previsão favorável não é uma transação concluída pela aplicação. Quando a ferramenta apresenta um aviso sobre uma funcionalidade não suportada, preserva esse aviso na análise e atribui uma verificação alternativa. O espaço em branco não é um controlo aprovado. Considera uma API atrás de um external Application Load Balancer. O percurso até ao IP pode parecer compatível enquanto Cloud Armor bloqueia o pedido HTTP. A análise desse percurso não avalia a política Cloud Armor; a investigação precisa de observar a decisão dessa camada. Se houver regras de firewall com objetos FQDN potencialmente relevantes, o aviso de falta de suporte também impede declarar cobertura integral dessas regras. Estes limites orientam a recolha de evidência, sem demonstrar que os controlos estão desativados. Num segundo exemplo, todas as sondas enviadas a um balanceador têm êxito. Isso não identifica automaticamente todos os backends visitados. Faz uma lista dos destinos que importam ao serviço e compara-a com a cobertura efetivamente observada. Se faltar a relação entre sondas e destinos, declara essa limitação. Ao testar um IP externo à Google, também não extrapoles a medição até ao processamento do parceiro. O relatório deve dizer onde termina a observação e quem confirma o passo seguinte do fluxo.
3. Investigar divergências e o percurso de retorno
Uma divergência entre configuração e observação é informação útil. Começa por confirmar que os resultados se referem ao mesmo fluxo e à mesma revisão. Uma alteração recente pode tornar incomparáveis dois relatórios recolhidos em momentos diferentes. No ticket, regista a hora, o identificador da mudança, os endpoints e a direção do teste. Preserva também os erros de permissão ou os argumentos não suportados que impediram completar a análise. Se a previsão indica entrega mas a aplicação falha, verifica o processo em escuta e o firewall do sistema operativo. Não comeces por abrir todas as portas: essa alteração pode aumentar a exposição sem resolver um processo parado. Se as sondas num sentido têm êxito mas as respostas falham, investiga também o sentido inverso. Uma análise unidirecional não cobre automaticamente o retorno. Identifica quem pode observar cada lado e correlaciona a tentativa cliente com o processamento e a resposta no destino. No caso fictício, APS recolhe três tentativas novas falhadas após a mudança. Rede tem um teste anterior favorável e desenvolvimento confirma que uma sessão antiga continua útil. O PM não escolhe o resultado mais conveniente. Mantém os três factos, verifica se pertencem à mesma revisão e pede uma tentativa nova com observação coordenada. Se o tempo até ao rollback se esgotar sem demonstrar o fluxo necessário, usa o critério de decisão previamente acordado. O prazo da janela limita a investigação; não transforma uma lacuna em evidência favorável.
4. Separar ligações existentes de novas ligações
O firewall VPC mantém estado das ligações permitidas. O tráfego de resposta que corresponde a esse estado não deve ser interpretado como uma nova ligação independente. Isto é relevante ao desenhar uma mudança e ao interpretar testes: uma sessão persistente pode continuar a funcionar sem demonstrar que a aplicação consegue abrir outra sessão nas condições atuais. Especifica se o teste usa uma ligação nova ou reutiliza um pool existente. Para rever duas regras VPC aplicáveis a uma ligação nova, considera a prioridade antes da especificidade aparente do alvo. Uma allow de prioridade 420 não é anulada só porque uma deny de prioridade 900 nomeia um alvo mais restrito. Num empate entre allow e deny aplicáveis, deny prevalece. Não transportes estas regras de comparação local para uma lista global que misture políticas hierárquicas sem respeitar a ordem de avaliação. No exercício de revisão, escreve duas colunas: comportamento da sessão existente e comportamento de uma tentativa nova. Define antecipadamente qual é necessário após restart do serviço. Se o critério exige reconexão, uma resposta pelo pool antigo não o satisfaz. Guarda também um resultado negativo esperado para uma origem fora do âmbito autorizado. Quando apresentares o teste, identifica o contexto que gerou a observação e evita dizer apenas “porta aberta”. Essa expressão esconde quem tentou, em que direção, com que estado anterior e para que finalidade. Uma boa evidência permite repetir a tentativa sem depender da memória do operador.
5. Rever associações, origens e observabilidade
Uma hierarchical firewall policy criada ainda precisa da associação que determina onde se aplica. Na revisão, mostra a organização ou pasta associada e a hierarquia dos recursos abrangidos. Distingue uma decisão allow ou deny num nível superior da delegação com goto_next. Um controlo inferior não anula uma decisão terminal superior apenas por ter um número de prioridade mais pequeno. Esta distinção evita propor uma correção local incapaz de produzir o efeito pretendido. Nas regras VPC, verifica também o conjunto real de origens. Combinar um range com uma source network tag pode parecer uma exigência simultânea, mas a combinação válida forma uma união. Um exercício útil é enumerar três origens fictícias: uma que pertence ao range, outra identificada pela tag e outra sem nenhuma condição. Explica quais correspondem antes de aceitar o desenho. Se o serviço acrescentar IPv6, revê essa família explicitamente; a cobertura IPv4 não demonstra o novo percurso. A observabilidade exige uma revisão própria. Ativar logging numa ligação continuamente ativa não obriga a surgir um novo registo nesse instante. Além disso, as regras implícitas referidas na documentação não oferecem o mesmo suporte de logging que uma regra explícita configurada para o efeito. Num relatório sem registos, confirma a regra, o protocolo, a hora de ativação e se houve uma tentativa nova. Ausência de log, ausência de tráfego e ausência de bloqueio são afirmações diferentes que precisam de evidência adequada.
6. Diagnosticar Private Service Connect com o produtor
Num serviço publicado por PSC, o estado Accepted confirma aceitação da configuração, mas não garante entrega útil. Mantém o teste funcional na passagem a produção. Needs attention aponta para um problema do lado produtor e pode coexistir com tráfego parcial; uma subnet NAT esgotada é uma hipótese documentada. Closed após eliminação do attachment é terminal e exige um plano de recriação dos recursos envolvidos. Não confundas recuperação de ciclo de vida com uma simples alteração da accept list. Durante um incidente, identifica o endpoint e a direção do contador. dropped_sent_packets_count está associado ao máximo de ligações do endpoint. dropped_received_packets_count indica respostas para as quais PSC não encontra ligação correspondente. Respostas tardias e timeouts ajudam a investigar este segundo caso, mas um contador isolado não atribui automaticamente uma causa única à aplicação. Compara a mesma janela temporal no consumidor e no produtor e conserva exemplos de pedidos úteis e falhados. No serviço fictício de preços, o PM reúne APS e o produtor com uma pergunta concreta: os pedidos que expiram correspondem às respostas tardias e aos descartes de receção? Define quem recolhe cada evidência e quando se decide a mitigação. Verifica ainda dependências de configuração quando relevantes: global access requer compatibilidade do serviço publicado, e um conflito de zona DNS pode impedir a criação automática esperada. Antes de eliminar uma zona existente, identifica os consumidores que dependem dela. Corrigir um conflito de criação não justifica interromper outro serviço sem análise.
7. Exercício: construir uma matriz de evidência
O programa desta aula é uma folha de trabalho local, não um simulador do Google Cloud. Recebe requisitos declarados com origem, destino, protocolo e porta de destino. As observações incluem revisão, minuto, tipo configuration ou probe e resultado reachable, blocked ou unknown. Para sondas, o autor declara ainda se a ligação é new ou reused. O programa não verifica esses dados no exterior: a validade das declarações permanece uma responsabilidade de quem recolhe a evidência. Executa python3 run.py e observa os casos incluídos. No instante 100, com maxAge=10, uma observação do minuto 90 está na janela e uma do minuto 89 fica excluída. Uma observação de r6 não demonstra a revisão r7. As exclusões são listadas com motivo, sem desaparecerem silenciosamente. Resultados configuration são apresentados separadamente das sondas de ligações novas. Uma sessão reused permanece identificada, mas não comprova reconexão. Antes de alterar o código, prevê três resultados. Primeiro, a configuração indica reachable e a nova sonda indica blocked quando se esperava reachable: a falha prevalece. Segundo, uma sonda tem êxito e outra é unknown: a cobertura continua não demonstrada. Terceiro, um requisito espera blocked e a sonda confirma blocked: o resultado observado corresponde à restrição pretendida. Acrescenta depois um segundo fluxo obrigatório sem evidência. Mesmo que o primeiro esteja demonstrado, o conjunto declarado não fica completo. Explica a diferença entre resultado favorável de uma linha e cobertura da matriz inteira.
8. Apresentar uma decisão que possa ser revista
O resultado observed-as-expected descreve apenas as observações fornecidas para um requisito. Mesmo quando todas as linhas têm esse resultado, a aplicação pode continuar por testar e o inventário pode estar incompleto. O programa conserva inventoryCoverageUnproven separadamente. Não envia pacotes, não avalia regras reais e não autoriza produção. Os identificadores de origem e destino são labels de exercício; não representam validação de recursos, identidades ou todas as propriedades de uma ligação real. Para concluir o caso, escreve uma nota curta em inglês com quatro elementos: requisito, observação, limitação e ação. Por exemplo, identifica o fluxo de preços e a revisão, descreve a falha das novas ligações, indica que a sessão antiga não demonstra reconexão e atribui uma investigação conjunta antes do prazo de rollback. Acrescenta quem decide a passagem a produção e a condição concreta que falta satisfazer. Um destinatário que não participou na chamada deve conseguir distinguir um facto observado de uma hipótese. Revê também as alternativas rejeitadas. Abrir indiscriminadamente o acesso não demonstra causa; ocultar um resultado divergente reduz a qualidade da decisão; repetir um teste fora do âmbito necessário não preenche a lacuna. Se a recuperação for escolhida, regista a revisão resultante e repete os testes aplicáveis. A evidência da implementação falhada não substitui a confirmação da recuperação. O objetivo final é uma decisão rastreável com limites explícitos, responsabilidades e uma forma concreta de confirmar o serviço necessário.
"""Original offline worksheet. Inputs are declarations, not verified cloud evidence."""
import copy
import hashlib
import itertools
import json
from pathlib import Path
def require(condition, message):
if not condition:
raise ValueError(message)
def integer(value, minimum=0):
return type(value) is int and value >= minimum
def nonempty(value):
return isinstance(value, str) and bool(value.strip())
def flow(row):
require(isinstance(row, dict), 'record must be an object')
require(all(nonempty(row.get(k)) for k in ('source', 'destination')), 'flow endpoints')
require(row.get('protocol') in ('TCP', 'UDP'), 'protocol')
require(integer(row.get('destinationPort'), 1) and row['destinationPort'] <= 65535, 'port')
return tuple(row[k] for k in ('source', 'destination', 'protocol', 'destinationPort'))
def assess(data):
require(isinstance(data, dict), 'input object')
require(nonempty(data.get('revision')), 'revision')
require(integer(data.get('asOf')) and integer(data.get('maxAge')), 'integer minutes')
require(type(data.get('inventoryComplete')) is bool, 'inventory flag')
requirements, evidence = data.get('requirements'), data.get('evidence')
require(isinstance(requirements, list) and len(requirements) > 0, 'nonempty requirements')
require(isinstance(evidence, list), 'evidence list')
ids, flows = set(), set()
for row in requirements:
key = flow(row)
require(nonempty(row.get('id')) and row['id'] not in ids, 'unique requirement id')
require(key not in flows, 'unique requirement flow')
require(row.get('expected') in ('reachable', 'blocked'), 'expected result')
ids.add(row['id']); flows.add(key)
ids = set()
for row in evidence:
flow(row)
require(nonempty(row.get('id')) and row['id'] not in ids, 'unique evidence id')
ids.add(row['id'])
require(nonempty(row.get('revision')), 'evidence revision')
require(integer(row.get('checkedAt')) and row['checkedAt'] <= data['asOf'], 'observation time')
require(row.get('kind') in ('configuration', 'probe'), 'evidence kind')
require(row.get('outcome') in ('reachable', 'blocked', 'unknown'), 'outcome')
if row['kind'] == 'probe':
require(row.get('connection') in ('new', 'reused'), 'connection kind')
result, excluded = [], []
usable = []
for row in evidence:
reasons = []
if flow(row) not in flows: reasons.append('other-flow')
if row['revision'] != data['revision']: reasons.append('other-revision')
if row['checkedAt'] < data['asOf'] - data['maxAge']: reasons.append('stale')
if reasons: excluded.append({'id': row['id'], 'reasons': reasons})
else: usable.append(row)
for requirement in sorted(requirements, key=lambda r: r['id']):
matching = [r for r in usable if flow(r) == flow(requirement)]
config = sorted({r['outcome'] for r in matching if r['kind'] == 'configuration'})
fresh = [r for r in matching if r['kind'] == 'probe' and r['connection'] == 'new']
observed = sorted({r['outcome'] for r in fresh})
unexpected = any(x not in (requirement['expected'], 'unknown') for x in observed)
if unexpected: status = 'observed-unexpected'
elif observed == [requirement['expected']]: status = 'observed-as-expected'
else: status = 'not-demonstrated'
result.append({'id': requirement['id'], 'expected': requirement['expected'],
'configurationStates': config, 'newProbeStates': observed,
'newProbeIds': sorted(r['id'] for r in fresh),
'reusedProbeIds': sorted(r['id'] for r in matching if r['kind'] == 'probe' and r['connection'] == 'reused'),
'status': status, 'conflictingNewProbes': 'reachable' in observed and 'blocked' in observed})
return {'requirements': result, 'excluded': sorted(excluded, key=lambda r: r['id']),
'allDeclaredFlowsObservedAsExpected': all(r['status'] == 'observed-as-expected' for r in result),
'inventoryCoverageUnproven': not data['inventoryComplete'],
'actualNetworkTested': False, 'applicationHealthVerified': False,
'productionAcceptanceAuthorized': False}
def sample():
f = dict(source='batch-a', destination='pricing', protocol='TCP', destinationPort=8443)
return dict(revision='r7', asOf=100, maxAge=10, inventoryComplete=True,
requirements=[dict(f, id='prices', expected='reachable')],
evidence=[dict(f, id='p1', revision='r7', checkedAt=95, kind='probe', connection='new', outcome='reachable')])
def checks():
fixtures = []
def check(name, data, expected):
before = copy.deepcopy(data); actual = assess(data)
assert data == before
assert actual['requirements'][0]['status'] == expected, name
fixtures.append(dict(id=name, **actual))
return actual
base = sample()
check('current-success', base, 'observed-as-expected')
x=sample(); x['evidence'][0]['kind']='configuration'
check('configuration-only', x, 'not-demonstrated')
x=sample(); x['evidence'][0]['connection']='reused'
check('old-pool', x, 'not-demonstrated')
x=sample(); x['evidence'][0]['outcome']='blocked'
x['evidence'].append(dict(x['evidence'][0], id='c1', kind='configuration', outcome='reachable'))
check('prediction-cannot-hide-failure', x, 'observed-unexpected')
x=sample(); x['evidence'].append(dict(x['evidence'][0], id='p2', outcome='blocked'))
assert check('mixed-probes', x, 'observed-unexpected')['requirements'][0]['conflictingNewProbes']
x=sample(); x['evidence'].append(dict(x['evidence'][0], id='p2', outcome='unknown'))
check('unknown-plus-success', x, 'not-demonstrated')
x=sample(); x['evidence'][0]['checkedAt']=90
check('window-boundary', x, 'observed-as-expected')
x=sample(); x['evidence'][0]['checkedAt']=89
assert check('stale-result', x, 'not-demonstrated')['excluded'][0]['reasons']==['stale']
x=sample(); x['evidence'][0]['revision']='r6'
assert check('previous-revision', x, 'not-demonstrated')['excluded'][0]['reasons']==['other-revision']
x=sample(); x['evidence'][0]['destinationPort']=443
assert check('different-port', x, 'not-demonstrated')['excluded'][0]['reasons']==['other-flow']
x=sample(); x['inventoryComplete']=False
assert check('incomplete-inventory', x, 'observed-as-expected')['inventoryCoverageUnproven']
x=sample(); x['requirements'][0]['expected']='blocked'; x['evidence'][0]['outcome']='blocked'
check('negative-check', x, 'observed-as-expected')
x=sample(); x['requirements'].append(dict(x['requirements'][0], id='admin', destinationPort=22, expected='blocked'))
assert not check('missing-second-flow', x, 'not-demonstrated')['allDeclaredFlowsObservedAsExpected']
combinations=0
for expected,outcome,kind,connection,revision in itertools.product(('reachable','blocked'),('reachable','blocked','unknown'),('configuration','probe'),('new','reused'),('r7','r6')):
x=sample(); x['requirements'][0]['expected']=expected
x['evidence'][0].update(outcome=outcome,kind=kind,connection=connection,revision=revision)
target='not-demonstrated'
if kind=='probe' and connection=='new' and revision=='r7' and outcome!='unknown':
target='observed-as-expected' if outcome==expected else 'observed-unexpected'
assert assess(x)['requirements'][0]['status']==target
combinations+=1
x=sample(); x['evidence'] += [dict(x['evidence'][0],id='p2',outcome='blocked'),dict(x['evidence'][0],id='p3',revision='r6')]
reference=assess(x); permutations=0
for p in itertools.permutations(x['evidence']):
y=copy.deepcopy(x); y['evidence']=list(p); assert assess(y)==reference; permutations+=1
invalid=[]
for key,value in [('revision',''),('asOf',True),('maxAge',-1),('inventoryComplete','yes'),('requirements',[]),('evidence',None)]:
x=sample(); x[key]=value; invalid.append(x)
for key,value in [('destinationPort',True),('destinationPort',65536),('source',''),('expected','unknown')]:
x=sample();x['requirements'][0][key]=value;invalid.append(x)
for key,value in [('checkedAt',101),('checkedAt',1.5),('kind','live'),('connection','unknown'),('outcome','pass'),('protocol','ICMP'),('revision','')]:
x=sample();x['evidence'][0][key]=value;invalid.append(x)
x=sample();x['evidence'].append(dict(x['evidence'][0]));invalid.append(x)
x=sample();x['requirements'].append(dict(x['requirements'][0],id='duplicate-flow'));invalid.append(x)
x=sample();del x['evidence'][0]['checkedAt'];invalid.append(x)
for x in invalid:
try: assess(x)
except ValueError: pass
else: raise AssertionError('invalid input accepted')
return {'fixtures':fixtures,'stateCombinations':combinations,'inputPermutations':permutations,
'invalidInputs':len(invalid),'inputPreserved':True,'orderIndependent':True,
'network':False,'cloudExecuted':False,'persistentWrites':False,
'scriptSha256':hashlib.sha256(Path(__file__).read_bytes()).hexdigest()}
if __name__ == '__main__':
print(json.dumps(checks(), ensure_ascii=False, indent=2))
Numa migração fictícia, o pool antigo responde mas três ligações novas falham. A equipa conserva os resultados, verifica a revisão e investiga destino e retorno antes do go/no-go.
Armadilhas comuns
Tratar Accepted como saúde da aplicação, confundir sondas com cobertura integral, ignorar ligações reutilizadas ou converter ausência de logs em ausência de bloqueio.
Tópicos relacionados: Gestão de mudanças e rollback · Monitorização e observabilidade · Resiliência e passagem a produção
Define a afirmação que cada teste suporta, conserva as falhas e as lacunas e verifica os fluxos necessários na revisão que vai ser aceite.
Referência: Connectivity Tests overview · Current linked guide; edition date unconfirmed (2026-09-30 inspection)