AWS Transfer Family で SFTPサーバーを作ってみたら便利だった話

メルペイSREの @myoshida です。この記事は、Merpay Advent Calendar 2023 の21日目の記事です。

メルカリグループではGoogle Cloud Platform(GCP) を広く利用しており、一般的にはGCPを利用したシステム構築が推奨されています。しかし、他のプラットフォームを利用した方が要件を実現しやすかったり、よりスマートに構築できる場合はAmazon Web Services(AWS)なども利用することあります。

今回はAWS Transfer Familyを利用してSFTPでファイルを送受信する環境を構築した件について簡単にお伝えできればと思います。

SFTPでのファイル送受信について

SFTP(SSH File Transfer Protocol)は、その名の通り、SSHを利用してファイル転送を行います。SSHを利用して暗号化通信が行えるため、FTPと比べて安全に利用できます。
ログインには、SSHで使用する鍵をそのまま認証に利用できます。鍵認証でログインできるため、パスワードは不要です。

一方でFTP(File Transfer Protocol)は、IDとパスワードでログインします。また、暗号化がサポートされていないため、セキュリティ面で問題があり、利用は推奨されません。

SFTPは昔から存在する枯れた方式だと思いますが、業務の現場では今も根強く採用されています。日次のバッチで処理して作られたCSVを、連携先の外部企業に渡すといった場面で利用されたりします。

AWS Transfer Family での SFTP環境構築

AWS Transfer Family を利用したシステム構成は以下のようになります。

構成図

SFTPサーバーに該当する Transfer Server を用意し、利用するサブネットの数だけEIPを払い出し、Transfer Serverに紐づけます。それによりTransfer Serverに専用のエンドポイントが割り当てられ、ユーザーはそれを指定してSFTPクライアントで接続できます。

エンドポイントが割り当てられた様子

SFTPユーザーはTranfer Serverに紐づいており、ユーザーごとに公開鍵を複数持つことができます。IAMユーザーを作成する必要はありません。

ストレージはS3バケットを利用します。1つのS3バケットにユーザーごとのホームディレクトリを定義して共用することも可能ですし、ユーザーごとにS3バケットを用意して、ログインするユーザーごとに専用のS3バケットに接続させることも可能です。今回は後者を採用しました。

構築にはTerraformを利用します。locals を利用してユーザー名を変数とすることで、S3バケット・SFTPユーザー・SFTPユーザーが利用するIAMロールなどをまとめて作成することが可能です。

localsの定義例

  sftp_name = "merpay-foo-bar"
  sftp_users = {
    test-user-1 = {
      ssh_keys = [
        "ssh-rsa dummy", 
      ]
    }

    test-user-2 = {
      ssh_keys = [
        "ssh-rsa dummy", 
      ]
    }

  }

  sftp_user_keys = flatten([
    for user, attrs in local.sftp_users : [
      for ssh_key in attrs["ssh_keys"] : {
        user    = user
        ssh_key = ssh_key
      }
    ]
  ])
}

ログインに利用する公開鍵は、上記terraform内の ssh-keys にリストで列挙することでterraform経由でSFTPユーザーに保持させることも可能ですが、今回はユーザー作成後にAWSにログインして、手動で登録することにしました。

S3バケットの定義例

resource "aws_s3_bucket" "sftp_bucket" {
  for_each = local.sftp_users
  bucket   = "${local.sftp_name}-${each.key}"

  versioning {
    enabled = true
  }

  logging {
    target_bucket = aws_s3_bucket.sftp-bucket-log[each.key].id
    target_prefix = "log/"
  }

  tags = {
  }
}

IAMポリシーの定義例

resource "aws_iam_policy" "s3_read_write" {
  for_each    = local.sftp_users
  name        = "s3_rw_merpay-sftp-${each.key}"
  path        = "/system/"
  description = "for enabling file tansfer to buckets"

  policy = <<EOF
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "s3:ListBucket",
                "s3:GetBucketLocation"
            ],
            "Resource": "arn:aws:s3:::${local.sftp_name}-${each.key}"
        },
        {
            "Effect": "Allow",
            "Action": [
                "s3:PutObject",
                "s3:GetObject",
                "s3:DeleteObject",
                "s3:GetObjectAcl",
                "s3:PutObjectAcl",
                "s3:GetObjectVersion",
                "s3:DeleteObjectVersion"
            ],
            "Resource": "arn:aws:s3:::${local.sftp_name}-${each.key}/*"
        }
    ]
}
EOF
}

