01-[AEWS]-Amazon EKS 기본기

1. EKS(Elastic Kubernetes Service)

🔠EKS란?

AWS에서 제공하는 서비스로 정확한 명칭은 Elastic Kubernetes Service이다. On-premise에서 Kubernetes를 동작하는것과 다르게 AWS에서 클러스터 관리, 보안 업데이트, 자동 확장, 로드 밸런싱을 자동으로 할 수 있어서 사용자가 애플리케이션에 집중하고 Kubernetes 클러스터의 운영 부담을 최소화할 수 있는 장점이 있다.

EKS Components

  1. Control Plane (마스터 노드): AWS Managed
    • API Server: Kubernetes API 요청을 받아들이고 처리하는 중앙 제어 요소입니다.
    • Controller Manager: 클러스터의 상태를 감시하고 조정하는 컨트롤러를 실행합니다.
    • Scheduler: 새로운 파드가 어떤 워커 노드에서 실행될지 결정합니다.
    • etcd: 클러스터의 상태와 구성을 저장하는 분산 데이터 스토어입니다.
  2. Worker Nodes (워커 노드): User Managed
    • kubelet: 각 노드에서 실행되는 에이전트로, API 서버와 상호 작용하여 노드의 상태를 보고하고 파드를 실행합니다.
    • kube-proxy: 네트워크 프록시로, 서비스 및 파드의 네트워크 트래픽을 로드 밸런싱하고 관리합니다.
    • container runtime: 컨테이너를 실행하는 데 사용되는 런타임 환경입니다. Amazon EKS에서는 일반적으로 containerd가 사용됩니다.
  3. Networking:
    • Amazon VPC (Virtual Private Cloud): Amazon EKS 클러스터가 배포되는 가상 네트워크 환경입니다.
    • VPC CNI (Container Networking Interface): Kubernetes 클러스터 내의 파드 간 통신 및 인터넷에 대한 네트워크 구성을 제공합니다.
    • AWS Load Balancer Controller: 서비스를 외부에 노출하는 데 사용되는 로드 밸런서를 자동으로 구성하고 관리합니다.
  4. Storage:
    • Amazon EBS (Elastic Block Store): Kubernetes 볼륨에 사용되는 영구 스토리지를 제공합니다.
    • Amazon EFS (Elastic File System): 여러 파드에서 공유하는 공유 파일 시스템을 제공합니다.
  5. Authentication & Authorization:
    • AWS IAM (Identity and Access Management): 클러스터 및 리소스에 대한 인증 및 권한 부여를 처리합니다.
    • Kubernetes RBAC (Role-Based Access Control): Kubernetes 내의 리소스에 대한 엑세스를 관리합니다.

EKS Deploy

Host Server Deploy

배포 방법은 대표적으로 웹 콘솔, eksctl, IaC(CDK, CloudFormation, Terraform)등이 있으며 그 중에 템플릿을 이용한 CloudFormation 배포를 진행합니다.

myeks-1week.yaml

AWSTemplateFormatVersion: '2010-09-09'

Metadata:
  AWS::CloudFormation::Interface:
    ParameterGroups:
      - Label:
          default: "<<<<< EKSCTL MY EC2 >>>>>"
        Parameters:
          - ClusterBaseName
          - KeyName
          - SgIngressSshCidr
          - MyInstanceType
          - LatestAmiId
      - Label:
          default: "<<<<< Region AZ >>>>>"
        Parameters:
          - TargetRegion
          - AvailabilityZone1
          - AvailabilityZone2
      - Label:
          default: "<<<<< VPC Subnet >>>>>"
        Parameters:
          - VpcBlock
          - PublicSubnet1Block
          - PublicSubnet2Block
          - PrivateSubnet1Block
          - PrivateSubnet2Block

