GitLabリポジトリをCodeCommitにミラーリングしてCloudFront+S3に配置する(Vue.js+CloudFormation 編)

GitLabリポジトリをCodeCommitにミラーリングしてCloudFront+S3に配置する(Vue.js+CloudFormation 編)
この記事をシェアする

はじめに

こんにちは!スカイアーチHRソリューションズのnakaoです!
前回、こちらの記事「GitLabリポジトリをCodeCommitにミラーリングしてS3に配置する(Vue.js 編)」でVue.jsのプロジェクトをローカル上からGitLabにPushしてS3に配置するミラーリングを試しました。
今回は構成をCloudFront+S3に変更して、Vue.jsのソースコードをミラーリングしてみようと思います。
また、前回まで手動でAWSマネジメントコンソールから作成していたリソースをCloudFormationで管理してみようと思い、そちらも挑戦してみました!

アーキテクチャ

全体のアーキテクチャはこちらです!
こちらに記載してあるパイプラインはアプリケーション用のパイプラインです。

処理の流れとしては、以下の順番で実行されます。

①GitLabにソースコード(Vue.js)をPushする。
②ミラーリングが実行され、CodeCommitリポジトリにソースコードがミラーリングされる。
③CodeCommitへのミラーリングをトリガーに、CodePipelineが実行される。
④CodePipeline上でCodeBuildが実行される。
⑤CodePipeline上でS3へのデプロイが実行される。
⑥CodePipeline上でCloudFrontのキャッシュを削除するlambdaが実行される。

今回は構成をCloudFront+S3に変更したので、CloudFrontのキャッシュを考慮する必要がありました。
デフォルトではS3オブジェクトのキャッシュを24時間保持します。
今回はキャッシュを削除するlambdaを用意し、S3にコンテンツが配置されたらキャッシュを削除するようにしました。
以下、参考にした記事です。

また、クライアント側のブラウザキャッシュに関しても即時反映するようにしたかったため、CloudFrontのレスポンスヘッダーにCache-Control:no-cacheを設定するようにしました。
以下、参考にした記事です。

アプリケーション用のパイプラインとは別で、インフラリソース用のパイプラインをもう一つ用意します。
リソースのデプロイはより慎重に実施したかったので、手動デプロイを途中の手順として組み込んでいます。
デプロイを自動化してない、CIパイプラインにしました。

処理の流れとしては、以下の順番で実行されます。

①GitLabにソースコード(CloudFormationの.ymlファイル)をPushする。
②ミラーリングが実行され、CodeCommitリポジトリにソースコードがミラーリングされる。
③CodeCommitへのミラーリングをトリガーに、CodePipelineが実行される。
④CodePipeline上でCodeBuildが実行され、CloudFormationのChange setを作成する。
⑤AWSマネジメントコンソールからChange setを確認し、Change setを手動で実行、リソースのデプロイを行う。

分かりやすい記事にしたかったため、アプリケーション用のパイプライン、インフラリソース用のパイプラインは手動で作成します。
パイプラインも含めた完全自動化、インフラリソースのCI/CDパイプラインはまた別記事で紹介したいと思います。

インフラリソース用のパイプライン作成~実行

まずはじめに、インフラリソース用のパイプライン作成から実行まで実施します。

CodeCommitリポジトリの作成、IAMユーザーの作成、GitLabリポジトリの作成まで

以前執筆したこちらの記事「GitLabリポジトリをCodeCommitにミラーリングしてS3に配置する」と同様に、CodeCommitリポジトリの作成、IAMユーザーの作成、GitLabリポジトリを作成してください。
CodeCommitリポジトリの作成、IAMユーザーの作成、GitLabリポジトリの作成に関しては以上です。

CodeBuildビルドプロジェクトの作成と設定まで

こちらの記事「GitLabリポジトリをCodeCommitにミラーリングしてS3に配置する」と同様に、CodeBuildビルドプロジェクトを作成してください。IAMロールの修正と環境変数の設定を追加で実施します。

