DEV Community

Hai Tran
Hai Tran

Posted on

CI/CD Pipeline to Deploy Lambda with Latest ECR Image Tag Using SSM Parameter Store

CI/CD Pipeline for Lambda with ECR and SSM for updating tag

This note shows using SSM parameter in CI/CD for passing ECR image tag from CodeBuild to deployment stacks. So the latest ECR image is used in the latest deployed stack such as a lambda function. The default ecr tag is latest and this might cause CloudFormation think that there is not update after pushing an image to ecr. So, it is better to push image with a tag by build number, CODEBUILD_RESOLVED_SOURCE_VERSION, or Git SHA, etc. There are other solutions such as exported variables in CodeBuild, then overrideParameter in deployment stacks. Here SSM is an easy way.

  • CodeBuild to build a Docker image, tag, and push to an ecr repository
  • The tag written to SSM (system parameter store)
  • CodeBuild CDK synth application stack
  • CodeDeploy deploy the application stack, and the latest ecr tag is read from the SSM

Architecture

aws_devops drawio (1)

CodeBuild role to push ecr and put-paraemter to ssm

const role = new aws_iam.Role(
  this,
  'IamRoleForCodeBuildPushEcr',
  {
    assumedBy: new aws_iam.ServicePrincipal('codebuild.amazonaws.com')
  }
)

role.attachInlinePolicy(
  new aws_iam.Policy(
    this,
    "PushEcrPolicy", {
      statements: [
        new aws_iam.PolicyStatement({
          effect: aws_iam.Effect.ALLOW,
          actions: ['ecr:*'],
          resources: ['*']
        }),
        new aws_iam.PolicyStatement({
          effect: aws_iam.Effect.ALLOW,
          actions: ['ssm:*'],
          resources: ['*']
        })
      ]
    }
  )
)

Enter fullscreen mode Exit fullscreen mode

CodeBuild project

// codebuild project
    const codeBuild = new aws_codebuild.PipelineProject(
      this,
      'CodeBuildProject',
      {
        role: role,
        environmentVariables: {
          AWS_ACCOUNT_ID: {value: 'AWS_ACCOUNT_ID'}
        },
        environment: {
          buildImage: aws_codebuild.LinuxBuildImage.STANDARD_5_0,
          computeType: aws_codebuild.ComputeType.MEDIUM,
          privileged: true
        },
        buildSpec: aws_codebuild.BuildSpec.fromObject({
          version: '0.2',
          phases: {
            install: {
              commands: [
                'echo Logging in to Amazon ECR...'
              ]
            },
            // login in ecr
            pre_build: {
              commands: [
                'aws ecr get-login-password --region ap-southeast-1 | docker login --username AWS --password-stdin ${AWS_ACCOUNT_ID}.dkr.ecr.ap-southeast-1.amazonaws.com'
              ]
            },
            // build ecr image
            build: {
              commands: [
                'docker build -t  ecr-image-name:${CODEBUILD_RESOLVED_SOURCE_VERSION} ./lib/lambda/',
                'docker tag ecr-image-name:${CODEBUILD_RESOLVED_SOURCE_VERSION} ${AWS_ACCOUNT_ID}.dkr.ecr.ap-southeast-1.amazonaws.com/ecr-image-name:${CODEBUILD_RESOLVED_SOURCE_VERSION}'
              ]
            },
            // push ecr image
            post_build: {
              commands: [
                'export imageTag=${CODEBUILD_RESOLVED_SOURCE_VERSION}',
                'docker push ${AWS_ACCOUNT_ID}.dkr.ecr.ap-southeast-1.amazonaws.com/ecr-image-name:${CODEBUILD_RESOLVED_SOURCE_VERSION}',
                'echo ${CODEBUILD_RESOLVED_SOURCE_VERSION}',
                'aws ssm put-parameter --name FhrEcrImageTagDemo --type String --value ${CODEBUILD_RESOLVED_SOURCE_VERSION} --overwrite'
              ]
            }
          },
          env: {
            'exported-variables': [
              'imageTag'
            ]
          }
        })
      }
    )

Enter fullscreen mode Exit fullscreen mode

SSM parameters for CI/CD

create a ssm

