はじめに
AWS Transfer Family第2弾。
以前の記事はこちら。
前回はSFTPのプロトコルで実装したが、要件上InternalなのでFTPでも充分な場合の実装をしてみる。
IAM
IAMは前回の記事と同じ内容で、サービスロールとユーザ用の両方を用意する。Lambda用のパーミッションも必要になるが、それはLambdaの項でまとめて書く。
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が明確なのであれば、そこで絞り込みをするとよりセキュアになる。
なお、ポートは公式のユーザーガイドに記載されているとおり、制御ポートとして21、データ転送ポートとして8192~8200を使用するので、その範囲を開けておく。
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バケットも前回の記事と同じ内容で用意する。同じ内容と言いながらも中身が微妙に違うのは、前回記事を書いた後にTerraformとAWS両方のアップデートが間に入っているためだ。
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を以下のように作成する。
スクリプト
スクリプトは、公式のユーザーガイドに従い、以下のように書いておく。公式でベタ書きしている部分は、環境変数で外部から注入できるようにしておこう。
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
こちらも基本は公式のユーザーガイドに従い作っていく。
Lambda関数
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
のリソースを使用する。
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"
}
ログ取得
resource "aws_cloudwatch_log_group" "lambda_ftp_auth" {
name = "/aws/lambda/${local.lambda_function_name}"
retention_in_days = 3
}
IAM
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エンドポイントを永続で保持するのであれば、自動払い出しをやめることはできないので、前回記事を参考にしながらterraform import
をしよう。
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分程度追加でかかるので、少し待つようにしy)。Lambdaを作る手間を考えると、鍵の管理とあまり変わらない気がするが、鍵ファイルの管理が不要な分、運用が軽いはずだ。オンプレからのデータ移行等で一時的な作成であればこの方式は悪くは無いと思う。