Open10

K8s クラスタ上に HA 構成の Gitea を構築する

zenogawazenogawa

https://gitea.com/gitea/helm-chart

gitea は helm チャートがあり k8s クラスタ上に簡単にデプロイできる。High Availability を参照することで HA 構成も構築可能。
HA 構成ではデータベースやメモリキャッシュも外部に外出しして可用性を確保する。

name product 機能
indexer meilisearch issue 等の検索
memory cache dragonfly セッションやキャッシュの保存
Relational DB cloudnativePG 全般的なデータ保存
storage minio assets などの保存

database, memory cache はデフォルトでは postgresql (mysql), redis が使用できるが、ここではせっかくなのでそれぞれ postgres 互換の cloudnativePG, redis 互換の dragonfly を使ってみる。
上記の product もそれぞれ helm chart でデプロイできるので、各 product を構築した後、gitea 設定をカスタマイズしてデプロイするという流れ。


概要図

zenogawazenogawa

Dragonfly

Dragonfly operator をインストールした後、Instance と呼ばれる CRD を使ってデプロイする。

operator のインストール

# Install the CRD and Operator
kubectl apply -f https://raw.githubusercontent.com/dragonflydb/dragonfly-operator/main/manifests/dragonfly-operator.yaml

Instance の作成

kubectl apply -f https://raw.githubusercontent.com/dragonflydb/dragonfly-operator/main/config/samples/v1alpha1_dragonfly.yaml

pod と service が作成される。

$ kubectl get pod,svc -l app=dragonfly-sample
NAME                     READY   STATUS    RESTARTS   AGE
pod/dragonfly-sample-0   1/1     Running   0          22h
pod/dragonfly-sample-1   1/1     Running   0          22h

NAME                       TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)    AGE
service/dragonfly-sample   ClusterIP   10.109.200.27   <none>        6379/TCP   22h

redis 互換であるので svc に対して redis-cli で接続可能。

$ redis-cli -h 10.109.200.27
10.109.200.27:6379> get key
(nil)
10.109.200.27:6379> set key value
OK
10.109.200.27:6379> get key
"value"
10.109.200.27:6379>
zenogawazenogawa

CloudNativePG

dragonfly と同様 operator をインストールした後 Cluster という CRD を作成することでデプロイする。

https://github.com/cloudnative-pg/charts

operator をインストール。

helm repo add cnpg https://cloudnative-pg.github.io/charts
helm upgrade --install cnpg \
  --namespace cnpg-system \
  --create-namespace \
  cnpg/cloudnative-pg

Cluster 作成前にデフォルトユーザーを記載した secret を作成。username/password はいずれも gitea.

apiVersion: v1
data:
  username: Z2l0ZWE=
  password: Z2l0ZWE=
kind: Secret
metadata:
  name: gitea-postgres-secret
type: kubernetes.io/basic-auth

Cluster 作成時に bootstrap を実行して user や database を作成するために以下を参考に bootstrap フィールドを設定。
https://cloudnative-pg.io/documentation/current/bootstrap/

Cluster CRD のマニフェストは以下。

apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
  name: cluster-example-initdb
spec:
  instances: 3
  bootstrap:
    initdb:
      database: gitea
      owner: gitea
      secret:
        name: gitea-postgres-secret
  storage:
    size: 1Gi

これを適用すると instances に設定した数の pod と接続用の svc が作成される。

$ kubectl get pod,svc -n gadget  -l cnpg.io/cluster=cluster-example-initdb
NAME                           READY   STATUS    RESTARTS   AGE
pod/cluster-example-initdb-1   1/1     Running   0          21h
pod/cluster-example-initdb-2   1/1     Running   0          21h
pod/cluster-example-initdb-3   1/1     Running   0          21h

NAME                                TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)    AGE
service/cluster-example-initdb-r    ClusterIP   10.102.12.26     <none>        5432/TCP   21h
service/cluster-example-initdb-ro   ClusterIP   10.107.192.79    <none>        5432/TCP   21h
service/cluster-example-initdb-rw   ClusterIP   10.101.109.103   <none>        5432/TCP   21h

CloudNativePG は postgres と互換性があるので psql などで接続できる。

