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

PID namespace 외에 Network namespace 를 알아보겠습니다.

처음 글을 시작한 의도는 docker 를 잘 활용하자는 의미였는데 하다보니 아직 그 내용은 들어가 보지도 못했네요.


개발자나 운영자들은 잘 인식하지 못하지만 linux 의 Network 는 모두 namespace 구조하에 움직이고 있습니다.


기본적으로 host 기반으로 사용하는 network 의 namespace 는 1 입니다. 그리고 docker 가 실행되면 docker process 에 대해서 내부적인 network 를 사용합니다.


network namespace 와 network namespace 간의 연결은 virtual ethernet (veth) 를 사용하여 연결합니다. 즉, veth pair 를 만들어서 하나는 docker 내부에 다른 하나는 host 로 하여 서로 네트워크로 연결하고 host 에 있는 veth 는 host 의 bridge 로 연결하면 docker 내부에서 host 의 bridge 로 연결이 됩니다.

 

docker 컨테이너 (eth0)  <-> host veth  <->  host bridge (docker0)  <->  host network (ens2f0)

 


먼저, host veth 의 리스트를 보겠습니다.


$ sudo ip addr show


15479: vethf5fa5c1@if15478: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master docker0 state UP

    link/ether 32:83:61:96:55:4a brd ff:ff:ff:ff:ff:ff link-netnsid 1

15481: veth4667f7d@if15480: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master docker0 state UP

    link/ether 56:39:6e:41:3d:50 brd ff:ff:ff:ff:ff:ff link-netnsid 0

15483: vethd61c546@if15482: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master docker0 state UP

    link/ether b2:4a:88:71:d0:03 brd ff:ff:ff:ff:ff:ff link-netnsid 2

15489: vethbe5dd5b@if15488: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master docker_gwbridge state UP

    link/ether fe:85:37:ac:ea:76 brd ff:ff:ff:ff:ff:ff link-netnsid 4

15493: veth66998b7@if15492: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master docker0 state UP

    link/ether 42:5f:03:92:45:9d brd ff:ff:ff:ff:ff:ff link-netnsid 5

 


총 5개가 보이는데 이중 하나는 docker gateway bridge 로 연결되고 나머지 4개는 1:1 로 각각 docker 내부의 eth0 와 연결되어 있습니다.


이를 좀 쉽게 찾기 위해서 veth.sh 파일을 만들었습니다.


#!/bin/bash


VETHS=`ifconfig -a | grep veth | sed 's/ .*//g'`

DOCKERS=$(docker ps -a | grep Up | awk '{print $1}')


for VETH in $VETHS

do

  PEER_IFINDEX=`ethtool -S $VETH 2>/dev/null | grep peer_ifindex | sed 's/ *peer_ifindex: *//g'`

  for DOCKER in $DOCKERS

  do

    PEER_IF=`docker exec $DOCKER ip link list 2>/dev/null | grep "^$PEER_IFINDEX:" | awk '{print $2}' | sed 's/:.*//g'`

    if [ -z "$PEER_IF" ]; then

      continue

    else

      printf "%-10s is paired with %-10s on %-20s\n" $VETH $PEER_IF $DOCKER

      break

    fi

  done

done



위 파일을 실행하면 아래와 같이 나옵니다.


veth4667f7d: is paired with eth0@if15481 on b75a945f84fa        

veth66998b7: is paired with eth0@if15493 on 67246d3e1c68        

vethd61c546: is paired with eth0@if15483 on 87faa6bacbba        

vethf5fa5c1: is paired with eth0@if15479 on 0b4bc5ba78f9

 

마지막에 보이는 것은 docker id 입니다.

 

host 의 docker0 bridge 가 어떻게 연결되어 있는 지는 brctl 로 알 수 있습니다. 


$ sudo yum install -y bridge-utils


$ sudo brctl show




docker0 bridge 가 어떻게 host 의 외부 네트워크로 나가는 지는 iptables 와 routing 을 보면 알 수 있습니다.


$ sudo iptables -t nat -S


