Setting the file. One moment.
Migration Preflight · Amazon Elasticache · aws/agent-toolkit-for-aws · Skills Docs
ContentsBack to the top of the page 10
Setup DevOps Agent
33
AWS Deployment
61.5
Recipe Gallery
scripts/migration_preflight.py
scripts/ migration_preflight.py
Python · 411 lines · 19 KB
17 # With TLS
18 python migration_preflight.py --host redis.example.com --port 6379 --tls
19
20 # With auth
21 python migration_preflight.py --host redis.example.com --password mypassword
22
23 # JSON output
24 python migration_preflight.py --host redis.example.com --json
25
26 # Via tunnel (local port forward)
27 python migration_preflight.py --host localhost --port 6379
28
29 Requires: valkey (pip install valkey)
30 """
31
32 import argparse
33 import json
34 import sys
35
36 # ---------------------------------------------------------------------------
37 # Dependency check -- fail early with an actionable message instead of a
38 # raw ImportError traceback.
39 # ---------------------------------------------------------------------------
40 try :
41 import valkey as redis
42 except ImportError :
43 print (
44 "Error: the 'valkey' package is required for this script. \n "
45 "Install with: \n "
46 " pip install valkey"
47 )
48 sys.exit( 1 )
49
50
51 # Commands not available on ElastiCache (restricted or modified)
52 ELASTICACHE_RESTRICTED_COMMANDS = {
53 "BGSAVE" , "BGREWRITEAOF" , "CONFIG" , "DEBUG" , "MIGRATE" ,
54 "SAVE" , "SHUTDOWN" , "SLAVEOF" , "REPLICAOF" , "SYNC" ,
55 }
56
57 # Known Redis modules and their ElastiCache availability
58 MODULE_COMPATIBILITY = {
59 "search" : { "name" : "RediSearch" , "elasticache" : False , "note" : "Not available on ElastiCache. Valkey 8.2 provides native vector search (FT.* commands) on node-based clusters." },
60 "ReJSON" : { "name" : "RedisJSON" , "elasticache" : False , "note" : "The ReJSON module cannot be loaded on ElastiCache. However, ElastiCache provides native JSON support (JSON.* commands) in Valkey 7.2+ and Redis OSS 6.2.6+. Verify command compatibility before migration." },
61 "timeseries" : { "name" : "RedisTimeSeries" , "elasticache" : False , "note" : "Not available on ElastiCache. Consider CloudWatch or Timestream." },
62 "bf" : { "name" : "RedisBloom" , "elasticache" : False , "note" : "No (module) / Yes (native BF.* commands in Valkey 8.1+)." },
63 "graph" : { "name" : "RedisGraph" , "elasticache" : False , "note" : "Deprecated. Consider Neptune or OpenSearch." },
64 "ai" : { "name" : "RedisAI" , "elasticache" : False , "note" : "Not available on ElastiCache. Use SageMaker for ML inference." },
65 }
66
67
68 def run_preflight (host, port, password = None , use_tls = False , username = None ):
69 """Run all preflight checks against a source Redis/Valkey instance."""
70 findings = []
71 info = { "host" : host, "port" : port}
72
73 try :
74 r = redis.Redis(
75 host = host, port = port,
76 password = password, username = username,
77 ssl = use_tls,
78 decode_responses = True ,
79 socket_timeout = 10 ,
80 socket_connect_timeout = 10 ,
81 )
82 r.ping()
83 findings.append({ "check" : "Connectivity" , "status" : "PASS" , "detail" : f "Connected to { host } : { port } " })
84 except (redis.ConnectionError, redis.TimeoutError) as e:
85 return { "error" : f "Cannot connect to { host } : { port } : { e } " , "info" : info, "findings" : [
86 { "check" : "Connectivity" , "status" : "FAIL" , "detail" : str (e)}
87 ]}
88 except redis.AuthenticationError as e:
89 return { "error" : f "Authentication failed: { e } " , "info" : info, "findings" : [
90 { "check" : "Connectivity" , "status" : "FAIL" , "detail" : f "Auth failed: { e } " }
91 ]}
92
93 # Server info
94 try :
95 server_info = r.info()
96 except Exception as e:
97 findings.append({ "check" : "Server info" , "status" : "ERROR" , "detail" : f "Could not retrieve INFO: { e } " })
98 r.close()
99 return { "info" : info, "findings" : findings}
100
101 # Engine and version
102 redis_version = str (server_info.get( "redis_version" , "unknown" ))
103 info[ "redis_version" ] = redis_version
104 info[ "os" ] = server_info.get( "os" , "unknown" )
105 info[ "uptime_days" ] = round (server_info.get( "uptime_in_seconds" , 0 ) / 86400 , 1 )
106
107 parts = redis_version.split( "." )
108 try :
109 version_tuple = tuple ( int (p) for p in parts[: 2 ]) if len (parts) >= 2 else ( int (parts[ 0 ]), 0 )
110 except ( ValueError , IndexError ):
111 version_tuple = ( 0 , 0 )
112
113 if version_tuple >= ( 7 , 0 ):
114 findings.append({ "check" : "Engine version" , "status" : "PASS" ,
115 "detail" : f "Redis { redis_version } can migrate to Valkey 7.2 (direct in-place switch) or Redis OSS 7+ on ElastiCache" })
116 elif version_tuple >= ( 6 , 0 ):
117 findings.append({ "check" : "Engine version" , "status" : "WARN" ,
118 "detail" : f "Redis { redis_version } standard support ends January 31, 2027. Extended Support charges begin February 1, 2027. Recommend upgrading to Valkey before then." })
119 elif version_tuple >= ( 5 , 0 ):
120 findings.append({ "check" : "Engine version" , "status" : "FAIL" ,
121 "detail" : f "Redis { redis_version } standard support ended January 31, 2026. Extended Support charges are now active (since Feb 1, 2026). Upgrade to Valkey to eliminate surcharges." })
122 findings.append({ "check" : "Extended Support" , "status" : "FAIL" ,
123 "detail" : f "Redis { redis_version } is enrolled in Extended Support. Year 1-2 premium: 80% surcharge. Year 3 premium: 160% surcharge. Run scripts/price_calculator.py --extended-support to see costs." })
124 else :
125 findings.append({ "check" : "Engine version" , "status" : "FAIL" ,
126 "detail" : f "Redis { redis_version } standard support ended January 31, 2026. Extended Support charges are now active. Upgrade to Valkey to eliminate surcharges." })
127 findings.append({ "check" : "Extended Support" , "status" : "FAIL" ,
128 "detail" : f "Redis { redis_version } is enrolled in Extended Support. Year 1-2 premium: 80% surcharge. Year 3 premium: 160% surcharge. Run scripts/price_calculator.py --extended-support to see costs." })
129
130 # Cluster mode
131 cluster_enabled = server_info.get( "cluster_enabled" , 0 )
132 info[ "cluster_mode" ] = bool (cluster_enabled)
133 if cluster_enabled:
134 findings.append({ "check" : "Cluster mode" , "status" : "INFO" ,
135 "detail" : "Cluster mode enabled — target must also be cluster mode enabled" })
136 else :
137 findings.append({ "check" : "Cluster mode" , "status" : "INFO" ,
138 "detail" : "Standalone or replica mode — can target cluster mode disabled or enabled" })
139
140 # Connected replicas
141 connected_replicas = server_info.get( "connected_slaves" , 0 )
142 info[ "connected_replicas" ] = connected_replicas
143 findings.append({ "check" : "Replication" , "status" : "INFO" ,
144 "detail" : f " { connected_replicas } connected replica(s)" })
145
146 # Memory
147 used_memory_mb = round (server_info.get( "used_memory" , 0 ) / ( 1024 * 1024 ), 1 )
148 used_memory_gb = round (used_memory_mb / 1024 , 2 )
149 peak_memory_mb = round (server_info.get( "used_memory_peak" , 0 ) / ( 1024 * 1024 ), 1 )
150 info[ "used_memory_mb" ] = used_memory_mb
151 info[ "used_memory_gb" ] = used_memory_gb
152 info[ "peak_memory_mb" ] = peak_memory_mb
153 findings.append({ "check" : "Memory usage" , "status" : "INFO" ,
154 "detail" : f "Current: { used_memory_mb } MB ( { used_memory_gb } GB), Peak: { peak_memory_mb } MB" })
155
156 # Key count
157 try :
158 db_info = {k: v for k, v in server_info.items() if k.startswith( "db" )}
159 total_keys = sum (v.get( "keys" , 0 ) for v in db_info.values() if isinstance (v, dict ))
160 info[ "total_keys" ] = total_keys
161 info[ "databases_in_use" ] = len (db_info)
162 findings.append({ "check" : "Key count" , "status" : "INFO" ,
163 "detail" : f " { total_keys :,} keys across { len (db_info) } database(s)" })
164
165 if len (db_info) > 1 :
166 findings.append({ "check" : "Multiple databases" , "status" : "WARN" ,
167 "detail" : f " { len (db_info) } databases in use — ElastiCache cluster mode uses only db0. Plan key migration." })
168 except Exception :
169 info[ "total_keys" ] = "unknown"
170
171 # Persistence
172 rdb_enabled = server_info.get( "rdb_last_save_time" , 0 ) > 0
173 aof_enabled = server_info.get( "aof_enabled" , 0 ) == 1
174 info[ "rdb_enabled" ] = rdb_enabled
175 info[ "aof_enabled" ] = aof_enabled
176 persistence_detail = []
177 if rdb_enabled:
178 persistence_detail.append( "RDB snapshots active" )
179 if aof_enabled:
180 persistence_detail.append( "AOF enabled" )
181 if not persistence_detail:
182 persistence_detail.append( "No persistence" )
183 findings.append({ "check" : "Persistence" , "status" : "INFO" ,
184 "detail" : ", " .join(persistence_detail)})
185
186 # Modules
187 try :
188 modules = r.execute_command( "MODULE" , "LIST" )
189 loaded_modules = []
190 for mod in modules:
191 if isinstance (mod, list ):
192 mod_name = mod[ 1 ] if len (mod) > 1 else str (mod)
193 elif isinstance (mod, dict ):
194 mod_name = mod.get( "name" , str (mod))
195 else :
196 mod_name = str (mod)
197 loaded_modules.append(mod_name)
198
199 info[ "modules" ] = loaded_modules
200
201 if not loaded_modules:
202 findings.append({ "check" : "Modules" , "status" : "PASS" , "detail" : "No modules loaded" })
203 else :
204 for mod_name in loaded_modules:
205 compat = MODULE_COMPATIBILITY .get(mod_name)
206 if compat:
207 if compat[ "elasticache" ]:
208 findings.append({ "check" : f "Module: { compat[ 'name' ] } " , "status" : "INFO" ,
209 "detail" : str (compat[ "note" ])})
210 else :
211 findings.append({ "check" : f "Module: { compat[ 'name' ] } " , "status" : "FAIL" ,
212 "detail" : str (compat[ "note" ])})
213 else :
214 findings.append({ "check" : f "Module: { mod_name } " , "status" : "WARN" ,
215 "detail" : f "Unknown module ' { mod_name } ' — verify compatibility with ElastiCache" })
216 except redis.ResponseError:
217 info[ "modules" ] = []
218 findings.append({ "check" : "Modules" , "status" : "INFO" ,
219 "detail" : "MODULE LIST not available (may be restricted or old version)" })
220
221 # Commandstats — check for restricted command usage
222 cmdstats = {}
223 try :
224 cmdstats = r.info( "commandstats" )
225 for cmd_name in ELASTICACHE_RESTRICTED_COMMANDS :
226 stat_key = f "cmdstat_ { cmd_name.lower().replace( ' ' , '|' ) } "
227 if stat_key in cmdstats:
228 calls = cmdstats[stat_key].get( "calls" , 0 )
229 if calls > 0 :
230 findings.append({ "check" : "Restricted command usage" , "status" : "WARN" ,
231 "detail" : f "Command { cmd_name } called { calls } times. This command is restricted on ElastiCache. Review usage before migration." })
232 except redis.ResponseError:
233 findings.append({ "check" : "Restricted command usage" , "status" : "INFO" ,
234 "detail" : "INFO COMMANDSTATS not available (may be restricted by ACL or unsupported on this engine version)" })
235 except (redis.ConnectionError, redis.TimeoutError) as e:
236 findings.append({ "check" : "Restricted command usage" , "status" : "INFO" ,
237 "detail" : f "Could not retrieve commandstats: { e } " })
238
239 # Lua script detection
240 lua_detected = False
241 try :
242 eval_calls = 0
243 for key in ( "cmdstat_eval" , "cmdstat_evalsha" ):
244 if key in cmdstats:
245 eval_calls += cmdstats[key].get( "calls" , 0 )
246 if eval_calls > 0 :
247 lua_detected = True
248 findings.append({ "check" : "Lua scripts" , "status" : "WARN" ,
249 "detail" : f "Lua scripts detected ( { eval_calls } eval calls). Test all Lua scripts against the target engine. Scripts using module commands or hardcoded key names may break." })
250 except ( KeyError , TypeError , AttributeError ):
251 findings.append({ "check" : "Lua scripts" , "status" : "INFO" ,
252 "detail" : "Unable to detect Lua script usage from commandstats" })
253 info[ "lua_scripts_detected" ] = lua_detected
254
255 # Large key sampling
256 try :
257 large_keys_found = []
258 for _ in range ( 20 ):
259 key = r.randomkey()
260 if key is None :
261 break
262 try :
263 usage = r.memory_usage(key)
264 if usage is not None and usage > 1048576 :
265 size_mb = round (usage / ( 1024 * 1024 ), 2 )
266 large_keys_found.append((key, size_mb))
267 except Exception :
268 findings.append({ "check" : "Large key sampling" , "status" : "INFO" ,
269 "detail" : "MEMORY USAGE command not available, skipping large key check" })
270 break
271 for key, size_mb in large_keys_found:
272 key_type = r.type(key) if key else "unknown"
273 findings.append({ "check" : "Large key sampling" , "status" : "WARN" ,
274 "detail" : f "Large key detected ( { key_type } , { size_mb } MB). Large keys can cause replication timeouts and event loop blocking during migration." })
275 except Exception :
276 pass
277
278 # Maxmemory policy
279 # CONFIG GET is restricted on ElastiCache managed endpoints. Detect them by
280 # hostname and skip CONFIG in that case, advising the user to check via the
281 # AWS API (describe-cache-parameters) instead.
282 is_elasticache = host.endswith( ".cache.amazonaws.com" )
283 if is_elasticache:
284 info[ "maxmemory_policy" ] = "unknown (ElastiCache source)"
285 findings.append({ "check" : "Eviction policy" , "status" : "INFO" ,
286 "detail" : "CONFIG command is restricted on ElastiCache. "
287 "Check the eviction policy via: aws elasticache describe-cache-parameters "
288 "--cache-parameter-group-name <param-group> "
289 "--query \" Parameters[?ParameterName=='maxmemory-policy'] \" " })
290 else :
291 try :
292 config = r.config_get( "maxmemory-policy" )
293 policy = config.get( "maxmemory-policy" , "unknown" )
294 info[ "maxmemory_policy" ] = policy
295 if policy in ( "noeviction" ,):
296 findings.append({ "check" : "Eviction policy" , "status" : "WARN" ,
297 "detail" : f "Policy is ' { policy } ' -- writes will fail when memory is full. Consider volatile-lru or allkeys-lru." })
298 else :
299 findings.append({ "check" : "Eviction policy" , "status" : "INFO" ,
300 "detail" : f "Policy: { policy } " })
301 except redis.ResponseError:
302 info[ "maxmemory_policy" ] = "unknown"
303
304 # Data tiering advisory
305 findings.append({ "check" : "Data tiering advisory" , "status" : "INFO" ,
306 "detail" : "If targeting r6gd node types (data tiering), note: online migration is not supported for r6gd clusters. "
307 "Use backup/restore instead. Data tiering only supports volatile-lru, allkeys-lru, volatile-lfu, allkeys-lfu, and noeviction eviction policies." })
308
309 # Sizing recommendation
310 findings.append({ "check" : "Sizing recommendation" , "status" : "INFO" ,
311 "detail" : f "Source dataset: { used_memory_gb } GB. Use scripts/price_calculator.py to estimate serverless and node-based costs for your workload." })
312
313 try :
314 r.close()
315 except Exception :
316 pass
317 return { "info" : info, "findings" : findings}
318
319
320 def format_report (result):
321 """Format preflight results as a human-readable report."""
322 if "error" in result:
323 info = result.get( "info" , {})
324 lines = [ f "ERROR: { result[ 'error' ] } " ]
325 if info:
326 lines.append( f " Host: { info.get( 'host' , '?' ) } : { info.get( 'port' , '?' ) } " )
327 return " \n " .join(lines)
328
329 info = result[ "info" ]
330 findings = result[ "findings" ]
331
332 lines = []
333 lines.append( "=" * 72 )
334 lines.append( "ElastiCache Migration Preflight Report" )
335 lines.append( "=" * 72 )
336 lines.append( "" )
337 lines.append( f " Source: { info[ 'host' ] } : { info[ 'port' ] } " )
338 lines.append( f " Redis version: { info.get( 'redis_version' , 'unknown' ) } " )
339 lines.append( f " OS: { info.get( 'os' , 'unknown' ) } " )
340 lines.append( f " Uptime: { info.get( 'uptime_days' , '?' ) } days" )
341 lines.append( f " Cluster mode: { 'enabled' if info.get( 'cluster_mode' ) else 'disabled' } " )
342 lines.append( f " Memory: { info.get( 'used_memory_mb' , '?' ) } MB ( { info.get( 'used_memory_gb' , '?' ) } GB)" )
343 lines.append( f " Keys: { info.get( 'total_keys' , '?' ) } " )
344 lines.append( f " Replicas: { info.get( 'connected_replicas' , '?' ) } " )
345 lines.append( f " Modules: { ', ' .join(info.get( 'modules' , [])) or 'none' } " )
346 lines.append( "" )
347
348 fail_count = sum ( 1 for f in findings if f[ "status" ] == "FAIL" )
349 warn_count = sum ( 1 for f in findings if f[ "status" ] == "WARN" )
350 pass_count = sum ( 1 for f in findings if f[ "status" ] == "PASS" )
351
352 if fail_count == 0 :
353 lines.append( f " Verdict: READY TO MIGRATE ( { pass_count } passed, { warn_count } warnings)" )
354 else :
355 lines.append( f " Verdict: BLOCKERS FOUND ( { fail_count } failures, { warn_count } warnings)" )
356 lines.append( "" )
357 lines.append( "-" * 72 )
358
359 status_order = { "FAIL" : 0 , "WARN" : 1 , "ERROR" : 2 , "PASS" : 3 , "INFO" : 4 }
360 sorted_findings = sorted (findings, key =lambda f: status_order.get(f[ "status" ], 5 ))
361
362 for f in sorted_findings:
363 icon = { "PASS" : "[OK] " , "FAIL" : "[FAIL]" , "WARN" : "[WARN]" , "INFO" : "[INFO]" , "ERROR" : "[ERR] " }
364 lines.append( f " { icon.get(f[ 'status' ], '[?] ' ) } { f[ 'check' ] } " )
365 lines.append( f " { f[ 'detail' ] } " )
366 lines.append( "" )
367
368 if fail_count > 0 :
369 lines.append( "=" * 72 )
370 lines.append( "MIGRATION BLOCKERS" )
371 lines.append( "=" * 72 )
372 lines.append( "" )
373 for f in sorted_findings:
374 if f[ "status" ] == "FAIL" :
375 lines.append( f " - { f[ 'check' ] } : { f[ 'detail' ] } " )
376 lines.append( "" )
377 lines.append( " Resolve blockers before starting migration." )
378 lines.append( " See references/migration/instructions.md for guidance." )
379
380 lines.append( "" )
381 lines.append( " Next steps:" )
382 lines.append( " 1. Resolve any blockers above" )
383 lines.append( " 2. Run: python scripts/price_calculator.py to estimate target cost" )
384 lines.append( " 3. Use the AWS CLI test-migration command to validate connectivity to the target cluster" )
385 lines.append( " 4. See references/migration/instructions.md for the full migration workflow" )
386 lines.append( "" )
387
388 return " \n " .join(lines)
389
390
391 if __name__ == "__main__" :
392 parser = argparse.ArgumentParser( description = "ElastiCache Migration Preflight Check" )
393 parser.add_argument( "--host" , required = True , help = "Source Redis/Valkey hostname" )
394 parser.add_argument( "--port" , type = int , default = 6379 , help = "Port (default: 6379)" )
395 parser.add_argument( "--password" , default = None , help = "AUTH password" )
396 parser.add_argument( "--username" , default = None , help = "Username (for ACL-based auth)" )
397 parser.add_argument( "--tls" , action = "store_true" , help = "Use TLS" )
398 parser.add_argument( "--json" , action = "store_true" , help = "Output as JSON" )
399 args = parser.parse_args()
400
401 result = run_preflight(args.host, args.port, args.password, args.tls, args.username)
402
403 if args.json:
404 print (json.dumps(result, indent = 2 , default = str ))
405 else :
406 print (format_report(result))
407
408 if "error" in result:
409 sys.exit( 1 )
410 fail_count = sum ( 1 for f in result.get( "findings" , []) if f[ "status" ] == "FAIL" )
411 sys.exit( 1 if fail_count > 0 else 0 )