CLOVER🍀

That was when it all began.

GitLab CI/CDのDocker Executorで、ジョブを実行するシェルを確認する

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

GitLab CI/CDのRunnerとしてDocker Executorをよく使うのですが、ジョブのscriptがどのシェルで動作しているのかを知りたいと思いまして。

ドキュメントと実際の動作の2つを確認してみたいと思います。

GitLab CI/CDのDocker Executorで実行されるシェルはなにか?

たとえばGitLab CI/CDのチュートリアルにあるこういう.gitlab-ci.ymlがあったとして、ここに書かれているscript要素がどのシェルで実行されて
いるのかが気になる、という話です。

build-job:
  stage: build
  script:
    - echo "Hello, $GITLAB_USER_LOGIN!"

test-job1:
  stage: test
  script:
    - echo "This job tests something"

test-job2:
  stage: test
  script:
    - echo "This job tests something, but takes more time than test-job1."
    - echo "After the echo commands complete, it runs the sleep command for 20 seconds"
    - echo "which simulates a test that runs 20 seconds longer than test-job1"
    - sleep 20

deploy-prod:
  stage: deploy
  script:
    - echo "This job deploys something from the $CI_COMMIT_BRANCH branch."
  environment: production

Tutorial: Create and run your first GitLab CI/CD pipeline / Create a .gitlab-ci.yml file

答えとしてはここに書かれています。

Docker executor / Configure a Docker ENTRYPOINT

ジョブのスクリプトを実行するコンテナを起動して、shまたはbashCOMMANDとして渡すようです。

It passes sh or bash as COMMAND to start a container that runs the job script.

つまり、以下と同等だと書かれています。

docker run <image> sh -c "echo 'It works!'" # or bash

GitLab Runnerのソースコードだと、こちらが該当しそうです。

   execConfig := container.ExecOptions{
        Tty:          false,
        AttachStderr: true,
        AttachStdout: true,
        Cmd:          append([]string{"sh", "-c"}, script...),
    }

https://gitlab.com/gitlab-org/gitlab-runner/-/blob/v18.3.0/executors/docker/docker.go?ref_type=tags#L1533-1538

ちなみに、デフォルトでは指定したDockerイメージのENTRYPOINTをオーバーライドしないようです。

By default, the Docker executor doesn’t override the ENTRYPOINT of a Docker image.

ENTRYPOINTが指定してあるDockerイメージで、scriptを実行させるのが難しい場合は以下のようにENTRYPOINTをオーバーライドする
必要があります。

image:
  name: my-image
  entrypoint: [""]

というわけで、どうやらscriptはshコマンドで実行されるようですが、実際どうなのか確認してみましょう。

環境

今回の環境はこちら。

$ sudo gitlab-rake gitlab:env:info

System information
System:         Ubuntu 24.04
Current User:   git
Using RVM:      no
Ruby Version:   3.2.8
Gem Version:    3.6.9
Bundler Version:2.7.1
Rake Version:   13.0.6
Redis Version:  7.2.10
Sidekiq Version:7.3.9
Go Version:     unknown

GitLab information
Version:        18.3.1
Revision:       bccd1993b5d
Directory:      /opt/gitlab/embedded/service/gitlab-rails
DB Adapter:     PostgreSQL
DB Version:     16.8
URL:            http://192.168.0.6
HTTP Clone URL: http://192.168.0.6/some-group/some-project.git
SSH Clone URL:  git@192.168.0.6:some-group/some-project.git
Using LDAP:     no
Using Omniauth: yes
Omniauth Providers:

GitLab Shell
Version:        14.44.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.3.1
- default Git Version:  2.50.1.gl1

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

環境はTerraformで構築します。

$ terraform version
Terraform v1.13.1
on linux_amd64

準備

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

terraform.tf

terraform {
  required_version = "1.13.1"

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

main.tf

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

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

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_user_runner" "group_runner" {
  runner_type = "group_type"

  group_id = gitlab_group.sample_group.id

  description = "sample group runner"
  untagged    = true
}

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

GitLabの操作に必要なアクセストークンは、環境変数で定義。

$ export TF_VAR_root_access_token=...

リソースを作成。

$ terraform init
$ terraform apply

GitLab Runnerのトークンを確認して

$ terraform output runner_authentication_token
"glrt-xxxxxxxxxx"

GitLab RunenrをGitLabに登録します。

$ RUNNER_TOKEN=...
$ sudo gitlab-runner register \
  --non-interactive \
  --url "http://192.168.0.6/" \
  --token "$RUNNER_TOKEN" \
  --executor "docker" \
  --docker-image ubuntu:24.04 \
  --docker-privileged \
  --docker-volumes "/certs/client" \
  --description "sample group runner"

設定ファイルはこちら。

