【Storage Browser for Amazon S3】任意のバケット名でS3を作成する

【Storage Browser for Amazon S3】任意のバケット名でS3を作成する
この記事をシェアする

はじめに

はじめまして!
クラウドビルダーズのKawabataと申します。

こちらは前回の記事の続きになります。

Storage Browser for Amazon S3で任意のバケット名でS3を作成したいと思ったので、やってみました。
今回紹介するやり方は、あくまで私が検証して実装したやり方なので、もしかしたら他にもっとスマートなやり方があるかもしれません。

AmplifyがS3を参照する仕組みについて

いろいろ調査してみた結果、amplify_outputs.jsonというファイルにAmplifyがバックエンドとして利用するリソースの情報が記載されていることがわかりました。
npx ampx pipeline-deployコマンドを実行すると、backend.tsに記載されているリソースがデプロイされます。
その結果として、作成されたリソースの情報がamplify_outputs.jsonに記載されます。

Amplifyの機能でバックエンドのリソースを作成してしまうと、Amplifyが良しなにリソース名を生成してしまうので、こちらで設定したい名前を利用することはできませんでした。

どうやって任意のバケット名で作成したS3を利用するのか

では、どうやったら任意のバケット名のS3を利用できるかと考えたときに、amplify_outputs.jsonと同じ構成のファイルを生成し、S3のバケット名の部分をこちらで作成したバケット名に書き換えればいいと考えました。

いろいろな方法を検証してみましたが、CDKでバックエンドで利用するリソースを生成し、そのリソース情報を使ってamplify_outputs.jsonを生成する方法がよさそうでした。

やってみた

Amplifyの作成方法とコードについては、前述のブログを確認ください。

コードについては以下からcloneすることも可能です。

また、今回作成したコードは以下になります。

CDKプロジェクトの作成

  1. プロジェクトのルートでCDKのプロジェクトをセットアップします。
mkdir cdk
cd cdk
cdk init app --language typescript

2. constructsフォルダを作成します。

mkdir lib/constructs

3. constructs配下にCognito、S3、IAMのconstructを作成します。

  • Cognito
    • ユーザープール、IDプール、ユーザープールクライアントを作成
    • getCognitoAmplifyConfigでamplify_config.jsonでCognitoを扱うためのjsonを作成
import { AccountRecovery, CfnIdentityPool, UserPool, UserPoolClient, VerificationEmailStyle } from 'aws-cdk-lib/aws-cognito';
import { Construct } from 'constructs';
import { StackProps } from 'aws-cdk-lib';

export interface CognitoProps extends StackProps {
    bucketName: string;
}

export class Cognito extends Construct {
    /** IDプール */
    public readonly identityPool: CfnIdentityPool;
    /** ユーザープール */
    public readonly userPool: UserPool;
    /** ユーザープールクライアント */
    public readonly userPoolClient: UserPoolClient;

    constructor(scope: Construct, id: string, props: CognitoProps) {
        super(scope, id);

        // ユーザープールの作成
        this.userPool = new UserPool(this, 'UserPool', {
            userPoolName: 'storage-browser-sample-user-pool',
            signInAliases: {
                email: true
            },
            selfSignUpEnabled: true,
            accountRecovery: AccountRecovery.EMAIL_ONLY,
            userVerification: {
                emailStyle: VerificationEmailStyle.CODE
            },
            passwordPolicy: {
                minLength: 8,
                requireLowercase: true,
                requireUppercase: true,
                requireDigits: true,
                requireSymbols: true
            },
            standardAttributes: {
                email: {
                    required: true,
                    mutable: true
                }
            }
        });

        // ユーザープールクライアントの作成
        this.userPoolClient = this.userPool.addClient('UserPoolClient', {
            userPoolClientName: 'storage-browser-sample-user-pool-client',
            authFlows: {
                adminUserPassword: true,
                userPassword: true,
                userSrp: true
            }
        });

        // IDプールの作成
        this.identityPool = new CfnIdentityPool(this, 'IdentityPool', {
            identityPoolName: 'storage-browser-sample-identity-pool',
            allowUnauthenticatedIdentities: false,
            cognitoIdentityProviders: [
                {
                    clientId: this.userPoolClient.userPoolClientId,
                    providerName: this.userPool.userPoolProviderName
                }
            ]
        });
    }

