Minecraft サーバを GCE 上に建てるコマンド

僕は Minecraft を遊んだことはないが最近知人からマイクラサーバを建ててほしいと頼まれたのでドキュメントを諸々読みながら GCE 上に建ててみた。

コマンド自体は Minecraft に限らず応用が効くと思うので将来のためにメモっておく。

最低限

CONTAINER_IMAGE="itzg/minecraft-server:java8"
INSTANCE_NAME="minecraft"
MACHINE_TYPE="e2-highcpu-2"
ZONE="asia-northeast1-a"
MEMORY="1500M"
DISK_AUTO_DELETE="no"

CONTAINER_ENV="\
EULA=TRUE,\
TZ=Asia/Tokyo,
MEMORY=$MEMORY\
"

gcloud compute instances create-with-container $INSTANCE_NAME \
  --container-image $CONTAINER_IMAGE \
  --machine-type $MACHINE_TYPE \
  --zone $ZONE \
  --container-env $CONTAINER_ENV \
  --create-disk name=$INSTANCE_NAME-data,device-name=$INSTANCE_NAME-data,size=10GB,auto-delete=$DISK_AUTO_DELETE \
  --container-mount-disk mount-path="/data",name=$INSTANCE_NAME-data \
  --tags minecraft-server

gcloud compute firewall-rules create allow-minecraft \
  --allow tcp:25565 --target-tags minecraft-server

MACHINE_TYPEZONEMEMORY あたりは要件に応じて変えてください)

コマンドの意味は gcloud CLI リファレンス 1 を見れば全部書いてあるけれど順に解説していく。

create-with-container

まずサーバを建てるときに Docker イメージが公開されていたら嬉しいなと思って検索したら既に公開してくれている先人がいたので感謝しかないですね:

github.com

Tag に latest を指定するとこの記事を書いている時点では Java 11 でサーバが起動するようだが個人的に latest よりもバージョンは明示的に指定したいと思っているのと Java 15 は LTS ではないようなので java8 にした (筆者は Minecraft にも Java にも詳しくないという事情もある)。

GCE では「Docker コンテナをデプロイして起動するように仮想マシンVMインスタンスまたはインスタンス テンプレートを構成でき」る 2 ので、これを使いましょう。 そのコマンドが

gcloud compute instances create-with-container INSTANCE_NAME \
  --container-image CONTAINER_IMAGE

です。コマンドのリファレンスはこちら:

https://cloud.google.com/sdk/gcloud/reference/compute/instances/create-with-container

ちなみに各 VM インスタンスにデプロイできるコンテナは 1 つのみという制限 3 があり、 複数コンテナをデプロイしたい場合は Kubernetes (GKE) を使ってくださいということらしい。

さてこれでサーバは起動するのですが Minecraft はゲームなのでゲームデータという状態を持ち、その状態はコンテナ外のどこかに永続化したい。

上記の minecraft-server の README を読んでみるとホスト側のデータ保存用ディレクトリをコンテナの /data ディレクトリにマウントすれば良いと書いてあり、 gcloud compute instances create-with-container コマンドでは --create-disk オプションを指定するとインスタンスの作成時と同時に永続ディスクを作成してインスタンスにアタッチすることができ、さらに --container-mount-disk オプションを一緒に指定することでその作成したディスクをコンテナの指定した path にマウントできる、のでそいつを使おうという寸法です。

ここで注意すべき点としては --container-mount-disk でマウントするディスクはそのディスク名と device-name(そのディスクの、OS が認識する名前(?)。詳しいことはよくわからない……)が同じでなければならない、ということです (これらが異なるとマウントに失敗します)。 なので --create-disk 時に明示的に namedevice-name に同じに名前を指定しています。

また auto-delete はこのインスタンスが削除されたときに自動でこの永続ディスクも一緒に削除するかどうかを指定するフラグです。 これを no に指定することで、誤ってインスタンスを削除してしまい今まで作ったワールドデータが全部吹っ飛んでしまう、というような事故を防ぎます。 デフォルト値は yes なので、検証用に別のサーバを一時的に建てるというような場合は auto-delete を指定する必要はないかもしれません。