/etc/gitlab-runner/config.toml

concurrent = 1
check_interval = 0
shutdown_timeout = 0

[session_server]
  session_timeout = 1800

[[runners]]
  name = "sample group runner"
  url = "http://192.168.0.6/"
  id = 10
  token = "glrt-xxxxxxxxxx"
  token_obtained_at = 2025-08-31T09:59:30Z
  token_expires_at = 0001-01-01T00:00:00Z
  executor = "docker"
  [runners.cache]
    MaxUploadedArchiveSize = 0
    [runners.cache.s3]
    [runners.cache.gcs]
    [runners.cache.azure]
  [runners.docker]
    tls_verify = false
    image = "ubuntu:24.04"
    privileged = false
    disable_entrypoint_overwrite = false
    oom_kill_disable = false
    disable_cache = false
    volumes = ["/cache"]
    shm_size = 0
    network_mtu = 0

これで準備はできました。

scriptがどのシェルで実行されているか確認してみる

それでは、scriptに定義したスクリプトがどのシェルで実行されているのか確認してみましょう。

こんな.gitlab-ci.ymlを用意。

.gitlab-ci.yml

stages:
  - run

ubuntu:
  stage: run
  image: ubuntu:24.04
  script:
    - |
      echo -n 'script execution command: '
      cat /proc/$$/cmdline | tr '\0' ' '
      echo
      ls -l $(cat /proc/$$/cmdline | tr '\0' ' ')

debian:
  stage: run
  image: debian:bookworm
  script:
    - |
      echo -n 'script execution command: '
      cat /proc/$$/cmdline | tr '\0' ' '
      echo
      ls -l $(cat /proc/$$/cmdline | tr '\0' ' ')

rocky:
  stage: run
  image: rockylinux:9.3
  script:
    - |
      echo -n 'script execution command: '
      cat /proc/$$/cmdline | tr '\0' ' '
      echo
      ls -l $(cat /proc/$$/cmdline | tr '\0' ' ')

alpine:
  stage: run
  image: alpine:3.22
  script:
    - |
      echo -n 'script execution command: '
      cat /proc/$$/cmdline | tr '\0' ' '
      echo
      ls -l $(cat /proc/$$/cmdline | tr '\0' ' ')

Ubuntu LinuxDebian、Rocky Linux、Alpine Linuxそれぞれのイメージで、どのシェルで実行されているのか確認します。

結果はこちら。

# Ubuntu Linux
script execution command: /usr/bin/bash 
-rwxr-xr-x 1 root root 1446024 Mar 31  2024 /usr/bin/bash


# Debian
script execution command: /usr/bin/bash 
-rwxr-xr-x 1 root root 1265648 Apr 18 22:47 /usr/bin/bash


# Rocky Linux
script execution command: /usr/bin/bash 
-rwxr-xr-x 1 root root 1388928 Jan 23  2023 /usr/bin/bash


# Alpine Linux
script execution command: /bin/sh 
lrwxrwxrwx    1 root     root            12 Jul 15 10:42 /bin/sh -> /bin/busybox

Alpine Linuxは予想していたとおり/bin/shなのですが、それ以外は軒並みbashですね。

ちょっと予想外です。

