LoginSignup
3
2

More than 3 years have passed since last update.

kubeadmでFedora CoreOSにKubernetes1.17

Last updated at Posted at 2020-01-17

kubeadmでFedora CoreOSにKubernetes1.18を入れる場合はこちら

メモ

・Fedora CoreOSは、CoreOS Container Linuxと同様にデフォルトで自動アップデートが有効なので、新しい安定版が出ると自動でアップデートします。

・Calicoのデプロイや設定を変えた時(kubectl apply -f calico.yaml)にすべてのノードの再起動(sudo systemctl reboot)しないとRedisにserviceでアクセスできなかった。

CPUの数

・Fedora CoreOS testing 31.20200310.2.0でも同じ結果
 (2020/03/19追記)
CoreOS incorrectly reports CPU info via /proc/cpuinfo #413
Google翻訳

(AWS上の)Fedora CoreOSの最新の安定リリースを実行していて、/ proc / cpuinfoの内容が正しくない。

[core@ip-10-36-176-20 ~]$ rpm-ostree status ・・・
2つのCPUを備えたt3.smallは1を示します。4を備えたt3.xlargeは2を示します。これは、Kubernetes(正確にはcadvisor)が/ proc / cpuinfoを使用して容量用のCPUの数を決定するため、問題を引き起こします。 これにより、ノードの容量が必要な半分になると報告されます。

Fedora CoreOS: [core@ip-10-36-176-20 ~]$ cat /proc/cpuinfo ・・・

@jlebon
FCOSが必要に応じてSMTを無効にしている可能性があります : https : //github.com/coreos/fedora-coreos-tracker/blob/master/Design.md#automatically-disable-smt-when-needed-to-address-vulnerabilities 。 EC2インスタンスタイプで引用されているvCPU数はハイパースレッディングを想定していると思います。

@ cyrus-mc
それはそれのようです。 迅速な対応に感謝します。

を使用してカーネルオプションを更新しました

rpm-ostree kargs --replace="mitigations=auto"

nosmtを削除します

Fedora CoreOSで/proc/cpuinfoの内容をEC2インスタンスのvCPU数と同じにする設定する場合はこちらを参照してください

環境

Master Node

EC2   OS Kubernetes Docker
t3.medium Fedora CoreOS stable 31.20200310.3.0 1.17.4 18.09.8

Fedora CoreOS preview AMI のユーザー名は core です

Master Nodeは、2CPU、メモリ2GB以下の場合、kubeadm init実行時にエラーになります。
ディスクサイズはデフォルトの8G
Master NodeとWorker Nodeは同じVPC

Worker Node

EC2   OS Kubernetes Docker
t3.small Fedora CoreOS stable 31.20200310.3.0 1.18.0 18.09.8

発生したエラー

・CPU 2つのt3.smallでは、kubeadm init実行時に
 以下のエラーが出る。
 ・CPU 4つのt3.xlargeは問題なし。
 <参考情報(2020/1/22追記)>
 t3.smallのままで、
