AEWS STUDY 3주차 - Amazon EKS Storage & Node

Hanryang·2023년 5월 13일
0

AEWS 스터디

목록 보기
3/7

가시다님께서 진행하신 AEWS라는 스터디를 진행하면서 작성하는 글입니다.

AEWS= AWS EKS Workshop Study

AEWS 스터디는 4월 23일 ~ 6월 4일동안7번 진행될 예정입니다.

PKOS 스터디에서 정말 좋은 경험을 했어서 다시 이렇게 참가하게 되었습니다.

가시다님께서 진행하시는 스터디에 관심있으신 분들은 Cloudnet@Blog에 들어가시면 자세한 정보를 확인하실 수 있습니다.

실습 환경 배포


1,2주차와는 다르게 EFS 생성이 될 수 있게 가시다님께서 Cloudformation템플릿을 변경해주셨습니다.

Cloudformation 배포 완료 후 EFS가 생성이 됐는지 AWS콘솔에서 보겠습니다.

잘 생성이 되었습니다.

이제 EFS를 마운트하고 마운트 되었느지 확인해보겠습니다.

# EFS 마운
mount -t nfs4 -o nfsvers=4.1,rsize=1048576,wsize=1048576,hard,timeo=600,retrans=2,noresvport fs-031920bded8982b4e.efs.ap-northeast-2.amazonaws.com:/ /mnt/myefs
**df -hT --type nfs4**
mount | grep nfs4
# 연결 확인
**echo "efs file test" > /mnt/myefs/memo.txt**
cat /mnt/myefs/memo.txt
**rm -f /mnt/myefs/memo.txt**


그리고 AWS LB/ExternalDNS, kube-ops-view 설치해보겠습니다.

# AWS LB Controller
helm repo add eks https://aws.github.io/eks-charts
helm repo update
helm install aws-load-balancer-controller eks/aws-load-balancer-controller -n kube-system --set clusterName=$CLUSTER_NAME \
  --set serviceAccount.create=false --set serviceAccount.name=aws-load-balancer-controller

# ExternalDNS
MyDomain=hanryang.link
MyDnzHostedZoneId=$(aws route53 list-hosted-zones-by-name --dns-name "${MyDomain}." --query "HostedZones[0].Id" --output text)
echo $MyDomain, $MyDnzHostedZoneId
curl -s -O https://raw.githubusercontent.com/gasida/PKOS/main/aews/externaldns.yaml
MyDomain=$MyDomain MyDnzHostedZoneId=$MyDnzHostedZoneId envsubst < externaldns.yaml | kubectl apply -f -

# kube-ops-view
helm repo add geek-cookbook https://geek-cookbook.github.io/charts/
helm install kube-ops-view geek-cookbook/kube-ops-view --version 1.2.2 --set env.TZ="Asia/Seoul" --namespace kube-system
kubectl patch svc -n kube-system kube-ops-view -p '{"spec":{"type":"LoadBalancer"}}'
kubectl **annotate** service kube-ops-view -n kube-system "external-dns.alpha.kubernetes.io/hostname=**kubeopsview**.$MyDomain"
echo -e "Kube Ops View URL = http://**kubeopsview**.$MyDomain:8080/#scale=1.5"

kube-ops-view는 클러스터의 상태를 시각적으로 볼 수 있는 간단한 페이지입니다. 모니터링 및 운영 관리의 목적으로 사용되진 않으나 Cluster Autoscaler와 같이 클러스터 오토스케일링 작업 시, 스케일 인/아웃의 과정을 시각적으로 관찰할 수 있습니다.

스토리지 이해


상태 저장 애플리케이션을 실행할 때, 영구 스토리지가 없을 경우, 데이터는 pod 또는 Container의 수명 주기에 연결됩니다. 그리고 Pod가 충돌하거나 종료되면 데이터는 손실됩니다.

그래서 k8s는 데이터 손실을 막기 위해서 3가지 간단한 스토리지 요구사항을 준수해야합니다.

  • 스토리지는 포드의 수명 주기에 의존 X
  • 스토리지는 Kubernetes 클러스터의 모든 포드 및 노드에서 사용할 수 있어야 합니다.
  • 스토리지는 충돌이나 애플리케이션 오류에 관계없이 가용성이 높아야 합니다.

Persistent Volumes