Parameters:
  ClusterBaseName:
    Type: String
    Default: myeks
    AllowedPattern: "[a-zA-Z][-a-zA-Z0-9]*"
    Description: must be a valid Allowed Pattern '[a-zA-Z][-a-zA-Z0-9]*'
    ConstraintDescription: ClusterBaseName - must be a valid Allowed Pattern

  KeyName:
    Description: Name of an existing EC2 KeyPair to enable SSH access to the instances. Linked to AWS Parameter
    Type: AWS::EC2::KeyPair::KeyName
    ConstraintDescription: must be the name of an existing EC2 KeyPair.

  SgIngressSshCidr:
    Description: The IP address range that can be used to communicate to the EC2 instances
    Type: String
    MinLength: '9'
    MaxLength: '18'
    Default: 0.0.0.0/0
    AllowedPattern: (\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})/(\d{1,2})
    ConstraintDescription: must be a valid IP CIDR range of the form x.x.x.x/x.

  MyInstanceType:
    Description: Enter t2.micro, t2.small, t2.medium, t3.micro, t3.small, t3.medium. Default is t2.micro.
    Type: String
    Default: t3.medium
    AllowedValues: 
      - t2.micro
      - t2.small
      - t2.medium
      - t3.micro
      - t3.small
      - t3.medium

  LatestAmiId:
    Description: (DO NOT CHANGE)
    Type: 'AWS::SSM::Parameter::Value<AWS::EC2::Image::Id>'
    Default: '/aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-gp2'
    AllowedValues:
      - /aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-gp2

  TargetRegion:
    Type: String
    Default: ap-northeast-2

  AvailabilityZone1:
    Type: String
    Default: ap-northeast-2a

  AvailabilityZone2:
    Type: String
    Default: ap-northeast-2c

  VpcBlock:
    Type: String
    Default: 192.168.0.0/16

  PublicSubnet1Block:
    Type: String
    Default: 192.168.1.0/24

  PublicSubnet2Block:
    Type: String
    Default: 192.168.2.0/24

  PrivateSubnet1Block:
    Type: String
    Default: 192.168.3.0/24

  PrivateSubnet2Block:
    Type: String
    Default: 192.168.4.0/24

Resources:
# VPC
  EksVPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: !Ref VpcBlock
      EnableDnsSupport: true
      EnableDnsHostnames: true
      Tags:
        - Key: Name
          Value: !Sub ${ClusterBaseName}-VPC

# PublicSubnets
  PublicSubnet1:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone: !Ref AvailabilityZone1
      CidrBlock: !Ref PublicSubnet1Block
      VpcId: !Ref EksVPC
      MapPublicIpOnLaunch: true
      Tags:
        - Key: Name
          Value: !Sub ${ClusterBaseName}-PublicSubnet1
        - Key: kubernetes.io/role/elb
          Value: 1

  PublicSubnet2:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone: !Ref AvailabilityZone2
      CidrBlock: !Ref PublicSubnet2Block
      VpcId: !Ref EksVPC
      MapPublicIpOnLaunch: true
      Tags:
        - Key: Name
          Value: !Sub ${ClusterBaseName}-PublicSubnet2
        - Key: kubernetes.io/role/elb
          Value: 1

  InternetGateway:
    Type: AWS::EC2::InternetGateway

  VPCGatewayAttachment:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      InternetGatewayId: !Ref InternetGateway
      VpcId: !Ref EksVPC

  PublicSubnetRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref EksVPC
      Tags:
        - Key: Name
          Value: !Sub ${ClusterBaseName}-PublicSubnetRouteTable

  PublicSubnetRoute:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref PublicSubnetRouteTable
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId: !Ref InternetGateway

  PublicSubnet1RouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PublicSubnet1
      RouteTableId: !Ref PublicSubnetRouteTable

  PublicSubnet2RouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PublicSubnet2
      RouteTableId: !Ref PublicSubnetRouteTable