CodeBuild用IAMロールの修正

今回新しく作成したCodeBuild用のIAMロールだと、Buildspec実行時にCloudFormationへのアクセス権限が不足してしまいます。
「許可を追加」から「ポリシーをアタッチ」して、AWSCloudFormationFullAccess権限を付与しておきましょう。
※本来FullAccess権限のポリシーを付与するのは良くないですが、今回はこのままFullAccess権限を付与して進めます。必要最低限のポリシーを付与するように心掛けましょう。

CodeBuild用IAMロールの修正に関しては以上です。

CodeBuild用環境変数の設定

Buildspec実行時に指定するCloudFormationスタック名、S3バケット名、CloudFrontのキャッシュポリシーを環境変数に設定しておきましょう。

CodeBuild用環境変数の設定に関しては以上です。

CodePipelineの作成まで

CodePipelineを作成していきます。
こちらの記事「GitLabリポジトリをCodeCommitにミラーリングしてS3に配置する」と同様に、ソースステージ、ビルドステージを追加します。
デプロイステージのみ、必要ないのでスキップして作成してください。

CodePipelineの作成に関しては以上です。

GitLabリポジトリにソースコードをPushしてChange Setの作成

以降の作業は先ほど作成したインフラリソース用のGitLabリポジトリをローカル上にcloneして作業しているとします。
以下の構成でGitLabにソースコードを作成します。

「cfn」ディレクトリ以下にCloudFormationでリソースの設定を.ymlファイルで記述しています。
設定値の詳細までは説明しませんが、前回の記事で手動作成した際のデフォルト値と同じになるよう意識して記述しました。

AWSTemplateFormatVersion: 2010-09-09

Description:
  Create CloudFront and S3

Mappings:
  CachePolicyMap:
    Managed-CachingOptimized:
      Id: 658327ea-f89d-4fab-a63d-7e88639e58f6

Parameters:
  BucketName:
    Type: String
  CachePolicyName:
    Type: String

