【AWS Organizations】アカウントが 特定OUに所属したタイミングで Lambda を実行させる

CFnスタックセットのOrganizations連携 および Lambda-backed カスタムリソースを組み合わせた活用例です。S3パブリックアクセスブロックの自動有効化を試します。
2021.10.25

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

はじめに

AWS Organizations の 特定OU(Organizational Unit) にアカウントが所属したタイミングで、 そのアカウント内で特定 Lamdbaを実行する構成を作ってみます。

どうやって作るか?

AWS CloudFormation(CFn)の 2つの機能を使います。

  • CFnの Lambda-backed カスタムリソース
  • CFnスタックセットの Organizations 連携

CFnの Lambda-backed カスタムリソース

Lambda-backed カスタムリソース は CFnテンプレートで作ることができるリソースの1つです。 実際にリソースが作られるわけではなく、バックエンドで Lambda関数を実行することで リソース作成(or 更新) を完了とします。

詳しい説明や使う際のポイントは以下ブログが良くまとまっているので、ぜひ参照ください。

CFnスタックセットの Organizations 連携

CFnスタックセットは AWS Organizations 連携が可能です。 この連携により以下のような機能が使えるようになります。

  • OUをターゲットでスタックをデプロイ
  • スタックの自動デプロイ/削除

特に今回は 後者の『自動デプロイ/削除機能』を意識します。 これは OUに所属したアカウントへ自動でスタックを展開、 または OUから離脱したアカウントから自動でスタックを削除する機能です。

詳しい使い方は以下ブログを参照ください。

つまり

  • Lambda-backed カスタムリソースを含めた CFnテンプレートを作成
  • この CFnテンプレートを使って Organizations 連携の スタックセットを作成 (自動デプロイを ONにする)

これでアカウントが特定OUに所属したタイミングで 特定Lambda が実行されるようになります。

作ってみる

今回は例題として 『アカウントが 特定OUに所属したタイミングで Lambda(アカウントレベルの S3パブリックアクセスブロックを有効化) を実行させる』 仕組みを作ってみます。

CFnテンプレートの準備

以下リソースが必要です。

リソース名 タイプ 備考
EnableS3PublicAccessBlock カスタムリソース LambdaFunction を呼び出す
LambdaExecutionRole IAMロール LambdaFunction の実行ロール
LambdaFunction Lambda関数 Create時にアカウントレベルの S3ブロックパブリックアクセスアクセスを有効化する

CFnテンプレートを作成してみました。

AWSTemplateFormatVersion: "2010-09-09"

Parameters:
  RoleName:
    Type: String
    Default: InitialSetupRole-EnableS3PublicAccessBlock

Resources:
  EnableS3PublicAccessBlock:
    Type: Custom::EnableS3PublicAccessBlock
    Properties:
      ServiceToken: !GetAtt "LambdaFunction.Arn"      

  LambdaFunction:
    Type: AWS::Lambda::Function
    Properties:
      Role: !GetAtt "LambdaExecutionRole.Arn"
      Runtime: "python3.8"
      Handler: index.lambda_handler
      Timeout: 60
      Code:
        ZipFile: |
          import boto3
          import cfnresponse
          from logging import getLogger, INFO
          
          logger = getLogger()
          logger.setLevel(INFO)
          
          
          def enable_s3_public_access_block(event, context):
              logger.info('[START] enable_s3_public_access_block')
              logger.info("boto3 version:{}".format(boto3.__version__))
          
              try:
                  account_id = boto3.client('sts').get_caller_identity()['Account']
                  response = boto3.client('s3control').put_public_access_block(
                      PublicAccessBlockConfiguration={
                          'BlockPublicAcls': True,
                          'IgnorePublicAcls': True,
                          'BlockPublicPolicy': True,
                          'RestrictPublicBuckets': True
                      },
                      AccountId=account_id
                  )
                  if response['ResponseMetadata']['HTTPStatusCode'] == 200:
                      logger.info("  enable success")
                  else:
                      logger.info("  enable failed")
                  logger.info('[END] enable_s3_public_access_block')
              except Exception as e:
                  logger.error(e)
                  cfnresponse.send(event, context, cfnresponse.FAILED,
                                   {'Response': 'Failure'})
                  exit()
          
          
          def lambda_handler(event, context):
              if event['RequestType'] == 'Create':
                  # Create時に実行したい処理 --> S3パブリックアクセスブロック有効化
                  enable_s3_public_access_block(event, context)
                  cfnresponse.send(event, context, cfnresponse.SUCCESS,
                                   {'Response': 'Success'})
              if event['RequestType'] == 'Delete':
                  # Delete時に実行したい処理 --> 無し
                  cfnresponse.send(event, context, cfnresponse.SUCCESS,
                                   {'Response': 'Success'})
              if event['RequestType'] == 'Update':
                  # Update時に実行したい処理 --> 無し
                  cfnresponse.send(event, context, cfnresponse.SUCCESS,
                                   {'Response': 'Success'})

  LambdaExecutionRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Ref RoleName
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal:
              Service: "lambda.amazonaws.com"
            Action: "sts:AssumeRole"
      ManagedPolicyArns: 
        - "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
      Policies:
        - PolicyName: S3PutAccountPublicAccessBlock
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: Allow
                Action:
                  - "sts:GetCallerIdentity"
                  - "s3:PutAccountPublicAccessBlock"
                Resource: "*"

スタックセットの作成

適当に作成した SecureOU にスタックセットを仕込みます。

img

STACKSET_NAME="InitialSetup-EnableS3PublicAccessBlock"
OU_ID="ou-jk11-42ets6of"

# スタックセット作成
aws cloudformation create-stack-set \
--stack-set-name "${STACKSET_NAME}" \
--template-body file://${テンプレートのパス} \
--permission-model SERVICE_MANAGED \
--auto-deployment Enabled=true,RetainStacksOnAccountRemoval=false \
--capabilities CAPABILITY_IAM CAPABILITY_NAMED_IAM

# スタックインスタンス作成
aws cloudformation create-stack-instances \
--stack-set-name "${STACKSET_NAME}" \
--regions ap-northeast-1 \
--deployment-targets OrganizationalUnitIds=${OU_ID}

以下のようなスタックセットができました( SecureOU にはまだアカウントを所属させていないのでスタックインスタンスは無い)。

img

確認してみる

事前にメンバーアカウントの パブリックアクセスブロック を全て無効化しておきます。

img

メンバーアカウントを SecureOU へ移動させます。

img

移動後にスタックセットを確認すると CREATE オペレーションが開始されていました。 スタックインスタンスが作成されました。

img

メンバーアカウントを確認します。 パブリックアクセスブロック が有効になっていました。

img

CloudWatch ロググループへ配信しているログからも、実行完了を確認できました。

img

おわりに

CFnスタックセットのOrganizations 連携、および Lambda-backed カスタムリソースを組み合わせた活用例でした。

アカウントの初期セットアップの自動化に この自動デプロイ機能は活用できると思います。 CFnリソースに対応していないものは今回のように Lambda-backed カスタムリソースを使うことで対応できます。

他にも色々と活用例があると思います。有用そうなものがあればまたブログにします。

参考