반응형

kubernetes 에 서비스를 올릴 때 Service 의 Type 으로 LoadBalancer 를 선택하면 cloud 에서 자동으로 LB (external-ip ) 가 생성되어 서비스 pod 에 연결된다. 어떻게 Kubernetes 에서 설정한 값이 cloud 에 연결될까? 이는 Cloud Provider 가 있어 가능하다.

Cloud Provider 는 초기에 Kubernetes Controller 에 포함되어 있다. 하지만 지금은 External Kubernete Cloud Provider 로 Kubernetes 에서 제외되었으며, 이전 Kubernetes Controller 에 포함된 Cloud Provider 는 Legacy Cloud Provider 로 불리고 있다.

AWS Cloud Provider 의 경우에는 아직 1.23 (Kubernetes 와 같이 버전을 맞춰가고 있음) alpha 버전이라 아직은 Legacy Cloud Provider 를 사용하는 것이 안정적이다.

aws 에서는 아래의 순서대로 적용한다.

  1. IAM Policy, Role 생성
  2. VPC, Subnet, Routing Table, Internet Gateway, Nat Gateway 생성
  3. VM 생성
  4. aws resource 에 Tag 적용
  5. Kubernetes Cluster 생성

1. IAM Policy, Role 생성

Control plane 과 Node 2개의 Policy 를 생성한다.

Control Plane Policy

control node 에서 사용할 policy 이다.

정책명: control-plane.cluster-api-provider-aws.sigs.k8s.io

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "autoscaling:DescribeAutoScalingGroups",
        "autoscaling:DescribeLaunchConfigurations",
        "autoscaling:DescribeTags",
        "ec2:DescribeInstances",
        "ec2:DescribeRegions",
        "ec2:DescribeRouteTables",
        "ec2:DescribeSecurityGroups",
        "ec2:DescribeSubnets",
        "ec2:DescribeVolumes",
        "ec2:DescribeAvailabilityZones",
        "ec2:CreateSecurityGroup",
        "ec2:CreateTags",
        "ec2:CreateVolume",
        "ec2:ModifyInstanceAttribute",
        "ec2:ModifyVolume",
        "ec2:AttachVolume",
        "ec2:AuthorizeSecurityGroupIngress",
        "ec2:CreateRoute",
        "ec2:DeleteRoute",
        "ec2:DeleteSecurityGroup",
        "ec2:DeleteVolume",
        "ec2:DetachVolume",
        "ec2:RevokeSecurityGroupIngress",
        "ec2:DescribeVpcs",
        "elasticloadbalancing:AddTags",
        "elasticloadbalancing:AttachLoadBalancerToSubnets",
        "elasticloadbalancing:ApplySecurityGroupsToLoadBalancer",
        "elasticloadbalancing:CreateLoadBalancer",
        "elasticloadbalancing:CreateLoadBalancerPolicy",
        "elasticloadbalancing:CreateLoadBalancerListeners",
        "elasticloadbalancing:ConfigureHealthCheck",
        "elasticloadbalancing:DeleteLoadBalancer",
        "elasticloadbalancing:DeleteLoadBalancerListeners",
        "elasticloadbalancing:DescribeLoadBalancers",
        "elasticloadbalancing:DescribeLoadBalancerAttributes",
        "elasticloadbalancing:DetachLoadBalancerFromSubnets",
        "elasticloadbalancing:DeregisterInstancesFromLoadBalancer",
        "elasticloadbalancing:ModifyLoadBalancerAttributes",
        "elasticloadbalancing:RegisterInstancesWithLoadBalancer",
        "elasticloadbalancing:SetLoadBalancerPoliciesForBackendServer",
        "elasticloadbalancing:AddTags",
        "elasticloadbalancing:CreateListener",
        "elasticloadbalancing:CreateTargetGroup",
        "elasticloadbalancing:DeleteListener",
        "elasticloadbalancing:DeleteTargetGroup",
        "elasticloadbalancing:DescribeListeners",
        "elasticloadbalancing:DescribeLoadBalancerPolicies",
        "elasticloadbalancing:DescribeTargetGroups",
        "elasticloadbalancing:DescribeTargetHealth",
        "elasticloadbalancing:ModifyListener",
        "elasticloadbalancing:ModifyTargetGroup",
        "elasticloadbalancing:RegisterTargets",
        "elasticloadbalancing:DeregisterTargets",
        "elasticloadbalancing:SetLoadBalancerPoliciesOfListener",
        "iam:CreateServiceLinkedRole",
        "kms:DescribeKey"
      ],
      "Resource": [
        "*"
      ]
    }
  ]
}

Node Policy

일반 Node 에 대한 policy 이다.

정책명: nodes.cluster-api-provider-aws.sigs.k8s.io

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "ec2:DescribeInstances",
        "ec2:DescribeRegions",
        "ecr:GetAuthorizationToken",
        "ecr:BatchCheckLayerAvailability",
        "ecr:GetDownloadUrlForLayer",
        "ecr:GetRepositoryPolicy",
        "ecr:DescribeRepositories",
        "ecr:ListImages",
        "ecr:BatchGetImage"
      ],
      "Resource": "*"
    }
  ]
}

policy 를 생성했으면 이제 Role 을 생성한다.

Control plane Role

Policy 를 활용할 수 있는 Role 을 만들어서 Policy 와 연결한다.

Role 명: control-plane.cluster-api-provider-aws.sigs.k8s.io

연결할 Policy 리스트
- control-plane.cluster-api-provider-aws.sigs.k8s.io
- nodes.cluster-api-provider-aws.sigs.k8s.io

Node Role

Role 명: nodes.cluster-api-provider-aws.sigs.k8s.io

연결할 Policy 리스트
- nodes.cluster-api-provider-aws.sigs.k8s.io

2. VPC, Subnet, Routing Table, Internet Gateway, Nat Gateway 생성

서울 리전의 경우 VPC 를 1개 만들고, public 용도의 subnet 4개, private 용도의 subnet 4개를 만든다.

Internet Gateway 1개를 만들어서 public subnet 에 연결하고, Nat Gateway 4개를 만들어서 각각 private subnet 에 연결한다.

Routing Table 은 subnet 갯수에 맞는 8개를 만들어서 각각 연결한다. private 용 4개의 Routing table 은 0.0.0.0/0 → nat gateway 를 대상으로 설정하고, public 용 4개의 Routing table 은 0.0.0.0/0 → internet gateway 대상으로 설정한다.

1. vpc : 1개
   - vpc

2. subent : 8개
   - public-subnet-a
   - public-subnet-b
   - public-subnet-c
   - public-subnet-d
   - private-subnet-a
   - private-subnet-b
   - private-subnet-c
   - private-subnet-d

3. internat gateway : 1개
   - igw

4. Nat Gateway : 4개
   - nat-private-a
   - nat-private-b
   - nat-private-c
   - nat-private-d

5. Routing Table : 4개
   - rt-public-a (0.0.0.0/0 -> igw)
   - rt-public-b (0.0.0.0/0 -> igw)
   - rt-public-c (0.0.0.0/0 -> igw)
   - rt-public-d (0.0.0.0/0 -> igw)
   - rt-priabe-a (0.0.0.0/0 -> nat-private-a)
   - rt-priabe-b (0.0.0.0/0 -> nat-private-b)
   - rt-priabe-c (0.0.0.0/0 -> nat-private-c)
   - rt-priabe-d (0.0.0.0/0 -> nat-private-d)

3. VM 생성

VM 은 Controler Plane 3대는 각 private subnet 에 1대씩 생성하고(subnet 1개는 남는다), Node 용 4대는 각 private subnet 1대씩 생성하다.

bastion 노드로 public subnet 에 1대 생성한다.

1. bastion VM 1대
   - public-subnet-a

2. Control Plane VM 3대
   - private-subnet-a
   - private-subnet-b
   - private-subnet-c

3. Node VM 4대
   - private-subnet-a
   - private-subnet-b
   - private-subnet-c
   - private-subnet-d

4. Aws Resource 에 Tag 설정

aws cloud provider 가 리소스를 파악하기 위해서는 aws 에 적절한 값을 설정해야 한다.

4-1. VM 에 IAM Role 을 할당

VM 에서 권한을 얻기 위해서는 반드시 IAM Role 을 할당해야 한다.

Control node 에는 control-plane.cluster-api-provider-aws.sigs.k8s.io role 을 할당한다.

일반 Node 에는 nodes.cluster-api-provider-aws.sigs.k8s.io role 을 할당한다.

4-2 VM 에 Tag 설정

VM 에는 Kubernetes Cluster Name 을 Tag 로 지정한다. kubernetes.io/cluster/<cluster name> 과 같이 지정하는데 Cluster Name 은 Kubernetes 를 설치할 때 지정할 수 있다. 기본 값은 cluster.local 인데 여기서는 ahnsk 로 이름을 지정하였다.

그리고 vm 의 역할을 지정해야 하는데 Controler Node 는 control-plane 으로, Node 는 node 로 지정한다.

4-3. Subnet 에 Tag 설정

Load Balancer 를 핸들링 하기 위해서 Kubernetes Cluster 가 어느 Subnet 과 Routing Table 을 사용해야 하는지 알아야 한다.

