[AWS, CloudFormation] Amazon Connect 階層グループ更新イベントの自動同期方法 🚀

## 1. イントロダクション

### 1.1 ソリューション概要と利点

Amazon Connect の階層グループ（ユーザー階層グループ）の変更を自動的に検知し、その情報を S3 に保存する機構です
まるで「階層グループの変化を嗅ぎ分ける優秀な番犬」のような存在🐶
よくあるアーキテクチャですがCloudFormation の記述含めご説明します

#### 主な利点

| 利点 | 説明 |
|------|------|
| リアルタイム同期 | 階層グループの変更を即座に検知し、S3に反映 |
| データの永続化 | 重要な階層情報を安全にS3に保存 |
| 自動化 | 人手を介さずに自動的に同期処理を実行 |
| 監査性 | 変更履歴をS3に保存することで、監査証跡として活用可能 |
| コスト効率 | サーバーレスアーキテクチャにより、運用コストを最小化 |

## 2. アーキテクチャ解説

### 2.1 全体フロー

```
[Amazon Connect] → [CloudTrail] → [EventBridge] → [Lambda] → [S3]
```

### 2.2 各コンポーネントの役割

| コンポーネント | 役割 | 特徴 |
|--------------|------|------|
| Amazon Connect | 階層グループの管理 | コールセンターの組織構造を管理 |
| CloudTrail | API呼び出しの記録 | すべてのAPIアクションをログとして記録 |
| EventBridge | イベントルーティング | 特定のイベントを検知してLambdaをトリガー |
| Lambda | 処理実行 | 階層情報を取得しS3に保存 |
| S3 | データ保存 | 階層情報をJSON形式で保存 |

実際の使用イメージとしてはバックエンドAPIが適宜S3の階層グループを参照しに行き、最新の情報を取得できるといったものです

### 2.3 データフローの詳細

1. **イベント発生**
   - Amazon Connectで階層グループの変更が発生
   - 変更操作（作成/更新/削除）が実行される

2. **イベント検知**
   - CloudTrailがAPI呼び出しを記録
   - EventBridgeが特定のイベントパターンを検知

3. **処理実行**
   - Lambda関数がトリガーされる
   - 最新の階層グループ情報を取得

4. **データ保存**
   - 取得した情報をJSON形式でS3に保存

## 3. 実装手順


