私の戦闘力は53万です

awsとgcpについて書きます

Amazon CodeGuruハンズオン(JAWS-UG千葉)に参加しました

f:id:remmemento:20211112004547p:plain Amazon CodeGuruハンズオン(JAWS-UG千葉)に参加しました。
千葉と言いつつも、オンライン開催をされていて全国から参加し放題です。
今回はCodeGuruのハンズオンがあり、
なんとAWS所属の方々もスピーカー参加されておりました。
(しかもBlackbeltと同じkanasugiさん!)
またハンズオンのシナリオが公式のworkshopかと思うほど
きれいで学びになったのでブログでシェアできればと思い書いてみます。

jawsug-chiba.connpass.com

参考資料

ハンズオンの資料公開OKをもらったので
資料のリンクを貼っておきます。 この資料があればハンズオンできますので興味ある人はぜひ。 drive.google.com ハンズオン内で、コピペしたい箇所のテキストはこちら drive.google.com

資料前半の説明部分はBlackbeltに似た説明があるので
併せて聴くと良いと思います。 www.youtube.com

CodeGuru機能概要

今回のハンズオンではCodeGuruの下記2つの機能を使いました。

CodeGuru Reviewer

f:id:remmemento:20211111232449p:plain こちらはソースのレビューを実施し、改善箇所を指摘してくれる機能です。
今回のシナリオではCodeCommitでPRをして、
その際にCodeGuruがレビューをしてくれる流れになっています。
また、今回のハンズオンでは実施していませんが、
既存のGithub等のリポジトリをレビューをかけるようなこともできます。

CodeGuru Profiler

f:id:remmemento:20211111232617p:plain こちらは実稼働しているアプリケーションの実績をもとに、
どのコード部分がリソース消費に影響を与えているかを解析する機能です。
本ハンズオンでは、EC2上でAgentが動作し、 EC2の稼働状況のデータがCodeGuru Profilerに送られ、 その結果を見るシナリオがあります。

ハンズオンの概要

ハンズオンは2部構成で、Part1はCICDのパイプラインを作成します。
cloud9からcode系サービスを利用し、EC2にdeployできる環境を作ります。
こちらは前準備のようなもので、cfnを使って展開します。 f:id:remmemento:20211111230705p:plain

Part2で主役のCodeGuruを利用します。
初期のサンプルソースではCodeGuruから指摘が出るのですが、
その指摘部分を改善修正することで、
より指摘が少なくなる、といった具合で
ハンズオンのシナリオが組まれています。 f:id:remmemento:20211111230733p:plain

CodeGuru Reviewerを使ってみて

ハンズオン用のサンプルソースでは予め指摘が出るようになっていました。
例えば、AWSのベストプラクティスに反する箇所の指摘
(アクセスキー使わないでRole使って)だったり、
f:id:remmemento:20211111233815p:plain

一般的なJavaの指摘をしてくれます。
f:id:remmemento:20211111233843p:plain

指摘内容は理解しやすいよう
ほぼ全てリンクが付与されていました。
AWSの使い方に関する指摘は、AWSの公式ページのリンクが付与されています。

CodeGuru Profilerを使ってみて

Profilerは動作すると、下記のようなステータスが見れます。

f:id:remmemento:20211111234338p:plain リソースの利用状況を可視化でき、
例えば上記でVisualize CPUを押すと下記のような画面に遷移します。

f:id:remmemento:20211111234648p:plain 情報てんこ盛りですが、
libraryのコード(緑色)と自分のコード(青色)をひとめで区別できたり、
推奨事項を確認できたりと
実際の稼働状況から得られる情報が良い感じで集約されています。
便利ですね。

PythonでもCodeGuru Reviewerを使ってみた

ハンズオンは上記のような流れですが、
pythonでもやってみたかったので
ハンズオン終わった後に、CodeGuru Reviewerで
自分のgithub上のpythonのソースをレビューしてみました。

温度感を表すために、下記1個だけ指摘のケースを書きます。 f:id:remmemento:20211112000546p:plain learn moreのリンク先はpythonの公式ドキュメントで下記でした。 docs.python.org 指摘内容としてはget()の第二引数にNoneをわざわざ指定する必要ないよ、といった指摘でした。 修正してPRすると、指摘が解消されていることが確認できました。 このように一般的なベストプラクティスのような部分を指摘してくれるので良いですね。
これに慣れると、人間は業務ロジックのような
本来の部分に集中できるのかなと思います。

修正前

 eventArn = event.get('arn', None)

修正後

 eventArn = event.get('arn')

さいごに

今回ハンズオン実施頂いた主催者の方々、ありがとうございました!

JAWS-UG千葉支部のハンズオン、
いつも細部まで綺麗で、よく勉強させてもらっています、ありがとうございます。
今後も参加させてもらいます!

AWSの基礎を学ぼう Gateway Load Balancerに参加しました

AWSの基礎を学ぼう Gateway Load Balancerに参加しました
AWSの亀田さんが、ほぼ毎週開催されている
いろんなサービスを触ってみる勉強会です。
今週はGateway Load Balancer でした。
awsbasics.connpass.com

作成した環境

こんな感じの構成をハンズオンで構築できました。

f:id:remmemento:20210904152635p:plain
全体図

VPCが4つ存在し若干細々しているので、コメントを付けてみます f:id:remmemento:20210904154153p:plain

  • 左側のApplicationのVPCから通信がスタートするとします
  • Internet Out時にTransit Gatewayを経由し、右側のVPC(security用)に通信します
  • 右側のVPCでsecurityのcheckをします。このcheckをsecurity applianceのEC2が実施しており、
    そこへの負荷分散でGateway Load Balancerが登場します
  • securityのcheckを通過したら上のVPCを通じてinternet outします

最終的な通信のイメージは下記のような流れです。
f:id:remmemento:20210904155735p:plain

cloudformationを見てみる

実際のハンズオンではcloudformation templateをご提供頂きました。
Gateway Load Balancerや内部のネットワーク構成を
どう構築されているかcfnを見てみました。

※実際のハンズオンでは手順とcloudformation templateをご提供頂いたのですが、
全部公開はNGで、部分的には載せて良いですよ言って頂けたので一部だけ載せさせて頂きます。

Gateway Load Balancer

ALBとほぼ同じでした

  GWLB:
    Type: AWS::ElasticLoadBalancingV2::LoadBalancer
    Properties:
      Name: gwlb-lab
      Type: gateway
      Subnets:
        - !Ref SecurityApplianceSubnet1
        - !Ref SecurityApplianceSubnet2
      Tags:
      - Key: Name
        Value: gwlb-lab

ターゲットグループでEC2を登録しています

  GWLBTargetGroup:
    Type: AWS::ElasticLoadBalancingV2::TargetGroup
    Properties:
      Name: gwlb-target
      Port: 6081
      Protocol: GENEVE
      TargetGroupAttributes:
      - Key: deregistration_delay.timeout_seconds
        Value: 20
      VpcId: !Ref SecurityVPC
      HealthCheckPort: 22
      HealthCheckProtocol: TCP
      TargetType: instance
      Targets:
        - Id: !Ref ApplianceAZ1EC2Instance
        - Id: !Ref ApplianceAZ2EC2Instance
      Tags:
      - Key: Name
        Value: gwlb-target

VPC Endpoint

VPC Endpointを作成しています
ただ、VPC Endpointには直接loadbalancerの紐付けがされていませんでした。

  GWLBEAZ1:
    Type: AWS::EC2::VPCEndpoint
    Properties:
      VpcId: !Ref SecurityVPC
      ServiceName: !GetAtt GWLBESerivceName.Data
      VpcEndpointType: GatewayLoadBalancer
      SubnetIds:
       - !Ref SecurityApplianceSubnet1

  GWLBEAZ2:
    Type: AWS::EC2::VPCEndpoint
    Properties:
      VpcId: !Ref SecurityVPC
      ServiceName: !GetAtt GWLBESerivceName.Data
      VpcEndpointType: GatewayLoadBalancer
      SubnetIds:
       - !Ref SecurityApplianceSubnet2

代わりにVPC endpointサービスでGatewayLoadBalancerと紐付けをしていました。

  GWLBEService:
    Type: AWS::EC2::VPCEndpointService
    Properties:
      GatewayLoadBalancerArns:
        - !Ref GWLB
      AcceptanceRequired: false

そして、VPC endpointサービスと
VPC endopointを紐づけているようでした。
こちらにカスタムリソースが利用されておりました

  GWLBESerivceName:
    DependsOn: GWLBEService
    Type: Custom::DescribeVpcEndpointServiceConfigurations
    Properties:
      ServiceToken: !GetAtt DescribeGWLBEService.Arn
      Input: !Ref GWLBEService