주의해야 할 점은 public subnet 의 경우 [kubernetes.io/role/elb](http://kubernetes.io/role/elb) 이지만, private subnet 의 경우에는 [kubernetes.io/role/internal-elb](http://kubernetes.io/role/internal-elb) 로 해야 한다.

4-4. Routing Table 에 Tag 설정

5. Kubernetes Cluster 생성

Kubernetes cluster 이름은 앞에서 설명했듯이 ahnsk 로 설정한다.

kubeadm 혹은 kubespray 를 사용할 수 있으며 여기서 생성 방법을 생략한다.

aws cloud provider 를 활성화 하기 위해서는 API Server, Controller, Kubelet 에 --cloud-provider=aws 옵션을 추가해야 한다.

kube-apiserver.yaml

apiVersion: v1
kind: Pod
metadata:
  annotations:
    kubeadm.kubernetes.io/kube-apiserver.advertise-address.endpoint: 172.31.22.52:6443
  creationTimestamp: null
  labels:
    component: kube-apiserver
    tier: control-plane
  name: kube-apiserver
  namespace: kube-system
spec:
  containers:
  - command:
    - kube-apiserver
    - --advertise-address=172.31.22.52
...
    **- --cloud-provider=aws**
...
    image: registry.k8s.io/kube-apiserver:v1.24.6
...

kube-controller-manager.yaml

apiVersion: v1
kind: Pod
metadata:
  creationTimestamp: null
  labels:
    component: kube-controller-manager
    tier: control-plane
  name: kube-controller-manager
  namespace: kube-system
spec:
  containers:
  - command:
    - kube-controller-manager
    - --allocate-node-cidrs=true
...
    **- --cloud-provider=aws
...**
    image: registry.k8s.io/kube-controller-manager:v1.24.6
...

kubelet.env

...
KUBELET_CLOUDPROVIDER="**--cloud-provider=aws --cloud-config=/etc/kubernetes/cloud_config**"
...

kubelet 은 aws 의 리소스를 위해 추가해야 할 값들이 있는데 cloud_config 을 만들어서 옵션을 전달하였다.

[Global]
zone=
vpc=vpc-b342d5d8
subnetId=
routeTableId=
roleArn=
kubernetesClusterTag=ahnsk
kubernetesClusterId=ahnsk
disableSecurityGroupIngress=false
disableStrictZoneCheck=false
elbSecurityGroup=

값을 다 채워 넣으면 apiserver 가 kube-apiserver-master-dummy 라는 이름으로 잘못 실행되므로 조심해야 한다. master-dummy 로 띄우는 방법은 aws account 가 다를 경우에만 사용하는 방법이다. 이는 아래 소스를 보면 알 수 있다.

https://github.com/kubernetes/legacy-cloud-providers/blob/707ecda639b086132369678680a1b34d4d2b5c7c/aws/aws.go#L1251

...
  tagged := cfg.Global.KubernetesClusterTag != "" || cfg.Global.KubernetesClusterID != ""
    if cfg.Global.VPC != "" && (cfg.Global.SubnetID != "" || cfg.Global.RoleARN != "") && tagged {
        // When the master is running on a different AWS account, cloud provider or on-premise
        // build up a dummy instance and use the VPC from the nodes account
        klog.Info("Master is configured to run on a different AWS account, different cloud provider or on-premises")
        awsCloud.selfAWSInstance = &awsInstance{
            nodeName: "master-dummy",
            vpcID:    cfg.Global.VPC,
            subnetID: cfg.Global.SubnetID,
        }
        awsCloud.vpcID = cfg.Global.VPC
    } else {
        selfAWSInstance, err := awsCloud.buildSelfAWSInstance()
        if err != nil {
            return nil, err
        }
        awsCloud.selfAWSInstance = selfAWSInstance
        awsCloud.vpcID = selfAWSInstance.vpcID
    }
...

RoleARN 의 값을 넣으면 안되는데 값이 없으면서도 어떻게 kubelet 이 해당 Role 로 인증을 받을 수 있을까? 이는 앞에서 설명한 VM 에 IAM Role 인 control-plane.cluster-api-provider-aws.sigs.k8s.io 이나 nodes.cluster-api-provider-aws.sigs.k8s.io 이 설정되어 있기 때문에 가능하다.

kubelet-config.yaml

kubernetes node 정보에 providerID 값이 들어가 있어야 한다. 만약 이 정보가 없다면 LoadBalancer 가 생성된다고 하더라도 인스턴스가 LoadBalancer 에 할당되지 않아 제대로 사용할 수 가 없다.

providerID 는 kubelet-config.yaml 에 추가한다. aws:///<zone-id>/<instance-id> 값으로 추가한다.

$ sudo vi /etc/kubernetes/kubelet-config.yaml
...
providerID: "aws:///ap-northeast-2d/i-0f59f059e2d64213f"
...

이미 노드가 생성된 경우에는 해당 값을 변경하고 kubelet 서비스를 다시 restart 한다고 값이 추가되지는 않는다. 그래서 patch 명령으로 동적으로 추가하는 것이 좋다.

$ kubectl patch node ip-172-31-46-179.ap-northeast-2.compute.internal -p '{"spec": {"providerID": "aws:///ap-northeast-2d/i-08cb6f884239f894c"}}'

5. Nginx 로 테스트

nginx 를 생성하고 service 를 LoadBalancer type 으로 생성하여 잘 접속이 되는지 확인해 보자.

$ kubectl create deploy nginx --image=nginx
deployment.apps/nginx created
$ kubectl get pods
NAME                    READY   STATUS    RESTARTS   AGE
nginx-8f458dc5b-nhkfd   1/1     Running   0          55s
$ kubectl expose deployment nginx --name nginx-svc --target-port=80 --port=80 --type=LoadBalancer
$ kubectl get svc
NAME         TYPE           CLUSTER-IP     EXTERNAL-IP                                                                   PORT(S)        AGE
kubernetes   ClusterIP      10.233.0.1     <none>                                                                        443/TCP        5h28m
nginx-svc    LoadBalancer   10.233.7.131   a76a315f5d7c14652a4db83cc3b25125-113127685.ap-northeast-2.elb.amazonaws.com   80:30796/TCP   27s
반응형
Posted by seungkyua@gmail.com

댓글을 달아 주세요

반응형

command line 명령을 사용할 때 터미널에서 여러 기능들을 사용해야 할 때가 있다. 예를 들어 여러 윈도우(창)을 띄워놓고 필요에 따라 옮긴다던가 하나의 윈도우을 상하, 혹은 좌우로 구분하여 나눠서 사용하는 경우가 있다.

보통은 명령어를 여러 디렉토리를 옮겨가며 작업하기 때문에 디렉토리 마다 윈도우를 만들어 사용한다. 혹은 포그라운드로 프로세스를 띄워 출력되는 로그를 확인하고, 다른 윈도에서 작업하는 경우도 있다.

위와 같은 기능을 제공하는 툴에는 tmux 라는 sw가 있다. 일반적으로 linux 에는 기본으로 설치되어 있고 mac 에서는 쉽게 설치할 수 있으므로 쉽게 활용할 수 가 있다.

1. 세션 관리

세션 만들기

tmux 는 세션 단위로 구분하여 관리한다. 자신만의 새로운 세션을 만들어 독립으로 사용할 수 있다.

$ tmux new -s ahnsk

ahnsk 이라는 새로운 세션(session)이 생성되면서 1:zsh 라는 윈도우(window) 1개가 기본적으로 생성되었다.

$ exit

exit 는 현재의 윈도우를 삭제한다. 지금은 세션에 1개의 윈도우 밖에 없기 때문에 윈도우가 삭제되면 해당 세션도 삭제된다.

command prefix

tmux 에서는 vi 처럼 명령모드를 사용할 수 있다. Ctrl + b (control 키와 b 키를 동시에 누름)를 입력한 이후에 명령어를 입력하면 되는데 명령 모드를 알려주는 이 키 조합을 command prefix 라고 한다. 줄여서 앞으로는 prefix 라고 한다.

prefix + t 를 누르면 화면에 시간에 표시된다.

Detaching 과 Attaching

exit 는 윈도우를 삭제(경우에 따라서는 세션까지도 삭제) 하기 때문에 이전의 세션과 윈도우를 계속 유지 시키면서 tmux 에서 빠져나오고 싶을 때도 있다. 이 때 사용해야할 기능이 detaching 기능이다.

$ top

prefix + d 를 입력하여 detaching 한다.

[detached (from session ahnsk)] 가 출력되고 원래의 터미널 창으로 빠져나온 것이 확인 된다.

-d 옵션으로 새로운 세션을 만들고 바로 detaching 할 수 도 있다.

$ tmux new -s dummy -d

세션을 조회하여 기존에 만든 세션의 리스트를 확인할 수 있다.

$ tmux ls

--- output ---
ahnsk: 1 windows (created Sat Jan 14 14:51:09 2023)
dummy: 1 windows (created Sat Jan 14 15:25:39 2023)

기존의 만든 ahnsk 세션으로 들어가고 싶으면 attach 명령을 쓰면 된다.

$ tmux attach -t ahnsk

세션 삭제

kill-session 옵션으로 세션을 삭제할 수 있다.

$ tmux kill-session -t ahnsk
$ tmux kill-session -t dummy

2. 윈도우 관리

윈도우 만들기

session 을 만들면 새로운 윈도우도 기본으로 생성된다고 했다. 이 때 -n 옵션을 사용하여 생성되는 윈도우에 이름을 넣을 수 있다.

$ tmux new -s ahnsk -n shell

prefix + c 명령으로 새로운 윈도우를 만들 수 있다. 이렇게 새로 만든 윈도우에 top 명령을 실행해 보자.

윈도우 이름 변경

첫번째 윈도우의 이름은 shell 이고 두번째 top 이 실행되고 있는 윈도우의 이름은 top 이다. (top 이 실행되고 있으므로) 두번째 윈도우 이름을 process 로 변경해 보자.

prefix + , 로 윈도우 이름을 변경할 수 있다.

이름을 넣은 뒤에 enter 를 치면 된다.

윈도우 이동

작업 윈도우를 아래 명령으로 옮겨 다닐 수 있다.

prefix + n : 현재 윈도우 다음(next) 윈도우로 이동하기

prefix + p : 현재 윈도우 이전(previous) 윈도우로 이동하기

prefix + 1 : 첫번째(1) 윈도우로 이동하기

prefix + 5 : 다섯번째(5) 윈도우로 이동하기

prefix + w : 윈도우 리스트를 보여주고 선택하여 이동하기

윈도우 닫기

prefix + & 로 명령으로 확인 입력(y)을 받으면 윈도우를 닫을 수(삭제할 수) 있다. exit 를 입력하여 확인없이 바로 윈도우가 삭제된다.

윈도우 이름으로 생성하기

prefix + : 으로 명령어 입력창을 띄울 수 있다. 여기에 new-window -n monitor 라고 입력하고 enter 를 치면 새로운 윈도우에 이름을 지정하여 생성할 수 있다.

3. 패인 관리

패인(pane) 만들기

윈도우 안의 구분되는 영역을 패인이라 한다. 윈도우를 수평으로 2개의 패인으로 나누거나 수직으로 2개의 패인으로 나눌 수 있다.

prefix + % : 윈도우를 수평(좌우)으로 2개의 패인으로 나눈다.

prefix + " (double quote) : 윈도우를 수직(위아래)으로 2개의 패인으로 나눈다.

패인 이동하기

prefix + Left 화살표 : 좌측 패인으로 이동하기

prefix + Right 화살표 : 우측 패인으로 이동하기

prefix + Up 화살표 : 위 패인으로 이동하기

prefix + Downe 화살표 : 아래 패인으로 이동하기

prefix + o : 시계 방향으로 패인 이동하기

패인 삭제하기

prefix + x 명령으로 현재 선택된 패인을 삭제할 수 있다. exit 로도 삭제가 가능하다. 가끔 화면에 키보드 입력이 안돼서 exit 를 화면에 입력할 수 없는 경우가 있다. 이 경우에는 명령모드인 prefix + x 로 삭제해야 하므로 명령모드도 알아 두는 것이 좋다.

반응형
Posted by seungkyua@gmail.com

댓글을 달아 주세요

반응형

언어를 배울 때 가장 기본적인 것 중에 하나가 비교문(if), 반복문(for), 함수(function) 이다.

 

 

If 문

if 는 비교 구문에서 사용되는 키워드 이다. 변수 선언과 동시에 비교를 할 수 있는데 이 때 선언된 변수는 해당 if 문 block 안에서만 유효하다. 그렇기 때문에 마지막 라인은 num 변수는 인식하지 못해 에러가 난다.

func main() {
	if num := 5; num == 0 {
		fmt.Println("False")
	} else if num < 10 {
		fmt.Println("True")
	} else {
		fmt.Println("Big number")
	}
	fmt.Println(num)
}

--- output ---
./main_05_for_function.go:17:14: undefined: num

 

 

 

if 문 밖에서 선언한 변수는 if 문 안과 밖 모두에서 사용할 수 있다. 하지만 if 문 안에서 같은 변수명을 재정의 하면 해당 변수 값은 오버라이드 된다.

func main() {
	num := 10
	if num > 0 {
		fmt.Println("outer num =", num)
		num := 0
		fmt.Println("inner num =", num)
	}
	fmt.Println("outer num =", num)
}

--- output ---
outer num = 10
inner num = 0
outer num = 10

 

 

For 문

for lloop 는 반복문이며 slice 나 map 타입에 대해서 range 를 사용하여 item 을 하나씩 가져온다. slice 를 range 로 가져오면 index, value 2개의 값으로 넘어온다. value 만을 사용하고 싶으면 index 부분은 _ 로 처리하여 버릴 수 있다. 또한 index 1개의 값만 가져올 수 있다.

func main() {
	nums := []int{10, 20, 30, 40, 50}
	for i, v := range nums {
		fmt.Println("index =", i, ",", "value =", v)
	}
	for _, v := range nums {
		fmt.Println("value =", v)
	}
	for i := range nums {
		fmt.Println("index =", i)
	}
}

--- output ---
index = 0 , value = 10
index = 1 , value = 20
index = 2 , value = 30
index = 3 , value = 40
index = 4 , value = 50
value = 10
value = 20
value = 30
value = 40
value = 50
index = 0
index = 1
index = 2
index = 3
index = 4

 

 

 

map 도 slice 와 마찬가지로 range 로 가져올 수 있다. 이 때 return 되는 값은 key, value 이다.

func main() {
  m := map[string]int{
		"a": 1,
		"b": 2,
		"c": 3,
	}
	for k, v := range m {
		fmt.Println("key =", k, ",", "value =", v)
	}
	for _, v := range m {
		fmt.Println("value =", v)
	}
	for k := range m {
		fmt.Println("key =", k)
	}
}

--- output ---
key = c , value = 3
key = a , value = 1
key = b , value = 2
value = 2
value = 3
value = 1
key = a
key = b
key = c

 

 

Switch

선택문은 switch 로 가능하다. 각 case 마다 break 문이 없어도 되며, case 에는 , 로 여러 개의 값을 지정할 수 있다.

func main() {
  alpha := []string{"a", "ab", "abc", "abcd", "abcde", "abcdef", "abedefg"}
	for _, a := range alpha {
		switch l := len(a); l {
		case 1, 2:
			fmt.Println(a, "length = 2 or 3")
		case 3:
			fmt.Println(a, "length = 3")
		case 4, 5, 6:
			fmt.Println(a, "length = 4, 5 or 6")
		default:
			fmt.Println(a, "length > 6")
		}
	}
}

--- output ---
a length = 2 or 3
ab length = 2 or 3
abc length = 3
abcd length = 4, 5 or 6
abcde length = 4, 5 or 6
abcdef length = 4, 5 or 6
abedefg length > 6

 

 

Function

함수는 func, function name, parameter, return value 로 만들 수 있다. 아래 2개의 숫자를 받아서 나누는 div 함수는 아래와 같이 정의 할 수 있다. parameter 가 같은 타입이면 앞의 타입은 생략할 수 있다.

func main() {
  result := div(4, 2)
	fmt.Println(result)
}

// func div(n int, d int) int { 는 n, d 파라미터의 타입이 같아 아래와 같이 바꿀 수 있음
func div(n, d int) int {
	if d == 0 {
		return 0
	}
	return n / d
}

--- output ---
2

 

 

 

, 로 구분하여 여러 개의 아규먼트를 넘길 수 있으며 이 때 받는 함수에서는 ... 를 활용하여 가변 파라미터로 받을 수 있다. 또한 slice... 와 같이 slice 를 가변인자로 보낼 수 있다.

func main() {
  fmt.Println(addTo(1))
	fmt.Println(addTo(1, 2))
	fmt.Println(addTo(1, 2, 3))
	i := []int{4, 5}
	fmt.Println(addTo(3, i...))   // slice를 가변인자로 보내기
	fmt.Println(addTo(6, []int{7, 8, 9}...))
}

// 가변인자 (variadic parameter)
func addTo(base int, vals ...int) []int {
	result := make([]int, 0, len(vals))
	for _, v := range vals {
		result = append(result, base+v)
	}
	return result
}

--- output ---
[]
[3]
[3 4]
[7 8]
[13 14 15]

 

 

 

Function 의 리턴 값을 여러 개로 할 수 있다. (multi return values)

func main() {
  result, remainder, err := multiReturnValues(10, 5)
	if err != nil {
		fmt.Println(err)
	}
	fmt.Println(result, remainder)
}

// multiple retrun values
func multiReturnValues(n, d int) (int, int, error) {
	if d == 0 {
		return 0, 0, errors.New("cannot divid by zero")
	}
	return (n / d), (n % d), nil
}

--- output ---
2 0

 

 

 

Function 은 변수에 담을 수 있다. cal 함수 내부에 보면 map 의 value 로 Function 타입을 선언하여 할당한 것을 볼 수 있다. Function 을 변수로 선언할 수 있고, 파라미터로 받을 수 있고 return value 로도 사용할 수 있다.

func main() {
  exp := []string{"3", "+", "4"}
	result, err := cal(exp)
	if err != nil {
		fmt.Println(err)
	}
	fmt.Println(result)
}

func cal(exp []string) (int, error) {
	// Function Type
	type opFuncType func(int, int) int
	var opMap = map[string]opFuncType{
		"+": add,
		"-": sub,
	}

	a, err := strconv.Atoi(exp[0])
	if err != nil {
		fmt.Println(err)
		return 0, err
	}

	b, err := strconv.Atoi(exp[2])
	if err != nil {
		fmt.Println(err)
		return 0, err
	}

	op := exp[1]
	opFunc, ok := opMap[op]
	if !ok {
		fmt.Println("unsupported operator: ", op)
		return 0, err
	}
	return opFunc(a, b), nil
}

func add(a, b int) int {
	return a + b
}

func sub(a, b int) int {
	return a - b
}

--- output ---
7

 

 

 

struct 와 마찬가지로 Function 도 anonymous 로 만들 수 있다.

func main() {
  for i := 0; i < 5; i++ {
		func(j int) {
			fmt.Println(j)
		}(i)
	}
}

--- output ---
0
1
2
3
4

 

 

 

Function 안에는 Function 을 중첩해서 선언할 수 있는데 이 것을 clousure (클로저)라 한다. 클로저는 앞에서 설명한 Function 을 파라미터와 리턴 값으로 사용할 수 있는 성질을 많이 이용한다.

 

클로저는 변수 값이 사라지지 않고 계속 유지되는 특성을 갖는다. 아래 multiple(2) 을 호출 할 때 생성된 파라미터 base 변수 에는 2가 할당 되고 funcA(3) 을 호출 할 때도 계속 값이 남아 있어 base * factor 의 값은 2 * 3 이 되어 6 이 출려된다.

func main() {
  funcA := multiple(2)
	fmt.Println(funcA(3))

	funcA = multiple(4)
	fmt.Println(funcA(5))
}

// high-ooder fuctnion
func multiple(base int) func(int) int {
	return func(factor int) int {
		return base * factor
	}
}

--- output ---
6
20

 

 

 

함수를 호출할 때 모든 것은 Call by Value 이다. 아래과 같이 기본 타입이나 Struct 타입은 함수를 통해 넘겨받은 변수의 값을 변경하더라도 원래의 값은 변경되지 않는다. 즉, 아래와 같이 person 을 modify 함수를 통해 넘긴 다음, modify 함수에서 값을 수정해도 원래의 person 의 값은 수정되지 않는다.

func main() {
  i := 1
	s := "Hello"
	p := person{}
	modify(i, s, p)
	fmt.Println(i, s, p)
}

type person struct {
	name string
	age  int
}

// Call by Value (여기서 수정해도 원래 변수 값은 수정되지 않는다)
func modify(i int, s string, p person) {
	i = i + 1
	s = "modified"
	p.name = "ask"
	p.age = 100
}

--- output ---
1 Hello { 0}

 

 

 

그러나 포인터로 넘기면 원래의 값도 수정할 수 있다. 포인터는 다음에 설명하기로 하고 여기서는 slice 와 map 이 포인터와 같이 작동한다고 이해하면 된다.

 

한가지 중요한 것이 있다. modSlice(s) 에서의 s 변수 가 가리키는 주소와 modSlice(s []int) 함수에서 s 변수 가 가리키는 주소는 동일하다. (마치 포인터 처럼 동작한다) 하지만 s 변수 의 경우 modSlice 함수 통해 넘기는데 처음 생성할 때 길이:2, 크기:2 로 생성했다. 즉, 길이와 크기가 같아 modSlice 함수에서 append 로 item 을 추가할 경우 크기를 늘린 새로운 slice 를 만들어서 리턴 된다.

 

s = append(s, 3) 에서 s 가 가리키는 주소는 처음 modSlice(s []int) 일 때 가리키는 주소가 아니라 크기가 늘어나 새롭게 생성된 slice 를 가리키게 된다. 그래서 그 이전에 수정된 s 의 값은 변화가 있고, 그 이후에 수정된 s 의 값은 변화가 없다.

func main() {
  m := map[string]int{
		"one": 1,
		"two": 2,
	}
	modMap(m)
	fmt.Println(m)

	s := []int{1, 2}
	modSlice(s)
	fmt.Println(s)
}

// Call by Value but slice and map like pointer
func modMap(m map[string]int) {
	m["two"] = 20
	m["three"] = 3
	delete(m, "one")
}

func modSlice(s []int) {
	s[0] = s[0] * 10
	s = append(s, 3)  // 길이 == 크기 이므로 새로운 크기의 slice 가 만들어져서 리턴된다
	s[1] = s[1] * 10
}

--- output ---
map[three:3 two:20]
[10 2]

 

반응형
Posted by seungkyua@gmail.com

댓글을 달아 주세요

반응형

MLOps 라고 불리는 s/w 가 여러개 있는데 그 중에서 Kubeflow 는 Kubernetes 기반의 MLOps 를 쉽게 구축할 수 있는 오픈 소스이다. 초창기 개발은 Google 이 주축이 되어 Arrikto 가 같이 참여하여 개발하는 형식이었는데 이제는 많은 글로벌 회사에서 같이 참여하여 점차 확대되고 있는 추세이다.

Kubeflow 는 Kubernetes 위에서만 돌아가기 때문에 Kubernetes 를 알아야 한다는 단점이 있지만, 일단 Kubernets 를 알고 있다면 설치가 아주 쉽다. 물론 그 안에 들어가는 컴포넌트들이 많고, MLOps 의 특성상 자동화는 workflow 를 잘 작성해서 pipeline 을 어떻게 구성하느냐가 중요하기 때문에 어려운 사용법을 익혀야 한다.

KServe 는 Kubeflow 의 여러 기능 중에서 ML Model Serving 에 해당하는 컴포넌트이며, 얼마전 kubeflow 내의 KFServe 컴포넌트 이었다가 독립적인 Add-Ons 으로 빠져 나오면서 KServe 로 이름을 바꾸고 자체 github repository 를 만들었다.
 
 

KServe Architecture

KServe 아키텍처는 다음과 같다.

[출처: https://www.kubeflow.org/docs/external-add-ons/kserve/kserve/]
 
 
그림과 같이 런타임으로 TensorFlow, PYTORCH, SKLearn, XGBoost, ONNX 등 다양한 모델 프레임워크를 지원하며 필요하면 커스텀 런타임을 만들어서 지원할 수 도 있다.

KServe 하단에는 Knative 와 Istio (Serverless Layer) 를 갖을 수 있는데 하단에는 다음과 같이 구성할 수 있다.

  1. KServe + Knative + Istio
  2. KServe + Istio

Knative 는 옵션이기는 하나 Knative 를 설치하면서 로깅 (fluentbit + ElasticSearch + Kibana), 모니터링 (Prometheus, Exporter), 트레이싱(Jaeger + ElasticSearch) 을 쉽게 연결할 수 있다는 장점이 있다. 또한 Istio 가 제공하는 Network 핸들링 기능을 쉽게 사용할 수 있다.

KServe 설치는 Istio 설치 → Knative 설치 → KServe 설치 순으로 진행하며, 이에 맞는 버전은 다음과 같다.
 
 

Recommended Version Matrix

Kubernetes Version Istio Version Knative Version
1.20 1.9, 1.10, 1.11 0.25, 0.26, 1.0
1.21 1.10, 1.11 0.25, 0.26, 1.0
1.22 1.11, 1.12 0.25, 0.26, 1.0

여기서는 Kubernetes 1.22 에 맞춰서 설치한다.
 
 

Istio 설치

Istio 는 Service Mesh 를 쉽게 구성가능하도록 지원하는 플랫폼으로 proxy 가 sidecar 형태로 추가되어 네트워크를 조절할 수 있다. 네트워크를 조절기능의 대표적인 것은 네트워크 쉬프팅이 있다. Canary Release 나 A/B Test 에서는 서로 다른 버전의 서비스로 호출되는 네트워크의 흐름 비중을 조절가능해야 한다.

이런 이유로 요즘 Service Mesh 는 Sidecar 활용 패턴을 사용하는데 Istio 에서는 서비스 배포 시에 Sidecar 를 자동으로 Injection 해주는 기능을 지원하고 있으며, 많은 곳에서 대부분 auto injeciton 을 사용하고 있다.

하지만 Knative 에서는 auto injection 을 사용하지 않는다. auto injection 은 kubernetes namespace 에 label 을 추가하여 (istio-injeciton=enabled) 자동으로 해당 namespace 에 배포되는 pod 에는 sidecar proxy 가 자동으로 설치되는 기능이라, Service Mesh 를 사용하고 싶지 않은 서비스들에게도 영향을 줄 수 있기 때문에 auto injection 을 disable 할 것을 권장하고 있다.

Istio 설치는 helm chart 로 쉽게 설치할 수 있다.

helm repo 를 추가하고 value 값을 오버라이드할 파일을 만든다.

$ helm repo add istio https://istio-release.storage.googleapis.com/charts
$ helm repo update

$ vi istiod_1.12.8_default_values.yaml
global:
  proxy:
        autoInject: disabled   # 원래 값은 enabled 임

 
 
Istio-system 네임스페이스를 생성한 후 helm chart 를 설치한다.

base 는 crd 를 설치하며, istiod 가 실제 데몬 서비스다.

$ kubectl create namespace istio-system
$ helm upgrade -i istio-base istio/base --version 1.12.8 -n istio-system -f istiod_1.12.8_default_values.yaml
$ helm upgrade -i istiod istio/istiod --version 1.12.8 -n istio-system -f istiod_1.12.8_default_values.yaml --wait

 
 
외부에서 서비스로 접근하기 위한 North - South 통신은 Istio Ingress Gateway 를 통해서 가능하다. 그러므로 Istio Gateway 를 추가로 설치해 준다.

먼저, value 값을 오버라이드할 파일을 만든다.

$ vi gateway_1.12.8_default_values.yaml
podAnnotations:
  prometheus.io/port: "15020"
  prometheus.io/scrape: "true"
  prometheus.io/path: "/stats/prometheus"
  inject.istio.io/templates: "gateway"
  sidecar.istio.io/inject: "true" 

 
 
istio-ingress 네임스페이스를 생성하고 istio ingress gateway 를 helm chart로 설치한다.

$ kubectl create ns istio-ingress
$ helm upgrade -i istio-ingress istio/gateway --version 1.12.8 -n istio-ingress -f gateway_1.12.8_default_values.yaml

 
 
아래와 같이 잘 설치되어 있음을 확인할 수 있다.

$ kubectl get pods -n istio-system
NAME                      READY   STATUS    RESTARTS   AGE
istiod-68d7bfb6d8-nt82m   1/1     Running   0          20d

$ kubectl get pods -n istio-ingress
NAME                             READY   STATUS    RESTARTS   AGE
istio-ingress-69495c6667-7njv8   1/1     Running   0          20d

 
 

Knative 설치

Knative 는 Serverless 플랫폼이라 생각하면 된다. 서비스를 Istio 를 활용하여 배포하면 Gateway, VirtualServie 를 만들어서 연결해야 하는데 Knative 를 이를 자동으로 생성해주기 때문에 편리하다. 또한 앞에서도 설명한 모니터링, 로깅, 트레이싱이 잘 연결되기 때문에 일단 설치를 한다면 사용하기 편리하다.

Knative 는 설치 모듈이 Serving 과 Eventing 2개로 나눠져 있다. 일단 API 서비스가 가능한 Serving 모듈만 설치하고 테스트를 한다. 또한 모니터링, 로깅, 트레이싱도 다음에 설명하고 지금은 Knative Serving 기능에 집중한다.

Knative 는 Yaml 과 Operator 로 설치할 수 있는데 공식 문서에서 Operator 는 개발/테스트 환경에서만 사용하라고 권고하기 때문에 yaml 로 설치한다.

먼저, crd 를 설치하고, Serving 모듈을 설치한다.

$ kubectl apply -f https://github.com/knative/serving/releases/download/knative-v1.5.0/serving-crds.yaml
--- output ---
customresourcedefinition.apiextensions.k8s.io/certificates.networking.internal.knative.dev created
customresourcedefinition.apiextensions.k8s.io/configurations.serving.knative.dev created
customresourcedefinition.apiextensions.k8s.io/clusterdomainclaims.networking.internal.knative.dev created
customresourcedefinition.apiextensions.k8s.io/domainmappings.serving.knative.dev created
customresourcedefinition.apiextensions.k8s.io/ingresses.networking.internal.knative.dev created
customresourcedefinition.apiextensions.k8s.io/metrics.autoscaling.internal.knative.dev created
customresourcedefinition.apiextensions.k8s.io/podautoscalers.autoscaling.internal.knative.dev created
customresourcedefinition.apiextensions.k8s.io/revisions.serving.knative.dev created
customresourcedefinition.apiextensions.k8s.io/routes.serving.knative.dev created
customresourcedefinition.apiextensions.k8s.io/serverlessservices.networking.internal.knative.dev created
customresourcedefinition.apiextensions.k8s.io/services.serving.knative.dev created
customresourcedefinition.apiextensions.k8s.io/images.caching.internal.knative.dev created

$ kubectl apply -f https://github.com/knative/serving/releases/download/knative-v1.5.0/serving-core.yaml
--- output ---
namespace/knative-serving created
clusterrole.rbac.authorization.k8s.io/knative-serving-aggregated-addressable-resolver created
clusterrole.rbac.authorization.k8s.io/knative-serving-addressable-resolver created
clusterrole.rbac.authorization.k8s.io/knative-serving-namespaced-admin created
clusterrole.rbac.authorization.k8s.io/knative-serving-namespaced-edit created
clusterrole.rbac.authorization.k8s.io/knative-serving-namespaced-view created
clusterrole.rbac.authorization.k8s.io/knative-serving-core created
clusterrole.rbac.authorization.k8s.io/knative-serving-podspecable-binding created
serviceaccount/controller created
clusterrole.rbac.authorization.k8s.io/knative-serving-admin created
clusterrolebinding.rbac.authorization.k8s.io/knative-serving-controller-admin created
clusterrolebinding.rbac.authorization.k8s.io/knative-serving-controller-addressable-resolver created
customresourcedefinition.apiextensions.k8s.io/images.caching.internal.knative.dev unchanged
customresourcedefinition.apiextensions.k8s.io/certificates.networking.internal.knative.dev unchanged
customresourcedefinition.apiextensions.k8s.io/configurations.serving.knative.dev unchanged
customresourcedefinition.apiextensions.k8s.io/clusterdomainclaims.networking.internal.knative.dev unchanged
customresourcedefinition.apiextensions.k8s.io/domainmappings.serving.knative.dev unchanged
customresourcedefinition.apiextensions.k8s.io/ingresses.networking.internal.knative.dev unchanged
customresourcedefinition.apiextensions.k8s.io/metrics.autoscaling.internal.knative.dev unchanged
customresourcedefinition.apiextensions.k8s.io/podautoscalers.autoscaling.internal.knative.dev unchanged
customresourcedefinition.apiextensions.k8s.io/revisions.serving.knative.dev unchanged
customresourcedefinition.apiextensions.k8s.io/routes.serving.knative.dev unchanged
customresourcedefinition.apiextensions.k8s.io/serverlessservices.networking.internal.knative.dev unchanged
customresourcedefinition.apiextensions.k8s.io/services.serving.knative.dev unchanged
image.caching.internal.knative.dev/queue-proxy created
configmap/config-autoscaler created
configmap/config-defaults created
configmap/config-deployment created
configmap/config-domain created
configmap/config-features created
configmap/config-gc created
configmap/config-leader-election created
configmap/config-logging created
configmap/config-network created
configmap/config-observability created
configmap/config-tracing created
horizontalpodautoscaler.autoscaling/activator created
poddisruptionbudget.policy/activator-pdb created
deployment.apps/activator created
service/activator-service created
deployment.apps/autoscaler created
service/autoscaler created
deployment.apps/controller created
service/controller created
deployment.apps/domain-mapping created
deployment.apps/domainmapping-webhook created
service/domainmapping-webhook created
horizontalpodautoscaler.autoscaling/webhook created
poddisruptionbudget.policy/webhook-pdb created
deployment.apps/webhook created
service/webhook created
validatingwebhookconfiguration.admissionregistration.k8s.io/config.webhook.serving.knative.dev created
mutatingwebhookconfiguration.admissionregistration.k8s.io/webhook.serving.knative.dev created
mutatingwebhookconfiguration.admissionregistration.k8s.io/webhook.domainmapping.serving.knative.dev created
secret/domainmapping-webhook-certs created
validatingwebhookconfiguration.admissionregistration.k8s.io/validation.webhook.domainmapping.serving.knative.dev created
validatingwebhookconfiguration.admissionregistration.k8s.io/validation.webhook.serving.knative.dev created
secret/webhook-certs created

 
 
다음은 Isito 와 연동하기 위한 network 들을 설치한다.

$ kubectl apply -f https://github.com/knative/net-istio/releases/download/knative-v1.5.0/net-istio.yaml

 
 
그대로 설치하면 Isito gateway 와 연동되지 않는다. 그렇기 때문에 아래와 같이 selector 를 수정해 줘야 한다.

$ kubectl edit gateway -n knative-serving knative-ingress-gateway
...
spec:
  selector:
    istio: ingressgateway
    istio: ingress          # 추가

$ kubectl edit gateway -n knative-serving knative-local-gateway
...
spec:
  selector:
    istio: ingressgateway
    istio: ingress          # 추가

 
 

Istio 의 Ingress gateway 앞단에는 LoadBalancer 가 연결되어 있다. LoadBalancer 가 External IP 로 연결되어 있으면 IP 를 dns 로 연결해 주는 magic dns (sslip.io) 를 사용할 수 있고, LoadBalancer 가 domain name 으로 연결되어 있으면 실제 DNS 에 CNAME 을 등록하여 연결하면 된다.

$ kubectl get svc -n istio-ingress
NAME            TYPE           CLUSTER-IP       EXTERNAL-IP 
istio-ingress   LoadBalancer   10.107.111.229   xxxxx.ap-northeast-2.elb.amazonaws.com

 
 

여기서는 aws 를 사용하고 있기 때문에 Route53 에 CNAME 을 등록하였다.

서비스 도메인: helloworld-go-default.taco-cat.xyz
target: xxxxx.ap-northeast-2.elb.amazonaws.com
type: CNAME

 
 

Knative ConfigMap 설정

마지막으로 Knative ConfigMap 에 기본 도메인과 full 도메인 설정을 세팅한다.

이 설정은 앞서 Route53 에 등록한 서비스 도메인과 같은 형식으로 설정되게 구성해야 한다.

## Domain: taco-cat.xyz
$ kubectl edit cm config-domain -n knative-serving
apiVersion: v1
data:
  taco-cat.xyz: ""
kind: ConfigMap
[...]

## Name: helloworld-go
## Namesapce: default
## Domain: taco-cat.xyz
$ kubectl edit cm config-network -n knative-serving
apiVersion: v1
data:
  domain-template: "{{.Name}}-{{.Namespace}}.{{.Domain}}"

 
 

Knative sample 배포 테스트

Knative 에서 제공하는 helloworld-go 샘플 프로그램을 배포해 보자.

서비스 이름이 helloworld-go, Namespace 가 default 로 앞서 Route53 및 ConfigMap 에 설정한 도메인 형식과 동일함을 할 수 있다.

$ git clone https://github.com/knative/docs knative-docs
$ cd knative-docs/code-samples/serving/hello-world/helloworld-go

$ vi service.yaml
apiVersion: serving.knative.dev/v1
kind: Service
metadata:
  name: helloworld-go
  namespace: default
spec:
  template:
    spec:
      containers:
      - image: gcr.io/knative-samples/helloworld-go
        env:
        - name: TARGET
          value: "Go Sample v1"

$ kubectl apply -f service.yaml

 
 

배포가 제대로 되었는지 확인해 보자.

$ kubectl get route
NAME            URL                                         READY   REASON
helloworld-go   http://helloworld-go-default.taco-cat.xyz   True

 
 

Knative 는 zero replicas 를 사용한다

Serverless 를 어떻게 구현했을까? 사실 Knative 는 Kubernetes 의 zero replicas 를 사용했다.

배포 후에 deployment 를 조회하면 아래과 같이 Ready 와 Available 이 0 상태임을 알 수 있다.

$ kubectl get deploy
NAME                                       READY   UP-TO-DATE   AVAILABLE   AGE
helloworld-go-00001-deployment             0/0     0            0           10d

 
 

배포도 잘되고 Route53 에 dns 도 어느 정도 시간이 지났다면 브라우저 혹은 curl 로 확인할 수 있다.

$ curl http://helloworld-go-default.taco-cat.xyz
--- output ---
Hello Go Sample v1!

 
 

이렇게 요청이 들어오면 실제로 pod 가 실행되고 있음을 알 수 있다. 1분 동안 아무런 요청이 없으면 pod 는 다시 사라지고 대기 상태가 된다. (요청이 없더라도 중간에 다시 pod 가 생겨서 실제로는 일정 시간 동안 새로운 pod 로 교체된다)

$ kubectl get deploy
NAME                                       READY   UP-TO-DATE   AVAILABLE   AGE
helloworld-go-00001-deployment             1/1     1            1           10d

 
 

TLS 인증서 적용

인증서를 가지고 있다면 gateway 에 tls 를 적용하여 tls termination 을 할 수 있다.

아래는 istio-ingress 네임스페이스에 secret 으로 taco-cat-tls 인증서를 설치한 후 gateway 에서 해당 인증서를 읽을 수 있도록 tls 를 추가한 부분이다.

$ kubectl edit gateway knative-ingress-gateway -n knative-serving
...
spec:
  selector:
    istio: ingress
  servers:
  - hosts:
    - '*'
    port:
      name: http
      number: 80
      protocol: HTTP
  - hosts:
    - '*.taco-cat.xyz'
    port:
      name: https
      number: 443
      protocol: HTTPS
    ## tls 추가
    tls:
      mode: SIMPLE
      credentialName: taco-cat-tls

 
 

HPA 설치

서비스에 요청이 신규로 들어오거나, 많아지면 replicas 수를 조절하여 pod 를 실행해주는 activator 가 있다. 이 activator 를 auto scaling 하는 hpa 를 설치한다.

$ kubectl apply -f https://github.com/knative/serving/releases/download/knative-v1.5.0/serving-hpa.yaml

 
 

hpa 를 조회해서 확인할 수 있다.

$ kubectl get hpa -n knative-serving
NAME        REFERENCE              TARGETS   MINPODS   MAXPODS   REPLICAS   AGE
activator   Deployment/activator   0%/100%   1         20        1          13d
webhook     Deployment/webhook     3%/100%   1         5         1          13d

 
 

KServe 설치

KServe 는 이제 막 Helm chart 가 만들어지고 있다. 그렇기 때문에 일단은 yaml 로 설치를 진행한다.

먼저 KServe 컴포넌트를 설치하고 이어서 각종 ML Framework 를 나타내는 Runtime을 설치한다.

$ kubectl apply -f https://github.com/kserve/kserve/releases/download/v0.8.0/kserve.yaml
--- output ---
namespace/kserve created
customresourcedefinition.apiextensions.k8s.io/clusterservingruntimes.serving.kserve.io created
customresourcedefinition.apiextensions.k8s.io/inferenceservices.serving.kserve.io created
customresourcedefinition.apiextensions.k8s.io/servingruntimes.serving.kserve.io created
customresourcedefinition.apiextensions.k8s.io/trainedmodels.serving.kserve.io created
serviceaccount/kserve-controller-manager created
role.rbac.authorization.k8s.io/leader-election-role created
clusterrole.rbac.authorization.k8s.io/kserve-manager-role created
clusterrole.rbac.authorization.k8s.io/kserve-proxy-role created
rolebinding.rbac.authorization.k8s.io/leader-election-rolebinding created
clusterrolebinding.rbac.authorization.k8s.io/kserve-manager-rolebinding created
clusterrolebinding.rbac.authorization.k8s.io/kserve-proxy-rolebinding created
configmap/inferenceservice-config created
configmap/kserve-config created
secret/kserve-webhook-server-secret created
service/kserve-controller-manager-metrics-service created
service/kserve-controller-manager-service created
service/kserve-webhook-server-service created
statefulset.apps/kserve-controller-manager created
certificate.cert-manager.io/serving-cert created
issuer.cert-manager.io/selfsigned-issuer created
mutatingwebhookconfiguration.admissionregistration.k8s.io/inferenceservice.serving.kserve.io created
validatingwebhookconfiguration.admissionregistration.k8s.io/inferenceservice.serving.kserve.io created
validatingwebhookconfiguration.admissionregistration.k8s.io/trainedmodel.serving.kserve.io created

$ kubectl apply -f https://github.com/kserve/kserve/releases/download/v0.8.0/kserve-runtimes.yaml
--- output---
clusterservingruntime.serving.kserve.io/kserve-lgbserver created
clusterservingruntime.serving.kserve.io/kserve-mlserver created
clusterservingruntime.serving.kserve.io/kserve-paddleserver created
clusterservingruntime.serving.kserve.io/kserve-pmmlserver created
clusterservingruntime.serving.kserve.io/kserve-sklearnserver created
clusterservingruntime.serving.kserve.io/kserve-tensorflow-serving created
clusterservingruntime.serving.kserve.io/kserve-torchserve created
clusterservingruntime.serving.kserve.io/kserve-tritonserver created
clusterservingruntime.serving.kserve.io/kserve-xgbserver created

 
 

KServe 설치를 확인한다.

$ kubectl get pod -n kserve
NAME                          READY   STATUS    RESTARTS   AGE
kserve-controller-manager-0   2/2     Running   0          7d10h

 
 

Rumtime 도 설치되었는지 확인한다.

$ kubectl get clusterservingruntimes
NAME                        DISABLED   MODELTYPE    CONTAINERS         AGE
kserve-lgbserver                       lightgbm     kserve-container   7d10h
kserve-mlserver                        sklearn      kserve-container   7d10h
kserve-paddleserver                    paddle       kserve-container   7d10h
kserve-pmmlserver                      pmml         kserve-container   7d10h
kserve-sklearnserver                   sklearn      kserve-container   7d10h
kserve-tensorflow-serving              tensorflow   kserve-container   7d10h
kserve-torchserve                      pytorch      kserve-container   7d10h
kserve-tritonserver                    tensorrt     kserve-container   7d10h
kserve-xgbserver                       xgboost      kserve-container   7d10h

 
 

Sample model 을 KServe 를 활용하여 배포

tensorflow 로 개발된 mnist 샘플 모델을 KServe 로 배포해 보자.

KServe 는 model in load 패턴을 적용하여 인퍼런스 서비스를 수행한다. 아래에서는 모델이 gs 에 저장되어 있으면 이를 가져와서 서빙하는 구조이다.

runtime 은 앞서 설치한 clusterservingruntime 중에 하나인 kserve-tensorflow-serving 이고 버전이 2 임을 알 수 있다.

$ vi mnist_tensorflow.yaml
---
apiVersion: "serving.kserve.io/v1beta1"
kind: "InferenceService"
metadata:
  name: "mnist"
spec:
  predictor:
    model:
      modelFormat:
        name: tensorflow
        version: "2"
      storageUri: "gs://kserve/models/mnist"
      runtime: kserve-tensorflow-serving
    logger:
      mode: all

$ kubectl apply -f mnist_tensorflow.yaml

 
 

서빙 배포 확인은 다음과 같다.

KServe 에서 도메인을 만들 때 namespace 를 추가로 붙히기 때문에 도메인이 아래와

$ kubectl get isvc
NAME    URL     READY   PREV   LATEST   PREVROLLEDOUTREVISION  LATESTREADYREVISION
mnist   http://mnist-default.taco-cat.xyz   True           100 mnist-predictor-default-00001

$ kubectl get route
NAME                      URL                                                   READY   REASON
mnist-predictor-default   http://mnist-predictor-default-default.taco-cat.xyz   True

 
 

Route53 에 도메인을 추가한다.

서비스 도메인: mnist-predictor-default-default.taco-cat.xyz
target: xxxxx.ap-northeast-2.elb.amazonaws.com
type: CNAME

 
 

아래와 같이 요청하여 결과값이 제대로 나오는지 확인한다.

$ curl https://mnist-predictor-default-default.taco-cat.xyz/v1/models/mnist:predict \
   -H 'Content-Type: application/json' \
   -d @mnist.json
--- output ---
{
    "predictions": [[3.2338352e-09, 1.66207215e-09, 1.17224181e-06, 0.000114716699, 4.34008705e-13, 4.64885304e-08, 3.96761454e-13, 0.999883413, 1.21785089e-08, 6.44099089e-07]
    ]
}

 
 

반응형
Posted by seungkyua@gmail.com

댓글을 달아 주세요

반응형

Slice 와 Map 은 값을 여러개 자질 수 있는 타입이다. 그러나 선언한 타입과 같은 타입만 저장해야 하는 문제가 있다. 또한 class 와 같이 다양한 멤버 변수를 가질 수 없다.

 

그래서 Go 에서는 다양한 값을 가질 수 있도록 Struct 타입을 지원한다.

 

Struct

struct 의 를 만들려면 type 키워드, struct명, struct 키워드로 만든다. 그리고 그 안에 다양한 변수를 포함할 수 있으며 {} 로 묶어주면 된다.

 

type 으로 struct 를 선언했으면 그 자체가 하나의 type 이 된다. 그럼 var 로 새로운 만든 type을 활용하여 변수 선언을 할 수 있다. 아래와 같이 name 과 age 를 필드로 갖는 person 이라는 새로운 타입을 선언하고 jina 와 jamie 라는 변수를 만들 수 있다.

 

변수 선언만 한 struct 내의 필드들은 zero 값을 갖는다.

 

person 타입의 변수를 초기화 할 때는 {} 로 초기화 할 수 도 있다.

package main

import "fmt"

func main() {
	type person struct {
		name string
		age  int
	}

	var jina person
	fmt.Println(jina)

	jamie := person{}
	fmt.Println(jamie)
}

--- output ---
{ 0}
{ 0}

 

혹은 값을 지정하여 초기화 할 수 있다.

 

값을 지정하여 초기화할 때는 json 타입으로 변수:  으로 지정하거나  으로 지정할 수 있다. 변수 사이는 , 로 구분하며, 마지막 변수 끝에도 , 를 넣어야 한다.

 

Go 에서는 타입을 선언할 때는 { 앞에 공백을 넣지만, 값을 초기화할 때는 { 앞에 공백을 넣지 않는다. 또한 타입을 선언할 때는 필드 사이에 , 가 없지만, 값을 초기화 할 때는 필드값 사이에 , 를 넣는다.

jina := person{
	name: "Jina",
	age:  21,
}
fmt.Println(jina)

jamie := person{
	"Jamie",
	18,
}
fmt.Println(jamie)

--- output ---
{Jina 21}
{Jamie 18}

 

 

초기화는 아래와 같이 할 수 도 있다.

 

단, jina[”age”] 와 같이 인덱싱을 통한 접근은 지원하지 않는다.

var jina person
jina.name = "Jina"
jina.age = 21
// jina["age"] = 21  // 인덱싱은 지원하지 않음

fmt.Println(jina.name)

--- output ---
Jina

 

 

Anonymous struct

type 이 없는 struct 도 가능하다.

 

이 때는 type 키워드를 쓰지 않는다.

 

아래와 같이 바로 jina 라는 변수를 struct 형태로 지정하고 . 을 통해 값을 지정할 수 있다. 혹은 {} 를 사용하여 jamie 변수를 선언하면서 초기 값을 지정할 수 있다.

var jina struct {
		name string
		age  int
}

jina.name = "Jina"
jina.age = 21
fmt.Println(jina)

jamie := struct {
	name string
	age  int
}{
	name: "Jamie",
	age:  18,
}
fmt.Println(jamie)

--- output ---
{Jina 21}
{Jamie 18}

 

 

Struct 비교

go 에서는 type 이 같으면 대입(=), 비교(==) 를 할 수 있다. 하지만 type 이 다르면 비교나 대입을 할 수 없다.

 

하지만 type 을 convert 하여 맞춰 준다면 대입, 비교가 가능하다.

 

jina 와 jina2 는 type 이 같고 필드 값도 같다. 그래서 비교를 하면 true 값이 나온다.

 

하지만 jamie 는 jina 와 type 이 같지만 필드 값이 달라서 비교를 하면 false 값이 나온다.

jina := person{
	name: "Jina",
	age:  21,
}

jina2 := person{
	name: "Jina",
	age:  21,
}
fmt.Println(jina == jina2)

jamie := person{
	name: "Jamie",
	age:  18,
}
fmt.Println(jina == jamie)

--- output ---
true
false

 

 

 

새로운 anotherPerson type 을 만들어서 살펴보자.

 

아래와 같이 jina 는 person type 이고 jamie 는 anotherPerson type 이므로 서로 type 이 달라서 비교나 대입을 하면 에러가 난다.

type anotherPerson struct {
	name string
	age  int
}

jina := person{
	name: "Jina",
	age:  21,
}
fmt.Println(jina)

jamie := anotherPerson{
	name: "Jamie",
	age:  18,
}

fmt.Println(jina == jamie)

--- output ---
./main.go:88:19: invalid operation: jina == jamie (mismatched types person and anotherPerson)

 

 

 

이번에는 jamie 를 person type 으로 바꾸는 type converson 을 하여 비교하여 보자.

 

타입 변환도 잘 되고 값도 정상적으로 대입되는 것을 알 수 있다.

type anotherPerson struct {
	name string
	age  int
}

jina := person{
	name: "Jina",
	age:  21,
}
fmt.Println(jina)

jamie := anotherPerson{
	name: "Jamie",
	age:  18,
}

jina = person(jamie)
fmt.Println(jina)

--- output ---
{Jina 21}
{Jamie 18}

 

 

 

한가지 중요한 점은 struct 사이에 타입 변환은 타입, 필드명, 필드 순서, 필드 갯수 가 모두 동일해야 가능하다.

 

그럼 type 으로 선언된 struct 와 anonymous struct 는 타입을 제외한 필드명, 필드 순서, 필드 갯수 가 같다면 대입이나 비교, 타입 변환이 가능할까?

 

정답은 가능하다이다.

jina := person{
	name: "Jina",
	age:  21,
}
fmt.Println(jina)

var jamie struct {
	name string
	age  int
}

jamie = jina
fmt.Println(jina == jamie)

--- output ---
{Jina 21}
true

 

jamie := struct {
	name string
	age  int
}{
	name: "Jamie",
	age:  18,
}

var jina person

jina = person(jamie)
fmt.Println(jina)
fmt.Println(jina == jamie)

--- output --
{Jamie 18}
true

 

 

사실 person(jamie) 로 타입 변환을 하지 않아도 정상적으로 동작한다.

반응형
Posted by seungkyua@gmail.com
TAG go, golang, struct

댓글을 달아 주세요

반응형

 

Map 은 key, value 로 저장할 수 있는 타입이다. HashMap 을 사용하기 때문에 key 는 유일한 값으로 key 를 사용하면 value 를 바로 찾아서 쓸 수 있다.

앞서 slice 와 동일하게 zero 값은 nil 이다. 그러므로 map 변수를 선언만 하면 nil 값으로 비교할 수 있다. slice 는 nil 값이라도 append 를 사용하면 값을 추가하면서 새로운 slice 가 생성되어 return 되었다. 하지만 map 은 nil 값에는 어떠한 key, value 도 추가할 수 있다.

 

Map

map 은 key, value 쌍으로 이루어진 데이터 구조이다. 파이썬에서는 dictionary 와 비슷하다고 생각하면 된다. 단지 다른 것은 하나의 map 에는 사전에 지정된 타입의 key, value 만 저장될 수 있다.

다음은 string 타입을 key 로하고 int 를 value 로 저장하는 map 을 선언한 것이다. map[key타입]value타입 으로 선언할 수 있다.

이렇게 선언만 한 map 변수의 zero 값은 nil 이다.

var m map[string]int
fmt.Println(m == nil)

--- output ---
true

 

이렇게 nil 값이 상태의 map 에는 key, value 를 추가할 수 없다(slice 와는 다르다).

var m map[string]int
m["one"] = 0

--- output ---
panic: assignment to entry in nil map

 

아래와 같이 empty map 을 생성할 수 있으며, 여기에는 key, value 를 추가할 수 있다.

var m1 = map[string]int{}
fmt.Println(m1 == nil)
m1["one"] = 0
fmt.Println(m1)

--- output ---
false
map[one:0]

 

초기 값을 아래와 같이 생성할 수 도 있다. 초기화 할 때 key, 와 value 는 : 로 구분하고 연속적으로 key, value 를 입력하기 위해서는 , 구분자를 사용한다.

m2 := map[string]int{
    "one":  1,
    "two": 2,
}
fmt.Println(m2)

--- output ---
map[one:1 two:2]

 

make 함수

slice 와 동일하게 make 내장 함수를 사용하여 empty map 을 생성할 수 있다. 이렇게 생성된 map 은 길이는 0, 사이즈는 5 가 되고, 길이가 사이즈 보다 커질 때에도 사이즈가 확장되어 key, value 를 추가할 수 있다. (사이즈 확장은 때에 따라 성능 감소의 요인이 될 수 있다)

출력 결과와 같이 map 에 추가된 key, value 는 순서가 없다.

m := make(map[string]int, 5)
fmt.Println(len(m))

m["one"] = 1
m["two"] = 2
m["three"] = 3
m["four"] = 4
m["five"] = 5
fmt.Println(m, len(m))

m["six"] = 6
fmt.Println(m, len(m))

--- output ---
0
map[five:5 four:4 one:1 three:3 two:2] 5
map[five:5 four:4 one:1 six:6 three:3 two:2] 6

 

comma ok idiom

map 에서 없는 key 값으로 value 를 요청하면 value 타입의 zero 값이 리턴된다. 즉 에러가 발생하지 않는다.

m := map[string]int{
    "one": 1,
    "two": 2,
}
fmt.Println(m["three"])

--- output ---
0

 

그렇기 때문에 해당 key 값의 value 가 제대로 넘어왔는지 알 수 있게 , 로 구분되어 value 와 boolen 값이 리턴된다.

m := map[string]int{
    "one": 1,
    "two": 2,
}

v, ok := m["one"]
fmt.Println(v, ok)

v, ok = m["three"]
fmt.Println(v, ok)

--- output ---
1 true
0 false

 

map 의 item 을 삭제하려면 delete 내장 함수 를 사용한다. delete 함수에 첫번째 아규먼트에는 map 을, 두번째 아규먼트에는 key 를 넣으면 된다. 해당 key 가 map 에 없다고 해서 에러를 만들지는 않는다.

m := map[string]int{
    "one": 1,
    "two": 2,
}

delete(m, "one")
fmt.Println(m)
delete(m, "three")

--- output ---
map[two:2]

 

map 의 key 는 중복될 수 없다. key, value 를 추가하는데 기존의 map 에 key 가 존재한다면, key 에 해당 하는 value 값이 업데이트 된다.

Go 에는 Set 타입이 없는데 참고로 key 가 중복될 수 없다는 이 특징을 잘 활용하면 Set 을 구현할 수 있다.

m := map[string]int{
    "one": 1,
    "two": 2,
}
m["two"] = 3
fmt.Println(m)

--- output ---
map[one:1 two:3]

 

 

반응형
Posted by seungkyua@gmail.com

댓글을 달아 주세요

반응형

 

 

기본 내장 타입에 추가하여 Array, Slice, Map 타입에 대해서 살펴본다.

Go 에서는 Array 는 거의 사용하지 않는다. 그러므로 이번에는 Slice 와 Map 타입에 대해서만 살펴 본다.

참고로 Go 에서는 nil 이라는 값이 있다. 이는 값이 없다라는 뜻인데, 기본 타입에는 이 nil 과의 비교문 (==)을 쓸 수 없다. (이전에 기본 타입은 전부 zero 값이 nil 이 아니다.)

**nil 과 비교문이 가능한 타입은 앞으로 설명할 slice, map, pointer 이며, 이 3개의 zero 값이 nil 이다.**

 

Slice

slice 는 값을 순차적으로 가질 수 있는 데이터 구조이다. 파이썬에서는 list 와 비슷하다고 생각하면 된다. 단지 다른 것은 하나의 slice 에는 사전에 지정된 타입만 저장될 수 있다.

다음은 int 타입을 slice 저장 한 후에 출력한 결과이다. [] 는 slice 를 만들겠다는 의미이고, 바로 이어서 type 이 오며, 초기값을 지정하기 위해서 { } 를 사용한다.

var s = []int{1, 2, 3, 4, 5}
fmt.Println(s)

--- output ---
[1 2 3 4 5]

 

slice 는 초기화 값을 지정하지 않으면 nil 을 나타낸다. 즉, 변수 선언만 하면 항상 nil 값을 갖게 된다. lencap 을 사용하면 slice 가 가지가 있는 길이와 용량을 알 수 있다.

Slice 는 연속적인 값을 가지고 있는 메모리 주소인 포인터, 길이, 용량 으로 구성되어 있다. (이 부분은 매우 중요하다.)

길이는 실제 들어있는 값이고 용량은 해당 값을 가질 수 있는 크기라고 할 수 있다.

아래 s1 은 nil 값이기 때문에 len 과 cap 이 모두 0 이다.

var s1 []int
fmt.Println(s1 == nil)
fmt.Println(len(s1), cap(s1))

--- output ---
true
0 0

 

슬라이스는 배열처럼 사용하여 s1[0] 첫번째 값을 가져올 수 있다.

하지만 선언만 하여 nil 인 상태에서는 해당 index 를 사용하게 되면 런타임 에러가 난다.

var s1 []int
fmt.Println(s1[0])

--- output ---
panic: runtime error: index out of range [0] with length 0

 

s1 slice 가 nil 인 상태에서도 append 내장 함수를 사용하면 값을 추가할 수 있다(그렇기 때문에 특별한 경우를 제외하고는 nil 값인 변수 선언만 하여 사용하면 된다). append 함수를 쓰면 nil 이라 하더라도 slice 에 값을 추가 할 수 있다. 물론 값이 추가된 결과 return 값은 변수에 할당해야 한다. (변수가 달라도 됨)

var s1 []int
s1 = append(s1, 1)
fmt.Println(s1)

--- output ---
[1]

 

slice 에 slice 를 합쳐서 추가할 수 있다. python 의 extend 와 비슷하다고 보면된다. 두번째 slice 에는 여러 아규먼트가 들어간다는 것을 표현해 주기 위해서 ... 을 붙혀야 한다.

var s1 []int
s1 = append(s1, 1)

s2 := []int{2, 3, 4, 5}
s1 = append(s1, s2...)  // append(s1, 2, 3, 4, 5) 와 동일함
fmt.Println(s1)

 

slice 는 값이 추가되어 길이가 커져서 용량에 도달하면 용량이 자동으로 확장된다.

var s []int
fmt.Println(s, len(s), cap(s))
s = append(s, 1)
fmt.Println(s, len(s), cap(s))
s = append(s, 2)
fmt.Println(s, len(s), cap(s))
s = append(s, 3)
fmt.Println(s, len(s), cap(s))
s = append(s, 4)
fmt.Println(s, len(s), cap(s))
s3 := append(s, 5)
fmt.Println("s:", s, len(s), cap(s))
fmt.Println("s3", s3, len(s3), cap(s3))

-- output ---
[] 0 0
[1] 1 1
[1 2] 2 2
[1 2 3] 3 4
[1 2 3 4] 4 4
s: [1 2 3 4] 4 4
s3 [1 2 3 4 5] 5 8

 

 

make 내장 함수

slice 와 map 에서 make 내장 함수를 사용하면 nil 이 아닌 empty slice 를 만들 수 있다. make 함수의 첫번째 아큐먼트는 slice type, 두번째는 길이, 세번째는 용량이다. 세번째 용량은 생략이 가능하다.

아래 make 로 생성한 s 는 nil 과의 비교가 false 임을 알 수 있다.

s := make([]int, 0, 5)
fmt.Println(s == nil)
fmt.Println(s, len(s), cap(s))

--- output ---
false
[] 0 5

 

make 로 길이를 지정하면 그 길이 만큼은 zero 값이 채워진 slice 가 만들어 진다. 그러니 필요없이 길이를 1 이상으로 지정하면 초기 메모리만 차지할 수 있다. 아래는 make 로 만든 s1 slice 에 0번째와 1번째 index 까지 0 값이 채워진 것을 볼 수 있다.

s1 := make([]int, 2)
fmt.Println(s1)

s1 = append(s1, 1)
fmt.Println(s1)

--- output ---
[0 0]
[0 0 1]

 

길이가 0 인 slice 는 아래와 같이 만들 수 도 있다. nil 이 아니면서 길이가 0 인 slice 는 JSON 으로 변환할 때 사용된다.

var s = []int{}   // make([]int, 0) 와 동일하다.

 

 

Slice 자르기

Slice 를 자르는 것은 : 을 이용해서 자를 수 있다. 하지만 자른다고 해도 최종적으로 보는 Reference 는 동일하다. 아래를 보면 이를 이해할 수 있다.

첫번째 단락에서 slice 를 자르면, b 는 0 index 이상, 3 index 미만 까지의 slice 를 갖고, c 는 2 index 이상 끝까지의 slice 값을 갖는다. 중요한 것은 c 처럼 중간에서 마지막까지 자른 slice 는 용량이 그 길이만큼으로 줄어든다는 것이다. (앞의 자른 부분이 없어졌기 때문이다)

두번째 단락에서 c[0] 에 30 을 대입하면 slice 자른 것은 같은 reference 를 바라보기 때문에 a 와 b 에 모두 영향을 준다.

세번째 단락도 마찬가지 이다.

a := []int{1, 2, 3, 4, 5}
b := a[:3]
c := a[2:]
fmt.Println("cap(a):", cap(a), "cap(b):", cap(b), "cap(c):", cap(c))
fmt.Println("a:", a)
fmt.Println("b:", b)
fmt.Println("c:", c)
fmt.Println("================")

c[0] = 30
fmt.Println("a:", a)
fmt.Println("b:", b)
fmt.Println("c:", c)
fmt.Println("================")

b = append(b, 40)
fmt.Println("a:", a)
fmt.Println("b:", b)
fmt.Println("c:", c)

--- output ---
cap(a): 5 cap(b): 5 cap(c): 3
a: [1 2 3 4 5]
b: [1 2 3]
c: [3 4 5]
================
a: [1 2 30 4 5]
b: [1 2 30]
c: [30 4 5]
================
a: [1 2 30 40 5]
b: [1 2 30 40]
c: [30 40 5]

 

Slice 를 자른 것이 append 를 사용해서 추가할 때 같은 레퍼런스를 처다보지 않게 하려면, 용량을 길이와 동일하게 하면 된다. 그러면 append 했을 경우 용량이 초과되어 새로운 slice 가 생겨서 return 되므로 서로 영향이 가지 않는다.

아래 처럼 a 를 잘라서 b 를 만들 때 :3:3 과 같이 마지막에 용량 3을 추가하여 b slice 의 길이와 용량을 똑같이 맞췄다. 그렇게 되면 c[0] 처럼 값을 바꾸는 것은 영향을 받지만, append(b, 40) 과 같이 append 한 것은 새로운 slice 가 만들어져서 return 되므로 a 와 c 에 영향을 주지 않는다.

a := []int{1, 2, 3, 4, 5}
b := a[:3:3]
c := a[2:]
fmt.Println("cap(a):", cap(a), "cap(b):", cap(b), "cap(c):", cap(c))
fmt.Println("a:", a)
fmt.Println("b:", b)
fmt.Println("c:", c)

fmt.Println("===== (c[0] = 30) =====")
c[0] = 30
fmt.Println("cap(a):", cap(a), "cap(b):", cap(b), "cap(c):", cap(c))
fmt.Println("a:", a)
fmt.Println("b:", b)
fmt.Println("c:", c)

fmt.Println("===== append(b, 40) ======")
b = append(b, 40)
b[0] = 10
fmt.Println("cap(a):", cap(a), "cap(b):", cap(b), "cap(c):", cap(c))
fmt.Println("a:", a)
fmt.Println("b:", b)
fmt.Println("c:", c)


--- output ---
cap(a): 5 cap(b): 3 cap(c): 3
a: [1 2 3 4 5]
b: [1 2 3]
c: [3 4 5]
===== (c[0] = 30) =====
cap(a): 5 cap(b): 3 cap(c): 3
a: [1 2 30 4 5]
b: [1 2 30]
c: [30 4 5]
===== append(b, 40) ======
cap(a): 5 cap(b): 6 cap(c): 3
a: [1 2 30 4 5]
b: [10 2 30 40]
c: [30 4 5]
c: [30 4 5]
===== append(b, 40) ======
a: [1 2 30 4 5]
b: [1 2 30 40]
c: [30 4 5]

 

반응형
Posted by seungkyua@gmail.com
TAG go, nil, slice

댓글을 달아 주세요

반응형

 

 

C 와 Java 를 안다면 Go 를 공부하면 헷갈리는 부분들이 다소 있다. 일반적으로 기본 내장 타입은 비슷해서 이해하기 쉽지만 pointer 와 interface 등을 공부하다 보면 헷갈리기 마련이다.

일단 먼저 기본 타입에 대해서 설명한다.

 

Built-in Type

기본 내장 타입에는 boolean, integer, rune, float, byte, string 이 있다. 물론 complex type 도 있지만 이는 생략하겠다. 기본 내장 타입은 zero 값을 갖고 있다.

이 중에서 숫자 타입은 integer, rune, float 이다.

 

boolean

boolean 의 zero 값은 false 이다.

var isDev bool
var isTrue = true

fmt.Println(isDev)
fmt.Println(isTrue)

--- output ---
false
true

변수 선언은 var 변수명 타입 으로 선언할 수 있다. 이렇게 설명하면 zero 값으로 false 가 자동 대입된다.

다른 방법은 var 변수명 = 값 과 같이 변수를 선언하면서 값을 대입하는 것이다. 그럼 값을 보고 알아서 타입을 결정해 준다.

 

Integer (숫자 타입 중 하나)

Integer type 은 int8, int16, int32, int64, uint8, uint16, uint32, uint64 이다. 그리고 특별한 integer 타입으로 byterune 타입이 있다.

byteuint8 의 별칭으로 unit8 보다는 byte 를 주로 쓰게 된다.

runeint32 의 별칭이다.

보통 Integer Type 은 int 로 많이 사용하는데, 32bit cpu 이면 int32, 64bit cpu 이면 int64 와 동일하다.

int 의 zero 값은 0 이다.

var x int
var y int = 1
var z = 2
fmt.Println(x)
fmt.Println(y)
fmt.Println(z)

--- output ---
0
1
2

 

Float (숫자 타입 중 하나)

Float type 은 float32, float64 이 있다.

float type 의 zero 값은 int 와 동일하게 0 이다.

var p float64
var q = 0.0
fmt.Println(p)
fmt.Println(q)

--- output ---
0
0

숫자 타입 간에는 편하게 해당 타입으로 변환이 가능하다.

var x int = 10
var p float64 = 20.2
fmt.Println(x + int(p))
fmt.Println(float64(x) + p)

--- output ---
30
30.2

 

String

string 의 zero 값은 empty string 이다. Java 에서는 null string 이 있지만 Go 에서는 null 이 아니다.

string 과 자주 사용되는 타입은 runebyte 이다.

앞에서 숫자 타입은 서로 타입 변환이 해당 타입으로 감싸주면 된다고 설명했는데 string, rune, byte 도 서로 타입 변환이 가능하다.

Go 최신 버전에서는 숫자 타입을 단순히 string 으로 감싸는 것은 체크 시에 에러가 발생한다.

var r rune = 'r'
var s string = string(r)
var b byte = 'b'
var s2 string = string(b)
fmt.Println(r)
fmt.Println(b)
fmt.Println(s)
fmt.Println(s2)

--- output ---
114
98
r
b

runebyteint 타입의 별칭이라 숫자 값이 출력된 것을 이해하자.

나중에 설명하겠지만 [] 를 사용하여 slice (배열과 비슷한) 타입을 활용하면 string[]byte 혹은 []rune 으로 변환할 수 있다.

var s string = "Hello, 😀"
var bs []byte = []byte(s)
var rs []rune = []rune(s)
fmt.Println(bs)
fmt.Println(rs)

--- output ---
[72 101 108 108 111 44 32 240 159 152 128]
[72 101 108 108 111 44 32 128512]

😀 이모지 값은 utf-8 으로 표시해야 되기 때문에 byte 는 [240 159 152 128] 와 같이 4 바이트로 표시되고 rune 은 [128512] 로 표시된다. byte 는 int8 이고, rune 은 int32 임을 기억하자.

이렇게 보면 string 은 rune 으로 변환하는 것이 편리하다.

마지막으로 Short Variable Delcaration Operator(:=) 를 사용하여 vartype 키워드 없이 변수를 선언하고 바로 할당할 수 있다.

var a, b int = 1, 2
var c, d = 3, "a"

i := 10
x, s := 20.5, "Hello World"

--- output ---
1 2 3 a
10 20.5 Hello World

 

 

반응형
Posted by seungkyua@gmail.com
TAG go, golang, type

댓글을 달아 주세요

반응형

 

 

Kubernetes source code 를 디버깅하면서 분석하기 위해 golang 환경 설정과 사용을 계속해서 설명하고자 한다.

 

오늘은 첫번째로 무료 IDE 인 (thx MS) VSCode 를 설치하고 세팅하는 법을 알아보자.

golang 설치

mac 에서 golang 은 home brew 로 간단히 설치할 수 있다. 혹시 라도 이전에 낮은 버전이 설치되어 있는지 확인한다.

$ brew list | grep go
$ go@1.12

$ go version
go version go1.12.9 darwin/amd64

새로운 버전을 install 한다.

$ brew install go@1.16

현재 1.12 버전을 unlink 한다.

$ brew unlink go

새로운 버전을 link 한다.

$ brew link --force go@1.16

새로운 버전을 check 하면 업그레이드 되어 있는 것을 알 수 있다.

$ go version
go version go1.16.14 darwin/amd64

Go Workspace

go 는 install 명령어로 다른 go tool 들을 설치할 수 있다. 설치 위치는 workspace 라고 부르는 곳인데 특별히 지정하지 않으면 디폴트로 $HOME/go 디렉토리에 설치 된다.

 

go install 로 설치된 툴들은 소스는 $HOME/go/src 에, 바이너리 실행파일은 $HOME/go/bin 에 설치된다.

 

workspace 는 변경할 수 있는데 아래와 같이 .zshrc (zsh 일 경우) 이나 .bash_profile (bash 일 경우) 파일에 환경 변수를 추가하면 된다. 설정하기 전에 해당 workspace 디렉토리는 미리 만들어 줘야 한다.

## 디렉토리 만들기
$ mkdir -p $HOME/Documents/go_workspace

## .zshrc 파일에 환경변수 세팅
export GOPATH=$HOME/Documents/go_workspace
export PATH=$PATH:$GOPATH/bin

Go Tool 설치

코드를 체크해 주는 golint 툴이 있다. 이를 설치해 보자

$ go install golang.org/x/lint/golint@latest

$ which golint
/Users/ahnsk/Documents/go_workspace/bin/lint

특정 디렉토리에서 lint 체크를 하고 싶으면 아래 명령어를 쓰면 된다. 코드를 만든 프로젝트 디렉토리에서 수행하면 모든 파일을 lint 한다.

$ golint ./...

그리고 go 를 설치하면 일부 tool 들은 자동으로 설치되는데 그 중 vet 이라는 유용한 tool 있다. vet 은 error 를 잡아주는 tool 이다.

$ go vet ./...

VS Code 설치

VS Code 는 아래 url 에 접속해서 OS 별로 설치하면 된다. 설치 화면은 생략한다.

 

https://code.visualstudio.com/Download

 

VS Code 를 띄우고 나서 아무 화면에서나 Cmd+Shift+P 키를 입력하면 Command Palette 가 화면 위에 생성된다. 거기에서 shell command 로 검색하여 install ‘code’ command in PATH 를 선택한다.

 

 

이렇게 설치한 후 터미널을 새롭게 띄워서 특정 디렉토리에서 code 명령어를 이력하면 VS Code 가 실행된다.

 

아래와 같이 go-sample 디렉토리에서 실행하면 VS Code 가 뜨는 것을 볼 수 있다.

$ cd go-sample
$ code

VS Code Extension 설치

VS Code 는 다양한 Extension 을 설치할 수 있다. 그 중에 go 를 써야 하니 Go extension 을 설치해 보자.

 

VS Code 화면에 보면 맨 좌측에 세로로 아이콘들이 여러개 있다. 그 중 위에서 5번째 혹은 6번째에 있는 extension 아이콘을 선택하고 go 를 검색해서 go Team at Google 에서 만든 Go 를 설치해 준다.

 

아래 그림에서 맨 처음 줄의 Go 의 install 파란 버튼을 클릭해서 설치한다.

 

 

이제 기본 설치는 끝났다.

 

hello world 를 출력하는 간단한 소스를 만들어서 실행해 보자.

 

VS Code 에서 새로운 파일을 생성해서 main.go 파일을 만들어서 저장한다.

package main

import "fmt"

func main() {
	fmt.Println("Hello world!!!")
}

VS Code 상단 메뉴에 Terminal >> New Terminal 을 클릭하면 터미널을 VS Code 에서 사용할 수 있다.

$ go run main.go
Hello world!!!

VS Code 화면을 보면 다음과 같다.

 

 

이제 세팅은 모두 끝났다. 디버깅 세팅하는 방법이 있는데 go 를 설명하다가 어느 정도 지나면 디버깅 세팅도 추가로 올릴 예정이다.

반응형
Posted by seungkyua@gmail.com

댓글을 달아 주세요

반응형
.
├── config
│   ├── __init__.py
│   ├── __pycache__
│   │   ├── __init__.cpython-39.pyc
│   │   ├── settings.cpython-39.pyc
│   │   ├── urls.cpython-39.pyc
│   │   ├── views.cpython-39.pyc
│   │   └── wsgi.cpython-39.pyc
│   ├── asgi.py
│   ├── settings.py
│   ├── urls.py
│   ├── views.py
│   └── wsgi.py
├── db.sqlite3
├── firstapp
│   ├── __init__.py
│   ├── __pycache__
│   │   ├── __init__.cpython-39.pyc
│   │   ├── urls.cpython-39.pyc
│   │   └── views.cpython-39.pyc
│   ├── admin.py
│   ├── apps.py
│   ├── migrations
│   │   └── __init__.py
│   ├── models.py
│   ├── tests.py
│   ├── urls.py
│   └── views.py
└── manage.py

위와 같이 django 디렉토리가 있을 때 package, module import 하는 방법이 헷갈려서 정리하는 차원이다.

 

바로 하위 디렉토리 config 와 firstapp 은 패키지 이다.

그리고 config 아래의 urls.py 와 같은 python 파일은 모듈이다.

 

import 방법은 다음과 같다.

 

 

import {패키지}.{모듈}

import {패키지}.{모듈}.{함수}

 

from {패지키} import {모듈}

from {패키지}.{모듈} import {함수}

 

 

 

 

 

반응형
Posted by seungkyua@gmail.com

댓글을 달아 주세요