Setting the file. One moment.
Find Tunnel Host · 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/find_tunnel_host.py
scripts/ find_tunnel_host.py
Python · 184 lines · 7 KB
from
__future__
import
annotations
18
19 import argparse
20 import os
21 import sys
22
23 try :
24 import boto3
25 except ImportError :
26 print (
27 "Error: the 'boto3' package is required. \n "
28 "Install it with: \n "
29 " pip install boto3"
30 )
31 sys.exit( 2 )
32
33
34 def find_tunnel_hosts (vpc_id: str , region: str , profile: str | None ) -> list[ dict ]:
35 session = boto3.Session( profile_name = profile, region_name = region)
36 ec2 = session.client( "ec2" )
37 ssm = session.client( "ssm" )
38
39 print ( f "Scanning VPC { vpc_id } in { region } for SSM-managed instances ... \n " )
40
41 paginator = ec2.get_paginator( "describe_instances" )
42 pages = paginator.paginate(
43 Filters = [
44 { "Name" : "vpc-id" , "Values" : [vpc_id]},
45 { "Name" : "instance-state-name" , "Values" : [ "running" ]},
46 ]
47 )
48
49 instances = []
50 for page in pages:
51 for reservation in page[ "Reservations" ]:
52 for inst in reservation[ "Instances" ]:
53 name = ""
54 for tag in inst.get( "Tags" , []):
55 if tag[ "Key" ] == "Name" :
56 name = tag[ "Value" ]
57 break
58 instances.append(
59 {
60 "instance_id" : inst[ "InstanceId" ],
61 "name" : name,
62 "type" : inst[ "InstanceType" ],
63 "az" : inst[ "Placement" ][ "AvailabilityZone" ],
64 "private_ip" : inst.get( "PrivateIpAddress" , "" ),
65 }
66 )
67
68 if not instances:
69 print ( "No running EC2 instances found in this VPC. \n " )
70 print ( "Options:" )
71 print ( " 1. Create a minimal jump host: t4g.nano (~$3/month)" )
72 print ( " Launch one with: aws ec2 run-instances (attach the AmazonSSMManagedInstanceCore IAM policy), then run scripts/start_tunnel.py to connect" )
73 print ( " 2. Deploy your app to Lambda/ECS/EKS in the VPC (no tunnel needed)" )
74 return []
75
76 ssm_managed = set ()
77 try :
78 instance_ids = [inst[ "instance_id" ] for inst in instances]
79 ssm_paginator = ssm.get_paginator( "describe_instance_information" )
80 for page in ssm_paginator.paginate(
81 Filters = [{ "Key" : "InstanceIds" , "Values" : instance_ids}]
82 ):
83 for info in page[ "InstanceInformationList" ]:
84 ssm_managed.add(info[ "InstanceId" ])
85 except Exception as exc:
86 error_code = ""
87 if hasattr (exc, "response" ):
88 error_code = exc.response.get( "Error" , {}).get( "Code" , "" )
89 if error_code in ( "AccessDeniedException" , "AccessDenied" , "UnauthorizedAccess" ):
90 print ( f "ERROR: Access denied querying SSM. Ensure your IAM role/user has "
91 f "ssm:DescribeInstanceInformation permission." )
92 print ( "Listing all instances -- SSM status unknown. \n " )
93 else :
94 print ( f "Warning: could not query SSM: { exc } " )
95 print ( "Listing all instances -- SSM status unknown. \n " )
96
97 results = []
98 for inst in instances:
99 inst[ "ssm" ] = inst[ "instance_id" ] in ssm_managed
100 results.append(inst)
101
102 results.sort( key =lambda x: ( not x[ "ssm" ], x[ "type" ]))
103 return results
104
105
106 def main () -> None :
107 parser = argparse.ArgumentParser(
108 description = "Find SSM-managed EC2 instances in a VPC for tunnel use." ,
109 formatter_class = argparse.RawDescriptionHelpFormatter,
110 epilog = __doc__ ,
111 )
112 parser.add_argument( "--vpc-id" , required = True , help = "VPC ID to scan" )
113 _default_region = (
114 os.environ.get( "AWS_REGION" )
115 or os.environ.get( "AWS_DEFAULT_REGION" )
116 or "us-east-1"
117 )
118 parser.add_argument(
119 "--region" , default = _default_region, help = f "AWS region (default: { _default_region } )"
120 )
121 parser.add_argument( "--profile" , default = None , help = "AWS profile name" )
122 args = parser.parse_args()
123
124 try :
125 results = find_tunnel_hosts(args.vpc_id, args.region, args.profile)
126 except Exception as e:
127 error_code = ""
128 if hasattr (e, "response" ):
129 error_code = e.response.get( "Error" , {}).get( "Code" , "" )
130 if error_code in ( "AccessDeniedException" , "AccessDenied" , "UnauthorizedAccess" ):
131 print ( f "ERROR: Access denied. Ensure your IAM role/user has ec2:DescribeInstances "
132 f "and ssm:DescribeInstanceInformation permissions." )
133 else :
134 print ( f "ERROR: { e } " )
135 sys.exit( 1 )
136
137 if not results:
138 sys.exit( 1 )
139
140 ssm_ready = [r for r in results if r[ "ssm" ]]
141 not_ssm = [r for r in results if not r[ "ssm" ]]
142
143 if ssm_ready:
144 print ( f "Found { len (ssm_ready) } SSM-managed instance(s) (ready for tunnel, $0 extra cost): \n " )
145 print ( f " { 'Instance ID' :<22} { 'Name' :<25} { 'Type' :<14} { 'AZ' :<16} { 'Private IP' } " )
146 print ( f " { '-' * 22 } { '-' * 25 } { '-' * 14 } { '-' * 16 } { '-' * 15 } " )
147 for r in ssm_ready:
148 print ( f " { r[ 'instance_id' ] :<22} { r[ 'name' ] :<25} { r[ 'type' ] :<14} { r[ 'az' ] :<16} { r[ 'private_ip' ] } " )
149
150 best = ssm_ready[ 0 ]
151 print ( f " \n Recommended: { best[ 'instance_id' ] } ( { best[ 'name' ] or best[ 'type' ] } )" )
152 print ( f " \n Next step: start a tunnel with:" )
153 print ( f " python scripts/start_tunnel.py \\ " )
154 print ( f " --instance-id { best[ 'instance_id' ] } \\ " )
155 print ( f " --cache-host <your-cache-endpoint> \\ " )
156 print ( f " --region { args.region } " )
157 print ( f " \n For ElastiCache Serverless, you must also tunnel port 6380:" )
158 print ( f " python scripts/start_tunnel.py \\ " )
159 print ( f " --instance-id { best[ 'instance_id' ] } \\ " )
160 print ( f " --cache-host <your-cache-endpoint> \\ " )
161 print ( f " --cache-port 6380 \\ " )
162 print ( f " --local-port 6380 \\ " )
163 print ( f " --region { args.region } " )
164 print ( f " \n Note: ElastiCache Serverless requires TLS. When connecting through" )
165 print ( f " the tunnel, use --tls with --sni <your-cache-endpoint> to pass the" )
166 print ( f " the real cache hostname for reference." )
167 else :
168 print ( f "Found { len (not_ssm) } instance(s) but none have SSM agent: \n " )
169 print ( f " { 'Instance ID' :<22} { 'Name' :<25} { 'Type' :<14} " )
170 print ( f " { '-' * 22 } { '-' * 25 } { '-' * 14 } " )
171 for r in not_ssm:
172 print ( f " { r[ 'instance_id' ] :<22} { r[ 'name' ] :<25} { r[ 'type' ] :<14} " )
173 print ( " \n Options:" )
174 print ( " 1. Install SSM agent on one of these instances (free):" )
175 print ( " Attach the AmazonSSMManagedInstanceCore IAM policy to the instance role" )
176 print ( " 2. Create a minimal jump host (~$3/month):" )
177 print ( " Launch one with: aws ec2 run-instances (attach the AmazonSSMManagedInstanceCore IAM policy), then run scripts/start_tunnel.py to connect" )
178
179 if not_ssm and ssm_ready:
180 print ( f " \n ( { len (not_ssm) } additional instance(s) without SSM agent not shown)" )
181
182
183 if __name__ == "__main__" :
184 main()