# PrivateSubnets
  PrivateSubnet1:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone: !Ref AvailabilityZone1
      CidrBlock: !Ref PrivateSubnet1Block
      VpcId: !Ref EksVPC
      Tags:
        - Key: Name
          Value: !Sub ${ClusterBaseName}-PrivateSubnet1
        - Key: kubernetes.io/role/internal-elb
          Value: 1

  PrivateSubnet2:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone: !Ref AvailabilityZone2
      CidrBlock: !Ref PrivateSubnet2Block
      VpcId: !Ref EksVPC
      Tags:
        - Key: Name
          Value: !Sub ${ClusterBaseName}-PrivateSubnet2
        - Key: kubernetes.io/role/internal-elb
          Value: 1

  PrivateSubnetRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref EksVPC
      Tags:
        - Key: Name
          Value: !Sub ${ClusterBaseName}-PrivateSubnetRouteTable

  PrivateSubnet1RouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PrivateSubnet1
      RouteTableId: !Ref PrivateSubnetRouteTable

  PrivateSubnet2RouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PrivateSubnet2
      RouteTableId: !Ref PrivateSubnetRouteTable

# EKSCTL-Host
  EKSEC2SG:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: eksctl-host Security Group
      VpcId: !Ref EksVPC
      Tags:
        - Key: Name
          Value: !Sub ${ClusterBaseName}-HOST-SG
      SecurityGroupIngress:
      - IpProtocol: '-1'
        CidrIp: !Ref SgIngressSshCidr

  EKSEC2:
    Type: AWS::EC2::Instance
    Properties:
      InstanceType: !Ref MyInstanceType
      ImageId: !Ref LatestAmiId
      KeyName: !Ref KeyName
      Tags:
        - Key: Name
          Value: !Sub ${ClusterBaseName}-host
      NetworkInterfaces:
        - DeviceIndex: 0
          SubnetId: !Ref PublicSubnet1
          GroupSet:
          - !Ref EKSEC2SG
          AssociatePublicIpAddress: true
          PrivateIpAddress: 192.168.1.100
      BlockDeviceMappings:
        - DeviceName: /dev/xvda
          Ebs:
            VolumeType: gp3
            VolumeSize: 30
            DeleteOnTermination: true
      UserData:
        Fn::Base64:
          !Sub |
            #!/bin/bash
            hostnamectl --static set-hostname "${ClusterBaseName}-host"

            # Config Root account
            echo 'root:qwe123' | chpasswd
            sed -i "s/^#PermitRootLogin yes/PermitRootLogin yes/g" /etc/ssh/sshd_config
            sed -i "s/^PasswordAuthentication no/PasswordAuthentication yes/g" /etc/ssh/sshd_config
            rm -rf /root/.ssh/authorized_keys
            systemctl restart sshd

            # Config convenience
            echo 'alias vi=vim' >> /etc/profile
            echo "sudo su -" >> /home/ec2-user/.bashrc
            sed -i "s/UTC/Asia\/Seoul/g" /etc/sysconfig/clock
            ln -sf /usr/share/zoneinfo/Asia/Seoul /etc/localtime

            # Install Packages
            yum -y install tree jq git htop

            # Install kubectl & helm
            cd /root
            curl -O https://s3.us-west-2.amazonaws.com/amazon-eks/1.28.5/2024-01-04/bin/linux/amd64/kubectl
            install -o root -g root -m 0755 kubectl /usr/local/bin/kubectl
            curl -s https://raw.githubusercontent.com/helm/helm/master/scripts/get-helm-3 | bash

            # Install eksctl
            curl -sL "https://github.com/eksctl-io/eksctl/releases/latest/download/eksctl_Linux_amd64.tar.gz" | tar xz -C /tmp
            mv /tmp/eksctl /usr/local/bin

            # Install aws cli v2
            curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"
            unzip awscliv2.zip >/dev/null 2>&1
            ./aws/install
            complete -C '/usr/local/bin/aws_completer' aws
            echo 'export AWS_PAGER=""' >>/etc/profile
            export AWS_DEFAULT_REGION=${AWS::Region}
            echo "export AWS_DEFAULT_REGION=$AWS_DEFAULT_REGION" >> /etc/profile

            # Install YAML Highlighter
            wget https://github.com/andreazorzetto/yh/releases/download/v0.4.0/yh-linux-amd64.zip
            unzip yh-linux-amd64.zip
            mv yh /usr/local/bin/

            # Install krew
            curl -L https://github.com/kubernetes-sigs/krew/releases/download/v0.4.4/krew-linux_amd64.tar.gz -o /root/krew-linux_amd64.tar.gz
            tar zxvf krew-linux_amd64.tar.gz
            ./krew-linux_amd64 install krew
            export PATH="$PATH:/root/.krew/bin"
            echo 'export PATH="$PATH:/root/.krew/bin"' >> /etc/profile

            # Install kube-ps1
            echo 'source <(kubectl completion bash)' >> /etc/profile
            echo 'alias k=kubectl' >> /etc/profile
            echo 'complete -F __start_kubectl k' >> /etc/profile

            git clone https://github.com/jonmosco/kube-ps1.git /root/kube-ps1
            cat <<"EOT" >> /root/.bash_profile
            source /root/kube-ps1/kube-ps1.sh
            KUBE_PS1_SYMBOL_ENABLE=false
            function get_cluster_short() {
              echo "$1" | cut -d . -f1
            }
            KUBE_PS1_CLUSTER_FUNCTION=get_cluster_short
            KUBE_PS1_SUFFIX=') '
            PS1='$(kube_ps1)'$PS1
            EOT

            # Install krew plugin
            kubectl krew install ctx ns get-all neat # ktop df-pv mtail tree

            # Install Docker
            amazon-linux-extras install docker -y
            systemctl start docker && systemctl enable docker

            # CLUSTER_NAME
            export CLUSTER_NAME=${ClusterBaseName}
            echo "export CLUSTER_NAME=$CLUSTER_NAME" >> /etc/profile

            # Create SSH Keypair
            ssh-keygen -t rsa -N "" -f /root/.ssh/id_rsa

