[アップデート]AWS Transfer Family側がクライアントとなりSFTPサーバ・S3間のデータ通信を実現するSFTP connectorsがリリースされました

2023.07.27

初めに

昨日のアップデートでAWS Transfer Family側がクライアントとなりSFTPサーバとS3バケットと間のデータ転送を行うSFTP connectorsがリリースされました。

従来のTransfer Familyを利用したSFTP通信といえば、Transfer FamilyがSFTPの受け口となりWinSCP等のSFTPクライアントからの接続を受けS3とデータ通信を行うものでしたが、SFTP connectorsは実質的にTransfer Family側がクライアントとして稼働しSFTPサーバに対して接続を行った上データをS3間でやり取りする機能となります。

SFTPの接続先とそれの接続に用いる1ユーザ情報をSFTP connectorsに設定しTransfer Familyからはその情報を用いてSFTPサーバにアクセスするため、用途としてはユーザ対サーバではなくバッチ処理の大量データ転送のサーバ対サーバのような用途となります。

What's newを見る限りもデータレイク等の大規模なデータ転送といったものを想定しているようです。

なおこれ以前でもAS2プロトコルがこの方式に対応していたようですが、悲しいことにAS2 transfer familyとかで調べてもほぼ情報がありませんでした。

料金

従来のTransfer Familyをエンドポイントとした場合ではエンドポイントの有効時間+データ転送量に対して課金が発生する形でしたが
SFTP connectorsを利用する場合は実行時のみの稼働となるためか時間課金ではなくコネクタの呼び出し回数+データ転送量の課金となります。

執筆時点では英語版のみに記載がありますが東京リージョンの場合は以下のとおりです。

https://aws.amazon.com/aws-transfer-family/pricing/

項目 料金
Cost per connector call $0.001 per connector call
Cost per GB of data transfer $0.40 per GB sent or retrieved

1パス=1 connector callのようなので実質的には転送ファイル数+データ転送量でしょうか(ワイルドカードの指定は可能?)

設定

Transfer Family側の設定に加えEC2にSFTPで接続できるように設定をします。

SFTPユーザの準備

Transfer Familyの接続先となるユーザを準備します。
sshd周りの設定はデバッグのために少しだけログ周りを触りましたが、今回の検証の機能的な部分ではAmazon Linux 2であればデフォルトでも十分です。

適当なEC2を立てて、ユーザの作成と公開鍵認証用の鍵をssh-keygenで作成し準備しておきます。

