AWS Configの自動修復アクションにおいて実行ループが止まらないのですがどうですればいいでしょうか?

こんにちは、普段AWSやSnowflakeを中心としたデータ活用に関するサービス開発・案件を担当しているSCSKの高本です。

早速ですが、皆さんAWS Configはお使いでしょうか。そして、AWS Configはお好きでしょうか。

今回はAWS Configの自動修復アクションに関するお話です。本ブログ執筆前に(私が)そのままググってヒットして欲しかったタイトルをつけてみました。

AWS Configって何でしたっけ?

今回は少し内容が長くなりそうなので、AWS Configとは?の概要については割愛させていただきたいと思います。

超ざっくりで言うと、各種AWSリソースの「あるべき姿(設定、Config)」を定義し、各リソースが「あるべき姿」になっているかを都度「評価」し、必要に応じて通知や現行の設定に対する是正措置などの「アクション」を実行してくれるAWSのマネージドサービスとなっています。

よく用いられる例で言うと、セキュリティグループがある日突然フルオープンになっていた場合、AWS Configを利用することで、確実にその変更を検知し、セキュリティ管理者に対してアラート通知をしたり、事前に定めた定義に従ってセキュリティ上好ましくない現行の設定を自動的に修復したりすることができます。

背景とゴール

AWS Configでは非準拠判定となったリソースに対する修復対応として、手動修復か自動修復の2パターンが選択できます。

  • 手動修復
  • 自動修復

このうち、自動修復に関して、AWSのドキュメントに以下のような記載があります。

自動修復後もリソースがまだ非準拠である場合は、自動修復を再試行するようにルールを設定できます。・・・

・・・修復スクリプトを複数回実行するとコストがかかります。修復が失敗し、指定された期間内に処理が行われた場合にのみ再試行を実行します。例えば、300 秒に 5 回再試行します。

自動修復アクション実行後も依然として対象のリソースが非準拠である場合、AWS Config側で修復アクションを再実行してくれます。また、再試行ポリシーとして回数と期間を指定できるので、対象のリソースが非準拠であり続けた場合に、半永久的に修復アクションが実行されることも未然に防いでくれます。

・・・のはずだったのですが、実際に試したところ、なぜか修復アクションの無限ループが止まりませんでした。

結論だけ先に言うと、自動修復アクション設定時に指定するパラメータの解釈ミスが原因でした。

試行錯誤の過程やどう対処したか、についてはまとめて後述しようと思いますが、なぜ無限ループが起きたのか、また、意図しない無限ループを防ぐにはどうしたらいいのか、という点の整理をモチベーションに書いていきたいと思います。

検証シナリオ・前提

まず、再現環境を構築するために、以下のシナリオを立てたいと思います。

1.AWS Configで評価・修復対象とするサービス:AWS Lambda
2.非準拠とする条件:Lambda関数の関数URLがパブリック公開になっている場合、「非準拠」判定とする
3.非準拠だった場合に実行する自動修復アクション:Lambda関数の関数URLが依然パブリック公開状態のまま設定を上書きする

1. 2. については、何となくとっつきやすそうなAWS Lambdaの関数URLをターゲットに選びました。

3. については、自動修復アクションの再試行に関する挙動を確認するために、実行しても未来永劫、非準拠のままとなり続ける”BAD”アクションを定義します。つまり、再度パブリック公開を許す設定で上書きするアクションを自動修復アクションとして定義します。

しかし、そもそも、Lambda関数の関数URLが依然パブリック公開状態のまま設定を上書きした場合、AWS Configはそれを設定変更として認識・検知してくれるのでしょうか。

答えは「Yes」です。

もちろんサービスによって例外あるかと思いますが、AWS Lambdaの場合はソースコードやその他設定に実質何も変更点がなくてもマネジメントコンソールやAWS CLIから更新をかければ、しかるべきUpdateのAPIが観測され、それがAWS Config側にちゃんと伝わるようになっています。

ということで、このシナリオ前提に立って具体的な環境を用意していきます。

環境準備・動作確認

