Staff Engineer · Cloud Migration · Pickles Asia (Dec 2025 – Mar 2026)

AWS Lambda Regional Migration
Singapore to Malaysia. Runtime upgrades. Implementing GitOps discipline.

Pickles Asia had 71 Lambda functions deployed across Node.js and Python, all running on end-of-life runtimes out of the Singapore region - no tests, no version control, and no runbooks. The company's customers were in Malaysia, and AWS had opened the ap-southeast-5 region. Moving the Lambda fleet there made sense for latency and data residency compliance. What didn't make sense was doing it carelessly. I designed a 3-phase migration framework: Phase 1 targeted the 35 production-critical functions, Phase 2 addressed 10 dormant functions with unclear ownership, and Phase 3 handled decommissioning. Every function passed 4 validation layers before any Singapore traffic was cut over. The outcome was a completely modernised Lambda fleet: Node 22, Python 3.12, full test coverage, per-function GitHub repos, CI/CD pipelines, and deployment runbooks. All delivered with zero downtime and zero production incidents.

Singapore to Malaysia migrationComplete
71
Total
35
Migrated
4
Validation layers
0
Incidents
phased rollout with zero downtime
The Challenge

71 functions with old runtimes, no tests, and no documentation.

Pickles Asia had built a large serverless system over 6 years. 71 Lambda functions, mix of Node.js and Python, all running in Singapore. The problem was clear when we looked closely: almost every function was on old, unsupported runtimes. Node 8.10, 10, 12, 14, 16 - all past their end of life. Even the Python functions were on 3.7 and 3.8, both deprecated.

The company's customers were in Malaysia, and AWS had just opened a region there. Moving Lambda functions there made perfect sense for latency and compliance. But it wasn't going to be a simple copy-paste operation. Most of the functions were deployed through the AWS console or Serverless Framework. No GitHub. No CI/CD pipelines. No test coverage. Some hadn't been invoked in over 450 days and nobody knew who owned them. We needed to understand each function, upgrade it safely, write tests, and document everything.

  • All 71 functions on end-of-life runtimes: Node 8 through 16, Python 3.7 and 3.8, past AWS support windows
  • Zero test coverage across the entire fleet - not a single unit test for any function
  • Every deployment done through the AWS console by hand - no version control, no audit trail
  • ~10 functions with no invocations in 450+ days and no documented owner
  • No playbook for rolling back a bad deployment. Just panic and manual fixes
  • No documentation on how to safely operate any of these functions

lambda_fleet.runtime_audit71 Functions · All EOL
71
On old runtimes
0%
Test coverage
0
Version control
RuntimeCountStatus
nodejs8.102EOL 2019
nodejs10.x15EOL 2021
nodejs12.x12EOL 2022
nodejs14.x13EOL 2023
nodejs16.x6EOL 2024
python3.74EOL 2023
python3.84EOL 2024
nodejs18.x / 22.x13Supported
python3.122Supported
full fleet audit · ap-southeast-1 (SG)
How I Approached It

Audit, plan, validate, deploy, monitor.

1
Audit all 71 functions - Comprehensive inventory of all functions:
  • Documented triggers, permissions, runtime, dependencies
  • Sorted into 3 groups: 35 critical (Phase 1), 10 dormant, rest for cleanup
  • Established dependency map & ownership records
  • Identified all old runtime EOL risks
2
Put them in GitHub - Version controlled Lambda source code:
  • Created one repo per function
  • Extracted functions from console-deployed ZIPs
  • Reconstructed source code & dependencies
3
Test on old runtime - Establish baseline for comparison:
  • Ran each function in Docker with original runtime
  • Captured real production events
  • Replayed events to establish baseline output
  • Identified edge cases & dependency requirements
4
Upgrade to Node 22 and Python 3.12 - Modernize runtime & close security gaps:
  • Updated all runtimes to Node.js 22 & Python 3.12
  • Updated required dependencies to latest versions
  • Rewrote legacy Node 8 functions completely
  • Fixed security vulnerabilities across fleet
5
Write test suites - Full coverage with production event replay:
  • Created tests for all production functions
  • Used Jest for Node.js, Pytest for Python
  • Replayed production events on new runtime
  • Confirmed identical output between old & new
6
Deploy to Malaysia - 24-hour shadow run comparing both regions:
  • Deployed to Malaysia staging environment
  • Ran Malaysia & Singapore in parallel 24 hours
  • Compared logs, latency, errors, & output data
  • Verified behavior parity before cutover
7
Canary traffic shift - Gradual traffic migration with automatic rollback:
  • Moved traffic in stages: 10% → 50% → 100%
  • CloudWatch alarms monitored errors & latency
  • Watched for data anomalies or performance spikes
8
Document the process - Operational knowledge & deployment playbooks:
  • README for each function with ownership & triggers
  • Documented permissions & deployment steps
  • Created playbooks for deploy & rollback
  • Enabled future maintainers to operate safely
