CLOVER🍀

That was when it all began.

REST Assured 6.0をWildFly 39+Arquillianで試す

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

2025年12月にREST Assuredが6.0.0になっていて、Eclipse Yasson 3をサポートするようになっていたので少し見ておこうかなと。

REST Assured 6.0とそれ以前

REST AssuredはREST APIをテストするためのライブラリーです。

REST Assured

Jakarta EEなどでのテストでは、よく見かけるライブラリーなのかなと思います。

REST Assured自体は便利なのですが、JSON-B 3.0を長らくサポートしていないという問題がありました。

Support JSON-B 3.0 (Jakarta EE 10) · Issue #1651 · rest-assured/rest-assured · GitHub

これがREST Assured 6.0になり解消されたというのが今回注目しているポイントです。

現在のREST Assuredは6.0と5.5の2系統がメンテナンスされているようですね。

それぞれのポイントはこちら。

  • 6.0
    • ReleaseNotes60 · rest-assured/rest-assured Wiki · GitHub
    • Java 17以上が必要
    • 内部的にGroovy 5.xを使用
    • Spring Framework 7.xをサポート、最小バージョンは5.3
    • Jackson 3をサポート
    • Eclipse Yasson 3をサポート、かつ最小バージョン
    • Apache Johnzon 2をサポート、かつ最小バージョン
  • 5.5
    • Java 8以上が必要
    • 内部的にGroovy 4.xを使用
    • Spring Framework 7.xをサポート(5.5.7以降)、最小バージョンは5.1
    • Jacksonのサポートは2まで
    • Eclipse Yasson 1をサポート
    • Apache Johnzon 1をサポート
    • Jakarta EE 10に対応

Jakarta EE 10に対応するにはREST Assured 6.0以降になるのかなと思ったのですが、どうやらそうではなさそうです。

どちらかというとJava 17以降を必要としていることが大きく、これで対応可能なライブラリーなどが変わった感じですね。
Eclipse Yassonなどが良い例です。

で、自分がJSON-B 3.0(Eclipse Yasson 3)への対応を気にしていたのは、Jakarta EEサーバーではJSONを扱う時に
JacksonではなくJSON-Bを使用していることが多く、JSON-B 3.0を使えなかったのはけっこう引っかかっていたからですね。

JSONのシリアライザーの優先順位は、Jacksonが最初のまま変わらないようですが。

  1. JSON using Jackson 3 (databind)
  2. JSON using Jackson 2 (Faster Jackson (databind))
  3. JSON using Jackson (databind)
  4. JSON using Gson
  5. JSON using Johnzon
  6. JSON-B using Eclipse Yasson
  7. XML using Jakarta EE
  8. XML using JAXB

Usage / Object Mapping / Serialization / Content-Type based Serialization

今回はWildFly 39を使って、REST Assured 6.0を簡単に試してみましょう。

環境

今回の環境はこちら。

$ java --version
openjdk 25.0.2 2026-01-20
OpenJDK Runtime Environment (build 25.0.2+10-Ubuntu-124.04)
OpenJDK 64-Bit Server VM (build 25.0.2+10-Ubuntu-124.04, mixed mode, sharing)


$ mvn --version
Apache Maven 3.9.14 (996c630dbc656c76214ce58821dcc58be960875b)
Maven home: $HOME/.sdkman/candidates/maven/current
Java version: 25.0.2, vendor: Ubuntu, runtime: /usr/lib/jvm/java-25-openjdk-amd64
Default locale: ja_JP, platform encoding: UTF-8
OS name: "linux", version: "6.8.0-107-generic", arch: "amd64", family: "unix"

準備

まずはテスト対象のアプリケーションを作成しましょう。

