TECHSTEP

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

【メモ】AWS CloudFormation Guardを試す

CloudFormation Guardとは

AWS CloudFormation Guard (以降cfn-guard) は、汎用的なPolicy as Codeを実現するOpen Sourceツールの一つです。cfn-guardはDomain-Specific Language (DSL) でルールを記載し、テスト対象のコードがルールに従うかをチェックします。チェック時はDSLでルールを記載したファイルとテスト対象のテンプレートを指定し、 cfn-guard コマンドから実行します。なお名称にCloudFormationとありますが、TerraformやKubernetesのファイルに対しても利用できます。

なお今回は検証しませんでしたが、cfn-guardは、作成したルールファイルが想定通り動作するかをテストしたり、AWS Config で、AWS CloudFormation Guardを使用したカスタム AWS Config ルールを作成することもできます。

※参考:

cfn-guardを検証する

ここから検証をします。今回は AWS Cloud9 環境を利用しました。

インストール

cfn-guardのインストールは複数オプション用意されていますが、今回は以下のコマンドを実行してインストールします。インストール後はPATHの追加も忘れずに行います。

# インストール
$ curl --proto '=https' --tlsv1.2 -sSf https://raw.githubusercontent.com/aws-cloudformation/cloudformation-guard/main/install-guard.sh | sh

cfn-guard 2.1.3

(以降割愛)

Remember to SET PATH include PATH=${PATH}:~/.guard/bin

# PATHの追加
$ export PATH=${PATH}:~/.guard/bin

# コマンド確認
$ cfn-guard -V
cfn-guard 2.1.3

rulegen

まずはcfn-guardの動作を確認するため、 rulegen というコマンドを使用します。これは既存のテンプレートからcfn-guardルールファイルを生成するコマンドで、ここでは以下のテンプレートを使用しています。

AWSTemplateFormatVersion: '2010-09-09'
Description: test for cfn-guard

Resources:
  ALB:
    Type: AWS::ElasticLoadBalancingV2::LoadBalancer
    Properties:
      Type: "application"
      Scheme: "internet-facing"
      Name: "test-alb"
      IpAddressType: ipv4
      Subnets: 
        - "subnet-1234abcd1234abcd0"
        - "subnet-5678efgh5678efgh0"
      SecurityGroups: 
        - "sg-9012ijkl9012ijkl0"

上記テンプレートを使って rulegen コマンドを実行します。

$ cfn-guard rulegen -t test-alb.yaml -o test-elb.guard
$ 

上記コマンドを実行すると、test-elb.guard というファイルが生成されました。

let aws_elasticloadbalancingv2_loadbalancer_resources = Resources.*[ Type == 'AWS::ElasticLoadBalancingV2::LoadBalancer' ]
rule aws_elasticloadbalancingv2_loadbalancer when %aws_elasticloadbalancingv2_loadbalancer_resources !empty {
  %aws_elasticloadbalancingv2_loadbalancer_resources.Properties.IpAddressType == "ipv4"
  %aws_elasticloadbalancingv2_loadbalancer_resources.Properties.Subnets == ["subnet-1234abcd1234abcd0","subnet-5678efgh5678efgh0"]
  %aws_elasticloadbalancingv2_loadbalancer_resources.Properties.Scheme == "internet-facing"
  %aws_elasticloadbalancingv2_loadbalancer_resources.Properties.SecurityGroups == ["sg-9012ijkl9012ijkl0"]
  %aws_elasticloadbalancingv2_loadbalancer_resources.Properties.Type == "application"
  %aws_elasticloadbalancingv2_loadbalancer_resources.Properties.Name == "test-alb"
}

上記ルールファイルの読み方を簡単に残しておきます。

  • let ~: ここでは aws_elasticloadbalancingv2_loadbalancer_resources という変数に、テスト対象のAWSリソースを定義しています。
  • rule ~ when ~ !empty: aws_elasticloadbalancingv2_loadbalancer という名称のルールを定義します。前述した aws_elasticloadbalancingv2_loadbalancer_resources に該当するリソースが存在すれば、以降のルールを適用します。
  • %aws_elasticloadbalancingv2_loadbalancer_resources.Properties ~: AWS ELBの各パラメータが一致するかをチェックします。

cfn-guardDSLの記法、詳細は公式ドキュメントに記載されています。

validate

続いて、用意したルールファイルを使ってテスト対象のテンプレートを評価する validate コマンドを実行します。

まず前項で生成したルールファイルをそのまま使うと、ルールファイルとテンプレートの内容が一致しているため、エラーは検出されずパスします。

$ cfn-guard validate -d test-alb.yaml -r test-elb.guard
$ 

ここでエラーを返すよう、テンプレートファイルの一部パラメータを変更します。