customリソースを利用してvpc endpoitのサービス名を取得しているようです そして、取得したサービス名でVPC endpointとendpoint serviceを
紐づけているようです。

こうすることで2つのAZに存在するVPC endpointを1つのendpoint serviceに
まとめているようでした。
こんな作り方できるのかととても勉強になりました。

 DescribeGWLBEService:
    Type: AWS::Lambda::Function
    Properties:
      Handler: "index.handler"
      Role: !GetAtt
        - DescribeGWLBEServiceLambdaExecutionRole
        - Arn
      Code:
        ZipFile: |
          import boto3
          import cfnresponse
          import json
          import logging
          import time
          def handler(event, context):
            time.sleep(600)
            logger = logging.getLogger()
            logger.setLevel(logging.INFO)
            responseData = {}
            responseStatus = cfnresponse.FAILED
            logger.info('Received event: {}'.format(json.dumps(event)))
            if event["RequestType"] == "Delete":
              responseStatus = cfnresponse.SUCCESS
              cfnresponse.send(event, context, responseStatus, responseData)
            if event["RequestType"] == "Create":
              try:
                VpceServiceId = event["ResourceProperties"]["Input"]
              except Exception as e:
                logger.info('VPC Endpoint Service Id retrival failure: {}'.format(e))
              try:
                ec2 = boto3.client('ec2')
              except Exception as e:
                logger.info('boto3.client failure: {}'.format(e))
              try:
                response = ec2.describe_vpc_endpoint_service_configurations(
                  Filters=[
                    {
                      'Name': 'service-id',
                      'Values': [VpceServiceId]
                    }
                  ]
                )
              except Exception as e:
                logger.info('ec2.describe_vpc_endpoint_service_configurations fa: {}'.format(e))
              ServiceName = response['ServiceConfigurations'][0]['ServiceName']
              logger.info('service name: {}'.format(ServiceName))
              responseData['Data'] = ServiceName
              responseStatus = cfnresponse.SUCCESS
              cfnresponse.send(event, context, responseStatus, responseData)
      Runtime: python3.7
      Timeout: 900

ありがとうございました!

Gateway load balanerは初めて触ったのですが、
それ以外にも上記のように普段触ることないNW構成を触ることができ
とても勉強になりました
ありがとうございました!

cdkのsolution constructを利用してみた

f:id:remmemento:20210802095610p:plain

cdkのsolution constructが便利だったのでブログに書いてみます。

solution constructとは

AWSの公式ドキュメントに説明がありました。
aws.amazon.com

AWS Solutions Constructs は、2 つ以上の CDK のリソースを組み合わせ、ロギングや暗号化などのベストプラクティスを実装する複数サービスのパターンを提供します。

要はよくあるパターンをAWSが準備してくれているということですね。

本記事で利用したもの

今回はaws-events-rule-snsを利用してみました。
docs.aws.amazon.com

こちらのイメージ図は下記です。
よくある構成です。 f:id:remmemento:20210802095448p:plain

動かしてみる

下記のようにソースをサンプルで書いてみました。
enableEncryptionWithCustomerManagedKeyをfalseにしておくと
kmsをデフォルトキーで利用してくれます。
(私は最初こちらを指定していなくて無駄なkmsを作成してしまいました)

フォルダ構成イメージ

lib
└── aws-events-rule-sns-stack.ts
patterns
├── SecurityhubNotify.ts
└── index.ts

aws-events-rule-sns-stack.ts

import * as cdk from '@aws-cdk/core';
import { EventsRuleToSnsProps, EventsRuleToSns } from "@aws-solutions-constructs/aws-events-rule-sns";

import * as patterns from '../patterns/index'

export class AwsEventsRuleSnsStack extends cdk.Stack {
  public readonly constructStack: EventsRuleToSns;

  constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);
       
    const patternToNotify = patterns[patterns.patternName]
  
    const envcode = new cdk.CfnParameter(this, "envcode", {
      type: "String",
      description: "envcode",
      default:"test"
    });
      
    const constructStack = new EventsRuleToSns(this, 'eveSns', {
      eventRuleProps: {
        eventPattern: patternToNotify,
        ruleName: `${envcode.valueAsString}-${patterns.patternName}`
      },
      topicProps:{
        topicName:`${envcode.valueAsString}-${patterns.patternName}`
      },
      enableEncryptionWithCustomerManagedKey: false
    });
    this.constructStack=constructStack
  }
}

index.ts

export { SecurityhubNotify } from "./SecurityhubNotify";

export const patternName='SecurityhubNotify' ;

SecurityhubNotify.ts

export const SecurityhubNotify={
    "source": ["aws.securityhub"],
    "detail-type": ["Security Hub Findings - Imported"],
    "detail": {
      "findings": {
        "Compliance": {
          "Status": ["FAILED", "WARNING", "NOT_AVAILABLE"]
        }
      }
    } 
}

package.json(関連部分のみ抜粋しています)

  "dependencies": {
    "@aws-cdk/core": "^1.114.0",
    "@aws-solutions-constructs/aws-events-rule-sns": "^1.114.0",
    "source-map-support": "^0.5.19"
  },

deployとリソース確認

deployします

cdk deploy --parameters envcode=test

events+snssnsポリシーが作成されています。
ポリシーも良い感じでeventsへの許可が記載されています。

 {
      "Sid": "2",
      "Effect": "Allow",
      "Principal": {
        "Service": "events.amazonaws.com"
      },
      "Action": "sns:Publish",
      "Resource": "arn:aws:sns:ap-northeast-1:xxxxxx:test-SecurityhubNotify"
    }

触ってみて

感覚的にはeventsのフィルター部分だけ書けば、
events + snsを作成してくれるので便利でした。

他にもlambdaやs3等を絡めたパターンもあるようなので
機会があれば触ってみようと思います。

SAMでgithub actionsを利用する時にIAMユーザを払い出さないで利用したい

f:id:remmemento:20210703211848p:plain
先日こちらでAWS SAMとgithub actionsの連携を試してみました。

cloud-aws-gcp.hateblo.jp

上記利用する際に、IAMユーザとクレデンシャルを払い出す必要がありました。
ただ、諸事情によりIAMユーザを払い出さないで利用したいというケースがありました。
私の環境ではAWS SSOを利用していたため
AWS SSOで一時的なcredentialを取得し、
その一時的なcredentialをgitにsecretとして登録し、
SAMのdeployができないかと考え試してたところ
できたのでブログ化してみました。

設定方法

AWS SSOでcredentialを取得

まずはAWS SSOでcredentialを取得します。 f:id:remmemento:20210721094908p:plain これは、STS等を利用しても取得が可能ですが、
SSOを利用すると1クリックで取得可能なので便利です
上記の「click to... 」の箇所をクリックすればOKです
下記のようなクレデンシャルが取得できると思います。

export AWS_ACCESS_KEY_ID="xxxxxxxxxxx"
export AWS_SECRET_ACCESS_KEY="xxxxxxxx"
export AWS_SESSION_TOKEN="xxxxxxxxxxx"

そして取得した一時credentialを
githubのsecretとして設定します。
そのまま貼り付ければOKでした。 下記の例ではEXPORTSとして登録しています。

f:id:remmemento:20210721095106p:plain

github actionsの設定ファイルを下記のように記載し
pushしたところ正常にdeployができました。
変化点は最後のsam deployのところで、SSOのcredentialを設定した上で、
sam deployを実行しています。

on:
  push:
    branches:
      - master
env:
  stackName: aws-xxxxxxx
  s3BucketName: aws-xxxxxxxxx
  deployRegion: ap-southeast-1
jobs:
  build-deploy:
    runs-on: ubuntu-latest
    steps:
      - run: echo ${{ secrets.DOCKERHUB_TOKEN }} | docker login -u ${{ secrets.DOCKERHUB_USER }} --password-stdin
      - uses: actions/checkout@v2
      - uses: actions/setup-python@v2
        with:
          python-version: '3.7'
      - uses: aws-actions/setup-sam@v1
      # - uses: aws-actions/configure-aws-credentials@v1
      #   with:
      #     aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
      #     aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
      #     aws-region: ap-northeast-1
      
      # sam build 
      - run: sam build --use-container
      
      # Run Unit tests- Specify unit tests here 
      # - run : pip install -r requirements.txt

      # sam deploy
      - run: ${{ secrets.EXPORTS }} &&  sam deploy --no-confirm-changeset --no-fail-on-empty-changeset --stack-name ${{ env.stackName }} --s3-bucket ${{ env.s3BucketName }} --capabilities CAPABILITY_IAM --region ${{ env.deployRegion }}

