2022年11月6日日曜日

docker-registry-proxy でコンテナイメージをキャッシュする

色々実験していると、各種 Container Registry のレートリミットが怖くなってくるので、ローカルにキャッシュサーバーを建てることにした。

ただ、Dockerfile や Manifest 内のイメージ URL を変更するのは辛いので、透過的にキャッシュを取得する方法を実現したい。

apt-cacher-ng を知っている方は、「それのコンテナイメージ版が欲しい」という感じで理解していただければ OK。

rpardini/docker-registry-proxy が、Man in the Middle 方式でキャッシュしてくれるようなので、それを試す。

前提

  • OS: Windows 11 Pro 21H2 ビルド 22000.1042
  • Docker Desktop: 4.12.0 (85629)

docker-registry-proxy のコンテナを起動

今回は、Docker Compose で建てることにした。 以下 yaml ファイルを作成し、 docker compose up -d する。

version: '3'

services:
  docker-registry-proxy:
    image: rpardini/docker-registry-proxy:0.6.4
    restart: always
    environment:
      # k8s のマニフェストキャッシュ機能を有効化
      ENABLE_MANIFEST_CACHE: true
      # `registry-1.docker.io` 以外にキャッシュしたいレジストリをスペース区切りで記載
      REGISTRIES: "k8s.gcr.io gcr.io quay.io"
    volumes:
      # キャッシュデータ格納用ディレクトリ
      - docker-registry-proxy:/docker_mirror_cache
      # 証明書関連データ格納ディレクトリ
      - docker-registry-proxy-ca:/ca
    ports:
      - 0.0.0.0:3128:3128
volumes:
  # キャッシュデータ格納用ボリューム
  docker-registry-proxy:
    name: docker-registry-proxy
  # 証明書関連データ格納ボリューム
  docker-registry-proxy-ca:
    name: docker-registry-proxy-ca

Windows の設定

ファイアウォールに穴をあける

今回は、同じネットワークの他ホストからも参照させたいので、 TCP 3128 の受信を許可する。

docker-registry-proxy の証明書インストール

docker-registry-proxy の Web サーバーから取得できるので、 Invoke-WebRequest で取得して、 Import-Certificate でインストールする。

# docker-registry-proxy の証明書を取得
Invoke-WebRequest -Uri http://localhost:3128/ca.crt -OutFile ca.crt

# 取得した証明書を Windows に、 `信頼されたルート証明機関` としてインストール。
Import-Certificate -FilePath .\ca.crt -CertStoreLocation Cert:\CurrentUser\Root

セキュリティ警告 のダイアログが出て、 この証明書をインストールしますか? と聞かれるので はい(Y) を押下。

証明書のインストール後、 Docker Desktop を再起動する。

Docker Engine の設定

プロキシ設定

  1. タスクトレイの Docker アイコン右クリック -> Settings -> Resources -> Proxies と選択し、必要な設定を入力後、 Apply & Restart ボタンを押下
    • Manual proxy configuration: オンにする
    • Web Server (HTTP):http://localhost:3128`
    • Web Server (HTTPS):http://localhost:3128`

docker info コマンドで、プロキシ設定が有効になっていることを確認できる。

Docker Desktop 再起動

Apply & Restart では、証明書の再読み込みまでしてくれないらしいので、 Docker Desktop の再起動を行う。

タスクトレイの Docker アイコン右クリック -> Restart 押下。

動作確認

Measure-Command で docker pull の時間を計ってみる。

docker image rmi hello-world
docker image prune

Measure-Command {docker pull hello-world}
# => TotalSeconds      : 5.4094275

docker image rmi hello-world
docker image prune

Measure-Command {docker pull hello-world}
# => TotalSeconds      : 2.0318131

docker compose logs にも、ミスキャッシュしてからヒットしたというログが残っている。

