Lambda Layers を使って再利用可能な共通コンポーネントを切り出す

記事タイトルとURLをコピーする

CI2部 技術2課の山﨑です。

前回のブログでは AWS Lambdaを使ってAWS Config Rules のカスタムルールを作成しました。

blog.serverworks.co.jp

今回のブログでは AWS Lambdaの機能の1つであるLambda Layersを使ってカスタムルールとして定義したLambdaファンクションのコードをシンプルに書き換えたいと思います。

AWS Lambda Layersとは

以下、AWSドキュメントの引用です。

Lambda レイヤーは、Lambda 関数で使用できるライブラリとその他の依存関係をパッケージ化するための便利な方法を提供します。レイヤーを使用することで、アップロードされたデプロイメントアーカイブのサイズを削減し、コードをデプロイするスピードを速めることができます。 レイヤーは、追加のコードまたはデータを含むことができる .zip ファイルアーカイブです。レイヤーには、ライブラリ、 カスタムランタイム 、データ、または設定ファイルを含めることができます。レイヤーを使用すると、コードの共有と責任の分離を促進し、ビジネスロジックの記述をより迅速に繰り返すことができます。

Lambda レイヤーの作成と共有 - AWS Lambda

重要な点は「レイヤーを使用すると、コードの共有と責任の分離を促進し、ビジネスロジックの記述をより迅速に繰り返すことができます。」この部分です。

開発をしていると実行頻度が高い処理を「関数」に、実行頻度が高い関数を「モジュール」にすることでコードの再利用性を高めて不要なコーディングをしないように工夫するシーンがあると思います。

Lambda Layers はイメージとしてはこの「モジュール」を作る仕組みに似ています。様々なLambdaファンクションで利用する共通コンポーネントを「Layer」という形で事前定義することにより、複数のLambdaファンクションが参照できるようになります。

f:id:swx-yamasaki:20220319084126p:plain
Lambda Layersのイメージ

今回実装するLambda Layersの概要

切り出す共通コンポーネント

前回作成したConfig Rules 用に作成したLambdaファンクション内で定義している関数を列挙してみると、実際の評価ロジックを定義あるいは実行しているのは赤字の関数のみです。つまり、赤字以外の関数はConfig RulesのカスタムルールをLambdaファンクションを使って実装する上で利用可能な共通コンポーネントとなります。今回は赤字以外の関数をLambda Layersとして切り出します。

  • check_defined

  • get_client

  • get_assume_role_credentials

  • is_oversized_changed_notification

  • convert_api_configuration

  • get_configuration

  • get_configuration_item

  • is_applicable

  • check_ipv4Range_cidrs

  • evaluate_change_notification_compliance

  • lambda_handler

Lambda Layersの作成

共通コンポーネントをzipファイルにアーカイブ

コードの内容に大きな変更はありませんので中身のご紹介は割愛しますが、切り出した共通コンポーネントのファイルをzip化します。

Lambda ランタイムごとに、PATH 変数に /opt ディレクトリ内の特定のフォルダが含まれます。レイヤー .zip ファイルアーカイブに同じフォルダ構造を定義すると、関数コードはパスを指定しなくても、レイヤーコンテンツにアクセスできます。

Lambda レイヤーの作成と共有 - AWS Lambda

Lambda Layersのコードは Lambdaが実行されるコンテナのOS内では /opt 以下に展開されるため、上記AWSドキュメントに記載されているようにレイヤー .zip ファイルアーカイブに同じフォルダ構造を定義すると、関数コードはパスを指定しなくても、レイヤーコンテンツにアクセスできます。今回利用するPythonでは python/レイヤー.zip の構造を取ることでレイヤーコンテンツにアクセス可能になるためこれに倣います。

[Shohei@ ~]$ mkdir python
[Shohei@ ~]$ cd python
[Shohei@ ~/python]$ vi config_rules_layer.py # 共通コンポーネントとして切り出すコードを定義
[Shohei@ ~/python]$ cd
[Shohei@ ~]$ zip -r config_rules_layer.zip python/
  adding: python/ (stored 0%)
  adding: python/config_rules_layer.py (deflated 69%)

Lambda Layers にzipファイルをアップロード

Lambda Layersの作成をクリックします。

f:id:swx-yamasaki:20220319091512p:plain
Lambda Layersのメニュー画面

Lambda Layersの作成画面でzipファイルをアップロードし、ランタイムやCPUアーキテクチャを選択した後に作成を実行します。

f:id:swx-yamasaki:20220319091616p:plain
Lambda Layersの作成画面

Lambda Layersの作成はこれだけです。

f:id:swx-yamasaki:20220319091940p:plain
Lambda Layersの作成完了

LambdaファンクションからLayersを呼び出す

呼び出し元のLambdaファンクションの編集画面を開き、画面上部の図中に表示されている「Layers」をクリックします。

f:id:swx-yamasaki:20220319095206p:plain
Lambda ファンクションの画面

すると編集画面下部に移動するので「レイヤーの追加」をクリックします。