Persistent Volumes은 위에서 설명한 스토리지 요구사항 세 가지를 충족합니다.

  • Persistent Volumes을 사용하면 애플리케이션, 컨테이너, 포드, 노드 또는 클러스터 자체의 수명 주기와 관계없이 데이터가 지속됩니다.
  • Persistent Volume(PV) 객체는 애플리케이션 데이터를 유지하는 데 사용되는 스토리지 볼륨을 나타냅니다. PV는 Kubernetes Pods의 수명 주기와 별개로 자체의 수명 주기를 가집니다.

k8s는 다양한 유형의 pv를 지원합니다.

  • CSI(Container Storage Interface) ⇒ 예: Amazon EFS , Amazon EBS , Amazon FSx 등
  • ISCSI
  • local ⇒ 노드에 마운트된 로컬 저장 장치
  • nfs

PV는 3가지 접근 모드를 지원합니다.

  • ReadWriteOnce
    • 볼륨은 동시에 하나의 노드에서만 읽기/쓰기를 허용합니다.
  • ReadOnlyMany
    • 볼륨은 동시에 여러 노드에서 읽기 전용 모드를 허용합니다.
  • ReadWriteMany
    • 볼륨은 동시에 여러 노드에서 읽기/쓰기를 허용합니다.

Persistent volume claims


Kubernetes는 PV를 포드에 연결하는데 필요한 추가 추상화 계층인 PersistentVolumeClaim(PVC)을 가지고 있습니다.

PV 는 실제 스토리지 볼륨을 나타내며, PVC는 Pod가 실제 스토리지를 얻기 위해 수행하는 스토리지 요청

CSI(Container Storage Interface) 드라이버


CSI는 Kubernetes에서 다양한 스토리지 솔루션을 쉽게 사용할 수 있도록 설계된 추상화입니다.

AWS는 Amazon EBS , Amazon EFS 및 Amazon FSx for Lustre 용 CSI 플러그인을 제공하고 있습니다.

일반적인 CSI driver 구조는 아래와 같습니다.

  • 오른쪽 StatefulSet 또는 Deployment로 배포된 controller Pod이 AWS API를 사용하여 실제 EBS volume을 생성하는 역할을 합니다.
  • 왼쪽 DaemonSet으로 배포된 node Pod은 AWS API를 사용하여 Kubernetes node (EC2 instance)에 EBS volume을 attach 해줍니다.

AWS는 EC2 Type에 따라 노드에 연결할 수 있는 볼륨 수에 제한이 있습니다.

제가 현재 설치한 EC2 Type에는 25개 볼륨만 연결할 수 있는 것을 확인할 수 있습니다.

정적 프로비저닝


먼저 관리자가 하나 이상의 PV를 생성하고 애플리케이션 개발자는 PVC를 생성합니다. 이를 정적 프로비저닝이라고 합니다. Kubernetes에서 PV 및 PVC를 수동으로 만들어야 하므로 정적입니다.

이 정적 프로비저닝은 대규모 환경에서는 관리하기가 어렵습니다.

동적 프로비저닝


동적 프로비저닝을 사용하면 PV객체를 생성할 필요가 없습니다. 대신에, PVC를 생성할 때 내부적으로 자동으로 생성됩니다.

Kubernetes는 Storage Class라는 다른 객체를 사용하여 이를 수행합니다.

  • Storage Class
    • 컨테이너 애플리케이션에 사용되는 백엔드 영구 스토리지(예: Amazon EFS 파일 스토리지, Amazon EBS 블록 스토리지 등)의 클래스를 정의하는 추상화입니다.

AWS에서는 아래와 같이 사용하실 수 있습니다.

스토리지 간단 실습

임시 파일 시스템


우선 기본 컨테이너 환경의 임시 파일 시스템을 사용하여 데이터가 어떻게 되는지에 대해서 확인해 보겠습니다.

date-busybox-pod.yaml

apiVersion: v1
kind: Pod
metadata:
  name: busybox
spec:
  terminationGracePeriodSeconds: 3
  containers:
  - name: busybox
    image: busybox
    command:
    - "/bin/sh"
    - "-c"
    - "while true; do date >> /home/pod-out.txt; cd /home; sync; sync; sleep 10; done"

배포를 한 후에 파일을 확인해 보겠습니다.

해당 기록이 pod를 재생성했을 때에도 남아있는지 확인해보겠습니다.

