LoginSignup
8

More than 1 year has passed since last update.

posted at

updated at

2020年に登場したサーバーレス開発を支えるサービスたちを使ってみる(CDK Pipelines / CodeArtifact / CodeGuru for Python)

この記事は Serverless Advent Calendar 2020 の20日目の投稿となります。
今回は、2020年に登場した主にAWS Lambdaを使用したサーバーレスアプリケーションの開発を支えるサービスについて、主にPythonを使用した開発で役立てそうなものを紹介いたします。

今回触れるサービス

  • CDK Pipelines
    • 2020年10月に発表
    • 2020年12月20日時点ではDeveloper Preview
    • AWS CDKを使ってCI/CDパイプラインとリソース群のデプロイを一緒に管理することができる
    • これを使うことでクロスアカウントのデプロイがめちゃくちゃ簡単に行える
  • AWS CodeArtifact
    • 2020年6月に発表
    • 公式リポジトリ(PyPiやnpm等)のキャッシュとして利用
    • 組織内で承認されたパッケージのみを使わせるなどの統制をきかせることができる
    • 独自パッケージを他の公式リポジトリのパッケージと同様に扱って管理することができる
  • Amazon CodeGuru
    • 2019年12月のre:Inventにて発表され、2020年7月にGAとなった
    • 機械学習を利用し、コードのレビューを自動的に行い推奨事項をしてきする(AWS CodeGuru Reviewer)
    • アプリケーションの実行においてコストの掛かっている行を特定したり、可視化をする(AWS CodeGuru Profiler)
    • 2020年12月のre:InventにてこれまでサポートしていたJavaに加え、Pythonもサポートした

CDK Pipelines

Lambda等を利用したサーバーレスアプリケーションも、既にプロダクションでバリバリ運用しているという方も多いはず。
VPC内で利用するEC2のようなリソースに比べて、LambdaやDynamoDBなどサーバーレスで利用するリソースは、
複数のステージ(開発、検証、本番など)のリソースが1つのアカウント内に入っているとなかなか運用しづらいということもあります。

そんなときに、作る構成が下図のようになってきます。
account.png

これまでもCDKなどを使用してこのようなアカウント構成でのデプロイは行えましたが、クロスアカウントでのデプロイはCI/CDを構築するだけで一苦労です。

そこで、このCDK Pipelineを活用しましょう

前提

アカウントの構成は下記の通り(カッコ内はアカウントID)
アカウントA(111111111111): CI/CDパイプラインやGitリポジトリを配置したアカウント
アカウントB(222222222222): 検証用アカウント
アカウントC(333333333333): 本番用アカウント

アカウントAに配置したCodeCommitリポジトリのmasterブランチにプッシュすると、自動的にLambda Functionが各環境にデプロイされる
デプロイはまずStg環境(アカウントB)に行われ、手動承認を経て、Prod環境(アカウントC)に行われる

準備

アカウントA〜CについてのAWS CLIのプロファイルを下記の通り登録しておく
アカウントA: manage
アカウントB: stg
アカウントC: prod

各アカウントのアカウント番号は事前に調べておくこと。
下記のコマンドでそれぞれ調べられます。

$ aws sts get-caller-identity --profile manage
{
    "UserId": "AROAIZMAXJNBKV2ORNQAG:botocore-session-1000000000",
    "Account": "111111111111", <--これ
    "Arn": "arn:aws:iam::111111111111:user/k1nakayama"
}

また、CodeCommitリポジトリをアカウントAに作成してください。(ここでは serverless2020 リポジトリを作成します)

CDK init

作成したリポジトリをクローン[^1]し、CDK initを行います。

$ git clone codecommit://cbd@serverless2020
$ cd serverless2020
$ cdk init --language=python

CDK Pipelinesによるデプロイのためのbootstrap

CDKを使用してデプロイする際の様々なアセット等をアップロードしておくためのS3バケットの作成や、デプロイに必要なIAMロールの作成を行う処理としてbootstrapという作業を行います。

bootstrapを行う前に、cdk.jsonに対し設定を追加します。

