{ "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 }