**kubectl delete pod busybox**
kubectl apply -f date-busybox-pod.yaml
**kubectl exec busybox -- tail -f /home/pod-out.txt**

임시 파일 시스템을 사용하면 pod가 삭제될 때 같이 데이터도 삭제된다는 것을 확인하실 수 있습니다.

호스트 Path 를 사용하는 PV/PVC


  1. local-path-provisioner 스트리지 클래스 배포
# 배포
**curl -s -O https://raw.githubusercontent.com/rancher/local-path-provisioner/master/deploy/local-path-storage.yaml**
~~~~**kubectl apply -f local-path-storage.yaml**

# 확인
kubectl **get-all** -n local-path-storage

  1. PVC 생성

    # pvc yaml
    apiVersion: v1
    kind: PersistentVolumeClaim
    metadata:
      name: localpath-claim
    spec:
      accessModes:
        - ReadWriteOnce
      resources:
        requests:
          storage: 1Gi
      storageClassName: "local-path"

    pvc만 생성하고 아직 pod를 pvc와 연결하지 않았기에 pending 상태인 것을 describe로 확인할 수 있습니다.

  2. pod 생성

    # 배포할 pod
    apiVersion: v1
    kind: Pod
    metadata:
      name: app
    spec:
      terminationGracePeriodSeconds: 3
      containers:
      - name: app
        image: centos
        command: ["/bin/sh"]
        args: ["-c", "while true; do echo $(date -u) >> /data/out.txt; sleep 5; done"]
        volumeMounts:
        - name: persistent-storage
          mountPath: /data
      volumes:
      - name: persistent-storage
        persistentVolumeClaim:
          claimName: localpath-claim

    pod 를 생성한 후 pvc가 정상으로 bound 된 것을 확인 할 수 있습니다.

    pod가 데이터를 잘 기록하고 있는지 확인해보겠습니다.

    pod는 node 3에 배포가 되어있으므로 node3에 out.txt라는 파일이 존재하는지도 확인해보겠습니다.

    pod를 삭제 후 재생성을 해도 데이터가 유지되는 지 확인해보겠습니다.

    이렇게 PV/PVC를 사용하면 pod를 삭제해도 데이터가 유지된다는 것을 확인할 수 있었습니다.

AWS EBS Controller


AWS EBS Controller드라이버에서는Amazon EKS 클러스터가 영구 볼륨을 위해 Amazon EBS 볼륨의 수명 주기를 관리할 수 있게 합니다.

⇒ CSI-Controller와 CSI-Node는 pod로 실행됩니다.

  • 왜 EBS pv와 pvc의 accessModes는 ReadWriteOnce로 설정할까? ⇒ EBS 스토리지 기본 설정이 동일 AZ에 있는 EC2 인스턴스만 사용이 가능
    • ReadWriteOnce
      • 볼륨은 동시에 하나의 노드에서만 읽기/쓰기를 허용합니다.

AWS EBS Controller 설치


  1. EBS CSI드라이버가 AWS API를 사용해야 하므로 CSI-controller pod ISRA 설정,
    EBS CSI driver addon 추가

    ```yaml
    # ISRA 설정 : AWS관리형 정책 AmazonEBSCSIDriverPolicy 사용
    eksctl create **iamserviceaccount** \
      --name **ebs-csi-controller-sa** \
      --namespace kube-system \
      --cluster ${CLUSTER_NAME} \
      --attach-policy-arn arn:aws:iam::aws:policy/service-role/**AmazonEBSCSIDriverPolicy** \
      --approve \
      --role-only \
      --role-name **AmazonEKS_EBS_CSI_DriverRole**
    
    # Amazon EBS CSI driver addon 추가
    eksctl create **addon** --name aws-ebs-csi-driver --cluster ${CLUSTER_NAME} --service-account-role-arn arn:aws:iam::${ACCOUNT_ID}:role/**AmazonEKS_EBS_CSI_DriverRole** --force
    
    ```

  2. ebs-csi-controller 파드에 6개 컨테이너가 생성되었는지 확인

  3. CSI-Controller와 CSI-Node pod가 생성 확인

