FTPがローカル環境では成功したのにDockerに移すと失敗する

# 背景

- アプリケーションから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サーバー側）

### イメージ

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2384027/4a8014b9-6160-a4cc-2832-eb2fc7b54e13.png)

## 環境設定

- 以下のような設定ファイルを使用
- Springアプリケーションでftpはパッシブモードで行う
- Dockerfileはアプリケーション用で今回は重要じゃないけど環境がイメージできるように記載

``` yml: application.yml
# ローカル環境で使用する設定
ftp:
  host: localhost
  port: 21
  username: username
  password: password
```

``` yml: 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"]
```

``` yml: 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クライアントを追加でインストール

``` sh: 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に変更する

``` yml: 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

``` sh: 転送ファイルの存在確認（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](https://github.com/stilliard/docker-pure-ftpd/blob/master/docker-compose.yml)でそう設定してたから…

# 参考情報

## FTP概要（特にパッシブ）

- [トラブルシューティングのためのFTPプロトコル解説](https://netwiz.jp/ftp-protocol/)
- [FTPのpassiveモード(PASVモード)とは何ですか？](http://www.rtpro.yamaha.co.jp/RT/FAQ/TCPIP/ftp-passive-mode.html)
- [FTPのアクティブモードとパッシブモードの違いを徹底解説！](https://it-trend.jp/file_transfer/article/104-0006)

## FTP構築

- [stilliard/pure-ftpd(hub.docker.com)](https://hub.docker.com/r/stilliard/pure-ftpd/)
- [stilliard/docker-pure-ftpd(github.com)](https://github.com/stilliard/docker-pure-ftpd)