たくさんの自由帳

このサイトとほぼ同じ構成の CloudFormation ができました

投稿日 : | 0 日前

文字数(だいたい) : 17023

目次

どうもこんにちわ。初恋マスターアップ攻略しました。マスターアップってそういうことなのか・・・?

はわちゃん!!!が一番仕事してた。かわいい

感想

感想

2人は本当に、、、、?

感想

なつかしい!!!見返したらAndroid 13とかの頃っぽい(ぱっとわからない言い方)

感想

、、、、、このかさんルートを最後にやるといいのかも?どっちかというと楽しみだから最後にとっておいただけなんだけど

感想
↑ここの話好き、セーブして見返してる

感想

!!!かわいい

感想

理由が不純すぎるのすき

感想

ネタバレなのであれだけど、全員のルートでそれぞれある、、、、

感想

感想

なんでも屋のハルルカさんとしずりさんも(サブヒロインなので短い、、、)攻略できます!
そんなに読んでいるのか、、、(ちがう)

感想

感想

あとこのスクショめっちゃ使えそうじゃない?というわけで貼ります

感想

アプリ開発だからいつか使えそう!

本題

アプリの話では、、、、ありません!
CloudFormation 触ってたので、忘れないうちにまとめようと思って。

このサイトを含めてCloudFront+S3の構成のサイトがいくつかあります。雑ですが構成図はこうで

構成図

構成の詳細ですが、S3の機能にある静的ウェブサイト機能は使っていません。
代わりにCloudFrontオリジンアクセスコントロール (OAC)機能を使って、S3にアクセスできるようにしています。これを使うことでS3へアクセスできるのはCloudFrontだけになり、直接アクセスされるのを防ぐことが出来るってわけ。
(直接S3にアクセスできるとhttpしか受け付けないとか!カスタムドメインでCloudFrontつかうので!)

一方オリジンアクセスコントロールも万能ではなく、URL末尾が/aboutだけだとアクセスできなくて、/about/index.htmlと言った感じに.htmlまで書く必要があります。
あんまりindex.htmlまで書かないですよね、静的サイトホスティングサービスであれば自動的に処理してくれるし、レンタルサーバーの場合はNginxあたりがよしなに返してくれてるので。

というわけでCloudFront Functionです。これはCloudFrontが処理する前に任意のJavaScript コードが介入できる機能で、これを使いURLの末尾に/index.htmlを付与します。
これで/aboutだけでアクセスできるようになる。というわけ。

CloudFrontに介入する方法としてはLambda@Edgeという似たような機能がありますが、URL書き換えであればすぐ出来るのでCloudFront Functionで十二分だと思います。それにこれには十分すぎる無料枠があります。

まあそんな感じでS3CloudFrontオリジンアクセスコントロールCloudFront Functionと、あとドメイン。をやる必要があります。
説明書無しではほぼ覚えてない状態になります。

完成品

Parameters:

  BucketName:
    Type: String
    Description: WebSite Output S3 Bucket Name

  OriginAccessControlDescription:
    Type: String
    Default: cloudfront-s3-oac
    Description: Origin Access Control Description

  CloudFrontComment:
    Type: String
    Default: s3-cloudfront
    Description: CloudFront memo

  CloudFrontFunctionCreateOrReuse:
    Type: String
    Default: Create
    AllowedValues:
      - Create
      - Reuse
    Description: Append index.html suffix CloudFront Function. If created some
      function, Reuse. Don't have, Create

  CloudFrontFunctionName:
    Type: String
    Default: function-add-index
    Description: CloudFront Function Name. If Reuse, created function name.

  CachePolicyCreatedOrManaged:
    Type: String
    Default: 658327ea-f89d-4fab-a63d-7e88639e58f6
    Description: Cache Policy. Default is CachingOptimized.

Conditions:

  # CloudFront Functions を作るか
  CreateCloudFrontFunction: !Equals
    - !Ref CloudFrontFunctionCreateOrReuse
    - Create