AWS EBS Controller 실습


  1. gp3 스토리지 클래스 생성

    kind: **StorageClass**
    apiVersion: storage.k8s.io/v1
    metadata:
      name: gp3
    **allowVolumeExpansion: true**
    **provisioner: ebs.csi.aws.com**
    volumeBindingMode: WaitForFirstConsumer
    parameters:
      **type: gp3**
      allowAutoIOPSPerGBIncrease: 'true'
      encrypted: 'true'

  2. pvc 생성

    # awsebs-pvc**.yaml**
    apiVersion: v1
    kind: **PersistentVolumeClaim**
    metadata:
      name: **ebs-claim**
    spec:
      accessModes:
        - **ReadWriteOnce**
      resources:
        requests:
          **storage: 4Gi**
      **storageClassName: gp3**

  3. pvc와 연결할 파드 생성

    # awsebs-pod**.yaml**
    apiVersion: v1
    kind: Pod
    metadata:
      name: **app**
    spec:
      terminationGracePeriodSeconds: 3
      containers:
      - name: app
        image: centos
        command: ["/bin/sh"]
        args: ["-c", "while true; do echo **\**$(date -u) >> /data/out.txt; sleep 5; done"]
        volumeMounts:
        - name: persistent-storage
          mountPath: /data
      volumes:
      - name: persistent-storage
        **persistentVolumeClaim**:
          claimName: **ebs-claim**

  4. 데이터 저장 확인

  5. 볼륨 증가

    • 현재 pvc 볼륨 크기 4G
    • 볼륨 크기 4 → 10G
      
      kubectl get **pvc** ebs-claim -o jsonpath={.spec.resources.requests.storage} ; echo
      kubectl get **pvc** ebs-claim -o jsonpath={.status.capacity.storage} ; echo
      **kubectl patch pvc ebs-claim -p '{"spec":{"resources":{"requests":{"storage":"10Gi"}}}}'**
  6. 삭제

    kubectl delete pod app & kubectl delete pvc ebs-claim

AWS Volume SnapShots Controller


VolumeSnapshot은 스토리지 시스템에 있는 볼륨의 스냅샷을 나타냅니다.

  • Kubernetes 볼륨 스냅샷을 사용하면 특정 시점에 Amazon EBS 볼륨의 복사본을 생성할 수 있습니다.
  • 이 사본을 사용하여 볼륨을 이전 상태로 되돌리거나 새 볼륨을 프로비저닝할 수 있습니다.
  • Kubernetes 버전 1.17 이상부터 Amazon EBS 볼륨 스냅샷을 프로비저닝하고 pod에 연결할 수 있습니다.

Volumesnapshots 컨트롤러 설치를 해보겠습니다.

# Install Snapshot CRDs
curl -s -O https://raw.githubusercontent.com/kubernetes-csi/external-snapshotter/master/client/config/crd/snapshot.storage.k8s.io_volumesnapshots.yaml
curl -s -O https://raw.githubusercontent.com/kubernetes-csi/external-snapshotter/master/client/config/crd/snapshot.storage.k8s.io_volumesnapshotclasses.yaml
curl -s -O https://raw.githubusercontent.com/kubernetes-csi/external-snapshotter/master/client/config/crd/snapshot.storage.k8s.io_volumesnapshotcontents.yaml
kubectl apply -f snapshot.storage.k8s.io_volumesnapshots.yaml,snapshot.storage.k8s.io_volumesnapshotclasses.yaml,snapshot.storage.k8s.io_volumesnapshotcontents.yaml
kubectl get crd | grep snapshot
kubectl api-resources  | grep snapshot

# Install Common Snapshot Controller
curl -s -O https://raw.githubusercontent.com/kubernetes-csi/external-snapshotter/master/deploy/kubernetes/snapshot-controller/rbac-snapshot-controller.yaml
curl -s -O https://raw.githubusercontent.com/kubernetes-csi/external-snapshotter/master/deploy/kubernetes/snapshot-controller/setup-snapshot-controller.yaml
kubectl apply -f rbac-snapshot-controller.yaml,setup-snapshot-controller.yaml
kubectl get deploy -n kube-system snapshot-controller
kubectl get pod -n kube-system -l app=snapshot-controller

# Install Snapshotclass
curl -s -O https://raw.githubusercontent.com/kubernetes-sigs/aws-ebs-csi-driver/master/examples/kubernetes/snapshot/manifests/classes/snapshotclass.yaml
kubectl apply -f snapshotclass.yaml
kubectl get vsclass # 혹은 volumesnapshotclasses