    // Amplify用の認証設定を取得
    public getCognitoAmplifyConfig() {
        return {
            user_pool_id: this.userPool.userPoolId,
            aws_region: this.userPool.stack.region,
            user_pool_client_id: this.userPoolClient.userPoolClientId,
            identity_pool_id: this.identityPool.ref,
            mfa_methods: [],
            standard_required_attributes: ['email'],
            username_attributes: ['email'],
            user_verification_types: ['email'],
            groups: [],
            mfa_configuration: 'NONE',
            password_policy: {
                min_length: 8,
                require_lowercase: true,
                require_numbers: true,
                require_symbols: true,
                require_uppercase: true
            },
            unauthenticated_identities_enabled: false
        };
    }
}
  • S3
    • S3バケット、バケットポリシー、Cognitoで認証されたユーザーへのS3バケットへのアクセス権限を作成
    • getS3AmplifyConfigでamplify_config.jsonでS3を扱うためのjsonを作成
    • S3バケットにCORSを設定する必要がある
import { RemovalPolicy, StackProps } from 'aws-cdk-lib';
import { AnyPrincipal, ArnPrincipal, Effect, PolicyStatement, Role } from 'aws-cdk-lib/aws-iam';
import { BlockPublicAccess, Bucket, BucketEncryption, BucketPolicy, HttpMethods } from 'aws-cdk-lib/aws-s3';
import { Construct } from 'constructs';

export class S3 extends Construct {
    /** S3バケット */
    public readonly bucket: Bucket;
    /** バケットポリシー */
    private readonly bucketPolicy: BucketPolicy;

    constructor(scope: Construct, id: string, props: StackProps) {
        super(scope, id);

        // S3バケットの作成
        this.bucket = new Bucket(this, 'Bucket', {
            bucketName: 'storage-browser-sample-bucket',
            encryption: BucketEncryption.S3_MANAGED,
            blockPublicAccess: BlockPublicAccess.BLOCK_ALL,
            removalPolicy: RemovalPolicy.DESTROY,
            cors: [
                {
                    allowedHeaders: ['*'],
                    allowedMethods: [
                        HttpMethods.GET,
                        HttpMethods.HEAD,
                        HttpMethods.PUT,
                        HttpMethods.POST,
                        HttpMethods.DELETE,
                    ],
                    allowedOrigins: ['*'],
                    exposedHeaders: ['x-amz-server-side-encryption', 'x-amz-request-id', 'x-amz-id-2', 'ETag'],
                    maxAge: 3000,
                },
            ],
        });

        // バケットポリシーの作成
        this.bucketPolicy = new BucketPolicy(this, 'BucketPolicy', {
            bucket: this.bucket,
        });

        // HTTPSのみを許可するポリシーを追加
        this.bucketPolicy.document.addStatements(
            new PolicyStatement({
                effect: Effect.DENY,
                principals: [new AnyPrincipal()],
                actions: ['s3:*'],
                resources: [this.bucket.bucketArn, `${this.bucket.bucketArn}/*`],
                conditions: {
                    Bool: {
                        'aws:SecureTransport': 'false',
                    },
                },
            })
        );
    }

    /**
     * 認証済みロールにバケットアクセス権限を付与
     */
    public addAuthenticatedRolePolicy(authenticatedRole: Role): void {
        this.bucketPolicy.document.addStatements(
            new PolicyStatement({
                effect: Effect.ALLOW,
                principals: [new ArnPrincipal(authenticatedRole.roleArn)],
                actions: ['s3:PutBucketPolicy', 's3:GetBucket*', 's3:List*', 's3:DeleteObject*'],
                resources: [this.bucket.bucketArn, `${this.bucket.bucketArn}/*`],
            })
        );
    }

    /**
     * Amplify用のストレージ設定を取得
     */
    public getS3AmplifyConfig() {
        return {
            aws_region: this.bucket.stack.region,
            bucket_name: this.bucket.bucketName,
            buckets: [
                {
                    name: this.bucket.bucketName,
                    bucket_name: this.bucket.bucketName,
                    aws_region: this.bucket.stack.region,
                    paths: {
                        'public/*': {
                            'guest': ['read'],
                            'authenticated': ['read', 'write', 'delete']
                        },
                        'protected/*': {
                            'authenticated': ['read'],
                            'private': ['read', 'write', 'delete']
                        },
                        'private/*': {
                            'private': ['read', 'write', 'delete']
                        }
                    }
                },
            ],
        };
    }
}
  • IAM
    • Cognito認証済みユーザー用のIAM Roleを作成
