AWS CodeCommitで始めるモノリポジトリCICD

# はじめに
　AWS CodePipelineでは、CodeCommitをソースとするときに、標準オプションではディレクトリ単位での指定ができません。  
　CodePipelineのユーザーガイドを見てみても、[指定できるのはブランチ名のみ](https://docs.aws.amazon.com/ja_jp/codepipeline/latest/userguide/action-reference-CodeCommit.html)です。  
　しかし、モノリポジトリ戦略をとる場合は、同一ブランチの中でも変更があったディレクトリによって別のパイプラインを起動させたいというニーズが発生します。  
　この記事は、CodeCommitおよびCodePipelineを使って、ディレクトリごとにアプリケーションを分けている場合、ソースに変更のあったアプリケーションのみパイプラインでビルドさせるためのTIPSを紹介します。  

# やりたいこと
　今回のやりたいことを図で表すと下記のような形です。
![yaritaikoto.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/681482/5b230b1e-3d4e-182d-3821-a8f2fd3237d7.png)
　CodeCommit内のディレクトリ構造は、アプリケーションごとにディレクトリが切られていて各ディレクトリの下にアプリケーションのソースコードとbuildspecが配置してあります。
　例えばアプリケーションAのソースコードのみを修正した場合、CodePipelineAのみが起動してほしいのですが、標準の機能で構成した場合CodePipelineAとCodePipelineBが同時に起動してしまいます。

# なぜできないのか
　CodePipelineではCodeCommitをポーリングして変更を検知する[PollForSourceChanges](https://docs.aws.amazon.com/codepipeline/latest/userguide/action-reference-CodeCommit.html#action-reference-CodeCommit-config)というオプションを持っていますが、現在ではEventBridgeのトリガーを推奨されているため、基本的にはEventBridgeでCodeCommitの変更を検知し、ルールのターゲットとしてCodePipelineを指定することで起動することになると思います。  
　しかし、EventBridgeで検知できるCodeCommitのイベントの中に、ディレクトリ単位で範囲を制限できるようなものはありません。EventBridgeで利用できる[CodeCommitのイベント](https://docs.aws.amazon.com/ja_jp/codecommit/latest/userguide/monitoring-events.html)を見ると、ソースの変更(Branchの作成も含めて)に関係するのはreferenceCreatedとreferenceUpdatedというイベントくらいなのですが、変更があったブランチ名でフィルターはかけられてもディレクトリに関するフィールドはなさそうです。
　結果として、EventBridgeでディレクトリ単位の更新を検知できないことがCodeCommitにおけるモノリポジトリCICDを阻んでいると考えられます。

# 解決策
## 解決策の概要
　EventBridgeの後にLambdaを挟み、どこのディレクトリに変更があったかを判断した上で対象となるPipelineを選定し、起動します。  
　簡易なアーキテクチャは下記のようになります。  
![architecture.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/681482/61846302-3ce8-81e2-bfdd-ba80ac7de9f3.png)
　次のセクションから、実際にEventBridgeとLambdaでどのような設定を行うとよいかを解説します。  
　なお、CodePipelineとCodeCommitの作り方についてはこの記事の趣旨からずれるので説明を省略しますが、CodePipelineのパイプライン名については、後述するルールで命名しないと動作しない点にご注意ください。  

## Lambda関数
### 関数コード
　下記がLamdbaのコードです。(エラー処理等は考慮されていません。）Python3.9のランタイムで動作を確認しています。
```python
import os
import json
import boto3

def lambda_handler(event, context):
  afterCommitId = event['detail']['commitId']
  beforeCommitId = event['detail']['oldCommitId']
  repositoryName = event['detail']['repositoryName']
  pjName = os.environ['PJNAME']
  env = os.environ['ENV']
  pathList = [x.strip() for x in os.environ['PIPELINE_DICT'].split(',')]
  codecommitclient = boto3.client('codecommit')
  codepipelineclient = boto3.client('codepipeline')

  for pathName in pathList:
      response = codecommitclient.get_differences(
          repositoryName=repositoryName,
          beforeCommitSpecifier=beforeCommitId,
          afterCommitSpecifier=afterCommitId,
          beforePath=pathName,
          afterPath=pathName
      )
      if len(response['differences']) > 0:
          pipelineName = pjName + '-' + env + '-' + pathName.replace('/', '-')
          codepipelineclient.start_pipeline_execution(name=pipelineName)

  return {
      'statusCode': 200,
      'body': json.dumps('CICD Pipeline Triggered')
  }
```

### 環境変数
　この関数は、環境変数としてPJNAME, ENV, PIPELINE_DICTという3つの変数を定義されています。
　PJNAME, ENVはそれぞれ固有のプロジェクト名と環境名(devやprodなど)だと考えてください。
　PIPELINE_DICTは、アプリケーションのディレクトリパスが記載されたカンマ区切りのリストです。リポジトリのルートディレクトリは含みません。
　例えば、下記のようなディレクトリ構造の3つのアプリケーションを内包するリポジトリがあったとします。
```text
repositoryroot
　∟ applicationa
　∟ utility
　　∟ applicationb
　　∟ applicationc
```
　この場合、PIPELINE_DICTは、
```text
applicationa,utility/applicationb,utility/applicationc
```
となります。

### CodePipelineのパイプライン名(注意)
　それぞれのアプリケーションと紐づくCodePipelineのパイプライン名は、下記命名ルールに従う必要があります。
```text:パイプライン名
<PJNAME>-<ENV>-<アプリケーションのパスのスラッシュをハイフンに変換したもの>
```
例えば、
PJNAME：sample
ENV：dev
アプリケーションパス：utility/applicationb
だったとすると、下記のような名前です。
```text:【サンプル】applicationbのパイプライン名
sample-dev-utility-applicationb
```

### 関数内の処理の解説
　関数内で何をやっているかについてかいつまんで解説します。
　まず、
```python
  afterCommitId = event['detail']['commitId']
  beforeCommitId = event['detail']['oldCommitId']
  repositoryName = event['detail']['repositoryName']
```
の部分で、EventBridgeのイベントから更新時のCommitIDと、一つ前のCommitID、およびリポジトリ名を受け取ります。
　その後、
```python
      response = codecommitclient.get_differences(
          repositoryName=repositoryName,
          beforeCommitSpecifier=beforeCommitId,
          afterCommitSpecifier=afterCommitId,
          beforePath=pathName,
          afterPath=pathName
      )
```
の部分で、codecommitのget_differences APIを利用して、対象のアプリケーションのディレクトリパスの中の差分を取得します。
　最後に、
```python
      if len(response['differences']) > 0:
          pipelineName = pjName + '-' + env + '-' + pathName.replace('/', '-')
          codepipelineclient.start_pipeline_execution(name=pipelineName)
```
の部分で、差分があった場合のみ対象のパイプラインをスタートさせています。
　CodeCommitのget_difference APIを利用すれば簡単なのですが、なかなかこれでできるというところに気づくまでは時間がかかりました。

## EventBridge
　イベントブリッジのルールはすごくシンプルです。下記はCloudFormationのテンプレートですが、下記の通りCodeCommitのreferenceUpdateを検知するルールを作り、ターゲットを先ほどのLambda関数にするだけです。（Lambda関数の呼び出しのための権限等は適宜調整してください。）
```YAML
  AmazonCloudWatchEventRule:
    Type: AWS::Events::Rule
    Properties:
      EventPattern:
        source:
          - aws.codecommit
        detail-type:
          - 'CodeCommit Repository State Change'
        resources:
          - !Sub arn:aws:codecommit:${AWS::Region}:${AWS::AccountId}:${AppRepositoryName}
        detail:
          event:
            - referenceUpdated
          referenceType:
            - branch
          referenceName:
            - !Ref BranchName
      Targets:
        - Arn: !GetAtt LambdaFunction.Arn
          Id: pipeline-trigger-lambda
```

# まとめ
　今回は、CodeCommit ⇒ EventBridge ⇒ CodePipeline の流れの中にLambdaを挟むことで、ディレクトリ単位のパイプラインの起動を実現しました。  
　この記事が、少しでもCodeシリーズを利用している皆様の助けになれば幸いです。

　記載されている会社名、製品名、サービス名、ロゴ等は各社の商標または登録商標です。