LoginSignup
27
18

More than 1 year has passed since last update.

KeycloakによるKubernetesのOIDC認証を試す

Last updated at Posted at 2022-05-08

1.はじめに

一般的にKubernetesはユーザー管理の仕組みを有しておらず何かしら外部の仕組みを用いてユーザーの認証を行うことになり、kubeadmminikubeなどによりバニラなKubernetesクラスターを構築した場合、通常はx509証明書(adminの証明書が払い出される)によりユーザー認証を行うことになります。

ここではOIDC(OpenID Connect)IDプロバイダーの1つであるKeycloakでユーザー情報の管理およびOIDC認証を行い、その認証情報(id_token)を用いてKubernetes(API)での認証を行うことを目指します。

Keycloak
https://www.keycloak.org/

なお、KubernetesにおけるOIDC認証のシーケンスは公式ドキュメントで示されている以下が参考になります。

pic0.png

OpenID Connect Tokens
https://kubernetes.io/docs/reference/access-authn-authz/authentication/#openid-connect-tokens

2.前提

  • minikube v1.25.2(Kubernetes v1.23.3)のシングルNode構成で検証した
  • Keycloakは現時点の最新バージョンであるv18.0.0を用いることとし、Kubernetes(minikube)クラスター上に構築した(一般的なKubernetesクラスター上に構築するケースでもそんなに大差はない想定)
  • 検証用なのでKeycloakは以下の条件で起動した
    • dev mode(本来であればprod modeで起動すべき)
    • KeycloakのデータストアはInternal DB(H2)を永続化して利用(本来であればPostgreSQLなど外部DBを利用すべき)
    • 証明書は自己証明書を利用した(本来であれば信頼できるCAで署名されたものを利用すべき)
  • Keycloakの名前解決にはDNSではなくhostsファイルを使用した
    • Keycloakにはそれぞれkube-apiserverとクライアントが同じホスト名でアクセスできる必要があるため暫定対処
    • 本来はDNSの名前解決でアクセスできるのが望ましい
  • Keycloakのアクセスエンドポイント(HTTP/HTTPS)はNodePortとした
    • Keycloakにはそれぞれkube-apiserverとクライアントが同じポートでアクセスできる必要があるため暫定対処
    • 本来はingressLBに証明書のインポートを行いTLS終端すべき

基本的にKeycloak公式ドキュメントに沿っているが、必要に応じて設定を変更している
Getting started/Kubernetes
https://www.keycloak.org/getting-started/getting-started-kube

3.Keycloakの証明書を作成

まずはKeycloakTLSに用いるキーペアを作成する。
この後キーペアをKubernetesクラスター上にデプロイするPodhostPathでマウントするためminikube VM上で作成を行う。

minikubeSSH接続する。

minikube ssh

ディレクトリ作成

sudo su -
mkdir /srv/keycloak-ssl
cd /srv/keycloak-ssl 

証明書設定ファイルの作成

sslcert.conf

[req]
distinguished_name = req_distinguished_name
x509_extensions = v3_req
prompt = no
[req_distinguished_name]
CN = keycloak 
[v3_req]      
keyUsage = keyEncipherment, dataEncipherment
extendedKeyUsage = serverAuth, clientAuth   
subjectAltName = @alt_names                 
[alt_names]
DNS.1 = keycloak                            
DNS.2 = keycloak.example.com

キーペアの作成

# openssl req -x509 -nodes -days 730 -newkey rsa:2048 -keyout tls.key -out tls.crt -config sslcert.conf -extensions 'v3_req'

ls
sslcert.conf  tls.crt  tls.key 

公開鍵の内容確認
Issuer: CNSubject: CNが同じ自己証明書(所謂オレオレ証明書)になっていることがわかる。

# openssl x509 -in tls.crt --text --noout
Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number:
            53:e8:c5:1e:e4:6c:dd:13:59:f8:4d:81:0d:6f:56:97:06:e3:4f:f3
        Signature Algorithm: sha256WithRSAEncryption
        Issuer: CN = keycloak
        Validity
            Not Before: May  8 06:27:59 2022 GMT
            Not After : May  7 06:27:59 2024 GMT
        Subject: CN = keycloak
        Subject Public Key Info:
            Public Key Algorithm: rsaEncryption
                RSA Public-Key: (2048 bit)
                Modulus:
                ・・・

