Planear unidades de migração coerentes
Um inventário de servidores não é ainda um plano de migração. Para cada fluxo de negócio, identifica quem chama quem, versões compatíveis, dados partilhados e limites de latência. No exercício, Gateway e Writer usam um protocolo síncrono ligado à mesma versão; separá-los durante uma semana cria um estado intermédio que precisa de validação própria. Desenha o estado atual, o intermédio e o final. Em cada desenho, marca a transação, os caminhos de comunicação e quem confirma o resultado. Se o estado intermédio falha o requisito, agrupa os componentes numa unidade coerente ou demonstra uma interface temporária suportada antes de aprovar as vagas. Isto não exige migrar toda a aplicação de uma vez. Exige que cada fronteira escolhida tenha um contrato e evidência. Um servidor com poucos recursos pode transportar uma dependência crítica; o tamanho da VM, isoladamente, não mede o risco da sequência.
Calcular o último momento para validar
Uma janela precisa de reservar o caminho de falha, além do caminho de sucesso. Considera uma janela fictícia de 50 minutos, validação final de 10, rollback de 12 e margem acordada de 3. Sem sobreposição, o último início de validação é 50−10−12−3=25. O último início de rollback é 35, mas esse valor não serve para começar a validar. Ao minuto 32, restam 18 minutos: os 15 de rollback e margem cabem; os 25 de validação, rollback e margem já não cabem. Regista estes dois marcos no plano e ensaia a decisão com quem tem autoridade. Se existir uma extensão aprovada e dependências compatíveis, recalcula; a vontade de evitar custos não altera por si só os critérios. Tempos determinísticos facilitam o exercício. Num projeto real, mede as fases, considera variação e confirma que o rollback continua tecnicamente possível após cada passo.
Comparar custo total com pressupostos visíveis
Compara opções que cumprem os mesmos requisitos e usa o mesmo período. Num modelo mensal fictício, a opção autogerida custa 4200 unidades de infraestrutura e 60 horas de operação a 50 unidades por hora: total 7200. A opção gerida custa 5600 e 12 horas ao mesmo valor: total 6200. A diferença é 1000, apesar de a fatura de infraestrutura ser superior na opção gerida. Este resultado pertence ao modelo; não demonstra que serviços geridos são sempre mais baratos. Explicita custos considerados iguais e despesas ausentes, como migração ou formação. Valorizar horas libertadas também não prova uma redução imediata de pagamentos: pode representar capacidade disponível para outras tarefas. No relatório, distingue custo modelado, despesa observada e benefício esperado. Pede ao responsável operacional que valide as horas e a FINOPS que confirme a base financeira antes de usar a comparação numa decisão.
Imputar custos partilhados sem os duplicar
Uma regra de imputação deve ser explícita, compreensível e aplicada uma vez. Custody tem custo direto de 1200 e Analytics de 1800. O serviço partilhado custa 900; a regra interna do exercício atribui 40% a Custody e 60% a Analytics. Obténs 1560 e 2340, totalizando 3900. Atribuir os 900 completos a ambas duplica despesa; deixar o serviço fora esconde-a. Para valores com arredondamento, o laboratório trabalha em cêntimos inteiros: distribui as partes inteiras e depois os cêntimos restantes pelos maiores restos, usando ordem alfabética em empates. Essa regra foi escolhida para o exercício, não é uma norma contabilística nem uma regra Google. Confirma que a soma é conservada e documenta a convenção usada. Liga o relatório a responsáveis por decisões e ações; um dashboard correto não resolve sozinho quem pode alterar um serviço partilhado ou validar o impacto nos consumidores.
Separar lacunas de dados de ausência de custo
A exportação de billing para BigQuery tem intervalos de reporte variáveis e não oferece uma garantia de latência de entrega. Uma consulta vazia poucos minutos após o arranque não demonstra custo zero. Define um estado provisório para períodos incompletos e distingue a hora da consulta do período de consumo representado. Adicionar hoje uma label de centro de custo também não reescreve linhas já exportadas. Para classificar histórico, conserva a origem e acrescenta no reporting um mapeamento governado com datas de vigência, explicando alterações de responsabilidade. Não apagues linhas para provocar uma reconstrução: dados exportados eliminados não são repostos automaticamente. No exercício, escreve duas notas para a gestão: o que ainda não chegou e o que não será retroativamente alterado pela label. São problemas diferentes e pedem tratamentos diferentes; nenhum justifica apresentar uma estimativa como fecho financeiro confirmado.
Distinguir compromisso financeiro, quota e reserva
Um pico temporário durante dual run não equivale à procura estável após a migração. Antes de comprar um compromisso baseado em recursos, avalia a utilização elegível esperada: o compromisso não pode ser cancelado depois da compra e continua a ser faturado quando não é utilizado. Esta obrigação é diferente da capacidade de criar VMs. Quota suficiente define um limite de utilização, não garante inventário disponível numa zona. Uma reserva, por sua vez, exige correspondência das propriedades e da zona, além das condições de consumo aplicáveis. Se o desenho mudou o tipo de máquina, revê essa correspondência antes de depender da reserva no cutover. Prepara três linhas de evidência: obrigação financeira, limite autorizado e capacidade compatível. Evita usar a aprovação de uma delas como prova das outras. No trabalho de projeto, esta distinção ajuda a envolver FINOPS e infraestrutura nas decisões certas.
Medir recuperação até ao resultado de negócio
Recuperar VMs não demonstra que a equipa de serviço consegue operar. Inclui no ensaio os operadores reais, as permissões e o caminho alternativo aprovado. Depois desenha dependências temporais: acesso demora 4 minutos; dados demoram 18 e rede 10 após o acesso, em paralelo; a aplicação demora 7 após ambos; a validação de negócio demora 6. O serviço fica validado aos 35 minutos, não aos 18 da tarefa de dados nem aos 45 obtidos somando tudo em sequência. Se a rede passar a 25, o resultado muda para 42. Se os dados baixarem para 8, a rede torna-se limitante e o resultado é 27. Estes valores assumem recursos suficientes para o paralelismo e não incluem deteção ou esperas adicionais. No projeto real, acrescenta essas dependências e mede-as antes de comparar o resultado com o objetivo de recuperação acordado.
Praticar cálculos e decidir a saída de dual run
Antes de executar o código, prevê quatro resultados: último início de validação, custos imputados por equipa, diferença mensal e instante de validação do serviço. Guarda o exemplo como run.py e executa python3 run.py com Python 3.12 ou posterior. A saída inclui resultados e verificações locais. Altera um pressuposto de cada vez e explica que decisão muda; não confundas passar as verificações com provar tempos de uma migração real. O laboratório não contacta Google Cloud e usa apenas valores fictícios. Para fechar o caso, prepara uma decisão de saída de dual run com evidência dos fluxos relevantes, consumidores menos frequentes, tratamento dos dados, recuperação e custos residuais. Define responsável e momento da aceitação. A poupança prevista só se torna realizada quando os custos correspondentes cessam. Se falta uma dependência ou uma aprovação, regista a lacuna e a ação necessária em vez de declarar o projeto financeiramente concluído.
"""Original dr.pt exercise: fictional timings and costs, not a cloud simulator.
Run with Python 3.12+. No network, credentials or filesystem writes.
"""
import json
import platform
def whole(value):
if type(value) is not int or value < 0:
raise ValueError('Expected a nonnegative integer, not bool or float')
return value
def rollback_room(window, elapsed, validation, rollback, margin):
for value in (window, elapsed, validation, rollback, margin):
whole(value)
# Preserve both failure recovery and the agreed margin. No phase overlap.
return {
'latest_validation_start': window - validation - rollback - margin,
'latest_rollback_start': window - rollback - margin,
'can_validate_then_rollback': elapsed + validation + rollback + margin <= window,
'can_rollback': elapsed + rollback + margin <= window,
}
def allocate_shared(total_cents, weights):
whole(total_cents)
if not isinstance(weights, dict) or not weights:
raise ValueError('Nonempty named allocation weights required')
for name, weight in weights.items():
if not isinstance(name, str) or not name or whole(weight) == 0:
raise ValueError('Names and positive integer weights required')
denominator = sum(weights.values())
parts = {name: divmod(total_cents * weight, denominator)
for name, weight in weights.items()}
result = {name: quotient for name, (quotient, _) in parts.items()}
remainder = total_cents - sum(result.values())
# Fictional exercise rule: largest remainder, alphabetical tie break.
# This is not an accounting standard or Google billing behavior.
order = sorted(parts, key=lambda name: (-parts[name][1], name))
for name in order[:remainder]:
result[name] += 1
return result
def monthly_cost(infrastructure_cents, operating_hours, hourly_cents):
return whole(infrastructure_cents) + whole(operating_hours) * whole(hourly_cents)
def recovery_finish(durations, dependencies):
"""Earliest finishes for a small DAG with unlimited parallel capacity.
Dependencies consume no time; fixed durations exclude detection and waiting.
"""
if not isinstance(durations, dict) or not durations:
raise ValueError('Named tasks required')
if not isinstance(dependencies, dict) or set(dependencies) != set(durations):
raise ValueError('Every task needs an explicit dependency list')
for name, duration in durations.items():
if not isinstance(name, str) or not name:
raise ValueError('Task names must be nonempty strings')
whole(duration)
parents = dependencies[name]
if not isinstance(parents, list) or any(
not isinstance(p, str) or p not in durations for p in parents):
raise ValueError('Unknown dependency or invalid dependency list')
finished, visiting = {}, set()
def visit(name):
if name in visiting:
raise ValueError('Cyclic dependency')
if name not in finished:
visiting.add(name)
start = max((visit(p) for p in dependencies[name]), default=0)
finished[name] = start + durations[name]
visiting.remove(name)
return finished[name]
for name in durations:
visit(name)
return finished
def run():
names = []
def check(name, actual, expected):
if actual != expected:
raise AssertionError(f'{name}: {actual!r} != {expected!r}')
names.append(name)
def rejects(name, action):
try:
action()
except ValueError:
names.append(name)
else:
raise AssertionError(f'{name}: invalid model accepted')
room = rollback_room(50, 32, 10, 12, 3)
check('case05 preserves rollback but cannot start full validation', room,
{'latest_validation_start': 25, 'latest_rollback_start': 35,
'can_validate_then_rollback': False, 'can_rollback': True})
check('validation at minute25 fits exactly', rollback_room(50, 25, 10, 12, 3)['can_validate_then_rollback'], True)
check('validation at minute26 misses required margin', rollback_room(50, 26, 10, 12, 3)['can_validate_then_rollback'], False)
check('rollback at minute35 fits exactly', rollback_room(50, 35, 10, 12, 3)['can_rollback'], True)
check('rollback at minute36 misses required margin', rollback_room(50, 36, 10, 12, 3)['can_rollback'], False)
check('zero-duration model is feasible', rollback_room(0, 0, 0, 0, 0)['can_validate_then_rollback'], True)
check('infeasible initial plan has negative latest start', rollback_room(5, 0, 10, 12, 3)['latest_validation_start'], -20)
for value in (-1, 1.5, True):
rejects(f'timing rejects {value!r}', lambda value=value: rollback_room(50, value, 10, 12, 3))
weights = {'Custody': 40, 'Analytics': 60}
allocation = allocate_shared(90000, weights)
check('shared900 allocates360 and540', allocation, {'Custody': 36000, 'Analytics': 54000})
totals = {'Custody': 120000 + allocation['Custody'], 'Analytics': 180000 + allocation['Analytics']}
check('direct plus shared costs', totals, {'Custody': 156000, 'Analytics': 234000})
check('no shared-cost double counting', sum(totals.values()), 390000)
check('weights remain unchanged', weights, {'Custody': 40, 'Analytics': 60})
check('rounding ties use alphabetical names', allocate_shared(100, {'C': 1, 'B': 1, 'A': 1}), {'A': 34, 'B': 33, 'C': 33})
check('largest remainder precedes alphabet', allocate_shared(1, {'A': 1, 'B': 2}), {'A': 0, 'B': 1})
check('zero-cost allocation conserves zero', allocate_shared(0, weights), {'Custody': 0, 'Analytics': 0})
rejects('empty allocation rejected', lambda: allocate_shared(1, {}))
rejects('zero weight rejected', lambda: allocate_shared(1, {'A': 0}))
rejects('negative amount rejected', lambda: allocate_shared(-1, weights))
rejects('fractional weight rejected', lambda: allocate_shared(1, {'A': 0.5}))
rejects('boolean weight rejected', lambda: allocate_shared(1, {'A': True}))
costs = {'self_managed': monthly_cost(420000, 60, 5000), 'managed': monthly_cost(560000, 12, 5000)}
check('both options include operating effort', costs, {'self_managed': 720000, 'managed': 620000})
check('model saving is1000 units', costs['self_managed'] - costs['managed'], 100000)
check('zero operating hours keeps infrastructure', monthly_cost(420000, 0, 5000), 420000)
rejects('negative operating hours rejected', lambda: monthly_cost(1, -1, 1))
durations = {'access': 4, 'data': 18, 'network': 10, 'app': 7, 'business': 6}
deps = {'access': [], 'data': ['access'], 'network': ['access'], 'app': ['data', 'network'], 'business': ['app']}
finish = recovery_finish(durations, deps)
check('critical path includes application and business validation', finish,
{'access': 4, 'data': 22, 'network': 14, 'app': 29, 'business': 35})
check('network becomes critical at25minutes', recovery_finish({**durations, 'network': 25}, deps)['business'], 42)
check('faster data still waits for network', recovery_finish({**durations, 'data': 8}, deps)['business'], 27)
check('independent tasks finish in parallel', recovery_finish({'A': 2, 'B': 5}, {'A': [], 'B': []}), {'A': 2, 'B': 5})
check('zero-duration checkpoint preserves predecessor finish', recovery_finish({'A': 2, 'B': 0}, {'A': [], 'B': ['A']}), {'A': 2, 'B': 2})
rejects('cycle rejected', lambda: recovery_finish({'A': 1, 'B': 1}, {'A': ['B'], 'B': ['A']}))
rejects('unknown dependency rejected', lambda: recovery_finish({'A': 1}, {'A': ['X']}))
rejects('missing dependency declaration rejected', lambda: recovery_finish({'A': 1}, {}))
rejects('negative task duration rejected', lambda: recovery_finish({'A': -1}, {'A': []}))
return {'passed': True, 'checks': len(names), 'checkNames': names,
'python': platform.python_version(), 'rollback': room,
'allocationCents': totals, 'monthlyCostCents': costs,
'recoveryFinishMinutes': finish,
'scope': 'Local deterministic arithmetic with fictional inputs; no cloud execution, prices, performance measurement or migration approval.'}
if __name__ == '__main__':
print(json.dumps(run(), indent=2))
Ao minuto 32 de uma janela de 50, a equipa precisa de decidir entre validar durante 10 minutos ou preservar os 12 de rollback e 3 de margem.
Armadilhas comuns
Contar poupança prevista como realizada, confundir quota com capacidade, ignorar horas de operação e reservar apenas o caminho de sucesso.
Tópicos relacionados: FINOPS e imputação · Dependências e recuperação · Governance de migração
Uma decisão defensável mostra cálculo, âmbito, evidência, dependências e autoridade; um número favorável isolado não demonstra aceitação.
Referência: Assess and discover workloads · Current linked standard guide; edition date unconfirmed (2026-09-30 inspection)