Resources:
  ALB:
    Type: AWS::ElasticLoadBalancingV2::LoadBalancer
    Properties:
      Type: "application"
      Scheme: "internet-facing"
      Name: "test-alb-2" # Nameを変更

テンプレートを変更後に改めて validate コマンドを実行すると、今度は以下のようなエラーが検出されます。エラーの内容も、修正した Properties.Name を比較して値が異なる旨が表示されています。

$ cfn-guard validate -d test-alb.yaml -r test-elb.guard 
test-alb.yaml Status = FAIL
FAILED rules
test-elb.guard/aws_elasticloadbalancingv2_loadbalancer    FAIL
---
Evaluating data test-alb.yaml against rules test-elb.guard
Number of non-compliant resources 1
Resource = ALB/Properties {
  Type      = application
  Rule = aws_elasticloadbalancingv2_loadbalancer {
    ALL {
      Check =  %aws_elasticloadbalancingv2_loadbalancer_resources[*].Properties.Name EQUALS  "test-alb" {
        ComparisonError {
          Error            = Check was not compliant as property value [Path=/Resources/ALB/Properties/Name[L:9,C:12] Value="test-alb-2"] not equal to value [Path=[L:0,C:0] Value="test-alb"].
          PropertyPath    = /Resources/ALB/Properties/Name[L:9,C:12]
          Operator        = EQUAL
          Value           = "test-alb-2"
          ComparedWith    = "test-alb"
          Code:
                7.    Properties:
                8.      Type: "application"
                9.      Scheme: "internet-facing"
               10.      Name: "test-alb-2"
               11.      IpAddressType: ipv4
               12.      Subnets: 

        }
      }
    }
  }
}

AWS ELBのログ有効化をテストする

今回はルールファイルの書き方をもう少し知るために、AWS ELBに対する追加のテストを作成しました。

AWS ELBはログ出力を有効にするか否か、 LoadBalancerAttributesaccess_logs.s3.enabled というパラメータで制御をします。今回はこれを有効にしているかチェックするルールを作成してみます。

以下のようなテンプレートを用意し、これに対して有効なルールを設定します。

AWSTemplateFormatVersion: '2010-09-09'
Description: test for cfn-guard

Resources:
  ALB:
    Type: AWS::ElasticLoadBalancingV2::LoadBalancer
    Properties:
      Type: "application"
      Scheme: "internet-facing"
      Name: "test-alb"
      IpAddressType: ipv4
      Subnets: 
        - "subnet-1234abcd1234abcd0"
        - "subnet-5678efgh5678efgh0"
      SecurityGroups: 
        - "sg-9012ijkl9012ijkl0"
      LoadBalancerAttributes:
        - Key: access_logs.s3.enabled
          Value: "true"

上記テンプレートに対して access_logs.s3.enabled というパラメータを評価する際、cfn-guard はKey:Value形式のデータに対してもチェックが可能なため、以下のようなルールを利用できます。

let aws_elasticloadbalancingv2_loadbalancer_resources = Resources.*[ Type == 'AWS::ElasticLoadBalancingV2::LoadBalancer' ]
rule aws_elasticloadbalancingv2_loadbalancer when %aws_elasticloadbalancingv2_loadbalancer_resources !empty {
  %aws_elasticloadbalancingv2_loadbalancer_resources.Properties.LoadBalancerAttributes == [{"Key":"access_logs.s3.enabled","Value":"true"}]
}

ただ上記ルールではいくつか問題があり、実用的とは言えない状態です。

  • access_logs.s3.enabled 以外のパラメータが存在する時エラー判定になる
  • LoadBalancerAttributes に2つ以上のパラメータが存在する時エラー判定になる

まず access_logs.s3.enabled 以外のパラメータが LoadBalancerAttributes に設定されていると、このルールはエラーと判定してしまいます。LoadBalancerAttributes はELBのログの出力先バケットの指定やログ以外の設定などもできるため、上記ルールは使える場面がかなり限定されます。

これに対しては正規表現を使うことで解消することもできます。 cfn-guard正規表現にも対応しており、以下のような書き方をすると access_logs.s3.enabled 以外は何であれパスすることもできます。

let aws_elasticloadbalancingv2_loadbalancer_resources = Resources.*[ Type == 'AWS::ElasticLoadBalancingV2::LoadBalancer' ]
rule aws_elasticloadbalancingv2_loadbalancer when %aws_elasticloadbalancingv2_loadbalancer_resources !empty {
  %aws_elasticloadbalancingv2_loadbalancer_resources.Properties.LoadBalancerAttributes == [{"Key":"access_logs.s3.enabled","Value":"true"},{"Key":/.*/,"Value":/.*/}]
}

ただしこの書き方にも問題があり、LoadBalancerAttributes にパラメータが3つ以上あるとエラーになります。

