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 ルールを作成することもできます。
※参考:
- AWS Document - Testing AWS CloudFormation Guard rules
- AWS Document - Creating AWS Config Custom Policy Rules
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-guard
のDSLの記法、詳細は公式ドキュメントに記載されています。
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はログ出力を有効にするか否か、 LoadBalancerAttributes
の access_logs.s3.enabled
というパラメータで制御をします。今回はこれを有効にしているかチェックするルールを作成してみます。
- AWS Document - AWS CloudFormation: AWS::ElasticLoadBalancingV2::LoadBalancer LoadBalancerAttribute
- AWS Document - AWS Config: elb-logging-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-guard
で some
を使うと、該当するものが含まれていればパスする、という書き方ができます。
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.enabled
が true
であればパスされます。
また access_logs.s3.enabled
のValueを false
に変更すると、以下の通りエラーを返すことも確認できます。
$ 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
が含まれない場合、あるいは有効化されていない場合はエラーを返すルールが作成できました。