In this post, I will share how to set up a continuous integration and continuous delivery (CI/CD) pipeline on AWS for API gateway. The following AWS services will be used:

  • Cloudformation allows you to model your entire infrastructure and application resources
  • CodePipeline is a continuous delivery service
  • CodeCommit is a version control service
  • CodeBuild is a fully managed build service in the cloud
  • Simple Storage Service (Amazon S3) is storage for the Internet
  • API Gateway is an AWS service for creating, publishing, maintaining, monitoring, and securing REST, HTTP, and WebSocket APIs
  • AWS Lambda is a serverless compute service

Create CodeCommit repository:

  • Login AWS Console
  • Choose Developer Tools > CodeCommit > Repositories
  • Click Create repository
  • Enter the repository name as my-api and description (optional)
    I refer it as ServiceName in this project
  • Click Create to create the repository

Cloudformation to provision the CI/CD pipeline

IAM Roles

  • Role to execute Lambda
  • Role to run CodeBuild
  • Role to run CodePipeline
  • Role that AWS CloudFormation assumes when it operates on resources in the specified stack
LambdaRole:
Type: AWS::IAM::Role
Properties:
RoleName: LambdaRole'
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
Action:
- 'sts:AssumeRole'
Effect: Allow
Principal:
Service:
- lambda.amazonaws.com
Path: /
ManagedPolicyArns:
- 'arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole'
- 'arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole'
Policies:
- PolicyName: LambdaRoleParameterStorePolicy
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Resource:
- !Sub 'arn:aws:ssm:ap-east-1:${AWS::AccountId}:parameter/my-api*'
Action:
- 'ssm:GetParameters'
- 'ssm:GetParameter'
- 'ssm:GetParametersByPath'
- Effect: Allow
Resource: '*'
Action:
- 'ssm:DescribeParameters'
CBSrvRole:
Type: AWS::IAM::Role
Properties:
RoleName: !Sub '${AWS::StackName}-${AWS::Region}-CBSrvRole'
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Action:
- 'sts:AssumeRole'
Effect: Allow
Principal:
Service:
- codebuild.amazonaws.com
Path: /
ManagedPolicyArns:
- 'arn:aws:iam::aws:policy/AWSCodeCommitFullAccess'
- !Ref CFCPExecPolicy
- 'arn:aws:iam::aws:policy/AdministratorAccess'
Policies:
- PolicyName: CodeBuildAccess
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Resource:
- !Sub 'arn:aws:logs:ap-southeast-1:${AWS::AccountId}:log-group:/aws/codebuild/*'
- !Sub 'arn:aws:logs:ap-southeast-1:${AWS::AccountId}:log-group:/aws/codebuild/*:*'
- !Sub 'arn:aws:logs:ap-east-1:${AWS::AccountId}:log-group:/aws/codebuild/*'
- !Sub 'arn:aws:logs:ap-east-1:${AWS::AccountId}:log-group:/aws/codebuild/*:*'
Action:
- 'logs:CreateLogGroup'
- 'logs:CreateLogStream'
- 'logs:PutLogEvents'
- Effect: Allow
Resource:
- '*'
Action:
- 's3:GetObject'
- 's3:GetObjectVersion'
- 's3:PutObject'
CPExecRole:
Type: AWS::IAM::Role
Properties:
RoleName: CPExecRole'
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Action:
- 'sts:AssumeRole'
Effect: Allow
Principal:
Service:
- codepipeline.amazonaws.com
Path: /
ManagedPolicyArns:
- 'arn:aws:iam::aws:policy/AWSCodeCommitFullAccess'
- 'arn:aws:iam::aws:policy/AmazonS3FullAccess'
- 'arn:aws:iam::aws:policy/AWSCodeDeployFullAccess'
- !Ref CFCPExecPolicy
Policies:
- PolicyName: CodePipelineAccess
PolicyDocument:
Version: '2012-10-17'
Statement:
- Action:
- 'iam:PassRole'
- 'lambda:InvokeFunction'
- 'lambda:ListFunctions'
- 'lambda:InvokeAsyc'
Effect: Allow
Resource: '*'
CFCPExecPolicy:
Type: AWS::IAM::ManagedPolicy
Properties:
Description: CloudFormation Pipeline Execution Policy
Path: "/"
PolicyDocument:
Version: '2012-10-17'
Statement:
Effect: Allow
Action:
- 'cloudformation:CreateStack'
- 'cloudformation:DescribeStacks'
- 'cloudformation:DeleteStack'
- 'cloudformation:UpdateStack'
- 'cloudformation:CreateChangeSet'
- 'cloudformation:ExecuteChangeSet'
- 'cloudformation:DeleteChangeSet'
- 'cloudformation:DescribeChangeSet'
- 'cloudformation:SetStackPolicy'
- 'cloudformation:SetStackPolicy'
- 'cloudformation:ValidateTemplate'
- 'codebuild:StartBuild'
- 'codebuild:BatchGetBuilds'
- 'sns:Publish'
Resource: "*"
CFExecRole:
Type: AWS::IAM::Role
Properties:
RoleName: CFExecRole
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
Action:
- 'sts:AssumeRole'
Effect: Allow
Principal:
Service:
- cloudformation.amazonaws.com
Path: /
ManagedPolicyArns:
- 'arn:aws:iam::aws:policy/AdministratorAccess'

