BIGLOBEの「はたらく人」と「トガッた技術」

GitHub Actions で AWS を操作する(Lambda編)

こんにちは。開発部門(プロダクト技術本部)の宮下です。
BIGLOBE では GitHub Actions による作業効率化に取り組んでいます。
本記事では、GitHub Actions が得意とする点を踏まえつつ、AWS Lambda と連携して手作業を大幅に減らす実例をソースコード付きで紹介します。

想定読者

  • 手作業を自動化し、作業を効率化させたい
  • GitHub Actions をこれから使ってみたい
  • GitHub Actions の導入事例が知りたい
  • GitHub Actions から Lambda を実行したい

GitHub Actions を使うと何がうれしいのか?

GitHub Actions はアクションを組み合わせ、ワークフローとして定義することで様々なプロセスを自動化できます。
また、数多くのアクションが Marketplace に公開されており、容易にワークフローを作成できます。
他にも Composite Action や、Reusable workflows というものでユーザー独自の再利用可能なアクション・ワークフローを実装できます。
このように、開発効率を高めながらメンテナンスを容易にする仕組みがあるのはうれしいポイントですね。
具体的に GitHub Actions が威力を発揮するユースケースと、あまり向かないユースケースを挙げます。

代表的なユースケース

メインのユースケースは CI/CD になるかと思いますが、GitHub 上でのアクティビティを自動化も容易です。
以下にいくつか例を挙げます。

  • CI
    • Pull Request が作成されたら単体テストや Linter を実行する
  • CD
    • テストが通り、マージされたら開発環境にデプロイする
    • Pull Request が承認され、リリースブランチが main ブランチにマージされたら本番環境にデプロイする
  • GitHub 上でのアクティビティを自動化
    • Issue が作成されたら GitHub Projects に追加し、ランダムで担当者をアサインする
    • 1カ月以上放置された Issue を close する

あまり向かないユースケース

GitHub Actions には ワークフローをスケジュール起動する仕組みもあります。
しかしながら、ワークフロー実行の負荷が高い時間帯は遅延する可能性もあり、時間通りにワークフローを実行したいというユースケースには向いていません。
GitHub 公式ドキュメント にもその点について書かれていますので注意してください。

事例紹介

課題

単体テストは Pull Request 作成をトリガーに CI が実行されていましたが、結合テストやパフォーマンステストは手作業が多く煩雑でした。
テスト結果はリリース可否を判定するためのエビデンスとして、 GitHub の Issue に残す運用になっています。
そのため、以下の流れで行っていました。

  • Issue 作成
  • AWS マネージドコンソールにログイン
  • リグレッションテスト用 Lambda を実行
    • アプリケーションの機能に変更が入らない場合はこれを結合テストとしています
  • テスト結果を Issue にコメント
  • パフォーマンステスト用 Lambda を実行
  • テスト結果を Issue にコメント

例えば依存ライブラリのバージョンアップ対応など、アプリケーションの機能に変更が入らないリリースをさくっとするためには、この手作業が阻害要因の1つとなっていました。
そこで、この一連の流れを GitHub Actions で自動化しました。

改善後

多少ぼかしていますが、おおよそ以下のような構成となっています。

API コンテナ

ECS Fargate で構成された REST API アプリケーションのコンテナです。
テスト用の Lambda からも API が実行されるため、テストデータを色付けする仕組みを持っています。

リグレッションテスト用 Lambda

各ユースケースにデグレードがないか(今回の修正が新たな不具合を起こしていないか)を確認しています。
ユースケースの確認は API を順次実行することで実現しています。
回線契約のライフサイクルを管理するアプリケーションを例にとると以下のようなイメージです。

  • ユースケース:光回線の契約完了
    • 申込API -> 契約開始API -> 契約完了API
  • ユースケース:光回線の申込キャンセル
    • 申込API -> 申込キャンセルAPI

パフォーマンステスト用 Lambda

当たり前のことですが、事前に性能要件を決めておくことが重要です。
パフォーマンステストはそれに則り、特定の条件下で基準を満たすかチェックしています。
例えば、以下のような形です。

  • 条件
    • ECS タスク数: 3
    • 多重度: 10
    • 5回ずつユースケースを実行
  • 基準
    • 99パーセンタイルのターンアラウンドタイムが 100 ms 以下

