これは、なにをしたくて書いたもの?
GitLab CI/CDを使っていると、.gitlab-ci.ymlを変更してコミットしてpushして確認…というのが面倒になってきます。
そうなるとパイプラインをローカルで動かしたいと思い、いろいろと調べてみました。
GitLab Runnerのexecとその廃止
今回のお題そのものの解決方法が、GitLab Runner自身から出ていました。今はもうありませんけど。
GitLab Runnerのexecコマンドがこの機能だったようです。
- Announced in GitLab 15.7 (Dec 2022)
- Removal in GitLab 17.0 (May 2024) (breaking change)
- To discuss this change or learn more, see the deprecation issue.
Deprecations and removals by version / The gitlab-runner exec command is deprecated
GitLab 17.0 · GitLab.org / GitLab · GitLab
GitLab Runner 17.0.0のCHANGELOGを見ると、確かに削除されています。
Remove gitlab-runner exec command !4740
関連するissueやMerge Requestを見ると、GitLab CI/CDの進化にこの機能を追従させることが難しくなったからのようです。
The gitlab-runner exec feature was initially developed to provide the ability to validate a GitLab CI pipeline on a local system without needing to commit the updates to a GitLab instance. However, with the continued evolution of GitLab CI, replicating all GitLab CI features into gitlab-runner exec was no longer viable.
With this context, we have determined that it is no longer tenable to continue to incur the technical debt of this legacy feature. As a result, we plan to include a new deprecation notice in the 15.8 release post and fully remove gitlab-runner exec from the runner code base in the 16.0 release.
Remove `gitlab-runner exec` command (!4740) · Merge requests · GitLab.org / gitlab-runner · GitLab
現在はどうしているかというと、Pipeline editor上でのlintやシミュレーションを行うことを勧めています。
サードパーティー製の選択肢
GitLab本体からGitLab CI/CDをローカルで動作する方法はもう出ることがないと思うので、他の選択肢はあるのかな?と思って調べてみると
このあたりが見つかりました。
gitlab-ci-local。
GitHub - firecow/gitlab-ci-local: Tired of pushing to test your .gitlab-ci.yml?
gcil。
RadianDevCore / Tools / gcil · GitLab
今回はgitlab-ci-localを試してみたいと思います。
gitlab-ci-local
gitlab-ci-localは、Shell ExecutorやDocker ExecutorとしてローカルでGitLabのパイプラインを動かせるソフトウェアです。
Run gitlab pipelines locally as shell executor or docker executor.
GitHub - firecow/gitlab-ci-local: Tired of pushing to test your .gitlab-ci.yml?
最初の1歩的な使い方が書かれていませんが、カレントディレクトリーに.gitlab-ci.ymlがある状態でジョブを指定して実行するようです。
当たり前(?)といえば当たり前ですが変数などをすべて解決できるわけでもないので、解決できないvariablesは、ファイルで用意するようです。
- gitlab-ci-local / Quirks / Home file variables
- gitlab-ci-local / Quirks / Remote file variables
- gitlab-ci-local / Quirks / Project file variables
ちなみに、GitLabへアクセスできない状態でも動作します。
環境
今回の環境はこちら。Ubuntu Linux 24.04 LTSです。
$ lsb_release -a No LSB modules are available. Distributor ID: Ubuntu Description: Ubuntu 24.04.3 LTS Release: 24.04 Codename: noble $ uname -srvmpio Linux 6.8.0-79-generic #79-Ubuntu SMP PREEMPT_DYNAMIC Tue Aug 12 14:42:46 UTC 2025 x86_64 x86_64 x86_64 GNU/Linux
GitLab。
$ 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.3で動作しているものとします。
環境はTerraformで作成します。
$ terraform version Terraform v1.13.1 on linux_amd64
準備
最初にGitLabに関するリソースを作成します。
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.3/" } 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 Runnerのトークンも作成していますが、今回のエントリー内では特に使いません。
GitLabの操作に必要なアクセストークンは、環境変数で定義。
$ export TF_VAR_root_access_token=...
リソースを作成。
$ terraform init $ terraform apply
これでGitLabリソースの準備は完了です。
それから、プロジェクトも必要ですね。sample-appというGitLabプロジェクトを作成しておきましたが内容はREADME.mdと.gitlab-ci.ymlのみに
しておきます。
.gitlab-ci.ymlは使う時になったら載せましょう。
gitlab-ci-localを試してみる
それでは、gitlab-ci-localを試してみましょう。
まずはインストールから。今回はaptでインストールすることにします。
Installation / Linux based on Debian
aptリポジトリーを追加して
$ sudo wget -O /etc/apt/sources.list.d/gitlab-ci-local.sources https://gitlab-ci-local-ppa.firecow.dk/gitlab-ci-local.sources $ sudo apt update
パッケージが認識できるようになったことを確認。
$ apt show gitlab-ci-local Package: gitlab-ci-local Version: 4.61.1 Maintainer: Mads Jon Nielsen <madsjon@gmail.com> Installed-Size: unknown Depends: rsync Homepage: https://github.com/firecow/gitlab-ci-local Author: Mads Jon Nielsen <madsjon@gmail.com> Name: gitlab-ci-local Website: https://github.com/firecow/gitlab-ci-local Download-Size: 19.7 MB APT-Sources: https://gitlab-ci-local-ppa.firecow.dk ./ Packages Description: Tired of pushing to test your .gitlab-ci.yml? N: There are 10 additional records. Please use the '-a' switch to see them.
インストール。
$ sudo apt install gitlab-ci-local
インストールされるファイルは実行バイナリーのみのようですね。
$ dpkg -L gitlab-ci-local /. /usr /usr/local /usr/local/bin /usr/local/bin/gitlab-ci-local
バージョン。
$ gitlab-ci-local --version 4.61.1
ヘルプ。
$ gitlab-ci-local --help
Find more information at https://github.com/firecow/gitlab-ci-local.
Note: To negate an option use '--no-(option)'.
Positionals:
job Jobname's to execute [string]
Options:
--help Show help [boolean]
--version Show version number [boolean]
--manual One or more manual jobs to run during a pipeline [array]
--list List job information, when:never excluded [boolean]
--list-all List job information, when:never included [boolean]
--list-json List job information in json format, when:never included [boolean]
--list-csv List job information in csv format, when:never excluded [boolean]
--list-csv-all List job information in csv format, when:never included [boolean]
--preview Print YML with defaults, includes, extends and reference's expanded [boolean]
--cwd Path to a current working directory [string]
--variables-file Path to the project file variables [string] [default: ".gitlab-ci-local-variables.yml"]
--completion Generate tab completion script [boolean]
--evaluate-rule-changes Whether to evaluate rule:changes. If set to false, rules:changes will always evaluate to true [boolean] [default: true]
--needs Run needed jobs, when executing specific jobs [boolean]
--only-needs Run needed jobs, except the specified jobs themselves [boolean]
--stage Run all jobs in a specific stage [string]
--variable Add variable to all executed jobs (--variable HELLO=world) [array]
--unset-variable Unsets a variable (--unset-variable HELLO) [array]
--remote-variables Fetch variables file from remote location [string]
--state-dir Location of the .gitlab-ci-local state dir, relative to cwd, eg. (symfony/.gitlab-ci-local/) [string]
--file Location of the .gitlab-ci.yml, relative to cwd, eg. (gitlab/.gitlab-ci.yml) [string]
--home Location of the HOME .gitlab-ci-local folder ($HOME/.gitlab-ci-local/variables.yml) [string]
--shell-isolation Enable artifact isolation for shell-executor jobs [boolean]
--force-shell-executor Forces all jobs to be executed using the shell executor. (Only use this option for trusted job) [boolean]
--shell-executor-no-image Whether to use shell executor when no image is specified. [boolean]
--default-image When using --shell-executor-no-image=false which image to be used for the container. Defaults to docker.io/ruby:3.1 if not set. [string]
--helper-image When using --shell-executor-no-image=false which image to be used for the utils container. Defaults to
docker.io/firecow/gitlab-ci-local-util:latest if not set. [string]
--mount-cache Enable docker mount based caching [boolean]
--umask Sets docker user to 0:0 [boolean]
--userns Set docker executor userns option [string]
--privileged Set docker executor to privileged mode [boolean]
--ulimit Set docker executor ulimit [number]
--network Add networks to docker executor [array]
--volume Add volumes to docker executor [array]
--extra-host Add extra docker host entries [array]
--pull-policy Set image pull-policy (always or if-not-present) [string]
--fetch-includes Fetch all external includes one more time [boolean]
--maximum-includes The maximum number of includes [number]
--artifacts-to-source Copy the generated artifacts into cwd [boolean]
--cleanup Remove docker resources after they've been used [boolean]
--quiet Suppress all job output [boolean]
--timestamps Show timestamps and job duration in the logs [boolean]
--max-job-name-padding Maximum padding for job name (use <= 0 for no padding) [number]
--json-schema-validation Whether to enable json schema validation [boolean]
--ignore-schema-paths The json schema paths that will be ignored [array] [default: []]
--concurrency Limit the number of jobs that run simultaneously [number]
--container-executable Command to start the container engine (docker or podman) [string]
--container-mac-address Container MAC address (e.g., aa:bb:cc:dd:ee:ff) [string]
--container-emulate The name, without the architecture, of a gitlab hosted runner to emulate. See here:
https://docs.gitlab.com/ee/ci/runners/hosted_runners/linux.html#machine-types-available-for-linux---x86-64
[string] [choices: "saas-linux-small", "saas-linux-medium", "saas-linux-large", "saas-linux-xlarge", "saas-linux-2xlarge"]
--color Enables color [default: true]
それでは使ってみます。
ひとまず、.gitlab-ci.ymlがないと実行できないようです。
$ gitlab-ci-local /path/to/.gitlab-ci.yml could not be found
まずはこんなファイルを用意。2つのステージと3つのジョブを用意しました。Docker ExecutorのGitLab Runnerで動かすことを前提にしています。
.gitlab-ci.yml
stages: - build workflow: rules: # Merge Requestを対象 - if: $CI_PIPELINE_SOURCE == "merge_request_event" # デフォルトブランチを対象 - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH # Protectedブランチを対象 - if: $CI_COMMIT_REF_PROTECTED == "true" # 手動実行 - if: $CI_PIPELINE_SOURCE == "web" # それ以外は実行しない - when: never build-job1: stage: build image: ubuntu:24.04 script: - | echo "Hello World1" build-job2: stage: build image: ubuntu:24.04 script: - | echo "Hello World2" test-job: stage: build image: ubuntu:24.04 script: - | echo "Hello Test"
とりえずコマンドを実行してみると、Dockerイメージが取得できないと言われます。
$ gitlab-ci-local
Using fallback git commit data
Using fallback git user.name
Using fallback git user.email
Unable to retrieve default remote branch, falling back to `main`.
Using fallback git remote data
parsing and downloads finished in 31 ms.
json schema validated in 161 ms
job starting ubuntu:24.04 (build)
Error: Command failed with ENOENT: docker pull ubuntu:24.04
spawn docker ENOENT
at ChildProcess._handle.onexit (node:internal/child_process:285:19)
at onErrorNT (node:internal/child_process:483:16)
at process.processTicksAndRejections (node:internal/process/task_queues:90:21)
というわけで、Docker Engineを追加。
$ docker version Client: Docker Engine - Community Version: 28.3.3 API version: 1.51 Go version: go1.24.5 Git commit: 980b856 Built: Fri Jul 25 11:34:09 2025 OS/Arch: linux/amd64 Context: default Server: Docker Engine - Community Engine: Version: 28.3.3 API version: 1.51 (minimum version 1.24) Go version: go1.24.5 Git commit: bea959c Built: Fri Jul 25 11:34:09 2025 OS/Arch: linux/amd64 Experimental: false containerd: Version: 1.7.27 GitCommit: 05044ec0a9a75232cad458027ca83437aae3f4da runc: Version: 1.2.5 GitCommit: v1.2.5-0-g59923ef docker-init: Version: 0.19.0 GitCommit: de40ad0
実行。
$ gitlab-ci-local Using fallback git commit data Using fallback git user.name Using fallback git user.email Unable to retrieve default remote branch, falling back to `main`. Using fallback git remote data parsing and downloads finished in 49 ms. json schema validated in 171 ms build-job1 starting ubuntu:24.04 (build) test-job starting ubuntu:24.04 (build) build-job2 starting ubuntu:24.04 (build) build-job1 copied to docker volumes in 580 ms build-job2 copied to docker volumes in 620 ms test-job copied to docker volumes in 632 ms build-job1 $ echo "Hello World1" # collapsed multi-line command build-job1 > Hello World1 test-job $ echo "Hello Test" # collapsed multi-line command test-job > Hello Test build-job2 $ echo "Hello World2" # collapsed multi-line command build-job2 > Hello World2 build-job1 finished in 1.22 s test-job finished in 1.23 s build-job2 finished in 1.3 s PASS build-job1 PASS build-job2 PASS test-job pipeline finished in 1.58 s
最初はいろいろ言われましたが、2回目からは静かに(?)なりました。
parsing and downloads finished in 41 ms. json schema validated in 166 ms build-job1 starting ubuntu:24.04 (build) test-job starting ubuntu:24.04 (build) build-job2 starting ubuntu:24.04 (build) build-job1 copied to docker volumes in 580 ms build-job2 copied to docker volumes in 620 ms test-job copied to docker volumes in 632 ms build-job1 $ echo "Hello World1" # collapsed multi-line command build-job1 > Hello World1 test-job $ echo "Hello Test" # collapsed multi-line command test-job > Hello Test build-job2 $ echo "Hello World2" # collapsed multi-line command build-job2 > Hello World2 build-job1 finished in 1.22 s test-job finished in 1.23 s build-job2 finished in 1.3 s PASS build-job1 PASS build-job2 PASS test-job pipeline finished in 1.58 s
すべてのジョブが実行されていますね。ただステージ間の関係は考慮されていないように見えます。
指定したジョブだけを実行することもできます。
$ gitlab-ci-local build-job1 parsing and downloads finished in 43 ms. json schema validated in 171 ms build-job1 starting ubuntu:24.04 (build) build-job1 copied to docker volumes in 620 ms build-job1 $ echo "Hello World1" # collapsed multi-line command build-job1 > Hello World1 build-job1 finished in 1.28 s PASS build-job1 $ gitlab-ci-local build-job1 test-job parsing and downloads finished in 37 ms. json schema validated in 166 ms build-job1 starting ubuntu:24.04 (build) test-job starting ubuntu:24.04 (build) test-job copied to docker volumes in 612 ms build-job1 copied to docker volumes in 627 ms test-job $ echo "Hello Test" # collapsed multi-line command test-job > Hello Test build-job1 $ echo "Hello World1" # collapsed multi-line command build-job1 > Hello World1 test-job finished in 1.19 s build-job1 finished in 1.2 s PASS build-job1 PASS test-job
GitLab CI/CD変数を使うようにしてみましょう。
build-job1: stage: build image: ubuntu:24.04 script: - | echo "branch: $CI_COMMIT_BRANCH" echo "source: $CI_PIPELINE_SOURCE" echo "registry: $CI_REGISTRY" echo "Hello World1"
Predefinedなものは解決してくれそうな感じがありますね。
$ gitlab-ci-local build-job1 parsing and downloads finished in 43 ms. json schema validated in 164 ms build-job1 starting ubuntu:24.04 (build) build-job1 copied to docker volumes in 586 ms build-job1 $ echo "branch: $CI_COMMIT_BRANCH" # collapsed multi-line command build-job1 > branch: ci build-job1 > source: push build-job1 > registry: local-registry.192.168.0.3 build-job1 > Hello World1 build-job1 finished in 1.13 s PASS build-job1
もっとも、$CI_REGISTRYの値を見ると「それっぽい値を入れる」のような気がしますが。
このあたりみたいですね。
predefinedVariables["CI_JOB_ID"] = `${this.jobId}`; predefinedVariables["CI_PIPELINE_ID"] = `${this.pipelineIid + 1000}`; predefinedVariables["CI_PIPELINE_IID"] = `${this.pipelineIid}`; predefinedVariables["CI_JOB_NAME"] = `${this.name}`; predefinedVariables["CI_JOB_NAME_SLUG"] = `${this.name.replace(/[^a-z\d]+/ig, "-").replace(/^-/, "").slice(0, 63).replace(/-$/, "").toLowerCase()}`; predefinedVariables["CI_JOB_STAGE"] = `${this.stage}`; predefinedVariables["CI_BUILDS_DIR"] = ciBuildsDir; predefinedVariables["CI_PROJECT_DIR"] = this.ciProjectDir; predefinedVariables["CI_JOB_URL"] = `${predefinedVariables["CI_SERVER_URL"]}/${gitData.remote.group}/${gitData.remote.project}/-/jobs/${this.jobId}`; // Changes on rerun. predefinedVariables["CI_PIPELINE_URL"] = `${predefinedVariables["CI_SERVER_URL"]}/${gitData.remote.group}/${gitData.remote.project}/pipelines/${this.pipelineIid}`; predefinedVariables["CI_ENVIRONMENT_NAME"] = this.environment?.name ?? ""; predefinedVariables["CI_ENVIRONMENT_SLUG"] = this.environment?.name?.replace(/[^a-z\d]+/ig, "-").replace(/^-/, "").slice(0, 23).replace(/-$/, "").toLowerCase() ?? ""; predefinedVariables["CI_ENVIRONMENT_URL"] = this.environment?.url ?? ""; predefinedVariables["CI_ENVIRONMENT_TIER"] = this.environment?.deployment_tier ?? ""; predefinedVariables["CI_ENVIRONMENT_ACTION"] = this.environment?.action ?? ""; if (opt.nodeIndex !== null) { predefinedVariables["CI_NODE_INDEX"] = `${opt.nodeIndex}`; } predefinedVariables["CI_NODE_TOTAL"] = `${opt.nodesTotal}`; predefinedVariables["CI_REGISTRY"] = `local-registry.${this.gitData.remote.host}`; predefinedVariables["CI_REGISTRY_IMAGE"] = `$CI_REGISTRY/${predefinedVariables["CI_PROJECT_PATH"].toLowerCase()}`;
https://github.com/firecow/gitlab-ci-local/blob/4.61.1/src/job.ts#L306-L327
今度は独自の変数を追加してみます。
build-job1: stage: build image: ubuntu:24.04 script: - | echo "branch: $CI_COMMIT_BRANCH" echo "source: $CI_PIPELINE_SOURCE" echo "registry: $CI_REGISTRY" echo "my var: $MY_VARIABLE" echo "Hello World1"
変数自体はGitLabプロジェクトに登録しています。

