VPC InternalなAWS Transfer for FTPをTerraformで構築する

# はじめに
AWS Transfer Family第2弾。
以前の記事はこちら。
- [EIP＋セキュリティグループでセキュアにしたAWS Transfer for SFTPをTerraformで構築する](https://qiita.com/neruneruo/items/91c4b091adc9eaa62daf)

前回はSFTPのプロトコルで実装したが、要件上InternalなのでFTPでも充分な場合の実装をしてみる。

# IAM
IAMは[前回の記事](https://qiita.com/neruneruo/items/91c4b091adc9eaa62daf#iam)と同じ内容で、サービスロールとユーザ用の両方を用意する。Lambda用のパーミッションも必要になるが、それはLambdaの項でまとめて書く。

```HCL
resource "aws_iam_role" "ftp_server" {
  name               = local.ftp_server_role_name
  assume_role_policy = data.aws_iam_policy_document.ftp_server_assume.json
}

data "aws_iam_policy_document" "ftp_server_assume" {
  statement {
    effect = "Allow"

    actions = [
      "sts:AssumeRole",
    ]

    principals {
      type = "Service"
      identifiers = [
        "transfer.amazonaws.com",
      ]
    }
  }
}

resource "aws_iam_role_policy" "ftp_server" {
  role   = aws_iam_role.ftp_server.name
  name   = local.ftp_server_policy_name
  policy = data.aws_iam_policy_document.ftp_server_custom.json
}

data "aws_iam_policy_document" "ftp_server_custom" {
  statement {
    effect = "Allow"

    actions = [
      "logs:CreateLogGroup",
      "logs:CreateLogStream",
      "logs:PutLogEvents",
    ]

    resources = [
      "*",
    ]
  }
}

resource "aws_iam_role" "ftp_user" {
  name               = local.ftp_user_role_name
  assume_role_policy = data.aws_iam_policy_document.ftp_user_assume.json
}

data "aws_iam_policy_document" "ftp_user_assume" {
  statement {
    effect = "Allow"

    actions = [
      "sts:AssumeRole",
    ]

    principals {
      type = "Service"
      identifiers = [
        "transfer.amazonaws.com",
      ]
    }
  }
}

resource "aws_iam_role_policy" "ftp_user" {
  role   = aws_iam_role.ftp_user.name
  name   = local.ftp_user_policy_name
  policy = data.aws_iam_policy_document.ftp_user_custom.json
}

data "aws_iam_policy_document" "ftp_user_custom" {
  statement {
    effect = "Allow"

    actions = [
      "s3:*",
    ]

    resources = [
      "${aws_s3_bucket.ftp.arn}",
      "${aws_s3_bucket.ftp.arn}/*",
    ]
  }
}
```

# セキュリティグループ
VPCエンドポイントにアタッチするセキュリティグループは、最低限、以下のように用意しておこう。
アクセス元のVPCが明確なのであれば、そこで絞り込みをするとよりセキュアになる。

なお、ポートは[公式のユーザーガイド](https://docs.aws.amazon.com/ja_jp/transfer/latest/userguide/create-server-ftp.html)に記載されているとおり、制御ポートとして21、データ転送ポートとして8192～8200を使用するので、その範囲を開けておく。

```HCL
resource "aws_security_group" "ftp" {
  vpc_id = data.aws_vpc.my.id
  name   = local.security_group_name

  ingress {
    from_port   = 21
    to_port     = 21
    protocol    = "TCP"
    cidr_blocks = ["0.0.0.0/0"]
  }

  ingress {
    from_port   = 8192
    to_port     = 8200
    protocol    = "TCP"
    cidr_blocks = ["0.0.0.0/0"]
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
}
```

# S3バケット
S3バケットも[前回の記事](https://qiita.com/neruneruo/items/91c4b091adc9eaa62daf#s3%E3%83%90%E3%82%B1%E3%83%83%E3%83%88)と同じ内容で用意する。同じ内容と言いながらも中身が微妙に違うのは、前回記事を書いた後にTerraformとAWS両方のアップデートが間に入っているためだ。

```HCL
resource "aws_s3_bucket" "ftp" {
  bucket = local.s3_bucket_name

  force_destroy = true
}

resource "aws_s3_bucket_ownership_controls" "ftp" {
  bucket = aws_s3_bucket.ftp.id

  rule {
    object_ownership = "BucketOwnerEnforced"
  }
}

resource "aws_s3_bucket_public_access_block" "ftp" {
  bucket = aws_s3_bucket.ftp.id

  block_public_acls       = true
  block_public_policy     = true
  ignore_public_acls      = true
  restrict_public_buckets = true
}
```

# Lambda
今回の構成の場合、認証をLambdaで行うことになる。
スクリプトとTerraformを以下のように作成する。

## スクリプト
スクリプトは、[公式のユーザーガイド](https://docs.aws.amazon.com/ja_jp/transfer/latest/userguide/custom-identity-provider-users.html#authentication-lambda-examples)に従い、以下のように書いておく。公式でベタ書きしている部分は、環境変数で外部から注入できるようにしておこう。

```Python:auth.py
import os
import json

def lambda_handler(event, context):
  if event['serverId'] != '' and event['username'] == os.environ['FTP_USER_NAME'] and event['password'] == os.environ['FTP_PASSWORD']:
    response = {
      'Role': os.environ['IAM_ROLE_ARN'],
      'HomeDirectoryType': "LOGICAL",
      'HomeDirectoryDetails': json.dumps([
        {
          'Entry': '/',
          'Target': '/' + os.environ['S3_BUCKET_ID']
        }
      ])
    }
  else:
    response = {}

  return response
```

## Terraform
こちらも基本は[公式のユーザーガイド](https://docs.aws.amazon.com/ja_jp/transfer/latest/userguide/custom-identity-provider-users.html#authentication-lambda-examples)に従い作っていく。

### Lambda関数
```HCL
data "archive_file" "ftp_auth" {
  type        = "zip"
  source_dir  = "../scripts/auth"
  output_path = "../outputs/auth.zip"
}

resource "aws_lambda_function" "ftp_auth" {
  depends_on = [
    aws_cloudwatch_log_group.lambda_ftp_auth,
  ]

  function_name    = local.lambda_function_name
  filename         = data.archive_file.ftp_auth.output_path
  role             = aws_iam_role.lambda_ftp_auth.arn
  handler          = "auth.lambda_handler"
  source_code_hash = data.archive_file.ftp_auth.output_base64sha256
  runtime          = "python3.10"

  memory_size = 128
  timeout     = 30

  environment {
    variables = {
      FTP_USER_NAME = local.ftp_user_name
      FTP_PASSWORD  = local.ftp_password
      S3_BUCKET_ID  = aws_s3_bucket.ftp.id
      IAM_ROLE_ARN  = aws_iam_role.ftp_user.arn
    }
  }
}
```
### リソースベースポリシー
リソースベースポリシーは、Terraformの場合は```aws_lambda_permission```のリソースを使用する。

```HCL
resource "aws_lambda_permission" "ftp_auth" {
  statement_id  = "AllowExecutionFromTransfer"
  action        = "lambda:InvokeFunction"
  function_name = aws_lambda_function.ftp_auth.function_name
  principal     = "transfer.amazonaws.com"
}
```

### ログ取得
```HCL
resource "aws_cloudwatch_log_group" "lambda_ftp_auth" {
  name              = "/aws/lambda/${local.lambda_function_name}"
  retention_in_days = 3
}
```

### IAM
```HCL
resource "aws_iam_role" "lambda_ftp_auth" {
  name               = local.lambda_ftp_auth_role_name
  assume_role_policy = data.aws_iam_policy_document.lambda_ftp_auth_assume.json
}

data "aws_iam_policy_document" "lambda_ftp_auth_assume" {
  statement {
    effect = "Allow"

    actions = [
      "sts:AssumeRole",
    ]

    principals {
      type = "Service"
      identifiers = [
        "lambda.amazonaws.com",
      ]
    }
  }
}

resource "aws_iam_role_policy" "lambda_ftp_auth_custom" {
  name   = local.lambda_ftp_auth_policy_name
  role   = aws_iam_role.lambda_ftp_auth.id
  policy = data.aws_iam_policy_document.lambda_ftp_auth_custom.json
}

data "aws_iam_policy_document" "lambda_ftp_auth_custom" {
  statement {
    effect = "Allow"

    actions = [
      "logs:CreateLogGroup",
      "logs:CreateLogStream",
      "logs:PutLogEvents",
    ]

    resources = [
      "*",
    ]
  }
}
```

# AWS Transfer for FTP
さて、ここまで作れれば後は簡単だ。
```endpoint_type```は```VPC```で設定しておく。

上で作成したLambdaのARNを、functionで指定すれば勝手に呼び出してくれる。

VPCエンドポイントを永続で保持するのであれば、自動払い出しをやめることはできないので、[前回記事](https://qiita.com/neruneruo/items/91c4b091adc9eaa62daf#vpc%E3%82%A8%E3%83%B3%E3%83%89%E3%83%9D%E3%82%A4%E3%83%B3%E3%83%88)を参考にしながら```terraform import```をしよう。

```HCL
resource "aws_transfer_server" "ftp" {
  protocols     = ["FTP"]
  endpoint_type = "VPC"

  identity_provider_type = "AWS_LAMBDA"
  function               = aws_lambda_function.ftp_auth.arn

  endpoint_details {
    vpc_id             = data.aws_vpc.my.id
    subnet_ids         = data.aws_subnets.my_vpc.ids
    security_group_ids = [aws_security_group.ftp.id]
  }

  logging_role = aws_iam_role.ftp_server.arn
}
```

ここまでやれば、FTPサーバ作成時に払い出されたIPアドレスに標準のFTPクライアントで接続できるようになっているはずだ(TerraformのApplyが完了しても、VPCエンドポイントがアクティベーションされるのに2～3分程度追加でかかるので、少し待つようにしｙ)。Lambdaを作る手間を考えると、鍵の管理とあまり変わらない気がするが、鍵ファイルの管理が不要な分、運用が軽いはずだ。オンプレからのデータ移行等で一時的な作成であればこの方式は悪くは無いと思う。