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のサポートを借りることでルールのロジックに集中することができるので、効率的に実装を進めることができました。
記事執筆時点ではまだオープンベータです。今後の発展に期待します。