Serverless Dashboardを使って爆速でCICD環境をセットアップ #pytest

Serverless Framework DashboardのCICD機能を使ってデプロイパイプラインを構築する方法をご紹介します。
2020.09.28

Serverless Frameworkのダッシュボードがあるのを最近知って使ってみました。
かなり使い心地が良くて画面上からサクッとCICD環境を作成できたので、この記事で手順を残しておきます。

今回はデプロイ先はAWSでブランチ毎にAWSアカウントを切り替える、ランタイムはPythonでpytestをデプロイ前に実行する、という構成にしていきます。

環境

  • Serverless Framework
    • Framework Core: 2.2.0
    • Plugin: 4.0.4
    • SDK: 2.3.2
    • Components: 3.1.4
  • Python: 3.8.5
  • Pipenv: 2020.8.13
  • pytest: 6.0.2
  • moto: 1.3.16

セットアップ

serverlessが入っていない場合はインストール

$ npm i -g serverless

プロジェクトを用意

$ mkdir sample-sls-cicd
$ cd sample-sls-cicd

ここではpipenvを使ってPython環境をセットアップします。(Pythonの環境構築等の説明は割愛します)

$ pipenv --python 3.8.5
$ pipenv shell
$ pipenv install -d autopep8 pylint pytest moto

Serverless Frameworkにサインアップ

Serverless Dashboardにサインアップします。
CICDはProプランの機能になりますが、最初は無料利用枠で試せます。
アカウントはGithub, Gmail, Eメールのいずれかで登録できます。

(任意)orgを作成

Serverless Frameworkではorgという単位でプロジェクトを管理できます。1つのorgの中に複数のappを作成して デフォルトでユーザー名と同一のorgが作成されているので任意のorgにアプリケーションを所属させたい場合はorgを事前に作成してください。

CLIからログイン

アカウントを作成したらCLIを認証します。

プロジェクトルートに serverless.yml を用意します。

service:
  name: sample-sls-cicd

provider:
  name: aws
  region: ap-northeast-1

一旦最低限の記述でCLIから認証します。

$ sls login
Serverless: Logging you in via your default browser...
Serverless: You sucessfully logged in to Serverless.

ブラウザが開くので、作成したアカウントでログインできていることを確認します。

serverlessコマンドを実行して、orgに対してアプリケーションを追加します。

$ sls
You can monitor, troubleshoot, and test your new service with a free Serverless account.

Serverless: Would you like to enable this? Yes

Serverless: What org do you want to add this to? experiment (orgを選択)
Serverless: What do you want to name this application? sample-sls-cicd (任意のアプリ名を入力)
Serverless: What deployment profile do you want to use? default (デプロイ用プロファイルを選択)

Your project is setup for monitoring, troubleshooting and testing

Deploy your project and monitor, troubleshoot and test it:
- Run “serverless deploy” to deploy your service.
- Run “serverless dashboard” to view the dashboard.

orgには、先程作成したorg名もしくはデフォルトのorg名を入力してください。appは任意のアプリ名でOKです。

ダッシュボードを開くとアプリケーションが追加されています。

Lambda + API Gateway + DynamoDBを作成

今回はAPI Gateway + Lambda + DynamoDBのシンプルなAPIを1本だけ用意しました。

org: experiment
app: sample-sls-cicd

service:
  name: sample-sls-cicd

provider:
  name: aws
  region: ap-northeast-1
  stage: ${opt:stage, self:custom.defaultStage}
  profile: ${self:custom.profiles.${opt:stage, self:custom.defaultStage}}
  runtime: python3.8
  stackName: sample-sls-cicd
  apiName: sample-sls-cicd
  logRetentionInDays: 7
  versionFunctions: false
  iamRoleStatements:
    - Effect: Allow
      Action:
        - logs:CreateLogGroup
        - logs:CreateLogStream
        - logs:PutLogEvents
        - lambda:GetLayerVersion
        - dynamodb:DescribeTable
        - dynamodb:Query
        - dynamodb:Scan
        - dynamodb:GetItem
        - dynamodb:PutItem
        - dynamodb:UpdateItem
        - dynamodb:DeleteItem
      Resource: "*"
  environment:
    DEFAULT_DATA_LIMIT: "20"
    ENV: ${self:custom.environments.ENV}

custom:
  defaultStage: dev
  profiles:
    dev: dev-profile
    stg: stg-profile
    prd: prd-profile
  pythonRequirements:
    usePipenv: true
    layer: true
  environments: ${file(./config/config.${opt:stage, self:custom.defaultStage}.yml)}