```connect-hierarchy-sync.yml
AWSTemplateFormatVersion: 2010-09-09
Description: "Lambda Function Stack For Amazon Connect Hierarchy Sync Stack"

# -------------------------------------
# Metadata
# -------------------------------------
Metadata:
  AWS::CloudFormation::Interface:
    ParameterGroups:
      - Label:
          default: "Project Configuration"
        Parameters:
          - ProjectName
          - Environment
      - Label:
          default: "Lambda Configuration"
        Parameters:
          - LambdaArchiveBucketName
          - LambdaArchiveBucketObjectKey
          - LambdaHandler
          - LambdaMemorySize
          - LambdaTimeout
          - LambdaRuntime
      - Label:
          default: "EventBridge Configuration"
        Parameters:
          - EventBridgeRuleName
          - ConnectInstanceId
      - Label:
          default: "S3 Configuration"
        Parameters:
          - ConnectHierarchyBucketName
          - ECSTaskRoleArn

# -------------------------------------
# Input parameters
# -------------------------------------
Parameters:
  ProjectName:
    Description: "Enter the project name. (ex: your-project-name)"
    Type: String
    MinLength: 1
    ConstraintDescription: "ProjectName must be enter."
    Default: your-project-name
  Environment:
    Description: "Select the environment."
    Type: String
    AllowedValues:
      - dev
      - stg
      - prd
    ConstraintDescription: "Environment must be select."
  LambdaArchiveBucketName:
    Description: "Enter the S3 bucket name for storing Lambda function deployment package. (ex: your-project-name-<environment>-lambda-function-archive)"
    Type: String
  LambdaArchiveBucketObjectKey:
    Description: "Enter the Lambda function package file (zip) object key of the Lambda archive bucket. (ex: connect-hierarchy-sync/lambda_function.zip)"
    Type: String
  LambdaHandler:
    Description: "Enter the Lambda function name for Connect hierarchy sync. (ex: lambda_function.lambda_handler)"
    Type: String
    Default: lambda_function.lambda_handler
  LambdaMemorySize:
    Description: "Enter the Lambda function memory size (MiB). (default: 128)"
    Type: Number
    Default: 128
    MinValue: 128
    MaxValue: 512
  LambdaTimeout:
    Description: "Enter the Lambda function timeout second. (default: 60)"
    Type: Number
    Default: 60
  LambdaRuntime:
    Description: "Select the Lambda function runtime. (default: python3.13)"
    Type: String
    AllowedValues:
      - python3.13
    Default: python3.13
    ConstraintDescription: "Runtime must be select."
  EventBridgeRuleName:
    Description: "Enter the EventBridge rule name for Connect hierarchy sync. (ex: notice-connect-hierarchy-sync-your-project-name-<environment>)"
    Type: String
  ConnectInstanceId:
    Description: "Enter the Amazon Connect Instance ID."
    Type: String
  ConnectHierarchyBucketName:
    Description: "Enter the S3 bucket name for storing Amazon Connect hierarchy group data. (ex: your-project-name-<environment>-connect-hierarchy-sync)"
    Type: String
  ECSTaskRoleArn:
    Description: "Specify the IAM Role ARN required to run the application. (ex: arn:aws:iam::012345678910:role/ECSTaskRole)"
    Type: String

# -------------------------------------
# Resources
# -------------------------------------
Resources:
  # -------------------------------------
  # IAM
  # -------------------------------------
  LambdaFunctionRolePolicy:
    DeletionPolicy: Retain
    UpdateReplacePolicy: Retain
    Type: AWS::IAM::ManagedPolicy
    Properties:
      ManagedPolicyName: !Sub LambdaToConnectAndS3Access-${Environment}
      PolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Action: logs:CreateLogGroup
            Resource: !Sub arn:aws:logs:${AWS::Region}:${AWS::AccountId}:*
          - Effect: Allow
            Action:
              - logs:CreateLogStream
              - logs:PutLogEvents
            Resource: !Sub arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/${ProjectName}-${Environment}-connect-hierarchy-sync:*
          - Effect: Allow
            Action:
              - connect:ListUserHierarchyGroups
            Resource: !Sub arn:aws:connect:${AWS::Region}:${AWS::AccountId}:instance/${ConnectInstanceId}
          - Effect: Allow
            Action:
              - s3:PutObject
            Resource: !Sub arn:aws:s3:::${ConnectHierarchyBucketName}/*
  LambdaFunctionRole:
    DeletionPolicy: Retain
    UpdateReplacePolicy: Retain
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub LambdaToConnectAndS3Role-${Environment}
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service: lambda.amazonaws.com
            Action: sts:AssumeRole
      Path: /
      ManagedPolicyArns:
        - !Ref LambdaFunctionRolePolicy

  # -------------------------------------
  # Lambda
  # -------------------------------------
  LambdaFunction:
    Type: AWS::Lambda::Function
    Properties:
      Code:
        S3Bucket: !Ref LambdaArchiveBucketName
        S3Key: !Ref LambdaArchiveBucketObjectKey
      Description: "This function syncs Amazon Connect hierarchy data to S3."
      Environment:
        Variables:
          CONNECT_INSTANCE_ID: !Ref ConnectInstanceId
          S3_BUCKET_NAME: !Ref ConnectHierarchyBucketName
      FunctionName: !Sub ${ProjectName}-${Environment}-connect-hierarchy-sync
      Handler: !Ref LambdaHandler
      MemorySize: !Ref LambdaMemorySize
      PackageType: Zip
      Timeout: !Ref LambdaTimeout
      Role: !GetAtt LambdaFunctionRole.Arn
      Runtime: !Ref LambdaRuntime
  LambdaFunctionPermission:
    Type: AWS::Lambda::Permission
    Properties:
      Action: lambda:InvokeFunction
      FunctionName: !GetAtt LambdaFunction.Arn
      Principal: events.amazonaws.com
      SourceArn: !GetAtt ConnectHierarchyRule.Arn

  # -------------------------------------
  # EventBridge
  # -------------------------------------
  ConnectHierarchyRule:
    Type: AWS::Events::Rule
    Properties:
      Name: !Ref EventBridgeRuleName
      Description: "Amazon Connect の階層グループ変更時に指定する Lambda 関数をトリガー"
      EventPattern:
        source:
          - aws.connect
        detail-type:
          - AWS API Call via CloudTrail
        detail:
          eventSource:
            - connect.amazonaws.com
          eventName:
            - CreateUserHierarchyGroup
            - UpdateUserHierarchyGroupName
            - DeleteUserHierarchyGroup
          requestParameters:
            InstanceId:
              - !Ref ConnectInstanceId
      Targets:
        - Arn: !GetAtt LambdaFunction.Arn
          Id: Lambda
        - Arn: !GetAtt EventBridgeLogGroup.Arn
          Id: CloudWatchLogs

  # -------------------------------------
  # CloudWatchLogs LogGroup
  # -------------------------------------
  EventBridgeLogGroup:
    DeletionPolicy: Retain
    UpdateReplacePolicy: Retain
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName: !Sub /aws/events/eventbridge/${EventBridgeRuleName}
      RetentionInDays: 30
      Tags:
        - Key: ProjectName
          Value: !Ref ProjectName
        - Key: Environment
          Value: !Ref Environment 
  EventBridgeLogGroupPolicy:
    Type: AWS::Logs::ResourcePolicy
    Properties:
      PolicyName: !Sub EventBridgeLogGroupPolicy-${Environment}
      PolicyDocument: !Sub |
        {
          "Version": "2012-10-17",
          "Statement": [
            {
              "Sid": "EventBridgeToCloudWatchLogs",
              "Effect": "Allow",
              "Principal": {
                "Service": "events.amazonaws.com"
              },
              "Action": [
                "logs:CreateLogStream",
                "logs:PutLogEvents"
              ],
              "Resource": "${EventBridgeLogGroup.Arn}"
            }
          ]
        }

  # -------------------------------------
  # S3
  # -------------------------------------
  ConnectHierarchyBucket:
    Type: AWS::S3::Bucket
    DeletionPolicy: Retain
    UpdateReplacePolicy: Retain
    Properties:
      BucketName: !Ref ConnectHierarchyBucketName
      BucketEncryption:
        ServerSideEncryptionConfiguration:
          - ServerSideEncryptionByDefault:
              SSEAlgorithm: AES256
      PublicAccessBlockConfiguration:
        BlockPublicAcls: true
        BlockPublicPolicy: true
        IgnorePublicAcls: true
        RestrictPublicBuckets: true
      Tags:
        - Key: ProjectName
          Value: !Ref ProjectName
        - Key: Environment
          Value: !Ref Environment
  ConnectHierarchyBucketPolicy:
    Type: AWS::S3::BucketPolicy
    Properties:
      Bucket: !Ref ConnectHierarchyBucket
      PolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Deny
            Principal: "*"
            Action: "s3:*"
            Resource: 
              - !Sub ${ConnectHierarchyBucket.Arn}
              - !Sub ${ConnectHierarchyBucket.Arn}/*
            Condition:
              Bool:
                "aws:SecureTransport": false
          - Effect: Allow
            Principal:
              AWS: !Ref ECSTaskRoleArn
            Action:
              - s3:GetObject
            Resource: !Sub ${ConnectHierarchyBucket.Arn}/*
```
### 3.1 CloudFormation テンプレートの解説