「sudo kubeadm init --config kubeadm-config.yaml」のうしろに「--ignore-preflight-errors=NumCPU」を付けてkubeadm init~flannelのデプロイまで確認
[How to get kubeadm init to not fail? #733]
その時点でメモリの空きが78MBでした。
少し上のインスタンスにすることを推奨します。
How to get kubeadm init to not fail? #733

error execution phase preflight: [preflight] Some fatal errors occurred:
    [ERROR NumCPU]: the number of available CPUs 1 is less than the required 2

・kubeadm init実行で「[WARNING FileExisting-ethtool]: ethtool not found in system path」が出た。
→「sudo rpm-ostree install ethtool -r」でインストール

参考情報

/etc/docker/daemon.jsonを設定しても再起動後に/etc/dockerに
daemon.jsonがなくなっていた。
違う設定方法で設定する必要がありそう(2020/03/01追記)

Podman確認

podman -v

podman version 1.8.1

.bashrc編集など

vi .bashrc

export PATH=$PATH:/opt/bin

source ~/.bashrc

sudo visudo

# :/opt/bin を追加
Defaults    secure_path = /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/opt/bin

SELinux設定確認

# 現在の状態確認
#・Enforcing ・・・ SELinuxは有効で、アクセス制限も有効。
#・Permissive ・・・ SELinuxは有効だが、アクセス制限は行わず警告を出力。
#・Disabled ・・・ SELinux機能は無効。
getenforce

# 実行結果
Enforcing

# Disabled への変更
sudo sed -i --follow-symlinks 's/SELINUX=enforcing/SELINUX=disabled/g' /etc/sysconfig/selinux

# 再起動
sudo systemctl reboot

# 再起動後に再度sshで接続
getenforce

# 実行結果
Disabled 

swap確認

# ある場合はswapoff -a
free

Docker設定

docker -v

# 実行結果
Docker version 18.09.8, build 0dd43dd

# Dockerの自動起動有効
sudo systemctl enable docker

# ログインしているユーザーがdockerグループの追加されているか確認
# デフォルトでは設定されていない
cat /etc/group | grep docker

# 実行結果
docker:x:982:

# ログインしているユーザーをdockerグループに追加
sudo usermod -a -G docker $USER

# 確認コマンド
$ cat /etc/group | grep docker

# 実行結果
docker:x:993:core

# ログアウト
# すぐにsshで再接続
exit

Docker の設定例

cgroupdriverを「systemd」
log-driverを「json-file」。max-size「100m」
insecure-registryに「hogehoge:5000」

Control Docker with systemd
systemd と Docker の管理・設定
docker18.09.0にあげたらdocker.serviceが起動しなくなった

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

# Fedora CoreOSのDockerは18.09なので dockerdのうしろに「-H fd://」は付けないこと
sudo tee /etc/systemd/system/docker.service.d/docker.conf <<EOF
[Service]
ExecStart=
ExecStart=/usr/bin/dockerd --exec-opt native.cgroupdriver=systemd --log-driver=json-file \
          --log-opt max-size=100m \
          --insecure-registry=hogehoge:5000
EOF

sudo systemctl daemon-reload
sudo systemctl restart docker

sysctlでネットワークをブリッジできるようする

# sysctlでネットワークをブリッジできるようする
sudo tee /etc/sysctl.d/k8s.conf <<EOF
net.bridge.bridge-nf-call-iptables  = 1
net.ipv4.ip_forward                 = 1
net.bridge.bridge-nf-call-ip6tables = 1
EOF

# カーネルパラメータを手動で反映
sudo sysctl --system

kubelet、kubectl、kubeadmのインストール

再度sshで接続

CNI_VERSION="v0.8.5"
sudo mkdir -p /opt/cni/bin
curl -L "https://github.com/containernetworking/plugins/releases/download/${CNI_VERSION}/cni-plugins-linux-amd64-${CNI_VERSION}.tgz" | sudo tar -C /opt/cni/bin -xz

CRICTL_VERSION="v1.17.0"
sudo mkdir -p /opt/bin
curl -L "https://github.com/kubernetes-incubator/cri-tools/releases/download/${CRICTL_VERSION}/crictl-${CRICTL_VERSION}-linux-amd64.tar.gz" | sudo tar -C /opt/bin -xz

# 1.17.0の場合はv1.17.0を指定してください
RELEASE="v1.17.4"
sudo mkdir -p /opt/bin
cd /opt/bin
sudo curl -L --remote-name-all https://storage.googleapis.com/kubernetes-release/release/${RELEASE}/bin/linux/amd64/{kubeadm,kubelet,kubectl}
sudo chmod +x {kubeadm,kubelet,kubectl}

cd
curl -sSL "https://raw.githubusercontent.com/kubernetes/kubernetes/${RELEASE}/build/debs/kubelet.service" | sed "s:/usr/bin:/opt/bin:g" > ./kubelet.service
sudo mv ./kubelet.service /etc/systemd/system/

sudo mkdir -p /etc/systemd/system/kubelet.service.d
curl -sSL "https://raw.githubusercontent.com/kubernetes/kubernetes/${RELEASE}/build/debs/10-kubeadm.conf" | sed "s:/usr/bin:/opt/bin:g" > ./10-kubeadm.conf
sudo mv 10-kubeadm.conf /etc/systemd/system/kubelet.service.d

kubelet 起動

sudo systemctl enable --now kubelet

Masterノードのセットアップ

Fedore CoreOSの場合、kubeadm init時にconfigファイルを用意する必要があります。詳細は以下のissuesを見てください。

failed to join the cluster with /usr/libexec/kubernetes: read-only file system #88210
add workarounds for Fedora Coreos's R/O /usr/libexec/ #2031

# kubeadm init実行で
#「[WARNING FileExisting-ethtool]: 
# ethtool not found in system path」
# が出るためインストール
sudo rpm-ostree install ethtool -r

# 再起動後にsshで再接続

# Calicoの場合、「podSubnet」に「192.168.0.0/16」を指定
# Flannelの場合、「podSubnet」に「10.244.0.0/16」を指定
# 例) Calico用のファイル作成
cat <<EOF > ./kubeadm-config.yaml
apiVersion: kubeadm.k8s.io/v1beta2
kind: InitConfiguration
nodeRegistration:
  kubeletExtraArgs:
    volume-plugin-dir: "/opt/libexec/kubernetes/kubelet-plugins/volume/exec/"
---
apiVersion: kubeadm.k8s.io/v1beta2
kind: ClusterConfiguration
controllerManager:
  extraArgs:
    flex-volume-plugin-dir: "/opt/libexec/kubernetes/kubelet-plugins/volume/exec/"
networking:
  podSubnet: 192.168.0.0/16
EOF

# CPU2つ(AWS EC2の場合、t3.smallやt3.medium)のままで、
#「sudo kubeadm init --config kubeadm-config.yaml」を
# 実行する場合は、うしろに「--ignore-preflight-errors=NumCPU」を付けて
# 実行してください。
sudo kubeadm init --config kubeadm-config.yaml --ignore-preflight-errors=NumCPU

kubectl 設定

mkdir -p $HOME/.kube
sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
sudo chown $(id -u):$(id -g) $HOME/.kube/config

確認

kubectl get all --all-namespaces

# 実行結果
NAMESPACE     NAME                                          READY   STATUS    RESTARTS   AGE
kube-system   pod/coredns-6955765f44-k5bgd                  1/1     Running   0          107s
kube-system   pod/coredns-6955765f44-qc64d                  1/1     Running   0          107s
kube-system   pod/etcd-ip-172-31-23-58                      1/1     Running   0          2m3s
kube-system   pod/kube-apiserver-ip-172-31-23-58            1/1     Running   0          2m3s
kube-system   pod/kube-controller-manager-ip-172-31-23-58   1/1     Running   0          2m2s
kube-system   pod/kube-proxy-csmvq                          1/1     Running   0          107s
kube-system   pod/kube-scheduler-ip-172-31-23-58            1/1     Running   0          2m3s

NAMESPACE     NAME                 TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)                  AGE
default       service/kubernetes   ClusterIP   10.96.0.1    <none>        443/TCP                  2m5s
kube-system   service/kube-dns     ClusterIP   10.96.0.10   <none>        53/UDP,53/TCP,9153/TCP   2m4s

