TECHSTEP

ITインフラ関連の記事を公開してます。

IaCの継続的テストを検証する: Amazon S3に対するテスト

背景

以前IaCの書籍をいくつか取り上げ、IaCのテストをどう実行すべきか調査したことがありました。書籍の中には具体的なツールや例を取り上げて記載されている箇所もありますが、やはり実際に動かしてみることで見えることもあると思い、今回簡単に検証してみました。

なお、今回はなるべく特定の種類のリソースに制限せず、幅広いリソースで適用できるようなテストを選択しました。

テストの観点

今回は、書籍の内容をベースに、以下の観点から検証内容を選定しています。

利用するリソースの大部分をコードで管理する

大前提として、テスト対象のインフラリソースはIaCで管理します。またインフラリソースだけでなく、テスト等で利用するリソースなどもできる限りコードで管理することが推奨されます。

今回は以下のリソースをコードで管理しています。

  • テスト対象のAWSリソース: Amazon S3バケット
  • CI/CDジョブ: GitHub Actionsのworkflow定義ファイル
  • その他CI/CDに必要なリソース: IAMなど

ただしGitHubリポジトリの作成、GitHubリポジトリの設定、および更新対象のCloudFormation Stackの新規作成は手動で行っています。

継続的なテストとデプロイを自動化プロセスの中で実行する

インフラリソースをより安全に変更するには、IaCコードでリソースを管理し、変更はすべてIaCを介して実行することが推奨されます。またIaCファイルでの管理だけでなく、適用前のテストの実行とデプロイ、そしてそれらをすべて自動化プロセスの中で実現することが必要です。

今回は、まずIaCコードをGItHub上で管理し、IaCコードへのテスト・デプロイをGitHub ActionsによるCI/CDで実行しました。またCI/CDの起動するトリガーには、GitHub上でのPull Requestの作成とマージを選択しました。

なお、ブランチ戦略は特に考慮していませんが、GitHub Flowのようなシンプルな形にしています。

テスト内容は静的解析中心とする

IaCのテストの実行は、いくつかの課題があることが指摘されています。今回はそれらをできる限り回避するため、静的解析 (オフラインテスト) を中心に実施しています。

まず、CI/CDで実施するテストは、短時間で済むことが理想です。これに対しIaCのテストは、一度環境にリソースを作成する必要があるため、長く時間がかかりがちになります。そのためIaCへのテストでは、短時間で完了する静的解析の重要性はより高いと考えています。また静的解析は扱うリソースの違いが影響することは少ないため、今回は静的解析を中心にテストを実行しています。

さらに静的解析に加え、IaCファイルをより安全に適用するため、リソース変更ドリフトのチェックを追加しました。IaCで管理したリソース特有の問題として、IaCの記載と実際のリソースの設定が異なった際のエラー・予期せぬ変更による障害発生などがあります。そのため、IaCを変更した際のチェック項目に、更新対象のリソース・Stackがドリフトを起こしていないかチェックするポイントを追加しています。

具体的なテスト項目は大きく3つになります。

  • コード解析: IaCファイルの構文チェック、セキュリティチェック
  • 更新内容のプレビュー: 変更したIaCファイルを適用したときの更新内容の確認
  • ドリフトのチェック: ドリフトがあればWorkflowを停止

検証

ここから実際にIaC + CI/CDの例として検証したものを紹介します。今回はAmazon S3をターゲットに、GitHub Actionsを使ってCI/CDを設定し、IaCの継続的なテストとデプロイをどう実践するか、検証しました。

環境

今回の検証環境は以下の通りです。

  • 環境: AWS
  • IaCツール: AWS CloudFormation
  • CI/CDツール: GitHub Actions
  • テストツール: cfn-lint trivy

構成は以下の通り非常にシンプルです。

処理の流れ

処理の流れは以下の通りです。

  • (更新対象のCloudFormation Stackはあらかじめ作成しておく)
  • Amazon S3を定義するIaCファイルを更新し、GitHub上でPRを作成する
  • PRの作成をトリガーに、GitHub ActionsがCIを実行する
    • コード解析
    • ドリフトのチェック
    • Change setの作成
  • CIの結果を確認し、問題なければマージする
  • マージをトリガーにGitHub ActionsがCDを実行する
    • CIと同様の処理
    • Change setの適用

使用したIaC等のコードは以下に載せておきます。なおGitHubリポジトリ上のディレクトリ構成は以下の通りです。

.
├── .github
│   └── workflows
│       └── s3-github-actions-check.yaml
├── README.md
├── iam.yaml
└── s3-github-actions-check
    └── s3.yaml

s3-github-actions-check.yaml

name: IaC check & apply at Pull Request

