【AWS×Webアプリ】CloudFrontのキャッシュ削除（SAM）

## 目的
・AWS上の静的Webサイトホスティングを有効にしたS3をCloudFrontで公開。
・S3のコンテンツを更新した際に、CloudFrontのキャッシュ削除を行うLambdaを実装。

## 前提条件
・SAMを使用してAWS上にリソースを作成する。
・Pythonを使用してLambdaを実装する。

## 完成イメージ
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3637963/4052117a-cea8-9fa3-e301-6712765433d2.png)

## SAMテンプレート
```yaml:template.yaml
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31

Resources:
  WebBucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Sub ${AWS::StackName}-webbucket-${AWS::AccountId}
      VersioningConfiguration:
        Status: Enabled
      WebsiteConfiguration:
        IndexDocument: index.html
      PublicAccessBlockConfiguration:
        IgnorePublicAcls: false
  WebBucketBucketPolicy:
    Type: AWS::S3::BucketPolicy
    Properties:
      Bucket: !Ref WebBucket
      PolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Principal:
              Service: cloudfront.amazonaws.com
            Action:
              - s3:GetObject
            Effect: Allow
            Resource:
              - !Sub ${WebBucket.Arn}/*
            Condition:
              StringEquals:
                AWS:SourceArn:
                  - !Sub arn:aws:cloudfront::${AWS::AccountId}:distribution/${WebBucketDistribution}
  WebBucketDistribution:
    Type: AWS::CloudFront::Distribution
    Properties:
      DistributionConfig:
        DefaultCacheBehavior:
          TargetOriginId: WebBucketOrigin
          Compress: true
          ViewerProtocolPolicy: redirect-to-https
          AllowedMethods:
            - GET
            - HEAD
          CachedMethods:
            - GET
            - HEAD
          CachePolicyId: !Ref WebBucketCachePolicy
          OriginRequestPolicyId: !Ref WebBucketOriginRequestPolicy
        PriceClass: PriceClass_200
        DefaultRootObject: index.html
        Enabled: true
        Origins:
          - DomainName: !GetAtt WebBucket.RegionalDomainName
            Id: WebBucketOrigin
            OriginAccessControlId: !GetAtt WebBucketOriginAccessControl.Id
            S3OriginConfig:
              OriginAccessIdentity: ''
  WebBucketOriginAccessControl:
    Type: AWS::CloudFront::OriginAccessControl
    Properties:
      OriginAccessControlConfig:
        SigningBehavior: always
        OriginAccessControlOriginType: s3
        SigningProtocol: sigv4
        Name: !Sub ${AWS::StackName}-OAC-${AWS::AccountId}
  WebBucketCachePolicy:
    Type: AWS::CloudFront::CachePolicy
    Properties:
      CachePolicyConfig:
        Name: WebBucketCachePolicy
        DefaultTTL: 86400
        MaxTTL: 31536000
        MinTTL: 3600
        ParametersInCacheKeyAndForwardedToOrigin:
          EnableAcceptEncodingGzip: true
          EnableAcceptEncodingBrotli: true
          CookiesConfig:
            CookieBehavior: none
          HeadersConfig:
            HeaderBehavior: none
          QueryStringsConfig:
            QueryStringBehavior: none
  WebBucketOriginRequestPolicy:
    Type: AWS::CloudFront::OriginRequestPolicy
    Properties:
      OriginRequestPolicyConfig:
        Name: WebBucketOriginRequestPolicy
        CookiesConfig:
          CookieBehavior: none
        HeadersConfig:
          HeaderBehavior: none
        QueryStringsConfig:
          QueryStringBehavior: none
  DeleteCacheLambdaRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service: lambda.amazonaws.com
            Action: sts:AssumeRole
      Policies:
        - PolicyName: LambdaS3Policy
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action:
                  - logs:CreateLogGroup
                  - logs:CreateLogStream
                  - logs:PutLogEvents
                Resource: arn:aws:logs:*:*:*
              - Effect: Allow
                Action:
                  - cloudfront:CreateInvalidation
                  - cloudfront:ListDistributions
                Resource: '*'
  DeleteCache:
    Type: AWS::Serverless::Function
    Properties:
      Handler: index.lambda_handler
      Runtime: python3.11
      Role: !GetAtt DeleteCacheLambdaRole.Arn
      Environment:
        Variables:
          Stack_NAME: !Sub ${AWS::StackName}
      CodeUri: ./DeleteCache
      Events:
        WebBucket:
          Type: S3
          Properties:
            Bucket: !Ref WebBucket
            Events:
              - s3:ObjectCreated:*
              - s3:ObjectRemoved:*
            Filter:
              S3Key:
                Rules:
                  - Name: suffix
                    Value: index.html
  LambdaInvokePermission:
    Type: AWS::Lambda::Permission
    Properties:
      FunctionName: !Ref DeleteCache
      Action: lambda:InvokeFunction
      Principal: s3.amazonaws.com
      SourceAccount: !Ref AWS::AccountId
      SourceArn: !Sub arn:aws:s3:::${WebBucket}
Outputs:
  WebsiteURLonCF:
    Description: URL for website hosted on CloudFront
    Value: !GetAtt WebBucketDistribution.DomainName
```

