AWSサービスを利用した機械学習モデルのデプロイメント

- [English Version (英語版)](https://qiita.com/ps_ap/items/4f4228f45847f91af8f6)

機械学習モデルをGPUデバイスや組み込み機器などに実装することは、とても良い方法の一つです。
しかし、その一方で、非常にコストがかかります。

そこで、NVIDIA Jetson Nano Developer Kit B01を使って、物体検出モデル(Pytorch)と認識モデル(Tensorflow-Keras)を実行し、コスト削減を図りました。

しかし、Jetson nanoに展開するために最適化されたモデルを使用しても、メモリを多く消費するようで、同じデバイス上で両方のモデルを実行することは非常に困難でした。そこで、1つのモデルをAWSのクラウドシステムを利用して展開することにしました。

Jetson Nano上の検出モデルとAWS Cloud Services（AWS）上の認識モデルにより、システム全体の性能と速度を向上させることができました。

次の図は、組み込み機器とAWSへのモデル展開について説明しています:
![benchmark-report-jp.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1167939/0f2a363e-d2cd-caec-4d8b-251cb2f7de45.png)

今回は、AWS Lambda関数、docker、Cloudformationを利用しました。Lambda関数とdockerイメージは、AWS Cloudformation pipelineを使って自動的に作成されます。Cloudformation pipelineはyamlファイルを使って作成します。JSONフォーマットを利用することにもできますが、Cloudformationを扱う上でYAMLはJSONよりも[多くの利点](https://markrichman.com/yaml-for-aws-cloudformation/)があります。

![setup-jp.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1167939/48acdc0b-886d-38dd-9c10-c2ea276ea007.png)


##Dockerfileを作成する。

こちらがdockerfileの例です。

```Dockerfile
# ベースイメージにpython 3.8のLambdaランタイムを使用
FROM public.ecr.aws/lambda/python:3.8

# 変数の格納にはArgumentを使用
ARG AWS_CONTAINER_CREDENTIALS_RELATIVE_URI
ARG AWS_DEFAULT_REGION

# このapp.pyが機械学習の推論スクリプトになります。
COPY app.py   ./ 

# requirements.txtからpythonの要件をインストールします。
RUN python3.8 -m pip install -r requirements.txt

# CMDをラムダ関数ハンドラに設定する
CMD ["app.handler"]
```

このDockerfileは、次のセクションで説明するCloudformationパイプラインを実行すると、イメージが作成されます。
Dockerについてもっと知りたい方は、[こちらをご覧ください](https://qiita.com/sebastianrettig/items/f68b23c150dde419bb96)。

##Cloudformationを使ったパイプラインテンプレートの作成

AWSでは、各サービスについて非常に優れたドキュメントを用意しています。そこで、Lambda関数の作成には[AWS::Lambda::Function](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-lambda-function.html)のドキュメントを使用します。

パラメータテンプレートスニペットの設定は以下のように行います:

Note：この方法では、各オプションのデフォルト名を設定することができます。例えば、アプリケーション名、S3バケット名、dockerイメージ名、ラムダ関数名などです。パラメーターセクションで名前を設定しないと、パイプラインを実行すると、`長くて気持ち悪い名前`が作成されてしまいます。

```pipeline.yml
AWSTemplateFormatVersion: 2010-09-09
Parameters:
  ApplicationName:
    Default: project_name_abc
    Type: String
    Description: Enter the name of your application
  CodeBuildImage:
    Default: 'aws/codebuild/standard:2.0'
    Type: String
    Description: Name of CodeBuild image.
  SourceBranch:
    Default: 'master'
    Type: String
    Description: Name of the branch.
  Environment:
    Default: 'development'(or 'production') # We can change depending on the environment
    Type: String
    Description: The environment to build.
  LambdaFunction:
    Default: 'lambda-function-name'
    Type: String
    Description: The predict lambda name.
```
基本的なスニペットを作成したら、今度はパイプライン用の`Resources`を作成します。`Resources`を書く前に注意すべき点があります:

*1. リポジトリ名は、命名規則に従ってください。*
*2. 名前はアルファベットで始まり、アルファベットの小文字、数字、ハイフン、アンダースコア、スラッシュのみを含むことができます。*
*3. すべてのリソースにAWS::IAM:Roleを与えることが推奨されます。*
*4. [Amazon CloudWatch](https://aws.amazon.com/cloudwatch/features/)は、AWS、ハイブリッド、オンプレミスのアプリケーションやインフラリソースのデータや実用的なインサイトを提供する監視・管理サービスです。CloudWatchは、アプリケーション、インフラ、サービスといったスタック全体を監視し、アラーム、ログ、イベントデータを活用して自動化されたアクションを取ることができます。*
*5. [AWS::CodeBuild::Project](https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-resource-codebuild-project.html)リソースは、AWS CodeBuildがどのようにソースコードをビルドするかを設定します。*
*6. ルートレベルで [buildspec.yaml](https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-properties-codebuild-project-source.html#cfn-codebuild-project-source-buildspec)を用意します。*
*7. [AWS::CodePipeline::Pipeline](https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-resource-codepipeline-pipeline.html)リソースは、ソフトウェアの一部が変更されたときに、リリースプロセスがどのように機能するかを説明するパイプラインを作成します。*
*8. [AWS::Lambda::Function](https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-resource-lambda-function.html)では、Lambda関数を作成しています。実行時にLambda関数が使用できる最大メモリは10GBです。*

```pipeline.yml
Resources:
  SourceRepository:
    Type: 'AWS::CodeCommit::Repository'
    Properties:
      RepositoryName: !Sub '${ApplicationName}-${Environment}'
      RepositoryDescription: !Sub 'Source code for ${ApplicationName}'
  MyRepository:
    Type: AWS::ECR::Repository
    Properties:
      RepositoryName: !Sub '${ApplicationName}-${Environment}/repository_name'
  CodeBuildRole:
    Type: 'AWS::IAM::Role'
    Properties:
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Action:
              - 'sts:AssumeRole'
            Effect: Allow
            Principal:
              Service:
                - codebuild.amazonaws.com
  CodeBuildPolicy:
    Type: 'AWS::IAM::Policy'
    Properties:
      PolicyName: CodeBuildPolicy
      PolicyDocument:
        Version: 2012-10-17
        Statement:
          - Action:
              - 'logs:CreateLogGroup'
              - 'logs:CreateLogStream'
              - 'logs:PutLogEvents'
            Resource: '*'
            Effect: Allow
      Roles:
        - !Ref CodeBuildRole
  MyContainerBuild:
    Type: 'AWS::CodeBuild::Project'
    Properties:
      Artifacts:
        Type: CODEPIPELINE
      Environment:
        ComputeType: BUILD_GENERAL1_SMALL # ビルドには最大で3GBのメモリと2つのvCPUを使用します。
        Image: !Ref CodeBuildImage
        Type: LINUX_CONTAINER
        PrivilegedMode: True
        EnvironmentVariables:
          - Name: REPOSITORY_URI # 環境変数で設定できます（本記事では説明していません）。
            Value: !Sub '${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/${MyRepository}'
          - Name: ENVIRONMENT # Not covered in this article
            Value: !Sub '${Environment}'
      Name: !Sub '${ApplicationName}-${Environment}-MyContainer-Build'
      ServiceRole: !GetAtt
        - CodeBuildRole
        - Arn
      Source:
        Type: CODEPIPELINE
        BuildSpec: 'buildspec.yaml'
  AppPipeline:
    Type: 'AWS::CodePipeline::Pipeline'
    Properties:
      Name: !Sub '${ApplicationName}-${Environment}-Pipeline'
      ArtifactStore:
        Type: S3
        Location: !Ref ArtifactBucketStore
      RoleArn: !GetAtt
        - CodePipelineRole
        - Arn
      Stages:
        - Name: Source
          Actions:
            - ActionTypeId:
                Category: Source
                Owner: AWS
                Version: '1'
                Provider: CodeCommit
              Configuration:
                BranchName: !Ref SourceBranch
                RepositoryName: !GetAtt
                  - SourceRepository
                  - Name
              OutputArtifacts:
                - Name: SourceRepo
              RunOrder: 1
              Name: Source
        - Name: Build-Containers
          Actions:
            - InputArtifacts:
              - Name: SourceRepo
              Name: Build-My-Container
              ActionTypeId:
                Category: Build
                Owner: AWS
                Version: '1'
                Provider: CodeBuild
              Configuration:
                ProjectName: !Ref MyContainerBuild
              RunOrder: 1
  CodePipelineRole:
    Type: 'AWS::IAM::Role'
    Properties:
      Policies:
        - PolicyName: DefaultPolicy
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Action:
                  - 'codecommit:CancelUploadArchive'
                  - 'codecommit:GetBranch'
                  - 'codecommit:GetCommit'
                Resource: '*'
                Effect: Allow
              - Action:
                  - 'cloudwatch:*'
                  - 'iam:PassRole'
                Resource: '*'
                Effect: Allow
              - Action:
                  - 'lambda:InvokeFunction'
                  - 'lambda:ListFunctions'
                Resource: '*'
                Effect: Allow
              - Action:
                  - 'codebuild:BatchGetBuilds'
                  - 'codebuild:StartBuild'
                Resource: '*'
                Effect: Allow
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Action:
              - 'sts:AssumeRole'
            Effect: Allow
            Principal:
              Service:
                - codepipeline.amazonaws.com

  LambdaFunctionRole:
    Type: 'AWS::IAM::Role'
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
        - Effect: Allow
          Principal:
            Service:
              - lambda.amazonaws.com
          Action:
            - sts:AssumeRole
      Path: "/"
      Policies:
      - PolicyName: LambdaFunctionPolicy
        PolicyDocument:
          Version: '2012-10-17'
          Statement:
          - Action:
              - logs:CreateLogGroup
              - logs:CreateLogStream
              - logs:PutLogEvents
            Resource: '*'
            Effect: Allow
          - Action:
              - 'lambda:InvokeFunction'
              - 'lambda:InvokeAsync'
            Resource: '*'
            Effect: Allow

  LambdaFunction:
    Type: 'AWS::Lambda::Function'
    Properties:
      FunctionName: !Sub '${ApplicationName}-${MyLambda}-${Environment}'
      MemorySize: 4096 
      Timeout: 500
      Role: !GetAtt LambdaFunctionRole.Arn
      Code:
        ImageUri: !Sub '${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/${MyRepository}:latest'
      PackageType: Image
      Environment:
        Variables:
          S3BUCKETNAME: !Sub ${BucketName}
          S3BUCKETREGION: !Sub ${AWS::Region}

```

さて、CloudFormationのテンプレートを用意したら、いよいよスタックの作成です。
![create-stack.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1167939/311bfe22-9024-d705-f9c2-44e9d1994a22.png)

まず、「Upload a template file」オプションを選択し、`pipeline.yml` ファイルを選択します。

**スタック名の入力**:  *(スタック名には、アルファベット（A-Z、a-z）、数字（0-9）、ダッシュ（-）が使用できます。)*
また、パイプラインの作成時に設定したすべてのパラメータを確認することができます。

そして最後に、スタックを作成しましょう。
問題やエラーがなければ、dockerイメージが作成され、[Elastic Container Registry](https://docs.aws.amazon.com/ecr/) にアクセスできるようになります。

![docker-deploy.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1167939/168451e1-2a83-ddc6-cbcc-94478ac31d14.png)

Lambda関数が作成できたら、次にAWSコンソールから最新版のdockerイメージを手動でデプロイする必要があります。`latest`のDockerイメージを選択します。現在のところ、Cloudformation pipelineのビルドから自動的にイメージをデプロイすることはできません。

以上、ご紹介しました。