サーバレスコンピューティングサービスであるAWS Lambdaを使って開発する際に、一つ一つの小さな関数を関連する機能でまとめて、1プロジェクトとして開発する際のであれば、CircleCI のダイナミックコンフィグを使い、追加・修正された関数だけを自動でビルド、テスト、デプロイすることができます。

SAMを使ったLambda関数の開発とは?

AWS Lambdaを使って開発する際に、AWS Serverless Application Model(AWS SAM) というフレームワークを使って、開発効率を高めることができます。AWS SAMを使うことで、Node.jsやPython, Ruby, Go, Java, .NETに対応したコンパイラ言語やスクリプト言語を使って、サーバレスの実装やビルド、デプロイを簡単に行うことができます。

例えば、Javaを使って、sam-java-appというLambda関数を実装するのであれば、プロジェクトは次のようなディレクトリ構成を取ります。

ディレクトリ構成

同様に、Node.jsを使って、sam-nodejs-appというLambda関数を実装するのであれば、プロジェクトは次のようなディレクトリ構成を取ります。

sam-nodejs-app ディレクトリ構成

AWS SAMではコマンドラインから利用可能な、AWS SAM CLIコマンドを提供しており、どちらのプロジェクトであっても、アプリケーションのビルドは sam buildコマンドで、デプロイは sam deploy コマンドで行うことができます。

一方、テストに関しては、Javaの例であれば、HelloWorldFunctionディレクトリに移動し、mvn test コマンドを実行することで、src/test/java/helloworld/AppTest.java で定義されたテストを実行することができます。Node.jsの例であれば、hello-worldディレクトリに移動し、npm install そして npm run test の順に実行することで、tests/unit/test-handler.js で定義されたテストを実行することができます。

CircleCIを使ったビルド、テスト、デプロイ

CircleCI が備える Orb (設定を再利用可能な部品として切り出したもの) を使うことで、ビルド、テスト、デプロイの自動化手順を簡潔に記述することができます。

これから、前述のJavaで書かれた sam-java-app というlambda関数の自動化手順を例にとって、ジョブ build_test_deploy-sam-java-appジョブの各ステップを記述していきます。 なお、CircleCIでは設定ファイルをプロジェクトのルートディレクトリにある .circleci ディレクトリ内の config.yml ファイルの中に記述してきますが、以下の説明は、sam-java-app ディレクトリにある .circleci ディレクトリ内の config.yml ファイルの内容を基に説明していきます。

初期設定

jobs:
  build_test_deploy-sam-java-app:
    machine:
      image: ubuntu-2004:202010-01
      docker_layer_caching: true
    steps:
      - checkout
      - aws-cli/setup:
          role-arn: arn:aws:iam::XXXXXXXXXXXX:role/mshk-sam-role
      - run:
          name: AWS SAM installation
          command: |
            curl -L https://github.com/aws/aws-sam-cli/releases/latest/download/aws-sam-cli-linux-x86_64.zip -o aws-sam-cli-linux-x86_64.zip
            unzip aws-sam-cli-linux-x86_64.zip -d sam-installation
            sudo ./sam-installation/install

まずは、sam-nodejs-app関数のビルド、テスト、デプロイを実行する build_test_deploy-sam-java-app ジョブを定義します。通常であれば、ジョブ名を build_test_deploy といった感じにしてしまうのですが、他の関数(sam-nodejs-app)用に用意したジョブ名と同じになってしまうことがないよう、ジョブ名に関数名を付加することで、ユニークな名前になるようにしています。

初期設定では、リポジトリの内容をチェックアウト(checkout)した後、aws-cli Orb を使い、setupコマンド を使って初期設定をしています。この際、(AWSのシークレットを使ってではなく)OpenID Connectトークンを使ってAWSにアクセスするために、必要なIAMロールのARNを指定しています。

次に、AWS SAM CLIのインストールを行なっています。AWS SAM CLIが提供する機能に対応した aws-sam-serverless Orbが用意されており、後ほど使用するのですが、この Orb が提供するinstallコマンドOpenID Connect に対応していない(つまり、AWS_ACCESS_KEY_IDAWS_SECRET_ACCESS_KEYを必要とする)ため、ここでは手動でインストールを行なっています。

ビルド

  - aws-sam/build:
      template:  sam-java-app/template.yaml

ここでは、AWS SAM CLIのsam build コマンドに対応する aws-sam-serverless Orbのbuildコマンドを実行します。

テスト

  - run:
      name: Test HelloWorldFunction
      command: |
        cd sam-java-app/HelloWorldFunction
        mvn test

mvn testコマンドを直接実行することで、テスト(具体的には AppTest.javaで定義されたテスト)を実行します。

デプロイ

- aws-sam/deploy:
      stack-name: sam-java-app
      image-repositories: $AWS_ECR_HOST/mshk-sam-java-app

最後にデプロイです。AWS SAM CLIのsam deployコマンドに対応するaws-sam-serverless Orb の deploy コマンドを実行し、CloudFormationのsam-java-appスタックを作成、または更新します。

変更された関数だけをビルド〜デプロイする

