Python subprocess调用AWS SSM连接EC2执行命令卡死如何解决
你的代码存在以下几个核心问题
- 参数配置冲突:你同时传入了拆分后的命令列表
command.split()和shell=True参数,二者搭配使用时,只有列表第一个元素会被识别为要执行的命令,其余参数会被传递给底层shell本身而非aws cli,导致SSM会话根本没有正常启动。 - 命令未触发执行:所有通过
stdin.write()写入的命令末尾都没有加换行符\n,等价于在终端敲完命令没按回车,shell不会解析执行这些输入;同时写入后没有调用flush()方法,数据会滞留在Python的写缓冲区,根本不会发送给子进程。 - 输出读取逻辑死锁:
stdout.read()和communicate()默认会持续读取输出直到管道收到EOF(也就是子进程完全退出),但SSM启动的是长交互式会话,只要你不主动退出,管道永远不会关闭,所以程序会无限阻塞,连Ctrl+C都没法响应——因为子进程运行在前台时信号被拦截了。另外你没有重定向stderr,子进程的报错信息直接打印到原终端,既看不到报错原因,还容易因为管道缓冲区满触发死锁。 - 没有时序控制:你启动子进程后立刻连续写入ssh、ls命令,根本没有等待SSM会话建立完成、ssh跳转成功,命令要么发得太早被丢弃,要么主进程很快执行到末尾退出,子进程被强制终止,远端自然收不到完整指令,也不会执行你下发的shutdown之类的操作。
实现方案
优先推荐用专门的交互式会话库实现,不要硬写原生subprocess处理长交互,坑非常多,新手很难排查。
方案1:使用pexpect实现(新手友好,稳定性高)
pexpect是专门用来处理交互式命令行的库,自动帮你处理缓冲区、匹配输出时序的问题,先安装:pip install pexpect(Windows系统替换为pip install wexpect,接口完全一致)
参考代码:
import pexpect # 启动SSM会话 child = pexpect.spawn( 'aws ssm start-session --region us-east-2 --target i-01234567abcdef --profile profile-one', encoding='utf-8', timeout=15 ) # 等待跳板机shell提示符加载完成,正则可以根据你自己的机器提示符调整,比如自定义的prompt可以改匹配规则 child.expect(r'[\$#>]\s*$') # 发送ssh跳转命令,sendline自动在末尾加回车 child.sendline('ssh mirrora') # 等待目标机shell提示符加载完成,如果有主机指纹确认、密码提示可以在这里加匹配分支 child.expect(r'[\$#>]\s*$') # 执行ls命令 child.sendline('ls') child.expect(r'[\$#>]\s*$') # before里存的是从上次匹配结束到这次匹配到提示符之间的输出,也就是ls的执行结果 ls_result = child.before.strip() print("ls命令执行结果:\n", ls_result) # 后续需要执行其他运维命令,重复上面的sendline+expect流程即可 # child.sendline('你的其他运维命令') # child.expect(r'[\$#>]\s*$') # print(child.before.strip()) # 操作完成后逐层退出会话 child.sendline('exit') # 退出mirrora的ssh连接 child.expect(r'[\$#>]\s*$') child.sendline('exit') # 退出SSM会话 child.close()
如果ssh跳转时出现Are you sure you want to continue connecting (yes/no/[fingerprint])?提示,只要在对应expect步骤加匹配规则,发送yes即可;如果需要输密码,匹配password:提示后发送对应密码就行。
方案2:原生subprocess实现(不推荐新手使用)
如果坚持要用原生subprocess,必须修正前面提到的所有问题,注意规避死锁:
import subprocess import select import time # shell=False时直接传命令列表,不要开shell=True,同时把stderr重定向到stdout避免缓冲区堵死 proc = subprocess.Popen( ["aws", "ssm", "start-session", "--region", "us-east-2", "--target", "i-01234567abcdef", "--profile", "profile-one"], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, bufsize=0, universal_newlines=True ) def wait_for_prompt(timeout=15): """逐字符读取输出,直到匹配到shell提示符,避免全量read卡死""" output = [] deadline = time.time() + timeout while time.time() < deadline: # 用select非阻塞检查是否有可读数据,不用一直卡着等 readable, _, _ = select.select([proc.stdout], [], [], 0.5) if readable: single_char = proc.stdout.read(1) output.append(single_char) current_output = ''.join(output).rstrip() # 匹配提示符,根据自己实际环境调整 if current_output.endswith(('$', '#', '>')): return current_output raise TimeoutError("等待会话响应超时,当前输出:" + ''.join(output)) # 等待SSM跳板机会话就绪 print("SSM会话启动输出:\n", wait_for_prompt()) # 发送ssh跳转命令 proc.stdin.write("ssh mirrora\n") proc.stdin.flush() # 强制刷新缓冲区,把数据立刻发给子进程 print("ssh跳转输出:\n", wait_for_prompt()) # 发送ls命令 proc.stdin.write("ls\n") proc.stdin.flush() ls_output = wait_for_prompt() print("ls命令输出:\n", ls_output) # 操作完成逐层退出 proc.stdin.write("exit\n") proc.stdin.flush() wait_for_prompt() proc.stdin.write("exit\n") proc.stdin.flush() proc.wait(timeout=10)
这个方案有几个限制:Windows系统不支持select监听管道,没法用;如果命令输出里刚好有和提示符一样的字符,会提前截断输出,稳定性不如pexpect方案。
额外提示
如果是生产环境长期用,更推荐直接用AWS官方提供的SSM SDK,不需要维护交互式shell会话,直接通过API向目标实例发送命令、获取返回结果,稳定性比手动维护会话高很多。
内容的提问来源于stack exchange,提问作者Narendra Babu
相关产品推荐
相关产品推荐

