AWS CodeArtifactでJavaのライブラリを管理してみる

# はじめに
2020年6月にGAされたAWS CodeArtifact、大きいプロジェクトなんかで、共通のライブラリを管理するのにローカルリポジトリが欲しいけど、わざわざそのためにEC2立てたりコンテナ立てたりするのはな……と思っている人には大変使い勝手の良さそうなもの。

というわけで、今回は実際にAWS CodeArtifactに触ってJavaのライブラリを管理してみる。
せっかくだから、MavenとGradleで動かしてみるし、CodeArtifactと連携したライブラリのCI/CDまでやってみる。
お供になるのは、いつでも[公式のユーザーガイド](https://docs.aws.amazon.com/codeartifact/latest/ug/welcome.html)(なお、この記事を書いている時点ではまだ日本語マニュアルは存在しない)

あと、ユーザーガイドに書いてあるように、CLIは最新版にアップデートしないと使えないので注意。

# まずはリポジトリを作ってみる
サービス選択から```CodeArtifact```を検索してトップ画面にいき、「リポジトリを作成」ボタンを押す。
すると、以下の様にリポジトリの名前を求められるので、テキトーな名前をつける。

![キャプチャ1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/111314/61e865f8-e838-4507-7210-86eb959b10c0.png)

次に、ドメインを作成する。
クロスアカウントは今回はしないので、ひとまず「このAWSアカウント」を選択し、テキトーなドメイン名をつける。セキュリティのためにカスタマーマスターキーは設定しておこう。

![キャプチャ2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/111314/419ec07d-1405-ebb5-106c-1c7098436970.png)

確認画面では何もせず、「リポジトリを作成」！

![キャプチャ3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/111314/f13aee50-41f2-9c79-0e32-24108c506ac6.png)

これで、作成は完了である。次は接続をしてみよう。
接続設定は、作ったリポジトリの詳細画面で「接続手順の表示」を押し、

![キャプチャ4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/111314/ee9eaffa-b037-062a-9d7f-6fb52963f299.png)

パッケージマネージャークライアントの選択をすると以下のように表示される。
何故か```id```のハイフンが二重になる。こういうものなのかバグなのかは不明。謎。

![キャプチャ5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/111314/4245e4e8-f5ae-ca28-578b-3432884270f4.png)

最初のトークン取得をして、画面に出た長－－－－いパスワードを保管しておく。
トークン有効期限が最大12時間で、短くすることはできるが、それ以上の長さにはできないようだ。
少し不便……

# Mavenプロジェクトで使ってみる
Mavenプロジェクトで以下のようなライブラリを作ってみよう。

## ライブラリの格納

<details><summary>Greeting.java</summary><div>

```Java:Greeting.java
package com.amazonaws.lambda.demo;

public class Greeting {
    private String greetings;
    private String name = null;
	private String version = null;

    public String getGreetings() {
        return greetings;
    }
	public void setGreetings(String greetings) {
        this.greetings = greetings;
    }
    public String getName() {
		return name;
	}
	public void setName(String name) {
		this.name = name;
	}
	public String getVersion() {
		return version;
	}
	public void setVersion(String version) {
		this.version = version;
	}

    public Greeting(String greetings, String name, String version) {
		this.greetings = greetings;
		
		if(name != null) {
			this.name = name;
		}
		if(version != null) {
			this.version = version;
		}
		else {
			this.version = "1.0";
		}
	}
}
```

</div></details>

<details><summary>pom.xml</summary><div>

```XML:pom.xml
<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.amazonaws.lambda</groupId>
  <artifactId>Greeting</artifactId>
  <version>1.0.0</version>
  <packaging>jar</packaging>
  <name>Greeting</name>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  </properties>

  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>3.6.0</version>
        <configuration>
          <source>1.8</source>
          <target>1.8</target>
          <encoding>UTF-8</encoding>
          <forceJavacCompilerUse>true</forceJavacCompilerUse>
        </configuration>
      </plugin>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-shade-plugin</artifactId>
        <version>3.2.1</version>
        <executions>
          <execution>
            <phase>package</phase>
            <goals>
              <goal>shade</goal>
            </goals>
          </execution>
        </executions>
      </plugin>
    </plugins>
  </build>

  <dependencies>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.12</version>
      <scope>test</scope>
    </dependency>
  </dependencies>
  
  <distributionManagement>
    <repository>
      <id>test-domain-TestRepository</id>
      <name>test-domain-TestRepository</name>
      <url>[リポジトリのURL]</url>
    </repository>
  </distributionManagement>
</project>
```

</div></details>

<details><summary>settings.xml</summary><div>

```XML:settings.xml
<?xml version="1.0" encoding="UTF-8"?>
<settings>
  <servers>
    <server>
      <id>test-domain-TestRepository</id>
      <username>aws</username>
      <password>[↑のトークン取得で払い出したトークン]</password>
    </server>
  </servers>
</settings>
```

</div></details>

さて、これでmvn deployしてSUCCESSすると…

![キャプチャ6.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/111314/ad86341f-5dd0-9aed-74a0-2378e090ec85.png)

無事、リポジトリが追加されたぞ！

## ライブラリの利用
利用側のコードは以下のような感じにする。
setting.xmlは↑と同じもので構わない。
Lambdaで動かすことを前提にしているので、ちょっと変なことをしている。

Lambdaのハンドラ関数内で、↑のライブラリで定義しているクラスの

```Java
Greeting greetingBody = new Greeting("Hello", "Taro", null);
```

を使っている。

<details><summary>LambdaFunctionHandler.java</summary><div>

```Java:LambdaFunctionHandler.java
package com.amazonaws.lambda.demo;

import java.util.logging.Logger;

import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.LambdaLogger;
import com.amazonaws.services.lambda.runtime.RequestHandler;

import java.util.Map;
import java.util.HashMap;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;

public class LambdaFunctionHandler implements RequestHandler<Map<String, Object> , Response> {
	private static final Logger LOG = Logger.getLogger(LambdaFunctionHandler.class.getName());

    @Override
    public Response handleRequest(Map<String, Object> input, Context context) {
    	LambdaLogger logger = context.getLogger();
    	logger.log("received: " + input);

        Greeting greetingBody = new Greeting("Hello", "Taro", null);
        
        // TODO: implement your handler	
        return new Response.Builder()
        		.setStatusCode(200)
        		.setHeaders(new HashMap<String, String>(){{put("Content-Type","application/json");}})
        		.setObjectBody(greetingBody)
        		.build();
    }

}

```

</div></details>

<details><summary>Response.java</summary><div>

```Java:Response.java
package com.amazonaws.lambda.demo;

import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.Collections;
import java.util.Map;

//import org.apache.log4j.Logger;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.annotation.JsonInclude;


public class Response {
	private final int statusCode;
	private final Map<String, String> headers;
	private final boolean isBase64Encoded;
	private final String body;
		
	public int getStatusCode() {
		return statusCode;
	}

	public String getBody() {
		return body;
	}

	public Map<String, String> getHeaders() {
		return headers;
	}

	// API Gateway expects the property to be called "isBase64Encoded" => isIs
	public boolean isIsBase64Encoded() {
		return isBase64Encoded;
	}

	public static Builder builder() {
		return new Builder();
	}

	public static class Builder {
//		private static final Logger LOG = Logger.getLogger(Response.Builder.class);

		private static final ObjectMapper objectMapper = new ObjectMapper()
				.setSerializationInclusion(JsonInclude.Include.NON_NULL)
				.setSerializationInclusion(JsonInclude.Include.NON_EMPTY);

		private int statusCode = 200;
		private Map<String, String> headers = Collections.emptyMap();
		private String rawBody;
		private Object objectBody;
		private byte[] binaryBody;
		private boolean base64Encoded;

		public Builder setStatusCode(int statusCode) {
			this.statusCode = statusCode;
			return this;
		}

		public Builder setHeaders(Map<String, String> headers) {
			this.headers = headers;
			return this;
		}

		/**
		 * Builds the {@link Response} using the passed raw body string.
		 */
		public Builder setRawBody(String rawBody) {
			this.rawBody = rawBody;
			return this;
		}

		/**
		 * Builds the {@link Response} using the passed object body
		 * converted to JSON.
		 */
		public Builder setObjectBody(Object objectBody) {
			this.objectBody = objectBody;
			return this;
		}

		/**
		 * Builds the {@link Response} using the passed binary body
		 * encoded as base64. {@link #setBase64Encoded(boolean)
		 * setBase64Encoded(true)} will be in invoked automatically.
		 */
		public Builder setBinaryBody(byte[] binaryBody) {
			this.binaryBody = binaryBody;
			setBase64Encoded(true);
			return this;
		}

		/**
		 * A binary or rather a base64encoded responses requires
		 * <ol>
		 * <li>"Binary Media Types" to be configured in API Gateway
		 * <li>a request with an "Accept" header set to one of the "Binary Media
		 * Types"
		 * </ol>
		 */
		public Builder setBase64Encoded(boolean base64Encoded) {
			this.base64Encoded = base64Encoded;
			return this;
		}

		public Response build() {
			String body = null;
			if (rawBody != null) {
				body = rawBody;
			} else if (objectBody != null) {
				try {
					body = objectMapper.writeValueAsString(objectBody);
				} catch (JsonProcessingException e) {
//					LOG.error("failed to serialize object", e);
					throw new RuntimeException(e);
				}
			} else if (binaryBody != null) {
				body = new String(Base64.getEncoder().encode(binaryBody), StandardCharsets.UTF_8);
			}
			return new Response(statusCode, headers, base64Encoded, body);
		}
	}
	
    public Response(int statusCode, Map<String, String> headers, boolean isBase64Encoded, String body) {
    	this.statusCode = statusCode;
    	this.headers = headers;
    	this.isBase64Encoded = isBase64Encoded;
        this.body = body;
    }
}
```

</div></details>

キモになるのは、pom.xmlの以下の部分。
CodeArtifactに格納したライブラリの依存関係の定義、と、接続設定だ。

```XML
    <dependency>
      <groupId>com.amazonaws.lambda</groupId>
      <artifactId>Greeting</artifactId>
      <version>1.0.0</version>
    </dependency>
```

```XML
  <profiles>
    <profile>
      <id>test-domain-TestRepository</id>
      <activation>
        <activeByDefault>true</activeByDefault>
      </activation>
      <repositories>
        <repository>
          <id>test-domain-TestRepository</id>
          <name>test-domain-TestRepository</name>
          <url>[リポジトリのURL]</url>
        </repository>
      </repositories>
    </profile>
  </profiles>
```

<details><summary>pom.xml</summary><div>

```XML:pom.xml
<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.amazonaws.lambda</groupId>
  <artifactId>LambdaTest3</artifactId>
  <version>1.0.0</version>
  <packaging>jar</packaging>
  <name>LambdaTest3</name>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  </properties>

  <profiles>
    <profile>
      <id>test-domain-TestRepository</id>
      <activation>
        <activeByDefault>true</activeByDefault>
      </activation>
      <repositories>
        <repository>
          <id>test-domain-TestRepository</id>
          <name>test-domain-TestRepository</name>
          <url>[リポジトリのURL]</url>
        </repository>
      </repositories>
    </profile>
  </profiles>

  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>3.6.0</version>
        <configuration>
          <source>1.8</source>
          <target>1.8</target>
          <encoding>UTF-8</encoding>
          <forceJavacCompilerUse>true</forceJavacCompilerUse>
        </configuration>
      </plugin>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-shade-plugin</artifactId>
        <version>3.2.1</version>
        <executions>
          <execution>
            <phase>package</phase>
            <goals>
              <goal>shade</goal>
            </goals>
          </execution>
        </executions>
      </plugin>
    </plugins>
  </build>

  <dependencyManagement>
    <dependencies>
      <dependency>
        <groupId>com.amazonaws</groupId>
        <artifactId>aws-java-sdk-bom</artifactId>
        <version>1.11.256</version>
        <type>pom</type>
        <scope>import</scope>
      </dependency>
    </dependencies>
  </dependencyManagement>

  <dependencies>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.12</version>
      <scope>test</scope>
    </dependency>

    <dependency>
      <groupId>com.amazonaws</groupId>
      <artifactId>aws-lambda-java-events</artifactId>
      <version>1.3.0</version>
    </dependency>
    <dependency>
      <groupId>com.amazonaws</groupId>
      <artifactId>aws-lambda-java-core</artifactId>
      <version>1.1.0</version>
    </dependency>
    
    <dependency>
      <groupId>com.amazonaws.lambda</groupId>
      <artifactId>Greeting</artifactId>
      <version>1.0.0</version>
    </dependency>
  </dependencies>
</project>
```

</div></details>

これで、mvn packageすると、しっかりライブラリを取り込んでビルドが完了する。
settings.xmlをデフォルトパスから変えている場合は、ちゃんと```-s```オプションを指定しよう。

# Gradleプロジェクトで使ってみる
今度は、MavenでビルドしたものをGradleビルドで移植してみる。
Javaソースの中身は変更しない。

## ライブラリの格納
以下のbuild.gradleを作成して、```gradlew publish```する。

```groovy:build.gradle
plugins {
    id 'maven-publish'
    id 'java-library'
}

repositories {
    jcenter()
}

dependencies {
    testImplementation 'junit:junit:4.12'
}

publishing {
  publications {
      mavenJava(MavenPublication) {
          groupId = 'com.amazonaws.lambda'
          artifactId = 'Greeting'
          version = '1.0.1'
          from components.java
      }
  }
  repositories {
      maven {
          url '[リポジトリのURL]'
          credentials {
              username "aws"
              password "[↑のトークン取得で払い出したトークン]"
          }
      }
  }
}
```

なお、以下のエラーが表示されるが、アップロード自体は上手くいっているように見える。

```
> Task :publishMavenJavaPublicationToMavenRepository
Cannot upload checksum for module-maven-metadata.xml. Remote repository doesn't support sha-512. Error: Could not PUT 'https://test-domain-xxxxxxxxxxx.d.codeartifact.ap-northeast-1.amazonaws.com/maven/TestRepository/com/amazonaws/lambda/Greeting/maven-metadata.xml.sha512'. Received status code 400 from server: Bad Request
```

## ライブラリの利用
ライブラリ利用側は以下のような感じでbuild.gradleを設定する。
ライブラリ側と同じく、Javaソースは変更不要。

なお、FatJarを作るために、```gradle build```ではなく```gradlew shadowJar```で実行する。

```groovy:build.gradle

plugins {
    id 'com.github.johnrengelman.shadow' version '2.0.3'
    id 'java-library'
}

repositories {
    jcenter()

    maven {
        url '[リポジトリのURL]'
        credentials {
            username "aws"
            password "[↑のトークン取得で払い出したトークン]"
        }
    }
}

dependencies {
    implementation platform('com.amazonaws:aws-java-sdk-bom:1.11.256')
    implementation 'com.amazonaws:aws-lambda-java-events:1.3.0'
    implementation 'com.amazonaws:aws-lambda-java-core:1.1.0'
    runtimeOnly 'com.amazonaws:aws-lambda-java-log4j2:1.2.0'
    implementation 'com.amazonaws.lambda:Greeting:1.0.0'

    testImplementation 'junit:junit:4.12'
}

jar {
    manifest {
        attributes "Main-Class" : "LambdaFunctionHandler.java"
    }
}
```

Mavenだけでなく、Gradleでも問題なく動作することが確認できる。

# 共有のライブラリだってCI/CDしたいでしょ
という要望はあるかは分からないけど。
もちろんCodeArtifactはCodeBuildに組み込むこともできる。

CodeBuildとの連携は、[ユーザーガイドのこの節](https://docs.aws.amazon.com/codeartifact/latest/ug/using-maven-packages-in-codebuild.html)に書いてある。
書いてある通り、パブリッシュするために以下の権限をCodeBuildのサービスロールにアタッチしておこう。

```JSON
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "codeartifact:GetAuthorizationToken",
                "codeartifact:ReadFromRepository",
                "codeartifact:GetRepositoryEndpoint",
                "codeartifact:PublishPackageVersion",
                "codeartifact:PutPackageMetadata",
                "sts:GetServiceBearerToken"
            ],
            "Resource": "*"
        }
    ]
}
```

ポイントになってくるのはbuildspec.ymlだが、その前に、今回作ったsettings.xmlを見直しておこう。
いちいちCI/CIを回すたびにトークンの取得が手作業になっているのはあんまりなので、```password```タグを以下のようにしておく。

```XML
      <password>${env.CODEARTIFACT_TOKEN}</password>
```

その上で、buildspec.ymlを以下のように定義しておこう。
ちなみに、```aws codeartifact```の部分はユーザーガイドに書いてある通りに実行すると動かない。```--query authorizationToken```が無いと動かないので要注意だ。このオプション、[CLIのドキュメント](https://docs.aws.amazon.com/cli/latest/reference/codeartifact/get-authorization-token.html)にも載ってなかったりで、かなり謎だ……。とりあえず、このオプションを付与しないとタイムスタンプが出力されてしまって期待した環境変数にならない。

```YAML:buildspec.yml
version: 0.2
 
phases:
  install:
    runtime-versions:
      java: corretto8
    commands:
      - pip install --upgrade pip
      - pip install --upgrade awscli
  pre_build:
    commands:
      - export CODEARTIFACT_TOKEN=$(aws codeartifact get-authorization-token --domain test-domain --domain-owner [アカウントID] --query authorizationToken --output text)
  build:
    commands:
      - echo Build started on `date`
      - mvn -s ./settings.xml deploy
      - echo Build ended on `date`
cache:
  paths:
    - '/root/.m2/**/*'
```

これで無事、ライブラリのCI/CDパイプラインも完成した！