Apex

バッチApexでAmazon S3のファイルを読み込む組み立て方

S3のファイルを1件ずつ同期処理で読み込むと、件数が増えたときにコールアウトの回数制限に当たります。一覧取得と個別読み込みを分け、バッチApexに乗せる組み立て方を説明します。

2023.02.27

なぜバッチで読み込むのか

S3に置かれたファイルを1つだけ読むなら、ボタン1つの同期処理でも足ります。ですが「バケットの中身をまとめて取り込む」ような場面では、ファイルの一覧を取ってから1件ずつダウンロードする、2段階のコールアウトが必要になります。Apexの1トランザクションで送れるコールアウトは、同期・非同期を問わず最大100回です。件数が増えるほど、同期処理の中でループしてコールアウトを重ねるやり方は限界に当たります。

バッチApexなら、対象を小さな単位(スコープ)に分けてexecuteメソッドを繰り返し呼び出せます。executeが1回呼ばれるごとに、そこで使えるコールアウトはまた最大100回に戻ります。スコープサイズを絞れば、ファイルが増えてもバッチの回数が増えるだけで、1回のexecuteが制限に当たることはありません。

用意するもの

用意するもの内容
S3バケット読み込む元。本記事では example-bucket を使います
Salesforce側の認証経路S3への発信を許可し署名を作る仕組み
読み込んだ内容の保存先Salesforce側の ContentVersion
Amazon S3のバケット一覧画面。右上のCreate bucketボタンが赤枠で示されている
S3の入り口です。読み込み元のバケットは、赤枠のCreate bucketから作ります。
S3のバケット作成画面。バケット名の入力欄とAWSリージョンの選択欄が並んでいる
バケット名とリージョンを決める画面です。リージョンはApex側のエンドポイントと必ず合わせてください。
バケット作成画面の最下部。Create bucketボタンが赤枠で示されている
入力し終えたら、画面のいちばん下にある赤枠のボタンで作成します。

認証経路は、送る側のアップロードと同じ考え方です。現在の公式ドキュメントが勧めているのは、リモートサイト設定ではなく、AWS Signature Version 4に対応した外部ログイン情報と指定ログイン情報の組み合わせです。アクセスキーとシークレットは外部ログイン情報側に保存し、Apexのコードには書きません。以降のコード例では、署名の仕組みを示すために自前でHMAC署名を組み立てていますが、実運用でアクセスキーを文字列としてApexに書く手順はお勧めしません。

BatchableとDatabase.AllowsCallouts

S3のファイル名(キー)の一覧をIterable<String>としてstartメソッドから返し、executeメソッドで1件ずつダウンロードする形にします。バッチApexでコールアウトを送るクラスは、Database.Batchableに加えてDatabase.AllowsCalloutsを実装する必要があります。この実装が無いと、コールアウトを送る行で実行時エラーになります。

public with sharing class S3FileReadBatch implements Database.Batchable<String>, Database.AllowsCallouts {
  public Iterable<String> start(Database.BatchableContext bc) {
    return new S3KeyIterable();
  }
  public void execute(Database.BatchableContext bc, List<String> keys) {
    for (String key : keys) {
      downloadAndSave(key);
    }
  }
  public void finish(Database.BatchableContext bc) {
  }
}

バッチのクラス宣言部分だけを抜き出しています。startとexecuteは後述します

一覧を取ってから個別に読む

startが返すIterable<String>の中身は、バケットへのGETリクエストで取った一覧です。バケット直下へのGETは、オブジェクトの中身ではなく、キーの一覧をXMLで返します。この一覧取得も1回のコールアウトなので、startが呼ばれた最初の段階でまとめて済ませておきます。

String host = 'example-bucket.s3.ap-northeast-1.amazonaws.com';
HttpRequest req = new HttpRequest();
req.setMethod('GET');
req.setEndpoint('https://' + host + '/');
req.setHeader('Host', host);
req.setHeader('Date', Datetime.now().formatGMT('EEE, dd MMM yyyy HH:mm:ss z'));
req.setHeader('Authorization', authHeaderForList);
HttpResponse res = new Http().send(req);
Dom.Document doc = res.getBodyDocument();

バケット直下へのGETでキーの一覧を取得する部分だけを抜き出しています

一覧が取れたら、あとはexecuteの中でキーごとにGETを送るだけです。一覧取得とファイル本体の取得は、URLもレスポンスの形も別物です。一覧はXMLのドキュメント、ファイル本体はバイナリのボディが返ります。

