AWS TransferFamily + S3 でSFTP構成

# AWS TransferFamily + S3 でSFTP構成

　　パスワード認証SFTPを利用してS3の特定のバケットにファイルを置いたり削除したりする構成

　　下記を参考に構築します。（CloudFormationテンプレートを使用してお手軽に構築していますが、今回は勉強のために全て手作業）
　　参考：https://aws.amazon.com/jp/blogs/storage/enable-password-authentication-for-aws-transfer-for-sftp-using-aws-secrets-manager/

　　所要時間：かなり多くの設定があるため１〜２時間ほど

## 使用するAWSサービス

　S3：ファイル格納先
　Transfer Family：SFTPサーバ
　Secret Manager：SFTPサーバの接続先、S3のディレクトリを保存
　APIGateway：外からどうこうするために
　Lambda：SecretManagerからキーバリューを取得
　IAM：上記サービス間でやりとりするために各サービスに付与する
　
## IAMロールの作成

　**目的**：API GatewayがLambdaを呼び出したり、LambdaがSecretManagerからキーバリューを取得したり、TransferFamilyがS3へアクセスするために設定します。

　**流れ**：ポリシーを作成　-> ポリシーを付与した、ロールを作成 -> 信頼関係を編集
***

###ポリシーの作成

① まずはIAMのコンソール画面に移動し、左のナビゲーションから[ポリシー]を選択します。

![スクリーンショット 2021-12-23 16.09.31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/267420/ec990761-f23a-b784-00c3-58b6fa91f811.png)


***
② [ポリシーを作成]を選択し、[サービス]、[アクション]、[リソース]を設定する。

③ 次へ進み、必要であれば[タグ]をつける

④ [名前]と、[説明]をつけて完了

■ ①〜④の繰り返しになるため、必要なポリシーは下に列挙します。
　　※リソースは要件によると思いますが、今回は勉強のためなので、[すべて]にしておきます。
　　※名前は自由ですが、ないと説明しにくいので記載します。

　・**API_Gatewai_Get_Policy**(APIGatewayでGETを呼び出すためのポリシー)
　　サービス：API Gateway
　　アクション：GET
　　リソース：すべて

　・**Lambda_Invoke_Policy**（APIGatewayがLambda関数を呼び出すためのポリシー）
　　サービス：Lambda
　　アクション：
　　　　　　　　"lambda:InvokeFunction",
　　　　　　　　"lambda:GetFunction",
　　　　　　　　"lambda:ListFunctions",
　　　　　　　　"lambda:ListVersionsByFunction",
　　リソース：すべて

　・**SecretManager_Get_Secret_Value_Policy**
　　　　（Lambda関数がSecretManagerからキーバリューを取得するためのポリシー）
　　サービス：Secret Manager
　　アクション：
　　　　　　　　"secretsmanager:GetRandomPassword",
　　　　　　　　"secretsmanager:GetResourcePolicy",
　　　　　　　　"secretsmanager:GetSecretValue",
　　　　　　　　"secretsmanager:DescribeSecret",
　　　　　　　　"secretsmanager:ListSecretVersionIds",
　　　　　　　　"secretsmanager:ListSecrets"
　　リソース：すべて

　　・**SFTP_S3_Access_Policy**
このポリシーのみ、リソース部にSecretManagerに格納するディレクトリパスを指定するためJSONで記載します。

この設定により、SecretManagerにキー：HomeDirectoryとして設定したディレクトリパスを読み取り、
特定のバケットにのみ接続を制限できます。
（バケットのどこにアクセスしても構わないよ、という場合は"アクション"部分のロールをコンソールで選択してください。

　　　サービス：S3
　　　アクション：JSONで記載

