CloudWatch cross-account observabilityを、CloudFormation StackSetsで複数リージョンに展開する

# はじめに
CloudWatch cross-account observabilityは、複数のアカウントのCloudWatchのデータを、一つのアカウントで集約して閲覧可能になる機能です。
単に集約するというだけでなく、本番環境に直接入ることなくメトリクスやログなどを確認するという用途にも使えます。
https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch-Unified-Cross-Account.html

複数アカウント環境において監視する際にとても便利な機能なのですが、各リージョンごとに設定しないといけないという点が大きな手間です。
複数リージョンと複数アカウントといえばCloudFormationのStackSetということで、StackSetでまとめて展開していきます。

# 手順
最終的にできあがる全体図はこちらです。
CloudWatchのデータを提供する側をソースアカウント(`222222222222`)、そのデータの監視を行う側をモニタリングアカウント(`111111111111`)として、以降進めます。
```mermaid
flowchart TD
  subgraph m ["モニタリングアカウント(111111111111)"]
    a["IAM Role(admin role)"]
    ea["IAM Role(execution role)"]
    subgraph StackSet
      subgraph sr ["Stack(各リージョン)"]
        Sink["Sink(CloudWatch)"]
        ss["StackSet(各リージョン)"]
      end
    end
  end
  subgraph s ["ソースアカウント(222222222222)"]
    subgraph Stack
      Link["Link(CloudWatch)"]
    end
    es["IAM Role(execution role)"]
  end
  StackSet-- ServiceRole -->a
  ss-- ServiceRole -->a
  a-- AssumeRole --> es
  ss-- StackInstance -->Stack
  es-- Create -->Stack
  es-- Create -->Link
  Sink-- cross-account observability ---Link
  a-- AssumeRole --> ea
  ea-- Create -->Sink
```
## 1. IAM Roleの準備
StackSet用のIAM Roleを、モニタリングアカウントとソースアカウントとの両方に作成します。

### モニタリングアカウント
StackSetには、`service-managed`と`self-managed`という二つの実行方法があります。
前者の`service-managed`はOrganizationのCloudFormation委任アカウントで使用するものです、
https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/stacksets-orgs-delegated-admin.html

今回は、委任アカウントではないであろうモニタリングアカウントでStackSetを実行したいので、後者の`self-managed`タイプを使用します。
`self-managed`の場合、Stackとリソースの作成を実際に行う`execution role`と、`execution role`へAssumeRoleする権限を持つ`admin role`の二つのIAM Roleが必要です。
![スクリーンショット 2024-09-05 9.36.22.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3468968/26cd351f-d57d-b90b-48ab-a381573e56e5.png)

<details><summary>CloudFormationで作成する場合はこちら</summary>

```yaml
AWSTemplateFormatVersion: "2010-09-09"
Description: IAM roles for CloudWatch cross-account observability
Resources:
  AdminRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: "Allow"
            Principal:
              Service: "cloudformation.amazonaws.com"
            Action: "sts:AssumeRole"
      Policies:
        - PolicyName: AssumeRole
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: "Allow"
                Action:
                  - "sts:AssumeRole"
                Resource: "*"
  SinkStackSetExecutionRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: "Allow"
            Principal:
              AWS:
                - !GetAtt AdminRole.Arn
            Action:
              - "sts:AssumeRole"
      Policies:
        - PolicyName: policies
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: Allow
                Resource: !GetAtt AdminRole.Arn
                Action:
                  - iam:PassRole
              - Effect: Allow
                Resource: "*"
                Action:
                  - oam:CreateSink
                  - oam:DeleteSink
                  - oam:GetSink
                  - oam:GetSinkPolicy
                  - oam:PutSinkPolicy
                  - cloudformation:GetTemplateSummary
                  - cloudformation:DescribeStackSet
                  - cloudformation:DescribeStacks
                  - cloudformation:DescribeStackSetOperation
                  - cloudformation:DeleteStackSet
                  - cloudformation:DeleteStack
                  - cloudformation:CreateStackSet
                  - cloudformation:CreateStack
                  - cloudformation:UpdateStackSet
                  - cloudformation:UpdateStack
                  - cloudformation:CreateStackInstances
                  - cloudformation:DeleteStackInstances
                  - cloudformation:UpdateStackInstances
                  - cloudformation:DescribeStackInstance
```
</details>

