AWS SAM/CircleCI/LocalStackを利用した実践的なCI/CD – ClassmethodサーバーレスAdvent Calendar 2017 #serverless #adventcalendar #reinvent

2017.12.10

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

はじめに

こんにちは、中山です。

このエントリはServerless Advent Calendar 2017 10日目の記事です。

今回はAWS SAM/CircleCI/LocalStackを利用した 実践的 なCI/CDをご紹介したいと思います。 実践的 とは、もし私が新規でサーバーレスアプリケーションを構築するのであればこういったCI/CD環境を整えるという意味です。つまり、現時点での私の考えをまとめてみました。まだまだ改善点もあるのですが現状こういった方式であればうまくいくのではないかと思っています。

本エントリではサーバーレスアプリケーションの管理にAWS SAMを利用しています。ただ、もちろんServerless Frameworkを利用するのもありです。どちらも最終的にCloudFormationテンプレートに変換してスタックを作成する、という流れなので基本機能はほぼ同じです。最近触る機会が多いで今回はAWS SAMを採用しています。

説明だけではあまりおもしろくないのでサンプルとなるリポジトリを作成しました。こちらをベースにして説明します。

それでは解説していきます。

目次

複数のAWSアカウントに対応する

 

サーバーレスに関わらず、AWS上にアプリケーションを構築する場合は複数のAWSアカウントを使い分けるパターンが多いと思います。例えば、アプリケーション毎に1つのアカウント、あるいは環境(開発/ステージング/本番)毎に1つのアカウントといった管理方法です。

1つのAWSアカウントを使い回した場合どういった問題が起きるのでしょうか。例えば以下のような問題点が考えられます。

  • AWSアカウント単位のリソース制限に引っかかってしまう可能性がある
  • オペミスを誘発する
  • 誤って本番環境のAWSリソースを削除してしまった
  • IAMを厳密に管理しようとするととたんに難しくなる
  • 例えば操作対象のAWSリソースのみに制限したい場合など

AWS Organizationsの発表などを見ていると、AWS的にも複数アカウントを利用した管理方法を推奨しているのではないでしょうか。いずれにせよ特別な理由がない限りこの方式でアプリケーションを構築するのが王道パターンだと思います。

CD(継続的デリバリ)を設計する

AWSアカウントの管理はこの方法を採用するとして、設計が必要になるのはCDです。CD(今回はCircleCIが担当)する際にどのAWSアカウントへデプロイするのか、またデプロイ対象となる各AWSアカウントのリソースを操作する権限をどのように取得するのかといった点を考慮する必要があります。

CircleCIはIAMクレデンシャルを設定できます。IMAユーザから払い出したクレデンシャルを指定することでAWSリソースを操作可能です。ただしビルド毎に1つしか指定できません。また、IAMクレデンシャルが漏洩してしまう可能性を考慮するとパーミッションをなるべく制限したいがデプロイには実質Admin権限が必要になる、といったジレンマもあります。

この問題に対して私はSTSを利用したAssume Roleが適していると思います。つまり、CircleCIから各AWSアカウントのIAM RoleにAssumeすることで一時的なセキュリティ認証情報を取得する方式です。Assume Roleについては以下のエントリを参考にしてください。

IAMロール徹底理解 〜 AssumeRoleの正体

図にすると以下のような構成です。

この方式にすることで以下のようなセキュリティ上のメリットがあります。

  • IAMクレデンシャルには最低限のパーミッションのみ許可できる
  • STSは一時的なものなので漏洩しても被害が低減可能
  • CircleCIに設定しているクレデンシャルが漏洩してもAssumeされない限りほぼ何もできない

具体的な設定を見てみましょう。まず、Assume元となるIAMユーザ(またはIAMグループ)のポリシーを以下のようにしておきます。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "sts:AssumeRole"
            ],
            "Resource": [
                "arn:aws:iam::111111111111:role/circleci-role",
                "arn:aws:iam::222222222222:role/circleci-role"
            ]
        }
    ]
}

