CLOVER🍀

That was when it all began.

Moto(Server Mode)を使って、AWSのサービスをローカルで試してみる

これは、なにをしたくて書いたもの?

AWSのサービスをローカル環境で試す時に使うものといえば、LocalStackが著名なのではないかなと思います。

LocalStack - A fully functional local cloud stack

今回はMotoというものを試してみたいと思います。

Moto

Motoは、AWSのインフラを簡単にモックしてテストしやすくするためのライブラリーです。

A library that allows you to easily mock out tests based on AWS infrastructure.

Moto: Mock AWS Services — Moto 4.1.9.dev documentation

Pythonで実装されています。

GitHub - getmoto/moto: A library that allows you to easily mock out tests based on AWS infrastructure.

使用するサービスを指定してインストールしたり、全部インストールしたりしてPythonから使います。

$ pip3 install 'moto[ec2,s3,..]'

$ pip3 install 'moto[all]'

実装されているAWSサービスはこちらに一覧があり、

Implemented Services — Moto 4.1.9.dev documentation

各サービスのページを見ると実装されているAPI、実装されていないAPIが確認できます。

s3 — Moto 4.1.9.dev documentation

使い方は、こちら。

Getting Started with Moto — Moto 4.1.9.dev documentation

Pythonコード内でデコレーターやコンテキストマネージャーを使ってモックしたり、直接Motoを使ったり、テストコード内での
使い方が載っています。

Pythonコード以外からMotoを使う

こう書いていると、MotoはPythonのみで使えるライブラリーのようにも見えますが、Motoにはスタンドアロンのサーバーモードがあり、
こちらを使うことでPython以外からもMotoを使うことができます。

Non-Python SDK’s / Server Mode — Moto 4.1.9.dev documentation

インストール方法としては、パッケージとして導入する方法、Dockerイメージを使う方法があります。

$ pip3 install moto[server]
$ moto_server

$ docker container run motoserver/moto:latest

サンプルには、PythonJavaScala、Terraformでの使用例が載っています。

Non-Python SDK’s / Server Mode / Example Usage

ただ、この使い方はAWS SDK for Python(Boto3)からアクセスする場合には推奨されていないことには注意しましょう。

However, this method isn’t encouraged if you’re using boto3, the best solution would be to use a decorator method.

Getting Started with Moto / Moto usage / Stand-alone server mode

MotoとLocalStack

Motoのサーバーモードの話を見ると、LocalStackと競合しそうなライブラリーに見えますが、MotoとLocalStackの関係については
双方に記載があったりします。

Motoでは、LocalStackはより高度な機能を付けた兄弟のような説明が書かれています。

LocalStack is Moto’s bigger brother with more advanced features, such as EC2 VM’s that you can SSH into and Dockerized RDS-installations that you can connect to.

FAQ for Developers / What … / Alternatives are there?

一方、LocalStackでは多くのサービスのバックエンドでMotoを拡張して使用していることが書かれています。

Many LocalStack service providers use moto as a backend. Moto is an open-source library that provides mocking for Python tests that use Boto, the Python AWS SDK. We re-use a lot of moto’s internal functionality, which provides mostly CRUD and some basic emulation for AWS services. We often extend services in Moto with additional functionality. Moto plays such a fundamental role for many LocalStack services, that we have introduced our own tooling around it, specifically to make requests directly to moto.

LocalStack Concepts / Service / call_moto

というわけで、MotoはLocalStackの基礎の役割にもなっている、という感じですね。

今回は、MotoのサーバーモードをTerraform、AWS SDK for JavaScriptから扱ってみたいと思います。

環境

今回の環境は、こちら。

Python

$ python3 -V
Python 3.10.6


$ pip3 -V
pip 22.0.2 from /usr/lib/python3/dist-packages/pip (python 3.10)

AWS CLI

$ aws --version
aws-cli/2.11.18 Python/3.11.3 Linux/5.15.0-71-generic exe/x86_64.ubuntu.22 prompt/off

Terraform。

$ terraform version
Terraform v1.4.6
on linux_amd64

Node.js。

$ node --version
v18.16.0


$ npm --version
9.5.1

Moto(サーバーモード)をインストールする

まずはMoto(サーバーモード)をインストールします。

仮想環境の作成。

$ python3 -m venv venv
$ . venv/bin/activate

Moto(サーバーモード)のインストール。

$ pip3 install moto[server]

