背景
- アプリケーションからFTPでファイル転送することになったので、最初はローカル環境で開発しててうまくいっていた
- ECSでコンテナ管理するにあたり、まずローカル環境のdocker上で動作確認したが、なぜかFTPサーバーログイン後の応答が確認できずファイルも転送されなかった
結論
原因
- アプリケーション側に問題はなく、テスト用に建てたFTPサーバー側の設定が問題だった
- FTPサーバーからFTPクライアントに渡すデータコネクションのIPが、FTPクライアント側で見るとFTPサーバーに到達できないものになっていた
解決策
- FTPサーバーが渡すデータコネクションのIPをhost.docker.internalにした
詳細
検証環境
- docker for Windows 4.11.1
- docker image
- amazonlinux:2(FTPクライアント側)
- stilliard/pure-ftpd(FTPサーバー側)
イメージ
環境設定
- 以下のような設定ファイルを使用
- Springアプリケーションでftpはパッシブモードで行う
- Dockerfileはアプリケーション用で今回は重要じゃないけど環境がイメージできるように記載
application.yml
# ローカル環境で使用する設定
ftp:
host: localhost
port: 21
username: username
password: password
application-local.yml
# Dockerで使用する設定
ftp:
host: host.docker.internal
port: 21
username: username
password: password
Dockerfile
FROM eclipse-temurin:11-jre as builder
WORKDIR application
ARG JAR_FILE=target/*.jar
COPY ${JAR_FILE} application.jar
RUN java -Djarmode=layertools -jar application.jar extract
FROM amazonlinux:2
RUN amazon-linux-extras install -y java-openjdk11 && yum clean all
# アプリケーションのルートになるディレクトリ
WORKDIR local
COPY --from=builder application/dependencies/ ./
COPY --from=builder application/spring-boot-loader/ ./
COPY --from=builder application/snapshot-dependencies/ ./
COPY --from=builder application/application/ ./
EXPOSE 8083
# 実行時に指定する
ENTRYPOINT ["java", "-Dspring.profiles.active=local","org.springframework.boot.loader.JarLauncher"]
docker-compose.yml
version: "3"
services:
# FTPを実行するアプリケーション
app:
container_name: sample-app
build:
context: .
args:
- JAR_FILE=sample-app-*.jar
image: sample-app:latest
ports:
# アプリケーション接続用
- 8083:8083
# 検証に使用するFTPサーバー
ftpd_server:
image: stilliard/pure-ftpd
container_name: pure-ftpd
# pure-ftpd のデフォルトで待ち受けてるポート
ports:
- "21:21"
- "30000-30009:30000-30009"
environment:
# 失敗時
PUBLICHOST: "localhost"
# 成功時
# PUBLICHOST: "host.docker.internal"
FTP_USER_NAME: username
FTP_USER_PASS: password
FTP_USER_HOME: /home/ftpusers/username
restart: always
FTP失敗時
状況
- ローカル開発環境のsample-appから接続でき、ファイル転送も成功する
- コンテナ:sample-appからは接続できるが、ファイル転送は失敗する
- 失敗時にログは出力されない
調査
- アプリケーション側が原因か環境が原因か切り分けをするため、コンテナ:sample-appからftpコマンドを実行してファイル転送できるか検証
- なのでftpクライアントを追加でインストール
ftpインストール(FTPクライアント側)
sh-4.2# yum install -y ftp
- 以下の出力が得られた
ftpコマンド出力(FTPクライアント側)
sh-4.2# ftp host.docker.internal
Connected to host.docker.internal (192.168.65.2).
220---------- Welcome to Pure-FTPd [privsep] [TLS] ----------
220-You are user number 2 of 5 allowed.
220-Local time is now 08:35. Server port: 21.
220-This is a private system - No anonymous login
220-IPv6 connections are also welcome on this server.
220 You will be disconnected after 15 minutes of inactivity.
Name (host.docker.internal:root): username
331 User username OK. Password required
Password:
230 OK. Current directory is /
Remote system type is UNIX.
Using binary mode to transfer files.
ftp> put sample.txt
local: sample.txt sample.txt
---> TYPE I
200 TYPE is now 8-bit binary
ftp: setsockopt (ignored): 許可がありません
---> PASV
227 Entering Passive Mode (127,0,0,1,117,49)
ftp: connect: 接続を拒否されました
ftp>
原因
- コマンドレベルで転送に失敗するので、アプリケーションではなく環境側に原因がある
- 以下の記述がポイント
---> PASV
227 Entering Passive Mode (127,0,0,1,117,49)
ftp: connect: 接続を拒否されました
- ここの
127,0,0,1,117,49
はFTPクライアントから見たFTPサーバーへの送信先IPとポートを示している - この状態だとローカルホストを指してしまっているのでFTPサーバーには到達できない
- FTPクライアント自身を見にいってしまっている
FTP成功時
- docker-compose.ymlのFTPサーバーが渡すデータコネクションのIPをdocker for Windowsで解決できるhostnameに変更する
docker-compose.yml
version: "3"
services:
... 省略 ...
# 検証に使用するFTPサーバー
ftpd_server:
image: stilliard/pure-ftpd
container_name: pure-ftpd
# pure-ftpd のデフォルトで待ち受けてるポート
ports:
- "21:21"
- "30000-30009:30000-30009"
environment:
# 失敗時
# PUBLICHOST: "localhost" # こっちから
# 成功時
PUBLICHOST: "host.docker.internal" # こっちに変更
... 省略 ...
調査
- 以下の出力が得られた
ftpコマンド出力(FTPクライアント側)
sh-4.2# ftp host.docker.internal
Connected to host.docker.internal (192.168.65.2).
220---------- Welcome to Pure-FTPd [privsep] [TLS] ----------
220-You are user number 1 of 5 allowed.
220-Local time is now 22:52. Server port: 21.
220-This is a private system - No anonymous login
220-IPv6 connections are also welcome on this server.
220 You will be disconnected after 15 minutes of inactivity.
Name (host.docker.internal:root): username
331 User username OK. Password required
Password:
230 OK. Current directory is /
Remote system type is UNIX.
Using binary mode to transfer files.
ftp> put sample.txt
local: sample.txt sample.txt
227 Entering Passive Mode (192,168,65,2,117,54)
150 Accepted data connection
226 File successfully transferred
ftp>
- データコネクションのIPがちゃんと変わっていて、後続処理も成功してるところがポイント
227 Entering Passive Mode (192,168,65,2,117,54)
150 Accepted data connection
226 File successfully transferred
- 上記の出力から成功はしてそうだけど、念のためFTPサーバー側でも確認
- もちろんFTPクライアント側のlsでもOK
転送ファイルの存在確認(FTPサーバー側)
# cd /home/ftpusers/username
# ls
sample.txt
#
- その後、アプリケーションからのFTPも無事に成功した
まとめ
- FTPの仕組みを理解して出力をちゃんと見れば、ただコネクション張れていないってだけの単純な問題だった
- データコネクションがうまくいかない要因と確認観点
- ポートがだめ → firewall, Dockerfileのexpose, docker起動時のport mapping
- IPがだめ → FTPクライアントをから対象IPに到達できるか、同一ネットワークか、名前解決できるか(今回はこれ)
- けどそこら辺の知見がなくアプリケーションのログからがんばろうとしていたので、無駄に時間がかかってしまった…
- 改めて、問題を切り分けながら調査していくことの大切さが身に染みた事例でした
その他
- 今回はpure-ftpdを使用しましたが、他のftpサーバーでもデータコネクションのIPを書き換えることは可能です
- 設定箇所や名称はまちまちなのでドキュメントを読みましょう
- 例:vsftpdではpasv_address
- 設定箇所や名称はまちまちなのでドキュメントを読みましょう
- そもそもなんでPUBLICHOSTにlocalhostを設定したのか?
- 公式のdocker-compose.ymlでそう設定してたから…