--tags は後述の firewall-rules で説明します。

firewall-rules

無事マイクラサーバが建ってもクライアント側から接続できないと意味が無いので、マイクラ用のポートだけファイアウォールに穴を開けたいと思います。

さてガイドが存在して:

cloud.google.com

コンテナポートには、ホスト VM ポートへの 1 対 1 のマッピングがあります。たとえば、コンテナポート 80 はホスト VM ポート 80 にマップされます。Compute Engine ではポートの公開(-p)フラグをサポートしていないため、このフラグを指定しなくても、マッピングは機能します。

コンテナのポートを公開するには、ホスト VM インスタンスのポートへのアクセスを許可するようにファイアウォール ルールを構成します。コンテナの対応するポートには、ファイアウォールのルールに従って自動的にアクセスできるようになります。

具体的には create-with-container 時に --tags オプションでインスタンスにタグを付け、firewall-rules createファイアウォールルールを作成し、そのルールを適用したい対象インスタンスのタグを --target-tags オプションで指定すれば良いというわけですね。

https://cloud.google.com/sdk/gcloud/reference/compute/firewall-rules/create#--target-tags

Minecraft はデフォルトで 25565 番ポートを使用するようなのでそのポートのトラフィックを許可することにします。

これで無事マイクラサーバが順当に建ち、クライアントから接続して遊べるようになるでしょう。

--container-env についてもう少し

Minecraft には op 権限を持つユーザという概念があるらしく、上述の minecraft-server Docker image では OPS 環境変数Minecraft ユーザ名をカンマ区切りで指定することで op 権限を付与することができる 4 ので、 必要に応じて CONTAINER_ENV に追加されたし。

また Minecraft 界隈ではサードパーティによる独自拡張である "mod" が多数公開されていて、複数の mods をクライアント・サーバに配置して遊ぶことがよく行われている(らしい)。 上述の minecraft-server Docker image では MODS 環境変数に jar ファイルの URL をカンマ区切りで指定するとコンテナ起動時に自動でその jar ファイルをダウンロードしてくれる 5 (便利ですね)。

ここで読者諸賢は --container-env=[KEY=VALUE, …,…] という構文中で VALUE としてナイーブにカンマを使うと KEY=VALUE の組を分ける意味のカンマと衝突し構文エラーになることに気付くであろう。 実際 create-with-container しようとすると以下のようにエラーが表示される:

ERROR: (gcloud.compute.instances.create-with-container) argument --container-env: Bad syntax for dict arg: [...]. Please see `gcloud topic flags-file` or `gcloud topic escaping` for information on providing list or dictionary flag values with special characters.

エラーメッセージが親切にも案内してくれているように gcloud topic escaping を読んでみましょう:

https://cloud.google.com/sdk/gcloud/reference/topic/escaping

……なるほど!!!つまり --container-env=^DELIM^ と書くことで各 KEY=VALUE 同士の delemeter をカンマではなく DELIM に変更することができるので、その上で VALUE にカンマを含めることができるというわけですね。 DELIM: とか - のような一文字だけでなく :: とか -- のような複数文字も許されています。MODS に URL を指定することを考えると :: あたりが良い選択肢ではないでしょうか。

具体例として今回僕は Buildcraft, Storage Drawers, Packing Tape という mod を入れることにしたので、 CONTAINER_ENV は以下のようになりました:

CONTAINER_ENV="\
^::^\
EULA=TRUE::\
TZ=Asia/Tokyo::\
MEMORY=$MEMORY::\
TYPE=FORGE::\
MODS=\
https://edge.forgecdn.net/files/2502/739/buildcraft-7.1.23.jar,\
https://github.com/jaquadro/StorageDrawers/releases/download/sd-1.10.8/StorageDrawers-1.7.10-1.10.8.jar,\
https://github.com/gigaherz/PackingTape/releases/download/v0.3.5/Packing.Tape-0.3.5.jar::\
VERSION=1.7.10::\
OPS=pione30,foo_user,bar_user\
"

おわりに