NAMESPACE     NAME                        DESIRED   CURRENT   READY   UP-TO-DATE   AVAILABLE   NODE SELECTOR                 AGE
kube-system   daemonset.apps/kube-proxy   1         1         1       1            1           beta.kubernetes.io/os=linux   2m4s

NAMESPACE     NAME                      READY   UP-TO-DATE   AVAILABLE   AGE
kube-system   deployment.apps/coredns   2/2     2            2           2m4s

NAMESPACE     NAME                                 DESIRED   CURRENT   READY   AGE
kube-system   replicaset.apps/coredns-6955765f44   2         2         2       107s

Flannelデプロイ

Super slow access to service IP from host (& host-networked pods) with Flannel CNI #1245

kubectl apply -f https://raw.githubusercontent.com/coreos/flannel/master/Documentation/kube-flannel.yml

# 確認
kubectl get all --all-namespaces

# 実行結果
NAMESPACE     NAME                                          READY   STATUS    RESTARTS   AGE
kube-system   pod/coredns-6955765f44-k5bgd                  1/1     Running   0          2m46s
kube-system   pod/coredns-6955765f44-qc64d                  1/1     Running   0          2m46s
kube-system   pod/etcd-ip-172-31-23-58                      1/1     Running   0          3m2s
kube-system   pod/kube-apiserver-ip-172-31-23-58            1/1     Running   0          3m2s
kube-system   pod/kube-controller-manager-ip-172-31-23-58   1/1     Running   0          3m1s
kube-system   pod/kube-flannel-ds-amd64-tz8vh               1/1     Running   0          16s
kube-system   pod/kube-proxy-csmvq                          1/1     Running   0          2m46s
kube-system   pod/kube-scheduler-ip-172-31-23-58            1/1     Running   0          3m2s

