ELB経由のFTPサーバでS3にファイル転送したい

2016.02.25

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

西澤です。今回は敢えてのアンチパターンへの挑戦。深遠なる理由によりFTPサーバ経由でS3バケットへのファイル転送が必要となった際の構成について検討してみました。ひとまず、ひと通り動作することは確認できましたのでご紹介させていただきます。本番環境ですぐに利用できる構成ではないという前提でお読みいただければありがたいです。

今回の要件

今回は下記のような要件で実現案を考えました。

  • クライアント側の実装により転送プロトコルはFTP縛り
  • FTPサーバに十分な可用性を持たせたい
  • 無駄なEC2起動は避ける為、Active/Standbyは避けたい
  • ファイル数が多い為、ローカルディスクとS3の差分を管理するよりも、S3マウントを利用した方が安全
  • S3に配置するファイルは厳密な排他制御は不要で、必要に応じて上書きできれば良い

構成案

ということで、こんな構成にできたら、余分な作り込みをせずに要件を満たせるのではないかと考えました。

ELB+FTP+S3

  • ELB配下にFTPサーバをActive/Activeに配置
  • FTPサーバをPASVモードで構成し、制御ポートおよび転送ポートをすべてEC2に振り分け
  • FTPサーバからs3fs-useまたはgoofysを使ってS3バケットをマウント

課題その1(ELB経由での複数FTPサーバへの負荷分散)

FTPサーバにはActive(Port)モードとPassiveモードの2種類の通信方式があります。ELB経由で通信させる場合には、Passiveモードであれば問題ないだろうと考えました。ただ、Passiveモードにも問題がありました。

  • 制御通信(21/tcp)
    • FTPクライアント -> ELB -> FTPサーバ(21/tcp)にて制御
  • 転送通信(サーバ側でIP範囲設定、任意)
    • FTPクライアント -> ELB -> FTPサーバ(指定した範囲のポート)を利用してファイル転送

どのELBを経由して接続してくるかがわからない

FTPクライアントはELBを経由して通信してきます。ただし、ELBは自動でスケールし、台数が増減しますので、FTPサーバから見た場合、転送ポートが開始されるときに異なるクライアントから接続してきたように見えるはずです。これはまさにFXP(File eXchange Protocol)と呼ばれるクライアントを介さない通信方式で、どのFTPサーバでもセキュリティの観点からデフォルトでは利用できないようになっています。PASVモードに移行した際に、制御ポートで通信したクライアントかどうかをサーバ側でチェックし、通信元が異なっている場合にはこの転送要求を拒否してしまいます。

セキュリティ的な観点は置いておく(接続元IPは十分に絞るものとして)と、FXPを有効にすることでこの問題は解決出来ることがわかりました。

  • vsftpdの場合
    • pasv_promiscuous=YES(デフォルトNO)
  • proftpdの場合
    • AllowForeignAddress on(デフォルトoff)

FXPを有効にしない状態で、異なる経路からPASVに入ろうとすると、以下のような結果になりました(vsftpdの場合)。

$ ftp -nv $VSFTPD << EOF
> user ftpuser Password
> debug
> ls
> bye
> EOF
Trying 54.xxx.xxx.65...
Connected to vsftpd-123456789.us-west-2.elb.amazonaws.com (54.xxx.xxx.65).
220 (vsFTPd 2.2.2)
331 Please specify the password.
230 Login successful.
Debugging on (debug=1).
ftp: setsockopt (ignored): 許可がありません
---> PASV
227 Entering Passive Mode (52,xxx,xxx,248,156,67).
---> LIST
425 Security: Bad IP connecting.
---> QUIT
500 OOPS: close

この例ですと、制御通信は54.xxx.xxx.65から、転送通信は52.xxx.xxx.248(ELBの持つ異なるIP)から別経路で要求を行った為、通信元が異なると判定され拒否されています(425 Security: Bad IP connecting.)が、FXPを有効にすればこの問題は発生しませんでした。

これで少なくともELB配下にFTPサーバ1台のみであれば、利用できそうな目処が立ちました。

どのFTPサーバに接続するかわからない

ELB配下に複数台のFTPサーバが稼働している場合、PASVモードに移行した際に、どこにPASV通信要求がかけられるかわかりません。全く別のFTPサーバに接続されてしまう可能性があります。この場合は、制御ポートへの通信が何も無い状態でいきなりPASVポートに要求を送りつけても、そもそもポートがLISTENしていませんので、転送ももちろんできません。

色々と調べたのですが、この問題を解決する方法は見つかりませんでした。FTPクライアントによっては、制御通信経路をきちんと覚えて通信できるようなものがあるかもしれません。何かご存知の方がいればお願いします。

ということで、ELB配下にFTPサーバを複数配置することは断念することにしました。じゃあELB不要では?というご意見もあるかとは思いますが、ELBはヘルスチェックをしてくれますし、AutoScalingとの相性が良いので、異なるAZで自動復旧する仕組みも簡単に作ることができます。可用性はFTP1台を自動でなるべく早く復旧させる(AutoScaling等を利用)ことで維持することで我慢することにして、構成を見直すことにしました。

修正案

FTPサーバは残念ながら1台のみの構成となりました。

ELB+FTP+S3_revised

  • ELB配下にFTPサーバをActive/Activeに配置 -> FTPサーバは1台でAutoScaling等で自動復旧させる

課題その2(ELBのリスナー数)

PASVポートは接続するクライアント数に応じて多めに確保しておきたいです。ただし、ELBにはリスナー数が100までという制限があり、上限緩和はできません。必然的に制御ポートを除くと、PASVポートは99までしか利用できないことになります。これで十分かどうかは同時接続数等に依存すると思います。今回はこれを許容できるものと判断しました(実際の検証はこれから)。

