[アップデート] cfn-guard-lambda (AWS CloudFormation Guard as a Lambda) が SAM CLI で簡単にデプロイ出来るようになったので試してみた

2023.07.04

いわさです。

CloudFormation の静的解析を行うことが出来るツールとして、CloudFormation Guard を使うことが出来ます。
CloudFormation Guard は CloudFormation テンプレートに限らず、Config を始め様々なポリシー評価ツールとして使うことが出来ます。

先日のアップデートで CloudFormation Guard のメジャーバージョンが更新され、これまでの 2.x.x 系から 3.x.x 系となりました。

json_parse()をルール内で解析用に使用出来るようになるなどいくつか大きなアップデートが上記アナウンスで紹介されているのですが、cfn-guard-lambdaの新しいデプロイ方法が提供されるようにもなっていました。

cfn-guard-lambdaというのは CloudFormation Guard の機能を Lambda 経由で使えるようにする単純なラッパー関数です。
通常 CloudFormation Guard はインストールして使うツールですが事前に Lambda 関数としてデプロイしておくことで Guard の解析を Lambda 上で実行することが出来ます。

これまでセットアップ手順が少し面倒だったのですが、今回のアップデートで SAM CLI を使って一発でデプロイ出来るようになっていました。
そこで、良い機会なのでcfn-guard-lambdaの初使用を兼ねてデプロイから試してみることにしました。

デプロイ

前提として SAM CLI が導入済みである必要があります。
また、cfn-guard-lambdaは Rust 製なので SAM でビルドする場合は-use-containerオプションを使う必要があるため、Docker もセットアップ済みであることが条件です。

詳細な手順は以下に記載されています。

流れとしては、CloudFormation Guard のリポジトリをクローンするとguard-lambdaというディレクトリが含まれており、そこに SAM テンプレートが用意されているので、それをそのままデプロイするだけです。

ビルド

私の環境でビルドに 10 分程度かかりました。

# クローン
% git clone https://github.com/aws-cloudformation/cloudformation-guard.git
Cloning into 'cloudformation-guard'...
remote: Enumerating objects: 5310, done.
remote: Counting objects: 100% (1684/1684), done.
remote: Compressing objects: 100% (444/444), done.
remote: Total 5310 (delta 1440), reused 1340 (delta 1237), pack-reused 3626
Receiving objects: 100% (5310/5310), 8.59 MiB | 18.21 MiB/s, done.
Resolving deltas: 100% (3713/3713), done.

# ビルド
% cd cloudformation-guard/guard-lambda/
% sam build --use-container
Starting Build inside a container                                                                                                                                                                     
Building codeuri: /Users/iwasa.takahito/work/hoge0704guard/cloudformation-guard runtime: provided.al2 metadata: {'BuildMethod': 'makefile'} architecture: x86_64 functions: CloudFormationGuardLambda 

Fetching public.ecr.aws/sam/build-provided.al2:latest-x86_64 Docker container image.............................................................................................................................................................................................................................................................................................................................................................................................................................
Mounting /Users/iwasa.takahito/work/hoge0704guard/cloudformation-guard as /tmp/samcli/source:ro,delegated, inside runtime container                                                                   

Build Succeeded

Built Artifacts  : .aws-sam/build
Built Template   : .aws-sam/build/template.yaml

Commands you can use next
=========================
[*] Validate SAM template: sam validate
[*] Invoke Function: sam local invoke
[*] Test Function in the Cloud: sam sync --stack-name {{stack-name}} --watch
[*] Deploy: sam deploy --guided
Running CustomMakeBuilder:CopySource
Running CustomMakeBuilder:MakeBuild
Current Artifacts Directory : /tmp/samcli/artifacts
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y

stable-x86_64-unknown-linux-gnu installed - rustc 1.70.0 (90c541806 2023-05-31)


Rust is installed now. Great!

To get started you may need to restart your current shell.
This would reload your PATH environment variable to include
Cargo's bin directory ($HOME/.cargo/bin).

To configure your current shell, run:
source "$HOME/.cargo/env"
source /root/.cargo/env && rustup target add x86_64-unknown-linux-musl
source /root/.cargo/env && cd guard-lambda && cargo build --release --target x86_64-unknown-linux-musl
cp -r /tmp/samcli/scratch/target/x86_64-unknown-linux-musl/release/cfn-guard-lambda /tmp/samcli/artifacts/bootstrap