Outputs:
  eksctlhost:
    Value: !GetAtt EKSEC2.PublicIp

위의 템플릿으로 Cloud Formation 결과

  • VPC 생성
    • myeks-VPC
  • 서브넷 생성
    • myeks-PublicSubnet1 – 192.168.1.0/24 (ap-northeast-2a)
    • myeks-PublicSubnet2 – 192.168.2.0/24 (ap-northeast-2c)
    • myeks-PrivateSubnet3 – 192.168.3.0/24 (ap-northeast-2a)
    • myeks-PrivateSubnet4 – 192.168.4.0/24 (ap-northeast-2c)
  • 인터넷게이트웨이 생성
  • 라우팅 테이블 생성
    • [인터넷게이트웨이] – [myeks-PublicSubnet1 – 192.168.1.0/24 (ap-northeast-2a)]
    • [인터넷게이트웨이] – [myeks-PublicSubnet2 – 192.168.2.0/24 (ap-northeast-2c)]
  • EC2 생성
    • instancetype : t3.medium
    • AMI : /aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-gp2
    • IP address : 192.168.1.100
    • EBS volume : gp3 / 30GB
    • 생성 후 shell script 진행
      • 호스트명 : myeks-host
      • 패키지 설치 : tree, jq, git, htop, kubectl, helm, eksctl, aws_cli_v2, yh(yaml highlighter), krew, krew plugin, kube-ps1, docker

Worker Node Deploy

🔠Node

Kubernetes에서는 Docker, containerd, CRI-O 등과 같은 컨테이너 런타임을 사용하여 컨테이너를 실행합니다. 이 런타임은 kubelet에 의해 관리되며, Pod 내의 컨테이너를 생성하고 관리

eksctl deploy

# 자격 구성 설정 없이 확인
aws ec2 describe-instances

# IAM User 자격 구성 : 실습 편리를 위해 administrator 권한을 가진 IAM User 의 자격 증명 입력
aws configure
AWS Access Key ID [None]: AKIA5...
AWS Secret Access Key [None]: CVNa2...
Default region name [None]: ap-northeast-2
Default output format [None]: json

