クロスアカウント環境におけるAWS S3バケットへのアクセスで詰まっていた

2025-07-24

はじめに

最近、業務でAWS S3を利用したシステム開発を行う中で、クロスアカウント環境において CopyObjectCommand を使ってS3オブジェクトをコピーしようとしたところ、バケットへのアクセス権限に関する問題でつまずく場面がありました。この問題の調査・対応を進める中で、AssumeRoleを通じたアクセス時のIAM設定の注意点や、S3のクロスアカウント権限管理など、実運用において重要なポイントをいくつも学ぶことが出来ました。

この記事では、その時の経験とともに、具体的な構成や問題の原因、そして解決策について自分用に整理して記録しておきます。

以下が今回直面したシステム構成の概要図です。2つのAWSアカウントが存在し、アカウントAが所有するS3バケットに対して、システムからファイルがアップロードされます。ファイルのアップロード完了をトリガーとしてS3のイベント通知が発火し、それによりLambda関数が呼び出され、CopyObjectCommand を用いてオブジェクトを別のバケットへコピーするという流れになっています。

AWS S3を用いたシステム構成図

本記事内のコード例はすべてChatGPTに生成してもらったものをコピペしているだけなので動作する保証はありません。こういうイメージで書いたという前提でお願いします。

バケットにアクセス出来ない

開発中は同一AWSアカウント内に、2つのS3バケット(バケットAとバケットB)を用意し、先述の通りにファイルがバケットAにアップロードされた際に、それをバケットBへコピーするLambda関数を実装していました。

両バケットが同一アカウント内に存在しているため、Lambda関数にはIAMロールを通じてS3の操作権限(s3:GetObject および s3:PutObject など)を付与することで、特別なクロスアカウント設定を行うことなく、バケットAとバケットBの両方にアクセスすることが可能でした。

開発時のAWS S3を用いたシステム構成図

import {
  S3Client,
  CopyObjectCommand,
} from "@aws-sdk/client-s3";
import { S3Event, S3Handler } from "aws-lambda";

const s3Client = new S3Client({ region: "ap-northeast-1" });
const DEST_BUCKET = "destination-bucket-B";

export const handler: S3Handler = async (event: S3Event) => {
  console.log("Received S3 Event:", JSON.stringify(event, null, 2));

  for (const record of event.Records) {
    const srcBucket = record.s3.bucket.name;
    const srcKey = decodeURIComponent(record.s3.object.key.replace(/+/g, " "));
    const copySource = `${srcBucket}/${srcKey}`;

    try {
      await s3Client.send(new CopyObjectCommand({
        Bucket: DEST_BUCKET,
        Key: srcKey,
        CopySource: copySource,
      }));

      console.log(`Copied: ${copySource} → ${DEST_BUCKET}/${srcKey}`);
    } catch (error) {
      console.error("Copy failed:", error);
      throw error;
    }
  }
};

このような構成とLambdaを本番環境にデプロイし、アカウントAからアカウントBに対してバケット間でファイルをコピー出来るものだと確信していました。しかし、ファイルコピー時にCopyObjectCommandでファイルがコピー出来ないという問題に直面してしまいました。

問題の切り分け

ファイルが二つのアカウントのS3バケット間でコピー出来ないという問題だけに囚われてしまい、どうやって問題を解決すればよいか悩んでしまったため、次のように問題の切り分けを行うようアドバイスをいただきました。問題の切り分けと実際に検証してみた結果を以下に示します。

  • そもそもファイルコピーではなくそれぞれのバケットにアクセス出来るのか
    • バケットAにはアクセス出来るのか -> 出来た
    • バケットBにはアクセス出来るのか -> 出来なかった
  • バケット内のオブジェクトは取得出来るのか
    • バケットAでオブジェクトを取得出来るのか -> 出来た
    • バケットBでオブジェクトを取得出来るのか -> そもそもバケットにアクセス出来なかったので出来ないことは自明

この時点でファイルコピーの操作によって問題が起きているのではなく、バケットBにアクセス出来ないという原因によってこの問題が起きていることが分かりました。バケットBにアクセス出来ない原因を探るためにBucket変数やKeyCopySourceの中身を見てみましたがtypoやファイル名などの間違いは無く、正しい値が設定されていました。

ここでまたつまずいてしまったので、クロスアカウント環境でのS3バケットのアクセスについて教えていただきました。以下の2記事を参考にアカウントAにおけるS3クライアントにAssumeRoleを行って、それぞれのバケットにアクセス出来るか試してみることにしました。

原因、解決策

AssumeRoleとは以下の図のように、アカウントAからアカウントBに対して、アカウントBのS3バケットにアクセスできるRoleを引き受けてくれます。アカウントAがAssumeRoleを使うことで、一時的なセッション間においてアカウントBのクレデンシャル情報をAWS STSというサービスを使用して、まるでアカウントBになりきってアカウントBのバケットなどにアクセスしてくれます。

AssumeRoleについて図解

LambdaにおいてAssumeRoleする処理を書き、バケットAとバケットBにアクセスできるか試してみました。

const getS3ClientByAssumeRole = async (
  roleArn: string,
  externalId: string
): Promise<S3Client | undefined> => {
  const creds = await getAssumedCredentials(roleArn, externalId);
  if (!creds) return undefined;

  return new S3Client({
    region: "ap-northeast-1",
    credentials: {
      accessKeyId: creds.AccessKeyId!,
      secretAccessKey: creds.SecretAccessKey!,
      sessionToken: creds.SessionToken!,
    },
  });
};

const getAssumedCredentials = async (
  roleArn: string,
  externalId: string
): Promise<STSCredentials | undefined> => {
  const sts = new STSClient({ region: "ap-northeast-1" });

  const command = new AssumeRoleCommand({
    RoleArn: roleArn,
    ExternalId: externalId,
    RoleSessionName: `assumed-role-${Date.now()}`,
  });

  try {
    const response = await sts.send(command);
    return response.Credentials;
  } catch (error) {
    console.error("Failed to assume role:", error);
    return undefined;
  }
};

export const handler: S3Handler = async (event: S3Event) => {
  console.log("Received S3 Event:", JSON.stringify(event, null, 2));

  const s3 = await getS3ClientByAssumeRole(ROLE_ARN, EXTERNAL_ID);
  if (!s3) {
    console.error("S3 client initialization failed");
    return;
  }

  // snip...
}

そうすると本節の冒頭で書いた問題の切り分けに変化が生じました。

  • そもそもファイルコピーではなくそれぞれのバケットにアクセス出来るのか
    • バケットAにはアクセス出来るのか -> 出来なかった
    • バケットBにはアクセス出来るのか -> 出来た
  • バケット内のオブジェクトは取得出来るのか
    • バケットAでオブジェクトを取得出来るのか -> そもそもバケットにアクセス出来なかったので出来ないことは自明
    • バケットBでオブジェクトを取得出来るのか -> 出来た

つまり、AssumeRoleしたことによってアカウントAによってアクセスできていたバケットが反転してしまいました。クロスアカウント環境においてAssumeRoleをするかしないかによってアクセスできるバケットがどちらか一方になってしまっています。なぜならアカウントAがAssumeRoleをすると別のアカウントBになり切ってしまうからです。

どちらか一方のアカウントのバケットにしかアクセスできないということはCopyObjectCommandを使用すると、その中の引数でBucketでコピー先のバケット名、CopySourceでコピー元のバケット名を必要とするために、CopyObjectCommandが使えないというのが今回の大きな問題の原因でした。

この大きな問題を解決するためにはアカウントAでバケットAにアクセスすることの出来るS3クライアントと、アカウントAでAssumeRoleしてアカウントBのバケットBにアクセス出来るS3クライアントの二つを用意します。アカウントAのバケットAにアクセスすることの出来るS3クライアントがバケットAからgetObjectを行ってBufferを取得します。取得したBufferをアカウントBのバケットBにアクセスすることの出来るS3クライアントでputObjectを行いファイルをバケットBにアップロードします。これにより、疑似的にCopyObjectCommandを行うことが出来たため、悩んでいた問題は解決しました。

AssumeRoleを行ったRoleで別アカウントのバケットにアクセスする

const s3ClientA = new S3Client({ region: REGION });

// snip...

export const handler: S3Handler = async (event: S3Event) => {
  console.log("Received S3 Event:", JSON.stringify(event, null, 2));

  const s3ClientB = await getS3ClientByAssumeRole(ROLE_ARN, EXTERNAL_ID);
  if (!s3ClientB) {
    console.error("Failed to initialize assumed S3 client.");
    return;
  }

  for (const record of event.Records) {
    const srcBucket = record.s3.bucket.name;
    const srcKey = decodeURIComponent(record.s3.object.key.replace(/+/g, " "));
    console.log(`Processing ${srcBucket}/${srcKey}`);

    try {
      // --- getObject from バケットA (アカウントA) ---
      const getResp = await s3ClientA.send(
        new GetObjectCommand({
          Bucket: srcBucket,
          Key: srcKey,
        })
      );

      const bodyStream = getResp.Body as Readable;
      const fileBuffer = await streamToBuffer(bodyStream);
      const contentType = getResp.ContentType || "application/octet-stream";

      // --- putObject to バケットB (アカウントB) ---
      await s3ClientB.send(
        new PutObjectCommand({
          Bucket: DEST_BUCKET,
          Key: srcKey,
          Body: fileBuffer,
          ContentType: contentType,
        })
      );

      console.log(`Successfully copied ${srcKey} to ${DEST_BUCKET}`);
    } catch (error) {
      console.error(`Error processing ${srcKey}:`, error);
    }
  }
};

疑問点、そして終わりに

疑問に思ったこととして、AssumeRoleされるアカウントBのロールにアカウントAのバケットAについてのアクセス権限をつけることでこの問題自体は解決できるのではないかと考えました。しかし、それはアカウントAにアクセスできるようになるため(バケットAだけ、といった特定のリソースであっても)セキュリティホールになるということを教えていただき、なるほどな~と思いました。

AssumeRoleについて知らない状態からAssumeRoleやAWSの権限などについてを知り、それが何を行っているのかさわりだけでも分かったことでAWSの権限管理への興味や関心が強くなりました。JAWS UG広島において、この内容を話したうえでIAMや権限周りについての面白さが体験できたことを話すと、この調子でIAMについて勉強してみると良いとのアドバイスをいただけました。AWSといえば、という主要なサービスは沢山ありますが、僕はIAMへの理解をもっと深めていきたいです。

Article written by.
Written by
Haruki Tazoe
November 17, 1999

Copyright © 2025 - All right reserved