Gitのサブモジュール機能をCodeCommitとEgitで連動させてみる

# はじめに
GitをベースにしたCI/CDパイプラインを作る際、共通モジュールをどのように管理したらよいかという課題に対してアプローチを考える。

- CodeArtifactのようなローカルリポジトリを作ってそこに共通モジュールをPushしておく
- Gitのサブモジュール機能を使う

CodeArtifactは以前[記事にまとめてみた](https://qiita.com/neruneruo/items/c231d64bb1eb52ca8fa9)ので、そちらを参照してもらうとして、今回はGitのサブモジュール機能について実際に使ってみて掘り下げをしてみる。

# 事前準備
今回、メインモジュールとサブモジュールのリポジトリをあらかじめCodeCommitに作成しておく。
以下のようなイメージだ。

![キャプチャ0.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/111314/eb245e18-ea23-c763-00a4-9e5f47010933.png)

# サブモジュールを設定する
まず、CodeCommitのマネージメントコンソール画面では、サブモジュールを設定するアクションは無い。
このため、一旦EC2等にクローンしてきてからgit submoduleコマンドを使うか、Egit側から設定するしかない。
今回はEgit側から設定してみる。

まずは、上記のリポジトリのうち、メインモジュール側(```-test```の方)をチェックアウトしてくる。

gitのパースペクティブを表示して、メインモジュールを右クリックする。

![キャプチャ1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/111314/3aebaa7d-e7c9-7688-5300-1267cb177503.png)

と、メニューに「サブモジュールの追加」が表示されるので、これをクリックすると、ダイアログが開く。

![キャプチャ2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/111314/c1873c85-c349-a71d-8d58-350f85873c09.png)

このダイアログは、メインモジュールのプロジェクト内でのパスを記載する。
「次へ」をクリックすると、以下のダイアログが開く。

![キャプチャ3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/111314/84835dbd-20d5-4fb4-124a-c86e73f94f54.png)

ここで、最初に作ったサブモジュール側(```-submodule```の方)のリポジトリURIを入れる。ホストとリポジトリー・パスは自動で埋められる。
完了ボタンを押せば、パースペクティブ上のディレクトリ構成が以下のようになり、サブモジュールの設定が完了する。

![キャプチャ4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/111314/a33c249b-ae97-dd8c-996c-6d5f06f5d488.png)

# サブモジュールを使ったコードを書く
## 全体構成
今回の構成は以下の通り。

```
gitsubmodule-test
├── mainmodule
│   ├── pom.xml
│   └── src
│       ├── main
│       │   ├── java
│       │   │   └── com
│       │   │       └── gitsubmoduletest
│       │   │           ├── AWSLambdaHandler.java
│       │   │           ├── Hello.java
│       │   │           └── SpringCloudFunctionExampleApplication.java
│       │   └── resources
│       │       └── application.properties
│       └── test
│           └── java
│               └── com
│                   └── springcloudfunctiontest
│                       └── SpringCloudFunctionExampleApplicationTests.java
├── pom.xml
└── submodule
    ├── pom.xml
    └── src
        ├── main
        │   └── java
        │       └── com
        │           └── gitsubmoduletest
        │               └── common
        │                   └── user
        │                       └── User.java
        └── test
            └── java
                └── com
                    └── gitsubmoduletest
                        └── common
                            └── user
                                └── UserTests.java
```

## 依存関係の定義
とりあえず、POMはそれぞれのモジュールと全体で必要なので書いていく。

トップディレクトリのpom.xmlのポイントは
- 下位で必要なモジュールを定義する
- ```<modules>```で配下のモジュールを定義する
あたりだ。

```XML:トップディレクトリのpom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
        
    <groupId>com.gitsubmoduletest</groupId>
    <artifactId>git-submodule-test</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>pom</packaging>

    <name>git-submodule-test</name>
    <description>Git Submodule Test</description>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.6.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <properties>
        <java.version>1.8</java.version>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    </properties>
    
    <modules>
        <module>mainmodule</module>
        <module>submodule</module>
    </modules>
    
    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-function-adapter-aws</artifactId>
        </dependency>
        <dependency>
            <groupId>com.amazonaws</groupId>
            <artifactId>aws-lambda-java-events</artifactId>
            <version>2.0.2</version>
        </dependency>
        <dependency>
            <groupId>com.amazonaws</groupId>
            <artifactId>aws-lambda-java-core</artifactId>
            <version>1.1.0</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>Greenwich.SR2</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>
</project>
```

次に、サブモジュールのpom.xml。これ自体は特別なことはない。

```XML:submodule/pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
        
    <artifactId>user</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>user</name>
    <description>Submodule for Git Submodule Test</description>

    <properties>
        <java.version>1.8</java.version>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
    </properties>

    <parent>
        <groupId>com.gitsubmoduletest</groupId>
        <artifactId>git-submodule-test</artifactId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>
    
    <dependencies>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.assertj</groupId>
            <artifactId>assertj-core</artifactId>
            <version>3.16.1</version>
            <scope>test</scope>
        </dependency>
    </dependencies>
</project>
```

最後にメインモジュール。
これも特別なことはあまりない、普通の小さいSpringCloudFunctionだが、サブモジュールの依存関係の部分はしっかり書いておこう。忘れると動作しない。
また、サブモジュールを使用しない場合同様、```maven-shade-plugin```はここに書いて、FatJarを作ろう。

```XML:mainmodule/pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <artifactId>spring-cloud-function-example</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>spring-cloud-function-example</name>
    <description>Spring Cloud Function for Git Submodule Test</description>

    <parent>
        <groupId>com.gitsubmoduletest</groupId>
        <artifactId>git-submodule-test</artifactId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>

    <properties>
        <java.version>1.8</java.version>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <spring-cloud.version>Greenwich.SR2</spring-cloud.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-function-adapter-aws</artifactId>
        </dependency>
        <dependency>
            <groupId>com.amazonaws</groupId>
            <artifactId>aws-lambda-java-events</artifactId>
            <version>2.0.2</version>
        </dependency>
        <dependency>
            <groupId>com.gitsubmoduletest</groupId>
            <artifactId>user</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
        <dependency>
            <groupId>com.amazonaws</groupId>
            <artifactId>aws-lambda-java-core</artifactId>
            <version>1.1.0</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>Greenwich.SR2</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <dependencies>
                    <dependency>
                        <groupId>org.springframework.boot.experimental</groupId>
                        <artifactId>spring-boot-thin-layout</artifactId>
                        <version>1.0.10.RELEASE</version>
                    </dependency>
                </dependencies>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-shade-plugin</artifactId>
                <configuration>
                    <createDependencyReducedPom>false</createDependencyReducedPom>
                    <shadedArtifactAttached>true</shadedArtifactAttached>
                    <shadedClassifierName>aws</shadedClassifierName>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>
```
##  コード
###  メインモジュール
Spring Cloud Functionについては(別の記事)[https://qiita.com/neruneruo/items/710a981c0ad3877e1988]で書いているので細かいことはそちらを参照。
上記記事と同じアプリケーションで、idに紐付けてNameを返す部分をモジュールに切り出している。

```java:mainmodule/src/main/java/com/gitsubmoduletest/AWSLambdaHandler.java
package com.gitsubmoduletest;

import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent;
import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent;
import org.springframework.cloud.function.adapter.aws.SpringBootRequestHandler;

public class AWSLambdaHandler extends SpringBootRequestHandler<APIGatewayProxyRequestEvent, APIGatewayProxyResponseEvent> {
}
```

```java:mainmodule/src/main/java/com/gitsubmoduletest/SpringCloudFunctionExampleApplication.java
package com.gitsubmoduletest;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class SpringCloudFunctionExampleApplication {

    public static void main(String[] args) {
        SpringApplication.run(SpringCloudFunctionExampleApplication.class, args);
    }
}
```


```java:mainmodule/src/main/java/com/gitsubmoduletest/Hello.java
package com.gitsubmoduletest;

import java.util.Map;
import java.util.function.Function;

import org.springframework.stereotype.Component;

import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent;
import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent;

import com.gitsubmoduletest.common.user.User;

@Component
public class Hello implements Function<APIGatewayProxyRequestEvent, APIGatewayProxyResponseEvent> {
    @Override
    public APIGatewayProxyResponseEvent apply(APIGatewayProxyRequestEvent input) {
        Map<String, String> queryStringParameter = input.getQueryStringParameters();

        String id = queryStringParameter.get("id");
        User user = new User(id);
        String name = user.getName();

        APIGatewayProxyResponseEvent responseEvent = new APIGatewayProxyResponseEvent();
        responseEvent.setStatusCode(200);
        responseEvent.setBody("{\"name\":" + name + "}");
        return responseEvent;
    }
}
```

###  サブモジュール
切り出したモジュールを以下のように定義する。

```java:submodule/src/main/java/com/gitsubmoduletest/common/user/User.java
package com.gitsubmoduletest.common.user;

public class User {
    private String id = null;

    public String getId() {
        return id;
    }
	public void setId(String id) {
        this.id = id;
    }

	public String getName() {
		if( this.id.equals("11111") ) {
            return "\"Taro\"";
		} else if( this.id.equals("22222") ) {
			return "\"Jiro\"";
		} else if( this.id.equals("33333") ) {
			return "\"Saburo\"";
        } else {
        	return "\"Nanashi-no-gonbe\"";
        }
	}

	public User(String id) {
		this.id = id;
	}
}
```

###  テストコード
テストコードは今回の本筋ではないので割愛。
ちゃんとトップディレクトリで```mvn test```すれば、それぞれのテストコードに対するテストが自動実行される。

# リポジトリへのPush
テストによる正常性確認が終わったら、リポジトリに対してPushする。
ちゃんと
- サブモジュールに対するPushはサブモジュールに反映
- メインモジュールに対するPushはメインモジュールに反映
されるようになっている。

![キャプチャ5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/111314/cc909f7f-0a6d-6444-50ef-99caf7bd2b82.png)

なお、メインモジュールに対しては、サブモジュールを作ったときに作成される.gitmodulesとのファイルと、submoduleのディレクトリをPushしておこう。これは、

```
[submodule "submodule"]
	path = submodule
	url = https://git-codecommit.ap-northeast-1.amazonaws.com/v1/repos/xxxxxxxx-gitsubmodule-submodule
```

といった感じでサブモジュールが管理されている。
これをメインモジュール側に入れておくことで、```git clone --recursive```したときに、サブモジュールごと引っ張ってきてくれる。Eclipseでは以下のチェックボックスを入れるのが、```--recursive```と等価である。

![キャプチャ6.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/111314/1785eccf-d571-6332-836c-fcf3c25aac8d.png)

逆に言えば、.gitmodulesとディレクトリをPushしておかないと、--recursiveを付与してもサブモジュールはチェックアウトされないので気を付けよう。

正しくPushされていると、CodeCommitのマネージメントコンソール上では以下のように表示されるようになる。

![キャプチャ8.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/111314/925b4398-46c1-b50c-ed57-2ee9cc674b5e.png)


一旦リポジトリをローカルから削除して、```--recursive```なオプションでcloneすると以下のようになり、メインもサブも正しくcloneされる。

![キャプチャ7.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/111314/2a6e3fd8-6a2c-bb3f-32fe-f3b91a49a5d8.png)

# パイプラインに組み込む
## 概要
さて、ここまでやったらCI/CDパイプラインの動きを確認したくなるというもの。
早速、SAMテンプレートでデプロイするパイプラインを作ってみよう。

と思いきや、なんと[公式のトラブルシューティング](https://docs.aws.amazon.com/ja_jp/codepipeline/latest/userguide/troubleshooting.html)によると

> **問題:** CodePipeline は git サブモジュールをサポートしていません。CodePipeline は、サブモジュールをサポートしていない GitHub のアーカイブリンク API を使用しています。
> 
> **解決方法:** 別スクリプトの一部として直接 GitHub リポジトリをクローンすることを検討してください。たとえば、Jenkins スクリプトにクローンアクションを含めることができます。

らしい。ええー……

ちなみに、この「直接GitHubリポジトリをクローンする」というのが曲者で、超大変だった。

まずは、通常はCodePipelineであればCodeCommitからのソース取り出しをCodePipelineがやってくれるので、CodeBuildのサービスロールには不要な以下の権限を付与する必要がある(直接アクセスするので)。

```
        "codecommit:BatchGet*",
        "codecommit:BatchDescribe*",
        "codecommit:Describe*",
        "codecommit:Get*",
        "codecommit:List*",
        "codecommit:GitPull",
```

また、clone前に、gitに認証情報を渡す必要があるため、以下のようにする。
さらに、CodePipelineで取得するソースはgit cloneするわけではなく.gitがないので、git submodule をすることができない。なので、git clone でディレクトリ名を変更して取得する必要がある。

```YAML:buildspec.yml抜粋
  pre_build:
    commands:
      - git config --global credential.helper "!aws codecommit credential-helper $@"
      - git config --global credential.UseHttpPath true
      - git clone https://git-codecommit.ap-northeast-1.amazonaws.com/v1/repos/xxxxxxxx-gitsubmodule-submodule submodule
```

さて、これが通ればあとはSAMのデプロイだけだ。
しかし、その前にTerraformでパイプラインを定義しておこう。

## Terraformの定義
Terraformの全体構成としては以下の通り。
実は、これサブモジュールを駆使すれば良い感じにモジュール化できそうな気がしてきたな。

```
Terraform
├── 01_main.tf
├── 02_iam.tf
├── 03_s3.tf
├── 04_cloudwatchlogs.tf
├── 05_codepipeline.tf
└── 06_cloudformation_parameter.json
```
```HCL:01_main.tf
######################################################################
# 変数定義                                                            #
######################################################################
variable "prefix" {
  default = "GitSubModule-Test"
}

locals{
  ##############################################################################
  # IAM                                                                        #
  ##############################################################################
  codebuild_role_name      = "${var.prefix}-CodeBuildRole"
  codepipeline_role_name   = "${var.prefix}-CodePipelineRole"
  cloudformation_role_name = "${var.prefix}-CloudFormationRole"

  ##############################################################################
  # S3 Bucket                                                                  #
  ##############################################################################
  bucket_name = lower("${var.prefix}-artifact-bucket")

  ##############################################################################
  # CloudWatch Logs                                                            #
  ##############################################################################
  codebuild_logstream_name = "${var.prefix}-CodeBuildLogStream"

  ##############################################################################
  # CodeCommit                                                                 #
  ##############################################################################
  repository_name = lower("${var.prefix}")

  ##############################################################################
  # CodeBuild                                                                  #
  ##############################################################################
  buildspec_file_name = "buildspec.yml"
  build_project_name = "${var.prefix}-BuildProject"

  ##############################################################################
  # CodePipeline                                                               #
  ##############################################################################
  pipeline_name = "${var.prefix}-Pipeline"

  ##############################################################################
  # SAM                                                                        #
  ##############################################################################
  stack_name                           = "${var.prefix}-SAMStack"
  cf_param_lambda_function_name        = "${var.prefix}-Function"
  cf_param_lambda_execution_role_name  = "${var.prefix}-LambdaExecutionRole"
}
```
```HCL:02_iam.tf

######################################################################
# for CodeBuild                                                      #
######################################################################
resource "aws_iam_role" "codebuild" {
  name = "${local.codebuild_role_name}"

  assume_role_policy = <<EOF
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": "codebuild.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}
EOF
}

resource "aws_iam_role_policy" "codebuild" {
  name = "codebuild_policy"
  role = "${aws_iam_role.codebuild.id}"

  policy = <<POLICY
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "codecommit:BatchGet*",
        "codecommit:BatchDescribe*",
        "codecommit:Describe*",
        "codecommit:Get*",
        "codecommit:List*",
        "codecommit:GitPull",
        "logs:CreateLogGroup",
        "logs:CreateLogStream",
        "logs:PutLogEvents"
      ],
      "Resource": [
        "*"
      ]
    },
    {
      "Effect": "Allow",
      "Action": [
        "s3:*"
      ],
      "Resource": [
        "${aws_s3_bucket.artifact.arn}",
        "${aws_s3_bucket.artifact.arn}/*"
      ]
    }
  ]
}
POLICY
}

######################################################################
# for CodePipeline                                                   #
######################################################################
resource "aws_iam_role" "codepipeline" {
  name = "${local.codepipeline_role_name}"

  assume_role_policy = <<EOF
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": "codepipeline.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}
EOF
}

resource "aws_iam_role_policy" "codepipeline" {
  name = "codepipeline_policy"
  role = "${aws_iam_role.codepipeline.id}"

  policy = <<EOF
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect":"Allow",
      "Action": [
        "s3:GetObject",
        "s3:GetObjectVersion",
        "s3:GetBucketVersioning",
        "s3:PutObject"
      ],
      "Resource": [
        "${aws_s3_bucket.artifact.arn}",
        "${aws_s3_bucket.artifact.arn}/*"
      ]
    },
    {
      "Effect": "Allow",
      "Action": [
        "cloudformation:DescribeStacks",
        "cloudformation:CreateStack",
        "cloudformation:UpdateStack",
        "codecommit:GetRepository",
        "codecommit:GetBranch",
        "codecommit:GetCommit",
        "codecommit:ListBranches",
        "codecommit:GetUploadArchiveStatus",
        "codecommit:UploadArchive",
        "codecommit:CancelUploadArchive",
        "codebuild:StartBuild",
        "codebuild:StopBuild",
        "codebuild:BatchGet*",
        "codebuild:Get*",
        "codebuild:List*",
        "iam:PassRole"
      ],
      "Resource": "*"
    }
  ]
}
EOF
}

######################################################################
# for CloudFormation                                                 #
######################################################################
resource "aws_iam_role" "cloudformation" {
  name = "${local.cloudformation_role_name}"

  assume_role_policy = <<EOF
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": "cloudformation.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}
EOF
}

resource "aws_iam_role_policy" "cloudformation" {
  name = "cloudformation_policy"
  role = "${aws_iam_role.cloudformation.id}"

  policy = <<EOF
{
  "Version": "2012-10-17",
  "Statement": [
        {
            "Action": [
                "apigateway:*",
                "cloudformation:CreateChangeSet",
                "codedeploy:*",
                "elasticloadbalancing:DescribeTargetGroups",
                "elasticloadbalancing:DescribeTargetHealth",
                "elasticloadbalancing:CreateTargetGroup",
                "elasticloadbalancing:DeleteTargetGroup",
                "elasticloadbalancing:ModifyTargetGroup",
                "elasticloadbalancing:RegisterTargets",
                "elasticloadbalancing:DeRegisterTargets",
                "elasticloadbalancing:DescribeListeners",
                "elasticloadbalancing:CreateListener",
                "elasticloadbalancing:DeleteListener",
                "iam:GetRole",
                "iam:CreateRole",
                "iam:DeleteRole",
                "iam:PutRolePolicy",
                "iam:AttachRolePolicy",
                "iam:DeleteRolePolicy",
                "iam:DetachRolePolicy",
                "iam:PassRole",
                "iam:UpdateAssumeRolePolicy",
                "lambda:*",
                "logs:*",
                "s3:GetObject",
                "s3:GetObjectVersion",
                "s3:GetBucketVersioning",
                "s3:PutObject"
            ],
            "Resource": "*",
            "Effect": "Allow"
        }
    ]
}
EOF
}
```
```HCL:03_s3.tf
resource "aws_s3_bucket" "artifact" {
  bucket = "${local.bucket_name}"
}
```
```HCL:04_cloudwatchlogs.tf
resource "aws_cloudwatch_log_group" "codebuild" {
  name = "${local.codebuild_logstream_name}"
}
```
```HCL:05_codepipeline.tf
resource "aws_codebuild_project" "application" {
  name         = "${local.build_project_name}"
  service_role = "${aws_iam_role.codebuild.arn}"

  source {
    type      = "CODEPIPELINE"
    buildspec = "${local.buildspec_file_name}"
  }

  artifacts {
    type = "CODEPIPELINE"
  }
  
  environment {
    type            = "LINUX_CONTAINER"
    compute_type    = "BUILD_GENERAL1_SMALL"
    image           = "aws/codebuild/amazonlinux2-x86_64-standard:3.0"
    privileged_mode = "true"

    environment_variable {
        name  = "CF_BUCKET_NAME"
        value = "${local.bucket_name}"
    }
  }
 
  logs_config {
    cloudwatch_logs {
      group_name = "${aws_cloudwatch_log_group.codebuild.name}"
    }
  }

  cache {
    type  = "LOCAL"
    modes = [
      "LOCAL_CUSTOM_CACHE",
    ]
  }
}

resource "aws_codepipeline" "pipeline" {
  name     = "${local.pipeline_name}"
  role_arn = "${aws_iam_role.codepipeline.arn}"

  artifact_store {
    type     = "S3"
    location = "${aws_s3_bucket.artifact.bucket}"
  }

  stage {
    name = "Source"

    action {
      run_order        = 1
      name             = "Source"
      category         = "Source"
      owner            = "AWS"
      provider         = "CodeCommit"
      version          = "1"
      output_artifacts = ["SourceArtifact"]

      configuration = {
        RepositoryName = "${local.repository_name}"
        BranchName     = "master"
      }
    }
  }

  stage {
    name = "Build"

    action {
      run_order        = 2
      name             = "Build"
      category         = "Build"
      owner            = "AWS"
      provider         = "CodeBuild"
      version          = "1"
      input_artifacts  = ["SourceArtifact"]
      output_artifacts = ["BuildArtifact"]

      configuration = {
        ProjectName = "${aws_codebuild_project.application.name}"
      }
    }
  }

  stage {
    name = "Deploy"

    action {
      run_order        = 3
      name             = "Deploy"
      category         = "Deploy"
      owner            = "AWS"
      provider         = "CloudFormation"
      version          = "1"
      input_artifacts  = [
        "BuildArtifact",
      ]

      configuration = {
        StackName          = "${local.stack_name}"
        ActionMode         = "CREATE_UPDATE"
        RoleArn            = "${aws_iam_role.cloudformation.arn}"
        TemplatePath       = "BuildArtifact::output-template.yml"       
        Capabilities       = "CAPABILITY_AUTO_EXPAND,CAPABILITY_NAMED_IAM"
        ParameterOverrides = "${data.template_file.cloudformation_parameter.rendered}"
      }
    }
  }
}

data "template_file" "cloudformation_parameter" {
  template = file("${path.module}/06_cloudformation_parameter.json")

  vars = {
    cf_param_lambda_function_name       = "${local.cf_param_lambda_function_name}"
    cf_param_lambda_execution_role_name = "${local.cf_param_lambda_execution_role_name}"
  }
}
```
```JSON:06_cloudformation_parameter.json
{
    "LambdaFunctionName": "${cf_param_lambda_function_name}",
    "LambdaExecutionRoleName": "${cf_param_lambda_execution_role_name}"
}
```

TerraformとSAMが混ざると本当にアレで、SAM側でリソースの参照ができないので、CloudFormationのパラメータで無理矢理渡している。CodePipelineの ```provider = "CloudFormation"```の場合の```configuration```の書き方がググっても全然見つからなくて、```ParameterOverrides```はJSON形式で書く必要があるので、結局は外部ファイルに出してあげることにした。

## Buildspec
buildspecは、前述したCodeCommitの面倒くささはあるものの、素直に書ける。

```YAML:buildspec.yml
version: 0.2

phases:
  install:
    runtime-versions:
      java: corretto8
    commands:
      - pip install --upgrade awscli
  pre_build:
    commands:
      - git config --global credential.helper "!aws codecommit credential-helper $@"
      - git config --global credential.UseHttpPath true
      - git clone https://git-codecommit.ap-northeast-1.amazonaws.com/v1/repos/xxxxxxxx-gitsubmodule-submodule submodule
  build:
    commands:
      - echo Build started on `date`
      - mvn package
  post_build:
    commands:
      - echo Build ended on `date`
      - echo CloudFormation Package started on `date`
      - aws cloudformation package --template-file template.yml --output-template-file output-template.yml --s3-bucket ${CF_BUCKET_NAME}
      - echo CloudFormation Package ended on `date`
artifacts:
  type: zip
  files:
    - output-template.yml
cache:
  paths:
    - '/root/.m2/**/*'
```

最後にSAMテンプレートを以下のように定義する。
今回はLambdaを作るところまでなので少し手抜き。
ちゃんと```Parameters```をTerraformと連動できるようにしておこう。
ちなみに、超簡単なプログラムではあるが、メモリは128MBではOut Of Memoryになった……。
やっぱりSpring Cloud Functions、お手軽にやる分には燃費が悪いのでは……。

あと、注意すべき点としては、```CodeUri```に指定するJARファイルは、必ず```-aws```の方を指定すること。無印の方はハンドラ等を含まず正しく動作しない。

```YAML:template.yml
AWSTemplateFormatVersion: 2010-09-09
Transform: AWS::Serverless-2016-10-31
Description: Test for Lambda CI/CD Pipeline

Parameters:
  LambdaFunctionName:
    Description: "Lambda Function Name"
    Type: "String"
    Default: "LambdaFunctionName"
  LambdaExecutionRoleName:
    Description: "Lambda Execution Role Name"
    Type: "String"
    Default: "LambdaExecutionRoleName"
  
Globals:
    Function:
        Timeout: 60

Resources:
  # ------------------------------------------------------------#
  #  IAM Role
  # ------------------------------------------------------------#
  LambdaExecutionRole:
    Type: AWS::IAM::Role
    Properties: 
      RoleName: !Sub ${LambdaExecutionRoleName}
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - lambda.amazonaws.com 
            Action: sts:AssumeRole
      ManagedPolicyArns: 
        - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole 
  # ------------------------------------------------------------#
  #  Lambda
  # ------------------------------------------------------------#
  LambdaTest:
    Type: AWS::Serverless::Function
    Properties:
      FunctionName: !Sub ${LambdaFunctionName}
      Handler: org.springframework.cloud.function.adapter.aws.SpringBootStreamHandler
      Runtime: java8
      MemorySize: 256
      Role: !GetAtt LambdaExecutionRole.Arn
      CodeUri: ./mainmodule/target/spring-cloud-function-example-0.0.1-SNAPSHOT-aws.jar
      AutoPublishAlias: Prod
      DeploymentPreference:
        Type: AllAtOnce
```

ちなみに、メイン側のモジュールから見ると、サブモジュールに対してはリンクしている状態だけなので、サブモジュール側をPushしたとしても、パイプラインは走らない(トリガが違う)。
サブモジュールの更新に合わせて呼び出し元を更新するのであれば、サブモジュールのパイプライン完了のステータスを拾うCloudWatch Eventを設定する必要があるが、それはかなりメンテナンスが大変だろうから、どのようにするのがベストプラクティスなのだろうか……。