Action でAssume Roleを許可、 Resource にAssumeさせたいIAM RoleのARNを指定してください。はまりポイントなのですが、クロスアカウントでAssume Roleさせる場合は明示的に sts:AssumeRole を許可しておく必要があります。

続いて、AssumeされるIAM Roleの信頼関係を以下のようにします。

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::111111111111:user/circleci"
      },
      "Action": "sts:AssumeRole",
      "Condition": {
        "StringEquals": {
          "sts:ExternalId": "testtesttesttest"
        }
      }
    }
  ]
}

ポイントは2つあります。

  • Principal はroot( arn:aws:iam::111111111111:root )ではなくIAMユーザのARNにすべき
  • つまり、特定のIAMユーザのみAssumeを許可する
  • ConditionExternal IDを設定してよりセキュリアにすべき

では、CDする際にどのようにAssume Roleさせればいいでしょうか。やりかたは色々とあるのですがAWS CLIを使ったスクリプトが手っ取り早いでしょう。例えば以下のような感じです。 aws sts assume-role コマンドを利用して一時セキュリティ情報を取得すればOKです。

  • assume.sh
#!/usr/bin/env bash

set -xeuo pipefail

ROLE_SESSION_NAME="$CIRCLE_USERNAME"
DURATION_SECONDS="900"

aws_sts_credentials="$(aws sts assume-role \
  --role-arn "$AWS_IAM_ROLE_CIRCLECI" \
  --role-session-name "$ROLE_SESSION_NAME" \
  --external-id "$AWS_IAM_ROLE_EXTERNAL_ID" \
  --duration-seconds "$DURATION_SECONDS" \
  --query "Credentials" \
  --output "json")"
<snip>

CloudFormationのService Roleを利用したよりセキュアな構成

AWS SAMはCloudFormationのスタックを構築する関係上、デプロイに実質Admin権限が必要です。AssumeされるIAM RoleにAdmin権限をアタッチしてもいいのですが、もっとセキュリティを向上できる方法があります。CloudFormationのService Roleを利用した方式です。この機能を利用するとAssumeされるIAM Roleのパーミッションも制限しつつデプロイに必要な権限を利用可能になります。より詳しくは以下のエントリを参照してください。

いつの間にかCloudFormationがIAM Roleに対応していました!

図にすると以下のようなフローになります。つまり、IAM Roleを多段に使い分けた構成です。

AWS SAMをデプロイする場合、基本的に aws cloudformation deploy (もしくはSAM Local)コマンドを利用します。このコマンドでは --role-arn オプションにService RoleのARNを指定することが可能です。例えばこんな感じで使えます。

ブランチ戦略

権限周りは上記方法で解決できそうです。では、CircleCIがCDすべきAWSアカウントを判別するにはどうすればよいでしょうか。Gitを前提にしていますが、ブランチまたはタグを利用しましょう。つまりブランチ/タグとAWSアカウントをひも付けて管理する方式です。

例えば以下のように関連付けるとよいでしょう。

  • master ブランチは開発環境
  • staging ブランチはステージング
  • タグのPushは本番環境

CircleCIにはWorkflowにfiltersを設定することであるブランチならXXをする、タグであればYYをするといった指定が可能です。例えば以下のように設定できます。

  • .circleci/config.yml
<snip>
workflows:
  version: 2
  aws_sam_ci_cd_workflow:
    jobs:
      - guess_and_save_envs:
          filters:
            tags:
              only: /.*/
      - build:
          requires:
            - guess_and_save_envs
          filters:
            tags:
              only: /.*/
      - test_unit:
          requires:
            - build
          filters:
            tags:
              only: /.*/
      - deploy:
          requires:
            - test_unit
          filters:
            branches:
              only:
                - master
            tags:
              only: /v([0-9]+\.){2}[0-9]/

今回のリポジトリでは以下のようにしてみました。

  • master ブランチは開発環境
  • vX.X.X というタグは本番環境
  • どれにもマッチしない場合(PullRequestなど)はデプロイを行わない(ユニットテストまで)