デプロイ

デプロイはすぐですね。

% sam deploy --guided

Configuring SAM deploy
======================

	Looking for config file [samconfig.toml] :  Not found

	Setting default arguments for 'sam deploy'
	=========================================
	Stack Name [sam-app]: hoge0704guard
	AWS Region [ap-northeast-1]: 

:

---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Outputs                                                                                                                                                                                           
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Key                 CloudFormationGuardLambdaFunctionName                                                                                                                                         
Description         -                                                                                                                                                                             
Value               hoge0704guard-CloudFormationGuardLambda-NuOezGGmIrDS                                                                                                                          
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------


Successfully created/updated stack - hoge0704guard in ap-northeast-1

出力された関数名を使って Lambda の実行を行います。

使ってみる

今回の CloudFormation Guard 3.0 のアップデート前からcfn-guard-lambdaは使うことが出来ていましたが、使用方法を紹介したものがあまりなかったので、今回は使うところも試してみたいと思います。

次の記事を参考にサンプルルールを使って、NG のリソーステンプレートで静的解析を行い、エラーメッセージに従ってテンプレートを修正して OK の状態に治すところまで実施してみましょう。

実行方法も先ほどのデプロイ手順と同じ README に記載されています。
ポイントとしては Lambda へのペイロードで 2 つの必須パラメータを指定する点です。

ひとつめはdataです。検査対象のテンプレートを指定します。
GitHub リポジトリのサンプルでは JSON テンプレートに対して検査していました。私は今回 YAML で試してみたのですが、どちらもサポートされています。

ふたつめはrulesです。ここでルールを指定します。

先程のブログ記事の内容にあわせて、次のような Lambda ペイロードを用意しました。
ルールは EBS の暗号化とサイズをチェックする内容になっています。

テンプレートはそのルールに反した EBS リソースを 2 つ作成するものとなっています。

hoge.json

{
    "data": "Resources:\n      NewVolume:\n        Type: AWS::EC2::Volume\n        Properties:\n          Size: 500\n          Encrypted: false\n          AvailabilityZone: !Select\n            - 0\n            - Fn::GetAZs: !Ref AWS::Region\n      NewVolume2:\n        Type: AWS::EC2::Volume\n        Properties:\n          Size: 50\n          Encrypted: false\n          AvailabilityZone: !Select\n            - 0\n            - Fn::GetAZs: !Ref AWS::Region",
    "rules": [
        "let encryption_flag = true\n        AWS::EC2::Volume Properties.Encrypted == %encryption_flag\n        AWS::EC2::Volume Properties.Size <= 100"
    ]
}

上記ペイロードを Lambda 関数に引き渡してみます。

% aws lambda invoke --function-name hoge0704guard-CloudFormationGuardLambda-NuOezGGmIrDS --payload file://hoge.json --cli-binary-format raw-in-base64-out output.json
{
    "StatusCode": 200,
    "ExecutedVersion": "$LATEST"
}

出力内容を確認してみましょう。
ちょっと量が多いのでポイントのみ抜粋します。

