n-daisuke-blog-deployment-s.../infra/cfn/blog-lambda-pipeline.yaml

382 lines
13 KiB
YAML

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"