Lambda Migration: SG → MY Fleet
ap-southeast-1 · Singapore71 Lambda functions · All EOLNode 8.10 / 10 / 12 / 14 / 16Python 3.7 / 3.8 · no testsno version control · console deploysMIGRATEap-southeast-5 · Malaysia35 functions · ModernizedNode 22 · Python 3.12GitHub repos · CI/CD pipelinesTest suites · Runbooks · 0 incidentsPER-FUNCTION MIGRATION PIPELINE1. AuditRuntime version · invocation historyOwnership · trigger mapping2. GitOps SetupGitHub repo · Reconstruct ZIPREADME · permissions documented3. Baseline TestingSAM · Docker · prod event replayCapture edge cases on old runtime4. Runtime UpgradeNode 8–16 → Node 22.xPython 3.7/3.8 → Python 3.125. Test Suite (0→full)Jest (Node) · Pytest (Python)Prod event replay · output verified6. Deploy to MalaysiaGitHub Actions CI/CD pipeline24h shadow run: SG ‖ MY parallel7. Canary Traffic Shift10% → 50% → 100% MY trafficCloudWatch alarms · auto-rollback8. Decommission SGSG function removedRunbook committed · owned ✓3-PHASE MIGRATION FRAMEWORKPhase 135 prod-criticalFull pipelinePriority migrationPhase 210 dormant funcs450+ days inactiveOwnership resolvedPhase 3Decommission SG26 remaining cleanedFull fleet modernized35 functions · Node 22 + Python 3.12 · 0 production incidents · lower latency (MY customers)GitOps discipline · CI/CD pipelines · test suites · deployment runbooks - delivered per function
GitOps Architecture + Canary Routing
BEFORE → AFTER: ARCHITECTURE COMPARISONBEFORE · ap-southeast-1 (SG)Deploy: AWS Console - manual, no audit trailVCS: None - some repositoriesTests: 0 unit tests across entire fleetRuntime: Node 8–16 · Python 3.7/3.8 (all EOL)AFTER · ap-southeast-5 (MY)Deploy: GitHub Actions - automated, auditedVCS: GitHub (one repo per function)Tests: Jest + Pytest · prod event replayRuntime: Node 22 · Python 3.12 (LTS)GITOPS MIGRATION PIPELINE · PER FUNCTION1. Extractconsole ZIPsource recover2. GitHubper-fn repoversion control3. TestJest / PytestSAM + Docker4. UpgradeNode 22Python 3.125. DeployLambda (MY)canary 10%CANARY TRAFFIC ROUTING · CLOUDWATCH MONITOREDAPI Gateway · Route53 Weighted Routingper-function gradual cutover: 10% → 50% → 100%100%→0%0%→100%ap-southeast-1 (Singapore)71 fns · Node 8–16 · Python 3.7/3.8no VCS · no tests · console-deployedtraffic: 100% → 50% → 0%decommissioned after cutoverap-southeast-5 (Malaysia)35 fns · Node 22 · Python 3.12GitHub · CI/CD · Jest · runbookstraffic: 0% → 50% → 100%CloudWatch · auto-rollback ready35 functions · Node 22 + Python 3.12 · 0 downtime · 0 incidentsGitOps discipline · per-function canary · CloudWatch gate · auto-rollback at each stage
The Safety Net

Every function had to pass 4 checks before we turned off Singapore

01
Local Tests

I ran tests locally on both the old runtime and the new one. I used AWS SAM with Docker containers so the tests ran in the exact same environment that Lambda uses. This caught any breaking changes early before anything went to the cloud.

02
Test With Real Events

I captured actual events from production and ran them through both the old and new versions of each function. If the output wasn't identical, we stopped and fixed it. This found subtle bugs that normal tests would have missed.

03
Run Both Versions for 24 Hours

After deploying to Malaysia, both the Singapore and Malaysia versions ran at the same time for a full day. I compared logs, response times, errors, and output between them in real time. Singapore stayed live as backup the whole time. Only after 24 hours of matching results did we proceed to the next step.

04
Slowly Shift Traffic

Traffic moved to Malaysia in stages: 10 percent first, then 50 percent, then all of it. At each stage, alerts watched for errors, slow responses, or bad data. If anything triggered an alarm, the system automatically switched back to Singapore without waiting for anyone to manually fix it.

Issues Caught Before Production

4 problems that testing found early

The validation layers caught real bugs that would have broken production. Each one was found, fixed, and verified before any customer traffic moved.

CRITICAL - FIXED
Error handling broken on new runtime

A Finance Lambda returned empty fields on the new runtime. The old code used AWS SDK v2 callbacks that silently swallowed errors on Node 8. Node 22 handled it differently, so errors disappeared without throwing. Fixed by rewriting to SDK v3 with proper async/await.

CRITICAL - FIXED
SQS dead-letter queue flooding

An SQS processor failed 6 hours into the shadow run, sending 50 messages per hour to the dead-letter queue. The code parsed JSON twice, which worked on Node 12 but broke on Node 22. Fixed the deserialization logic and verified with 24-hour clean run.

SECURITY - HALTED MIGRATION
Overly permissive IAM roles

Found 3 functions with wildcard permissions on SQS and S3. Set up years ago, never reviewed. Halted their migration, locked down permissions to specific resources, got security sign-off before proceeding.