Resources:  
  # ------------------------------------------------------------#
  #  S3
  # ------------------------------------------------------------#
  s3Bucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Ref BucketName
      BucketEncryption:
        ServerSideEncryptionConfiguration:
          - ServerSideEncryptionByDefault:
              SSEAlgorithm: AES256
            BucketKeyEnabled: true
      WebsiteConfiguration:
        IndexDocument: index.html

  s3BucketPolicy:
    Type: AWS::S3::BucketPolicy
    Properties:
      Bucket: !Ref s3Bucket
      PolicyDocument:
        Version: 2012-10-17
        Statement:
          - Sid: AllowCloudFrontServicePrincipalReadOnly
            Effect: Allow
            Principal:
              Service:
                - cloudfront.amazonaws.com
            Action: s3:GetObject
            Resource: !Sub ${s3Bucket.Arn}/*
            Condition: 
              StringEquals:
                AWS:SourceArn: !Sub arn:aws:cloudfront::${AWS::AccountId}:distribution/${cloudfrontDistribution.Id}
  # ------------------------------------------------------------#
  #  CloudFront
  # ------------------------------------------------------------#
  cloudfrontDistribution:
    Type: AWS::CloudFront::Distribution
    Properties:
      DistributionConfig:
        Origins:
          - DomainName: !GetAtt s3Bucket.RegionalDomainName
            Id: myS3Origin
            S3OriginConfig:
              OriginAccessIdentity: ''
            OriginAccessControlId: !GetAtt OAC.Id
        DefaultCacheBehavior:
          ViewerProtocolPolicy: redirect-to-https
          AllowedMethods:
            - GET
            - HEAD
          Compress: true
          CachePolicyId: !FindInMap [ CachePolicyMap, !Ref CachePolicyName, Id]
          ResponseHeadersPolicyId: !GetAtt cacheControl.Id
          TargetOriginId: myS3Origin
        DefaultRootObject: index.html
        HttpVersion: http2
        Enabled: true
  
  OAC:
    Type: AWS::CloudFront::OriginAccessControl
    Properties:
      OriginAccessControlConfig:
        Name: OAC
        OriginAccessControlOriginType: s3
        SigningBehavior: always
        SigningProtocol: sigv4
  
  cacheControl:
    Type: AWS::CloudFront::ResponseHeadersPolicy
    Properties:
      ResponseHeadersPolicyConfig:
        Name: add-cache-control
        CustomHeadersConfig:
          Items:
            - Header: cache-control
              Value: no-cache
              Override: false
  # ------------------------------------------------------------#
  #  Lambda
  # ------------------------------------------------------------#
  lambdaFunction:
    Type: AWS::Lambda::Function
    Properties:
      FunctionName: clear-clf-cache
      Role: !GetAtt lambdaRole.Arn
      Runtime: python3.11
      Handler: index.lambda_handler
      Environment:
        Variables:
          DistributionId: !GetAtt cloudfrontDistribution.Id
      Code:
        ZipFile: |
          import json
          import os
          import boto3
          import time

          codepipeline = boto3.client('codepipeline')
          cloudfront = boto3.client('cloudfront')

          def lambda_handler(event, context):
            try:
              job_id = event['CodePipeline.job']['id']
              invalidation = cloudfront.create_invalidation(DistributionId=os.environ['DistributionId'],
                InvalidationBatch = {
                  'Paths': {
                  'Items': ['/*'],
                  'Quantity': 1
                  },
                  'CallerReference': str(time.time())
                }
              )
              codepipeline.put_job_success_result(jobId = job_id)
            
            except Exception as e:
              codepipeline.put_job_failure_result(
                jobId = job_id,
                failureDetails={
                  'type': 'JobFailed',
                  'message': str(e)
                }
              )
            return {
              'statusCode': 200,
              'body': json.dumps('cloudfront cache deletion completed')
            }
    
  lambdaRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: allow
            Principal:
              Service:
                - lambda.amazonaws.com
            Action: sts:AssumeRole
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
      Policies:
        - PolicyName: CloudFrontCreateInvalidationPolicy
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Effect: Allow
                Action:
                  - cloudfront:CreateInvalidation
                Resource: !Sub arn:aws:cloudfront::${AWS::AccountId}:distribution/*
        - PolicyName: CodePilelinePutJobResultPolicy
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Effect: Allow
                Action:
                  - codepipeline:PutJobSuccessResult
                  - codepipeline:PutJobFailureResult
                Resource: "*"

CloudFormation記述に関して、弊社社員の技術記事も参考にしました。ありがとうございます!

ルートディレクトリ直下に「buildspec.yml」を配置しています。
CloudFormationの構文チェックを実施してから、デプロイするようにしています。
デプロイコマンドに–no-execute-changesetオプションを付与することで、Change Setの作成・更新で処理が止まるようになります。オプションを付与しない場合は、Change Setの作成・更新からChange Setの実行まで自動で行います。

version: 0.2

phases:
  install:
    commands:
      - aws --version
  pre_build:
    commands:
      - echo Check syntax errors in cloudformation template file...
      - aws cloudformation validate-template --template-body file://cfn/cl-s3.yml
  build:
    commands:
      - echo cloudformation deploy start... 
      - aws cloudformation deploy --template-file cfn/cl-s3.yml --stack-name ${STACK_NAME} --parameter-overrides BucketName=${S3_BUCKET_NAME} CachePolicyName=${CACHE_POLICY_NAME} --capabilities CAPABILITY_IAM --no-execute-changeset
      - echo cloudformation deploy completed...

それでは、GitLabにPushします!

GitLabへのPush成功です!
CodeCommitリポジトリを確認します。

GitLabから、CodeCommitへのミラーリング成功です!
CodePipelineを確認します。

CodePipelineのステージ全て成功してますね!
インフラリソース用のパイプライン作成から実行までは以上です。

Change Setの手動デプロイ

インフラリソース用のパイプラインで作成したChange Setを手動デプロイします。
AWSマネジメントコンソールからCloudFormationを開き、作成したスタック名からChange Setを確認します。
作成するリソースに間違いがないかを確認して、Change Setを実行して手動デプロイします。

リソースが全て作成されました!

Change Setの手動デプロイに関しては以上です。

アプリケーション用のパイプライン作成~実行

次に、アプリケーション用のパイプライン作成から実行まで実施します。

CodeCommitリポジトリの作成、IAMユーザーの作成、GitLabリポジトリの作成、CodeBuildプロジェクトの作成まで

前回の記事「GitLabリポジトリをCodeCommitにミラーリングしてS3に配置する(Vue.js 編)」と同様に、CodeCommitリポジトリの作成、IAMユーザーの作成、GitLabリポジトリの作成、CodeBuildプロジェクトを作成してください。
CodeCommitリポジトリの作成、IAMユーザーの作成、GitLabリポジトリの作成、CodeBuildプロジェクトの作成に関しては以上です。

CodePipelineの作成まで

CodePipelineを作成していきます。
こちらの記事「GitLabリポジトリをCodeCommitにミラーリングしてS3に配置する」と同様に、ソースステージ、ビルドステージ、デプロイステージを追加して作成します。
作成したCodePipelineを編集して、Deployステージの後に、CloudFrontのキャッシュを削除するClearCacheステージを追加します。
ステージを追加したら、アクショングループを追加します。

アクションプロパイダーとしてlambdaを選択し、インフラリソース用のパイプラインで作成したlambda関数名を選択して、完了を押下します。

CodePipelineの作成に関しては以上です。

GitLabリポジトリにソースコードをPushして静的ウェブサイトの確認

以降の作業は先ほど作成したアプリケーション用のGitLabリポジトリをローカル上にcloneして作業しているとします。
前回の記事「GitLabリポジトリをCodeCommitにミラーリングしてS3に配置する(Vue.js 編)」と同様に、ローカル上にVue.jsのプロジェクトとbuildspec.ymlを作成してください。

それでは、GitLabにPushします!

GitLabへのPush成功です!
CodeCommitリポジトリを確認します。

GitLabから、CodeCommitへのミラーリング成功です!
CodePipelineを確認します。

CodePipelineのステージ全て成功してますね!
CloudFrontの確認をしていきましょう!
作成したディストリビューションのキャッシュ削除を確認すると、S3デプロイ後、キャッシュが削除されているのを確認できますね!

ディストリビューションドメインにアクセスして、ブラウザキャッシュも確認してみましょう。
Google ChromeデベロッパーツールのNetworkタブ、Response Headersを確認するとCache-Control:no-cacheで設定されていますね。

アプリケーション用のパイプライン作成から実行までは以上です。
再度アプリケーション用のリソースをGitで編集してPushしてみてください。編集したリソースが即時反映されることを確認できると思います。

おわりに

お疲れ様でした!
インフラリソース、アプリケーションそれぞれを自動化して管理することができましたね。
今回はブラウザのキャッシュをCache-Control:no-cacheにしましたが、サーバーへ毎回アクセスする必要があるデメリットもあり、コスト面などで問題があるかと思いました。
ブラウザのキャッシュ保持時間をある程度設けてコスト面の対応もしつつ、デプロイ後の反映時間のラグ対応としてはメンテナンス時間を設けて反映確認後、クライアント側へ伝えるなどの設計が良いのではないかと思いました。

また、パイプラインを手動で作成しましたが、パイプラインも含めて自動化したり、CloudFormationをCDKに変更して管理してみるなどまだまだ活用方法はありそうです!
CloudFrontのステータスが有効で公開状態になっているため、無効に設定するかリソースを削除しておきましょう。

私の記事が少しでも皆様のご参考になれば幸いです。

この記事をシェアする
著者:nakao
IoT、サーバーレスな開発に興味深々。AWSエンジニア。