今回KeycloakのPodはhostPathを用いてキーペア格納フォルダをマウントするため権限を変更しておく。
※厳密にはtls.keyRead権限を付与しておけば良いはず。

# chmod -R 777 /srv/keycloak-ssl 

Master Nodeへの公開鍵配置

# mkdir -p /etc/kubernetes/ssl/
# cp tls.crt /etc/kubernetes/ssl/kc-ca.crt
# ls /etc/kubernetes/ssl/
kc-ca.crt

4.Keycloak永続化用ディレクトリの作成

KeycloakPodとしてデプロイする際、通常はKeycloak内部のローカルDB(H2)に情報が保存され、コンテナが再起動するとそれらのデータは揮発してしまう。
本来は外部DB(PostgreSQLなど)を保存先として指定すべきだが、今回はデータ保存先ディレクトリをminikube VM上のディレクトリとし、PodhostPathとしてvolumeMountsすることで対応した。

minikube VM上で保存先ディレクトリを作成する。

# mkdir -p /srv/keycloak
# chmod -R 777 /srv/keycloak
# ls -la /srv
total 0
drwxr-xr-x  4 root root  80 May  8 06:46 .
drwxr-xr-x 19 root root 500 May  8 06:24 ..
drwxrwxrwx  2 root root  40 May  8 06:46 keycloak
drwxrwxrwx  2 root root 100 May  8 06:27 keycloak-ssl

5.Keycloak構築

minikubeの操作権限のあるクライアントからKubernetesクラスタ上にKeycloakPodとしてデプロイする。

Keycloakのデプロイ

Keycloak公式ドキュメントを参考に一部manifestを変更し、以下のmanifestを用いてデプロイした。

Keycloak起動設定留意点

  • dev modeで起動
  • HTTPSポートの指定(デフォルト8443)
  • NodePortでサービスを公開
  • TLSキーペアの指定と格納先をhostPathとしてマウント

keycloak.yaml

apiVersion: v1
kind: Namespace
metadata:
  name: keycloak
---
apiVersion: v1
kind: Service
metadata:
  name: keycloak
  namespace: keycloak
  labels:
    app: keycloak
spec:
  ports:
  - name: http
    port: 8080
    targetPort: 8080
    nodePort: 31008 # NodePortを明示
  - name: https # HTTPアクセス用に8443ポートを公開
    port: 8443
    targetPort: 8443
    nodePort: 32084 # NodePortを明示
  selector:
    app: keycloak
  type: NodePort
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: keycloak
  namespace: keycloak
  labels:
    app: keycloak
spec:
  replicas: 1
  selector:
    matchLabels:
      app: keycloak
  template:
    metadata:
      labels:
        app: keycloak
    spec:
      containers:
      - name: keycloak
        image: quay.io/keycloak/keycloak:18.0.0
        args: ["start-dev"] # dev modeで起動
        env:
        - name: KEYCLOAK_ADMIN
          value: "admin"
        - name: KEYCLOAK_ADMIN_PASSWORD
          value: "admin"
        - name: KC_PROXY
          value: "edge"
        - name: "KC_HTTP_ENABLED" # HTTP接続有効化(ブラウザアクセス用)
          value: "true"
        - name: "KC_HTTPS_PORT" # HTTPSポート
          value: "8443"
        - name: KC_HTTPS_CERTIFICATE_FILE # 公開鍵のパス
          value: "/opt/keycloak/tls/tls.crt"
        - name: KC_HTTPS_CERTIFICATE_KEY_FILE # 秘密鍵のパス
          value: "/opt/keycloak/tls/tls.key"
        ports:
        - name: http
          containerPort: 8080
        - name: https # TLS用に8443ポートを公開
          containerPort: 8443
        readinessProbe:
          httpGet:
            path: /realms/master
            port: 8080
        volumeMounts: # 再起動してもデータが揮発しないよう暫定でhostPathにマウント
        - name: "keycloak-persistent-storage"
          mountPath: "/opt/keycloak/data"
        - name: "keycloak-ssl" # キーペア格納先
          mountPath: "/opt/keycloak/tls"
      volumes:
      - name: keycloak-persistent-storage
        hostPath:
          path: /srv/keycloak
          type: DirectoryOrCreate
      - name: keycloak-ssl # 証明書格納先
        hostPath:
          path: /srv/keycloak-ssl     
          type:  DirectoryOrCreate