import { StackProps } from 'aws-cdk-lib';
import { CfnIdentityPool, CfnIdentityPoolRoleAttachment } from 'aws-cdk-lib/aws-cognito';
import { Effect, FederatedPrincipal, PolicyStatement, Role } from 'aws-cdk-lib/aws-iam';
import { Construct } from 'constructs';

/**
 * IAMのプロパティ
 */
export interface IamProps extends StackProps {
    /** S3バケット名 */
    bucketName: string;
    /** Cognito Identity Pool */
    identityPool: CfnIdentityPool;
}

/**
 * IAMリソース
 * Cognito認証済みユーザー用のロールとポリシーを作成する
 */
export class Iam extends Construct {
    /** 認証済みユーザー用のロール */
    public readonly authenticatedRole: Role;

    constructor(scope: Construct, id: string, props: IamProps) {
        super(scope, id);

        // 認証済みユーザー用のロールを作成
        this.authenticatedRole = new Role(this, 'AuthenticatedRole', {
            roleName: 'storage-browser-sample-auth-role',
            assumedBy: new FederatedPrincipal(
                'cognito-identity.amazonaws.com',
                {
                    StringEquals: {
                        'cognito-identity.amazonaws.com:aud': props.identityPool.ref,
                    },
                    'ForAnyValue:StringLike': {
                        'cognito-identity.amazonaws.com:amr': 'authenticated',
                    },
                },
                'sts:AssumeRoleWithWebIdentity'
            ),
        });

        // パスごとのポリシー設定
        const pathConfigs = [
            {
                path: 'public/*',
                actions: ['s3:GetObject', 's3:PutObject', 's3:DeleteObject', 's3:ListBucket'],
            },
            {
                path: 'protected/{entity_id}/*',
                actions: ['s3:GetObject', 's3:ListBucket'],
            },
            {
                path: 'private/{entity_id}/*',
                actions: ['s3:GetObject', 's3:PutObject', 's3:DeleteObject', 's3:ListBucket'],
                conditions: {
                    'StringLike': {
                        'cognito-identity.amazonaws.com:sub': '${cognito-identity.amazonaws.com:sub}'
                    }
                }
            }
        ];

        // パスごとにポリシーを作成
        for (const config of pathConfigs) {
            // GetObject, PutObject, DeleteObject用のポリシー
            const objectActions = config.actions.filter(action => action !== 's3:ListBucket');
            if (objectActions.length > 0) {
                this.authenticatedRole.addToPolicy(
                    new PolicyStatement({
                        actions: objectActions,
                        resources: [`arn:aws:s3:::${props.bucketName}/${config.path}`],
                        effect: Effect.ALLOW,
                        conditions: config.conditions,
                    })
                );
            }

            // ListBucket用のポリシー
            if (config.actions.includes('s3:ListBucket')) {
                this.authenticatedRole.addToPolicy(
                    new PolicyStatement({
                        actions: ['s3:ListBucket'],
                        resources: [`arn:aws:s3:::${props.bucketName}`],
                        effect: Effect.ALLOW,
                        conditions: {
                            StringLike: {
                                's3:prefix': [config.path, config.path.replace('/*', '/')],
                            },
                            ...config.conditions,
                        },
                    })
                );
            }
        }

        // IdentityPoolにロールをアタッチ
        new CfnIdentityPoolRoleAttachment(this, 'IdentityPoolRoleAttachment', {
            identityPoolId: props.identityPool.ref,
            roles: {
                authenticated: this.authenticatedRole.roleArn,
            },
        });
    }
}

4. cdk-stack.tsを以下に修正します。

  • 各リソースを作成
  • S3バケットに認証済みロールからのアクセス許可を追加
  • amplify_outputs.json生成のために、CloudFormationのOutputに同じ形式のjsonを出力
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import { S3 } from './constructs/s3';
import { Cognito } from './constructs/cognito';
import { Iam } from './constructs/iam';

export class CdkStack extends cdk.Stack {
    constructor(scope: Construct, id: string, props: cdk.StackProps) {
        super(scope, id, props);

            // S3リソースを作成
            const s3 = new S3(this, 'S3', {
            ...props,
        });

        // Cognitoリソースを作成
        const cognito = new Cognito(this, 'Cognito', {
            ...props,
            bucketName: s3.bucket.bucketName,
        });

        // IAMリソースを作成
        const iam = new Iam(this, 'IAM', {
            ...props,
            bucketName: s3.bucket.bucketName,
            identityPool: cognito.identityPool,
        });

        // S3バケットに認証済みロールのポリシーを追加
        s3.addAuthenticatedRolePolicy(iam.authenticatedRole);

        // Amplify設定をJSON形式で出力
        const amplifyOutputs = {
            auth: cognito.getCognitoAmplifyConfig(),
            storage: s3.getS3AmplifyConfig(),
            version: '1.3',
        };

        // Amplify設定を出力
        new cdk.CfnOutput(this, 'AmplifyOutputs', {
            value: JSON.stringify(amplifyOutputs),
        });
    }
}