Maven依存関係など。

    <properties>
        <maven.compiler.release>25</maven.compiler.release>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    </properties>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.wildfly.bom</groupId>
                <artifactId>wildfly-ee-with-tools</artifactId>
                <version>39.0.1.Final</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <dependency>
                <groupId>org.junit</groupId>
                <artifactId>junit-bom</artifactId>
                <version>5.11.3</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <dependencies>
        <!-- Maven War Pluginにweb.xmlを省略させるため -->
        <dependency>
            <groupId>jakarta.servlet</groupId>
            <artifactId>jakarta.servlet-api</artifactId>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>jakarta.ws.rs</groupId>
            <artifactId>jakarta.ws.rs-api</artifactId>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>jakarta.enterprise</groupId>
            <artifactId>jakarta.enterprise.cdi-api</artifactId>
            <scope>provided</scope>
        </dependency>

        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.jboss.arquillian.junit5</groupId>
            <artifactId>arquillian-junit5-container</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.wildfly.arquillian</groupId>
            <artifactId>wildfly-arquillian-container-remote</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.jboss.shrinkwrap.resolver</groupId>
            <artifactId>shrinkwrap-resolver-depchain</artifactId>
            <version>3.3.5</version>
            <scope>test</scope>
            <type>pom</type>
        </dependency>

        <dependency>
            <groupId>org.assertj</groupId>
            <artifactId>assertj-core</artifactId>
            <version>3.27.7</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>io.rest-assured</groupId>
            <artifactId>rest-assured</artifactId>
            <version>6.0.0</version>
            <scope>test</scope>
        </dependency>
        <!-- wildfly-ee BOMより-->
        <dependency>
            <groupId>org.eclipse</groupId>
            <artifactId>yasson</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <finalName>ROOT</finalName>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-failsafe-plugin</artifactId>
                <version>3.5.5</version>
                <executions>
                    <execution>
                        <goals>
                            <goal>integration-test</goal>
                            <goal>verify</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
            <plugin>
                <groupId>org.wildfly.plugins</groupId>
                <artifactId>wildfly-maven-plugin</artifactId>
                <version>5.1.5.Final</version>
                <executions>
                    <execution>
                        <id>package</id>
                        <goals>
                            <goal>package</goal>
                        </goals>
                    </execution>
                    <execution>
                        <id>start-before-integration-test</id>
                        <phase>pre-integration-test</phase>
                        <goals>
                            <goal>start</goal>
                        </goals>
                    </execution>
                    <execution>
                        <id>shutdown-after-integration-test</id>
                        <phase>post-integration-test</phase>
                        <goals>
                            <goal>shutdown</goal>
                        </goals>
                    </execution>
                </executions>
                <configuration>
                    <overwrite-provisioned-server>true</overwrite-provisioned-server>
                    <discover-provisioning-info>
                        <version>39.0.1.Final</version>
                    </discover-provisioning-info>
                </configuration>
            </plugin>
        </plugins>
    </build>

依存ライブラリーのバージョンは基本的にWildFlyのBOMで解決します。テストコードではArquillianを使いますが、この時点では
JUnit 5を使うことになります(JUnit 6は次のWildFly Arquillian Adapterでの対応です)。

Arquillianの実行形態はRemoteです。

REST Assuredは単純に追加。

GettingStarted · rest-assured/rest-assured Wiki · GitHub

Eclipse YassonはこれもWildFlyのBOMで管理されているバージョンを指定します。現時点では3.0.4です。

WildFlyはWildFly Maven Pluginでプロビジョニングします。またインテグレーションテスト時に起動と停止もできるように
しておきます。

Maven Failsafe Pluginを入れておくことをお忘れなく。

Jakarta RESTful Web Servicesの有効化。

src/main/java/org/littlewings/restassured/RestApplication.java

package org.littlewings.restassured;

import jakarta.ws.rs.ApplicationPath;
import jakarta.ws.rs.core.Application;

@ApplicationPath("/")
public class RestApplication extends Application {
}

リソースクラス。

src/main/java/org/littlewings/restassured/echo/EchoResource.java

package org.littlewings.restassured.echo;

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.core.MediaType;
import java.util.Optional;

@Path("/echo")
@ApplicationScoped
public class EchoResource {
    @GET
    @Produces(MediaType.APPLICATION_JSON)
    public EchoResponse get(@QueryParam("word") String word) {
        return new EchoResponse("★★★Hello %s!!★★★".formatted(Optional.ofNullable(word).orElse("World")));
    }

    @POST
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    public EchoResponse post(EchoRequest echoRequest) {
        return new EchoResponse("★★★Hello %s!!★★★".formatted(Optional.ofNullable(echoRequest.word()).orElse("World")));
    }
}

リクエスト、レスポンスクラス。

src/main/java/org/littlewings/restassured/echo/EchoRequest.java

package org.littlewings.restassured.echo;

public record EchoRequest(
        String word
) {
}

src/main/java/org/littlewings/restassured/echo/EchoResponse.java

package org.littlewings.restassured.echo;

public record EchoResponse(
        String message
) {
}

動作確認しておきましょう。WildFlyを起動。

$ mvn wildfly:dev

確認。