# 자격 구성 적용 확인 : 노드 IP 확인
aws ec2 describe-instances

# EKS 배포할 VPC 정보 확인
export VPCID=$(aws ec2 describe-vpcs --filters "Name=tag:Name,Values=$CLUSTER_NAME-VPC" | jq -r .Vpcs[].VpcId)
echo "export VPCID=$VPCID" >> /etc/profile
echo $VPCID

# EKS 배포할 VPC에 속한 Subnet 정보 확인
aws ec2 describe-subnets --filters "Name=vpc-id,Values=$VPCID" --output json | jq
aws ec2 describe-subnets --filters "Name=vpc-id,Values=$VPCID" --output yaml

## 퍼블릭 서브넷 ID 확인
aws ec2 describe-subnets --filters Name=tag:Name,Values="$CLUSTER_NAME-PublicSubnet1" | jq
aws ec2 describe-subnets --filters Name=tag:Name,Values="$CLUSTER_NAME-PublicSubnet1" --query "Subnets[0].[SubnetId]" --output text
export PubSubnet1=$(aws ec2 describe-subnets --filters Name=tag:Name,Values="$CLUSTER_NAME-PublicSubnet1" --query "Subnets[0].[SubnetId]" --output text)
export PubSubnet2=$(aws ec2 describe-subnets --filters Name=tag:Name,Values="$CLUSTER_NAME-PublicSubnet2" --query "Subnets[0].[SubnetId]" --output text)
echo "export PubSubnet1=$PubSubnet1" >> /etc/profile
echo "export PubSubnet2=$PubSubnet2" >> /etc/profile
echo $PubSubnet1
echo $PubSubnet2

eksctl create cluster --name $CLUSTER_NAME --region=$AWS_DEFAULT_REGION --nodegroup-name=$CLUSTER_NAME-nodegroup --node-type=t3.medium \
--node-volume-size=30 --vpc-public-subnets "$PubSubnet1,$PubSubnet2" --version 1.28 --ssh-access --external-dns-access --verbose 4
  • 클러스터 이름 및 지역 설정
    • cluster name : myeks
    • region : ap-northeast-2
  • 노드 그룹 설정
    • nodegroup name : myeks-nodegroup
    • node type : t3.medium
    • volume size : 30GB
  • VPC 및 서브넷 설정
    • 각 public subnet에서 워커 노드 배포
  • EKS 버전 설정
    • v1.28
  • SSH 및 외부 DNS 액세스 설정
    • SSH 액세스
    • 외부 DNS 액세스
  • Verbose 모드 설정
    • –verbose 4 플래그를 사용하여 자세한 정보출력

Pod Deploy

🔠pod

Kubernetes에서 가장 기본적인 배포 단위입니다. Pod은 Kubernetes 클러스터에서 실행되는 하나 이상의 컨테이너 그룹입니다. 일반적으로 한 Pod에는 주로 함께 동작하는 여러 컨테이너가 포함됩니다. 이러한 컨테이너는 네트워크와 스토리지 등을 공유하며 동일한 호스트에서 실행

Sample pod deploy

kubectl create deployment my-webs --image=gcr.io/google-samples/kubernetes-bootcamp:v1 --replicas=3
  • kubectl을 통해 Deployment 로 pod 3개 배포
  • 강제 삭제 하면서 Kubernetes의 장점중 하나인 멱등성(Idempotence)을 확인
💡멱등성(idempotent) : 연산을 여러 번 적용하더라도 결과가 달라지지 않는 성질

Example

  1. HTTP 메서드:
    • HTTP 메서드 중 PUT과 DELETE는 멱등성을 가집니다. 예를 들어, 같은 PUT 요청을 여러 번 실행하더라도 동일한 리소스가 생성되며, 같은 DELETE 요청을 여러 번 실행하더라도 동일한 리소스가 삭제됩니다.
  2. 수학적 연산:
    • 예를 들어, 정수에 대한 절대값을 계산하는 연산은 멱등성을 가집니다. 어떤 정수에 대한 절대값을 여러 번 계산하더라도 결과는 항상 동일합니다.
  3. 시스템 설정 변경:
    • 시스템 설정을 변경하는 작업 중에도 멱등성을 고려할 수 있습니다. 예를 들어, 설정 파일을 업데이트하는 작업이 있을 때, 동일한 설정을 여러 번 적용해도 시스템 상태는 동일하게 유지되어야 합니다.