-A POSTROUTING -s 172.17.0.0/16 ! -o docker0 -j MASQUERADE

 


source 가 docker 내부 ip 대역이고 host 입장에서 패킷이 나가는 인터페이스가  docker0 가 아니면 jump 를 하고 host의 ip 로 변경해서 (masquerade) 나가라고 되어 있습니다. 

 


$ netstat -rn


Destination     Gateway         Genmask         Flags   MSS Window  irtt Iface

0.0.0.0            192.168.30.1    0.0.0.0           UG        0 0          0 ens2f0

172.17.0.0        0.0.0.0            255.255.0.0     U         0 0          0 docker0

172.18.0.0        0.0.0.0            255.255.0.0     U         0 0          0 docker_gwbridge

 

그리고 위와 같이 외부로 나갈 때는 default gateway 로 외부로 나가게 됩니다.

 

 

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

컨테이너가 linux 의 cgroup 과 namespace 를 활용하여 동작하는 것은 이제 보편적으로 알려진 내용입니다. cgroup 은 자원의 활용에 사용하고 namespace 는 자원의 isolation 에 사용합니다.


이 중에서 namespace 의 종류는 다음과 같습니다.


ipc : 프로세스간 양방향 통신

mnt : 파일 시스템

net : 네트워크

pid : 프로세스 id

user : 사용자 계정

uts : Unix time sharing

 

위의 namespace를 기본으로 docker 의 프로세스 구조를 보면 다음과 같습니다.

먼저, pstree 명령어를 설치합니다.  어떤 패키지인지 모르면 아래 명령어로 패키지명을 찾을 수 있습니다.


$ sudo yum whatprovides pstree

psmisc-22.20-15.el7.x86_64 : Utilities for managing processes on your system

Repo        : base

Matched from:

Filename    : /usr/bin/pstree

 

$ sudo yum install -y psmisc

 

이제 pstree 로 docker 의 패키지를 봅니다.


$ pstree -lSps $(pidof dockerd)




() 안에 들어간 번호는 프로세스 id 입니다.  dockerd 프로세스는 15472 이고 이후의 프로세스 명이 잘려서 보이니 ps 로 프로세스 명을 보면 다음과 같습니다.


$ ps -ef | grep 15489


15489 는 docker-containerd 입니다.

 

$ ps -ef | grep 15675



15675 는 docker-container-shim 입니다.


$ ps -ef | grep 15711


그리고 실제 컨테이너로 띄고 있는 minio 프로세스는 15711 입니다.



위의 내용을 보면 docker 프로세스는 다음과 같은 구조로 되어 있습니다.


dockerd (15472, docker 데몬)   ->   docker-containerd (15489, docker cotainerd) -> docker-container-shim (15675) -> minio (15711, 실행중인 docker 컨테이너)


docker-containerd 는 docker runtime 과 관련 있습니다. 컨테이너 실행은 runc 와 containerd 를 따릅니다. runc 는 Open Container Initiative(OCI) 에 의해 정의된 컨테이너를 실행시키는 부분으로 namespace 와 보안에 관한 표준입니다. containerd 는 runc 를 기반으로 컨테이너 이미지 관리, 스토리지, 실행 등의 기능을 확장한 것으로 CNCF 에 기부된 프로젝트 입니다.

 

docker-container-shim 은 docker 에서 자체적으로 추가한 것으로, 실행되는 컨테이너와 containerd 사이에서 컨테이너를 연결하여 containerd  가 문제가 생기더라도 실행되는 컨테이너에는 여향을 주지 않기 위해서 만들었습니다. 그런데 사실 이게 왜 더 필요한지는 모르겠습니다. 괜히 관리 레이어만 하나 더 추가된 부분으로 보이긴 합니다.

 

PID namespace 를 docker 에서 어떻게 적용되는지 보겠습니다.


앞에서 minio 컨테이너는 host 에서 보면 프로세스 id 가 15711 이고 그 부모 프로세스는 15675 인 docker-container-shim 인 것을 확인했습니다.


이제 minio 컨테이너를 아래와 같이 확인 합니다.