$ psql "postgresql://gitea:gitea@10.101.109.103:5432/gitea"
psql (14.11 (Ubuntu 14.11-0ubuntu0.22.04.1), server 16.2 (Debian 16.2-1.pgdg110+2))
WARNING: psql major version 14, server major version 16.
         Some psql features might not work.
SSL connection (protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384, bits: 256, compression: off)
Type "help" for help.

gitea=>
gitea=> \l
                             List of databases
   Name    |  Owner   | Encoding | Collate | Ctype |   Access privileges
-----------+----------+----------+---------+-------+-----------------------
 gitea     | gitea    | UTF8     | C       | C     |
 postgres  | postgres | UTF8     | C       | C     |
 template0 | postgres | UTF8     | C       | C     | =c/postgres          +
           |          |          |         |       | postgres=CTc/postgres
 template1 | postgres | UTF8     | C       | C     | =c/postgres          +
           |          |          |         |       | postgres=CTc/postgres
(4 rows)

gitea=>
zenogawazenogawa

Minio

minio は operator をインストールした後、tenant (aws のリージョンみたいなもの) を作成する。

https://min.io/docs/minio/kubernetes/upstream/operations/install-deploy-manage/deploy-operator-helm.html

まず operator をデプロイ。

helm repo add minio-operator https://operator.min.io
helm install \
  --namespace minio-operator \
  --create-namespace \
  operator minio-operator/operator

operator をデプロイすると operator の console アクセス用の svc などが作成される。

次に tenant を作成する。ドキュメントでは operator console を使って作成する方法が推奨されているが、community が管理する tenant 用 helm chart があるのでこちらを使ってデプロイする。

https://min.io/docs/minio/kubernetes/upstream/operations/install-deploy-manage/deploy-minio-tenant-helm.html

chart 設定ファイルを取得。

helm show values minio-operator/tenant > values.yml

ここでは tenant 名を ap-northeast-1 とする。また、デフォルトでは pod 4 つ、pvc は 1 pod につき 4 つ作成されるが、ここでは pod 2, pvc 2 に変更する。

values.yml
tenant:
  name: ap-northeast-1
  
  pools:
    ###
    # The number of MinIO Tenant Pods / Servers in this pool.
    # For standalone mode, supply 1. For distributed mode, supply 4 or more.
    # Note that the operator does not support upgrading from standalone to distributed mode.
-    - servers: 4
+    - servers: 2
      ###
      # Custom name for the pool
      name: pool-0
      ###
      # The number of volumes attached per MinIO Tenant Pod / Server.
-      volumesPerServer: 4
+      volumesPerServer: 2
      ###

デプロイ

helm install -n minio --create-namespace minio minio-operator/tenant -f values.yml

これで ap-northeast-1 tenant 内に 2 つの minio pod が起動する。

$  kubectl get pod,svc,pvc
NAME                          READY   STATUS    RESTARTS   AGE
pod/ap-northeast-1-pool-0-0   2/2     Running   0          35s
pod/ap-northeast-1-pool-0-1   2/2     Running   0          35s

NAME                             TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)    AGE
service/ap-northeast-1-console   ClusterIP   10.100.94.142   <none>        9443/TCP   36s
service/ap-northeast-1-hl        ClusterIP   None            <none>        9000/TCP   36s
service/minio                    ClusterIP   10.104.135.47   <none>        443/TCP    36s

NAME                                                  STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS       VOLUMEATTRIBUTESCLASS   AGE
persistentvolumeclaim/data0-ap-northeast-1-pool-0-0   Bound    pvc-c2a427a6-9ed0-4705-bb36-6bb9703b6ce4   10Gi       RWO            openebs-hostpath   <unset>                 9m14s
persistentvolumeclaim/data0-ap-northeast-1-pool-0-1   Bound    pvc-74bef1ac-f740-4fd5-a1a7-84b0fc991512   10Gi       RWO            openebs-hostpath   <unset>                 9m14s
persistentvolumeclaim/data1-ap-northeast-1-pool-0-0   Bound    pvc-c5ac58ef-d09d-4ab2-9642-f0df356f9d06   10Gi       RWO            openebs-hostpath   <unset>                 9m14s
persistentvolumeclaim/data1-ap-northeast-1-pool-0-1   Bound    pvc-e79e49a2-8279-4dc2-935e-19d955651527   10Gi       RWO            openebs-hostpath   <unset>                 9m14s