評価対象のLambda関数を作成

AWS Configが評価する対象のLambda関数「awsConfigTestLambdaFunction」を用意します。特にソースコードにはデフォルトのまま修正を加えず、関数URLだけ追加設定します。この時関数URLの認証タイプには「NONE」を指定します。

テスト用Lambdaの作成

払い出された関数URLのエンドポイントにブラウザ(インターネット経由)からアクセスできることを確認します。関数URLの認証タイプは「NONE」を設定しているので、本エンドポイントを知り得るクライアントからは認証なしにアクセスができる状態であり、セキュリティ的に脆弱な状態です。

関数URL動作確認

AWS Configマネージドルールを作成

AWS ConfigにはマネージドルールとしていくつかのAWS管理の評価ルールが予め用意されています。カスタムLambdaルールは作成せず、今回のユースケースに合うマネージドルール「Lambda-function-public-access-prohibited」を選択します。先ほど作成したLambda関数「awsConfigTestLambdaFunction」を本ルールを用いて評価していきます。

マネージドルールの作成

評価モードの設定では、トリガー条件を限定する+AWS CloudTrailのAPIログのノイズ排除を目的に、変更範囲を先ほど作成したLambda関数のみに限定します。

マネージドルール_評価モードの設定

以上の設定でマネージドルールを作成します。「lambda-function-pubilc-access-prohibited-001」というルール名で作成します。ルールが正常に機能していれば、さきほど作成したLambda関数に対して実際にConfigルールの評価が走り、「最後に成功した検出評価」に緑色のチェックマークと最新の評価日時が表示されます。対象のLambda関数はパブリック公開状態のため、「コンプライアンス」では当然「非準拠」の評価となっています。

マネージドルール作成後

 

自動修復アクションの設定

作成したConfigルール「lambda-function-pubilc-access-prohibited-001」に対して自動修復アクションを設定していきます。修復アクションを定義・設定することで、「非準拠」判定となったリソースに対してなんらかの是正措置を取ることができます。

AWS Configでは、この修復アクションを実行する際、裏側でAWS Systems Manager Automationを利用します。

検証シナリオのおさらいですが、今回は自動修復アクションの再試行に関する挙動を確認したいので、実行しても未来永劫、非準拠のままとなり続ける”BAD”アクションを自動修復アクションで定義します。具体的には、対象のLambda関数の関数URLを、依然パブリック公開状態のまま設定を上書きするような操作をAutomationのランブック(awsConfigTestRunbook001とします)で定義します。

SSM_Automation_Lambda

schemaVersion: '0.3'

description: AWS Configの自動修復アクションに使用するランブック

parameters:

  AutomationAssumeRole:

    type: String

    allowedValues:

      - arn:aws:iam::XXXXXXX:role/testRoleForAWSConfigTest

assumeRole: arn:aws:iam::XXXXXXX:role/testRoleForAWSConfigTest

mainSteps:

  - description: AWS Configの自動修復アクションに使用するランブック

    name: InvokeLambdaFunction

    action: aws:invokeLambdaFunction

    maxAttempts: 10

    timeoutSeconds: 60

    isEnd: true

    inputs:

      InvocationType: RequestResponse

      FunctionName: arn:aws:lambda:ap-northeast-1:XXXXXXX:function:modifyLambdaFunctionUrlPublicAccessSetting

このランブックの中で別のLambda関数()である「modifyLambdaFunctionUrlPublicAccessSetting」を呼び出します。上のランブック(.yml)は単なるwrapperであって、具体的な自動修復アクションの中身として、Lambda関数「modifyLambdaFunctionUrlPublicAccessSetting」を以下の通り定義します。

import boto3
from typing import Dict
import time

target_function_name: str = 'awsConfigTestLambdaFunction'
lambda_client: object = boto3.client('lambda')

def lambda_handler(event, context):
  try:
    if is_the_target_lambda_func_exist():
      lambda_remove_permission()
      time.sleep(1)
      lambda_add_permission()
      return {
        "message": f"Successfully updated the permission attached to this func: {target_function_name}."
      }
    else:
      return {
        "message": f"the target lambda func may not exist: {target_function_name}."
      }
  except Exception as e:
    return e