デプロイ実行

# kubectl apply -f keycloak.yaml
namespace/keycloak created
service/keycloak created
deployment.apps/keycloak created

# kubectl -n keycloak get all
NAME                            READY   STATUS    RESTARTS   AGE
pod/keycloak-679b66bbd8-l2n9g   1/1     Running   0          5m16s

NAME               TYPE       CLUSTER-IP     EXTERNAL-IP   PORT(S)                         AGE
service/keycloak   NodePort   10.102.40.27   <none>        8080:31008/TCP,8443:32084/TCP   5m16s

NAME                       READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/keycloak   1/1     1            1           5m16s

NAME                                  DESIRED   CURRENT   READY   AGE
replicaset.apps/keycloak-679b66bbd8   1         1         1       5m16s

Keycloak名前解決設定

今回は検証なので/etc/hostsにレコード追加して対応した。
Keycloakにブラウザアクセスする端末とminikube VM上それぞれで追加

minikube VMのIPアドレス確認

# minikube ip
192.168.59.101

/etc/hosts

・・・
<minikube VM IP>	keycloak.example.com
・・・

6.Keycloakアクセス確認

Keycloakへのアクセス確認を行う。

curlアクセス確認(HTTPS)

minikube VMからcurlコマンドでHTTPSアクセスできるか確認する。
Keycloakは自己証明書を利用しているため、公開鍵をCAとして署名している。
そのためTLSするクライアント側でKeycloakのCA証明書(=公開鍵)を信頼することでアクセスできるようにする。

# curl -v --cacert /etc/kubernetes/ssl/kc-ca.crt https://keycloak.example.com:32084

ブラウザアクセス確認(HTTP)

ブラウザで以下にアクセスする。
※ブラウザから直接HTTPSでアクセスしてもERR_SSL_KEY_USAGE_INCOMPATIBLEで画面表示できなかったため、HTTPアクセスするようにした

http://keycloak.example.com:31008

7.Keycloakの設定

Keycloakにブラウザからアクセスして以下の設定を行う。

管理者画面にアクセス

ID/Password=adminAdministration Consoleにログイン。

pic1.png
pic2.png

Realmの作成

Keycloakにおいてテナントの概念に該当するRealmを作成する。
今回はkubernetesという名前で作成した。

pic3.png
pic4.png
pic5.png

ClientsとClientScopesの作成

Keycloakの認証受口になるClientとその設定に該当するClient Scopeを作成する。

Clientの作成

  • Client ID: kubernetes
  • Access Type: confidential
  • Valid Redirect URIs: http://* https://*

pic6.png
pic7.png
pic8.png
pic9.png

Client Scopeの作成

  • Client Scope: groups
    • Mappers: name groupsを追加

pic10.png
pic11.png
pic12.png
pic13.png
pic14.png
pic15.png
pic16.png
pic17.png

ClientClient Scopesgroupsを追加

pic18.png
pic19.png
pic20.png

ClientCredentialsからSecret取得

pic21.png

ユーザーとグループの作成

認証に用いるユーザーとグループを作成する。

administratorsdevelopersというグループを作成。
これらのグループに対して後程RBACによりKubernetesクラスターに対する権限を付与する

  • administrators: Kubernetesクラスターの管理者権限を持つグループ
  • developers: Kubernetesクラスターに対して特定の権限のみしか持たないグループ

※キャプチャではadministratorsの作成しか示していないがdevelopersについても同様に作成する。

pic22.png
pic23.png
pic24.png

admin-userdev-userというユーザーをそれぞれ作成。

  • admin-user: administratorsグループに所属
  • dev-user: developersグループに所属

※キャプチャではadmin-userの作成しか示していないがdev-userについても同様に作成する。
※パスワードは任意の値を設定(ここではP@ssw0rdという値を設定した)