f:id:swx-yamasaki:20220319095316p:plain
レイヤーの追加

レイヤーの追加画面で作成したLayersを指定して追加します。

f:id:swx-yamasaki:20220319095410p:plain
レイヤーの追加画面

レイヤーが追加されるとLambdaファンクションの編集画面上部の「Layers」の数が増えます。

f:id:swx-yamasaki:20220319095744p:plain
レイヤーの追加確認

あとはLambda関数からLayersをインポートし、Layersから関数を呼び出すようにコードを修正すればOKです。Lambda Layers をインポートすることで実際の評価ロジックのコーディングに集中することが可能ですし、また元のコード(167行)の半分以下のコード量(78行)で収まりました。

import json
import logging
 
import config_rules_layer
 
 
logger = logging.getLogger()
logger.setLevel(logging.INFO)
 
def check_ipv4Range_cidrs(inbound_permissions):
    for permission in inbound_permissions:
        logger.info(f'Permission: {permission}')
        if not permission['userIdGroupPairs']:
            for ipv4Range in permission['ipv4Ranges']:
                logger.info(f'ipv4Range: {ipv4Range}')
                if ipv4Range['cidrIp'] == '0.0.0.0/0':
                    logger.warning('Inbound rule includes risks for accessing from unspecified hosts.')
                    return ['NON_COMPLIANT', 'Inbound rule includes risks for accessing from unspecified hosts.']
            return ['COMPLIANT', 'No Problem.']
        else:
            return ['COMPLIANT', 'No Problem.']
 
def evaluate_change_notification_compliance(configuration_item, rule_parameters):
    try:
        config_rules_layer.check_defined(configuration_item, 'configuration_item')
        config_rules_layer.check_defined(configuration_item['configuration'], 'configuration_item[\'configuration\']')
        config_rules_layer.check_defined(configuration_item['configuration']['ipPermissions'], 'ipPermissions')
        inboundPermissions = configuration_item['configuration']['ipPermissions']
        logger.info(f'InboundPermissions: {inboundPermissions}')
        if rule_parameters:
            config_rules_layer.check_defined(rule_parameters, 'rule_parameters')
         
        if configuration_item['resourceType'] != 'AWS::EC2::SecurityGroup':
            logger.info('Resource type is not AWS::EC2:SecurityGroup. This is ', configuration_item['resourceType'], '.')
            return ['NOT_APPLICABLE', 'Resource type is not AWS::EC2:SecurityGroup.']
        
        if inboundPermissions == []:
            return ['COMPLIANT','Inbound rule does not have any permissions.']
        else:
            logger.info('check_ipv4Range_cidrs')
            return check_ipv4Range_cidrs(inboundPermissions)
 
    except KeyError as error:
        logger.error(f'KeyError: {error} is not defined.')
        return ['NOT_APPLICABLE', 'Cannot Evaluation because object is none.']
 
def lambda_handler(event, context):
    global AWS_CONFIG_CLIENT
    AWS_CONFIG_CLIENT = config_rules_layer.get_client('config', event)
 
    invoking_event = json.loads(event['invokingEvent'])
    logger.info(f'invoking_event: {invoking_event}')
    rule_parameters = {}
    if 'ruleParameters' in event:
        rule_parameters = json.loads(event['ruleParameters'])
 
    configuration_item = config_rules_layer.get_configuration_item(invoking_event)
    logger.info(f'configuration_item: {configuration_item}')
    compliance_type = 'NOT_APPLICABLE'
    annotation = 'NOT_APPLICABLE.'
 
    if config_rules_layer.is_applicable(configuration_item, event):
        compliance_type, annotation = evaluate_change_notification_compliance(
            configuration_item, rule_parameters
        )
 
    response = AWS_CONFIG_CLIENT.put_evaluations(
        Evaluations=[
            {
                'ComplianceResourceType': invoking_event['configurationItem']['resourceType'],
                'ComplianceResourceId': invoking_event['configurationItem']['resourceId'],
                'ComplianceType': compliance_type,
                'Annotation': annotation,
                'OrderingTimestamp': invoking_event['configurationItem']['configurationItemCaptureTime']
            },
        ],
        ResultToken=event['resultToken']
    )

Config Rules のカスタムルールを実行してみるときちんと動作しました。

f:id:swx-yamasaki:20220319100724p:plain
カスタムルールの実行

まとめ

今回はConfig Rules のカスタムルールという実例をもとにして、Lambda Layers を利用してみました。仮にカスタムルールを10個作成する、となった場合のことを想像してみると Lambda Layers で切り出した共通コンポーネントについては都度実装する必要がなくなるのでかなり省力化できるなと感じました。他にも利用可能なシーンは多数あると思いますので是非お試しください。

山﨑 翔平 (Shohei Yamasaki) 記事一覧はコチラ

カスタマーサクセス部所属。2019年12月にインフラ未経験で入社し、AWSエンジニアとしてのキャリアを始める。2023 Japan AWS Ambassadors/2023-2024 Japan AWS Top Engineers