具体例として Minecraft の話をしましたが Docker Hub に image が公開されていて環境変数の指定だけで設定を変えてスッと起動できる場合であればコマンド数本で手軽に GCE にインスタンスを建てられるんだな〜ということがわかったので良い収穫でした。

やはり Docker image を起点としてなるべく状態を持たないように起動できると検証用にサーバを建てたり壊したりするのが簡単かつ気軽にできて良いですね。

GitHub Actions の if, 否定演算子, そして YAML

GitHub Actions のワークフロー構文に jobs.<job_id>.if 1jobs.<job_id>.steps[*].if 2 というものがあり 、その job や step の実行を if に書かれた条件が満たされた場合だけに限定することができます。

また GitHub Actions のワークフロー構文として "expression syntax" (式構文)3 というものが存在します。 これは ${{ <expression> }} と書くと <expression> 部分が文字列ではなく式として評価されるようになる、という特殊な構文です。

そしてドキュメントの該当部分 4 を読むと面白いことが書いてあります:

When you use expressions in an if conditional, you may omit the expression syntax (${{ }}) because GitHub automatically evaluates the if conditional as an expression.

(日本語訳) 5

if 条件の中で式を使用する際には、式構文 (${{ }})を省略できます。これは、GitHubif 条件を式として自動的に評価するためです。

興味深いですね。つまり例えばこういう YAML が書けるわけです:

name: CI

on:
  pull_request:
    types:
      - opened
      - reopened
      - synchronize

jobs:
  if-omit-expression-syntax-test:
    runs-on: ubuntu-20.04
    steps:
      - name: Explicit expression syntax
        if: ${{ github.actor == 'pione30' }}
        run: echo "Explicitly github.actor is pione30"
      - name: Expression syntax omitted
        if: github.actor == 'pione30'
        run: echo "github.actor is pione30 even though the expression syntax omitted"

上記の steps はどちらも syntax error など起こさず実行されます:

https://github.com/pione30/github-actions-sandbox/runs/1800678250

ところで GitHub Actions の式の中において利用できる演算子があり、否定演算子 ! もあります 6

そこで例えばこんな steps を追加するとどうでしょうか:

name: CI

on:
  pull_request:
    types:
      - opened
      - reopened
      - synchronize

jobs:
  if-omit-expression-syntax-test:
    runs-on: ubuntu-20.04
    steps:
      - name: Explicit expression syntax
        if: ${{ github.actor == 'pione30' }}
        run: echo "Explicitly github.actor is pione30"
      - name: Expression syntax omitted
        if: github.actor == 'pione30'
        run: echo "github.actor is pione30 even though the expression syntax omitted"
      # 以下の steps を追加
      - name: Not operator on the head in the explicit expression syntax
        if: ${{ !startsWith(github.head_ref, 'pione30/') }}
        run: echo "Explicitly github.head_ref does NOT startsWith 'pione30/'"
      - name: Not operator on the head without the expression syntax
        if: !startsWith(github.head_ref, 'pione30/')
        run: echo "github.head_ref does NOT startsWith 'pione30/'"

……残念ながらこのワークフローは実行されません。最後の step の if: !startsWith(github.head_ref, 'pione30/') の部分で "You have an error in your yaml syntax on line 24" と言われてしまいます:

https://github.com/pione30/github-actions-sandbox/actions/runs/525066684

どうしてでしょうか?わからないので YAML の仕様書を見に行きましょう。

まず Preview をざっと眺めると Tags 7 という節が目に留まります。 YAML のノード(スカラー、シーケンス、マッピングのいずれか 8 )は ! 記号を使って明示的に型を指定することができる、例えば !something とか !!str とかのように、と書かれています。

もう少し詳しく BNF による形式的定義 9, 10 を追っていくと、タグは

  • Verbatim Tag
  • Tag Shorthand
  • Non-Specific Tag

の 3 種類に大別されることがわかります。


Verbatim Tag

!< URI として利用可能な文字列 > の形式。 !<tag:yaml.org,2002:str> が例として挙げられています。

Tag Shorthand

Tag Shorthand は先頭が Tag Handle、その後 Tag Char の連続と記されています。 Tag Handle は !, !! のいずれか 11 、Tag Char は URI として利用可能な文字セットから !, ,, [, ], {, } を除いたもの 12 です。