LocalStackを利用したユニットテスト

 

Lambda関数のユニットテストはどうすればいいでしょうか。Lambdaは結局のところ関数を実行しているだけと捉えられるので、テストフレームワークからテスト対象となる関数を実行、結果をアサーションライブラリでチェックという従来の方法が使えます。

ただし、Lambda関数はさまざまなAWSリソースと協調して動作する関係上、外部依存性が高いという事情があります。一般的にユニットテストはこういった外部依存性を排除した形でテストし、実行する環境に関係なくコードのロジックを検証する必要があります。

この問題に関してはすでに多くのツールが登場しており、AWSリソースへの外部依存性を低減する方法はたくさんあります。そのなかでも個人的にはLocalStackというアトラシアン社がオープンソースで公開しているツールが気に入ってます。プログラミング言語に依存せず利用できる点、Dockerイメージが公開されているのでセットアップも簡単など扱いやすさが主な理由です。

ちなみに、LocalStackを利用したLambda関数のテストについては以下の資料がとても参考になります。サンプルリポジトリを作成する際にも参考にさせていただきました。

実際の使い方を見てみましょう。利用シーンは2つあります。開発マシン、CI(今回はCircleCIが担当)の2つです。

開発マシンでの利用方法

まず開発マシンでの利用。上述したようにDockerイメージが公開されているので以下のような docker-compose.yml を用意しておけば簡単に立ち上げできます。

  • docker-compose.yml
version: "3.3"

services:
  localstack:
    container_name: localstack
    image: localstack/localstack
    ports:
      - "4572:4572"
    environment:
      - SERVICES=s3
      - DEFAULT_REGION=ap-northeast-1
      - DOCKER_HOST=unix:///var/run/docker.sock

今回の場合、localhost:4572でS3(のモック)が立ち上がるのでAWS SDKのエンドポイントをそこに向ければローカルのS3にアクセスできます。例えば、boto3の場合以下のようにS3クライアントオブジェクトを設定できます。

  • src/handlers/file_processor/tests/conftest.py
s3 = boto3.client('s3', endpoint_url='http://localhost:4572')

ただこのコードはテストで利用するS3クライントオブジェクトです。Lambdaが実行する関数の中で endpoint_url='http://localhost:4572' を指定してしまうとLambda自体にアクセスしてしまいます。では、例えば環境変数にテスト用の変数を仕込んでおきそれを元に条件分岐させるのはどうでしょう。しかし、プロダクションのコードにテストロジックを仕込むのはアンチパターンです。

この問題に対しては先程の資料に解決方法が書かれています。最高ですね。

つまり、「接合部(シーム)」を利用してS3クライアントオブジェクトをLambda関数に渡す方法です。サンプルリポジトリはPythonベース(最近触ってるので)なのでPythonの場合どうやるのか具体的なコードで説明します。例えば以下のようにするとよいかと思います。

  • Lambda関数のハンドラ( src/handlers/file_processor/index.py )
import boto3

from file_processor import FileProcessor

s3 = boto3.client('s3')


def handler(event, context):
    file_processor = FileProcessor(event, context, s3)
    file_processor.main()

ハンドラはメソッドを呼び出したら終わりです。実際のロジックはクラス側でやらせます。ポイントはインスタンスを作成する際にS3クライアントオブジェクトをコンストラクタに渡している点です。コンストラクタの処理は以下のようになります。

  • FileProcessor クラスのコンストラクタ( src/handlers/file_processor/file_processor.py )
class FileProcessor(object):
    def __init__(self, event, context, s3):
        self.event = event
        self.context = context
        self.s3 = s3
<snip>

アトリビュートに渡されたS3クライアントオブジェクトを代入しています。ここが接合部です。

ではテストコードはどういった形になるでしょうか。

  • src/handlers/file_processor/tests/conftest.py
