serverless frameworkをCodePipelineでデプロイするCloudFormationを作成する

2020.04.06

目標

CodeCommitにホストされたserverless frameworkのコードをCodePipelineでデプロイするためのCloudFormationを作成します。

serverless frameworkのデプロイコマンドにはstageオプションがあり、ステージ(環境)ごとにデプロイされるようになっています。それにあわせてCodePipelineもステージ別に作成するように実装してみます。今回はdevprdという2環境むけに作成します。

環境

今回は下記の環境で確認しています。

  • serverless framework: 1.67.0
  • Codepipeline runtime: Node.js 12.x

serverless framework側 ソースコードの変更

CloudFormationテンプレート作成の前にまずserverless framework側(CodeCommitでホストするソースコード側)で必要な変更を行います。

serverless.yml

serverless.ymlの内容です。serviceregionは後述するCloudFormationと同じものにします。

service: serverless-deploy-example

provider:
  name: aws
  runtime: nodejs12.x
  region: ap-northeast-1
  stage: ${opt:stage, self:custom.defaultStage}

custom:
  defaultStage: dev

functions:
  hello:
    handler: handler.hello

package.json

ここには、デプロイ時に実行するnpm scriptコマンドを記述します。ここに環境別向けのデプロイコマンドを記述します。

{
  "name": "serverless-deploy-example",
  "version": "1.0.0",
  "dependencies": {
    "serverless": "^1.67.0"
  },
  "devDependencies": {},
  "scripts": {
    "deploy:dev": "sls deploy --stage dev",
    "deploy:prd": "sls deploy --stage prd"
  }
}

buildspec.yml

CodeBuildで使用するbuildspec.ymlを作成します。${DEPLOY_ENV}には後述するCloudFormationテンプレートでCodeBuildの環境変数を埋め込むようにします。

version: 0.2

phases:
  install:
    runtime-versions:
      nodejs: 12
    commands:
      - npm install
  build:
    commands:
      - npm run deploy:${DEPLOY_ENV}

CloudFormationテンプレート

次にCloudFormationテンプレートを作成します。

基本となるCloudFormationテンプレートは先日書いたフロントエンド環境をS3にデプロイするCodePipelineをCloudFormationで作成するのものと同じです。

Parameters.Envでスタック作成時にdevprdを選択するようにします。

AWSTemplateFormatVersion: 2010-09-09
Description: serverless framework deploy pipeline example
Parameters:
  ServiceName:
    Description: serverless framework deploy pipeline example
    Type: String
    Default: serverless-deploy-example
  Env:
    Type: String
    Default: dev
    AllowedValues:
      - dev
      - prd

