変更履歴
- 2026-10-31:公開
- 2026-11-08:文言修正・説明の追加
初めに
外部のシステムから SFTP でファイルを受け取り、Windows のファイルサーバーに置きたい場面がある。
EC2 に SFTP サーバーを立て、Amazon FSx for Windows File Server(以下、FSx for Windows)をマウントすれば実現できるが、どうせなら受け口はマネージドの AWS Transfer Family に任せたい。
ただし、Transfer Family の保存先に選べるのは Amazon S3 と Amazon EFS で、FSx for Windows は選べない。そこで、いったん S3 で受けて、FSx for Windows へ写す。
FSx for NetApp ONTAP なら S3 アクセスポイント経由で直接書けるが、今回は FSx for Windows を使う前提で外した。
今回は、AWS の公式ブログでも紹介されている、Transfer Family → S3 → Lambda → FSx for Windows の流れを使った。
Lambda の起動方法を 3 通りで比べ、SFTP でファイルを送り終えてから、FSx for Windows にファイルが置かれるまでの時間を測った。題名の「リアルタイム」は、送っている途中から書き写すことではなく、送り終えてから数秒のうちに置かれることを指す。
| 起動方法 | 概説 |
|---|---|
| ① S3 のイベント通知 | S3 にオブジェクトができると、S3 が Lambda を呼ぶ。Transfer Family を経由したかどうかは問わない |
| ② Transfer Family のマネージドワークフロー1 | ファイルを受け取り終えると、Transfer Family がワークフローの手順として Lambda を呼ぶ。Lambda は処理の成否を Transfer Family に返す |
| ③ EventBridge | Transfer Family が出す「ファイルの受信完了」のイベントを EventBridge のルールで拾い、Lambda を呼ぶ |
あわせて、SFTP の通信を途中で切ったときに、不完全なファイルが FSx for Windows に置かれないかも確かめた。
結論
正常に送り終えたファイルが FSx for Windows に置かれるまでの時間
Lambda は、FSx for Windows に一時の名前で書いてから、元の名前に置き換える。SFTP でファイルを送り終えてから、FSx for Windows に元の名前のファイルが置かれるまでの秒数を、Lambda を起動する 3 通りの方法で比べた。FSx for Windows に置かれたファイルは、どれも送ったファイルとファイルサイズが同じだった(中身のハッシュは比べていない)。
Lambda は、一度呼ばれると、処理を動かす環境(実行環境)をしばらく残し、次の呼び出しで使い回す。使い回せれば準備が要らないので速い。しばらく呼ばれないと環境は片付けられ、次のファイルは環境の準備から始まる(コールドスタート)。なので、結果は次の 2 つに分けた。
- 続けて送ったとき:直前にも Lambda が呼ばれていて、環境を使い回せた状態。ファイルサイズ違いで 10 回ずつ検証した。中央値と最大値(括弧の中)は以下の通り。
- 環境を作り直した後の最初のファイル:Lambda の設定を変えて実行環境を作り直し、コールドスタートにした状態。間が空いて環境が片付けられたときの代わりで、実際に長く間を空けては測っていない。1 MB のファイルで 3 回測った平均。
| 起動方法 | 続けて送ったとき 1 MB [s] | 同 10 MB [s] | 同 100 MB [s] | 環境を作り直した後の最初のファイル(1 MB)[s] |
|---|---|---|---|---|
| S3 のイベント通知 | 1.26(1.52) | 1.36(1.53) | 2.03(2.13) | 2.48 |
| マネージドワークフロー | 0.77(0.85) | 0.82(0.93) | 1.82(1.89) | 1.98 |
| EventBridge | 0.77(1.89) | 0.52(1.04) | 1.58(2.60) | 1.78 |
検証時の FSx for Windows は、Single-AZ・スループット 32 MB/s の構成。全ての検証の記録は、6 章に掲載している。
中央値では S3 のイベント通知がいちばん遅く、1 MB・10 MB でほかより 0.5〜0.8 秒長かった。差は、送信の完了から Lambda の処理が始まるまでの時間で出た(6 章)。EventBridge は中央値では速いが、1 MB と 100 MB ではばらつきが大きかった。
SFTP の通信を途中で切ったとき
SFTP の通信を途中で切り、不完全なファイルが S3 に置かれるケースも実施した。100 MB のファイルを半分の 50 MB まで送ったところで接続を切り、起動方法ごとに 1 回ずつ試した。S3 のイベント通知だけが、不完全な 50 MB のファイルを元の名前で FSx for Windows に置いた。マネージドワークフローと EventBridge では、切ってから 5 分後まで見た範囲で、FSx for Windows に何も置かれなかった。
本記事の構成
- 構成
- Transfer Family の設定値
- Lambda を起動する 3 通りの方法
- FSx for Windows へ書き込む Lambda
- 測定方法
- 結果
- 注意点
- 費用
1. 構成
検証の構成は以下の通り。
EC2 から SFTP で送ったファイルを Transfer Family が S3 に置き、①〜③のどれかの方法で Lambda が起動して FSx for Windows に書く。EC2 は同時に FSx for Windows を見て、ファイルが置かれた時刻を取る。
リージョンは東京(ap-northeast-1)。環境は CloudFormation のテンプレートで作った。
作ったものは以下の通り。
| 構成要素 | 設定 |
|---|---|
| AWS Transfer Family | 外部からの SFTP の受け口。SFTP・パブリックのエンドポイント・保存先は S3。ID プロバイダーはサービスマネージドで、SSH の鍵で認証する |
| Lambda | Python 3.12・arm64・メモリ 1024 MB。VPC の中に置き、S3 から読んで FSx for Windows に書く |
| FSx for Windows | ファイルを置く先の Windows のファイルサーバー。1 つの AZ に置く構成(デプロイの種類は SINGLE_AZ_2)・SSD 32 GiB(選べる最小)。スループット容量は 32 MB/s(8 MB/s から選べる)。短い時間ならこれを超えられる |
| AWS Managed Microsoft AD | FSx for Windows は AD への参加が必須なので、検証用に Standard を作った |
| EC2(t4g.small) | ファイルを送ってくる外部のシステムの代わり。SFTP のクライアントとして Transfer Family へファイルを送る。同時に FSx for Windows を SMB で見て、ファイルが置かれた時刻を取る |
Lambda にマウントできるファイルシステムは EFS だけで、FSx for Windows はマウントできない。なので、Lambda のコードが SMB(Windows のファイル共有のプロトコル)のクライアントになり、FSx for Windows に接続して書き込む。今回は Python の smbprotocol2(1.17.0)を使った。
Lambda はインターネットへの出口が無いサブネットに置いた。なので、S3(ゲートウェイ型)と Secrets Manager の VPC エンドポイントを作った。マネージドワークフローで成否を返すときは、Transfer Family の API のエンドポイントも要る。
2. Transfer Family の設定値
Transfer Family のサーバーとユーザーの設定値を、コンソールの作成画面の順に並べる。S3 で受けて Lambda で FSx for Windows に写す今回の構成で効くのは、保存先(ドメイン)・ID プロバイダー・エンドポイント・マネージドワークフロー・ホームディレクトリの 5 つ。残りは、今回は既定のままにした。遅延と、通信が途中で切れたときの結果だけを読みたい人は、3 章へ進んでよい。
表の「推奨」には、公式の文書が勧めているものに「(公式)」、外部から SFTP でファイルを受ける今回の用途での自分の判断に「(自分)」と付けた。既定値と推奨は、2026 年 10 月 30 日に公式の文書で調べた。文書どうしで食い違うものは、その行か、表の下の段落に書いた。
既定値は、作り方(コンソール・API・CLI・CloudFormation)で違うことがあり、違う行にはそれぞれの値を書いた。
サーバー:プロトコルと ID プロバイダー
作成画面のこの部分の設定は以下の通り。
| 設定 | 何を決めるか | 既定値 | 推奨 | 今回 |
|---|---|---|---|---|
| Protocols(プロトコル) | 接続に使うプロトコル。SFTP・FTPS・FTP・AS2 から 1〜4 個 | SFTP | 受けるだけなら SFTP だけ(自分) | SFTP |
| Identity provider(ID プロバイダー) | 利用者の認証のしかた | サービスマネージド | 相手が少なく、鍵で認証できるならサービスマネージド(自分) | サービスマネージド |
ID プロバイダーで、使える認証が決まる。サービスマネージドは鍵だけで、パスワードは使えない。AWS Directory Service(AD)はパスワードだけ。パスワードと鍵を組み合わせたいなら、カスタム(Lambda か API Gateway)になる。
FTP・FTPS を足すと、エンドポイントを VPC にする必要があり、サービスマネージドの ID プロバイダーは使えなくなる。FTPS には ACM の証明書も要る。
サーバー:エンドポイント
作成画面のこの部分の設定は以下の通り。
| 設定 | 何を決めるか | 既定値 | 推奨 | 今回 |
|---|---|---|---|---|
| Endpoint type(エンドポイントのタイプ) | インターネットへの公開のしかた | パブリック | 相手の IP アドレスで絞るなら、VPC(インターネット向け)にしてセキュリティグループで絞る(自分) | パブリック |
| IP address type(IP アドレスのタイプ) | IPv4 だけか、IPv4 と IPv6 の両方(デュアルスタック)か | IPv4 | 相手が IPv6 で来るときだけデュアルスタック(自分) | IPv4 |
| Custom hostname(カスタムホスト名) | 利用者が接続先に打つ名前 | 無し(AWS が付けた名前) | 相手に渡す名前を自分のドメインにしたいなら、Route 53 の別名(自分) | 無し |
| FIPS | 米国の暗号の基準(FIPS 140)に沿ったエンドポイントにするか。北米のリージョンだけ | オフ | 基準への準拠を求められなければオフ(自分) | オフ(東京の作成画面では、項目は出るが選べなかった) |
パブリックのエンドポイントには固定の IP アドレスが無く、IP アドレスは変わりうる。送信元の IP アドレスで絞ることもできず、使えるプロトコルは SFTP だけ。相手側のファイアウォールで許可するときは、IP アドレスではなく DNS 名で書くよう、公式の文書が勧めている。
エンドポイントのタイプと IP アドレスのタイプは、あとから変えられる。ただし、サーバーを止めてから変える必要がある。
サーバー:保存先(ドメイン)
作成画面のこの部分の設定は以下の通り。
保存先(ドメイン)は S3 か EFS の 2 択で、既定は S3。作成したあとは変えられないので、最初に決める。
サーバー:追加の詳細
作成画面のこの部分の設定は以下の通り。
| 設定 | 何を決めるか | 既定値 | 推奨 | 今回 |
|---|---|---|---|---|
| Logging(ログ) | JSON 形式の構造化ログを送る CloudWatch Logs のロググループ | コンソール:有効(新しいロググループを作る)。CloudFormation:指定しなければ無し | 有効(公式) | 従来の形(下の段落) |
| Security policy(セキュリティポリシー) | 接続で許す暗号の組。FIPS 用のポリシーにすると、FIPS ではないものへは戻せない | コンソール・API・CLI:TransferSecurityPolicy-2024-01。CloudFormation:指定しなければ TransferSecurityPolicy-2018-11(コンソールと CloudFormation は画面で確かめた。API・CLI は文書による) |
最新のものを名前で指定する(公式)。2026 年 10 月時点の最新は TransferSecurityPolicy-2025-03。相手のクライアントがその暗号に対応しているかは、先に確かめる(自分) |
指定なし(下の段落) |
| Server host key(ホストキー) | サーバーを示す鍵と、その保存先 | 自動で作られる。保存先は、コンソール:AWS Secrets Manager。API・CLI:Transfer Family が管理(SERVICE_MANAGED。文書による) |
別のサーバーから移るとき以外は変えない(公式) | 自動(CloudFormation で作ったサーバーには RSA の鍵が付いた) |
| Managed workflows(マネージドワークフロー) | ファイルを受け取り終えたときと、途中で切れたときに動かすワークフロー | 無し | 受けたあとの処理を Transfer Family に順に任せたいときに付ける(自分) | 起動方法を比べるときだけ付けた(3 章) |
| Display banner(表示バナー) | 認証の前と後に出す文言。SFTP は認証の後のバナーに対応していない | 無し | 用途に合わせる(自分) | 無し |
| SetStat option | 属性の変更(タイムスタンプなど)の要求で、S3 が返すエラーを無視するか | 無視しない(DEFAULT) |
属性の変更を送ってくるクライアントで、エラーを避けたいなら ENABLE_NO_OP にする。このとき属性は変わらない(公式)。タイムスタンプを実際に残したいなら EFS を使う(公式) |
既定(図10 では「無視」と表示) |
| Directory listing optimization | S3 のフォルダの一覧を速くするか | コンソール:有効。CloudFormation:指定しなければ無効(どちらも画面で確かめた) | 公式の文書に推奨の記載は無い | 指定なし |
| SFTP ports(ポート) | パブリックの SFTP が待ち受けるポート。22 か 2000〜65535 から 3 つまで(2026 年 10 月に追加) | 22 | 相手がポート 22 で出られないときだけ変える(自分) | 22 |
ログは、IAM のログ記録ロールを渡して /aws/transfer/<サーバー ID> に出す従来の形と、指定したロググループに JSON で出す構造化ログの 2 つがある。公式の文書は構造化ログを勧めている。今回は Lambda のログで時間を見たので、Transfer Family のログは従来の形のままにした。
セキュリティポリシーは、作る方法で既定値が変わるうえ、既定値そのものも変わりうると公式の文書に書かれている。サーバーの作成手順の文書には「最新が既定」とあり、セキュリティポリシーの文書と食い違う。今回は、CloudFormation で既定のまま作るとどの値になるかを見るため、指定しなかった。作ったサーバーには TransferSecurityPolicy-2018-11 が付いた(図10)。コンソールで作るときより古い組で、本番なら名前で指定したほうがよい。
図10 では、SetStat のオプションが「無視」と表示されていて、既定値(無視しない)と食い違う。テンプレートでは指定しておらず、理由は確かめていない。
コンソールで構造化ログを一度有効にすると、コンソールからは無効に戻せない(API なら戻せる)。
FTPS・FTP だけで使う設定(TLS セッションの再開・パッシブ IP)と、エンドポイントのタイプが VPC のときだけ使う設定(PROXY プロトコル v2)もこの画面にあるが、今回は使わないので省く。
ユーザー
作成画面のこの部分の設定は以下の通り。
| 設定 | 何を決めるか | 既定値 | 推奨 | 今回 |
|---|---|---|---|---|
| User name(ユーザー名) | 接続に使う名前。3〜100 文字。あとから変えられない | 無し(必須) | 接続してくる相手が分かる名前にする(自分) |
fsxlabuser(検証用) |
| Role(ロール) | ユーザーが S3 を読み書きするときの IAM ロール | 無し(必須) | 受け取る場所(プレフィックス)だけに絞る(自分) | 受け取り用のプレフィックスだけ |
| Policy(セッションポリシー) | 1 つのロールを複数のユーザーで使うときの絞り込み | 無し | ロールを使い回すなら付ける(公式) | 無し |
| Home directory(ホームディレクトリ) | 接続したときの場所と、見える範囲 | 空ならバケットのいちばん上 | 論理ディレクトリで、受け取る場所だけを / に見せる。バケット名を見せず、上にも出られない(公式) |
論理ディレクトリ |
| SSH public key(公開鍵) | 鍵認証に使う公開鍵。RSA・ED25519・ECDSA | 無し | 4096 ビットの RSA か ED25519・ECDSA(自分) | ED25519 |
論理ディレクトリは、CloudFormation では HomeDirectoryType: LOGICAL と、見せ方の対応(HomeDirectoryMappings)で書く。
TransferUser:
Type: AWS::Transfer::User
Properties:
ServerId: !GetAtt Server.ServerId
UserName: fsxlabuser
Role: !GetAtt TransferUserRole.Arn
HomeDirectoryType: LOGICAL
HomeDirectoryMappings:
- { Entry: /, Target: !Sub '/fsxlab-in-${AWS::AccountId}/in' }
ユーザーから見ると / に、バケットの in/ の中身だけが見える。ユーザーのロール(TransferUserRole)が S3 に許すのも、バケットの一覧と、in/ の中のオブジェクトの読み書き・削除だけ。
ユーザーのロール(TransferUserRole)の定義
TransferUserRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- { Effect: Allow, Principal: { Service: transfer.amazonaws.com }, Action: sts:AssumeRole }
Policies:
- PolicyName: bucket
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action: [s3:ListBucket, s3:GetBucketLocation]
Resource: !Sub arn:aws:s3:::fsxlab-in-${AWS::AccountId}
- Effect: Allow
Action: [s3:PutObject, s3:GetObject, s3:DeleteObject, s3:GetObjectVersion, s3:AbortMultipartUpload]
Resource: !Sub arn:aws:s3:::fsxlab-in-${AWS::AccountId}/in/*
コンソールでは、論理ディレクトリを使う設定が「制限付き」という名前になっている。ユーザーの追加(図11)でバケットとフォルダを選び、「制限付き」に印を付ける。
図12 には、論理ディレクトリの割り当て(/ とバケットの in/ の対応)が出ている。
3. Lambda を起動する 3 通りの方法
ファイルを受け取り終えたことを Lambda に伝える方法は 3 通りある。
| 起動方法 | Lambda に届くもの | Lambda がすること |
|---|---|---|
| S3 のイベント通知 | S3 の ObjectCreated イベント |
書き込むだけ |
| マネージドワークフロー | ワークフローのカスタムステップの呼び出し | 書き込んだあと、SendWorkflowStepState で成否を Transfer Family に返す |
| EventBridge |
SFTP Server File Upload Completed イベント |
書き込むだけ |
S3 のイベント通知は、Transfer Family を経由したかどうかを問わず、バケットにオブジェクトができたら Lambda を起動する。今回は、ほかの書き込みで起動しないよう、受け取り用のフォルダ(in/)に絞った。
S3 のイベント通知の設定
バケットの定義の一部。UseS3 は、テンプレートのパラメーター TriggerMode が s3 のときに真になる条件で、そのときだけ通知が付く。
NotificationConfiguration: !If
- UseS3
- LambdaConfigurations:
- Event: s3:ObjectCreated:*
Function: !GetAtt CopyFn.Arn
Filter:
S3Key:
Rules: [{ Name: prefix, Value: in/ }]
- !Ref AWS::NoValue
残りの 2 つは Transfer Family が出すもので、送ってきたユーザーやセッションの情報が付いてくる。
今回のワークフローは以下の通り。Lambda を呼ぶカスタムステップを 1 つ置いただけ。
図13 では、カスタムステップの呼び先に、今回の Lambda が指定されている。
Lambda が起動するのはいつか
正常に送り終えた場合、3 通りとも、Lambda が起動するのはファイルを送り終えた後。送り始めた瞬間に起動するものは無い。S3 のオブジェクトは書き込みが終わるまで置かれず、Transfer Family のイベントとマネージドワークフローも、クライアントがファイルを閉じたところで出る。
これは、送る側が元の名前で直接書いて閉じる場合の話。送っている間だけ別の名前にし、送り終えてから名前を変えるクライアントもある。その場合、閉じた時点ではまだ元の名前になっていない。そのときの動きは試していない。
違いが出るのは、送っている途中で接続が切れたとき。公式の文書によると、ファイルを開いたまま接続が切れると、Transfer Family はそこまでに届いたデータを保存する(実際には全部届いている場合もある)。保存先が S3 なら、途中までのオブジェクトが S3 にできる。
| 起動方法 | 正常に送り終えたとき | 途中で切れたとき | 届け方 |
|---|---|---|---|
| S3 のイベント通知 | オブジェクトができたら起動する(s3:ObjectCreated) |
途中までのオブジェクトでも起動する | 少なくとも 1 回。たいてい数秒で届くが、1 分以上かかることもある |
| マネージドワークフロー |
OnUpload のワークフローが動く(0 バイトのファイルでは動かない。今回は試していない) |
OnUpload は動かない。OnPartialUpload に指定したワークフローが動く |
Transfer Family が実行を管理する |
| EventBridge | SFTP Server File Upload Completed |
SFTP Server File Upload Failed(文書では、failure-code に PARTIAL_UPLOAD などの値が入る。今回はこのイベントを拾っていない) |
ベストエフォート(届く保証は無い) |
S3 のイベント通知だけは、途中で切れたものと、正常に届いたものを区別しない。何もしなければ、途中までのファイルも FSx for Windows に元の名前で置かれる。今回の検証でも、そうなった(6 章)。
マネージドワークフローと EventBridge は、正常に送り終えたときと途中で切れたときが別の入り口で届くので、正常な方だけを FSx for Windows へ送れる。今回のワークフローは OnUpload だけを拾う。EventBridge のルールは、送り元・Upload Completed のイベント・今回のサーバー ID に絞った。
EventBridge のルールの定義
UseEventBridge は、テンプレートのパラメーター TriggerMode が eventbridge のときに真になる条件。
EventRule:
Type: AWS::Events::Rule
Condition: UseEventBridge
Properties:
Description: fsxlab upload completed -> copy to FSx
EventPattern:
source: [aws.transfer]
detail-type: [SFTP Server File Upload Completed]
detail:
server-id: [!GetAtt Server.ServerId]
Targets:
- { Id: copy, Arn: !GetAtt CopyFn.Arn }
同じ通知が 2 回届くとき
S3 のイベント通知は同じ通知が 2 回届くことがあるので、Lambda は同じファイルを 2 回書いても困らない作りにしておく。今回の Lambda は、一時の名前(末尾に .part)で書いてから元の名前に置き換える(4 章)ので、2 回目は同じファイルを書き直す作りではある。ただ、2 回の処理が同時に走ると、同じ .part に 2 つの Lambda が書き込む。今回のコードは、同時に走る 2 回の処理には備えていない。なお、今回の検証では、通知が 2 回届いたケースは発生しなかった。
3 通りを 1 つの Lambda で受ける
3 通りとも同じ Lambda で受けるので、届いたイベントの形で起動方法を見分けている。
def _parse(event):
"""(起動方法, バケット, キー, ワークフローの情報) を返す。"""
if "Records" in event:
rec = event["Records"][0]
return "s3", rec["s3"]["bucket"]["name"], unquote_plus(rec["s3"]["object"]["key"]), None
if "serviceMetadata" in event:
loc = event["fileLocation"]
return "workflow", loc["bucket"], loc["key"], event
if event.get("source") == "aws.transfer":
bucket, key = event["detail"]["file-path"].lstrip("/").split("/", 1)
return "eventbridge", bucket, key, None
raise ValueError("unknown event shape")
S3 のイベントのキーは URL エンコードされて届くので、unquote_plus で戻す。Records は配列だが、検証用に先頭の 1 件だけを見ている。複数が入りうる前提なら、順に処理する。
EventBridge のイベントでは、ファイルの場所が /バケット名/キー の形の 1 つの文字列で届く。
4. FSx for Windows へ書き込む Lambda
書き込みの部分は以下の通り。S3 から 1 MB ずつ読み、FSx for Windows に一時の名前(末尾に .part)で書いてから、元の名前に置き換える。HOST・SHARE・TARGET_DIR は環境変数から読んだ FSx for Windows の IP アドレス・共有名・フォルダ名で、CHUNK は 1 MB。wf_event には、マネージドワークフローから呼ばれたときだけ、届いたイベントが入る。S3 のキーのフォルダの部分は捨て、ファイル名だけで TARGET_DIR に置く(別のフォルダから同じ名前のファイルが届くと、ぶつかる)。書いている間は .part のファイルが見えるので、ファイルサーバー側のアプリは .part の付いたファイルを拾わない前提にしている。
base = rf"\\{HOST}\{SHARE}\{TARGET_DIR}"
tmp, final = rf"{base}\{name}.part", rf"{base}\{name}"
obj = s3.get_object(Bucket=bucket, Key=key)
size = 0
with smbclient.open_file(tmp, mode="wb") as f:
for chunk in obj["Body"].iter_chunks(CHUNK):
f.write(chunk)
size += len(chunk)
t_written = time.time()
smbclient.replace(tmp, final)
t_final = time.time()
if wf_event:
_callback(wf_event, "SUCCESS")
size と t_written・t_final は、ログに出す値(5 章)。_callback は、SendWorkflowStepState で成否を Transfer Family に返す関数。
copy_to_fsx.py の全文
"""fsxlab: S3 に着いたファイルを FSx for Windows へ SMB でコピーする(PoC)。
起点は 3 通りで、イベントの形から見分ける。
- S3 の ObjectCreated(Records)
- Transfer Family のマネージドワークフローのカスタムステップ(serviceMetadata・fileLocation)
- EventBridge の Transfer Family イベント(source = aws.transfer)
一時名(.part)で書き終えてから正式名に置き換えるので、業務アプリには完成したファイルだけが見える。
測った時刻は 1 行の JSON でログに出す(集計は measure.py の結果とファイル名で突き合わせる)。
⚠ PoC の割り切り:AD の Admin で、FSx に IP で NTLM 接続する。本番は専用のサービスアカウントと、
DNS 名での Kerberos にする。
"""
import json
import os
import time
from urllib.parse import unquote_plus
import boto3
import smbclient
s3 = boto3.client("s3")
secrets = boto3.client("secretsmanager")
_transfer = None
_session_host = None
_cold = True
HOST = os.environ["FSX_HOST"]
SHARE = os.environ.get("FSX_SHARE", "share")
TARGET_DIR = os.environ.get("FSX_DIR", "in")
CHUNK = 1024 * 1024
def _ensure_session():
global _session_host
if _session_host == HOST:
return
password = secrets.get_secret_value(SecretId=os.environ["AD_SECRET_ARN"])["SecretString"]
smbclient.register_session(
HOST,
username=f'{os.environ["AD_DOMAIN"]}\\{os.environ["AD_USER"]}',
password=password,
auth_protocol="ntlm",
)
_session_host = HOST
def _parse(event):
"""(起動方法, バケット, キー, ワークフローの情報) を返す。"""
if "Records" in event:
rec = event["Records"][0]
return "s3", rec["s3"]["bucket"]["name"], unquote_plus(rec["s3"]["object"]["key"]), None
if "serviceMetadata" in event:
loc = event["fileLocation"]
return "workflow", loc["bucket"], loc["key"], event
if event.get("source") == "aws.transfer":
bucket, key = event["detail"]["file-path"].lstrip("/").split("/", 1)
return "eventbridge", bucket, key, None
raise ValueError("unknown event shape")
def _callback(wf_event, status):
global _transfer
if _transfer is None:
_transfer = boto3.client("transfer")
meta = wf_event["serviceMetadata"]["executionDetails"]
_transfer.send_workflow_step_state(
WorkflowId=meta["workflowId"],
ExecutionId=meta["executionId"],
Token=wf_event["token"],
Status=status,
)
def handler(event, context):
global _cold
t_start = time.time()
cold, _cold = _cold, False
mode, bucket, key, wf_event = _parse(event)
name = key.rsplit("/", 1)[-1]
try:
_ensure_session()
t_session = time.time()
base = rf"\\{HOST}\{SHARE}\{TARGET_DIR}"
tmp, final = rf"{base}\{name}.part", rf"{base}\{name}"
obj = s3.get_object(Bucket=bucket, Key=key)
size = 0
with smbclient.open_file(tmp, mode="wb") as f:
for chunk in obj["Body"].iter_chunks(CHUNK):
f.write(chunk)
size += len(chunk)
t_written = time.time()
smbclient.replace(tmp, final)
t_final = time.time()
if wf_event:
_callback(wf_event, "SUCCESS")
print(json.dumps({
"fsxlab": "copied", "mode": mode, "name": name, "bytes": size, "cold": cold,
"t_start": t_start, "t_session": t_session, "t_written": t_written, "t_final": t_final,
"s3_event_time": event.get("Records", [{}])[0].get("eventTime"),
}))
except Exception as e:
print(json.dumps({"fsxlab": "failed", "mode": mode, "name": name, "error": repr(e)}))
if wf_event:
_callback(wf_event, "FAILURE")
raise
一時の名前(.part)で書いてから元の名前に置き換えるのは、書きかけのファイルをアプリに読ませないため。置き換えは同じフォルダの中での名前の変更なので、元の名前のファイルは書き終えてから置かれる想定。名前の変更の瞬間にアプリが読んだ場合の動きは、今回は試していない。
書き込みの途中で失敗すると、.part が FSx for Windows に残る。今回のコードは、失敗したらワークフローに FAILURE を返して例外を投げるだけで、.part は消さない(抜粋では例外の処理を省いた)。Lambda は非同期で呼ばれるので、失敗すると既定で 2 回まで呼び直される。
_callback は、元の名前に置き換えたあとで呼ぶ。成否を返すのに失敗すると例外になるので、FSx for Windows にはファイルがあるのに、ワークフローは失敗として終わりうる。
FSx for Windows への接続は、Lambda が起動するたびに張り直さず、実行環境の中で使い回す。認証に使う AD のユーザーのパスワードは、Secrets Manager から読む。
今回は検証のため、AD の管理者(Admin)で、FSx for Windows に IP アドレスを指定して NTLM で接続した。今回は、VPC の DNS から AD の DNS へ問い合わせを回す設定(Route 53 Resolver の転送ルール)を作っておらず、AD のドメイン名を引けないため。本番なら、その転送ルールで AD の DNS に問い合わせられるようにし、専用のサービスアカウントで DNS 名を使った Kerberos 認証にする形になる(今回は試していない)。認証の方式が違うので、接続にかかる時間は本番と同じとは限らない。
5. 測定方法
遅延は、次の 2 つの時刻の差とした。どちらもファイルを送った EC2 で取る。
- 送信の完了:SFTP でファイルを送り、閉じる処理が返った時刻。
- FSx for Windows に置かれた時刻:同じ EC2 から SMB で 50 ミリ秒おきに FSx for Windows を見て、元の名前のファイルが見えた時刻。
業務のアプリと同じく VPC の中から FSx for Windows を見るので、アプリから見た待ち時間に近い値になる。50 ミリ秒おきなので、0.05 秒の刻みで測れる。あわせて、置かれた時点のファイルサイズを記録し、送ったファイルと同じかを確かめた。中身は比べていない。
あわせて Lambda のログに、起動した時刻・FSx for Windows への接続を終えた時刻・書き終えた時刻・名前を置き換えた時刻を出し、どこに時間がかかったかを見た。遅延は同じ EC2 の時計どうしの差なので、時計のずれは差し引きで消える。内訳は Lambda の時計と並べるので、ずれが入る。EC2 は Amazon Time Sync Service で時刻を合わせている。Lambda が名前を置き換えた時刻から EC2 で見えた時刻までの差は、103 ケースすべてで 0〜0.06 秒に収まり、負になったケースは無かった。103 ケースは、下の表の続けて送った 90 ケースと環境を作り直した後の 9 ケースに、試しに送ったものなどを加えた数。ただし、この差には見つけるまでの待ちも入るので、時計のずれだけを測ったものではない。内訳は目安として読む。
測ったものは以下の通り。
| 項目 | やり方 | ケース数 |
|---|---|---|
| 続けて送ったとき | 1 MB・10 MB・100 MB の順に 1 ファイルずつ送るのを 10 周する。前後のファイルの処理が重ならないよう、前のファイルが FSx for Windows に置かれてから 5 秒あけて次を送る。各起動方法で 1 ファイル試しに送ってから測り、90 ケースとも実行環境を使い回せていたことを Lambda のログで確かめた | 起動方法ごと、ファイルサイズごとに 10 ケース |
| 環境を作り直した後の最初のファイル | Lambda の設定(説明文)を変えてから 1 MB を送る。設定を変えると実行環境が作り直されるので、間が空いて環境が片付けられたときに近い状態になる(実際に長く間を空けては測っていない)。作り直された環境で動いたかは Lambda のログで見る | 起動方法ごとに 3 ケース |
| SFTP の通信を途中で切る | 100 MB のうち半分(50 MB)を送ったところで接続を切り、送ったバイト数を記録する。切ってから 90 秒後と 5 分後に、FSx for Windows に何が置かれていたかを見る | 起動方法ごとに 1 ケース |
100 MB は 1.6〜1.7 秒で送り終わる。時間で切ると全部届いてしまうので、送ったデータが 50 MB に達したところで切った。送るのは Python の paramiko で、100 MB(104,857,600 バイト。この記事の MB は 1,048,576 バイト)のうち 50 MB を書いたところで、ファイルを閉じずに接続を切った。S3 に置かれたファイルが元のファイルと同一でない(50 MB で途切れている)ことは、Transfer Family のログ(PARTIAL_CLOSE)と S3 のオブジェクトのサイズで確かめた。
90 秒は、正常に送ったときの遅延(いちばん遅いファイルで 2.6 秒)より十分に長い。ただ、S3 のイベント通知は 1 分以上遅れることがある。Lambda は関数のエラーなら、1 分後と、さらに 2 分後に呼び直される。それも過ぎた 5 分後に、もう一度見た。それより後に届かないことまでは確かめていない(スロットルなどのエラーでは、呼び直しは既定で最大 6 時間続く)。
ファイルサイズは 1 MB → 10 MB → 100 MB の順に 1 ファイルずつ送り、これを 10 周した。同じサイズをまとめて送ると、たまたまその時間帯だけ速い・遅いといった偏りが、サイズの差に見えてしまうため。一方、起動方法は S3 のイベント通知・マネージドワークフロー・EventBridge の順に続けて測ったので、起動方法の差には時間帯の差も混ざっている可能性がある。起動方法は、テンプレートのパラメーター(TriggerMode)を変えてスタックを更新し、切り替えた。
環境を作り直した後の最初のファイルを別に測ったのは、実際の運用では、ファイルが届く間隔が空くことも多いため。間が空くと、Lambda は実行環境の準備と FSx for Windows への接続からやり直すので、その分どれだけ遅れるかを確認したかった。
6. 結果
遅延
結果は以下の通り。
| 起動方法 | ファイルサイズ | 中央値 [s] | 最大 [s] | 最小 [s] |
|---|---|---|---|---|
| S3 のイベント通知 | 1 MB | 1.26 | 1.52 | 0.66 |
| S3 のイベント通知 | 10 MB | 1.36 | 1.53 | 0.72 |
| S3 のイベント通知 | 100 MB | 2.03 | 2.13 | 1.68 |
| マネージドワークフロー | 1 MB | 0.77 | 0.85 | 0.55 |
| マネージドワークフロー | 10 MB | 0.82 | 0.93 | 0.71 |
| マネージドワークフロー | 100 MB | 1.82 | 1.89 | 1.68 |
| EventBridge | 1 MB | 0.77 | 1.89 | 0.22 |
| EventBridge | 10 MB | 0.52 | 1.04 | 0.28 |
| EventBridge | 100 MB | 1.58 | 2.60 | 1.26 |
表の値は、続けて送ったときの各 10 ケースから出した。90 ケースのうち、3 秒を超えたケースは無かった。3 通りの中では S3 のイベント通知がいちばん遅く、1 MB・10 MB の中央値で、ほかより 0.5〜0.8 秒遅れた。100 MB では、どの起動方法も 1 MB より 0.8〜1 秒長くかかった。EventBridge は中央値では速いが、1 MB と 100 MB では、最大が 3 通りの中でいちばん大きく、ばらつきも広かった。10 MB では、最大もばらつきも S3 のイベント通知のほうが大きかった。
起動方法をマネージドワークフローに切り替えた直後の約 2 分は、S3 のイベント通知も動き続け、同じファイルが二重に処理された。そのため、表のマネージドワークフローの 1 MB と 10 MB の 1 回目は外し、測り直した値に入れ替えた。
続けて送ったときの全部の測定値(90 ケース)
送信の完了から、FSx for Windows に元の名前のファイルが置かれるまでの時間。1 回目〜10 回目は、送った順に並べた。
| 起動方法 | ファイルサイズ | 1 回目 [s] | 2 回目 [s] | 3 回目 [s] | 4 回目 [s] | 5 回目 [s] | 6 回目 [s] | 7 回目 [s] | 8 回目 [s] | 9 回目 [s] | 10 回目 [s] |
|---|---|---|---|---|---|---|---|---|---|---|---|
| S3 のイベント通知 | 1 MB | 1.52 | 1.27 | 1.47 | 1.03 | 1.42 | 0.71 | 1.25 | 0.82 | 1.52 | 0.66 |
| S3 のイベント通知 | 10 MB | 1.36 | 0.72 | 1.43 | 1.32 | 1.36 | 1.42 | 0.88 | 1.42 | 1.36 | 1.53 |
| S3 のイベント通知 | 100 MB | 2.00 | 2.12 | 1.91 | 2.08 | 2.07 | 2.11 | 1.68 | 1.96 | 2.13 | 1.96 |
| マネージドワークフロー | 1 MB | 0.77 | 0.85 | 0.82 | 0.71 | 0.67 | 0.55 | 0.71 | 0.77 | 0.77 | 0.77 |
| マネージドワークフロー | 10 MB | 0.82 | 0.76 | 0.89 | 0.87 | 0.77 | 0.82 | 0.93 | 0.82 | 0.80 | 0.71 |
| マネージドワークフロー | 100 MB | 1.79 | 1.85 | 1.74 | 1.83 | 1.80 | 1.86 | 1.89 | 1.85 | 1.68 | 1.75 |
| EventBridge | 1 MB | 0.71 | 1.89 | 0.82 | 0.38 | 0.28 | 0.77 | 1.85 | 0.22 | 1.09 | 0.77 |
| EventBridge | 10 MB | 0.60 | 0.66 | 0.49 | 0.28 | 0.39 | 1.04 | 0.66 | 0.28 | 0.55 | 0.28 |
| EventBridge | 100 MB | 2.18 | 2.60 | 1.48 | 1.47 | 1.63 | 1.53 | 1.26 | 1.76 | 2.39 | 1.31 |
時間の内訳
S3 のイベント通知の遅れは、Lambda の処理が始まる前に出ていた。Lambda のログの時刻と並べた内訳は以下の通り。「完了」は送信の完了。各列は、続けて送ったときの各 10 ケースのそれぞれの中央値で、足しても遅延の中央値とは一致しない。
| 起動方法 | ファイルサイズ | 完了→処理の開始 [s] | 接続 [s] | S3 から読んで書く [s] | 名前の変更 [s] | 名前の変更→EC2 で検出 [s] |
|---|---|---|---|---|---|---|
| S3 のイベント通知 | 1 MB | 1.15 | 0.00 | 0.08 | 0.01 | 0.02 |
| S3 のイベント通知 | 10 MB | 1.17 | 0.00 | 0.16 | 0.01 | 0.04 |
| S3 のイベント通知 | 100 MB | 0.87 | 0.00 | 1.08 | 0.01 | 0.02 |
| マネージドワークフロー | 1 MB | 0.67 | 0.00 | 0.06 | 0.01 | 0.04 |
| マネージドワークフロー | 10 MB | 0.62 | 0.00 | 0.15 | 0.01 | 0.03 |
| マネージドワークフロー | 100 MB | 0.66 | 0.00 | 1.08 | 0.01 | 0.05 |
| EventBridge | 1 MB | 0.69 | 0.00 | 0.07 | 0.01 | 0.03 |
| EventBridge | 10 MB | 0.34 | 0.00 | 0.15 | 0.01 | 0.03 |
| EventBridge | 100 MB | 0.37 | 0.00 | 1.11 | 0.01 | 0.03 |
「完了→処理の開始」は、S3 のイベント通知が 0.9〜1.2 秒、ほかの 2 つが 0.3〜0.7 秒だった。接続は実行環境の中で使い回したので、2 回目からは 0 秒になった。接続を張り直すのは、環境を作り直した後の最初のファイルだけだった。
読み書きは、100 MB で起動方法によらず 1.1 秒ほど。約 90 MB/s で、スループット容量の 32 MB/s を上回った。この容量でもバーストできる構成(ディスクは 1 日 30 分まで 260 MB/s)だが、今回の速さがそれによるものかは、メトリクスで確かめていない。
環境を作り直した後の最初のファイル
環境を作り直した後の最初のファイルの結果は以下の通り。
| 起動方法 | 試行 1 [s] | 試行 2 [s] | 試行 3 [s] | 平均 [s] | うち完了→処理の開始 [s] | うち接続 [s] |
|---|---|---|---|---|---|---|
| S3 のイベント通知 | 2.39 | 2.55 | 2.51 | 2.48 | 2.24 | 0.11 |
| マネージドワークフロー | 1.79 | 2.02 | 2.12 | 1.98 | 1.76 | 0.10 |
| EventBridge | 2.12 | 1.58 | 1.64 | 1.78 | 1.56 | 0.10 |
右の 2 列も 3 回の平均。9 ケースとも、作り直された環境で動いた。
「小さいファイルでは接続に時間がかかる」と見込み、FSx for Windows への接続を張る時間が大きく乗ると思っていた。実際は 0.1 秒ほどだった。続けて送ったときとの差の大半は、処理の開始までの 0.9〜1.1 秒。ログの Init Duration(実行環境の準備)は、12 回で 0.67〜1.02 秒、平均 0.87 秒だった。12 回は、この 9 ケースに、試しに送った 2 ケースと、S3 のイベント通知と二重に起動したときの 1 回を加えた数。
SFTP の通信を途中で切ったとき
起動方法ごとに 1 回ずつ、100 MB のファイルを 50 MB まで送ったところで接続を切った。3 回とも、Transfer Family のログには PARTIAL_CLOSE が出て、S3 には 50 MB のオブジェクトが置かれた。
| 起動方法 | S3 に残ったもの | Lambda | FSx for Windows |
|---|---|---|---|
| S3 のイベント通知 | 50 MB のオブジェクト | 起動して、50 MB を書いた | 切ってから 1 分以内に、元の名前で 50 MB のファイルが置かれた |
| マネージドワークフロー | 50 MB のオブジェクト | 起動しなかった | 5 分後まで何も無し |
| EventBridge | 50 MB のオブジェクト | 起動しなかった | 5 分後まで何も無し |
S3 のイベント通知では、Lambda は 50 MB のオブジェクトを正常なファイルと同じように扱い、元の名前で FSx for Windows に置いた。FSx for Windows には、半分しか無いファイルが、書き終えたファイルと同じ名前で置かれる。名前だけでは見分けがつかない。
マネージドワークフローでは OnPartialUpload を付けていないので、何も動かなかった。EventBridge では、ルールが Upload Completed だけを拾うので Lambda は起動しなかった。Upload Failed のイベントが出たかは、ルールで拾っていないので見ていない。
7. 注意点
S3 のイベント通知で起動するなら、途中で切れたファイルを見分ける
S3 のイベント通知で起動するなら、通信が途中で切れたファイルを Lambda の側で見分ける仕組みが要る。たとえば、送る側からファイルサイズやハッシュを別に受け取り、S3 のオブジェクトと比べてから書く。
8. 費用
検証で資源が動いていたのは、2026 年 10 月 30 日 22:35 から 31 日 0:34 までの約 2 時間。
公開した時点では、Cost Explorer にまだ額が出ていなかった。下は、米国東部の単価で出した規模の目安で、東京での請求額の見込みではない。
| 項目 | 動いていた時間 | 1 時間あたり | 概算 |
|---|---|---|---|
| Transfer Family(SFTP のエンドポイント) | 約 1.6 時間 | 0.30 ドル | 0.48 ドル |
| AWS Managed Microsoft AD(Standard) | 約 2 時間 | 0.15 ドル前後 | 0.30 ドル |
| FSx for Windows(SSD 32 GiB・32 MB/s) | 約 1.4 時間 | 0.12 ドル前後 | 0.17 ドル |
| VPC エンドポイント・EC2・公開 IPv4 アドレス | 約 2 時間 | 0.06 ドル前後 | 0.12 ドル |
| SFTP で送ったデータ(約 3.5 GB) | — | 1 GB あたり 0.04 ドル | 0.14 ドル |
| 合計 | 約 1.2 ドル |
単価は公式の料金ページの例をもとにした。Cost Explorer に額が出たら、ここに実績を足す。
いちばん大きいのは Transfer Family のエンドポイントで、止めていても作ってある間は 1 時間ごとにかかる。
最後に
今回の検証では、測定時間より、通信が途中で切れたときの扱いのほうが有益な情報が得られた。どの起動方法でも 3 秒以内に置かれたので、起動方法は、通信が途中で切れたときの扱いや、受けたあとにほかの手順を続けたいか(マネージドワークフローなら手順を並べられる)で選べばよさそうだ。EventBridge は正常に終わったものだけを選べるが、通知が届かないこともあるので、S3 と FSx for Windows の突き合わせのような拾い直しが別に要る。
参考(2026-10-30 確認)
- AWS Transfer Family:ストレージの設定
- AWS Transfer Family:マネージドワークフロー
- AWS Transfer Family:EventBridge のイベント
- AWS Transfer Family:FSx for NetApp ONTAP の S3 アクセスポイントを使う
- AWS Transfer Family:SFTP 対応のサーバーを作る
- AWS Transfer Family supports custom ports for public SFTP servers(What's New)
- AWS Transfer Family:セキュリティポリシー
- AWS Transfer Family:サーバーのログ
- AWS Transfer Family:サービスマネージドのユーザー
- AWS Transfer Family:CreateServer(API リファレンス)
- AWS ブログ:Amazon FSx でサーバレスワークロードの SMB 共有を実現する(原文は 2021-11-02)
- smbprotocol(GitHub)












