I tried @user1322092's approach and got into trouble when multiple clients tried to access the db, since each one would attempt to open a connection (all but the first would get an error).
I created a monstrous solution involving spawning an SSH process on the dyno and using that for all communications. Gory details are in https://stackoverflow.com/a/27361295/558639. Aside from being really kludgy, it incurs a delay every time a process starts up, whether or not you're accessing the remote database.
update
So here's a better approach that appears to work well: just trap (and ignore) Errno::EADDRINUSE
errors. Here's the code:
require 'net/ssh/gateway'
class Mole
TUNNEL_HOST = <redacted>
TUNNEL_USER = <redacted>
AWS_HOST = <redacted>
TUNNEL_PORT_NUMBER = 3307
attr_reader :tunnel_gateway, :tunnel_port
# Call this to open an SSH tunnel from the current machine. If the
# the tunnel was already opened (e.g. by another process) , you'll
# get a warning message
def open_tunnel
@tunnel_gateway = Net::SSH::Gateway.new(TUNNEL_HOST, TUNNEL_USER)
begin
@tunnel_port = @tunnel_gateway.open(AWS_HOST, 3306, TUNNEL_PORT_NUMBER)
rescue Errno::EADDRINUSE => e
$stderr.puts("Warning: #{e.class}: #{e.message}")
end
end
# You won't normally call this, because the tunnel is a system wide
# resource; you don't know when other processes want to release it.
def close_tunnel
r = @tunnel_gateway.close(@tunnel_port) if @tunnel_gateway
@tunnel_gateway.shutdown!
r
end
end