amplify_config.jsonの作成

  1. amplify_config.jsonを作成するためのスクリプトを作成します。
// ルート配下にscriptsフォルダを作成
mkdir scripts
  • CDKのOutputからamplify_config.jsonの情報を取得し、amplify_config.jsonを生成
import { writeFileSync } from 'fs';
import { join } from 'path';
import { CloudFormation } from '@aws-sdk/client-cloudformation';

/**
 * CloudFormationからスタックの出力を取得する関数
 */
async function getStackOutputs() {
    const cfn = new CloudFormation({ region: 'ap-northeast-1' });

    // CDKのスタック名を定義
    const stackName = 'CdkStack';

    const { Stacks } = await cfn.describeStacks({ StackName: stackName });
    const outputs = Stacks?.[0]?.Outputs ?? [];

    // AmplifyOutputsを探して解析
    const amplifyOutput = outputs.find(output => output.OutputKey === 'AmplifyOutputs');
    if (!amplifyOutput?.OutputValue) {
        throw new Error('AmplifyOutputs not found in stack outputs');
    }

    return JSON.parse(amplifyOutput.OutputValue);
}

/**
 * 設定ファイルを生成する関数
 */
async function generateOutputs() {
    try {
        const outputs = await getStackOutputs();
        writeFileSync(
            join(process.cwd(), 'amplify_outputs.json'),
            JSON.stringify(outputs, null, 2)
        );
        console.log('✅ Generated amplify_outputs.json successfully');
    } catch (error) {
        console.error('❌ Failed to generate amplify_outputs.json:', error);
        process.exit(1);
    }
}

// スクリプトが直接実行された場合のみ実行
if (require.main === module) {
    generateOutputs();
}

2. page.tsxを修正

"use client";

import "@aws-amplify/ui-react/styles.css";
import "@aws-amplify/ui-react-storage/storage-browser-styles.css";
import outputs from "@/amplify_outputs.json";
import { Authenticator, Button } from "@aws-amplify/ui-react";
import { StorageBrowser } from "@aws-amplify/ui-react-storage";
import { Amplify } from "aws-amplify";

Amplify.configure(outputs);

export default function App() {
  return (
    <Authenticator>
      {({ signOut }) => (
        <>
          <Button onClick={signOut}>Sign Out</Button>
          <StorageBrowser />
        </>
      )}
    </Authenticator>
  );
}

3. 最後にamplify.ymlでCDKをデプロイして、amplify_config.jsonを作成できるように修正します。

version: 1
backend:
  phases:
    build:
      commands:
        - npm ci --cache .npm --prefer-offline
        - cd cdk
        - npm install -g aws-cdk
        - cdk deploy --require-approval never
        - cd ..
        - npx tsx scripts/generateAmplifyConfig.ts
frontend:
  phases:
    build:
      commands:
        - npm run build
  artifacts:
    baseDirectory: .next
    files:
      - '**/*'
  cache:
    paths:
      - .next/cache/**/*
      - .npm/**/*
      - node_modules/**/*

デプロイと確認

  1. Amplifyのコンソールから今回作成したアプリを選択し、アプリケーションの設定 -> 全般設定と遷移してサービスロールを確認します。
  2. IAMコンソールから確認したIAM Roleを選択し、AWSCloudFormationReadOnlyAccessポリシーを追加します。
  3. GitHubに変更をPushします。
git add .
git commit -m "add cdk"
git push -u origin main 

4. デプロイが始まるので完了するまで待ちます。

5. デプロイが完了したらブラウザからドメインにアクセスします。

6. アカウントを作成し、ログインします。

7. S3のバケット名が「storage-browser-sample-bucket」になっていれば成功です。

8. publicフォルダにファイルをアップロードしてみます。

9. S3バケットにファイルがアップロードされています。

さいごに

今回はStorage Browser for S3で任意のバケットを設定する方法を紹介しました。
任意のバケットを使いたいという話は割とありそうなので、今後実装されるかもしれませんね。

この記事をシェアする
著者:kawabata
2023年 Jr.Champions選出 2023, 2024年 All Certificate選出 最近はもっぱらCDKとAIばかりです