[Terraform] CodeCommitで管理するモノレポの変更に応じてCodePipelineの実行を振り分ける

# はじめに

本記事では、CodeCommitで管理する単一リポジトリ（モノレポ）において、
変更されたディレクトリに応じて、実行するCodePipelineを振り分けるCIパイプラインをTerraformで構築する方法について記載しています。

# Terraform で構築する全体構成図
![codepipeline_Trgger_01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/283246/e09a2839-9b1d-86ca-fc7b-6bf829a8aab1.png)

# 構成の概要
1. Systems Managerの Parameter Storeには、`リポジトリ内のディレクトリ名`と`実行する CodePipeline名`がJSON形式で格納されています。
2. 開発者がCodeCommit のリポジトリにコードをプッシュすると EventBridgeがリポジトリの変更イベントを検知してLambda関数をトリガー実行します。
3. Lambda関数は、Systems Managerのパラメータを読み取って、変更があったディレクトリに応じたCodePipeline を実行します。
4. 変更があったディレクトリが複数の場合は、複数のCodePipelineが同時に実行されます。

# Terraform のコードと構成

https://github.com/okubo-t/aws-tf-monorepo-codepipeline-trigger.git

### ディレクトリ構成
```terminal
aws-tf-monorepo-codepipeline-trigger
├── main.tf
├── provider.tf
└── modules
    ├── codecommit
    │   ├── codecommit.tf
    │   └── variables.tf
    ├── codepipeline
    │   ├── buildspec.yml
    │   ├── codebuild.tf
    │   ├── codepipeline.tf
    │   ├── iam.tf
    │   ├── s3.tf
    │   └── variables.tf
    ├── eventbridge
    │   ├── event_rule.tf
    │   └── variables.tf
    ├── lambda
    │   ├── dist
    │   ├── iam.tf
    │   ├── lambda.tf
    │   ├── src
    │   │   └── codepipeline_trigger.py
    │   └── variables.tf
    └── ssm
        ├── ssm.tf
        └── variables.tf
```

### main.tf
```terraform:main.tf
locals {
  aws_region = "ap-northeast-1"

  repository_name = "monorepo"
  branch_name     = "main"

  ssm_parameter_name = "codepipeline-trigger"
  ssm_parameter_value = jsonencode({
    "project1" : "project1-codepipeline",
    "project2" : "project2-codepipeline"
  })

  event_rule_name = "monorepo-change-event"

  function_name = "codepipeline-trigger"

  project1_codepipeline_name = "project1-codepipeline"
  project2_codepipeline_name = "project2-codepipeline"
}

module "ssm" {
  source = "./modules/ssm"

  ssm_parameter_name  = local.ssm_parameter_name
  ssm_parameter_value = local.ssm_parameter_value
}

module "codecommit" {
  source = "./modules/codecommit"

  repository_name = local.repository_name
}

module "eventbridge" {
  source = "./modules/eventbridge"

  event_rule_name = local.event_rule_name
  function_name   = local.function_name
  repository_name = local.repository_name
  branch_name     = local.branch_name
}

module "lambda" {
  source = "./modules/lambda"

  function_name      = local.function_name
  ssm_parameter_name = local.ssm_parameter_name
}

module "project1_codepipeline" {
  source = "./modules/codepipeline"

  codepipeline_name = local.project1_codepipeline_name
  repository_name   = local.repository_name
  branch_name       = local.branch_name
}

module "project2_codepipeline" {
  source = "./modules/codepipeline"

  codepipeline_name = local.project2_codepipeline_name
  repository_name   = local.repository_name
  branch_name       = local.branch_name
}
```

### CodePipeline を実行するLambda関数