使ってみて

上記は一時的なcredentialなので、時間が切れば権限が切れます。
デプロイする都度上記の設定するのは面倒ですが、
一時的なcredentialの時間制限は最大12時間なので、
一回払い出して設定すれば、その日ぐらいは使いまわせると思います。
たまにしか更新しないものであれば、払い出すIAMの管理も面倒だし、
このやり方も一つかなと思いました。
もっと良いやり方があるという方がいらっしゃたら教えて頂けますと嬉しいです。

AWS SAM がgithub actionsを使えるようになったので試してみた

f:id:remmemento:20210703211848p:plainf:id:remmemento:20210703211705p:plain

AWS SAMがgithub actionsを利用できるようになったようなので使ってみました。
使ってみるとめちゃくちゃ良かったので、記事にしてみました。

事前準備

まず何でも良いのでsamを作成します

sam init -r python3.8 -n github-actions-with-aws-sam --app-template "hello-world"

続いて.github/workflows/sam-pipeline.ymlを追加します
※regionとs3バケットを書き換えてください

on:
  push:
    branches:
      - main
jobs:
  build-deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - uses: actions/setup-python@v2
      - uses: aws-actions/setup-sam@v1
      - uses: aws-actions/configure-aws-credentials@v1
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          aws-region: ##region##
      # sam build 
      - run: sam build --use-container

# Run Unit tests- Specify unit tests here 

# sam deploy
      - run: sam deploy --no-confirm-changeset --no-fail-on-empty-changeset --stack-name sam-hello-world --s3-bucket ##s3-bucket## --capabilities CAPABILITY_IAM --region ##region##

credentialの登録

githubのsettings > secretsで下記を登録します。

pushしてみる

上記で準備ができましたのでmainにpushしてみます

git add .
git commit -m "add github actions"
git push origin main

f:id:remmemento:20210703204759p:plain

無事にactionsが完了しました。 APIGatewayが作成されていますので
実行してみるとレスポンスが返ってくることを確認できました。

curl -X GET  https://xxxxxxx.execute-api.ap-northeast-1.amazonaws.com/Prod/hello
{"message": "hello world"}

変更してみる

さらに、lambdaを変更し、再度pushすると、
変更が反映されていることが確認できました。

cat hello_world/app.py

import json

# import requests


def lambda_handler(event, context):
  
    return {
        "statusCode": 200,
        "body": json.dumps({
            "message": "hello world update",
            # "location": ip.text.replace("\n", "")
        }),
    }
curl -X GET  https://xxxxxxx.execute-api.ap-northeast-1.amazonaws.com/Prod/hello
{"message": "hello world update"}

テスト(pytest)を実行してみる

pytest利用する場合も試してみました。
簡単にテストを書いてみます

cat tests/unit/test_handler.py
import json
import pytest
from hello_world import app

def test_lambda_handler():

    ret = app.lambda_handler("", "")
    assert ret["statusCode"] == 200

def test_err_lambda_handler():
    ret = app.lambda_handler("", "")
    assert ret["statusCode"] == 400

github actionsでpytest可能なよう設定します

cat requirements.txt
pytest

.github/workflows/sam-pipeline.ymlにテスト部分を追記します

 # sam build 
      - run: sam build --use-container

      # Run Unit tests- Specify unit tests here 
      - run : pip install -r requirements.txt # 追記
      - run: pytest -s -k test_lambda_handler # 追記
      # - run: pytest -s -k test_err_lambda_handler # 追記

pytestが実行されることが確認できました f:id:remmemento:20210703205512p:plain

上記のコメントアウトしていた失敗する方のテストケースも流してみます。 .github/workflows/sam-pipeline.ymlを修正します

 # sam build 
      - run: sam build --use-container

      # Run Unit tests- Specify unit tests here 
      - run : pip install -r requirements.txt # 追記
      - run: pytest -s -k test_lambda_handler # 追記
      - run: pytest -s -k test_err_lambda_handler # 追記

きちんと失敗してくれていることが確認できました f:id:remmemento:20210703205549p:plain

失敗した場合は、後続の処理が行われず
デプロイがされずに止まってくれます。

最後に

個人的には今後samを利用するときは、
この使い方がベースになりそうなほど良かったです。

今回試してみたのの元ネタはこちらです。
細かい箇所はこちらも併せてご参考ください。 aws.amazon.com

AWS organizationsでcloudtrailを別Accountへ権限移譲する

AWS Organizationsの連携機能を色々と試しており、
cloudtrailでも連携機能を試してみました。

下記を参考にさせて頂いており、
KMSも含めて権限移譲をやろうとすると割と面倒で、
最終的にcloudformationを作成したのでブログ化してみました。 fu3ak1.hatenablog.com

Organizations とCloudtrail連携機能(AWS純正)

AWS Organizationsでは、Cloudtrailと連携し、
複数アカウントでのcloudtrail設定を簡単に実現できます。
cloudtrailで単純にorganizations連携をすると
下記のようになります。

f:id:remmemento:20210501182513p:plain
単純なorganizations+cloudtrail

上記でも十分にcloudtrailログを集約できるのですが
AWSのマルチアカウントのベストプラクティスでは、
ログ集約用の目的別のアカウント
(以下、LogArchiveアカウントと表現します)の別途作成を推奨しています。
要は何でもかんでも、親アカウントに集めるのは
権限が集約しすぎて良くないということですね。
上記理由から、純正機能でのcloudtrail集約をそのまま利用すると
ベストプラクティスに沿っていないのかと思いました。
aws.amazon.com

ベストプラクティス風にするには

LogArchiveアカウントを別にした構成を考えると
下記のように設定したいと考えました。

f:id:remmemento:20210501180719p:plain
構成イメージ

別途LogArchiveアカウントを作成し、
AdminAccountで収集したcloudtrailログを
LogArchiveアカウントに集約します。

上記、手動で設定しても良いですが、
割とポリシー系が面倒だったので、
cloudformationを作ってしてみました。

1.LogArchiveアカウントでの作業

f:id:remmemento:20210501180719p:plain まず、LogArchiveアカウントで、
S3とKMSを作成します。
LogArchiveアカウントで以下のcloudformationを適応します。

AWSTemplateFormatVersion: "2010-09-09"
Description: A templete for

Parameters:
  EnvPrefix:
    Type: String
    Default: OrgTrail
  orgAccountId:
    Type: String
    Default: xxxxxxxxxxxx
    Description: "Organization Admin Account Id"
  S3BucketName:
    Type: String
    Default: xxxxxxxxxxxxxxxxxxxxxxxxxx
    Description: "bucket name for cloudtrail"