스냅샵 실습


  • 스냅샷을 테스트 하기 위해서 pvc와 pod를 생성해보겠습니다.
    # PVC 생성
    **kubectl apply -f** awsebs-pvc**.yaml**
    
    # 파드 생성
    **kubectl apply -f** awsebs-pod**.yaml**
    
    # 파일 내용 추가 저장 확인
    kubectl exec app -- tail -f /data/out.txt
  • 스냅샷을 생성해보겠습니다.
    # ebs-volume-snapshot.yaml
    
    apiVersion: snapshot.storage.k8s.io/v1
    kind: VolumeSnapshot
    metadata:
      name: ebs-volume-snapshot
    spec:
      volumeSnapshotClassName: csi-aws-vsc
      source:
        persistentVolumeClaimName: ebs-claim
    
    # VolumeSnapshot 생성
    **kubectl apply -f ebs-volume-snapshot.yaml**
  • 스냅샷이 생성되었는지 확인해보겠습니다.
  • 스냅샷 기능을 테스트 하기 위해서 pod와 pvc를 제거하겠습니다.

    # app & pvc 제거 : 강제로 장애 재현
    **kubectl delete pod app && kubectl delete pvc ebs-claim**
  • pvc를 스냅샷으로 복원해보겠습니다.

    # ebs-snapshot-restored-claim.yaml
    apiVersion: v1
    kind: PersistentVolumeClaim
    metadata:
      name: ebs-snapshot-restored-claim
    spec:
      **storageClassName: gp3**
      accessModes:
        - ReadWriteOnce
      resources:
        requests:
          storage: 4Gi
      **dataSource:**
        name: **ebs-volume-snapshot**
        kind: **VolumeSnapshot**
        apiGroup: snapshot.storage.k8s.io
    
    # pvc 생성
    **kubectl apply -f ebs-snapshot-restored-claim.yaml**
  • pvc와 연결하기 위한 pod또한 생성하겠습니다.

    # **ebs-snapshot-restored-pod.yaml**
    
    apiVersion: v1
    kind: Pod
    metadata:
      name: app
    spec:
      containers:
      - name: app
        image: centos
        command: ["/bin/sh"]
        args: ["-c", "while true; do echo $(date -u) >> /data/out.txt; sleep 5; done"]
        volumeMounts:
        - name: persistent-storage
          mountPath: /data
      volumes:
      - name: persistent-storage
        persistentVolumeClaim:
          claimName: ebs-snapshot-restored-claim
  • 파일 내용을 확인해보겠습니다.

    • 그대로 이전 내용까지 살아있는 것을 확인할 수 있습니다.

AWS EFS Controller


AWS EFS Controller는 AWS에서 실행되는 Kubernetes 클러스터가 Amazon EFS 파일 시스템의 수명 주기를 관리할 수 있게 해주는 CSI 인터페이스를 제공합니다.

AWS EFS Controller를 설치해보겠습니다.

Amazon EFS CSI 드라이버를 Amazon EKS 클러스터에 배포하려면 사용자를 대신하여 CSI 드라이버 서비스 계정에서 AWS API를 호출할 수 있도록 하는 IAM 정책을 만들어야 합니다. 그리고 IRSA를 설정합니다.

# iam 정책 생성

curl -s -O https://raw.githubusercontent.com/kubernetes-sigs/aws-efs-csi-driver/master/docs/**iam-policy-example.json**
aws iam create-policy --policy-name **AmazonEKS_EFS_CSI_Driver_Policy** --policy-document file://iam-policy-example.json

