ルール開発キット(rdk)を使ってAWS Configのカスタムルールを作成してみた
AWS事業本部のイシザワです。
ルール開発キット(rdk)を使ってAWS ConfigのカスタムLambdaルールを作成する手順をまとめます。
rdk とは
rdkとはAWS ConfigのカスタムLambdaルール作成のサポートをするツールです。
カスタムルールに使用するLambda関数コードの雛形を作成したり、カスタムルールを簡単にAWS環境にデプロイしたりすることができます。
今回作成するカスタムルール
VPCのCIDRが規定のCIDRに含まれているかを検査するカスタムルールを作成します。
既定のCIDRはカスタムルール作成時にパラメータとして渡せるようにします。
やってみた
まずpipを使ってrdkをインストールします。
PS C:\work> pip install rdk
rdkがインストールされたら、rdkの初期セットアップを行います。 ここでデプロイ先のAWS環境においてAWS Configの有効化とrdkで使うS3バケットの作成が行われます。
PS C:\work> rdk init [ap-northeast-1]: Running init! [ap-northeast-1]: Found Config Recorder: default [ap-northeast-1]: Found Config Role: arn:aws:iam::123456789012:role/config-role [ap-northeast-1]: Found Bucket: config-bucket-123456789012 [ap-northeast-1]: Config Service is ON [ap-northeast-1]: Config setup complete. [ap-northeast-1]: Creating Code bucket config-rule-code-bucket-123456789012-ap-northeast-1
rdkの初期化が完了したらカスタムルールの作成を行います。
今回はVPCの構成変更時に検査するようにしたいので--resource-types
オプションにVPCのリソースタイプを選択します。
PS C:\work> rdk create CheckVpcCidr --runtime python3.9 --resource-types AWS::EC2::VPC --input '{\"cidrBlock\":\"192.168.0.0/16\"}' {"cidrBlock":"192.168.0.0/16"} Running create! Local Rule files created.
実行するとカレントディレクトリ配下にCheckVpcCidrディレクトリが作成されます。
PS C:\work> ls .\CheckVpcCidr\ ディレクトリ: C:\work\CheckVpcCidr Mode LastWriteTime Length Name ---- ------------- ------ ---- -a---- 2023/02/12 23:06 18095 CheckVpcCidr.py -a---- 2023/02/12 23:06 7462 CheckVpcCidr_test.py -a---- 2023/02/12 23:06 353 parameters.json
CheckVpcCidr.py
がLambda関数コードの雛形になります。雛形にはヘルパー関数とボイラープレートが含まれているので、開発者はロジックの実装に集中をすることができます。
基本的には以下の部分にカスタムルールのロジックを書いていきます。
############## # Parameters # ############## # Define the default resource to report to Config Rules DEFAULT_RESOURCE_TYPE = "AWS::::Account" # Set to True to get the lambda to assume the Role attached on the Config Service (useful for cross-account). ASSUME_ROLE_MODE = False # Other parameters (no change needed) CONFIG_ROLE_TIMEOUT_SECONDS = 900 ############# # Main Code # ############# def evaluate_compliance(event, configuration_item, valid_rule_parameters): """Form the evaluation(s) to be return to Config Rules Return either: None -- when no result needs to be displayed a string -- either COMPLIANT, NON_COMPLIANT or NOT_APPLICABLE a dictionary -- the evaluation dictionary, usually built by build_evaluation_from_config_item() a list of dictionary -- a list of evaluation dictionary , usually built by build_evaluation() Keyword arguments: event -- the event variable given in the lambda handler configuration_item -- the configurationItem dictionary in the invokingEvent valid_rule_parameters -- the output of the evaluate_parameters() representing validated parameters of the Config Rule Advanced Notes: 1 -- if a resource is deleted and generate a configuration change with ResourceDeleted status, the Boilerplate code will put a NOT_APPLICABLE on this resource automatically. 2 -- if a None or a list of dictionary is returned, the old evaluation(s) which are not returned in the new evaluation list are returned as NOT_APPLICABLE by the Boilerplate code 3 -- if None or an empty string, list or dict is returned, the Boilerplate code will put a "shadow" evaluation to feedback that the evaluation took place properly """ ############################### # Add your custom logic here. # ############################### return "NOT_APPLICABLE" def evaluate_parameters(rule_parameters): """Evaluate the rule parameters dictionary validity. Raise a ValueError for invalid parameters. Return: anything suitable for the evaluate_compliance() Keyword arguments: rule_parameters -- the Key/Value dictionary of the Config Rules parameters """ valid_rule_parameters = rule_parameters return valid_rule_parameters
以下のコマンドでカスタムルールのトリガー時(VPCの構成変更時)にconfiguration_item
に入る値のサンプルが見れます。実装する際の参考にしましょう。
コマンドと実行結果
PS C:\work> rdk sample-ci AWS::EC2::VPC { "version": "1.2", "accountId": "264683526309", "configurationItemCaptureTime": "2016-10-29T19:52:09.494Z", "configurationItemStatus": "OK", "configurationStateId": "1477770729494", "configurationItemMD5Hash": "81e6fc8379f93bd68526f8a6769776b9", "arn": "arn:aws:ec2:us-east-1:264683526309:vpc/vpc-0990dc6d", "resourceType": "AWS::EC2::VPC", "resourceId": "vpc-0990dc6d", "awsRegion": "us-east-1", "availabilityZone": "Multiple Availability Zones", "tags": {}, "relatedEvents": [ "ca566c16-81b7-48cf-8573-241ab67880e7" ], "relationships": [ { "resourceType": "AWS::EC2::NetworkAcl", "resourceId": "acl-b48fd1d0", "relationshipName": "Contains NetworkAcl" }, { "resourceType": "AWS::EC2::NetworkInterface", "resourceId": "eni-49ec78b5", "relationshipName": "Contains NetworkInterface" }, { "resourceType": "AWS::EC2::NetworkInterface", "resourceId": "eni-67e4348d", "relationshipName": "Contains NetworkInterface" }, { "resourceType": "AWS::EC2::NetworkInterface", "resourceId": "eni-70a4b7bf", "relationshipName": "Contains NetworkInterface" }, { "resourceType": "AWS::EC2::NetworkInterface", "resourceId": "eni-c09a049e", "relationshipName": "Contains NetworkInterface" }, { "resourceType": "AWS::EC2::Instance", "resourceId": "i-29e75b19", "relationshipName": "Contains Instance" }, { "resourceType": "AWS::EC2::Instance", "resourceId": "i-ebf3e058", "relationshipName": "Contains Instance" }, { "resourceType": "AWS::EC2::InternetGateway", "resourceId": "igw-a5f227c1", "relationshipName": "Is attached to InternetGateway" }, { "resourceType": "AWS::EC2::RouteTable", "resourceId": "rtb-50b9b034", "relationshipName": "Contains RouteTable" }, { "resourceType": "AWS::EC2::SecurityGroup", "resourceId": "sg-0d46b170", "relationshipName": "Contains SecurityGroup" }, { "resourceType": "AWS::EC2::SecurityGroup", "resourceId": "sg-1d56fc66", "relationshipName": "Contains SecurityGroup" }, { "resourceType": "AWS::EC2::SecurityGroup", "resourceId": "sg-2b353152", "relationshipName": "Contains SecurityGroup" }, { "resourceType": "AWS::EC2::SecurityGroup", "resourceId": "sg-2ed0d557", "relationshipName": "Contains SecurityGroup" }, { "resourceType": "AWS::EC2::SecurityGroup", "resourceId": "sg-4161cb3a", "relationshipName": "Contains SecurityGroup" }, { "resourceType": "AWS::EC2::SecurityGroup", "resourceId": "sg-48b41e33", "relationshipName": "Contains SecurityGroup" }, { "resourceType": "AWS::EC2::SecurityGroup", "resourceId": "sg-4f49e334", "relationshipName": "Contains SecurityGroup" }, { "resourceType": "AWS::EC2::SecurityGroup", "resourceId": "sg-62bba619", "relationshipName": "Contains SecurityGroup" }, { "resourceType": "AWS::EC2::SecurityGroup", "resourceId": "sg-649a301f", "relationshipName": "Contains SecurityGroup" }, { "resourceType": "AWS::EC2::SecurityGroup", "resourceId": "sg-7d7cd606", "relationshipName": "Contains SecurityGroup" }, { "resourceType": "AWS::EC2::SecurityGroup", "resourceId": "sg-96b471ec", "relationshipName": "Contains SecurityGroup" }, { "resourceType": "AWS::EC2::SecurityGroup", "resourceId": "sg-b854bbc5", "relationshipName": "Contains SecurityGroup" }, { "resourceType": "AWS::EC2::SecurityGroup", "resourceId": "sg-e627199e", "relationshipName": "Contains SecurityGroup" }, { "resourceType": "AWS::EC2::SecurityGroup", "resourceId": "sg-e868d092", "relationshipName": "Contains SecurityGroup" }, { "resourceType": "AWS::EC2::SecurityGroup", "resourceId": "sg-f693878d", "relationshipName": "Contains SecurityGroup" }, { "resourceType": "AWS::EC2::Subnet", "resourceId": "subnet-29428871", "relationshipName": "Contains Subnet" }, { "resourceType": "AWS::EC2::Subnet", "resourceId": "subnet-32e85b44", "relationshipName": "Contains Subnet" }, { "resourceType": "AWS::EC2::Subnet", "resourceId": "subnet-38c20312", "relationshipName": "Contains Subnet" }, { "resourceType": "AWS::EC2::Subnet", "resourceId": "subnet-e83ba2d5", "relationshipName": "Contains Subnet" } ], "configuration": { "vpcId": "vpc-0990dc6d", "state": "available", "cidrBlock": "172.31.0.0/16", "dhcpOptionsId": "dopt-3a32ab5f", "tags": [], "instanceTenancy": "default", "isDefault": true }, "supplementaryConfiguration": {} } For more info, try checking: https://github.com/awslabs/aws-config-resource-schema/blob/master/config/properties/resource-types/
他にも実装する際の参考としてAWS Configルールリポジトリを挙げておきます。
以下が今回作成するカスタムルールの実装となります。ロジックの内容は本記事の主眼でないため説明は省略します。
############## # Parameters # ############## # Define the default resource to report to Config Rules DEFAULT_RESOURCE_TYPE = "AWS::EC2::VPC" # Set to True to get the lambda to assume the Role attached on the Config Service (useful for cross-account). ASSUME_ROLE_MODE = False # Other parameters (no change needed) CONFIG_ROLE_TIMEOUT_SECONDS = 900 ############# # Main Code # ############# def evaluate_compliance(event, configuration_item, valid_rule_parameters): ip_address, subnet_mask = get_ip_address_and_subnet_mask(configuration_item["configuration"]["cidrBlock"]) expected_ip_address, expected_subnet_mask = get_ip_address_and_subnet_mask(valid_rule_parameters["cidrBlock"]) if subnet_mask >= expected_subnet_mask and \ ip_address & expected_subnet_mask == expected_ip_address & expected_subnet_mask: return build_evaluation_from_config_item(configuration_item, "COMPLIANT") return build_evaluation_from_config_item(configuration_item, "NON_COMPLIANT") def get_ip_address_and_subnet_mask(s): m = re.match(r'(\d+)\.(\d+)\.(\d+)\.(\d+)/(\d+)', s) ip_address = 0 for i in range(1, 5): part = int(m.group(i)) ip_address <<= 8 ip_address += part prefix_length = int(m.group(5)) subnet_mask = ((1 << 32) - 1) - ((1 << (32 - prefix_length)) - 1) return ip_address, subnet_mask def evaluate_parameters(rule_parameters): m = re.match(r'(\d+)\.(\d+)\.(\d+)\.(\d+)/(\d+)', rule_parameters["cidrBlock"]) if not m: raise ValueError("The parameter is not valid.") for i in range(1, 5): part = int(m.group(i)) if not (0 <= part and part <= 255): raise ValueError("The parameter is not valid.") prefix_length = int(m.group(5)) if not (0 <= prefix_length and prefix_length <= 32): raise ValueError("The parameter is not valid.") valid_rule_parameters = rule_parameters return valid_rule_parameters
CheckVpcCidr_test.py
に簡単なテストコードを作成します。このファイルにはテスト用のヘルパー関数が含まれています。
############## # Parameters # ############## # Define the default resource to report to Config Rules DEFAULT_RESOURCE_TYPE = "AWS::EC2::VPC" ############# # Main Code # ############# CONFIG_CLIENT_MOCK = MagicMock() STS_CLIENT_MOCK = MagicMock() class Boto3Mock: @staticmethod def client(client_name, *args, **kwargs): if client_name == "config": return CONFIG_CLIENT_MOCK if client_name == "sts": return STS_CLIENT_MOCK raise Exception("Attempting to create an unknown client") sys.modules["boto3"] = Boto3Mock() RULE = __import__("CheckVpcCidr") class ComplianceTest(unittest.TestCase): def test_cidr_is_valid(self): invoking_event = '{"configurationItem":{"configuration":{"cidrBlock":"172.31.0.0/16"}, "relationships":[], "configurationItemStatus":"OK", "resourceType":"AWS::EC2::VPC", "configurationItemCaptureTime":"2019-04-28T07:49:40.797Z", "resourceId": "vpc-0990dc6d"}, "messageType":"ConfigurationItemChangeNotification"}' rule_parameters = '{"cidrBlock":"172.16.0.0/12"}' response = RULE.lambda_handler(build_lambda_configurationchange_event(invoking_event, rule_parameters), {}) resp_expected = [] resp_expected.append(build_expected_response("COMPLIANT", "vpc-0990dc6d")) assert_successful_evaluation(self, response, resp_expected) def test_cidr_is_not_valid(self): invoking_event = '{"configurationItem":{"configuration":{"cidrBlock":"172.31.0.0/16"}, "relationships":[], "configurationItemStatus":"OK", "resourceType":"AWS::EC2::VPC", "configurationItemCaptureTime":"2019-04-28T07:49:40.797Z", "resourceId": "vpc-0990dc6d"}, "messageType":"ConfigurationItemChangeNotification"}' rule_parameters = '{"cidrBlock":"10.0.0.0/8"}' response = RULE.lambda_handler(build_lambda_configurationchange_event(invoking_event, rule_parameters), {}) resp_expected = [] resp_expected.append(build_expected_response("NON_COMPLIANT", "vpc-0990dc6d")) assert_successful_evaluation(self, response, resp_expected)
以下のコマンドを実行することでローカルでテストを実行できます。
PS C:\work> rdk test-local CheckVpcCidr Running local test! Testing CheckVpcCidr Looking for tests in C:\work\CheckVpcCidr CheckVpcCidr_test.py Debug! <unittest.suite.TestSuite tests=[<unittest.suite.TestSuite tests=[<CheckVpcCidr_test.ComplianceTest testMethod=test_cidr_is_not_valid>, <CheckVpcCidr_test.ComplianceTest testMethod=test_cidr_is_valid>]>, <unittest.suite.TestSuite tests=[<CheckVpcCidr_test.TestStsErrors testMethod=test_sts_access_denied>, <CheckVpcCtest_cidr_is_not_valid (CheckVpcCidr_test.ComplianceTest.test_cidr_is_not_valid) ... ok test_cidr_is_valid (CheckVpcCidr_test.ComplianceTest.test_cidr_is_valid) ... ok test_sts_access_denied (CheckVpcCidr_test.TestStsErrors.test_sts_access_denied) ... ok test_sts_unknown_error (CheckVpcCidr_test.TestStsErrors.test_sts_unknown_error) ... ok ---------------------------------------------------------------------- Ran 4 tests in 0.005s OK <unittest.runner.TextTestResult run=4 errors=0 failures=0>
以下のコマンドでAWS環境へのデプロイができます。
PS C:\work> rdk deploy CheckVpcCidr [ap-northeast-1]: Running deploy! [ap-northeast-1]: Found Custom Rule. [ap-northeast-1]: Zipping CheckVpcCidr [ap-northeast-1]: Uploading CheckVpcCidr [ap-northeast-1]: Upload complete. [ap-northeast-1]: Creating CloudFormation Stack for CheckVpcCidr [ap-northeast-1]: Waiting for CloudFormation stack operation to complete... [ap-northeast-1]: Waiting for CloudFormation stack operation to complete... [ap-northeast-1]: Waiting for CloudFormation stack operation to complete... [ap-northeast-1]: Waiting for CloudFormation stack operation to complete... [ap-northeast-1]: Waiting for CloudFormation stack operation to complete... [ap-northeast-1]: Waiting for CloudFormation stack operation to complete... [ap-northeast-1]: Waiting for CloudFormation stack operation to complete... [ap-northeast-1]: Waiting for CloudFormation stack operation to complete... [ap-northeast-1]: Waiting for CloudFormation stack operation to complete... [ap-northeast-1]: Waiting for CloudFormation stack operation to complete... [ap-northeast-1]: Waiting for CloudFormation stack operation to complete... [ap-northeast-1]: Waiting for CloudFormation stack operation to complete... [ap-northeast-1]: Waiting for CloudFormation stack operation to complete... [ap-northeast-1]: CloudFormation stack operation complete. [ap-northeast-1]: Config deploy complete.
AWSのコンソールにてカスタムルールがデプロイされたことを確認できます。パラメータの値はparameters.json
で指定できるので、編集して再デプロイすることで変更することができます。
試しにルールに準拠するVPCと準拠しないVPCを作成します。
再度カスタムルールの詳細情報ページに移動すると、カスタムルールでVPCが検査されていることを確認できます。
まとめ
rdkを使ってカスタムLambdaルールを作成し、AWS環境にデプロイするまでの手順をまとめました。 rdkのサポートを借りることでルールのロジックに集中することができるので、効率的に実装を進めることができました。
記事執筆時点ではまだオープンベータです。今後の発展に期待します。