$ cfn-guard validate -d test-alb.yaml -r test-alb.guard  -S
test-alb.yaml Status = FAIL
FAILED rules
test-alb.guard/aws_elasticloadbalancingv2_loadbalancer    FAIL
---
Evaluating data test-alb.yaml against rules test-alb.guard
Number of non-compliant resources 1
Resource = ALB/Properties {
  Type      = application
  Rule = aws_elasticloadbalancingv2_loadbalancer {
    ALL {
      Check =  %aws_elasticloadbalancingv2_loadbalancer_resources[*].Properties.LoadBalancerAttributes EQUALS  [{"Key":"access_logs.s3.enabled","Value":"true"},{"Key":"/.*/","Value":"/.*/"}] {
        ComparisonError {
          Error            = Check was not compliant as property value [Path=/Resources/ALB/Properties/LoadBalancerAttributes[L:17,C:8] Value=[{"Key":"access_logs.s3.enabled","Value":"false"},{"Key":"access_logs.s3.bucket","Value":"test-bucket-20230529"},{"Key":"access_logs.s3.prefix","Value":"test-"}]] not equal to value [Path=[L:0,C:0] Value=[{"Key":"access_logs.s3.enabled","Value":"true"},{"Key":"/.*/","Value":"/.*/"}]].
          PropertyPath    = /Resources/ALB/Properties/LoadBalancerAttributes[L:17,C:8]
          Operator        = EQUAL
          Value           = [{"Key":"access_logs.s3.enabled","Value":"false"},{"Key":"access_logs.s3.bucket","Value":"test-bucket-20230529"},{"Key":"access_logs.s3.prefix","Value":"test-"}]
          ComparedWith    = [{"Key":"access_logs.s3.enabled","Value":"true"},{"Key":"/.*/","Value":"/.*/"}]
          Code:
               15.      SecurityGroups: 
               16.        - "sg-9012ijkl9012ijkl0"
               17.      LoadBalancerAttributes:
               18.        - Key: access_logs.s3.enabled
               19.          Value: "false"
               20.        - Key: access_logs.s3.bucket

        }
      }
    }
  }
}

この問題を解消するため、ここでは some を使ってみます。 cfn-guardsome を使うと、該当するものが含まれていればパスする、という書き方ができます。

let aws_elasticloadbalancingv2_loadbalancer_resources = Resources.*[ Type == 'AWS::ElasticLoadBalancingV2::LoadBalancer' ]
rule aws_elasticloadbalancingv2_loadbalancer when %aws_elasticloadbalancingv2_loadbalancer_resources !empty {
  some %aws_elasticloadbalancingv2_loadbalancer_resources.Properties.LoadBalancerAttributes[*] == {"Key":"access_logs.s3.enabled","Value":"true"}
}

このルールの場合は 他のパラメータが複数あっても access_logs.s3.enabledtrue であればパスされます。

また access_logs.s3.enabledValuefalse に変更すると、以下の通りエラーを返すことも確認できます。

$ cfn-guard validate -d test-alb.yaml -r test-alb-2.guard  -S

test-alb.yaml Status = FAIL
FAILED rules
test-alb-2.guard/aws_elasticloadbalancingv2_loadbalancer    FAIL
---
Evaluating data test-alb.yaml against rules test-alb-2.guard
Number of non-compliant resources 1
Resource = ALB/Properties {
  Type      = application
  Rule = aws_elasticloadbalancingv2_loadbalancer {
    ALL {
      Check =  %aws_elasticloadbalancingv2_loadbalancer_resources[*].Properties.LoadBalancerAttributes[*] EQUALS  {"Key":"access_logs.s3.enabled","Value":"true"} {
        ComparisonError {
          Error            = Check was not compliant as property value [Path=/Resources/ALB/Properties/LoadBalancerAttributes/0[L:17,C:10] Value={"Key":"access_logs.s3.enabled","Value":"false"}] not equal to value [Path=[L:0,C:0] Value={"Key":"access_logs.s3.enabled","Value":"true"}].
          PropertyPath    = /Resources/ALB/Properties/LoadBalancerAttributes/0[L:17,C:10]
          Operator        = EQUAL
          Value           = {"Key":"access_logs.s3.enabled","Value":"false"}
          ComparedWith    = {"Key":"access_logs.s3.enabled","Value":"true"}
          Code:
               15.      SecurityGroups: 
               16.        - "sg-9012ijkl9012ijkl0"
               17.      LoadBalancerAttributes:
               18.        - Key: access_logs.s3.enabled
               19.          Value: "false"
               20.        - Key: access_logs.s3.bucket

        }
      }

(以降割愛)

このように some を利用することで、 access_logs.s3.enabled が含まれない場合、あるいは有効化されていない場合はエラーを返すルールが作成できました。

参考リンク