Kubernetes 는 GoLang 으로 만들어진 대표적인 s/w 입니다. Kubernetes  구조와 비슷하게 GoLang 으로 프로젝트를 만들 때 사용되는 일반적인 디렉토리 구조를 설명하겠습니다.

 

제일 먼저 GOPATH 를 지정하고, bin 디렉토리를 PATH에 추가로 지정합니다.

$ GOPATH=/Users/ahnsk/Documents/go_workspace
$ PATH=/Users/ahnsk/Documents/go_workspace/bin:$PATH

 

다음은 go get 으로 govendor 를 설치합니다. govendor 는 dependency module 을 쉽게 다운받고 관리할 수 있습니다.

$ go get -u github.com/kardianos/govendor
$ govendor -version
v1.0.9

 

이제 본인이 생성할 프로젝트를 만들어 보겠습니다. github.com 에 있는 go 프로젝트를 다운로드 받습니다.

$ mkdir -p /Users/ahnsk/Documents/go_workspace/cookiemonster2
$ GOPATH=/Users/ahnsk/Documents/go_workspace/cookiemonster2
$ go get -u github.com/seungkyua/cookiemonster2

GOPATH 는 각 프로젝트마다 자신만의 용도로 사용하기 위해 각각 지정하는 것이 편리합니다.

 

cookiemonster2 라는 프로젝트는 GOPATH 아래의 src 디렉토리에 패키지 경로(github.com/seungkyua/cookiemonster2)로 다운로드 됩니다.

/Users/ahnsk/Documents/go_workspace/cookiemonster2/src/
github.com/seungkyua/cookiemonster2

 

프로젝트 디렉토리로 이동해서 보면 다음과 같습니다.

$ tree -L 2 .
.
├── Dockerfile.cookiemonster
├── LICENSE
├── Makefile
├── README.md
├── cmd
│   ├── cluster_client.go
│   ├── cluster_client2.go
│   └── server.go
├── config
│   └── config.yaml
├── manifest
│   ├── cookiemonster-cm-config.yaml
│   ├── cookiemonster-deployment.yaml
│   ├── cookiemonster-rbac.yaml
│   └── cookiemonster-service.yaml
├── pkg
│   ├── domain
│   └── handler
└── vendor
    └── ...

 

일반적으로 entrypoint 가 되는 go 파일은 cmd 디렉토리 아래에 위치하고 나머지 go 파일들은 pkg 디렉토리에 패키지 구조로 위치합니다.

vendor 디렉토리는 dependency module 을 다운받은 곳이므로 이 디렉토리는 삭제를 하고, 처음부터 새롭게 구성해 보겠습니다.

$ rm -rf vendor

 

govendor init 으로 vendor 디렉토리를 생성합니다.  새롭게 생성된 vendor 디렉토리에 vendor.json 파일이 보이는데 이것은 향후 dependency module 을 다운 받을 때 활용됩니다.

$ govendor init

$ tree -L 2 .
.
├── Dockerfile.cookiemonster
├── LICENSE
├── Makefile
├── README.md
├── cmd
│   ├── cluster_client.go
│   ├── cluster_client2.go
│   └── server.go
├── config
│   └── config.yaml
├── manifest
│   ├── cookiemonster-cm-config.yaml
│   ├── cookiemonster-deployment.yaml
│   ├── cookiemonster-rbac.yaml
│   └── cookiemonster-service.yaml
├── pkg
│   ├── domain
│   └── handler
└── vendor
    └── vendor.json

 

vendor 디렉토리 밑으로 디펜던시 module 을 다운로드 받습니다.

$ govendor get github.com/seungkyua/cookiemonster2

$ tree -L 2 .
.
├── Dockerfile.cookiemonster
├── LICENSE
├── Makefile
├── README.md
├── cmd
│   ├── cluster_client.go
│   ├── cluster_client2.go
│   └── server.go
├── config
│   └── config.yaml
├── manifest
│   ├── cookiemonster-cm-config.yaml
│   ├── cookiemonster-deployment.yaml
│   ├── cookiemonster-rbac.yaml
│   └── cookiemonster-service.yaml
├── pkg
│   ├── domain
│   └── handler
└── vendor
    ├── appengine
    ├── appengine_internal
    ├── github.com
    ├── golang.org
    ├── google.golang.org
    ├── gopkg.in
    ├── k8s.io
    ├── sigs.k8s.io
    └── vendor.json

 

vendor 디렉토리에 dependency module 의 특정버전까지 다운 받을 수 있으니 관리가 편리해 집니다. 

현재 버전에서 labstack 의 echo v4 모듈에 버그가 있어 v3 로 다시 다운받기 위해 해당 디렉토리를 지우고 다시 다운 받을 수 있습니다. @v3 는 v3  이상의 최신 버전에 해당하는 git 브랜치나 태그를 다운받으라는 의미입니다.

$ rm -rf vendor/github.com/labstack/echo

$ govendor get github.com/labstack/echo@v3

 

여기서 govendor 의 버그로 echo 는 다운받았지만 하위 디렉토리인 echo/middleware 는 다운이 안되었으므로 추가로 vendor.json 을 수정하여 다운로드 받습니다.

$ vi vendor/vendor.json
{
   "checksumSHA1": "ynPXfBgVKquHSKkdFWk3oqSYf+g=",
   "path": "github.com/labstack/echo",
   "revision": "38772c686c76b501f94bd6cd5b77f5842e93b559",
   "revisionTime": "2019-01-28T14:12:53Z",
   "version": "v3.3.10",
   "versionExact": "v3.3.10"
},
{
   "checksumSHA1": "Rp/k+BJKpaeB9vyjEPFBW4LeFP8=",
   "path": "github.com/labstack/echo/middleware",
   "revision": "38772c686c76b501f94bd6cd5b77f5842e93b559",
   "revisionTime": "2019-01-28T14:12:53Z",
   "version": "v3.3.10",
   "versionExact": "v3.3.10"
}

$ govendor fetch github.com/labstack/echo/middleware

 

 

 

 

 

 

 

 

 

 

 

Posted by Kubernetes Korea co-leader seungkyua@gmail.com

Kubespray 와 같이 많은 role 을 가진 ansible playbook 에서는 선언된 변수와 실제 할당된 값을 살펴보는 것이 쉽지 않습니다. ansible 에서는 이러한 변수들에 대한 값, 특히 hostvar 값을 볼 수 가 있습니다.

일단 먼저 inventory 파일인 hosts.ini 로 어떤 host 와 group 이 지정되었는지 확인할 수 있습니다.

$ vi inventory/k1-seungkyua/hosts.ini
[all]
k1-master01 ip=192.168.30.13
k1-master02 ip=192.168.30.14
k1-master03 ip=192.168.30.15
k1-node01 ip=192.168.30.12
k1-node02 ip=192.168.30.17
k1-node03 ip=192.168.30.18
k1-node04 ip=192.168.30.21
k1-node05 ip=192.168.30.20

[etcd]
k1-master01
k1-master02
k1-master03

[kube-master]
k1-master01
k1-master02
k1-master03

[kube-node]
k1-node01
k1-node02
k1-node03
k1-node04
k1-node05

[k8s-cluster:children]
kube-master
kube-node