pic25.png
pic26.png
pic27.png

確認

以下コマンドによりKeycloakに認証を行いid-tokenrefresh-tokenを取得できることを確認する。

# curl -k -d "grant_type=password" -d "scope=openid" -d "client_id=kubernetes" -d "client_secret=<client=kubernetesのSecret>" -d "username=<Keycloakで作成したユーザー名>" -d "password=<Keycloakで作成したユーザーパスワード>" https://keycloak.example.com:32084/realms/kubernetes/protocol/openid-connect/token | jq .

・・・
{
  "access_token": ・・・,
  "expires_in": 300,
  "refresh_expires_in": 1800,
  "refresh_token": ・・・
  "id_token": ・・・,
  "not-before-policy": 0,
  "session_state": ・・・,
  "scope": "openid profile email groups"
}

以下コマンドにより各tokenの検証が行える。

  • exp: token有効期限(UNIX時間)
  • iat: token発行時間(UNIX時間)
  • active: tokenの有効有無
# curl -k --user "kubernetes:<client=kubernetesのsecret>" -d "token=<token>" https://keycloak.example.com:32084/realms/kubernetes/protocol/openid-connect/token/introspect | jq .

・・・
{
  "exp": 1651997178,
  "iat": 1651996878,
  ・・・
  "active": true
}

8.kube-apiserverの設定

kube-apiserverKeycloakを用いてOIDC認証を行うための設定を行う。
minikube VMにアクセスし、kube-apiservermanifestに起動OPおよびKeycloakCA証明書(=公開鍵)格納先をvolumeMountsする。
なお、kube-apiserverStatic Podとして起動しているためmanifest変更後に自動的に再デプロイされる。

/etc/kubernetes/manifests/kube-apiserver.yaml

・・・
spec:
  containers:
  - command:
    - kube-apiserver
    ・・・
    - --oidc-issuer-url=https://keycloak.example.com:32084/realms/kubernetes
    - --oidc-client-id=kubernetes
    - --oidc-username-claim=name
    - --oidc-groups-claim=groups
    - --oidc-ca-file=/etc/kubernetes/ssl/kc-ca.crt
    ・・・
    volumeMounts:
    ・・・
    - mountPath: /etc/kubernetes/ssl
      name: keycloak-ca-certificates
      readOnly: true
  ・・・
  volumes: 
  ・・・
  - hostPath:
      path: /etc/kubernetes/ssl
      type: DirectoryOrCreate
    name: keycloak-ca-certificates
status: {} 

9.RBACの作成

Keycloakで作成したグループに対してRBACの設定を行う。
今回はそれぞれ以下のような権限設定とする。

  • administrators: クラスターに対する全ての権限を付与(ビルドインのClusterRole=cluster-adminをバインド)
  • developers: NamespacePodの参照権限のみを付与(独自でClusterRole=developer-roleを作成してバインド)

adminrole.yaml

kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: administrator-crb
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: cluster-admin
subjects:
- kind: Group
  name: "administrators"
  apiGroup: rbac.authorization.k8s.io

devrole.yaml

kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: developer-role
rules:
  - apiGroups: [""]
    resources: ["namespaces","pods"]
    verbs: ["get", "watch", "list"]
---
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: developer-crb
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: developer-role
subjects:
- kind: Group
  name: "developers"
  apiGroup: rbac.authorization.k8s.io

クラスターに適用

# kubectl apply -f adminrole.yaml
clusterrolebinding.rbac.authorization.k8s.io/administrator-crb created

# kubectl apply -f devrole.yaml
clusterrole.rbac.authorization.k8s.io/developer-role created
clusterrolebinding.rbac.authorization.k8s.io/developer-crb created

10.kubectlの設定

kubectlにはOIDC ID プロバイダーにログインする仕組みが用意されていないため、
手動でログインおよびkubeconfigへのtoken設定を行う。

pic28.png

kubectlからログインを行うためのプラグインも存在する模様
kubelogin
https://github.com/int128/kubelogin

手順①:Keycloakへのログイン

以下コマンドによりKeycloakに認証を行いid-tokenrefresh-tokenを取得する。