構築してみた

そもそもこのような構成で検証レベルの動作を確認した手順をご紹介します。

S3マウント

実際には、s3fs-useとgoofysの両方を試しました。詳細な手順は今回は省略しますが、検証で用いたfstabの設定だけご紹介しておきます。FTP用のアカウントとして、ftpuser(uid=501,gid=501)は事前に作成してパスワードを設定しておきました。

$ sudo cat /etc/fstab
:::(追記箇所のみ抜粋)
goofys#test-bucket /home/ftpuser/goofys fuse _netdev,allow_other,--uid=501,--gid=501 0 0
s3fs#test-bucket /home/ftpuser/s3fs fuse _netdev,allow_other,uid=501,gid=501,iam_role=EC2Role 0 0
  • _netdev: ネットワークが有効化されてからmount
  • allow_other: root以外からmountした領域を利用許可

その他の手順は、下記記事に詳しく書いたので、ぜひご欄ください。

PASVモードでFTPサーバ構築(vsftpd編)

デフォルトから変更が必要だった箇所を記載しておきます。

$ sudo yum install vsftpd -y
$ sudo vi /etc/vsftpd/vsftpd.conf
:::(追記・変更箇所のみ抜粋)
chroot_local_user=YES
pasv_enable=YES
pasv_min_port=40001
pasv_max_port=40010
pasv_address=vsftpd-123456789.us-west-2.elb.amazonaws.com
pasv_addr_resolve=YES
pasv_promiscuous=YES
$ sudo service vsftpd start
  • chrootを有効化(今回必須の要件ではない)
  • PASV通信要求アドレス(asv_address)にELBを指定
  • PASV転送ポート: 40001-40010/tcp
  • pasv_addressを名前で書く場合は、pasv_addr_resolveも必要
  • FXP有効化(前述の通り)

PASVモードでFTPサーバ構築(proftpd編)

こちらも設定方針は同様です。IPv6を明示的に無効化しないとELBのヘルスチェックが不安定でした。

$ sudo yum install proftpd --enablerepo=epel -y
$ sudo vi /etc/proftpd.conf
:::(追記・変更箇所のみ抜粋)
PassivePorts            50001 50010
UseIPv6             off
MasqueradeAddress       proftpd-123456789.us-west-2.elb.amazonaws.com
AllowForeignAddress     on
$ sudo service proftpd start
  • PASV通信要求アドレス(MasqueradeAddress)にELBを指定
  • PASV転送ポート: 50001-50010/tcp
  • UseIPv6だとELBヘルスチェックが不安定だったので無効化
  • FXP有効化(前述の通り)

ELB作成(vsftpd用)

ELBを作成します。制御ポートと転送ポートを全てリスナーとして設定する必要があります。

Load Balancer Protocol Load Balancer Port Instance Protocol Instance Port
TCP 21 TCP 21
TCP 40001 TCP 40001
TCP 40002 TCP 40002
TCP 40003 TCP 40003
TCP 40004 TCP 40004
TCP 40005 TCP 40005
TCP 40006 TCP 40006
TCP 40007 TCP 40007
TCP 40008 TCP 40008
TCP 40009 TCP 40009
TCP 40010 TCP 40010

ELB作成(proftpd用)

こちらも同様です。制御ポートと転送ポートを全てリスナーとして設定しました。

Load Balancer Protocol Load Balancer Port Instance Protocol Instance Port
TCP 21 TCP 21
TCP 50001 TCP 50001
TCP 50002 TCP 50002
TCP 50003 TCP 50003
TCP 50004 TCP 50004
TCP 50005 TCP 50005
TCP 50006 TCP 50006
TCP 50007 TCP 50007
TCP 50008 TCP 50008
TCP 50009 TCP 50009
TCP 50010 TCP 50010

課題その3(vsftpd+goofysの相性)

vsftpd+goofysの組み合わせに問題がありそうです。vsftpd経由で0バイトより大きいオブジェクトを上書きputしようとしたところ、451 Failure writing to local file.となり接続が切断されてしまいました。goofys側でdebugログを確認してみると、fuse.DEBUG WriteFile: only sequential writes supportedというエラーでした。こちらはgoofysの仕様とvsftpdの上書きの仕様の相性の問題かと思います。今回は、vsftpd+goofysの組み合わせでは使えないという結論としました。

課題その4(WEB公開用の場合)

本筋から外れるところもありますが、S3に配置するオブジェクトをWEB公開したい場合にのみ考慮が必要となる課題も合わせて記載しておきます。

goofys経由のファイル配置はContent-Typeが自動設定されない

S3に配置するオブジェクトがWEB公開用の場合、Content-Typeが正しく設定されることが必要となりますが、goofys経由でのS3アップロードでは、全てのオブジェクトがbinary/octet-streamとなりました。一方でs3fs-useの場合は、 /etc/mime.types(mailcapパッケージをインストールすれば生成される)をマッピング情報として、Content-Type自動設定が可能です。

goofys経由のファイル配置はデフォルトACL指定ができない

S3に配置するオブジェクトがWEB公開用の場合、オブジェクトのACLをpublic-readに設定しておく必要があります。ところが、goofysではこれを指定できるようなオプションは見当たりませんでした。s3fsではdefault_acl=public-readというように指定することが可能です。これらのgoofysの課題は、Lambdaで実装という選択肢ももちろんありです。

まとめ

アンチパターンに挑戦して悪戦苦闘した結果を記録として報告させていただきます。いずれにしてもS3配置の為にFTPはなるべく使わない方が良いと思います。

どこかの誰かの参考になれば嬉しいです。