9
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

KubernetesのネットワークオブザーバビリティプラットフォームRetinaを試してみた<前編:構築編>

Posted at

はじめに

三菱電機のノザワです。初めての投稿です。

本記事では、KubernetesクラスターのネットワークオブザーバビリティプラットフォームであるRetinaについて調査したこと、手元のオンプレKubernetesクラスター上でお試し環境を作ってみたことをまとめます。

機能の細かいところまで触ってみた内容は別途執筆予定の後編にまとめる予定です。

本記事で触れる内容

  • Retinaの概要
  • Retinaの環境構築
  • Retinaの動作確認

本記事で触れない内容

  • Kubernetesの基礎知識
  • kubectlhelmコマンドの使い方
  • PrometheusやGrafanaの使い方

登場したばかりのOSSなのでまだまだ情報が少なく、おそらくこの投稿が試してみた記事としては現時点で初めての日本語記事だと思います。
皆さんが使用される際の参考になれば嬉しいです。

Retinaとは

今回取り上げるRetinaは、Microsoft社が2024年3月19日にOSSとして公開したKubernetesクラスターのネットワークオブザーバビリティツールです。
某ディスプレイと同じ名前なので検索しづらい……。

執筆時の2024年7月25日時点でのバージョンは、v0.0.12です。
Goで実装されていて、MITライセンスで公開されています。
まだまだリリースされたばかりですが、公式ドキュメントはしっかりとしていて、導入手順の説明も充実しています。

機能詳細

公式サイトで以下のように謳われている通り、特定のクラウドへの依存がなく、オンプレのクラスターでも利用可能なツールです。

Retina is a cloud-agnostic, open-source Kubernetes Network Observability platform which helps with DevOps, SecOps and compliance use cases.
引用元:Hello from Retina | Retina

OSについてはLinuxだけでなくWindowsにも対応していますが、今回はLinuxだけ取り扱います。

連携先

収集された情報は、PrometheusやAzure Monitorをはじめとしたさまざまな監視スタックへのエクスポートやGrafanaやAzure Log Analyticsをはじめとした可視化ツールに対応しています。

キャプチャ結果は、Amazon S3へのアップロードにも対応していますが、Microsoft社が公開しているだけあって全体的にAzureへの言及が強めです。

特長

特長として以下の機能が挙げられています。

  • トラフィックのインサイト
  • eBPFベース
  • メトリックとフローログ
  • 分散型のパケットキャプチャ
  • 任意のCNIに対応
  • 任意のKubernetesプラットフォームに対応

オンデマンドなネットワークの調査と継続的なクラスター監視を強みとしているようです。

Retina lets you investigate network issues on-demand and continuously monitor your clusters.
引用元:Why Retina?

収集できる情報

Retinaを使って収集できる監視情報の例は以下の通りです。

  • 送信/受信トラフィック
  • ドロップされたパケット
  • TCP/UDPコネクションの統計情報
  • DNSリクエスト/レスポンス
  • APIサーバーのレイテンシー
  • ノード、インターフェースの統計情報

この他に、ノードやPodを指定したパケットのキャプチャ機能も提供されています。

アーキテクチャ

ここまで紹介してきた機能は、Kubernetesクラスターの各ノードにRetinaのエージェントが稼働することで実現されます。
エージェントは、ネットワークに関する各種統計情報を取集し、メトリクスのエンドポイントとして公開します。

公式が作成しているアーキテクチャ図を見るとイメージが付きやすいので、掲載しておきます。

Retinaのアーキテクチャ
画像出典元:https://github.com/microsoft/retina/blob/main/docs/retina-components.png

eBPF

深入りはしませんが、Retinaで使われているeBPF (extended Berkeley Packet Filter)についても軽く触れておきます。

eBPF (extended Berkeley Packet Filter)は、Linuxカーネルのソースコードを修正したりカーネルモジュールを導入したりすることなく、サンドボックス化された小さなプログラムやスクリプトをLinuxシステムのカーネル空間で実行できる技術です。
引用元:eBPF | クラウドネイティブ用語集

CNCFの用語集の説明通り、eBPFを使うことでカーネルに改変を加えずにOSレベルで安全に機能を拡張できます。
Retinaのようなパケットキャプチャのほか、リソース制御などの処理に活用されている技術です。

