【Express】IAMの一時的な認証情報(STS)を使用してS3にアクセスする

この記事では、ExpressアプリケーションからAWSの一時的な認証情報(STS)を使用してS3にアクセスする方法を解説する。IAMロールを利用した一時的な認証情報の取得から、取得したクレデンシャルを使ってS3バケットにアクセスするまでの手順を具体的に示し、AWSリソースを操作する方法を紹介する。

IAM の一時的な認証情報 – AWS Identity and Access Management

S3以外でも応用できる内容になっている。

前提

  • IAMに関する基礎知識がある
  • ローカルで実行可能なIAMユーザーを作成済み
  • 検証に使用できるアプリケーションがある

全体の手順

  • 【AWSの設定】IAMロールを作成し、必要な権限を付与
  • 【ロジック】一時的な認証情報を取得する
  • 【ロジック】一時的な認証情報を使用して、S3にアクセスする

1.【AWSの設定】IAMロールを作成し、必要な権限を付与

まずは、一時的な認証情報を取得するために必要なIAMロールを作成する。以下の手順で進める。

  1. IAMロールを作成
  2. 必要な権限を付与
  3. ロールのアタッチ

1. IAMロールの作成

  1. AWS Management Consoleにログイン
  2. IAM >「ロール」を選択
  3. 「ロールを作成」をクリック
  4. 信頼されたエンティティの種類として「AWSサービス」を選択
  5. ユースケースで「Elastic Container Service」を選択(←ECSタスク実行を想定)
  6. 「Elastic Container Service Task」を選択し、「次へ」をクリック

2. 許可を追加

このロールがアクセスするAWSサービスに必要な権限を付与する。

※わかりやすさ重視でAmazonS3FullAccessを選択しているが、アプリケーションで想定される操作に応じて最小限の権限に変更すること。例えば、ファイルの参照のみを許可する場合は以下のように変更する。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "s3:Get*",
                "s3:List*",
            ],
            "Resource": "*"
        }
    ]
}

3. 名前、確認、および作成

  1. ロール名を入力
    1. ロール名: sample-ecs-task-role
  2. ステップ 1: 信頼されたエンティティを選択する

上記の通り設定した場合、信頼されたエンティティはデフォルトで以下のように設定されている。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "",
            "Effect": "Allow",
            "Principal": {
                "Service": [
                    "ecs-tasks.amazonaws.com"
                ]
            },
            "Action": "sts:AssumeRole"
        }
    ]
}

信頼関係ポリシーは、特定のサービスやユーザーがIAMロールを引き受けることを許可するための重要な設定。今回の場合だと、ECSタスクが「sample-ecs-task-role」ロールを引き受け、そのロールに付与されたアクセス権限(例えば、S3へのアクセス権限)を使用して、AWSサービスにアクセスできるようになる。

4.作成したロールのARNを控える

ロールの作成が完了したら、ロールの詳細画面でARNをコピーしておく。このARNは一時的な認証情報を取得する次のステップで使用する。

2.【ロジック】一時的な認証情報を取得する

Expressアプリケーションで一時的な認証情報を取得 → 使用するところまで解説する。

  1. 一時的な認証情報を取得するクラスを作成
  2. アプリケーション起動時に処理を呼び出す

1. 一時的な認証情報を取得するクラスを作成

AWSの一時的な認証情報を取得するための専用のクラスを実装した。

AWS STS SDK for JavaScript (v3) を使用した の例 – AWS SDK for JavaScript

環境変数に以下をセットしておく。

  • AWS_REGION: 対象のAWSリージョン
  • AWS_IAM_ROLE_ARN: 上記(4.作成したロールのARNを控える)で確認したロールのARN
import { STSClient, AssumeRoleCommand, type AssumeRoleResponse } from "@aws-sdk/client-sts";
import dayjs from "dayjs";
import utc from "dayjs/plugin/utc";
import timezone from "dayjs/plugin/timezone";
import dotenv from "dotenv";

dayjs.extend(utc);
dayjs.extend(timezone);
dayjs.tz.setDefault("Asia/Tokyo");
dayjs.locale("ja");

dotenv.config();

/**
 * 一時的な認証情報を取得するクラス
 */
class AWSCredentialsManager {
  private static instance: AWSCredentialsManager;
  private stsClient: STSClient;
  private credentials?: AssumeRoleResponse["Credentials"] | undefined = undefined;

  private constructor() {
    this.stsClient = new STSClient({ region: process.env.AWS_REGION });
  }

  public static getInstance(): AWSCredentialsManager {
    if (!AWSCredentialsManager.instance) {
      AWSCredentialsManager.instance = new AWSCredentialsManager();
    }
    return AWSCredentialsManager.instance;
  }

  /**
   * 一時クリデンシャルの生成/取得
   */
  public async getTemporaryCredentials() {
    const nowInJST = dayjs().tz();
    if (
      this.credentials &&
      this.credentials.Expiration &&
      dayjs(this.credentials.Expiration).tz().isAfter(nowInJST)
    ) {
      return this.credentials;
    }
    try {
      const command = new AssumeRoleCommand({
        // Amazon Resource Name (ARN)
        RoleArn: process.env.AWS_IAM_ROLE_ARN,
        // セッション識別子
        RoleSessionName: `session-${Date.now()}`,
        // 有効期限
        DurationSeconds: 900,
      });
      const response = await this.stsClient.send(command);
      this.credentials = response.Credentials;
    } catch (error) {
      throw new Error("Failed to obtain temporary credentials: " + error);
    }
  }