def lambda_remove_permission() -> None:
  try:
    lambda_client.remove_permission(
      FunctionName=target_function_name,
      StatementId='FunctionURLAllowPublicAccess'
      )
  except Exception as e:
    print(e)

def lambda_add_permission() -> None:
  try:
    lambda_client.add_permission(
      FunctionName=target_function_name,
      StatementId='FunctionURLAllowPublicAccess',
      Action='lambda:InvokeFunctionUrl',
      Principal='*',
      FunctionUrlAuthType='NONE'
    )
  except Exception as e:
    print(e)

def is_the_target_lambda_func_exist() -> bool:
  try:
    funcs: Dict = lambda_client.list_functions()
    if 'NextMarker' in funcs:
      next_marker: str = funcs['NextMarker']
      print(f'list_functions API NextMarker: {next_marker}')
    else:
      next_marker = ''
      print(f'list_functions API NextMarker: {next_marker}')
    for func in funcs['Functions']:
      if func['FunctionName'] == target_function_name:
        return 1
      else:
        pass
    
    while len(next_marker) > 0:
      funcs: Dict = lambda_client.list_functions(Marker=next_marker)
      if 'NextMarker' in funcs:
        next_marker: str = funcs['NextMarker']
        print(f'list_functions API NextMarker: {next_marker}')
      else:
        next_marker = ''
        print(f'list_functions API NextMarker: {next_marker}')
      for func in funcs['Functions']:
        if func['FunctionName'] == target_function_name:
          return 1
        else:
          pass
    
    return 0
  except Exception as e:
    print(e)

このLambda関数「modifyLambdaFunctionUrlPublicAccessSetting」では、AWS Configの評価ターゲットとなる別のLambda関数「awsConfigTestLambdaFunction」に適用されている既存のリソースベースポリシーを一度引きはがして、関数URLの認証タイプ「NONE」のリソースベースポリシーを当該関数に再適用する”BAD”アクションを実行します。当然このLambda関数が実行されても、Lambda関数「awsConfigTestLambdaFunction」は「非準拠」の評価のままとなります。

以下の図は修復アクションを実行した際のConfigルールの画面になります。アクションは正常に実行されるものの、コンプライアンスは「非準拠」の評価のままです。

動作確認_lambda初期実行後のconfigルール状態

上の画面は修復アクション設定後の画面なのですが、実は、修復アクションの設定において説明を省いていたパラメータがあります。以下の通り、自動修復アクションでは再試行に関わる回数と期間(秒単位)を指定できます。

マネージドルール_10再試行_600秒

パラメータとして「再試行までに:10」「秒:600」で自動修復アクションを設定したところ、無限ループが発生しました。てっきり600秒間に10回自動修復アクションを再試行してくれるのかと勝手に思っていました。以下は、AWS Systems Manager Automationのランブック実行履歴ですが、2~3分ぐらいの間隔で修復アクションの再実行が繰り返されました。10回以上再試行され、かつ初回のランブック実行から600秒経過しても再試行が止まりません。

Automation_executions_history

試しに以下の通り「再試行までに:1」「秒:60」で自動修復アクションを再設定しても同様に無限ループが発生しました。再試行回数1回に設定しているのになぜ複数回実行されてしまうのか、この時点ではわからず。。。

とりあえず、無限ループ発生の再現自体はここまでで完了となります。

マネージドルール_1再試行_60秒

Automation_executions_history_2

 

結論:自動修復アクションの無限ループの止め方

記事の冒頭でちらっと述べましたが、結論、自動修復アクションで指定する本パラメータの解釈ミスが原因でした。切り分けの過程でAWS CLI Commad Referenceの本設定に関するAPI仕様を確認していたところ、より具体的なパラメータの解説が記載されていました。

MaximumAutomaticAttempts -> (integer)

The maximum number of failed attempts for auto-remediation. If you do not select a number, the default is 5.

