아키텍처


Kubernetes cluster 및 아키텍처 구축

공통

1. 준비

  1. 머신 성능
    [최소 성능]
    RAM: 2GB
    CPU: 2 Core

  2. 인스턴스 준비
    [최소 개수]
    Master Node: 1
    Worker Node: 2

  3. Registry, Redis, MariaDB 준비
    1. Docker Registry
    2. Redis
      !!! 만약 redis 서버가 netstat -nlpt 에서 127.0.0.1:6379로 바인딩 돼있으면 외부통신이 불가하므로
      /etc/redis.conf 파일에서 bind 0.0.0.0으로 수정하고 restart 해주자
    3. MariaDB

2. 리눅스 내부 세팅

  1. hosts 설정
     cat <<EOF | tee /etc/hosts
     10.10.100.6 ldk-k8s-master
     10.10.100.7 ldk-k8s-worker1
     10.10.100.11 ldk-k8s-worker2
  2. SELinux disable
     setenforce 0
     sed -i 's/^SELINUX=enforcing$/SELINUX=permissive/' /etc/selinux/config
  3. 모듈 로드
     cat <<EOF | tee /etc/modules-load.d/k8s.conf
     overlay
     br_netfilter
     EOF
     
     modprobe overlay
     modprobe br_netfilter
  4. cluster network set
     cat <<EOF | tee /etc/sysctl.d/k8s.conf
     net.bridge.bridge-nf-call-iptables = 1
     net.bridge.bridge-nf-call-ip6tables = 1
     net.ipv4.ip_forward = 1
     EOF
  5. swap off
     swapoff -a
     sed -i '/ swap / s/^/#/' /etc/fstab
     
     sysctl --system

3. 모든 Node에 대해 고유한 Hostname, Mac address, UUID 확인

4. Cgroup Driver (택 1)

Kubernetes Cluster에서 리눅스 컨트롤 그룹(cgroups)을 관리하는 방식에 대해 결정하는 역할
시스템 리소스(예: CPU, 메모리, 디스크 I/O 등)를 제한 및 각 프로세스나 서비스에 할당 방법을 감독하는 기능을 제공

  1. 일관된 환경: Kubernetes 클러스터의 각 노드에서 systemd를 사용함으로써 부팅 스크립트, 서비스 관리 및 로깅 시스템 등에서 일관된 환경을 제공 -> 용이한 관리 및 트러블슈팅

  2. 보다 나은 통합: systemd는 cgroups를 사용하여 리소스 제한과 프로세스 관리를 수행. Kubernetes는 이러한 기능을 활용하여 컨테이너의 리소스 사용을 제어 및 systemd와의 통합을 통해 강력하고 일관된 방식으로 관리 가능

  3. 에러 최소화: 서로 다른 초기화 시스템이나 프로세스 관리자를 사용 시, 설정 차이나 호환성 문제로 인해 예기치 않은 에러가 발생할 수 있는데 systemd를 통일함으로써 호환성 문제를 최소화 가능

  • systemd (k8s 공식 문서 권장, 필자가 사용한 Cgroup Driver )

    • 시스템 서비스와의 일관성: Kubernetes가 시스템의 다른 서비스와 동일한 cgroup 계층을 사용하게 되어 리소스 관리에서 충돌을 방지 및 전체 시스템의 리소스 할당을 일관되게 관리 가능

    • 보다 나은 자원 제어: cgroup을 더 체계적으로 관리하고, 시스템 리소스를 더 효율적으로 할당 및 모니터링할 수 있는 기능을 제공

    • 보안과 안정성: 보안 업데이트와 시스템 통합이 더 자주 이루어지므로, 보안과 안정성 측면에서 이점을 제공

  • cgroupfs

    • 간단한 구성과 직접적인 제어
      각 cgroup을 파일 시스템으로 직접 제어할 수 있는 인터페이스를 제공하여 관리자가 리소스 관리를 직접적으로 수행할 수 있게 해주며, 복잡한 계층적 구조 없이도 세밀한 리소스 제어가 가능

    • 운영 체제와의 독립성
      systemd를 사용하지 않는 리눅스 배포판에서도 일관된 환경을 제공하여 다양한 리눅스 환경에서의 포터빌리티(portability)를 향상시킴

    • 환경의 단순화
      특정 시스템에 systemd의 복잡한 설정과 관리 로직이 필요 없을 경우, cgroupfs를 사용하면 시스템 단순화 및 시스템 오버헤드 감소로 더 가벼운 환경을 만드는 데 유리함