svc の ap-northeast-1-console にアクセスすることで webUI を使用できるが、ここでは minio client の mc を使って bucket を作成する。

https://min.io/docs/minio/linux/reference/minio-mc.html?ref=docs#quickstart

mc のインストール

curl https://dl.min.io/client/mc/release/linux-amd64/mc -o mc
chmod +x mc
sudo mv mc /usr/local/bin/mc
mc --help

作った tenant を mc の alias に設定。書式は以下。

mc alias set [alias_name] [endpoint] [user] [password]

username/password は tenant operator の values.yaml に記載されている。デフォルトではそれぞれ minio minio123。接続先は headless の service/myminio-hl を指定する。

mc alias set minio https://ap-northeast-1-hl.minio.svc.cluster.local:9000 minio minio123

もしくは minio svc を宛先に指定しても ok。この場合は --insecure をつける。

mc alias set minio https://10.104.135.47 minio minio123 --insecure

gitea セットアップ時に使用するため tenant に bucket gitea を作成。

$ mc mb minio/gitea
Bucket created successfully `minio/gitea`.

# minio svc の場合
# mc mb --insecure minio/gitea

$ mc ls minio/
[2024-04-21 06:12:14 GMT]     0B gitea/
zenogawazenogawa

Gitea

準備がかなり長くなったが必要なものが揃ったので gitea をカスタムしてデプロイする。

helm repo add gitea-charts https://dl.gitea.com/charts/
helm show values gitea-charts/gitea > values.yml

values.yml の中身を変更していく。
indexer を meilisearch に指定。

gitea:
  indexer:
    ISSUE_INDEXER_CONN_STR: http://meilisearch.meilisearch.svc.cluster.local:7700
    ISSUE_INDEXER_ENABLED: true
    ISSUE_INDEXER_TYPE: meilisearch
    REPO_INDEXER_ENABLED: false

メモリキャッシュを dragonfly に指定。

gitea:
  config:
    queue:
      TYPE: redis
      CONN_STR: redis://dragonfly-sample.dragonfly.svc.cluster.local:6379
    session:
      PROVIDER: redis
      PROVIDER_CONFIG: redis://dragonfly-sample.dragonfly.svc.cluster.local:6379
    cache:
      ENABLED: true
      ADAPTOR: false
      HOST: redis://dragonfly-sample.dragonfly.svc.cluster.local:6379

redis-cluster:
  enabled: false

RDB を cloudnativePG に指定。

gitea:
  config:
    database:
      DB_TYPE: postgres
      HOST: cluster-example-initdb-rw.cnpg.svc.cluster.local:5432
      NAME: gitea
      USER: gitea
      PASSWD: gitea

postgresql-ha:
  enabled: false

外部ストレージを minio に指定

persistence:
  enabled: false
  accessModes:
    - ReadWriteMany

gitea:
  config:
    picture:
      AVATAR_STORAGE_TYPE: minio
    storage:
      STORAGE_TYPE: minio
      SERVE_DIRECT: false
      MINIO_ENDPOINT: ap-northeast-1.minio.svc.cluster.local:9000
      MINIO_LOCATION: ap-northeast-1
      MINIO_ACCESS_KEY_ID: minio
      MINIO_SECRET_ACCESS_KEY: minio123
      MINIO_BUCKET: gitea
      MINIO_USE_SSL: true
      MINIO_INSECURE_SKIP_VERIFY: true

デプロイ

helm install -n gitea gitea gitea-charts/gitea -f values.yml --create-namespace

これでひとまず動く gitea がデプロイされる。
以降は補助機能や使い勝手をカスタマイズしていく。

zenogawazenogawa

LDAP

gitea デプロイ時に LDAP サーバー接続設定もまとめて行う。

https://gitea.com/gitea/helm-chart/src/branch/main#ldap-settings

bindpass, binduser は secret に保存する

apiVersion: v1
kind: Secret
metadata:
  name: gitea-ldap-secret
type: Opaque
stringData:
  bindDn: "uid=admin,ou=people,dc=ldap,dc=centre,dc=com"
  bindPassword: password