For example, if you specify MaximumAutomaticAttempts as 5 with RetryAttemptSeconds as 50 seconds, Config will put a RemediationException on your behalf for the failing resource after the 5th failed attempt within 50 seconds.

RetryAttemptSeconds -> (long)

Time window to determine whether or not to add a remediation exception to prevent infinite remediation attempts. If MaximumAutomaticAttempts remediation attempts have been made under RetryAttemptSeconds , a remediation exception will be added to the resource. If you do not select a number, the default is 60 seconds.

For example, if you specify RetryAttemptSeconds as 50 seconds and MaximumAutomaticAttempts as 5, Config will run auto-remediations 5 times within 50 seconds before adding a remediation exception to the resource.

この黄色マーカの記載でようやく腑に落ちました。つまり、マネージドルール上の「再試行までに:XX」で設定した回数分の自動修復アクションが、「秒:XX」で設定した秒数以内に完遂した場合、RemediationExceptionが投げられて自動修復アクション再試行が停止する、という解釈が正しかったようです。言い換えれば、「再試行までに:XX」で設定した回数分の自動修復アクションが、「秒:XX」で設定した秒数以内に完遂しない場合、RemediationExceptionが投げられず、自動修復アクション再試行が停止しない、という解釈になります。

無限ループが発生したケースでは、①「再試行までに:10」「秒:600」と、②「再試行までに:1」「秒:60」の2パターンを検証しました。10秒間に1回ペースの頻度では指定時間内に指定回数の自動修復アクション実行が終わらず、結果無限ループが発生していた、ということが推察できます。

無限ループを止めてみる

それではパラメータの使い方が正しく解釈できたところで、再度トライしてみます。

マネージドルール_1再試行_600秒

「再試行までに:1」「秒:600」というかなり緩めの設定をすることで、自動修復アクションの無限ループが止まることを期待します。

さきほどはAWS Systems Manager Automationのランブック実行履歴から確認しましたが、AWS CloudTrailで別角度からより詳細に確認してみます。

CloudTrail_history

一番下の赤枠の「2月27, 2024, 16:57:24」「2月27, 2024, 16:57:25」の部分は、先ほど作成したLambda関数「modifyLambdaFunctionUrlPublicAccessSetting」をLambdaコンソール上から直接実行した際のAPIコールを示しています。

このAPIコールを契機に真ん中赤枠「2月27, 2024, 16:57:54」の部分で「PutEvaluations」が呼ばれています。このAPIコールは、AWS Configが今回の評価ターゲットであるLambda関数「awsConfigTestLambdaFunction」を実際に評価し、「非準拠」の判定を下している部分です。

そしてさらに、この「PutEvaluations」を契機に「StartAutomationExecution」が呼ばれていています。このAPIコールは、AWS Configが自動修復アクションを開始している部分です。自動修復アクションが開始されると、アクションに紐づくLambda関数「modifyLambdaFunctionUrlPublicAccessSetting」が再度呼ばれ、「AddPermission」と「RemovePermission」が再度観測されます。

無限ループが発生していた時は、一番上の赤枠の「PutEvaluations」確認後、

「PutEvaluations」→「StartAutomationExecution」→「RemovePermission」→「AddPermission」→「PutEvaluations」→「StartAutomationExecution」→「RemovePermission」→「AddPermission」→「PutEvaluations」→・・・

という感じでループが発生していました。

しかし、自動修復アクションにて「再試行までに:1」「秒:600」の設定をしたことで、一番上の赤枠の「PutEvaluations」以降、「StartAutomationExecution」が観測されることはありませんでした。(もちろん、AWS Systems Manager Automationのランブック実行履歴上も再実行が繰り返されていないことも確認済み)

…ということで無事、無限ループ解消となります!

 

最後に

いかがでしたでしょうか。

最初からドキュメントしっかり読みこめば済む話ではありましたが、おかげでAWS Configと少しだけ仲良くなれたような気がします。

本記事がいつかどこかで少しでも皆様のお役に立てれば幸いです。

タイトルとURLをコピーしました