다양한 그룹이 있음을 볼 수 있는데 맨 마지막의 k8s-cluster 그룹은 하위로 kube-master 와 kube-node 를 가지고 있으므로, k8s-cluster 그룹이 모든 노드를 포함한다는 것을 알 수 있습니다.

이제 ansible 명령어로 하나의 host 노드 "k1-master01" 에 대해 적용된 변수를 볼 수 있습니다.
 
$ ansible -i inventory/k1-seungkyua/hosts.ini -m debug -a "var=hostvars['k1-master01']" k8s-cluster

이에 대한 결과는 다음과 같습니다.

k1-node01 | SUCCESS => {
    "hostvars['k1-master01']": {
        "admin_token": "QVFBTmRVVlh5UmZoTlJBQTMyZTh6Qk5uajV1VElrMDJEbWFwWmc9PQ==",
        "ansible_all_ipv4_addresses": [
            "192.168.230.13",
            "192.168.30.13",
            "172.16.31.64",
            "172.17.0.1",
            "10.233.0.1",
            "10.233.0.3",
            "10.233.55.72",
            "10.233.18.191"
        ],
        "ansible_all_ipv6_addresses": [
            "fe80::3ea8:2aff:fe1c:cfd4",
            "fe80::5eb9:1ff:fe8c:67dc",
            "fe80::42:98ff:fec4:7ad1",
            "fe80::ecee:eeff:feee:eeee",
            "fe80::ecee:eeff:feee:eeee"
        ],
        "ansible_apparmor": {
            "status": "enabled"
        },
        "ansible_architecture": "x86_64",
        "ansible_bios_date": "05/06/2015",
        "ansible_bios_version": "P89",
        "ansible_cali1845c564081": {
            "active": true,
            "device": "cali1845c564081",
            "features": {
                "busy_poll": "off [fixed]",
                "fcoe_mtu": "off [fixed]",
                "generic_receive_offload": "on",
                "generic_segmentation_offload": "on",
                "highdma": "on",
                "hw_tc_offload": "off [fixed]",
                "l2_fwd_offload": "off [fixed]",
                "large_receive_offload": "off [fixed]",
                "loopback": "off [fixed]",
                "netns_local": "off [fixed]",
                "ntuple_filters": "off [fixed]",
                "receive_hashing": "off [fixed]",
                "rx_all": "off [fixed]",
                "rx_checksumming": "on",
                "rx_fcs": "off [fixed]",
                "rx_vlan_filter": "off [fixed]",
                "rx_vlan_offload": "on",
                "rx_vlan_stag_filter": "off [fixed]",
                "rx_vlan_stag_hw_parse": "on",
                "scatter_gather": "on",
                "tcp_segmentation_offload": "on",
                "tx_checksum_fcoe_crc": "off [fixed]",
                "tx_checksum_ip_generic": "on",
                "tx_checksum_ipv4": "off [fixed]",
                "tx_checksum_ipv6": "off [fixed]",
                "tx_checksum_sctp": "off [fixed]",
                "tx_checksumming": "on",
                "tx_fcoe_segmentation": "off [fixed]",
                "tx_gre_segmentation": "on",
                "tx_gso_robust": "off [fixed]",
                "tx_ipip_segmentation": "on",
                "tx_lockless": "on [fixed]",
                "tx_nocache_copy": "off",
                "tx_scatter_gather": "on",
                "tx_scatter_gather_fraglist": "on",
                "tx_sit_segmentation": "on",
                "tx_tcp6_segmentation": "on",
                "tx_tcp_ecn_segmentation": "on",
                "tx_tcp_segmentation": "on",
                "tx_udp_tnl_segmentation": "on",
                "tx_vlan_offload": "on",
                "tx_vlan_stag_hw_insert": "on",
                "udp_fragmentation_offload": "on",
                "vlan_challenged": "off [fixed]"
            },
            "hw_timestamp_filters": [],
            "ipv6": [
                {
                    "address": "fe80::ecee:eeff:feee:eeee",
                    "prefix": "64",
                    "scope": "link"
                }
            ],
            "macaddress": "ee:ee:ee:ee:ee:ee",
            "mtu": 1500,
            "promisc": false,
            "speed": 10000,
            "timestamping": [
                "rx_software",
                "software"
            ],
            "type": "ether"
        },
        "ansible_uptime_seconds": 46351496,
        "ansible_user_dir": "/root",
        "ansible_user_gecos": "root",
        "ansible_user_gid": 0,
        "ansible_user_id": "root",
        "ansible_user_shell": "/bin/bash",
        "ansible_user_uid": 0,
        "ansible_userspace_architecture": "x86_64",
        "ansible_userspace_bits": "64",
        "ansible_verbosity": 0,
        "ansible_version": {
            "full": "2.7.7",
            "major": 2,
            "minor": 7,
            "revision": 7,
            "string": "2.7.7"
        },
        "ansible_virtualization_role": "host",
        "ansible_virtualization_type": "kvm",
        "calico_ip_auto_method": "can-reach=8.8.8.8",
        "ceph_version": "mimic",
        "dashboard_enabled": true,
        "docker_insecure_registries": [
            "tacorepo:5000"
        ],
        "gather_subset": [
            "all"
        ],
        "global_as_num": "65000",
        "group_names": [
            "etcd",
            "k8s-cluster",
            "kube-master"
        ],
        "groups": {
            "all": [
                "k1-master01",
                "k1-master02",
                "k1-master03",
                "k1-node01",
                "k1-node02",
                "k1-node03",
                "k1-node04",
                "k1-node05"
            ],
            "etcd": [
                "k1-master01",
                "k1-master02",
                "k1-master03"
            ],
            "k8s-cluster": [
                "k1-node01",
                "k1-node02",
                "k1-node03",
                "k1-node04",
                "k1-node05",
                "k1-master01",
                "k1-master02",
                "k1-master03"
            ],
            "kube-master": [
                "k1-master01",
                "k1-master02",
                "k1-master03"
            ],
            "kube-node": [
                "k1-node01",
                "k1-node02",
                "k1-node03",
                "k1-node04",
                "k1-node05"
            ],
            "ungrouped": []
        },
        "helm_enabled": true,
        "ingress_nginx_enabled": true,
        "ingress_nginx_host_network": true,
        "ingress_nginx_node_count": 1,
        "ingress_nginx_nodeselector": {
            "node-role.kubernetes.io/ingress": "true"
        },
        "inventory_dir": "/home/seungkyua/deploy/my-kubespray/inventory/k1-seungkyua",
        "inventory_file": "/home/seungkyua/deploy/my-kubespray/inventory/k1-seungkyua/hosts.ini",
        "inventory_hostname": "k1-master01",
        "inventory_hostname_short": "k1-master01",
        "ip": "192.168.30.13",
        "ipip_mode": "Never",
        "kubeadm_enabled": true,
        "kubeconfig_localhost": true,
        "kubectl_localhost": true,
        "local_volume_provisioner_enabled": true,
        "module_setup": true,
        "monitors": "192.168.30.23:6789,192.168.30.24:6789,192.168.30.25:6789",
        "nat_outgoing": true,
        "omit": "__omit_place_holder__4491a572b322dda2550f5ce4d8005946f456f738",
        "override_system_hostname": false,
        "peer_with_router": true,
        "peers": [
            {
                "as": "65000",
                "router_id": "192.168.30.1"
            }
        ],
        "playbook_dir": "/home/seungkyua/deploy/my-kubespray",
        "pool_name": "kubes",
        "populate_inventory_to_hosts_file": false,
        "storageclass_name": "rbd",
        "user_id": "kube",
        "user_secret_namespace": "openstack",
        "user_token": "QVFDdC9CcFlpZ0o3TVJBQTV2eStjbDM5RXNLcFkzQyt0WEVHckE9PQ=="
    }
}




