Please enable JavaScript to view the comments powered by Disqus.9장. 디플로이먼트: 선언적 애플리케이션 업데이트
Search
🎭

9장. 디플로이먼트: 선언적 애플리케이션 업데이트

이전 책에서는 deployment 에 대해서 정확히 다루지 못했는데 확실히 k8s in action이 좋은 책인것 같긴 하다 그간 했갈렸던 deployment 에 대한 내용이 확실히 정리가 되었다.
여기서 다룰 내용
파드를 최신 버젼으로 교체
관리되는 파드 update
deployment resource 로 파드의 선언적 update
rolling update 수행
잘못된 버젼의 roll out 자동 차단
이전 버젼으로 파드 되돌리기
어떻게 k8s 가 무중단 업데이트 프로세스로 전환하는데 도움을 주는지 설명한다.

pod 에서 실행중인 application 업데이트

pod 가 replication controller 또는 replica set 을 지원한다는 것을 이미 알고있다.
client 가 pod 에 서비스를 통해 접근하기도 하는데 k8s 에 실행되는 기본 app 은 다음과 같다
k8s 에 실행되는 application 기본 구성
여기서 pod 가 v1 에서 v2 로 업데이트 된다고 할 때 우리가 할 수 있는 방법은 다음과 같다.
기존 파드를 모두 삭제한 다음 새 파드를 시작한다
잠시동안 application 을 사용할 수 없다.
새로운 파드를 시작하고, 기동하면 기존 파드를 삭제한다. 새 파드를 모두 추가한 다음 한꺼번에 기존 파드를 삭제하거나 순차적으로 새 파드를 추가하고 기존 파드를 점진적으로 제거해 이 작업을 수행할 수 있다.
동시에 두 가지 버젼을 실행해야 한다.
새 버젼이 이전 버젼을 손상 시킬 수 있는 data schema 나 수정을 해서는 안된다
k8s 에서 이 두가지 update 방법을 어떻게 수행할까?

오래된 파드를 삭제하고 새 파드로 교체(수동)

새 버젼의 파드로 교체하기 위해 replication controller 를 사용하는 방법이다.
pod template 을 수정하여 업데이트할 수 있는데 새 인스턴스를 생성할 때 업데이트 된 pod template 을 참조하도록 한다
v1 pod set 을 관리하는 replication controller 가 있는 경우 v2 를 참조하도록 pod template을 수정한 다음 이전 pod 인스턴스를 삭제해 쉽게 교체할 수 있다.
label selector 와 일치하는 파드가 있는지 확인하고 없으면 새 instance 를 시작한다
updating pods by changing a ReplicationController's pod template and deleting old pods
이 방법은 이전 pod 가 삭제되고 새 pod 가 시작되는 동안 짧은 시간의 downtime 을 허용할 수 있어야 한다

새 파드 기동과 이전 파드 삭제

downtime 이 발생하지 않고 한번에 여러 version 의 application 이 실행되는 것을 지원하는 경우
프로세를 먼저 전환해 새 pod 를 모두 기동한 후 이전 pod 를 삭제할 수 있다.
단 잠시동안 두배의 pod 가 기동되므로 더 많은 h/w 리소스가 필요하다

한번에 이전 버젼에서 새 버젼으로 전환

blue-green 배포 라고 하는 방식이다.
이전 파드에서 새 파드로 서비스 전환
새 버젼의 파드를 불러오는 동안 서비스는 이전의 pod 를 바라보고 새 pod 가 모두 실행되면 label selector 를 변경하여 새 파드로 전환할 수 있도록 한다.
전환이 완료되면 이전 pod를 제거한다.

롤링 업데이트 수행

새 파드가 실행되고 모두 한꺼번에 삭제하는 방법이 아니다
이전 버젼은 천천히 scale down 한다
새로운 버젼을 천천히 scale up 한다
서비스의 pod selector 에 이전 pod, 새 pod 모두 포함하게 해 요청을 두 pod set 으로 보낼 수 있다.
A rolling update of pods using two ReplicationControllers
수동으로 rolling update 를 하기는 어렵다. 올바른 순서로 많은 명령어를 실행해야 하는데 k8s 에서는 하나의 명령으로 rolling update 를 수행할 수 있게 했고 다음절에서 다룬다

ReplicationController 로 자동 rolling update 수행