PERFORMANCE - AUTO-HALTED AND FIXED
Cold start latency spike

Response times jumped from 340ms to 870ms during canary at 10%. Auto-rollback caught it immediately. Larger deployment package increased cold starts. Enabled Provisioned Concurrency on the 3 busiest functions. Resumed canary and finished at 180ms (47% faster than Singapore).

What This Cost

3 months of careful work instead of a quick copy-paste

Speed vs safety. Every tradeoff was worth it.

Time Investment
3 months

A naive migration would have taken 2 weeks. This took 3 months because we audited everything, wrote tests, ran shadow deployments, and got things right. Worth every day.

Production Incidents
Zero

No emergency pages. No 3 AM calls. No rollbacks in production. No customers noticing anything wrong. That's what the extra time bought us.

Operational Complexity
Increased

CloudWatch alarms, traffic shifting, parallel runs, monitoring. More moving parts. But this complexity kept us safe at production scale.

Future Work
Much Easier

Next time someone needs to migrate Lambda functions, they're not starting from zero. They have playbooks, templates, and a proven framework. The investment compounds.

The Results

35 functions moved. Modern runtimes. Everything in version control. No incidents.

35
Functions successfully migrated

All 35 production functions moved from Singapore to Malaysia. All the triggers, permissions, and settings verified and working.

Node 22 + Python 3.12
Modern runtimes deployed

Every function now runs current, supported code. Old functions on Node 8 got rewritten where needed. All dependencies updated. Security vulnerabilities fixed.

0
Problems in production

Zero incidents. Zero downtime. The testing, shadow runs, and automatic rollback safeguards meant every deploy was safe and reversible.

100%
Documented and tested

Every function is now in GitHub with tests, a README explaining what it does and how to deploy it, and a playbook for deployment and rollback. No more mystery black boxes in the console.

Engineering Decisions

Architectural choices, tradeoffs, and what nearly broke production

Why test locally first instead of just comparing both versions in the cloud?

Couldn't we skip SAM testing and just run both versions side by side?

If the old and new runtimes produce different outputs, the cloud is the wrong place to find out. Local testing lets us catch and fix functional issues before any customers are affected. The shadow run then verifies deployment and performance, not core functionality. Testing locally saves days of production incident response.

Why 24 hours for the parallel test, not just a few hours?

Wouldn't testing for 2 or 3 hours be faster and still catch most problems?

Lambda functions don't run on a 2-hour cycle. Some only run at 2 AM. Some only run on weekends. A short test window might miss whole categories of traffic. 24 hours covers a full day's worth of real patterns, peak times, off-peak times, and time-dependent edge cases.

Why did we catch issues in testing that our normal tests missed?

Why didn't regular unit tests find all these problems?

Unit tests test happy paths and specific scenarios we think of. Event replay uses real production events, real data shapes, and real concurrency patterns. When a function handles 10,000 different message types, unit tests can't cover all of them. Real events do. The SDK error swallowing and double JSON parsing never showed up in our unit tests but failed immediately with production data.

Tests mock everything. Real deployments deal with actual AWS services, actual latency, actual error behaviors.

Why automatically roll back on a latency spike instead of investigating manually?

Couldn't we pause and investigate before rolling back?

When customers see response times jump from normal to 800 milliseconds on an API endpoint, they stop using it. Timeouts start. Real impact happens. Auto-rollback within seconds prevents customer damage. Investigation and fixes can happen post-incident when production is stable. Your customers' experience comes first.

Before vs After

Transformative Results: Before vs After

✕Before
  • Old runtimes everywhere (Node 8 through 16, Python 3.7)Security risk
  • Deployments through AWS console by handMistakes happen
  • No tests at allBugs go unnoticed
  • Only in SingaporeSlow for Malaysia users
  • No safety checks before deployingThings break in prod
→
✓After
  • Modern Node.js 22 and Python 3.12Secure and fast
  • Deployments through GitHub with automationConsistent and safe
  • Full test coverage for all functionsConfident deploys
  • Running in both Singapore and MalaysiaBetter for users
  • 4 layers of validation before going liveZero problems in prod
⇪Business Impact
35 functions migrated
0 downtime or incidents
4-layer validation gate
Tech Stack

What was used

ComputeAWS Lambda
Target runtimesNode.js 22.x, Python 3.12
Testing (Node)Jest, AWS SAM
Testing (Python)Pytest, AWS SAM
Local runtimeDocker, SAM CLI
MonitoringCloudWatch, X-Ray
RollbackLambda Aliases, Traffic shifting
TriggersAPI Gateway, SQS, SNS, S3, EventBridge
CI/CDGitHub Actions, Azure DevOps
GitOpsGitHub (per-function repos)
Source regionap-southeast-1 (Singapore)
Target regionap-southeast-5 (Malaysia)

Migrating Lambda functions or serverless infrastructure?

I've executed this at production scale - 35 functions, zero downtime, zero incidents. I can design the migration strategy, build the validation and testing framework, handle the engineering work, and document everything so your team owns it afterward.