# iam 정책
{
    "Policy": {
        "PolicyName": "AmazonEKS_EFS_CSI_Driver_Policy",
        "PolicyId": "ANPA3BIZM37S3MLUQPB4K",
        "Arn": "arn:aws:iam::758651871205:policy/AmazonEKS_EFS_CSI_Driver_Policy",
        "Path": "/",
        "DefaultVersionId": "v1",
        "AttachmentCount": 0,
        "PermissionsBoundaryUsageCount": 0,
        "IsAttachable": true,
        "CreateDate": "2023-05-13T14:12:45+00:00",
        "UpdateDate": "2023-05-13T14:12:45+00:00"
    }

# ISRA 설정 
eksctl create **iamserviceaccount** \
  --name **efs-csi-controller-sa** \
  --namespace kube-system \
  --cluster ${CLUSTER_NAME} \
  --attach-policy-arn arn:aws:iam::${ACCOUNT_ID}:policy/AmazonEKS_EFS_CSI_Driver_Policy \
  --approve

모두 설정 완료한 후에 EFS Controller 설치를 진행합니다.

# EFS Controller 설치
helm repo add aws-efs-csi-driver https://kubernetes-sigs.github.io/aws-efs-csi-driver/
helm repo update
helm upgrade -i aws-efs-csi-driver aws-efs-csi-driver/aws-efs-csi-driver \
    --namespace kube-system \
    --set image.repository=602401143452.dkr.ecr.${AWS_DEFAULT_REGION}.amazonaws.com/eks/aws-efs-csi-driver \
    --set controller.serviceAccount.create=false \
    --set controller.serviceAccount.name=efs-csi-controller-sa

설치가 잘 되었는지 확인을 해보겠습니다.


실습


EFS 파일시스템을 다수의 파드가 사용하게 설정

  • 실습 코드를 clone하고 EFS 스토리지 클래스를 생성 및 확인하겠습니다.
    # 실습 코드 clone
    git clone https://github.com/kubernetes-sigs/aws-efs-csi-driver.git /root/efs-csi
    cd /root/efs-csi/examples/kubernetes/multiple_pods/specs && tree
    
    # EFS 스토리지클래스 생성 및 확인
    cat storageclass.yaml | yh
    kubectl apply -f storageclass.yaml
    kubectl get sc efs-sc
  • PV와 PVC를 생성하고 확인하겠습니다.
    # PV 생성 및 확인 : volumeHandle을 자신의 EFS 파일시스템ID로 변경
    **EfsFsId=**$(aws efs describe-file-systems --query "FileSystems[*].FileSystemId" --output text)
    sed -i "s/**fs-4af69aab**/**$EfsFsId**/g" pv.yaml
    
    **cat pv.yaml | yh**
    apiVersion: v1
    kind: PersistentVolume
    metadata:
      name: efs-pv
    spec:
      capacity:
        **storage: 5Gi**
      volumeMode: **Filesystem**
      accessModes:
        - **ReadWriteMany**
      persistentVolumeReclaimPolicy: Retain
      storageClassName: **efs-sc**
      csi:
        driver: efs.csi.aws.com
        volumeHandle: **fs-05699d3c12ef609e2**
    
    **kubectl apply -f pv.yaml**
    kubectl get pv; kubectl describe pv
    
    # PVC 생성 및 확인
    cat claim.yaml | yh
    apiVersion: v1
    kind: PersistentVolumeClaim
    metadata:
      name: efs-claim
    spec:
      accessModes:
        - ReadWriteMany
      storageClassName: efs-sc
      resources:
        requests:
          storage: 5Gi
    
    **kubectl apply -f claim.yaml**
    **kubectl get pvc**
  • 파드를 생성하고 연동을 확인해보겠습니다.
    cat pod1.yaml pod2.yaml | yh
    apiVersion: v1
    kind: Pod
    metadata:
      name: app1
    spec:
      containers:
      - name: app1
        image: busybox
        command: ["/bin/sh"]
        args: ["-c", "while true; do echo $(date -u) >> /data/out1.txt; sleep 5; done"]
        volumeMounts:
        - name: persistent-storage
          mountPath: /data
      volumes:
      - name: persistent-storage
        persistentVolumeClaim:
          claimName: efs-claim
    apiVersion: v1
    kind: Pod
    metadata:
      name: app2
    spec:
      containers:
      - name: app2
        image: busybox
        command: ["/bin/sh"]
        args: ["-c", "while true; do echo $(date -u) >> /data/out2.txt; sleep 5; done"]
        volumeMounts:
        - name: persistent-storage
          mountPath: /data
      volumes:
      - name: persistent-storage
        persistentVolumeClaim:
          claimName: efs-claim
    
    **kubectl apply -f pod1.yaml,pod2.yaml**

마무리


정말 여러번 반복하고 보면서 이해가 조금씩 되긴 했지만 아직도 쿠버네티스는 여전히 어렵다..

다시 복습하면서 도전과제들을 정리해봐야겠다..

profile
한량이 되고싶다..

0개의 댓글