mito’s blog

IT技術メインの雑記。思い立ったが吉日。

[terraform] S3とTransferFamilyでsftpサーバを作る

この記事は、terraform Advent Calendar 2024 の7日目のエントリです。

はじめに

terraformで、AWS S3とTransferFamilyを組み合わせて、さっとsftpサーバを作ります。
sftpユーザや鍵ファイルも併せて作るので、apply後にsftp接続できます。 なお、tfstateに公開鍵も秘密鍵も記載されているため気を付けてください。
(Transfer Familyを触ったのは初めてでした。知らないサービス多すぎ!)

AWS Transfer Familyとは
完全マネージドなファイル転送サービスで、SFTP、FTPS、FTPを使用して、安全にデータをAWSに移動できます。
各接続方式や接続先サービス(s3、EFS)を選択したサーバを用意し、それに対して、アクセスするユーザやロール、接続先(S3バケット名など)を指定します。 Transferのサーバーはあくまで接続に関する情報のまとまりであり、例えばS3バケット名を紐づけるわけではないです。そのため、1つのサーバーに対し、異なるロールや異なるアクセス先をもつユーザを設定できます。

参考 Terraform Registry




環境

環境と設定

  • terraform : v1.9.4
  • バケット名 : advent-server
  • sftpユーザ名 : sftp-advent


ファイル構成

$ tree ./
./
├── main.tf
├── output.tf
├── terraform.tfvars
└── variables.tf


各ファイルについて

terraform.tfvars

バケット名、sftpユーザ名、アクセス記録のためのロググループの変数を用意します。

bucket_name = "advent-server"
sftp_user = "sftp-advent"
log_group = "/transfer/advent-server"


variables.tf

各変数を定義します。

variable "bucket_name" {
  type = string
}

variable "sftp_user" {
  type = string
}

variable "log_group" {
  type = string
}


main.tf

作成するリソースとポイントを記載します。

  • s3バケット
    • force_destroy = true
  • transferに設定するロールとポリシー
    • ロールにポリシーをアタッチします
  • transferに設定するロググループ
  • transferサーバー
    • sftp接続で作成します
  • sftpユーザと鍵ファイル
provider "aws" {
  region     = "ap-northeast-1"
  access_key = "AKIA**********"
  secret_key = "**************"
}

resource "aws_s3_bucket" "bucket" {
  bucket        = var.bucket_name
  force_destroy = true                 # バケット削除時にオブジェクトも削除する
}

resource "aws_iam_role" "transfer_role" {
  name = "TransferFamilyS3AccessRole"
  assume_role_policy = jsonencode({
    Version = "2012-10-17",
    Statement = [
      {
        "Sid" : "",
        "Effect" : "Allow",
        "Principal" : {
          "Service" : "transfer.amazonaws.com"
        },
        "Action" : "sts:AssumeRole"
      }
    ]
  })
}

resource "aws_iam_policy" "s3_access_policy" {
  name = "S3AccessPolicyForTransferFamily"
  policy = jsonencode({
    Version = "2012-10-17",
    Statement = [
      {
        "Sid" : "Statement1",
        "Effect" : "Allow",
        "Action" : [
          "s3:ListBucket",
          "s3:GetBucketLocation"
        ],
        "Resource" : "${aws_s3_bucket.bucket.arn}"
      },
      {
        "Sid" : "Statement2",
        "Effect" : "Allow",
        "Action" : [
          "s3:PutObject",
          "s3:GetObject",
          "s3:DeleteObject"
        ],
        "Resource" : "${aws_s3_bucket.bucket.arn}/*"
      }
    ]
  })
}

resource "aws_iam_role_policy_attachment" "attach_s3_policy" {
  role       = aws_iam_role.transfer_role.name
  policy_arn = aws_iam_policy.s3_access_policy.arn
}

resource "aws_cloudwatch_log_group" "transfer" {
  name_prefix = var.log_group
}

resource "aws_transfer_server" "sftp_server" {
  domain                 = "S3"
  protocols              = ["SFTP"]             # SFTPは鍵認証のみ使えます
  identity_provider_type = "SERVICE_MANAGED"
  endpoint_type          = "PUBLIC"             # 外部から接続できるようにする

  structured_log_destinations = [
    "${aws_cloudwatch_log_group.transfer.arn}:*"
  ]

  tags = {
    Name = "transfer-${var.bucket_name}"
  }

}

resource "aws_transfer_user" "sftp_user" {
  server_id      = aws_transfer_server.sftp_server.id
  user_name      = var.sftp_user
  role           = aws_iam_role.transfer_role.arn
  home_directory = "/${var.bucket_name}"
}

resource "tls_private_key" "sftp_tls" {
  algorithm = "RSA"
  rsa_bits  = 4096
}

resource "aws_transfer_ssh_key" "sftp_ssh_key" {      # SFTP接続は鍵認証のみ。
  server_id = aws_transfer_server.sftp_server.id
  user_name = aws_transfer_user.sftp_user.user_name
  body      = trimspace(tls_private_key.sftp_tls.public_key_openssh)
}

resource "local_file" "private_key_file" {
  content         = tls_private_key.sftp_tls.private_key_pem
  file_permission = "0600"
  filename        = pathexpand("~/.ssh/${var.sftp_user}.pem")
}


output.tf

接続するためのsftpエンドポイントを表示します。

output "sftp_endpoint" {
  value = aws_transfer_server.sftp_server.endpoint
}


terraformの実行

terraform apply

transferの作成に3分ほどかかりました。

$ terraform apply

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # aws_cloudwatch_log_group.transfer will be created
  + resource "aws_cloudwatch_log_group" "transfer" {
(略)

Plan: 10 to add, 0 to change, 0 to destroy.

Changes to Outputs:
  + sftp_endpoint = (known after apply)

Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.

  Enter a value: yes

tls_private_key.sftp_tls: Creating...
aws_cloudwatch_log_group.transfer: Creating...
aws_iam_role.transfer_role: Creating...
aws_s3_bucket.bucket: Creating...
(略)
Apply complete! Resources: 10 added, 0 changed, 0 destroyed.

Outputs:

sftp_endpoint = "******.server.transfer.ap-northeast-1.amazonaws.com"
$ 


sftp接続

sftp接続し、ファイルのアップロードやダウンロードが確認できました。

$ sftp -i ~/.ssh/sftp-advent.pem sftp-advent@******.server.transfer.ap-northeast-1.amazonaws.com
Connected to ******.server.transfer.ap-northeast-1.amazonaws.com.
sftp> pwd
Remote working directory: /advent-server
sftp> ls
sftp> put main.tf 
Uploading main.tf to /advent-server/main.tf
maintf                 100% 2565    59.0KB/s   00:00    
sftp> ls
main.tf  
sftp> get main.tf 
Fetching /advent-server/main.tf to main.tf
main.tf       
sftp> exit
$ 


その他

terraform destroyで、バケットにファイルが残っていても全て削除できます。ローカルの秘密鍵も削除されます。

また、カスタムホスト名を付与するならRoute53でホストゾーンとレコードを作成します。
マネジメントコンソールでカスタムホスト名を付けてPlanしたところ、タグのみの差異だったため、ちょっとめんどくさがってコードにそのタグを付けたらホストゾーンとレコードも作成されました。この場合、destroyしてもホストゾーンは残っていたので、このやり方はあまりよくないんだろうなと思いました。