Resources:
  # ビルド成果物を格納するS3バケット
  ArtifactsBucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Sub ${ServiceName}-artifacts-${Env}
      LifecycleConfiguration:
        Rules:
          - Id: DeleteRule
            Status: Enabled
            ExpirationInDays: 7
  # CodeBuild
  CodeBuildProject:
    Type: AWS::CodeBuild::Project
    Properties:
      Name: !Sub ${ServiceName}-${Env}
      Artifacts:
        Type: CODEPIPELINE
      Source:
        Type: CODEPIPELINE
      Environment:
        ComputeType: BUILD_GENERAL1_SMALL
        Image: aws/codebuild/standard:3.0
        PrivilegedMode: true
        Type: LINUX_CONTAINER
        EnvironmentVariables:
          - Name: DEPLOY_ENV
            Value: !Sub ${Env}
      ServiceRole: !GetAtt CodeBuildServiceRole.Arn
  # CodeBuildのIAMロール
  CodeBuildServiceRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName:
        !Sub ${ServiceName}-CodeBuildServiceRole-${Env}
      Policies:
        - PolicyName: !Sub ${ServiceName}-CodeBuild-ServiceRolePolicy-${Env}
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Action:
                  - logs:CreateLogGroup
                  - logs:CreateLogStream
                  - logs:PutLogEvents
                  - logs:DeleteLogGroup
                  - logs:DescribeLogGroups
                Resource:
                  - !Sub arn:aws:logs:ap-northeast-1:${AWS::AccountId}:log-group:/aws/codebuild/${ServiceName}-${Env}:*
                  - !Sub arn:aws:logs:ap-northeast-1:${AWS::AccountId}:log-group:/aws/lambda/*
                  - !Sub arn:aws:logs:ap-northeast-1:${AWS::AccountId}:log-group::log-stream:*
                Effect: Allow
              - Action:
                  - codebuild:CreateReportGroup
                  - codebuild:CreateReport
                  - codebuild:UpdateReport
                  - codebuild:BatchPutTestCases
                Resource:
                  - !Sub arn:aws:codebuild:ap-northeast-1:${AWS::AccountId}:project/${ServiceName}-${Env}
                Effect: Allow
              - Action:
                  - s3:CreateBucket
                  - s3:DeleteBucket
                  - s3:PutBucket
                  - s3:GetObject
                  - s3:GetObjectVersion
                  - s3:GetBucketAcl
                  - s3:GetBucketLocation
                  - s3:PutObject
                  - s3:ListBucket
                  - s3:SetBucketEncryption
                  - s3:GetEncryptionConfiguration
                  - s3:PutEncryptionConfiguration
                  - s3:PutBucketPolicy
                Resource:
                  - '*'
                Effect: Allow
              - Action:
                  - cloudformation:DescribeStackEvents
                  - cloudformation:DescribeStackResources
                  - cloudformation:DescribeStackResource
                  - cloudformation:DescribeStacks
                  - cloudformation:ValidateTemplate
                  - cloudformation:CreateStack
                  - cloudformation:UpdateStack
                  - cloudformation:DeleteStack
                  - cloudformation:ListStackResources
                Resource:
                  - '*'
                Effect: Allow
              - Action:
                  - iam:CreateRole
                  - iam:DeleteRole 
                  - iam:DeleteRolePolicy
                  - iam:PutRolePolicy
                  - iam:GetRole
                  - iam:PassRole
                Resource:
                  - '*'
                Effect: Allow
              - Action:
                  - lambda:UpdateFunctionCode
                  - lambda:GetFunction
                  - lambda:GetFunctionConfiguration
                  - lambda:CreateFunction
                  - lambda:DeleteFunction
                  - lambda:ListVersionsByFunction
                  - lambda:PublishVersion
                Resource:
                  - !Sub arn:aws:lambda:ap-northeast-1:${AWS::AccountId}:function:${ServiceName}-${Env}*
                Effect: Allow
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Sid: ''
            Effect: Allow
            Principal:
              Service: codebuild.amazonaws.com
            Action: sts:AssumeRole
  # CodePipeline
  CodePipelineProject:
    Type: AWS::CodePipeline::Pipeline
    Properties:
      Name: !Sub ${ServiceName}-${Env}
      RoleArn: !GetAtt CodePipelineServiceRole.Arn
      Stages:
        - Name: Source
          Actions:
            - Name: SourceAction
              ActionTypeId:
                Category: Source
                Owner: AWS
                Version: 1
                Provider: CodeCommit
              OutputArtifacts:
                - Name: SourceArtifact
              # デプロイもとのCodeCommitのリポジトリとブランチ
              Configuration:
                RepositoryName: serverless-example-repository
                BranchName: master
                # push時に自動でデプロイされないようにする
                PollForSourceChanges: false
              RunOrder: 1
        - Name: Build
          Actions:
            - Name: BuildAction
              InputArtifacts:
                - Name: SourceArtifact
              OutputArtifacts:
                - Name: BuildArtifact
              ActionTypeId:
                Category: Build
                Owner: AWS
                Version: 1
                Provider: CodeBuild
              Configuration:
                ProjectName:
                  !Ref CodeBuildProject
              RunOrder: 2
      ArtifactStore:
        Type: S3
        Location:
          !Ref ArtifactsBucket
   # CodePipelineのIAMロール
  CodePipelineServiceRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub ${ServiceName}-CodePipelineServiceRole-${Env}
      Policies:
        - PolicyName: !Sub ${ServiceName}-CodePipelineServiceRolePolicy-${Env}
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Action:
                  - s3:GetObject
                  - s3:GetObjectVersion
                  - s3:GetBucketVersioning
                  - s3:PutObject
                Resource:
                  - arn:aws:s3:::codepipeline*
                Effect: Allow
              - Action:
                  - codecommit:CancelUploadArchive
                  - codecommit:GetBranch
                  - codecommit:GetCommit
                  - codecommit:GetUploadArchiveStatus
                  - codecommit:UploadArchive
                Resource:
                  - '*'
                Effect: Allow
              - Action:
                  - codebuild:BatchGetBuilds
                  - codebuild:StartBuild
                Resource:
                  - '*'
                Effect: Allow
              - Action:
                  - codedeploy:CreateDeployment
                  - codedeploy:GetApplication
                  - codedeploy:GetApplicationRevision
                  - codedeploy:GetDeployment
                  - codedeploy:GetDeploymentConfig
                  - codedeploy:RegisterApplicationRevision
                Resource:
                  - '*'
                Effect: Allow
              - Action:
                  - codestar-connections:UseConnection
                Resource:
                  - '*'
                Effect: Allow
              - Action:
                  - s3:*
                Resource:
                  - '*'
                Effect: Allow
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          -
            Effect: Allow
            Principal:
              Service:
                - codepipeline.amazonaws.com
            Action:
              - sts:AssumeRole

スタックを作成する

先程のEnvを指定してそれぞれdevprdのスタックを作成します。

デプロイする

スタック作成後に作成されたCodePipelineからデプロイできるかどうか確認します。

devprdでそれぞれのパイプラインが作成されているので選択して「変更をリリースする」を選択します。

デプロイが成功すればserverless frameworkが作成したスタックを確認することができます。

Lambda関数もdevprdでそれぞれ別に出来上がっています。

おわりに

今回のCloudFormationテンプレートだとデプロイが全てmasterブランチからとなっていますが、環境ごとにデプロイもととなるブランチを変えたいといった場合などにはデプロイもとのブランチ名もEnvなどを含めたものにあわせて変更するなど、もうひと工夫必要になります。

参考