#### 重要なパラメータ

| パラメータ名 | 説明 |
|------------|------|
| ProjectName | プロジェクト名 |  |
| Environment | 環境（dev/stg/prd） |
| ConnectInstanceId | Amazon ConnectインスタンスID | 
| ConnectHierarchyBucketName | 階層情報保存用S3バケット名 | 

#### 主要なリソース定義

| リソース | 説明 | 重要な設定 |
|---------|------|-----------|
| LambdaFunctionRole | Lambda実行用IAMロール | ConnectとS3へのアクセス権限 |
| LambdaFunction | Lambda関数定義 | メモリ128MB、タイムアウト60秒 |
| ConnectHierarchyRule | EventBridgeルール | 階層グループ変更イベントの検知 |
| ConnectHierarchyBucket | S3バケット | 暗号化とパブリックアクセス制限 |

### 3.2 Lambda 関数のコード解説

```python
# 主要な処理ロジック
def lambda_handler(event, context):
    try:
        # 1. Connectから階層グループ情報を取得
        response = connect.list_user_hierarchy_groups(
            InstanceId=CONNECT_INSTANCE_ID
        )
        
        # 2. 階層グループ名を抽出
        hierarchy_groups = []
        for group in response["UserHierarchyGroupSummaryList"]:
            hierarchy_groups.append(group["Name"])
        
        # 3. S3にJSON形式で保存
        s3.put_object(
            Bucket=S3_BUCKET_NAME,
            Key=S3_HIERARCHY_GROUPS_KEY,
            Body=json.dumps({"name": hierarchy_groups}, default=str, ensure_ascii=False),
            ContentType="application/json"
        )
        
        return {"statusCode": 200}
        
    except Exception as e:
        return {
            "statusCode": 500,
            "body": json.dumps({"error": str(e)}, default=str, ensure_ascii=False)
        }
```

今回は階層グループ名のみを取得していますが、別の Amazon Connect API を使用すれば大体の情報は取得できると思います


https://docs.aws.amazon.com/ja_jp/connect/latest/APIReference/hierarchy-groups-api.html
## 5. セキュリティのベストプラクティス

| セキュリティ対策 | 実装内容 | 効果 |
|----------------|---------|------|
| IAMロール | 最小権限の原則 | 必要な権限のみ付与 |
| S3暗号化 | サーバーサイド暗号化 | データの保護 |
| パブリックアクセス制限 | バケットポリシー | 不正アクセスの防止 |
| セキュアトランスポート | HTTPS強制 | 通信の暗号化 |

## 6. まとめ

「階層グループの番犬」として、組織の構造変化を確実に記録し、必要な時にすぐに参照できる状態を維持しますU^ェ^U
まるで優秀な番犬が、家の出入りを記録するように、階層グループの変更を確実に記録していくのです・・・U・x・U