{
"cells": [
{
"cell_type": "markdown",
"id": "8c28a3a1",
"metadata": {},
"source": [
"# Deploy Serverless endpoint - Object Detection (YOLO-v3)\n",
"---\n",
"\n",
"***Note: 본 핸즈온에 사용된 추론 코드와 Dockerfile은 https://github.com/kts102121/lambda_container 에서 확인할 수 있으며, Lambda 추론에 대한 더 많은 예제들을 확인할 수 있습니다.***\n",
"\n",
"## Overview\n",
"\n",
"re:Invent 2020에 소개된 Lambda 컨테이너 기능 지원으로 기존 Lambda에서 수행하기 어려웠던 대용량 머신 모델에 대한 추론을 보다 수월하게 실행할 수 있게 되었습니다. Lambda 컨테이너 이미지를 Amazon ECR(Amazon Elastic Container Registry)에 푸시하였다면 Lambda 함수를 생성하여 직접 컨테이너 이미지를 배포하거나 SageMaker의 API 호출로 Serverless endpoint를 쉽게 배포할 수 있습니다.\n",
"\n",
"자세한 내용은 아래 링크를 참조해 주세요.\n",
"- AWS Lambda의 새로운 기능 — 컨테이너 이미지 지원: https://aws.amazon.com/ko/blogs/korea/new-for-aws-lambda-container-image-support/\n",
"- SageMaker Serverless Inference: https://sagemaker.readthedocs.io/en/stable/overview.html?highlight=lambdamodel#serverless-inference\n",
"- AWS Builders Online - AWS Lambda 컨테이너 이미지 서비스 활용하기 (김태수 SA): https://www.youtube.com/watch?v=tTg9Lp7Sqok"
]
},
{
"cell_type": "markdown",
"id": "67e65193",
"metadata": {},
"source": [
"
\n",
"\n",
"## 1. Preparation\n",
"---\n",
"\n",
"필요한 함수들을 정의하고 Serverless 추론에 필요한 권한을 아래와 같이 설정합니다. 참고로, 직접 Lambda Container 함수를 배포 시에는 ECR 리포지토리에 대한 억세스를 자동으로 생성해 줍니다.\n",
"\n",
"- SageMaker과 연결된 role 대해 ECR 억세스를 허용하는 policy 생성 및 연결\n",
"- SageMaker 노트북에서 lambda를 실행할 수 있는 role 생성\n",
"- Lambda 함수가 ECR private 리포지토리에 연결하는 억세스를 허용하는 policy 생성 및 연결 "
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "20d4da8b",
"metadata": {},
"outputs": [],
"source": [
"import json\n",
"import time\n",
"import boto3\n",
"import sagemaker\n",
"import base64\n",
"from sagemaker import get_execution_role\n",
"iam = boto3.client('iam')\n",
"ecr = boto3.client('ecr')\n",
"\n",
"sm_role_arn = get_execution_role()\n",
"sm_role_name = sm_role_arn.split('/')[-1]\n",
"boto_session = boto3.session.Session()\n",
"region = boto_session.region_name\n",
"account = boto3.client('sts').get_caller_identity()['Account']"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "cbed6a1e",
"metadata": {},
"outputs": [],
"source": [
"def attach_sm_ecr_policy(sm_role_name):\n",
" iam = boto3.client('iam')\n",
" try:\n",
" policy_response = iam.attach_role_policy(\n",
" RoleName=sm_role_name,\n",
" PolicyArn='arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryFullAccess'\n",
" )\n",
" return policy_response\n",
" except iam.exceptions.from_code('iam:AttachRolePolicy'):\n",
" print(f'[ERROR] SageMaker is not authorized to perform: iam:AttachRolePolicy on {sm_role_name}. Please add iam policy to this role') \n",
"\n",
"def attach_private_ecr_policy(repository_name, region, account):\n",
" ecr = boto3.client('ecr') \n",
" ecr_policy_json = {\n",
" \"Version\": \"2008-10-17\",\n",
" \"Statement\": [\n",
" {\n",
" \"Sid\": \"LambdaECRImageRetrievalPolicy\",\n",
" \"Effect\": \"Allow\",\n",
" \"Principal\": {\n",
" \"Service\": \"lambda.amazonaws.com\"\n",
" },\n",
" \"Action\": [\n",
" \"ecr:BatchGetImage\",\n",
" \"ecr:DeleteRepositoryPolicy\",\n",
" \"ecr:GetDownloadUrlForLayer\",\n",
" \"ecr:GetRepositoryPolicy\",\n",
" \"ecr:SetRepositoryPolicy\"\n",
" ],\n",
" \"Condition\": {\n",
" \"StringLike\": {\n",
" \"aws:sourceArn\": f\"arn:aws:lambda:{region}:{account}:function:*\"\n",
" }\n",
" }\n",
" }\n",
" ]\n",
" }\n",
" \n",
" try:\n",
" response = ecr.set_repository_policy(repositoryName=repository_name, policyText=json.dumps(ecr_policy_json))\n",
" return response\n",
" except ecr.exceptions.from_code('AccessDeniedException'):\n",
" print(f'Please add ECR policy on {sm_role_name}') \n",
" \n",
"\n",
"def create_lambda_role(role_name):\n",
" iam = boto3.client('iam')\n",
" lambda_policy = {\n",
" \"Version\": \"2012-10-17\",\n",
" \"Statement\": [\n",
" {\n",
" \"Effect\": \"Allow\",\n",
" \"Principal\": {\n",
" \"Service\": \"lambda.amazonaws.com\"\n",
" },\n",
" \"Action\": [ \n",
" \"sts:AssumeRole\"\n",
" ]\n",
" }\n",
" ]\n",
" } \n",
" \n",
" response = iam.create_role(\n",
" RoleName=role_name,\n",
" AssumeRolePolicyDocument=json.dumps(lambda_policy)\n",
" ) \n",
" print(response)\n",
"\n",
" policy_response = iam.attach_role_policy(\n",
" RoleName=role_name,\n",
" PolicyArn='arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole'\n",
" )\n",
" return response['Role']['Arn']\n",
" \n",
" \n",
"def delete_lambda_role(role_name):\n",
" iam = boto3.client('iam')\n",
" response = iam.detach_role_policy(\n",
" RoleName=role_name,\n",
" PolicyArn='arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole'\n",
" )\n",
" response = iam.delete_role(RoleName=role_name)"
]
},
{
"cell_type": "markdown",
"id": "3282d8b3",
"metadata": {},
"source": [
"### Attach SageMaker policy"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "bfd3204b",
"metadata": {},
"outputs": [],
"source": [
"attach_sm_ecr_policy(sm_role_name)"
]
},
{
"cell_type": "markdown",
"id": "48c6c789",
"metadata": {},
"source": [
"### Create Lambda Role for Serverless Inference"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "481f2aa2",
"metadata": {},
"outputs": [],
"source": [
"role_name = 'lambda-role-cv-hol'\n",
"repository_name = 'yolov3'\n",
"lambda_role_arn = create_lambda_role(role_name)\n",
"attach_private_ecr_policy(repository_name, region, account)\n",
"time.sleep(10)"
]
},
{
"cell_type": "markdown",
"id": "6fa769f2",
"metadata": {},
"source": [
"
\n",
"\n",
"## 2. Deploy & Test\n",
"---\n",
"\n",
"도커 이미지가 ECR에 푸시되고 적절한 Lambda Role이 생성되었다면, 단 두 줄의 코드로 `LambdaModel` 및 `LambdaPredictor` 리소스를 순차적으로 생성하여 Serverless Endpoint를 쉽게 생성할 수 있습니다. Serverless Endpoint는 내부적으로 Lambda Container 함수와 동일하므로 Endpoint에 대한 내역을 AWS Console 페이지의 AWS Lambda에서 확인할 수 있으며, 배포 전 Lambda 콘솔 창에서 직접 테스트를 수행할 수도 있습니다. "
]
},
{
"cell_type": "markdown",
"id": "192d391d",
"metadata": {},
"source": [
"### Deploy"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "44334f7b",
"metadata": {},
"outputs": [],
"source": [
"from sagemaker.serverless import LambdaModel\n",
"image_uri = f'{account}.dkr.ecr.{region}.amazonaws.com/{repository_name}:latest'\n",
"model = LambdaModel(image_uri=image_uri, role=lambda_role_arn)\n",
"predictor = model.deploy(\"my-lambda-function-cv\", timeout=50, memory_size=2048)"
]
},
{
"cell_type": "markdown",
"id": "02f6324c",
"metadata": {},
"source": [
"### Test\n",
"\n",
"Lambda 최초 호출 시 Cold start로 지연 시간이 발생하지만, 최초 호출 이후에는 warm 상태를 유지하기 때문에 빠르게 응답합니다. 물론 수 분 동안 호출이 되지 않거나 요청이 많아지면 cold 상태로 바뀐다는 점을 유의해 주세요."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "8ea4d02f",
"metadata": {},
"outputs": [],
"source": [
"def get_cv_prediction(img_path):\n",
" with open(img_path, 'rb') as fp:\n",
" bimage = fp.read()\n",
"\n",
" input_json = {\n",
" \"body\": \"{\\\"image\\\": \\\"\" + base64.b64encode(bimage).decode('utf-8') + \"\\\"}\"\n",
" }\n",
"\n",
" results = predictor.predict(input_json) \n",
" return json.loads(results['body'])\n",
"\n",
"\n",
"def load_image_from_base64(img_string, bgr2rgb=True):\n",
" # Decode the base64 string into an image\n",
" base_img = base64.b64decode(img_string)\n",
" npimg = np.frombuffer(base_img, dtype=np.uint8)\n",
" img = cv2.imdecode(npimg, 1)\n",
" if bgr2rgb:\n",
" img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)\n",
" height, width = img.shape[:2]\n",
" return img, height, width"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "8c681070",
"metadata": {},
"outputs": [],
"source": [
"import cv2\n",
"import numpy as np\n",
"import matplotlib.pyplot as plt\n",
"\n",
"img_path = 'sample_images/remote-control.jpeg'\n",
"result = get_cv_prediction(img_path)\n",
"img, height, width = load_image_from_base64(result['body'])\n",
"plt.figure(figsize = (12,12))\n",
"plt.imshow(img)"
]
},
{
"cell_type": "markdown",
"id": "9cd9e7e6",
"metadata": {},
"source": [
"최초 호출 이후에는 빠르게 추론 결과를 얻을 수 있습니다."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "5d487916",
"metadata": {},
"outputs": [],
"source": [
"img_path = 'sample_images/remote-control.jpeg'\n",
"result = get_cv_prediction(img_path)\n",
"img, height, width = load_image_from_base64(result['body'])\n",
"plt.figure(figsize = (12,12))\n",
"plt.imshow(img)"
]
},
{
"cell_type": "markdown",
"id": "1ed79cc3",
"metadata": {},
"source": [
"
\n",
"\n",
"## Clean up\n",
"---\n",
"\n",
"테스트를 완료했으면 `delete_model()` 및 `delete_predictor()` 메소드를 사용하여 `LambdaModel` 및 `LambdaPredictor` 리소스를 해제합니다."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "818af892",
"metadata": {},
"outputs": [],
"source": [
"model.delete_model()\n",
"predictor.delete_predictor()\n",
"delete_lambda_role(role_name)"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "conda_pytorch_latest_p37",
"language": "python",
"name": "conda_pytorch_latest_p37"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.7.12"
}
},
"nbformat": 4,
"nbformat_minor": 5
}