← Professional Cloud Architect: architecture and operations
12 / 14 · 120 MIN

Migration: costs, recovery and acceptance

Plan coherent waves, calculate rollback margins and connect financial decisions to operational evidence.

Plan coherent migration units

A server inventory is not yet a migration plan. For each business flow, identify callers, compatible versions, shared data and latency limits. In the exercise, Gateway and Writer use a synchronous protocol tied to the same version; separating them for a week creates an intermediate state needing its own validation. Draw the current, intermediate and final states. On each drawing, mark the transaction, communication paths and who confirms the outcome. If the intermediate state fails the requirement, group components into a coherent unit or demonstrate a supported temporary interface before approving waves. This does not require migrating the entire application at once. It requires a contract and evidence for each chosen boundary. A small server can carry a critical dependency; VM size alone does not measure sequencing risk. Record which observation would invalidate the proposed grouping.

Calculate the latest validation start

A window must reserve the failure path as well as the success path. Consider a fictional 50-minute window, 10-minute final validation, 12-minute rollback and an agreed 3-minute margin. With no overlap, the latest validation start is 50−10−12−3=25. The latest rollback start is 35, but that is not the time to start validating. At minute 32, 18 minutes remain: the 15 needed for rollback and margin fit; the 25 needed for validation, rollback and margin do not. Record both checkpoints in the plan and rehearse the decision with authorized owners. If an approved extension and compatible dependencies exist, recalculate; avoiding cost does not itself change the criteria. Deterministic times simplify this exercise. In a real project, measure phases, consider variation and confirm rollback remains technically possible after each step.

Compare total cost with visible assumptions

Compare options meeting the same requirements over the same period. In a fictional monthly model, self-managed infrastructure costs 4200 units plus 60 operating hours valued at 50 units each: total 7200. The managed option costs 5600 plus 12 hours at the same rate: total 6200. The difference is 1000 despite the managed option’s higher infrastructure invoice. This result belongs to the model; it does not establish that managed services are always cheaper. State costs assumed equal and omitted expenditure, such as migration or training. Valuing released hours also does not establish an immediate payment reduction: it may represent capacity for other work. In reporting, distinguish modeled cost, observed expenditure and expected benefit. Ask the operational owner to validate hours and FINOPS to confirm the financial basis before using the comparison for a decision.

Allocate shared costs without duplicating them

An allocation rule should be explicit, understandable and applied once. Custody has direct cost of 1200 and Analytics 1800. A shared service costs 900; the exercise’s internal rule assigns 40% to Custody and 60% to Analytics. Results are 1560 and 2340, totaling 3900. Assigning the full 900 to both duplicates expenditure; leaving the service out hides it. For rounding, the lab uses integer cents: distribute integer shares, then remaining cents by largest remainder, using alphabetical order for ties. This rule was chosen for the exercise; it is neither an accounting standard nor a Google rule. Check that the total is conserved and document the convention used. Connect reporting to decision and action owners; an accurate dashboard alone does not determine who may change a shared service or validate its impact on consumers.

Separate missing data from absent cost

Billing export to BigQuery has varying reporting intervals and no delivery latency guarantee. An empty query a few minutes after startup does not establish zero cost. Mark incomplete periods as provisional and distinguish query time from the consumption period represented. Adding a cost-center label today also does not rewrite already exported rows. To classify history, preserve the source and add a governed reporting mapping with effective dates, explaining ownership changes. Do not delete rows to provoke reconstruction: deleted exported data is not automatically restored. In the exercise, write two management notes: what has not arrived yet and what the label will not retroactively change. These are different problems requiring different treatment; neither justifies presenting an estimate as a confirmed financial close. Include the reporting cutoff and outstanding information in the decision record.

Distinguish financial commitment, quota and reservation

A temporary dual-run peak is not the same as stable demand after migration. Before buying a resource-based commitment, assess expected eligible use: the commitment cannot be canceled after purchase and remains billed when unused. That obligation differs from the ability to create VMs. Sufficient quota defines a usage limit; it does not guarantee available inventory in a zone. A reservation, in turn, requires matching properties and zone, alongside applicable consumption conditions. If the design changed machine type, review that match before relying on the reservation at cutover. Prepare three evidence lines: financial obligation, authorized limit and compatible capacity. Avoid treating approval of one as proof of the others. In project work, the distinction brings FINOPS and infrastructure into the appropriate decisions. Keep the assumptions aligned with the final deployment configuration.

Measure recovery through the business outcome

Restoring VMs does not establish that the service team can operate. Include actual operators, permissions and the approved alternate access path in rehearsal. Then draw timing dependencies: access takes 4 minutes; data takes 18 and networking 10 after access, in parallel; the application takes 7 after both; business validation takes 6. Service is validated at 35 minutes, not the data task’s 18 or the 45 obtained by adding everything sequentially. If networking takes 25, the result becomes 42. If data drops to 8, networking becomes limiting and the result is 27. These values assume enough resources for parallel work and exclude detection or additional waiting. In a real project, add and measure those dependencies before comparing the result with the agreed recovery objective. State precisely which event starts and ends the measurement.

Practise calculations and decide dual-run exit

Before running the code, predict four results: latest validation start, allocated cost by team, monthly difference and service-validation time. Save the example as run.py and run python3 run.py with Python 3.12 or later. Output includes results and local checks. Change one assumption at a time and explain which decision changes; do not confuse passing checks with establishing real migration timings. The lab does not contact Google Cloud and uses only fictional values. To close the case, prepare a dual-run exit decision with evidence for relevant flows, less frequent consumers, data treatment, recovery and residual costs. Define the acceptance owner and timing. Forecast savings become realized only when corresponding costs cease. If a dependency or approval is missing, record the gap and necessary action rather than declaring the project financially complete.

"""Original BigSavant 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))
IN PRACTICE

At minute 32 of a 50-minute window, the team must decide between 10-minute validation and preserving 12-minute rollback plus a 3-minute margin.

Common pitfalls

Counting forecast savings as realized, confusing quota with capacity, ignoring operating hours and reserving only the success path.

Related topics: FINOPS and allocation · Dependencies and recovery · Migration governance

Take this idea with you

A defensible decision shows calculation, scope, evidence, dependencies and authority; a favorable number alone does not establish acceptance.

Create account

Reference: Assess and discover workloads · Current linked standard guide; edition date unconfirmed (2026-09-30 inspection)

Google Cloud is a trademark of Google LLC. bigsavant.com is an independent preparation platform and is not affiliated with, associated with, sponsored, authorised or endorsed by Google. Content and questions are original, are not official exam questions, and completing our tests does not award or guarantee any certification. Names are used only to identify the subject. All other trademarks belong to their respective owners.