lambdaからEC2内のコンテナにSSH, FTP接続する

# はじめに
lambdaからEC2内のコンテナにSSH、FTP接続したときのメモです。lambdaでpythonのparamikoをインポートしようとすると下記のエラーが出て躓いてましたが、chatGPTの力を借りて解決できました。
```
libc.musl-x86_64.so.1: cannot open shared object file: No such file or directory
```
# 目的
- 同じVPC内にあるlambdaからEC2内のコンテナにSSH,FTP接続する。
- pythonのparamikoライブラリを使ってSSH接続する。
- pythonのftplibライブラリを使ってFTP接続する。

![構成図.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1319419/224db2f8-635f-df00-a2e9-2693d4f8bce9.png)

# 環境
- EC2
  - Red Hat Enterprise Linux
  - t2.micro

- lambda
  - Python 3.9
  - x86_64

# EC2
## セキュリティグループ
一旦全てのアクセス、全てのポートからアクセス可能に設定しておく。

## コンテナ作成
### podman network作成
下記のコマンドを実行し、test_networkという名前のpodman networkを作成する。
```
podman network create test_network
```
### serverコンテナ作成
pure-ftpdをベースに下記のDockerfileから作成する。
```
# ベースイメージとしてftpdを使用
FROM docker.io/stilliard/pure-ftpd

# arg
ARG PUBLICHOST
ARG USER
ARG PASSWORD

#env
ENV PUBLICHOST=$PUBLICHOST
ENV FTP_USER_NAME=$USER
ENV FTP_USER_PASS=$PASSWORD
ENV FTP_USER_HOME="/home/$USER"

# 必要なパッケージをインストール
RUN apt-get update && \
    apt-get install -y openssh-server && \
    mkdir /var/run/sshd

# パスワードのみでのSSH接続を許可する
RUN sed -i 's/#PasswordAuthentication yes/PasswordAuthentication yes/' /etc/ssh/sshd_config
# RUN sed -i 's/#PubkeyAuthentication yes/PubkeyAuthentication yes/' /etc/ssh/sshd_config

# testuserを作成
RUN useradd -m -s /bin/bash "$FTP_USER_NAME" && \
echo "$FTP_USER_NAME:$FTP_USER_PASS" | chpasswd && \
chmod 777 -R "/home/$FTP_USER_NAME"

# SSHサービスを起動するスクリプトを作成
RUN echo '#!/bin/bash\nservice ssh start\nexec "$@"' > /start.sh && chmod +x /start.sh

# SSHとPure-FTPdの両方を起動
CMD ["sh", "-c", "/start.sh && /run.sh -l puredb:/etc/pure-ftpd/pureftpd.pdb -E -j -R -P $PUBLICHOST"]
```
下記コマンドでコンテナイメージをビルドする。イメージの名前はserverとした。
```
podman build \
    -t server \
    --build-arg PUBLICHOST=localhost \
    --build-arg USER=testuser \
    --build-arg PASSWORD=password \
    .
```

下記コマンドでコンテナを起動する。コンテナの名前はserverとした。  
SSH用のポート22をホストの2022に、FTP用のポート21をホストの2021にそれぞれマウントする。また、コンテナのipアドレスを10.89.0.6に固定した。
```
podman run -d --tty \
    --name server \
    --network test_network \
    -p 2021:21 \
    -p 2022:22 \
    -p 30000-30009:30000-30009 \
    --ip 10.89.0.6 \
    --privileged \
    server
```


### clientコンテナ作成（任意）
SSH、FTPをEC2内で確認するためにclientコンテナを作成する。

下記のDockerfileから作成する。
```
FROM python:3.9-alpine

RUN apk update

RUN apk --update-cache add \
    python3 \
    python3-dev \
    py3-pip \
    gcc \
    g++ \
    curl \
    bash \
    openssh-client

RUN pip install --upgrade pip
RUN pip install --upgrade setuptools

```
下記コマンドでコンテナイメージをビルドする。イメージの名前はclientとした。

```
podman build -t client .
```

下記コマンドでコンテナを起動する。コンテナの名前はclientとした。  
また、ipアドレスを10.89.0.5に固定した。
```
podman run -d --tty \
    --name client \
    --network test_network \
    --ip 10.89.0.5 \
   client
```


# lambda
## アクセス権限
IAMロールを修正する。  
下記2つポリシーを追加する。
- AmazonEC2ReadOnlyAccess
- AWSLambdaVPCAccessExecutionRole

## VPC、セキュリティグループ
一旦全てのアクセス、全てのポートからアクセス可能に設定しておく。  
VPCはEC2と同じものを設定する。

## zipファイル作成
Dockerを使ってzipファイルを作成する。  
AWSのlambdaと同じ環境でビルドしないと下記のエラーが出てpythonでparamikoがインポートできなかった。  
```
libc.musl-x86_64.so.1: cannot open shared object file: No such file or directory
```
AWSのlambda python3.9のイメージ(public.ecr.aws/lambda/python:3.9)が公開されているので、これを使用する。

下記のDockerfileを使用して作成する。ここではpythonのライブラリparamiko, scp, pysftpをインストールしているが、SSH接続するだけならparamikoだけでOK。