values.yml を編集して LDAP サーバーのホストや bind, ユーザー検索フィルターなどを追加する。

values.yml
gitea:
  ldap:
    - name: "LLDAP"
      securityProtocol: unencrypted
      host: ldap.centre.com
      port: 3890
      userSearchBase: "ou=people,dc=ldap,dc=centre,dc=com"
      userFilter: "(&(objectClass=person)(|(uid=%[1]s)(mail=%[1]s)))"
      adminFilter: "(memberof=cn=gitea_admin,ou=groups,dc=ldap,dc=centre,dc=com)"
      emailAttribute: mail
      existingSecret: gitea-ldap-secret # 上記で作成した secret 名
      usernameAttribute: uid
      firstnameAttribute: givenName
      surnameAttribute: sn
      emailAttribute: mail
      avatarAttribute: jpegPhoto
      synchronizeUsers: true

helm の README に乗っていないプロパティは admin の add-ldap: Add new LDAP (via Bind DN) authentication source などの引数から読み取る。項目は - を削除して大文字でつなぐことで対応する。例えば --firstname-attribute firstnameAttribute となる。
https://docs.gitea.com/administration/command-line#admin

なお、Gitea の UI から追加するときは LDAP 側の group に基づいて自動で org に所属するように設定することができるが、この機能はまだ未対応らしい。gitea CLI でこの機能が実装されていないため helm chart でも指定できない。
https://github.com/go-gitea/gitea/issues/26069

zenogawazenogawa

OAuth

oauth を設定する場合は LDAP と同様に values.yml の gitea.oauth に設定していく。

gitea:
  oauth:
    - name: authelia
      provider: "openidConnect"
      key: gitea
      existingSecret: gitea-oauth-secret
      autoDiscoverUrl: "https://auth.example.com/.well-known/openid-configuration"
      iconUrl: https://gitea.ops.com/assets/authelia.png
      groupClaimName: groups
      adminGroup: admin
      groupTeamMap: '{"gitea_developer": {"developer": []}}'

oauth の client の secret も k8s secret として作成し、value.yml では existingSecret に secret 名を記載する。

gitea-oauth-secret.yml
apiVersion: v1
kind: Secret
metadata:
  name: gitea-oauth-secret
type: Opaque
stringData:
  secret: test

oauth のサインインボタンに独自の画像を設定する場合は gitea コンテナ内に画像をマウントする必要があるが、configmap の binaryData を使えば可能。
例えば authelia.png をアイコンに設定する場合はまず configmap を作成。

kubectl create configmap gitea-icons \
  --save-config \
  --from-file=authelia.png=authelia.png

gitea コンテナ内の /data/gitea/public/assets/authelia.png にマウントするため values.yml に追加。

values.yml
extraVolumes:
  - name: gitea-icons
    configMap:
      name: gitea-icons
      items:
        - key: "authelia.png"
          path: "authelia.png"

extraContainerVolumeMounts:
  - name: gitea-icons
    mountPath: "/data/gitea/public/assets"

gitea:
  oauth:
    - name: authelia
      iconUrl: https://gitea.ops.com/assets/authelia.png

LDAP や OAuth は記事を書いた際に docker で構築した設定を参考にしているが、これらを k8s にデプロイすればこちらも HA 構成にできる。
https://zenn.dev/zenogawa/articles/try_ldap_lldap
https://zenn.dev/zenogawa/articles/try_authelia

authelia は helm chart があるのでコレを使えばよさそう。
https://github.com/authelia/chartrepo?tab=readme-ov-file

LLDAP は現時点で公式の chart がなく HA 構成も対応してないので、k8s にデプロイする際は openldap の chart などを使う必要がある。
https://github.com/jp-gouin/helm-openldap

zenogawazenogawa

UI Theme

UI のテーマにカスタムテーマを使いたい場合は https://gitea.com/gitea/helm-chart#themes に指定方法が書いてあるが、試してもうまく適用されなかったので docker と同じ方法で設定する。

template の内容を書いた configmap を作成。

gitea-theme.yml
apiVersion: v1
kind: ConfigMap
metadata:
  name: gitea-template
  namespace: gitea