Resources:
  mykms:
    Type: AWS::KMS::Key
    Properties:
      Description: !Sub ${EnvPrefix}Key
      Enabled: true
      EnableKeyRotation: true
      KeyUsage: ENCRYPT_DECRYPT
      PendingWindowInDays: 30
      KeyPolicy:
        Version: '2012-10-17'
        Id: key-policy
        Statement:
        - Sid: Enable IAM User Permissions
          Effect: Allow
          Principal:
            AWS: !Sub arn:aws:iam::${AWS::AccountId}:root
          Action: kms:*
          Resource: "*"
        - Sid: Allow use of the key
          Effect: Allow
          Principal:
            AWS: !Sub arn:aws:iam::${orgAccountId}:root
          Action:
          - kms:Encrypt
          - kms:Decrypt
          - kms:ReEncrypt*
          - kms:GenerateDataKey*
          - kms:DescribeKey
          Resource: "*"
        - Sid: Allow attachment of persistent resources
          Effect: Allow
          Principal:
            AWS: !Sub arn:aws:iam::${orgAccountId}:root
          Action:
          - kms:CreateGrant
          - kms:ListGrants
          - kms:RevokeGrant
          Resource: "*"
          Condition:
            Bool:
              kms:GrantIsForAWSResource: 'true'
        - Sid: Enable IAM User Permissions
          Effect: Allow
          Principal:
            AWS:
            - !Sub arn:aws:iam::${orgAccountId}:root
          Action: kms:*
          Resource: "*"
        - Sid: Allow CloudTrail to encrypt logs
          Effect: Allow
          Principal:
            Service: cloudtrail.amazonaws.com
          Action: kms:GenerateDataKey*
          Resource: "*"
          Condition:
            StringLike:
              'kms:EncryptionContext:aws:cloudtrail:arn': !Sub arn:aws:cloudtrail:*:${orgAccountId}:trail/*
        - Sid: Allow CloudTrail to describe key
          Effect: Allow
          Principal:
            Service: cloudtrail.amazonaws.com
          Action: kms:DescribeKey
          Resource: "*"
        - Sid: Allow principals in the account to decrypt log files
          Effect: Allow
          Principal:
            AWS: "*"
          Action:
          - kms:Decrypt
          - kms:ReEncryptFrom
          Resource: "*"
          Condition:
            StringEquals:
              kms:CallerAccount: !Ref orgAccountId
            StringLike:
              'kms:EncryptionContext:aws:cloudtrail:arn': !Sub arn:aws:cloudtrail:*:${orgAccountId}:trail/*
        - Sid: Allow alias creation during setup
          Effect: Allow
          Principal:
            AWS: "*"
          Action: kms:CreateAlias
          Resource: "*"
          Condition:
            StringEquals:
              kms:CallerAccount: !Ref orgAccountId
              kms:ViaService: ec2.us-east-1.amazonaws.com
        - Sid: Enable cross account log decryption
          Effect: Allow
          Principal:
            AWS: "*"
          Action:
          - kms:Decrypt
          - kms:ReEncryptFrom
          Resource: "*"
          Condition:
            StringEquals:
              kms:CallerAccount: !Ref orgAccountId
            StringLike:
              'kms:EncryptionContext:aws:cloudtrail:arn': !Sub arn:aws:cloudtrail:*:${orgAccountId}:trail/*

# Outputs:
#   Outputskmsid:
#     Description: kms-trail-organizations
#     Value: !GetAtt mykms.Arn
#     Export:
#       Name: !Sub ${EnvPrefix}-kms



  S3BucketOrg:
      Type: "AWS::S3::Bucket"
      Properties:
        BucketName: !Ref S3BucketName
        PublicAccessBlockConfiguration:
          BlockPublicAcls: true
          BlockPublicPolicy: true
          IgnorePublicAcls: true
          RestrictPublicBuckets: true

  S3BucketpublicdataBucketPolicy:
      Type: AWS::S3::BucketPolicy
      Properties:
        Bucket: !Ref S3BucketOrg
        PolicyDocument:
          Statement:
            - Sid: AWSCloudTrailAclCheck
              Effect: Allow
              Principal:
                Service: cloudtrail.amazonaws.com
              Action: "s3:GetBucketAcl"
              Resource: !Sub arn:aws:s3:::${S3BucketName}
            - Sid: AWSCloudTrailWrite
              Effect: Allow
              Principal:
                Service: cloudtrail.amazonaws.com
              Action: 's3:PutObject'
              Resource: !Sub arn:aws:s3:::${S3BucketName}/AWSLogs/${orgAccountId}/*
              Condition:
                StringEquals:
                  's3:x-amz-acl': bucket-owner-full-control
            - Sid: AWSCloudTrailWriteMyaccount
              Effect: Allow
              Principal:
                Service: cloudtrail.amazonaws.com
              Action: 's3:PutObject'
              Resource: !Sub arn:aws:s3:::${S3BucketName}/AWSLogs/${AWS::AccountId}/*
              Condition:
                StringEquals:
                  's3:x-amz-acl': bucket-owner-full-control

2.AdminAccount(親アカウント)での作業

次にAdminAccount(organizationsの親アカウント)で、
cloudtrailを作成します。
この時、パラメータに上記で作成したLogArchiveアカウントの
S3の名前とKMSのidを設定します。
AdminAccountで以下のcloudformationを適応します。

AWSTemplateFormatVersion: "2010-09-09"
Description: A templete for taf init

Parameters:
  S3BucketName:
    Type: String
    Default: xxxxxxxxxxxxxxxxxx
    Description: "bucket name for cloudtrail. this is expected to be log Archive's account bucket name"
  kmsid:
    Type: String
    Default: 3e20a5ef-xxxxxxx-xxxx-xxxx
    Description: "kms-id in log Archive Account"
  AccountIdLog:
    Type: String
    Default: xxxxxxxxxx
    Description: "AWS Account Id of logArchive"

Resources :
  mytrail:
    Type: AWS::CloudTrail::Trail
    Properties:
#      CloudWatchLogsLogGroupArn: String
#      CloudWatchLogsRoleArn: String
      EnableLogFileValidation: true
#      EventSelectors:
#        - EventSelector
      IncludeGlobalServiceEvents: true
      IsLogging: true
      IsMultiRegionTrail: true
      KMSKeyId: !Sub arn:aws:kms:us-east-1:${AccountIdLog}:key/${kmsid}
      S3BucketName: !Ref S3BucketName
#      S3KeyPrefix: String
#      SnsTopicName: String
#      Tags:
#        - Resource Tag
      TrailName: !Ref S3BucketName

cloudtrailをorganizations連携する

上記適応後に、AWSコンソールから作成されたcloudtrailを開き、
「Enable for all accounts in my organization」を有効化します。
cloudformationでは、この設定値が設定できなかったので、
ここだけ手動設定になります。

f:id:remmemento:20210501190447p:plain
cloudtrail

上記でベストプラクティス風の構成が実現できました。 f:id:remmemento:20210501180719p:plain

さいごに

AWSでcloudtrailの権限移譲を構成してみました。
冒頭で紹介したブログは、cloudtrail以外にも、
Organiztions関連の連携まとめが記載されており分かりやすいです。
いつも参考にさせてもらっています。
ありがとうございます。
このブログもどなたか参考にして頂けると嬉しいです。 fu3ak1.hatenablog.com

AWS Control Towerのcloudformationを読み解いたので誰かに褒めてほしい

f:id:remmemento:20210527140256p:plain

AWS Control Towerを触ってみた

Contorl Towerの東京リージョンがサポートされたので触ってみました。
色々と調査する過程で、Control Towerを有効化した時に
裏側で流れるcloudformationを読み解いて図示したので
きっと誰かの役に立つと思いブログ化してみました。

Control Towerを有効化する前

簡単に、有効化前はorganizationsの配下にたくさんアカウントがいると仮定します。

Control Towerを有効化した後

control towerを有効化しても、既存アカウントに即座には影響はありません。

control towerが有効化されているOUの配下にアカウントを持ってくると
各種設定(cloudformationやらSCPやら)が有効化されます。

ここで、SCPは分かりやすくドキュメントに整理されていました。 docs.aws.amazon.com

ただ、cloudformationの方は、分かりやすく記載されたものが
見つけられませんでした。
ドキュメントをみてみると、cloudformaitonを確認するのが良いと書いてあったので、
大変でしたが自分で読み解いてみました。
読み解くと結構時間かかったので、
今後、同じ想いで苦労する人を救いたい一心で結果を図示してみました。

裏側のcloudformationを図示

全部読み解くと、下記のようになっていました。
control towerの全体図です。

裏側のcloudformation詳細

ここからは各stackの中身を記載していきます。
読み解く時にはメモが参考になると思います。
もし間違いを見つけた場合は、ご連絡いただけますと嬉しいです。

AWSControlTowerBP-BASELINE-CLOUDTRAIL

概要

trailを有効化し結果をS3とlogsに出力する
S3はアーカイブアカウント宛で、マルチリージョンを記録します
logs連携は有効化され、retentionは14日
configはsnsを設定、セキュリティアカウントの「aws-controltower-AllConfigNotification」に通知 します

デプロイ対象

下記のcontroltowerを有効化した1つのリージョン
core ou
custome ou

cloudformation template
AWSTemplateFormatVersion: 2010-09-09
Description: Configure AWS CloudTrail

Parameters:
  ManagedResourcePrefix:
    Type: 'String'
    Description: 'Prefix for the managed resources'

  EnableLogFileValidation:
    Type: String
    Default: 'true'
    Description: Indicates whether CloudTrail validates the integrity of log files.
    AllowedValues:
      - 'true'
      - 'false'

  IncludeGlobalEvents:
    Type: String
    Default: 'true'
    Description: Indicates whether the trail is publishing events from global services, such as IAM, to the log files.
    AllowedValues:
      - 'true'
      - 'false'

  MultiRegion:
    Type: String
    Default: 'true'
    Description: Indicates whether the CloudTrail trail is created in the region in which you create the stack (false) or in all regions (true).
    AllowedValues:
      - 'true'
      - 'false'

  AllConfigTopicName:
    Type: String
    Default: ''
    Description: All Configuration Notification SNS Topic in Security Account that AWS Config delivers notifications to.

  SecurityAccountId:
    Type: 'String'
    Description: AWS Account Id of the Security account.

  AuditBucketName:
    Type: String
    Default: ''
    Description: Audit Bucket name from the Log Archive Account

  PublishToCloudWatchLogs:
    Type: String
    Default: 'true'
    Description: Indicates whether notifications are published to CloudWatch Logs.
    AllowedValues:
      - 'true'
      - 'false'

  LogsRetentionInDays:
    Description: 'Specifies the number of days you want to retain CloudTrail log events in the CloudWatch Logs.'
    Type: Number
    Default: 14
    AllowedValues: [1, 3, 5, 7, 14, 30, 60, 90, 120, 150, 180, 365, 400, 545, 731, 1827, 3653]

  AWSLogsS3KeyPrefix:
    Type: 'String'
    Description: 'Organization ID to use as the S3 Key prefix for storing the audit logs'

Conditions:
  IsMultiRegion: !Equals
    - !Ref MultiRegion
    - 'true'

  IsPublishToCloudWatchLogs: !Equals
    - !Ref PublishToCloudWatchLogs
    - 'true'

Resources:
  Trail:
    Type: AWS::CloudTrail::Trail
    Properties:
      TrailName: !Sub ${ManagedResourcePrefix}-BaselineCloudTrail
      S3BucketName: !Ref AuditBucketName
      S3KeyPrefix: !Ref AWSLogsS3KeyPrefix
      SnsTopicName: !Sub arn:aws:sns:${AWS::Region}:${SecurityAccountId}:${AllConfigTopicName}
      IsLogging: True
      EnableLogFileValidation: !Ref EnableLogFileValidation
      IncludeGlobalServiceEvents: !If
        - IsMultiRegion
        - True
        - !Ref IncludeGlobalEvents
      IsMultiRegionTrail: !Ref MultiRegion
      CloudWatchLogsLogGroupArn: !If
        - IsPublishToCloudWatchLogs
        - !GetAtt TrailLogGroup.Arn
        - !Ref AWS::NoValue
      CloudWatchLogsRoleArn: !If
        - IsPublishToCloudWatchLogs
        - !GetAtt TrailLogGroupRole.Arn
        - !Ref AWS::NoValue

  TrailLogGroup:
    Type: 'AWS::Logs::LogGroup'
    Condition: IsPublishToCloudWatchLogs
    Properties:
      LogGroupName: !Sub ${ManagedResourcePrefix}/CloudTrailLogs
      RetentionInDays: !Ref LogsRetentionInDays

  TrailLogGroupRole:
     Type: 'AWS::IAM::Role'
     Condition: IsPublishToCloudWatchLogs
     Properties:
       RoleName: !Sub ${ManagedResourcePrefix}-CloudWatchLogsRole
       AssumeRolePolicyDocument:
         Version: '2012-10-17'
         Statement:
         - Sid: CloudTrailAssumeRole
           Effect: Allow
           Principal:
             Service: 'cloudtrail.amazonaws.com'
           Action: 'sts:AssumeRole'
       Policies:
       - PolicyName: 'cloudtrail-policy'
         PolicyDocument:
           Version: '2012-10-17'
           Statement:
           - Sid: AWSCloudTrailCreateLogStream
             Effect: Allow
             Action: 'logs:CreateLogStream'
             Resource: !GetAtt 'TrailLogGroup.Arn'
           - Sid: AWSCloudTrailPutLogEvents
             Effect: Allow
             Action: 'logs:PutLogEvents'
             Resource: !GetAtt 'TrailLogGroup.Arn'

Outputs:
  BaselineCloudTrail:
    Description: Baseline CloudTrail
    Value: !GetAtt 'Trail.Arn'
  CloudWatchLogsGroupArn:
    Description: CloudWatch Log Group ARN for Baseline CloudTrail
    Value: !GetAtt 'TrailLogGroup.Arn'
  CloudWatchLogsGroupName:
    Description: CloudWatch Log Group Name for Baseline CloudTrail
    Value: !Ref TrailLogGroup

AWSControlTowerBP-BASELINE-CLOUDWATCH

概要

LambdaとSNS
configのルールに準拠していない場合は、子アカウントのLambdaを経由して
AuditアカウントのSNSに通知する
デフォルトではSNSの名前は下記になっている
aws-controltower-AggregateSecurityNotifications

デプロイ対象

下記のcontroltowerがサポートされている全リージョン
core ou
custome ou

cloudformation template
---
AWSTemplateFormatVersion: '2010-09-09'
Description: Configure Cloudwatch Rule, local SNS Topic, forwarding notifications from local SNS Topic to Security Topic
Parameters:
  ManagedResourcePrefix:
    Type: 'String'
    Description: 'Prefix for the managed resources'
  SecurityTopicName:
    Type: String
    Description: Security Notification SNS Topic Name.
  SecurityAccountId:
    Type: 'String'
    Description: AWS Account Id of the Security account.
  LogsRetentionInDays:
    Description: 'Specifies the number of days you want to retain notification forwarding log events in the Lambda log group.'
    Type: Number
    Default: 14
    AllowedValues: [1, 3, 5, 7, 14, 30, 60, 90, 120, 150, 180, 365, 400, 545, 731, 1827, 3653]
  EnableConfigRuleComplianceChangeAlarm:
    Type: String
    Description: "Enable notifications for AWS Config rule compliance status changes?"
    Default: true
    AllowedValues:
    - true
    - false

Mappings:
  TopicNameSuffix:
    LocalTopicName:
      Suffix: 'SecurityNotifications'

Conditions:
  EnableConfigRuleChangeNotification: !Equals
    - !Ref EnableConfigRuleComplianceChangeAlarm
    - 'true'

Resources:

  ForwardSnsNotification:
    Type: 'AWS::Lambda::Function'
    Properties:
      FunctionName: !Sub ${ManagedResourcePrefix}-NotificationForwarder
      Description: SNS message forwarding function for aggregating account notifications.
      Code:
        ZipFile:
          !Sub |
            from __future__ import print_function
            import boto3
            import json
            import os
            def lambda_handler(event, context):
                #print("Received event: " + json.dumps(event, indent=2))
                sns = boto3.client('sns')
                subject=event['Records'][0]['Sns']['Subject']
                if subject is None:
                    subject = 'None'
                message = event['Records'][0]['Sns']['Message']
                try:
                    msg = json.loads(message)
                    message = json.dumps(msg, indent=4)
                    if 'detail-type' in msg:
                      subject = msg['detail-type']
                except:
                    print('Not json')
                response = sns.publish(
                    TopicArn=os.environ.get('sns_arn'),
                    Subject=subject,
                    Message=message
                )
                print(response)
                return response
      Handler: 'index.lambda_handler'
      MemorySize: 128
      Role: !Sub arn:aws:iam::${AWS::AccountId}:role/${ManagedResourcePrefix}-ForwardSnsNotificationRole
      Runtime: 'python3.6'
      Timeout: 60
      Environment:
        Variables:
          sns_arn: !Sub arn:aws:sns:${AWS::Region}:${SecurityAccountId}:${SecurityTopicName}

  ForwardSnsNotificationGroup:
    Type: 'AWS::Logs::LogGroup'
    Properties:
      LogGroupName: !Sub '/aws/lambda/${ForwardSnsNotification}'
      RetentionInDays: !Ref LogsRetentionInDays

  LocalSecurityTopic:
    Type: AWS::SNS::Topic
    Properties:
      DisplayName: !Join [ "-", [ !Ref ManagedResourcePrefix, !FindInMap [TopicNameSuffix, LocalTopicName, Suffix] ]]
      TopicName: !Join [ "-", [ !Ref ManagedResourcePrefix, !FindInMap [TopicNameSuffix, LocalTopicName, Suffix] ]]

  SNSNotificationPolicy:
    Type: AWS::SNS::TopicPolicy
    Metadata:
      cfn_nag:
        rules_to_suppress:
          - id: F18
            reason: "Condition restricts permissions to current account."
    Properties:
      Topics:
        - !Ref LocalSecurityTopic
      PolicyDocument:
        Statement:
          - Sid: __default_statement_ID
            Effect: Allow
            Principal:
              AWS: "*"
            Action:
            - SNS:GetTopicAttributes
            - SNS:SetTopicAttributes
            - SNS:AddPermission
            - SNS:RemovePermission
            - SNS:DeleteTopic
            - SNS:Subscribe
            - SNS:ListSubscriptionsByTopic
            - SNS:Publish
            - SNS:Receive
            Resource: !Ref LocalSecurityTopic
            Condition:
              StringEquals:
                AWS:SourceOwner: !Sub ${AWS::AccountId}
          - Sid: TrustCWEToPublishEventsToMyTopic
            Effect: Allow
            Principal:
              Service: events.amazonaws.com
            Action: sns:Publish
            Resource: !Ref LocalSecurityTopic

  SNSNotificationSubscription:
    Type: "AWS::SNS::Subscription"
    Properties:
      Endpoint: !GetAtt ForwardSnsNotification.Arn
      Protocol: lambda
      TopicArn: !Ref LocalSecurityTopic

  SNSInvokeLambdaPermission:
    Type: AWS::Lambda::Permission
    Properties:
      Action: lambda:InvokeFunction
      Principal: sns.amazonaws.com
      SourceArn: !Ref LocalSecurityTopic
      FunctionName: !GetAtt ForwardSnsNotification.Arn

  # Enable notifications for AWS Config Rule compliance changes
  CWEventRuleComplianceChangeEvent:
    Type: AWS::Events::Rule
    Condition: EnableConfigRuleChangeNotification
    Properties:
      Name: !Sub ${ManagedResourcePrefix}-ConfigComplianceChangeEventRule
      Description: 'CloudWatch Event Rule to send notification on Config Rule compliance changes.'
      EventPattern:
        {
          "source": [
            "aws.config"
          ],
          "detail-type": [
            "Config Rules Compliance Change"
          ]
        }
      State: ENABLED
      Targets:
      - Id: !Sub 'Compliance-Change-Topic'
        Arn: !Ref LocalSecurityTopic

Outputs:
  LocalSecurityTopic:
    Description: Local Security Notification SNS Topic ARN
    Value: !Ref LocalSecurityTopic
  LocalSecurityTopicName:
    Description: Local Security Notification SNS Topic Name
    Value: !GetAtt LocalSecurityTopic.TopicName

AWSControlTowerBP-BASELINE-CONFIG

概要

configを有効化し、security(Audit)のアカウントにアグリゲーションする configの通知はsecurity(Audit)のアカウントの下記に通知する aws-controltower-AllConfigNotifications

デプロイ対象

下記のcontroltowerがサポートされている全リージョン
core ou
custome ou

cloudformation template
AWSTemplateFormatVersion: 2010-09-09
Description: Configure AWS Config

Parameters:
  ManagedResourcePrefix:
    Type: 'String'
    Description: 'Prefix for the managed resources'

  AllSupported:
    Type: String
    Default: 'true'
    Description: Indicates whether to record all supported resource types.
    AllowedValues:
      - 'true'
      - 'false'

  IncludeGlobalResourceTypes:
    Type: String
    Default: 'true'
    Description: Indicates whether AWS Config records all supported global resource types.
    AllowedValues:
      - 'true'
      - 'false'

  ResourceTypes:
    Type: CommaDelimitedList
    Description: A list of valid AWS resource types to include in this recording group. Eg. AWS::CloudTrail::Trail

  Frequency:
    Type: String
    Default: 1hour
    Description: The frequency with which AWS Config delivers configuration snapshots.
    AllowedValues:
      - 1hour
      - 3hours
      - 6hours
      - 12hours
      - 24hours

  AllConfigTopicName:
    Type: String
    Default: ''
    Description: All Configuration Notification SNS Topic in Security Account that AWS Config delivers notifications to.

  SecurityAccountId:
    Type: 'String'
    Description: AWS Account Id of the Security account.

  AuditBucketName:
    Type: String
    Default: ''
    Description: Audit Bucket name from the Log Archive Account

  AWSLogsS3KeyPrefix:
    Type: 'String'
    Description: Organization ID to use as the S3 Key prefix for storing the audit logs

  HomeRegionName:
    Type: 'String'
    Description: The name of the home region for the customer

  IsHomeRegionInitialControlTowerRegion:
    Type: 'String'
    AllowedValues:
       - 'true'
       - 'false'
    Description: Indicates whether the Home Region of the customer is one of the initial regions in which AWS Control Tower launched.

Conditions:
  IsAllSupported: !Equals
    - !Ref AllSupported
    - 'true'

  CreateHomeRegionConfigAggregatorAuthorization: !Equals
   -  !Ref IsHomeRegionInitialControlTowerRegion
   -  'false'

  CreateRegionalConfigAggregatorAuthorization: !Equals
   -  !Ref IsHomeRegionInitialControlTowerRegion
   -  'true'

Mappings:
  Settings:
    FrequencyMap:
      1hour   : One_Hour
      3hours  : Three_Hours
      6hours  : Six_Hours
      12hours : Twelve_Hours
      24hours : TwentyFour_Hours

Resources:
  ConfigRecorder:
    Type: AWS::Config::ConfigurationRecorder
    Properties:
      Name: !Sub ${ManagedResourcePrefix}-BaselineConfigRecorder
      RoleARN: !Sub arn:aws:iam::${AWS::AccountId}:role/${ManagedResourcePrefix}-ConfigRecorderRole
      RecordingGroup:
        AllSupported: !Ref AllSupported
        IncludeGlobalResourceTypes: !Ref IncludeGlobalResourceTypes
        ResourceTypes: !If
          - IsAllSupported
          - !Ref AWS::NoValue
          - !Ref ResourceTypes

  ConfigDeliveryChannel:
    Type: AWS::Config::DeliveryChannel
    Properties:
      Name: !Sub ${ManagedResourcePrefix}-BaselineConfigDeliveryChannel
      ConfigSnapshotDeliveryProperties:
        DeliveryFrequency: !FindInMap
          - Settings
          - FrequencyMap
          - !Ref Frequency
      S3BucketName: !Ref AuditBucketName
      S3KeyPrefix: !Ref AWSLogsS3KeyPrefix
      SnsTopicARN: !Sub arn:aws:sns:${AWS::Region}:${SecurityAccountId}:${AllConfigTopicName}

  AuthorizerPdx:
    Condition: CreateRegionalConfigAggregatorAuthorization
    Type: "AWS::Config::AggregationAuthorization"
    Properties:
      AuthorizedAccountId: !Ref SecurityAccountId
      AuthorizedAwsRegion: us-west-2
  AuthorizerIad:
    Condition: CreateRegionalConfigAggregatorAuthorization
    Type: "AWS::Config::AggregationAuthorization"
    Properties:
      AuthorizedAccountId: !Ref SecurityAccountId
      AuthorizedAwsRegion: us-east-1
  AuthorizerCmh:
    Condition: CreateRegionalConfigAggregatorAuthorization
    Type: "AWS::Config::AggregationAuthorization"
    Properties:
      AuthorizedAccountId: !Ref SecurityAccountId
      AuthorizedAwsRegion: us-east-2
  AuthorizerDub:
    Condition: CreateRegionalConfigAggregatorAuthorization
    Type: "AWS::Config::AggregationAuthorization"
    Properties:
      AuthorizedAccountId: !Ref SecurityAccountId
      AuthorizedAwsRegion: eu-west-1
  AuthorizerSyd:
    Condition: CreateRegionalConfigAggregatorAuthorization
    Type: "AWS::Config::AggregationAuthorization"
    Properties:
      AuthorizedAccountId: !Ref SecurityAccountId
      AuthorizedAwsRegion: ap-southeast-2
  HomeRegionAuthorizer:
    Condition: CreateHomeRegionConfigAggregatorAuthorization
    Type: "AWS::Config::AggregationAuthorization"
    Properties:
      AuthorizedAccountId: !Ref SecurityAccountId
      AuthorizedAwsRegion: !Ref HomeRegionName

Outputs:
  BaselineConfigRecorder:
    Description: Baseline Config Recorder
    Value:  !Ref ConfigRecorder
  BaselineConfigDeliveryChannel:
    Description: Baseline Config Delivery Channel
    Value: !Ref ConfigDeliveryChannel

AWSControlTowerBP-BASELINE-ROLES

概要

Auditのアカウントからスイッチロールが可能なよう
readonlyとadminの2種類のroleを作成する

デプロイ対象

下記のcontroltowerが有効化された単一リージョン
core ou
custome ou

cloudformation template
AWSTemplateFormatVersion: 2010-09-09
Description: Configure the Cross-Account IAM Security Roles for the member accounts.

Parameters:
  ManagedResourcePrefix:
    Type: 'String'
    Description: 'Prefix for the managed resources'
  SecurityAccountAdminRoleArn:
    Type: String
    Description: Admin role ARN from the security account.
  SecurityAccountReadOnlyRoleArn:
    Type: String
    Description: Admin role ARN from the security account.
  EnableAdminRole:
    Type: String
    Default: 'true'
    Description: Create an administrative cross-account role from Security Account to this account.
    AllowedValues:
      - 'true'
      - 'false'
  EnableReadOnlyRole:
    Type: String
    Default: 'true'
    Description: Create a read-only cross-account role from Security Account to this account.
    AllowedValues:
      - 'true'
      - 'false'

Conditions:
  CreateAdminRole: !Equals
    - !Ref EnableAdminRole
    - 'true'
  CreateReadOnlyRole: !Equals
    - !Ref EnableReadOnlyRole
    - 'true'

Resources:
  AdminExecutionRole:
    Type: AWS::IAM::Role
    Condition: CreateAdminRole
    Properties:
      RoleName: !Sub ${ManagedResourcePrefix}-AdministratorExecutionRole
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              AWS:
                - !Ref SecurityAccountAdminRoleArn
            Action:
              - sts:AssumeRole
      Path: /
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/AdministratorAccess

  ReadOnlyExecutionRole:
    Type: AWS::IAM::Role
    Condition: CreateReadOnlyRole
    Properties:
      RoleName: !Sub ${ManagedResourcePrefix}-ReadOnlyExecutionRole
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              AWS:
                - !Ref SecurityAccountReadOnlyRoleArn
            Action:
              - sts:AssumeRole
      Path: /
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/ReadOnlyAccess

AWSControlTowerBP-BASELINE-SERVICE-ROLES

概要

AWSControlTowerBP-BASELINE-CLOUDWATCH
の実行に必要なlambdaとconfig用のロールを作成する

デプロイ対象

下記のcontroltowerが有効化された単一リージョン core ou custome ou

cloudformation template
AWSTemplateFormatVersion: 2010-09-09
Description: Configure AWS Config and SNS Notification Forward IAM Roles

Parameters:
  ManagedResourcePrefix:
    Type: 'String'
    Description: 'Prefix for the managed resources'

  SecurityTopicName:
    Type: String
    Description: Security Notification SNS Topic Name.

  SecurityAccountId:
    Type: 'String'
    Description: AWS Account Id of the Security account.

Resources:

  ConfigRecorderRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub ${ManagedResourcePrefix}-ConfigRecorderRole
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - config.amazonaws.com
            Action:
              - sts:AssumeRole
      Path: /
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AWSConfigRole
        - arn:aws:iam::aws:policy/ReadOnlyAccess

  ForwardSnsNotificationLambdaRole:
    Type: 'AWS::IAM::Role'
    Properties:
      RoleName: !Sub ${ManagedResourcePrefix}-ForwardSnsNotificationRole
      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'
      Policies:
      - PolicyName: sns
        PolicyDocument:
          Statement:
          - Effect: Allow
            Action:
            - 'sns:publish'
            Resource: !Sub arn:aws:sns:*:${SecurityAccountId}:${SecurityTopicName}

Outputs:
  BaselineConfigRole:
    Description: Baseline Config Role
    Value: !GetAtt 'ConfigRecorderRole.Arn'

AWSControlTowerBP-SECURITY-TOPICS

概要

SNSのtopicを2つ作成する
AggregateSecurityNotifications
AllConfigNotifications
各アカウントで何かあったときの受け口となるSNS Topic

対象

下記のcontroltowerがサポートされている全リージョン audit

cloudformation template
AWSTemplateFormatVersion: 2010-09-09
Description: Configure the SNS Topics for Security Account

Parameters:
  ManagedResourcePrefix:
    Type: 'String'
    Description: 'Prefix for the managed resources'
  AllConfigurationEmail:
    Type: 'String'
    Description: Email for receiving all AWS configuration events
  SecurityNotificationEmail:
    Type: 'String'
    Description: Email for the security administrator(s)
  OrgID:
    Type: 'String'
    Description: AWS Organizations ID to allow notifications from member accounts
  SubscribeToAllConfigurationTopic:
    Type: String
    Default: false
    Description: Indicates whether AllConfigurationEmail will be subscribed to the AllConfigurationTopicName topic.
    AllowedValues:
      - true
      - false

Conditions:
  Subscribe: !Equals
    - !Ref SubscribeToAllConfigurationTopic
    - 'true'

Mappings:
  TopicNameSuffix:
    AllConfigurationTopicName:
      Suffix: 'AllConfigNotifications'
    NotifyTopicName:
      Suffix: 'AggregateSecurityNotifications'

Resources:
  SNSAllConfigurationTopic:
    Type: AWS::SNS::Topic
    Properties:
      DisplayName: !Join [ "-", [ !Ref ManagedResourcePrefix, !FindInMap [TopicNameSuffix, AllConfigurationTopicName, Suffix] ]]
      TopicName: !Join [ "-", [ !Ref ManagedResourcePrefix, !FindInMap [TopicNameSuffix, AllConfigurationTopicName, Suffix] ]]

  SNSAllConfigurationTopicPolicy:
    Type: AWS::SNS::TopicPolicy
    Properties:
      Topics:
        - !Ref SNSAllConfigurationTopic
      PolicyDocument:
        Statement:
          - Sid: AWSSNSPolicy
            Action:
              - sns:Publish
            Effect: Allow
            Resource: !Ref SNSAllConfigurationTopic
            Principal:
              Service:
                - cloudtrail.amazonaws.com
                - config.amazonaws.com

  SNSAllConfigurationEmailNotification:
    Condition: Subscribe
    Type: AWS::SNS::Subscription
    Properties:
      Endpoint: !Ref AllConfigurationEmail
      Protocol: email
      TopicArn: !Ref SNSAllConfigurationTopic

  SNSNotification:
    Type: AWS::SNS::Topic
    Properties:
      DisplayName: !Join [ "-", [ !Ref ManagedResourcePrefix, !FindInMap [TopicNameSuffix, NotifyTopicName, Suffix] ]]
      TopicName: !Join [ "-", [ !Ref ManagedResourcePrefix, !FindInMap [TopicNameSuffix, NotifyTopicName, Suffix] ]]
      Subscription:
      - Protocol: email
        Endpoint: !Ref SecurityNotificationEmail

  SNSNotificationPolicy:
    Type: AWS::SNS::TopicPolicy
    Metadata:
      cfn_nag:
        rules_to_suppress:
          - id: F18
            reason: "Conditions restrict permissions to Organization account and publishing only to member accounts."
    Properties:
      Topics:
        - !Ref SNSNotification
      PolicyDocument:
        Statement:
          - Sid: __default_statement_ID
            Effect: Allow
            Principal:
              AWS: "*"
            Action:
            - SNS:GetTopicAttributes
            - SNS:SetTopicAttributes
            - SNS:AddPermission
            - SNS:RemovePermission
            - SNS:DeleteTopic
            - SNS:Subscribe
            - SNS:ListSubscriptionsByTopic
            - SNS:Publish
            - SNS:Receive
            Resource: !Ref SNSNotification
            Condition:
              StringEquals:
                AWS:SourceOwner: !Sub ${AWS::AccountId}
          - Sid: AWSSNSPolicy
            Effect: Allow
            Principal:
              AWS: "*"
            Action: sns:Publish
            Resource: !Ref SNSNotification
            Condition:
              StringEquals:
                aws:PrincipalOrgID: !Ref OrgID

Outputs:
  SecurityTopicARN:
    Description: Security Notification SNS Topic ARN
    Value: !Ref SNSNotification
  SecurityTopicName:
    Description: Security Notification SNS Topic Name
    Value: !GetAtt SNSNotification.TopicName
  AllConfigTopicARN:
    Description: All Configuration Notification SNS Topic ARN
    Value: !Ref SNSAllConfigurationTopic
  AllConfigTopicName:
    Description: All Configuration Notification SNS Topic Name
    Value: !GetAtt SNSAllConfigurationTopic.TopicName

AWSControlTowerGuardrailAWS-GR-AUDIT-BUCKET-PUBLIC-READ-PROHIBITED

概要

config ruleを作成する
ルール:S3_BUCKET_PUBLIC_READ_PROHIBITED

デプロイ対象

下記のcontroltowerがサポートされている全リージョン Log archive
audit

cloudformation template
AWSTemplateFormatVersion: 2010-09-09
Description: Configure AWS Config rules to check that your S3 buckets do not allow public access

Parameters:
  ConfigRuleName:
    Type: 'String'
    Description: 'Name for the Config rule'

Resources:
  CheckForS3PublicRead:
    Type: AWS::Config::ConfigRule
    Properties:
      ConfigRuleName: !Sub ${ConfigRuleName}
      Description: Checks that your S3 buckets do not allow public read access. If an S3 bucket policy or bucket ACL allows public read access, the bucket is noncompliant.
      Source:
        Owner: AWS
        SourceIdentifier: S3_BUCKET_PUBLIC_READ_PROHIBITED
      Scope:
        ComplianceResourceTypes:
          - AWS::S3::Bucket

AWSControlTowerGuardrailAWS-GR-AUDIT-BUCKET-PUBLIC-WRITE-PROHIBITED

概要

config ruleを作成する
ルール:S3_BUCKET_PUBLIC_WRITE_PROHIBITED

デプロイ対象

下記のcontroltowerがサポートされている全リージョン
Log archive
audit

AWSTemplateFormatVersion: 2010-09-09
Description: Configure AWS Config rules to check that your S3 buckets do not allow public access

Parameters:
  ConfigRuleName:
    Type: 'String'
    Description: 'Name for the Config rule'

Resources:
  CheckForS3PublicWrite:
    Type: AWS::Config::ConfigRule
    Properties:
      ConfigRuleName: !Sub ${ConfigRuleName}
      Description: Checks that your S3 buckets do not allow public write access. If an S3 bucket policy or bucket ACL allows public write access, the bucket is noncompliant.
      Source:
        Owner: AWS
        SourceIdentifier: S3_BUCKET_PUBLIC_WRITE_PROHIBITED
      Scope:
        ComplianceResourceTypes:
          - AWS::S3::Bucket

AWSControlTowerLoggingResources

概要

バケットを2つ作成する
1つはaudit。ここに他アカウントからのconfigやtrail結果が書き込まれる
2つ目は上記のs3へのアクセスログを記録するためのバケット

デプロイ対象

下記のcontroltowerが有効化された単一リージョン
Log archive

cloudformation template
AWSTemplateFormatVersion: 2010-09-09
Description: Configure an Audit S3 bucket for the Log Archive account.

Parameters:
  SSEAlgorithm:
    Type: 'String'
    Default: 'AES256'
    Description: S3 bucket SSE Algorithm.
    AllowedValues:
    - 'AES256'
    - 'aws:kms'
  KMSMasterKeyID:
    Type: 'String'
    Description: 'KMS key ID required if SSE algorithm is aws:kms.'
  ManagedResourcePrefix:
    Type: 'String'
    Description: 'Prefix for the managed resources'
  RetentionDays:
    Type: String
    Description: 'No of Days to retain the logs, after which it will be permanently deleted'
  TransitionToGlacier:
    Type: String
    Description: 'Do you wish to transition the logs to Glacier before permanently deleting?'
    Default: 'No'
    AllowedValues:
    - 'Yes'
    - 'No'
  TransitionDays:
    Type: String
    Description: 'No of Days to transition the data from S3 to Glacier'
  AWSLogsS3KeyPrefix:
    Type: 'String'
    Description: 'Organization ID to use as the S3 Key prefix for storing the audit logs'

Conditions:
  UseKMS: !Equals
    - !Ref SSEAlgorithm
    - 'aws:kms'
  MoveToGlacier: !Equals
    - !Ref TransitionToGlacier
    - 'Yes'

Resources:
  # Create S3 Server Access Logging bucket
  S3LoggingBucket:
    DeletionPolicy: Retain
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Sub ${ManagedResourcePrefix}-s3-access-logs-${AWS::AccountId}-${AWS::Region}
      AccessControl: LogDeliveryWrite
      VersioningConfiguration:
        Status: Enabled
      BucketEncryption:
        ServerSideEncryptionConfiguration:
        - !If
          - UseKMS
          - ServerSideEncryptionByDefault:
              SSEAlgorithm: !Ref SSEAlgorithm
              KMSMasterKeyID: !Ref KMSMasterKeyID
          - ServerSideEncryptionByDefault:
              SSEAlgorithm: !Ref SSEAlgorithm
  # Create S3 Audit bucket
  S3AuditBucket:
    DeletionPolicy: Retain
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Sub ${ManagedResourcePrefix}-logs-${AWS::AccountId}-${AWS::Region}
      VersioningConfiguration:
        Status: Enabled
      LoggingConfiguration:
        DestinationBucketName: !Ref S3LoggingBucket
      BucketEncryption:
        ServerSideEncryptionConfiguration:
        - !If
          - UseKMS
          - ServerSideEncryptionByDefault:
              SSEAlgorithm: !Ref SSEAlgorithm
              KMSMasterKeyID: !Ref KMSMasterKeyID
          - ServerSideEncryptionByDefault:
              SSEAlgorithm: !Ref SSEAlgorithm
      LifecycleConfiguration:
        Rules:
        - !If
          - MoveToGlacier
          - Id: RetentionRule
            Status: Enabled
            ExpirationInDays: !Ref RetentionDays
            NoncurrentVersionExpirationInDays: !Ref RetentionDays
            Transitions:
                - TransitionInDays: !Ref TransitionDays
                  StorageClass: Glacier
            NoncurrentVersionTransitions:
                - TransitionInDays: !Ref TransitionDays
                  StorageClass: Glacier
          - Id: RetentionRule
            Status: Enabled
            ExpirationInDays: !Ref RetentionDays
            NoncurrentVersionExpirationInDays: !Ref RetentionDays
  # Create Bucket Policy for S3 Audit bucket
  S3AuditBucketPolicy:
    Type: AWS::S3::BucketPolicy
    Properties:
      Bucket: !Ref S3AuditBucket
      PolicyDocument:
        Version: 2012-10-17
        Statement:
          - Sid: AWSBucketPermissionsCheck
            Effect: Allow
            Principal:
              Service:
                - cloudtrail.amazonaws.com
                - config.amazonaws.com
            Action: s3:GetBucketAcl
            Resource:
              - !Sub "arn:aws:s3:::${S3AuditBucket}"
          - Sid: AWSBucketDelivery
            Effect: Allow
            Principal:
              Service:
                - cloudtrail.amazonaws.com
                - config.amazonaws.com
            Action: s3:PutObject
            Resource:
                    - Fn::Join:
                        - ""
                        -
                          - "arn:aws:s3:::"
                          - !Ref "S3AuditBucket"
                          - !Sub "/${AWSLogsS3KeyPrefix}/AWSLogs/*/*"

