AWS
Lambda Practice
Andrii Chykharivskyi
Solution
Experience
Initially, both for the local and for the pipeline, we used a function that was hosted on AWS.
Solution
Experience
Over time, we ran into a major problems with this approach:
- depending on the Internet connection
- limited invoking count for free tier
Solution
Experience
What do we need?
- should work locally
- should work with AWS S3 (Minio locally)
Creating
Creating
import os
from src import s3_client
from PyPDF3 import PdfFileReader, PdfFileWriter
def handler(event, context):
input_s3_bucket_name = event['input_s3_bucket_name']
input_file_token = event['input_file_token']
input_file_path = event['input_file_path']
aws_bucket = s3_client.get_client().Bucket(input_s3_bucket_name)
local_directory = '/tmp' + os.sep + input_file_token
os.makedirs(local_directory, exist_ok=True)
local_path = local_directory + os.sep + 'input.pdf'
aws_bucket.download_file(input_file_path, local_path)
local_input_pdf = PdfFileReader(open(local_path, "rb"))
return {
"statusCode": 200,
"pagesCount": local_input_pdf.getNumPages(),
"generatedFilePath": s3_output_path
}
Get PDF file pages count
Creating
FROM public.ecr.aws/lambda/python:3.8
# Copy function code
COPY src/app.py ${LAMBDA_TASK_ROOT}
# Install the function's dependencies using file requirements.txt
# from your project folder.
COPY requirements.txt .
RUN pip3 install -r requirements.txt --target "${LAMBDA_TASK_ROOT}"
ADD . .
ARG AWS_ACCESS_KEY_ID
ARG AWS_SECRET_ACCESS_KEY
ARG AWS_DEFAULT_REGION
ARG AWS_S3_ENDPOINT_URL
ENV AWS_ACCESS_KEY_ID ${AWS_ACCESS_KEY_ID}
ENV AWS_SECRET_ACCESS_KEY ${AWS_SECRET_ACCESS_KEY}
ENV AWS_DEFAULT_REGION ${AWS_DEFAULT_REGION}
ENV AWS_S3_ENDPOINT_URL ${AWS_S3_ENDPOINT_URL}
# Set the CMD to your handler (could also be done as a parameter override outside of the Dockerfile)
CMD [ "app.handler" ]
File Dockerfile
Creating
version: '3.7'
services:
pdf-pages:
build:
context: ./
dockerfile: Dockerfile
restart: unless-stopped
environment:
AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID:-tenantcloud}
AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY:-tenantcloud}
AWS_DEFAULT_REGION: ${AWS_DEFAULT_REGION:-us-east-1}
AWS_S3_ENDPOINT_URL: ${AWS_S3_ENDPOINT_URL}
volumes:
- ./src:/var/task/src
ports:
- "${DOCKER_PDF_PAGES_PORT:-3000}:8080"
networks:
home:
networks:
home:
name: "${COMPOSE_PROJECT_NAME:-tc-lambda}_network"
File docker-compose.yml
Creating
networks:
home:
name: "${COMPOSE_PROJECT_NAME:-tc-lambda}_network"
File docker-compose.yml
If we have Minio S3 deployed inside this network, it will be available inside the container of our function
Creating
import os
import boto3
s3_endpoint_url = os.getenv('AWS_S3_ENDPOINT_URL')
def get_client():
if s3_endpoint_url:
return boto3.resource('s3',
endpoint_url=s3_endpoint_url if s3_endpoint_url else None,
config=boto3.session.Config(signature_version='s3v4'),
verify=False
)
return boto3.resource('s3')
Updated S3 client, that supports Minio
Creating
version: '3.7'
services:
minio:
image: ${REGISTRY:-registry.tenants.co}/minio:RELEASE.2020-10-28T08-16-50Z-48-ge773e06e5
restart: unless-stopped
entrypoint: sh
command: -c "minio server --address :443 --certs-dir /root/.minio /export"
environment:
MINIO_ACCESS_KEY: ${AWS_ACCESS_KEY_ID:-tenantcloud}
MINIO_SECRET_KEY: ${AWS_SECRET_ACCESS_KEY:-tenantcloud}
volumes:
- ${DOCKER_MINIO_KEY:-./docker/ssl/tc.loc.key}:/root/.minio/private.key
- ${DOCKER_MINIO_CRT:-./docker/ssl/tc.loc.crt}:/root/.minio/public.crt
networks:
home:
aliases:
- minio.${HOST}
networks:
home:
name: "${COMPOSE_PROJECT_NAME:-tc-lambda}_network"
File docker-compose.minio.yml
Creating
Invoking Docker container function after build
curl -XPOST "http://localhost:3000/2015-03-31/functions/function/invocations" -d '{
"input_s3_bucket_name": "bucketname",
"input_file_token": "randomstring",
"input_file_path": “tmp/original.pdf",
}'
http://localhost:3000/2015-03-31/functions/function/invocations
Port from docker-compose.yml
Creating
S3 dependency
Previously, we used a separate bucket for each function.
Disadvantages of this approach:
- new function for application that use own bucket
Creating
S3 dependency
CI/CD
Build. File .gitlab.ci.yml
image: ${REGISTRY}/docker-pipeline:19.03.15-dind
variables:
FF_USE_FASTZIP: 1
FF_SCRIPT_SECTIONS: 1
COMPOSE_DOCKER_CLI_BUILD: 1
DOCKER_BUILDKIT: 1
DOCKER_CLIENT_TIMEOUT: 300
COMPOSE_HTTP_TIMEOUT: 300
COMPOSE_FILE: "docker-compose.yml:docker-compose.minio.yml"
REPOSITORY_NAME: $CI_PROJECT_NAME
SOURCE_BRANCH_NAME: $CI_COMMIT_BRANCH
SLACK_TOKEN: $DASHBOARD_SLACK_TOKEN
COMPOSE_PROJECT_NAME: ${CI_JOB_ID}
GIT_CLONE_PATH: $CI_BUILDS_DIR/$CI_PROJECT_NAME/$CI_JOB_ID
SLACK_CHANNEL: "#updates"
stages:
- build
- deploy
CI/CD
Build. File .gitlab.ci.yml
Build container:
<<: *stage_build
tags: ["dind_pipeline1"]
extends:
- .dind_service
script:
- sh/build.sh
rules:
- if: $CI_PIPELINE_SOURCE == 'push' && ($CI_COMMIT_BRANCH == 'master')
when: on_success
CI/CD
Build. File sh/build.sh
function getLatestTag() {
# Get your latest version and increment current version
}
function ecrLogin() {
#Login to AWS ECR
}
# Login to Production account
ecrLogin "$AWS_ACCESS_KEY_ID" \
"$AWS_SECRET_ACCESS_KEY" \
"$AWS_DEFAULT_REGION" \
"$AWS_ACCOUNT_ID"
function pushContainer() {
AWS_ACCOUNT_ID=$1
docker push "${1}.dkr.ecr.us-west-2.amazonaws.com/$REPOSITORY_NAME:${NEW_VERSION}"
}
echo "Building containers..."
docker build \
--tag "$AWS_ACCOUNT_ID".dkr.ecr."$AWS_DEFAULT_REGION".amazonaws.com/"$REPOSITORY_NAME":"${NEW_VERSION}" .
echo "Pushing container to ECR..."
pushContainer "$AWS_ACCOUNT_ID"
CI/CD
Deploy. File .gitlab-ci.yml
Deploy to production:
<<: *stage_deploy
tags: ["dind_pipeline1"]
needs:
- job: Build container
extends:
- .dind_service
script:
- sh/deploy.sh
after_script:
- |
if [ $CI_JOB_STATUS == 'success' ]; then
echo "$SLACK_TOKEN" > /usr/local/bin/.slack
slack chat send --color good ":bell: $REPOSITORY_NAME function updated" "$SLACK_CHANNEL"
fi
rules:
- if: $CI_PIPELINE_SOURCE == 'push' && ($CI_COMMIT_BRANCH == 'master')
when: manual
CI/CD
Deploy. File sh/deploy.sh
#!/bin/bash
# shellcheck disable=SC2034,SC2001
set -e
export AWS_ACCESS_KEY_ID=$AWS_ACCESS_KEY_ID
export AWS_SECRET_ACCESS_KEY=$AWS_SECRET_ACCESS_KEY
export AWS_DEFAULT_REGION=$AWS_DEFAULT_REGION
export AWS_ACCOUNT_ID=$AWS_ACCOUNT_ID
# Update lambda function
echo "Update lambda function..."
aws lambda update-function-code \
--function-name "$REPOSITORY_NAME" \
--image-uri "$AWS_ACCOUNT_ID".dkr.ecr."$AWS_DEFAULT_REGION".amazonaws.com/"$REPOSITORY_NAME""
CI/CD
Versioning
Lambda allows publish one or more immutable versions for individual Lambda functions. Each function version has a unique ARN.
Versioning
Where to find?
Versioning
Lambda allows provide a description for published version.
The Function consumers can continue using a previous version without any disruption.
Advantages:
Versioning
ARN - Amazon Resource Name, unique identifier for resources in the AWS Cloud.
arn:aws:lambda:us-east-1:465951521245:function:print-prime-numbers:1
version
name
region
resource name
When the version is included, it`s called qualified ARN. When the version is omitted is said to be unqualified ARN.
ARN
Versioning
- only incremented value (bad for hotfixes)
- can`t split routing traffic to different versions
Disadvantages:
Versioning
Don`t forget that published function is fully immutable(including configuration)
Reminder.
Versioning
Aliases.
- each alias points to a certain function version
- can be random string or random numeric
- cannot point to another alias, only to a function version
- useful in routing traffic to new versions after proper testing
Versioning
Aliases.
Versioning
Realization.
# Update lambda function
echo "Update lambda function..."
aws lambda update-function-code \
--function-name "$REPOSITORY_NAME" \
--image-uri "$AWS_ACCOUNT_ID".dkr.ecr."$AWS_DEFAULT_REGION".amazonaws.com/"$REPOSITORY_NAME":"${CURRENT_VERSION}"
Versioning
Realization. Blocking.
# Waiting for successfully deployment
echo "Waiting for successfully update lambda function..."
aws lambda wait function-updated --function-name "$REPOSITORY_NAME"
You cannot publish new version when function updating status "in progress"
Versioning
Realization. Publishing.
# Publish new lambda function version
echo "Publish new lambda function version..."
aws lambda publish-version \
--function-name "$REPOSITORY_NAME" \
--description "${CURRENT_VERSION}"
# Create new alias
echo "Create new lambda function alias..."
FUNCTION_VERSION=$(echo "$CURRENT_VERSION" | sed 's/^.//')
aws lambda create-alias \
--function-name "$REPOSITORY_NAME" \
--description "$CURRENT_VERSION" \
--function-version "$FUNCTION_VERSION" \
--name "$CURRENT_VERSION"
Versioning
Traffic shifting
Versioning
Traffic shifting
Pluses:
- easy implementing Rolling or Canary deployment strategy (current and new versions can coexist, each receiving traffic)
- development team can identify any possible issues before active using
Credits to Maksym Bilozub and Viktor Fedkiv
Andrii Chykharivskyi
AWS Lambda Practice
By TenantCloud
AWS Lambda Practice
AWS Lambda, serverless architecture. Function-as-a-Service.
- 229