#### `admin role`
前述の通り、`execution role`へAssumeRoleするだけのIAM Roleです。
今回は`Resource: "*"`としていて、AssumeRole先を制限していません。

<details><summary>trust policy</summary>

```json
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
                "Service": "cloudformation.amazonaws.com"
            },
            "Action": "sts:AssumeRole"
        }
    ]
}
```
</details>

<details><summary>permission policy</summary>

```json{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Action": [
                "sts:AssumeRole"
            ],
            "Resource": "*",
            "Effect": "Allow"
        }
    ]
}
```
</details>

#### `execution role`
`admin role`からAssumeRoleされる側その一です。

<details><summary>trust policy</summary>

```json
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
                "AWS": "arn:aws:iam::111111111111:role/IAMRolesForCrossAccountObservability-AdminRole"
            },
            "Action": "sts:AssumeRole"
        }
    ]
}
```
</details>

ポリシーでは、主に三つの権限を付与しています。
- iam:PassRole
ややこしいのですが、StackSetで作成したStackの中から、さらにStackSetを作成します。
そのため、二つ目のStackSetを実行する際に、`admin role`をPassRoleする権限が必要となります。
- oam
モニタリングアカウント側でcross-account observabilityの設定をする権限です。
- CloudFormation
StackとStackSetを管理する権限を付与しています。

<details><summary>permission policy</summary>

```json
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Action": [
                "iam:PassRole"
            ],
            "Resource": "arn:aws:iam::111111111111:role/IAMRolesForCrossAccountObservability-AdminRole",
            "Effect": "Allow"
        },
        {
            "Action": [
                "oam:CreateSink",
                "oam:DeleteSink",
                "oam:GetSink",
                "oam:GetSinkPolicy",
                "oam:PutSinkPolicy",
                "cloudformation:GetTemplateSummary",
                "cloudformation:DescribeStackSet",
                "cloudformation:DescribeStacks",
                "cloudformation:DescribeStackSetOperation",
                "cloudformation:DeleteStackSet",
                "cloudformation:DeleteStack",
                "cloudformation:CreateStackSet",
                "cloudformation:CreateStack",
                "cloudformation:UpdateStackSet",
                "cloudformation:UpdateStack",
                "cloudformation:CreateStackInstances",
                "cloudformation:DeleteStackInstances",
                "cloudformation:UpdateStackInstances",
                "cloudformation:DescribeStackInstance"
            ],
            "Resource": "*",
            "Effect": "Allow"
        }
    ]
}
```
</details>


### ソースアカウント
モニタリングアカウントのStackSetが使用するIAM Role(`execution role`)を用意します。
ソースアカウントが複数ある場合は、IAM Role名を共通にしておく必要がありますので、ご注意ください。
<details><summary>CloudFormationで作成する場合はこちら</summary>

```yaml
AWSTemplateFormatVersion: 2010-09-09
Description: IAM Role for CloudWatch cross-account observability

Parameters:
  MonitoringAccountId:
    Type: String
    MaxLength: "12"
    MinLength: "12"
  ExecutionRoleName:
    Type: String
    Default: CloudWatchCrossAccountObservabilityExecutionRole

Resources:
  ExecutionRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Ref ExecutionRoleName
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              AWS:
                - !Ref MonitoringAccountId
            Action:
              - sts:AssumeRole
      Policies:
        - PolicyName: CloudWatchCrossAccountObservability
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Effect: Allow
                Action:
                  - oam:CreateLink
                  - oam:UpdateLink
                  - oam:DeleteLink
                  - oam:GetLink
                  - oam:TagResource
                Resource: arn:*:oam:*:*:link/*
              - Effect: Allow
                Action:
                  - oam:CreateLink
                  - oam:UpdateLink
                Resource: arn:*:oam:*:*:sink/*
                Condition:
                  StringEquals:
                    aws:ResourceAccount: !Ref MonitoringAccountId
              - Effect: Allow
                Action:
                  - oam:ListLinks
                  - cloudwatch:Link
                  - cloudformation:DescribeStacks
                  - cloudformation:DeleteStack
                  - cloudformation:CreateStack
                  - cloudformation:UpdateStack
                Resource: "*"
```
</details>


