クロスアカウントでSPAを自動デプロイする環境をCFnで構築する

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

はじめに

どうも、クラスメソッドの岡です。
今回アカウントをまたいでCodeCommitで管理しているAngular製SPAを継続的にデプロイする環境を作成するCFnテンプレートを作ってみました。

サーバーレスで作りやすいSPAですが、AWSで開発をする場合実際に稼働している環境と開発する環境はアカウントが分かれているパターンが多いかと思います。
また、ソースコードをCodeCommitで管理している場合はCodePipelineでデプロイフローを作るのが一般的ですがアカウントをまたぐ場合はIAMの設定等に気を使います。

ここでは、CloudFormationでAssumeRole元のIAMロールからCopePipeline、実際にS3にデプロイするCodeBuildプロジェクトまで一気に作成する方法をご紹介したいと思います。

シチュエーション

SPAの構成

まずデプロイ対象のSPAの構成は以下です。

デプロイ構成

  • ソース管理は開発アカウントのCodeCommit
  • ソースコードの環境の振り分けはブランチ名で
  • アカウントをまたいでアクセスするアーティファクト用のS3バケットはKMSで暗号化
  • Angularのデプロイ(S3に上書き)はCodeBuildのbuildファイルで設定

AngularをCodeBuildでデプロイする方法はこちらの記事でご紹介しています。

Angular 製 SPA を AWS CodeBuild を使ってデプロイする

ここではビルドファイルをそれぞれ

  • codebuild/buildspec.itg.yml
  • codebuild/buildspec.stg.yml
  • codebuild/buildspec.prd.yml

で作成済みとします。

CFnテンプレート

まずテンプレートを3つに分けました。 デプロイ先のアカウントとソースアカウントで作るリソースが変わるためです。

  • デプロイ先のアカウントにpipeline,buildプロジェクトを作成するテンプレート
  • ソースアカウントにAssumeRoleするためのIAMRoleを作成するテンプレート
  • ソースアカウントにpipeline,buildプロジェクトを作成するテンプレート

デプロイ先のアカウント(ここではstagingかproduction)
ソースアカウント(ここでは開発アカウント)

デプロイ先のアカウントで実行するテンプレート

デプロイ先のアカウント、stagingとproductionで実行するテンプレートです。  

作成するリソースは以下となります。

  • CodePipelineのIAMRole
  • CodebuildのIAMRole
  • アーティファクトバケットの暗号化用のKMSキー
  • アーティファクトバケット(S3)
  • バケットポリシー
  • CodePipelineのパイプライン
  • CodeBuildのプロジェクト
AWSTemplateFormatVersion: '2010-09-09'
Description: 'Create resources for automatic deployment of SPA'

Parameters:
  Env:
    Type: String
    Default: stg
    AllowedValues:
      - stg
      - prd
    Description: 'Set Enter Environment for SPA'

  DeployAccountId:
    Type: String
    Default: 123456789012
    AllowedValues:
      - 123456789012
      - 098765432109
    Description: 'Set Enter Account ID for SPA'

  SourceAccountId:
    Type: String
    Default: 678901234567
    Description: 'Set Enter Account ID for SPA source'
  
  SpaName:
    Type: String
    Default: member-spa
    AllowedValues:
      - member-spa
      - qrcode-spa
      - management-console
    Description: 'Set Enter SPA name'
  
  BranchName:
    Type: String
    Default: staging
    AllowedValues:
      - staging
      - master
    Description: 'Set Enter BranchName for SPA'

