プルリクエストを作ったタイミングで、AWS SAM のサンドボックス環境を作成する

プルリクエストを作ったタイミングで、AWS SAM のサンドボックス環境を作成する

Clock Icon2025.04.21

こんにちは!クラウド事業本部コンサルティング部のたかくに(@takakuni_)です。

Step Functions のデプロイ管理、みなさんどのように運用していますでしょうか。

プルリクエストを作ったタイミングで、もう1つ環境一式が欲しくなったケースないでしょうか。

事の発端

とあるプロダクトで Step Functions を使うことになったのですが、ほとんどの Actions に Lambda を使うことが想定される/sam local といったローカルでのテストが便利/AWS Toolkit を使ったワークフローの定義のしやすさの観点から、AWS SAM を採用することにしました。

ステートマシンのイメージは次のとおりで、並列で動かす関数と集計関数の 2 種類に分かれます。

Untitled(157).png

新機能の追加は並列処理内に関数を増やしていく運用をとっており、私は次の課題にぶつかりました。(主に 1 つのステートマシンを共有している部分に課題がありました。)

  • 新機能の開発に次のステップを踏む必要がある
    • 並列処理内の関数を開発し、ステートマシンにデプロイする必要がある
    • 並列処理を実装した後にステートマシンを実行、テストデータを取得してから集計関数の実装が必要
  • 1 つのステートマシンを複数人で共有している
    • 互いの関数に依存があるわけではないが、心理的に統合テストした状態でステートマシンの反映をしたい
    • 並列関数から、集計関数まで一気通貫で実装したい

そこでプルリクエストを作成したタイミングで、サンドボックス用ステートマシンを作成するワークフローファイルを作成しました。

Untitled(157) (2).png

ワークフロー

作成したワークフローは以下のとおりです。

name: Deploy-PR-Environment

on:
  pull_request:
    branches:
      - develop
    types: [opened, reopened, synchronize, closed]
    paths:
      - 'functions/**/*'
      - 'layers/**/*'
      - 'statemachine/statemachine.asl.json' # 適宜変更
      - 'samconfig.toml'
      - 'template.yaml'

env:
  TEMPLATE_FILE: template.yaml
  SAM_CLI_TELEMETRY: 0

permissions:
  id-token: write
  contents: write
  pull-requests: write

jobs:
  deploy:
    runs-on: ubuntu-22.04
    permissions:
      id-token: write
      contents: write
    if: github.event.action != 'closed'
    steps:
      - name: Checkout
        uses: actions/checkout@v4
      - name: Setup Python
        uses: actions/setup-python@v5
        with:
          python-version: '3.12'
      - name: Install AWS SAM CLI
        uses: aws-actions/setup-sam@v2
        with:
          use-installer: true
      - name: Configure AWS Credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123456789012:role/github-actions-role
          aws-region: ap-northeast-1
          role-session-name: SamDeploy
      - name: SAM Deploy
        run: |
          # Replace invalid characters with hyphens in branch name for valid stack name
          # CloudFormation stack name must match pattern: [a-zA-Z][-a-zA-Z0-9]*
          STACK_NAME="$(echo ${{ github.head_ref }} | sed -E 's/[^a-zA-Z0-9]/-/g')"
          
          # Check if stack exists and is in ROLLBACK_COMPLETE state
          if aws cloudformation describe-stacks --stack-name "$STACK_NAME" 2>/dev/null | grep -q "ROLLBACK_COMPLETE"; then
            echo "Stack is in ROLLBACK_COMPLETE state. Deleting it before redeploying..."
            aws cloudformation delete-stack --stack-name "$STACK_NAME"
            # Wait for stack deletion to complete
            aws cloudformation wait stack-delete-complete --stack-name "$STACK_NAME"
          fi
          
          sam build
          sam deploy \
            --stack-name "$STACK_NAME" \
            --no-confirm-changeset \
            --no-fail-on-empty-changeset \
            --capabilities CAPABILITY_NAMED_IAM

  cleanup:
    runs-on: ubuntu-22.04
    permissions:
      id-token: write
      contents: write
    if: github.event.action == 'closed'
    steps:
      - name: Checkout
        uses: actions/checkout@v4
      - name: Setup Python
        uses: actions/setup-python@v5
        with:
          python-version: '3.12'
      - name: Install AWS SAM CLI
        uses: aws-actions/setup-sam@v2
        with:
          use-installer: true
      - name: Configure AWS Credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123456789012:role/github-actions-role
          aws-region: ap-northeast-1
          role-session-name: SamDelete
      - name: Empty S3 Buckets and Delete Stack
        run: |
          # Replace invalid characters with hyphens in branch name for valid stack name
          # CloudFormation stack name must match pattern: [a-zA-Z][-a-zA-Z0-9]*
          STACK_NAME="$(echo ${{ github.head_ref }} | sed -E 's/[^a-zA-Z0-9]/-/g')"
          
          # Get bucket names from CloudFormation outputs 適宜変更
          BUCKET=$(aws cloudformation describe-stacks --stack-name "$STACK_NAME" --query 'Stacks[0].Outputs[?OutputKey==`BucketName`].OutputValue' --output text)

          # Empty the buckets if they exist and are not empty
          if [ ! -z "$BUCKET" ]; then
            echo "Emptying billing input bucket: $BUCKET"
            aws s3 rm s3://$BUCKET --recursive
          fi

          # Delete the stack
          aws cloudformation delete-stack --stack-name "$STACK_NAME"
          # Wait for stack deletion to complete
          aws cloudformation wait stack-delete-complete --stack-name "$STACK_NAME"
          echo "Stack $STACK_NAME deleted successfully."