aws ssm put-parameter --name 'parameterName' --description 'keep track ecr image tag' --value 'b05517a66933f6fde060efe2ecd78784767f6ce1' --type 'String'
Enter fullscreen mode Exit fullscreen mode

get a ssm

aws ssm get-parameter --name 'parameterName'
Enter fullscreen mode Exit fullscreen mode

update a ssm

aws ssm put-parameter --name 'parameterName' --type 'String' --value 'b05517a66933f6fde060efe2ecd78784767f6ce1' --overwrite
Enter fullscreen mode Exit fullscreen mode
aws ssm put-parameter --name parameterName --type String --value '0a95b18303e05f2de9315bbb385da173398b9661' --overwrite
Enter fullscreen mode Exit fullscreen mode

Entire pipeline

import { 
  aws_codebuild,
  aws_codecommit, 
  aws_codepipeline, 
  aws_codepipeline_actions, 
  aws_ecr, 
  aws_iam, 
  aws_lambda, 
  aws_s3, 
  aws_ssm, 
  Duration, 
  Stack, 
  StackProps } from 'aws-cdk-lib';
import { Construct } from 'constructs';

export class CodebuildPushEcrStack extends Stack {

  constructor(scope: Construct, id: string, props?: StackProps) {
    super(scope, id, props);

    // role and polices for codebuild to push ecr image
    const role = new aws_iam.Role(
      this,
      'IamRoleForCodeBuildPushEcr',
      {
        assumedBy: new aws_iam.ServicePrincipal('codebuild.amazonaws.com')
      }
    )

    role.attachInlinePolicy(
      new aws_iam.Policy(
        this, 
        "PushEcrPolicy", {
          statements: [
            new aws_iam.PolicyStatement({
              effect: aws_iam.Effect.ALLOW,
              actions: ['ecr:*'],
              resources: ['*']
            }),
            new aws_iam.PolicyStatement({
              effect: aws_iam.Effect.ALLOW,
              actions: ['ssm:*'],
              resources: ['*']
            })
          ]
        }
      )
    )

    // codecommit 
    const repository = aws_codecommit.Repository.fromRepositoryName(
      this, 
      'CodeCommitRepository',
      `codebuild-push-ecr-${this.account}`
    )

    // codepipeline artifact 
    const artifactBucket = aws_s3.Bucket.fromBucketName(
      this, 
      'ArtifactBucket',
      'fhr-codepipeline-artifact'
    )

    // artifact folders for source, codebuild 
    const sourceOutput = new aws_codepipeline.Artifact('SourceOutput')
    const codeBuildOutput = new aws_codepipeline.Artifact("CodeBuildOutput")
    const cdkBuildOutput = new aws_codepipeline.Artifact('CdkBuildOutput')


    // codebuild project 
    const codeBuild = new aws_codebuild.PipelineProject(
      this, 
      'CodeBuildProject',
      {
        role: role,
        environmentVariables: {
          AWS_ACCOUNT_ID: {value: 'AWS_ACCOUNT_ID'}
        },
        environment: {
          buildImage: aws_codebuild.LinuxBuildImage.STANDARD_5_0,
          computeType: aws_codebuild.ComputeType.MEDIUM,
          privileged: true
        },
        buildSpec: aws_codebuild.BuildSpec.fromObject({
          version: '0.2', 
          phases: {
            install: {
              commands: [
                'echo Logging in to Amazon ECR...'
              ]
            },
            // login in ecr 
            pre_build: {
              commands: [
                'aws ecr get-login-password --region ap-southeast-1 | docker login --username AWS --password-stdin ${AWS_ACCOUNT_ID}.dkr.ecr.ap-southeast-1.amazonaws.com'
              ]
            },
            // build ecr image 
            build: {
              commands: [
                'docker build -t  fhr-ecr-image:${CODEBUILD_RESOLVED_SOURCE_VERSION} ./lib/lambda/',
                'docker tag fhr-ecr-image:${CODEBUILD_RESOLVED_SOURCE_VERSION} ${AWS_ACCOUNT_ID}.dkr.ecr.ap-southeast-1.amazonaws.com/fhr-ecr-image:${CODEBUILD_RESOLVED_SOURCE_VERSION}'
              ]
            },
            // push ecr image 
            post_build: {
              commands: [
                'export imageTag=${CODEBUILD_RESOLVED_SOURCE_VERSION}',
                'docker push ${AWS_ACCOUNT_ID}.dkr.ecr.ap-southeast-1.amazonaws.com/fhr-ecr-image:${CODEBUILD_RESOLVED_SOURCE_VERSION}',
                'echo ${CODEBUILD_RESOLVED_SOURCE_VERSION}',
                'aws ssm put-parameter --name FhrEcrImageTagDemo --type String --value ${CODEBUILD_RESOLVED_SOURCE_VERSION} --overwrite'
              ]
            }
          }, 
          env: {
            'exported-variables': [
              'imageTag'
            ]
          }
        })
      }
    )

    //
    const buildAction = new aws_codepipeline_actions.CodeBuildAction({
      actionName: 'BuildEcrImage',
      project: codeBuild, 
      input: sourceOutput, 
      outputs: [codeBuildOutput]
    })


    // CodeBuild project for cdk build 
    const cdkBuild = new aws_codebuild.PipelineProject(
      this, 
      'CdkBuikd',
      {
        environment: {
          buildImage: aws_codebuild.LinuxBuildImage.STANDARD_5_0
        },
        buildSpec: aws_codebuild.BuildSpec.fromObject({
          version: '0.2',
          phases: {
            install: {
              commands: [
                'npm install'
              ]
            },
            pre_build: {
              commands: [
                'npm run build',
                'npm run cdk synth -- -o dist'
              ]
            }
          },
          artifacts: {
            'base-directory': 'dist',
            files: [
              '*template.json'
            ]
          }
        })
      }
    )

    // codepipeline 
    new aws_codepipeline.Pipeline(
      this, 
      'CodePiplineProject', 
      {
        artifactBucket: artifactBucket,
        stages: [
          {
            stageName: 'Source',
            actions: [
              new aws_codepipeline_actions.CodeCommitSourceAction({
                actionName: 'ConnectRepository',
                repository: repository, 
                output: sourceOutput
              })
            ]
          }, 
          {
            stageName: 'Build', 
            actions: [
              buildAction,
              new aws_codepipeline_actions.CodeBuildAction({
                actionName: 'BuildStack',
                project: cdkBuild,
                input: sourceOutput,
                outputs: [cdkBuildOutput]
              })
            ]
          },

          {
            stageName: 'Deploy',
            actions: [
              new aws_codepipeline_actions.CloudFormationCreateUpdateStackAction({
                actionName: 'DeployLambdaEcrDemo',
                templatePath: cdkBuildOutput.atPath('ApplicationStack.template.json'),
                stackName: 'ApplicationStackEcrTagDemo',
                parameterOverrides: {
                },
                adminPermissions: true
              })
            ]
          }
        ]
      }
    )

  }
}