```
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "AllowListingOfUserFolder",
            "Action": [
                "s3:ListBucket"
            ],
            "Effect": "Allow",
            "Resource": [
                "arn:aws:s3:::${transfer:HomeBucket}"
            ],
            "Condition": {
                "StringLike": {
                    "s3:prefix": [
                        "${transfer:HomeFolder}/*",
                        "${transfer:HomeFolder}"
                    ]
                }
            }
        },
        {
            "Sid": "HomeDirObjectAccess",
            "Effect": "Allow",
            "Action": [
                "s3:PutObject",
                "s3:GetObject",
                "s3:DeleteObject",
                "s3:GetObjectVersion"
            ],
            "Resource": "arn:aws:s3:::${transfer:HomeDirectory}*"
        }
    ]
}
```

***

### ロールを作成

次にロールを作成します。

**流れ**：上で作成したポリシーをロールに付与　-> 信頼関係を追加

今度は、上の画像の[ロール]を選択し、[ロールの作成]を選択してください。

![スクリーンショット 2021-12-23 16.40.30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/267420/b4ea6fd5-713b-2434-db0b-7e069d0efbbb.png)

***

① 信頼されたエンティティの種類を選択：AWS

② ユースケースの選択：連携先のサービス名
　　AssumeRoleというやつですね。いわゆる認証周りです。

③ Attach アクセス権限ポリシー：作成したポリシーを選択

④ 必要であれば[タグ]をつけます

⑤ ロールの名前をつけて作成完了

⑥ 必要であれば、信頼関係を追加
　　ロールを選択後、[信頼関係]タブから、[信頼関係の編集]を選択し、追加
***
![スクリーンショット 2021-12-23 16.51.23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/267420/39b0da1a-f99d-c588-c333-2b9cd291c987.png)


***

先ほどのポリシーと同様に列挙する形にします。
 

・**TransferFamily_Role**：AWS TransferFamilyのSFTPサーバーに付与するロール
　　信頼されたエンティティの種類：AWS
　　ユースケースの選択：Lambda
　　Attach アクセス権限ポリシー：API_Gatewai_Get_Policy
　　　　　　　　　　　　　　　　　SFTP_S3_Access_Policy
　　
　　信頼関係の追加：２回目以降は省略します。サービス部に必要なサービスを追加してください。

```
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": [
          "s3.amazonaws.com",
          "transfer.amazonaws.com",
          "apigateway.amazonaws.com"
        ]
      },
      "Action": "sts:AssumeRole"
    }
  ]
}
```

・**Lambda_List_and_Invoke_Role**：APIGatewayに付与するロール
　　信頼されたエンティティの種類：AWS
　　ユースケースの選択：Lambda
　　Attach アクセス権限ポリシー：Lambda_Invoke_Policy
　　　　　　　　　　　　　　　　　
　　信頼関係の追加：

```
        "Service": [
          "apigateway.amazonaws.com",
          "lambda.amazonaws.com"
        ]
```

・**Lambda_Get_Secret_Manager_Key_Role**：Lambdaに付与するロール
　　信頼されたエンティティの種類：AWS
　　ユースケースの選択：Api Gateway
　　Attach アクセス権限ポリシー：SecretManager_Get_Secret_Value_Policy
　　　　　　　　　　　　　　　　　
　　信頼関係の追加：

```
        "Service": [
          "apigateway.amazonaws.com",
          "lambda.amazonaws.com"
        ]
```

これでロールの作成は完了です。

次にS3バケットを用意します。

## S3 バケットの作成とアクセスコントロール

S3では格納先のバケットを作成し、AWS SecretManagerに登録してあるディレクトリのパスのみにアクセスできるように、バケットポリシーを設定します。

### バケットの作成

① S3のコンソールへ移動し、左のナビゲーションから[バケット]を選択し、[バケットの作成]を選択します。

![スクリーンショット 2021-12-23 17.02.39.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/267420/b236ec7c-5944-b6ad-4c8b-0f25899af7a2.png)

バケット名、フォルダ名は自由ですが、説明のためにつけておきます。

**バケット名**：test_bucket
    

※バージョニング：バージョニングを設定する場合は、ポリシーに"GetObjectVersion"やらが必要になってくるので注意してください。

