matt
matt

Reputation: 9412

How do I use the results of an SSM port forwarding session started with ruby?

I'm having trouble reconciling the differences between using the aws cli and the ruby sdk to make ssm connections to an instance. For example, if I try using the command like like this:

aws ssm start-session \
  --target 'i-abc123' \
  --document-name AWS-StartPortForwardingSession \
  --parameters '{
    "portNumber": ["3000"], 
    "localPortNumber": ["13000"]
  }'
Starting session with SessionId: username-def456
Port 13000 opened for sessionId username-def456.
Waiting for connections...

Connection accepted for session [username-def456]

the tool will pause as it opens up a port forwarding session from port 3000 on the destination instance to port 13000 on my local machine. I can then open up a web browser on my machine and point it at http://localhost:13000 to browse around an app running on the remote instance. Also, I can look at the AWS console UI and see that there's an active session until I Ctrl-C

The trouble I'm having is when I try to use the ruby sdk to do the same thing:

result = ssm.start_session(
  target: 'i-abc123',  
  document_name: 'AWS-StartPortForwardingSession',  
  parameters: {  
    'portNumber' => %w[3000],    
    'localPortNumber' => %w[13000]    
  }    
)  

The result object is a struct that looks like the following:

=> #<struct Aws::SSM::Types::StartSessionResponse
 session_id="username-def456",
 token_value="...",
 stream_url="wss://ssmmessages.us-east-1.amazonaws.com/v1/data-channel/username-def456? 
 role=publish_subscribe">

Again, I can see in the console UI that there is an active session. However, if I try to browse to http://localhost:13000 on my machine, the browser can't reach anything. How do I use the resulting stream url and token to actually create a connection to the ec2 instance?

additional details:

Upvotes: 1

Views: 1499

Answers (2)

Guss
Guss

Reputation: 32394

Building on the information in Feico de Boer's response, I got this to work:

# Use the AWS CLI Session Manager plugin to execute a command
# on an ECS task. This method will never return - it uses
# kernel#exec to execute an TTY interactive command.
#
# @param task [Aws::ECS::Types::Task] task to connect to.
#    The task is expected to have only one container that
#    should have /bin/bash installed.
# @param ecs_client [Aws::ECS::Client] SDK client
#    The client is expected to have the region and profile configured.
# @param command [string] optional command to run in a bash shell.
#    If provided, the output will be shown, otherwise an interactive
#    bash shell will be started.
def execute_ecs_shell task, ecs_client, command = ''
  region = ecs_client.config.region
  profile = ecs_client.config.profile
  cluster = task.cluster_arn.split("/").last
  container_runtime_id = task.containers.first.runtime_id
  
  resp = ecs_client.execute_command({
    cluster: task.cluster_arn,
    task: task.task_arn,
    # the container name is optional for single container tasks
    #container: "some container name",
    interactive: true,
    # supposedly we'd want to do interactive: command.empty?,
    # but 'false' is not currently supported by the SDK
    command: if command.empty? then
      '/bin/bash'
    else
      "/bin/bash -c '#{command}'"
    end
  })
  
  session = {
    "SessionId" => resp.session.session_id,
    "StreamUrl" => resp.session.stream_url,
    "TokenValue" => resp.session.token_value,
  }.to_json
  params = {
    "Target" =>"ecs:#{cluster}_#{task.id}_#{containers_runtime_id}"
  }.to_json
  exec("session-manager-plugin", session, region, "StartSession",
    profile, params, "https://ssm.#{region}.amazonaws.com")
end

The main gaps I had were to figure out the correct format for the create_session_response and parameters arguments. Reading the source code for session-manager-plugin helped me to understand that they mean the JSON formatted API response - which I'm pretty sure is originally an XML and the Ruby implementation converts to a structure that responds poorly to to_json, so the AWS CLI and the plugin basically rely on a language-specific side effect of the Python SDK library. The other problem was to understand the "target" parameter that is sent to the AWS CLI ssm start-session command and I couldn't figure it out except reading this comment on the SSM agent issue tracker - where it isn't explained either but I found a format that works for me - assuming single container tasks. If you have multi-container tasks, you'll need to specify the container name in the execute_command parameters and also fix the target definition, YMMV.

A correct implementation would be to grab a websocket client and implement the full SSM API, using a full readline-style interactive terminal session - I might want to do that at some point because the session-manager-plugin has annoying output before and after the session.

Upvotes: 1

Feico de Boer
Feico de Boer

Reputation: 11

What you get back from the API is a reference to a web socket. So you either need to create your own web socket to local listener proxy or use the session-manager-plugin as the aws cli utility does.

I recently figured out how to use the session-manager-plugin, the following snippet is Python but should be obvious enough to figure it out.

def start_aws_ssm_plugin(self, create_session_response, parameters, profile, region):
    print('start_aws_ssm_plugin() called: ' + str( create_session_response))

    arg0 = '"' + self.config.get_ssm_plugin() + '"'
    arg1 = '"' + str(create_session_response).replace('\'', '\\"') + '"'
    arg2 = region
    arg3 = 'StartSession'
    arg4 = profile
    arg5 = '"' + str(parameters).replace('\'', '\\"') + '"'
    arg6 = 'https://ssm.{region}.amazonaws.com'.format(region=region)

    command = arg0 + ' ' + arg1 + ' ' + arg2 + ' ' + arg3 + ' ' + arg4 + ' ' + arg5 + ' ' + arg6

    print(command)
    # print('session-manager-plugin', arg1, arg2, arg3, arg4, arg5, arg6)

    pid = subprocess.Popen(command).pid
    return pid

# end def

(And yes, this was just a quick and dirty prototype.;))

Upvotes: 1

Related Questions