trust policyでは、モニタリングアカウントの`admin role`を許可します。
`admin role`の名前が決まっている場合は、`arn:aws:iam::111111111111:root`のところにRole arnを入れることで、アクセス制限を強められます。
<details><summary>trust policy</summary>

```json
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
                "AWS": "arn:aws:iam::111111111111:root"
            },
            "Action": "sts:AssumeRole"
        }
    ]
}
```
</details>

ソースアカウント側で必要な権限は主に二つです。
- oam
ソースアカウント側でcross-account observabilityの設定をする権限です。
- cloudformation
モニタリングアカウント側のStackSetからの設定をもとに、ソースアカウント側にStackを作成する権限です。

<details><summary>permission policy</summary>

```json
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Action": [
                "oam:CreateLink",
                "oam:UpdateLink",
                "oam:DeleteLink",
                "oam:GetLink",
                "oam:TagResource"
            ],
            "Resource": "arn:*:oam:*:*:link/*",
            "Effect": "Allow"
        },
        {
            "Condition": {
                "StringEquals": {
                    "aws:ResourceAccount": "111111111111"
                }
            },
            "Action": [
                "oam:CreateLink",
                "oam:UpdateLink"
            ],
            "Resource": "arn:*:oam:*:*:sink/*",
            "Effect": "Allow"
        },
        {
            "Action": [
                "oam:ListLinks",
                "cloudwatch:Link",
                "cloudformation:DescribeStacks",
                "cloudformation:DeleteStack",
                "cloudformation:CreateStack",
                "cloudformation:UpdateStack"
            ],
            "Resource": "*",
            "Effect": "Allow"
        }
    ]
}
```
</details>

## 2. StackSet作成
まずはCloudFormationテンプレートを紹介します。
```yaml
AWSTemplateFormatVersion: "2010-09-09"
Description: Cross-Account Observability
Parameters:
  SinkName:
    Type: String
    Default: Sink
  SourceAccountIds:
    Type: List<String>
  StackSetName:
    Type: String
    Default: CloudWatchCrossAccountObservability
  AdministrationRoleARN:
    Type: String
  ExecutionRoleName:
    Type: String
    Default: CloudWatchCrossAccountObservabilityExecutionRole
Resources:
  Sink:
    Type: AWS::Oam::Sink
    Properties:
      Name: !Ref SinkName
      Policy:
        Statement:
          - Effect: Allow
            Action:
              - oam:CreateLink
              - oam:UpdateLink
            Resource: "*"
            Principal:
              AWS: !Ref SourceAccountIds
            Condition:
              ForAllValues:StringEquals:
                oam:ResourceTypes: AWS::CloudWatch::Metric
  SourceAccount:
    Type: AWS::CloudFormation::StackSet
    Properties:
      PermissionModel: SELF_MANAGED
      StackSetName: !Ref StackSetName
      AdministrationRoleARN: !Ref AdministrationRoleARN
      ExecutionRoleName: !Ref ExecutionRoleName
      TemplateBody: |
        AWSTemplateFormatVersion: 2010-09-09
        Parameters:
          DestinationAccountId:
            Type: String
          SinkArn:
            Type: String
        Conditions:
          SkipMonitoringAccount: !Not
            - !Equals
              - !Ref AWS::AccountId
              - !Ref DestinationAccountId
        Resources:
          Link:
            Type: AWS::Oam::Link
            Condition: SkipMonitoringAccount
            Properties:
              LabelTemplate: "$AccountName"
              ResourceTypes:
                - "AWS::CloudWatch::Metric"
              SinkIdentifier: !Ref SinkArn
      Parameters:
        - ParameterKey: DestinationAccountId
          ParameterValue: !Ref AWS::AccountId
        - ParameterKey: SinkArn
          ParameterValue: !GetAtt Sink.Arn
      StackInstancesGroup:
        - DeploymentTargets:
            Accounts: !Ref SourceAccountIds
          Regions:
            - !Ref AWS::Region
```