CloudFormation tempate to provision CI/CD

  1. Define parameters for using in the template or pass to CodeBuild, and Lambda functions
  2. Define a S3 bucket for build artifacts
    • Use as build cache in CodeBuild
    • Use as Artifacstore in CodePipeline
    • BuildArtifactsBucket is the logical ID of the S3 bucket
  3. CodeBuild project
  4. CodePipeline
    • SAM TemplatePath: BuiltZip::template-packaged.yaml
  5. A complete Cloudformation template:
    AWSTemplateFormatVersion: 2010-09-09
    Description: CI/CD for my-api

    Parameters:
    ServiceName:
    Type: String
    Default: my-api
    Description: CodeCommit repository name
    BuildImageName:
    Description: Docker image for application build
    Type: String
    Default: aws/codebuild/standard:5.0
    BranchName:
    Description: CodeCommit branch to build and deploy
    Type: String
    Default: release/dev
    Env:
    Type: String
    Default: uat
    SecurityGroup:
    Type: String
    Default: sg-xxxxxxxxxxxxxxx
    Subnet1:
    Type: String
    Default: subnet-xxxxxxxxxxxxxxxx
    Subnet2:
    Type: String
    Default: subnet-xxxxxxxxxxxxxxxxx

    Resources:
    BuildArtifactsBucket:
    Type: AWS::S3::Bucket

    CodeBuildProject:
    Type: AWS::CodeBuild::Project
    Properties:
    Description:
    !Join
    - ''
    - - !Sub 'Build project for the ${ServiceName}'
    - ' ('
    - !Ref AWS::StackName
    - ')'
    Artifacts:
    Type: CODEPIPELINE
    Cache:
    Location:
    !Join
    - '/'
    - - !Ref BuildArtifactsBucket
    - !Sub '${ServiceName}_cache'
    Type: S3
    Environment:
    Type: LINUX_CONTAINER
    ComputeType: BUILD_GENERAL1_SMALL
    Image: !Sub ${BuildImageName}
    EnvironmentVariables:
    - Name: ENV
    Value: !Ref Env
    - Name: SecurityGroup
    Value: !Ref SecurityGroup
    - Name: Subnet1
    Value: !Ref Subnet1
    - Name: Subnet2
    Value: !Ref Subnet2
    - Name: LambdaRole
    Value: LambdaRole
    - Name: SourceVpc
    Value: !Ref SourceVpc
    ServiceRole:
    !Join
    - ''
    - - !Sub 'arn:aws:iam::${AWS::AccountId}:role/'
    - CBSrvRole
    Source:
    Type: CODEPIPELINE

    Pipeline:
    Type: AWS::CodePipeline::Pipeline
    Properties:
    ArtifactStore:
    Location: !Ref BuildArtifactsBucket
    Type: S3
    RoleArn:
    !Join
    - ''
    - - !Sub 'arn:aws:iam::${AWS::AccountId}:role/'
    - CPExecRole
    Stages:
    - Name: Source
    Actions:
    - Name: CodeCommitRepo
    ActionTypeId:
    Category: Source
    Owner: AWS
    Provider: CodeCommit
    Version: 1
    Configuration:
    RepositoryName: !Sub ${ServiceName}
    BranchName: !Sub ${BranchName}
    OutputArtifacts:
    - Name: SourceZip
    RunOrder: 1
    - Name: Build
    Actions:
    - Name: CodeBuild
    ActionTypeId:
    Category: Build
    Owner: AWS
    Provider: CodeBuild
    Version: 1
    Configuration:
    ProjectName: !Ref CodeBuildProject
    InputArtifacts:
    - Name: SourceZip
    OutputArtifacts:
    - Name: BuiltZip
    - Name: Deploy
    Actions:
    - Name: CreateChangeSetPublicAPI
    ActionTypeId:
    Category: Deploy
    Owner: AWS
    Provider: CloudFormation
    Version: 1
    Configuration:
    ActionMode: CHANGE_SET_REPLACE
    RoleArn:
    !Join
    - ''
    - - !Sub 'arn:aws:iam::${AWS::AccountId}:role/'
    - CFExecRole
    StackName: !Sub '${ServiceName}-stack-${Env}'
    ChangeSetName: !Sub '${ServiceName}-changeset-${Env}'
    TemplatePath: BuiltZip::template-packaged.yaml
    ParameterOverrides: !Sub '{ "Env": "${Env}", "SecurityGroup": "${SecurityGroup}", "Subnet1": "${Subnet1}", "Subnet2": "${Subnet2}"}'
    InputArtifacts:
    - Name: BuiltZip
    RunOrder: 1
    - Name: ExecuteChangeSetPublicAPI
    ActionTypeId:
    Category: Deploy
    Owner: AWS
    Provider: CloudFormation
    Version: 1
    Configuration:
    ActionMode: CHANGE_SET_EXECUTE
    RoleArn:
    !Join
    - ''
    - - !Sub 'arn:aws:iam::${AWS::AccountId}:role/'
    - CFExecRole
    StackName: !Sub '${ServiceName}-stack-${Env}'
    ChangeSetName: !Sub '${ServiceName}-changeset-${Env}'
    OutputArtifacts:
    - Name: !Sub '${ServiceName}${Env}ChangeSet'
    RunOrder: 2