NAMESPACE     NAME                 TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)                  AGE
default       service/kubernetes   ClusterIP   10.96.0.1    <none>        443/TCP                  3m4s
kube-system   service/kube-dns     ClusterIP   10.96.0.10   <none>        53/UDP,53/TCP,9153/TCP   3m3s

NAMESPACE     NAME                                     DESIRED   CURRENT   READY   UP-TO-DATE   AVAILABLE   NODE SELECTOR                 AGE
kube-system   daemonset.apps/kube-flannel-ds-amd64     1         1         1       1            1           <none>                        16s
kube-system   daemonset.apps/kube-flannel-ds-arm       0         0         0       0            0           <none>                        16s
kube-system   daemonset.apps/kube-flannel-ds-arm64     0         0         0       0            0           <none>                        16s
kube-system   daemonset.apps/kube-flannel-ds-ppc64le   0         0         0       0            0           <none>                        16s
kube-system   daemonset.apps/kube-flannel-ds-s390x     0         0         0       0            0           <none>                        16s
kube-system   daemonset.apps/kube-proxy                1         1         1       1            1           beta.kubernetes.io/os=linux   3m3s

NAMESPACE     NAME                      READY   UP-TO-DATE   AVAILABLE   AGE
kube-system   deployment.apps/coredns   2/2     2            2           3m3s

NAMESPACE     NAME                                 DESIRED   CURRENT   READY   AGE
kube-system   replicaset.apps/coredns-6955765f44   2         2         2       2m46s

FlannelではなくCalicoをデプロイする場合 (2020/03/01追記)

Install Calico -Kubernetes-
How To Configure Bird Ip In Calico Node Container
Project Calicoとはなにか
Project Calicoのアーキテクチャを見てみよう
Project CalicoをKubernetesで使ってみる:構築編
Project CalicoをKubernetesで使ってみる:ネットワークポリシー編
Calico について調べたメモ
DockerとKubernetesのPodのネットワーキングについてまとめました
Project CalicoによるKubernetesのPod間ネットワーキングを試してみました
CalicoによるKubernetesピュアL3ネットワーキング
k8s: calico の Pod が Pending ステータスから変わらない
Kubernetes1.11 + Calico3.1 BGPモード 構築 (kubeadm)
Calico readiness and liveliness probe fails #2042

curl -L -O https://docs.projectcalico.org/v3.13/manifests/calico.yaml

sed -i -e "s?/usr/libexec/kubernetes/kubelet-plugins/volume/exec/nodeagent~uds?/opt/libexec/kubernetes/kubelet-plugins/volume/exec/nodeagent~uds?g" calico.yaml

# name: calico-nodeのenvに以下の設定を追加
vi calico.yaml

- name: IP_AUTODETECTION_METHOD
  value: "interface=eth.*"

kubectl apply -f calico.yaml 

# 確認 
$ kubectl get po -A

# 実行結果
NAMESPACE     NAME                                       READY   STATUS    RESTARTS   AGE
kube-system   calico-kube-controllers-788d6b9876-4bl9h   1/1     Running   0          36s
kube-system   calico-node-cz5dw                          1/1     Running   0          36s
kube-system   coredns-6955765f44-6nlkx                   1/1     Running   0          116s
kube-system   coredns-6955765f44-7dblr                   1/1     Running   0          116s
kube-system   etcd-ip-172-31-28-127                      1/1     Running   0          2m9s
kube-system   kube-apiserver-ip-172-31-28-127            1/1     Running   0          2m9s
kube-system   kube-controller-manager-ip-172-31-28-127   1/1     Running   0          2m9s
kube-system   kube-proxy-wbqld                           1/1     Running   0          116s
kube-system   kube-scheduler-ip-172-31-28-127            1/1     Running   0          2m9s
kubectl get node