GitLab Runnerのソースコードを見ると、このあたりが気になりますね。

   BashDetectShellScript = `if [ -x /usr/local/bin/bash ]; then
  exec /usr/local/bin/bash $@
elif [ -x /usr/bin/bash ]; then
  exec /usr/bin/bash $@
elif [ -x /bin/bash ]; then
  exec /bin/bash $@
elif [ -x /usr/local/bin/sh ]; then
  exec /usr/local/bin/sh $@
elif [ -x /usr/bin/sh ]; then
  exec /usr/bin/sh $@
elif [ -x /bin/sh ]; then
  exec /bin/sh $@
elif [ -x /busybox/sh ]; then
  exec /busybox/sh $@
else
  echo shell not found
  exit 1
fi

https://gitlab.com/gitlab-org/gitlab-runner/-/blob/v18.3.0/shells/bash.go?ref_type=tags#L20-37

どうもヘルパーというものの機能のような気がします。これはExecutorがDocker、Docker+Machine、Kubernetesの時に使われるようです。

Advanced configuration / Helper image

実際、GitLab Runnerにはgitlab-runner-helperというイメージが入ってきますしね。

$ docker image ls
REPOSITORY                                                          TAG              IMAGE ID       CREATED          SIZE
registry.gitlab.com/gitlab-org/gitlab-runner/gitlab-runner-helper   x86_64-v18.3.0   6d44a66eea97   36 minutes ago   92.7MB
debian                                                              bookworm         3d2058890b96   2 weeks ago      117MB
ubuntu                                                              24.04            e0f16e6366fe   4 weeks ago      78.1MB
alpine                                                              3.22             9234e8fb04c4   6 weeks ago      8.31MB
rockylinux                                                          9.3              9cc24f05f309   21 months ago    176MB

このあたりでしょうか。

       Shell: common.ShellScriptInfo{
            Shell:         "bash",
            Type:          common.NormalShell,
            RunnerCommand: "/usr/bin/gitlab-runner-helper",
        },

https://gitlab.com/gitlab-org/gitlab-runner/-/blob/v18.3.0/executors/docker/docker_command.go?ref_type=tags#L371-375

      e.ExecutorOptions.Shell.Shell = "bash"
        e.ExecutorOptions.Shell.RunnerCommand = "/usr/bin/gitlab-runner-helper"

https://gitlab.com/gitlab-org/gitlab-runner/-/blob/v18.3.0/executors/docker/docker.go?ref_type=tags#L1402-1403

ちょっとこのスクリプトがどう使われているかどうかは追いきれないので、少し試してみましょう。

こんなDockerfileを追加。

Dockerfile

FROM ubuntu:24.04

RUN mv /usr/bin/bash /usr/local/bin/sh

/usr/bin/bashを移動します。ちなみにこれで/bin/bashもなくなります。

.gitlab-ci.ymlをこう変更。

.gitlab-ci.yml

stages:
  - build
  - run

my-image-build:
  stage: build
  services:
    - name: docker:28.3.3-dind
      command: ["--insecure-registry=192.168.0.6:5050"]
  image: docker:28.3.3
  script:
    - |
      echo $CI_REGISTRY
      echo "$CI_REGISTRY_PASSWORD" | docker login $CI_REGISTRY -u $CI_REGISTRY_USER --password-stdin
      docker image build -t $CI_REGISTRY_IMAGE/my-ubuntu:24.04 .
      docker image push $CI_REGISTRY_IMAGE/my-ubuntu:24.04

ubuntu:
  stage: run
  image: $CI_REGISTRY_IMAGE/my-ubuntu:24.04
  script:
    - |
      echo -n 'script execution command: '
      cat /proc/$$/cmdline | tr '\0' ' '
      echo
      ls -l $(cat /proc/$$/cmdline | tr '\0' ' ')

debian:
  stage: run
  image: debian:bookworm
  script:
    - |
      echo -n 'script execution command: '
      cat /proc/$$/cmdline | tr '\0' ' '
      echo
      ls -l $(cat /proc/$$/cmdline | tr '\0' ' ')

rocky:
  stage: run
  image: rockylinux:9.3
  script:
    - |
      echo -n 'script execution command: '
      cat /proc/$$/cmdline | tr '\0' ' '
      echo
      ls -l $(cat /proc/$$/cmdline | tr '\0' ' ')

alpine:
  stage: run
  image: alpine:3.22
  script:
    - |
      echo -n 'script execution command: '
      cat /proc/$$/cmdline | tr '\0' ' '
      echo
      ls -l $(cat /proc/$$/cmdline | tr '\0' ' ')

自分でビルドしたDockerイメージを使うようにします。

ubuntu:
  stage: run
  image: $CI_REGISTRY_IMAGE/my-ubuntu:24.04
  script:
    - |
      echo -n 'script execution command: '
      cat /proc/$$/cmdline | tr '\0' ' '
      echo
      ls -l $(cat /proc/$$/cmdline | tr '\0' ' ')

結果はこうなりました。

script execution command: /usr/local/bin/sh 
-rwxr-xr-x 1 root root 1446024 Mar 31  2024 /usr/local/bin/sh

ということは、やっぱりこちらのスクリプトが使われていそうですね。

   BashDetectShellScript = `if [ -x /usr/local/bin/bash ]; then
  exec /usr/local/bin/bash $@
elif [ -x /usr/bin/bash ]; then
  exec /usr/bin/bash $@
elif [ -x /bin/bash ]; then
  exec /bin/bash $@
elif [ -x /usr/local/bin/sh ]; then
  exec /usr/local/bin/sh $@
elif [ -x /usr/bin/sh ]; then
  exec /usr/bin/sh $@
elif [ -x /bin/sh ]; then
  exec /bin/sh $@
elif [ -x /busybox/sh ]; then
  exec /busybox/sh $@
else
  echo shell not found
  exit 1
fi

少なくとも、bashが利用できる環境ではbashを使おうとすることがわかりました。

おわりに

GitLab CI/CDのDocker Executorで、ジョブを実行するシェルを確認してみました。

ドキュメントを読んでいるとshまたはbashということでしたが、意外とbash優先のような気がしますね。

ジョブを実行するスクリプトがどうやってシェルを選んでいるか、もう少し追ってみたかったりもするのですが今回の簡単な確認までにします。