data:
  body_outer_pre.tmpl: |
    {{ if .IsSigned }}
      {{ if and (ne .SignedUser.Theme "gitea") (ne .SignedUser.Theme "arc-green") }}
        <link rel="stylesheet" href="https://theme-park.dev/css/base/gitea/{{.SignedUser.Theme}}.css">
      {{end}}
    {{ else if and (ne DefaultTheme "gitea") (ne DefaultTheme "arc-green") }}
      <link rel="stylesheet" href="https://theme-park.dev/css/base/gitea/{{DefaultTheme}}.css">
    {{end}}

コンテナ内の /data/gitea/templates/custom/body_outer_pre.tmpl にマウントするよう values.yml を編集。 gitea.config.ui に選択可能なテーマ一覧とデフォルトテーマを指定。

values.yml
extraVolumes:
  - name: gitea-theme-template
    configMap:
      name: gitea-template
      items:
        - key: "body_outer_pre.tmpl"
          path: "body_outer_pre.tmpl"

extraContainerVolumeMounts:
  - name: gitea-theme-template
    mountPath: "/data/gitea/templates/custom"

gitea:
  config:
    ui:
      THEMES: gitea,arc-green,plex,aquamarine,dark,dracula,hotline,organizr,space-gray,hotpink,onedark,overseerr,nord
      DEFAULT_THEME: dracula

これで起動時にデフォルトテーマが適用される。各ユーザの外観から theme に指定した他のテーマに切替可能。
テーマは theme-park.dev にあるものが使える。内部的にはここにあるテーマを指定するとそれに対応した github 上の css を読み込む形式になっている。例えば dracula theme を指定した際は以下の css が読み込まれている。

zenogawazenogawa

Runner

Gitea は ver. 1.19 から Actions に対応しており、github workflow とほぼ同じ構文の yaml ファイルを repository に置くことでワークフローを実行できる。runner はこのワークフローの実行環境のこと。
github や gitlab の self-hosted runner と同様に gitea でも runner は自分で用意する必要がある。

helm chart では現時点で runner は未対応。ただ以下のような PR が作成されており先月にコメントされているので近いうちにマージされるかもしれない。
https://gitea.com/gitea/helm-chart/pulls/596

k8s 用の runner のマニフェストは以下にあるので、これを使うことで runner 自体は k8s 上にデプロイできる。
https://gitea.com/gitea/act_runner/src/branch/main/examples/kubernetes/rootless-docker.yaml

gitlab と同様に gitea runner でも以下のレベルでそれぞれ runner を登録できる。
https://docs.gitea.com/usage/actions/act-runner#runner-levels

You can register a runner in different levels, it can be:
Instance level: The runner will run jobs for all repositories in the instance.
Organization level: The runner will run jobs for all repositories in the organization.
Repository level: The runner will run jobs for the repository it belongs to.

基本的には gitea 側で作成したいレベルに応じて registration token を発行 → token を指定した runner pod を作成 → 使用可能という流れになる。

ここでは Instance level (gitea 全体)で使用可能な runner を登録する。
admin ユーザーで gitea にログインし、Site Administration > Actions > Create New Runner を選択すると文字列の registration token が取得できるので、これを保存した k8s secret を作成する。
githea のマニフェスト例を使うと以下のようになる。

kind: PersistentVolumeClaim
apiVersion: v1
metadata:
  name: act-runner-vol
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 1Gi

---
apiVersion: v1
stringData:
  token: qCKPicvoWCfzch2hKEQsXNEIMrRsRe9UfSbjFXYL
kind: Secret
metadata:
  name: runner-secret
type: Opaque

---
apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: act-runner
  name: act-runner