①以下、前回記事より設定内容に変更なし
・WebBucket（S3バケット）
・WebBucketBucketPolicy（バケットポリシー）
・WebBucketDistribution（CloudFrontディストリビューション）
・WebBucketOriginAccessControl（OAC）
・WebBucketCachePolicy（キャッシュポリシー）
・WebBucketOriginRequestPolicy（オリジンリクエストポリシー）

②DeleteCacheLambdaRole（Lambda用IAMロール）
・ログを書き込むための権限を付与（logs:～）
・CloudFrontのキャッシュ削除を行うための権限を付与（cloudfront:CreateInvalidation）
・CloudFrontディストリビューションの一覧を取得するための権限を付与（cloudfront:ListDistributions）

③DeleteCache（キャッシュ削除用Lambda）
・ディストリビューションIDやS3ドメイン名を直接環境変数に設定すると循環参照が発生してしまうため、Lambda内でS3ドメイン名を作成しディストリビューションIDを取得する。そのためにはバケット名が必要となるためLambdaから取得できないStack名を環境変数に設定（バケット名：Stack名-webbucket-アカウントId）
・WebBucketで「index.html」の作成・削除が行われた際に実行されるよう設定（Events:～）

④LambdaInvokePermission（Lambdaに付与するリソースベースのポリシー）

## Lambda
```index.py
import os
import boto3

def lambda_handler(event, context): 
    #アカウントIDの取得
    sts_client = boto3.client("sts")
    account_id = sts_client.get_caller_identity()["Account"]
    
    #S3ドメイン名の設定
    s3_domain_name = os.environ['Stack_NAME'] + '-webbucket-' + account_id + '.s3.' + os.environ['AWS_REGION'] + '.amazonaws.com'
    paths_to_invalidate = ['/index.html']

    #CloudFrontディストリビューションの一覧を取得
    cloudfront_client = boto3.client('cloudfront')
    response = cloudfront_client.list_distributions()
    distributions = response['DistributionList']['Items']

    #S3ドメイン名が一致するCloudFrontディストリビューションのIDを取得
    for distribution in distributions:
        if distribution['Origins']['Items'][0]['DomainName'] == s3_domain_name:
            distribution_id = distribution['Id']
            break
    
    #CloudFrontのキャッシュ削除
    cloudfront_client.create_invalidation(
        DistributionId=distribution_id,
        InvalidationBatch={
            'Paths': {
                'Quantity': len(paths_to_invalidate),
                'Items': paths_to_invalidate
            },
            'CallerReference': str(context.aws_request_id)
        }
    )
```

## 動作確認
①作成されたS3バケットへ「index.html」をアップロードする
```index.html
<html xmlns="http://www.w3.org/1999/xhtml" >
<head>
    <title>My Website Home Page</title>
</head>
<body>
  <h1>Welcome to my website(CloudFront)</h1>
  <p>Now hosted on Amazon S3(Before)!</p>
</body>
</html>
```

②Webサイトへアクセス
・正常にアクセスできることを確認
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3637963/ec71f540-5f37-6cac-2396-076f804ce971.png)

③S3バケットへ「index.html」をアップロードする（更新）
```index.html
<html xmlns="http://www.w3.org/1999/xhtml" >
<head>
    <title>My Website Home Page</title>
</head>
<body>
  <h1>Welcome to my website(CloudFront)</h1>
  <p>Now hosted on Amazon S3(After)!</p>
</body>
</html>
```

④ブラウザの更新
・表示される画面が更新され最新のコンテンツが配信されていることを確認
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3637963/4a97d573-7784-9d51-3f8d-342082523e47.png)

⑤ログ（CloudWatch）にエラーが出ていないことの確認
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3637963/3d028345-257f-b2bf-eac6-6ecb8c536e58.png)


## 参考（前回記事）
https://qiita.com/Haruki-N/items/c80e91d89ce9724d0fd8