$ curl localhost:8080/echo
{"message":"★★★Hello World!!★★★"}


$ curl localhost:8080/echo?word=REST+Assured
{"message":"★★★Hello REST Assured!!★★★"}


$ curl -X POST localhost:8080/echo --json '{}'
{"message":"★★★Hello World!!★★★"}


$ curl -X POST localhost:8080/echo --json '{"word": "REST Assured"}'
{"message":"★★★Hello REST Assured!!★★★"}

OKですね。1度結果を削除。

$ mvn clean

REST Assuredを使ってテストコードを書く

では、REST Assuredを使ってテストコードを書きます。

src/test/java/org/littlewings/restassured/echo/EchoResourceIT.java

package org.littlewings.restassured.echo;

import io.restassured.RestAssured;
import io.restassured.config.ObjectMapperConfig;
import io.restassured.config.RestAssuredConfig;
import io.restassured.mapper.ObjectMapperType;
import jakarta.ws.rs.ApplicationPath;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import java.io.File;
import java.net.URL;
import java.nio.file.Path;
import org.jboss.arquillian.container.test.api.Deployment;
import org.jboss.arquillian.container.test.api.RunAsClient;
import org.jboss.arquillian.junit5.container.annotation.ArquillianTest;
import org.jboss.arquillian.test.api.ArquillianResource;
import org.jboss.shrinkwrap.api.ShrinkWrap;
import org.jboss.shrinkwrap.api.spec.WebArchive;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.littlewings.restassured.RestApplication;

import static io.restassured.RestAssured.given;
import static io.restassured.RestAssured.when;
import static org.assertj.core.api.Assertions.assertThat;

@ArquillianTest
@RunAsClient
class EchoResourceIT {
    @ArquillianResource
    private URL deploymentUrl;

    private String resourcePrefix =
            RestApplication.class
                    .getAnnotation(ApplicationPath.class)
                    .value()
                    .replaceFirst("^/", "");

    @Deployment
    static WebArchive createDeployment() {
        /*
        // 依存ライブラリーが必要な場合は以下を追加
        File[] compileAndRuntimeScopeDependencyFiles =
                Maven
                        .resolver()
                        .loadPomFromFile("pom.xml")
                        .importCompileAndRuntimeDependencies()
                        .resolve()
                        .withTransitivity()
                        .asFile();
         */

        File mainResources = Path.of("src/main/resources").toFile();

        return ShrinkWrap
                .create(WebArchive.class)
                .addPackages(true, RestApplication.class.getPackage())
                .addAsResource(mainResources, "");
        // 依存ライブラリーが必要な場合は以下を追加
        // .addAsLibraries(compileAndRuntimeScopeDependencyFiles);
    }

    @BeforeEach
    void setUp() {
        RestAssured.baseURI = deploymentUrl + resourcePrefix;
        // ObjectMapperをJSON-Bで固定
        RestAssured.config = RestAssuredConfig.config().objectMapperConfig(
          ObjectMapperConfig.objectMapperConfig().defaultObjectMapperType(ObjectMapperType.JSONB)
        );
    }

    @Test
    void echoGet() {
        EchoResponse response1 =
                when()
                        .get("/echo")
                        .then()
                        .statusCode(Response.Status.OK.getStatusCode())
                        .contentType(MediaType.APPLICATION_JSON)
                        .extract()
                        .as(EchoResponse.class);

        assertThat(response1.message()).isEqualTo("★★★Hello World!!★★★");

        EchoResponse response2 =
                given()
                        .queryParam("word", "REST Assured")
                        .when()
                        .get("/echo")
                        .then()
                        .statusCode(Response.Status.OK.getStatusCode())
                        .contentType(MediaType.APPLICATION_JSON)
                        .extract()
                        .as(EchoResponse.class);

        assertThat(response2.message()).isEqualTo("★★★Hello REST Assured!!★★★");
    }

    @Test
    void echoPost() {
        EchoResponse response1 =
                given()
                        .contentType(MediaType.APPLICATION_JSON)
                        .body("{}")
                        .when()
                        .post("/echo")
                        .then()
                        .contentType(MediaType.APPLICATION_JSON)
                        .extract()
                        .as(EchoResponse.class);

        assertThat(response1.message()).isEqualTo("★★★Hello World!!★★★");

        EchoResponse response2 =
                given()
                        .contentType(MediaType.APPLICATION_JSON)
                        .body(new EchoRequest("REST Assured"))
                        .when()
                        .post("/echo")
                        .then()
                        .contentType(MediaType.APPLICATION_JSON)
                        .extract()
                        .as(EchoResponse.class);

        assertThat(response1.message()).isEqualTo("★★★Hello World!!★★★");
    }
}