cdk.json
{
  "app": "python3 app.py",
  "context": {
    "@aws-cdk/core:enableStackNameDuplicates": "true",
    "aws-cdk:enableDiffNoFail": "true",
    "@aws-cdk/core:stackRelativeExports": "true",
    "@aws-cdk/aws-ecr-assets:dockerIgnoreSupport": true,
    "@aws-cdk/aws-secretsmanager:parseOwnedSecretName": true,
    "@aws-cdk/aws-kms:defaultKeyPolicies": true,
    "@aws-cdk/core:newStyleStackSynthesis": "true" <--この行を追加
  }
}

次にbootstrapコマンドを実行します。
この作業用コマンドは、それぞれのアカウントに対して行いますが、CI/CDを構築するアカウント(アカウントA)と、リソースを配置するアカウント(アカウントB、アカウントC)では異なります。

アカウントA:

$ cdk bootstrap --profile manage --cloudformation-execution-policies arn:aws:iam::aws:policy/AdministratorAccess aws://111111111111/ap-northeast-1

アカウントB/C:

$ cdk bootstrap --profile stg --trust 111111111111 --cloudformation-execution-policies arn:aws:iam::aws:policy/AdministratorAccess aws://222222222222/ap-northeast-1
$ cdk bootstrap --profile prod --trust 111111111111 --cloudformation-execution-policies arn:aws:iam::aws:policy/AdministratorAccess aws://333333333333/ap-northeast-1

CDKモジュールのインストール

CDK Pipelinesを使用してCI/CDを構築するためには下記のモジュールを読み込む必要があります。

aws-cdk.core
aws-cdk.aws-codepipeline
aws-cdk.aws-codepipeline-actions
aws-cdk.aws-codecommit
aws-cdk.pipelines

また、今回Lambda Functionをデプロイするため、下記のモジュールも使用します。

aws_cdk.aws_lambda
aws-cdk.aws_logs

これらについて、setup.pyのinstall_requiresに定義します。

setup.py
    install_requires=[
        "aws-cdk.core",
        "aws-cdk.aws-codepipeline",
        "aws-cdk.aws-codepipeline-actions",
        "aws-cdk.aws-codecommit",
        "aws-cdk.pipelines",
        "aws_cdk.aws_lambda",
        "aws-cdk.aws_logs"
    ],

上記を定義したら、インストールしましょう

$ pip install -r requirements.txt

Lambda Functionのデプロイ用CDK

ここについては、これまでのCDKのコードと変わりありませんので、ソースコードのみ貼っておきます。

serverless2020/serverless2020_stack.py
from aws_cdk import (
    core,
    aws_lambda as lambda_,
    aws_logs as logs,
)


class Serverless2020Stack(core.Stack):

    def __init__(self, scope: core.Construct, construct_id: str, **kwargs) -> None:
        super().__init__(scope, construct_id, **kwargs)

        hello_serverless_func = lambda_.Function(self, "HelloServerlessFunction",
                                              code=lambda_.Code.from_asset('functions/app'),
                                              handler="index.lambda_handler",
                                              runtime=lambda_.Runtime.PYTHON_3_8,
                                              tracing=lambda_.Tracing.ACTIVE,
                                              timeout=core.Duration.seconds(29),
                                              memory_size=128,
                                              )

        logs.LogGroup(self, 'HelloServerlessFunctionLogGroup',
                      log_group_name='/aws/lambda/' + hello_serverless_func.function_name,
                      retention=logs.RetentionDays.TWO_MONTHS
                      )

Pipeline用CDK

ある種ここがCDK Pipelinesの肝の部分ですが、詳しい説明は他のブログなどを参考にしていただくとして、
こちらも必要なソースコードを貼っておきます。

pipeline_lib/pipeline_stage.py
from aws_cdk import (
    core
)
from serverless2020.serverless2020_stack import Serverless2020Stack


class PipelineStage(core.Stage):

    def __init__(self, scope: core.Construct, id: str, **kwargs) -> None:
        super().__init__(scope, id, **kwargs)

        Serverless2020Stack(self, 'Serverless2020Stack')
pipeline_lib/pipeline_master_stack.py
from aws_cdk import (
    aws_codepipeline as codepipeline,
    aws_codepipeline_actions as actions,
    aws_codecommit as codecommit,
    pipelines,
    core
)
from pipeline_lib.pipeline_stage import PipelineStage