export class ApplicationStack extends Stack {
  constructor(scope: Construct, id: string, props?: StackProps) {
    super(scope, id, props)

    // lambda fron ecr image uri
    const fn = new aws_lambda.Function(
      this,
      'LambdaFromEcrDemo',
      {
        runtime: aws_lambda.Runtime.FROM_IMAGE,
        handler: aws_lambda.Handler.FROM_IMAGE,
        timeout: Duration.seconds(90),
        environment: {
          'FHR_ENV': 'DEPLOY'
        },
        code: aws_lambda.Code.fromEcrImage(
          aws_ecr.Repository.fromRepositoryName(
            this,
            'EcrImageRepositoryDemo',
            'fhr-ecr-image',
          ),
          {
            tag: aws_ssm.StringParameter.valueForStringParameter(
              this, 
              'FhrEcrImageTagDemo'
            )
          }
        )
      }
    )
  }
}

export class RepositoryStack extends Stack {
  constructor(scope: Construct, id: string, props?: StackProps) {
    super(scope, id, props)

    // create a repository 
    new aws_codecommit.Repository(
      this, 
      "CodeBuildPushEcrRepository", 
      {
        repositoryName: `codebuild-push-ecr-${this.account}`
      }
    )

  }
}

Enter fullscreen mode Exit fullscreen mode

Top comments (0)