feat: simplify blog lambda deployment pipeline

This commit is contained in:
Daisuke Nakahara 2026-01-11 17:03:10 +09:00
parent 04237038fe
commit a59a8e6461
3 changed files with 166 additions and 23 deletions

View file

@ -6,6 +6,7 @@ phases:
- aws ecr get-login-password --region $AWS_DEFAULT_REGION | docker login --username AWS --password-stdin $ECR_REPOSITORY_URI - aws ecr get-login-password --region $AWS_DEFAULT_REGION | docker login --username AWS --password-stdin $ECR_REPOSITORY_URI
- IMAGE_TAG=$(date +%s) - IMAGE_TAG=$(date +%s)
- echo "Image tag will be ${IMAGE_TAG}" - echo "Image tag will be ${IMAGE_TAG}"
- if [ -z "${ECR_REPOSITORY_NAME:-}" ]; then ECR_REPOSITORY_NAME="${ECR_REPOSITORY_URI##*/}"; fi
build: build:
commands: commands:
- echo Build started on `date` - echo Build started on `date`
@ -18,3 +19,10 @@ phases:
- docker push $ECR_REPOSITORY_URI:$IMAGE_TAG - docker push $ECR_REPOSITORY_URI:$IMAGE_TAG
- docker push $ECR_REPOSITORY_URI:latest - docker push $ECR_REPOSITORY_URI:latest
- echo "Image pushed with tags ${IMAGE_TAG} and 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

View file

@ -12,6 +12,11 @@ Parameters:
Default: blog-lambda-source.zip Default: blog-lambda-source.zip
Description: S3 object key for source code archive 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: Resources:
SourceBucket: SourceBucket:
@ -59,8 +64,13 @@ Resources:
- ecr:UploadLayerPart - ecr:UploadLayerPart
- ecr:CompleteLayerUpload - ecr:CompleteLayerUpload
- ecr:PutImage - ecr:PutImage
- ecr:DescribeImages
Resource: Resource:
Fn::ImportValue: BlogDeployment-RepositoryArn Fn::ImportValue: BlogDeployment-RepositoryArn
- Effect: Allow
Action:
- lambda:UpdateFunctionCode
Resource: "*"
- Effect: Allow - Effect: Allow
Action: Action:
- s3:GetObject - s3:GetObject
@ -92,11 +102,138 @@ Resources:
Value: !Ref AWS::Region Value: !Ref AWS::Region
- Name: AWS_ACCOUNT_ID - Name: AWS_ACCOUNT_ID
Value: !Ref AWS::AccountId Value: !Ref AWS::AccountId
- Name: ECR_REPOSITORY_NAME
Value:
Fn::ImportValue: BlogDeployment-RepositoryName
Source: Source:
Type: CODEPIPELINE Type: CODEPIPELINE
BuildSpec: ci/buildspec.yml BuildSpec: ci/buildspec.yml
TimeoutInMinutes: 30 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: CodePipelineRole:
Type: AWS::IAM::Role Type: AWS::IAM::Role
Properties: Properties:
@ -135,6 +272,10 @@ Resources:
Resource: Resource:
- !Sub "arn:aws:codebuild:${AWS::Region}:${AWS::AccountId}:build/*" - !Sub "arn:aws:codebuild:${AWS::Region}:${AWS::AccountId}:build/*"
- !Sub "arn:aws:codebuild:${AWS::Region}:${AWS::AccountId}:project/*" - !Sub "arn:aws:codebuild:${AWS::Region}:${AWS::AccountId}:project/*"
- Effect: Allow
Action:
- lambda:InvokeFunction
Resource: !GetAtt PipelineDeployLambda.Arn
- Effect: Allow - Effect: Allow
Action: Action:
- codepipeline:PutApprovalResult - codepipeline:PutApprovalResult
@ -175,8 +316,22 @@ Resources:
Version: "1" Version: "1"
InputArtifacts: InputArtifacts:
- Name: SourceOutput - Name: SourceOutput
OutputArtifacts:
- Name: ImageDetails
Configuration: Configuration:
ProjectName: !Ref BlogLambdaBuildProject 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: S3SourceChangeRule:
Type: AWS::Events::Rule Type: AWS::Events::Rule
@ -225,9 +380,3 @@ Outputs:
Value: !Ref SourceBucket Value: !Ref SourceBucket
Export: Export:
Name: !Sub "${AWS::StackName}-SourceBucket" Name: !Sub "${AWS::StackName}-SourceBucket"
PipelineName:
Description: CodePipeline name
Value: !Ref BlogLambdaPipeline
Export:
Name: !Sub "${AWS::StackName}-PipelineName"

View file

@ -22,14 +22,6 @@ Parameters:
Default: main Default: main
Description: Git repository branch 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: Resources:
MyLambdaRole: MyLambdaRole:
@ -79,13 +71,7 @@ Resources:
Properties: Properties:
FunctionName: blog-deployment-webhook-handler FunctionName: blog-deployment-webhook-handler
PackageType: Image PackageType: Image
ImageUri: !If ImageUri: !Sub
- UseDigest
- !Sub
- "${RepoUri}@${Digest}"
- RepoUri: !ImportValue BlogDeployment-RepositoryUri
Digest: !Ref ImageDigest
- !Sub
- "${RepoUri}:latest" - "${RepoUri}:latest"
- RepoUri: !ImportValue BlogDeployment-RepositoryUri - RepoUri: !ImportValue BlogDeployment-RepositoryUri
Timeout: 300 Timeout: 300