Posted by Kubernetes Korea co-leader seungkyua@gmail.com


https://github.com/seungkyua/my-kubespray

my-kubespray

You can easily add your custom logic without modifying kubespray.

The my-kubespray has serveral parameters to override and can deploy ceph rbd provisioner into namespace.

Prerequisites

  • All servers must have the same user, and the user must set root permissions to NOPASSWD in sudo.

  • In the deployment node (or master 1 node), you need to enable ssh access to all other servers (including localhost) without a password to the corresponding account.

  • Register all cluster servers in the /etc/hosts file.

  • Time synchronization between servers with ntp.

  • Enable network restart with systemctl restart network.service .

  • Disable firewall such as selinux or ufw.

  • swapoff.

  • Install python 2.7.x in ansible runner node.

Quick Start

To deploy the cluster you can use :

# Download kubespray and my-kubespray
$ git clone https://github.com/kubernetes-sigs/kubespray.git
$ git clone https://github.com/seungkyua/my-kubespray.git

# Change directory into my-kubespray
$ cd my-kubespray

# Install dependencies from ``requirements.txt``
$ sudo pip install -r ../kubespray/requirements.txt

# Copy ``inventory/k2-seungkyua`` as ``inventory/mycluster``
$ cp -rfp inventory/k2-seungkyua inventory/mycluster

# Update Ansible inventory file
$ vi inventory/mycluster/hosts.ini
[all]
k2-master01 ip=192.168.30.151
k2-master02 ip=192.168.30.152
k2-master03 ip=192.168.30.153
k2-ctrl01 ip=192.168.30.154
k2-ctrl02 ip=192.168.30.155
k2-ctrl03 ip=192.168.30.156
k2-cn01 ip=192.168.30.157

[etcd]
k2-master01
k2-master02
k2-master03

[kube-master]
k2-master01
k2-master02
k2-master03

[kube-node]
k2-ctrl01
k2-ctrl02
k2-ctrl03
k2-cn01

[k8s-cluster:children]
kube-node
kube-master

# Review and change parameters under ``inventory/mycluster/group_vars/k8s-cluster.yml``
$ cat inventory/mycluster/group_vars/k8s-cluster.yml
populate_inventory_to_hosts_file: false
override_system_hostname: false
helm_enabled: true
etcd_memory_limit: 8192M
kubeconfig_localhost: true
kubectl_localhost: true
ipip_mode: Never
calico_ip_auto_method: "can-reach=8.8.8.8"
kubeadm_enabled: true
docker_insecure_registries:
- seungkyua:5000
dashboard_enabled: true
local_volume_provisioner_enabled: true
ingress_nginx_enabled: true
ingress_nginx_host_network: true
ingress_nginx_nodeselector:
node-role.kubernetes.io/ingress: true
ceph_version: mimic
storageclass_name: rbd
monitors: 192.168.30.23:6789,192.168.30.24:6789,192.168.30.25:6789
admin_token: QBTEBmRVVlh5UmZoTlJBQTMyZTh6Qk5uajV1VElrMDJEbWFwWmc9WA==
user_secret_namespace: default
pool_name: kubes
user_id: kube
user_token: QEFESG9CcFlpZ0o3TVJBQTV2eStjbDM5RXNLcFkzQyt0WEVHckE9WA==

# Run Ansible Playbook to deploy kubernetes cluster
$ ansible-playbook -b -f 30 -i inventory/mycluster/hosts.ini ../kubespray/cluster.yml

# Run Ansible Playbook to deploy ceph rbd provisioner
$ ansible-playbook -b -f 30 -i inventory/mycluster/hosts.ini storage.yml

Miscellaneous

After installation, you can find the artifacts which are kubectl and admin.conf in inventory/mycluster/artifacts directorin on deploy node(ansible runner).

$ ls -al inventory/mycluster/artifacts
total 173620
drwxr-x--- 2 seungkyua seungkyua     4096 Feb 22 05:25 .
drwxrwxr-x 4 seungkyua seungkyua     4096 Feb 22 02:38 ..
-rw-r----- 1 seungkyua seungkyua     5449 Feb 22 05:25 admin.conf
-rwxr-xr-x 1 seungkyua seungkyua 177766296 Feb 22 05:25 kubectl
-rwxr-xr-x 1 seungkyua seungkyua       65 Feb 22 05:25 kubectl.sh

If desired, copy admin.conf to ~/.kube/config and kubectl to /usr/local/bin/kubectl

$ mkdir -p ~/.kube
$ cp inventory/mycluster/artifacts/admin.conf ~/.kube/config
$ sudo cp inventory/mycluster/artifacts/kubectl /usr/local/bin/kubectl



Posted by Kubernetes Korea co-leader seungkyua@gmail.com
변수의 key 값을 가져오기 위해서는 regex_replace 함수를 사용하여 substring 을 할 수 도 있지만 set_fact 를 이용할 수 도 있습니다.

아래 내용은 set_fact 를 활용한 내용입니다.

예제는 ingress controller 가 실행될 node label 을 세팅하기 위한 방법입니다.
node label 은 다음과 같은 명령어로 세팅할 수 있습니다.
$ kubectl label --overwrite node k2-ctrl01 node-role.kubernetes.io/ingress=true


먼저 변수를 세팅하는 defaults/main.yml 파일은 다음과 같습니다.
ingress_nginx_nodeselector:
  node-role.kubernetes.io/ingress: "true"
ingress_nginx_node_count: 1

ingress_nginx_nodeselector 의 dict 값은 node-role.kubernetes.io/ingress: "true" 인데 node-role.kubernetes.io 는 key 값으로 이 값이 변경될 수 있으므로 key 를 사용하기에는 어렵습니다.

key 값을 유동적으로 활용하기 위해서는 set_fact 를 활용합니다.
- name: Set ingress nginx node role
  set_fact:
    ingress_nginx_node_role:
      key: "{{ item.key }}"
      value: "{{ item.value }}"
  with_dict: "{{ ingress_nginx_nodeselector }}"
  when:
    - inventory_hostname == groups['kube-master'][0]

위 태스크가 실행되면 ingress_nginx_node_role 변수에 {"key" : "node-role.kubernetes.io/ingress", "value" : "true"} 라고 저장됩니다.
그럼 이제 쉽게 ingress_nginx_node_role.key 로 해당 값을 가져올 수 있습니다.

- name: Set ingress nginx node list
  set_fact:
    ingress_nginx_node_list:
      - "{{ groups['kube-node'][item|int] }}"
  with_sequence: start=0 end={{ (ingress_nginx_node_count-1)|int }}
  when:
    - inventory_hostname == groups['kube-master'][0]

- name: NGINX Ingress Controller | Set node label
  shell: |
    {{ bin_dir }}/kubectl label --overwrite node {{ item }} {{ ingress_nginx_node_role.key }}={{ ingress_nginx_node_role.value }}
  loop: "{{ ingress_nginx_node_list }}"
  when:
    - inventory_hostname == groups['kube-master'][0]

label 을 세팅하기 전에 node 를 선택해야 하기 때문에 node 를 선택하는 "Set ingress nginx node list" 태스크를 먼저 수행했고 "NGINX Ingress Controller | Set node label" 태스크가 node label 을 지정하는 명령어 입니다.



 




Posted by Kubernetes Korea co-leader seungkyua@gmail.com

Kubernetes Node 가 NotReady 인 경우 해결을 위해서 제일 먼저 해당 노드의 syslog 를 확인 하는 것이 좋다.


## Node 의 syslog 확인 (ubuntu 인 경우)
# vi /etc/log/syslog

Feb 20 12:14:49 k1-node01 kubelet[31100]: E0220 12:14:49.401890   31100 raw.go:146] Failed to watch directory "/sys/fs/cgroup/devices/system.slice": inotify_add_watch /sys/fs/cgroup/devices/system.slice/run-ref5f878051f440b98dfd9bd843a01b58.scope: no space left on device
Feb 20 12:14:49 k1-node01 kubelet[31100]: F0220 12:14:49.401909   31100 kubelet.go:1369] Failed to start cAdvisor inotify_add_watch /sys/fs/cgroup/devices/system.slice/run-ref5f878051f440b98dfd9bd843a01b58.scope: no space left on device
Feb 20 12:14:49 k1-node01 kubelet[31100]: E0220 12:14:49.640153   31100 kubelet.go:2266] node "k1-node01" not found
Feb 20 12:14:49 k1-node01 systemd[1]: kubelet.service: Main process exited, code=exited, status=255/n/a
Feb 20 12:14:49 k1-node01 systemd[1]: kubelet.service: Unit entered failed state.
Feb 20 12:14:49 k1-node01 systemd[1]: kubelet.service: Failed with result 'exit-code'.


위와 같이 inotify_add_watch 에서 no space left on device 로 에러가 날 경우에 sysctl 로 값을 늘려줘야 한다.

## fs.inotify.max_user_watches 값을 늘려줌
# vi /etc/sysctl.d/99-sysctl.conf

net.ipv4.ip_forward=1
net.ipv4.ip_local_reserved_ports=30000-32767
net.bridge.bridge-nf-call-iptables=1
net.bridge.bridge-nf-call-arptables=1
net.bridge.bridge-nf-call-ip6tables=1
fs.inotify.max_user_watches=1048576


해당 값을 적용하고 나서 kubelet 이 activating 상태이거나 failed 상태인 경우가 많으므로 아래와 같이 kubelet 을 재 시작해 준다.

## sysctl 로 적용 및 kubelet 재실행

# sysctl -p
# sudo systemctl restart kubelet 






Posted by Kubernetes Korea co-leader seungkyua@gmail.com

kubernetes 에서 ceph rbd provisioner 활용 방법입니다.


1. 먼저 rbd provisioner 가 사용할 rbac 권한 yaml 파일을 만듭니다.
# vi rbd-rbac.yaml
---
kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: rbd-provisioner
rules:
  - apiGroups: [""]
    resources: ["persistentvolumes"]
    verbs: ["get", "list", "watch", "create", "delete"]
  - apiGroups: [""]
    resources: ["persistentvolumeclaims"]
    verbs: ["get", "list", "watch", "update"]
  - apiGroups: ["storage.k8s.io"]
    resources: ["storageclasses"]
    verbs: ["get", "list", "watch"]
  - apiGroups: [""]
    resources: ["events"]
    verbs: ["create", "update", "patch"]
  - apiGroups: [""]
    resources: ["services"]
    resourceNames: ["kube-dns","coredns"]
    verbs: ["list", "get"]
  - apiGroups: [""]
    resources: ["endpoints"]
    verbs: ["get", "list", "watch", "create", "update", "patch"]

---
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: rbd-provisioner
subjects:
  - kind: ServiceAccount
    name: rbd-provisioner
    namespace: kube-system
roleRef:
  kind: ClusterRole
  name: rbd-provisioner
  apiGroup: rbac.authorization.k8s.io

---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: rbd-provisioner
  namespace: kube-system

---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: rbd-provisioner
  namespace: kube-system
rules:
- apiGroups: [""]
  resources: ["secrets"]
  verbs: ["get"]
- apiGroups: [""]
  resources: ["endpoints"]
  verbs: ["get", "list", "watch", "create", "update", "patch"]

---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: rbd-provisioner
  namespace: kube-system
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: Role
  name: rbd-provisioner
subjects:
- kind: ServiceAccount
  name: rbd-provisioner
  namespace: kube-system 

# kubectl create -f rbd-rbac.yaml 



2. kube-system 네임스페이스로 admin secret 과 user secret 을 생성한다.
# ceph auth get client.admin 2>&1 |grep "key = " |awk '{print  $3'} |xargs echo -n > /tmp/secret.admin
# kubectl create secret generic ceph-secret-admin --type=kubernetes.io/rbd --from-file=/tmp/secret.admin --namespace=kube-system

# ceph auth get-key client.kube > /tmp/secret.user
# kubectl create secret generic ceph-secret-user --type=kubernetes.io/rbd --from-file=/tmp/secret.user --namespace=kube-system 




3. rbd provisioner 가 사용할 storage class yaml 파일을 만듭니다.
pool 이름과 userId 를  정확히 입력해야 합니다.

# vi rbd-storageclass.yaml

---
apiVersion: storage.k8s.io/v1beta1
kind: StorageClass
metadata:
  name: "rbd"
  annotations:
    storageclass.beta.kubernetes.io/is-default-class: "true"
provisioner: ceph.com/rbd
reclaimPolicy: Delete
parameters:
  monitors: "192.168.30.23:6789,192.168.30.24:6789,192.168.30.25:6789"
  adminId: "admin"
  adminSecretName: "ceph-secret-admin"
  adminSecretNamespace: "kube-system"
  pool: "kubes"
  userId: "kube"
  userSecretName: "ceph-secret-user"
  userSecretNamespace: "kube-system"
  imageFormat: "2"
  imageFeatures: "layering" 


# kubectl create -f rbd-storageclass.yaml 



4. rbd provisioner 를 deployment 타입 yaml 로 생성합니다.
# vi deployment-rbd-provisioner.yaml

---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: rbd-provisioner
  namespace: kube-system
  labels:
    app: rbd-provisioner
    version: v2.1.1-k8s1.11
spec:
  replicas: 2
  strategy:
    type: Recreate
  selector:
    matchLabels:
      app: rbd-provisioner
      version: v2.1.1-k8s1.11
  template:
    metadata:
      labels:
        app: rbd-provisioner
        version: v2.1.1-k8s1.11
    spec:
      priorityClassName: system-cluster-critical
      serviceAccount: rbd-provisioner
      containers:
        - name: rbd-provisioner
          image: quay.io/external_storage/rbd-provisioner:v2.1.1-k8s1.11
          imagePullPolicy: IfNotPresent
          env:
            - name: PROVISIONER_NAME
              value: ceph.com/rbd
            - name: POD_NAME
              valueFrom:
                fieldRef:
                  fieldPath: metadata.name
          command:
            - "/usr/local/bin/rbd-provisioner"
          args:
            - "-id=${POD_NAME}" 


# kubectl create -f deployment-rbd-provisioner.yaml 




5. 이제 default 네임스페이스에 테스트를 해 보겠습니다.
먼저 default 네임스페이스에 user secret 을 생성합니다. (rbd provisioner 를 사용하고자 하는 네임스페이스에는 user secret 을 만들어 줘야 합니다.)
# ceph auth get-key client.kube > /tmp/secret.user
# kubectl create secret generic ceph-secret-user --type=kubernetes.io/rbd --from-file=/tmp/secret.user --namespace=default


6. 테스트용 pod 생성.

# vi rbd-test.yaml

---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: rbd-test
  namespace: default
spec:
  accessModes:
    - ReadWriteOnce
  storageClassName: rbd
  resources:
    requests:
      storage: 1Gi

---
apiVersion: v1
kind: Pod
metadata:
  name: rbd-test
  namespace: default
spec:
  containers:
  - name: pod-test
    image: gcr.io/google_containers/busybox:1.24
    command:
    - "/bin/sh"
    args:
    - "-c"
    - "touch /mnt/SUCCESS && exit 0 || exit 1"
    volumeMounts:
    - name: pvc
      mountPath: "/mnt"
  restartPolicy: "Never"
  volumes:
  - name: pvc
    persistentVolumeClaim:
      claimName: rbd-test 



# kubectl create -f rbd-test.yaml 





Posted by Kubernetes Korea co-leader seungkyua@gmail.com

CVE-2018-1002105: proxy request handling in kube-apiserver can leave vulnerable TCP connections




[ Enable API Aggregation ]


## kube-apiserver 에 옵션에 다음과 같이 설정

--requestheader-client-ca-file=/etc/kubernetes/ssl/front-proxy-ca.crt
--requestheader-allowed-names=front-proxy-client
--requestheader-extra-headers-prefix=X-Remote-Extra-
--requestheader-group-headers=X-Remote-Group
--requestheader-username-headers=X-Remote-User
--proxy-client-cert-file=/etc/kubernetes/ssl/front-proxy-client.crt
--proxy-client-key-file=/etc/kubernetes/ssl/front-proxy-client.key



[ Metrics API 서버 설치 ]

$ mkdir -p ~/ahnsk/metric-server && cd ~/ahnsk
$ cd metrics-server

$ kubectl create -f deploy/1.8+/

$ kubectl -n kube-system get pods -l k8s-app=metrics-server


## metric-server 로그 보기
kubectl -n kube-system logs $(kubectl get pods --namespace=kube-system -l k8s-app=metrics-server -o name)


## metric-server 가 hostname 을 알지 못한다는 로그가 나오면 metric-server 를 변경

$ kubectl edit deployment metrics-server -n kube-system
image: k8s.gcr.io/metrics-server-amd64:v0.3.0
command:
- /metrics-server
- --kubelet-insecure-tls
- --kubelet-preferred-address-types=InternalIP



[ 취약점 확인 ]

## 1.12.2 버전에서는 metric-server 가 node 의 stats 을 가져오고 있음
$ kubectl version
Client Version: version.Info{Major:"1", Minor:"12", GitVersion:"v1.12.2", GitCommit:"17c77c7898218073f14c8d573582e8d2313dc740", GitTreeState:"clean", BuildDate:"2018-10-24T06:43:59Z", GoVersion:"go1.10.4", Compiler:"gc", Platform:"linux/amd64"}
Server Version: version.Info{Major:"1", Minor:"12", GitVersion:"v1.12.2", GitCommit:"17c77c7898218073f14c8d573582e8d2313dc740", GitTreeState:"clean", BuildDate:"2018-10-24T06:43:59Z", GoVersion:"go1.10.4", Compiler:"gc", Platform:"linux/amd64"}

$ kubectl get --raw /apis/metrics.k8s.io/v1beta1/nodes
{"kind":"NodeMetricsList","apiVersion":"metrics.k8s.io/v1beta1","metadata":{"selfLink":"/apis/metrics.k8s.io/v1beta1/nodes"},"items":[{"metadata":{"name":"k1-master01","selfLink":"/apis/metrics.k8s.io/v1beta1/nodes/k1-master01","creationTimestamp":"2018-12-05T08:38:13Z"},"timestamp":"2018-12-05T08:37:09Z","window":"30s","usage":{"cpu":"425054529n","memory":"13992336Ki"}},{"metadata":{"name":"k1-master02","selfLink":"/apis/metrics.k8s.io/v1beta1/nodes/k1-master02","creationTimestamp":"2018-12-05T08:38:13Z"},"timestamp":"2018-12-05T08:37:17Z","window":"30s","usage":{"cpu":"174426537n","memory":"10275528Ki"}},{"metadata":{"name":"k1-master03","selfLink":"/apis/metrics.k8s.io/v1beta1/nodes/k1-master03","creationTimestamp":"2018-12-05T08:38:13Z"},"timestamp":"2018-12-05T08:37:15Z","window":"30s","usage":{"cpu":"322881470n","memory":"10741952Ki"}},{"metadata":{"name":"k1-node01","selfLink":"/apis/metrics.k8s.io/v1beta1/nodes/k1-node01","creationTimestamp":"2018-12-05T08:38:13Z"},"timestamp":"2018-12-05T08:37:14Z","window":"30s","usage":{"cpu":"309956501n","memory":"14327216Ki"}},{"metadata":{"name":"k1-node02","selfLink":"/apis/metrics.k8s.io/v1beta1/nodes/k1-node02","creationTimestamp":"2018-12-05T08:38:13Z"},"timestamp":"2018-12-05T08:37:12Z","window":"30s","usage":{"cpu":"363826460n","memory":"17359508Ki"}},{"metadata":{"name":"k1-node03","selfLink":"/apis/metrics.k8s.io/v1beta1/nodes/k1-node03","creationTimestamp":"2018-12-05T08:38:13Z"},"timestamp":"2018-12-05T08:37:07Z","window":"30s","usage":{"cpu":"256312214n","memory":"15248572Ki"}},{"metadata":{"name":"k1-node04","selfLink":"/apis/metrics.k8s.io/v1beta1/nodes/k1-node04","creationTimestamp":"2018-12-05T08:38:13Z"},"timestamp":"2018-12-05T08:37:13Z","window":"30s","usage":{"cpu":"124689837n","memory":"11814000Ki"}},{"metadata":{"name":"k1-node05","selfLink":"/apis/metrics.k8s.io/v1beta1/nodes/k1-node05","creationTimestamp":"2018-12-05T08:38:13Z"},"timestamp":"2018-12-05T08:37:15Z","window":"30s","usage":{"cpu":"114905912n","memory":"8340284Ki"}}]}



## 1.11.5 패치 적용된 버전에서는 metric-server 가 system:anonymous 계정으로 node 의 stats 을 가져오지 못함
$ kubectl version
Client Version: version.Info{Major:"1", Minor:"11", GitVersion:"v1.11.3", GitCommit:"a4529464e4629c21224b3d52edfe0ea91b072862", GitTreeState:"clean", BuildDate:"2018-09-09T18:02:47Z", GoVersion:"go1.10.3", Compiler:"gc", Platform:"linux/amd64"}
Server Version: version.Info{Major:"1", Minor:"11", GitVersion:"v1.11.5", GitCommit:"753b2dbc622f5cc417845f0ff8a77f539a4213ea", GitTreeState:"clean", BuildDate:"2018-11-26T14:31:35Z", GoVersion:"go1.10.3", Compiler:"gc", Platform:"linux/amd64"}

$ kubectl get --raw /apis/metrics.k8s.io/v1beta1/nodes
{"kind":"NodeMetricsList","apiVersion":"metrics.k8s.io/v1beta1","metadata":{"selfLink":"/apis/metrics.k8s.io/v1beta1/nodes"},"items":[]}

$ kubectl logs metrics-server-695bdf6996-sg429 -n kube-system
E1205 08:14:30.157105       1 manager.go:102] unable to fully collect metrics: unable to fully scrape metrics from source kubelet_summary:node1: unable to fetch metrics from Kubelet node1 (192.168.0.93): request failed - "403 Forbidden", response:"Forbidden (user=system:anonymous, verb=get, resource=nodes, subresource=stats)"
E1205 08:15:30.148565       1 manager.go:102] unable to fully collect metrics: unable to fully scrape metrics from source kubelet_summary:node1: unable to fetch metrics from Kubelet node1 (192.168.0.93): request failed - "403 Forbidden", response:"Forbidden (user=system:anonymous, verb=get, resource=nodes, subresource=stats)"



[ work around 로 해결하는 방법 ]


1. kube-apiserver 옵션에 --anonymous-auth=false 를 넣는 방법 (default 는 true 임)

2. 아래 clusterrolebinding 의 system:basic-user 와 system:discovery 에 들어있는 system:unauthenticated Group 을 삭제

$ kubectl get clusterrolebindings system:basic-user -o yaml
kubectl get clusterrolebindings system:basic-user -o yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  annotations:
    rbac.authorization.kubernetes.io/autoupdate: "true"
  creationTimestamp: 2018-11-21T08:44:59Z
  labels:
    kubernetes.io/bootstrapping: rbac-defaults
  name: system:basic-user
  resourceVersion: "113"
  selfLink: /apis/rbac.authorization.k8s.io/v1/clusterrolebindings/system%3Abasic-user
  uid: bae78dd9-ed69-11e8-b322-3ca82a1c0c40
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: system:basic-user
subjects:
- apiGroup: rbac.authorization.k8s.io
  kind: Group
  name: system:authenticated
- apiGroup: rbac.authorization.k8s.io
  kind: Group
  name: system:unauthenticated


그리고 clusterrole 의 system:basic-user 와 system:discovery 에 세팅된 auto repaired 를 false 로 변경


$ kubectl get clusterroles system:basic-user -o yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  annotations:
    rbac.authorization.kubernetes.io/autoupdate: "true"
  creationTimestamp: 2018-11-21T08:44:59Z
  labels:
    kubernetes.io/bootstrapping: rbac-defaults
  name: system:basic-user
  resourceVersion: "58"
  selfLink: /apis/rbac.authorization.k8s.io/v1/clusterroles/system%3Abasic-user
  uid: baae6ff5-ed69-11e8-b322-3ca82a1c0c40
rules:
- apiGroups:
  - authorization.k8s.io
  resources:
  - selfsubjectaccessreviews
  - selfsubjectrulesreviews
  verbs:
  - create







Posted by Kubernetes Korea co-leader seungkyua@gmail.com

처음에는 대충 볼려고 했는데 내용이 좋아서 끝까지 집중해서 봤네요. 전체 요약해 봤습니다.

AWS re:Invent 2018 - Keynote with Werner Vogels

2004-12-12 Amazon Oracle 장애 Lessons learned

  • SOA (Service Oriented Architecture) 로 서비스를 Flexible 하게 구성하였으나 Database Oracle의 로깅 코드 버그로 장애 발생, 12시간 동안 크리스마스 시즌 주문을 받지 못함
  • Oracle 은 SPOF (Single Point of Failure) 이고, Black Box 라 scale, reliability 를 높일 수 있는 방법이 없었음.
  • 1차 변경: Sharding 을 통해 데이터를 분산하여 performance 와  reliability 를 높힘
  • Reducing your blast radius : 장애가 발생하면 모든 면에서 영향을 최소화 해라.
  • 2차 변경: Cell based architectures 로 변경 (cell 단위 독립적으로 구성, 한 곳에 문제가 생겨도 영향도를 최소화함)
  • Cell based Architectures: AWS 는 모두 Cell 단위로 구성되어 있음 (심지어 AZ (Availability Zone) 안에서도 Cell 단위로 구성되어 영향도 최소화 했음) → 근데 왜 한국 Region 은 장애가....???)
  • AZ 서비스에서 DynamoDB 는 Cell 단위로 구성되어 DB 장애가 일어나도 최소화 됨.
  • Relational databases: not designed for the cloud (Sharding 을 하더라도 결국 Database 로 인해 서비스가 종속되게 됨)
  • Decompose the system into fundamental building blocks -> 그래야 multi tenant, fault tolerant, self healing 서비스를 구현 가능


Amazon Aurora: Scaled-out distributed architecture (Database aware storage service)

  • AWS 에서 가장 빠르게 성장하는 서비스 중에 하나임
  • AZ 단위로 분산이 가능하고 Cell based Architectures 로 mission critical 한 서비스 구현이 가능
  • On quorums and failures 를 적용하였기에 이런 서비스를 구현 가능했음 (AZ 단위로 분산)
  • AZ 마다 2개의 인스턴스를 띄우고 AZ 가 하나 죽고 다른 AZ 에서 인스턴스 1개가 죽어도 read 는 가능, 하지만 쓰기는 못함
  • 10G 링크로 되어 있어 click 만으로 빠르게 복구되어 MTBF(Mean time between failures) 와 MTTR(Mean time to repair) 를 줄일 수 있음
  • Aurora 는 log 만 복사하기 때문에 복제 및 복구가 빠름 (storage node 에서 복사가 이루어짐)
  • AWS Aurora 는 Cloud native database as a foundation for innovation 이다.
  • ASW 유형별 Databases 종류

    • Relational - Amazon Aurora, Amazon RDS
    • Key Value - Amazon DynamoDB
    • Document - Amazon DynamoDB
    • In-Memory - Amazon ElasticCache
    • Graph - Amazon Neptune
    • Time-Series (New) - Amazon Timestream
    • Ledger (New) - Amazon QLDB

How to migrate Oracle to DynamoDB

  • Amazon item master service : sharding 된 Oracle databases 24개로 구성 (6,000억 레코드가 있으며, 50억 update 가 1day 에 일어남), 1년에 30~40% 정도 증가함

  • Oracle 을 DynamoDB 로 변경 (schema 변경 및 Database Migration 진행)
  • Database Migration Service (6,000억 레코드 migration을 live 로 진행)

AWS S3 - Mai-lan Tomsen Bukovec (Vice President, Amazon S3 and Glacier)

  • 전체 10 Trillions objects 가 저장되어 있음
  • 한개의 리전에서 최대 1초에 60 Terabytes 를 관리함
  • 초기에 8개의 microservices 로 구성됨, 현재는 235개 이상의 분산 mircroservices 로 구성됨

AWS Redshift

  • 2018년 11월 1일 아주 큰 Oracle data warehouse 를 Redshift 로 옮김 (올해 가장 기쁜 순간)
  • 성능 향상을 이룸
  • 지난주 (11월 4째주) Redshift concurrency scaling 기능 새로 소개

AWS Lambda

  • 고객들이 serverless 로 가려고 하는가?


    • No Infrastructure provisioning, no management
    • Automatic scaling
    • Highly available and secure
    • Pay for value
  • 2014년 Preview 공개 후 2015년 GA 로 오픈
  • Worker: 고객의 function 코드가 로딩되고 실행되는 환경
  • Firecracker (New): serverless computing을 위한 안전하고 빠른 microVMs


    • KVM hardware virtualizations
    • 실행되는데 125ms 소요 (1개의 호스트에서 1초에 150 microVMs 실행 가능)
    • 1개의 microVM 당 5MB 메모리 사용 (1개의 호스트에 수천개의 microVM 을 띄울 수 있음)
  • Lambda runtime 환경



AWS 의 기능과 서비스의 95% 고객의 피드백으로 부터 개발된 것임

AWS Toolkits 새로운 IDEs

  • PyCharm
  • IntelliJ
  • VS Code

AWS Lambda 신규 기능

  • 사실상 모든 언어 지원 가능해 졌음
    • 기존 지원 언어: NodeJS, JavaScript, Python, Java, .Net, Go
    • 신규 지원 언어: Ruby
    • Lambda 에 Custom Runtime 적용 가능 (Ruby 도 그렇게 구현한 것으로 C++, Erlang 등 다른 언어 들도 적용하여 쓸 수 있음)
  • Lambda Layers: Lambda 를 라이브러리 형태의 함수로 만들어 재사용 가능
  • Nested Applications using Serverless Application Repository (New)

AWS Step Functions 

  • 기존에는 workflow 를 Sequential steps, Parallel steps, Branching steps 로 지원
  • Step Functions service Integrations (New): Step Functions 을 AWS 의 8개 서비스와 함께 쓸 수 있게 지원
    • Step Functions 함께 쓸 수 있는 AWS 8개 서비스: Batch, ECS, Fargate(컨테이너 배포 서비스), Glue(ETL서비스), DynamoDB, SNS, SQS, SageMaker

AWS API Gateway 에서 WebSocket 지원 

  • WebSocket 을 통해 Stateful 지원 가능

AWS ALB 에서 Lambda 지원

  • ALB (Amazon Load Balancer) 뒷단에 Lambda 를 바로 연결 가능 

Amazon Managed Streaming for Kafka

  • Fully managed and highly-available Apache Kafka service



AWS Well-Architected site

Amazon Well-Architected



AWS Well-Architected Tool

  • Measure and improve your architecture using AWS Well-Architectured best practices


Posted by Kubernetes Korea co-leader seungkyua@gmail.com

이번에는 docker 이미지 최적화에 대해서 설명하겠습니다.


이미지 최적화를 위해서는 다음의 3가지를 잘 활용하면 됩니다.


1. 레이어를 줄이기 위해서 다중 RUN 명령어는 하나의 RUN 명령어로 구성
2. 파일 복사와 라이브러리 Install 은 순서가 중요
3. 컴파일과 같은 작업은 Multistep build 를 이용

 


alpine linux 로 nginx 를 실행시기 위한 방법으로 다음과 같은 docker 이미지를 만들 수 있습니다.


먼저, nginx.conf 파일을 로컬 컴퓨터에 생성합니다.

$ vi nginx.conf

user www;
worker_processes auto;

error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;

events {
    worker_connections 1024;
}

http {
    include /etc/nginx/mime.types;
    default_type application/octet-stream;
    sendfile on;
    access_log /var/log/nginx/access.log;
    keepalive_timeout 3000;
    server {
       listen 80;
       root /www;
       index index.html index.htm;
       server_name localhost;
       client_max_body_size 32m;
       error_page 500 502 503 504 /50x.html;
       location = /50x.html {
             root /var/lib/nginx/html;
       }
    }
}



다음은 간단한 index.html 입니다.

$ vi index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8" />
    <title>HTML5</title>
</head>
<body>
    Server is online
</body>
</html>

 


이 두 파일을 활용한 Dockerfile 은 다음과 같습니다.

$ vi Dockerfile


FROM alpine:3.8
RUN apk update
RUN apk add --no-cache nginx
RUN adduser -D -g 'www' www
RUN mkdir /www
RUN chown -R www:www /var/lib/nginx
RUN chown -R www:www /www

COPY nginx.conf /etc/nginx/nginx.conf
COPY index.html /www/index.html

ENTRYPOINT ["nginx"]
CMD ["-g", "daemon off;"]

 


docker 이미지를 빌드하고 실행시키면 index.html 결과를 얻을 수 있습니다.


$ sudo docker build -t seungkyua/nginx-alpine .

$ docker run -d -p 30080:80 --name nginx-alpine seungkyua/nginx-alpine

$ curl http://localhost:30080

 

하나의 RUN 명령어로 구성

여기서 첫번째 이미지 최적화 포인트가 보입니다.


앞의 Dockerfile 에서 하나의 RUN 은 하나의 이미지 레이어가 되므로 이것을 하나로 다음과 같이 줄일 수 있습니다.

 

RUN apk update && \

    apk add --no-cache nginx && \

    adduser -D -g 'www' www && \

    mkdir /www && \

    chown -R www:www /var/lib/nginx && \

    chown -R www:www /www

 




이번에는 nodejs docker 이미지를 만들어 보겠습니다.

 

$ package.json
{
    "name": "docker_web_app",
    "version": "1.0.0",
    "description": "Node.js on Docker",
    "private": true,
    "author": "Seungkyu Ahn <seungkyua@gmail.com>",
    "main": "server.js",
    "scripts": {
       "start": "node server.js"
    },
    "dependencies": {
       "express": "^4.16.1"
    }
}



$ vi server.js

'use strict';

const express = require('express');

const PORT = 8080;

const HOST = '0.0.0.0';

const app = express();

app.get('/', (req, res) => {

  res.send('Hello world\n');

});

app.listen(PORT, HOST);

console.log(`Running on http://${HOST}:${PORT}`);

 


$ vi Dockerfile

FROM node:8

RUN mkdir -p /app

COPY package*.json /app/

WORKDIR /app

COPY . /app

RUN npm install --only=production

EXPOSE 8080

CMD [ "npm", "start" ]



파일 COPY 와 관련 라이브러리 설치 순서가 중요

위의 Dockerfile 의 경우 현재 디렉토리 소스를 COPY 한 후에 npm install 을 수행합니다.


docker 이미지는 변경된 레이어만 build 되지만 연관된 하위 레이어까지 build 됩니다.


여기서는 현재 디렉토리 소스가 변경되면 npm install 을 매번 다시 수행합니다.


그러므로 일단 package 설치를 먼저하고 COPY 를 나중에 하면 package 설치 내용이 변경되지 않는다면 npm install 은 캐시를 바로 사용하여 설치하지 않습니다.

 


RUN npm install --only=production

COPY . /app

 




마지막으로, 컴파일 하는 소스의 docker 이미지를 살펴보겠습니다.

 

$ vi hello.c

#include <stdio.h>

int main () {

  printf ("Hello, world!\n");

  return 0;

}



$ vi Dockerfile

FROM alpine:3.8

RUN apk update && \

    apk add --update alpine-sdk

RUN mkdir -p /app

COPY . /tmp

WORKDIR /tmp

RUN gcc hello.c -o hello

ENTRYPOINT ["/tmp/hello"]


 

Multistep build 활용

위의 경우에 c 컴파일을 하기 위해 c 컴파일로가 들어있는 sdk 패키지를 설치하고 바이너리 파일로 컴파일을 하므로 이미지 사이즈가 커집니다.


여기서 build 단계를 활용하면 sdk 패키지는 제외하고 최종 바이너리 파일만 docker 이미지에 넣을 수 있습니다.

 

$ vi Dockerfile

FROM alpine:3.8 AS build

RUN apk update && \

    apk add --update alpine-sdk

RUN mkdir -p /app

COPY . /tmp

WORKDIR /tmp

RUN gcc hello.c -o hello


FROM alpine:3.8

COPY --from=build /tmp/hello /app/hello

ENTRYPOINT ["/app/hello"]

 




아래 이미지 사이즈는 build 스텝을 활용하지 않은 파일 사이즈와 활용한 사이즈의 차이입니다.



seungkyua/c-hello-world      176MB

 


seungkyua/c-hello-world      4.42MB


Posted by Kubernetes Korea co-leader seungkyua@gmail.com

docker 이미지가 어떻게 구성되는지 이해하기 위해서는 Copy-on-write 정책을 이해해야 합니다.


아래와 같은 Dockerfile 이 있고 이를 이미지로 만들었을 때 이미지가 차지하는 스토리지 구성은 다음과 같습니다.

(아래 Dockerfile 은 sample 파일로 make tool 까지 있다고 가정합니다.)

 

FROM ubuntu:15.04
COPY . /app
RUN make /app
CMD python /app/app.py

 

container-layers.jpg  

이미지 출처: https://docs.docker.com/v17.09/engine/userguide/storagedriver/images/container-layers.jpg

 

가장 아래 레이어 d3a1f33e8a5a 는 ubuntu:15.04 이미지 입니다.


그 다음 바로 위의 c22013c84729 는 현재 디렉토리의 이하의 모든 소스를 /app 디렉토리 아래 복사한 이미지 레이어입니다. (COPY . /app 명령에 의한)


d74508fb6632 는 RUN make /app 가 실행된 이미지 레이어이고, 마지막으로 91e54dfb1179 는 CMD python /app/app.py 이미지 레이어입니다.


Dockerfile 에서 하나의 명령어는 하나의 이미지 레이어 이며, 여기까지 이미지는 Read Only 이미지로 컨테이너가 실행되어도 바꿀 수 없습니다.

 

위의 Dockerfile 로 만든 이미지로 컨테이너가 실행되면 제일 상단의 Container Layer 로 Thin Read/Write 스토리지가 생기며, 여기에만 데이터 쓰기가 가능합니다.


같은 이미지로 컨테이너 인스턴스를 여러 개 실행시켜도 이미지는 공유하며 Container Layer 만 각 인스턴스별로 생겨 자신의 쓰기 스토리지가 생기는 구조이며, 기본적으로 컨테이너가 실행되면서 생긴 스토리지는 실행된 컨테이너가 삭제될 때 같이 삭제됩니다.  

 

위의 Dockerfile 에서 두번째 라인이 현재 디렉토리를 /app 아래로 복사는 명령어에서 현재 디렉토리에 있는 파일이 변경되었다고 가정해 봅시다.


다시 이미지를 만들 경우 맨아래 d74508fb6632 이미지 레이어는 링크로 그대로 활용되고, c22013c84729 이미지 레이어를 먼저 복사하고(copy), 수정된 파일들을 /app 디렉토리 아래로 복사하는(write) 부분이 일어나며 그 이후의 이미지 레이어들은 계속 실행됩니다.



Dockerfile 을 작성할 때 기본 예약 명령어는 다음과 같습니다.


FROM nginx:alpine


nginx 이미지의 alpine 태그를 Base 이미지로 사용합니다.



RUN mkdir -p /app


mkdir -p /app 명령을 실행합니다. shell 명령어를 쓸 때 사용합니다.



ADD source.tar /app/


source.tar 를 /app 디렉토리 아래에 풉니다.



ADD http://ahnseungkyu.com/release /data/


인터넷의 http://ahnseungkyu.com/release 파일을 /data 디렉토리 아래에 다운받습니다.



COPY . /app


현재 디렉토리의 모든 파일과 하위 디렉토리를 /app 디렉토리 아래로 복사합니다. 



COPY ./conf /app/conf


conf 디렉토리 아래의 모든 파일과 하위 디렉토리를 /app/conf 디렉토리 아래로 복사합니다.



COPY ./*.js /app/


현재 디렉토리으 모든 js 파일을 /app 디렉토리 아래로 복사합니다.




추가로 Dockerfile 을 작성할 때 가장 기본이 되는 CMD 와 ENTRYPOINT 의 차이에 대해서 알아보겠습니다.

Dockerifle 에서는 기본적으로 최소 하나의 CMD 혹은 ENTRYPOINT 가 있어야 하며, 둘 다 있을 수 도 있습니다.

즉, docker 를 실행할 때 CMD 로 호출하거나 ENTRYPOINT 로 호출 할 수 있습니다.

아래 alpine ping Dockerfile 을 예로 살펴보겠습니다.


From alpine:latest

RUN apk update && apk add --update iputils \

  && rm -rf /var/cache/apk/*

CMD ["ping", "-c", "3", "8.8.8.8"]



여기서는 CMD 를 사용하여 ping -c 3 8.8.8.8 명령어를 실행했습니다.

docker 이미지를 만들고 실행해보면 결과가 잘 나옵니다.


$ sudo docker build -t seungkyua/alpine-sample .


$ sudo docker run --name alipine-sample --rm -it seungkyua/alpine-sample



이것을 ENTRYPOINT 사용으로 바꾸면 다음과 같습니다.


From alpine:latest

RUN apk update && apk add --update iputils \

  && rm -rf /var/cache/apk/*

ENTRYPOINT ["ping", "-c", "3", "8.8.8.8"]



똑같이 결과가 나타납니다.


그럼 이 둘의 차이는 무엇일까요? 그건 둘 다 사용했을 때 알 수 있습니다. 다음과 같이 바꿔보겠습니다.


From alpine:latest

RUN apk update && apk add --update iputils \

  && rm -rf /var/cache/apk/*

ENTRYPOINT ["ping"]

CMD ["-c", "3", "8.8.8.8"]



결과가 똑같습니다. 하지만 여기서 아래와 같이 docker 를 실행할 때 8.8.8.8 을 맨 뒤에 추가하면 결과가 달라집니다.


$ sudo docker run --name alipine-sample --rm -it seungkyua/alpine-sample 8.8.8.8


마지막의 CMD 부분이 argument 로 처리 되는데 8.8.8.8 이라는 argument 가 override 되어 -c 3 8.8.8.8 부분이 8.8.8.8 로 되어 버립니다. 


결과적으로 ENTRYPOINT 와 CMD 를 함께 쓰는 경우는 CMD argument 를 override 하고 싶을 때 사용할 수 있기 때문에 가능하면 둘 다 사용하는 방법을 추천드립니다.






Posted by Kubernetes Korea co-leader seungkyua@gmail.com
TAG docker, Layer