{
  "message": [
    {
      "context": "File(rules=1)",
      "container": {
        "FileCheck": {
          "name": "lambda-payload",
          "status": "FAIL",
          "message": null
        }
      },

:

                {
                  "context": "TypeBlock#AWS::EC2::Volume/0",
                  "container": {
                    "TypeBlock": "FAIL"
                  },
                  "children": [
                    {
                      "context": "GuardAccessClause#block Properties.Encrypted EQUALS  %encryption_flag",
                      "container": {
                        "GuardClauseBlockCheck": {
                          "at_least_one_matches": false,
                          "status": "FAIL",
                          "message": null
                        }
                      },
                      "children": [
                        {
                          "context": " Properties.Encrypted EQUALS  %encryption_flag",
                          "container": {
                            "ClauseValueCheck": {
                              "Comparison": {
                                "comparison": [
                                  "Eq",
                                  false
                                ],
                                "from": {
                                  "Resolved": {
                                    "path": "/Resources/NewVolume/Properties/Encrypted",
                                    "value": false
                                  }
                                },
                                "to": {
                                  "Resolved": {
                                    "path": "",
                                    "value": true
                                  }
                                },
                                "message": null,
                                "custom_message": null,
                                "status": "FAIL"
                              }
                            }
                          },
                          "children": []
                        }
                      ]
                    }
                  ]
                },

:

                {
                  "context": "TypeBlock#AWS::EC2::Volume/0",
                  "container": {
                    "TypeBlock": "FAIL"
                  },
                  "children": [
                    {
                      "context": "GuardAccessClause#block Properties.Size LESS THAN EQUALS  100",
                      "container": {
                        "GuardClauseBlockCheck": {
                          "at_least_one_matches": false,
                          "status": "FAIL",
                          "message": null
                        }
                      },
                      "children": [
                        {
                          "context": " Properties.Size LESS THAN EQUALS  100",
                          "container": {
                            "ClauseValueCheck": {
                              "Comparison": {
                                "comparison": [
                                  "Le",
                                  false
                                ],
                                "from": {
                                  "Resolved": {
                                    "path": "/Resources/NewVolume/Properties/Size",
                                    "value": 500
                                  }
                                },
                                "to": {
                                  "Resolved": {
                                    "path": "",
                                    "value": 100
                                  }
                                },
                                "message": null,
                                "custom_message": null,
                                "status": "FAIL"
                              }
                            }
                          },
                          "children": []
                        }
                      ]
                    }
                  ]
                }

:

良いですね。
エラーが検出されています。

ではエラー内容に従ってテンプレートを修正してみます。
ストレージサイズを 500 GB から 50 GB に変更し、ディスク暗号化も有効にします。

hoge.json

{
    "data": "Resources:\n      NewVolume:\n        Type: AWS::EC2::Volume\n        Properties:\n          Size: 50\n          Encrypted: true\n          AvailabilityZone: !Select\n            - 0\n            - Fn::GetAZs: !Ref AWS::Region\n      NewVolume2:\n        Type: AWS::EC2::Volume\n        Properties:\n          Size: 50\n          Encrypted: true\n          AvailabilityZone: !Select\n            - 0\n            - Fn::GetAZs: !Ref AWS::Region",
    "rules": [
        "let encryption_flag = true\n        AWS::EC2::Volume Properties.Encrypted == %encryption_flag\n        AWS::EC2::Volume Properties.Size <= 100"
    ]
}

もう一度実行してみます。

{
    "message": [
      {
        "context": "File(rules=1)",
        "container": {
          "FileCheck": {
            "name": "lambda-payload",
            "status": "PASS",
            "message": null
          }
        },

:

                "children": [
                  {
                    "context": "Filter/Map#1",
                    "container": {
                      "Filter": "PASS"
                    },
                    "children": [
                      {
                        "context": "GuardAccessClause#block Type EQUALS  \"AWS::EC2::Volume\"",
                        "container": {
                          "GuardClauseBlockCheck": {
                            "at_least_one_matches": false,
                            "status": "PASS",
                            "message": null
                          }
                        },
                        "children": [
                          {
                            "context": " Type EQUALS  \"AWS::EC2::Volume\"",
                            "container": {
                              "ClauseValueCheck": "Success"
                            },
                            "children": []
                          }
                        ]
                      }
                    ]
                  },

:

                  {
                    "context": "TypeBlock#AWS::EC2::Volume/0",
                    "container": {
                      "TypeBlock": "PASS"
                    },
                    "children": [
                      {
                        "context": "GuardAccessClause#block Properties.Encrypted EQUALS  %encryption_flag",
                        "container": {
                          "GuardClauseBlockCheck": {
                            "at_least_one_matches": false,
                            "status": "PASS",
                            "message": null
                          }
                        },
                        "children": [
                          {
                            "context": " Properties.Encrypted EQUALS  %encryption_flag",
                            "container": {
                              "ClauseValueCheck": "Success"
                            },
                            "children": []
                          }
                        ]
                      }
                    ]
                  },

:

エラーが解消され、ステータスがPASSになることが確認出来ました。

さいごに

本日は cfn-guard-lambda (AWS CloudFormation Guard as a Lambda) が SAM CLI で簡単にデプロイ出来るようになっていたので、初めて cfn-guard-lambda を使ってみました。

Lambda ペイロードとして色々とインプットする関係で、普通にユーザーが使う分にはツールで使ったほうがわかりやすい気がしますね。
ただ、API とかで実行したくて実行環境のセットアップが難しい場合は出番がありそうです。
導入も簡単になったので使ってみてください。