FastAPI AWS Lambda
CI/CD Cloudformation template
pipeline.yml
AWSTemplateFormatVersion: 2010-09-09
Description: The CloudFormation template for the CodePipeline.
Parameters:
ProjectName:
Type: String
Default: model-api
ENV:
Type: String
Default: dev
GithubUserName:
Type: String
Default: <user name>
GithubRepo:
Type: String
Default: <repo name>
Type: String
Default: <branch name>
GithubOAuthToken:
Type: String
Default: ghp_** <github user access token>
Resources:
S3Bucket:
Type: 'AWS::S3::Bucket'
Properties:
BucketName: !Join
- '-'
- - !Ref ProjectName
- !Ref ENV
ECRRepository:
Type: AWS::ECR::Repository
Properties:
RepositoryName: !Join
- '-'
- - !Ref ProjectName
- !Ref ENV
CodePipeLineExecutionRole:
Type: 'AWS::IAM::Role'
Properties:
AssumeRolePolicyDocument:
Statement:
- Effect: Allow
Principal:
Service: codepipeline.amazonaws.com
Action: 'sts:AssumeRole'
ManagedPolicyArns:
- 'arn:aws:iam::aws:policy/AdministratorAccess'
CodeBuildExecutionRole:
Type: 'AWS::IAM::Role'
Properties:
AssumeRolePolicyDocument:
Statement:
Effect: Allow
Principal:
Service: codebuild.amazonaws.com
Action: 'sts:AssumeRole'
ManagedPolicyArns:
- 'arn:aws:iam::aws:policy/AdministratorAccess'
CloudformationExecutionRole:
Type: 'AWS::IAM::Role'
Properties:
AssumeRolePolicyDocument:
Statement:
- Effect: Allow
Principal:
Service: cloudformation.amazonaws.com
Action: 'sts:AssumeRole'
ManagedPolicyArns:
- 'arn:aws:iam::aws:policy/AdministratorAccess'
# CodeBuildDockerCacheRole:
# Type: 'AWS::IAM::Role'
# Properties:
# AssumeRolePolicyDocument:
# Statement:
# - Effect: Allow
# Principal:
# Service: ecr.amazonaws.com
# Action: 'sts:AssumeRole'
# ManagedPolicyArns:
# - 'arn:aws:iam::aws:policy/AdministratorAccess'
BuildProject:
Type: 'AWS::CodeBuild::Project'
Properties:
Artifacts:
Type: CODEPIPELINE
Environment:
ComputeType: BUILD_GENERAL1_SMALL
Image: 'aws/codebuild/standard:5.0'
ImagePullCredentialsType: CODEBUILD
PrivilegedMode: true
Type: LINUX_CONTAINER
EnvironmentVariables:
- Name: ECR_REPOSITORY_URI
Value: !Join [ ".", [ !Ref "AWS::AccountId", "dkr.ecr", !Ref "AWS::Region", !Join [ "/", [ "amazonaws.com", !Ref "ECRRepository" ] ] ] ]
Name: !Join
- '-'
- - !Ref ProjectName
- BuildProject
- !Ref ENV
ServiceRole: !Ref CodeBuildExecutionRole
Source:
Type: CODEPIPELINE
BuildSpec: buildspec.yml
Cache:
Type: LOCAL
Modes:
- LOCAL_CUSTOM_CACHE
- LOCAL_DOCKER_LAYER_CACHE
- LOCAL_SOURCE_CACHE
CodePipeLine:
Type: 'AWS::CodePipeline::Pipeline'
DependsOn: S3Bucket
Properties:
ArtifactStore:
Location: !Join
- '-'
- - !Ref ProjectName
- !Ref ENV
Type: S3
Name: !Join
- '-'
- - !Ref ProjectName
- CodePipeLine
- !Ref ENV
RestartExecutionOnUpdate: false
RoleArn:
'Fn::GetAtt':
- CodePipeLineExecutionRole
- Arn
Stages:
- Name: Source
Actions:
- Name: Source
ActionTypeId:
Category: Source
Owner: ThirdParty
Provider: GitHub
Version: 1
Configuration:
Repo: !Ref GithubRepo
Branch: !Ref GithubBranch
Owner: !Ref GithubUserName
OAuthToken: !Ref GithubOAuthToken
RunOrder: 1
OutputArtifacts:
- Name: source-output-artifacts
- Name: Build
Actions:
- Name: Build
ActionTypeId:
Category: Build
Owner: AWS
Version: 1
Provider: CodeBuild
OutputArtifacts:
- Name: build-output-artifacts
InputArtifacts:
- Name: source-output-artifacts
Configuration:
ProjectName: !Ref BuildProject
RunOrder: 1
API Cloudformation template
template.yml
AWSTemplateFormatVersion: '2010-09-09'
Transform: 'AWS::Serverless-2016-10-31'
Description: An AWS Serverless Specification template for text classification model.
Globals:
Function:
Timeout: 30
MemorySize: 10000
Environment:
Variables:
MODEL_DIR: /mnt/ml/models/
NETWORK_DIR: /mnt/ml/network/
Parameters:
SrcBucket:
Type: String
Description: Name of S3 bucket which will have the new ML models
Default: text-classifier-api-dev-models
Resources:
MyS3Bucket:
Type: 'AWS::S3::Bucket'
Properties:
BucketName: !Ref SrcBucket
EfsLambdaVpc:
Type: AWS::EC2::VPC
Properties:
CidrBlock: "10.0.0.0/16"
InternetGateway:
Type: 'AWS::EC2::InternetGateway'
Properties:
Tags:
- Key: Name
Value: !Sub '10.0.0.0/16'
VPCGatewayAttachment:
Type: 'AWS::EC2::VPCGatewayAttachment'
Properties:
VpcId: !Ref EfsLambdaVpc
InternetGatewayId: !Ref InternetGateway
SubnetAPublic:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref EfsLambdaVpc
AvailabilityZone: !Select [ 0, !GetAZs '' ]
MapPublicIpOnLaunch: true
CidrBlock: "10.0.0.0/24"
SubnetAPrivate:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref EfsLambdaVpc
AvailabilityZone: !Select [ 1, !GetAZs '' ]
CidrBlock: "10.0.1.0/24"
RouteTableAPublic:
Type: 'AWS::EC2::RouteTable'
Properties:
VpcId: !Ref EfsLambdaVpc
RouteTableAPrivate:
Type: 'AWS::EC2::RouteTable'
Properties:
VpcId: !Ref EfsLambdaVpc
RouteTableAssociationAPublic:
Type: 'AWS::EC2::SubnetRouteTableAssociation'
Properties:
SubnetId: !Ref SubnetAPublic
RouteTableId: !Ref RouteTableAPublic
RouteTableAssociationAPrivate:
Type: 'AWS::EC2::SubnetRouteTableAssociation'
Properties:
SubnetId: !Ref SubnetAPrivate
RouteTableId: !Ref RouteTableAPrivate
RouteTablePublicAInternetRoute:
Type: 'AWS::EC2::Route'
DependsOn: VPCGatewayAttachment
Properties:
RouteTableId: !Ref RouteTableAPublic
DestinationCidrBlock: '0.0.0.0/0'
GatewayId: !Ref InternetGateway
EIPA:
Type: 'AWS::EC2::EIP'
Properties:
Domain: vpc
NatGatewayA:
Type: 'AWS::EC2::NatGateway'
Properties:
AllocationId: !GetAtt 'EIPA.AllocationId'
SubnetId: !Ref SubnetAPublic
RouteA:
Type: 'AWS::EC2::Route'
Properties:
RouteTableId: !Ref RouteTableAPrivate
DestinationCidrBlock: '0.0.0.0/0'
NatGatewayId: !Ref NatGatewayA
EfsLambdaSecurityGroup:
Type: 'AWS::EC2::SecurityGroup'
Properties:
GroupDescription: 'Security group for NAT Gateway Lambda'
VpcId: !Ref EfsLambdaVpc
SecurityGroupEgress:
- CidrIp: "0.0.0.0/0"
# FromPort: 0
# ToPort: 65535
IpProtocol: -1
SecurityGroupIngress:
- CidrIp: "0.0.0.0/0"
# FromPort: 0
# ToPort: 65535
IpProtocol: -1
EfsFileSystem:
Type: AWS::EFS::FileSystem
MountTargetA:
Type: AWS::EFS::MountTarget
Properties:
FileSystemId: !Ref EfsFileSystem
SubnetId: !Ref SubnetAPrivate
SecurityGroups:
- !Ref EfsLambdaSecurityGroup
AccessPoint:
Type: AWS::EFS::AccessPoint
Properties:
FileSystemId: !Ref EfsFileSystem
PosixUser:
Gid: "1000"
Uid: "1000"
RootDirectory:
Path: "/ml"
CreationInfo:
OwnerGid: "1000"
OwnerUid: "1000"
Permissions: "755"
MLModelUploadFunction:
Type: AWS::Serverless::Function
DependsOn:
- MountTargetA
Properties:
CodeUri: s3-efs/
Handler: app.lambda_handler
Runtime: python3.7
FunctionName: lambda-model-s3-efs
VpcConfig:
SecurityGroupIds:
- !Ref EfsLambdaSecurityGroup
SubnetIds:
- !Ref SubnetAPrivate
FileSystemConfigs:
- Arn: !GetAtt AccessPoint.Arn
LocalMountPath: /mnt/ml
Policies:
- S3CrudPolicy:
BucketName: !Ref SrcBucket
- EFSWriteAccessPolicy:
FileSystem: !Ref EfsFileSystem
AccessPoint: !Ref AccessPoint
Events:
UploadMLModelEvent:
Type: S3
Properties:
Bucket: !Ref MyS3Bucket
Events: s3:ObjectCreated:*
TextClassifierAPIFunction:
Type: 'AWS::Serverless::Function'
DependsOn:
- MountTargetA
Properties:
PackageType: Image
Description: ''
FunctionName: lambda-model-api
VpcConfig:
SecurityGroupIds:
- !Ref EfsLambdaSecurityGroup
SubnetIds:
- !Ref SubnetAPrivate
FileSystemConfigs:
- Arn: !GetAtt AccessPoint.Arn
LocalMountPath: /mnt/ml
Policies:
- S3CrudPolicy:
BucketName: !Ref SrcBucket
- EFSWriteAccessPolicy:
FileSystem: !Ref EfsFileSystem
AccessPoint: !Ref AccessPoint
Events:
Api1:
Type: Api
Properties:
Path: '/{proxy+}'
Method: ANY
RestApiId:
Ref: FastapiGateway
Api2:
Type: Api
Properties:
Path: /
Method: ANY
RestApiId:
Ref: FastapiGateway
Environment:
Variables:
STAGE: dev
Metadata:
Dockerfile: Dockerfile
DockerContext: ./app
DockerTag: latest
FastapiGateway:
Type: AWS::Serverless::Api
Properties:
StageName: dev
OpenApiVersion: '3.0.0'
Buildspec
buildspec.yml
version: 0.2
env:
variables:
CONTAINER_REPO_URL: <account-id>.dkr.ecr.<region>.amazonaws.com
CONTAINER_REPO_NAME: <reco-name>
REGION: <region>
TAG_NAME: latest
phases:
install:
commands:
- nohup /usr/local/bin/dockerd --host=unix:///var/run/docker.sock --host=tcp://127.0.0.1:2375 --storage-driver=overlay2 &
- timeout 15 sh -c "until docker info; do echo .; sleep 1; done"
pre_build:
commands:
- aws ecr get-login-password --region $REGION | docker login --username AWS --password-stdin $CONTAINER_REPO_URL
build:
commands:
- sam build --use-container
- sam deploy --no-confirm-changeset --no-fail-on-empty-changeset --stack-name sam-$CONTAINER_REPO_NAME --s3-bucket $CONTAINER_REPO_NAME --image-repository $CONTAINER_REPO_URL/$CONTAINER_REPO_NAME --capabilities CAPABILITY_IAM --region $REGION
S3-EFS Lambda
/s3-efs/app.py
import boto3
import os
model_dir = os.getenv('MODEL_DIR', '/mnt/ml/models/')
s3 = boto3.client('s3')
def lambda_handler(event, context):
raw_dir = os.path.join(model_dir, 'raw')
processed_dir = os.path.join(model_dir, 'processed')
os.makedirs(raw_dir, exist_ok=True)
os.makedirs(processed_dir, exist_ok=True)
bucket_name = event['Records'][0]['s3']['bucket']['name']
key = event['Records'][0]['s3']['object']['key']
save_path = os.path.join(model_dir, key)
print(save_path)
os.makedirs(os.path.dirname(save_path), exist_ok=True)
s3.download_file(bucket_name, key, save_path)
print("ML Model file downloaded!")
Dockerfile
/app/Dockerfile
FROM public.ecr.aws/lambda/python:3.8
RUN yum install -y openblas-serial\
gmp gmp-devel
COPY requirements.txt ./
RUN python3.8 -m pip install -r requirements.txt
RUN mkdir -p /mnt/ml
RUN mkdir -p ./app
COPY app.py ./app/
COPY src/ ./app/src/
COPY __init__.py ./app/
CMD ["app.app.handler"]