```python:codepipeline_trigger.py
import json
import boto3
import os

codepipeline = boto3.client('codepipeline')
ssm = boto3.client('ssm')

PARAMETER_NAME = os.environ['PARAMETER_NAME']
  
def lambda_handler(event, context):

    project_pipelines = load_project_pipelines_from_parameter_store(PARAMETER_NAME)

    commit_id = event['detail']['commitId']
    repository_name = event['detail']['repositoryName']
    changed_files = get_changed_files(repository_name, commit_id)

    for project_dir, pipeline_name in project_pipelines.items():
        if any(file.startswith(project_dir) for file in changed_files):
            start_pipeline(pipeline_name)

def load_project_pipelines_from_parameter_store(parameter_name):
    response = ssm.get_parameter(Name=parameter_name, WithDecryption=True)
    parameter_value = response['Parameter']['Value']
    project_pipelines = json.loads(parameter_value)
    return project_pipelines

def get_changed_files(repository_name, commit_id):
    codecommit = boto3.client('codecommit')
    response = codecommit.get_commit(
        repositoryName=repository_name,
        commitId=commit_id
    )

    commit = response['commit']
    parent_commit_id = commit['parents'][0]

    response = codecommit.get_differences(
        repositoryName=repository_name,
        afterCommitSpecifier=commit_id,
        beforeCommitSpecifier=parent_commit_id
    )

    changed_files = [difference['afterBlob']['path'] for difference in response['differences']]
    return changed_files

def start_pipeline(pipeline_name):
    response = codepipeline.start_pipeline_execution(name=pipeline_name)
    print(f"Started pipeline: {pipeline_name}, executionId: {response['pipelineExecutionId']}")
```

### 本構成で想定しているCodedCommit リポジトリ
```
monorepo
├── project1
│   └── code
└── project2
    └── code
```

# 使い方
## Terraform の動作確認環境

```terraform
$ terraform -version

Terraform v1.4.4
on darwin_amd64
+ provider registry.terraform.io/hashicorp/aws v4.62.0
```

## 変数の設定
`main.tf` 内の `Local Values` を設定します。
|変数名|説明|
|:---|:---|
|aws_region|リージョン|
|repository_name|リポジトリ名|
|branch_name|ブランチ名|
|ssm_parameter_name|SSMのParameter Storeのパラメータ名|
|ssm_parameter_value|リポジトリ内のディレクトリ名(Key)と実行する CodePipeline名(Value)をJSON形式で設定します|
|event_rule_name|EventBridgeのイベントルール名|
|function_name|Lambda関数名|
|project1_codepipeline_name|CodePipeline名|
|project2_codepipeline_name|CodePipeline名|

## Terraform の実行
```terminal
$ export AWS_PROFILE= [ YOUR AWS ACCOUNT PROFILE NAME ]
```

```terraform
$ terraform init

$ terraform plan
Plan: 39 to add, 0 to change, 0 to destroy.

$ terraform apply
Apply complete! Resources: 39 added, 0 changed, 0 destroyed.
```
# 動作確認
1. リポジトリ内の`project1`と`project2`のディレクトリを変更します。
![01-codepipeline-trigger-exec.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/283246/da1af77c-0b47-4e44-0378-bfd2df174bcd.png)

2. `project1`と`project2`のCodePipelineが実行されることを確認できました。
![02-codepipeline-trigger-exec.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/283246/8ea8f0f2-9881-0777-29b1-237350923164.png)

3. `project1`のディレクトリのみ変更します。
![03-codepipeline-trigger-exec.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/283246/0cecc758-22eb-e369-8ef1-34a458e8229c.png)

4. `project1`のCodePipelineが実行されることを確認できました。
![04-codepipeline-trigger-exec.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/283246/a50c1c06-0db5-6326-0a89-122324936ac2.png)

5. `project2`のディレクトリのみ変更します。
![05-codepipeline-trigger-exec.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/283246/5759b83d-9c36-ff5a-cc14-161bdb004f58.png)

6. `project2`のCodePipelineが実行されることを確認できました。
![06-codepipeline-trigger-exec.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/283246/d82d31e1-c49d-88de-ff5e-fb4e6d749a50.png)

以上で、変更されたディレクトリに応じて、実行するCodePipelineが振り分けられていることを確認できました。

# さいごに
こちらは、あくまでサンプルコードになります。参考になれば幸いです。