ルール開発キット(rdk)を使ってAWS Configのカスタムルールを作成してみた

ルール開発キット(rdk)を使ってAWS ConfigのカスタムLambdaルールを作成する手順をまとめます。
2023.02.16

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

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のサポートを借りることでルールのロジックに集中することができるので、効率的に実装を進めることができました。

記事執筆時点ではまだオープンベータです。今後の発展に期待します。