さて、今回開発している sam-java-app関数とsam-nodejs-app関数ですが、常に両方をビルド、テスト、デプロイするのではなく、何らかの変更や更新のあった方だけビルド〜デプロイしたいというのは、当然の希望でしょう。

このような希望に応えるため、CircleCIではダイナミックコンフィグと呼ばれる機能を提供しています。ダイナミック(動的)という名前が示す通り、設定ファイル(コンフィグ)をあらかじめ定義しておいて(静的)実行するのではなく、実際にビルド〜デプロイするための手順をその場で(動的に)設定ファイルとして作成し、作成したファイルに処理を引き継ぐ、という機能です。

処理の引き継ぎ自体は、continuation Orbが提供する機能を使うことで、比較的簡単に行うことができますが、「動的に設定ファイル(コンフィグ)を生成」するところのハードルが高いものでした(必ずしも動的に生成しなくても、複数用意しておいた設定ファイルのうちの、いずれかを選んで処理を引き継ぐことも可能)。

この設定ファイルの生成を用意にするためのOrbとして、弊社のエンジニア @bufferings さんが split-config Orb を開発、公開しました。

設定ファイル(コンフィグ)の生成

 - split-config/generate-config:
      fixed-config-paths: |
        ./sam-java-app/.circleci/config.yml
        ./sam-nodejs-app/.circleci/config.yml
      generated-config-path: /tmp/generated_config.yml
      continuation: false
      post-steps:
        - persist_to_workspace:
            root: /tmp
            paths:
              - generated_config.yml

ここでは、sam-java-appのビルド〜デプロイ用に用意した設定ファイルとsam-nodejs-appのビルド〜デプロイ用に用意した設定ファイルをマージして、1つのファイル /tmp/generated_config.yml として出力しています。なお、マージする設定ファイルは、この例のように直接パス名を指定する(fixed-config-paths)ことも、あるいは正規表現にマッチ(find-config-regex)させたり、外部ファイルから読み出す(config-list-path)ことも可能です。

出力した設定ファイル(/tmp/generated_config.yml)は、別のジョブからも利用できるように、ワークスペースの中に保存しておきます。

追加・変更・修正箇所の検出(パスフィルタリング)

  - path-filtering/filter:
      workspace_path: /tmp
      config-path: /tmp/generated_config.yml
      mapping: |
          sam-java-app/.*   run-build_test_deploy-sam-java-app   true
          sam-nodejs-app/.* run-build_test_deploy-sam-nodejs-app true
      requires:
        - split-config/generate-config

ここでは、mappingの中で、

  • 指定された正規表現で示されるパス中のファイルが追加・変更・修正されていれば
  • パラメータ(ここでは run-build_test_deploy-sam-java-apprun-build_test_deploy-sam-nodejs-app)に
  • 値(ここでは boolean値 true)

を設定した上で、ワークスペースから読み出された設定ファイル(/tmp/generated_config.yml)に処理が移ります。

生成された設定ファイルでの継続処理

parameters:
  run-build_test_deploy-sam-java-app:
    type: boolean
    default: false
workflows:
  sam-java-app-workflow:
    when: << pipeline.parameters.run-build_test_deploy-sam-java-app >>
    jobs:
      - build_test_deploy-sam-java-app:
          context: oidc-aws

設定ファイル(generated_config.yml)では、パイプラインパラメータ(上の例では run-build_test_deploy-sam-java-app)が定義されています。継続処理時にこのパラメータが引き渡されると(つまり trueだと)、ワークフロー sam-java-app-workflowwhen節にあるpipeline.parameters.run-build_test_deploy-sam-java-apptrueと評価される結果、build_test_deploy-sam-java-appジョブが実行されるわけです。

実行例

sam-java-app関数とsam-nodejs-app関数の双方が更新されていると、ワークフロー generate-config が実行された後、ワークフロー sam-java-app-workflow と ワークフロー sam-node-js-app-workflowが並行して実行されます。

![sam-node-js-app-workflow実行]cci-nodejs-app-workflow.jpeg)

一方、sam-java-app関数だけが更新された場合、ワークフロー generate-configが実行された後、ワークフロー sam-java-app-workflowだけが実行されます。

![sam-java-app-workflow]sam-java-app-workflow.jpeg)

おわりに

これまでにCircleCIを使用し、設定ファイル(コンフィグ)を記述したことがある人であれば、今回取り上げた AWS SAM(サーバーレス)の実装にとどまらず、1つのリポジトリの中で、関連する複数のアプリケーションやサービスを追加・変更・修正箇所に応じて、自動的にビルド〜デプロイするようなダイナミックコンフィグを容易に記述することが可能になります。

また、今回は個別の関数が固有の設定ファイルを持っていましたが、それ以外にも、共通して使われる設定ファイルと、アプリやサービス固有の設定ファイルを動的に組み合わせるような用途も考えられます。

長い設定ファイルの見通しを良くするには、Orbによる部品化/再利用も有効な手段ではありますが、ダイナミックコンフィグのように、サブプロジェクト単位で設定ファイルを用意して、必要に応じて組み合わせて利用するといったアプローチも併せてご検討いただければと思います。

開発参考情報