Java - Spring Batchのjarをビルドした際にDockerイメージもビルドする(Gradle)
Spring Batchで作成した.jarをDockerコンテナとして実行したい事情があった。
Gradleのビルド時にイメージビルドまでまとめてできないかと調べが、情報が少なそうだったので書く。
環境
- JDK: OpenJDK Runtime Environment Temurin-17.0.6+10 (build 17.0.6+10)
- Spring Boot: 2.7.10
- Spring Batch: 4.3.8
- Gradle: 7.6.1
- Docker: 23.0.1, build a5ee5b1dfc
やり方
build.gradleに、ビルド結果の.jarを含めてDockerイメージを作成するタスク定義を追加する。
以下3ファイルをコピペすれば動くはず。
plugins { id 'java' id 'org.springframework.boot' version '2.7.10-SNAPSHOT' id 'io.spring.dependency-management' version '1.0.15.RELEASE' } group = 'com.example' version = '0.0.1-SNAPSHOT' sourceCompatibility = '17' repositories { mavenCentral() maven { url 'https://repo.spring.io/milestone' } maven { url 'https://repo.spring.io/snapshot' } } dependencies { implementation 'org.springframework.boot:spring-boot-starter-batch' runtimeOnly 'com.h2database:h2' compileOnly 'org.projectlombok:lombok' annotationProcessor 'org.projectlombok:lombok' testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'org.springframework.batch:spring-batch-test' } + // buildタスクの実行後、生成された.jarをDockerイメージビルド用にコピーする。 + // ./build/libs配下に置いたままイメージビルドするとビルドコンテキストが + // 大きくなってしまったりignoreを書くのが面倒だったりなので。 + build.doLast { + copy { + from "./build/libs/demo-0.0.1-SNAPSHOT.jar" + into "./docker/" + rename "demo-0.0.1-SNAPSHOT.jar", "demo.jar" + } + // jarビルド時にDockerイメージのビルドも一緒にやってしまった方が楽な場合もあるかもしれない。 + // exec { + // executable 'sh' + // args '-c', 'docker build -t sample_img -f ./docker/Dockerfile ./docker/' + // } + } + clean.doFirst { + delete './docker/demo.jar' + } + // Dockerイメージビルドタスク + task docker(type: Exec) { + executable 'sh' + args '-c', 'docker build -t sample_img -f ./docker/Dockerfile ./docker/' + } + docker.dependsOn build tasks.named('test') { useJUnitPlatform() }
package com.example.demo; import org.springframework.batch.core.Job; import org.springframework.batch.core.Step; import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing; import org.springframework.batch.core.configuration.annotation.JobBuilderFactory; import org.springframework.batch.core.configuration.annotation.StepBuilderFactory; import org.springframework.batch.core.launch.support.RunIdIncrementer; import org.springframework.batch.repeat.RepeatStatus; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import lombok.extern.slf4j.Slf4j; /** * 実行時の引数「nanika」の内容をログ出力するJob. * nanikaが指定されない場合は「デフォルト値です!」をログ出力する. */ @Configuration @EnableBatchProcessing @Slf4j public class SampleJobConfig { @Autowired public JobBuilderFactory jobFactory; @Autowired public StepBuilderFactory stepFactory; @Value("${nanika:デフォルト値です!}") private String nanika; @Bean public Job sampleJob(Step sampleStep1) { return jobFactory .get("sampleJob") .incrementer(new RunIdIncrementer()) .start(sampleStep1) .build(); } @Bean public Step sampleStep1() { return stepFactory .get("sampleStep1") .tasklet((contrib, context) -> { log.warn(nanika); return RepeatStatus.FINISHED; }) .build(); } }
FROM eclipse-temurin:17.0.4.1_1-jre-jammy RUN addgroup --system spring \ && adduser --system spring \ && usermod -aG spring spring USER spring:spring COPY ./demo.jar demo.jar ENTRYPOINT ["java", "-jar", "/demo.jar"]
jarとDockerイメージのビルド
Gradleでdockerタスクを実行すれば、jarのビルドからDockerイメージの作成までやってくれる。
./gradlew docker
Dockerコンテナ実行
実行すると標準出力の警告ログに「なんらかのパラメータ」が表示されるはず。
docker run --rm sample_img --nanika=なんらかのパラメータ
Java - Visual Studio CodeでDomaの注釈処理を使う
Visual Studio CodeでDomaの注釈処理を使ってDao付近を自動生成したくて調べたが、情報がなさそうだったので書く。
環境
- Visual Studio Code: 1.72.0-1
- JDK: 17.0.4.1 (Temurin)
- Gradle: 7.5
- Doma: 2.53.1 (doma-core)
- (MariaDB: 10.9.3-MariaDB-1:10.9.3+maria~ubu2204 mariadb.org binary) *1
やり方
事前準備
自動生成が動くにはorg.eclipse.buildship.core.prefs
が生成されている必要があるようだ。
このファイルの中身はbuild.gradle
の依存関係がインポートされる際に生成されるが、インポートの時点でファイルがなかったり空ファイルだったりした場合は中身の生成がスキップされてしまい、その後のソースの自動生成も反応してくれない。
このため、最初にファイルを作成し以下2行を記載しておく。
connection.project.dir= eclipse.preferences.version=1
注釈処理の有効化
Visual Studio CodeでGradleを使って注釈処理するという内容は以下リンクにて言及されていた。
これを参考にDomaも扱えるようにする。
Is Annotation Processing Supported for Gradle project? · Issue #1039 · redhat-developer/vscode-java · GitHub
注釈処理の依存関係を追加
plugins { id 'org.springframework.boot' version '2.7.4' id 'io.spring.dependency-management' version '1.0.14.RELEASE' id 'java' id 'war' + // 注釈処理 + id 'com.diffplug.eclipse.apt' version '3.39.0' } group = 'com.example' version = '0.0.1-SNAPSHOT' sourceCompatibility = '17' repositories { mavenCentral() } dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-jdbc' implementation 'org.springframework.boot:spring-boot-starter-web' runtimeOnly 'org.mariadb.jdbc:mariadb-java-client' providedRuntime 'org.springframework.boot:spring-boot-starter-tomcat' testImplementation 'org.springframework.boot:spring-boot-starter-test' compileOnly 'org.projectlombok:lombok' annotationProcessor 'org.projectlombok:lombok' + // doma関連 + implementation 'org.seasar.doma:doma-core:2.53.1' + implementation 'org.seasar.doma.boot:doma-spring-boot-starter:1.6.0' + annotationProcessor 'org.seasar.doma:doma-processor:2.53.1' } + // 自動生成ファイルの出力先ディレクトリ + eclipse { + jdt { + apt { + genSrcDir = file('src-gen/main/java') + genTestSrcDir = file('src-gen/test/java') + } + } + } + // SQLテンプレートのコピータスク + task _copySqls(type: Copy) { + from 'src/main/resources/META-INF' + into 'bin/default/META-INF' + } tasks.named('test') { useJUnitPlatform() }
設定ファイル生成
以下のビルドタスクを実行して設定ファイルを生成する。
- eclipseJdtApt
- eclipseJdt
- eclipseFactorypath
Visual Studio Codeを再起動
再起動するとビルドタスクで自動生成ファイルの出力先として指定したディレクトリ(src-gen
)が作成される。
Gradleのリフレッシュ
手元の環境ではこれをやらないと自動生成してくれなかった。
build.gradle
に空行を足して保存すれば、依存関係を追記した際のように同期するかを聞いてくるのでyes
すればよい。
自動生成の動作確認
ここまでの手順がうまくいっていれば、以下のファイルを作成すると同時にsrc-gen
ディレクトリの配下に_Employee.java
が自動生成されるはず。
package com.example.demo; import org.seasar.doma.*; import lombok.Data; @Entity @Data public class Employee { @Id @GeneratedValue(strategy = GenerationType.SEQUENCE) @SequenceGenerator(sequence = "EMPLOYEE_SEQ") Integer id; String name; }
サンプルプロジェクト
自動生成されたクラスを使って実際に動作するサンプルとして、http://localhost:8080/employees で待機し、employeeテーブルの内容をJSON形式で応答するAPIを作成してみる。
Spring Bootで新規プロジェクトを作り、この記事のソースをコピペすれば動くはず。
DBを作る
CREATE OR REPLACE DATABASE testdb1; CREATE OR REPLACE TABLE employee ( id int not null auto_increment primary key, name varchar(255) ); INSERT INTO employee VALUE (1, 'Lorem ipsum'); INSERT INTO employee VALUE (2, 'dolor sit'); INSERT INTO employee VALUE (3, 'amet consectetur'); INSERT INTO employee VALUE (4, 'adipiscing elit'); SELECT * FROM employee;
ソース類を書く
作成するソースは以下の通り。
application.properties
Employee.java
(自動生成の動作確認で作成したもの)EmployeeDao.java
EmployeeController.java
all.sql
spring.datasource.url=jdbc:mariadb://mariadb:3306/testdb1 spring.datasource.username=root spring.datasource.password=password spring.datasource.driverClassName=org.mariadb.jdbc.Driver
package com.example.demo; import java.util.List; import org.seasar.doma.*; import org.seasar.doma.boot.ConfigAutowireable; // @ConfigAutowireableを付けておくと、 // Implを自動生成した際に@Repositoryを付加しておいてくれる。 @Dao @ConfigAutowireable public interface EmployeeDao { @Select List<Employee> all(); }
EmployeeDao
を作成するとall()
メソッドでエラーになるが、いったんそのまま進める。
この手順の最後でSQLファイルをbin配下にコピーすることで解消できる。
package com.example.demo; import java.util.List; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; import org.springframework.web.bind.annotation.*; @RestController public class EmployeeController { @Lazy @Autowired private EmployeeService service; @GetMapping("/employees") public List<Employee> index() throws Exception { return service.list(); } @Service public class EmployeeService { @Autowired EmployeeDao dao; public List<Employee> list() throws Exception { return dao.all(); } } }
SELECT /*%expand*/* FROM employee ORDER BY id
SQLファイルをbin配下へコピーする
以下のビルドタスクでSQLファイルをbin配下へコピーする。
- _copySqls
タスク実行後、EmployeeDao
を再コンパイルするとエラーが解消する。
これでRun and Debug
からサーバ起動すれば動くはず。
参考リンク
Is Annotation Processing Supported for Gradle project? · Issue #1039 · redhat-developer/vscode-java · GitHub
Visual Studio Codeでの注釈処理のやり方。
この記事の元ネタ。Gradleでeclipseプロジェクト用ファイルを生成するときのTips - Qiita
.settings/org.eclipse.buildship.core.prefs
が生成されず途方に暮れていた時に、connection
以降の2行を書いておけばよいというヒントになった。
Java - AWSのNLB越しに長時間かかるリクエストを送ると応答待ちのまま終わらなくなる場合の対策
EC2上のWebアプリから別のEC2上にあるWebアプリのAPIをリクエストした際、応答までに350秒以上経過するとリクエストした側が応答待ちのまま終わらなくなる、という事象があった。
調べた時に具体的な対策サンプルが少なそうだったので書く。
構成
- EC2: Linux
- Web Application: Java17 (Spring Boot 2.7.3)
原因
Network Load Balancer - Elastic Load Balancing
NLBを経由するリクエストがアイドル状態のまま350秒以上経過すると、NLBはその接続を切断する。
この時通知などは行われないため、リクエストした側は切断されたことを検知できず永久に応答を待ち続けることになる。
また、応答した側もエラーにはならないため何が起きたのか分かりづらい。
対策
TCP KeepAliveで継続的にパケットを流すことで、350秒のカウンターをリセットし続けることができる。
リクエストする側で以下2点を修正する。
- リクエスト時のソケット設定でTCP KeepAliveを有効にする
- OSの設定でTCP KeepAliveパケット送出開始までの待機時間を350秒未満にする
TCP KeepAliveパケットの送出開始までの待機時間はOSが制御している。
デフォルトでは7200秒(2時間)になっているようなので、350秒未満に変更する。
サンプルコード
http://localhost:8080/greeting
で待機し、リクエストを受け付けると外部APIへTCP KeepAliveを有効にしたリクエストを送るサンプル。
Spring Bootでプロジェクトを新規作成し、以下の2ファイルをコピペすれば動くはず。
1. ソース
plugins { id 'org.springframework.boot' version '2.7.3' id 'io.spring.dependency-management' version '1.0.13.RELEASE' id 'java' id 'war' } group = 'com.example' version = '0.0.1-SNAPSHOT' sourceCompatibility = '17' repositories { mavenCentral() } dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' // ソケットでTCP KeepAliveを設定するために使用 implementation 'org.apache.httpcomponents:httpclient:4.5.13' providedRuntime 'org.springframework.boot:spring-boot-starter-tomcat' testImplementation 'org.springframework.boot:spring-boot-starter-test' }
package com.example.demo; import org.apache.http.config.SocketConfig; import org.apache.http.impl.client.HttpClients; import org.slf4j.*; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.web.client.RestTemplateBuilder; import org.springframework.context.annotation.*; import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; import org.springframework.scheduling.annotation.*; import org.springframework.stereotype.Service; import org.springframework.web.bind.annotation.*; import org.springframework.web.client.RestTemplate; @EnableAsync @RestController public class GreetingController { private Logger logger = LoggerFactory.getLogger(GreetingController.class); @Lazy @Autowired private ExternalApiAccessor api; @GetMapping("/greeting") public String greeting() throws Exception { logger.info("greeting"); api.access(); return "Hello, World!"; } @Bean public RestTemplate restTemplate() { // ソケットのTCP KeepAliveを有効にする var socketCfg = SocketConfig.custom().setSoKeepAlive(true).build(); var httpclient = HttpClients.custom().setDefaultSocketConfig(socketCfg).build(); var reqFactory = new HttpComponentsClientHttpRequestFactory(httpclient); var template = new RestTemplateBuilder().requestFactory(() -> reqFactory).build(); return template; } @Service public class ExternalApiAccessor { @Autowired private RestTemplate restTemplate; @Async public void access() throws Exception { logger.info("API access"); // NLBの向こう側にいて350秒以上かかるAPI var apiUrl = "https://example.com/external_api/heavy_work"; var response = restTemplate.getForEntity(apiUrl, String.class); logger.info(response.getBody()); } } }
2. OS設定変更
## 現状確認 # sysctl -a | grep tcp_keepalive net.ipv4.tcp_keepalive_intvl = 75 net.ipv4.tcp_keepalive_probes = 9 net.ipv4.tcp_keepalive_time = 7200 ## 修正 (このファイルがない場合は新規作成する) # vi /etc/sysctl.conf
: ## TCP KeepAliveパケット送出開始までの待機時間 / 350秒未満にする net.ipv4.tcp_keepalive_time = 120 :
これで大丈夫になるはず。
GitLab - オンプレのLXDにGitLabを立てる
GitLabなどを載せているLXDのホストをDebian(buster)から(bullseye)に引っ越すついでにクリーンインストールした。
インストールのやり方などを調べていて、ミドルやアプリを組み合わせた場合のサンプルがもっとあるといいなと思ったので書く。
そりゃみんなSaaS使うよね、という内容。
できあがりイメージ
- 物理マシン(Windows)に入れたVirtualBox上でDebianを動かしLXDのホストにする
- LXDでGitLab用とDocker用の2つのコンテナを動かす
- GitLab用コンテナ
GitLab本体の他、Container Registry(CIで使用するDockerイメージ保管用)と、Pages(カバレッジレポート表示用)を動かす。
ただ、Pages周りはヘンテコ運用になってしまった。残念。 - Docker用コンテナ
GitLab Runnerを動かす。
図のGitLab Runnerの構成は以前の記事で使用した際のもの。
- GitLab用コンテナ
- VMはブリッジ接続し、LXDコンテナはポート転送する
I/O激遅になるかもと思ったが、一人で使う分には大丈夫そう。
複数人だとGitLab Runnerを別マシンに移すなりしないと厳しいと思われる。
目安として、GitLabのWebUIはプロジェクトのトップページ(ファイル一覧)の表示が0.9秒くらい。
CIでは、Rustのwarp
+mongodb
を使ったHello Worldレベルのプロジェクトで、ビルドが1.5分、カバレッジ取得(テスト5ケースほど)が2分くらい。
CI実行中はWebUIの表示も2秒掛かるくらい重くなる。
環境
- VirtualBox: 6.1.34-150636
- Debian: 11.3 bullseye
- LXD: 5.1
- ZFS: 2.1.4-1~bpo11+1
- Gitlab: gitlab-ee 14.10.2 (Omnibus)
- Docker: 20.10.16, build aa7e414
- VMへの割り当てリソース
作る
VirtualBox
VM(ハコ)作成
環境の内容でハコを作る。
OSインストール
Debian 11.3 bullseyeを入れる。
今回の記事で関連しそうな設定は以下の通り。
- Graphical Installを使用 (sudo未インストール)
- ホスト名は
debian1
- ユーザ名は
user1
- ストレージは小さい方(20GB)が
/dev/sda
で、大きい方(50GB)が/dev/sdb
になるようにする - aptミラーは
ftp.jp.debian.org
- Software selectionは
SSH Server
のみを選択
ツール類インストール
user1@debian1:~$ su -
root@debian1:~# apt update
root@debian1:~# apt install sudo snapd parted
root@debian1:~# usermod -aG sudo user1
- この後、
user1
をログインし直せばsudo
できるようになっているはず snapd
はlxd
のインストール用で、parted
はZFSのパーティション作成用- 必須ではないが、この他に
vim
,bash-completion
,ncdu
,btop
(contrib)を入れた
IPアドレス割り当て
- 静的IP割り当て
: # The primary network interface - allow-hotplug enp0s3 - iface enp0s3 inet dhcp + auto enp0s3 + iface enp0s3 inet static + address 192.0.2.101 + netmask 255.255.255.0 + gateway 192.0.2.1 :
- サービス再起動
user1@debian1:~$ sudo systemctl restart networking.service
SSH接続設定
サーバコンソールだとコピペできなくてつらいので、早めにssh接続できるようにしておきたい。
まだ鍵を持っていない場合は作る
https://duckduckgo.com/?q=ssh+keygen&df=yクライアント側のssh configに追記
: + # LXD host + Host debian1 + HostName 192.0.2.101 + PreferredAuthentications publickey + IdentityFile ~/.ssh/id_rsa # ついでにgit cloneとかで使う分も追記しておく + # git remote + Host 192.0.2.101 + HostName 192.0.2.101 + Preferredauthentications publickey + IdentityFile ~/.ssh/id_rsa + Port 1022
- クライアントマシンからサーバへ鍵を送る
user1@pc1:~$ ssh-copy-id user1@192.0.2.101 # 鍵で接続できるようになったはず user1@pc1:~$ ssh debian1
エイリアス設定
必須ではないが、やっておくと後々ラク。
user1@debian1:~$ echo -e "\n\nalias ll='ls -Flh'\nalias lk='ls -AFlh'" >> ~/.bashrc user1@debian1:~$ source ~/.bashrc
ZFS
LXDのストレージバックエンドとしてZFSを使う。
以前は、お試しで入れたディレクトリバックエンドをそのまま使い続けていたが、環境を移行しようとlxc publish
しようとしたところ、びっくりするほど遅くて諦めたので、今回はZFSを使うことにした。
インストール
ZFSはcontrib
コンポーネントにある。
今回はbackportsからインストールする。
# contribとbackportsを追加 user1@debian1:~$ sudo sed -i.original -e 's# main$# main contrib#g' /etc/apt/sources.list user1@debian1:~$ echo "deb http://ftp.jp.debian.org/debian bullseye-backports main contrib" | sudo tee -a /etc/apt/sources.list user1@debian1:~$ echo "deb-src http://ftp.jp.debian.org/debian bullseye-backports main contrib" | sudo tee -a /etc/apt/sources.list user1@debian1:~$ sudo apt update # ZFSインストール user1@debian1:~$ sudo apt install linux-headers-amd64 user1@debian1:~$ sudo apt install -t bullseye-backports zfsutils-linux user1@debian1:~$ sudo modprobe zfs
パーティション作成
/dev/sdb
をZFSで初期化する。
user1@debian1:~$ sudo parted /dev/sdb GNU Parted 3.4 Using /dev/sdb Welcome to GNU Parted! Type 'help' to view a list of commands. (parted) p Error: /dev/sdb: unrecognised disk label Model: ATA VBOX HARDDISK (scsi) Disk /dev/sdb: 53.7GB Sector size (logical/physical): 512B/512B Partition Table: unknown Disk Flags: (parted) mklabel gpt (parted) mkpart zfs1 zfs 2048s 100% (parted) p Model: ATA VBOX HARDDISK (scsi) Disk /dev/sdb: 53.7GB Sector size (logical/physical): 512B/512B Partition Table: gpt Disk Flags: Number Start End Size File system Name Flags 1 1049kB 53.7GB 53.7GB zfs zfs1 (parted) q Information: You may need to update /etc/fstab.
LXD
インストール
snapd
で入れる。
Linux Containers - LXD - LXDを使い始めるには
user1@debian1:~$ sudo snap install lxd
user1@debian1:~$ sudo usermod -aG lxd user1
- この後、
user1
をログインし直せばlxc
コマンドが使えるようになっているはず - Ubuntuと比べて、たまに
lxc
コマンドの反応が遅いような……
初期設定
イメージの自動更新はOFFにしておく。
頻繁にコンテナを作るわけではないので、せっかく最新に保ってくれていても回線やストレージI/Oの無駄遣いになってしまう。
user1@debian1:~$ lxd init Would you like to use LXD clustering? (yes/no) [default=no]: Do you want to configure a new storage pool? (yes/no) [default=yes]: Name of the new storage pool [default=default]: zfs_pool1 Name of the storage backend to use (lvm, zfs, ceph, btrfs, dir) [default=zfs]: Create a new ZFS pool? (yes/no) [default=yes]: Would you like to use an existing empty block device (e.g. a disk or partition)? (yes/no) [default=no]: yes Path to the existing block device: /dev/sdb1 Would you like to connect to a MAAS server? (yes/no) [default=no]: Would you like to create a new local network bridge? (yes/no) [default=yes]: What should the new bridge be called? [default=lxdbr0]: What IPv4 address should be used? (CIDR subnet notation, “auto” or “none”) [default=auto]: What IPv6 address should be used? (CIDR subnet notation, “auto” or “none”) [default=auto]: none Would you like the LXD server to be available over the network? (yes/no) [default=no]: Would you like stale cached images to be updated automatically? (yes/no) [default=yes]: no Would you like a YAML "lxd init" preseed to be printed? (yes/no) [default=no]:
GitLab用コンテナ作成
コンテナ(ハコ)作成
- 作成したコンテナを誤って削除できないようガードを掛ける
Linux Containers - LXD - Advanced guide
Method 1のやり方(プロンプトを出す)だと「アーハイハイ」でEnterしてしまいがちなので、明確なオペレーションが発生するMethod 2のやり方にした。 - VMの外からHttpなどでアクセスできるようにポート転送する
# コンテナ作成 user1@debian1:~$ lxc launch images:debian/bullseye gitlab1 # コンテナの誤削除防止 user1@debian1:~$ lxc config set gitlab1 security.protection.delete=true # ポート転送 # - 1081: GitLabのWebUI # - 1086: GitLabのContainer Registry # - 1022: gitクライアント向けのssh (こっちを22番にした方がいいかも?) user1@debian1:~$ lxc config device add gitlab1 http1081_gitlab proxy listen=tcp:0.0.0.0:1081 connect=tcp:127.0.0.1:1081 bind=host user1@debian1:~$ lxc config device add gitlab1 http1086_registry proxy listen=tcp:0.0.0.0:1086 connect=tcp:127.0.0.1:1086 bind=host user1@debian1:~$ lxc config device add gitlab1 ssh1022_gitlab proxy listen=tcp:0.0.0.0:1022 connect=tcp:127.0.0.1:22 bind=host
GitLabインストール
今回はLXD上で運用するため、公式手順とは異なるやり方をした。
また、旧環境からデータを移行する場合は個別の考慮が必要になる。
Download and install GitLab | GitLab
LXDコンテナで動かすためにカーネルパラメータの変更をスキップ
通常の手順ではインストールスクリプト内のreconfigure
によってカーネルパラメータが変更されるが、非特権コンテナではエラーになる。
以下のように進めることでこれを回避する。-
apt install
時にEXTERNAL_URL
を指定しない
指定しなければreconfigure
は実行されない。 - GitLabのインストール後、設定ファイルでカーネルパラメータを変更しないよう指定
- 手動で
gitlab-ctl reconfigure
を実行
-
旧環境からデータを移行する場合はGitLabのバージョンを旧環境と合わせる
GitLabのバックアップは取得したバージョンと同じバージョンにしかリストアできない。
このため、移行する場合は旧環境を最新バージョンにしておくか、新環境へいったん旧バージョンを入れてリストアしてから最新版へ更新する必要がある。
今回は後者のやり方にした。
Restore GitLab
user1@debian1:~$ lxc exec gitlab1 bash
# aptをJPミラーに向けておく root@gitlab1:~# sed -i.original 's#/deb.debian.org/debian #/ftp.jp.debian.org/debian #g' /etc/apt/sources.list root@gitlab1:~# apt update root@gitlab1:~# apt install curl openssh-server ca-certificates perl # メール配信しないので`postfix`はインストールしない root@gitlab1:~# curl https://packages.gitlab.com/install/repositories/gitlab/gitlab-ee/script.deb.sh | bash # - `EXTERNAL_URL`を指定しない # - 旧環境からデータ移行する場合は旧環境と同じバージョンをインストールする(ここでは仮に14.8.2としておく) root@gitlab1:~# apt install gitlab-ee=14.8.2-ee.0
設定ファイル編集
GitLabを起動する前に、設定ファイルでエラーの回避指定や起動するサービスの選別などをしておく。
WebUIアクセス用URL指定
通常の手順ではインストール時に指定するEXTERNAL_URL
。
: - external_url 'http://gitlab.example.com' + external_url 'http://192.0.2.101:1081' :
カーネルパラメータ変更のスキップ
gitlab-ctl reconfigure
実行時にカーネルパラメータを変更しないようにする。
: ##! Attempt to modify kernel paramaters. To skip this in containers where the ##! relevant file system is read-only, set the value to false. - # package['modify_kernel_parameters'] = true + package['modify_kernel_parameters'] = false :
(有効化) GitLab Pages
CIのカバレッジレポート表示用。
GitLab Pages administration | GitLab
IP指定だと思い通りにいかなかったので、ここだけ間に合わせのホスト名を割り振り、クライアントPCのhostsにも対応するホスト名を追記してやり過ごすことにした。
: - # pages_external_url "http://pages.example.com/" - # gitlab_pages['enable'] = false + pages_external_url "http://gitlab-pages.test:1081/" + gitlab_pages['enable'] = true :
実際に生成されるURLでは、pages_external_url
で定義したホストの先頭にプロジェクトが属するグループ名が付加される。
例えば上記の定義であれば、hoge
グループに属するfuga
プロジェクトのCIでカバレッジレポートを生成した場合、URLの先頭部分は次のようになる。
http://hoge.gitlab-pages.test:1081/-/fuga/
~
このため、クライアントPCのhostsにもグループ数分のエントリを追加するか、DNSを立てる必要がある。
user1@pc1:~$ echo "192.0.2.101 hoge.gitlab-pages.test" >> /etc/hosts
力及ばずヘンテコな運用になってしまい無念である。
(有効化) GitLab Container Registry
CIでのDockerイメージ格納用。
ポート番号はGitLabのWbUIとは別にしておく。
GitLab Container Registry administration | GitLab
: - # registry_external_url 'https://registry.example.com' + registry_external_url 'http://192.0.2.101:1086' :
(無効化) Prometheus
メトリクス収集。
欲しい情報はGitLabの基本的な機能で足りているので今回は使わない。
Monitoring GitLab with Prometheus | GitLab
: # To completely disable prometheus, and all of it's exporters, set to false - # prometheus_monitoring['enable'] = true + prometheus_monitoring['enable'] = false :
(無効化) Grafana
Prometheusが収集したメトリクスをダッシュボードで視覚化してくれる。
Prometheus同様今回は使わない。
Grafana Dashboard Service | GitLab
: - # grafana['enable'] = true + grafana['enable'] = false :
(無効化) GitLab agent server for Kubernetes (KAS)
Kubernetesクラスターとの接続を構成・監視する?仕組みらしい。
Kubernetesが分かっていないせいか説明を読んでもサッパリだが、使ってないはずなのでヨシ!
Install the GitLab agent server for Kubernetes (KAS) | GitLab
: ##! Enable GitLab KAS - # gitlab_kas['enable'] = true + gitlab_kas['enable'] = false :
編集内容を反映
# 反映 root@gitlab4:~# gitlab-ctl reconfigure # WebUIの起動確認 / 起動するまでは`502` root@gitlab3:~# curl -ILs localhost:1081 | grep HTTP
旧環境のバックアップをリストア
移行しないならこの手順は不要。
Back up and restore GitLab
バックアップ/リストア関連の手順は割と頻繁に変わってそう。
earlier/laterがいっぱいある。
旧環境からバックアップファイルを転送
# 旧環境 → GitLabコンテナ root@gitlab1:~# sftp user1@old_server sftp> get gitlab_config_1652870735_2022_05_18.tar sftp> get 1652869171_2022_05_18_14.8.2-ee_gitlab_backup.tar sftp> bye
リストア
# バックアップファイルを所定の場所に配置 root@gitlab1:~# mkdir /etc/gitlab/config_backup/ root@gitlab1:~# mv gitlab_config_1652870735_2022_05_18.tar /etc/gitlab/config_backup/ root@gitlab1:~# mv 1652869171_2022_05_18_14.8.2-ee_gitlab_backup.tar /var/opt/gitlab/backups/ root@gitlab1:~# chown git:git /var/opt/gitlab/backups/1652869171_2022_05_18_14.8.2-ee_gitlab_backup.tar # サービス停止 root@gitlab1:~# gitlab-ctl stop puma root@gitlab1:~# gitlab-ctl stop sidekiq # リストアして再起動 root@gitlab1:~# gitlab-backup restore BACKUP=1652869171_2022_05_18_14.8.2-ee root@gitlab1:~# gitlab-ctl reconfigure root@gitlab1:~# gitlab-ctl restart root@gitlab1:~# gitlab-rake gitlab:check SANITIZE=true # 何かのチェック root@gitlab1:~# gitlab-rake gitlab:doctor:secrets root@gitlab1:~# gitlab-rake gitlab:artifacts:check root@gitlab1:~# gitlab-rake gitlab:lfs:check root@gitlab1:~# gitlab-rake gitlab:uploads:check
最新バージョンへ更新
リストアのために古いバージョンのGitLabをインストールしていた場合は、最新バージョンへ更新しておく。
たまにUpgrade pathsに引っ掛かることがある。
# 最新版に直接アップグレードできそうか見てみる root@gitlab1:~# apt update root@gitlab1:~# apt upgrade -s Reading package lists... Done Building dependency tree... Done Reading state information... Done Calculating upgrade... Done The following packages will be upgraded: gitlab-ee 1 upgraded, 0 newly installed, 0 to remove and 0 not upgraded. Inst gitlab-ee [14.8.2-ee.0] (14.10.2-ee.0 gitlab-ee:1/bullseye [amd64]) Conf gitlab-ee (14.10.2-ee.0 gitlab-ee:1/bullseye [amd64]) # Upgrade pathsを見ると、14.8.2からは14.9.0を経由しないと # 最新版(14.10.2)にできないので、いったん14.9.0に更新する root@gitlab1:~# apt install gitlab-ee=14.9.0-ee.0 # 14.9.0が入ったら最新版へ root@gitlab1:~# apt upgrade
Docker用コンテナ作成
コンテナ(ハコ)作成
LXDコンテナ内でDockerコンテナを動かせるよう、コンテナのネストを許可しておく。
# コンテナ作成 / ネストを許可 user1@debian1:~$ lxc launch images:debian/bullseye docker1 -c security.nesting=true # コンテナの誤削除防止 user1@debian1:~$ lxc config set docker1 security.protection.delete=true
Dockerインストール
公式の手順通りで大丈夫。
Install Docker Engine on Debian | Docker Documentation
user1@debian1:~$ lxc exec docker1 bash
# aptをJPミラーに向けておく root@docker1:~# sed -i.original 's#/deb.debian.org/debian #/ftp.jp.debian.org/debian #g' /etc/apt/sources.list root@docker1:~# apt-get remove docker docker-engine docker.io containerd runc root@docker1:~# apt-get update root@docker1:~# apt-get install ca-certificates curl gnupg lsb-release root@docker1:~# curl -fsSL https://download.docker.com/linux/debian/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg root@docker1:~# echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/debian $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null root@docker1:~# apt-get update root@docker1:~# apt-get install docker-ce docker-ce-cli containerd.io docker-compose-plugin root@docker1:~# docker run --rm hello-world
GitLab Runnerインストール
GitLabのWebUIでRunnersの編集画面にコマンドが書いてあるのでコピペ。
WebUIへログインするためのパスワードはGitLabの新規ユーザ登録を参照。
Menu > Admin > Overview(サイドメニュー) > Runners > Register an instance runner > Show runner installation and registration instructions
gitlab-runner register
指定サンプル:
root@docker1:~# sudo gitlab-runner register --url http://192.0.2.101:1081/ --registration-token b41a-9VWVPFsjiqv-AJB Runtime platform arch=amd64 os=linux pid=5329 revision=f761588f version=14.10.1 Running in system-mode. Enter the GitLab instance URL (for example, https://gitlab.com/): [http://192.0.2.101:1081/]: Enter the registration token: [b41a-9VWVPFsjiqv-AJB]: Enter a description for the runner: [docker1]: Example Runner 1 Enter tags for the runner (comma-separated): Enter optional maintenance note for the runner: Registering runner... succeeded runner=b41a-9VW Enter an executor: docker-ssh+machine, kubernetes, docker, shell, ssh, virtualbox, custom, docker-ssh, parallels, docker+machine: docker Enter the default Docker image (for example, ruby:2.7): debian:stable-slim Runner registered successfully. Feel free to start it, but if it's running already the config should be automatically reloaded!
gitlab-runner
ユーザをdocker
グループに追加
しないとCI実行時にエラー。
dial unix /var/run/docker.sock: connect: permission denied.
root@dockers:~# usermod -aG docker gitlab-runner
gitlab-runner
ユーザの.bash_logout
を無効化
しないとCI実行時にエラー。
ERROR: Job failed: prepare environment: exit status 1. Check https://docs.gitlab.com/runner/shells/index.html#shell-profile-loading for more information
Types of shells supported by GitLab Runner | GitLab
root@dockers:~# mv /home/gitlab-runner/.bash_logout{,.bk}
コンテナレジストリへのHTTPアクセスを許可
しないとCI実行時にエラー。
Error response from daemon: Get "https://192.0.2.101:1086/v2/": http: server gave HTTP response to HTTPS client
Test an insecure registry | Docker Documentation
root@docker1:~# echo '{ "insecure-registries" : ["192.0.2.101:1086"] }' >> /etc/docker/daemon.json root@docker1:~# systemctl restart docker
GitLabの新規ユーザ登録
移行した場合は(たぶん)不要。
インストール直後はユーザがrootしかいないので、rootでWebUIにログインして他のユーザを作る。
rootのパスワードはGitLabをインストールした際に生成されている。
# WARNING: This value is valid only in the following conditions # 1. If provided manually (either via `GITLAB_ROOT_PASSWORD` environment variable or via `gitlab_rails['initial_root_password']` setting in `gitlab.rb`, it was provided before database was seeded for the first time (usually, the first reconfigure run). # 2. Password hasn't been changed manually, either via UI or via command line. # # If the password shown here doesn't work, you must reset the admin password following https://docs.gitlab.com/ee/security/reset_user_password.html#reset-your-root-password. Password: ******************************* # NOTE: This file will be automatically deleted in the first reconfigure run after 24 hours.
メールサーバを立てていない場合、新規ユーザの初期パスワードはどこからも参照できないが、ユーザの編集画面で任意のパスワードにリセットできる。
gitlab signup users without email confirmation - Stack Overflow
これでできあがり。
GitLab - Rustで使う用の.gitlab-ci.yml (Runnerを専有可能な環境向け)
RustなプロジェクトのCIを回す際、ビルドやカバレッジ取得のたびに依存関係がリコンパイルされたり、キャッシュが大きくなったりと色々つらかったので何とかしようと試行錯誤したメモ。
やった事は主に以下二点で、今のところは満足。
一部GitLab Runnerの専有が必要という環境制約あり。
- CIジョブ実行時の
git clean
の対象から./target
を除外する - CIジョブで使用するtarpaulin入りのDockerイメージをあらかじめ作っておく
目次
環境
- GitLab: 14.8.2-ee *1
制約
GitLab Runnerを専有する必要がある
今回のやり方では、Docker executorのvolume上に./target
を保持したままにすることで、volumeそのものをキャッシュ代わりにする。
そのため、共有RunnerのようにCIジョブ実行都度volumeが片付けられてしまう環境では効果がない。
定義ファイル類
配置
イメージをつかむのに必要そうなものだけ書いた。
. |-- .gitlab-ci.yml |-- Cargo.toml |-- Docker | |-- CI | | `-- Dockerfile.rust.ci … CIジョブで使用するimageのビルド用 | `-- Dockerfile.rust … 開発環境用 |-- docker-compose.yml |-- src | `-- main.rs `-- target
.gitlab-ci.yml
パイプラインイメージ
- 各ジョブとも任意のタイミングでGitLabのWebUIから個別に実行できる
- 個別に実行する場合、ビルドとカバレッジ計測は実行時のVariablesとして
CLEAN
を付与することでクリーンビルドできる
.gitlab-ci.yml
variables: CARGO_INCREMENTAL: 0 FF_USE_FASTZIP: "true" # ARTIFACT_COMPRESSION_LEVEL: fast # https://zenn.dev/masakura/articles/1ba1d9ec95cfad # GitLab CI ジョブの高速化 (キャッシュを使わない) GIT_CLEAN_FLAGS: -ffdx -e target/ # CI時に使用するrustのイメージ名 CI_RUST_IMAGE: "$CI_REGISTRY_IMAGE/ci-rust" stages: - build - coverage - rebuild_ci_image image: rust:slim # 通常のフローでは`./target`を掃除できなくなっているので、 # フォールバック手段を用意しておく。 # GitLabのPipeline実行画面で変数`CLEAN`(値は任意)を指定して実行すると、 # ビルド実行前に`./target`を削除できる。 # あと、本体へのマージ時にも一応。 .clean: &clean > [ "${CLEAN:-}" != "" ] || [ "${CI_MERGE_REQUEST_TARGET_BRANCH_NAME:-}" = $CI_DEFAULT_BRANCH ] && echo ----- CLEAN ------------------------------ && rm -rf ./target && ls -la build: stage: build rules: - if: $CI_PIPELINE_SOURCE == "merge_request_event" when: never - if: $REBUILD_RUST_IMAGE when: never - when: always image: $CI_RUST_IMAGE script: # 始めはビルドやテストなどを個別のジョブに細分化していたが、ジョブごとに # キャッシュの圧縮/展開を繰り返してストレージに優しくない感じだったので # 1ジョブにまとめた。 - ls -la - cargo --version - *clean - > : ----- FORMAT ---------------------------- - cargo fmt --version - time cargo fmt -- --check - > : ----- BUILD ----------------------------- - rustc --version - time cargo build - > : ----- LINT ------------------------------ - cargo clippy --version - time cargo clippy - > : ----- BUILD - TEST ---------------------- - time cargo test --no-run - > : ----- TEST ------------------------------ - time cargo test - ls -la coverage: stage: coverage tags: # coverage用のrunnerに振り分ける。 # buildと一緒のrunnerで処理すると毎回リコンパイルされてしまうので。 - coverage rules: - if: $CI_PIPELINE_SOURCE == "merge_request_event" image: $CI_RUST_IMAGE script: - > : ----- TEMP MERGE ---------------------------- # たまに`*** Please tell me who you are.`されるのでダミーを入れておく。 - git config --local user.name "gitlab-runner" - git config --local user.email "gitlab-runner@example.com" - git fetch origin $CI_MERGE_REQUEST_TARGET_BRANCH_NAME # https://ngyuki.hatenablog.com/entry/2020/02/15/205425 # Gitlab CI でマージリクエストのマージ結果でパイプラインを実行する - git checkout "origin/$CI_MERGE_REQUEST_TARGET_BRANCH_NAME" - git merge --squash -v - - git diff --stat --staged - ls -la - *clean - > : ----- COVERAGE ------------------------------ - cargo tarpaulin --version - cargo tarpaulin --skip-clean --out Html Xml --output-dir ./coverage - ls -la coverage: '/^\d+.\d+% coverage/' artifacts: paths: # カバレッジレポートをGitLab Pagesで参照できるようにする。 - coverage/tarpaulin-report.html reports: cobertura: # Merge RequestのChangesタブで変更箇所のカバレッジ状況を参照できるようにする。 - coverage/cobertura.xml expose_as: 'coverage report' rebuild_ci_image: stage: rebuild_ci_image rules: - if: $REBUILD_RUST_IMAGE tags: - rebuild_image script: - ls -la - docker login -u "gitlab-ci-token" -p "$CI_BUILD_TOKEN" $CI_REGISTRY - docker build --pull -t $CI_RUST_IMAGE -f Docker/CI/Dockerfile.rust.ci Docker/CI/ - docker push $CI_RUST_IMAGE
Dockerfile.rust.ci
ビルドやカバレッジ取得で使用するDockerイメージのビルドファイル。
カバレッジ取得はtarpaulinの公式イメージを使うという手もあるのだが、パイプライン内でgit操作したかったり、プロジェクトで使用しているクレートが外部のパッケージに依存していたりするため、個別にDockerイメージを作ることにした。
FROM rust:slim # pkg-config, libssl-dev: # - cargo-tarpaulinのコンパイルに必要 # - プロジェクトで利用しているrequestクレートのコンパイルに必要 # git: # - Merge Requestのパイプライン内で一時的にマージ状態を作るためにgit操作が必要 RUN apt-get update -qq && \ apt-get install -qqy pkg-config libssl-dev git && \ rustup component add rustfmt clippy && \ cargo install cargo-tarpaulin
/etc/gitlab-runner/config.toml
GitLab Runnerをインストールした際に自動生成される定義ファイル。
Docker executorがgit clone
する際にホスト名を名前解決できるように追記しておく。
また、tarpaulinを使用するexecutorには特権が必要になる。
[[runners]] name = "Docker executor for build" executor = "docker" : [runners.docker] volumes = ["/cache"] : + extra_hosts = ["MY_HOST_NAME:192.0.2.1"] + privileged = true # tarpaulinを使用するexecutorのみ
参考リンク
GitLab CI ジョブの高速化 (キャッシュを使わない)
Docker executorのvolumeをキャッシュ代わりにするアイデアの紹介記事。executorが一生懸命
./target
を圧縮・展開してるのを見てう~ん…と思ったり、かと言って./target
配下のキャッシュ対象を選り分けるのも難しそうだし…、などと逡巡していた時にとても助かった。
*1:EEでもライセンスなしの場合はCEと同じ状態になる。
Community EditionとEnterprise Editionの違い | GitLab.JP
Unity - 複数のTerrainに対してプレイヤーに近い順に何か処理する
Unityで、MapMagicが自動生成したTerrainに対して、プレイヤーに近い順に特定の処理をしたいことがあったのでメモ。
なお、今回の記事ではサンプルの題材として「Terrainにオブジェクトを配置する」という内容にしているが、オブジェクトの配置が目的であればMapMagicの公式追加アセット「MapMagic 2 Objects」を使った方がもっと高機能だし楽なはず。
環境
- Unity: 2020.3.33f1 Personal
- MapMagic 2: 2.1.10
できあがりイメージ
画像のリンク先はGoogleDriveに置いたGIF。75MBくらい。
考え方
- 生成されたTerrainから早い者勝ちで処理しようとすると割とばらつきが出る。
遠くのTerrainが先に処理されて、近くのTerrainがしばらく待たされたり。 - なので、プレイヤーの目に付きやすい近くのTerrainから先に処理したい。
遠くのTerrainはあまり見えないので後からゆっくりやってくれればいい。
- Terrainを1枚生成し終わったら、そのTerrainに対して処理したいタスクを生成してキューに入れる。
- キューから取り出す際にプレイヤーと各Terrainの距離を測定し、最も近くにあるTerrainのタスクを取り出して実行する。
- 優先順位は取り出し時に決める。
キューに入れる時に決めると、プレイヤーが高速で移動した場合やセーブデータのロードなどで一気に遠くのTerrainへ移動した場合に、元いたTerrainのタスクが高優先扱いのまま残ってしまう。 - 古いエントリを削除してしまうと、元のTerrain方面へ引き返した際にそのタスクを実行できないので、優先順位が低い状態で残ってほしい。
サンプルコード
各クラスの関連と流れ
- MapMagicが発行するOnAllCompleteイベント(Terrain生成完了)を検知し、タスクを生成してタスクキューに登録する
- タスクキューは内部的に、距離測定用のクラスでタスクをラップしてからキューに入れる
- 取り出し時はキュー内の全エントリを走査して各Terrainとプレイヤーとの距離を測定し、最も近いエントリを取り出す
- 取り出したタスクを実行する
TerrainEventHandler
using UnityEngine; using MapMagic.Core; using MapMagic.Terrains; public class TerrainEventHandler : MonoBehaviour { private Transform _objects; private Transform tile => transform.parent; void OnEnable() { // 遠距離用の`/MapMagic/Tile *,*/Draft Terrain`や、コピー元の`/MapMagic`は対象外。 // 近距離用の`/MapMagic/Tile *,*/Main Terrain`だけ処理したい。 // e.g. `Main Terrain@Tile 0,0` var nameWithParent = $"{name}@{tile?.name}"; if (!nameWithParent.StartsWith("Main Terrain@Tile ")) { return; } _objects = tile.Find("Objects"); TerrainTile.OnAllComplete -= CreateMapObjects; TerrainTile.OnAllComplete += CreateMapObjects; } void OnDisable() { TerrainTile.OnAllComplete -= CreateMapObjects; } public void CreateMapObjects(MapMagicObject terrain) { // オブジェクトを生成する時は、`Tile *.*/Objects`配下に生成する。 // ここに既にオブジェクトが生成されていた場合は、処理済みのTerrainとみなしてスキップ。 var alreadyGenerated = (_objects == null || _objects.transform.childCount > 0); if (alreadyGenerated) { return; } var task = new TerrainTask(tile); TerrainTaskManager.Enqueue(task); TerrainTile.OnAllComplete -= CreateMapObjects; } }
TerrainTaskManager
using System.Collections; using System.Collections.Generic; using System.Linq; using UnityEngine; public class TerrainTaskManager : MonoBehaviour { [SerializeField] private float _pollingInterval = 0.1f; private static LinkedList<TerrainTaskQueueEntry> _queue = new LinkedList<TerrainTaskQueueEntry>(); void Start() { StartCoroutine(PollQueue()); } private IEnumerator PollQueue() { while (true) { var entry = Dequeue(); if (entry != null) { yield return entry.Run(); } yield return new WaitForSecondsRealtime(_pollingInterval); } } public static void Enqueue(TerrainTask task) { // Destroyと生成のギリギリの位置で行ったり来たりすると、そのTerrainに対するタスクが // 実行されない内に(距離が遠いので優先度が低い)どんどん新しくキューイングされてしまうので、 // キューイング済みならスキップする。 var alreadyQueued = _queue.Any(e => e.Name == task._name); if (alreadyQueued) { return; } var entry = new TerrainTaskQueueEntry(task); _queue.AddLast(entry); } private static TerrainTaskQueueEntry Dequeue() { if (_queue.Count < 1) { return null; } // 最短距離にあるTerrainに対するタスクを探す var closestTask = _queue.Aggregate((cur, nxt) => cur.Closer(nxt)); _queue.Remove(closestTask); return closestTask; } }
TerrainTaskQueueEntry
タスク側に含めてしまってもよかったが、散らかった感じになるかなと思って分けた。
using System.Collections; using UnityEngine; public class TerrainTaskQueueEntry { public TerrainTask _task; public string Name => _task._name; public IEnumerator Run() => _task.Run(); public TerrainTaskQueueEntry(TerrainTask task) { this._task = task; } public TerrainTaskQueueEntry Closer(TerrainTaskQueueEntry another) { var fromThis = this.DistanceToPlayer(); var fromAnother = another.DistanceToPlayer(); var closer = (fromThis < fromAnother) ? this : another; return closer; } private float DistanceToPlayer() { // プレイヤーの移動によってTerrainがDestroyされている場合がある if (!_task._tile) { return float.MaxValue; } var playerPos = GlobalRefs.Player.transform.position; var terrainPos = _task._tile.transform.position + GlobalRefs.TileCenterGap; return Vector3.Distance(terrainPos, playerPos); } }
TerrainTask
今回はSphereをいくつか生成するだけ。
using System.Collections; using System.Linq; using UnityEngine; public class TerrainTask { public Transform _tile { get; private set; } public string _name { get; private set; } private Transform _objectsParent; public TerrainTask(Transform parentTile) { _tile = parentTile; _name = $"{_tile.name}/{_tile.GetComponentInChildren<Terrain>(false).name}"; _objectsParent = _tile.Find("Objects"); } public IEnumerator Run() { Debug.Log($"ここでTerrainに対する処理。アイテム置いたりとか。 @{_name}"); var something = Enumerable.Range(0, 5).ToArray(); foreach (var i in something) { // プレイヤーの移動によってTerrainがDestroyされている場合がある if (!_tile) break; CreateDummySphere(); yield return null; } } private void CreateDummySphere() { var sphere = GameObject.CreatePrimitive(PrimitiveType.Sphere); sphere.transform.parent = _objectsParent; var rndX = Random.Range(250, 750); var rndZ = Random.Range(250, 750); // 簡単のためY座標は固定値 sphere.transform.localPosition = new Vector3(rndX, 100, rndZ); sphere.transform.localScale = Vector3.one * 200; } }
GlobalRefs
距離計算する際に毎回GameObject.Find
するのはつらいので、取得結果を持っててもらう。
今回の記事の内容とはあまり関係ないので上の図には描いていないが、ソースコピペで動かすレベルでも負荷上がってしまうかなと思ったので一応。
using UnityEngine; using MapMagic.Core; public class GlobalRefs { private static GameObject _player; public static GameObject Player { get { if (!_player) { _player = GameObject.FindWithTag("Player"); } return _player; } } // MapMagicのTerrain1枚の大きさ。 // Playerとの距離を測る際にTerrainの中心を求める用。 private static Vector3 _tileCenterGap = Vector3.up; // Y is never used public static Vector3 TileCenterGap { get { if (_tileCenterGap == Vector3.up) { // デフォルトだと1000*1000のはず。 // MapMagicの内部の値なのであまり深入りしない方がいいかも。 var tileSize = GameObject.Find("MapMagic") .GetComponent<MapMagicObject>().tileSize; _tileCenterGap = new Vector3(tileSize.x / 2, 0, tileSize.z / 2); } return _tileCenterGap; } } }
スクリプトのアタッチ
TerrainEventHandler
MapMagicObjectコンポーネントと同じオブジェクトにアタッチする。
併せて、MapMagicObjectコンポーネントのCopy Components to Terrains
をONにする。
これで自動生成されたTerrainにもこのスクリプトをコピーしてくれる。
TerrainTaskManager
ルート辺りにEmptyを作ってアタッチする。
キューを監視する部分が動き出してくれればいいだけなので、どこでもよい。
今回はルートにScripts
という名前のEmptyを作ってそこに付けた。
使用したアセット
- MapMagic 2 | Terrain | Unity Asset Store
あらかじめパラメータを設定しておくことで動的にTerrainを生成してくれるアセット。
実行中にどんどん生成してくれるのでどこまでも歩いて行ける。
ノイズで地形をデコボコさせたり、草を生やしたり地表テクスチャをブレンドしたりしていい感じのTerrainを作ってくれる。
動作も軽い。
基本的な部分は無料で、追加機能としていくつかの有料アセットがある。
Unity - スクリプトでTerrainに草を生やす
前回Terrainに自動で木を植えたので下草も生やしたいなと思って調べたが、やっぱり情報があんまりない感じだったので書く。
環境
- Unity: 2020.3.26f1 Personal
- Universal RP: 10.8.1
できあがりイメージ
考え方
木の時とだいたい同じ。
こちらはTerrainの座標に対応する配列にフラグ値を詰めていく形。
- 生成する草のPrefabをDetailPrototypeにセットする
- Terrain上の座標に対応する二次元配列(
int[,]
)に、草を生やすかどうかのフラグ値を詰めていく0
… 生やさない1
以上 … 生やす
- 詰め終わった二次元配列をTerrainDataにセットすると草が生成される
サンプルスクリプト
以下のスクリプトをTerrainオブジェクトにくっつけ、Inspectorで草のTexture2Dを設定すればPlayした時に自動的に草を生成する。
TerrainGrassGenerator .cs
using System.Linq; using UnityEngine; public class TerrainGrassGenerator : MonoBehaviour { public Texture2D _grassTexture; [Range(0, 100)] public int _density = 50; void Start() { Terrain terrain = GetComponent<Terrain>(); GenerateTerrainGrass(terrain); } private void GenerateTerrainGrass(Terrain terrain) { var terrainData = terrain.terrainData; // prototypeTextureはnullでもエラーにならなくて分かりづらいので例外投げとく if (_grassTexture == null) { throw new MissingReferenceException("コンポーネントに草が付いてないよ!"); } // すべての草の元ネタ terrainData.detailPrototypes = new DetailPrototype[] { new DetailPrototype() { prototypeTexture = _grassTexture, renderMode = DetailRenderMode.GrassBillboard } }; // デフォルトは1024のはず var detailResolution = terrainData.detailResolution; var noise = new CreationNoiseGener(detailResolution, _density); // 草の生成状態を数値で表す四角形配列 / 1以上の値が入っている座標に草が生える var detailMap = new int[detailResolution, detailResolution]; var detailPoints = Enumerable.Range(0, detailResolution).ToList(); detailPoints.AsParallel().ForAll(x => { detailPoints.Where(z => noise.ShouldCreate(x, z)) .ToList().ForEach(z => { detailMap[x, z] = 1; }); }); terrainData.SetDetailLayer(0, 0, 0, detailMap); } /// <summary> /// 各座標にオブジェクトを生成するかランダム判定する用ユーティリティ /// </summary> private class CreationNoiseGener { private int[] _randAtPoint; private int _threshold; private int _resolution; public CreationNoiseGener(int resolution, int density) { _threshold = 100 - density; _resolution = resolution; var ints = GenerateRandomInts(resolution * resolution); _randAtPoint = ints.Select(e => e * 100 / byte.MaxValue).ToArray(); } public bool ShouldCreate(int x, int z) { return _threshold < _randAtPoint[x * _resolution + z]; } private int[] GenerateRandomInts(int size) { var bytes = new byte[size]; new System.Random().NextBytes(bytes); var ints = new int[size]; bytes.CopyTo(ints, 0); return ints; } } }
参考リンク
Placing grass on terrain in script on a certain height - Unity Answers
Removing grass from a terrain in Unity | Independent Software