Cloud Run jobsでFTPサーバからCloud Storageにファイル転送してみた

記事タイトルとURLをコピーする

G-gen 又吉です。今回は Cloud Run jobs を用いて、FTP サーバから Cloud Storage にファイル転送する仕組みを作成していきます。

概要

データ分析パイプラインなどで、データソースが FTP サーバ上に存在する時、FTP サーバからクラウド上のデータレイクにデータ転送が必要なケースもあるかと思います。 今回は、先日 (2023 年 4 月) GA した Cloud Run jobs を用いて、FTP サーバから Cloud Storage にファイル転送する仕組みを作成していきたいと思います。

今回作成する構成図

Cloud Run jobs は Cloud SchedulerCloud Workflows と連携することでスケジュール実行することも可能ですが、今回は検証のため gcloud コマンドを用いて手動実行していきます。

Cloud Run jobs の詳細については、以下の記事をご参照下さい。

blog.g-gen.co.jp

事前準備

開発環境の準備

ディレクトリ構成

開発環境には Cloud Shell を使用します。Cloud Shell を起動したら、以下のディレクトリ構成で各ファイルを作成して下さい。

get_file_from_ftp_server
|-- Dockerfile
|-- main.py
`-- requirements.txt

Dockerfile

# Pythonイメージを取得
FROM python:3.10
  
# ローカルコードをコンテナイメージにコピー
ENV APP_HOME /app
WORKDIR $APP_HOME
COPY . ./
  
# 依存関係のインストール
RUN pip install --no-cache-dir -r requirements.txt
  
# コンテナの起動時の実行コマンド
CMD ["/usr/local/bin/python3", "main.py"]

main.py

main.py には以下を記述して下さい。 使用したライブラリについて、FTP 接続には ftplib を、Cloud Storage バケットへのアップロードには、Python Client for Google Cloud Storage を使用しました。

from ftplib import FTP
import os
  
from google.cloud import storage
  
  
FTP_SERVER_IP = os.environ.get("FTP_SERVER_IP")
FTP_USER = os.environ.get("FTP_USER")
FTP_PASSWD = os.environ.get("FTP_PASSWD")
FILE_NAME = os.environ.get("FILE_NAME")
BUCKET_NAME = os.environ.get("BUCKET_NAME")
  
if __name__ == "__main__":
    try:
        # FTP サーバへ接続
        ftp = FTP(host=FTP_SERVER_IP, user=FTP_USER, passwd=FTP_PASSWD)
        # パッシブモードを有効
        ftp.set_pasv(True)
    except Exception as e:
        print("An error occurred connecting to the ftp server:", str(e))
        raise
    else:
        try:
            # FTP サーバからファイル取得
            filename = FILE_NAME
            ftp.retrbinary("RETR " + filename, open(filename, "wb").write)
            ftp.quit()
        except Exception as e:
            print("An error occurred when retrieving files from the ftp server:", str(e))
            raise
        else:
            try:
                # Cloud Storage へアップロード
                storage_client = storage.Client()
                bucket = storage_client.bucket(BUCKET_NAME)
                blob = bucket.blob(filename)
                blob.upload_from_filename(filename)
            except Exception as e:
                print("An error occurred while uploading a file to Cloud Storage:", str(e))
                raise

requirements.txt

google-cloud-storage==2.9.0

使用するリソースの作成

Cloud Shell で get_file_from_ftp_server のディレクトリ階層に移動したら、以下コマンドを順次実行します。

# 環境変数の設定
export PROJECT_ID={プロジェクト ID を入力}
export PROJECT_NAME={プロジェクト名を入力}
export BILLING_ACCOUNT_ID={請求先アカウント ID を入力}
export BUCKET_NAME={取得するファイルの格納先バケット名を入力}
export FTP_USER=ftpuser
export FTP_SERVER_PW=1234
export FILE_NAME=sample.txt
  
# プロジェクト作成
gcloud projects create ${PROJECT_ID} --name=${PROJECT_NAME}
  
# プロジェクト設定の変更
gcloud config set project ${PROJECT_ID}
  
# 請求先アカウントの紐づけ
gcloud beta billing projects link ${PROJECT_ID} \
--billing-account=${BILLING_ACCOUNT_ID}
  
# API の有効化
gcloud services enable compute.googleapis.com \
secretmanager.googleapis.com \
run.googleapis.com \
artifactregistry.googleapis.com \
cloudbuild.googleapis.com
  
# FW の作成
gcloud compute firewall-rules create ftp-rule \
--allow tcp:20-21,tcp:40000-45000
  
# VM の作成
gcloud compute instances  create "ftp-server" \
--zone="us-central1-a" \
--machine-type="e2-micro" \
--image-family="debian-11" \
--image-project="debian-cloud" \
--boot-disk-size="10" \
--boot-disk-type="pd-standard"
  
# バケットの作成
gcloud storage buckets create gs://${BUCKET_NAME}
  
#シークレットの作成
gcloud secrets create my_password --replication-policy="automatic"
  
# シークレットにバージョンの追加
echo -n ${FTP_SERVER_PW} | gcloud secrets versions add my_password --data-file=-
  
# プロジェクト番号を環境変数に追加
export PROJECT_NO=$(gcloud projects list --filter=${PROJECT_ID} --format="value(projectNumber)")
  
# シークレットへ読み取り権限をCompute Engineのデフォルトサービスアカウントに付与
gcloud secrets add-iam-policy-binding my_password \
--member="serviceAccount:${PROJECT_NO}-compute@developer.gserviceaccount.com" \
--role="roles/secretmanager.secretAccessor"
  
# アーティファクトリポジトリの作成
gcloud artifacts repositories create docker-repo-ftp \
 --repository-format="docker" \
 --location="us-central1" \
 --description="Docker repository"
  
# Dockerイメージのビルド
gcloud builds submit --region="us-central1" \
--tag="us-central1-docker.pkg.dev/${PROJECT_ID}/docker-repo-ftp/jobs:latest"
  
# VM の外部 IP を環境変数に追加
export FTP_SERVER_IP=$(gcloud compute instances describe ftp-server \
--zone="us-central1-a" \
--format="get(networkInterfaces[0].accessConfigs[0].natIP)")
  
# Cloud Run jobsの作成
gcloud run jobs create ftp-job \
--image="us-central1-docker.pkg.dev/${PROJECT_ID}/docker-repo-ftp/jobs:latest" \
--region="us-central1" \
--set-env-vars="FTP_SERVER_IP=${FTP_SERVER_IP}" \
--set-env-vars="FTP_USER=${FTP_USER}" \
--set-env-vars="FILE_NAME=${FILE_NAME}" \
--set-env-vars="BUCKET_NAME=${BUCKET_NAME}" \
--set-secrets="FTP_PASSWD=my_password:latest"

FTP サーバの初期設定

先程作成した ftp-server VM に SSH でログインし、FTP サーバの初期設定を行っていきます。

gcloud compute ssh ftp-server --zone="us-central1-a" --project=${PROJECT_ID}

ターミナルに ユーザー名@ホスト名:~$ が表示されたらログイン成功です。

はじめに、OS のバージョンを確認します。

# OS のバージョン確認
matayuuu@ftp-server:~$ lsb_release
No LSB modules are available.
Distributor ID: Debian
Description:    Debian GNU/Linux 11 (bullseye)
Release:        11
Codename:       bullseye

次に、以下コマンドを実行します。

# パッケージのアップデート
sudo apt-get update
  
#  FTPサーバーをインストール
sudo apt-get install vsftpd
  
# VM の外部 IP を確認 (後ほど使用するので出力された IP アドレスをメモしておく)
curl -s ifconfig.me
  
# vsftpd 設定ファイルを編集
sudo vi /etc/vsftpd.conf

vsftpd 設定ファイルの中身を以下のように加筆修正します。

  1. 既存の設定を修正
    • listen=NO → listen=YES
    • listen_ipc6=YES → listen_ipc6=NO
  2. 新規の設定を追加 (最後の行に追記)
    • pasv_enable=YES
    • pasv_min_port=40000
    • pasv_max_port=45000
    • pasv_address=${先程メモしたVMの外部IP}

1. 既存の設定を修正 では、FTP サーバがスタンドアロンモードで動作するよう、また IPv4 アドレスのみを許可するように設定しています。

2. 新規の設定を追加 では、FTP サーバがパッシブモードで動作するよう、また パッシブモード時に使用するポート範囲と FTP 接続を確立する IP アドレスを設定しています。

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

# vsftpdを再起動
sudo systemctl restart vsftpd
  
# フォルダの作成
sudo mkdir /home/ftpuser
  
# ファイルを作成
echo "Hello, FTP!" | sudo tee /home/ftpuser/sample.txt
  
# ユーザーの作成(検証のためパスワードは「1234」と簡潔なものとし、その他はデフォルト設定)
sudo adduser ftpuser
  
# ユーザーをFTPグループに追加
sudo usermod -aG ftp ftpuser
  
# ftpuserディレクトリの所有者をftpuser、グループをftpに変更
sudo chown ftpuser:ftp /home/ftpuser
  
# ftpuserディレクトリの書き込み権限を全削除
sudo chmod a-w /home/ftpuser
  
# ログアウト
exit

動作確認

Cloud Run jobs の実行

以下のコマンドで Cloud Run jobs を手動実行します。

gcloud run jobs execute ftp-job \
--region="us-central1"

Cloud Run jobs のコンソール画面からジョブの実行結果を確認できます。

ジョブの詳細画面

Cloud Storage に FTP サーバから取得した sample.txt も保存されていることが確認できました。

Cloud Storage の画面

sample.txt

リソースの削除

以下のコマンドを実行し、検証で作成したプロジェクトを削除します。

gcloud projects delete ${PROJECT_ID}

本番運用に向けての考慮事項

接続方式

IP アドレス

今回は FTP サーバに外部 IP を付与しすべてのソース元 IP アドレスを許可しましたが、本番運用では①内部 IP アドレスのみを許可、もしくは②許可した外部 IP アドレスのみを許可する構成が想定されます。 必要に応じ、サーバレス VPC アクセスCloud NAT を用いて Cloud Run jobs から FTP サーバへの通信を制御していく必要があります。

それぞれの構成図

参考 : Connect to a VPC network

参考 : Static outbound IP address

プロトコル

インターネット経由でファイル転送を行う際は、セキュリティの観点から FTP プロトコルより SFTP プロトコルを採用することが多いです。その際は、SFTP サーバの SSH 認証キーを Secret Manager に保存して管理する等の必要があります。

サービスアカウントの権限

今回は Cloud Run に Compute Engine のデフォルトサービスアカウントをアタッチしましたが、必要最低限のロールを付与したサービスアカウントを作成することが推奨されます。

又吉 佑樹(記事一覧)

クラウドソリューション部

はいさい、沖縄出身のクラウドエンジニア!

セールスからエンジニアへ転身。Google Cloud 全 11 資格保有。Google Cloud Champion Innovator (AI/ML)。Google Cloud Partner Top Engineer 2024。Google Cloud 公式ユーザー会 Jagu'e'r でエバンジェリストとして活動中。好きな分野は AI/ML。