API Project Sample Source Code

  • Create S3 bucket to store the .zip for each function (e.g. api-ap-east-1-build-stack-uat)

  • Now, you can develop your own API in Node.js:

    .
    ├── api-test
    │   ├── app.js
    │   ├── node_modules
    │   ├── package.json
    │   └── package-lock.json
    ├── buildspec.yaml
    └── template.yaml
    • buildspec.yaml
      version: 0.2

      phases:
      pre_build:
      commands:
      - n 14.17.3
      - cd api-test && npm install
      - echo "dependencies install for api-test completed `date`"
      - cd ../.
      build:
      commands:
      - echo "Starting build `date` in `pwd`"
      - aws cloudformation package --template-file template.yaml \
      --output-template-file template-packaged.yaml--s3-prefix api-package \
      --s3-bucket api-ap-east-1-build-stack-${ENV} --region ap-east-1

      artifacts:
      files:
      - template-packaged.yaml
      discard-paths: yes

      • template.yaml
    • template.yaml
      Transform: AWS::Serverless-2016-10-31
      Description: API SAM template

      # More info about Globals: https://github.com/awslabs/serverless-application-model/blob/master/docs/globals.rst

      # Don't change anything
      Globals:
      Function:
      Runtime: nodejs14.x
      Handler: app.lambdaHandler
      Timeout: 3

      # Don't change anything
      Parameters:
      Env:
      Type: String
      AllowedValues:
      - uat
      - preprod
      - prod
      - test
      Default: uat
      SecurityGroup:
      Type: String
      Subnet1:
      Type: String
      Subnet2:
      Type: String
      LambdaRole:
      Type: String
      Default: LambdaRole

      Resources:
      SampleFunction:
      Type: AWS::Serverless::Function
      Properties:
      Timeout: 15
      Handler: app.handler
      Layers:
      - !Sub 'arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:layer:oracle-linux-client-layer:1'
      CodeUri: api-test/
      Role:
      !Join
      - ''
      - - !Sub 'arn:aws:iam::${AWS::AccountId}:role/'
      - !Sub ${LambdaRole}
      Events:
      SampleFunction:
      Type: Api
      Properties:
      Path: /api-test
      Method: get
      Environment:
      Variables:
      Env: !Sub ${Env}
      VpcConfig:
      SecurityGroupIds:
      - !Sub ${SecurityGroup}
      SubnetIds:
      - !Sub ${Subnet1}
      - !Sub ${Subnet2}
      Outputs:
      SampleFunction:
      Description: "A sample function"
      Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/api-test"

  • Push the code to CodeCommit

    1
    git push origin release/dev
  • The Pipeline will be executed to:

    • Download the source code
    • Build the source code
    • Prepare the SAM template file
    • Deploy the API gateway

Troubleshooting

  • Returns the ID and status of each active change set for a stack.
    • Use AWS CLI
      $ aws cloudformation list-change-sets --stack-name stack-name --profile xxx --region ap-east-1

      {
      "Summaries": [
      {
      "StackId": "arn:aws:cloudformation:ap-east-1:account-id:stack/stack-name/d353b870-0a30-11ec-b48b-0e7bb5a4c8fc",
      "Status": "FAILED",
      "ChangeSetName": "change-set-name",
      "CreationTime": "2021-09-02T01:58:11.556Z",
      "StatusReason": "Transform AWS::Serverless-2016-10-31 failed with: Invalid Serverless Application Specification document. Number of errors found: 1. Resource with id [SampleFunction] is invalid. Type of property 'Events' is invalid.",
      "StackName": "stack-name",
      "ExecutionStatus": "UNAVAILABLE",
      "ChangeSetId": "arn:aws:cloudformation:ap-east-1:account-id:changeSet/change-set-name/xxxxxxx"
      }
      ]
      }
    • Use AWS Console
2021-08-30