  /** credentialsを返すgetter */
  public get getCredential() {
    return this.credentials;
  }
}

export default AWSCredentialsManager.getInstance();

アプリケーション起動時に処理を呼び出す

Express.jsの場合、src/index.tsの初期化処理にAWSCredentialsManager.getTemporaryCredentialsを実行する。

// サーバー起動
const startServer = async () => {
  try {
    await Promise.all([
      // DB接続
      connectDB(),
      // AWS一時クリデンシャル取得
      AWSCredentialsManager.getTemporaryCredentials(),
    ]);
    // サーバーを起動
    app.listen(PORT, () => {
      console.log(`App listening on port ${PORT}.`);
    });
  } catch (error) {
    console.error("Failed to connect to MongoDB:", error);
  }
};
startServer();

この処理が成功すれば、AWSの一時的な認証情報を使って、アプリケーションが各種サービスにアクセスできるようになる。

※起動した際にエラーが発生する場合

AccessDenied: User: arn:aws:iam::${AWSアカウントID}:user/${ユーザー名} is not authorized to perform: sts:AssumeRole on resource: arn:aws:iam::${AWSアカウントID}:role/sample-ecs-task-role

このエラーは、現在使用しているIAMユーザー(${ユーザー名})が、指定されたロール(sample-ecs-task-role)を引き受ける権限を持っていないことを示している。これを解決するには、IAMユーザーに対してAssumeRole権限を付与する必要がある。

  1. IAM > 「ユーザー」を選択
  2. エラーで指定されているユーザー(${ユーザー名})を選択
  3. 「許可を追加」>「インラインポリシーを作成」をクリック
  4. 「JSON」タブを選択し、以下のポリシーを入力
  5. ポリシーに名前を付けて保存する。

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": "sts:AssumeRole",
      "Resource": "arn:aws:iam::${AWSアカウントID}:role/sample-ecs-task-role"
    }
  ]
}

次に、ロール(sample-ecs-task-role)の信頼関係を設定して、指定したIAMユーザーがロールを引き受けることができるようにする。

  1. IAM >「ロール」を選択
  2. 引き受けるロール(sample-ecs-task-role)を選択
  3. 「信頼関係」タブを選択 →「信頼ポリシーを編集」をクリック
  4. 以下のように、IAMユーザーを信頼するエンティティとして追加
  5. 変更を保存
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::${AWSアカウントID}:user/${ユーザー名}"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}

こちらで再度起動すると、正常にアプリケーションが起動するはず。

3.【ロジック】一時的な認証情報を使用して、S3にアクセスする

以下は、アプリケーションがS3にアクセスする場合を想定した実装。

initializeClient にて、AWSCredentialsManager クラスから一時的な認証情報を取得し、S3Clientを生成。

これにより、uploadToS3 関数で一時的な認証情報を使ってS3にアクセスできるようになる。

import {
  S3Client,
  PutObjectCommand,
} from "@aws-sdk/client-s3";
import dotenv from "dotenv";
import { v4 as uuidv4 } from "uuid";
import AWSCredentialsManager from "./credentialsManager";

dotenv.config();

class AWSS3Config {
  private static instance: AWSS3Config;
  private s3Client: S3Client | undefined;

  private constructor() {}

  public static getInstance(): AWSS3Config {
    if (!AWSS3Config.instance) {
      AWSS3Config.instance = new AWSS3Config();
    }
    return AWSS3Config.instance;
  }

  private initializeClient() {
    const credentials = AWSCredentialsManager.getCredential;
    return new S3Client({
      region: process.env.AWS_REGION,
      credentials: {
        accessKeyId: credentials?.AccessKeyId!,
        secretAccessKey: credentials?.SecretAccessKey!,
        sessionToken: credentials?.SessionToken,
      },
    });
  }

  /**
   * S3にファイルをアップロードする
   * @param file ファイル
   * @param directory ディレクトリパス(例: 'post/{postId}')
   * @returns アップロードされたファイルのURL
   */
  public async uploadToS3(file: Express.Multer.File, directory: string) {
    if (!this.s3Client) {
      this.s3Client = this.initializeClient();
    }

    const params = {
      Bucket: process.env.AWS_S3_BUCKET_NAME,
      Key: `${directory}/${uuidv4()}-${file.originalname}`,
      Body: file.buffer,
      ContentType: file.mimetype,
    };

    try {
      const command = new PutObjectCommand(params);
      await this.s3Client.send(command);
    } catch (error) {
      console.error("Error uploading to s3: ", error);
      throw error;
    }
  }
}

export default AWSS3Config.getInstance();

忘備録: ECSタスク定義のロール追加

ローカル環境での起動に成功したが、ECSデプロイ後に以下のエラーに苦しんだので対処法をメモ。

AccessDenied: User: arn:aws:iam::${AWSアカウントID}:user/${ユーザー名} is not authorized to perform: sts:AssumeRole on resource: arn:aws:iam::${AWSアカウントID}:role/sample-ecs-task-role

対処1: タスク定義 > タスクロールを指定する

対処2: 対象ロール > 信頼関係の追加

自分自身を参照する信頼関係ポリシーを追加する。

{
            "Effect": "Allow",
            "Principal": {
                "AWS": "arn:aws:iam::${アカウントID}:role/sample-ecs-task-role"
            },
            "Action": "sts:AssumeRole"
        },

この記事は役に立ちましたか?

もし参考になりましたら、下記のボタンで教えてください。

関連記事

コメント

この記事へのコメントはありません。