あとは特に任意です。何もなければそのまま下までスクロールし、バケットを作成します。

② 作成したバケットを選択し、フォルダを作成します。
　　**フォルダ名**：SFTP

③ [アクセス許可]タブより、ブロックパブリックアクセス (バケット設定)の[編集]を選択します。
![スクリーンショット 2021-12-23 17.12.43.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/267420/e2e72c6e-d8ab-822b-fd67-435568938496.png)

④ [パブリックアクセスをすべて ブロック]のチェックを一旦外し[変更の保存]を選択します。


:::note alert
　　以下の設定が完了後、必ず元に戻してください
:::

![スクリーンショット 2021-12-23 22.55.53.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/267420/d1248dbd-b3cb-2022-3f5c-90eedad4f425.png)

⑤ 再び、[アクセス許可]タブに戻り、[バケットポリシー]の[編集]を選択します。

⑥ 以下のコードを記述し、保存します。**"Resource"の部分は作成したバケット名**になります。
　　{} はいりません。バージョニングをオンにしている場合には、各ポリシーもバージョニング版を選択しないとクロスリージョンコピーなどでdenyされるので注意

```
{
	"Version": "2012-10-17",
	"Id": "Policy1636522677868",
	"Statement": [
		{
			"Sid": "Stmt1636522659332",
			"Effect": "Allow",
			"Principal": "*",
			"Action": [
				"s3:GetBucketAcl",
				"s3:GetObject",
				"s3:GetObjectAcl",
				"s3:GetObjectVersion",
				"s3:GetObjectVersionAcl",
				"s3:ListBucket",
				"s3:ListBucketVersions",
				"s3:PutBucketAcl",
				"s3:PutObject",
				"s3:PutObjectAcl",
				"s3:PutObjectVersionAcl",
				"s3:DeleteObject",
				"s3:DeleteObjectVersion"
			],
			"Resource": [
				"arn:aws:s3:::{ここは作成したバケット名が入る}",
				"arn:aws:s3:::{ここは作成したバケット名が入る}/*"
			]
		}
	]
}
```

⑦ 保存したら先ほどの、[パブリックアクセスをすべて ブロック]のチェックを元に戻します。

以上でS3の設定は完了です。

次は、Lambdaを設定します。

## Lambdaの設定 Part1

① Lambdaのコンソール画面へ移動し、左のナビゲーションから[関数]を選択し、画面右で[関数の作成]を選択します。
![スクリーンショット 2021-12-23 23.03.36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/267420/c20dbc80-391b-267d-a5f8-e777441ef518.png)


② 下記内容通りに設定

　オプション：一から作成
　関数名：Get_Secrete_Manager(自由ですが説明のために名前をつけます。)
　ランタイム：Python3.9
　アーキテクチャ：x86_64
　デフォルトの実行ロールの変更：既存のロールを使用する。
　作成したロール「Lambda_Get_Secret_Manager_Key」を入力
　詳細設定：なし

　[関数の作成]を選択し、完了

③ 作成した関数を選択し、コードソースに以下のコードを入力
　　※pythonなのでインデントに注意してください。Quiitaに貼り付けてインデントが崩れているかもしれないので。