> docker compose logs
...(略)
tmp-docker-registry-proxy-1  | {"access_time":"02/Oct/2022:18:46:34 +0000","upstream_cache_status":"MISS","method":"HEAD
","uri":"/v2/library/hello-world/manifests/latest","request_type":"manifest-default","status":"200","bytes_sent":"0","up
stream_response_time":"0.720","host":"registry-1.docker.io","proxy_host":"registry-1.docker.io","upstream":"34.205.13.15
4:443"}
...(略)
tmp-docker-registry-proxy-1  | {"access_time":"02/Oct/2022:18:47:20 +0000","upstream_cache_status":"HIT","method":"HEAD"
,"uri":"/v2/library/hello-world/manifests/latest","request_type":"manifest-default","status":"200","bytes_sent":"0","ups
tream_response_time":"","host":"registry-1.docker.io","proxy_host":"registry-1.docker.io","upstream":""}
...(略)

キャッシュディレクトリにデータも入っている。

> docker compose exec docker-registry-proxy find /docker_mirror_cache
/docker_mirror_cache
/docker_mirror_cache/a
/docker_mirror_cache/a/47
/docker_mirror_cache/a/47/d0862d346f039b9a238728e97237d47a
/docker_mirror_cache/7
/docker_mirror_cache/7/c4
/docker_mirror_cache/7/c4/d2c014ee5630f39c4a0fb8a1ce8d5c47
/docker_mirror_cache/5
/docker_mirror_cache/5/cf
/docker_mirror_cache/5/cf/e44914286ca296d3a45dc06185d03cf5
/docker_mirror_cache/d
/docker_mirror_cache/d/36
/docker_mirror_cache/d/36/7a4ad43a77fb3fc1a5cb477b7e2f136d
/docker_mirror_cache/c
/docker_mirror_cache/c/71
/docker_mirror_cache/c/71/27629938b7c2102ec0b9185900e4871c

良さそう、以上。

Windows 以外でのプロキシ設定方法

systemd でマシンごとに設定する方法と、 ~/.docker/config.json でユーザーごとに設定する方法がある。

systemd でマシンごとに設定する方法

systemd の docker.service 向けの設定ファイルを作成し、その中で環境変数を渡す。

docker.service 用の設定ファイル格納ディレクトリを作成

sudo mkdir /etc/systemd/system/docker.service.d

作成したディレクトリ内に設定ファイルを作成

今回は http-proxy.conf という名前で作成。

[Service]
Environment=HTTP_PROXY=http://192.168.2.101:3128/
Environment=HTTPS_PROXY=http://192.168.2.101:3128/

Docker サービスリスタート

sudo systemctl daemon-reload
sudo systemctl restart docker.service

~/.docker/config.json でユーザーごとに設定する方法

~/.docker/config.json を作成し、以下のように設定する。

{
 "proxies":
 {
   "default":
   {
     "httpProxy": "http://192.168.1.12:3128",
     "httpsProxy": "http://192.168.1.12:3128",
     "noProxy": "*.test.example.com,.example2.com,127.0.0.0/8"
   }
 }
}

参考資料

C プログラムを WASI 向けの Wasm としてビルドし、 WasmEdge で実行する

そのうち Docker で Wasm コンテナが実行できるようになる(Docker DesktopがWebAssemblyランタイムを統合。コンテナと同様にWebAssemblyイメージを実行可能に - Publickey)らしいので、Wasm のビルドと実行を試してみる。

前提

  • OS: Windows 11 Pro 22H2 ビルド: 22621.755
  • Docker: Docker Desktop 4.13.1 (90346)
  • 使用イメージ: debian:bullseye-slim

イメージ起動

docker run -it --rm debian:bullseye-slim

環境構築

前提ツール類インストール

apt-get update
apt-get install -y curl git libxml2 file
  • curl : WasmEdge のインストールに必要
  • git : WasmEdge のインストールに必要
  • libxml2 : Wasm バイナリのビルドに必要なライブラリ
  • file : ビルドしたバイナリが Wasm バイナリになっているかを確認するために使用

wasi-sdk のインストール

Wasm のコンパイルに必要な SDK をインストールする。

curl -LO https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-16/wasi-sdk_16.0_amd64.deb
apt-get install -f ./wasi-sdk_16.0_amd64.deb

/opt/wasi-sdk/ にインストールされる。

WasmEdge のインストール

Wasm 実行に必要なランタイムをインストールする。

curl -sSf https://raw.githubusercontent.com/WasmEdge/WasmEdge/master/utils/install.sh | bash

~/.wasmedge にインストールされる。

ビルド

C のソースコードを作成

cat << EOF >> main.c
#include <stdio.h>

int main(int argc, char **argv) {
    printf("Hello, World!\n");
    return 0;
}
EOF

clang によるビルド

/opt/wasi-sdk/bin 内に clang があるので、それを使ってビルド。

/opt/wasi-sdk/bin/clang main.c --sysroot=/opt/wasi-sdk/share/wasi-sysroot/ -o ./main.wasm

バイナリの確認

file コマンドで確認すると、 WebAssembly であることが確認できる。

# file main.wasm
main.wasm: WebAssembly (wasm) binary module version 0x1 (MVP)

実行

wasmedge に、ビルドしたバイナリを渡す。

# ~/.wasmedge/bin/wasmedge ./main.wasm
Hello, World!

実行できた。以上。

参考資料

2022年10月3日月曜日

docker volume のバックアップ方法メモ

以下 3 ステップ。

  1. 新しいボリュームを作る
  2. バックアップ元ボリュームとバックアップ先を /src と /dest へマウントしたコンテナを作成
  3. /src から /dest へコピー

コマンド実行例は以下のような感じ。

docker volume create xxx-bkup
docker run -it --rm -v xxx:/src -v xxx-bkup:/dest debian:bullseye-slim
cp -a /src/* /dest
  • ※ debian:bullseye-slim : ローカルにあるイメージから適当に一番小さそうなものを選んだだけ。cp コマンドが使えればなんでもよい。

以上。

2022年8月28日日曜日

Windows Subsystem for Android で Yukari for Android を動かす

shibafu528/Yukari: Twitter/Mastodon Client for Android を Windows Subsystem for Android で動かしたい。

Twitter TL と Mastodon TL をひとつのカラムで混ぜて見れて、さらに TL の自動更新ができるアプリ、Yukari for Android しか知らないので、これを Windows で動かしたかった。

前提

Windows Subsystem for Android の有効化

Windows Insider Program の有効化

※ 2022/8/25 から、 Windows Insider Program への参加は不要になったようだ

  1. スタート -> 設定 -> Windows Update -> Windows Insider Program
  2. 使用を開始する ボタン押下
  3. ウィザードに従って情報入力
    1. アカウントと紐づけ
      • Microsoft アカウントに紐づけ
    2. Insider チャネルを選択
      • ベータ チャネル を選択
      • WSA はDev またはベータで使える
    3. デバイスの契約をレビューする
      • 利用規約とプライバシーステートメントを読む
    4. このデバイスを再起動して作業を完了します
      • 今すぐ再起動 を押下

Amazon アプリストアをインストール

スタート -> Microsoft Store -> Amazon アプリストア で検索し、 Amazon アプリストア を選択し、インストール。

開発者モードをオン

adb コマンドで WSA に接続できるようにするため、開発者モードをオンにする。

  1. スタート -> Android™用 Windows サブシステム設定 を実行
  2. 開発者 -> 開発者モード をオンにする

Yukari for Android のインストール

adb で WSA に接続

adb は、Android Studio をインストールしていれば ~/AppData/Local/Android/Sdk/platform-tools に入っているはず。

WSA が起動していないと、 adb で接続できないので、 Android™用 Windows サブシステム設定 を起動しておくこと。

Android™用 Windows サブシステム設定 を起動したうえで、以下コマンドで接続。

> C:\Users\mikoto\AppData\Local\Android\Sdk\platform-tools\adb.exe connect 127.0.0.1:58526
* daemon not running; starting now at tcp:5037
* daemon started successfully
failed to authenticate to 127.0.0.1:58526

ADB のデバッグを許可しますか? のダイアログが出るので、 許可 を押下。

adb で apk をインストール

以下コマンドで、署名済みの apk ファイルを転送・インストール。

C:\Users\mikoto\AppData\Local\Android\Sdk\platform-tools\adb.exe install .\Yukari\Yukari\stable\release\Yukari-stable-release.apk

以上。

Yukari for Android の設定をインポートしたいなら、設定ファイル一式を abd push で WSA のファイルシステムへ転送できる。

参考資料

Yukari for Android をビルドする(2022/8/29 版)

shibafu528/Yukari: Twitter/Mastodon Client for Android をビルドする。

前提

  • OS: Windows 11 Pro 21H2 ビルド: 22000.856

Android Studio のインストール

Android Studio のインストール  |  Android デベロッパー  |  Android Developers からダウンロードし、インストールする。

GitHub の Personal Access Token を作成

Package info.shibafu528.yukari.yukari-exvoice · shibafu528/yukari-exvoice パッケージをインストールするために、 GitHub パッケージを読み込みできるトークンを作成する。

read:package パーミッションの付いた Personal Access Token を作成し、トークンをメモ。

Yukari のビルド

  1. Android Studio に、 Yukari のプロジェクトをインポート

  2. Yukari/local.properties に、以下 2 行を追加

    gpr.user=mikoto2000
    gpr.key=<read:package のパーミッションを設定した Personal Access Token>
  3. メニューの Build -> Generate Signed Bundle or APK を選択

    1. Generate Signed Bundle or APK のダイアログが開くので、 APK を選択して Next ボタン押下
    2. 必要事項を記入して Next ボタン押下
      1. Module に Yukari.Yukari を選択
      2. セクション Keystore path の Create new... ボタンを押下、必要事項を記入して OK ボタン押下
    3. 必要事項を記入して Finish ボタン押下
      • Destination Folder : apk の出力パス。今回は /PATH/TO/Yukari/Yukari を設定。
      • Build Variants : stableRelease を選択

これで、 /PATH/TO/Yukari/Yukari/stable/release/Yukari-stable-release.apk が生成される。

2022年6月22日水曜日

Terraform で AWS の管理コンソールにログインできる IAM ユーザーを作成する

前提

  • Docker: Docker version 20.10.17, build 100c70180f
  • Terraform: Terraform v1.2.3
    • 使用する Docker イメージ: hashicorp/terraform:1.2.3
  • Terraform の tfstate を記録しておくための S3 バケットを作成済み
    • 今回は mikoto2000-terraform-user-admin という名前で作成した

今回作るユーザーたち

今回 Terraform で作成するユーザーは以下の通り。

ユーザー ロール
rookie0001 新人。EC2 利用者
rookie0002 新人。EC2 利用者
rookie0003 新人。EC2 利用者

AWS 側の設定

Terraform が使用するユーザーの追加

  1. 管理用ユーザーを追加
    • ユーザー名 : user-admin
    • AWS 認証情報タイプを選択 : アクセスキー - プログラムによるアクセス
  2. アクセス許可の設定
    1. 既存のポリシーを直接アタッチ を選択
    2. IAMFullAccess と AmazonS3FullAccess をチェック

ユーザー追加完了画面の アクセスキー ID と シークレットアクセスキー をメモ。

Terraform を使用するための準備

コンテナ起動

docker run -it --rm --name terraform -v "$(pwd):/work" --workdir /work --entrypoint sh hashicorp/terraform

必須パッケージインストール

IAM ユーザー作成で使用するツールをインストールする。

apk add gnupg jq
  • gnupg: 暗号化された初期パスワードを受け取るため、公開鍵を利用する。そのために必要なもの。
  • jq: Terraform からの出力をパースするために利用。

公開鍵作成

gpg2 コマンドで、公開鍵ペアを作成。

認証情報の暗号化と復号に使用する。

gpg2 --gen-key

作成した公開鍵を base64 エンコードして TF_VAR_user_admin_gpg_key の環境変数へ記録しておく。

Terraform がパスワードを暗号化するために利用する。

export TF_VAR_user_admin_gpg_key=$(gpg2 --export terraform-user-admin | base64 | tr -d '\n')

AWS 管理用の tf ファイルのたたき台作成

main.tf という名前で、 Terraform 用のファイルを作成。

terraform {
  # AWS を使いますよという定義
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 3.0"
    }
  }

  backend "s3" {
    bucket = "mikoto2000-terraform-user-admin"
    region = "ap-northeast-1"
    key = "terraform.tfstate"
  }
}

# AWS の設定
# アクセストークンとアスセスシークレットは、環境変数から取得する
provider "aws" {
  # リージョン
  region = "ap-northeast-1"
}

Terraform プロジェクトの初期化

export AWS_ACCESS_KEY_ID="xxxxxxxxxxxxxxxxxxxx"
export AWS_SECRET_ACCESS_KEY="yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy"

terraform init

接続確認

アクセスキー ID とシークレットアクセスキーが正しく設定されていれば、 terraform plan すると No changes. Your infrastructure matches the configuration. という情報が表示される。

terraform plan

ユーザー定義の作成

概要

「ユーザー定義をしてはい終了」というわけには行かず、以下のような順番でリソース定義をしていく。

  1. ユーザー定義(aws_iam_user)
    • 今回新規追加する 3 人のユーザーを定義
  2. ユーザーのログインプロファイル定義(aws_iam_user_login_profile)
    • 今回新規追加する 3 人のユーザーの初期パスワードを作成
  3. グループ定義(aws_iam_group)
    • 「新人グループ」を作成
  4. グループポリシー定義(aws_iam_group_policy_attachment)
    • 「新人グループ」に「EC2 を自由に使える権限」を付与
  5. ユーザーをグループに所属させるための定義(aws_iam_group_membership)
    • 今回新規追加する 3 人のユーザーを「新人グループ」に所属させる
  6. 初期パスワードを表示するためのアウトプット定義

gpg 公開鍵を渡すための変数を定義

ユーザーのログインプロファイルを作成するときに公開鍵を設定する必要がある。 その鍵を渡すための変数を定義。

# gpg 公開鍵を渡すための変数を定義
variable "user_admin_gpg_key" {}

Terraform 定義

今回作成した tf ファイルの最終形
terraform {
  # AWS を使いますよという定義
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 3.0"
    }
  }

  backend "s3" {
    bucket = "mikoto2000-terraform-user-admin"
    region = "ap-northeast-1"
    key = "terraform.tfstate"
  }
}

# AWS の設定
# アクセストークンとアスセスシークレットは、環境変数から取得する
provider "aws" {
  # リージョン
  region = "ap-northeast-1"
}

# gpg 公開鍵を渡すための変数を定義
variable "user_admin_gpg_key" {}

locals {
  # 作成するユーザーのリスト(新人ユーザーリスト)
  rookies = [
    "rookie0001"
    , "rookie0002"
    , "rookie0003"
  ]
}

resource "aws_iam_user" "rookies" {
  # `rookies` の要素ごとに繰り返し定義するという設定
  for_each = toset(local.rookies)

  # ユーザー名。 rookies に定義した文字列が設定される
  name = "${each.value}"
}

resource "aws_iam_user_login_profile" "rookie" {
  # `aws_iam_user` で作成したユーザーごとに繰り返し定義するという設定
  # ここで `local.rookies` とかを指定してしまうと、
  # ユーザーがまだ作成されないうちにプロファイル定義を作ろうとして、
  # 「ユーザーがいません」って怒られることがある。
  for_each = aws_iam_user.rookies

  # 対象ユーザー
  user = "${each.value.name}"

  # 初期パスワードを暗号化するための公開鍵
  pgp_key = "${var.user_admin_gpg_key}"
}

resource "aws_iam_group" "rookies" {
  # グループ名
  name = "rookies"

  # 謎。 See: https://dev.classmethod.jp/articles/aws-iam-with-path/
  path = "/"
}

resource "aws_iam_group_policy_attachment" "AmazonEC2FullAccess-to-rookies" {
  # 付与対象のグループ
  group = aws_iam_group.rookies.name

  # 付与するポリシー
  policy_arn = "arn:aws:iam::aws:policy/AmazonEC2FullAccess"
}

resource "aws_iam_group_policy_attachment" "IAMUserChangePassword-to-rookies" {
  # 付与対象のグループ
  group = aws_iam_group.rookies.name

  # 付与するポリシー
  policy_arn = "arn:aws:iam::aws:policy/IAMUserChangePassword"
}

resource "aws_iam_group_membership" "rookies" {
  # グループメンバーシップ名
  name = "rookies"

  # グループに所属させるユーザーの、名前のリストを指定
  users = [for v in aws_iam_user.rookies: v.name]

  # 対象のグループを指定
  group = aws_iam_group.rookies.name
}

output "rookies_encrypted_initia_password" {
  # aws_iam_user_login_profile.rookie の定義ごとに繰り返し、
  # `user` と `encrypted_password` をセットで表示するように指定
  value = "${[for v in aws_iam_user_login_profile.rookie: tomap({
    "user_name" = v.user
    "encrypted_password" = v.encrypted_password}
  )]}"
}

今回作成するユーザーのユーザー名が格納されたリストを作成

やはり同じ権限の人は for 文的なもので作成するほうが良いのでその方針で行くとする。

それに利用するため、ユーザー名が格納されたリストを作成する。

locals {
  # 作成するユーザーのリスト(新人ユーザーリスト)
  rookies = [
    "rookie0001"
    , "rookie0002"
    , "rookie0003"
  ]
}

ユーザー定義

for_each を使用して、今回作成する 3 名分の aws_iam_user を定義する。

resource "aws_iam_user" "rookies" {
  # `rookies` の要素ごとに繰り返し定義するという設定
  for_each = toset(local.rookies)

  # ユーザー名。 rookies に定義した文字列が設定される
  name = "${each.value}"
}

ユーザーログインプロファイル定義

今回作成する 3 名分の aws_iam_user_login_profile を定義する。

resource "aws_iam_user_login_profile" "rookie" {
  # `aws_iam_user` で作成したユーザーごとに繰り返し定義するという設定
  # ここで `local.rookies` とかを指定してしまうと、
  # ユーザーがまだ作成されないうちにプロファイル定義を作ろうとして、
  # 「ユーザーがいません」って怒られることがある。
  for_each = aws_iam_user.rookies

  # 対象ユーザー
  user = "${each.value.name}"

  # 初期パスワードを暗号化するための公開鍵
  pgp_key = "${var.user_admin_gpg_key}"
}

Terraform からは、ここで指定した pgp_key を使用して暗号化された初期パスワードが返ってくる(後述)

ユーザーグループ定義

rookies グループを作成し、必要なポリシーを割り当てる。

ユーザーグループ定義

aws_iam_group を定義する。

resource "aws_iam_group" "rookies" {
  # グループ名
  name = "rookies"

  # 謎。 See: https://dev.classmethod.jp/articles/aws-iam-with-path/
  path = "/"
}

ユーザーグループへグループポリシーを割り当てる

aws_iam_group_policy_attachment を使って、先程定義した rookies グループにポリシーを付与する。

EC2 への全権限を付与

resource "aws_iam_group_policy_attachment" "AmazonEC2FullAccess-to-rookies" {
  # 付与対象のグループ
  group = aws_iam_group.rookies.name

  # 付与するポリシー
  policy_arn = "arn:aws:iam::aws:policy/AmazonEC2FullAccess"
}

自分のパスワードを変更するための権限を付与

resource "aws_iam_group_policy_attachment" "IAMUserChangePassword-to-rookies" {
  # 付与対象のグループ
  group = aws_iam_group.rookies.name

  # 付与するポリシー
  policy_arn = "arn:aws:iam::aws:policy/IAMUserChangePassword"
}

ユーザーをグループに所属させる

resource "aws_iam_group_membership" "rookies" {
  # グループメンバーシップ名
  name = "rookies"

  # グループに所属させるユーザーの、名前のリストを指定
  users = [for v in aws_iam_user.rookies: v.name]

  # 対象のグループを指定
  group = aws_iam_group.rookies.name
}

初期パスワードを表示するためのアウトプット定義

output "rookies_encrypted_initia_password" {
  # aws_iam_user_login_profile.rookie の定義ごとに繰り返し、
  # `user` と `encrypted_password` をセットで表示するように指定
  value = "${[for v in aws_iam_user_login_profile.rookie: tomap({
    "user_name" = v.user
    "encrypted_password" = v.encrypted_password}
  )]}"
}

実際に AWS にユーザーを作る

ドライラン

terraform plan で、どんなリソースが作成されるか確認。

terraform plan

リソース作成

terraform apply でリソースを作成する。

terraform apply --auto-approve

パスワードの復号

以下コマンドを組み合わせて、初期パスワードを取得する。

  1. terraform output で 「base64 済み・暗号化済みのパスワード」を取得
  2. base64 -d で base64 デコード
  3. gpg -r で復号

以下は、 rookie0001 の初期パスワード複合例。

# base64 エンコード済み、かつ、暗号化済みのパスワードが記載された json を取得
ENCRYPTED_PASSWORD=$(terraform output -json rookies_encrypted_initia_password | jq -r '.[0].encrypted_password')

# encrypted password を base64 デコードして復号
echo $ENCRYPTED_PASSWORD | base64 -d | gpg -r terraform-user-admin  2> /dev/null; echo

動作確認

rookie0001 でログインしてみる。

  1. AWS コンソールを開く -> IAM ユーザー 選択
    • アカウント ID: ルートユーザーのアカウント ID を入力
    • ユーザー名: rookie0001
    • パスワード: 前述の手順で取得したパスワードを入力
  2. パスワード変更画面が表示されるので、変更する
  3. 権限確認
    • EC2 のサービスを見てみる -> 普通に使用できる。無料枠の範囲内のインスタンスが無事たったことを確認
      • SSH 接続もできた
    • EC2 以外のサービスを見てみる -> アクセス許可がありません みたいな表示になって操作できない

今後のために

他の管理者が認証情報の復号をしたくなった際に復号できるように、(そんなことあるか???) 公開鍵ペアをエクスポートしてどこかに保存しておく。

gpg -o ./terraform-user-admin.public.gpg  --export terraform-user-admin
gpg -o ./terraform-user-admin.private.gpg --export-secret-key terraform-user-admin

後片付け

terraform destroy で作成したリソースを削除。

terraform destroy --auto-approve

以上。

これで「新人のスキルアップのためにアカウント欲しい」って言われたときにさくっとユーザー作れるかな?…どうだろう

参考資料

2022年6月10日金曜日

Tailscale を使ってみる

前提

  • OS1: Windows 11 Pro 21H2 ビルド 22000.675
  • OS2: Arch Linux 2022/6/8

Tailscale へのサインアップから、ひとつめのデバイス登録まで

Windows 11 Pro に Tailscale をインストールし、デバイスの登録を行う。

  1. Web ブラウザ https://login.tailscale.com/start へアクセス
    • 今回は GitHub で Sign up した
  2. Select Network 画面が表示されるので、使用するネットワークを選択
    • Single-user Tailnet の mikoto2000 を選択
  3. Welcome! Let’s add your first device. 画面が表示されるので、 Windows のボタンを押下 -> Download Tailscale for Windows ボタン押下
  4. Download Tailscale 画面が表示されるので、 Download Tailscale for Windows ボタン押下でインストーラーをダウンロード
  5. tailscale-ipn-setup-1.26.0.exe を実行
    • デフォルトのまま Install ボタン押下 -> Close ボタン押下
  6. タスクトレイ内の Tailscale アイコン右クリック -> Log in...

これで、 Tailscale のネットワークに最初のデバイスが追加される。

DNS 設定

Tailscale ネットワークにぶら下がったデバイス間のアクセスがやりやすいように、 MagicDNS を使って名前解決するように設定する。

  1. Tailscale のログイン後画面 -> 左上のトップページへのリンクを選択 -> DNS
  2. DNS 画面になるので、必要な設定を行う
    1. Nameserver セクションの Add nameserver を選択 -> Cloudflare Public DNS を選択
      • MagicDNS で引っかからなかったホスト名を解決するために使う DNS, 必要に応じて IP を設定する
    2. MagicDNS セクションの Enable MagicDNS ボタンを押下

OK.

ふたつめのデバイス登録

Arch Linux を登録する。

  1. Web ブラウザ https://login.tailscale.com/start へアクセス
    • 今回は GitHub で Sign up した
  2. Select Network 画面が表示されるので、使用するネットワークを選択
    • Single-user Tailnet の mikoto2000 を選択
  3. Machines 画面が表示されるので、 Download リンクを選択
  4. Download Tailscale 画面になるので、 Manually install on のプルダウンから Arch Linux を選択
    • 以下、表示された説明の通りコマンドを実行
      1. パッケージインストール
        • sudo pacman -S tailscale
      2. 自動起動設定、 --now で同時に起動も行う
        • sudo systemctl enable --now tailscaled
      3. Tailscale にデバイスを接続(登録)
        • sudo tailscale up
        • 表示される URL に Web ブラウザでアクセスし、ログイン

これで、 Tailscale のネットワークに Arch Linux がぶら下がる。

動作確認

ふたつめのデバイスからひとつめのデバイスに SSH 接続してみる。

ssh mikoto@mydesktoppc

ひとつめのデバイスに設定した接続情報でログインできる。

参考資料

2022年6月8日水曜日

Windows 11 Pro に sshd をインストールして公開鍵認証方式でログインする

前提

  • OS: Windows 11 Pro 21H2 ビルド 22000.675
  • GitHub に SSH 接続用の公開鍵を追加済み

OpenSSH サーバーのインストール

  1. Windows キー -> 設定アイコン -> アプリ -> オプション機能 -> 機能を表示 ボタン押下
  2. OpenSSH サーバー にチェックを入れて 次へ ボタン押下
  3. インストール ボタン押下

OpenSSH サーバーの設定

sshd サービスを自動起動に設定

PowerShell で以下コマンドを実行。

start-process -verb runas powershell -ArgumentList Set-Service,-Name,'sshd',-StartupType,'Automatic'

sshd サービスを起動

PowerShell で以下コマンドを実行。

start-process -verb runas powershell -ArgumentList Start-Service,-Name,'sshd'

GitHub の公開鍵を authorized_keys に追記

Invoke-WebRequest で取得し、 Out-File で追記する。

(Invoke-WebRequest https://github.com/mikoto2000.keys).Content | Out-File -FilePath ~/.ssh/authorized_keys -Encoding utf8 -Append

今回使うユーザーが、 Administrator グループに入っているため、 C:\ProgramData\ssh\authorized_keys が参照される。

そちらにコピーして権限を設定。

# authorized_keys コピー
start-process -verb runas powershell -ArgumentList Copy-Item,c:/Users/mikoto/.ssh/authorized_keys,c:/ProgramData/ssh/administrators_authorized_keys

# 権限設定
start-process -verb runas powershell -ArgumentList icacls,.\administrators_authorized_keys,/inheritance:r,/grant,Administrators:F,/grant,SYSTEM:F

デフォルトシェルを PowerShell へ変更

start-process -verb runas powershell -ArgumentList New-ItemProperty,-Path,"HKLM:\SOFTWARE\OpenSSH",-Name,DefaultShell,-Value,"C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe",-PropertyType,String,-Force

公開鍵認証のみを許可するように設定変更

  1. 管理者権限で開いたテキストエディタで c:\ProgramData\ssh\sshd_config を開き、以下修正を行う
    • #PasswordAuthentication yes -> PasswordAuthentication no
  2. sshd サービスリスタート
    • start-process -verb runas powershell -ArgumentList Restart-Service,-Name,'sshd'

動作確認

ssh mikoto@localhost

ログインできた、 OK.

以上。

参考資料

2022年6月1日水曜日

Langium で DSL と LSP サーバーを作る(2) AST をもとにコード生成を行う

前回 の続き。 AST からコード生成を行う。

目標

以下の定義を元に、

person mikoto {
    age: 18;
    rank: 1;
}

person makoto {
    age: 19;
    rank: 2;
}

person mokoto {
    age: 20;
    rank: 3;
}

以下のコードを生成する。

export type Person = {
    age : number;
    rank : number;
}

const mikoto : Person = {
    age: 18,
    rank: 1
}

const makoto : Person = {
    age: 19,
    rank: 2
}

const mokoto : Person = {
    age: 20,
    rank: 3
}

export const allPersons = [mikoto, makoto, mokoto];

前提

  • OS: Arch Linux
  • Docker: Docker version 20.10.16, build aa7e414fdc
  • 使用する Docker イメージ: node:18

作業用コンテナ起動

docker run -it --rm -v "$(pwd):/work" --workdir /work node:18 bash

DLS 開発(コード生成まで)

コード生成機能実装

src/cli/generator.ts を作成し、「person 定義ごとに定数を作り、全 person が含まれた配列を作成」というコードを生成する実装を行う。

■ src/cli/generator.ts

import fs from 'fs';
import { CompositeGeneratorNode, NL, processGeneratorNode } from 'langium';
import path from 'path';
import { Model } from '../language-server/generated/ast';
import { extractDestinationAndName } from './cli-util';

// Person 型の定義
const TYPE_PERSON = `export type Person = {
    age : number;
    rank : number;
}`;

export function generateTypeScript(model: Model, filePath: string, destination: string | undefined): string {
    const data = extractDestinationAndName(filePath, destination);
    const generatedFilePath = `${path.join(data.destination, data.name)}.ts`;

    // これから生成するファイルの内容を表すインスタンスを生成
    const fileNode = new CompositeGeneratorNode();

    // 型定義コードを追加
    // TYPE_REASON 定数と、改行をふたつ
    fileNode.append(TYPE_PERSON, NL, NL);

    // DSL の person 定義ごとに Person 型の定数を追加
    model.persons.forEach(person => {
        fileNode.append(`const ${person.name} : Person = {
    age: ${person.age},
    rank: ${person.age}
}`, NL, NL)
    });

    // 全 person が含まれた Array を追加
    const allPersons = model.persons.map(person => person.name).join(', ');
    fileNode.append(`export const allPersons = [${allPersons}];`);


    // 生成先ディレクトリがなければ作る
    if (!fs.existsSync(data.destination)) {
        fs.mkdirSync(data.destination, { recursive: true });
    }

    // 生成先にファイル書き出し
    fs.writeFileSync(generatedFilePath, processGeneratorNode(fileNode));

    // 生成したファイルのファイルパスを返却
    return generatedFilePath;
}

cli からコード生成機能を呼び出す実装を追加

■ src/cli/index.ts

diff --git a/src/cli/index.ts b/src/cli/index.ts
index e0a4230..40d2e4c 100644
--- a/src/cli/index.ts
+++ b/src/cli/index.ts
@@ -4,6 +4,14 @@ import { Model } from '../language-server/generated/ast';
 import { FirststepLanguageMetaData } from '../language-server/generated/module';
 import { createFirststepServices } from '../language-server/firststep-module';
 import { extractAstNode } from './cli-util';
+import { generateTypeScript } from './generator';
+
+export const generateAction = async (fileName: string, opts: GenerateOptions): Promise<void> => {
+    const services = createFirststepServices().Firststep;
+    const model = await extractAstNode<Model>(fileName, services);
+    const generatedFilePath = generateTypeScript(model, fileName, opts.destination);
+    console.log(colors.green(`TypeScript code generated successfully: ${generatedFilePath}`));
+};
 
 export const testAction = async (fileName: string, opts: GenerateOptions): Promise<void> => {
     const services = createFirststepServices().Firststep;
@@ -24,6 +32,13 @@ export default function(): void {
         .version(require('../../package.json').version);
 
     const fileExtensions = FirststepLanguageMetaData.fileExtensions.join(', ');
+    program
+        .command('generate')
+        .argument('<file>', `source file (possible file extensions: ${fileExtensions})`)
+        .option('-d, --destination <dir>', 'destination directory of generating')
+        .description('generates TypeScript code that all Person array.')
+        .action(generateAction);
+
     program
         .command('test')
         .argument('<file>', `source file (possible file extensions: ${fileExtensions})`)
@@ -32,3 +47,4 @@ export default function(): void {
 
     program.parse(process.argv);
 }

相対パスでファイルを指定するとデータ名が空になる問題の修正

src/cli/cli-util.ts を修正。

diff --git a//src/cli/cli-util.ts b//src/cli/cli-uti
l.ts
index 53f6a94..b67cab6 100644
--- a//src/cli/cli-util.ts
+++ b//src/cli/cli-util.ts
@@ -43,7 +43,7 @@ interface FilePathData {
 }
 
 export function extractDestinationAndName(filePath: string, destination: string | undefined): FilePathDat
a {
-    filePath = filePath.replace(/\..*$/, '').replace(/[.-]/g, '');
+    filePath = path.basename(filePath, path.extname(filePath)).replace(/[.-]/g, '');
     return {
         destination: destination ?? path.join(path.dirname(filePath), 'generated'),
         name: path.basename(filePath)

ビルド

npm run build

コード生成動作確認

「目標」で示した DSL 定義ファイルを作成し、コード生成を行う。

cat << EOF >> test.firststep
person mikoto {
    age: 18;
    rank: 1;
}

person makoto {
    age: 19;
    rank: 2;
}

person mokoto {
    age: 20;
    rank: 3;
}
EOF

node bin/cli generate -d ./generated_code ./test.firststep

これで、 generated_code/test.ts が生成される。

# cat generated_code/test.ts
export type Person = {
    age : number;
    rank : number;
}

const mikoto : Person = {
    age: 18,
    rank: 18
}

const makoto : Person = {
    age: 19,
    rank: 19
}

const mokoto : Person = {
    age: 20,
    rank: 20
}

export const allPersons = [mikoto, makoto, mokoto];

今回はここまで。

参考資料

2022年5月26日木曜日

node_modules を対象に入れたままコミットを積み上げて、 master にマージまでしてしまったのを解消する

つらい。

前提

おひとり様リポジトリで、並行してほかの人が作業するようなことはない。

手順メモ

master ブランチをマージコミット直前にまで戻す

git checkout master
git reset --hard HEAD^
git push -f

やらかしたブランチをチェックアウト

git checkout -b langium-firststep origin/langium-firststep

filter-branch でブランチから対象ファイル群を削除

git filter-branch -f --tree-filter "rm -rf Langium/firststep/firststep/node_modules" --prune-empty -- --all
git gc --aggressive --prune=now
git push -f

あとは push しなおしたブランチを元に Pull Request を作り直してマージしなおす。

参考資料

Langium で DSL と LSP サーバーを作る(1) 文法定義から文法チェックまで

DSL を定義すると、 LSP サーバーの実装を吐き出してくれるツール Langium を試す。

目標

以下の文法の DSL を作る。

person <NAME> {
    age: <INTEGER>;
    rank: <INTEGER>;
}
  • <NAME>: 任意の名前, 半角小文字アルファベットのみ許容
  • <INTEGER>: 任意の整数

前提

  • OS: Arch Linux
  • Docker: Docker version 20.10.16, build aa7e414fdc
  • 使用する Docker イメージ: node:18

作業用コンテナ起動

docker run -it --rm -v "$(pwd):/work" --workdir /work node:18 bash

Langium のジェネレーターをインストール

npm i -g yo generator-langium

Langium のプロジェクトを作成

# su - node
$ cd /work
$ yo langium
? ==========================================================================
We're constantly looking for ways to make yo better! 
May we anonymously report usage statistics to improve the tool over time? 
More info: https://github.com/yeoman/insight & http://yeoman.io
========================================================================== Yes
┌─────┐ ─┐
┌───┐    │  ╶─╮ ┌─╮ ╭─╮ ╷ ╷ ╷ ┌─┬─╮
│ ,´     │  ╭─┤ │ │ │ │ │ │ │ │ │ │
│╱       ╰─ ╰─┘ ╵ ╵ ╰─┤ ╵ ╰─╯ ╵ ╵ ╵
`                   ╶─╯

Welcome to Langium! This tool generates a VS Code extension with a "Hello World" language to get started 
quickly. The extension name is an identifier used in the extension marketplace or package registry.
? Your extension name: firststep
The language name is used to identify your language in VS Code. Please provide a name to be shown in the 
UI. CamelCase and kebab-case variants will be created and used in different parts of the extension and 
language server.
? Your language name: firststep
Source files of your language are identified by their file name extension. You can specify multiple file 
extensions separated by commas.
? File extensions: .firststep
   create firststep/langium-config.json
   create firststep/langium-quickstart.md
   create firststep/language-configuration.json
   create firststep/package.json
   create firststep/tsconfig.json
   create firststep/bin/cli
   create firststep/src/extension.ts
   create firststep/src/cli/cli-util.ts
   create firststep/src/cli/generator.ts
   create firststep/src/cli/index.ts
   create firststep/src/language-server/firststep-module.ts
   create firststep/src/language-server/firststep-validator.ts
   create firststep/src/language-server/firststep.langium
   create firststep/src/language-server/main.ts
   create firststep/.vscode/extensions.json
   create firststep/.vscode/launch.json
   create firststep/.eslintrc.json
   create firststep/.vscodeignore

added 173 packages, and audited 174 packages in 23s

27 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities
npm notice 
npm notice New minor version of npm available! 8.9.0 -> 8.11.0
npm notice Changelog: https://github.com/npm/cli/releases/tag/v8.11.0
npm notice Run npm install -g npm@8.11.0 to update!
npm notice 

> firststep@0.0.1 langium:generate
> langium generate

Reading config from langium-config.json
src/language-server/firststep.langium:14:10 - This rule is declared but never referenced.
src/language-server/firststep.langium:15:10 - This rule is declared but never referenced.
Writing generated files to /work/firststep/src/language-server/generated
Writing textmate grammar to /work/firststep/syntaxes/firststep.tmLanguage.json
Langium generator finished successfully in 218ms

> firststep@0.0.1 build
> tsc -b tsconfig.json


No change to package.json was detected. No package manager install will be executed.

DSL 開発(文法チェックまで)

grammar コードの実装

以下コマンドで修正をウォッチしながら、src/language-server/firststep.langium を修正する。

npm run langium:watch

■ src/language-server/firststep.langium

// DSL の文法名
grammar Firststep

// モデル定義。
// 「この DSL ではゼロから複数の Person を定義できる」という定義をしている
entry Model:
    (persons+=Person)*;

/**
 * 以下形式の文法を定義。
 *
 * person <NAME> {
 *   age: <INTEGER>;
 *   rank: <INTEGER>;
 * }
 *
 * ※ このとき、 AST 上の person ノードは、 `age` と `rank` のパラメーターを持つ。

 */
Person:
    'person' name=NAME '{'
        'age' ':' age=INTEGER ';'
        'rank' ':' rank=INTEGER ';'
    '}'
    ;


// `<NAME>`: 任意の名前, 半角の小文字アルファベットのみ許容
terminal NAME: /[a-z]+/;

// `<INTEGER>`: 任意の整数
terminal INTEGER returns number: /[0-9]+/;

// 空白文字は AST に含めない
hidden terminal WS: /\s+/;

// マルチラインコメント(`/* XXX */`) は AST に含めない
hidden terminal ML_COMMENT: /\/\*[\s\S]*?\*\//;

// シングルラインコメント(`// XXX`) は AST に含めない
hidden terminal SL_COMMENT: /\/\/[^\n\r]*/;

grammar コードからパーサーコード(TypeScript)の生成

npm run langium:watch していればすでに生成済みのはずだが、ワンショットで再生成したい場合には以下コマンドを実行。

npm run langium:generate

パーサーコード(TypeScript)のビルド

以下コマンドでビルド。

npm run build

Langium プロジェクト生成時に実装済みの Person, Greeting 文法用のコードになっているため、ビルドに失敗する。

今回自分が実装した Person 文法用の実装に修正しなければならない。

Person, Greeting 文法用のコード削除

src/cli/generator.ts, src/language-server/firststep-validator.ts の削除

generator は、「Greeting 情報を読み込んでコンソールに出力する TypeScript コードを生成する」という実装になっているため、削除。

firststep-validator は、「Person の ID の先頭が大文字でないとだめ」という実装になっているため、削除。

rm src/cli/generator.ts
rm src/language-server/firststep-validator.ts

src/cli/index.ts の修正

src/cli/generator.ts を削除したことで、ビルドが失敗するようになるので解消。

diff --git a/firststep/src/cli/index.ts b/firststep/src/cli/index.ts
index eeafcac..4322246 100644
--- a/firststep/src/cli/index.ts
+++ b/firststep/src/cli/index.ts
@@ -1,17 +1,9 @@
-import colors from 'colors';
+//import colors from 'colors';
 import { Command } from 'commander';
-import { Model } from '../language-server/generated/ast';
-import { FirststepLanguageMetaData } from '../language-server/generated/module';
-import { createFirststepServices } from '../language-server/firststep-module';
-import { extractAstNode } from './cli-util';
-import { generateJavaScript } from './generator';
-
-export const generateAction = async (fileName: string, opts: GenerateOptions): Promise<void> => {
-    const services = createFirststepServices().Firststep;
-    const model = await extractAstNode<Model>(fileName, services);
-    const generatedFilePath = generateJavaScript(model, fileName, opts.destination);
-    console.log(colors.green(`JavaScript code generated successfully: ${generatedFilePath}`));
-};
+//import { Model } from '../language-server/generated/ast';
+//import { FirststepLanguageMetaData } from '../language-server/generated/module';
+//import { createFirststepServices } from '../language-server/firststep-module';
+//import { extractAstNode } from './cli-util';
 
 export type GenerateOptions = {
     destination?: string;
@@ -24,13 +16,7 @@ export default function(): void {
         // eslint-disable-next-line @typescript-eslint/no-var-requires
         .version(require('../../package.json').version);
 
-    const fileExtensions = FirststepLanguageMetaData.fileExtensions.join(', ');
-    program
-        .command('generate')
-        .argument('<file>', `source file (possible file extensions: ${fileExtensions})`)
-        .option('-d, --destination <dir>', 'destination directory of generating')
-        .description('generates JavaScript code that prints "Hello, {name}!" for each greeting in a sourc
e file')
-        .action(generateAction);
+    // const fileExtensions = FirststepLanguageMetaData.fileExtensions.join(', ');
 
     program.parse(process.argv);
 }

src/language-server/firststep-module.ts の修正

src/language-server/firststep-validator.ts を削除したことで、ビルドが失敗するようになるので解消。

--- a/firststep/src/language-server/firststep-module.ts
+++ b/firststep/src/language-server/firststep-module.ts
@@ -3,14 +3,12 @@ import {
     LangiumServices, LangiumSharedServices, Module, PartialLangiumServices
 } from 'langium';
 import { FirststepGeneratedModule, FirststepGeneratedSharedModule } from './generated/module';
-import { FirststepValidationRegistry, FirststepValidator } from './firststep-validator';
 
 /**
  * Declaration of custom services - add your own service classes here.
  */
 export type FirststepAddedServices = {
     validation: {
-        FirststepValidator: FirststepValidator
     }
 }
 
@@ -27,8 +25,6 @@ export type FirststepServices = LangiumServices & FirststepAddedServices
  */
 export const FirststepModule: Module<FirststepServices, PartialLangiumServices & FirststepAddedServices> 
= {
     validation: {
-        ValidationRegistry: (services) => new FirststepValidationRegistry(services),
-        FirststepValidator: () => new FirststepValidator()
     }
 };

パースに成功したかどうかを確認するためのサブコマンドを追加

src/cli/index.ts に、処理を実装する。

以下実装で、成功したときは「文法チェック OK」と表示し、失敗したときは、パーサーのエラーメッセージを表示するようになる。

diff --git a/firststep/src/cli/index.ts b/firststep/src/cli/index.ts
index 4322246..e0a4230 100644
--- a/firststep/src/cli/index.ts
+++ b/firststep/src/cli/index.ts
@@ -1,9 +1,16 @@
-//import colors from 'colors';
+import colors from 'colors';
 import { Command } from 'commander';
-//import { Model } from '../language-server/generated/ast';
-//import { FirststepLanguageMetaData } from '../language-server/generated/module';
-//import { createFirststepServices } from '../language-server/firststep-module';
-//import { extractAstNode } from './cli-util';
+import { Model } from '../language-server/generated/ast';
+import { FirststepLanguageMetaData } from '../language-server/generated/module';
+import { createFirststepServices } from '../language-server/firststep-module';
+import { extractAstNode } from './cli-util';
+
+export const testAction = async (fileName: string, opts: GenerateOptions): Promise<void> => {
+    const services = createFirststepServices().Firststep;
+    await extractAstNode<Model>(fileName, services);
+    console.log(colors.green(`文法チェック OK`));
+    // ※ extractAstNode から呼ばれる `extractDocument` 内で、 `process.exit(1)` されるので、 try-catch 
しない
+};
 
 export type GenerateOptions = {
     destination?: string;
@@ -16,7 +23,12 @@ export default function(): void {
         // eslint-disable-next-line @typescript-eslint/no-var-requires
         .version(require('../../package.json').version);
 
-    // const fileExtensions = FirststepLanguageMetaData.fileExtensions.join(', ');
+    const fileExtensions = FirststepLanguageMetaData.fileExtensions.join(', ');
+    program
+        .command('test')
+        .argument('<file>', `source file (possible file extensions: ${fileExtensions})`)
+        .description('文法チェック')
+        .action(testAction);
 
     program.parse(process.argv);
 }

動作確認

パーサーの動作確認

test.firststep ファイルを作って、先程実装した test サブコマンドで確認する。

$ node bin/cli test ./test-ok.firststep
文法チェック OK
$ node bin/cli test ./test-ng.firststep
There are validation errors:
line 1: Expecting keyword '{' but found `000`. [000]

OK.

今回はここまで。

コミット順序は前後するが、ここまでの修正を MiscellaneousStudy/Langium/firststep/firststep - GitHub に格納した。

あとは AST からコード生成などの所望の処理を行う実装をする感じ。

VSCode 拡張機能の動作確認

  1. npm run build でビルド
  2. Langium プロジェクトを、 ~/.vscode/extensions-oss にコピーして VSCode を起動
  3. 拡張子が .fiststep の空ファイルを作成し、 VSCode で開く
  4. grammar で実装した通りの補完やバリデーションの警告が出ることを確認

参考資料