on:
  pull_request:
    types: 
      - opened
      - synchronize
      - closed
    paths:
      - s3-github-actions-check/**

permissions:
  id-token: write
  contents: read

env:
  AWS_REGION: ap-northeast-1
  AWS_IAM_ROLE: ${{ secrets.AWS_IAM_ROLE }}
  AWS_STACK_NAME: s3-github-actions-check

jobs:
  pr:
    runs-on: ubuntu-22.04
    if: >-
      github.event.pull_request.state == 'open' || 
      github.event.pull_request.merged == true
    steps:
      - uses: actions/checkout@v3
      - name: Configure AWS Credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: ${{ env.AWS_IAM_ROLE }}
          aws-region: ${{ env.AWS_REGION }}
      - name: Testing with CFN Lint Command
        uses: scottbrenner/cfn-lint-action@v2
        with:
          command: cfn-lint -t ./s3-github-actions-check/s3.yaml
      - name: Testing with Trivy
        uses: aquasecurity/trivy-action@master
        with:
          scan-type: 'config'
          exit-code: '1'
          severity: 'CRITICAL'
      - name: Drift Detection
        run: |
          STACK_DRIFT_DETECT_ID=`aws cloudformation detect-stack-drift --stack-name ${{ env.AWS_STACK_NAME }} --query 'StackDriftDetectionId' --output text`
          sleep 10s
          DRIFT_STATUS=`aws cloudformation describe-stack-drift-detection-status --stack-drift-detection-id $STACK_DRIFT_DETECT_ID --query 'StackDriftStatus' --output text`
          echo "DRIFT_STATUS=${DRIFT_STATUS}" >> $GITHUB_ENV
      - name: Stop workflow when drift detected
        if: contains(env.DRIFT_STATUS, 'IN_SYNC') == false
        run: |
          aws cloudformation describe-stack-resource-drifts --stack-name ${{ env.AWS_STACK_NAME }}
          exit 1
      - name: Create changeset
        run: |
          CHANGE_SET_NAME="changeset-$(date "+%Y%m%d-%H%M%S")"
          echo "CHANGE_SET_NAME=${CHANGE_SET_NAME}" >> $GITHUB_ENV
          aws cloudformation create-change-set --template-body file://s3-github-actions-check/s3.yaml --stack-name ${{ env.AWS_STACK_NAME }} --change-set-name $CHANGE_SET_NAME
          aws cloudformation wait change-set-create-complete --stack-name ${{ env.AWS_STACK_NAME }} --change-set-name $CHANGE_SET_NAME
          aws cloudformation describe-change-set --stack-name ${{ env.AWS_STACK_NAME }} --change-set-name $CHANGE_SET_NAME
      - name: Execute changeset
        if: github.event.pull_request.merged == true
        run: |
          aws cloudformation execute-change-set --change-set-name ${{ env.CHANGE_SET_NAME }} --stack-name ${{ env.AWS_STACK_NAME }}
          aws cloudformation wait stack-update-complete --stack-name ${{ env.AWS_STACK_NAME }}
          aws cloudformation describe-stacks --stack-name ${{ env.AWS_STACK_NAME }} --query 'Stacks[*].StackStatus' --output text

iam.yaml

AWSTemplateFormatVersion: "2010-09-09"
Parameters:
  EnvName:
    Type: String
    Default: "iac-cicd-examples"

Resources:
  # OIDC Providerは作成済みのためコメントアウト
  #OIDCProvider:
  #  Type: AWS::IAM::OIDCProvider
  #  Properties:
  #    Url: https://token.actions.githubusercontent.com
  #    ClientIdList:
  #      - sts.amazonaws.com
  #    ThumbprintList:
  #      - ffffffffffffffffffffffffffffffffffffffff
  IAMRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub ${EnvName}-role
      AssumeRolePolicyDocument:
        Statement:
          - Effect: Allow
            Action: sts:AssumeRoleWithWebIdentity
            Principal:
              Federated: !Sub arn:aws:iam::${AWS::AccountId}:oidc-provider/token.actions.githubusercontent.com
            Condition:
              StringEquals:
                token.actions.githubusercontent.com:aud: sts.amazonaws.com
              StringLike:
                token.actions.githubusercontent.com:sub: repo:<GitHubユーザー名>/<GitHubリポジトリ>:*
      Policies:
        - PolicyName: !Sub ${EnvName}-policy
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Effect: Allow
                Action:
                  - cloudformation:*
                  - s3:*
                Resource: '*'

検証

上記仕組みを検証するため、いくつかのパターンを試しました。

S3に設定を追加

まずは通常通りS3のIaCファイルを修正し、変更内容をAWSに適用します。今回はオブジェクトのバージョニングを有効にするよう以下のような定義を追加しています。

    Properties:
      BucketName: !Ref BucketName
      VersioningConfiguration:
        Status: Enabled

上記変更後にPRを作成すると、GitHub Actionsが起動し、問題なく完了することを確認できます。この時はドリフトもない状態でWorkflowを実行したため、該当の箇所をスキップしてChange setの作成を行っています。

なおこの時のChange setの内容を一部取り出すと、想定通りバージョニングの設定が追加されることも確認できます。

{
    "Changes": [
        {
            "Type": "Resource",
            "ResourceChange": {
                "Action": "Modify",
                "LogicalResourceId": "S3",
                "PhysicalResourceId": "s3-github-actions-check",
                "ResourceType": "AWS::S3::Bucket",
                "Replacement": "False",
                "Scope": [
                    "Properties",
                    "Tags"
                ],
                "Details": [
                    {
                        "Target": {
                            "Attribute": "Properties",
                            "Name": "VersioningConfiguration",
                            "RequiresRecreation": "Never"
                        },
                        "Evaluation": "Static",
                        "ChangeSource": "DirectModification"
                    }
ドリフト発生時の挙動

次に、AWS上のS3バケットの設定を手動で変更し、CloudFormation Stackと差分を作成した状態でCIを実行しました。ここでは先ほど追加したオブジェクトバージョニングの設定を事前に修正した状態でCIを実行します。

この時はCloudFormation Stackがドリフト状態になっているため、CIの Stop workflow when drift detected のstepでworkflowを停止しています。

ドリフトの内容も想定通り出力されていました。

リソースを置き換える種類の変更

最後に、既存のリソースを更新するのでなく、新規にリソースを作成する種類の変更を試しました。AWSに限らずですが、IaCの定義ファイルは、修正箇所によって更新方法が変わります。特にリソースを新規に作成する Replacement タイプの修正は、変更時の影響も大きいため、CIの時点で検知したいところです。

ここではS3バケット名を修正することで、リソースを作り直すようにしました。

ここではCIは問題なく成功しますが、実行結果を確認すると以下のような出力があり、この修正がリソースの再作成を伴うことがわかります。

参考リンク