```
import os
import json
import boto3
import base64
from botocore.exceptions import ClientError

def lambda_handler(event, context):
    resp_data = {}

    if 'username' not in event or 'serverId' not in event:
        print("Incoming username or serverId missing  - Unexpected")
        return response_data

    # It is recommended to verify server ID against some value, this template does not verify server ID
    input_username = event['username']
    print("Username: {}, ServerId: {}".format(input_username, event['serverId']));

    if 'password' in event:
        input_password = event['password']
        if input_password == '' and (event['protocol'] == 'FTP' or event['protocol'] == 'FTPS'):
            print("Empty password not allowed")
            return response_data
    else:
        print("No password, checking for SSH public key")
        input_password = ''

    # Lookup user's secret which can contain the password or SSH public keys
    resp = get_secret("SFTP/" + input_username)

    if resp != None:
        resp_dict = json.loads(resp)
    else:
        print("Secrets Manager exception thrown")
        return {}

    if input_password != '':
        if 'Password' in resp_dict:
            resp_password = resp_dict['Password']
        else:
            print("Unable to authenticate user - No field match in Secret for password")
            return {}

        if resp_password != input_password:
            print("Unable to authenticate user - Incoming password does not match stored")
            return {}
    else:
        # SSH Public Key Auth Flow - The incoming password was empty so we are trying ssh auth and need to return the public key data if we have it
        if 'PublicKey' in resp_dict:
            resp_data['PublicKeys'] = [resp_dict['PublicKey']]
        else:
            print("Unable to authenticate user - No public keys found")
            return {}

    # If we've got this far then we've either authenticated the user by password or we're using SSH public key auth and
    # we've begun constructing the data response. Check for each key value pair.
    # These are required so set to empty string if missing
    if 'Role' in resp_dict:
        resp_data['Role'] = resp_dict['Role']
    else:
        print("No field match for role - Set empty string in response")
        resp_data['Role'] = ''

    # These are optional so ignore if not present
    if 'Policy' in resp_dict:
        resp_data['Policy'] = resp_dict['Policy']

    if 'HomeDirectoryDetails' in resp_dict:
        print("HomeDirectoryDetails found - Applying setting for virtual folders")
        resp_data['HomeDirectoryDetails'] = resp_dict['HomeDirectoryDetails']
        resp_data['HomeDirectoryType'] = "LOGICAL"
    elif 'HomeDirectory' in resp_dict:
        print("HomeDirectory found - Cannot be used with HomeDirectoryDetails")
        resp_data['HomeDirectory'] = resp_dict['HomeDirectory']
    else:
        print("HomeDirectory not found - Defaulting to /")

    print("Completed Response Data: "+json.dumps(resp_data))
    return resp_data

def get_secret(id):
    region = os.environ['SecretsManagerRegion']
    print("Secrets Manager Region: "+region)

    client = boto3.session.Session().client(service_name='secretsmanager', region_name=region)

    try:
        resp = client.get_secret_value(SecretId=id)
        # Decrypts secret using the associated KMS CMK.
        # Depending on whether the secret is a string or binary, one of these fields will be populated.
        if 'SecretString' in resp:
            print("Found Secret String")
            return resp['SecretString']
        else:
            print("Found Binary Secret")
            return base64.b64decode(resp['SecretBinary'])
    except ClientError as err:
        print('Error Talking to SecretsManager: ' + err.response['Error']['Code'] + ', Message: ' + str(err))
        return None
```

一旦Lambdaの設定を中断し、次にAPI Gatewayを作成します。　

## API Gatewayの設定

API Gatewayでは、外部からGETメソッドを叩いた際に先のLambdaを使用してSecretManagerに設定したS3のディレクトリなどの接続先情報を取得します。

**流れ**：RESTfulAPIを作成 -> 統合リクエスト、メソッドレスポンスを設定

① API Gatewayのコンソール画面へ移動し、[APIを作成]を選択します。

② APIタイプを選択で、[RESTfulAPI]を選択し、[構築]を選択します。
![スクリーンショット 2021-12-23 23.21.37.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/267420/63dc43f7-860a-2bc8-6c97-492f5e40041a.png)

③ 以下のように設定していきます。

　プロトコルを選択する：REST
　新しい API の作成：新しいAPI
　API名：GET_Connection_Infomation_API（自由ですが説明のために名前をつけます）
　エンドポイントタイプ：リージョン

　[APIの作成]を選択し次へ

④ 左のナビゲーションより、[リソース]を選択し、[アクション]プルダウンより、[リソースの作成]をしていきます。

　![スクリーンショット 2021-12-23 23.25.00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/267420/d6188b6b-c802-19a0-cf0c-e70ba0426d46.png)