kubectl 을 사용하면 수동에 비해 훨씬 간단하게 배포를 할 수 있지만 이것도 오래된 방식이다
하지만, 이 방법을 다루지 않고는 다음 방법을 다룰 수 없기 때문에 먼저 살펴본다

애플리케이션의 초기 버전 실행

const http = require('http'); const os = require('os'); console.log("Kubia server starting..."); var handler = function(request, response) { console.log("Received request from " + request.connection.remoteAddress); response.writeHead(200); response.end("This is v1 running in pod " + os.hostname() + "\n"); }; var www = http.createServer(handler); www.listen(8080);
JavaScript
복사
v1 버젼 app.js
HTTP 응답에서 버전 번호를 반환하도록 만들었다.
docker -p 8080:8080 yevgnenll/app-v1
JavaScript
복사

단일 yaml 파일을 사용한 application 실행과 서비스 노출

apiVersion: v1 kind: ReplicationController metadata: name: kubia-v1 spec: replicas: 3 template: metadata: name: kubia labels: app: kubia spec: containers: - image: yevgnenll/app-v1 name: nodejs --- apiVersion: v1 kind: Service metadata: name: kubia spec: type: LoadBalancer selector: app: kubia ports: - port: 80 targetPort: 8080
YAML
복사
kubectl create -f kubia-rc-and-service-v1.yaml
YAML
복사

kubectl 을 이용한 rolling update

동일한 이미지 태그로 업데이트 푸시하기 별로 좋은 생각은 아니지만 필요한 경우가 있다. 워커 노드에서 일단 이미지를 한 번 가져오면 이미지는 노드에 저장되고 동일한 이미지를 사용해 새 파드를 실행할 때 이미지를 다시 가져오지 않는다 (이미지를 가져오는 기본 정책) 즉, 변경한 내용을 같은 이미지 tag 로 푸시하면 이미지가 변경되지 않는다. 만약 같은 node 라면 이전 버전을 실행하고 또 다른 node 에서 파드를 실행하면 새로운 버젼이 되어 서로 다른 버젼이 서비스 될 수 있다. 이런일이 발생하지 않길 원한다면 imagePullPolicy 속성을 Always 로 설정해야 한다. 하지만 이미지를 변경할 때 마다 새로운 태그를 사용하는 것을 권장한다
v2 를 만들어 응답을 아래와 같이 수정한다
response.end("This is v2 running in pod " + os.hostname() + "\n");
JavaScript
복사
curl 요청을 계속 실행하게 하고 다른 터미널을 통해서 rolling update 를 실행해보자
while true; do curl 127.0.0.1:61440; done
JavaScript
복사
책에는 kubectl rolling-update 를 사용하라고 나오지만 입력해보면 다음과 같이 나온다
책에는 rolling update가 있지만, k8s 공식문서는 rolling update 를 원한다면 replica controller 가 아닌 deployment 를 사용할 것을 권장한다
따라서 예제 내용은 설명으로 갈음하고 다음으로 넘어간다

롤링 업데이트 시작전 kubectl 이 수행한 단계

pod 에서 app=kubia 레이블 외에 deployment 레이블이 있더라도 첫 번째 RC roller 의 셀렉터가 app=kubia로 설정돼 있기 때문에 새 컨트롤러의 파드들이 선택될 수 있다.
selector: app=kubia rolling-update 를 통해 확인하면 deployment=757d16a0f02f6a5c387f2b5edb62b155 도 함께 있다.
rolling-update 명령이 시작되면 실제 롤링 업데이트를 하기전 실행중인 pod 에 deployment=757d16a0f02f6a5c387f2b5edb62b155 레이블을 추가하도록 수정한다

레플리케이션컨트롤러 두 개를 스케일업해 새 파드로 교체

이 모든것을 설정한 다음 새로운 컨트롤러를 scale up 하여 파드를 하나씩 교체한다
하나씩 kubia-v2 를 scale up 하고 또, 하나씩 kubia-v1 을 scale down 한다
롤링 업데이트를 지속하면 v2 파드에 대한 요청 비율이 점점 높아지기 시젝한다

kubectl rolling-update 를 더 이상 사용하지 않는 이유

앞에서 이야기 했든 kubectl 은 현재 rolling-update 를 deprecated 된 정도가 아니라 아예 없어졌다.
이렇게 된 이유는
언급했던 pod를 변경하는 부분에 있다. 최초 개발자가 의도한것이 아닌 selector 가 붙는 것이 문제 된다.
kubectl 은 client 인데 client 가 API 를 호출하여 scale up, scale down 을 수행한다
client 가 이러한 작업을 요청하다 network 가 끊어지면 어떻게 될까?
rolling-update 중간 상태에 머물며 배포 작업이 종료된다
이 책은 시스템의 상태를 선언하고 k8s 가 그것을 달성할 수 있는 가장 좋은 방법을 찾아냄으로써 스스로 그 상태를 달성하도록 하는지에 대해 강조했다. 필요한 명령어를 입력하여 맞추기보다 k8s 가 알아서 그것을 해야한다

애플리케이션을 선언적으로 업데이트하기 위한 deployment 사용하기

deployment 는 애플리케이션을 배포하고 선언적(declarative) 으로 업데이트하기 위한 high-level 의 resource 이다.
deployment 를 생성하면 replica set 리소스가 그 아래 생성된다.
deployment 를 사용하면 pod 는 deployment 가 아닌, deployment 의 replica set 에 관리된다
application 업데이트에 추가 rc 를 도입하고 두 controller 가 잘 조화되도록 관리하기 위해 replica set 위에 또다른 resource 를 두었다.
하나의 deployment를 통해 원하는 상태를 정의하고 k8s 가 나머지를 처리하도록 하는데 목적이 있다

디플로이먼트 생성

레이블 셀렉터, 원하는 replica 수, pod template 으로 구성되어 있고, 배포 전략을 지정하는 필드도 있다.

Creating a deployment manifest

apiVersion: apps/v1beta1 kind: Deployment metadata: name: kubia # deployment 이름에 버전을 포함할 필요가 없다. spec: template: metadata: name: kubia labels: app: kubia spec: containers: - image: luksa/kubia:v2 name: nodejs
YAML
복사
이전에는 rc 가 특정 버전의 pod 를 관리했기 때문에 kubia-v1 으로 했으나, deployment 자체가 여러 버전을 실행할 수 있기 때문에 버전을 참조하진 않는다
먼저 모든 rc를 제거하자
kubectl delete rc --all
YAML
복사
deployment 를 만든다
kubectl create -f kubia-deployment-v1.yaml --record
YAML
복사
--record 옵션을 사용하면 개정 이력에 명령어를 기록해두어 나중에 유용하게 사용할 수 있다.
만들어진 deployment 상태를 확인하기 위해 다음의 명령어가 있다
kubectl get deployment kubectl describe deployment
YAML
복사
하지만 deployment 를 확인하는 특별한 명령어를 사용하자
kubectl rollout status deployment kubia
YAML
복사
NAME READY STATUS RESTARTS AGE kubia-74967b5695-2w97l 1/1 Running 0 2m25s kubia-74967b5695-dgbls 1/1 Running 0 2m25s kubia-74967b5695-fh94t 1/1 Running 0 2m25s
YAML
복사
이 가운데 번호는 다음의 rs 가 관리하는 번호와 일치한다
deployment 가 직접 pod 를 관리하는 것이 아니다 rs 를 두고 그것이 pod 를 관리하게 만든다
replica set, pod 에 74967b5695 라는 이름이 포함되어 있다. deployment 는 이 이름이 들어간 것들을 관리하는 것이다.

서비스로 파드 액세스

replica set 3개가 구동중이고 새 파드의 label 이 서비스의 selector 와 일치하게 되어 이전에 생성한 서비스를 사용해 액세스 할 수 있다.
아직은 왜 굳이 deployment 를 사용하는 것인지 이해가 되지 않을 것이다.

디플로이먼트 액세스

이전 방법은 rolluing-update 를 사용해 작업이 끝나기를 CLI 앞에서 기다려야 했고 rc 를 대체하는 새 rc 의 이름을 지정하는 작업을 해야했다(kubia-v1 → kubia-v2)
이제는 deployment template 만 수정하면 k8s 가 실제 시스템 상태를 resource 에 정의된 상태로 만드는 데 필요한 모든 단계를 수행한다.
deployment pod 템플릿에서 새 이미지 태그를 참조해 시스템이 원하는 상태가 되도록 k8s 에 맡긴다

사용 가능한 deployment 전략

RollingUpdate
이전 파드를 하나씩 제거 새로운 버전 파드를 하나씩 생성
두 버전이 동시에 동작할 수 있을 때 사용한다
Recreate
이전 파드를 모두 삭제
여러 버전을 벙렬로 실행하는 것을 지원하지 않고 새 버전을 시작하기 전에 이전 버전을 완전히 중지하는 경우

데모 목적으로 롤링 업데이트 속도 느리게 하기

kubectl patch deployment kubia -p '{"spec": {"minReadySeconds": 10}}'
YAML
복사
minReadySeconds 로 속성을 추가하고, 10초로 할당한다
kubectl patch 명령어는 텍스트 편집기에서 정의하지 않고도 resource 한두개 정도 수정할 때 유용하다
pod template 을 변경한 것이 아니기 때문에 pod 가 업데이트 되지 않는다.
의도하는 replica 수 또는 deployment ** 같은 다른 배포 속성을 변경해도 rollout 이 시작되지 않는다

롤링 업데이트 시작

while true; do curl 127.0.0.1:62978; done
YAML
복사
템플릿을 수정하는 대신 kubectl set image 명령어를 사용해 container 가 포함된 모든 리소르를 수정한다
kubectl set image deployment kubia nodejs=yevgnenll/app-v2
YAML
복사
v1, v2 가 함께 나온다
최종적으로 v2 만 나온다
이 명령을 사용하면 kubia deployment 파드 템플릿이 nodejs 컨테이너 사용된 이미지가
yevgnenll/app-v1 → yevgnenll/app-v2 로 변경된다
새 이미지를 사용하도록 deployment pod template 업데이트
파드 템플릿 수정 방법

디플로이먼트의 놀라움

파드의 template 을 변경하는 것 만으로 애플리케이션을 최신 버전으로 업데이트 했다.
client 가 process 를 수행하지 않는다
무엇을 해야하는지 k8s 알려주는 특별한 명령어를 입력하고 그것을 기다리는 방식이 아니다
수행되는 프로세스는 앞서 설명한 rolling-update 와 동일하다
kubectl get rs
YAML
복사
rolling update 와 차이는 kubia-74967b5695 가 삭제되지 않았다는 점이다.
하지만 우리가 한 작업이 아니기 때문에 신경 쓰지 않아도 된다

디플로이먼트 롤백

v3을 준비하자
const http = require('http'); const os = require('os'); var requestCount = 0; console.log("Kubia server starting..."); var handler = function(request, response) { console.log("Received request from " + request.connection.remoteAddress); if (++requestCount >= 5) { response.writeHead(500); response.end("Some internal error has occurred! This is pod " + os.hostname() + "\n"); return; } response.writeHead(200); response.end("This is v3 running in pod " + os.hostname() + "\n"); }; var www = http.createServer(handler); www.listen(8080);
JavaScript
복사
최초 5개의 요청만 올바르게 반환하고 그 다음부터는 500 에러를 반환한다

버전3 배포하기

kubectl set image deployment kubia nodejs=yevgnenll/app-v3 --record
JavaScript
복사
명령어를 입력하여 rolling update 를 실행한다
배포하며 에러 메세지가 반환된 것을 확인할 수 있다.

rollout 되돌리기

while true; do curl 127.0.0.1:62978; done
YAML
복사
response 를 지켜보자
9.3.6 절에서 잘못된 rollout 을 차단하는 방법을 다룰 것 이다
현재는 잘못된 rollout 을 수동으로 할 수 있는 방법을 살펴본다
kubectl rollout undo deployment kubia
JavaScript
복사
이렇게 하면 deployment 가 이전 버젼으로 롤백된다.
v3 버전에서 v2 로 롤백되고 있다
롤백이 완료 되었다.

디플로이먼트 롤아웃 이력 표시

kubectl rollout history deployment kubia
JavaScript
복사
deployment.apps/kubia REVISION CHANGE-CAUSE 1 kubectl create --filename=kubia-deployment-v1.yaml --record=true 10 kubectl create --filename=kubia-deployment-v1.yaml --record=true 14 kubectl set image deployment kubia nodejs=yevgnenll/app-v3 --record=true 15 kubectl create --filename=kubia-deployment-v1.yaml --record=true
Plain Text
복사
--record 에 의해 발생된 결과이다
그런데 kubectl rollout undo deployment kubia 여기에선 unknown flag 라고 나온다

특정 deployment revision 으로 롤백

kubectl rollout undo deployment kubia --to-revision=1
Plain Text
복사
특정 revision(개정) 으로 롤백시킬 수 있다
앞에서 비활성화 된 rs 이 남아 있던것을 기억하는가?
NAME DESIRED CURRENT READY AGE kubia-59d789f644 0 0 0 31m kubia-74967b5695 3 3 3 50m kubia-7797556d5f 0 0 0 20m kubia-7bddb8bfc7 0 0 0 17m
Plain Text
복사
그 남아 있던 rs 은 deployment 의 개정(revision)을 나타낸다
삭제 되지 않고 남아 있던 것으로 rollback 이 되는 것으로 수동 삭제는 안된다.
그렇게 하면 deployment 기록에서 특정 버전을 잃어 롤백 할 수 없게 된다.
하지만 editionHistoryLimit 속성을 이용해 제한된 개수만 갖도록 하는 것이 좋다
기본값은 2로 설정돼 있으므로 일반적으로 현재와 이전 버전만 기록에 표시된다.

롤아웃 속도 제어

v3 의 rollout 을 수행하고 kubectl rollout status 명령어를 사용해 진행 상황을 추적했을 때 먼저 새 파드가 생성되고, 그 다음에 old replicas 가 termination 되는 것을 확인하였다
이것을 오래된 파드가 남지 않을 때까지 계속된다.
롤링 업데이트 전략의 두 가지 추가 속성을 통해 새 파드를 만들고 기존 파드를 삭제하는 방법을 구성할 수 있다

롤링 업데이트 전략의 maxSurge 와 maxUnavailable 속성 소개

maxSurgemaxUnavaliable 을 사용해 strategy 속성의 rollingUopdate 속성 아래의 일부로 설정할 수 있다.
spec: strategy: rollingUpdate: maxSurge: 1 # surge: 급등하다 maxUnavailable: 0 type: RollingUpdate
YAML
복사
maxSurge, maxUnavailable 사용 사례
maxSurge
의도하는 replica 수 보다 얼마나 많은 pod instance 를 허용하는지 결정한다
default 25%
replica 가 4 인경우 5개 이상의 파드가 실행되지 않는다
백분율이 아니라 숫자로 변환하면 숫자가 반올림된다.(하나 또는 두개 파드 허용)
maxUnavailable
update 중 의도하는 replica 수를 기준으로 사용할 수 없는 pod instance 수를 결정한다
default 25%
사용 가능한 pod instance 의 수가 75% 이하로 떨어지지 않아야 한다
4 이면 1개의 파드만 사용할 수 없는 것이다
의도하는 replica 수가 3이고, 이러한 모든 속성이 default(25%) 라면 maxSurge 는 4까지 허용되고, maxUnavailable 은 사용할 수 없는 파드를 허용하지 않는다
레플리카 세 개와 기본 maxSurge 와 maxUnavailable이 있는 deployment update
extension/v1beta1 에 관한 설명은 생략한다.

Rollout process 일시 중지

v3 에서 버그를 경험한 후 v4 파드 하나를 실행하고 일부 사용자만 작동하는지 확인한다
모든것이 정상이면 기존 파드를 새 파드로 교체한다
만약 정상이 아니면 원복한다
그러려면 일시 정지가 필요하다

롤아웃 일시 정지

kubectl set image deployment kubia nodejs=yevgnenll/app-v4 --record kubectl rollout pause deployment kubia
YAML
복사
pause 명령어를 통해서 멈출 수 있다
새 파드를 생성했지만, 원본 파드도 계속 실행중이어야 한다
이렇게 하면 canary release 를 효과적으로 실행할 수 있다. 소수의 사용자만 초기 버전을 경험하는 것이다.

롤아웃 재개

확인이 완료된 후 새 버전이 제대로 작동한다고 확신하면 deployment를 다시 시작해 이전 파드를 모두 새 파드로 교체할 수 있다.
kubectl rollout resume deployment kubia
YAML
복사
이전에 일시정지된 것이 풀리며 모든 파드가 새로운 파드로 대체되기 시작한다
조만간 새로운 업그레이드 전략이 자동으로 이를 수행할 수 도 있지만 카나리 릴리즈를 수행하는 방법은 두가지 다른 deployment 를 사용해 적절하게 확장하는 것이다.

