📘

AWS Lambda と rclone の活用術:OneDrive から S3 にファイルコピー

2024/12/02に公開

はじめに

阿河です。

ファイル移行は、様々なプロジェクトで必要となるユースケースです。

本記事では、AWS Lambdaを活用し、OneDrive からS3への安全なファイル移行をrcloneを使って実現する方法を解説します。

誰かの参考になれば幸いです。

目次

  1. 事前準備
  2. Lambdaの実装
  3. 動作確認

1. 事前準備

Secrets Managerの準備

今回の実装では、シークレット情報群をSecrets Managerに保存して管理します。

  • Secret name: rclone_conf

https://rclone.org/onedrive/

  • Onedriveのトークン/Drive IDの入力
  • Onedriveのtypeは自身のドライブに合わせて入力(personal | business | documentLibrary)

https://rclone.org/s3/

  • アクセスキー/シークレットキーに紐づく権限は、S3に対する最低限の権限に絞ります。

rcloneの準備

適当なS3バケットにrclone関連のファイルをアップします。
以下の手順で準備しました。

$ curl -O https://downloads.rclone.org/rclone-current-linux-amd64.zip
$ unzip rclone-current-linux-amd64.zip
$ cd rclone-*-linux-amd64

$ mkdir rclone_bin
$ cp rclone rclone_bin/
$ zip -r rclone_bin.zip rclone_bin

上記で作成したrclone_bin.zipを、S3バケットにアップロードしました。

2. Lambdaの実装

Lambda設定

Lambda Runtime: Python3.13
Architecture: x86_64

Lambda関数を作成したら、環境変数を設定します。

ONEDRIVE_PATH:
→OneDriveのファイルが保存してあるフォルダのパス
RCLONE_SECRET_ARN:
→シークレットのARN
S3_BUCKET_PATH:
→s3:[※バケット名]/[※プレフィックス]

その他、タイムアウトは30秒前後に設定。
実行ロールにはS3とSecrets Managerに対する権限を付与。

コード実装

以下はサンプルコードです。
必要に応じてコードを書き換えてください。

import os
import boto3
from botocore.exceptions import ClientError
import json
import subprocess
import zipfile
import sys


# rcloneのzipファイルをS3からダウンロードして展開
def download_and_extract_s3_zip(bucket_name, zip_key, extract_path):
    s3_client = boto3.client('s3')
    zip_path = '/tmp/temp_rclone.zip'
    
    try:
        # S3からファイルをダウンロードして、Lambdaの一時領域(/tmp/temp_rclone.zip)に保存
        print(f"Downloading {zip_key} from S3 bucket {bucket_name} to {zip_path}")
        s3_client.download_file(bucket_name, zip_key, zip_path)

        # ダウンロード結果を確認(デバッグ用)
        # ファイルサイズも載せる
        if os.path.exists(zip_path):
            file_size = os.path.getsize(zip_path)
            print(f"DEBUG: File {zip_path} exists with size {file_size} bytes.")
        else:
            print(f"DEBUG: File {zip_path} does not exist after download.")
            return

        # zipファイルの解凍処理
        print(f"Extracting {zip_path} to {extract_path}")
        with zipfile.ZipFile(zip_path, 'r') as zip_ref:
            zip_ref.extractall(extract_path)

        # 解凍後のファイルリストを表示
        # DEBUG1: Extracted files: ['temp_rclone.zip', 'python']
        # DEBUG1: Extracted files: ['bin', 'temp_rclone.zip', 'python']
        extracted_files = os.listdir(extract_path)
        print(f"DEBUG1: Extracted files: {extracted_files}")
    except zipfile.BadZipFile:
        print(f"ERROR: The file {zip_path} is not a valid zip file.")
        raise
    except Exception as e:
        print(f"Error during extraction: {str(e)}")
        raise
    
    
# Secrets Managerから認証情報取得
def get_secret(secret_name, region_name):
    session = boto3.session.Session()
    client = session.client(service_name='secretsmanager', region_name=region_name)
    
    # rclone.confの内容を取得
    try:
        get_secret_value_response = client.get_secret_value(SecretId=secret_name)
    except ClientError as e:
        raise e

    # 必要な値を取り出す
    secret = get_secret_value_response['SecretString']

    # Pythonで扱えるようにする
    try:
        return json.loads(secret)
    except json.JSONDecodeError:
        raise ValueError("The secret is not a valid JSON format.")
        
# rclone.confを動的に生成
def create_rclone_conf(secret_dict):
    if not isinstance(secret_dict, dict):
        raise ValueError(f"Expected dictionary, got {type(secret_dict)} instead.")
    
    sections = {}

    for key, value in secret_dict.items():
        if "_" not in key:
            section = "default"
            field = key
        else:
            section, field = key.split("_", 1)

        # セクション名を特別処理: "test-onedrive" を "onedrive" に変換
        if section == "test-onedrive":
            section = "onedrive"
        elif section == "test-rclone":
            section = "s3"

        if section not in sections:
            sections[section] = {}

        # OneDrive の token を特別処理
        if section == "onedrive" and field == "token":
            try:
                token_data = json.loads(value)  # トークン情報を JSON として解釈
                sections[section]["token"] = json.dumps(token_data)  # 必要な形式に再エンコード
            except json.JSONDecodeError:
                raise ValueError(f"Invalid JSON format for token: {value}")
        else:
            sections[section][field] = value

    # rclone.conf の生成
    rclone_conf_content = ""
    for section, fields in sections.items():
        rclone_conf_content += f"[{section}]\n"
        for field, value in fields.items():
            if isinstance(value, str):
                rclone_conf_content += f"{field} = {value}\n"
            else:
                rclone_conf_content += f"{field} = {json.dumps(value)}\n"
        rclone_conf_content += "\n"
    
    #print(f"DEBUG: Generated rclone.conf content:\n{rclone_conf_content}")
    return rclone_conf_content