5. CRI (택 1)

  • CRI-O ( 필자가 사용한 CRI )
      PROJECT_PATH=prerelease:/maincat <<EOF | tee /etc/yum.repos.d/cri-o.repo
      [cri-o]
      name=CRI-O
      baseurl=https://pkgs.k8s.io/addons:/cri-o:/$PROJECT_PATH/rpm/
      enabled=1
      gpgcheck=1
      gpgkey=https://pkgs.k8s.io/addons:/cri-o:/$PROJECT_PATH/rpm/repodata/repomd.xml.key
      EOF
      
      yum install -y container-selinux cri-o iproute-tc
      systemctl enable --now crio
  • containerd

6. k8s yum 저장소 추가 및 설치

 cat <<EOF | tee /etc/yum.repos.d/kubernetes.repo
 [kubernetes]
 name=Kubernetes
 baseurl=https://pkgs.k8s.io/core:/stable:/v1.29/rpm/
 enabled=1
 gpgcheck=1
 repo_gpgcheck=1
 gpgkey=https://pkgs.k8s.io/core:/stable:/v1.29/rpm/repodata/repomd.xml.key
 exclude=kube*
 EOF
 
 yum install -y kubelet kubeadm kubectl --disableexcludes=kubernetes
 
 systemctl enable --now kubelet


Master

1. kubeadm init

 kubeadm init --apiserver-advertise-address=10.10.100.6 --pod-network-cidr=10.244.0.0/16

2. kubectl 권한주기

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

3. kubectl alias

 kubectl completion bash | tee /etc/bash_completion.d/kubectl > /dev/null
 echo 'alias k=kubectl' >>~/.bashrc
 echo 'complete -o default -F __start_kubectl k' >>~/.bashrc
 exec bash

Worker

1. kubeadm join

kubeadm init 하면서 얻은 토큰값으로 워커노드 전체에 kubeadm join

 kubeadm join 10.10.100.6:6443 --token jl7n01.taovls0ekgbmrlb8 \
         --discovery-token-ca-cert-hash sha256:171e155dd22b904e0827a8e262a54d777e9f4becf0aa24f066e2dcd3575e217c

2. crio.conf (insecure-registry)

도커 서버에서 insecure-registry 설정을 해줘도 k8s cluster와 이미지 관련 통신 때
cluster에서는 pod 생성 중 image pulling 과정에서 https 통신을 요청하기에 CRI에 insecure-registry 설정을 주어
k8s와 docker 사이의 통신을 맞춰준다. (https 통신을 원할경우 서로간의 ssl 인증설정이 추가적으로 필요)

 cat << EOF|tee /etc/containers/registries.conf.d/crio.conf
 unqualified-search-registries = ["docker.io", "quay.io"]
 
 [[registry]]
 prefix = ""
 location = "10.10.100.8:5000"
 insecure = true
 EOF
 
 systemctl restart crio

In Cluster

1. CNI 배포 ( 택 1 )

  • calico

     kubectl apply -f https://docs.projectcalico.org/manifests/calico.yaml
  • flannel ( 필자가 사용한 CNI )

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

2. Check Cluster

  • k get nodes
  • k get all --all-namespaces -o wide

3. 3-Tier Architecting ( Dev )

# 이미지는 내가 보여주고자 하는 내용을 담은 내가 만든 tomcat 9.0 이미지다.

  • Dev.yaml

      apiVersion: v1
      kind: Namespace
      metadata:
        name: dev
      ---
      apiVersion: apps/v1
      kind: Deployment
      metadata:
        name: dev-nginx-deployment
        namespace: dev
        labels:
          app: dev-nginx
      spec:
        replicas: 2
        selector:
          matchLabels:
            app: dev-nginx
        template:
          metadata:
            labels:
              app: dev-nginx
          spec:
            containers:
            - name: dev-nginx
              image: nginx:latest
              ports:
              - containerPort: 80
              volumeMounts:
              - name: dev-nginx-conf-volume
                mountPath: /etc/nginx/nginx.conf
                subPath: nginx.conf
            volumes:
            - name: dev-nginx-conf-volume
              configMap:
                name: dev-nginx-config
      ---
      apiVersion: v1
      kind: ConfigMap
      metadata:
        name: dev-nginx-config
        namespace: dev
      data:
        nginx.conf: |
          user  nginx;
          worker_processes  1;
          error_log  /var/log/nginx/error.log warn;
          pid        /var/run/nginx.pid;
          events {
              worker_connections  1024;
          }
          http {
              include       /etc/nginx/mime.types;
              default_type  application/octet-stream;
              log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                                '$status $body_bytes_sent "$http_referer" '
                                '"$http_user_agent" "$http_x_forwarded_for"';
              access_log  /var/log/nginx/access.log  main;
              sendfile        on;
              keepalive_timeout  65;
              server {
                  listen       80;
                  location / {
                      proxy_pass http://dev-tomcat-clusterip-service:8080;
                      proxy_set_header Host $host;
                      proxy_set_header X-Real-IP $remote_addr;
                      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
                      proxy_set_header X-Forwarded-Proto $scheme;
                  }
              }
          }
    
      ---
      
      apiVersion: v1
      kind: Service
      metadata:
        name: dev-nginx-clusterip-service
        namespace: dev
      spec:
        selector:
          app: dev-nginx
        ports:
          - protocol: TCP
            port: 80
            targetPort: 80
      ---
      apiVersion: v1
      kind: Service
      metadata:
        name: dev-nginx-nodeport-service
        namespace: dev
      spec:
        type: NodePort
        selector:
          app: dev-nginx
        ports:
          - protocol: TCP
            port: 80
            targetPort: 80
            nodePort: 30007
      ---
      apiVersion: apps/v1
      kind: Deployment
      metadata:
        name: dev-tomcat-deployment
        namespace: dev
      spec:
        replicas: 2
        selector:
          matchLabels:
            app: dev-tomcat
        template:
          metadata:
            labels:
              app: dev-tomcat
          spec:
            containers:
            - name: dev-tomcat
              image: 10.10.100.8:5000/tomcat-custom:dev
              ports:
              - containerPort: 8080
      ---
      apiVersion: v1
      kind: Service
      metadata:
        name: dev-tomcat-clusterip-service
        namespace: dev
      spec:
        type: ClusterIP
        selector:
          app: dev-tomcat
        ports:
        - protocol: TCP
          port: 8080
          targetPort: 8080

  • k get all -n dev -o wide


  • sessionCheck.jsp

  • checkDBConnection.jsp

4. [ Dev / Stg / Prd ] Separation

