[기술가이드] 2025년 Kubernetes 관리의 미래: kt cloud Cluster API 아키텍처 완벽 해설
[ kt cloud Foundation플랫폼팀 이지은 님 ]
“Turtles All The Way Down” 거북이는 끝없이 이어진다
무한 반복되는 계층적 구조
전해져 내려오는 이야기에 따르면 영국의 한 철학자가 우주가 어떻게 유지되는지 설명하는 강의를 하고 있었어요.
“이 세계는 무엇 위에 존재하는가?” 그러자 한 노부인이 손을 들고 말했습니다. "사실 우주는 커다란 거북이의 등 위에 놓여 있습니다!" 철학자가 "그렇다면 그 거북이는 무엇 위에 있나요?"라고 묻자, 노부인은 웃으며 "그것도 또 다른 거북이 위에 있고, 그 아래도 계속 거북이가 이어집니다. 끝없이요!" 라고 대답했어요. |
이 오래된 우화는 "Turtles All The Way Down"(거북이는 끝없이 이어진다)라는 표현으로 알려져 있으며, 무한 회귀와 계층적 구조의 개념을 설명하는 데 자주 인용됩니다.
흥미롭게도, Kubernetes 생태계의 중요한 도구인 Cluster API는 이 철학적 개념을 자신들의 아키텍처에 반영하면서 거북이 3마리를 로고로 정했습니다. Cluster API는 Kubernetes를 사용해서 Kubernetes를 관리하는 "무한 계층적 구조"진 거북이 3마리를 소개해드릴게요.🐢🐢🐢
🐢 거북이1 - Admin Cluster - Management Cluster 생성, CAPI 버전 관리, GitOps 파이프라인 관리…
🐢 거북이2 - Management Cluster - CAPI Controller가 실행되는 핵심 클러스터, Cluster 자체를 관리
🐢 거북이3 - Workload Cluster - Application을 배포하고 실행하는 클러스터
- 현재 kt cloud 의 Next 환경은 거북이1+2가 Admin Cluster 로 합쳐져있음
Kubernetes Cluster API
- Cluster API(CAPI)는 Kubernetes 클러스터의 생성, 업그레이드, 운영을 자동화하는 선언적 API 및 도구를 제공하는 Kubernetes 하위 프로젝트
- Kubernetes 네이티브 방식으로 클러스터의 생애주기를 관리 = Kubernetes의 리소스를 정의하는 것처럼 클러스터 자체도 리소스로 관리
- 예를 들어 Deployment yaml를 작성, 배포, 관리하는 것처럼 Cluster 를 정의한 yaml을 만들고 배포하고 관리
- Kubernetes 환경을 보다 쉽게 관리할 수 있도록 돕기 위한 목적 - 생성, 업그레이드, 삭제 자동화 → GitOps와 같은 CI/CD 방식(ArgoCD)과 연동
- 멀티 클러스터 환경에서 운영 및 배포 자동화와 일관성 유지 가능
- 여러 Infra Provider 중 byoh 를 사용 - infra provider 에 dependancy 없이 모든 환경에 동일한 방식으로 배포 가능
Management Cluster = 관리 클러스터 = Next 환경의 Admin 클러스터
- Cluster API 가 동작하는 곳. Cluster API Controller, Infra Provider가 실행되는 곳. Cluster API의 모든 CRD가 생성되어 있는 곳.
- 다른 클러스터를 생성하고 관리하는 역할 (Application 실행 목적 X ), Admin 클러스터에서 여러개의 Workload 클러스터를 관리
- Admin 클러스터에 장애가 발생했을 때 영향도? argocd를 통한 관리/배포 불가, Workload Cluster가 중단되지는 않음(독립적)
- Kubernetes Cluster를 기존 설치 방식으로 생성한 뒤 아래 명령어를 실행하면 Cluster API 리소스가 배포 됨
clusterctl init --infrastructure byoh
Controller
- CAPI Controller - Cluster와 Machine 리소스를 관리 / Control-Plane Controller - kubernetes apiserver, etcd, scheduler, controller manager 등 설정 / Bootstrap Controller - kubeadmconfig 리소스로 노드 부팅 과정과 kubelet 설정 관리
- 클러스터가 늘어날 수록 부하가 증가하므로 리소스 모니터링 필요
- byoh Controller - Infra Provider 컨트롤러는 각 환경(aws, gcp…)에서 Cluster API를 사용할 수 있도록 관리
CustomResourceDefinitions
- Cluster API를 사용해 Workload Cluster를 만들 때 필요한 사용자 리소스 정의 (아래에 자세히)
NameSpace 생성
- Cluster API 배포 후, Workload Cluster 를 배포하고 관리할 새로운 NameSpace를 생성
- Workload Cluster 마다 하나의 NameSpace를 가진다
- Workload Cluster가 생성되는 과정에서 Admin Cluster의 Namespace 하위에 인증서가 Secret으로 생성/관리 된다
- 아래에 해당하는 Kubernetes 인증서는 최초 생성 시 유효기간을 100년으로 설정할 예정
Managed Cluster = Workload Cluster = 관리 대상 클러스터 = pl-dev, svc-dev
- Cluster API를 통해 생성된 클러스터. Application이 실행되는 곳.
- Admin cluster가 물리 서버 또는 VM을 Kubernetes 노드로 관리하기 위해 byoh-agent 설치
- Admin의 Kubeconfig(클러스터 접속을 위한 인증 정보), NameSpace 등의 정보를 설정한 뒤 systemd로 실행
- byoh-agent 서비스가 중지되더라도 이미 생성된 클러스터의 노드 상태는 계속 Running. Not Ready가 되는 것은 아님
- Admin의 Kubeconfig(클러스터 접속을 위한 인증 정보), NameSpace 등의 정보를 설정한 뒤 systemd로 실행
# byoh 에이전트를 다운
wget https://github.com/vmware-tanzu/cluster-api-provider-bringyourownhost/releases/download/v0.3.0/byoh-hostagent-linux-amd64
# management cluster의 kubeconfig
vi /etc/systemd/system/byoh-agent.service
[Unit]
Description=byoh-agent service
After=network.target
[Service]
Type=simple
Environment="NS_ARGS=--namespace svc-dev"
Environment="CERT_ARGS=--certExpiryDuration 189216000"
Environment="CERTIFICATE_ROTATION=true"
Environment="METRIC_ARGS=--metricsbindaddress :10001"
Environment="LABEL_ARGS=--label env=svc-dev --label role=master --label hostname=byoh01 --label kubernetes=v1.30.7"
Environment="MGMT_ARGS=--bootstrap-kubeconfig /etc/byoh-agent/mgmt/kubeconfig"
Environment="INSTALL_ARGS=--skip-installation true"
ExecStart=/usr/bin/byoh-agent $NS_ARGS $CERT_ARGS $LABEL_ARGS $MGMT_ARGS $INSTALL_ARGS $METRIC_ARGS
User=root
Group=root
[Install]
WantedBy=multi-user.target
- Admin cluster에서 byohost 리소스가 조회되면 정상
kubectl get byohost -A --show-labels
NAMESPACE NAME OSNAME OSIMAGE ARCH LABELS
etcd-init d2 linux Ubuntu 24.04 LTS amd64 environment=etcd-init,hostname=d2,role=d2
etcd-init d3 linux Ubuntu 24.04 LTS amd64 environment=etcd-init,hostname=d3,role=d3
etcd e4 linux Ubuntu 24.04 LTS amd64 byoh.infrastructure.cluster.x-k8s.io/byomachine-name=etcd.etcd-control-plane-txl5n,cluster.x-k8s.io/cluster-name=etcd,environment=etcd,hostname=e4,role=e4
etcd e5 linux Ubuntu 24.04 LTS amd64 byoh.infrastructure.cluster.x-k8s.io/byomachine-name=etcd.etcd-md-0-xvq8t-hjvr9,cluster.x-k8s.io/cluster-name=etcd,environment=etcd,hostname=e5,role=e5
Workload Cluster 생성
- yaml 작성 및 생성
CRDs Dependency
- 상위 CRD를 생성한 후 하위 CRD를 생성 (삭제할 땐 반대로)
Cluster
├── BYOHCluster (클러스터 설정, ControlPlaneEndpoint 정의)
├── KubeadmControlPlane (컨트롤 플레인 관리)
│ ├── Machine (Control Plane 노드)
│ ├── KubeadmConfig (Control Plane 노드를 bootstrap, kubeadm init 설정)
│ └── BYOHMachine (BYOH 호스트와 연결)
├── MachineDeployment (Worker 노드 관리, MachineSet을 생성)
│ ├── MachineSet (Replica 관리, Machine을 생성/유지/삭제)
│ │ ├── Machine (Worker 노드)
│ │ ├── KubeadmConfig (Worker 노드를 bootstrap, kubeadm join 설정)
│ │ └── BYOHMachine (BYOH 호스트와 연결)
└── Node (최종적으로 Kubernetes에 등록됨)
CRDs 연관관계
- 리소스 생성 순서:
1. Cluster
워크로드 클러스터를 정의
apiVersion: cluster.x-k8s.io/v1beta1
kind: Cluster
metadata:
name: my-workload-cluster
namespace: default
spec:
clusterNetwork:
services:
cidrBlocks: ["10.96.0.0/12"] # 서비스 CIDR
pods:
cidrBlocks: ["192.168.0.0/16"] # Pod CIDR
controlPlaneRef:
apiVersion: controlplane.cluster.x-k8s.io/v1beta1
kind: KubeadmControlPlane
name: my-workload-cluster-control-plane
infrastructureRef:
apiVersion: infrastructure.cluster.x-k8s.io/v1beta1
kind: BYOHCluster
name: my-workload-cluster-infra
2. ByoCluster (ControlPlaneEndpoint 정의)
클러스터 네트워크 및 컨트롤 플레인 연결 설정
apiVersion: infrastructure.cluster.x-k8s.io/v1beta1
kind: BYOHCluster
metadata:
name: my-workload-cluster-infra
spec:
controlPlaneEndpoint:
host: 192.168.100.10 # API 서버 VIP 또는 LoadBalancer IP
port: 6443
controlPlaneRef:
apiVersion: controlplane.cluster.x-k8s.io/v1beta1
kind: KubeadmControlPlane
name: my-workload-cluster-control-plane
infrastructureRef:
apiVersion: infrastructure.cluster.x-k8s.io/v1beta1
kind: BYOHCluster
name: my-workload-cluster-infra
3. KubeadmControlPlane master node 생성성
Control Plane의 노드 초기화(init) 및 노드 join 설정
apiVersion: controlplane.cluster.x-k8s.io/v1beta1
kind: KubeadmControlPlane
metadata:
name: my-workload-cluster-control-plane
spec:
replicas: 3 # 마스터 노드 3개
version: v1.28.0
infrastructureTemplate: # 마스터 노드 등록
apiVersion: infrastructure.cluster.x-k8s.io/v1beta1
kind: BYOHMachine
name: my-control-plane-machine
kubeadmConfigSpec:
clusterConfiguration:
apiServer: # apiserver
extraArgs:
enable-admission-plugins: NodeRestriction
advertise-address: "172.26.55.150" # Control Plane API Server 주소
controllerManager: # controller manager
extraArgs:
bind-address: 0.0.0.0
scheduler: # scheduler
extraArgs:
bind-address: 0.0.0.0
etcd: #etcd
local:
dataDir: /var/lib/etcd
initConfiguration: # init
nodeRegistration:
name: "{{ ds.meta_data.local_hostname }}"
kubeletExtraArgs:
cloud-provider: external
container-runtime-endpoint: "unix:///run/containerd/containerd.sock"
cgroup-driver: systemd
certificateKey: "your-secure-certificate-key" # `kubeadm init` 실행 후 가져올 수 있음
joinConfiguration: # join
nodeRegistration:
name: "{{ ds.meta_data.local_hostname }}"
kubeletExtraArgs:
cloud-provider: external
container-runtime-endpoint: "unix:///run/containerd/containerd.sock"
cgroup-driver: systemd
controlPlane:
localAPIEndpoint:
advertiseAddress: "172.26.55.150"
bindPort: 6443
preKubeadmCommands:
- echo "Setting up Control Plane prerequisites"
- apt-get update && apt-get install -y containerd
- systemctl enable --now containerd
postKubeadmCommands:
- echo "Control Plane setup complete!"
files:
- path: /etc/kubernetes/kubelet.conf
content: |
KUBELET_EXTRA_ARGS=--cloud-provider=external
owner: root:root
permissions: "0644"
4. MachineDeployment & MachineSet
- Machine = Node 를 Cluster API의 리소스로 정의 한 것
- MachineDeployment : MachineSet : Machine = Deployment : ReplicaSet : Pod 와 비슷한 관계
- MachineDeployment는 Rolling Update와 장애복구를 효율적으로 처리하기 위해 Machine을 직접 관리하지 않고 MachineSet을 통해 처리함
워크로드 클러스터의 Worker 노드 배포 및 복제 관리 설정
# MachineDeployment: Worker 노드 관리
apiVersion: cluster.x-k8s.io/v1beta1
kind: MachineDeployment
metadata:
name: my-workload-worker-nodes
spec:
clusterName: my-workload-cluster
replicas: 2 # Worker 노드 2개
selector: # selector
matchLabels:
worker: "true"
template: # labels
metadata:
labels:
worker: "true"
spec:
version: v1.27.0
infrastructureRef:
apiVersion: infrastructure.cluster.x-k8s.io/v1beta1
kind: BYOHMachine
name: my-worker-machine
bootstrap:
configRef:
apiVersion: bootstrap.cluster.x-k8s.io/v1beta1
kind: KubeadmConfig
name: my-worker-bootstrap
# MachineSet: Worker 노드 복제 관리
apiVersion: cluster.x-k8s.io/v1beta1
kind: MachineSet
metadata:
name: my-workload-worker-machineset
spec:
clusterName: my-workload-cluster
replicas: 2 # Worker 노드 2개 유지
selector:
matchLabels:
worker: "true"
template:
metadata:
labels:
worker: "true"
spec:
version: v1.27.0
infrastructureRef:
apiVersion: infrastructure.cluster.x-k8s.io/v1beta1
kind: BYOHMachine
name: my-worker-machine
bootstrap:
configRef:
apiVersion: bootstrap.cluster.x-k8s.io/v1beta1
kind: KubeadmConfig
name: my-worker-bootstrap
#KubeadmConfig: Worker 노드가 Control Plane에 join
apiVersion: bootstrap.cluster.x-k8s.io/v1beta1
kind: KubeadmConfig
metadata:
name: my-worker-bootstrap
spec:
joinConfiguration: # join Configuration
nodeRegistration:
name: "{{ ds.meta_data.local_hostname }}"
kubeletExtraArgs:
cloud-provider: external
container-runtime-endpoint: "unix:///run/containerd/containerd.sock"
cgroup-driver: systemd
discovery:
bootstrapToken:
apiServerEndpoint: "172.26.55.150:6443" # Control Plane API 서버 주소
token: "abcdef.0123456789abcdef"
caCertHashes:
- "sha256:your-ca-cert-hash" # kubeadm token create --print-join-command로 가져오기
preKubeadmCommands:
- echo "Setting up prerequisites for BYOH Worker"
- apt-get update && apt-get install -y containerd
- systemctl enable --now containerd
postKubeadmCommands:
- echo "Worker node successfully joined the cluster!"
files:
- path: /etc/kubernetes/kubelet.conf
content: |
KUBELET_EXTRA_ARGS=--cloud-provider=external
owner: root:root
permissions: "0644"
5. ByohMachine BYOH 환경에서는 Kubernetes가 노드를 직접 생성하지 않기 때문에, 수동으로 등록할 물리/가상 머신을 정의
BYOH 환경에서 개별 노드의 인프라 및 연결 설정
apiVersion: infrastructure.cluster.x-k8s.io/v1beta1
kind: BYOHMachine
metadata:
name: my-control-plane-machine
spec:
os: ubuntu
osDetails:
version: "22.04"
kernelVersion: "5.15.0-105-generic"
hostname: master-node-1 # 실제 hostname을 정확하게 입력
ipAddresses:
- 172.26.55.150 # 실제 host의 ip
sshConfig:
user: ubuntu
privateKeyPath: /home/ubuntu/.ssh/id_rsa
6. KubeadmConfig
BYOH 노드의 kubeadm 초기화 및 조인 설정
apiVersion: bootstrap.cluster.x-k8s.io/v1beta1
kind: KubeadmConfig
metadata:
name: byoh-worker-bootstrap
spec:
joinConfiguration:
nodeRegistration:
name: "{{ ds.meta_data.local_hostname }}"
kubeletExtraArgs:
cloud-provider: external
container-runtime-endpoint: "unix:///run/containerd/containerd.sock"
cgroup-driver: systemd
discovery:
bootstrapToken:
apiServerEndpoint: "172.26.55.150:6443" # Control Plane API 서버 주소
token: "abcdef.0123456789abcdef"
caCertHashes:
- "sha256:your-ca-cert-hash" # kubeadm init 실행 후 kubeadm token create --print-join-command에서 가져오기
preKubeadmCommands:
- echo "Setting up prerequisites for BYOH Worker"
- apt-get update && apt-get install -y containerd
- systemctl enable --now containerd
postKubeadmCommands:
- echo "Worker node successfully joined the cluster!"
files:
- path: /etc/kubernetes/kubelet.conf
content: |
KUBELET_EXTRA_ARGS=--cloud-provider=external
owner: root:root
permissions: "0644"
7. Node (kubernetes에 등록)
- Workload Cluster의 kubeconfig 가져오기
kubectl get secret my-workload-cluster-kubeconfig -o jsonpath={.data.value} | base64 --decode > my-workload-cluster.kubeconfig
- Workload Cluster의 kubeconfig를 사용하여 클러스터 연결 확인
kubectl --kubeconfig=my-workload-cluster.kubeconfig get nodes
Cluster API 환경에서 Workload Cluster에 Worker Node 추가/삭제/스펙 변경
MachineSet, MachineDeployment, KubeadmConfigTemplate 등을 활용하여 새로운 Worker Node를 증설
- 현재 MachineDeployment와 MachineSet을 모두 사용 중으로, 가장 상위 개체인 MachineDeployment의 replicas 값을 늘리면 자동으로 Worker Node가 추가 됨
kubectl scale machinedeployment <machine-deployment-name> --replicas=3 -n <namespace>
Worker Node 제거
- 특정 노드를 삭제해야 하는 것이 아니라면 기본적으로 위와 동일하게 replicas로 노드 수를 조절
- But 삭제할 특정 노드가 정해져 있을 때 (예: 성능이 낮은 노드, 장애가 발생한 노드) 특정 Worker Node가 물리적인 이유로 제거되어야 할 때 (예: 특정 AZ에서 노드 제거 필요)는 특정 노드를 머신에서 삭제한 뒤 replicas 수정
- replicas를 수정하지 않을 경우, replicas 수를 맞추기 위해 사용할 수 있는 머신이 등록되어 자동 교체가 됨
kubectl drain <node-name> --ignore-daemonsets --delete-local-data
kubectl delete machine <machine-name> -n <namespace>
kubectl scale machinedeployment <machine-deployment-name> --replicas=<new-value> -n <namespace>
Worker Node 스펙 변경 시
AWS에서는 스펙이 변경되면 MachineTemplate에서 instanceType: t3.large 변경해줘야 하지만 byoh에서는 MachineTemplate을 사용하지 않고 MachineDeployment 에서 해당 설정은 하지 않음
→ 노드를 직접 업그레이드 후 재부팅 하는 기존 방식과 동일
Cluster API 환경에서 Kubernetes 설정 변경하기
Cluster API(CAPI)는 Kubernetes 클러스터 자체를 선언적으로 관리하는 구조이기 때문에 Kubernetes 기본 애드온(kube-proxy, scheduler, coredns, kube-apiserver 등) 설정을 변경할 때
직접 클러스터에서 kubeadm을 실행하는 것이 아니라, Cluster API 리소스를 수정
일반적인 애플리케이션(Nginx, Istio, Prometheus 등)의 설정 변경은 CAPI 방식이 아닌, Helm, Kustomize, ArgoCD 같은 방식으로 변경
- Kubernetes 자체 컴포넌트 설정 변경 → Cluster API 리소스 수정
- 일반적인 애플리케이션 설정 변경 → Helm/Kustomize/ArgoCD 활용
CoreDNS의 max_concurrent 값 변경 작업
CoreDNS의 max_concurrent 값(CoreDNS에서 한 번에 처리할 수 있는 최대 동시 요청 수)을 default 100에서 150으로 변경할 경우,
- 클러스터에서 사용 중인 KubeadmConfigTemplate 확인
kubectl get kubeadmconfigtemplate -n <namespace>
kubectl get kubeadmconfigtemplate <config-template-name> -n <namespace> -o yaml
- CoreDNS의 ConfigMap을 변경하기 위해 KubeadmConfigTemplate의 preKubeadmCommands에 CoreDNS 설정을 직접 패치하는 명령어를 추가
apiVersion: bootstrap.cluster.x-k8s.io/v1beta1
kind: KubeadmConfigTemplate
metadata:
name: my-cluster-worker-template
namespace: default
spec:
template:
spec:
preKubeadmCommands:
- |
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: ConfigMap
metadata:
name: coredns
namespace: kube-system
data:
Corefile: |
.:53 {
errors
health
ready
kubernetes cluster.local in-addr.arpa ip6.arpa {
pods insecure
fallthrough in-addr.arpa ip6.arpa
ttl 30
}
forward . /etc/resolv.conf
cache 30
loop
reload
loadbalance
prometheus :9153
max_concurrent 150 # 변경된 값
}
EOF
클러스터 업그레이드
기존에는 kubeadm upgrade plan → kubeadm upgrade apply 했지만,
CAPI 환경에서는 KubeadmControlPlane과 MachineDeployment의 버전을 변경하면 자동으로 롤링 업데이트 진행됨
- Control Plane → Worker 순서로 진행되며, 변경 사항이 적용되면 자동으로 롤링 업데이트됨
- 컨트롤 플레인 업그레이드 → KubeadmControlPlane의 spec.version 변경
- Worker Node 업그레이드 → MachineDeployment의 spec.template.spec.version 변경
apiVersion: controlplane.cluster.x-k8s.io/v1beta1
kind: KubeadmControlPlane
metadata:
name: my-cluster-control-plane
namespace: default
spec:
version: v1.32.0 # 여기서 새로운 Kubernetes 버전으로 변경
kubeadmConfigSpec:
clusterConfiguration:
apiServer:
extraArgs:
feature-gates: "SomeFeature=true"
initConfiguration:
nodeRegistration:
kubeletExtraArgs:
node-labels: "node-role.kubernetes.io/control-plane="
joinConfiguration:
nodeRegistration:
kubeletExtraArgs:
node-labels: "node-role.kubernetes.io/control-plane="
replicas: 3 # 컨트롤 플레인 노드 개수
apiVersion: cluster.x-k8s.io/v1beta1
kind: MachineDeployment
metadata:
name: my-worker-md
namespace: default
spec:
clusterName: my-cluster
replicas: 3 # Worker Node 개수
template:
spec:
version: v1.32.0 # 여기서 Worker Node의 버전 변경
bootstrap:
configRef:
name: my-worker-bootstrap
apiVersion: bootstrap.cluster.x-k8s.io/v1beta1
kind: KubeadmConfigTemplate
infrastructureRef:
name: my-worker-byoh
apiVersion: infrastructure.cluster.x-k8s.io/v1beta1
kind: ByoMachine
마무리
끝없이 이어지는 거북이들처럼, Cluster API의 아키텍처도 계층 위에 계층, 클러스터 위에 또 다른 클러스터가 얹힌 구조로 이루어져 있습니다. 처음엔 “왜 Kubernetes로 Kubernetes를 관리해야 할까?”라는 의문이 들 수 있지만, 실제 운영 환경이 커지고, 멀티 클러스터를 고려하게 되는 시점엔 이 구조가 왜 필요한지를 몸소 느끼게 됩니다.
Cluster API는 단순한 자동화 도구를 넘어, Kubernetes 클러스터 자체를 선언형으로 관리할 수 있는 새로운 패러다임을 제공합니다. GitOps, CI/CD 파이프라인, BYOH 등과의 조합을 통해 진정한 인프라 운영의 일관성과 유연성을 실현할 수 있게 해주죠.
"거북이는 끝없이 이어진다."
이 철학적인 우화를 통해 Cluster API가 추구하는 구조적 아름다움과 무한한 확장 가능성을 다시 한 번 되새겨 봅니다. 이 구조는 단순한 기술이 아니라, 앞으로 우리가 인프라를 바라보는 방식 그 자체를 바꿔나갈 수 있는 하나의 프레임워크입니다. 여러분의 클러스터 여정에도 🐢거북이 세 마리가 나란히 따라갈 준비가 되었길 바랍니다.
[관련/출처]
https://cluster-api.sigs.k8s.io/ |