docker-supermario pod deploy

mario.yaml

# mario.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: mario
  labels:
    app: mario
spec:
  replicas: 1
  selector:
    matchLabels:
      app: mario
  template:
    metadata:
      labels:
        app: mario
    spec:
      containers:
      - name: mario
        image: pengbai/docker-supermario
---
apiVersion: v1
kind: Service
metadata:
   name: mario
spec:
  selector:
    app: mario
  ports:
  - port: 80
    protocol: TCP
    targetPort: 8080
  type: LoadBalancer
(gyuroot@myeks:default) [root@myeks-host ~]# kubectl apply -f mario.yaml
(gyuroot@myeks:default) [root@myeks-host ~]# k get deploy
NAME    READY   UP-TO-DATE   AVAILABLE   AGE
mario   1/1     1            1           63m
(gyuroot@myeks:default) [root@myeks-host ~]# k get svc
NAME         TYPE           CLUSTER-IP      EXTERNAL-IP                                                                    PORT(S)        AGE
kubernetes   ClusterIP      10.100.0.1      <none>                                                                         443/TCP        9h
mario        LoadBalancer   10.100.101.35   xxxxxxxxxxxxxxxxxxxxxxx.ap-northeast-2.elb.amazonaws.com   80:32385/TCP   63m
(gyuroot@myeks:default) [root@myeks-host ~]# k get ep
NAME         ENDPOINTS                           AGE
mario        192.168.2.138:8080                  63m
오픈된 로드밸런서 URL로 접속 후 화면

노드에 배포된 컨테이너 정보 확인

ctr

  • Containerd 컨테이너 런타임의 CLI 도구
  • ctr을 사용하면 컨테이너의 생성, 실행, 중지 및 관리와 같은 다양한 작업을 수행가능
  • 주로 Containerd의 API를 활용하여 더 낮은 수준의 컨테이너 관리를 원할 때 사용
# 컨테이너 실행
ctr run --rm --tty docker.io/library/alpine:latest echo "Hello, World!"

# 컨테이너 목록 조회
ctr containers list

# 이미지 목록 조회
ctr images list

# Containerd에서 직접 컨테이너 생성
ctr container create --snapshotter=overlayfs alpine-container

nerdctl

  • Docker CLI와 유사한 명령어를 제공하면서도 Docker Daemon을 필요로 하지 않고, 대신에 Containerd를 직접 조작하여 컨테이너를 실행
  • nerdctl은 Docker와 호환되는 환경에서 Docker CLI 대신에 사용가능
  • 주로 Docker Daemon이 실행 중이지 않은 환경이거나 독립적인 컨테이너 관리를 원할 때 사용
# 컨테이너 실행
nerdctl run --rm docker.io/library/alpine:latest echo "Hello, World!"

# 컨테이너 목록 조회
nerdctl ps

# 이미지 목록 조회
nerdctl images

crictl

  • Kubernetes와 같은 CRI를 구현하는 컨테이너 런타임과 상호 작용하기 위한 CLI 도구
  • Kubernetes가 컨테이너 런타임과 통신할 때 사용
  • crictl을 사용하면 Kubernetes 클러스터 내의 컨테이너 런타임에 대한 정보를 조회하고, 컨테이너를 생성하고 관리하는 등의 작업을 수행
  • 주로 Kubernetes 클러스터에서의 컨테이너 관리 및 디버깅에 사용
# 컨테이너 목록 조회
crictl ps

# 이미지 목록 조회
crictl images

# 컨테이너 생성
crictl runp <sandbox-id> echo "Hello, World!"

Leave a Comment