From a59a8e646115b97f23092811a52a9492258b18e7 Mon Sep 17 00:00:00 2001 From: Daisuke Date: Sun, 11 Jan 2026 17:03:10 +0900 Subject: [PATCH] feat: simplify blog lambda deployment pipeline --- ci/buildspec.yml | 8 ++ infra/cfn/blog-lambda-pipeline.yaml | 161 +++++++++++++++++++++++- infra/cfn/template-lambda-function.yaml | 20 +-- 3 files changed, 166 insertions(+), 23 deletions(-) diff --git a/ci/buildspec.yml b/ci/buildspec.yml index cf0dcdf..392a1e6 100644 --- a/ci/buildspec.yml +++ b/ci/buildspec.yml @@ -6,6 +6,7 @@ phases: - aws ecr get-login-password --region $AWS_DEFAULT_REGION | docker login --username AWS --password-stdin $ECR_REPOSITORY_URI - IMAGE_TAG=$(date +%s) - echo "Image tag will be ${IMAGE_TAG}" + - if [ -z "${ECR_REPOSITORY_NAME:-}" ]; then ECR_REPOSITORY_NAME="${ECR_REPOSITORY_URI##*/}"; fi build: commands: - echo Build started on `date` @@ -18,3 +19,10 @@ phases: - docker push $ECR_REPOSITORY_URI:$IMAGE_TAG - docker push $ECR_REPOSITORY_URI:latest - echo "Image pushed with tags ${IMAGE_TAG} and latest" + - IMAGE_DIGEST=$(aws ecr describe-images --repository-name "$ECR_REPOSITORY_NAME" --image-ids imageTag="$IMAGE_TAG" --query "imageDetails[0].imageDigest" --output text) + - echo "IMAGE_TAG=${IMAGE_TAG}" > image-details.env + - echo "IMAGE_DIGEST=${IMAGE_DIGEST}" >> image-details.env + +artifacts: + files: + - image-details.env diff --git a/infra/cfn/blog-lambda-pipeline.yaml b/infra/cfn/blog-lambda-pipeline.yaml index f06d212..d4f2767 100644 --- a/infra/cfn/blog-lambda-pipeline.yaml +++ b/infra/cfn/blog-lambda-pipeline.yaml @@ -12,6 +12,11 @@ Parameters: 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: @@ -59,8 +64,13 @@ Resources: - ecr:UploadLayerPart - ecr:CompleteLayerUpload - ecr:PutImage + - ecr:DescribeImages Resource: Fn::ImportValue: BlogDeployment-RepositoryArn + - Effect: Allow + Action: + - lambda:UpdateFunctionCode + Resource: "*" - Effect: Allow Action: - s3:GetObject @@ -92,11 +102,138 @@ Resources: 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: @@ -135,6 +272,10 @@ Resources: 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 @@ -175,8 +316,22 @@ Resources: 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 @@ -225,9 +380,3 @@ Outputs: Value: !Ref SourceBucket Export: Name: !Sub "${AWS::StackName}-SourceBucket" - - PipelineName: - Description: CodePipeline name - Value: !Ref BlogLambdaPipeline - Export: - Name: !Sub "${AWS::StackName}-PipelineName" diff --git a/infra/cfn/template-lambda-function.yaml b/infra/cfn/template-lambda-function.yaml index 280563a..4a2c412 100644 --- a/infra/cfn/template-lambda-function.yaml +++ b/infra/cfn/template-lambda-function.yaml @@ -22,14 +22,6 @@ Parameters: Default: main Description: Git repository branch - ImageDigest: - Type: String - Default: "" - Description: "ECR image digest (e.g., sha256:abc123...). If empty, uses 'latest' tag. Use digest for deterministic deployments." - -Conditions: - UseDigest: !Not [!Equals [!Ref ImageDigest, ""]] - Resources: MyLambdaRole: @@ -79,15 +71,9 @@ Resources: Properties: FunctionName: blog-deployment-webhook-handler PackageType: Image - ImageUri: !If - - UseDigest - - !Sub - - "${RepoUri}@${Digest}" - - RepoUri: !ImportValue BlogDeployment-RepositoryUri - Digest: !Ref ImageDigest - - !Sub - - "${RepoUri}:latest" - - RepoUri: !ImportValue BlogDeployment-RepositoryUri + ImageUri: !Sub + - "${RepoUri}:latest" + - RepoUri: !ImportValue BlogDeployment-RepositoryUri Timeout: 300 MemorySize: 512 Architectures: