Building a Kubernetes cluster on Google Cloud [GCP LAB]

Published: 2021-09-30 07:37:50

### Introduction to Docker

Docker container를 GCP에서 run, build, debug하는지에 관한 실습. 이 실습에서는 

- docker hub / Google container registry에 어떻게 docker 이미지를 풀하는지
- GCR 에서부터 docker images를 어떻게 push받는지

를 실습했다. 

1. Dockerfile 만들기

![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/599ec413-5bc4-4a0a-b25d-07360e89c03f/Untitled.png)

Dockerfile을 만드는데 스펙을 다음과 같이 만듦

```docker
cat > Dockerfile << EOF

# base parent이미지를 명시한다 (이 경우에는 node version 6라고 명시함)
FROM node:6
# Set the working directory in the container to /app (container실행경로가 /app으로 하도록 작업경로 설정)
WORKDIR /app 

# Copy the current directory contents into the container at /app
ADD . /app (현재 작업경로의 컨텐츠를 container에 add함)

# Make the container's port 80 available to the outside world
EXPOSE 80 (container 포트 노출)

# Run app.js using node when the container launches
CMD ["node", "app.js"]
EOF
```

1. Node application 만들기

80번 포트로 접근할 수 있는 hello world 출력하는 간단 앱을 만든다.

```yaml
cat > app.js <<EOF
const http = require('http');
const hostname = '0.0.0.0';
const port = 80;
const server = http.createServer((req, res) => {
    res.statusCode = 200;
      res.setHeader('Content-Type', 'text/plain');
        res.end('Hello World\n');
});
server.listen(port, hostname, () => {
    console.log('Server running at http://%s:%s/', hostname, port);
});
process.on('SIGINT', function() {
    console.log('Caught interrupt signal and will exit');
    process.exit();
});
EOF
```

docker images 명령어로 확인해보면 0.1버전으로 도커파일이 명시되어 있다

```yaml
student_04_13d47ca1e9b3@cloudshell:~/test (qwiklabs-gcp-00-6a9fb388c3c3)$ docker images
REPOSITORY    TAG       IMAGE ID       CREATED          SIZE
node-app      0.1       4da28894f75a   39 seconds ago   884MB
hello-world   latest    d1165f221234   6 months ago     13.3kB
node          6         ab290b853066   2 years ago      884MB
```

다른 터미널을 열고, curl 로 요청했을 때의 결과

```yaml
student_04_13d47ca1e9b3@cloudshell:~ (qwiklabs-gcp-00-6a9fb388c3c3)$ curl http://localhost:4000
Hello World
```

docker bash에 접근하고 싶을 때는? (아까 /app 디렉토리에서 컨테이너를 실행한다고 명시했기 때문에 이 경로로 접속되는 걸 확인할 수 있음) 

`student_04_13d47ca1e9b3@cloudshell:~/test (qwiklabs-gcp-00-6a9fb388c3c3)$ docker exec -it e9c73fffe77f bash`

1. 이 이미지를 GCR(Google Container Registry) 로 Publish하기 

이미지를 GCR에게 호스팅되는 private registry로 push하려면, 이미지 registry name과 함께 태깅을 꼭 해야한다. The format is `[hostname]/[project-id]/[image]:[tag].` 

Tag와 이어서 gcr에 push까지 하는 cmd는 다음과 같다.

```yaml
student_04_13d47ca1e9b3@cloudshell:~/test (qwiklabs-gcp-00-6a9fb388c3c3)$ docker tag node-app:0.2 gcr.io/qwiklabs-gcp-00-6a9fb388c3c3/node-app:0.2

student_04_13d47ca1e9b3@cloudshell:~/test (qwiklabs-gcp-00-6a9fb388c3c3)$ docker push gcr.io/qwiklabs-gcp-00-6a9fb388c3c3/node-app:0.2
The push refers to repository [gcr.io/qwiklabs-gcp-00-6a9fb388c3c3/node-app]
cf85c86000d0: Pushed
8c8e6d8018ef: Pushed
f39151891503: Pushed
f1965d3c206f: Pushed
a27518e43e49: Layer already exists
910d7fd9e23e: Layer already exists
4230ff7f2288: Layer already exists
2c719774c1e1: Layer already exists
ec62f19bb3aa: Layer already exists
f94641f1fe1f: Layer already exists
```

기존 존재하던 docker를 모두 제거해준 후 GCR에서 image를 pull해본다

```yaml
student_04_13d47ca1e9b3@cloudshell:~/test (qwiklabs-gcp-00-6a9fb388c3c3)$ docker pull gcr.io/qwiklabs-gcp-00-6a9fb388c3c3/node-app:0.2
0.2: Pulling from qwiklabs-gcp-00-6a9fb388c3c3/node-app
c5e155d5a1d1: Pull complete
221d80d00ae9: Pull complete
4250b3117dca: Pull complete
3b7ca19181b2: Pull complete
425d7b2a5bcc: Pull complete
69df12c70287: Pull complete
ea2f5386a42d: Pull complete
d421d2b3c5eb: Pull complete
3d3f5cc96fd9: Pull complete
dfc21c45a13c: Pull complete
Digest: sha256:928d65b1be2111de18666af5e150cdd94bf62d631f4b8881928cb768f96d59c1
Status: Downloaded newer image for gcr.io/qwiklabs-gcp-00-6a9fb388c3c3/node-app:0.2
gcr.io/qwiklabs-gcp-00-6a9fb388c3c3/node-app:0.2
```

pull한 이미지를 해당 포트로 실행하면

```yaml
student_04_13d47ca1e9b3@cloudshell:~ (qwiklabs-gcp-00-6a9fb388c3c3)$ curl http://localhost:4000
Jiyoon
```

야호! GCR에서 이미지 땡겨와서 컨테이너 실행하기 quest 완성

### Kubernetes Engine: Qwik Start / GCP에서 Kubernetes Engine Cluster 구축해보기

다음 퀘스트는 GKE에서 클러스터를 orchestration하기 위한 kubernetes engine cluster를 구축한다.

Shell을 이용해 GKE Cluster를 구축해본다.

클러스터 creating 전에 먼저 default compute zone을 설정하는데

`gcloud config set compute/zone us-central1-a` 로 us-central / Zone A로 설정했다.

```yaml
student_00_6b130b2897a7@cloudshell:~ (qwiklabs-gcp-04-ba9754128013)$ gcloud container clusters create jiyoon-cluster

WARNING: Your Pod address range (`--cluster-ipv4-cidr`) can accommodate at most 1008 node(s).
WARNING: Starting with version 1.19, newly created clusters and node-pools will have COS_CONTAINERD as the default node image when no image type is specified.
Creating cluster jiyoon-cluster in us-central1-a...done.     
Created [https://container.googleapis.com/v1/projects/qwiklabs-gcp-01-500142021c34/zones/us-central1-a/clusters/jiyoon-cluster].
To inspect the contents of your cluster, go to: https://console.cloud.google.com/kubernetes/workload_/gcloud/us-central1-a/jiyoon-cluster?project=qwiklabs-gcp-01-500142021c34
kubeconfig entry generated for jiyoon-cluster.
NAME: jiyoon-cluster
LOCATION: us-central1-a
MASTER_VERSION: 1.20.9-gke.701
MASTER_IP: 104.154.182.127
MACHINE_TYPE: e2-medium
NODE_VERSION: 1.20.9-gke.701
NUM_NODES: 3
STATUS: RUNNING
```

1. 클러스터 Access auth credentials 받기

```yaml
gcloud container clusters get-credentials [CLUSTER-NAME]
```

1. 내 클러스터에 App deploy하기

You can now deploy a containerized application to the cluster. For this lab, you'll run hello-app in your cluster.

```yaml
student_00_df37a4b73fa7@cloudshell:~ (qwiklabs-gcp-01-500142021c34)$ kubectl create deployment hello-server --image=gcr.io/google-samples/hello-app:1.0

deployment.apps/hello-server created
```

To create a Kubernetes Service, which is a Kubernetes resource that lets you expose your application to external traffic, run the following kubectl expose command:

내 app을 external traffic으로 노출시키기 위해, K8S 오브젝트인 service를 생성한다. 

```yaml
student_00_df37a4b73fa7@cloudshell:~ (qwiklabs-gcp-01-500142021c34)$ kubectl expose deployment hello-server --type=LoadBalancer --port 8080

service/hello-server exposed
```

kubectl cmd로 서비스에 대한 정보를 얻는다.

```yaml
student_00_df37a4b73fa7@cloudshell:~ (qwiklabs-gcp-01-500142021c34)$ kubectl get service
NAME           TYPE           CLUSTER-IP     EXTERNAL-IP     PORT(S)          AGE
hello-server   LoadBalancer   10.7.254.122   34.133.67.214   8080:32531/TCP   53s
kubernetes     ClusterIP      10.7.240.1     <none>          443/TCP          7m20s
```

그리고 나와있는 external ip를 통해 cluster의 외부인 브라우저에서 접근해보면

![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/aee3e844-8819-435b-9686-2dcde5b6675f/Untitled.png)

꺅!

뭐.. 클러스터 삭제를 왜 하는지는 모르겠지만.. 현실에선 이럴일은 거의 없겠지만.. 그래도 실습에서 delete까지 해보면

```yaml
student_00_df37a4b73fa7@cloudshell:~ (qwiklabs-gcp-01-500142021c34)$ gcloud container clusters delete jiyoon-cluster
The following clusters will be deleted.
 - [jiyoon-cluster] in [us-central1-a]

Do you want to continue (Y/n)?  y

Deleting cluster jiyoon-cluster...working.
```

클러스터 삭제까지 실습 완료.

note) 클러스터 생성과 삭제는 provisioning에 시간이 좀 걸리는 경향이 있다. 아니 겁나 오래걸림 한 5분 이상? 

### Orchestrating the Cloud with Kubernetes (쿠버네티스를 cloud 환경에서 orchestration하기)

이 랩에서는 Kubernetes engine을 이용해서 kubernetes cluster를 provisioning하고, `kubectl`을 이용해서 docker containers를 managing 한 후, kubernetes deployment와 service를 이용해서 app을 microservice로 쪼개는 실습을 해봤다.

1. Google K8S Engine default region 세팅하기
    
    `gcloud config set compute/zone us-central1-b`
    
    그리고 io라는 이름의 클러스터를 시작해준다.
    
    `student_00_f2561c75d8fe@cloudshell:~ (qwiklabs-gcp-03-b5e575611d87)$ gcloud container clusters create io`
    
2. 이번 실습에 필요한 몇가지 설정파일들을 github repo에 공개해 놓았는데, 이 Gitrepo에서 파일들을 clone해온다.

1. Pod creation
    
    Pod에 대한 내용은 간략하게는.. kubernetes 의 core로써 하나 혹은 그 이상의 컨테이너 collection이다. 배포의 단위가 되며, 수명주기 관리의 단위가 된다. 
    
    container 사이에 강력한 dependency가 있는 경우에 single pod로 묶어서 배포한다. 
    
    ```yaml
    apiVersion: v1
    kind: Pod
    metadata:
      name: monolith
      labels:
        app: monolith
    spec:
      containers:
        - name: monolith
          image: kelseyhightower/monolith:1.0.0
          args:
            - "-http=0.0.0.0:80"
            - "-health=0.0.0.0:81"
            - "-secret=secret"
          ports:
            - name: http
              containerPort: 80
            - name: health
              containerPort: 81
          resources:
            limits:
              cpu: 0.2
              memory: "10Mi"
    ```
    
    이 yaml파일에서 세가지 사실을 확인할 수 있다.
    
    - monolith라는 하나의 컨테이너로 이루어져 있음
    - container가 시작할 때 몇가지 args를 전달한다.
    - 80번포트를 열어 http traffic을 컨트롤한다.
    
    이 yaml파일을 가지고 kuber cluster의 resource로 생성하는 cmd는 다음과 같다.
    
    `kubectl create -f pods/monolith.yaml`
    
    ```yaml
    student_00_f2561c75d8fe@cloudshell:~/orchestrate-with-kubernetes/kubernetes (qwiklabs-gcp-03-b5e575611d87)$ kubectl get pods
    NAME                    READY   STATUS    RESTARTS   AGE
    monolith                1/1     Running   0          11s
    nginx-56cd7f6b6-w7rfx   1/1     Running   0          5m37s
    ```
    
    1. Pod와 interacting하기
    
    기본적으로 pod는 private ip address로 생성되기 때문에  kubectl port-forward command를 이용해서 local port를 cluster 내부의 port와 일치시켜야한다. 
    
    ```yaml
    student_00_f2561c75d8fe@cloudshell:~/orchestrate-with-kubernetes/kubernetes (qwiklabs-gcp-03-b5e575611d87)$ kubectl port-forward monolith 10080:80
    Forwarding from 127.0.0.1:10080 -> 80
    ```
    
    설정되지 않은 엉뚱한 secure endpoint에 curl을 요청해보았더니
    
    ```yaml
    student_00_f2561c75d8fe@cloudshell:~/orchestrate-with-kubernetes/kubernetes (qwiklabs-gcp-03-b5e575611d87)$ curl http://127.0.0.1:10080/secure
    authorization failed
    ```
    
    역시나 권한 failed
    
    curl -u user [http://127.0.0.1:10080/login으](http://127.0.0.1:10080/login으로)로 접근해봤는데 super-secret password인 "password"로 로그인했고, 
    
    ```yaml
    student_00_f2561c75d8fe@cloudshell:~/orchestrate-with-kubernetes/kubernetes (qwiklabs-gcp-03-b5e575611d87)$ curl -u user http://127.0.0.1:10080/login
    Enter host password for user 'user':
    {"token":"eyJh이하생략"}
    ```
    
    토큰을 발급해주며,
    
    이제 우리는 이걸 ENV로 만들어본다. 이 환경변수에 저장된 토큰을 가지고 /secure url에 다시 접근해볼꺼다
    
    ```yaml
    student_00_f2561c75d8fe@cloudshell:~/orchestrate-with-kubernetes/kubernetes (qwiklabs-gcp-03-b5e575611d87)$ curl -H "Authorization: Bearer $TOKEN" http://127.0.0.1:10080/secure
    {"message":"Hello"}
    ```
    
    나도 hello~
    
    1. container bash 직접 접근하고 컨테이너 안에서 괜히 한번 ping test해보기
    
    ```yaml
    kubectl exec monolith --stdin --tty -c monolith /bin/sh
    
    tudent_00_f2561c75d8fe@cloudshell:~/orchestrate-with-kubernetes/kubernetes (qwiklabs-gcp-03-b5e575611d87)$ kubectl exec monolith --stdin --tty -c monolith /bin/sh
    kubectl exec [POD] [COMMAND] is DEPRECATED and will be removed in a future version. Use kubectl exec [POD] -- [COMMAND] instead.
    /
    / # ping -c 3 google.com
    PING google.com (172.253.119.101): 56 data bytes
    64 bytes from 172.253.119.101: seq=0 ttl=114 time=0.924 ms
    64 bytes from 172.253.119.101: seq=1 ttl=114 time=0.860 ms
    64 bytes from 172.253.119.101: seq=2 ttl=114 time=0.972 ms
    ```
    
    1. Service creation
    
    Pod는 persistant한 자원이 아니다.  바로 liveness / rediness 체크를 통해 provisioning에 실패할 시 바로 재 creation하는 stateless한 자원이다. 
    
    그래서 일정한 ip address로 pod에 접근할 수 있도록 보장하기 위해 service라는 자원을 만들어서 pod에 지속적으로 붙일 수 있다. 
    
    3가지 타입이 있는데,
    
    - ClusterIP : 이 서비스가 cluster 내에서만 visible함
    - NodePort : 클러스터의 각 노드가 외부에서 access 가능하도록 한다.
    - LoadBalancer : cloud provider에서 제공하는 Load balancer 타입으로, 그 LB안에서 service → Node로 트래픽을 서빙한다.
    
    service를 생성한 이후에는, 노출된 nodeport로 트래픽을 허용하기 위해 firewall-rules를 만들어준다. 
    
    ```yaml
    student_00_f2561c75d8fe@cloudshell:~/orchestrate-with-kubernetes/kubernetes (qwiklabs-gcp-03-b5e575611d87)$ gcloud compute firewall-rules create allow-monolith-nodeport \
    >   --allow=tcp:31000
    
    Creating firewall...working..Created [https://www.googleapis.com/compute/v1/projects/qwiklabs-gcp-03-b5e575611d87/global/firewalls/allow-monolith-nodeport].
    Creating firewall...done.
    NAME: allow-monolith-nodeport
    NETWORK: default
    DIRECTION: INGRESS
    PRIORITY: 1000
    ALLOW: tcp:31000
    DENY:
    DISABLED: False
    ```
    
    서비스를 노출하고, firewall rules까지 만들어줬는데
    
    ```yaml
    student_00_f2561c75d8fe@cloudshell:~/orchestrate-with-kubernetes/kubernetes (qwiklabs-gcp-03-b5e575611d87)$ curl -k https://34.71.149.193:31000
    curl: (7) Failed to connect to 34.71.149.193 port 31000: Connection refused
    ```
    
    external ip의 포트로 접근하면 connection fail이 뜬다. 아니 왜?
    
    왜냐하면 애초에 이 service를 creation할 때, app=monolith라는 label selector를 통해 pod를 선택해 이 pod의 트래픽을 고정시킨건데, 정작 pod에 label이 아직 안달려있기 때문
    
    이제 pod에 라벨을 달아서 service가 pod를 제대로 인식할 수 있게 해주자
    
    ```yaml
    student_00_f2561c75d8fe@cloudshell:~/orchestrate-with-kubernetes/kubernetes (qwiklabs-gcp-03-b5e575611d87)$ kubectl label pods secure-monolith 'secure=enabled'
    pod/secure-monolith labeled
    ```
    
    ```yaml
    student_00_f2561c75d8fe@cloudshell:~/orchestrate-with-kubernetes/kubernetes (qwiklabs-gcp-03-b5e575611d87)$ curl -k https://34.71.149.193:31000
    {"message":"Hello"}
    ```
    
    음 잘되는군 만족!
    
    Deployment
    
    배포의 2종류
    
    **Rolling Update**
    
    New replicaset을 만들고, new replicaset의 숫자를 천천히 증가하는 동시에 old replicaset의 숫자를 천천히 감소시킨다.
    
    최소의 overhead, 최소의 perfomance 영향도, 최소의 downtime으로 업데이트할수 있다는 부분이 benefical.
    
    ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/50bb6b67-ab7b-401f-b377-bde24530c316/Untitled.png)
    
    **Canary Update**
    
    완전히 seperated된 deployment를 만들고, 서비스의 타겟을 new deployment로 어느 순간 바꾼다
    
    ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/2ea90347-ca48-4b71-80e9-5788fecc0391/Untitled.png)
    
    Blue-Green deployments
    
    쿠버네티스가 Rolling update를 수행하는 방식 / old에 blue version, new 에 green version의 태그를 단다. 
    
    ## Note) Understanding Regions and Zones in 구글클라우드 (구글클라우드에서의 region / zone 개념)
    
    Certain Compute Engine resources live in regions or zones. A region is a specific geographical location where you can run your resources. Each region has one or more zones. For example, the us-central1 region denotes a region in the Central United States that has zones `us-central1-a`, `us-central1-b`, `us-central1-c`, and `us-central1-f`.
    
    ![Untitled](https://s3-us-west-2.amazonaws.com/secure.notion-static.com/d3f2e073-92bd-4db7-906f-3e8bf68087a6/Untitled.png)
    
    Resources that live in a zone are referred to as zonal resources. Virtual machine Instances and persistent disks live in a zone. To attach a persistent disk to a virtual machine instance, both resources must be in the same zone. Similarly, if you want to assign a static IP address to an instance, the instance must be in the same region as the static IP.