詳細は公式サイトや詳しく扱っている記事・書籍などもご確認ください。

構築作業

さて、ここからは本題の環境構築をしていきます。
Kubernetesクラスター上のLinuxノードにRetinaをデプロイし、収集したデータをダッシュボード上で確認するところまでを目指します。

環境要件

Retinaを導入するにあたっての環境要件(Linuxの場合)は以下の2点です。

  • Linuxカーネル:v5.4.0 以上
  • Helm:v3.8.0 以上

後で出てきますが、古めの環境を使っているとカーネルのバージョンがネックになります。

本記事での環境

ここでは、手元のVMで組んでいる検証用のオンプレKubernetesクラスターを使用します。

当初使おうとした環境

手元のKubernetesクラスターは、以下の構成のノード3台で組んでいました。

  • OS:CentOS Linux 7
  • Linuxカーネル:v3.10.0 → v6.9.3
  • Kubernetes:v1.29.5
  • CNI:Calico v3.25.0

Linuxカーネルのバージョンがv3.10.0だったので、v6.9.3にバージョンアップしてから構築を試してみましたが、結果として動作しなかったので、諦めました。
この辺りは、「つまずいたところ」で後述します。

そもそもCentOS 7は2024年6月末でEOLなので、使い続けるのは好ましくありません。

最終的に使った環境

どうあがいても元の環境は使えないようだったので、Ubuntuのノードを3台クラスターに追加しました。
以下の環境です。

  • OS:Ubuntu 22.04.4 LTS (Jammy Jellyfish)
  • Linuxカーネル:v6.5.0
  • Kubernetes:v1.29.5
  • CNI:Calico v3.25.0

手順

Helmに対応しているため、Retina自体の導入はコマンド一発です。
リポジトリがOCIベースなため、古いHelmを使っている場合は、要件の通りバージョンアップが必要な場合があります。

デプロイコマンド

コマンド等は公式ドキュメントに詳しく書かれているので、それに従います。

まずはシンプルなBasic Mode (with Capture support)でデプロイしてみます。
使用したい機能(プラグイン)をデプロイ時に指定することで有効化できます。
今回はドキュメントに記載のコマンドにNodeSelectorを追加しました。