Outputs:
  BucketName:
    Description: Audit S3 bucket name
    Value: !Ref S3AuditBucket
  LoggingBucketName:
    Description: S3 Access Logging Bucket name
    Value: !Ref S3LoggingBucket
  AuditLogsS3KeyPrefix:
    Description: S3 Key prefix for storing the audit logs
    Value: !Ref AWSLogsS3KeyPrefix

AWSControlTowerSecurityResources

概要

Lambda実行用のロール(adminとreadonlyの2つ)

対象

下記のcontroltowerが有効化された単一リージョン
audit

cloudformation template
AWSTemplateFormatVersion: 2010-09-09
Description: Configure the Cross-Account IAM Audit Roles for Audit Account

Parameters:
  ManagedResourcePrefix:
    Type: 'String'
    Description: 'Prefix for the managed resources'
  AuditAccountId:
    Type: 'String'
    Description: 'Audit account Id'
  LoggingAccountId:
    Type: 'String'
    Description: 'Logging account Id'

Resources:
  AdministrationRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub ${ManagedResourcePrefix}-AuditAdministratorRole
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service: lambda.amazonaws.com
            Action:
              - sts:AssumeRole
      Path: /
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/AWSLambdaExecute
      Policies:
        - PolicyName: !Sub AssumeRole-${ManagedResourcePrefix}-AuditAdministratorRole
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Effect: Allow
                Action:
                  - sts:AssumeRole
                Resource:
                  - !Sub "arn:aws:iam::*:role/${ManagedResourcePrefix}-AdministratorExecutionRole"

  ReadOnlyRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub ${ManagedResourcePrefix}-AuditReadOnlyRole
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service: lambda.amazonaws.com
            Action:
              - sts:AssumeRole
      Path: /
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/AWSLambdaExecute
      Policies:
        - PolicyName: !Sub AssumeRole-${ManagedResourcePrefix}-AuditReadOnlyRole
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Effect: Allow
                Action:
                  - sts:AssumeRole
                Resource:
                  - !Sub "arn:aws:iam::*:role/${ManagedResourcePrefix}-ReadOnlyExecutionRole"

  # AWSConfig Aggregator for Guardrail compliance
  GuardrailsComplianceAggregator:
    Type: AWS::Config::ConfigurationAggregator
    Properties:
      AccountAggregationSources:
        - AccountIds:
          - !Ref AuditAccountId
          - !Ref LoggingAccountId
          AllAwsRegions: true
      ConfigurationAggregatorName: !Sub ${ManagedResourcePrefix}-GuardrailsComplianceAggregator

Outputs:
  CrossAccountAdminRole:
    Description: Audit Administrator Role
    Value: !GetAtt 'AdministrationRole.Arn'
  CrossAccountReadOnlyRole:
    Description: Audit ReadOnly Role
    Value: !GetAtt 'ReadOnlyRole.Arn'

最後に

役に立った人がいれば褒めて欲しい。。。。
Organization管理は、自社の管理しか深く関わらず、
サンプル数が少ないので
他の詳しい人の考え方も聞きたいと思う事がよくあります。
何かあればコメント頂けると嬉しいです。