マネージド・サーバーレス・踏み台サーバーのススメ

***※ ネタです。***

AWSのVPCでプライベートなサブネットにおいてあるEC2にSSHでアクセスしたいとき、
踏み台サーバーを作成して、多段でアクセスする方法があります。

ただ、金やセキュリティのために踏み台EC2を削除をするので、SSHする度に作ったり落としたりするのもめんどくさいです。
そんなことを考えながら、ネット記事を見ていたらGoでaws lambdaにアクセスする方法が書いてありました。

SSH-ing into your AWS Lambda Functions
https://medium.com/clog/ssh-ing-into-your-aws-lambda-functions-c940cebf7646

SSHできるんだったら、トンネル開けてやれば5分間踏み台サーバーとして利用できそうです。

記事ではGoだったので、なんとなくpython（3.6）で。

## 考えること

（1）トンネル掘りのため、sshができるparamikoが必要
（2）lambdaはセキュリティグループで設定してもインバウンドのポート閉じちゃってるらしい

（1）については、lambda用にzipを作るときに`pip install -t paramiko .` でOK。
（2）については、lambdaからSSHでアクセスしてもらい、逆ポートフォワーディングでトンネルを作成。

## 準備

1. VPCでプライベートなEC2インスタンス
2. プライベートなサブネット2個以上。（VPCにlambda設置のため）
3. サブネットにNATゲートウェイ（プライベートサブネット内のlambdaが外と通信するため）
4. NAT越えのためngrok（手元のPCとlambdaをつなぎたいので）

## AWS Lambdaのコード

`paramiko`のところに[デモ](https://github.com/paramiko/paramiko/blob/master/demos/rforward.py)があったのでそれを流用。

```python
import os
import socket
import select
import sys
import threading
import paramiko

def handler(chan, host, port):
    sock = socket.socket()
    try:
        sock.connect((host, port))
    except Exception as e:
        print('Forwarding request to %s:%d failed: %r' % (host, port, e))
        return None

    print('Connected!  Tunnel open %r -> %r -> %r' %(chan.origin_addr, chan.getpeername(), (host, port)))

    while True:
        r, _w, _x = select.select([sock, chan], [], [])
        if sock in r:
            data = sock.recv(1024)
            if len(data) == 0:
                break
            chan.send(data)
        if chan in r:
            data = chan.recv(1024)
            if len(data) == 0:
                break
            sock.send(data)

    chan.close()
    sock.close()

    return None

def reverse_forward_tunnel(server_port, remote_host, remote_port, transport):
    transport.request_port_forward('', server_port)
    while True:
        chan = transport.accept(1000)
        if chan is None:
            continue
        thr = threading.Thread(target=handler, args=(chan, remote_host, remote_port))
        thr.setDaemon(True)
        thr.start()

def main_handler(event, context):
    client = paramiko.SSHClient()
    client.load_system_host_keys()
    client.set_missing_host_key_policy(paramiko.AutoAddPolicy())

    if os.environ.get('CIRCUIT_BREAKER') is not None: # 再起動用
        print("canncel handler.")
        return None

    try:
        host = os.environ.get('LOCAL_HOST', '0.tcp.ap.ngrok.io')
        port = os.environ['LOCAL_PORT']
        username = os.environ['LOCAL_USERNAME']
        password = os.environ['LOCAL_PASSWORD']

        reverse_port = os.environ.get('REVERSE_PORT', 9999)
        remote_host = os.environ['REMOTE_HOST']
        remote_port = os.environ.get('REMOTE_PORT', 22)

        client.connect(host, int(port), username = username, password = password)

        print('Create tunnel %s:%s:%s' % (reverse_port, remote_host, remote_port))
        reverse_forward_tunnel(int(reverse_port), remote_host, int(remote_port), client.get_transport())
    except Exception as e:
        print('Connect failed: %r' % e)

    return None

if __name__ == '__main__':
    from os.path import join, dirname
    from dotenv import load_dotenv

    dotenv_path = join(dirname(__file__), '.env')
    load_dotenv(dotenv_path)

    main_handler({}, {})
````

## 実行

1. ↑のソースコードをparamiko付きでプライベートなサブネット内lambdaに設置
2. lambdaの環境変数を設定
3. 手元のPCにngrokをダウンロード＆認証をしてやる
4. ngrokをtcp 22で起動(`ngrok tcp 22 --region ap`)。  
ngrokのトンネル用ポートが表示されるので、lambdaの環境変数に設定（LOCAL_PORT）
5. lambda起動
6. うまく起動できたら、ngrokを起動しているPCにトンネルが接続されているので  
SSHでプライベートなEC2にアクセス

SSHコマンドだと、こんな感じになっているかと。

```
# lambda内
$ ssh -R <REVERSE_PORT>:<REMOTE_HOST>:<REMOTE_PORT> <LOCAL_USERNAME>@<LOCAL_HOST> -p <LOCAL_PORT>

# 手元のPCなど
$ ssh 127.0.0.1 -p <REVERSE_PORT> -l ec2-user -i ~/.ssh/ec2-keypair.pem

```
## 感想

当たり前ですが、踏み台lambdaが落ちれば接続も切れますので5分しか使えません。
なので、5分間で作業を終わらしてください。（接続先EC2内にtmuxとか起動していれば……）

また、Goは標準でssh用のやつ（x/crypto/ssh）を持っているので、Goの方が楽そうです。