$ sudo useradd sftpuser
$ sudo su - useradd
$ mkdir .ssh && chmod 600 .ssh
$ cd .ssh && ssh-keygen
...
$ mv id_rsa authorized_keys
$ chmod 600 ./*

https://docs.aws.amazon.com/transfer/latest/userguide/configure-sftp-connector.html
Enter a private key for the user. This value must be stored in OpenSSH format, and must correspond to the public key that is stored for this user in the remote server.

なおドキュメント上は秘密鍵ははOpenSSHフォーマットでないといけないと記載がありますが、実際に試してみるとPEMでないとエラーとなるようです。

OpenSSH形式を利用して実行した際のログ

    {
      "operation": "SEND",
      "timestamp": "2023-07-26T12:58:47.789973Z",
      "connector-id": "c-xxxxx",
      "transfer-id": "xxxx",
      "file-transfer-id": "xxxx",
      "url": "sftp://xxxxx",
      "file-path": "/tmp/file.txt",
      "status-code": "FAILED",
      "failure-code": "CONNECTION_ERROR",
      "failure-message": "Private key not in PEM format"
  }

認証情報の登録

SFTPサーバに接続するための認証情報はSecrets Managerに登録します。

今回は公開鍵認証を利用していますが、パスワード認証も可能なようです。

Usernameに先ほど作成したユーザ名、PrivateKeyに先ほど作成した秘密鍵を格納します。

注意点としてマネージメントコンソールから秘密鍵を入力する際は改行を\nに置換し、プレーンテキストのタブから値を設定してください。

キー/値から入力すると\nがエスケープされて\\n扱いになるらしく接続の際にInvalid private key identityでエラーとなります。
(鍵をそのままコピペでキー/値のタブで入れると改行がスペース文字扱いになるので同様に接続時にエラー)

この仕様に気づかずに半日近くハマっていました。

https://docs.aws.amazon.com/transfer/latest/userguide/configure-sftp-connector.html
On the Configure secret page, enter a name and description for your secret. We recommend that you use a prefix of aws/transfer/ for the name. For example, you could name your secret aws/transfer/connector-1.

シークレットの名前はに明確な制約はないようですがドaws/transfer/から始まるものが推奨されています。

https://docs.aws.amazon.com/transfer/latest/userguide/connectors-configure-as2-connector.html
If you're using Basic authentication for your connector, the access role requires the secretsmanager:GetSecretValue permission for the secret. If the secret is encrypted by using a customer managed key instead of the AWS managed key in AWS Secrets Manager, then the role also needs the kms:Decrypt permission for that key. If you name your secret with the prefix aws/transfer/, you can add the necessary permission with a wildcard character (*), as shown in Example permission to create secrets.

AS2側のドキュメントのみですが、そうしておくとワイルドカードの指定ができるよ!みたいなことが書いてあるのでこの関係かもしれません。

コネクタの設定

さていよいよTransfer Family側の設定です。

SFTPコネクタは左メニューから「コネクタ」を選択し、その画面から作成を行います。

「コネクタを作成」を押した後、次の画面でSFTPを選択して「次へ」を押します

次の画面で各種設定を入力します。

のり弁状態ですが、それぞれ以下の値を入力します。

項目 備考
URL `sftp://{{ホスト名}}` 今回はEC2のIPを入力
アクセスロール 先ほどの秘密鍵の読み出し+通信先S3バケットの読み書き権限のあるロールを選択
ログ記録ロール CloudWatch Logsに書き出しの可能な権限のあるロール 出力先のロググループは /aws/transfer/{{コネクタID}}
コネクタの認証情報 先ほど作成したSecrets Managerの情報
信頼できるホストキー 先ほど作成した認証鍵のフィンガープリント

ホストキーは公開鍵を入れればいいのかなと思ったのですが実際試す限りどうも違うようで、先ほど作ったサーバ内でlocalhostに対してSSHを実行して生成されたフィンガープリントをknown_hostsから取り出してその値を入力したところ接続できるようになりました。

EC2のセキュリティグループの設定

EC2に対してTransfer FamilyからのPort 22に対するアクセス許可が必要です。

ただ、この接続元となるIPセットがどこから来ているかが今回全くわかりませんでした。

0.0.0.0/0でアクセスを開放し受け付けて見たところ52.199.26.16518.177.129.162から来たのですが、AWS IP アドレスの範囲で確認する限り固有のサービス名が割り当てられていない状態でした。

    {
      "ip_prefix": "52.196.0.0/14",
      "region": "ap-northeast-1",
      "service": "AMAZON",
      "network_border_group": "ap-northeast-1"
    },
...
    {
      "ip_prefix": "18.160.0.0/15",
      "region": "GLOBAL",
      "service": "AMAZON",
      "network_border_group": "GLOBAL"
    }

流石にSSH全開放は怖いのであちこち情報を探して見ましたが、マネージドプレフィックスリストにTransfer Familyらしきものもなく、AS2/SFTP connectorsのドキュメントにも特に言及が見当たりませんでした。

もしかしたら見落としがあるかもしれませんが、今回は機能を見ることを優先し断腸の思いでEC2に対して0.0.0.0/0を開けて検証しています。

SSHを全開放する設定は非常に危険ですので、実際の利用にはどうにかしてこの問題を解決する必要が出てきそうです。

事前の疎通確認

実際にデータ転送コマンドを実行して疎通確認することも可能ですがコマンドの指定値等別の要因も出てくるので切り分けが難しいケースも出てくるかと思います。

なんと、マネジメントコンソールにはコネクタからSFTPサーバに対しての接続確認機能が存在します。

コネクタの一覧画面で接続を確認したいコネクタを選択し「アクション>テスト接続」で疎通確認が可能です。

エラーが出た場合は設定情報や、SFTPサーバへの経路等に問題がある可能性があるためログ等を確認し必要であれば修正しましょう。

実行

執筆時点の話

SFTPの実行は直接SFTPコマンド等を実行するのではなくAWS側のAPIであるStartFileTransferを呼び出し実行します。

今回はAWS CLIで実行しますが執筆時点ではv1のみが対応しておりv2は未対応だったためv1を利用しています。

# venvで作成した環境にv1のコマンドを入れています。
% /tmp/awscli-pip/bin/aws --version
aws-cli/1.29.11 Python/3.11.4 Darwin/22.5.0 botocore/1.31.11

実行コマンド例はSend and retrieve files by using an SFTP connectorに載っています。

$ aws transfer start-file-transfer --send-file-paths file.txt --remote-path /tmp --connector-id c-1111AAAA2222BBBB3 --region us-east-2

$ aws transfer start-file-transfer --retrieve-file-paths /my/remote/file.txt --local-path DOC-EXAMPLE-BUCKET/prefix --connector-id c-2222BBBB3333CCCC4 --region us-east-2

と思いきや執筆時点ではunknown optionsとなる値がが指定されており、実際にhelpを見ても存在しないものがあるのでこれは使えません。

$ /tmp/awscli-pip/bin/aws transfer start-file-transfer help
...
SYNOPSIS
            start-file-transfer
          --connector-id <value>
          [--send-file-paths <value>]
          [--retrieve-file-paths <value>]
          [--local-directory-path <value>]
          [--remote-directory-path <value>]
          [--cli-input-json <value>]
          [--generate-cli-skeleton <value>]
          [--debug]
          [--endpoint-url <value>]
          [--no-verify-ssl]
          [--no-paginate]
          [--output <value>]
          [--query <value>]
          [--profile <value>]
          [--region <value>]
          [--version <value>]
          [--color <value>]
          [--no-sign-request]
          [--ca-bundle <value>]
          [--cli-read-timeout <value>]
          [--cli-connect-timeout <value>]

試す限り現時点ではhelp側が正のようですがどちらが正しい姿かは不明なので改めて試される際にドキュメントやAPIの情報をご確認ください。

S3からSFTPサーバへ

以下のコマンドを実行します。
コマンドはSFTPサーバ以外の環境から実行しても問題ありません。

--connector-idに利用するSFTP connectorの設定、--send-file-pathsにはS3側で転送したいファイルのパス、--remote-directory-pathにはSFTPサーバ側の配置先のパスを記載します。

--send-file-pathsは配列を受け入れるのでを行うのでまとめて複数のファイルを指定することも可能です。

# 実行に利用するファイルはいつも通り過去の検証データの残骸です
$ /tmp/awscli-pip/bin/aws transfer start-file-transfer --connector-id c-xxxxx /tmp --send-file-paths /{{バケット名}}/bimi-logo-1.svg --remote-directory-path
{
    "TransferId": "xxxx"
}

処理はリアルタイムではなくバッチで行われるためか、オプションの指定値に文法的な誤りがなければこの時点では特にエラーが出ずに実行されます。

実行結果はCloudWatch Logs上から確認します。
認証情報の誤りやIAMの権限ミスなど実際の実行フェーズでのエラーはこちらでの確認となります。

成功例

{
    "operation": "SEND",
    "timestamp": "2023-07-26T16:58:00.505693Z",
    "connector-id": "c-xxxx",
    "transfer-id": "xxxxx",
    "file-transfer-id": "xxxxx",
    "url": "sftp://xxxxx",
    "file-path": "/xxxxx/bimi-logo-1.svg",
    "status-code": "COMPLETED",
    "start-time": "2023-07-26T16:58:00.413839Z",
    "end-time": "2023-07-26T16:58:00.450577Z"
}

Glacierのファイルを指定して失敗している図

{
    "operation": "SEND",
    "timestamp": "2023-07-26T16:07:16.662588Z",
    "connector-id": "c-xxxxx",
    "transfer-id": "xxxxx",
    "file-transfer-id": "xxxxx",
    "url": "sftp://xxxxx",
    "file-path": "/xxxxx/bimi-logo.svg",
    "status-code": "FAILED",
    "failure-code": "READ_FILE_ERROR",
    "failure-message": "Error while reading source file: Internal Error"
}

なお動作確認の際の注意点としてエラーによってはファイルが生成されるがよくみると0バイトでログでエラー吐いているということがあるのでしっかり中身が正しいかまで確認しましょう。

# bimi-logo.svgは実行エラーとなったが空ファイルが作られている
$ ls -l /tmp/bimi-logo*
-rw-rw-r-- 1 sftpuser sftpuser 4722 Jul 26 16:58 /tmp/bimi-logo-1.svg
-rw-rw-r-- 1 sftpuser sftpuser    0 Jul 26 16:22 /tmp/bimi-logo.svg
$ cat /tmp/bimi-logo-1.svg
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<svg width="100%" height="100%" viewBox="0 0 100 100" version="1.2" xml:space="preserve" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="2" baseProfile="tiny-ps" xmlns="http://www.w3.org/2000/svg">
  <title>bimi-svg-tiny-12-ps</title>
  <g id="レイヤー1"/>
  <g id="アートボード11">
...
  </g>
  <rect id="アートボード1" x="0" y="0" width="100" height="100" fill="none"/>
</svg>

SFTPサーバからS3へ

以下のコマンドを実行します。

サブコマンド自体は同じですが指定オプションが異なり--retrieve-file-pathsにSFTPサーバからS3に渡したいファイルパス、--retrieve-file-pathsに受け渡し先のS3バケット名にプレフィックスをつけて指定します。

--retrieve-file-pathsは配列を受け入れるので複数ファイルをまとめて操作可能です。

#/tmp/helloはHello Worldと書いてあるテキストファイルです
$ /tmp/awscli-pip/bin/aws transfer start-file-transfer --connector-id c-xxxxx --retrieve-file-paths /tmp/hello --local-directory-path /xxxxx/receive
{
    "TransferId": "xxxxx"
}

operationがこちらの場合はSENDではなくRETRIEVEになるようです。

{
    "operation": "RETRIEVE",
    "timestamp": "2023-07-26T16:50:44.183559Z",
    "connector-id": "c-xxxxx",
    "transfer-id": "xxxxx",
    "file-transfer-id": "xxxxx",
    "url": "sftp://xxxxx",
    "file-path": "/tmp/hello",
    "status-code": "COMPLETED",
    "start-time": "2023-07-26T16:50:44.067760Z",
    "end-time": "2023-07-26T16:50:44.076403Z"
}

中身も確認しておきます。

$ aws s3 cp s3://xxxxx/received/hello ./
download: s3://xxx/received/hello to ./hello
$ cat hello
Hello World

余談ですが若干オプションのパス指定に癖があり、バケット指定はs3://bucket-name/ではなく/bucket-nameでないといけなかったり、SFTPサーバ側のパス指定は/tmp/ではダメで/tmpである必要があったりと地味に指定が細かい上にメッセージは親切ではないので注意してください(SEND側の指定も同様)。

# 上記とは逆の事例でS3側のパス末尾に/を入れても失敗する
%  /tmp/awscli-pip/bin/aws transfer start-file-transfer --connector-id c-xxxxx --retrieve-file-paths /tmp/hello --local-directory-path /xxxxx/received/

An error occurred (InvalidRequestException) when calling the StartFileTransfer operation: Invalid file path.

終わりに

まさかのTransfer Familyがクライアントとして動いてSFTPサーバとS3間を取り持ってくれる面白いアップデートでした。

転送のためのコマンド自体は何かしらで走らせないといけないですが、SFTPサーバ上にクライアントとしての機能を持たせたりサーバtoクライアントtoサーバみたいな中継点を自前で用意しなくて環境によっては非常に便利そうな予感がします。

また、料金形態としてもサーバのように常時受け口を維持する必要がなく必要な時だけかどうする関係か、時間課金がなく転送分だけを支払う料金形態となっているのが非常に嬉しいところです。

Step Functionsと組み合わせることでローコード+料金としても無駄がないみたいなものが作れそうな予感がします。

まだドキュメントがちょっと怪しいところがありますがこの辺りはリリースされたばかりなのもあるので、この辺りについてはもう少し見守りいい感じになって触りやすくなることを期待しましょう。