実行。
$ gitlab-ci-local build-job1 parsing and downloads finished in 42 ms. json schema validated in 169 ms build-job1 starting ubuntu:24.04 (build) build-job1 copied to docker volumes in 542 ms build-job1 $ echo "branch: $CI_COMMIT_BRANCH" # collapsed multi-line command build-job1 > branch: ci build-job1 > source: push build-job1 > registry: local-registry.192.168.0.3 build-job1 > my var: build-job1 > Hello World1 build-job1 finished in 1.07 s PASS build-job1
さすがにこの値はわからないみたいですね。
ちなみにGitLab CI/CD上で動かすとこうなります。
$ echo "branch: $CI_COMMIT_BRANCH" # collapsed multi-line command branch: source: merge_request_event registry: 192.168.0.3:5050 my var: foobar Hello World
では、変数を用意しましょう。
今回はプロジェクトファイル変数として用意します。
Quirks / Tracked Files / Project file variables
フォーマットはYAMLと.envがありますが、今回はYAMLで。
.gitlab-ci-local-variables.yml
MY_VARIABLE: hoge
再度実行。
$ gitlab-ci-local build-job1 parsing and downloads finished in 44 ms. json schema validated in 165 ms build-job1 starting ubuntu:24.04 (build) build-job1 copied to docker volumes in 463 ms build-job1 $ echo "branch: $CI_COMMIT_BRANCH" # collapsed multi-line command build-job1 > branch: ci build-job1 > source: push build-job1 > registry: local-registry.192.168.0.3 build-job1 > my var: hoge build-job1 > Hello World1 build-job1 finished in 1.03 s PASS build-job1
今度は独自の変数を認識できるようになりました。
今回はこのくらいで。
おわりに
GitLabのジョブをローカルで動かせるgitlab-ci-localを試してみました。
こういうタイプのツールには限界はあると思うのですが、GitLab CI/CD上で動かすまでの手間なども大変だったりするので、使えるところでは
使っていきたいですね。