自動化による効果

改善後では GitHub Actions を実行するだけで良くなり、作業時間も劇的に減りました。
何より、手作業のストレスから開放されたのが大きいです…

Before After
ステップ数 6 1
作業時間 約30分 約3分

Lambda を実行するアクション

ここまでは GitHub Actions を使ったプロセス自動化の効果について、事例を交えながらお話ししました。
ここからは、Lambda を実行するアクションについて技術的なポイントを触れます。

GitHub Actions で使えるアクション

GitHub Actions で使えるアクションは3種類あります。

今回はこの中でも Composite Action で実装しました。
Composite Action とは再利用可能な Action のことです。
Lambda を実行したいというユースケースは色々考えられますので、様々なワークフローから利用できるようにしています。

ソースコード

まずは、Composite Action のアウトラインごとに本アクションの概要を説明します。

  • name: アクションの名前
    • Execute Lambda synchronously
  • description: アクションの説明(省略可)
    • Lambda を同期実行します
  • inputs: アクションで使用するパラメータ(省略可)
    • AWS の情報、Lambda 実行時に必要な情報(関数名、入力パラメータとして渡す JSON 等)を受け取るようにしています。
  • outputs: アクションが出力するパラメータ(省略可)
    • Lambda の実行結果(OK / NG), Lambda 関数 が返すレスポンスを出力するようにしています。
  • runs: アクションの実行方法
    • using: どの方法で実行するかを指定
      • Composite Action で作成しているため、"composite" を指定しています。
    • steps: アクションで実行するステップ
      • 大きく inputs の JSON のチェック、AWS の認証情報取得、Lambda の実行の3つに分かれています。

ソースコードはこちらです。

name: Execute Lambda synchronously
description: Lambda を同期実行します

inputs:
  lambda-function-name:
    required: true
    description: 実行する Lambda の名前
  payload-json:
    required: false
    description: payload として渡す json
  payload-json-file-path:
    required: false
    description: payload として渡す json ファイル のパス
  aws-account-id:
    required: true
    description: AWSアカウントID
  iam-role-name:
    default: lambda-execution-with-gh-action
    required: true
    description: IAMロール名
  read-timeout-second:
    default: 300
    required: true
    description: 最大ソケット読み取り時間(秒)

outputs:
  result:
    value: ${{ steps.execute-lambda.outputs.result }}
    description: Lambda の実行結果 (OK / NG)
  response:
    value: ${{ steps.execute-lambda.outputs.response }}
    description: Lambda function のレスポンス