```
FROM public.ecr.aws/lambda/python:3.9

RUN yum install -y gcc libffi-devel python3-devel

RUN mkdir -p /workspace/python-pkg /workspace/python-pkg/pkg

# 必要なファイルをコピー
COPY lambda_function.py /workspace

# pythonのライブラリをインストール
WORKDIR /workspace/python-pkg
RUN pip install paramiko scp pysftp -t .

# pythonライブラリとlambda_function.pyをzipにまとめる
WORKDIR /workspace
RUN zip -r lambda-pkg.zip .

# ENTRYPOINTを上書きする
ENTRYPOINT ["/bin/bash", "-l", "-c", "cp /workspace/lambda-pkg.zip /workspace/pkg/lambda-pkg.zip"]
```

下記コマンドでコンテナイメージをビルドする。イメージの名前はclientとした。  
ビルドする前にローカル環境にlambda_function.pyを作成しておく。
```
podman build -t make-lambda-pkg .
```

下記コマンドでコンテナを作成する。  
コンテナにマウントするためのpkgディレクトリを作成しておく。
コンテナ起動時にlambdaにアップロードするためのlambda-pkg.zipファイルがマウントされたpkgディレクトリに作成される。
```
podman run --rm --tty \
    -v $(pwd)/pkg:/workspace/pkg:Z \
    make-lambda-pkg
```

## 関数作成
zipファイルをアップロードする。ファイルサイズが10M以上の場合はS3からアップロードする。  

lambda_function.pyを下記のように作成する。
```
import boto3
from ftplib import FTP
import json
import os
import sys

sys.path.append('python-pkg')

import paramiko
import scp 
import pysftp

CUR_DIR = os.getcwd()

def test_ssh():
    # ログイン情報設定
    host = '172.31.15.114' # EC2 private ip
    user = 'testuser'
    password = 'password'
    port=2022

    ssh = paramiko.SSHClient()
    ssh.load_system_host_keys()
    ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())


    # 接続
    try:
        ssh.connect(hostname=host, username=user, password=password, port=port)

        # コマンド実行例
        stdin, stdout, stderr = ssh.exec_command("echo 'Hello from container'")
        output = stdout.read().decode()
        error = stderr.read().decode()

        ssh.close()

        # 結果をログに記録
        print(f"Command Output: {output}")
        print(f"Command Error: {error}")

        return {"statusCode": 200, "body": output}
    except Exception as e:
        print(f"SSH connection failed: {e}")
        return {"statusCode": 500, "body": str(e)}
    finally:
        ssh.close()


def test_ftp():
    # ログイン情報
    host = '172.31.15.114' # EC2 private ip
    user = 'testuser'
    password = 'password'
    port = 2021

    ftp = FTP()
    # 接続
    try:
        ftp.connect(host, port)
        ftp.login(user, password)
        # ホームディレクトリのファイルリストを取得
        ret = ftp.retrlines('LIST')
        return {"result": ret}
    except Exception as e:
        print(f"FTP connection failed: {e}")
        return {"result": f"failed {e}"}
    finally:
        ftp.close() 


def lambda_handler(event, context):
    # ret = test_ftp()
    ret = test_ssh()
    
    return str(ret)

```

# lambdaからEC2内のserverコンテナへのSSH接続
lambda_handler関数のtest_ftp部分をコメントアウトしてテストを実行する。テストイベントはなんでもOK。
```
def lambda_handler(event, context):
    # ret = test_ftp()
    ret = test_ssh()
    
    return str(ret)
```

成功していれば、下記のような出力が得られる。
```
Status: Succeeded
Test Event Name: test_event

Response:
"{'statusCode': 200, 'body': 'Hello from container\\n'}"
```

# lambdaからEC2内のserverコンテナへのFTP接続
lambda_handler関数のtest_ssh部分をコメントアウトしてテストを実行する。テストイベントはなんでもOK。
```
def lambda_handler(event, context):
    ret = test_ftp()
    # ret = test_ssh()
    
    return str(ret)
```

成功していれば、下記のような出力が得られる。
```
Status: Succeeded
Test Event Name: test_event

Response:
"{'result': '226-Options: -l \\n226 0 matches total'}"
```
# clientコンテナ、EC2ホスト、PCからコンテナへのSSH接続
おまけ。  
各クライアントからEC2のserverコンテナへSSH接続する方法。  
指定するホストやポートが異なるので注意。  
ユーザにはserverコンテナのユーザを指定する。
## clientコンテナから
ホストにコンテナのIPアドレスを指定。ポートはデフォルトの22。
```
ssh testuser@10.89.0.6 -p 22
```
## EC2ホストから
ホストにlocalhostを指定。 ポートはコンテナにマウントした2022。
```
ssh testuser@localhost -p 2022
```
## PCから
ホストにEC2のパブリックIPアドレスを指定。 ポートはコンテナにマウントした2022。EC2のpemファイルも必要。
```
ssh -i xxxxx.pem testuser@3.22.205.47 -p 2022
```
# おわりに
AWSに慣れていないこともあり、時間がかかってしまいました。lambdaでpythonのライブラリを使用するときは、public.ecr.aws/lambda/python:3.9イメージを使ってビルドするのが良さそうです。  

# 参考
- https://docs.paramiko.org/en/stable/api/client.html
- https://github.com/stilliard/docker-pure-ftpd/blob/master/Dockerfile
- https://docs.python.org/ja/3/library/ftplib.html