# curl -k -d "grant_type=password" -d "scope=openid" -d "client_id=kubernetes" -d "client_secret=<client=kubernetesのsecret>" -d "username=<Keycloakで作成したユーザー名>" -d "password=<Keycloakで作成したユーザーパスワード>" https://keycloak.example.com:32084/realms/kubernetes/protocol/openid-connect/token | jq .

手順②:kubeconfigの設定

取得したid-tokenrefresh-tokenを用いてkubeconfigの設定を行う。

# kubectl config set-credentials <Keycloakで作成したユーザー名> \
    "--auth-provider=oidc" \
    "--auth-provider-arg=idp-issuer-url=https://keycloak.example.com:32084/realms/kubernetes" \
    "--auth-provider-arg=client-id=kubernetes" \
    "--auth-provider-arg=idp-certificate-authority=<keycloak公開鍵のパス>" \
    "--auth-provider-arg=client-secret=<client=kubernetesのsecret>" \
    "--auth-provider-arg=id-token=<id-token>" \
    "--auth-provider-arg=refresh-token=<refresh-token>"
    
# kubectl config set-context <Keycloakで作成したユーザー名>@<kubeconfigで定義されているk8sクラスタ名> --cluster=<kubeconfigで定義されているk8sクラスタ名> --user=<Keycloakで作成したユーザー名>

# kubectl config use-context <Keycloakで作成したユーザー名>@<kubeconfigで定義されているk8sクラスタ名>
Switched to context "<Keycloakで作成したユーザー名>@<kubeconfigで定義されているk8sクラスタ名>".

上記操作を一括で実施するshell

#!/bin/bash

scope=openid
client_id=kubernetes
client_secret=<client=kubernetesのsecret>
username=<Keycloakで作成したユーザー名>
password=<Keycloakで作成したユーザーパスワード>
oidc_url=https://keycloak.example.com:32084/realms/kubernetes/protocol/openid-connect/token
realm_url=https://keycloak.example.com:32084/realms/kubernetes
certificate=<keycloak公開鍵のパス>
cluster=<kubeconfigで定義されているk8sクラスタ名>

### Generate Authentication token

json_data=`curl -k -d "grant_type=password" -d "scope=${scope}" -d "client_id=${client_id}" -d "client_secret=${client_secret}" -d "username=${username}" -d "password=${password}" ${oidc_url}`

id_token=`echo $json_data | jq '.id_token' | tr -d '"'`
refresh_token=`echo $json_data | jq '.refresh_token' | tr -d '"'`
access_token=`echo $json_data | jq '.access_token' | tr -d '"'`

### Print tokens

echo "ID_TOKEN=$id_token"; echo
echo "REFRESH_TOKEN=$refresh_token"; echo
echo "ACCESS_TOKEN=$access_token"; echo

### Introspect the id token

token=`curl -k --user "${client_id}:${client_secret}" -d "token=${id_token}" ${oidc_url}/introspect`
token_details=`echo $token | jq .`
echo $token_details

### Update kubectl config

kubectl config set-credentials ${username} \
    "--auth-provider=oidc" \
    "--auth-provider-arg=idp-issuer-url=${realm_url}" \
    "--auth-provider-arg=client-id=${client_id}" \
    "--auth-provider-arg=client-secret=${client_secret}" \
    "--auth-provider-arg=refresh-token=${refresh_token}" \
    "--auth-provider-arg=idp-certificate-authority=${certificate}" \
    "--auth-provider-arg=id-token=${id_token}"

### Create new context

kubectl config set-context ${username}@${cluster} --cluster=${cluster} --user=${username}

### Set current context
kubectl config use-context ${username}@${cluster} 

### Validate access with new context

kubectl get pods

11.動作確認

Keycloakで作成したユーザーを用いてKubernetesクラスターへのアクセスを確認する。

admin-user

クラスターに対して全ての操作が可能

# kubectl -n kube-system get all
NAME                                   READY   STATUS    RESTARTS      AGE
pod/coredns-64897985d-dn9c4            1/1     Running   0             125m
pod/etcd-minikube                      1/1     Running   0             125m
pod/kube-apiserver-minikube            1/1     Running   0             15m
pod/kube-controller-manager-minikube   1/1     Running   0             125m
pod/kube-proxy-gbw5v                   1/1     Running   0             125m
pod/kube-scheduler-minikube            1/1     Running   0             125m
pod/storage-provisioner                1/1     Running   4 (15m ago)   125m