### Parameters
#### 必須
- `SourceAccountIds`
ソースアカウントのアカウントIDを、カンマ区切りで羅列してください。
- `AdministrationRoleARN`
手順1でモニタリングアカウント側に作成した`admin role`のarnを入力してください。
- `ExecutionRoleName`
手順1でソースアカウント側に作成したIAM Roleの名前を指定します。
Roleの名前を統一しておく必要があったのは、ここでRole名を一つしか指定できないためです。
#### 任意
- `SinkName`
cross-account observabilityの設定名です。
`Sink`は各リージョンにひとつずつしか作れないため、名前はなんでも構いません。
- `StackSetName`
各リージョンに作成されるStackSetの名前です。
他にStackSetを作成済みの場合、重複しないように変更することが可能です。

### 構成
直接作成するリソースは二つです。
#### `AWS::Oam::Sink`
cross-account observabilityで、モニタリングアカウント側に必要なリソースです。
今回は、メトリクスのみ共有する設定になっています。

#### `AWS::CloudFormation::StackSet`
各リージョンにStackを作成したあと、それぞれのリージョンで再びStackSetを作成します。
StackSetを使うことで、別アカウントの同一リージョンに、`AWS::Oam::Link`をデプロイします。
##### `AWS::Oam::Link`
ソースアカウント側のcross-account observabilityの設定用リソースです。
モニタリングアカウント側で作成した`AWS::Oam::Sink`のarnが必要なため、StackSetのパラメータ経由で指定しています。

# 補足
## メトリクス以外を集約したい場合
この記事のCloudFormationテンプレートでは、CloudWatchのうちメトリクスのみ集約されます。
ログなども集約したい場合、[手順2 StackSet作成](#2-stackset作成)の二箇所に、必要なリソースタイプを追加します。

cross-account observabilityで指定できるリソースタイプの一覧はこちらです。

```
AWS::Logs::LogGroup
AWS::CloudWatch::Metric
AWS::XRay::Trace
AWS::ApplicationInsights::Application
AWS::InternetMonitor::Monitor
```

リソースタイプを指定しているのは、モニタリングアカウントとソースアカウントでそれぞれ一箇所ずつです。
- `AWS::Oam::Sink`
モニタリングアカウント側に作成されるリソースです。
Sinkにはポリシーを設定していますので、必要なものを追加します。
```yaml
Condition:
  ForAllValues:StringEquals:
    oam:ResourceTypes:
      - AWS::Logs::LogGroup
      - AWS::CloudWatch::Metric
      - AWS::XRay::Trace
      - AWS::ApplicationInsights::Application
      - AWS::InternetMonitor::Monitor
```
- `AWS::Oam::Link`
ソースアカウント側です。
`ResrouceTypes`のところに追加します。
```yaml
ResourceTypes:
  - AWS::Logs::LogGroup
  - AWS::CloudWatch::Metric
  - AWS::XRay::Trace
  - AWS::ApplicationInsights::Application
  - AWS::InternetMonitor::Monitor
```

## Organization内で展開する場合
ソースアカウントを指定する際、この記事のテンプレートではアカウントIDを全て列挙する必要があります。
しかしStackSetを使用しているため、Organization全体や、Organization内のou(Organizational Unit)単位でのcross-account observabilityの設定も当然可能です。

その場合の修正箇所は、[手順2 StackSet作成](#2-stackset作成)のテンプレートに定義されている`AWS::CloudFormation::StackSet`リソースのパラメータである`StackInstancesGroup`です。

```yaml
StackInstancesGroup:
  - DeploymentTargets:
      Accounts: !Ref SourceAccountIds
    Regions:
      - !Ref AWS::Region
```

`DeploymentTargets`のところで、`Accounts`の代わりに`OrganizationalUnitIds`を使用して、対象のouを指定します。
https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-cloudformation-stackset-deploymenttargets.html
## oamとは
何度かoamという単語が出てきましたが、これは`CloudWatch Observability Access Manager`のことのようです。
https://docs.aws.amazon.com/OAM/latest/APIReference/Welcome.html
# 終わりに
複数リージョンでのcross-account observabilityの設定を楽にするためのCloudFormationテンプレートを紹介しました。