Resources:

  # Next.js の static exports した結果を入れる S3
  WebSiteBucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Ref BucketName
      BucketEncryption:
        ServerSideEncryptionConfiguration:
          - BucketKeyEnabled: true
            ServerSideEncryptionByDefault:
              SSEAlgorithm: AES256
      PublicAccessBlockConfiguration:
        BlockPublicAcls: true
        BlockPublicPolicy: true
        IgnorePublicAcls: true
        RestrictPublicBuckets: true

  # S3 - CloudFront をつなぐバケットポリシー
  BucketPolicy:
    Type: AWS::S3::BucketPolicy
    Properties:
      Bucket: !Ref WebSiteBucket
      PolicyDocument:
        Id: PolicyForCloudFrontPrivateContent
        Version: '2008-10-17'
        Statement:
          - Sid: AllowCloudFrontServicePrincipal
            Effect: Allow
            Principal:
              Service: cloudfront.amazonaws.com
            Action: s3:GetObject
            Resource: !Sub arn:aws:s3:::${BucketName}/*
            Condition:
              StringEquals:
                AWS:SourceArn: !Sub arn:aws:cloudfront::${AWS::AccountId}:distribution/${CloudFrontDistribution}

  # S3 に入っている Web サイトを配信する CloudFront
  CloudFrontDistribution:
    Type: AWS::CloudFront::Distribution
    Properties:
      DistributionConfig:
        Origins:
          - Id: !GetAtt WebSiteBucket.RegionalDomainName # マネジメントコンソールで作ると DomainName と同じ文字列になってそう
            DomainName: !GetAtt WebSiteBucket.RegionalDomainName # リージョンが入ってないとアクセスできなかった
            OriginAccessControlId: !GetAtt OriginAccessControl.Id
            S3OriginConfig:
              OriginAccessIdentity: ''
        DefaultCacheBehavior:
          AllowedMethods:
            - GET
            - HEAD
          TargetOriginId: !GetAtt WebSiteBucket.RegionalDomainName # Origins Id に合わせる
          ViewerProtocolPolicy: allow-all
          FunctionAssociations:
            # CloudFront Functions を作成する場合は、CloudFrontAddIndexFunction に依存するように書いておく。これで CloudFormation は先に Function を作る順番になるはず
            - EventType: viewer-request
              FunctionARN: !If
                - CreateCloudFrontFunction
                - !GetAtt CloudFrontAddIndexFunction.FunctionMetadata.FunctionARN
                - !Sub arn:aws:cloudfront::${AWS::AccountId}:function/${CloudFrontFunctionName}
          CachePolicyId: !Ref CachePolicyCreatedOrManaged
          Compress: true
        HttpVersion: http2
        Enabled: true
        Comment: !Ref CloudFrontComment

  # S3- CloudFront をつなぐ OAC
  OriginAccessControl:
    Type: AWS::CloudFront::OriginAccessControl
    Properties:
      OriginAccessControlConfig:
        Description: !Ref OriginAccessControlDescription
        Name: !GetAtt WebSiteBucket.RegionalDomainName
        OriginAccessControlOriginType: s3
        SigningBehavior: always
        SigningProtocol: sigv4

  # CloudFront Function を作成する場合
  # index.html を付与する Function
  # https://docs.aws.amazon.com/ja_jp/AmazonCloudFront/latest/DeveloperGuide/example_cloudfront_functions_url_rewrite_single_page_apps_section.html
  CloudFrontAddIndexFunction:
    Type: AWS::CloudFront::Function
    Condition: CreateCloudFrontFunction
    Properties:
      Name: !Ref CloudFrontFunctionName
      AutoPublish: true
      FunctionConfig:
        Comment: add index.html to url suffix
        Runtime: cloudfront-js-2.0
      FunctionCode: |
        async function handler(event) {
            var request = event.request;
            var uri = request.uri;

            // Check whether the URI is missing a file name.
            if (uri.endsWith('/')) {
                request.uri += 'index.html';
            }
            // Check whether the URI is missing a file extension.
            else if (!uri.includes('.')) {
                request.uri += '/index.html';
            }

            return request;
        }

Outputs:

  WebSiteBucket:
    Description: S3
    Value: !Ref WebSiteBucket

  CloudFrontDistribution:
    Description: CloudFront
    Value: !Ref CloudFrontDistribution

  OriginAccessControl:
    Description: CloudFront OAC
    Value: !Ref OriginAccessControl

  CloudFrontAddIndexFunction:
    Condition: CreateCloudFrontFunction
    Description: CloudFront Functions
    Value: !Ref CloudFrontAddIndexFunction

上記の構成が作れます。使い方は、CloudFormationを開いて、Stackを作るときに↑のyamlを読み込んで、パラメーターを埋めてください。

パラメーターの詳細です

パラメーター名説明
BucketNameS3バケット名。命名規則はS3に準拠します。
OriginAccessControlDescriptionOACの説明。名前はバケット名の{バケット名}.s3.{リージョン}.amazonaws.comになります
CloudFrontCommentCloudFrontのコメント欄
CloudFrontFunctionCreateOrReuseCreateReuseCreateなら新規作成/index.htmlを付与するCloudFrontFunctionがすでにある場合はReuse
CloudFrontFunctionNameCreateならこれから作るCloudFrontFunctionの名前。ReuseならすでにあるCloudFrontFunctionの名前
CachePolicyCreatedOrManagedCloudFrontのキャッシュポリシー。デフォルトはキャッシュ最適化

カスタムドメインはやってくれないので自分でやってください!

CloudFormation 概要

くらうどふぉーめーしょん

AWSで使いたいリソース(S3とかCloudFrontとか)を指示するというか、組み立て説明書みたいなのを渡すと自動で作ってくれる。

人の手でマネジメントコンソール(ブラウザ上)を使ってS3とかを作るとやり忘れが発生するかもしれないし、上記の問題のように同じ構成で他のサイトを構築したいだけなのに、いくつかのリソースにまたがって設定が必要で少し時間がかる。

これに対処したのがIaCと呼ばれるもので、人の手でS3を作る代わりに、組み立て説明書にS3を作れ!!って書くわけです。
これをCloudFormationに食わせると読み込んで作ってくれる。間違いも減るだろうし、繰り返し似たようなサイトを作りたくなってもCloudFormationの実行ボタンを押せばだいたい揃う。

CloudFormation で間違えたら

失敗したら途中まで作ったリソース全部消して回らないといけないのか?とか思ってたけど流石にそんなこと無かった。
AWSの説明だけ見ると横文字ばっかで実際に使うまで全然わからない・・・AWS文学来るか?

  • 失敗してしまった場合は、途中まで作ったリソース(S3とか)は自動で削除される
  • 成功しても、削除ボタンを押せば作ったリソースを全て削除できる

さっすが~

用語集

テンプレート

yamlとかで書いた組み立て説明書です。テキストファイル

リソース

S3とかCloudFrontみたいなサービスのこと

スタック

CloudFormationを実行すると、スタックが誕生します。CloudFormationの実行単位って感じですかね?
これは今の状態(作成中・完成・失敗したためロールバック)を表示したり、作ったリソースを表示したり、テンプレートに出力(println的なの)があれば表示します。
スタックを削除すると、それが作ったリソースをすべて消すことが出来ます。

GitHub Actionsを起動するとJobが誕生するのと似てる。

そしてこれは起動したものの失敗してロールバックしたスタック一覧です。うわーー

fail_stack_list

スタックの詳細画面、リソースタブでは作ったリソースが表示される

stack_detail_tab

やってもらうこと

  • 静的サイトの成果物を置くS3 バケット
  • 配信するCloudFront ディストリビューション
  • S3 - CloudFrontを繋ぐオリジンアクセスコントロール
  • /index.htmlを付与するためのCloudFront Function

を作れる組み立て説明書を書いてもらいます。

めちゃくちゃどうでもいいんだけど、IKEAの説明書にある人のキャラクターおもろい。IKEAの店から電話線引いてるやつ

ながれ

  • CloudFormationのテンプレート作る
  • 実行する
  • 動くまで頑張る

テンプレートを用意

組み立て説明書です。yamlで書いていきます。GitHub Actionsで予習済み...!

CloudFormationのここから作れます

template

そしたら新規作成か既存のを読み込むかが選べます

create_or_reuse

すでにあるテンプレートを読み込む

その場合はS3yamlをアップロードするなりしてください。

新規作成

新しく作る場合はInfrastructure Composer からビルドを押して、Infrastructure Composer で作成へ進みます。

create

そしたらこんな画面になります。ビジュアルエディター的なのがありますが、今回はyamlを書いていきます。
のでテンプレートを選びます。なんかVSCodeみたいな画面になれば成功です。

yaml_editor

とにもかくにも動かしてみる

とりあえずS3を作るテンプレートを書きました。詳しくはあとにして、とりあえず実行してみます。

Parameters:

  BucketName:
    Type: String
    Description: WebSite Output S3 Bucket Name

Resources:
    
  WebSiteBucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Ref BucketName
      BucketEncryption:
        ServerSideEncryptionConfiguration:
          - BucketKeyEnabled: true
            ServerSideEncryptionByDefault:
              SSEAlgorithm: AES256
      PublicAccessBlockConfiguration:
        BlockPublicAcls: true
        BlockPublicPolicy: true
        IgnorePublicAcls: true
        RestrictPublicBuckets: true

構文チェック

ここの検証ボタンを押すとyaml記法があっているか確認できます。↑コピペすれば有効になるよね?

syntax_check

保存する

S3に保存するように言われるので保存します。
次回以降はS3にあるテンプレートから起動できるってわけです。

save_to_s3

CloudFormation を実行する

VSCodeみたいな画面が閉じられて、次へボタンが押せるようになるので進みます。

next

次にスタックの名前と、このテンプレートはテキストボックスがあるので埋めます。
スタックの名前はわかりやすいのを、その下のテキストボックスはS3バケットの名前です。

input

その後はデフォルトのままでよいです。よくわかってない

出来るのを待つ

スタック一覧画面に行くので待ちます。終わるとCREATE_COMPLETEになります。嬉しいですね!!!

stack

スタックを押すと詳細が右に表示されます。ここでは作成のログや、失敗してしまったときの失敗理由、あとは作ったリソース(S3とか)が確認できます。

detail

削除してみる

上記の削除ボタンを押すことでスタックと、スタックに書いてあるリソースが消えます。

delete

マネジメントコンソールでS3の画面を開いても、今CloudFormationで作ったS3バケットが消えていると思います。

yaml で組み立て説明書を書いていく

今回は冒頭で紹介した、このサイトと大体同じ構成が作れるCloudFormationを読み解いていきます。
#完成品

パラメーター機能

CloudFormationを起動する際に、パラメーターを指定することが出来ます。
S3のバケット名などを、テンプレートを書いている段階ではなく実行する直前に決定することが出来ます。

Parameters:

  BucketName:
    Type: String
    Description: WebSite Output S3 Bucket Name

  OriginAccessControlDescription:
    Type: String
    Default: cloudfront-s3-oac
    Description: Origin Access Control Description

  CloudFrontComment:
    Type: String
    Default: s3-cloudfront
    Description: CloudFront memo

  CloudFrontFunctionCreateOrReuse:
    Type: String
    Default: Create
    AllowedValues:
      - Create
      - Reuse
    Description: Append index.html suffix CloudFront Function. If created some function, Reuse. Don't have, Create

  CloudFrontFunctionName:
    Type: String
    Default: function-add-index
    Description: CloudFront Function Name. If Reuse, created function name.

  CachePolicyCreatedOrManaged:
    Type: String
    Default: 658327ea-f89d-4fab-a63d-7e88639e58f6
    Description: Cache Policy. Default is CachingOptimized.

この例ではBucketNameとかOriginAccessControlDescriptionID?になって、このあとのリソース作成などの箇所で引用できるようになります。
この後のリソースもそうですが好きな名前をつけることが出来ます。

TypeStringを入れるとテキストボックスになるし、StringAllowedValuesを組み合わせるとドロップダウンメニューになります。他にも数字とかもあったと思います。

DescriptionCloudFormationで実際に入力するときに表示されてて、Defaultはその名のとおりです。

あとは今更ですけどダブルクオーテーション的なので囲う必要はない?

今回使うパラメーター解説

再掲しますがこれです

パラメーター名説明
BucketNameS3バケット名。命名規則はS3に準拠します。
OriginAccessControlDescriptionOACの説明。名前はバケット名の{バケット名}.s3.{リージョン}.amazonaws.comになります
CloudFrontCommentCloudFrontのコメント欄
CloudFrontFunctionCreateOrReuseCreateReuseCreateなら新規作成/index.htmlを付与するCloudFrontFunctionがすでにある場合はReuse
CloudFrontFunctionNameCreateならこれから作るCloudFrontFunctionの名前。ReuseならすでにあるCloudFrontFunctionの名前
CachePolicyCreatedOrManagedCloudFrontのキャッシュポリシー。デフォルトはキャッシュ最適化

条件分岐の定義

こうしき

Conditions:

  # CloudFront Functions を作るか
  CreateCloudFrontFunction: !Equals
    - !Ref CloudFrontFunctionCreateOrReuse
    - Create

CloudFormationでの条件分岐ですが、あらかじめ true か false か評価して定義しておく必要がある?
条件分岐が必要ならConditionsに書いて、その後必要な箇所で使う

この例だとCreateCloudFrontFunctionIDになる。
その後の!Equalsは、CloudFormationの組み込み関数で、この後に配列が続きます。インデックス0番目が比較したい文字で、インデックス1番目が等しいのを期待する文字を入れます。
この場合は、パラメーターCloudFrontFunctionCreateOrReuseを読み出してCreateだった場合はtrueになります。

!Refってのを使ってますが、とりあえずは先述のパラメーターから値を取り出すんだな~って
あと実際に条件分岐を使う方法も後で

改行する書き方とカッコの書き方がある

CreateCloudFrontFunction: !Equals
  - !Ref CloudFrontFunctionCreateOrReuse
  - Create
CreateCloudFrontFunction:
  !Equals [!Ref CloudFrontFunctionCreateOrReuse, Create]

これはyamlリストの書き方の違いで、同じものらしい

リソースを定義

ついにS3CloudFrontを作っていきます。全部張りました。

Resources:

  # Next.js の static exports した結果を入れる S3
  WebSiteBucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Ref BucketName
      BucketEncryption:
        ServerSideEncryptionConfiguration:
          - BucketKeyEnabled: true
            ServerSideEncryptionByDefault:
              SSEAlgorithm: AES256
      PublicAccessBlockConfiguration:
        BlockPublicAcls: true
        BlockPublicPolicy: true
        IgnorePublicAcls: true
        RestrictPublicBuckets: true

  # S3 - CloudFront をつなぐバケットポリシー
  BucketPolicy:
    Type: AWS::S3::BucketPolicy
    Properties:
      Bucket: !Ref WebSiteBucket
      PolicyDocument:
        Id: PolicyForCloudFrontPrivateContent
        Version: '2008-10-17'
        Statement:
          - Sid: AllowCloudFrontServicePrincipal
            Effect: Allow
            Principal:
              Service: cloudfront.amazonaws.com
            Action: s3:GetObject
            Resource: !Sub arn:aws:s3:::${BucketName}/*
            Condition:
              StringEquals:
                AWS:SourceArn: !Sub arn:aws:cloudfront::${AWS::AccountId}:distribution/${CloudFrontDistribution}

  # S3 に入っている Web サイトを配信する CloudFront
  CloudFrontDistribution:
    Type: AWS::CloudFront::Distribution
    Properties:
      DistributionConfig:
        Origins:
          - Id: !GetAtt WebSiteBucket.RegionalDomainName # マネジメントコンソールで作ると DomainName と同じ文字列になってそう
            DomainName: !GetAtt WebSiteBucket.RegionalDomainName # リージョンが入ってないとアクセスできなかった
            OriginAccessControlId: !GetAtt OriginAccessControl.Id
            S3OriginConfig:
              OriginAccessIdentity: ''
        DefaultCacheBehavior:
          AllowedMethods:
            - GET
            - HEAD
          TargetOriginId: !GetAtt WebSiteBucket.RegionalDomainName # Origins Id に合わせる
          ViewerProtocolPolicy: allow-all
          FunctionAssociations:
            # CloudFront Functions を作成する場合は、CloudFrontAddIndexFunction に依存するように書いておく。これで CloudFormation は先に Function を作る順番になるはず
            - EventType: viewer-request
              FunctionARN: !If
                - CreateCloudFrontFunction
                - !GetAtt CloudFrontAddIndexFunction.FunctionMetadata.FunctionARN
                - !Sub arn:aws:cloudfront::${AWS::AccountId}:function/${CloudFrontFunctionName}
          CachePolicyId: !Ref CachePolicyCreatedOrManaged
          Compress: true
        HttpVersion: http2
        Enabled: true
        Comment: !Ref CloudFrontComment

  # S3- CloudFront をつなぐ OAC
  OriginAccessControl:
    Type: AWS::CloudFront::OriginAccessControl
    Properties:
      OriginAccessControlConfig:
        Description: !Ref OriginAccessControlDescription
        Name: !GetAtt WebSiteBucket.RegionalDomainName
        OriginAccessControlOriginType: s3
        SigningBehavior: always
        SigningProtocol: sigv4

  # CloudFront Function を作成する場合
  # index.html を付与する Function
  # https://docs.aws.amazon.com/ja_jp/AmazonCloudFront/latest/DeveloperGuide/example_cloudfront_functions_url_rewrite_single_page_apps_section.html
  CloudFrontAddIndexFunction:
    Type: AWS::CloudFront::Function
    Condition: CreateCloudFrontFunction
    Properties:
      Name: !Ref CloudFrontFunctionName
      AutoPublish: true
      FunctionConfig:
        Comment: add index.html to url suffix
        Runtime: cloudfront-js-2.0
      FunctionCode: |
        async function handler(event) {
            var request = event.request;
            var uri = request.uri;

            // Check whether the URI is missing a file name.
            if (uri.endsWith('/')) {
                request.uri += 'index.html';
            }
            // Check whether the URI is missing a file extension.
            else if (!uri.includes('.')) {
                request.uri += '/index.html';
            }

            return request;
        }

WebSiteBucketCloudFrontDistributionIDになっています。
その後のTypeは、作りたいAWS リソースを入力します。AWS::S3::Bucketみたいな。Conditionは、上で作った!EqualsのやつのIDを入力することで、trueの時だけ実行される。
その後のPropertiesそれぞれ必要な値が異なります。

Properties に必要な値

まずはサンプル集を読むとどんな感じか掴みやすいと思います。

その後に、詳しい設定をしたい場合は各自リファレンスを調べてください。
S3のバケットポリシーはJSONyamlにしたようなものだったりする?

例えばCloudFront ディストリビューションIPv6を有効にするなら、IPV6Enabledをつけろって言われてるんでつけます

CloudFrontDistribution:
  Type: AWS::CloudFront::Distribution
  Properties:
    DistributionConfig:
      IPV6Enabled: true

どうでもいい話ですがAndroidアプリでIPv6を使うとうまく通信出来ない時があるので、バックエンドエンジニア(インフラ?)の皆さん、IPv6やるときはAndroidエンジニアチームに一声書けてください!!!!!
いちおう解決方法があるので特に問題はないと思いますが...

Ref と Sub と GetAtt

!Refは、パラメーターのIDの値を読み出したり、その他、リソースのIDを渡すとS3バケット名みたいなのが返ってくることもあります。

BucketName: !Ref BucketName

!Subは文字列の中にパラメーターのIDの値を入れたいときに使う。
アカウントIDみたいなのが取得できるAWSオブジェクトがあります。グローバル変数というか何もしなくてもAWS::のが使えるようです。

AWS:SourceArn: !Sub arn:aws:cloudfront::${AWS::AccountId}:distribution/${CloudFrontDistribution}

GetAttは、作ったリソースの詳細情報を取得したいときに利用します。
GetAttで取得可能な値もリファレンスに乗っています。リファレンスのReturn valuesのセクションまでスクロールすると、取得できる値が解説されています。

reference_getatt

例えばS3のリソースで以下のようにGetAttすると、、、{バケット名}.s3.{リージョン}.amazonaws.comの文字列が取得できます。

Name: !GetAtt WebSiteBucket.RegionalDomainName

OACもこんな感じ

OriginAccessControlId: !GetAtt OriginAccessControl.Id

並列実行と依存

GetAtt!Refで他のリソースを参照すると、勝手にいい感じに並列の順番を決めてくれます。
すでにリソースが作成されてないとGetAtt出来ないので、完成を待つ感じでしょうか?

CloudFrontDistribution:
  Type: AWS::CloudFront::Distribution
  Properties:
    DistributionConfig:
      Origins:
        - Id: !GetAtt WebSiteBucket.RegionalDomainName # マネジメントコンソールで作ると DomainName と同じ文字列になってそう
          DomainName: !GetAtt WebSiteBucket.RegionalDomainName # リージョンが入ってないとアクセスできなかった
          OriginAccessControlId: !GetAtt OriginAccessControl.Id
          S3OriginConfig:
            OriginAccessIdentity: ''
      DefaultCacheBehavior:
        AllowedMethods:
          - GET
          - HEAD
        TargetOriginId: !GetAtt WebSiteBucket.RegionalDomainName # Origins Id に合わせる
        ViewerProtocolPolicy: allow-all
        FunctionAssociations:
          # CloudFront Functions を作成する場合は、CloudFrontAddIndexFunction に依存するように書いておく。これで CloudFormation は先に Function を作る順番になるはず
          - EventType: viewer-request
            FunctionARN: !If
              - CreateCloudFrontFunction
              - !GetAtt CloudFrontAddIndexFunction.FunctionMetadata.FunctionARN
              - !Sub arn:aws:cloudfront::${AWS::AccountId}:function/${CloudFrontFunctionName}
        CachePolicyId: !Ref CachePolicyCreatedOrManaged
        Compress: true
      HttpVersion: http2
      Enabled: true
      Comment: !Ref CloudFrontComment

CloudFrontAddIndexFunction:
  Type: AWS::CloudFront::Function
  Condition: CreateCloudFrontFunction
  Properties:
    Name: !Ref CloudFrontFunctionName
    AutoPublish: true
    FunctionConfig:
      Comment: add index.html to url suffix
      Runtime: cloudfront-js-2.0
    FunctionCode: |
      async function handler(event) {
          var request = event.request;
          var uri = request.uri;

          // Check whether the URI is missing a file name.
          if (uri.endsWith('/')) {
              request.uri += 'index.html';
          }
          // Check whether the URI is missing a file extension.
          else if (!uri.includes('.')) {
              request.uri += '/index.html';
          }

          return request;
      }

例えば上記だと以下のようにCloudFrontAddIndexFunctionで作るCloudFront Fuctionの値に依存しています。
そのため先にCloudFrontAddIndexFunctionを作成して、その後CloudFrontDistributionの作成に進むって感じらしい。

FunctionARN: !If
  - CreateCloudFrontFunction
  - !GetAtt CloudFrontAddIndexFunction.FunctionMetadata.FunctionARN
  - !Sub arn:aws:cloudfront::${AWS::AccountId}:function/${CloudFrontFunctionName}
こんな感じに明示的に依存を書ければ良いのですが、そう簡単に依存するような書き方が出来ない場合もあると思います。
今回使うパラメーター解説 では、「Createならこれから作るCloudFrontFunctionの名前。ReuseならすでにあるCloudFrontFunctionの名前」と書きました。
CloudFront Function名前だけは起動前のパラメーターの時点でわかっているので、ARNを文字列補間で作ることが出来るって、わかりますか?
FunctionARN: !Sub arn:aws:cloudfront::${AWS::AccountId}:function/${CloudFrontFunctionName}

ただこれだと既にある場合でしか動きません
明示的に依存しているんだよ~感を出しておくと、先に依存しているリソースを作る判断ができますが、これだとただパラメーターの値を入れているだけになるので。

上記の例で試すと、CloudFront ディストリビューションを作るのにCloudFront Functionの完成を待たない、多分ですが完成よりも前にディストリビューションを作ってしまうので失敗してしまいます。
先述の通りCloudFormationは極力並列でリソースを作るため、先にCloudFront Functionを作ってくれれば動くかもですが、順番を守ってくれることのほうが少ないでしょう。

なので!DependsOnという機能があります!!いい感じに依存してるんだな~って感づいてくれないときに使えそう。今回は気が利いてくれたので問題なかった!

出力機能

作ったリソースや、パラメーターの値を出力して、スタックの詳細画面から見ることが出来る機能があります。
多分成功してないと何も表示されないと思います。(ROLLBACK_COMPLETEを見たけど何も出力されてなかった)

stack_output_tab

Outputs:

  WebSiteBucket:
    Description: S3
    Value: !Ref WebSiteBucket

  CloudFrontDistribution:
    Description: CloudFront
    Value: !Ref CloudFrontDistribution

  OriginAccessControl:
    Description: CloudFront OAC
    Value: !Ref OriginAccessControl

  CloudFrontAddIndexFunction:
    Condition: CreateCloudFrontFunction
    Description: CloudFront Functions
    Value: !Ref CloudFrontAddIndexFunction

WebSiteBucketCloudFrontDistributionがキーになり、Valueが値のところに出る

カスタムドメインは自動でやってくれません。

そういえば、カスタムドメインは自動でやってくれないので、CloudFormation実行中の隙に作ってください。(別にいつでも良いですが)
私がRoute53を使っていないせいだと思います・・・

使えればCloudFormationで生成できたらしい。Route53.devTLDがそもそも使えない。なのでありがとうクラウドフレア (英語にすると空目しそうで)

エラー集

数日くらいかかったので怒ってます

ROLLBACK_FAILED

CloudFormationで実行したけど失敗してスタックのリソースを消そうとして消せなかったエラー
もう一回削除ボタン?を押すと強制的に削除できます?

このスタックの削除を再試行しますか?
このスタックを削除すると、すべてのスタックリソースが削除されます。リソースは 削除ポリシー に従って削除されます。
このスタックを削除するが、リソースを保持次のリソースの削除に失敗したため、このスタックは以前削除に失敗しています。リソースを保持することを選択した場合、それらのリソースはこの削除オペレーション中にスキップされます。
このスタック全体を強制削除このスタックを強制削除すると、削除プロセス中に削除できなかったリソースが残る可能性があります。ただし、スタック自体は正常に削除されます。

日本語が文字化けする

雰囲気で英語を使う

japanese_not_working

Resource handler returned message: "Invalid request provided: AWS::CloudFront::OriginAccessControl" (RequestToken: , HandlerErrorCode: InvalidRequest)

CloudFrontOriginAccessControlの名前が長過ぎる

Resource handler returned message: "Invalid request provided: Exactly one of CustomOriginConfig, VpcOriginConfig and S3OriginConfig must be specified"

CloudFront ディストリビューションを作るときに、S3OriginConfigとその中にOriginAccessIdentityを指定する必要あり。OriginAccessIdentityは空文字でいいから必要

CloudFrontDistribution:
  Type: AWS::CloudFront::Distribution
  Properties:
    DistributionConfig:
      Origins:
        - Id: !GetAtt WebSiteBucket.RegionalDomainName # マネジメントコンソールで作ると DomainName と同じ文字列になってそう
          DomainName: !GetAtt WebSiteBucket.RegionalDomainName # リージョンが入ってないとアクセスできなかった
          OriginAccessControlId: !GetAtt OriginAccessControl.Id
          S3OriginConfig:
            OriginAccessIdentity: '' # ここに空文字が必要

Resource handler returned message: "Invalid request provided: AWS::CloudFront::Distribution: The specified cache policy does not exist. (Service: CloudFront, Status Code: 404, Request ID

わからない。

CachePolicyId: !Sub CachePolicyCreatedOrManaged

を、とりあえず成功させるために直接指定するようにした。!Refじゃないと成功しないのか?

CachePolicyId: 658327ea-f89d-4fab-a63d-7e88639e58f6

CloudFront にアクセスしたら Access Denied

S3の設定で、Amazon S3 マネージドキーを使用したサーバー側の暗号化 (SSE-S3)を使うようにする必要がありそう?適当にコピペした仇が

WebSiteBucket:
  Type: AWS::S3::Bucket
  Properties:
    BucketName: !Ref BucketName
    BucketEncryption:
      ServerSideEncryptionConfiguration:
        - BucketKeyEnabled: true
          ServerSideEncryptionByDefault:
            SSEAlgorithm: AES256 # これ

Resource handler returned message: "The XML you provided was not well-formed or did not validate against our published schema

普通にyaml書き方間違ってた

ServerSideEncryptionConfiguration:
  - ServerSideEncryptionByDefault:
      SSEAlgorithm: AES256
  - BucketKeyEnabled: true

↑間違い↓正解

ServerSideEncryptionConfiguration:
  - ServerSideEncryptionByDefault:
      SSEAlgorithm: AES256
    BucketKeyEnabled: true

Resource handler returned message: "Invalid request provided: AWS::CloudFront::Function: (Service: CloudFront, Status Code: 409, Request ID

ARNの値間違えてる?

Resource handler returned message: "Invalid request provided: AWS::CloudFront::Distribution: The parameter FunctionAssociationArn is invalid

CloudFront Functionの作成でAutoPublish: trueをつけ忘れていた?

CloudFrontAddIndexFunction:
  Type: AWS::CloudFront::Function
  Condition: CreateCloudFrontFunction
  Properties:
    Name: !Ref CloudFrontFunctionName
    AutoPublish: true # これ
  # 以下省略...

エラー画面が XML から 503 になった

503 ERROR
The request could not be satisfied.
The CloudFront function associated with the CloudFront distribution is invalid or could not run. We can't connect to the server for this app or website at this time. There might be too much traffic or a configuration error. Try again later, or contact the app or website owner.
If you provide content to customers through CloudFront, you can find steps to troubleshoot and help prevent this error by reviewing the CloudFront documentation.
Generated by cloudfront (CloudFront)
Request ID: ....

CloudFront Functionのサンプルコードでasync/awaitを使っているが、私はそれに気付かずJavaScript ランタイム 1.0を選んでしまった。
このコードはランタイム2.0限定だった。

CloudFrontAddIndexFunction:
  Type: AWS::CloudFront::Function
  Condition: CreateCloudFrontFunction
  Properties:
    Name: !Ref CloudFrontFunctionName
    AutoPublish: true
    FunctionConfig:
      Comment: add index.html to url suffix
      Runtime: cloudfront-js-2.0 # これ
  # 以下省略

やっぱり 503

S3OriginsIDDomainNameが微妙に間違えているらしい。
!GetAtt WebSiteBucket.DomainName間違いで!GetAtt WebSiteBucket.RegionalDomainNameじゃないとダメっぽい

CloudFrontDistribution:
  Type: AWS::CloudFront::Distribution
  Properties:
    DistributionConfig:
      Origins:
        - Id: !GetAtt WebSiteBucket.RegionalDomainName # マネジメントコンソールで作ると DomainName と同じ文字列になってそう
          DomainName: !GetAtt WebSiteBucket.RegionalDomainName # リージョンが入ってないとアクセスできなかった

違いはこうで、リージョンの有無?

  • !GetAtt WebSiteBucket.DomainName
    • {バケット名}.s3.amazonaws.com
  • !GetAtt WebSiteBucket.RegionalDomainName
    • {バケット名}.s3.{リージョン}.amazonaws.com

というのも、マネジメントコンソールでマウスぽちぽちして作ったディストリビューションを見たんですが、どうもリージョンがある方が期待値っぽいんですよね。

これで見れるようになった!!!

おわりに

数日かかった理由、↑に書いたCloudFrontS3OriginDomainNameのせいだと思ってる。
!GetAtt WebSiteBucket.DomainNameではダメで、RegionalDomainNameを使わないといけなかった。これに気付かなかった。

なんか検索してもDomainNameの方ばっかりでRegionalDomainNameなんて使って無い。じゃあなんでそっちがなんで動いているのかは不明です。
謎。

CloudFrontDistribution:
  Type: AWS::CloudFront::Distribution
  Properties:
    DistributionConfig:
      Origins:
        - Id: !GetAtt WebSiteBucket.RegionalDomainName # マネジメントコンソールで作ると DomainName と同じ文字列になってそう
          DomainName: !GetAtt WebSiteBucket.RegionalDomainName # リージョンが入ってないとアクセスできなかった
          OriginAccessControlId: !GetAtt OriginAccessControl.Id
          S3OriginConfig:
            OriginAccessIdentity: ''
      DefaultCacheBehavior:
        AllowedMethods:
          - GET
          - HEAD
        TargetOriginId: !GetAtt WebSiteBucket.RegionalDomainName # Origins Id に合わせる
        ViewerProtocolPolicy: allow-all
        FunctionAssociations:
          # CloudFront Functions を作成する場合は、CloudFrontAddIndexFunction に依存するように書いておく。これで CloudFormation は先に Function を作る順番になるはず
          - EventType: viewer-request
            FunctionARN: !If
              - CreateCloudFrontFunction
              - !GetAtt CloudFrontAddIndexFunction.FunctionMetadata.FunctionARN
              - !Sub arn:aws:cloudfront::${AWS::AccountId}:function/${CloudFrontFunctionName}
        CachePolicyId: !Ref CachePolicyCreatedOrManaged
        Compress: true
      HttpVersion: http2
      Enabled: true
      Comment: !Ref CloudFrontComment

おわりに2

「静的(漢字変換に成功!!!)サイトなのにAWS使ってるからこんな難しいんだろ」というコメントがありました。
いやーほn・・そうですね、そのとおりだと

じゃあなんでなんだよって話ですが、どっかで書いたような気もしなくないですがもう一回書いておくと、
お一人様MisskeyAmazon Lightsailで動いているので、もうバックエンドが必要になったらそっちに寄せていこうってことです。
バックエンドとかインフラはさっぱりなんで、やたらめったら増やすのはあれだしもうAWSに寄せていこうと思った。

ちなみに国産 VPSと違ってパット見Lightsailが安く見えるのですが、円安のせいでそんなことないと思います。

おわりに3

Amplifyが進化しているそうなのでそっち選んでも良いかもしれませんね?
わたしとしてはCloudFormation書いたのでしばらく困らないと思う。

おわりに4

AWSで障害があったらしいですが、基本的にはap-northeast-1(東京リージョン)を使っているので、特にこのサイトが見れなかった・・・事態にはなって無い、はずです。
ご利用料金の画面がずっとエラーを返してた。くらい。