CloudFormationで提供されていない処理をカスタムリソースで作ってみた。

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

※本ブログの内容は自己責任で検証また実施ください。

先日、Amazon Inspector を CloudFormationで一撃設定してみる(SNS手動)
公開したところ、手動がはいるなら、一撃じゃないじゃんというツッコミを盛大に頂いたので
改めて、一撃で頑張ってみます。

InspectorのSNSトピックの設定は現在CloudFormationで提供されていないんですが、
その部分をCloudFormationのカスタムリソースで作成してみました。

カスタムリソースは諸刃の剣に近い印象なので、
基本的にはサポートされていない処理、機能を積極的に組み込むのは推奨できませんが、
どうしても一撃で。とか、カスタムリソースのサンプルが欲しかった!!!とかいう人の役に立てば幸いです。

カスタムリソースとは

さて、カスタムリソースって何っていう人は ここをご参照ください https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/template-custom-resources.html

カスタムリソースを使用すると、テンプレートにカスタムのプロビジョニングロジックを記述し、ユーザーがスタックを作成、更新(カスタムリソースを変更した場合)、削除するたびに AWS CloudFormation がそれを実行します。 たとえば、AWS CloudFormation のリソースタイプとして使用できないリソースを含める必要があるとします。それらのリソースは、カスタム リソースを使用して含めることができます。この方法により、すべての関連リソースを 1 つのスタックで管理できます。

今回はサポートされていないリソースというよりは、単純な設定なのであまり当てはまりませんが、 カスタムリソースの実装の仕方という意味では、わかりやすいものになるかと思います。

ではさっそく。

作ってみる

今回はAWS Lambda-backed カスタムリソースを使用します。

実装したい処理はInspectorのSNS トピックの設定になります。

APIリファレンスから該当の処理を探します。 https://docs.aws.amazon.com/ja_jp/inspector/latest/APIReference/API_SubscribeToEvent.html

今回はPythonで実装するので Python SDK (Boto3) https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/inspector.html#Inspector.Client.subscribe_to_event

のサンプルを見つけます。

サンプル処理をまんま参考にしつつ、処理をPythonで実装して、CloudFormation内でLambdaとして定義します 処理に渡すパラメータはCloudFormationからのパラメータから'ServiceToken'のみ削ってはほぼそのまま渡して
そのまま、返しています。
それなりな人は、それなりな感じに修正してください。

  InspectorSubscribeToEventLambda:
    Type: AWS::Lambda::Function
    Properties:
      Code:
        ZipFile: |
          import json
          import boto3
          import cfnresponse
          def handler(event, context):
            try:
              params = dict([(k, v) for k, v in event['ResourceProperties'].items() if k != 'ServiceToken'])
              client = boto3.client('inspector')
              if event['RequestType'] == 'Create':
                response_data = client.subscribe_to_event(**params)
              if event['RequestType'] == 'Delete':
                response_data = client.unsubscribe_from_event(**params)
              if event['RequestType'] == 'Update':
                old_params = dict([(k, v) for k, v in event['OldResourceProperties'].items() if k != 'ServiceToken'])
                client.unsubscribe_from_event(**old_params)
                response_data = client.subscribe_to_event(**params)
              cfnresponse.send(event, context, cfnresponse.SUCCESS, response_data)
            except Exception as e:
              cfnresponse.send(event, context, cfnresponse.FAILED, {})
      Handler: index.handler
      MemorySize: 128
      Role: !GetAtt AmazonInspectorFullAccessRole.Arn
      Runtime: python3.6
      Timeout: 120

参照順が前後しますが、LambdaでInspectorを使用しますので、AWSLambdaBasicExecutionRoleAmazonInspectorFullAccessRoleを定義して Lambda関数に付与します。

  AmazonInspectorFullAccessRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service: lambda.amazonaws.com
            Action: sts:AssumeRole
      Path: /
      ManagedPolicyArns:
        - "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
        - "arn:aws:iam::aws:policy/AmazonInspectorFullAccess"

Lambdaができれば、これをカスタムリソースで呼び出します。 Propertiesから先のパラメータは、ResourcePropertiesとして、Lambdaに渡されるので ここでは、subscribe_to_eventのパラメータをそのまま定義しています。
"ASSESSMENT_RUN_STARTED"を指定して、Inspectorのテンプレート開始時にSNS通知が飛ぶように設定しています。

  CustomInspectorSubscribeToRunStarted:
    Type: Custom::InspectorSubscribeToEventLambda
    Version: 1.0
    Properties:
      ServiceToken: !GetAtt InspectorSubscribeToEventLambda.Arn
      event: "ASSESSMENT_RUN_STARTED"
      resourceArn: "arn:aws:inspector:ap-northeast-1:123456789012:target/xxxxxxxxx/template/xxxxxxxxx"
      topicArn: "arn:aws:sns:ap-northeast-1:123456789012:cfn-inspector-sns-InspectorSnsTopic-xxxxxxxxxx"

resourceArn, topicArnについては、templeateに組み込む場合は

      resourceArn: !Ref MyInspectorTemplate
      topicArn: !Ref InspectorSnsTopic

などとして、置き換えれます。

さて改めて、「Amazon Inspector を CloudFormationで一撃設定してみる。」ということで。
コンソールからまたは、CloudFormationを実行します。

今回Inspectorの設定する内容

対象
  • 指定したタグが設定されたインスタンス
    今回は"Inspector:true"がタグ付けされたインスタンス
ルール
  • 共通脆弱性識別子
  • CIS オペレーティングシステムのセキュリティ設定ベンチマーク
評価の実行の時間
  • 3600秒(1時間)
SNS通知
  • E-Mail通知
Inspector の実施間隔
  • 7日間

CloudFormationの実行

テンプレート URL https://s3-ap-northeast-1.amazonaws.com/pub-devio-blog-qrgebosd/template/cfn-inspector-custom-sns.yml

下のリンクをクリックするとパラメータが入力された状態で、CloudFormatioのスタック作成画面が表示されます。 cfn-inspector-custom-sns

対象となる、タグキーと値ここではInspector:trueをデフォルトでいれてますので
対象の環境に応じてタグキーと値を変更してください。
また、SNS通知を行うE-Mailアドレスをパラメータとして入力します。

「AWS CloudFormationによってカスタム名のついた IAM リソースが作成される場合があることを承認します。」のチェックをいれて実行します

結果確認

おお。手動で設定をする必要がなく、SNS設定が無事入ってますね。すばら。 (SNSトピックから承認通知がくるのでE-Mailで承認は実施ください)

削除確認

一応削除処理も実装していますので、スタックの削除を実施すれば綺麗に削除されますます。

まとめ

「この処理APIはあるけど、CloudFormationでまだサポートされていないんだよなー」という機会損失がなくなると良いです。 Lambdaが組める人はちょっと試してみてはいかかがでしょうか?

ただ、実際に本番で使用する際は、スタック削除のタイミングでAPIの挙動が変わっているかもしれない
パラメータで機密情報渡したらどうなんの?などなどツッコミどころや考慮するべきところは多いかと思いますので ご利用は計画的に

本エントリーでは、 knakayama さんの下記ブログがかなり役に立ちました。 (追伸、knakayamaさん、また福岡遊びにきてください)

参考URL

Lambda-backed Custom Resourceを利用してCloudFormationにループ処理を擬似的に実装する

AWS::Includeを利用してLambda-backed Custom Resourceをモジュール化する

Lambda-Backed Custom Resourceを利用してServerless Application Modelで定義したLambda関数にDead Letter Queueを設定する