Resources:
  #pipeline用のIAMロールを作成
  SpaPipelineRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
        - Effect: Allow
          Principal:
            Service: codepipeline.amazonaws.com
          Action: sts:AssumeRole
      Policies:
        - PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action:
                  - s3:*
                Resource:
                 - !Sub arn:aws:s3:::${Env}-auto-deploy-${SpaName}
                 - !Sub arn:aws:s3:::${Env}-auto-deploy-${SpaName}/*
              - Effect: Allow
                Action:
                  - codebuild:StartBuild
                  - codebuild:BatchGetBuilds
                Resource: 
                  - '*'
              - Effect: Allow
                Action:
                  - sts:AssumeRole
                Resource: !Sub arn:aws:iam::${SourceAccountId}:role/${Env}-auto-deploy-${SpaName}-for-codecommit
          PolicyName: !Sub ${Env}-auto-deploy-${SpaName}-for-pipeline
      RoleName: !Sub ${Env}-auto-deploy-${SpaName}-for-pipeline
  #codebuild用のIAMロールを作成
  SpaCodeBuildRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
        - Effect: Allow
          Principal:
            Service: codebuild.amazonaws.com
          Action: sts:AssumeRole
      Policies:
        - PolicyDocument:
            Version: '2012-10-17'
            Statement:
            - Effect: Allow
              Action:
               - logs:*
               - cloudfront:CreateInvalidation
              Resource: 
               - '*'
            - Effect: Allow
              Action:
               - s3:*
              Resource:
               - '*'
          PolicyName: !Sub ${Env}-auto-deploy-${SpaName}-for-codebuild
      RoleName: !Sub ${Env}-auto-deploy-${SpaName}-for-codebuild
  #S3暗号化の為のKMSキーを作成
  SpaKmsKey:
    Type: AWS::KMS::Key
    Properties: 
      Description: SPA deploy
      KeyPolicy:
        Version: '2012-10-17'
        Statement: 
          - Sid: 'Enable IAM User Permissions'
            Effect: Allow
            Principal: 
              AWS: !Sub arn:aws:iam::${DeployAccountId}:root
            Action: kms:*
            Resource: '*'
          - Sid: 'Allow access for Key Administrators'
            Effect: Allow
            Principal: 
              AWS:
                - !GetAtt SpaCodeBuildRole.Arn
                - !GetAtt SpaPipelineRole.Arn
            Action: 
              - kms:Create*
              - kms:Describe*
              - kms:Enable*
              - kms:List*
              - kms:Put*
              - kms:Update*
              - kms:Revoke*
              - kms:Disable*
              - kms:Get*
              - kms:Delete*
              - kms:TagResource
              - kms:UntagResource
            Resource: '*'
          - Sid: 'Allow use of the key'
            Effect: Allow
            Principal: 
              AWS: 
                - !Sub arn:aws:iam::${SourceAccountId}:root
                - !GetAtt SpaCodeBuildRole.Arn
                - !GetAtt SpaPipelineRole.Arn
            Action: 
              - kms:Encrypt
              - kms:Decrypt
              - kms:ReEncrypt*
              - kms:GenerateDataKey*
              - kms:DescribeKey
            Resource: '*'
          - Sid: 'Allow attachment of persistent resources'
            Effect: Allow
            Principal: 
              AWS: 
                - !Sub arn:aws:iam::${SourceAccountId}:root
                - !GetAtt SpaCodeBuildRole.Arn
                - !GetAtt SpaPipelineRole.Arn
            Action: 
              - kms:CreateGrant
              - kms:ListGrants
              - kms:RevokeGrant
            Resource: '*'
            Condition: 
              Bool: 
                kms:GrantIsForAWSResource: true
  #アーティファクト用S3バケットを作成
  SpaSourceS3:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Sub ${Env}-auto-deploy-${SpaName}
      BucketEncryption:
        ServerSideEncryptionConfiguration:
        - ServerSideEncryptionByDefault: 
            SSEAlgorithm: aws:kms
            KMSMasterKeyID: !Ref SpaKmsKey
  SpaSourceS3BacketPolicy:
    Type: AWS::S3::BucketPolicy
    Properties: 
      Bucket: !Ref SpaSourceS3
      PolicyDocument:
        Version: '2012-10-17'
        Id: SSEAndSSLPolicy
        Statement: 
          - Sid: CrossAccountS3GetPutPolicy
            Effect: Allow
            Principal: 
              AWS: !Sub arn:aws:iam::${SourceAccountId}:root
            Action: 
              - s3:Get*
              - s3:Put*
            Resource: !Sub arn:aws:s3:::${Env}-auto-deploy-${SpaName}/*
          - Sid: CrossAccountS3ListPolicy
            Effect: Allow
            Principal: 
              AWS: !Sub arn:aws:iam::${SourceAccountId}:root
            Action: s3:*
            Resource: !Sub arn:aws:s3:::${Env}-auto-deploy-${SpaName}/*
          - Sid: CodePipeline
            Effect: Allow
            Principal: 
              AWS: !GetAtt SpaPipelineRole.Arn
            Action: s3:*
            Resource: !Sub arn:aws:s3:::${Env}-auto-deploy-${SpaName}/*
  #SPAをS3にデプロイするCodebuildプロジェクトを作成
  SpaCodebuild:
    Type: AWS::CodeBuild::Project
    Properties:
      Artifacts:
        Type: CODEPIPELINE
      Description: 'Build a SPA across accounts'
      EncryptionKey: !GetAtt SpaKmsKey.Arn
      Environment:
        Type: LINUX_CONTAINER
        ComputeType: BUILD_GENERAL1_SMALL
        Image: 'aws/codebuild/nodejs:8.11.0'
      Name: !Sub ${Env}-auto-deploy-${SpaName}
      ServiceRole: !GetAtt SpaCodeBuildRole.Arn
      Source:
        Type: CODEPIPELINE
        BuildSpec: !Sub codebuild/buildspec.${Env}.yml
  #Codecommitからソースを読み込んでCodebuildに受け渡すCodePipelineを作成
  SpaPipeline:
    Type: AWS::CodePipeline::Pipeline
    Properties:
      Name: !Sub ${Env}-auto-deploy-${SpaName}
      ArtifactStore:
        Type: S3
        Location: !Sub ${Env}-auto-deploy-${SpaName}
        EncryptionKey:
          Id: !GetAtt SpaKmsKey.Arn
          Type: KMS
      RoleArn: !GetAtt SpaPipelineRole.Arn
      Stages:
      - Name: Source
        Actions:
        - ActionTypeId:
            Category: Source
            Owner: AWS
            Provider: CodeCommit
            Version: 1
          Name: Source
          Configuration:
            BranchName: !Sub ${BranchName}
            RepositoryName: !Sub ${SpaName}
          OutputArtifacts:
          - Name: SpaSourceCode
          RoleArn: !Sub arn:aws:iam::${SourceAccountId}:role/${Env}-auto-deploy-${SpaName}-for-codecommit
          RunOrder: 1
      - Name: build
        Actions:
        - ActionTypeId:
            Category: Build
            Owner: AWS
            Provider: CodeBuild
            Version: 1
          Name: Build
          Configuration:
            ProjectName: !Ref SpaCodebuild
          InputArtifacts:
          - Name: SpaSourceCode
          RunOrder: 1

ソースアカウントにAssumeRoleするためのIAMRoleを作成するテンプレート

こちらは開発アカウントで実行します。 ステージングと本番アカウントからAssumeroleするためのIAMロールを作成するのでパラメーターのEnvにはデプロイ先の環境名を入力しましょう。

  • Env:デプロイ先の環境名(staging=stg、production=prd)
  • DeployAccountId:デプロイ先のAWSアカウントID
  • SourceAccountId:CodeCommitのソースが置いてあるアカウントID
  • SpaName:CodeCommitのリポジトリ名
AWSTemplateFormatVersion: '2010-09-09'
Description: 'Create resources for automatic deployment of SPA'


Parameters:
  Env:
    Type: String
    Default: stg
    AllowedValues:
      - stg
      - prd
    Description: 'Set Enter Environment for SPA'

  DeployAccountId:
    Type: String
    Default: 123456789012
    AllowedValues:
      - 123456789012
      - 098765432109
    Description: 'Set Enter Account ID to deploy SPA'

  SourceAccountId:
    Type: String
    Default: 678901234567
    Description: 'Set Enter Account ID for SPA source'
  
  SpaName:
    Type: String
    Default: my-app
    AllowedValues:
      - my-app
      - my-app2
    Description: 'Set Enter SPA name'

Resources:
# Create an IAM role to access codecommit
  PipelineRole:
    Type: AWS::IAM::Role
    Properties: 
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
        - Effect: Allow
          Principal:
            AWS: !Sub arn:aws:iam::${DeployAccountId}:root
          Action: sts:AssumeRole 
      Policies:
        - PolicyName: !Sub ${Env}-auto-deploy-${SpaName}-for-codecommit
          PolicyDocument:
            Version: '2012-10-17'
            Statement: 
              - Effect: Allow
                Action: 
                  - s3:*
                Resource: 
                  - !Sub arn:aws:s3:::${Env}-auto-deploy-${SpaName}
                  - !Sub arn:aws:s3:::${Env}-auto-deploy-${SpaName}/*
              - Effect: Allow
                Action:
                  - kms:DescribeKey
                  - kms:GenerateDataKey*
                  - kms:Encrypt
                  - kms:ReEncrypt*
                  - kms:Decrypt
                Resource: 
                  - '*'
              - Effect: Allow
                Action:
                  - codecommit:GetBranch
                  - codecommit:GetCommit
                  - codecommit:UploadArchive
                  - codecommit:GetUploadArchiveStatus
                  - codecommit:CancelUploadArchive
                Resource: 
                  - !Sub arn:aws:codecommit:ap-northeast-1:${SourceAccountId}:${SpaName}
      RoleName: !Sub ${Env}-auto-deploy-${SpaName}-for-codecommit

ソースアカウントにpipeline,buildプロジェクトを作成するテンプレート

最後に開発アカウントにPipalineを作成します。 こちらはアカウントは跨がないのでシンプルなテンプレートになります。

AWSTemplateFormatVersion: '2010-09-09'
Description: 'Create resources for automatic deployment of SPA'

Parameters:
  Env:
    Type: String
    Default: itg
    Description: 'Set Enter Environment for SPA'

  DeployAccountId:
    Type: String
    Default: 678901234567
    AllowedValues:
      - 678901234567
    Description: 'Set Enter Account ID to deploy SPA'
  
  SpaName:
    Type: String
    Default: my-app
    AllowedValues:
      - my-app
      - my-app2
    Description: 'Set Enter Repository name'

  BranchName:
    Type: String
    Default: integration
    AllowedValues:
      - integration
    Description: 'Set Enter branch name'

Resources:
  # create IAM role for pipeline
  SpaPipelineRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
        - Effect: Allow
          Principal:
            Service: codepipeline.amazonaws.com
          Action: sts:AssumeRole
      Policies:
        - PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action:
                  - s3:*
                Resource:
                 - !Sub arn:aws:s3:::${Env}-auto-deploy-${SpaName}
                 - !Sub arn:aws:s3:::${Env}-auto-deploy-${SpaName}/*
              - Effect: Allow
                Action:
                  - codebuild:StartBuild
                  - codebuild:BatchGetBuilds
                Resource: 
                  - '*'
          PolicyName: !Sub ${Env}-auto-deploy-${SpaName}-for-pipeline
      RoleName: !Sub ${Env}-auto-deploy-${SpaName}-for-pipeline
  # create IAM role for codebuild project
  SpaCodeBuildRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
        - Effect: Allow
          Principal:
            Service: codebuild.amazonaws.com
          Action: sts:AssumeRole
      Policies:
        - PolicyDocument:
            Version: '2012-10-17'
            Statement:
            - Effect: Allow
              Action:
              - logs:*
              - cloudfront:CreateInvalidation
              Resource: '*'
            - Effect: Allow
              Action:
               - s3:*
              Resource: '*'
        - PolicyName: !Sub ${Env}-auto-deploy-${SpaName}-for-codebuild
      RoleName: !Sub ${Env}-auto-deploy-${SpaName}-for-codebuild
  # Create artifact S3 bucket
  SpaSourceS3:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Sub ${Env}-auto-deploy-${SpaName}
  SpaCodebuild:
    Type: AWS::CodeBuild::Project
    Properties:
      Artifacts:
        Type: CODEPIPELINE
      Description: 'Build a SPA'
      Environment:
        Type: LINUX_CONTAINER
        ComputeType: BUILD_GENERAL1_SMALL
        Image: 'aws/codebuild/nodejs:8.11.0'
      Name: !Sub ${Env}-auto-deploy-${SpaName}
      ServiceRole: !GetAtt SpaCodeBuildRole.Arn
      Source:
        Type: CODEPIPELINE
        buildspec: !Sub codebuild/buildspec.${Env}.yml
  SpaPipaline:
    Type: AWS::CodePipeline::Pipeline
    Properties:
      ArtifactStore:
        Type: S3
        Location: !Sub ${Env}-auto-deploy-${SpaName}
      Name: !Sub ${Env}-auto-deploy-{SpaName}
      RoleArn: !GetAtt SpaPipelineRole.Arn
      Stages:
        Name: 'Source'
        Actions:
        -
          ActionsTypeId:
            Category: Source
            Owner: AWS
            Provider: CodeCommit
            Version: 1
          Configuration:
            BranchName: !Sub ${BranchName}
            RepositoryName: !Sub ${SpaName}
          OutputArtifacts:
            - Name: SpaSourceCode
          RunOrder: 1
      Stages:
        Name: build
        Actions:
        -
          ActionsTypeId:
            Category: Build
            Owner: AWS
            Provider: CodeBuild
            Version: 1
          Configuration:
            ProjectName: !Ref SpaCodebuild
          InputArtifacts:
            - Name: SpaSourceCode
          RunOrder: 1

注意点

CFnでIAMロールを作成する場合、確認画面で CAPABILITY 項目でチェックを入れる必要があります。 チェックを入れないとスタック作成時に Requires capabilities : [CAPABILITY_NAMED_IAM] のエラーが発生してIAMロールを作成できません。

CLIで実行する場合は aws cloudformation create-stack もしくは aws cloudformation update-stack 実行時のオプション --capabilitiesに CAPABILITY_NAMED_IAM を指定します。

AWSドキュメント

参考

ソースコード