こんにちは。SCSK石原です。
最近、お仕事ではないのですが不特定多数の人から画像を集める必要がありました。その時使用したナレッジがほかでも使いまわせそうでしたので、今回記事にさせていただきました。
[不特定多数の人が画像をアップロード出来るシステム] の個人的な要望としては
- 30分くらいで枠組みは作りたい
 - 不特定多数なので認証不要でWebページにアクセスさせたい
 - コンピュート層を持ちたくない(メンテナンスしたくない/お金をかけたくない)
 - フロント側はすでにできているものを使いたい(HTMLやJavascriptが苦手)
 
上記の条件にぴったりな「Amazon S3バケットにフォトアルバムを作成するサンプル」を見つけましたので、こちらを利用させていただきました。ただし、AWS SDK for JavaScriptのサンプルになりますので、前提条件となるAWSサービスの詳細な説明がありませんでした。
本記事では、フォトアルバムを動作させる前提条件タスクとして定義されていることをCloudFormationテンプレートに変換し、ナレッジとして残しておくことが目的となります。
前提条件タスク
前述のサンプルには前提条件タスクというものがありました。
- Amazon S3 コンソールで、アルバムに写真を保存するために使用する Amazon S3 バケットを作成します。コンソールでのバケットの作成の詳細については、Amazon Simple Storage Service ユーザーガイドの「バケットの作成」を参照してください。オブジェクトで読み取りおよび書き込みの両方の許可があることを確認してください。バケットの許可の設定の詳細については、ウェブサイトアクセスに必要な許可の設定を参照してください。
 - Amazon Cognito コンソールで、Amazon S3 バケットと同じリージョンの認証されていないユーザーに対してアクセスが有効になっているフェデレーテッドアイデンティティを使用して Amazon Cognito アイデンティティプールを作成します。コード内の ID プール ID を含めて、ブラウザスクリプトの認証情報を取得する必要があります。Amazon Cognito アイデンティティの詳細については、Amazon Cognito デベロッパーガイドの Amazon Cognito アイデンティティプール (フェデレーテッドアイデンティティ)を参照してください。
 - IAM コンソールで、Amazon Cognito によって認証されていないユーザー用に作成された IAM ロールを見つけます。次のポリシーを追加し、Amazon S3 バケットに読み取りおよび書き込みの許可を付与します。IAM ロールの作成の詳細については、IAM ユーザーガイドのAWS のサービスに許可を委任するロールの作成を参照してください。
 
サンプルに記載のあるものを自身の言葉に落としたものが下記になります。
- Amazon S3 バケットを作成
- WebSiteホスティング用のS3バケットは公開設定でリソースを配置
 - 画像アップロード用にはCORSを設定した別バケットを作成
 
 - ユーザが認証なしで画像のアップロードバケットにアクセスできるようにCognitoのIDプールを設定
 - アップロード用バケットにPutやDelete権限を保持したIAMロールを作成してCognitoに紐づけ
 
アーキテクチャのアイコンを並べると下記の様になります。

AWSマネジメントコンソールで作成してもよいのですが、再利用可能にするためテンプレートを作成して構成します。
前提条件タスク(テンプレート)
Cloudformationのテンプレートは下記の通りです。こちらのテンプレートでは、前述したフォトアルバムのサンプルを動作させるものになります。
AWSTemplateFormatVersion: "2010-09-09"
Description: "SPA Template"
Parameters:
  ProjectID:
    Type: String