class PipelineMasterStack(core.Stack):

    def __init__(self, scope: core.Construct, id: str, **kwargs) -> None:
        super().__init__(scope, id, **kwargs)

        sourceArtifact = codepipeline.Artifact()
        cloudAssemblyArtifact = codepipeline.Artifact()

        pipeline = pipelines.CdkPipeline(self, 'Pipeline',
                                         pipeline_name="serverless2020-master-pipeline",
                                         cloud_assembly_artifact=cloudAssemblyArtifact,
                                         source_action=actions.CodeCommitSourceAction(
                                             action_name="CodeCommitgit",
                                             repository=codecommit.Repository.from_repository_name(
                                                 self, 'Repo', repository_name="serverless2020"),
                                             output=sourceArtifact,
                                             branch="master"
                                         ),
                                         synth_action=pipelines.SimpleSynthAction(
                                             synth_command="cdk synth",
                                             install_commands=[
                                                 "pip install --upgrade pip",
                                                 "npm i -g aws-cdk",
                                                 "pip install -r requirements.txt"
                                             ],
                                             source_artifact=sourceArtifact,
                                             cloud_assembly_artifact=cloudAssemblyArtifact,
                                             environment={
                                                 'privileged': True
                                             }
                                         )
                                         )

        stg = PipelineStage(self, "serverless2020-stg",
                            env={
                                'region': "ap-northeast-1", 'account': "222222222222"}
                            )

        stg_stage = pipeline.add_application_stage(stg)
        stg_stage.add_actions(actions.ManualApprovalAction(
            action_name="Approval",
            run_order=stg_stage.next_sequential_run_order()
        ))
        prod = PipelineStage(self, "serverless2020-prod",
                             env={
                                 'region': "ap-northeast-1", 'account': "333333333333"}
                             )
        pipeline.add_application_stage(app_stage=prod)
app.py
#!/usr/bin/env python3

from aws_cdk import core
from pipeline_lib.pipeline_master_stack import PipelineMasterStack

app = core.App()

PipelineMasterStack(app, "serverless2020-master-pipeline",
                    env={'region': "ap-northeast-1", 'account': "111111111111"}
                    )

app.synth()

デプロイ

これまでのコードを作成したら、いよいよデプロイです。
まずは、問題なくCDKコードが書けているかチェックしましょう

$ cdk synth

上記で無事にCloudFormationテンプレートが表示されればOKです。

続いてこれらのコードをgitリポジトリにコミットしプッシュしてください

$ git add .
$ git commit -m "init"
$ git push

それではお待ちかねのデプロイです。

cdk deploy --profile manage

完了した頃には既にCode Pipelineが進行中になっているはずです。
スクリーンショット 2020-12-18 1.31.48.png

こんなに簡単にクロスアカウントデプロイを構築することができました。
今後はmasterブランチにプッシュするのみで自動的にCI/CDが動きます。

AWS CodeArtifact

Lambdaを使用したサーバーレスアプリケーションの構築を進めていく上で、公式パブリックリポジトリのパッケージを使用することは多くあり、
頻繁にCI/CDパイプラインを使用してデプロイを繰り返していく上では、これらのキャッシュが効率的使えることが時間的な面でも、
統制を行う上でも必要になってくるでしょう。
また、サーバーレスアプリケーションを継続的に開発していく中では、自社のプロジェクトに共通して使用するライブラリを作成し共有することも
よく行われることかと思います。

これらのニーズに対して支援してくれるサービスが、AWS CodeArtifactというサービスになります。

CodeArtifactでは、組織全体(複数のAWSアカウントを束ねたグループをまとめた形)で1つのイメージで「ドメイン」を作成します。
このドメインとは別に各プロジェクトやステージ毎のポリシーの違いなどに合わせ、それぞれ「リポジトリ」を作成します。
パッケージへのアクセスはリポジトリ毎のエンドポイントに接続することで行います。
そのため、リポジトリ毎にアクセス権の管理やパッケージの承認等を管理することが可能です。
しかしながら、パッケージの各バージョンのキャッシュは「ドメイン」に対して溜まっていく形になります。

ドメイン・リポジトリの作成

それでは早速使っていきましょう。

今回は簡単に試すために、1つのアカウントに対して1つのドメインとリポジトリを作成して使ってみます。