雛形的にShrinkwrap Resolversを使って依存ライブラリーもデプロイするコードを書いていたのですが、今回はcompileおよび
runtimeスコープの依存ライブラリーがないのでそのままだと実行に失敗します。

    @Deployment
    static WebArchive createDeployment() {
        /*
        // 依存ライブラリーが必要な場合は以下を追加
        File[] compileAndRuntimeScopeDependencyFiles =
                Maven
                        .resolver()
                        .loadPomFromFile("pom.xml")
                        .importCompileAndRuntimeDependencies()
                        .resolve()
                        .withTransitivity()
                        .asFile();
         */

        File mainResources = Path.of("src/main/resources").toFile();

        return ShrinkWrap
                .create(WebArchive.class)
                .addPackages(true, RestApplication.class.getPackage())
                .addAsResource(mainResources, "");
        // 依存ライブラリーが必要な場合は以下を追加
        // .addAsLibraries(compileAndRuntimeScopeDependencyFiles);
    }

依存ライブラリーを含む場合は、コメントアウトしている箇所を外しましょう。

またJSONのシリアライザーの優先順位は以下に書いてあるとおりなのですが、

Usage / Object Mapping / Serialization / Content-Type based Serialization

Shrinkwrap ResolversにGsonが含まれているのでそちらが有効になってしまいます。今回はJSON-Bを使うように明示しました。

    @BeforeEach
    void setUp() {
        RestAssured.baseURI = deploymentUrl + resourcePrefix;
        // ObjectMapperをJSON-Bで固定
        RestAssured.config = RestAssuredConfig.config().objectMapperConfig(
          ObjectMapperConfig.objectMapperConfig().defaultObjectMapperType(ObjectMapperType.JSONB)
        );
    }

あとはmvn verifyでWildFlyのプロビジョニング・起動後にテストが行われ、終了後にWildFlyが停止します。

$ mvn verify

スタックトレースを見たりして確認してみましたが、サーバー側もテスト側もJSONのシリアライズ、デシリアライズには
JSON-Bが使われています。

これでやっとサーバーとテストでリクエスト、レスポンスに関するコードの共有がまともにできるようになりました…。

オマケ

mvn verifyでWildFlyまわりの処理をしてもいいのですが、毎回行うと手間です。今回は以下のようにしました。

テストとWildFlyへのデプロイは省略して、プロビジョニングだけ行ってWildFlyを起動。バックグラウンド実行になります。

$ mvn clean package -DskipTests -Dwildfly.package.deployment.skip && mvn wildfly:start

WildFly Maven Pluginの処理やWildFlyへのデプロイはスキップしてテストだけ実行。

$ mvn verify -Dwildfly.skip -Dwildfly.package.skip

これで毎回WildFlyをプロビジョニングしたり、起動・停止する時間を省略できます。なお、-Dwildfly.skipだけでよいのではと
思うかもしれませんがこのやり方だと両方必要です。

バックグラウンドで起動したままのWildFlyを停止するにはこちら。

$ mvn wildfly:shutdown

おわりに

REST Assured 6.0をWildFly 39+Arquillianで試してみました。

以前からREST AssuredでJakarta EE 10以降のJSON-Bが使えないことが気になっていたのですが、6.0になってようやく
解決しました。

これでソースコードとテストコードで同じライブラリーを使えるので、各ライブラリーごとにシリアライズ、デシリアライズの
挙動差や設定を調整しなくてよくなりますね。

GitLab Runnerのrun-singleコマンドを使って、単一のGitLab CI/CDジョブを実行する

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

GitLab Runnerにはrun-singleというコマンドがあります。

GitLab Runner commands / Run-related commands / gitlab-runner run-single

こちらを使うと、単一GitLab CI/CDジョブを実行できるGitLab Runnerを使えるようなので試してみます。

gitlab-runner run-singleコマンド

GitLab Runnerで使えるコマンドはこちらです。

GitLab Runner commands | GitLab Docs

run-singleコマンドについてはこちらです。

GitLab Runner commands / Run-related commands / gitlab-runner run-single

単一のGitLabインスタンスから単一のビルドを実行するコマンドであるとされています。

Use this supplementary command to run a single build from a single GitLab instance.