NAME               TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)                  AGE
service/kube-dns   ClusterIP   10.96.0.10   <none>        53/UDP,53/TCP,9153/TCP   125m

NAME                        DESIRED   CURRENT   READY   UP-TO-DATE   AVAILABLE   NODE SELECTOR            AGE
daemonset.apps/kube-proxy   1         1         1       1            1           kubernetes.io/os=linux   125m

NAME                      READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/coredns   1/1     1            1           125m

NAME                                DESIRED   CURRENT   READY   AGE
replicaset.apps/coredns-64897985d   1         1         1       125m

dev-user

許可されていないリソースの参照はエラーになる。

# kubectl -n kube-system get all
NAME                               READY   STATUS    RESTARTS      AGE
coredns-64897985d-dn9c4            1/1     Running   0             128m
etcd-minikube                      1/1     Running   0             128m
kube-apiserver-minikube            1/1     Running   0             17m
kube-controller-manager-minikube   1/1     Running   0             128m
kube-proxy-gbw5v                   1/1     Running   0             128m
kube-scheduler-minikube            1/1     Running   0             128m
storage-provisioner                1/1     Running   4 (18m ago)   128m
Error from server (Forbidden): replicationcontrollers is forbidden: User "https://keycloak.example.com:32084/realms/kubernetes#user dev" cannot list resource "replicationcontrollers" in API group "" in the namespace "kube-system"
Error from server (Forbidden): services is forbidden: User "https://keycloak.example.com:32084/realms/kubernetes#user dev" cannot list resource "services" in API group "" in the namespace "kube-system"
Error from server (Forbidden): daemonsets.apps is forbidden: User "https://keycloak.example.com:32084/realms/kubernetes#user dev" cannot list resource "daemonsets" in API group "apps" in the namespace "kube-system"
Error from server (Forbidden): deployments.apps is forbidden: User "https://keycloak.example.com:32084/realms/kubernetes#user dev" cannot list resource "deployments" in API group "apps" in the namespace "kube-system"
Error from server (Forbidden): replicasets.apps is forbidden: User "https://keycloak.example.com:32084/realms/kubernetes#user dev" cannot list resource "replicasets" in API group "apps" in the namespace "kube-system"
Error from server (Forbidden): statefulsets.apps is forbidden: User "https://keycloak.example.com:32084/realms/kubernetes#user dev" cannot list resource "statefulsets" in API group "apps" in the namespace "kube-system"
Error from server (Forbidden): horizontalpodautoscalers.autoscaling is forbidden: User "https://keycloak.example.com:32084/realms/kubernetes#user dev" cannot list resource "horizontalpodautoscalers" in API group "autoscaling" in the namespace "kube-system"
Error from server (Forbidden): cronjobs.batch is forbidden: User "https://keycloak.example.com:32084/realms/kubernetes#user dev" cannot list resource "cronjobs" in API group "batch" in the namespace "kube-system"
Error from server (Forbidden): jobs.batch is forbidden: User "https://keycloak.example.com:32084/realms/kubernetes#user dev" cannot list resource "jobs" in API group "batch" in the namespace "kube-system"

つづき

KeycloakのOIDC認証を利用してKubernetesでテナント制御を行う

参考

Keycloak/Getting started/Kubernetes
https://www.keycloak.org/getting-started/getting-started-kube

Kubernetes/Authenticating
https://kubernetes.io/docs/reference/access-authn-authz/authentication/#openid-connect-tokens

How to authenticate user with Keycloak OIDC Provider in Kubernetes
https://middlewaretechnologies.in/2022/01/how-to-authenticate-user-with-keycloak-oidc-provider-in-kubernetes.html


宣伝

Twitterやってます。
よかったらフォローいただけると嬉しいです。

@mochizuki875
プロになりたいITエンジニア Kubernetesが好き
a fox, not a raccoon dog.
https://twitter.com/mochizuki875

27
18
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
27
18