382 lines
13 KiB
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"
|