(正確には Tag Handle にはさらに !英数字ハイフンの連続! という形式も含まれますが、Tag Shorthand として使うためには TAG ディレクティブで指定する必要があり、今回は説明を省きます。)

例としては !foo !local !!int !!str とかがあります。

Non-Specific Tag

ただの ! が単一でタグになります。そのノードは強制的に tag:yaml.org,2002:seq, tag:yaml.org,2002:map, tag:yaml.org,2002:str のいずれかの型として解釈されます。


というわけで、式構文 (${{ }})を省略した上で否定演算子のつもりで先頭に ! をつけると YAML の仕様によりラベルとして解釈されてしまうことがわかりました。 GitHub Actions のドキュメントには明記されていないのですが意外とハマりポイントではないでしょうか 13

上記の !startsWith(github.head_ref, 'pione30/') を解釈しようとすると、Tag Char には , が含まれないので , の前 !startsWith(github.head_ref までが Tag Shorthand としてパースされるが、その直後に(Collection の開始記号 [ or { およびエントリー無しに), が来てしまうので syntax error となったのですね 14YAML 初心者なので間違っていたら教えてください)。

そうすると、否定演算子としての意味は無くなってしまいますが

if: !<tag:yaml.org,2002:str> startsWith(github.head_ref, 'pione30/')

とか

if: !!str always()

とか書いても構文的には合法な気がしたので試してみたのですが、"The workflow is not valid. .github/workflows/ci.yml: Unexpected tag 'tag:yaml.org,2002:str'" と怒られてしまいました。僕にはもうよくわかりません。

おわりに

GitHub Actions、ロジックを記述するのになんで YAML の構文で書くことになってしまったのか???

一年の振り返り的サムシング

なんとなく振り返りたくなったので振り返ってみる……

COVID-19 と在宅勤務みたいな話はもう前提だと思うので省く。

ゲーム

Baba Is You

switch で配信されて 2 月ぐらいまでやってた:

store-jp.nintendo.com

面白かったな〜。会社の Slack にも Baba の絵文字が追加されたので多用していました。

スーパーマリオ 3Dコレクション

www.nintendo.co.jp

スーパーマリオ64やっぱおもしれ〜というのは安定としてマリオサンシャイン未プレイだったんですがこっちもめちゃおもしれ〜となりました。モンテ族の人々が基本的に相手を信用しない話しぶりをするのでなんや不必要に感じ悪いなあと思ったけどそれ以外はゲームの難易度が高くて全く飽きずに楽しめました。シャインは 117 枚集めたんですが青コイン探しだけが残った状態になってしまっているので、攻略情報を見て単に回収していくのはそもそもつまらないし、しかしノーヒントでしらみつぶしにやっていく作業もつらそうだしな〜、とりあえずもう良いか……となり一旦やめにしました。

プログラミング言語と型

社では諸々あるんですが基本的には RoR の面倒を見るお仕事をやっていました。

………

やっていたんですが、しかし何年も前からわかっていたこととして僕は非常に注意力散漫でうっかりミスが多い人間なのでコンパイラの支援無しにコードを書くという行為がかなり辛い。

スコープに存在しない変数名とか存在しないメソッド名とかを間違って書いてしまっていたらコンパイル時に静的にエラーになってほしい……という、生命の素朴な欲求が満たされない環境にずっと晒されて、…とも言いたくはあるがしかしこれはただ僕の脳がこの言語に向いてないだけだとは思います。

Ruby はクソ、Rails は死んだ」と口で言うのは簡単なのですが、一般論として、ある対象の問題を指摘する際はその問題を独立して抽出してから批判を加えなければ筋の通った論として成立しません。とりあえず RubyRails に対する感情的な部分は無視するとして、Rails による苦しみは Ruby に型が付けば解決するのか?マヌケな NoMethodError が実行時(!)に発生するのは真に「動的型付け」のせいであると言えるのか?静的型付けは何が本質なのか?型システムによって何が静的に保証され何が保証できないのか……要するに少なくともまず型システムの知見を持っていなければ「動的型付け」言語に対してまっとうな発言ができないわけです。

(念のため書いておくと、仮に僕が型システムや型理論を完全に理解していたとしても「Ruby はクソ、Rails は死んだ」という発言を陽にすることはありません……RubyRails が好きな人は世界中にいるし、わざわざ水を差したり喧嘩を売ったりする必要なんてどこにもありません。それに社内にも、同じチーム内にも RubyRails が好きな人がたくさんいます(というかチーム内では僕以外全員好きだと思う)。暴言を吐くことで一緒に働く仲間とのチームビルディングを台無しにする、あるいは生産性を阻害する……明確にマイナスの影響しかもたらさない行為を良く思う人はいないでしょう。

たとえば Rust という良い側面をいくつも兼ね備えた言語がありますが、Rust を称賛するときに「Ruby には◯◯が無くてダメなんですが〜」みたいな枕詞を添える必要も別に無いわけです。これは単に品性の問題な気がしますが……それに自分が知らないだけで実は Ruby でもできることかもしれないし。いずれにせよそれは発言者がいわゆる「ダメな反対派」になってしまうだけで、称賛している対象のものの評価を逆に下げてしまうことになります。……なんか自明なことばかり無駄につらつら書き続けているだけな気持ちになってきたのでもうやめます。)

とにかく型の勉強をしたことがなかったので勉強したいな〜となり、「プログラミング言語の基礎概念」という教科書を読みました:

from:pione30 プログラミング言語の基礎概念 - Twitter Search

導出木が横に広がるので久々にキャンパスノートとペンを使って問題を解くという営みをやって楽しかったです。

その後 TaPL を購入して第Ⅰ部まで読んだところで気が抜けて積んでしまってる。またぼちぼち続きを読もうかな。

それで趣味ではサーバサイドを Rust (warp + diesel) で書くなどをして呼吸をしつつ、仕事ではここ 1 ヶ月くらいは関わっている Web サービスのフロントエンドに React (+ TypeScript) を導入して、とある画面を React で書き換えることを(メイン業務の合間を縫って)やっていました。

これは個人的な秘めたる思いとして Web 開発の領域でどうにか型をゲットしたい、となると TypeScript がとりあえずお手軽でコンセンサスも得やすい、かつ TypeScript と相性が良く実績もあるライブラリというと React だろうという気持ちで虎視眈々と機会を伺っていたところ、諸々があってチーム内に「宣言的 UI 最高!宣言的にもっと UI を書きたい!」と思ってくれる方が急に増えたので、独り善がりでなくやっていける環境が発生した感じです。

TypeScript の型システムは健全性に関して gotcha がたくさんあるという話もよく聞くしそういう記事もよく見るので今後の僕の精神状況がどう変化するかはまだなんとも言えませんが、現時点ではやっと辛うじて息継ぎができるようになったというのが率直な気持ちです。なのでしばらくやっていくつもりです。

正直それまで「現時点でも問題なく動いていて変更もあまり入らない JS のコードをあえて書き換えることに意味はあるのか」と問われたときに明確に回答できない……と自問不自答(?)して意気消沈することが何度かあったところを勢いで書き換えたのですが、予想しなかったその後の副次的な効果として「React ならアドバイスできるよ」と言ってくれる社内の先輩が数人どこからともなく現れて Slack のチャンネルやリポジトリにコメントを残していってくれるようになり、こんなところで言うのもアレですが感謝しかないッスね…… というか現在進行系で人気のある技術についてはそのぶん知見を持った人の数が相対的に多くて、最新の知見がインターネット上に多く存在したり、関連ライブラリが良くメンテナンスされていたりして、するとプロダクトに付加価値をつける新たなアイデアを実装するのも楽になるわけだなあと今さらながら気づきました。コミュニティに人が多いということはすなわちパワーだということを感じる……

来年以降は静的に型が付くとか型が推論されるような環境は前提にして、より発展的な型の話題の知見をためていきたいと思っています。

ピアノ

夏頃に電子ピアノとブルグミュラー 25の練習曲を買った。全然練習できていない。身体がもう一個欲しいですね。