VERSION=$( curl -sL https://api.github.com/repos/microsoft/retina/releases/latest | jq -r .name)
helm upgrade --install retina oci://ghcr.io/microsoft/retina/charts/retina \
    --version $VERSION \
    --namespace kube-system \
    --set image.tag=$VERSION \
    --set operator.tag=$VERSION \
    --set logLevel=info \
    --set image.pullPolicy=Always \
    --set operator.enabled=true \
    --set operator.enableRetinaEndpoint=true \
    --skip-crds \
    --set enabledPlugin_linux="\[dropreason\,packetforward\,linuxutil\,dns\,packetparser\]" \
    --set nodeSelector."kubernetes\.io/os-dist"=ubuntu

オプション指定

細かいオプションの指定はvalues.yamlファイルでも可能です。
設定できる項目、デフォルト値はこちら。

values.yaml(デフォルト値)
values.yaml
# Default values for retina.
# This is a YAML-formatted file.
# Declare variables to be passed into your templates.

# Support linux and windows by default.
os:
  linux: true
  windows: true

operator:
  enabled: false
  repository: ghcr.io/microsoft/retina/retina-operator
  tag: "v0.0.2"
  installCRDs: true
  enableRetinaEndpoint: false
  capture:
    debug: "true"
    jobNumLimit: 0
  resources:
    limits:
      cpu: 500m
      memory: 128Mi
    requests:
      cpu: 10m
      memory: 128Mi
  container:
    command:
      - "/retina-operator"
    args:
      - "--config"
      - "/retina/operator-config.yaml"

image:
  repository: ghcr.io/microsoft/retina/retina-agent
  initRepository: ghcr.io/microsoft/retina/retina-init
  pullPolicy: Always
  # Overrides the image tag whose default is the chart appVersion.
  tag: "v0.0.2"

enablePodLevel: false
remoteContext: false
enableAnnotations: false
bypassLookupIPOfInterest: false

imagePullSecrets: []
nameOverride: "retina"
fullnameOverride: "retina-svc"

namespace: kube-system

agent:
  name: retina-agent

agent_win:
  name: retina-agent-win

retinaPort: 10093

apiServer:
  host: "0.0.0.0"
  port: 10093

# Supported - debug, info, error, warn, panic, fatal.
logLevel: debug

enabledPlugin_linux: '["dropreason","packetforward","linuxutil","dns"]'
enabledPlugin_win: '["hnsstats"]'

enableTelemetry: false

# Interval, in seconds, to scrape/publish metrics.
metricsInterval: 10

azure:
  appinsights:
    instrumentation_key: "app-insights-instrumentation-key"

daemonset:
  container:
    retina:
      command:
        - "/retina/controller"
      args:
        - "--config"
        - "/retina/config/config.yaml"
      healthProbeBindAddress: ":18081"
      metricsBindAddress: ":18080"
      ports:
        containerPort: 10093

# volume mounts with name and mountPath
volumeMounts:
  debug: /sys/kernel/debug
  trace: /sys/kernel/tracing
  bpf: /sys/fs/bpf
  cgroup: /sys/fs/cgroup
  tmp: /tmp
  config: /retina/config

#volume mounts for windows
volumeMounts_win:
  retina-config-win: retina

securityContext:
  privileged: false
  capabilities:
    add:
      - SYS_ADMIN
      - SYS_RESOURCE
      - NET_ADMIN # for packetparser plugin
      - IPC_LOCK # for mmap() calls made by NewReader(), ref: https://man7.org/linux/man-pages/man2/mmap.2.html
  windowsOptions:
    runAsUserName: "NT AUTHORITY\\SYSTEM"

service:
  type: ClusterIP
  port: 10093
  targetPort: 10093
  name: retina

serviceAccount:
  annotations: {}
  name: "retina-agent"

resources:
  limits:
    memory: "300Mi"
    cpu: "500m"
  requests:
    memory: "300Mi"
    cpu: "500m"

## @param nodeSelector [object] Node labels for pod assignment
## Ref: https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/
##
nodeSelector: {}
## @param tolerations [array] Tolerations for pod assignment
## Ref: https://kubernetes.io/docs/concepts/configuration/taint-and-toleration/
##
tolerations: []

metrics:
  ## Prometheus Service Monitor
  ## ref: https://github.com/coreos/prometheus-operator/blob/master/Documentation/api.md#endpoint
  ##
  podMonitor:
    ## @param metrics.podMonitor.enabled Create PodMonitor Resource for scraping metrics using PrometheusOperator
    ##
    enabled: false
    ## @param metrics.podMonitor.namespace Namespace in which the PodMonitor should be created
    ##
    namespace: ~
    ## @param metrics.podMonitor.interval Specify the interval at which metrics should be scraped
    ##
    interval: 30s
    ## @param metrics.podMonitor.scrapeTimeout Specify the timeout after which the scrape is ended
    ##
    scrapeTimeout: 30s
    ## @param metrics.podMonitor.additionalLabels [object] Additional labels that can be used so PodMonitors will be discovered by Prometheus
    ## ref: https://github.com/coreos/prometheus-operator/blob/master/Documentation/api.md#prometheusspec
    ##
    additionalLabels: {}
    ## @param metrics.podMonitor.scheme Scheme to use for scraping
    ##
    scheme: http
    ## @param metrics.podMonitor.tlsConfig [object] TLS configuration used for scrape endpoints used by Prometheus
    ## ref: https://github.com/prometheus-operator/prometheus-operator/blob/master/Documentation/api.md#tlsconfig
    ## e.g:
    ## tlsConfig:
    ##   ca:
    ##     secret:
    ##       name: existingSecretName
    ##
    tlsConfig: {}
    ## @param metrics.podMonitor.relabelings [array] Prometheus relabeling rules
    ##
    relabelings: []

デプロイ実行

では、実際にデプロイを実行してみます。

# helm upgrade --install retina oci://ghcr.io/microsoft/retina/charts/retina \
>     --version $VERSION \
>     --namespace kube-system \
>     --set image.tag=$VERSION \
>     --set operator.tag=$VERSION \
>     --set logLevel=info \
>     --set image.pullPolicy=Always \
>     --set operator.enabled=true \
>     --set operator.enableRetinaEndpoint=true \
>     --skip-crds \
>     --set enabledPlugin_linux="\[dropreason\,packetforward\,linuxutil\,dns\,packetparser\]" \
>     --set nodeSelector."kubernetes\.io/os-dist"=ubuntu
Release "retina" does not exist. Installing it now.
Pulled: ghcr.io/microsoft/retina/charts/retina:v0.0.12
Digest: sha256:be0969e99fe78a510e81fd0c8e6feb8c87e9806ed978e3e941a75a95b0515df5
NAME: retina
LAST DEPLOYED: Thu Jul 25 12:27:54 2024
NAMESPACE: kube-system
STATUS: deployed
REVISION: 1
NOTES:
1. Installing retina service using helm: helm install retina ./deploy/manifests/controller/helm/retina/ --namespace kube-system --dependency-update
2. Cleaning up/uninstalling/deleting retina and dependencies related:
  helm uninstall retina -n kube-system

DaemonSetのretina-agentとDeploymentのretina-operatorがデプロイされました。

# kubectl get po -n kube-system | grep retina
retina-agent-ld9d5                                       0/1     Init:0/1            0               9s
retina-agent-wdlqm                                       0/1     Init:0/1            0               9s
retina-agent-xz9kh                                       0/1     Init:0/1            0               9s
retina-operator-cf6648b64-s2f4g                          0/1     ContainerCreating   0               9s

私の環境では、90秒ほどで全てのPodがRunning状態になりました。

# kubectl get po -n kube-system | grep retina
retina-agent-ld9d5                                       1/1     Running   0               95s
retina-agent-wdlqm                                       1/1     Running   0               95s
retina-agent-xz9kh                                       1/1     Running   0               95s
retina-operator-cf6648b64-s2f4g                          1/1     Running   0               95s

つまずいたところ

(古めの環境を使ったために)構築の過程でつまずいたところを残しておきます。

Linuxカーネルバージョン

前述の通り、CentOSのLinuxカーネルバージョンが要件を満たしていなかったので、まずは、こちらのサイトを参考にELRepoからv6.9.3のカーネルをインストールしました。

しかし、カーネルバージョンを上げてもRetinaの実行に必要なeBPFの一部機能(BTF)に非対応なようで、結局エージェントの起動は失敗に終わりました。

ts=2024-07-24T08:24:47.317Z level=error caller=dropreason/dropreason_linux.go:149 msg="Error loading objects: %w" error="field InetCskAccept: program inet_csk_accept: apply CO-RE relocations: load kernel spec: no BTF found for kernel version 6.9.3-1.el7.elrepo.x86_64: not supported"
ts=2024-07-24T08:24:47.317Z level=info caller=server/server.go:79 msg="gracefully shutting down HTTP server..."
ts=2024-07-24T08:24:47.318Z level=info caller=server/server.go:71 msg="HTTP server stopped with err: http: Server closed"
ts=2024-07-24T08:24:47.318Z level=panic caller=controllermanager/controllermanager.go:119 msg="Error running controller manager" error="failed to reconcile plugin dropreason: field InetCskAccept: program inet_csk_accept: apply CO-RE relocations: load kernel spec: no BTF found for kernel version 6.9.3-1.el7.elrepo.x86_64: not supported" errorVerbose="field InetCskAccept: program inet_csk_accept: apply CO-RE relocations: load kernel spec: no BTF found for kernel version 6.9.3-1.el7.elrepo.x86_64: not supported\nfailed to reconcile plugin dropreason\ngithub.com/microsoft/retina/pkg/managers/pluginmanager.(*PluginManager).Start\n\t/go/src/github.com/microsoft/retina/pkg/managers/pluginmanager/pluginmanager.go:169\ngithub.com/microsoft/retina/pkg/managers/controllermanager.(*Controller).Start.func1\n\t/go/src/github.com/microsoft/retina/pkg/managers/controllermanager/controllermanager.go:109\ngolang.org/x/sync/errgroup.(*Group).Go.func1\n\t/go/pkg/mod/golang.org/x/sync@v0.7.0/errgroup/errgroup.go:78\nruntime.goexit\n\t/usr/local/go/src/runtime/asm_amd64.s:1695"

CentOSでRetinaが事実上使えない件は、Issueとしても挙げられていますが、すでにクローズされています。
要はEOLもあるので諦めろということのようです。

BTF対応

先ほどのエラーログでは、BTF (BPF Type Format)が見つからないよと怒られていました。

BTFはBPFに必要な要素の一部で、これに対応していないとカーネルに関数・機能を挿入するために必要な情報が取得できません。

BTF (BPF Type Format) is the metadata format which encodes the debug info related to BPF program/map. The name BTF was used initially to describe data types. The BTF was later extended to include function info for defined subroutines, and line info for source/line information.
引用元:BPF Type Format (BTF) — The Linux Kernel documentation

CentOSでBPF、BTFに対応するワークアラウンドは存在するものの、一部機能が不足しているため、やはり動作させるのは厳しいようです。

とりあえず一通り使えるようにするまで

Retina自体のデプロイは完了したので、次はデータを収集、可視化する環境を構築していきます。
また、キャプチャ機能も試してみます。

PrometheusとGrafanaの用意

公式ドキュメントでは、Prometheus/Grafanaの構築手順が案内されているので、こちらを参考に構築します。

構築手順に従い、Helmを使ってPrometheusとGrafanaをデプロイします。
元から環境がある場合は、スクレイピングの設定を追加するだけでよいです。
今回は環境を一から作ります。

デプロイの実行

Helm実行前に、リポジトリからvalues.yamlを取得しておきます。
今回は、prometheus_values.yamlという名前にしました。
GrafanaはNodePortでアクセスしたかったので、prometheus_values.yamlの最後に以下の記述を追加しました。

prometheus_values.yaml
grafana:
  service:
    type: NodePort
    nodoPort: 30480

次のコマンドを実行します。

helm install prometheus -n kube-system -f prometheus_values.yaml prometheus-community/kube-prometheus-stack

しばらくすると正常にPodが立ち上がります。(詳細は省略)

スクレイピング状況の確認

Prometheusのダッシュボードを見ると、retina-agentの3つのPodが正常に発見され、スクレイピングされていることを確認できました。
4つ目のprometheus-operatorのPodのエラーは、スクレイピング設定がうまくできていないことによるものですが、ノードのネットワーク情報取得には直接関係ないPodのため、無視します。

Prometheusの画面

Grafanaダッシュボードの確認

Prometheusの環境を構築すると、同時にGrafanaもデプロイされます。

パスワードの確認

Grafanaにログインするパスワードは、デプロイ時に自動生成されているので次のコマンドで確認します。

kubectl get secret -n kube-system prometheus-grafana -o jsonpath="{.data.admin-password}" | base64 --decode ; echo

ダッシュボードのインポート

公式でダッシュボードのテンプレートが用意されているので、インポートして利用します。
既にできあがっているものが提供されているのはありがたいですね。

今回構築したGrafanaはインターネットに直接アクセスできない環境下にあるので、JSONファイルをダウンロードしてからインポートしました。

インポート後、実際のメトリクスが表示された以下のようなダッシュボードの画面を確認できました。
まだ取得できていないデータもありますが、すぐに使えそうです。

Grafanaのダッシュボード

キャプチャのお試し

今度は、Retinaのキャプチャ機能を試してみたいと思います。

キャプチャを実行するには、以下の2通りの方法があります。

  • CLIコマンド
  • Kubernetesのカスタムリソース定義

ここでは、1つ目のCLIコマンドを使用してキャプチャを実行してみたいと思います。

CLIのセットアップ

RetinaのCLIは、kubectlのプラグインマネージャーであるkrewを使って簡単にセットアップすることができます。
krewが導入されていない場合は、まずは導入が必要ですがここでの説明は割愛します。

# kubectl krew install retina
Updated the local copy of plugin index.
Installing plugin: retina
Installed plugin: retina
\
 | Use this plugin:
 |      kubectl retina
 | Documentation:
 |      https://github.com/microsoft/retina
/
WARNING: You installed plugin "retina" from the krew-index plugin repository.
   These plugins are not audited for security by the Krew maintainers.
   Run them at your own risk.

その他に、バイナリのダウンロードやソースコードから自前でビルドする方法もあります。

CLIによるキャプチャの開始

それではいよいよキャプチャの実行です。
ひとまず、公式ドキュメントにある一番単純なキャプチャコマンドをベースにUbuntuが稼働しているノードでキャプチャを実行してみます。
デフォルトでは、「キャプチャ時間が1分経過する」または「キャプチャサイズが100MBを超える」のいずれかを満たしたら、キャプチャが終了します。

# kubectl retina capture create --name capture-test --host-path /mnt/capture-test --node-selectors "kubernetes.io/os-dist=ubuntu" --no-wait=true
ts=2024-07-25T17:44:21.133+0900 level=info caller=capture/create.go:243 msg="The capture duration is set to 1m0s"
ts=2024-07-25T17:44:21.133+0900 level=info caller=capture/create.go:289 msg="The capture file max size is set to 100MB"
ts=2024-07-25T17:44:21.133+0900 level=info caller=utils/capture_image.go:56 msg="Using capture workload image ghcr.io/microsoft/retina/retina-agent:v0.0.12 with version determined by CLI version"
ts=2024-07-25T17:44:21.135+0900 level=info caller=capture/crd_to_job.go:201 msg="HostPath is not empty" HostPath=/mnt/capture-test
ts=2024-07-25T17:44:21.228+0900 level=info caller=capture/crd_to_job.go:876 msg="The Parsed tcpdump filter is \"\""
ts=2024-07-25T17:44:21.248+0900 level=info caller=capture/create.go:369 msg="Packet capture job is created" namespace=default capture job=capture-test-jnwwd
ts=2024-07-25T17:44:21.262+0900 level=info caller=capture/create.go:369 msg="Packet capture job is created" namespace=default capture job=capture-test-857gg
ts=2024-07-25T17:44:21.289+0900 level=info caller=capture/create.go:369 msg="Packet capture job is created" namespace=default capture job=capture-test-zbl5c
ts=2024-07-25T17:44:21.289+0900 level=info caller=capture/create.go:125 msg="Please manually delete all capture jobs"
NAMESPACE   CAPTURE NAME   JOBS                                                       COMPLETIONS   AGE
default     capture-test   capture-test-857gg,capture-test-jnwwd,capture-test-zbl5c   0/3           0s

実行中のキャプチャの確認

キャプチャはKubernetesのジョブとしてリソースが作成され実行されます。

# kubectl get job | grep capture-test
capture-test-857gg                0/1           17s        17s
capture-test-jnwwd                0/1           17s        17s
capture-test-zbl5c                0/1           17s        17s

Podの状況を確認してみます。

# kubectl get po | grep capture-test
capture-test-857gg-zw89r                          1/1     Running            0                23s
capture-test-jnwwd-wdtxh                          1/1     Running            0                23s
capture-test-zbl5c-78fxw                          1/1     Running            0                23s

キャプチャの終了と結果の確認

3分半ほどですべての対象ノードでキャプチャが終了しました。
キャプチャ以外の処理もあるので、実行時間は1分を超えます。

# kubectl get job | grep capture-test
capture-test-857gg                1/1           87s        4m46s
capture-test-jnwwd                1/1           3m26s      4m46s
capture-test-zbl5c                1/1           2m49s      4m46s

保存先としてHostPathを指定しているので、実際にノードの保存先をのぞいてみると、確かに結果がtarで固められて保存されていました。
ファイル名にはキャプチャ名、ホスト名、タイプスタンプが含まれています。

$ ls /mnt/capture-test/ -hla
合計 61M
drwxr-xr-x 2 root root 4.0K  7月 25 17:47 .
drwxr-xr-x 6 root root 4.0K  7月 25 17:43 ..
-rw-r--r-- 1 root root  61M  7月 25 17:47 capture-test-kredit-20240725084400UTC.tar.gz

キャプチャが無事完了しているのを見届けたところで、今回はここまでにしたいと思います。

まとめ

今回はKubernetesクラスターのオブザーバビリティプラットフォームであるRetinaのお試し環境を構築しました。
お試し環境では、ダッシュボードで取得したメトリクスを確認することとキャプチャを実行することまで実現できました。
リリースされたばかりですが、ドキュメントやデプロイ用のHelmチャートも整備されており、使い始めやすい印象を受けました。

次回はメトリクスやキャプチャなど機能のより詳しいところに踏み込んだいきたいと思います。

ここまでお読みいただきありがとうございました。
ご質問やコメントもお待ちしております。

9
2
0

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
9
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?