# 実行結果
NAME               STATUS   ROLES    AGE     VERSION
ip-172-31-28-127   Ready    master   3m30s   v1.17.4

WorkerNodeを構築する場合

以下はMaster Nodeの手順を参照
・.bashrc編集など
・SELinux設定確認
・swap確認
・Docker設定
・kubelet、kubeadm、kubectlインストール
・kubelet 起動

クラスタに参加

Fedore CoreOSの場合、Joinする時にconfigファイルを用意する必要があります。詳細は以下のissuesを見てください。
failed to join the cluster with /usr/libexec/kubernetes: read-only file system #88210
add workarounds for Fedora Coreos's R/O /usr/libexec/ #2031
kubeadm: add TS guide note about CoreOS read-only /usr #19166

「kubeadm join・・・」実行時の「WARNING: JoinControlPane.controlPlane settings will be ignored when control-plane flag is not set.」については、以下のissueを見てください。
kubeadm cannot join control plane node via --config flag

google翻訳

警告は偽です。 あなたの設定は引き続き尊重されます。 そのため、-control-planeを渡さず、-configを渡すだけで十分です。

ここで問題を追跡しています:
kubernetes / kubeadm#2065

修正は1.18.1で利用可能になりますが、1.17 *にバックポートすることはできません。

/閉じる
misleading warning on control-plane join #206

# kubeadm join実行で
#「[WARNING FileExisting-ethtool]: 
# ethtool not found in system path」
# が出るためインストール
sudo rpm-ostree install ethtool -r

# Master Nodeで実行
kubeadm token create --print-join-command

# configファイル作成
vi node-join-config.yaml

apiVersion: kubeadm.k8s.io/v1beta2
kind: JoinConfiguration
discovery:
  bootstrapToken:
    token: "kubeadm joinコマンドのトークンの値を指定"
    caCertHashes:
    - "kubeadm joinコマンドのdiscovery-token-ca-cert-hashを指定"
    apiServerEndpoint: "マスターノードのIPアドレスを指定:6443"
nodeRegistration:
  kubeletExtraArgs:
    volume-plugin-dir: "/opt/libexec/kubernetes/kubelet-plugins/volume/exec/"

# WorkerNodeで生成されたコマンドに先頭に「sudo 」をつけて実行
sudo kubeadm join マスターノードのIPアドレスを指定:6443 --config=node-join-config.yaml

MasterNodeとWorkerNodeの再起動

MasterNode

# 再起動
# 再起動後にsshで再接続
sudo systemctl reboot

WorkerNode

# 再起動
# 再起動後にsshで再接続
sudo systemctl reboot

Redisデプロイ・動作確認

Master Nodeで作業

vi redis.yaml
redis.yaml
apiVersion: v1
kind: Service
metadata:
  name: redis-svc
spec:
  ports:
    - port: 6379
      targetPort: 6379
  selector:
    app: redis
  clusterIP: None
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: redis.config
data:
  redis.conf: |
    requirepass password
    bind 0.0.0.0
---
apiVersion: apps/v1
kind: Deployment
metadata:
  # Deploymentの名前。Namespace内ではユニークである必要があります
  name: redis
spec:
  selector:
    matchLabels:
      app: redis
  # レプリカ数の指定
  replicas: 1
  # Podのテンプレート(PodTemplate)
  template:
    metadata:
      labels:
        # ラベル指定は必須
        app: redis
    spec:
      containers:
        - name: redis
          image: redis:5.0.8
          command: 
            - "redis-server"
            - "/redis-master/redis.conf"
          ports:
            - name: redis
              containerPort: 6379
          volumeMounts:
            - name: data
              mountPath: /redis-master-data
            - name: config
              mountPath: /redis-master
      volumes:
        - name: data
          emptyDir: {}
        - name: config
          configMap:
            name: redis.config
kubectl apply -f redis.yaml
kubectl run -it redis-cli --rm --image redis:5.0.8 --restart=Never -- bash
If you don't see a command prompt, try pressing enter.
root@redis-cli:/data# redis-cli -c -h redis-svc -p 6379 -a password
Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe.
redis-svc:6379> set a 1
OK
redis-svc:6379> get a
"1"
redis-svc:6379> exit
root@redis-cli:/data# exit