dev와 비슷한 구조로 namespace와 이미지를 다르게 해서 스테이징 환경 ( stg ), 프로덕션 환경 ( prd )을 배포해준다

  • stg.yaml

         apiVersion: v1
         kind: Namespace
         metadata:
           name: stg
         ---
         apiVersion: apps/v1
         kind: Deployment
         metadata:
           name: stg-nginx-deployment
           namespace: stg
           labels:
             app: stg-nginx
         spec:
           replicas: 2
           selector:
             matchLabels:
               app: stg-nginx
           template:
             metadata:
               labels:
                 app: stg-nginx
             spec:
               containers:
               - name: stg-nginx
                 image: nginx:latest
                 ports:
                 - containerPort: 80
                 volumeMounts:
                 - name: stg-nginx-conf-volume
                   mountPath: /etc/nginx/nginx.conf
                   subPath: nginx.conf
               volumes:
               - name: stg-nginx-conf-volume
                 configMap:
                   name: stg-nginx-config
         ---        
         apiVersion: v1
         kind: ConfigMap
         metadata:
           name: stg-nginx-config
           namespace: stg
         data:
           nginx.conf: |
             user  nginx;
             worker_processes  1;
             error_log  /var/log/nginx/error.log warn;
             pid        /var/run/nginx.pid;
             events {
                 worker_connections  1024;
             }
             http {
                 include       /etc/nginx/mime.types;
                 default_type  application/octet-stream;
                 log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                                   '$status $body_bytes_sent "$http_referer" '
                                   '"$http_user_agent" "$http_x_forwarded_for"';
                 access_log  /var/log/nginx/access.log  main;
                 sendfile        on;
                 keepalive_timeout  65;
                 server {
                     listen       80;
                     location / {
                         proxy_pass http://stg-tomcat-clusterip-service:8080;
                         proxy_set_header Host $host;
                         proxy_set_header X-Real-IP $remote_addr;
                         proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
                         proxy_set_header X-Forwarded-Proto $scheme;
                     }
                 }
             }
             
         ---
         
         apiVersion: v1
         kind: Service
         metadata:
           name: stg-nginx-clusterip-service
           namespace: stg
         spec:
           selector:
             app: stg-nginx
           ports:
             - protocol: TCP
               port: 80
               targetPort: 80
         ---
         apiVersion: v1
         kind: Service
         metadata:
           name: stg-nginx-nodeport-service
           namespace: stg
         spec:
           type: NodePort
           selector:
             app: stg-nginx
           ports:
             - protocol: TCP
               port: 80
               targetPort: 80
               nodePort: 30008
         ---
         apiVersion: apps/v1
         kind: Deployment
         metadata:
           name: stg-tomcat-deployment
           namespace: stg
         spec:
           replicas: 2
           selector:
             matchLabels:
               app: stg-tomcat
           template:
             metadata:
               labels:
                 app: stg-tomcat
             spec:
               containers:
               - name: stg-tomcat
                 image: 10.10.100.8:5000/tomcat-custom:stg
                 ports:
                 - containerPort: 8080
         ---
         apiVersion: v1
         kind: Service
         metadata:
           name: stg-tomcat-clusterip-service
           namespace: stg
         spec:
           type: ClusterIP
           selector:
             app: stg-tomcat
           ports:
           - protocol: TCP
             port: 8080
             targetPort: 8080

  • prd.yaml

         apiVersion: v1
         kind: Namespace
         metadata:
           name: prd
         ---
         apiVersion: apps/v1
         kind: Deployment
         metadata:
           name: prd-nginx-deployment
           namespace: prd
           labels:
             app: prd-nginx
         spec:
           replicas: 2
           selector:
             matchLabels:
               app: prd-nginx
           template:
             metadata:
               labels:
                 app: prd-nginx
             spec:
               containers:
               - name: prd-nginx
                 image: nginx:latest
                 ports:
                 - containerPort: 80
                 volumeMounts:
                 - name: prd-nginx-conf-volume
                   mountPath: /etc/nginx/nginx.conf
                   subPath: nginx.conf
               volumes:
               - name: prd-nginx-conf-volume
                 configMap:
                   name: prd-nginx-config
         ---
         apiVersion: v1
         kind: ConfigMap
         metadata:
           name: prd-nginx-config
           namespace: prd
         data:
           nginx.conf: |
             user  nginx;
             worker_processes  1;
             error_log  /var/log/nginx/error.log warn;
             pid        /var/run/nginx.pid;
             events {
                 worker_connections  1024;
             }
             http {
                 include       /etc/nginx/mime.types;
                 default_type  application/octet-stream;
                 log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                                   '$status $body_bytes_sent "$http_referer" '
                                   '"$http_user_agent" "$http_x_forwarded_for"';
                 access_log  /var/log/nginx/access.log  main;
                 sendfile        on;
                 keepalive_timeout  65;
                 server {
                     listen       80;
                     location / {
                         proxy_pass http://prd-tomcat-clusterip-service:8080;
                         proxy_set_header Host $host;
                         proxy_set_header X-Real-IP $remote_addr;
                         proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
                         proxy_set_header X-Forwarded-Proto $scheme;
                     }
                 }
             }
             
         ---
         
         apiVersion: v1
         kind: Service
         metadata:
           name: prd-nginx-clusterip-service
           namespace: prd
         spec:
           selector:
             app: prd-nginx
           ports:
             - protocol: TCP
               port: 80
               targetPort: 80
         ---
         apiVersion: v1
         kind: Service
         metadata:
           name: stg-nginx-nodeport-service
           namespace: stg
         spec:
           type: NodePort
           selector:
             app: stg-nginx
           ports:
             - protocol: TCP
               port: 80
               targetPort: 80
               nodePort: 30009
         ---
         apiVersion: apps/v1
         kind: Deployment
         metadata:
           name: prd-tomcat-deployment
           namespace: prd
         spec:
           replicas: 2
           selector:
             matchLabels:
               app: prd-tomcat
           template:
             metadata:
               labels:
                 app: prd-tomcat
             spec:
               containers:
               - name: prd-tomcat
                 image: 10.10.100.8:5000/tomcat-custom:prd
                 ports:
                 - containerPort: 8080
         ---
         apiVersion: v1
         kind: Service
         metadata:
           name: prd-tomcat-clusterip-service
           namespace: prd
         spec:
           type: ClusterIP
           selector:
             app: prd-tomcat
           ports:
           - protocol: TCP
             port: 8080
             targetPort: 8080


5. Ingress & Nginx Ingress Controller

  • Nginx Ingress Controller 설치

      k apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v1.6.4/deploy/static/provider/cloud/deploy.yaml

    이후 Master Node의 IP를 할당해준다.
    !!! 공인 IP를 할당받은 VM이기에 가능하므로 로컬에서 연습할 시 metallb를 추가해 진행할 것

      k patch svc -n ingress-nginx ingress-nginx-controller -p '{"spec": {"type": "LoadBalancer", "externalIPs":["110.165.18.225"]}}'

  • 각 namespace에 ingress 배포

      apiVersion: networking.k8s.io/v1
      kind: Ingress
      metadata:
        name: ingress-nginx
        namespace: dev
        annotations:
          nginx.ingress.kubernetes.io/rewrite-target: /
          kubernetes.io/ingress.class: nginx
      spec:
        ingressClassName: "nginx"
        rules:
        - http:
            paths:
            - path: /dev
              pathType: Prefix
              backend:
                service:
                  name: dev-nginx-clusterip-service
                  port:
                    number: 80
      ---
      apiVersion: networking.k8s.io/v1
      kind: Ingress
      metadata:
        name: ingress-nginx
        namespace: stg
        annotations:
          nginx.ingress.kubernetes.io/rewrite-target: /
          kubernetes.io/ingress.class: nginx
      spec:
        ingressClassName: "nginx"
        rules:
        - http:
            paths:
            - path: /stg
              pathType: Prefix
              backend:
                service:
                  name: stg-nginx-clusterip-service
                  port:
                    number: 80
      ---
      apiVersion: networking.k8s.io/v1
      kind: Ingress
      metadata:
        name: ingress-nginx
        namespace: prd
        annotations:
          nginx.ingress.kubernetes.io/rewrite-target: /
          kubernetes.io/ingress.class: nginx
      spec:
        ingressClassName: "nginx"
        rules:
        - http:
            paths:
            - path: /prd
              pathType: Prefix
              backend:
                service:
                  name: prd-nginx-clusterip-service
                  port:
                    number: 80


ETC

Cgroup Driver

리눅스 컨트롤 그룹(cgroups) 관리 방식 결정
컨테이너 리소스 제한 및 모니터링 기능 제공

systemd의 특징:

  • 시스템 및 서비스 매니저로 초기화, 관리, 종료 담당
  • 단위(unit) 파일로 서비스 관리
  • 병렬 처리로 부팅 시간 최적화
  • journal을 사용한 통합 로깅 시스템
  • cgroups를 통한 프로세스 및 리소스 관리

cgroupfs의 특징:

  • 리소스 제한 및 모니터링을 직접적으로 파일 시스템 인터페이스로 관리
  • 계층적 그룹 관리로 각 그룹에 리소스 제한 적용 가능
  • 프로세스 그룹화로 시스템 관리 효율성 증가
  • 다양한 리소스 관리 컨트롤러 제공