spec:
  replicas: 1
  selector:
    matchLabels:
      app: act-runner
  strategy: {}
  template:
    metadata:
      labels:
        app: act-runner
    spec:
      restartPolicy: Always
      volumes:
      - name: runner-data
        persistentVolumeClaim:
          claimName: act-runner-vol
      securityContext:
        fsGroup: 1000
      containers:
      - name: runner
        image: gitea/act_runner:nightly-dind-rootless
        imagePullPolicy: Always
        # command: ["sh", "-c", "while ! nc -z localhost 2376 </dev/null; do echo 'waiting for docker daemon...'; sleep 5; done; /sbin/tini -- /opt/act/run.sh"]
        env:
        - name: DOCKER_HOST
          value: tcp://localhost:2376
        - name: DOCKER_CERT_PATH
          value: /certs/client
        - name: DOCKER_TLS_VERIFY
          value: "1"
        - name: GITEA_INSTANCE_URL
          value: http://gitea-http.gitea.svc.cluster.local:3000
        - name: GITEA_RUNNER_REGISTRATION_TOKEN
          valueFrom:
            secretKeyRef:
              name: runner-secret
              key: token
        securityContext:
          privileged: true
        volumeMounts:
        - name: runner-data
          mountPath: /data

runner pod 作成、登録に成功すると runner が追加される。

適当に repository を作成し、.gitea/workflow/demo.yml を作って push する。
中身は https://docs.gitea.com/usage/actions/quickstart#use-actions と同じ。

demo.yml
name: Gitea Actions Demo
run-name: ${{ gitea.actor }} is testing out Gitea Actions 🚀
on: [push]

jobs:
  Explore-Gitea-Actions:
    runs-on: ubuntu-latest
    steps:
      - run: echo "🎉 The job was automatically triggered by a ${{ gitea.event_name }} event."
      - run: echo "🐧 This job is now running on a ${{ runner.os }} server hosted by Gitea!"
      - run: echo "🔎 The name of your branch is ${{ gitea.ref }} and your repository is ${{ gitea.repository }}."
      - name: Check out repository code
        uses: actions/checkout@v3
      - run: echo "💡 The ${{ gitea.repository }} repository has been cloned to the runner."
      - run: echo "🖥️ The workflow is now ready to test your code on the runner."
      - name: List files in the repository
        run: |
          ls ${{ gitea.workspace }}
      - run: echo "🍏 This job's status is ${{ job.status }}."

また、repository の設定から Action の有効化も設定しておく。
gitea -> runner への接続に成功すると Actions タブからワークフローの実行結果が確認できる。実行状況は runner pod のログからも確認可能。

なお runner を登録すると gitea pod で completed POST /api/actions/runner.v1.RunnerService/FetchTask という大量のログが定期的に出力されるが、現時点では仕様通り通りの挙動とのこと。issue で改善の提案がされている。
https://github.com/go-gitea/gitea/issues/24543

Action の使用

github 上のパブリックレポジトリで公開されている Github Action であれば https://github.com/[owner]/[repo]@[version] の書式でワークフロー内で使用できる。例えば python をセットアップして pip で依存パッケージをインストールするワークフローは以下。

---
name: Lint and static check codes
on: [push]
jobs:
  check:
    runs-on: ubuntu-22.04
    name: Python test
    steps:
      - uses: https://github.com/actions/checkout@v3
      - uses: https://github.com/actions/setup-python@v5
        with:
          python-version: '3.11'

      - name: Install dependencies
        run: pip install flake8 black mypy yamllint

      - name: Flake8
        run: flake8 *.py

      - name: Black
        run: black *.py

      - name: Mypy
        run: mypy *.py

      - name: Yaml lint
        run: yamllint .

初回実行時は action の tar.gz をダウンロードするため時間がかかるが、2 回目以降はキャッシュが効いてて短くなる。

::group::Installed versions
Version 3.11 was not found in the local cache
Version 3.11 is available for downloading
Download from "https://github.com/actions/python-versions/releases/download/3.11.9-8525206794/python-3.11.9-linux-22.04-x64.tar.gz"
Extract downloaded archive
[command]/usr/bin/tar xz --warning=no-unknown-keyword --overwrite -C /tmp/4693931b-bfc8-4669-95c8-59d620a2e360 -f /tmp/a67367ea-35a6-402d-9dc4-6969c6516735
Execute installation script
Check if Python hostedtoolcache folder exist...
Creating Python hostedtoolcache folder...
Create Python 3.11.9 folder
Copy Python binaries to hostedtoolcache folder
Create additional symlinks (Required for the UsePythonVersion Azure Pipelines task and the setup-python GitHub Action)
Upgrading pip...

github action が使えると github ワークフローの大部分を使い回せるので移行が楽になりそう。