runs:
  using: composite

  steps:
    # payload-json か payload-json-file-path のいずれかは必須
    - name: Required check for payload
      id: required-check-for-payload
      if: ${{ inputs.payload-json == '' && inputs.payload-json-file-path == '' }}
      shell: bash
      run: |
        echo "ERROR: Either payload-json or payload-json-file-path is required."
        exit 1

    - name: Check payload json file path
      id: check-payload-json-file-path
      if: ${{ inputs.payload-json-file-path != '' }}
      shell: bash
      run: |
        set -x
        if [ ! -f "${{ inputs.payload-json-file-path }}" ]; then
          echo "ERROR: Does not exist ${{ inputs.payload-json-file-path }}."
          exit 1
        fi

    # json 形式かチェック
    - name: Check json format
      id: check-json-format
      env:
        INPUT_PAYLOAD_JSON: ${{ inputs.payload-json }}
        INPUT_PAYLOAD_JSON_FILE_PATH: ${{ inputs.payload-json-file-path }}
      shell: bash
      run: |
        set -x
        payload_json=$(echo "${INPUT_PAYLOAD_JSON:-$(cat "${INPUT_PAYLOAD_JSON_FILE_PATH}")}")
        if ! echo "${payload_json}" | jq -e . > /dev/null 2>&1; then
          echo "ERROR: Invalid json format. ${payload_json}"
          exit 1
        fi
        echo "payload-json=$(echo "${payload_json}" | jq -c .)" >> "${GITHUB_OUTPUT}"

    - name: Configure AWS credentials
      uses: aws-actions/configure-aws-credentials@v4
      with:
        aws-region: ap-northeast-1
        role-to-assume: arn:aws:iam::${{ inputs.aws-account-id }}:role/${{ inputs.iam-role-name }}
        role-session-name: gh-action-${{ github.run_id }}

    # Lambda を実行することで標準出力されるのは以下。jq -s で 2つ の json object を配列にして $output 変数に代入している
    #   Array[0] Lambda function のレスポンス (outfile として/dev/stdoutを指定)
    #   Array[1] 呼び出された Lambda function のバージョンなどの追加データ (CLI のデフォルトの挙動)
    #      { "ExecutedVersion": "$LATEST", "StatusCode": 200, ... }
    # 詳しくは以下を参照
    #   https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/invocation-sync.html
    #   https://awscli.amazonaws.com/v2/documentation/api/latest/reference/lambda/invoke.html#output
    - name: Execute Lambda
      id: execute-lambda
      shell: bash
      env:
        PAYLOAD_JSON: ${{ steps.check-json-format.outputs.payload-json }}
        LAMBDA_FUNCTION_NAME: ${{ inputs.lambda-function-name }}
        READ_TIMEOUT_SECOND: ${{ inputs.read-timeout-second }}
      run: |
        output=$(
          aws lambda invoke \
            --function-name "${LAMBDA_FUNCTION_NAME}" \
            --cli-binary-format raw-in-base64-out \
            --payload "${PAYLOAD_JSON}" \
            --cli-read-timeout "${READ_TIMEOUT_SECOND}" \
            /dev/stdout \
            | jq -s
        )
        echo "${output}"

        if echo "${output}" | jq -e -r '.[1].FunctionError' > /dev/null; then
          echo "ERROR: Lambda execution failed."
          result="NG"
        else
          echo "INFO: Lambda execution success."
          result="OK"
        fi

        echo "result=${result}" >> "${GITHUB_OUTPUT}"
        response=$(echo "${output}" | jq .[0])
        echo "INFO: Lambda function response: "${response}

        response="${response//'%'/'%25'}"
        response="${response//$'\n'/'%0A'}"
        response="${response//$'\r'/'%0D'}"
        echo "response=${response}" >> "${GITHUB_OUTPUT}"

ポイント、はまったところ

ここからは Lambda を実行するアクションを実装するうえでのポイントやはまったところについて、以下の流れで説明します。

  • GitHub Actions の制御
    • ステップ間での値の受け渡し
    • 複数行の値を扱う
  • GitHub Actions と AWS との連携
    • AWS の認証情報取得
  • AWS CLI
    • コマンドラインオプション
  • Lambda
    • 同期実行について

GitHub Actions の制御 / ステップ間での値の受け渡し

アクションのステップ間で値を受け渡しするには、以下のようにすると良いです。

  • 値を渡すステップ
    {key}={value} の形式で GITHUB_OUTPUT にリダイレクトすることで、後続のステップで参照することができます。
    また、ステップに ID を指定する必要があります。
  • 値を受け取るステップ
    ${{ steps.<前ステップのID>.outputs.<key> }} の形式で値を受け取ることができます。
    ちなみに、${{ <expression> }} は GitHub Action のアクションやワークフローで式を評価するための構文です。
    詳しくはGitHub 公式ドキュメントを参照してください。

サンプルはこちらです。

- name: Set value
  id: step1
  run: |
    echo "value=step1の値です" >> "${GITHUB_OUTPUT}"
- name: Use value
  id: step2
  env:
    STEP1_VALUE: ${{ steps.step1.outputs.value }}
  run: |
    echo "${STEP1_VALUE}" # step1の値です

GitHub Actions の制御 / 複数行の値を扱う

複数行の値を受け渡しする場合、工夫が必要です。
何パターンか対応方法がありますが、作成したアクションでは事前にエスケープする方法で実装しました。

response="${response//'%'/'%25'}"
response="${response//$'\n'/'%0A'}"
response="${response//$'\r'/'%0D'}"
echo "response=${response}" >> "${GITHUB_OUTPUT}"