S3バケットのオブジェクト一覧。test.pngが1件、サイズと更新日時とともに表示されている
バケットに置いたファイルの一覧です。ここに出るName(キー)が、Apexで取る一覧の中身になります。

読み込んだ内容をSalesforceに保存する

読み込んだファイルはContentVersionとして保存し、ContentDocumentLinkで取引先などのレコードに紐づけます。

private void downloadAndSave(String key) {
  HttpRequest req = new HttpRequest();
  req.setMethod('GET');
  req.setEndpoint('https://example-bucket.s3.ap-northeast-1.amazonaws.com/' + EncodingUtil.urlEncode(key, 'UTF-8'));
  req.setHeader('Host', 'example-bucket.s3.ap-northeast-1.amazonaws.com');
  req.setHeader('Authorization', authHeaderForKey(key));
  HttpResponse res = new Http().send(req);
  if (res.getStatusCode() != 200) {
    return;
  }
  ContentVersion cv = new ContentVersion();
  cv.Title = key;
  cv.PathOnClient = key;
  cv.VersionData = res.getBodyAsBlob();
  insert cv;
}

1件のキーを読み込みContentVersionとして保存します。読み取ったファイルの中身以外、組織の既存データは変更しません

downloadAndSaveをキーの件数ぶん呼ぶと、DMLもファイルの件数だけ発生します。失敗をキャッチして文字列に貯めるだけでは、失敗した件数がどれだけあったのか後から追えません。バッチのfinishメソッドで、成功・失敗の件数をまとめて記録するところまでが1セットです。

大きいファイルを扱うときの制約

レスポンスのボディをgetBodyAsBlob()で受け取るときも、リクエストと同じ上限がかかります。非同期のバッチApexでは12MBまでです。バッチのスコープサイズを大きくしても、1件あたりのファイルがこの上限を超えていればその1件は読み込めません。大きいファイルが混ざる可能性があるバケットでは、事前にファイルサイズを一覧のレスポンスから確認し、上限を超えるものは別の手段(手動ダウンロードなど)に振り分けます。

ここで間違えやすい

間違い何が起きるか
Database.AllowsCalloutsを実装し忘れるコンパイルは通りますが、コールアウトを送る行で実行時エラーになります
一覧取得もキーごとのexecuteの中で毎回やり直す一覧取得ぶんのコールアウトが余計に発生し、100回の上限にすぐ近づきます
スコープサイズを大きくしすぎる1回のexecuteで送るコールアウトが100回に迫り、規模を増やすと超えます
レスポンスが200以外でも例外にせず処理を続ける失敗が黙って握りつぶされ、取り込めなかったファイルに気づけません
一覧取得と個別ダウンロードで別の署名の組み立て方を混同する対象のパスが違うため、片方だけ署名が合わず403になります

確認した環境

  • 2026年9月/Salesforce Summer '26(APIバージョン67.0)時点の公式ドキュメントで、バッチApexのコールアウト制限とDatabase.AllowsCalloutsの仕様を確認しています

まとめ

  • 複数ファイルの読み込みは、一覧取得と個別ダウンロードの2段階に分けます
  • コールアウトは1トランザクション最大100回。バッチApexならexecuteが呼ばれるたびに枠が戻ります
  • コールアウトを送るバッチクラスはDatabase.AllowsCalloutsの実装が必須です
  • 取り込んだファイルはContentVersionとして保存し、レコードにはContentDocumentLinkで紐づけます
  • レスポンスのボディも非同期で12MBまでという上限があり、大きいファイルは別の手段を検討します

参考:当時の画面

記事を最初に書いた当時の画面です。いまの手順と違うところは、各画像の説明に書いています。

Salesforceのすべてのリモートサイト一覧画面。新規リモートサイトボタンが赤枠で示されている
当時はここからS3への発信を許可していました。
リモートサイトの編集画面。リモートサイト名とURLの入力欄が赤枠で示されている
名前とURLを入れるだけの設定でした。
リモートサイトの詳細画面。URLにS3の東京リージョンのエンドポイントが入っている
登録し終えた状態です。URLがリージョン込みのS3エンドポイントになっている点を見てください。

Salesforceの導入・運用についてご相談ください

導入前の検討から、お使いの環境の改修・運用、AIとの連携まで承ります。状況を伺ったうえで、進め方をご提案します。