systemd vs cgroupfs:

  • 목적과 범위: systemd는 시스템 전체의 서비스 생명주기 및 리소스 관리에 초점. cgroupfs는 리소스 제한과 관리에 특화
  • 통합과 독립성: systemd는 cgroup 관리를 시스템의 다른 부분과 통합하여 관리. cgroupfs는 독립적으로 리소스 제어 가능
  • 인터페이스: systemd는 고수준의 서비스 관리 인터페이스 제공. cgroupfs는 저수준의 파일 시스템 인터페이스 사용

CRI (Container Runtime Interface)

컨테이너 런타임과 오케스트레이션 시스템 연결 표준 인터페이스
컨테이너 생성, 시작, 정지, 삭제 API 제공

CRI-O 특징:

  • 쿠버네티스 전용 경량 런타임
  • OCI 표준 준수
  • 네트워크, 스토리지 플러그인 지원
  • 컨테이너 격리 실행으로 보안 강화
  • 구조 간소화

containerd 특징:

  • 범용 컨테이너 런타임.
  • OCI 이미지, 런타임 규격 지원
  • 모듈형 설계
  • 확장성 있는 플러그인 구조
  • 강력한 커뮤니티 지원

CRI-O vs containerd 차이:

  • 목표: CRI-O는 쿠버네티스 전용, containerd는 다양한 플랫폼 지원
  • 플러그인: CRI-O는 쿠버네티스 최적화 플러그인, containerd는 범용 플러그인 구조
  • 사용 환경: CRI-O는 주로 쿠버네티스, containerd는 쿠버네티스 및 기타 도구 사용

CNI (Container Network Interface)

Kubernetes의 컨테이너 간 네트워킹 설정을 위한 Plugin Interface
네트워크 연결과 IP 주소 할당 등을 관리

Calico의 특징:

  • 고성능 데이터 플레인 제공
  • 확장 가능한 네트워크 정책
  • IPv4/IPv6 동시 지원

Cilium의 특징:

  • eBPF 기반으로 고성능 보장
  • 레이어 7 정책 실행 가능
  • 네트워크 보안에 초점

Flannel의 특징:

  • 간단한 설치 및 설정
  • 오버레이 네트워킹을 통한 트래픽 라우팅
  • VXLAN, UDP를 지원하는 네트워크 백엔드 제공

CNI들의 차이점:

  • Calico는 보안과 성능에 초점을 맞춘 네트워크 정책 기능이 강점.
  • Cilium은 최신 eBPF 기술을 활용해 보안과 성능을 강화한 CNI.
  • Flannel은 설치와 관리의 용이성에 초점을 맞춘, 기본적인 오버레이 네트워크 솔루션을 제공

Ingress Controller

쿠버네티스에서 외부 요청을 내부 서비스로 연결
규칙 기반 라우팅 제공

NGINX Ingress Controller 특징:

  • 리버스 프록시, 로드 밸런서로 사용되는 NGINX 사용
  • 설정 유연성과 성능에서 강점
  • 안정성 높고, 커스텀화 용이

ALB Ingress Controller 특징:

  • AWS의 Application Load Balancer 사용
  • 자동 스케일링 및 AWS 서비스와의 통합 용이
  • 비용 효율적으로 다수의 HTTP, HTTPS 트래픽 처리

차이점:

  • 구현 기반: NGINX는 오픈 소스 리버스 프록시, ALB는 AWS의 상업적 로드 밸런서
  • 플랫폼 통합: NGINX는 다양한 환경에 배포 가능, ALB는 AWS 환경에 최적화
  • 기능과 성능: NGINX는 커스텀 설정에 강점, ALB는 클라우드 특화 기능 및 자동화에 초점

flannel 설치 에러

error validating data: failed to download openapi: Get "https://10.10.100.6:6443/openapi/v2?timeout=32s": tls: failed to verify certificate: x509: certificate signed by unknown authority (possibly because of "crypto/rsa: verification error" while trying to verify candidate authority certificate "kubernetes"); if you choose to ignore these errors, turn validation off with --validate=false

보통 기존 kubeadm reset 이후 $HOME/.kube를 삭제하지 않아서 생기는 오류
막연하게 덮어쓰기 식으로 복붙 명령문 실행이 아닌

rm -rf $HOME/.kube

라는 명령어를 통해 삭제 후 진행해보자

profile
Just Practice

0개의 댓글