def lambda_handler(event, context):
    # download_file APIを利用する際に指定するバケット内のキー
    rclone_python_zip_key = "rclone_bin.zip"
    
    try:
        # S3からrclone_python.zipとrclone.zipをダウンロード
        download_and_extract_s3_zip("xxxxxxx", rclone_python_zip_key, "/tmp")
 
        
        # 解凍後の確認(デバッグ)
        extracted_files = os.listdir("/tmp")
        #print(f"DEBUG2: Files in /tmp: {extracted_files}")

         # rcloneバイナリのフルパスを指定(デバッグ用)
        rclone_executable = "/tmp/rclone_bin/rclone"

        # バイナリが存在するか確認(デバッグ)
        # DEBUG: rclone exists at /tmp/bin/rclone
        if os.path.exists(rclone_executable):
            print(f"DEBUG: rclone exists at {rclone_executable}")
        else:
            raise FileNotFoundError(f"rclone binary not found at {rclone_executable}")

        # 実行権限を付与
        os.chmod(rclone_executable, 0o755)
        #print(f"DEBUG: rclone file permissions set to executable.")

        # PATH環境変数を更新
        os.environ["PATH"] += os.pathsep + "/tmp/rclone_bin"
        #print(f"DEBUG: PATH updated: {os.environ['PATH']}")
        
        # 環境変数の取得
        secret_arn = os.environ["RCLONE_SECRET_ARN"]
        region_name = os.environ["AWS_REGION"]
        
        # Secrets Managerからシークレット情報を取得(rclone.confの情報)
        secret_dict = get_secret(secret_arn, region_name)
        #print(f"DEBUG: Retrieved secret: {secret_dict}")

        # rclone.conf内の文字列生成
        rclone_conf_content = create_rclone_conf(secret_dict)
        
        # 一時ディレクトリ内にrclone.confを作成するファイルパスを指定
        # ファイルへの書き込み
        rclone_conf_path = '/tmp/rclone.conf'
        with open(rclone_conf_path, 'w') as rclone_conf_file:
            rclone_conf_file.write(rclone_conf_content)

        # パスの指定
        os.environ["RCLONE_CONFIG"] = rclone_conf_path
        onedrive_path = os.environ.get("ONEDRIVE_PATH")
        s3_bucket_path = os.environ.get("S3_BUCKET_PATH")

        if not onedrive_path or not s3_bucket_path:
            raise ValueError("ONEDRIVE_PATHまたはS3_BUCKET_PATHが設定されていません。")
        
        #print("debug1")
        
        # rcloneの実行
        # 更新が必要なファイルだけコピー
        result = subprocess.run(
            ["rclone", "copy", onedrive_path, s3_bucket_path, "--update"],
            capture_output=True,
            text=True
        )
        print("complete run")

        # rcloneのログを出力
        print(f"Rclone log: {result.stdout}")
        print(f"Rclone error: {result.stderr}")

        # rcloneが失敗した場合のエラーハンドリング
        if result.returncode != 0:
            raise RuntimeError(f"Rclone failed: {result.stderr}")

        return {
            'statusCode': 200,
            'body': f"File copied successfully from {onedrive_path} to {s3_bucket_path}"
        }

    except Exception as e:
        print(f"Error occurred: {str(e)}")
        return {
            'statusCode': 500,
            'body': f"Exception occurred while copying file from {os.environ.get('ONEDRIVE_PATH', 'None')} to {os.environ.get('S3_BUCKET_PATH', 'None')}: {str(e)}"
        }

【xxx】には、rcloneファイルをアップロードしたS3バケット名を指定してください。

③動作イメージ

2つのファイルをOneDriveに保存

  • OneDrive側の操作

  • Lambda実行
Response:
{
  "statusCode": 200,
  "body": "File copied successfully from onedrive:/test-folder to s3:xxx-test-rclone/test"
}
  • S3バケット内を確認

  • 2つのファイルがコピーされました。

OneDrive側に1つファイルを追加

新規ファイルをOnDriveにアップします。

  • 新規ファイルぶんだけ反映がされます。

おわりに

今回の構成では、OneDrive からS3への安全なファイル転送をrcloneとAWS Lambdaを組み合わせて実現しました。

本記事が、セキュアなファイル転送の仕組みを構築する参考になれば幸いです。
今後も AWS の活用方法に関する記事を投稿予定です。ぜひご期待ください!

MEGAZONE株式会社 Tech Blog

Discussion