@pytest.fixture(scope='session')
def s3():
    s3 = boto3.client('s3', endpoint_url='http://localhost:4572')

    return s3
  • src/handlers/file_processor/tests/test_file_processor.py
def test_zip_bytes_io(s3):
    file_processor = FileProcessor({}, {}, s3)
<snip>

今回pytestというテストフレームワークを利用していますが、補足すると以下のような処理を行っています。

  • pytestのfixtureでエンドポイントをlocalhost:4572に変更したオブジェクトを作成
  • fixture にすることで各テストケースからこのオブジェクトを利用可能にする
  • def test_hoge(&lt;fixture名&gt;): という形式で呼び出せる
  • conftest.py というファイルに fixture を記述することでテストケースから共有可能
  • FileProcessor クラスを初期化する際にそのオブジェクトを渡す

よさそうですね。使い際は docker-compose up -d してLocalStackを立ち上げる、 python -m pytest でテスト実行すればOKです。

CIでの利用方法

CIからLocalStackを利用する場合どうするのがいいでしょうか。CircleCIはDockerが実行可能(Remote Docker機能)なのでこれをそのまま使えばよいかと思いましたがうまくいきませんでした。というのもRemote Dockerで立ち上げたコンテナはプライマリコンテナとは切り離された環境になっているためです。

該当のドキュメントを引用します。

It’s impossible to start a service in remote docker and ping it directly from a primary container (and vice versa). To solve that, you’ll need to interact with a service from remote docker, as well as through the same container: A different way to do this is to use another container running in the same network as the target container:

つまり、もしRemote DockerでLocalStackを立ち上げたい場合、以下のようにする必要があります。

  1. LocalStackコンテナ内でpytestのコードを実行させる
  2. pytestを実行するコンテナを立ち上げてLocalStackコンテナと同じDockerネットワークに所属させる

どちらもちょっと面倒なのでどうしたものかと思いましたが、CircleCIはexecutorに複数のDockerイメージを指定可能です。さらに、プライマリコンテナからは(デフォルトで) localhost という名前でアクセスできます。この機能を使うとジョブ実行時にLocalStack用コンテナを立ち上げ、プライマリコンテナからアクセスさせることが可能です。

具体的には以下のように .circleci/config.yml を設定します。

references:
  unit_test_container: &unit_test_container
    docker:
      - image: circleci/python:3.6.1
      - image: localstack/localstack
<snip>      
  test_unit:
    <<: *unit_test_container
    steps:
      - checkout
      - attach_workspace:
          at: *workspace_envs
      - attach_workspace:
          at: *workspace_artifacts
      - restore_cache:
          keys:
            - v1-pip-{{ .Branch }}-{{ checksum "requirements.txt" }}-{{ checksum "constraints.txt" }}
      - run:
          name: Test unit
          command: |
            set -x

            source ./envs.sh
            . ./.venv/bin/activate
            make lint test-sam test-unit

CircleCIの実行結果を見るとちゃんとアクセスできているようです。これでCIからもLocalStackが利用できますね。

<snip>
============================= test session starts ==============================
platform linux -- Python 3.6.1, pytest-3.3.1, py-1.5.2, pluggy-0.6.0 -- /home/circleci/project/.venv/bin/python
cachedir: ../../../.cache
rootdir: /home/circleci/project, inifile: setup.cfg
collected 5 items                                                              

tests/test_file_processor.py::test_valid_s3_prefix PASSED                [ 20%]
tests/test_file_processor.py::test_valid_bucket_key_list[single] PASSED  [ 40%]
tests/test_file_processor.py::test_valid_bucket_key_list[multi] PASSED   [ 60%]
tests/test_file_processor.py::test_zip_bytes_io PASSED                   [ 80%]
tests/test_file_processor.py::test_upload_file_obj PASSED                [100%]

=========================== 5 passed in 1.18 seconds ===========================

E2Eテストはどうやるか

 

