CodePipelineのS3へのデプロイを使って、CloudFormation一撃で静的Webサイトの構築をやってみる

福岡オフィスの梶原です。先日、CodePipelineでS3へのデプロイがサポートされました。

AWS CodePipeline で Amazon S3 へのデプロイのサポートを開始
https://aws.amazon.com/jp/about-aws/whats-new/2019/01/aws-codepipeline-now-supports-deploying-to-amazon-s3/

ということで、そのままお知らせするのもなんなので勢いあまって

GithubリポジトリにあるWebコンテンツをCodePipelineでS3にデプロイ展開し、CloudFrontで配信するAWS環境を一撃で作るCloudFormation Templateを作ったので公開します
(なげーよ

※注意(2019/02/25 追記)
S3へのデプロイですが、現時点ではgithub側のコンテンツ削除が同期されません。本番使用する場合はデプロイ前に削除 または 別途syncを実施するなどの対応が必要です。

構成図

いるもの

  • AWSアカウント
  • Githubアカウント
  • Webコンテンツ(とりあえず、index.html)

Github側の準備

リポジトリとコンテンツの準備

Githubにprivateのリポジトリを作成して、直下にwebコンテンツを置きます。 ここでは、単純にindex.htmlを置きます。

OAuthトークンの作成と取得

下記URLにアクセスしてGithubのOAuthのトークンを取得します

https://github.com/settings/tokens

  1. [generate new token]を押下します token descriptionはわかりやすい名前をつけてください
  2. □repo, □admin:repo_hookをチェックします。
  3. [generato token]を押下します。

tokenが生成されるのでメモしておきます

CloudFormationで一撃

使用するCloudFormationのテンプレートはこちらになります。

AWSTemplateFormatVersion: '2010-09-09'
Metadata: 
  AWS::CloudFormation::Interface: 
    ParameterGroups: 
      - Label: 
          default: "Github Repository Settings"
        Parameters: 
          - GitHubOwner
          - GitHubRepo
          - GitHubBranch
      - Label: 
          default: "Github OAuth"
        Parameters: 
          - GitHubOAuthToken
    ParameterLabels: 
      GitHubOwner: 
        default: "Owner"
      GitHubRepo: 
        default: "Repository"
      GitHubBranch: 
        default: "Branch"
      GitHubOAuthToken: 
        default: "OAuthToken"
Parameters:
  GitHubOwner:
    Type: String
  GitHubRepo:
    Type: String
  GitHubBranch:
    Type: String
    Default: master
  GitHubOAuthToken:
    Type: String
    NoEcho: true

Resources:
  # S3 bucket contains static contents
  S3Bucket:
    Type: AWS::S3::Bucket
    DeletionPolicy: Retain

  CloudFrontOriginAccessIdentity:
    Type: AWS::CloudFront::CloudFrontOriginAccessIdentity
    Properties:
      CloudFrontOriginAccessIdentityConfig:
        Comment: !Ref AWS::StackName

  # S3 bucket policy to allow access from CloudFront OAI
  S3BucketPolicy:
    Type: AWS::S3::BucketPolicy
    Properties:
      Bucket: !Ref S3Bucket
      PolicyDocument:
        Statement:
          - Sid: "1"
            Action: s3:GetObject
            Effect: Allow
            Resource: !Sub arn:aws:s3:::${S3Bucket}/*
            Principal:
              AWS: !Sub arn:aws:iam::cloudfront:user/CloudFront Origin Access Identity ${CloudFrontOriginAccessIdentity}

  # CloudFront Distribution for contents delivery
  S3Distribution:
    Type: AWS::CloudFront::Distribution
    Properties:
      DistributionConfig:
        Enabled: true
        Origins:
        - Id: !Sub S3-${S3Bucket.DomainName}
          DomainName: !GetAtt S3Bucket.DomainName
          S3OriginConfig:
            OriginAccessIdentity: !Sub origin-access-identity/cloudfront/${CloudFrontOriginAccessIdentity}
        DefaultRootObject: index.html
        DefaultCacheBehavior:
          TargetOriginId: !Sub S3-${S3Bucket.DomainName}
          ViewerProtocolPolicy: allow-all
          ForwardedValues:
            QueryString: false
        PriceClass: PriceClass_200

  CodePipelineArtifactStoreBucket:
    Type: AWS::S3::Bucket
    DeletionPolicy: Retain

  CodePipelineServiceRole:
    Type: AWS::IAM::Role
    Properties:
      Path: /service-role/
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service: codepipeline.amazonaws.com
            Action: sts:AssumeRole
      Policies:
        - PolicyName: root
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Action:
                  - iam:PassRole
                Resource: '*'
                Effect: Allow
                Condition:
                  StringEqualsIfExists:
                    iam:PassedToService:
                      - cloudformation.amazonaws.com
              - Action:
                  - codedeploy:CreateDeployment
                  - codedeploy:GetApplication
                  - codedeploy:GetApplicationRevision
                  - codedeploy:GetDeployment
                  - codedeploy:GetDeploymentConfig
                  - codedeploy:RegisterApplicationRevision
                Resource: '*'
                Effect: Allow
              - Action:
                  - cloudwatch:*
                  - s3:*
                  - cloudformation:*
                Resource: '*'
                Effect: Allow

  Pipeline:
    Type: AWS::CodePipeline::Pipeline
    Properties:
      ArtifactStore:
        Type: S3
        Location: !Ref CodePipelineArtifactStoreBucket
      RoleArn: !GetAtt CodePipelineServiceRole.Arn
      Stages:
        - Name: Source
          Actions:
            - Name: SourceAction
              ActionTypeId:
                Category: Source
                Owner: ThirdParty
                Version: 1
                Provider: GitHub
              Configuration:
                Owner: !Ref GitHubOwner
                Repo: !Ref GitHubRepo
                Branch: !Ref GitHubBranch
                OAuthToken: !Ref GitHubOAuthToken
                PollForSourceChanges: false
              OutputArtifacts:
                - Name: SourceArtifact
              RunOrder: 1
        - Name: Deploy
          Actions:
            - Name: DeployAction
              ActionTypeId:
                Category: Deploy
                Owner: AWS
                Provider: S3
                Version: 1
              InputArtifacts:
                - Name: SourceArtifact
              Configuration:
                BucketName: !Ref S3Bucket
                Extract: true
              # OutputArtifacts: []
              RunOrder: 1

  GitHubSecret:
    Type: AWS::SecretsManager::Secret
    Properties:
      GenerateSecretString:
        SecretStringTemplate: '{}'
        GenerateStringKey: "SecretToken"
        ExcludePunctuation: true
        PasswordLength: 40

  PipelineWebhook:
    Type: AWS::CodePipeline::Webhook
    Properties:
      Authentication: GITHUB_HMAC
      AuthenticationConfiguration:
        SecretToken: !Join ['', ['{{resolve:secretsmanager:', !Ref GitHubSecret, ':SecretString:SecretToken}}' ]]
      Filters:
        - 
          JsonPath: "$.ref"
          MatchEquals: refs/heads/{Branch}
      TargetPipeline: !Ref Pipeline
      TargetAction: SourceAction
      TargetPipelineVersion: !GetAtt Pipeline.Version
      RegisterWithThirdParty: true

Outputs:
  S3OriginBucket:
    Value: !Join [ "", [ "https://s3.console.aws.amazon.com/s3/buckets/", !Ref S3Bucket ]]
  CloudFrontUrl:
    Value: !Join [ "", [ "https://", !GetAtt [ S3Distribution, DomainName ]]]

ファイルはS3に置いたので、AWSアカウントにログイン後下記URLをクリックしてみるとCloudFormationのコンソールが立ち上がりますので

配布元のGithubリポジトリなどを変更して、先ほど取得したOAuthトークンを設定してください。

こちらをクリック

CloudFormationのポイント

CodePipeline のS3展開部分

        - Name: Deploy
          Actions:
            - Name: DeployAction
              ActionTypeId:
                Category: Deploy
                Owner: AWS
                Provider: S3
                Version: 1
              InputArtifacts:
                - Name: SourceArtifact
              Configuration:
                BucketName: !Ref S3Bucket
                Extract: true
              # OutputArtifacts: []
              RunOrder: 1

S3 のデプロイ部分は上記記載の部分になります。 S3のOAIを使用してCloudFrontコンテンツを配信するので、Extract:true を指定してオリジンのS3バケットにファイルを展開しています。

GithubのWebhook

  GitHubSecret:
    Type: "AWS::SecretsManager::Secret"
    Properties:
      GenerateSecretString:
        SecretStringTemplate: '{}'
        GenerateStringKey: "SecretToken"
        ExcludePunctuation: true
        PasswordLength: 40

  PipelineWebhook:
    Type: AWS::CodePipeline::Webhook
    Properties:
      Authentication: GITHUB_HMAC
      AuthenticationConfiguration:
        SecretToken: !Join ['', ['{{resolve:secretsmanager:', !Ref GitHubSecret, ':SecretString:SecretToken}}' ]]
      Filters:
        - 
          JsonPath: "$.ref"
          MatchEquals: refs/heads/{Branch}
      TargetPipeline: !Ref Pipeline
      TargetAction: SourceAction
      TargetPipelineVersion: !GetAtt Pipeline.Version
      RegisterWithThirdParty: true

GithubのOAuthの権限でWebHookを作成できるようにしてますので(repo_hookのチェックの部分) CodepipeLineのGithub WebHook連携機能を使って、GithubのWebHookを作成しCommitされた場合に自動でCodePipelineのデプロイが動くようにしています。 また、WebHookを作成する際は、CloudFormationでSecretsManagerにランダムなTokenを生成して動的な参照(Dynamic Reference)してWebHookのパラメータに使用しています。

Github のリポジトリページの[Settings][WebHook]にWebHookが作成されていますので確認してみてください。 また、生成したトークンの値はSecretsManagerでも確認できます。

アクセスしてみる

CloudFormationのOutputにCloudFrontのURLが出力されていますアクセスしてみてください。

※注
CloudFormationの作成が終わってもCloudFrontの展開状況によっては以下のアクセス拒否の下記エラーがでる可能性があります。解消まである程度時間がかかるのでお待ちください。

This XML file does not appear to have any style information associated with it. The document tree is shown below.

コンテンツを更新してみる

githubのリポジトリのコンテンツを更新また、作成してみてください。 CodePipelineのデプロイが走り、S3のコンテンツが更新されます。 再度CloudFrontのURLにアクセスしコンテンツが更新されていれば成功です。 (キャッシュが効いていることもあるのでスーパーリロードなど実施してください)

まとめ

S3のデプロイがサポートされる前は、CodeBuild等を使用してS3へsyncで展開などを行っていましたが、CodePipelineでサポートされたので手軽に静的コンテンツのデプロイが実施できるようになりました。 S3でWeb配信以外にも、単純なgithubのリポジトリのバックアップなどにも使えそうな気がしています。 (容量が大きくなるとS3に展開されるファイルも増えますので実際に使用する際は一時バケット(ArtifactStoreBucket)のLifeCycleなどの設定をお願いします)

おそらく、このテンプレートをそのまま使う事はあまりなく、独自ドメインが使いたいとかはきっとあると思うので、いい感じにカスタマイズして使用してください。
誰かの役に立つと嬉しいです。もっともっとカスタマイズしたい、もう無理ぽ。とかなれば

クラスメソッドの「プレミアムオプション」などもご検討ください。

https://classmethod.jp/services/members/

参考

https://docs.aws.amazon.com/ja_jp/codepipeline/latest/userguide/tutorials-s3deploy.html

https://docs.aws.amazon.com/codepipeline/latest/userguide/pipelines-webhooks-create-cfn.html

https://docs.aws.amazon.com/ja_jp/codepipeline/latest/userguide/GitHub-rotate-personal-token-CLI.html