またrunコマンドとは違い、GitLabのURLやRunnerのトークンを含め、すべてのオプションをCLIのオプションや環境変数として
指定します。

Take all options either as CLI parameters or environment variables, including the GitLab URL and Runner token. For example, a single job with all parameters specified explicitly:

ちょっと変わったコマンドだなと思うので、試してみましょう。

環境

今回の環境はこちら。

GitLab。192.168.0.7で動作しているものとします。

$ sudo gitlab-rake gitlab:env:info

System information
System:         Ubuntu 24.04
Current User:   git
Using RVM:      no
Ruby Version:   3.3.10
Gem Version:    3.7.1
Bundler Version:2.7.1
Rake Version:   13.0.6
Redis Version:  7.2.11
Sidekiq Version:7.3.9
Go Version:     unknown

GitLab information
Version:        18.10.1
Revision:       6bef35b5226
Directory:      /opt/gitlab/embedded/service/gitlab-rails
DB Adapter:     PostgreSQL
DB Version:     16.11
URL:            http://192.168.0.7
HTTP Clone URL: http://192.168.0.7/some-group/some-project.git
SSH Clone URL:  git@192.168.0.7:some-group/some-project.git
Using LDAP:     no
Using Omniauth: yes
Omniauth Providers:

GitLab Shell
Version:        14.47.0
Repository storages:
- default:      unix:/var/opt/gitlab/gitaly/gitaly.socket
GitLab Shell path:              /opt/gitlab/embedded/service/gitlab-shell

Gitaly
- default Address:      unix:/var/opt/gitlab/gitaly/gitaly.socket
- default Version:      18.10.1
- default Git Version:  2.53.gc61120c

GitLab Runner。

$ gitlab-runner --version
Version:      18.10.0
Git revision: ac71f4d8
Git branch:   18-10-stable
GO version:   go1.25.7 X:cacheprog
Built:        2026-03-16T14:23:19Z
OS/Arch:      linux/amd64

環境はTerraformで構築します。

$ terraform version
Terraform v1.14.8
on linux_amd64

準備

最初にGitLabプロジェクトとGitLab Runnerのトークンの作成を行います。

main.tf

terraform {
  required_version = "1.14.8"

  required_providers {
    gitlab = {
      source  = "gitlabhq/gitlab"
      version = "18.10.0"
    }
  }
}

variable "root_access_token" {
  type      = string
  ephemeral = true
}

provider "gitlab" {
  token    = var.root_access_token
  base_url = "http://192.168.0.7/"
}

resource "gitlab_group" "sample_group" {
  name = "sample group"
  path = "sample-group"

  visibility_level = "private"
}

resource "gitlab_project" "sample_app" {
  name         = "sample-app"
  namespace_id = gitlab_group.sample_group.id

  default_branch = "main"

  visibility_level = "private"

  auto_devops_enabled = false

  only_allow_merge_if_pipeline_succeeds            = true
  only_allow_merge_if_all_discussions_are_resolved = true
}

resource "gitlab_branch_protection" "main_branch" {
  project = gitlab_project.sample_app.id
  branch  = "main"

  allow_force_push = false

  merge_access_level     = "maintainer"
  push_access_level      = "no one"
  unprotect_access_level = "maintainer"
}

resource "gitlab_group_membership" "sample_user" {
  group_id     = gitlab_group.sample_group.id
  user_id      = gitlab_user.sample_user.id
  access_level = "owner"
}

resource "gitlab_user" "sample_user" {
  name     = "sample-user"
  username = "sample-user"
  password = "P@ssw0rd"
  email    = "sample-user@example.com"
}

resource "gitlab_personal_access_token" "sample_user_token" {
  user_id = gitlab_user.sample_user.id
  name    = "sample-user-pat"

  scopes = ["api"]
}

resource "gitlab_user_runner" "instance_runner" {
  runner_type = "instance_type"

  description = "sample instance runner"
  untagged    = true
}

resource "gitlab_user_runner" "group_runner" {
  runner_type = "group_type"

  group_id = gitlab_group.sample_group.id

  description = "sample group runner"
  untagged    = true
}
resource "gitlab_user_runner" "project_runner" {
  runner_type = "project_type"

  project_id = gitlab_project.sample_app.id

  description = "sample project runner"
  untagged    = true
}

output "user_personal_access_token" {
  value     = gitlab_personal_access_token.sample_user_token.token
  sensitive = true
}