package:
  exclude:
    - .git/**
    - .venv/**
    - .pytest_cache/**
    - config/**
    - tests
    - README.md

functions:
  ListItems:
    name: list_items
    handler: src/handlers/list_items.handler
    description: "商品一覧"
    events:
      - http:
          path: /items
          method: get
          cors: true

resources:
  Resources:
    ItemTable:
      Type: "AWS::DynamoDB::Table"
      Properties:
        TableName: Items
        AttributeDefinitions:
          - { AttributeName: item_id, AttributeType: S }
        KeySchema:
          - { AttributeName: item_id, KeyType: HASH }
        BillingMode: PAY_PER_REQUEST

Lambdaを用意

テスト実行のため、商品一覧を返す簡易的なLambdaを用意しておきます。

import json
import logging
import os
import boto3

logger = logging.getLogger()
logger.setLevel('DEBUG')


def list_items(limit, last_key=None):
    logging.debug(limit)
    dynamodb = boto3.resource('dynamodb')
    TABLE_NAME = 'Items'
    table = dynamodb.Table(TABLE_NAME)

    scan_kwargs = {
        'ConsistentRead': True,
        'Limit': limit
    }

    if last_key:
        scan_kwargs['ExclusiveStartKey'] = last_key

    response = table.scan(**scan_kwargs)
    logging.info(response)
    items = response.get('Items', [])
    return items


def handler(event, context):
    try:
        logging.info(event)
        logging.info(context)
        
        result = list_items(int(os.environ['DEFAULT_DATA_LIMIT']))
        logging.debug(result)
        return {
            'statusCode': 200,
            # ensure_ascii: 日本語文字化け対応
            'body': json.dumps(result, ensure_ascii=False)
        }

    except Exception as e:
        logging.error(e)

CICDの設定

Githubリポジトリに接続

対象のappのメニューからsettingsを開きます。

ci/cdconnect git でGithubの方を選択して接続します。

デプロイ対象のリポジトリを選択して、接続できました。

AWSアカウントに接続

次にデプロイ先のAWSアカウントに対象のロールを作成します。

connectを押すと、IAMロールを作成するCfnのスタック作成画面に遷移します。(connectの右の↓を押すと作成済みのロールを指定することもできます)
任意のスタック名とロール名に変更して、IAMの作成にチェックを入れたら、スタックの作成を押します。

注: スタックがus-east-1に作成されます。

IAMロールが作成されたら無事にAWSにも接続できました。

slackへの通知を設定

CICDの画面上からslackへの通知も設定できます。

add notification からslackを選択

slack側でserverlessアプリを許可して接続したら、通知タイミングとチャンネルを選ぶだけで通知を飛ばせるようになります。
任意でメンションも飛ばせます。

これで通知まで設定できました。

テストを用意

motoを使ってDynamoをモックするテストを1本用意しておきます。

import src.handlers.list_items as handler
from moto import mock_dynamodb2
import boto3
import json
import pytest
import os
ITEMS_TABLE_NAME = 'Items'


@mock_dynamodb2()
def test_list_items():
    '''
    商品一覧の取得結果が一致する
    '''
    dynamodb = boto3.resource('dynamodb')
    dynamodb.create_table(
        TableName=ITEMS_TABLE_NAME,
        KeySchema=[
            {
                'AttributeName': 'item_id',
                'KeyType': 'HASH'
            }
        ],
        AttributeDefinitions=[
            {
                'AttributeName': 'item_id',
                'AttributeType': 'S'
            },
        ],
        BillingMode='PAY_PER_REQUEST'
    )
    table = dynamodb.Table(ITEMS_TABLE_NAME)
    data = [
        {'item_id': 'item_0001',
         'item_name': 'サンプル品1', 'category': '食品'},
        {'item_id': 'item_0002',
         'item_name': 'サンプル品2', 'category': '食品'},
        {'item_id': 'item_0003',
         'item_name': 'サンプル品3', 'category': '食品'},
        {'item_id': 'item_0004',
         'item_name': 'サンプル品4', 'category': '雑貨'},
        {'item_id': 'item_0005',
         'item_name': 'サンプル品5', 'category': '雑貨'}
    ]
    for i in data:
        table.put_item(TableName=ITEMS_TABLE_NAME, Item=i)

    # 全件取得
    response = handler.list_items(20)
    assert response == data

    # 3件取得
    response2 = handler.list_items(3)
    assert len(response2) == 3

デプロイ前にpytestを実行させるため、package.jsonのscriptsを編集します。

{
  "name": "sample-serverless-api",
  "description": "",
  "version": "0.1.0",
  "dependencies": {},
  "devDependencies": {},
  "scripts": {
    "postinstall": "pip3 install -r requirements.txt",
    "test": "pytest"
  }
}

ライブラリのインストール時にpipenvを使いましたが、CICD環境で使えるようrequirements.txtも出力しておきます。

$ pipenv lock -r --dev > requirements.txt

ローカルでテストが通ることを確認しておきます。

~/p/s/s/sample-sls-cicd (develop)> pytest
=================================================================================================== test session starts ===================================================================================================
platform darwin -- Python 3.8.5, pytest-6.0.2, py-1.9.0, pluggy-0.13.1
rootdir: /Users/oka.haruna/playground/sample-sls-cicd
collected 1 item                                                                                                                                                                                                          

tests/unit/test_list_items.py .                                                                                                                                                                                     [100%]

==================================================================================================== 1 passed in 0.75s ====================================================================================================

動作確認

developブランチにcommitしてテストとデプロイが通るか確認します。

statusがsuccessに変わりました!

pytestも実行できています。

slackにも通知が来てました。

AWSコンソールに行ってみると、CFnスタックが作成されて無事にデプロイされています。

試しにデプロイしたLambdaをslsコマンドで動かしてみます。

$ sls invoke -f ListItems
{
    "statusCode": 200,
    "body": "[]"
}

DynamoDBにデータが入っていないので空配列ですが、200レスポンスが返ってきました!

ソースコード

今回のサンプルのソースコードは以下になります。

CM-haruna-oka/sample-sls-cicd - Github

まとめ

以上、Serverless Dashboardを使って簡単にCICD環境が構築することができました。

インフラの構成管理にServerless Frameworkを使っている場合は、非常に簡単にセットアップができて親和性も高くなります。
Serverless FrameworkがNode環境で動く以上、Typescriptの方が相性はいいですが、上記の手順でpytestも実行できることが検証できました!

個人的にいいなと思ったのがAWSへの接続をIAMロールで操作できる点です。他のCI/CDツールだと大体アクセスキーを発行する必要があるのでこれは嬉しいですね。
今回はFreeプランで試したので1人のユーザーしか使えませんが、Teamプラン以上であればサインアップした時のアカウントが最初のOwnerとなり、後からメンバーを追加(任意でOwnerに昇格)していくことでチームでの管理が可能となります。

他の機能も随時検証していきたいと思います!!