ApexでAmazon S3へファイルを送るときの注意点
取引先に添付したファイルを、手作業でS3にアップロードするのは面倒です。Apexのコールアウトなら自動化できますが、認証情報の置き場所と署名の作り方でつまずきます。ここを順番に押さえます。
なぜApexから直接アップロードするのか
Salesforceのレコードに添付したファイルを、外部の保管先や連携先に渡したい場面があります。ダウンロードしてS3のコンソールから手作業でアップロードすれば済みますが、件数が増えると続きません。Apexからコールアウトを1回送るだけで、レコードの保存やボタンのクリックに合わせて自動で転送できます。手作業を無くせるのが、Apexで直接送る一番の理由です。
用意するもの
| 用意するもの | 内容 |
|---|---|
| S3バケット | ファイルの送り先。本記事では example-bucket という名前で作成します |
| Salesforce側の認証経路 | S3への発信を許可し、署名を作る仕組み。リモートサイト設定と指定ログイン情報の2通りがあります |
| 取り込むファイル | Salesforce側で ContentVersion として保存されているファイル |
S3バケットを作る
Amazon S3のコンソールを開く
「Create bucket」を押します。
バケット名を入力する
ここでは
example-bucketとします。リージョンも合わせて選びます。既定値のまま作成する
「Create bucket」を押して作成します。パブリックアクセスは既定でブロックしたままにします。
作成されたことを確認する
バケット一覧に
example-bucketが表示されていれば完了です。
Salesforce側の認証経路を選ぶ
Apexからの発信は、まず送信先のドメインを許可する必要があります。古くからある方法はリモートサイト設定で、名前と送信先URLを登録するだけです。
指定ログイン情報を使う場合の構成は、次の2つに分かれます。
| 部品 | 持つ情報 |
|---|---|
| 外部ログイン情報 | 認証方式(AWS Signature Version 4)、アクセスキー、シークレット、AWSのリージョンとサービス名 |
| 指定ログイン情報 | コールアウト先のURL。外部ログイン情報と紐づけて使います |
外部ログイン情報にAWSの認証情報を入れてしまえば、Apex側の setEndpoint は callout:指定ログイン情報名/パス の形で書くだけになり、署名の計算はSalesforceの側で行われます。アクセスキーとシークレットをApexのコードに文字列として書く必要が無くなるのが、最大の利点です。
自分で署名する場合の仕組み
外部ログイン情報にまだ移行していない組織や、単純なリモートサイト設定のまま動かしたい場合は、Apex側でAWSの署名を組み立てる必要があります。仕組みを知っておくと、指定ログイン情報に切り替えたときの動きも理解しやすくなります。
署名の元になる文字列は、HTTPメソッドと対象のパス、日付などを改行でつないだものです。これを Crypto.generateMac でHMAC-SHA1署名し、Base64エンコードして Authorization ヘッダーに載せます。
String stringToSign = method + '\n\n' + contentType + '\n' + dateHeader + '\n' + '/' + bucketName + '/' + objectKey; Blob mac = Crypto.generateMac('HMACSHA1', Blob.valueOf(stringToSign), Blob.valueOf(secretKey)); String signature = EncodingUtil.base64Encode(mac); String authHeader = 'AWS' + ' ' + accessKey + ':' + signature;
署名文字列を作りHMAC-SHA1で署名する部分だけを抜き出しています。単体では動きません
ContentVersionからファイルを取り出す
以前の書き方では Attachment からファイルを読んでいましたが、Attachment は新規のファイル添付には使いません。取引先に添付されたファイルは ContentDocumentLink で紐づいた ContentVersion から取得します。
SELECT Id, Title, FileExtension, VersionData FROM ContentVersion WHERE ContentDocumentId IN ( SELECT ContentDocumentId FROM ContentDocumentLink WHERE LinkedEntityId = :accountId ) ORDER BY CreatedDate DESC LIMIT 1
取引先(取引先Id)に紐づく最新のファイル本体を1件取得します
VersionData には、ファイルの中身がBlobとして入っています。このBlobを、そのままアップロードのリクエストボディに使います。
Apexからアップロードする
PUTメソッドでファイルのパスへ直接送ります。ボディにはBase64にせず、元のバイナリのBlobをそのまま渡します。
public with sharing class S3FileUploader { public static Integer uploadToS3(ContentVersion cv, String accessKey, String secretKey) { String bucketName = 'example-bucket'; String host = bucketName + '.s3.ap-northeast-1.amazonaws.com'; String objectKey = EncodingUtil.urlEncode(cv.Title + '.' + cv.FileExtension, 'UTF-8'); String contentType = 'application/octet-stream'; String dateHeader = Datetime.now().formatGMT('EEE, dd MMM yyyy HH:mm:ss z'); String stringToSign = 'PUT\n\n' + contentType + '\n' + dateHeader + '\n' + '/' + bucketName + '/' + objectKey; Blob mac = Crypto.generateMac('HMACSHA1', Blob.valueOf(stringToSign), Blob.valueOf(secretKey)); String authHeader = 'AWS' + ' ' + accessKey + ':' + EncodingUtil.base64Encode(mac); HttpRequest req = new HttpRequest(); req.setMethod('PUT'); req.setEndpoint('https://' + host + '/' + objectKey); req.setHeader('Host', host); req.setHeader('Date', dateHeader); req.setHeader('Content-type', contentType); req.setHeader('Authorization', authHeader); req.setBodyAsBlob(EncodingUtil.base64Decode(EncodingUtil.base64Encode(cv.VersionData))); HttpResponse res = new Http().send(req); return res.getStatusCode(); } }
ContentVersionの中身をS3へPUTでアップロードします。読み取り専用のSOQLと外部への書き込みだけで、組織のデータは変更しません
Content-Length はヘッダーへ手で書きません。Base64エンコード後の文字列長を Content-Length に入れると、実際に送るバイナリの長さと食い違います。送信バイト数はSalesforce側が自動で計算するので、手計算は不要です。
大きいファイルを扱うときの制約
setBodyAsBlob で渡せるリクエストボディの大きさは、同期のApexで6MB、非同期(@future・バッチ)で12MBまでです。この上限はApexのヒープサイズ上限と同じ数字で、ContentVersion から読み込んだファイルをApexのメモリ上で扱っている間はヒープを消費します。動画のような大きいファイルは、同期の処理では送り切れないことがあります。バッチApexやキューアブルに乗せて非同期で送るか、送るファイルの大きさそのものを見直します。
ここで間違えやすい
| 間違い | 何が起きるか |
|---|---|
| アクセスキーとシークレットをApexの文字列に直書きする | コードを読める人に漏れます。外部ログイン情報に移します |
| 新しく作ったバケットにSignature Version 2で署名する | AWSが2020年6月24日以降に作られたバケットでは拒否します |
Content-Length をBase64後の文字列長で計算する | 実際に送るバイナリの長さと食い違い、リクエストが失敗します |
Attachment のままファイルを読み込む | 新規のファイル添付には使われないオブジェクトです。ContentVersion を使います |
| コールアウト1回あたりのタイムアウトを既定の10秒のまま大きいファイルに使う | 最大120,000ミリ秒まで延ばせますが、延ばさないとタイムアウトで失敗します |
確認した環境
- 2026年9月/Salesforce Summer '26(APIバージョン67.0)時点の公式ドキュメントで、コールアウトの制限と指定ログイン情報の仕様を確認しています
- AWS Signature Version 2の廃止時期は、AWSの公式ブログで確認しています
まとめ
- Apexのコールアウトで、レコードに添付したファイルをS3へ自動で送れます
- 認証情報は外部ログイン情報(指定ログイン情報)に置き、Apexのコードには書きません
- 自前で署名する方式(Signature Version 2)は、2020年6月24日以降に作ったバケットでは使えません
- ファイルは
AttachmentではなくContentVersionから取得します - 送れるボディの大きさは同期6MB・非同期12MBまでです
Salesforceの導入・運用についてご相談ください
導入前の検討から、お使いの環境の改修・運用、AIとの連携まで承ります。状況を伺ったうえで、進め方をご提案します。