output "instance_runner_authentication_toke" {
  value     = gitlab_user_runner.instance_runner.token
  sensitive = true
}

output "group_runner_authentication_token" {
  value     = gitlab_user_runner.group_runner.token
  sensitive = true
}

output "project_runner_authentication_toke" {
  value     = gitlab_user_runner.project_runner.token
  sensitive = true
}

グループ、プロジェクト、ユーザーを作成し、ユーザーのPersona Access TokenやGitLab Runnerのトークンも作成します。

rootアカウントのPersonal Access Tokenは.auto.tfvarsで設定することにしました。

secrets.auto.tfvars

root_access_token = "glpat-xxxxx"

リソースを作成。

$ terraform init
$ terraform apply

GitLab Runnerおよびトークンの組み合わせは全種類作っていますが、今回はグループを使うことにします。

$ terraform output group_runner_authentication_token
"glrt-xxxx"

またインストール済みのGitLab Runnerは停止しておきます。

$ sudo gitlab-runner stop

設定ファイルも削除。

$ sudo rm /etc/gitlab-runner/config.toml

これで準備は完了です。

GitLab Runnerをrun-singleコマンドで動かしてみる

では、GitLab Runnerをrun-singleコマンドで動かしてみましょう。

GitLab Runner commands / Run-related commands / gitlab-runner run-single

実行前にヘルプを見てみます。

runコマンドから。

$ gitlab-runner run --help
Runtime platform                                    arch=amd64 os=linux pid=9698 revision=ac71f4d8 version=18.10.0
NAME:
   gitlab-runner run - run multi runner service

USAGE:
   gitlab-runner run [command options] [arguments...]

OPTIONS:
   --listen-address value               Metrics / pprof server listening address [$LISTEN_ADDRESS]
   -c value, --config value             Config file (default: "/home/vagrant/.gitlab-runner/config.toml") [$CONFIG_FILE]
   -n value, --service value            Use different names for different services (default: "gitlab-runner")
   -d value, --working-directory value  Specify custom working directory
   -u value, --user value               Use specific user to execute shell scripts
   --syslog                             Log to system service logger [$LOG_SYSLOG]

run-single。runコマンドとはまったく異なる数のオプションが表示されます。

$ gitlab-runner run-single --help
Runtime platform                                    arch=amd64 os=linux pid=9707 revision=ac71f4d8 version=18.10.0
NAME:
   gitlab-runner run-single - start single runner

USAGE:
   gitlab-runner run-single [command options] [arguments...]

OPTIONS:
   --name value, --description value                                                          Runner name [$RUNNER_NAME]
   --limit value                                                                              Maximum number of builds processed by this runner (default: "0") [$RUNNER_LIMIT]
   --output-limit value                                                                       Maximum build trace size in kilobytes (default: "0") [$RUNNER_OUTPUT_LIMIT]
   --request-concurrency value                                                                Maximum concurrency for job requests (default: "0") [$RUNNER_REQUEST_CONCURRENCY]
   --strict-check-interval value                                                              When you set StrictCheckInterval to true, the runner disables the faster-than-check_interval re-polling loop that occurs when a runner receives a job. Instead, the runner waits <check_interval> seconds before it polls again, even if additional jobs are available. [$RUNNER_STRICT_CHECK_INTERVAL]
   --unhealthy-requests-limit value                                                           The number of unhealthy responses to new job requests after which a runner worker is turned off. (default: "0") [$RUNNER_UNHEALTHY_REQUESTS_LIMIT]
   --unhealthy-interval value                                                                 Duration that the runner worker is turned off after it exceeds the unhealthy requests limit. Supports syntax like '3600s' and '1h30min'.
   --job-status-final-update-retry-limit value                                                The maximum number of times GitLab Runner can retry to push the final job status to the GitLab instance. (default: "0") [$RUNNER_job_status_final_update_retry_limit]
   -u value, --url value                                                                      GitLab instance URL [$CI_SERVER_URL]
   -t value, --token value                                                                    Runner token [$CI_SERVER_TOKEN]
   --tls-ca-file value                                                                        File containing the certificates to verify the peer when using HTTPS [$CI_SERVER_TLS_CA_FILE]
   --tls-cert-file value                                                                      File containing certificate for TLS client auth when using HTTPS [$CI_SERVER_TLS_CERT_FILE]
   --tls-key-file value                                                                       File containing private key for TLS client auth when using HTTPS [$CI_SERVER_TLS_KEY_FILE]
   --executor value                                                                           Select executor, eg. shell, docker, etc. [$RUNNER_EXECUTOR]
   --builds-dir value                                                                         Directory where builds are stored [$RUNNER_BUILDS_DIR]
   --cache-dir value                                                                          Directory where build cache is stored [$RUNNER_CACHE_DIR]
   --clone-url value                                                                          Overwrite the default URL used to clone or fetch the git ref [$CLONE_URL]
   --env value                                                                                Custom environment variables injected to build environment [$RUNNER_ENV]
   --proxy-exec value                                                                         (Experimental) Proxy execution via helper binary [$RUNNER_PROXY_EXEC]
   --pre-get-sources-script value                                                             Runner-specific commands to be executed on the runner before updating the Git repository and updating submodules. [$RUNNER_PRE_GET_SOURCES_SCRIPT]
   --post-get-sources-script value                                                            Runner-specific commands to be executed on the runner after updating the Git repository and updating submodules. [$RUNNER_POST_GET_SOURCES_SCRIPT]
   --pre-build-script value                                                                   Runner-specific command script executed just before build executes [$RUNNER_PRE_BUILD_SCRIPT]
   --post-build-script value                                                                  Runner-specific command script executed just after build executes [$RUNNER_POST_BUILD_SCRIPT]