今回は、4.1.8がインストールされました。

$ pip3 freeze | grep moto
moto==4.1.8

なお、ダウンロードしたpipモジュールは300Mほどありました…。

起動。

$ moto_server

このまま本番環境では使わないこと、というのと、リッスンしているアドレスとポートが表示されます。

WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
 * Running on http://127.0.0.1:5000
Press CTRL+C to quit

ヘルプ。

$ moto_server -h
usage: moto_server [-h] [-H HOST] [-p PORT] [-r] [-s] [-c SSL_CERT] [-k SSL_KEY] [service]

positional arguments:
  service

options:
  -h, --help            show this help message and exit
  -H HOST, --host HOST  Which host to bind
  -p PORT, --port PORT  Port number to use for connection
  -r, --reload          Reload server on a file change
  -s, --ssl             Enable SSL encrypted connection with auto-generated certificate (use https://... URL)
  -c SSL_CERT, --ssl-cert SSL_CERT
                        Path to SSL certificate
  -k SSL_KEY, --ssl-key SSL_KEY
                        Path to SSL private key

リッスンするポートを変える場合。

$ moto_server -p 8000
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
 * Running on http://127.0.0.1:8000
Press CTRL+C to quit

リッスンするアドレスを変える場合。

$ moto_server -H 0.0.0.0
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
 * Running on all addresses (0.0.0.0)
 * Running on http://127.0.0.1:5000
 * Running on http://192.168.0.6:5000
Press CTRL+C to quit

今回は、なにも指定せずに起動しておくことにします。

$ moto_server

TerraformでMotoを操作する

次は、TerraformからMotoを操作してみましょう。

今回はAmazon S3バケットの作成と、AWS Systems Manager Parameter Storeにエントリーを登録してみたいと思います。

作成したTerraform構成ファイルはこちら。

main.tf

terraform {
  required_version = "1.4.6"

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

provider "aws" {
  access_key                  = "mock_access_key"
  secret_key                  = "mock_secret_key"
  region                      = "us-east-1"
  skip_credentials_validation = true
  skip_metadata_api_check     = true
  skip_requesting_account_id  = true
  s3_use_path_style           = true

  endpoints {
    s3  = "http://localhost:5000"
    ssm = "http://localhost:5000"
  }
}

resource "aws_s3_bucket" "my_bucket" {
  bucket = "my-bucket"
}

resource "aws_ssm_parameter" "ssm_parameter_word" {
  name  = "word"
  type  = "SecureString"
  value = "Parameter from Moto"
}

output "my_bucket_name" {
  value = aws_s3_bucket.my_bucket.bucket
}

output "parameter_name" {
  value = aws_ssm_parameter.ssm_parameter_word.name
}

ポイントはこのあたりなのですが。

provider "aws" {
  access_key                  = "mock_access_key"
  secret_key                  = "mock_secret_key"
  region                      = "us-east-1"
  skip_credentials_validation = true
  skip_metadata_api_check     = true
  skip_requesting_account_id  = true
  s3_use_path_style           = true

  endpoints {
    s3  = "http://localhost:5000"
    ssm = "http://localhost:5000"
  }
}

これは以下の2つを参考にして書きました。

Non-Python SDK’s / Server Mode / Example Usage

Custom Service Endpoint Configuration / Connecting to Local AWS Compatible Solutions / LocalStack

クレデンシャルは今回は直接書いています。

あとはinitしてapply

$ terraform init
$ terraform apply

AWS CLIで確認してみましょう。環境変数を設定。

$ export AWS_ACCESS_KEY_ID = test
$ export AWS_SECRET_ACCESS_KEY = test
$ export AWS_DEFAULT_REGION = us-east-1

Amazon S3バケットの作成確認。エンドポイントはMoto(サーバーモード)に向けています。

$ aws --endpoint http://localhost:5000 s3 ls
2023-05-06 23:18:36 my-bucket

AWS Systems Manager Parameter Storeの確認。

$ aws --endpoint http://localhost:5000 ssm get-parameter --name word --with-decryption
{
    "Parameter": {
        "Name": "word",
        "Type": "SecureString",
        "Value": "Parameter from Moto",
        "Version": 1,
        "LastModifiedDate": "2023-05-06T23:18:36.459000+09:00",
        "ARN": "arn:aws:ssm:us-east-1:123456789012:parameter/word",
        "DataType": "text"
    }
}

OKですね。

AWS SDK for JavaScript v3からMoto(サーバーモード)へアクセスする

最後は、Moto(サーバーモード)上に構築したリソースに、AWS SDK for JavaScript v3からアクセスしてみます。

Node.jsプロジェクトを作成。言語はTypeScriptを使うことにします。

$ npm init -y
$ npm i -D typescript
$ npm i -D @types/node@v18
$ npm i -D prettier
$ npm i -D jest @types/jest
$ npm i -D esbuild esbuild-jest
$ mkdir src test

AWS SDK for JavaScript v3のうち、Amazon S3AWS Systems Managerへのクライアントモジュールを追加。

$ npm i @aws-sdk/client-s3 @aws-sdk/client-ssm

@aws-sdk/client-s3

@aws-sdk/client-ssm

依存関係。

  "devDependencies": {
    "@types/jest": "^29.5.1",
    "@types/node": "^18.16.5",
    "esbuild": "^0.17.18",
    "esbuild-jest": "^0.5.0",
    "jest": "^29.5.0",
    "prettier": "^2.8.8",
    "typescript": "^5.0.4"
  },
  "dependencies": {
    "@aws-sdk/client-s3": "^3.328.0",
    "@aws-sdk/client-ssm": "^3.328.0"
  }

scripts

  "scripts": {
    "build": "tsc --project .",
    "build:watch": "tsc --project . --watch",
    "typecheck": "tsc --project ./tsconfig.typecheck.json",
    "typecheck:watch": "tsc --project ./tsconfig.typecheck.json --watch",
    "test": "jest",
    "format": "prettier --write src test"
  },

各種設定ファイル。

tsconfig.json

{
  "compilerOptions": {
    "target": "esnext",
    "module": "commonjs",
    "moduleResolution": "node",
    "lib": ["esnext"],
    "baseUrl": "./src",
    "outDir": "dist",
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "noFallthroughCasesInSwitch": true,
    "noImplicitOverride": true,
    "noImplicitReturns": true,
    "noPropertyAccessFromIndexSignature": true,
    "esModuleInterop": true
  },
  "include": [
    "src"
  ]
}

tsconfig.typecheck.json

{
  "extends": "./tsconfig",
  "compilerOptions": {
    "baseUrl": "./",
    "noEmit": true
  },
  "include": [
    "src", "test"
  ]
}

.prettierrc.json

{
  "singleQuote": true,
  "printWidth": 120
}

jest.config.js

module.exports = {
  testEnvironment: 'node',
  transform: {
    "^.+\\.tsx?$": "esbuild-jest"
  }
};

確認は、テストコードで行います。

作成したAmazon S3バケットへのオブジェクトのput/get。

test/moto-s3-access.test.ts

import {
  GetObjectCommand,
  GetObjectCommandInput,
  PutObjectCommand,
  PutObjectCommandInput,
  S3Client,
} from '@aws-sdk/client-s3';

test('create bucket and put/get', async () => {
  const client = new S3Client({
    credentials: {
      accessKeyId: 'mock_access_key',
      secretAccessKey: 'mock_secret_key',
    },
    region: 'us-east-1',
    endpoint: 'http://localhost:5000',
    forcePathStyle: true,
  });

  const bucketName = 'my-bucket';
  const objectKey = 'test-object';

  const putObjectCommandInput: PutObjectCommandInput = {
    Bucket: bucketName,
    Key: objectKey,
    Body: 'Hello Moto Server!!',
  };

  try {
    await client.send(new PutObjectCommand(putObjectCommandInput));

    expect('unreachable code').toBe('fail');
  } catch (e) {
    expect((e as Error).message).toBe('AWS SDK error wrapper for Error [ERR_STREAM_WRITE_AFTER_END]: write after end');
  }

  const getObjectCommandInput: GetObjectCommandInput = {
    Bucket: bucketName,
    Key: objectKey,
  };

  const getObjectCommandOutput = await client.send(new GetObjectCommand(getObjectCommandInput));

  expect(await getObjectCommandOutput.Body?.transformToString('utf-8')).toBe('Hello Moto Server!!');
});

妙なコードが入っていますが…。

  try {
    await client.send(new PutObjectCommand(putObjectCommandInput));

    expect('unreachable code').toBe('fail');
  } catch (e) {
    expect((e as Error).message).toBe('AWS SDK error wrapper for Error [ERR_STREAM_WRITE_AFTER_END]: write after end');
  }

これは、PutObjectCommandを実行した時にこんなErrorがスローされて失敗するからですね…。

Error: AWS SDK error wrapper for Error [ERR_STREAM_WRITE_AFTER_END]: write after end
    at asSdkError (/path/to/node_modules/@aws-sdk/middleware-retry/dist-cjs/util.js:11:12)
    at /path/to/node_modules/@aws-sdk/middleware-retry/dist-cjs/retryMiddleware.js:35:51
    at processTicksAndRejections (node:internal/process/task_queues:95:5)
    at /path/to/node_modules/@aws-sdk/middleware-flexible-checksums/dist-cjs/flexibleChecksumsMiddleware.js:58:20
    at /path/to/node_modules/@aws-sdk/middleware-logger/dist-cjs/loggerMiddleware.js:7:26

Amazon S3互換のサービスを使うと、ハマることがあるようです。

S3 Upload API fails with ERR_STREAM_WRITE_AFTER_END · Issue #3404 · aws/aws-sdk-js · GitHub

ちなみに、LocalStackに置き換えて試すとこうはなりませんでした…。

AWS Systems Manager Parameter Store。こちらは特に問題なかったです。

test/moto-ssm-parameter-store-access.test.ts

import { GetParameterCommand, GetParameterCommandInput, SSMClient } from '@aws-sdk/client-ssm';

test('get parameter', async () => {
  const client = new SSMClient({
    credentials: {
      accessKeyId: 'mock_access_key',
      secretAccessKey: 'mock_secret_key',
    },
    region: 'us-east-1',
    endpoint: 'http://localhost:5000',
  });

  const getParameterCommandInput: GetParameterCommandInput = {
    Name: 'word',
    WithDecryption: true,
  };

  const getParameterCommandOutput = await client.send(new GetParameterCommand(getParameterCommandInput));

  expect(getParameterCommandOutput.Parameter?.Value).toBe('Parameter from Moto');
});

Amazon S3でちょっと困ったことになりましたが、今回はこれで良しとしましょう。

まとめ

Moto(サーバーモード)を使って、TerraformやNode.jsからAWSのサービスの代替としてアクセスしてみました。

この使い方だとLocalStackと変わらない気もしますが、APIのカバー範囲などを見つつ使い分けていきたいなと思います。

Sonatype Nexus 3をTerraformで操作する

これは、なにをしたくて書いたもの?

TerraformのProviderを見ていると、Sonatype Nexus用のProviderがあったのでちょっと試してみたいなと。

Nexus Provider

Nexus Provider

Sonatype NexusのTerraform Providerです。

Nexus Provider

リポジトリーの定義などをスクリプトなどでできるといいなと思っていたのですが、REST APIかGroovyスクリプトしか方法を
知りませんでした。

REST and Integration API

Terraformで定義できるのなら、宣言的に書けて良さそうですね、ということで。

今回は、Mavenリポジトリーを作成してみたいと思います。

環境

今回の環境は、こちら。

$ terraform version
Terraform v1.4.6
on linux_amd64

Sonatype Nexusは、Dockerイメージを使うことにします。

sonatype/nexus3

起動。

$ docker container run -it --rm --name nexus3 sonatype/nexus3:3.53.0

Nexusコンテナにアクセスする際のIPアドレスは、172.17.0.2とします。

Sonatype NexusMavenリポジトリーをTerraformで定義する

では、Sonatype NexusMavenリポジトリーをNexus Providerを使って、Terraformで定義してみましょう。

今回はHostedリポジトリー、Proxyリポジトリー、そしてその2つを含んだGroupリポジトリーを作成してみます。

結果はこちら。

main.tf

terraform {
  required_version = "v1.4.6"

  required_providers {
    nexus = {
      source  = "datadrivers/nexus"
      version = "1.21.2"
    }
  }
}

provider "nexus" {
  username = "admin"
  # 初期パスワードは /nexus-data/admin.password に記載
  password = "admin123"
  url      = "http://172.17.0.2:8081"
  insecure = true
}

resource "nexus_repository_maven_hosted" "my_maven_hosted_repo" {
  name   = "my-maven-hosted-repo"
  online = true

  maven {
    layout_policy       = "STRICT"
    version_policy      = "MIXED"
    content_disposition = "INLINE"
  }

  storage {
    blob_store_name                = "default"
    strict_content_type_validation = true
    write_policy                   = "ALLOW"
  }
}

resource "nexus_repository_maven_proxy" "my_maven_proxy_repo" {
  name   = "my-maven-proxy-repo"
  online = true

  proxy {
    remote_url = "https://repo1.maven.org/maven2/"
  }

  maven {
    layout_policy       = "STRICT"
    version_policy      = "RELEASE"
    content_disposition = "INLINE"
  }

  storage {
    blob_store_name                = "default"
    strict_content_type_validation = true
  }

  http_client {
    auto_block = true
    blocked = false
  }

  negative_cache {
    enabled = true
    ttl = 1440
  }
}

resource "nexus_repository_maven_group" "my_maven_group_repo" {
  name   = "my-maven-group-repo"
  online = true

  group {
    member_names = [
      nexus_repository_maven_hosted.my_maven_hosted_repo.name,
      nexus_repository_maven_proxy.my_maven_proxy_repo.name
    ]
  }

  storage {
    blob_store_name                = "default"
    strict_content_type_validation = true
  }
}

providerにSonatype Nexusへの接続情報を定義。

provider "nexus" {
  username = "admin"
  # 初期パスワードは /nexus-data/admin.password に記載
  password = "admin123"
  url      = "http://172.17.0.2:8081"
  insecure = true
}

あとは、ドキュメントを見つつリポジトリーを定義。上から、Hostedリポジトリー、Proxyリポジトリー、Groupリポジトリーです。

resource "nexus_repository_maven_hosted" "my_maven_hosted_repo" {
  name   = "my-maven-hosted-repo"
  online = true

  ## 省略
}

resource "nexus_repository_maven_proxy" "my_maven_proxy_repo" {
  name   = "my-maven-proxy-repo"
  online = true

  ## 省略
}

resource "nexus_repository_maven_group" "my_maven_group_repo" {
  name   = "my-maven-group-repo"
  online = true

  ## 省略
}

設定値が不安な時は、Sonatype NexusのWeb UIでリポジトリーを作成する時の情報を見ると良いかもですね。

terraform initして

$ terraform init

apply

$ terraform apply

リポジトリーが作成できました。

REST APIで確認する場合は、こちら。

$ curl 172.17.0.2:8081/service/rest/v1/repositories

デプロイしてみる

作成したMavenリポジトリーに、アーティファクトをデプロイして動作確認してみましょう。

サンプルを作成。

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>org.littlewings</groupId>
    <artifactId>sample-app</artifactId>
    <version>0.0.1-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    </properties>

    <repositories>
        <repository>
            <id>my-maven-group-repo</id>
            <name>My Maven Group Repository</name>
            <url>http://172.17.0.2:8081/repository/my-maven-group-repo/</url>
            <releases>
                <enabled>true</enabled>
            </releases>
            <snapshots>
                <enabled>true</enabled>
            </snapshots>
        </repository>
    </repositories>

    <pluginRepositories>
        <pluginRepository>
            <id>my-maven-group-repo</id>
            <name>My Maven Group Repository</name>
            <url>http://172.17.0.2:8081/repository/my-maven-group-repo/</url>
        </pluginRepository>
    </pluginRepositories>

    <distributionManagement>
        <repository>
            <id>my-maven-hosted-repo</id>
            <name>My Maven Hosted Repository</name>
            <url>http://172.17.0.2:8081/repository/my-maven-hosted-repo/</url>
        </repository>
    </distributionManagement>
</project>

ソースコード

src/main/java/org/littlewings/sample/Greeting.java

package org.littlewings.sample;

public class Greeting {
    public String hello(String message) {
        return String.format("Hello %s!!", message);
    }
}

settings.xml

<?xml version="1.0" encoding="UTF-8"?>
<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0"
          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
          xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0
                      https://maven.apache.org/xsd/settings-1.0.0.xsd">
    <servers>
        <server>
            <id>my-maven-hosted-repo</id>
            <username>admin</username>
            <password>admin123</password>
        </server>
    </servers>
</settings>

デプロイ。

$ mvn package deploy -s settings.xml

確認。

OKですね。

ハマったこと

最初、必須項目だけ定義していたらProxyリポジトリーの作成でハマりました。

Providerがクラッシュするんですよね…。

nexus_repository_maven_proxy.my_maven_proxy_repo: Creating...
╷
│ Error: Plugin did not respond
│
│   with nexus_repository_maven_proxy.my_maven_proxy_repo,
│   on main.tf line 37, in resource "nexus_repository_maven_proxy" "my_maven_proxy_repo":
│   37: resource "nexus_repository_maven_proxy" "my_maven_proxy_repo" {
│
│ The plugin encountered an error, and failed to respond to the plugin.(*GRPCProvider).ApplyResourceChange call. The plugin logs may contain more details.
╵

Stack trace from the terraform-provider-nexus_v1.21.2 plugin:

panic: runtime error: index out of range [0] with length 0

goroutine 40 [running]:
github.com/datadrivers/terraform-provider-nexus/internal/services/repository.getMavenProxyRepositoryFromResourceData(0xde39e0?)
        github.com/datadrivers/terraform-provider-nexus/internal/services/repository/resource_repository_maven_proxy.go:45 +0xffe
github.com/datadrivers/terraform-provider-nexus/internal/services/repository.resourceMavenProxyRepositoryCreate(0x0?, {0xb7b2e0?, 0xc0003e3140})
        github.com/datadrivers/terraform-provider-nexus/internal/services/repository/resource_repository_maven_proxy.go:168 +0x66
github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema.(*Resource).create(0xde39e0?, {0xde39e0?, 0xc0003ce330?}, 0xd?, {0xb7b2e0?, 0xc0003e3140?})
        github.com/hashicorp/terraform-plugin-sdk/v2@v2.24.0/helper/schema/resource.go:695 +0x178
github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema.(*Resource).Apply(0xc000436620, {0xde39e0, 0xc0003ce330}, 0xc0002a5a00, 0xc0003e1800, {0xb7b2e0, 0xc0003e3140})
        github.com/hashicorp/terraform-plugin-sdk/v2@v2.24.0/helper/schema/resource.go:837 +0xa7a
github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema.(*GRPCProviderServer).ApplyResourceChange(0xc0003051e8, {0xde39e0?, 0xc0003ce210?}, 0xc0004669b0)
        github.com/hashicorp/terraform-plugin-sdk/v2@v2.24.0/helper/schema/grpc_provider.go:1021 +0xe3c
github.com/hashicorp/terraform-plugin-go/tfprotov5/tf5server.(*server).ApplyResourceChange(0xc000366460, {0xde39e0?, 0xc0002f79e0?}, 0xc0003dc380)
        github.com/hashicorp/terraform-plugin-go@v0.14.0/tfprotov5/tf5server/server.go:818 +0x574
github.com/hashicorp/terraform-plugin-go/tfprotov5/internal/tfplugin5._Provider_ApplyResourceChange_Handler({0xc840a0?, 0xc000366460}, {0xde39e0, 0xc0002f79e0}, 0xc0003dc310, 0x0)
        github.com/hashicorp/terraform-plugin-go@v0.14.0/tfprotov5/internal/tfplugin5/tfplugin5_grpc.pb.go:385 +0x170
google.golang.org/grpc.(*Server).processUnaryRPC(0xc0003f2000, {0xde6540, 0xc00048a1a0}, 0xc000354ea0, 0xc000432de0, 0x129b980, 0x0)
        google.golang.org/grpc@v1.50.1/server.go:1340 +0xd13
google.golang.org/grpc.(*Server).handleStream(0xc0003f2000, {0xde6540, 0xc00048a1a0}, 0xc000354ea0, 0x0)
        google.golang.org/grpc@v1.50.1/server.go:1713 +0xa1b
google.golang.org/grpc.(*Server).serveStreams.func1.2()
        google.golang.org/grpc@v1.50.1/server.go:965 +0x98
created by google.golang.org/grpc.(*Server).serveStreams.func1
        google.golang.org/grpc@v1.50.1/server.go:963 +0x28a

Error: The terraform-provider-nexus_v1.21.2 plugin crashed!

スタックトレースからソースコードを見ると、negative_cacheは設定上はオプションで、実際には必須なようなのでこちらを加えると
動作しました。

https://github.com/datadrivers/terraform-provider-nexus/blob/v1.21.2/internal/services/repository/resource_repository_maven_proxy.go#L45

まとめ

Nexus Providerを使って、Sonatype NexusをTerraformで操作してみました。

若干ハマるところがありましたが、REST APIで頑張るよりはTerraformで扱えた方が個人的にはわかりやすいですね。

使えるところでは、使っていきましょう。