IAMロールの定義例

resource "aws_iam_role" "sftp_user" {
  for_each = local.sftp_users
  name     = "transfer-server-user-role-${each.key}"

  assume_role_policy = <<-EOF
    {
      "Version": "2012-10-17",
      "Statement": [
        {
        "Effect": "Allow",
        "Principal": {
            "Service": "transfer.amazonaws.com"
        },
        "Action": "sts:AssumeRole"
        }
      ]
    }
    EOF
}

resource "aws_iam_role" "transfer_server_to_cloudwatch" {
  name = "transfer-server-to-cloudwatch-role"

  assume_role_policy = <<-EOF
    {
      "Version": "2012-10-17",
      "Statement": [
        {
        "Effect": "Allow",
        "Principal": {
            "Service": "transfer.amazonaws.com"
        },
        "Action": "sts:AssumeRole"
        }
      ]
    }
    EOF
}

IAMロールのポリシーアタッチメントの定義例

resource "aws_iam_role_policy_attachment" "s3_bucket_read_write" {
  for_each   = local.sftp_users
  role       = aws_iam_role.sftp_user[each.key].name
  policy_arn = aws_iam_policy.s3_read_write[each.key].arn
}

Transfer Serverの定義例

"aws_transfer_server" は endpoint_type を “VPC” にし、endpoint_details ブロック内でEIPを割り当てることで、マネージドなドメインが生成されます。

resource "aws_transfer_server" "sftp" {
  identity_provider_type = "SERVICE_MANAGED"
  endpoint_type          = "VPC"
  logging_role           = aws_iam_role.transfer_server_to_cloudwatch.arn

  endpoint_details {
    address_allocation_ids = [for eip in aws_eip.sftp : eip.id]
    subnet_ids             = aws_subnet.sftp_subnet[*].id
    vpc_id                 = aws_vpc.sftp.id
  }
  tags = {
    Name        = local.sftp_name
  }

  lifecycle {
    ignore_changes = all
  }
}

SFTP Userの定義例

SFTPユーザーのホームディレクトリは、"aws_transfer_user" 内の home_directory で、S3バケットのルートを指定しました。localsを参照してユーザーごとに作られるS3バケットをそのまま指定しているので、ユーザーごとに別のS3バケットを利用できるようになります。

resource "aws_transfer_user" "sftp_user" {
  for_each       = local.sftp_users
  server_id      = aws_transfer_server.sftp.id
  user_name      = each.key
  role           = aws_iam_role.sftp_user[each.key].arn
  home_directory = "/${aws_s3_bucket.sftp_bucket[each.key].id}/"
}

環境を構築してみて感じた利点

SFTPサーバの環境を作るにあたって、AWS Transfer FamilyとTerraformで利用することで、以下のようなメリットがあると感じました。

手動管理の量が少ない

マネージドな環境ですので、一度構築してしまえば、かなりメンテナンスフリーな感じで利用することができます。EC2などのサーバインスタンスを用意することもないため、管理がラクです。
アカウント追加・削除の作業もTerraformを更新することで行なうので、GitHubのPull Requestを通じてチーム内で確認を取りながら進められて安全です。
S3にはライフサイクルを指定しているため、古いファイルを削除するといった作業も発生しません。

横展開がしやすい

これは単純にTerraformの利点なのですが、.tfファイルにほぼすべての構築内容が定義されているため、類似の案件が発生した場合に流用しやすいです。

他のシステムとのつなぎ込みがしやすい

ファイルはS3に保存されるため、AWSのAPIを利用してファイルを取得したりすることで、業務の後続処理もスムーズに行わせることができます。

おわりに

今回はメルカリグループでは利用例が少ないAWSを利用したSFTP環境の構築について説明しました。既存のSFTP環境のリプレイスなどのお役に立てば幸いです。
Google Cloud Platformでも同様のサービスが登場してほしいなと思います。

明日の記事は @orfeonさんです。引き続きMerpay Advent Calendar 2023をお楽しみください。

  • X
  • Facebook
  • linkedin
  • このエントリーをはてなブックマークに追加