〜省略〜

やっぱり単発実行のコマンドなんですね。

トークンは変数として設定済みとします。

$ RUNNER_TOKEN=...

GitLabプロジェクトにはこんな.gitlab-ci.ymlを用意。

.gitlab-ci.yml

stages:
  - run

greeting:
  stage: run
  script: |
    echo 'Hello World'

.gitlab-ci.ymlを登録すると、パイプラインがPendingになります。

ここでGitLab Runnerをrun-singleコマンドで実行。事前のregisterコマンドの実行は不要です。

$ sudo gitlab-runner run-single \
  --url "http://192.168.0.7/" \
  --token "$RUNNER_TOKEN" \
  --executor "docker" \
  --docker-image ubuntu:24.04 \
  --docker-privileged \
  --docker-volumes "/certs/client" \
  --description "sample group runner"

そのままフォアグラウンドでGitLab Runnerが実行され、こんな感じでGitLab CI/CDジョブが実行されます。

Runtime platform                                    arch=amd64 os=linux pid=1515 revision=ac71f4d8 version=18.10.0
Starting runner for http://192.168.0.7/ with token Q4xdCJ8Z7 ...
Checking for jobs... received                       correlation_id=01KNESW8VQKJB372N8S0VTYSHW job=1 repo_url=http://192.168.0.7/sample-group/sample-app.git runner=Q4xdCJ8Z7 runner_name=sample group runner
Updating job...                                     bytesize=0 checksum= correlation_id=46408a0238f844a18d327e7fdae92774 job=1 runner=Q4xdCJ8Z7 runner_name=sample group runner
Submitting job to coordinator...ok                  bytesize=0 checksum= code=200 correlation_id=01KNESW9HY9G31W1E82HB607DK job=1 job-status=running runner=Q4xdCJ8Z7 runner_name=sample group runner update-interval=0s
Using default image                                 executor=docker gitlab_user_id=6 image=ubuntu:24.04 job=1 namespace_id=13 organization_id=1 project=6 project_full_path=sample-group/sample-app root_namespace_id=13 runner=Q4xdCJ8Z7 runner_name=sample group runner
Appending trace to coordinator...ok                 code=202 correlation_id=01KNESWCGJFR6PBXMXWVDGW0ZW job=1 job-log=0-509 job-status=running runner=Q4xdCJ8Z7 runner_name=sample group runner sent-log=0-508 status=202 Accepted update-interval=1m0s
Using default image                                 executor=docker gitlab_user_id=6 image=ubuntu:24.04 job=1 namespace_id=13 organization_id=1 project=6 project_full_path=sample-group/sample-app root_namespace_id=13 runner=Q4xdCJ8Z7 runner_name=sample group runner
Using default image                                 executor=docker gitlab_user_id=6 image=ubuntu:24.04 job=1 namespace_id=13 organization_id=1 project=6 project_full_path=sample-group/sample-app root_namespace_id=13 runner=Q4xdCJ8Z7 runner_name=sample group runner
Job succeeded                                       container_name=runner-q4xdcj8z7-project-6-concurrent-0-232d2ca0bd10075c-build duration_s=27.537055253 gitlab_user_id=6 job=1 job-status=success name=ubuntu namespace_id=13 organization_id=1 project=6 project_full_path=sample-group/sample-app root_namespace_id=13 runner=Q4xdCJ8Z7 runner_name=sample group runner
Appending trace to coordinator...ok                 code=202 correlation_id=01KNESX4EF32EM66WQ3RW6N2Y2 job=1 job-log=0-3511 job-status=running runner=Q4xdCJ8Z7 runner_name=sample group runner sent-log=509-3510 status=202 Accepted update-interval=3s
Updating job...                                     bytesize=3511 checksum=crc32:e2a49d15 correlation_id=0fe9f05a57b54a2caaee1b54a1dcc748 job=1 runner=Q4xdCJ8Z7 runner_name=sample group runner
Submitting job to coordinator...ok                  bytesize=3511 checksum=crc32:e2a49d15 code=200 correlation_id=01KNESX4K1WM878NS7X7XEXNSS job=1 job-status=success runner=Q4xdCJ8Z7 runner_name=sample group runner update-interval=0s