スタック名

スタック名はブランチの内容をもとに作成します。!Sub arn:aws:s3:::${AWS::StackName} のように、リソースの命名規則にスタック名を埋め込む際に、あまりにも長いブランチ名の場合、文字数超過でエラーが発生しますので、ご注意ください。

      - name: SAM Deploy
        run: |
          # Replace invalid characters with hyphens in branch name for valid stack name
          # CloudFormation stack name must match pattern: [a-zA-Z][-a-zA-Z0-9]*
          STACK_NAME="$(echo ${{ github.head_ref }} | sed -E 's/[^a-zA-Z0-9]/-/g')"

ROLLBACK_COMPLETE なスタックの存在をチェック

スタックをデプロイする前に ROLLBACK_COMPLETE なスタックが存在するかどうかをチェックします。

構文エラーなど、何かしらのエラーが発生した場合、スタックはロールバックを試みます。初回デプロイ時にロールバックが完了したスタックは ROLLBACK_COMPLETE 状態になります。(ちなみに2回目以降のロールバックが完了したスタックは UPDATE_ROLLBACK_COMPLETE 状態になります。)

ROLLBACK_COMPLETE なスタックは、再度スタックを削除するまたは、自動ロールバックを無効にしたデプロイが求められます。今回は、冪等性を担保したいため、前者のスタックを削除したデプロイを採用しました。

      - name: SAM Deploy
        run: |
          # Check if stack exists and is in ROLLBACK_COMPLETE state
          if aws cloudformation describe-stacks --stack-name "$STACK_NAME" 2>/dev/null | grep -q "ROLLBACK_COMPLETE"; then
            echo "Stack is in ROLLBACK_COMPLETE state. Deleting it before redeploying..."
            aws cloudformation delete-stack --stack-name "$STACK_NAME"
            # Wait for stack deletion to complete
            aws cloudformation wait stack-delete-complete --stack-name "$STACK_NAME"
          fi

https://repost.aws/ja/questions/QUjl_fJ_-bQEW-_a-i6qCVog/rollback-complete-state-and-can-not-be-updated

S3 バケットの削除(オプション)

S3 バケットや ECR リポジトリなどは中身(オブジェクトやコンテナイメージ)が入っている場合、削除できません。

そこで、スタックを削除する前に S3 の中身を空にする処理を組み込んでいます。S3 バケットとやりとりする必要があるアプリケーションの場合は、必要に応じて以下の処理を入れておきましょう。(バケットの情報は Output を通じてやりとりしています)

      - name: Empty S3 Buckets and Delete Stack
        run: |
          # Replace invalid characters with hyphens in branch name for valid stack name
          # CloudFormation stack name must match pattern: [a-zA-Z][-a-zA-Z0-9]*
          STACK_NAME="$(echo ${{ github.head_ref }} | sed -E 's/[^a-zA-Z0-9]/-/g')"
          
          # Get bucket names from CloudFormation outputs 適宜変更
          BUCKET=$(aws cloudformation describe-stacks --stack-name "$STACK_NAME" --query 'Stacks[0].Outputs[?OutputKey==`BucketName`].OutputValue' --output text)

          # Empty the buckets if they exist and are not empty
          if [ ! -z "$BUCKET" ]; then
            echo "Emptying billing input bucket: $BUCKET"
            aws s3 rm s3://$BUCKET --recursive
          fi

S3 バケット内の処理を空にする Actions として cls3 もあげられます。集計処理など大量のオブジェクトを扱う部分ではこちらの方が、Actions の実行時間を短縮できるためオススメです。

https://go-to-k.hatenablog.com/entry/cls3#GitHub-Actions

https://github.com/marketplace/actions/cls3-action

CloudFormation の Delete Stack

sam delte では無くあえて、CloudFormation の delete-stack を採用しています。

これは、sam delete の場合、キャッシュ領域のコードまで削除されてしまうため、マージ後の sam deploy が遅くなってしまうのを防ぎたかったからです。

          # Delete the stack
          aws cloudformation delete-stack --stack-name "$STACK_NAME"
          # Wait for stack deletion to complete
          aws cloudformation wait stack-delete-complete --stack-name "$STACK_NAME"
          echo "Stack $STACK_NAME deleted successfully."

キャッシュを利用した高速化は非常に有効ですが、不要なファイルが溜まり続けないよう、ライフサイクルルールを設定しておきましょう。

まとめ

「プルリクエストを作ったタイミングで、AWS SAM のサンドボックス環境を作成する」でした。

ステートマシンの変更量が少ない場合は、あまり刺さらないかもですが参考になれば幸いです。

クラウド事業本部コンサルティング部のたかくに(@takakuni_)でした!

Share this article

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.