⑤ [アクション]から[リソースの作成]を選択し、次の画像のように、リソース名を入力していきます。

:::note alert
　　{serverId}のように{}を入力すると、画像の[リソースパス]のように
　　なぜか　-serverId- と、-- に変換されるため注意してください。
:::

下記のようなツリーになります。
　　servers
　　　{serverId}
　　　　users
　　　　　{username}
　　　　　　　　　config

⑥ configまで作成したら最後に[アクション]から[メソッドの作成]を選択し、[GET]を選択します。

⑦ そしてGETをクリックするといい感じになっていると思うので、以下を作成していきます。　
　　・統合リクエスト
　　・統合レスポンス　　　　　

⑧ その前に左のナビゲーションから[モデル]を選択し、[作成]を選択

⑨ モデルのスキーマに以下のコードを入力

```
{
    "$schema": "http://json-schema.org/draft-04/schema#",
    "title": "Error Schema",
    "type": "object",
    "properties": {
        "message": {
            "type": "string"
        }
    }
}
```

⑩ リソースのGETに戻ります。

11 [メソッドリクエスト]を選択し、[統合リクエスト]を選択し、以下のように設定します。
　　
　　統合タイプ：Lambda
　　Lambda プロキシ統合の使用：チェックなし
　　Lambda リージョン：Lambdaを作成したリージョンを選択
　　Lambda 関数：作成したLambda関数を選択
　　実行ロール：上で作成した、Lambda_List_and_Invoke_Roleを選択
　　発信者の認証情報を使用した呼び出し：チェックなし
　　認証情報キャッシュ：発信者の認証情報をキャッシュキーに追加しない
　　デフォルトタイムアウト：そのまま

　　-> 下へスクロールし、[マッピングテンプレート]を選択

　　リクエスト本文のパススルー：テンプレートが定義されていない場合 (推奨)
　　Cotent-Type：マッピングテンプレートの追加を選択し、application/jsonを入力

　　-> application/json をクリックすると、**さらに下にスクロールできるようになる**
　　以下のコードを入力

```
 {
 "username": "$util.urlDecode($input.params('username'))",
 "password": "$util.escapeJavaScript($input.params('Password')).replaceAll("\\'","'")",
 "protocol": "$input.params('protocol')",
 "serverId": "$input.params('serverId')",
 "sourceIp": "$input.params('sourceIp')"
 }
```

　　保存が完了したら、再びGETに戻ります。

12 [メソッドレスポンス]を選択し、▶︎ を選択し行を展開し、200 のレスポンス本文 の[鉛筆]マークを選択する

![スクリーンショット 2021-12-23 23.43.57.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/267420/ce98055e-9d73-0b9e-acb9-c0c6cefa5299.png)

13 コンテンツタイプに、application/jsonを入力し、モデルで先ほど作成したモデルを選択。

14 左のナビゲーションより、[リソース] を選択。

15 [リソース] の[アクション]より、[APIのデプロイ] を選択　　

16 以下名前を任意で設定

　　　　　デプロイされるステージ：任意（初回は新しいステージ）
　　　　　ステージ名*：任意
　　　　　ステージの説明：任意
　　　　　デプロイメントの説明

:::note warn
API Gatewayは変更するたびにデプロイしないと反映しないため注意
:::

これでAPI Gatewayの設定は完了です。

次はTransferFamilyを設定します。

## TransferFamily
① AWS Transfer Family のコンソール画面へ移動し、 [サーバーを作成] を選択する。
　　以下を設定していく