そしてそのまま待機します。終了したかったらCtrl-cですね。

run-singleという名前の割に、どうして終了しないかはドキュメントに書かれています。

You can use the --max-builds option to control how many builds the runner executes before exiting. The default of 0 means that the runner has no build limit and jobs run forever.

GitLab Runner commands / Run-related commands / gitlab-runner run-single

デフォルトでは実行するビルド数に制限がなく、上限を設定するには--max-buildsオプションを使います。

では、--max-buildsを1にして登録してみましょう。

$ sudo gitlab-runner run-single \
  --url "http://192.168.0.7/" \
  --token "$RUNNER_TOKEN" \
  --executor "docker" \
  --max-builds 1 \
  --docker-image ubuntu:24.04 \
  --docker-privileged \
  --docker-volumes "/certs/client" \
  --description "sample group runner"

すると、先ほどの.gitlab-ci.ymlの内容だと1回ジョブを実行するとすぐに終了します。

This runner has processed its build limit, so now exiting

ところで、こういう使い方だと--max-buildsオプションの単位が気になりますね。

ビルド=ジョブと捉えてよさそうです。パイプラインではないことに注意です。

たとえば.gitlab-ci.ymlを以下のように変更します。ジョブが3つありますね。

.gitlab-ci.yml

stages:
  - stage1
  - stage2
  - stage3

foo:
  stage: stage1
  script: |
    echo 'foo'

bar:
  stage: stage2
  script: |
    echo 'bar'

fuga:
  stage: stage3
  script: |
    echo 'fuga'

このパイプラインに対して、--max-buildsを1にして実行すると最初のジョブ(foo)を実行したところでGitLab Runnerは
終了します。

$ sudo gitlab-runner run-single \
  --url "http://192.168.0.7/" \
  --token "$RUNNER_TOKEN" \
  --executor "docker" \
  --max-builds 1 \
  --docker-image ubuntu:24.04 \
  --docker-privileged \
  --docker-volumes "/certs/client" \
  --description "sample group runner"

このパイプラインのジョブをすべて実行するには、この設定のままだと3回GitLab Runnerを実行する必要があります。

もしくは以下のように--max-buildsを3にして実行します。

$ sudo gitlab-runner run-single \
  --url "http://192.168.0.7/" \
  --token "$RUNNER_TOKEN" \
  --executor "docker" \
  --max-builds 3 \
  --docker-image ubuntu:24.04 \
  --docker-privileged \
  --docker-volumes "/certs/client" \
  --description "sample group runner"

すると、パイプラインに含まれるすべてのジョブ(foo、bar、fuga)を実行し終えたところでGitLab Runnerも終了します。

ちょっとわかりにくいですね…パイプラインを実行の単位にして欲しいところではありました…。とはいえ、難しいところ
なのでしょうね。

使い方はおよそわかりました。

おわりに

GitLab Runnerのrun-singleコマンドを使って、単一のGitLab CI/CDジョブを実行してみました。
もっとも、デフォルトは単一ではなく無制限なのですが。

個人的にはパイプライン単位で扱えると嬉しいなと思ったのですが、単位がジョブなのでちょっと扱い方が難しいですね。
自分のジョブを実行できる専用のGitLab Runnerとして使えると便利かなと思ったのですが、工夫すればなんとかなるもの
でしょうか…?

今回は使い方がわかったのでよしとしましょう。