LocalStackを使えばほぼインテグレーションテストレベルのテストが行えます。ただ、やはりE2Eテストも実施して心に平穏をもたらしたいですよね。どう書くのがよいのでしょうか。

サンプルリポジトリはZipファイルがS3バケットに置かれるとLambda関数が起動(S3のEvent Notification)、別のバケットにその中身をアップロードという処理をしています。つまりここで検証したいことは「Lambda関数がZipファイルを適切に処理できるか」です。

具体的には以下のようにします。

  • スタックのアウトプットから必要な情報を取得( tests/conftest.py )
@pytest.fixture(scope='session')
def buckets(s3):
    buckets = {}
    stack_name = os.getenv('STACK_NAME')
    cfn = boto3.client('cloudformation')

    stack_outputs = cfn.describe_stacks(StackName=stack_name).get('Stacks')[0].get('Outputs')

    for stack_output in stack_outputs:
        for output_value in stack_output.values():
            if output_value == 'BucketOriginName':
                buckets.update({'BucketOriginName': stack_output.get('OutputValue')})
            elif output_value == 'BucketProcessedName':
                buckets.update({'BucketProcessedName': stack_output.get('OutputValue')})
    else:
        return buckets
  • E2Eに必要な処理(今回の場合はS3バケットへZipファイルをアップロード)を実施( tests/conftest.py )
@pytest.fixture(scope='function')
def upload_zip_obj(s3, s3_prefix, buckets, request):
    bytes_io = BytesIO()
    bucket_origin = buckets.get('BucketOriginName')
    bucket_processed = buckets.get('BucketProcessedName')
    key = 'test.zip'
    pytest.file_zipped = 'test.txt'
    pytest.body_zipped = 'test'

    with ZipFile(bytes_io, mode='w') as zip_obj:
        zip_obj.writestr(pytest.file_zipped, pytest.body_zipped)
    bytes_io.seek(0)
    s3.upload_fileobj(bytes_io, bucket_origin, key)

    def delete_zip_obj():
        for b, k in zip([bucket_origin, bucket_processed], [key, f'{s3_prefix}/{pytest.file_zipped}']):
            s3.delete_object(Bucket=b, Key=k)
    request.addfinalizer(delete_zip_obj)
  • Zipファイルが期待した状態に処理されているか実際のS3バケットからオブジェクトを取得してテスト( tests/test_e2e.py )
from io import BytesIO
import time

import pytest


@pytest.mark.usefixtures('upload_zip_obj')
def test_upload_file_obj(s3, buckets, s3_prefix):
    bytes_io = BytesIO()
    key = f'{s3_prefix}/{pytest.file_zipped}'
    bucket_processed = buckets.get('BucketProcessedName')

    time.sleep(5)

    s3.download_fileobj(bucket_processed, key, bytes_io)

    assert type(bytes_io) is BytesIO
    assert bytes_io.getvalue() == pytest.body_zipped.encode()

E2Eテストなのでデプロイ後実行します。CircleCIの設定は以下のようにすればOKです。

  • .circleci/config.yml
<snip>
      - deploy:
          name: Deploy AWS SAM
          command: |
            set -x

            source ./envs.sh
            . ./.venv/bin/activate
            make deploy AWS_ENV="$AWS_ENV"
      - run:
          name: Test e2e
          command: |
            set -x

            source ./envs.sh
            . ./.venv/bin/activate
            make test-e2e

AWS SAMの責任範囲を考える

 

「責任範囲」とはAWS SAMでどこまでやらせるかという話です。何度か書いていますが、AWS SAMは最終的にCloudFormationに変換されるため、CloudFormationができることはなんでもできます。つまり、AWS SAMに全てのAWSリソースを定義することも可能です。

ただ、私はAWS SAMとは別にAWSリソースを定義するCloudFormationを作成し、クロススタック参照で必要なリソースを参照させる方式が気に入ってます。この機能に関しては以下のエントリが詳しいです。

CloudFormationのスタック間でリソースを参照する