AWSマネージメントコンソールにログイン後、AWS CodeArtifactにアクセスをします。
スクリーンショット 2020-12-19 0.30.36.png
「ドメインの作成」を選択しましょう

ドメイン名には記載の通り通常組織名などを入力します。
スクリーンショット 2020-12-19 0.29.28.png
上記だけでドメインが作成できました。

続いて「リポジトリを作成」を選択し、本番公開用のリポジトリを作成しましょう
スクリーンショット 2020-12-19 0.31.27.png

基本的に開発環境と本番環境では、別のパッケージ(バージョン)を使う可能性があるため、それぞれの環境向けのリポジトリを作成し使い分けるのがよいと思います。
プロジェクトで使用する可能性のある公式リポジトリをアップストリームリポジトリとして選択してください。
スクリーンショット 2020-12-19 0.32.28.png

「リポジトリを作成」を選択することで、リポジトリの作成が完了します。
スクリーンショット 2020-12-19 0.32.48.png

リポジトリへの接続

リポジトリへの接続方法は、各リポジトリの画面にて「接続手順の表示」ボタンを選択することでリポジトリ毎に表示できます。
スクリーンショット 2020-12-19 0.59.29.png

例えばpipの場合下記のようなコマンドで接続ができます。
※ CDK Pipelinesのところでの例で考えた場合、defaultプロファイルではないアカウントに対して接続する場合は下記のように --profileをつけてプロファイルを指定してください。

$ aws codeartifact login --tool pip --repository serverless2020-prod --domain serverless2020 --domain-owner 111111111111 --profile manage

上記のコマンドにより12時間有効な接続トークンが発行され、以降pipの接続先はCodeArtifact経由となります。

リポジトリ経由でインストール

試しに、CDK Pipelinesで作成したプロジェクトのpipのパッケージインストールを行ってみます。
※既に上記の作業ですべてのパッケージがインストール済みとなっているため、一度仮想環境を作り直し実行しています。

$ pip install -r requirements.txt

実行後、先程のAWSマネージメントコンソール上のリポジトリの画面を確認すると、下記のようにインストールされたパッケージの情報が閲覧できます。
これでこのリポジトリで使用されたパッケージのそれぞれのバージョンとそのアーティファクトのキャッシュが作成されました。

スクリーンショット 2020-12-19 1.13.19.png

独自パッケージの登録

続いて独自パッケージの登録を行ってみたいと思います。
※予め独自パッケージは作成してあり、dist/ にパッケージ化されていることを前提とします。

まずはPython用パッケージをアップロードするために、twineに接続をします。

$ aws codeartifact login --tool twine --repository serverless2020-prod --domain serverless2020 --profile manage

pipの時と同様に、aws codeartifact login --tool twine --repository repository_name --domain domain_nameで接続が行えます。

続けて、twineを使用してアップロードするため、twineのインストールと、アップロードを実行します。

