2
3

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-07-19

はじめに

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

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

CodeArtifactは以前記事にまとめてみたので、そちらを参照してもらうとして、今回はGitのサブモジュール機能について実際に使ってみて掘り下げをしてみる。

事前準備

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

キャプチャ0.png

サブモジュールを設定する

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

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

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

キャプチャ1.png

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

キャプチャ2.png

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

キャプチャ3.png

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

キャプチャ4.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>で配下のモジュールを定義する
あたりだ。

トップディレクトリの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。これ自体は特別なことはない。

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を作ろう。

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を返す部分をモジュールに切り出している。

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> {
}
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);
    }
}
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;
    }
}

サブモジュール

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

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

なお、メインモジュールに対しては、サブモジュールを作ったときに作成される.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

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

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

キャプチャ8.png

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

キャプチャ7.png

パイプラインに組み込む

概要

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

と思いきや、なんと公式のトラブルシューティングによると

問題: 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 でディレクトリ名を変更して取得する必要がある。

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
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"
}
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
}
03_s3.tf
resource "aws_s3_bucket" "artifact" {
  bucket = "${local.bucket_name}"
}
04_cloudwatchlogs.tf
resource "aws_cloudwatch_log_group" "codebuild" {
  name = "${local.codebuild_logstream_name}"
}
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}"
  }
}
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の面倒くささはあるものの、素直に書ける。

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の方を指定すること。無印の方はハンドラ等を含まず正しく動作しない。

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を設定する必要があるが、それはかなりメンテナンスが大変だろうから、どのようにするのがベストプラクティスなのだろうか……。

2
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
3