Resources:
# ================================
# S3
# ================================
  S3BucketWebsiteHosting:
    Type: "AWS::S3::Bucket"
    Properties:
      BucketName: !Sub "${ProjectID}-website"
      PublicAccessBlockConfiguration:
        BlockPublicAcls: False
        BlockPublicPolicy: False
        IgnorePublicAcls: False
        RestrictPublicBuckets: False
      BucketEncryption:
        ServerSideEncryptionConfiguration:
          - ServerSideEncryptionByDefault:
              SSEAlgorithm: AES256
      WebsiteConfiguration:
        ErrorDocument: "error.html"
        IndexDocument: "index.html"
  S3BucketPolicyWebsiteHosting:
    Type: "AWS::S3::BucketPolicy"
    Properties:
      Bucket: !Ref S3BucketWebsiteHosting
      PolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Sid: "PublicReadGetObject"
            Action:
              - "s3:GetObject"
            Effect: Allow
            Resource:
              - "Fn::Join":
                  - ""
                  - - "arn:aws:s3:::"
                    - Ref: S3BucketWebsiteHosting
                    - /*
            Principal: "*"
  S3BucketUploadPicture:
    Type: "AWS::S3::Bucket"
    Properties:
      BucketName: !Sub "${ProjectID}-picture"
      PublicAccessBlockConfiguration:
        BlockPublicAcls: True
        BlockPublicPolicy: True
        IgnorePublicAcls: True
        RestrictPublicBuckets: True
      BucketEncryption:
        ServerSideEncryptionConfiguration:
          - ServerSideEncryptionByDefault:
              SSEAlgorithm: AES256
      LifecycleConfiguration:
        Rules:
          - Id: IntelligentTierRule
            Status: Enabled
            Transitions:
              - TransitionInDays: 0
                StorageClass: INTELLIGENT_TIERING
      CorsConfiguration:
        CorsRules:
          - AllowedHeaders:
              - '*'
            AllowedMethods:
              - GET
              - HEAD
              - PUT
              - POST
              - DELETE
            AllowedOrigins:
              - '*'
            ExposedHeaders:
              - ETag
            Id: myCORSRuleId1
# ================================
# Cognito
# ================================
  CognitoUnAuthIDPool:
    Type: AWS::Cognito::IdentityPool
    Properties: 
      AllowUnauthenticatedIdentities: True
      IdentityPoolName: !Sub "${ProjectID}-cognitoidpool"
  RoleAttachment:
    Type: AWS::Cognito::IdentityPoolRoleAttachment
    Properties:
      IdentityPoolId: !Ref CognitoUnAuthIDPool
      Roles:
        unauthenticated: !GetAtt IAMRoleCognitoUnauth.Arn
        authenticated: !GetAtt IAMRoleCognitoUnauth.Arn
# ================================
# IAM
# ================================
  IAMPolicyCognitoUnauth:
    Type: AWS::IAM::ManagedPolicy
    Properties:
      PolicyDocument:
        Version: "2012-10-17"
        Statement:
        - Effect: Allow
          Action:
          - mobileanalytics:PutEvents
          - cognito-sync:*
          Resource:
          - "*"
        - Effect: Allow
          Action:
          - s3:DeleteObject
          - s3:GetObject
          - s3:ListBucket
          - s3:PutObject
          - s3:PutObjectAcl
          Resource:
          - !Join
            - ''
            - - !GetAtt S3BucketUploadPicture.Arn
          - !Join
            - ''
            - - !GetAtt S3BucketUploadPicture.Arn
              - "/*"
  IAMRoleCognitoUnauth:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
        - Effect: Allow
          Action: "sts:AssumeRoleWithWebIdentity"
          Principal:
            Federated: cognito-identity.amazonaws.com
          Condition:
            StringEquals:
              "cognito-identity.amazonaws.com:aud":
                Ref: CognitoUnAuthIDPool
            ForAnyValue:StringLike:
              "cognito-identity.amazonaws.com:amr": unauthenticated
      ManagedPolicyArns: 
        - !Ref IAMPolicyCognitoUnauth
Outputs:
  WebSiteHostingURL:
    Description: WebSite URL
    Value: !GetAtt S3BucketWebsiteHosting.WebsiteURL
  CognitoIdPoolARN:
    Description: IdPool ARN
    Value: !Ref CognitoUnAuthIDPool 
フォトアルバムを動かすには
GitHubにサンプルコードがありますので、WebSiteホスティング用のS3バケットに下記の2ファイルを少し修正して配置すれば出来上がりです。
- s3_photoExample.js
- 
「BUCKET_NAME」をアップロード用のバケット名に置換
 - 「REGION」をアップロード用のバケットが存在するリージョンに置換
 - 「IDENTITY_POOL_ID」をCognitoのIDプールのIDに置換(CFNのアウトプットとして出力しています)
 
 - 
 - s3_photoExample.html(ファイル名をindex.htmlに変更)
- 
「SDK_VERSION_NUMBER」を利用するSDKのバージョンに置換
 
 - 
 
終わりに
これで30分ではなく、3分あればAWSにフォトアルバムをホスティングができるようになりました。
今回実施したS3ウェブサイトホスティングについても、もう少しお仕事向きなアーキテクチャにするのであれば、下記の様な構成が考えられます。
- CloudFrontの利用
 - Cognitoにて認証を必須に
 - Route53でドメイン取得&名前解決
 - ACMで証明書取得&インストール
 - CodeCommitでソース管理
 - CodePipelineで自動デプロイ
 

以上、ぜひご活用ください!!