この値を受け取るステップで元の値に戻す場合は、当然アンエスケープが必要となります。
エスケープとは逆の手順でアンエスケープすれば良いでしょう。
この対応は値を渡す側 / 受け取る側でエスケープ / アンエスケープするという暗黙的な制約を課され、あまりいけてないので直したいところです。

GitHub Actions と AWS との連携 / AWS の認証情報取得

GitHub Actions と AWS との連携では有効期間の短いトークンを発行して AWS 内のリソースにアクセスしています。
そのためには、以下が必要となります。

  • OIDC(OpenID Connect)ID プロバイダーの作成
  • IAM ロールの作成
  • 作成した IAM ロールに GitHub の OIDC ID プロバイダーの信頼ポリシーを設定
  • aws-actions/configure-aws-credentials アクション実行時に作成した IAM ロールを指定

GitHub と AWS の OIDC 連携について、詳細は GitHub 公式ドキュメントに載っていますので、こちらを参照してください。
注意点としては、信頼ポリシーの条件で GitHub の Organization やリポジトリを指定しないと、任意の GitHub リポジトリから AWS 内のリソースにアクセスできてしまいます。
必ず設定を入れてください。

AWS CLI / コマンドラインオプション

AWS CLI には共通した(グローバルな)コマンドラインオプションがあります。
その中でも本アクションに関連したオプションを取り上げます。

  • --cli-binary-format
    入力パラメータ(payload)に渡すデータの解釈方法を指定します。デフォルトは base64 です。
    プレーンなデータを渡したい場合は raw-in-base64-out を指定します。
  • --cli-read-timeout
    Lambda を実行してから応答があるまで待つ最大時間を秒単位で指定します。デフォルトは60秒です。
    テスト用の Lambda の実行は60秒以上かかりますが、実装初期はこのオプションを指定しておらず、原因の調査に1~2時間くらい時間を溶かしました...

Lambda / 同期実行について

  • 出力
    標準出力には 以下のように Lambda からのレスポンスヘッダーを含む情報が出力されます。
    json { "ExecutedVersion": "$LATEST", "StatusCode": 200 }
    Lambda 関数のレスポンスは指定のファイルに出力されます。
    今回のアクションでは /dev/stdout を指定しているため、標準出力に出力されます。
  • エラーの判定
    Lambda 関数内でエラーを返したとしても StatusCode は200になります。
    よって、エラーかどうかの判定に StatusCode は使えません。
    エラーの判定には FunctionError の有無で判定する必要があります。

詳しくは以下を参照してください。
AWS ドキュメント - 呼び出す
AWS ドキュメント - 同期呼び出し

まとめ

本記事では、GitHub Actions による自動化の効果と GitHub Actions から AWS Lambda を実行する方法について紹介しました。
GitHub Actions をうまく活用すると手作業の面倒な部分を自動化でき、別の作業に時間を割り当てられます。
最後に、GitHub Actions を実装するにあたり、大事だと思うポイントを挙げます。

  1. いきなり GitHub Actions のアクションやワークフローを実装しない
  2. どういうステップを踏めばやりたいことを実現できるか考える
    (例)フローチャートやアクティビティ図を書いてみる
  3. 手元のターミナル等でコマンドを実行し、動くことを確認する
    今回実装したアクションは1つ1つをみるとほとんどが Bash から実行可能なコマンドです。
  4. 3まできてはじめて GitHub Actions で実装する

上記のポイントを参考にしていただき、ぜひ明日からプロジェクトに導入してみてはいかがでしょうか。
GitHub Actions 活用の第一歩は、まず使ってみること。
BIGLOBE でも「自身の困りごとを GitHub Actions で解決しよう」という研修を開催し、活用できる人を増やそうとしています。
研修の様子は、GitHub Actionsを楽しく学ぶ:BIGLOBEとGitHub Japanのミニハッカソンレポートにて紹介していますので、よろしければご覧ください。

※ GitHubは、GitHub, Inc.の商標または登録商標です。

※ Amazon、AWS、Amazon Web Servicesは、米国その他の諸国における、Amazon.com, Inc. またはその関連会社の商標です。