何故AWS SAMで全てのAWSリソースを管理せず、クロススタック参照にした方がいいのでしょうか。議論が分かれるところもあると思いますが、個人的には以下の理由からこの方式を推奨しています。

  • 単純に1つのテンプレートが肥大すると読みづらい
  • デプロイの単位は小さくすべき
  • つまり、AWS SAMのアップデートに引きずられてスタックがアップデートされてしまう可能性は低減したい

能書きはいいとしてクロススタック参照を使う場合具体的にどうするのか。AWS SAMでクロススタック参照は普通に Fn::ImportValue 組み込み関数を使えばいいだけです。例えば以下の例ではIAM Roleと環境変数に設定するバケット名を参照しています。

  • sam.yml
<snip>
  FileProcessor:
    Type: AWS::Serverless::Function
    Properties:
      FunctionName: file-processor
      CodeUri: src/handlers/file_processor
      Handler: index.handler
      Runtime: python3.6
      MemorySize: !Ref MemorySize
      AutoPublishAlias: live
      Role:
        Fn::ImportValue: !Sub ${AWS::StackName}-iam-FileProcessorRoleArn
      Environment:
        Variables:
          BUCKET_PROCESSED:
            Fn::ImportValue: !Sub ${AWS::StackName}-s3-BucketOriginName
<snip>

参照される側のテンプレートはどう設定すればいいでしょうか。例えばIAM用テンプレートだったら以下のようにします。

  • iam.yml
---
AWSTemplateFormatVersion: 2010-09-09
Description: AWS SAM CI CD - IAM

Resources:
  FileProcessorRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service: lambda.amazonaws.com
            Action: sts:AssumeRole
      Path: !Sub /${AWS::StackName}/
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/AmazonS3FullAccess

Outputs:
  FileProcessorRoleArn:
    Value: !GetAtt FileProcessorRole.Arn
    Export:
      Name: !Sub ${AWS::StackName}-FileProcessorRoleArn

ただし、上述のドキュメントを見るとわかりますがクロススタック参照は少々制限があります。例えば参照されている値が変わるような変更ができなくなるなどです。結構ハマリポイントなので導入が難しいケースもあるかもしれません。そういった場合はAWS SAMの Parameters で参照したい値を渡せばよいでしょう。

Tips

 

章立てするほどでもないがちょっと工夫した点をご紹介したいと思います。

外部モジュールはAmazon Linuxコンテナから作成しよう

Lambda関数でバンドルされていない外部モジュールを利用する場合、基本的にAmazon Linuxコンテナを利用しましょう。LambdaもAmazon Linuxベースなので似た環境でビルドされたモジュールを使った方が思わぬ罠に引っかかりにくいです。特に、C言語などを内部的に利用しているモジュールはそうすべきです。ビルドする際のオプションの違いで上手く動かなかったりするためです。

具体的にはこうします。まず外部モジュールが必要なLambda関数が含まれるディレクトリにAmazon Linuxコンテナ用 Dockerfile を用意しておきます。

  • src/handlers/file_processor/Dockerfile
FROM amazonlinux:2017.09.0.20170930

WORKDIR /workdir
COPY *.txt ./
RUN yum install -y gcc python36 python36-devel

ENTRYPOINT ["pip-3.6", "install", "-r", "requirements.txt", "-c", "constraints.txt", "-t", "./vendored"]

コンテナでインストールされたモジュールをどうやってホスト側にもってくるか。開発マシンだったらボリュームをマウントさせればいいでしょう。

  • Makefile
			docker container run \
				-it \
				--rm \
				--volume $$PWD/vendored:/workdir/vendored \
				$(STACK_NAME)-$$handler; \

CircleCIの場合は少し工夫が必要です。LocalStackを利用したユニットテストでも書きましたが、プライマリコンテナとDocker Remote間は直接アクセスできません。幸いドキュメントにこの問題の解決策が書かれています。つまり、コンテナからファイルだけ取ってくればOKです。

  • Makefile
			docker container run -it --name $(STACK_NAME)-$$handler $(STACK_NAME)-$$handler; \
			docker container cp $(STACK_NAME)-$$handler:/workdir/vendored .; \

