devcontainer を利用して Java(Amazon Corretto) + Localstack の開発環境をつくってみた

devcontainer の Java の事前定義済みコンテナ構成では、Microsoft Build of OpenJDK が利用されています。

Amazon Corretto と Localstack を利用したかったので、devcontainer の設定を変えて開発環境をつくってみました。

動作確認としては Amazon SQS を Localstack 上に構成して、送受信を行いました。

## 前提とする環境

- WSL2 および Docker がインストール済み
  - [WSL2 + Docker で作る Docker 環境](2022-10-28_Docker_WSL2_Zscaler_5a04a732524c72ff809a.md)

## 動作確認用プロジェクトの作成

[Spring Initializr](https://start.spring.io/)を利用してプロジェクトを作成します。

画面からも作成することができますが、今回は curl から作成しました(パラメータは`curl https://start.spring.io`から確認することができます)。

```bash
$ curl -OL https://start.spring.io/starter.tgz \
  -d javaVersion=17 \
  -d bootVersion=2.7.2 \
  -d type=maven-project \
  -d groupId=org.ichiwork \
  -d name=spring-boot-sample \
  -d version=0.0.1-SNAPSHOT \
  -d dependencies=web \
  -d baseDir=spring-boot-sample \
  -d packageName=org.ichiwork.spring.sample
$ tar xvfz starter.tgz
```

## VSCode

VSCode がインストールされていなければインストールしておきます。

```dos
> winget install -q vscode --scope machine
```

また、拡張機能の[Dev Containers](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers)をインストールします。

## 事前定義済みのコンテナ構成の利用

事前定義済みのコンテナ構成を利用する場合、コマンドパレットからぽちぽち選択していけばよい。

1. 動作確認用プロジェクトを VSCode で開く
2. コマンドパレットで Dev Containers: Add Dev Container Configuration Files...を選択する
   ![2022-10-20-16-16-55.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/435938/17a65ce2-8078-e095-5c5a-892183e1eee5.png)
3. 選択していくと、プロジェクトフォルダ直下に`.devcontainer`が作成される
4. コンテナで開き直すかどうかを聞かれるので、開き直す
   ![2022-10-21-11-36-26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/435938/ea5cbe2a-a685-1ea5-5d2a-04c0bf98fb74.png)
5. しばらくすると、`.devcontainer`の設定に基づき Docker Container が作成され、そこで開発が行えるようになる

## amazoncorretto:17 を元にしたコンテナの利用

[microsoft/vscode-dev-containers](https://github.com/microsoft/vscode-dev-containers/tree/main/containers/java-postgres)を参考に`.devcontainer`を作成しました。

```
|--.devcontainer
|  |--devcontainer.json
|  |--docker-compose.yml
|  |--Dockerfile
```

作成したファイルは以下の通りで、コンテナで開き直せば準備完了です。

### devcontainer.json

```jsonc
// For format details, see https://aka.ms/devcontainer.json. For config options, see the README at:
// https://github.com/microsoft/vscode-dev-containers/tree/v0.245.2/containers/java
{
  "name": "Java & LocalStack",
  "dockerComposeFile": "docker-compose.yml",
  "service": "app",
  "workspaceFolder": "/workspace",

  // Configure tool-specific properties.
  "customizations": {
    // Configure properties specific to VS Code.
    "vscode": {
      // Set *default* container specific settings.json values on container create.
      "settings": {},

      // Add the IDs of extensions you want installed when the container is created.
      "extensions": ["vscjava.vscode-java-pack"]
    }
  },

  // Use 'forwardPorts' to make a list of ports inside the container available locally.
  // "forwardPorts": [],

  // Use 'postCreateCommand' to run commands after the container is created.
  // "postCreateCommand": "java -version",

  // Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root.
  "remoteUser": "vscode",
  "features": {}
}
```

### docker-compose.yml

```yml
version: "3.8"

services:
  app:
    container_name: javadev
    build:
      context: .
      dockerfile: Dockerfile
      args:
        JAVA_VERSION: 17

    volumes:
      - ..:/workspace:cached

    # Overrides default command so things don't shut down after the process ends.
    command: sleep infinity

    # Runs app on the same network as the aws container, allows "forwardPorts" in devcontainer.json function.
    network_mode: service:aws

    # Uncomment the next line to use a non-root user for all processes.
    # user: vscode

    # Use "forwardPorts" in **devcontainer.json** to forward an app port locally.
    # (Adding the "ports" property to this file will not forward from a Codespace.)

  aws:
    container_name: localstack
    image: localstack/localstack
    ports:
      - "4583:4583"
```

### Dockerfile

non-root で動作させたかったので、[Creating a non-root user](https://code.visualstudio.com/remote/advancedcontainers/add-nonroot-user#_creating-a-nonroot-user)を参考に vscode ユーザを追加しました。
なお、amazoncorretto の base イメージには`groupadd`、 `useradd`がないので shadow-utils を追加しています。

リソースの作成は Terraform で行えるようにイメージに追加しています。

```dockerfile
ARG JAVA_VERSION="17"
FROM amazoncorretto:${JAVA_VERSION}

ARG USERNAME=vscode
ARG USER_UID=1000
ARG USER_GID=$USER_UID

# Create the user
RUN yum install -y shadow-utils tar
RUN groupadd --gid $USER_GID $USERNAME \
    && useradd --uid $USER_UID --gid $USER_GID -m $USERNAME \
    #
    # [Optional] Add sudo support. Omit if you don't need to install software after connecting.
    && yum update -y \
    && yum install -y sudo \
    && echo $USERNAME ALL=\(root\) NOPASSWD:ALL > /etc/sudoers.d/$USERNAME \
    && chmod 0440 /etc/sudoers.d/$USERNAME

# ********************************************************
# * Anything else you want to do like clean up goes here *
# ********************************************************
# Add IaC Tools
RUN yum install -y yum-utils \
    && yum-config-manager --add-repo https://rpm.releases.hashicorp.com/AmazonLinux/hashicorp.repo \
    && yum -y install terraform

RUN yum install -y python3 \
    && pip3 install awscli --upgrade

# [Optional] Set the default user. Omit if you want to keep the default as root.
USER $USERNAME
```

## 動作確認の準備

### Amazon SQS のキューを作成する

Terraform でリソースを作成するにあたって、バージョンを確認しておきます。

```bash
$ terraform version
Terraform v1.3.3
on linux_amd64
```

リソース定義ファイルは以下のように用意して、`init->apply`してキューを作成します(required_version は Terraform のバージョンとあわせます)。

```hcl
terraform {
  required_version = "1.3.3"

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "4.36.0"
    }
  }
}

provider "aws" {
  access_key                  = "dummy"
  secret_key                  = "dummy"
  region                      = "ap-northeast-1"
  skip_credentials_validation = true
  skip_metadata_api_check     = true
  skip_requesting_account_id  = true

  endpoints {
    sqs = "http://localstack:4566"
  }
}

resource "aws_sqs_queue" "queue" {
  name                        = "my-queue"
}

output "queue_url" {
  value = aws_sqs_queue.queue.url
}
```

作成されたら、URL を利用するのでメモしておきます。

```
aws_sqs_queue.queue: Creation complete after 25s [id=http://localstack:4566/000000000000/my-queue]

Apply complete! Resources: 1 added, 0 changed, 0 destroyed.

Outputs:

queue_url = "http://localstack:4566/000000000000/my-queue"
```

### 動作確認用アプリケーションの準備

[Amazon SQS メッセージの送信、受信、削除](https://docs.aws.amazon.com/ja_jp/sdk-for-java/v1/developer-guide/examples-sqs-messages.html)を参考に、AWS SDK for Java からメッセージの送信、受信、削除を実装します。

#### 依存関係

[aws/aws-sdk-Java-v2](https://github.com/aws/aws-sdk-java-v2/#using-the-sdk)を参考に依存関係を`pom.xml`に追加します。

```xml
	<dependencyManagement>
		<dependencies>
			<dependency>
			<groupId>software.amazon.awssdk</groupId>
			<artifactId>bom</artifactId>
			<version>2.18.1</version>
			<type>pom</type>
			<scope>import</scope>
			</dependency>
		</dependencies>
	</dependencyManagement>
  ・
  ・
  ・
		<dependency>
			<groupId>software.amazon.awssdk</groupId>
			<artifactId>sqs</artifactId>
		</dependency>
```

#### SqsClient の設定

[Amazon SQS のキューを作成する](#amazon-sqs-のキューを作成する)と設定にあわせて Bean を定義しておきます。

```java
@Configuration
public class AwsConfig {
    @Bean
    public SqsClient sqsClient() {
        AwsCredentials awsCredentials =
                AwsBasicCredentials.create("dummy", "dummy");

        SqsClient sqsClient =
                SqsClient
                        .builder()
                        .defaultsMode(DefaultsMode.AUTO)
                        .credentialsProvider(StaticCredentialsProvider.create(awsCredentials))
                        .region(Region.AP_NORTHEAST_1)
                        .endpointOverride(URI.create("http://localstack:4566"))
                        .build();

        return sqsClient;
    }
}
```

#### 送信、受信、削除

適当な RestController を実装します。

```java
@RestController
@RequestMapping("/")
public class SampleController {
    SqsClient sqsClient;

    ObjectMapper objectMapper;

    private static final String QUEUE_URL = "http://localstack:4566/000000000000/my-queue";

    public SampleController(SqsClient sqsClient, ObjectMapper objectMapper) {
        this.sqsClient = sqsClient;
        this.objectMapper = objectMapper;
    }

    @PostMapping(value = "send", consumes = "application/json")
    public void send(@RequestBody Map<String, Object> message) throws JsonProcessingException {
        String messageAsString = objectMapper.writeValueAsString(message);

        SendMessageRequest request =
                SendMessageRequest
                        .builder()
                        .queueUrl(QUEUE_URL)
                        .messageBody(messageAsString)
                        .delaySeconds(5)
                        .build();

        sqsClient.sendMessage(request);
    }

    @GetMapping(value = "receive")
    public void receive() throws JsonProcessingException {
        ReceiveMessageRequest receiveMessageRequest =
        ReceiveMessageRequest.builder()
          .queueUrl(QUEUE_URL)
          .build();
        List<Message> messages= sqsClient.receiveMessage(receiveMessageRequest).messages();
        messages.forEach(m -> {
            System.out.println(m.body());
            DeleteMessageRequest deleteMessageRequest =
            DeleteMessageRequest.builder()
                .queueUrl(QUEUE_URL)
                .receiptHandle(m.receiptHandle())
                .build();
            sqsClient.deleteMessage(deleteMessageRequest);
        });
    }
}
```

## 動作確認

1. アプリケーションを起動させる
   `bash
   ./mvnw spring-boot:run
   `
2. アプリケーションを起動させて、送信してみます
   `bash
   curl -XPOST -H "Content-Type: application/json" localhost:8080/send -d "{\"messsage\": \"Hello Sqs\"}"
   `
3. 受信してみます
   `bash
   curl localhost:8080/receive
   `
4. 標準出力に受信した結果が出力されます
   ![2022-10-24-14-58-55.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/435938/1ca80ef1-65a2-e611-5a1c-914b002f441a.png)

## 参考

- [Create a development container](https://code.visualstudio.com/docs/remote/create-dev-container)
- [microsoft/vscode-dev-containers](https://github.com/microsoft/vscode-dev-containers)
- [Amazon Simple Queue Service ~Developer Guide~](https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/welcome.html)
- [AWS SDK for Java ~デベロッパーガイド~](https://docs.aws.amazon.com/ja_jp/sdk-for-java/v1/developer-guide/welcome.html)