　　　　　プロトコルを選択：SFTP
　　　　　IDプロパイダ：カスタム
　　　　　カスタムプロパイダ：作成したAPI GateWay のステージに表示されているエンドポイントURL
　　　　　ロール：作成したAWS Transfer Family用ポリシーがアタッチされているロール
　　　　　　-> TransferFamily_Role
　　　　　エンドポイントのタイプ：パブリックアクセス可能
　　　　　カスタムホスト名：なし
　　　　　ドメイン：S3
　　　　　CloudWatch ログ記録：任意
　　　　　暗号化アルゴリズムのオプション：最新のものを選択
　　　　　サーバーホストキー：なし
　　　　　タグ：任意
　　　　　アップロード後の処理：設定不要
　　　　　
　　　　　→ 設定完了後、[サーバーを作成]を選択し、数分待ちSFTPサーバーがオンラインになるのを待つ

これでTransfer Familyの設定は完了です。

次は、SecretManagerを設定します。

## AWS Secret Manager

① AWS Secret Manager のコンソール画面へ移動し、[新しいシークレットを保存する] を選択し、以下を設定

　　　　　シークレットのタイプ：その他のシークレットのタイプ
　　　　　暗号化キー ：DefaultEncryptionKey（任意）
　　　　　[キー: 値のペア]：（以下）
　　　　　username: 任意　下のnoteを参照
　　　　　Password: 任意
　　　　　Role: 作成したS3アクセス用ポリシーと
　　　　　　　　 APIGateway GET用ポリシーが付与されたロールのARN
　　　　　　-> TransferFamily_RoleのARN
　　　　　HomeDirectory: 作成した{S3バケット名/ディレクトリ}
　　　　　　-> /test-bucket/sftp
　　　　　serverId: 作成したAWS Trasnfer Family のサーバー名

　　　　　次へ進みます。

:::note info
　　-> シークレットの名前：SFTP/{任意の名前}
　　　※ SFTP/ 以降が接続に使用する username となるため一致させる必要があります。
　　　　例：SFTP/test_user であれば[test_user]がusernameになります。
　　　リソースのアクセス許可 - オプション：任意
:::

Lambdaの設定に戻ります

## Lambdaの設定 Part2

① Part1で作成した関数を選択すると、API Gatewayがトリガーされています。

② [設定]タブの左のナビゲーションの[アクセス権限]より、[実行ロール]を設定します
　　-> Lambda_Get_Secret_Manager_Key_Role

③ 左のナビゲーションの[環境変数]より、キー：「SecretsManagerRegion」値：「{リージョン}」を設定します。

④ [テスト]を選択し、以下を入力します。
　　

```
{
  "username": "{SecretManagerで設定したusername}",
  "password": "{SFTPのパスワード}",
  "serverId": "{TransferFamilyのSFTPサーバーID}",
  "protocol": "SFTP"
}
```

⑤ 作成した[テスト]を実行し、成功するとSecretManagerのRoleArnとHomeDirectoryを返却します。 

```
{
  "Role": "arn:aws:iam::123450123456:role/TransferFamily_Role",
  "HomeDirectory": "/test_bucket/SFTP"
}
```

これでLambdaの設定も完了です

## 環境の確認

ファイル転送プロトコルクライアントを使用しファイルを転送する
　　・FileZilla
　　　https://filezilla-project.org/download.php?type=client

ここではfilezillaを使用します。

　　① [ファイル] タブを選択し、[サイトマネージャ]を選択

　　② [新しいサイト]を選択

　　③ [一般]タブで、[プロトコル] より SFTP を選択する

　　④ [ホスト] に、以下を入力
　　　　　sftp://hostname
　　　　　※hostnameは、SFTPサーバーのエンドポイント。
　　　　　　AWS Trasfer Familyで作成したサーバーを選択し確認できる。

　　⑤ [ポート] に 22 を設定

　　⑥ [ログオンタイプ] で [パスワードを訪ねる] を選択する

　　⑦ [ユーザー] 名を入力する
　　　　　■ AWS Secret Managerのシークレット名の SFTP/以降がユーザー名となります

　　⑧ [接続] を選択して接続の完了を確認する

　　⑨ バケット領域へファイルをドラッグ＆ドロップし、転送されることをS３で確認

　　⑩ 更新や削除も、ロールにupdateやdeleteが付与してあれば可能です


お疲れ様でした