$ sudo docker (container) ls | grep minio

0b4bc5ba78f9        minio/minio                 "minio server /export"   18 months ago       Up 4 hours          0.0.0.0:9000->9000/tcp   minio

 

그리고, minio 컨테이너 안으로 들어가 보겠습니다.

sudo docker (container) exec -it 0b4bc5ba78f9 sh

 

프로세스를 조회해보면 다음과 같이 1번 프로세스로 minio 서버를 띄운 것이 보이고 (컨테이너 밖의 host 에서는 15711) 그 위의 부모 프로세스는 보이지 않습니다.




 

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

Kubernetes 는 여러 컨테이너를 관리할 수 있습니다. 그 이유는 kubernetes kubelet 과 컨테이너 데몬이 정해진 spec 에 따라 gRPC 통신을 하기 때문입니다.  Kubernetes 는 여러 컨테이너를 적용할 수 있는데 rkt, docker, CRI-O 등이 있습니다. 이중에서 가장 많이 사용하는 Docker 에 대해서 설명하고자 합니다.

앞서 kubespray 사용에 대해서 설명이 있는데, 여기를 보면 Production Level 로 사용 가능한 CentOS, RedHat, Ubuntu에 맞게 docker 를 쉽게 설치할 수 있습니다. 여기서는 Docker 를 이미 설치한 후에 어떻게 Docker 를 잘 활용할 수 있는지에 대해서 설명합니다.

1. dockerd config option reload

대부분의 Linux 는 이제 systemd 를 활용하고 있습니다. 그렇기 때문에 docker 데몬 실행은 systemd 에 설치된 docker.service 를 보면 됩니다. 디렉토리는 둘 중에 하나를 찾아보면 됩니다.

/etc/systemd/system/multi-user.target.wants/docker.service

/etc/systemd/system/docker.service

 

제 환경은 CentOS 로 이렇게 정의가 되어 있습니다.

MountFlags=shared

EnvironmentFile=-/etc/sysconfig/docker

ExecStart=/usr/bin/dockerd $DOCKER_OPTS

ExecReload=/bin/kill -s HUP $MAINPID

 

dockerd 의 환경 파일로 /etc/sysconfig/docker 를 읽게 되어 있고 해당 파일은 아래 옵션으로 정의되어 있습니다.

DOCKER_OPTS="--storage-driver=overlay2 --insecure-registry docker-registry:5000 --insecure-registry oreo:5000"

 

이 경우 systemctl restart docker.service 를 하면 dockerd 가 restart 되는데 문제는 이미 실행되고 있는 docker 컨테이너가 영향을 받아 stop 으로 빠집니다. (kubernetes 의 deployment, daemonset, statefulset 혹은 docker 의 --restart=always 가 아닌 경우) 그러므로, docker 옵션만 reload 해야 기존의 실행되고 있는 컨테이너가 영향을 받지 않습니다.

docker option reload 는 SIGHUP 을 보내면 됩니다. 

$ sudo kill -SIGHUP $(pidof dockerd)

 

하지만, 지금의 세팅으로는 다시 올라오지 않습니다.

dockerd 로그를 보면 다음과 같습니다. 로그는 다음 명령어로 볼 수 있습니다.

$ sudo journalctl --no-pager -u docker.service

kube-deploy dockerd[16391]: time="2018-10-27T18:36:17.767207453+09:00" level=info msg="Got signal to reload configuration, reloading from: /etc/docker/daemon.json"

kube-deploy dockerd[16391]: time="2018-10-27T18:36:17.767283042+09:00" level=error msg="open /etc/docker/daemon.json: no such file or directory"

 

디폴트로 dockerd 옵션은 /etc/docker/daemon.json 파일을 읽기 때문에 옵션  reload  가 안되는 문제가 있습니다.

이를 해결하기 위해서 다음과 같이 변경해 보겠습니다.

docker.service 에 정의된 EnvironmentFile 의 변수를 daemon.json 으로 옮기겠습니다.

/etc/systemd/system/multi-user.target.wants/docker.service

EnvironmentFile=-

 

