AWSTemplateFormatVersion: "2010-09-09" Description: S3 -> CodePipeline -> CodeBuild(ARM) -> ECR pipeline for Blog Lambda Parameters: SourceBucketName: Type: String Default: blog-lambda-source-bucket Description: S3 bucket name for source code SourceObjectKey: Type: String Default: blog-lambda-source.zip Description: S3 object key for source code archive LambdaFunctionName: Type: String Default: blog-deployment-webhook-handler Description: Lambda function name that receives updated images Resources: SourceBucket: Type: AWS::S3::Bucket Properties: BucketName: !Sub "${AWS::Region}-${AWS::AccountId}-${SourceBucketName}" Tags: - Key: Project Value: Blog-Deployment VersioningConfiguration: Status: Enabled NotificationConfiguration: EventBridgeConfiguration: EventBridgeEnabled: true CodeBuildRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Version: "2012-10-17" Statement: - Effect: Allow Principal: Service: codebuild.amazonaws.com Action: sts:AssumeRole Policies: - PolicyName: CodeBuildPolicy PolicyDocument: Version: "2012-10-17" Statement: - Effect: Allow Action: - logs:CreateLogGroup - logs:CreateLogStream - logs:PutLogEvents Resource: "*" - Effect: Allow Action: - ecr:GetAuthorizationToken Resource: "*" - Effect: Allow Action: - ecr:BatchCheckLayerAvailability - ecr:InitiateLayerUpload - ecr:UploadLayerPart - ecr:CompleteLayerUpload - ecr:PutImage - ecr:DescribeImages Resource: Fn::ImportValue: BlogDeployment-RepositoryArn - Effect: Allow Action: - lambda:UpdateFunctionCode Resource: "*" - Effect: Allow Action: - s3:GetObject - s3:PutObject - s3:ListBucket Resource: - !Sub "arn:aws:s3:::codebuild-${AWS::Region}-${AWS::AccountId}-input-bucket" - !Sub "arn:aws:s3:::codebuild-${AWS::Region}-${AWS::AccountId}-input-bucket/*" - !GetAtt SourceBucket.Arn - !Sub "${SourceBucket.Arn}/*" BlogLambdaBuildProject: Type: AWS::CodeBuild::Project Properties: Name: blog-lambda-build ServiceRole: !GetAtt CodeBuildRole.Arn Artifacts: Type: CODEPIPELINE Environment: Type: ARM_CONTAINER ComputeType: BUILD_GENERAL1_SMALL Image: aws/codebuild/amazonlinux2-aarch64-standard:3.0 PrivilegedMode: true EnvironmentVariables: - Name: ECR_REPOSITORY_URI Value: Fn::ImportValue: BlogDeployment-RepositoryUri - Name: AWS_DEFAULT_REGION Value: !Ref AWS::Region - Name: AWS_ACCOUNT_ID Value: !Ref AWS::AccountId - Name: ECR_REPOSITORY_NAME Value: Fn::ImportValue: BlogDeployment-RepositoryName Source: Type: CODEPIPELINE BuildSpec: ci/buildspec.yml TimeoutInMinutes: 30 PipelineDeployLambdaRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Principal: Service: lambda.amazonaws.com Action: sts:AssumeRole Policies: - PolicyName: PipelineDeployPermissions PolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Action: - logs:CreateLogGroup - logs:CreateLogStream - logs:PutLogEvents Resource: '*' - Effect: Allow Action: - s3:GetObject Resource: - !Sub "arn:aws:s3:::codebuild-${AWS::Region}-${AWS::AccountId}-input-bucket/*" - Effect: Allow Action: - lambda:UpdateFunctionCode Resource: !Sub "arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:function:${LambdaFunctionName}" - Effect: Allow Action: - codepipeline:PutJobSuccessResult - codepipeline:PutJobFailureResult Resource: '*' PipelineDeployLambda: Type: AWS::Lambda::Function Properties: FunctionName: blog-lambda-image-deployer Runtime: python3.13 Handler: index.handler Role: !GetAtt PipelineDeployLambdaRole.Arn Timeout: 60 MemorySize: 256 Environment: Variables: TARGET_FUNCTION_NAME: !Ref LambdaFunctionName REPOSITORY_URI: Fn::ImportValue: BlogDeployment-RepositoryUri Code: ZipFile: | import boto3 import os import zipfile import tempfile import logging logger = logging.getLogger() logger.setLevel(logging.INFO) codepipeline = boto3.client('codepipeline') lambda_client = boto3.client('lambda') def handler(event, context): job = event['CodePipeline.job'] job_id = job['id'] job_data = job['data'] temp_path = None try: temp_fd, temp_path = tempfile.mkstemp() os.close(temp_fd) _download_artifact(job_data, temp_path) metadata = _extract_metadata(temp_path) digest = metadata.get('IMAGE_DIGEST') if not digest: raise ValueError('IMAGE_DIGEST not found in artifact') image_uri = f"{os.environ['REPOSITORY_URI']}@{digest}" lambda_client.update_function_code( FunctionName=os.environ['TARGET_FUNCTION_NAME'], ImageUri=image_uri ) codepipeline.put_job_success_result(jobId=job_id) except Exception as exc: logger.exception('Lambda image update failed') codepipeline.put_job_failure_result( jobId=job_id, failureDetails={ 'type': 'JobFailed', 'message': str(exc) } ) raise finally: if temp_path and os.path.exists(temp_path): os.remove(temp_path) def _download_artifact(job_data, destination): artifact = job_data['inputArtifacts'][0] creds = job_data['artifactCredentials'] session = boto3.session.Session( aws_access_key_id=creds['accessKeyId'], aws_secret_access_key=creds['secretAccessKey'], aws_session_token=creds['sessionToken'], ) s3 = session.client('s3', region_name=os.environ['AWS_REGION']) location = artifact['location']['s3Location'] s3.download_file(location['bucketName'], location['objectKey'], destination) def _extract_metadata(archive_path): with zipfile.ZipFile(archive_path) as archive: target = next((n for n in archive.namelist() if n.endswith('image-details.env')), None) if not target: raise FileNotFoundError('image-details.env missing from artifact') with archive.open(target) as handle: content = handle.read().decode() result = {} for line in content.splitlines(): if '=' in line: key, value = line.split('=', 1) result[key.strip()] = value.strip() return result CodePipelineRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Version: "2012-10-17" Statement: - Effect: Allow Principal: Service: codepipeline.amazonaws.com Action: sts:AssumeRole Policies: - PolicyName: CodePipelinePolicy PolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Action: - s3:GetBucketAcl - s3:GetObjectTagging - s3:GetObjectVersionTagging - s3:GetObject - s3:GetObjectVersion - s3:PutObject - s3:ListBucket - s3:GetBucketLocation - s3:GetBucketVersioning Resource: - !Sub "arn:aws:s3:::codebuild-${AWS::Region}-${AWS::AccountId}-input-bucket" - !Sub "arn:aws:s3:::codebuild-${AWS::Region}-${AWS::AccountId}-input-bucket/*" - !GetAtt SourceBucket.Arn - !Sub "${SourceBucket.Arn}/*" - Effect: Allow Action: - codebuild:StartBuild - codebuild:BatchGetBuilds Resource: - !Sub "arn:aws:codebuild:${AWS::Region}:${AWS::AccountId}:build/*" - !Sub "arn:aws:codebuild:${AWS::Region}:${AWS::AccountId}:project/*" - Effect: Allow Action: - lambda:InvokeFunction Resource: !GetAtt PipelineDeployLambda.Arn - Effect: Allow Action: - codepipeline:PutApprovalResult - codepipeline:StartPipelineExecution Resource: !Sub "arn:aws:codepipeline:${AWS::Region}:${AWS::AccountId}:*" BlogLambdaPipeline: Type: AWS::CodePipeline::Pipeline Properties: Name: blog-lambda-pipeline PipelineType: V2 RoleArn: !GetAtt CodePipelineRole.Arn ArtifactStore: Type: S3 Location: !Sub "codebuild-${AWS::Region}-${AWS::AccountId}-input-bucket" Stages: - Name: Source Actions: - Name: S3Source ActionTypeId: Category: Source Owner: AWS Provider: S3 Version: "1" Configuration: S3Bucket: !Ref SourceBucket S3ObjectKey: !Ref SourceObjectKey PollForSourceChanges: false OutputArtifacts: - Name: SourceOutput - Name: Build Actions: - Name: BuildAndPushImage ActionTypeId: Category: Build Owner: AWS Provider: CodeBuild Version: "1" InputArtifacts: - Name: SourceOutput OutputArtifacts: - Name: ImageDetails Configuration: ProjectName: !Ref BlogLambdaBuildProject - Name: Deploy Actions: - Name: UpdateLambdaImage ActionTypeId: Category: Invoke Owner: AWS Provider: Lambda Version: "1" InputArtifacts: - Name: ImageDetails Configuration: FunctionName: !Ref PipelineDeployLambda S3SourceChangeRule: Type: AWS::Events::Rule Properties: Description: Trigger CodePipeline on S3 source update EventPattern: source: - aws.s3 detail-type: - Object Created detail: bucket: name: - !Ref SourceBucket object: key: - !Ref SourceObjectKey Targets: - Arn: !Sub "arn:aws:codepipeline:${AWS::Region}:${AWS::AccountId}:${BlogLambdaPipeline}" RoleArn: !GetAtt EventBridgeInvokePipelineRole.Arn Id: CodePipelineTarget EventBridgeInvokePipelineRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Principal: Service: events.amazonaws.com Action: sts:AssumeRole Policies: - PolicyName: AllowStartPipeline PolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Action: - codepipeline:StartPipelineExecution Resource: !Sub "arn:aws:codepipeline:${AWS::Region}:${AWS::AccountId}:${BlogLambdaPipeline}" Outputs: SourceBucketName: Description: S3 bucket for source code Value: !Ref SourceBucket Export: Name: !Sub "${AWS::StackName}-SourceBucket"