環境毎に変更したいパラメータは --parameter-overrides で吸収しよう

例えばLambda関数に割り当てるメモリを各環境毎に変更したいとします。(AWS SAMのデプロイに利用する) aws cloudformation deploy コマンドは --parameter-overrides オプションで Parameters に渡す値を変更可能です。

例えば、AWS SAMのテンプレートにメモリ用のパラメータを設定しておきます。

  • sam.yml
Parameters:
  Env:
    Type: String
  MemorySize:
    Type: Number

設定ファイルにしておいた方が管理しやすいと思うので環境毎のパラメータを以下のようにまとめておきましょう。今回は ini 形式にしていますがフォーマットは何でもOKです。

  • params/dev.ini
Env=dev
MemorySize=128
  • params/prd.ini
Env=prd
MemorySize=256

コマンドを実行する際は以下のようにします。 Makefile なのでちょっと分かりにくいですけど…

  • Makefile
		aws cloudformation deploy \
			--template-file .sam/sam_packaged.yml \
			--stack-name $(STACK_NAME) \
			--parameter-overrides $$(cat params/$(AWS_ENV).ini | grep -vE '^#' | tr '\n' ' ' | awk '{print}') \
			--capabilities CAPABILITY_IAM; \

改善点

 

今回ご紹介した方法である程度CI/CD環境を整えられるのですが、改善すべき点もあると思っています。具体的には以下です。

PullRequest用の環境が欲しい

GitHubを使った開発経験がある方なら、PullRequestを上げたら自分用の検証環境が自動で立ち上がりそこで好き勝手に検証できるとうれしいなと考えたことはないでしょうか。サーバーレスアプリケーションは言い換えるとフルマネージドサービスを活用したアーキテクチャです。これはもちろんユーザにとってうれしい反面、ローカル環境でのテストがやりにくいという別の問題があります。

例えばローカルで実施したユニットテストで補足できなかった思いもよらないバグが実際の環境で見つかった、などがあるある話かと思います。これを防ぐ方法としてPullRequestを上げた瞬間それ用の検証環境が立ち上がり自由に動かせるとバグも見つかりやすくなるのではないかと思っています。

E2Eテストに失敗した場合ロールバックさせたい

E2Eテストは一度実際の環境にデプロイしてから実施する必要があります。つまり、テスト前にエンドユーザにコードが利用されている訳です。根本的な解決(E2Eテストをデプロイ前に実施する)は難しい面もあるかと思いますが、せめてテストに失敗したらロールバックさせたい…

以前以下のエントリでCodeDeployを利用したLambda関数のデプロイについてご紹介しました。

AWS SAMを通してCodeDeployを利用したLambda関数のデプロイを理解する – ClassmethodサーバーレスAdvent Calendar 2017 #serverless #adventcalendar #reinvent

この機能を利用するとテストに失敗したらロールバックさせることも可能です。まだ実際の環境で試せてはいないのですが、導入するとより安全なデプロイが可能になるかと思います。

ユニットテストの責任範囲を拡張したい

LocalStackでもかなり幅広いAWSサービスをモック化可能なのですが、まだ未対応であったり少ししか対応してなかったりサービスによってまちまちです。AWS SAMにはSAM Localというツールがあります。多機能なツールなのですが特にAPI Gatewayのモック化が熱いです。

[新ツール]AWS SAMをローカル環境で実行できるSAM Localがベータリリース

こちらのツールを導入してユニットテストのレベルを引き上げられたらと思っています。

まとめ

いかがだったでしょうか。

AWS SAM/CircleCI/LocalStackを中心としたCI/CD環境についてご紹介しました。まだまだ改善点がありますがこういった方法もあるんだなと読んでいただければと思います。よりよい方法が見つかったら別エントリでご紹介できれば。

本エントリがみなさんの参考になれば幸いに思います。