$ sudo mkdir -p /etc/docker

$ sudo vi /etc/docker/daemon.json

{

    "storage-driver": "overlay2",

    "insecure-registries": ["docker-registry:5000", "oreo:5000"]

}

 

docker.service 를 바꿨으니 어쩔 수 없이 한 번의 docker restart 는 해야 합니다.

$ sudo systemctl daemon-reload

$ systemctl restart docker.service

 

이제 현재의 dockerd option 이 어떤지 보겠습니다.

$ sudo docker system info

 

Storage Driver: overlay2

Insecure Registries:

  docker-registry:5000

  oreo:5000

  127.0.0.0/8

 

Insecure Registries 의 oreo:5000 을 제거하고 싶다면 다음과 같이 daemon.json 에서 변경하면 됩니다.

{

    "storage-driver": "overlay2",

    "insecure-registries": ["docker-registry:5000"]

}

 

그리고 SIGHUP 을 보내서 option reload 를 하면 새로운 옵션이 적용됩니다.

$ sudo kill -SIGHUP $(pidof dockerd)

 

dockerd 옵션에 oreo:5000 이 Insecure Registries 에서 빠져 있는 것을 볼 수 있습니다.

$ sudo docker system info

 

Storage Driver: overlay2

Insecure Registries:

  docker-registry:5000

  127.0.0.0/8

 

 

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

일반적으로 RBAC 으로 Kubernetes Dashboard 를 설치하면 service account 의 token 을 가지고 접속해야 합니다.


admin clusterrole 을 가진 secret 을 찾아서 해당 token 으로 접속합니다.

$ kubectl -n kube-system describe secret clusterrole-aggregation-controller-token-fmnfg


Name:         clusterrole-aggregation-controller-token-fmnfg

Namespace:    kube-system

Labels:       <none>

Annotations:  kubernetes.io/service-account.name=clusterrole-aggregation-controller

              kubernetes.io/service-account.uid=ccbaf065-8f01-11e8-80c8-3ca82a1ccfd4


Type:  kubernetes.io/service-account-token


Data

====

ca.crt:     1025 bytes

namespace:  11 bytes

token:      xxxxxxxxxxxxxxxxxxxxxxxxxxxx


매번 이런 token 방식이 귀찮으면 아래와 같이 dashboard service account 에 admin clusterrole 을 지정하고 로그인 화면에서는 skip 을 클릭하여 넘어가면 됩니다.

$ kubectl get serviceaccount -n kube-system | grep kubernetes-dashboard

kubernetes-dashboard                 1         8d 


$ kubectl create clusterrolebinding kubernetes-dashboard-admin \

--clusterrole=cluster-admin \

--serviceaccount=kube-system:kubernetes-dashboard





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


Kubernetes dashboard 를 Ingress Controller 와 연결할 때 SSL 을 적용하고 싶으면 다음과 같이 Ingress 를 생성하면 된다.



Kubernetes dashboard ingress 


apiVersion: extensions/v1beta1
kind: Ingress
metadata:
annotations:
kubernetes.io/ingress.class: nginx
nginx.ingress.kubernetes.io/secure-backends: "true"
ingress.kubernetes.io/ssl-passthrough: "true"
nginx.org/ssl-backend: "kubernetes-dashboard"
kubernetes.io/ingress.allow-http: "false"
nginx.ingress.kubernetes.io/rewrite-target: /
nginx.ingress.kubernetes.io/proxy-body-size: "100M"
name: kubernetes-dashboard
namespace: kube-system
spec:
tls:
- secretName: kubernetes-dashboard-certs
rules:
- host: dashboard.k8s.stage
http:
paths:
- path: /
backend:
serviceName: kubernetes-dashboard
servicePort: 443



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

Kubernetes pause infra container 는 기존의 golang 에서 c 로 변경되었다.


main 함수를 보면 다음과 같다.

int main(int argc, char **argv) {
int i;
for (i = 1; i < argc; ++i) {
if (!strcasecmp(argv[i], "-v")) {
printf("pause.c %s\n", VERSION_STRING(VERSION));
return 0;
}
}

if (getpid() != 1)
/* Not an error because pause sees use outside of infra containers. */
fprintf(stderr, "Warning: pause should be the first process\n");

if (sigaction(SIGINT, &(struct sigaction){.sa_handler = sigdown}, NULL) < 0)
return 1;
if (sigaction(SIGTERM, &(struct sigaction){.sa_handler = sigdown}, NULL) < 0)
return 2;
if (sigaction(SIGCHLD, &(struct sigaction){.sa_handler = sigreap,
.sa_flags = SA_NOCLDSTOP},
NULL) < 0)
return 3;

for (;;)
pause();
fprintf(stderr, "Error: infinite loop terminated\n");
return 42;
}


pause 는 infra container 이므로 getpid() 가 1 인 최상의 프로세스가 되는 것이 Security 측면에서 바람직하다.


sigaction 은 signal 이 SIGINT (키보드 ctrl + c 로 종료), SIGTERM (종료) 가 들어오면 sigdown 함수를 handler 로 등록해서 수행한다.

자식 프로세스가 종료되거나 정지/재시작 될 때 SIGCHLD signal 이 발생하는데, SA_NOCLDSTOP flag 는 자식 프로세스가 정지되는 4가지의 signal - SIGSTOP(프로세스 정지), SIGTSTP(키보드 ctrl + z 로 발생한 프로세스 정지), SIGTTIN(백그라운드에서 제어터미널 읽기를 시도해서 정지), SIGTTOU(백그라운드에서 제어터미널 쓰기를 시도해서 정지) 등을 받아서 정지되거나 CONTINUE signal 을 받아서 재시작되어도 이를 통지 받지 않겠다는 의미이다. 즉, pause 가 부모 프로세스가 이지만 SIGCHLD signal 을 통보받을 필요가 없다고 생각하는 것이다.  하지만 그 나머지인 경우인 자식 프로세스가 종료되는 경우에는 signal 을 받을 수 밖에 없다. 이 때는 waitpid 함수를 통해서 혹시라도 자식 프로세스가 좀비가 되었을 때 좀비 프로세스를 제거할 수 있다.


sigaction 함수는 에러가 발생하면 -1 을 정상 처리되면 0을 리턴한다.


static void sigdown(int signo) {
psignal(signo, "Shutting down, got signal");
exit(0);
}

psignal 함수는 두번째 인자로 들어온 string 을 stderr 로 출력한다.



static void sigreap(int signo) {
while (waitpid(-1, NULL, WNOHANG) > 0)
;
}

자식 프로세스가 왜 종료 되었는지는 관심이 없고 단순히 자식 프로세스가 종료될 때 좀비 프로세스를 막고자 한다면 waitpid 함수를 위와 같이 사용한다.


첫번재 인자로 전달된 pid 값이 -1 이면 모든 자식 프로세스가 종료될 때 까지 기다린다. 하지만 마지막 인자로 WNOHANG 이 입력되면 부모 프로세스는 기다리지 않고 바로 리턴된다. 하나의 SIGCHLD 에 대해서 while 문으로 처리한 이유는 여러 자식 프로세스가 종료될 수 있는 가능성이 있기 때문이다. 즉, SIGCHLD signal 이전에 좀비 프로세스가 있으면 그것을 처리한다. 


그럼 결과적으로는 SIGCHLD signal 이 발생할 때 pause 프로세스는 아무것도 기다리지 않고 좀비 프로세스가 있으면 처리하고 바로 리턴한다.

그리고 마지막에 pause() 함수에 의해서 잠시 정지 상태가 된다. 



* 좀비 프로세스 : 자식 프로세스가 종료되어 사용하는 리소스는 모두 해제된 상태지만, 부모 프로세스가 자식 프로세스의 종료를 확인하지 못한 상태로 커널의 프로세스 테이블에는 관리되고 있는 상태

* 고아 프로세스 : 자식 프로세스 보다 부모 프로세스가 죽었을 경우 자식 프로세스가 pid 1 인 init 프로세스에 속하게 된 경우




 



 
















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