$ pip install -U twine
$ twine upload -r codeartifact dist/*

上記で私は「cp-lambda-utils」という独自パッケージをアップロードしました。

スクリーンショット 2020-12-19 1.26.05.png

無事アップロードできていることが分かります。

独自パッケージのインストール

それでは試しに、独自パッケージをインストールしてみましょう

$ pip install -U cp_lambda_utils

上記でインストールが成功し、pip freezeを確認して無事にインストールがされていることが分かります。
スクリーンショット 2020-12-19 1.31.31.png

CDKで使用する

CDKでデプロイするLambdaで使用するパッケージについてCodeArtifact経由で取得するようにします。

まずは、CDK Pipelinesで作成したCodeBuildプロジェクトがCodeArtifactへのアクセスが行える権限を得る必要があるため、
setup.pyにaws-cdk.aws_iamのインストールを追記後、pipeline_master_stack.pyを下記のように書き換えてください。

pipeline_lib/pipeline_master_stack.py
from aws_cdk import (
    aws_codepipeline as codepipeline,
    aws_codepipeline_actions as actions,
    aws_codecommit as codecommit,
    aws_iam as iam,
    pipelines,
    core
)

~~~中略~~~
        pipeline = pipelines.CdkPipeline(self, 'Pipeline',
                                         pipeline_name="serverless2020-master-pipeline",
                                         cloud_assembly_artifact=cloudAssemblyArtifact,
                                         source_action=actions.CodeCommitSourceAction(
                                             action_name="CodeCommitgit",
                                             repository=codecommit.Repository.from_repository_name(
                                                 self, 'Repo', repository_name="serverless2020"),
                                             output=sourceArtifact,
                                             branch="master"
                                         ),
                                         synth_action=pipelines.SimpleSynthAction(
                                             synth_command="cdk synth",
                                             install_commands=[
                                                 "pip install --upgrade pip",
                                                 "npm i -g aws-cdk",
                                                 "pip install -r requirements.txt"
                                             ],
                                             source_artifact=sourceArtifact,
                                             cloud_assembly_artifact=cloudAssemblyArtifact,
                                             environment={
                                                 'privileged': True
                                             }
                                         ),
                                         role_policy_statements=[
                                             iam.PolicyStatement(
                                                 actions=['codeartifact:*'], resources=['*']),
                                             iam.PolicyStatement(
                                                 actions=['sts:GetServiceBearerToken'], resources=['*'])
                                         ]
                                         )

これでまずはプッシュします。
続いて、CodeBuild内でCodeArtifactへの接続を行う記述を追加します。

pipeline_lib/pipeline_master_stack.py
                                             install_commands=[
                                                 "pip install --upgrade pip",
                                                 "aws codeartifact login --tool pip --repository serverless2020-prod --domain serverless2020 --domain-owner 111111111111",
                                                 "npm i -g aws-cdk",
                                                 "pip install -r requirements.txt"
                                             ],

再度これでプッシュを行います。
この様に段階を分けないと、パーミッションがないためエラーとなります。

続いて、Lambda Functionのパッケージ対象ディレクトリ内にrequirements.txtを配置し、インストールするパッケージを記載した上で、
serverless2020_stack.pyを下記のように書き換えます。

serverless2020/serverless2020_stack.py
from aws_cdk import (
    core,
    aws_lambda as lambda_,
    aws_logs as logs,
)
from pathlib import Path

~~中略~~

        hello_serverless_func = lambda_.Function(self, "HelloServerlessFunction",
                                                 code=lambda_.Code.from_asset(
                                                     'functions/app',
                                                     asset_hash_type=core.AssetHashType.SOURCE,
                                                     bundling=core.BundlingOptions(
                                                         image=lambda_.Runtime.PYTHON_3_8.bundling_docker_image,
                                                         command=[
                                                             "bash",
                                                             "-c",
                                                             " && ".join(
                                                                 [
                                                                     "pip install -r requirements.txt -t /asset-output",
                                                                     "cp -au . /asset-output",
                                                                 ]
                                                             ),
                                                         ],
                                                         user="root:root",
                                                         volumes=[
                                                             core.DockerVolume(
                                                                 container_path="/root/.config/pip/pip.conf",
                                                                 host_path=f"{Path.home()}/.config/pip/pip.conf",
                                                             ),
                                                         ],
                                                     ),
                                                 ),
                                                 handler="index.lambda_handler",
                                                 runtime=lambda_.Runtime.PYTHON_3_8,
                                                 tracing=lambda_.Tracing.ACTIVE,
                                                 timeout=core.Duration.seconds(
                                                     29),
                                                 memory_size=128,
                                                 )

上記の通り、現状LambdaをパッケージするためのDockerコンテナに対してAWSのクレデンシャルを渡すことなどができないため、
少し回りくどい方法をとっています。
これについては、issue#10298などで対応が検討されているようです。

上記で無事にCodeArtifactを使用したパッケージのインストールを行い、デプロイが行えるようになりました。

Amazon CodeGuru

サーバーレスアプリケーションに限った話ではありませんが、クラウドを活用した開発を行うエンジニアはまだまだ少数であり、適切なコードを書けているかをレビューしてもらえる環境が整っていないエンジニアも少なくありません。
そんな中、Amazon CodeGuruを使用することで、様々なコード上の問題を機械学習により見つけてくれて指摘をしてくれるCodeGuru Reviewerを利用することができます。

Amazon CodeGuru Reviewerが指摘してくれる内容は下記の通りです。

Amazon CodeGuru Reviewer は、同時実行の問題、潜在的な競合状態、悪意があるもしくは精査されていない入力、認証情報などの機密データの不適切な処理、リソースリークなどチェックし、さらに並行コードの競合状態や衝突による動作障害も検出します。さらに、AWS および Java でのベストプラクティスの提案を行い、コード内で統合可能な重複部分を検出し保守性を向上させます。

CodeGuru Reviewer FAQより引用

Amazon CodeGuru ReviewerをCodeCommitリポジトリに関連付ける

早速使ってみましょう
まずは、関連付けを行います。

Amazon CodeGuruのページから「関連付けられたリポジトリ」を選択してください。
スクリーンショット 2020-12-19 23.01.42.png

続いて「リポジトリの関連付け」を選択します
スクリーンショット 2020-12-19 23.02.13.png

ソースプロバイダにAWS CodeCommitを選択し、リポジトリの場所に対象のリポジトリを選択します。「関連付け」を選択することで関連付けが行なえます。
スクリーンショット 2020-12-19 23.02.35.png

ちなみに、この関連付けは、CodeCommitリポジトリを作成する際に、「Enable Amazon CodeGuru Reviewer for Java and Python」の項目にチェックを入れることでも設定できます。
スクリーンショット 2020-12-19 23.03.15.png

関連付けがされると、リポジトリ一覧に表示されます。
スクリーンショット 2020-12-19 23.04.09.png

Pull Requestを出してみる

関連付けが行えたら、早速PRを出してみましょう。
コードを書き換えてプッシュした後、CodeCommitのコンソールより、「プルリクエストの作成」を選択してください。
スクリーンショット 2020-12-19 23.22.08.png

タイトルを入力し、「プルリクエストの作成」を選択してください。
スクリーンショット 2020-12-19 23.22.52.png

PRを行うと、アクティビティタブを見ると、早速Amazon CodeGuru Reviewerのジョブステータスの欄でステータスが進行中と表示されて、レビューが行われていることが分かります。
スクリーンショット 2020-12-19 23.24.13.png

レビュー結果

数分後ステータスが完了済みとなり、アクティビティ履歴の欄に指摘が表示されています。
スクリーンショット 2020-12-19 23.41.46.png

今回PRを出したソースコードはとても簡単なものであり、指摘事項は1個でした。
スクリーンショット 2020-12-19 23.42.44.png

個人的には、list_objectsは古いAPIであり、list_objects_v2を使ったほうが良いということや、一回のリクエストではオブジェクト一覧を取り切れていない可能性があることから、IsTruncatedを検証することなどの指摘、そもそもobj_list変数が何も使われないまま処理が終わっていること、os.environで存在するか分からないKeyを指定していることなどを指摘してくれるかなと思って期待しましたが、まだそのあたりの指摘が出てこないようです。

CodeGuru Reviewerは機械学習に基づいてレビューをしてくれます。
つまり学習し続けて成長するレビュアーなので、しっかり期待していたことを教えてあげましょう

下記のようにレビューに対して絵文字でリアクションを行い、返信を行うことで、サービスの改善に使用される場合があるようです。
スクリーンショット 2020-12-20 0.03.54.png

Amazon CodeGuru Profiler

Amazon CodeGuruには、これ以外にProfilerという機能があり、Lambdaなどで実際に実行された状況をプロファイリングし、最もコストが掛かっている行などを見つけるための手助けをしてくれます。

今回はプロファイリングするのに足りる実行を行えないことなどから、試しておりませんが、これらの設定もCDKのオプションに少し追加するだけで設定することが出来るようです。

まとめ

いかがだったでしょうか?
今回はPythonを使用したLambda Functionの開発を支えるサービスを紹介いたしました。
この1年間で登場したものだけでも、大きく進化しており、よりサーバーレス開発を行いやすくなったことがお分かりいただけたら嬉しいです。

私個人的には、最後のAmazon CodeGuru Reviewerの指摘事項がもっと洗練されてきて、ビジネスを行う上でも頼れる存在に成長することが期待したいです。
是非みなさんもフィードバックをどんどん行ってしまいましょう!

[^1]: 例ではgit-remote-codecommitを使用してCloneしています

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
What you can do with signing up
8