その他

「sudo rpm-ostree install kubernetes-kubeadm -r」でインストールした場合のバージョンは1.15.8。kubeadm init時にエラー発生した
以下のサイトの対応で解消するかもしれない。未確認

kubeadmをセットアップしてみたが、いつも通りハマる

エラー
[wait-control-plane] Waiting for the kubelet to boot up the control plane as static Pods from directory "/etc/kubernetes/manifests". This can take up to 4m0s
[kubelet-check] Initial timeout of 40s passed.
[kubelet-check] It seems like the kubelet isn't running or healthy.
[kubelet-check] The HTTP call equal to 'curl -sSL http://localhost:10248/healthz' failed with error: Get http://localhost:10248/healthz: dial tcp [::1]:10248: connect: connection refused.
[kubelet-check] It seems like the kubelet isn't running or healthy.
[kubelet-check] The HTTP call equal to 'curl -sSL http://localhost:10248/healthz' failed with error: Get http://localhost:10248/healthz: dial tcp [::1]:10248: connect: connection refused.
[kubelet-check] It seems like the kubelet isn't running or healthy.
[kubelet-check] The HTTP call equal to 'curl -sSL http://localhost:10248/healthz' failed with error: Get http://localhost:10248/healthz: dial tcp [::1]:10248: connect: connection refused.
[kubelet-check] It seems like the kubelet isn't running or healthy.
[kubelet-check] The HTTP call equal to 'curl -sSL http://localhost:10248/healthz' failed with error: Get http://localhost:10248/healthz: dial tcp [::1]:10248: connect: connection refused.
[kubelet-check] It seems like the kubelet isn't running or healthy.
[kubelet-check] The HTTP call equal to 'curl -sSL http://localhost:10248/healthz' failed with error: Get http://localhost:10248/healthz: dial tcp [::1]:10248: connect: connection refused.

Unfortunately, an error has occurred:
    timed out waiting for the condition

This error is likely caused by:
    - The kubelet is not running
    - The kubelet is unhealthy due to a misconfiguration of the node in some way (required cgroups disabled)

If you are on a systemd-powered system, you can try to troubleshoot the error with the following commands:
    - 'systemctl status kubelet'
    - 'journalctl -xeu kubelet'

Additionally, a control plane component may have crashed or exited when started by the container runtime.
To troubleshoot, list all containers using your preferred container runtimes CLI, e.g. docker.
Here is one example how you may list all Kubernetes containers running in docker:
    - 'docker ps -a | grep kube | grep -v pause'
    Once you have found the failing container, you can inspect its logs with:
    - 'docker logs CONTAINERID'
error execution phase wait-control-plane: couldn't initialize a Kubernetes cluste

kubeadm reset

kubernetesでkubeadm initを叩きなおす方法

Calico

Pods failed to start after switch cni plugin from flannel to calico and then flannel

sudo kubeadm reset
rm $HOME/.kube/config
sudo rm /etc/cni/net.d/10-calico.conflist && sudo rm /etc/cni/net.d/calico-kubeconfig

Flannel

Kubernetes cannot cleanup Flannel
KubernetesでCoreDNS立ち上がらない
KubernetesでNodeをリセットする方法
Kubernetesでpodが起動しない_controllerがおかしい

2019年版・Kubernetesクラスタ構築入門

なお、このネットワーク設定を行った後にkubeadm resetコマンドを実行してクラスタを作り直す場合、
以下のようにして作成された「flannel.1」や「cni0」という仮想ネットワークインターフェイスを先に削除しておく必要がある。

ip link delete cni0
ip link delete flannel.1

参考URL

Fedora CoreOS
fedora-coreos-tracker
fedora-coreos-streams

2020年1月29日 Fedoraプロジェクト,「Fedora CoreOS」のGAとともに「CoreOS Container Linux」をEOLへ
Container Linuxの後継となる新たなコンテナ向けOS「Fedora CoreOS」
Fedora CoreOSを使う
Installing Kubeadm on Fedora CoreOS

kubeadmのインストール
kubeadmのトラブルシューティング

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