롤아웃을 방지하기 위한 일시 중지 기능 사용

디플로이먼트 일시 중지 기능을 사용하면 롤 아웃 프로세스가 시작되고
deployment 업데이트를 막을 수 있고, 여러번 변경하면 필요한 모든 변경을 완료한 후에 rollout 을 시작할 수도 있다.

잘못된 버전의 롤아웃 방지

앞에서 사용한 minReadySeconds 는 단순히 배포를 늦추는 것만이 아니라 오작동 버전의 배포를 방지한다

minReadySeconds 의 적용 가능성 이해

minReadySeconds 는 파드를 사용한 가능한 것으로 취급하기 전, 새 파드를 준비할 시간을 지정한다
(maxUnAvailable 속성을 함께 이용한다) 모든 파드가 readiness probe 가 성공하면 파드로 준비되고
readiness probe 에 문제가 생기면 새 파드가 제대로 작동하지 않고 새 버전의 rollout 을 차단한다
실제로는 minReadySeconds 를 매우 높게 설정하여 트래픽을 받아본 후 반영하도록 한다
적절한 구성으로 readiness probe 를 활용하여 버그가 있는 v3를 배포하지 못하게 해보자

버전 v3가 완전히 rollout 되는 것을 방지하기 위한 readiness probe 정의

apiVersion: apps/v1 kind: Deployment metadata: name: kubia spec: selector: matchLabels: app: kubia replicas: 3 minReadySeconds: 10 strategy: rollingUpdate: maxSurge: 1 maxUnavailable: 0 # deployment가 파드를 하나씩 교체하도록 한다 type: RollingUpdate template: metadata: name: kubia labels: app: kubia spec: containers: - image: yevgnenll/app-v3 name: nodejs readinessProbe: periodSeconds: 1 # 매초마다 실행된 readiness probe 정의 httpGet: # readiness probe 는 컨테이너에 HTTP get 요청을 수행한다 path: / port: 8080
YAML
복사
책과는 조금 다르다
kubectl apply -f kubia-deployment-v3-with-readinesscheck.yaml
YAML
복사
확인해보면 하나의 파드가 새로 생성됐다고 표시돼 있으므로 해당 서비스가 호출돼야 한다
그럼에도 불구하고 v3로 붙질 않는다 pod가 없는건가?
kubectl get po
YAML
복사
pod는 있지만, 준비되지 않았다. 무슨 문제일까?

Readiness probe가 잘못된 버전으로 rollout 되는 것을 방지하는 법

v3은 5번째 요청 까지만 200을 반환하고, 그 이후 부터는 500을 반환하여 readiness probe 가 실패하게 된다. 결과적으로 pod 는 서비스에서 제외된다. client 가 제대로 작동하지 않는 pod 에 접속하지 않도록 한다
readiness probe 실패로 차단한다
그러나 process roll-out 은 status 명령어로 봤을때 멈춰있음을 확인할 수 있다.
사용 가능한 것으로 간주되려면 10초 이상 준비돼 있어야 한다
사용간으할 때까지 roll-out process 는 새 파드를 만들지 않으며 maxUnavalialbe 속성을 0으로 설정했기 때문에 원래 파드도 제거하지 않는다
배포가 중단된 것은 좋은 일이다.

롤 아웃 데드라인 설정

기본적으로 rollout 이 10분 동안 진행되지 않으면 실패한 것으로 간주된다
kubectl describe deploy kubia
Plain Text
복사
Conditions: Type Status Reason ---- ------ ------ Available True MinimumReplicasAvailable Progressing False ProgressDeadlineExceeded OldReplicaSets: kubia-8f8dbfb74 (3/3 replicas created) NewReplicaSet: kubia-db775fc6f (1/1 replicas created)
Plain Text
복사
ProgressDeadlineExceeded 조건이 표시된 것을 확인할 수 있는데 실패로 간주되는 시간은 deployment 스펙의 lineSeconds 속성으로 설정할 수 있다.

잘못된 롤아웃 중지

롤아웃이 계속 진행되지 않기 때문에 지금 해야할 일은 중단이다
kubectl rollout undo deployment kubia
Plain Text
복사