레이블이 docker인 게시물을 표시합니다. 모든 게시물 표시
레이블이 docker인 게시물을 표시합니다. 모든 게시물 표시

golang pthread_create failed

# github action 과 비슷한 사내 환경에서 golang 이미지 기반의 빌드로 docker build 를 수행하면 다음과 같은 에러가 발생한다.
RUN go install github.com/swaggo/swag/cmd/swag@latest && swag init
go: downloading golang.org/x/sys v0.18.0
runtime/cgo: pthread_create failed: Operation not permitted
SIGABRT: abort

# 이 현상으로 이미 이슈가 등록되어 있지만 아직 해결되진 않았다.

근본적인 원인은 libseccomp 라는 Debian Debian Bookworm 패키지/라이브러리에 사용되는 새로운 syscall 이 차단되었기 때문이라고 한다. 

golang:1.23.3 (태그 버전에 suffix 가 없으면 현재 debian bookworm 버전임)
이미지를 사용할때 발생한 문제로
위 글중에 bullseye 를 사용하면 된다고 하는 커멘트가 있어 
golang:1.23.3-bullseye 로 변경해서 시도하니 된다.

# 참고로 로컬에 이미지 다운 받아 버전 확인해보면 debian 버전이 다르다.
docker run -it golang:1.23.3

docker run -it golang:1.23.3-bullseye

# 참고 golang 이미지

kaniko args

# k8s pod 환경에서 이미지 빌드를 위해 kaniko 를 사용한다.
# github pull, docker registry push 를 위해 다음 2가지를 준비한다.
# github > personal_access_token > repo 접근 권한체크해서 생성
kubectl create secret generic ysoftman-generic-secret \
  --from-literal=git-personal-access-token="abc123" \
  --namespace=ysoftman-test

# 이미지 푸시를 위새 docker secret 생성
kubectl create secret docker-registry ysoftman-secret \
  --docker-server=ysoftman \
  --docker-username=ysoftman \
  --docker-password=ysoftman123 \
  --namespace=ysoftman-test

# 이제 argo workflow 로 kaniko(executor) 로 실행하는데,
# dockerfile ARGS 에 전달하기 위해 --build-arg 옵션을 아래와 같이 사용했다.
apiVersion: argoproj.io/v1alpha1
kind: Workflow
metadata:
  generateName: ysoftman-test
  namespace: ysoftman-test
spec:
  entrypoint: build-image-and-push
  serviceAccountName: workflow-template
  templates:
    - name: build-image-and-push
      inputs:
        parameters:
          - name: fruit
            value: "lemon"
      script:
        image: "rockylinux:latest"
        command: [bash]
        source: |
          curl -X GET "https://httpbin.org/get" -H "accept: application/json"
          echo "-----"
          echo $ysoftman1
          echo $ysoftman2
        env:
          - name: ysoftman1
            value: lemon
          - name: ysoftman2
            valueFrom:
              secretKeyRef:
                name: my-secret # name of an existing k8s secret
                key: mypassword # 'key' subcomponent of the secret
      container:
        name: kaniko
        image: "gcr.io/kaniko-project/executor:debug"
        env:
          - name: github_personal_access_token
            valueFrom:
              secretKeyRef:
                name: ysoftman-generic-secret
                key: git-personal-access-token
        command: [executor]
        args:
          - "--context=git://$(github_personal_access_token)@github.com/ysoftman/foobar.git#refs/heads/branch1"
          - "--context-sub-path=./aaa/bbb"
          - "--dockerfile=Dockerfile"
          - "--destination=ysoftman/foobar:test"
          - "--build-arg var1={{inputs.parameters.fruit}}"
        volumeMounts:
          - name: kaniko-secret
            mountPath: /kaniko/.docker/
      volumes:
        - name: kaniko-secret
          secret:
            secretname: ysoftman-secret
            items:
              - key: .dockerconfigjson

# 그런데 pod 로그에 다음과 같이 에러가 발생한다.
Error: unknown flag: --build-arg var1

# --build-arg 사용시 IFS(Internal Field Separator) 공백구분을 지원하지 않아 export IFS='' 를 설정하라고 한다.
# 위 와 같은 yaml 에서는 IFS 설정이 안되니 다음과 같이 구분하면 된다.
args:
  - "--build-arg"
  - "var1={{inputs.parameters.fruit}}"

# 그리고 container > args 에서 env 참조시 $(VAR_NAME) 를 사용해야 한다.
args:
 - "foobar=$(github_personal_access_token)"

failed to start docker service

# docker 서비스 상태를 보면
sudo systemctl status docker.service

# 다음과 같이 failed 상태다.
● docker.service - Docker Application Container Engine
   Loaded: loaded (/usr/lib/systemd/system/docker.service; enabled; vendor preset: disabled)
   Active: failed (Result: exit-code) since Tue 2023-11-07 10:37:32 KST; 3min 6s ago
     Docs: https://docs.docker.com
  Process: 3637521 ExecStart=/usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock (code=exited, status=1/FAILURE)
 Main PID: 3637521 (code=exited, status=1/FAILURE)
11월 07 10:37:32 ysoftman-dev1 systemd[1]: docker.service: Start request repeated too quickly.
11월 07 10:37:32 ysoftman-dev1 systemd[1]: docker.service: Failed with result 'exit-code'.
11월 07 10:37:32 ysoftman-dev1 systemd[1]: Failed to start Docker Application Container Engine.

# 도커 버전 확인
/usr/bin/dockerd --version
Docker version 24.0.7, build 311b9ff

# 서비스 시작 명령을 수행하면
sudo /usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock

# 다음과 같이 daemon.json 에 호스트를 설정이 있는데 -H 옵션으로 또 지정해서 문제가 된다.
# -H, --host list (Daemon socket(s) to connect to)
unable to configure the Docker daemon with file /etc/docker/daemon.json: the following directives are specified both as a flag and in the configuration file: hosts: (from flag: [fd://], from file: [unix:///var/run/docker.sock tcp://0.0.0.0:2375])

# /usr/lib/systemd/system/docker.service 파일을 보면 다음과 같이 -H 옵션이 있다.
ExecStart=/usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock

# 찾아보니 같은 이슈가 많이 있다.
# 옵션 중복 허용을 하던가 설정 덮어쓰기를 하던가 수정이 필요해 보이는데 개선되지 않고 있다.
# 현재로선 daemon.json hosts 를 사용한다면 -H 옵션을 제거하는 방식으로 해결한다.

# 다음과 같이 -H 옵션은 제거해 daemon.json 설정만 사용하도록 했다.
ExecStart=/usr/bin/dockerd --containerd=/run/containerd/containerd.sock

docker build image apt-update error

# 잘 되던 docker 이미지 빌드 중 Dockerfile 에서 패키지 업데이트시
RUN apt-get update
...
#6 10.23 Reading package lists...
#6 10.24 W: The repository 'http://security.debian.org/debian-security stretch/updates Release' does not have a Release file.
#6 10.24 W: The repository 'http://deb.debian.org/debian stretch Release' does not have a Release file.
#6 10.24 W: The repository 'http://deb.debian.org/debian stretch-updates Release' does not have a Release file.
#6 10.24 E: Failed to fetch http://security.debian.org/debian-security/dists/stretch/updates/main/binary-amd64/Packages  404  Not Found
#6 10.24 E: Failed to fetch http://deb.debian.org/debian/dists/stretch/main/binary-amd64/Packages  404  Not Found
#6 10.24 E: Failed to fetch http://deb.debian.org/debian/dists/stretch-updates/main/binary-amd64/Packages  404  Not Found
#6 10.24 E: Some index files failed to download. They have been ignored, or old ones used instead.
------
executor failed running [/bin/sh -c apt-get update && apt-get install -y net-tools htop lsof wget curl rsync vim tar man-db traceroute]: exit code: 100

# 기본 이미지를 변경하면 잘 동작한다. 
FROM golang:1.17-stretch  -> FROM golang:1.19

# 참고로 데이안 계열 이미지 suffix 로 붙는 이름(코드네임)
-없는경우 debian latest
-bullseye: debian 11 
-buster: debian 10.4
-stretch: debian 9
-jessie: debian 8

kubectl top node error

# 설치
kubectl apply -f https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/components.yaml

# node 를 조회하면 다음 에러가 발생한다.(kubectl top pods 는 정상동작)
kubectl top node
error: metrics not available yet

# kube-system > metrics-server deployment 가 제대로 동작하고 있지 않았다.
kubectl get deployment metrics-server -n kube-system
NAME             READY   UP-TO-DATE   AVAILABLE   AGE
metrics-server   0/1     1            0           26h

# 이슈1 - 이미지 다운로드 실패
# 원인은 registry.k8s.io 에 대해서 방화벽 설정으로 이미지를 받아오지 못해서였다.
Pulling image "registry.k8s.io/metrics-server/metrics-server:v0.6.3"

# 우선 로컬에 이미지를 다운로드 받자.
docker pull registry.k8s.io/metrics-server/metrics-server:v0.6.3

# 이미지를 tar 로 만든다.
docker save -o metrics-server-v0.6.3.tar registry.k8s.io/metrics-server/metrics-server:v0.6.3

# 워커 노드에 tar 파일 전송
rsync -avz ./metrics-server-v0.6.3.tar ysoftman@worker1/home/ysoftman/
rsync -avz ./metrics-server-v0.6.3.tar ysoftman@worker2/home/ysoftman/
rsync -avz ./metrics-server-v0.6.3.tar ysoftman@worker3/home/ysoftman/

# 노드의 tar 이미지 파일을 로딩한다.
ssh ysoftman@worker1 "sudo docker load -i /home/ysoftman/metrics-server-v0.6.3.tar"
ssh ysoftman@worker2 "sudo docker load -i /home/ysoftman/metrics-server-v0.6.3.tar"
ssh ysoftman@worker3 "sudo docker load -i /home/ysoftman/metrics-server-v0.6.3.tar"

# metrics-server deployment 명세에 다음이 설정되어 있기 때문에
# imagePullPolicy: IfNotPresent
# pod 가 running 된다.

# 이슈2 - tls 비활성화
# metrics 로그를 보면 아래와같이 노드(kubelet)의 메트릭 수집시 실패한다고 나온다.
scraper.go:140] "Failed to scrape node" err="Get \"https://10.10.10.100:10250/metrics/resource\": read tcp 10.10.1.10:40752->10.10.10.100:10250: read: connection reset by peer" node="ysoftman-cpu1"

# metrics-server deployment> containers args 에 다음을 추가하면 위 에러는 발행하지 않고 정상 동작(scraping node ... scrap finished)한다.
- --secure-port=4443
- --kubelet-preferred-address-types=InternalIP,ExternalIP,Hostname
- --kubelet-use-node-status-port
- --metric-resolution=15s
- --kubelet-insecure-tls  # 추가옵션
- --v=6 # 추가옵션 (Scraping node "node=xxxx" 노드 수집 성공 메시지 확인용)

# 이슈3 - apiservice 적용 안되는 이슈
# 아직도 error:metrics not available yet 이 발생한다.
# 찾아보니 kubectl 에서 top 커맨드 사용시 발생하는 에러 메시지였다.

# k8s api 로 node 메트릭을 다음과 같이 실행하면 결과가 나온다.
NODE_NAME="ysoftman-cpu1"
kubectl get --raw /api/v1/nodes/$NODE_NAME/proxy/metrics/resource | grep -i -E "node_cpu|node_mem"

# metrics api 에서 pods 응답은 나온다.
kubectl get --raw /apis/metrics.k8s.io/v1beta1/pods | jq 

# 하지만 nodes 응답을 보면 items: [] 로 빈값으로 나온다.
kubectl get --raw /apis/metrics.k8s.io/v1beta1/nodes | jq 

# metrics-server pod log 는 다음과 같이 노드로부터 정보를 받아 저장했다고 나온다.
round_trippers.go:553] GET https://10.10.10.10:10250/metrics/resource 200 OK in 2 millisecond
server.go:139] "Storing metrics"
server.go:144] "Scraping cycle complete"

# metrics-server 를 삭제했는데, k top nodes/pods 에러가 발행하지 않는다.
kubectl delete -f https://github.com/kubernetes-sigs/metrics-server/releases/latest/downl
oad/components.yaml

# top 명령을 받고 있는 별도의 pod 가 있는것으로 의심된다.
# v1beta1.metrics.k8s.io 를 사용하는 api 서비를 조회하면
kubectl get apiservices | grep -i v1beta1.metrics.k8s.io

# 다음과 같이 prometheus-adapter 서비스가 나온다.
v1beta1.metrics.k8s.io     prometheus-adapter/prometheus-adapter

# kubectl top nodes 동작하는 클러스터는 다음과 같다.
v1beta1.metrics.k8s.io     kube-system/metrics-server

# metrics-server (kind:APIService) 부분이 반영이 안되는게 문제였다.
# prometheus-adapter > v1beta1.metrics.k8s.io 를 사용하고 있고, 이를 argo-cd 에서 항상 sync 하고 있어 삭제해도 다시 생성된다.

add timezone in alpine

# golang 에서 타임존이 변경된 time 사용을 위해 time.LoadLocation 를 사용했다.
loc, _ := time.LoadLocation("Asia/Seoul")
t = t.In(loc)

# 로컬에서는 동작했지만,
# 배포된 도커에서는 loc 를 찾을 수 없어 panic 이 발생하고 있었다.
[PANIC RECOVER] time: missing Location in call to Time.

# 컨테이너 사이즈를 줄이기 위해 alpine 이미지를 사용하고 있었는데,
# LoadLocation 가 참조하는 zoneinfo 데이터가 존재하지 않아 발생한 문제였다.

# dockerfile 에 tzdata 를 설치를 추가하자.
RUN apk add tzdata

# 이제 다음과 같은 위치에 timezone 데이터 값이 추가되고,
# time.LoadLocation 도 사용할 수 있다.
/usr/share/zoneinfo/Asia/Seoul

dive no space error, lazydocker, docker-slim

# mac 에서 잘동작하던 dive 이 도커 이미지 내용을 보려고 하면 다음과 같은 에러가 발생했다.
dive ysoftman-test-image:test
...
Error response from daemon: write /var/lib/docker/tmp/docker-export-988773316/206ab2532181c3c32223f640b05e841439179a3c
b349bb2818503a346ccfeda1/layer.tar: no space left on device

# system prune 으로 이미지를 삭제하면 된다.
docker system prune

# 참고로 docker root 경로를 조회하면 다음과 같이 나온다.
# docker info | rg -iN dir
Docker Root Dir: /var/lib/docker

# 하지만 mac 에선 /var/lib/docker 는 존재하진 않고 os 마다 데이터 위치가 다르다.
linux: /var/lib/docker/
windows: C:\ProgramData\DockerDesktop
mac: ${HOME}/Library/Containers/com.docker.docker/Data/vms/0/   

##########

# mac 에서 dive 실행시 다음과 같이 docker.sock 에 연결할 수 없다고 나온다.
dive ysoftman-test-image:test
Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running

# docker desktop 을 사용하고 있는데 다음과 같이 ~/.docker/ 로 관리되고 있다.

# /var/run/docker.sock 파일이 없어 다음과 같이 링크후 dive 를 실행하면된다.
sudo ln -s ~/.docker/run/docker.sock /var/run/docker.sock

##########

brew install docker-slim

# docker-slim 최신 버전은 커맨드가 slim 으로 변경되었다.
# docker-slim 내용 확인
slim xray ysoftman-test-image:test

https://github.com/jesseduffield/lazydocker 로도 이미지내용을 파악할 수 있다.
# 터미널 창으로 구분해서 보여줘서 docker-slim 보다 보기 편하다.
brew install lazydocker
lazydocker

docker log size per line

# 도커 로깅에서 stdout 한줄의 길이가 16K 로 제한되어 있다고 한다.
# 관련해서 설정을 변경하는 PR 있었지만 진행되지 않았음.

docker save and load image

# 현재 로컬에 운영중인 컨테이너를 이미지로 만들기
docker commit -m test_message container ysoftman/centos_simple:latest

# 로컬에 빌드된 이미지를 .tar 파일로 아카이빙
docker save -o ysoftman-app.tar ysoftman/centos_simple:latest

# 원격 호스트에 파일 복사
rsync -avz ysoftman-app.tar ysoftman@remotehost:~/

# 원격 호스트에서 .tar 파일 로딩하면 docker images 로 이미지가 있는것을 알수 있다.
docker load -i ysoftman-app.tar

docker alpine not found error

# app(바이너리파일)을 추가한 apline 도커 이미지에서 다음과 같은 에러가 발생했다.
/ysoftman # ./app
/bin/sh: ./app: not found

# 다음과 같이 lib6-compat(glibc 호환 라이브러리 패키지)를 설치하면 된다.
RUN apk add libc6-compat

# 추가로 libstdc++, libgcc 등 을 찾을 수 없는 경우 설치하자.
RUN apk add libstdc++ libgcc

docker 로 pod 따라하기

# docker 로 pod(컨테이너들이 mount,pid,network 네임스페이스 공유)따라하기
# pod 는 다음 컨테이너들로 구성한다.
pause
nginx
ghost (nginx proxy_pass 를 받아 처리할 컨테이너, 참고:https://github.com/TryGhost/Ghost)

# pause(pod 내 컨테이너들의 부모 컨테이너로, ipc(pid), network 네임스페이스를 공유할 수 있도록) 컨테이너 생성
docker run -d --name pause -p 8080:80 --ipc="shareable" k8s.gcr.io/pause:3.2

# 생성된 pause 확인
# sandboxkey 의 network 네임스페이스 들어가서
sandboxkey=$(docker inspect pause | grep -i sandboxkey | awk '{print $2}' | tr -d ,\")
nsenter --net=$sandboxkey

# network 네임스페이스 확인(호스트의 값과 같다.)후 종료
lsns -p $$ -t net
ip a
exit

# 이제 nginx
# nginx.conf 파일 생성
# / 요청은 localhost:2368(ghost)로 프록시 패스한다.
cat > nginx.conf << zzz
error_log stderr;
events { worker_connections 1024; }
http {
  access_log /dev/stdout combined;
  server {
    listen 80 default_server;
    location / {
      proxy_pass http://127.0.0.1:2368;
    }
  }
}
zzz
# nginx 컨터이너 생성
# 위에서 만든 nginx.conf 를 볼륨 마운트하고
# pause 컨테이너의 ipc, network 를 사용(공유)
docker run -d --name nginx -v $(pwd)/nginx.conf:/etc/nginx/nginx.conf --network=container:pause --ipc=container:pause nginx

# nginx 컨테이너에 ip 명령 설치
docker exec nginx apt update
docker exec nginx apt install -y iproute2

# nginx ip 확인
docker exec nginx ip a

# ghost 컨테이너 생성
# pause 컨테이너의 ipc, network 를 사용(공유)
docker run -d --name ghost --network=container:pause --ipc=container:pause ghost

# ghost 컨테이너에 ip 명령 설치
docker exec ghost apt update
docker exec ghost apt install -y iproute2

# ghost ip 확인
docker exec ghost ip a

# 이제 nginx 에서 / 로 접속하면 ghost 결과가 나온다.
docker exec nginx curl -I localhost

runc 사용하기

# runc 는 docker, kubernetes(k8s)등에서 사용하는 OCI(Open Container Initiative) 표준을 준수하는 container 기동(spawning and running) CLI 툴이다.

# golang 설치
wget https://go.dev/dl/go1.17.5.linux-amd64.tar.gz
sudo rm -rf /usr/local/go
sudo tar -C /usr/local/ -zxf go1.17.5.linux-amd64.tar.gz
export PATH=$PATH:/usr/local/go/bin
go version

# runc 빌드 및 설치(최신 golang 버전 필요)
cd runc
make && sudo make install

# 일반유저(vagrant)도 docker 명령을 실행할 수 있게 vagrant 계정을 docker 그룹에 추가
# 한번 exit 하고 다시 로그인해야 한다.
sudo usermod -aG docker vagrant

# busybox 이미지의 파일시스템(파일들)을 tar 로 export
docker export $(docker create busybox) -o busybox_exported.tar

# root 계정으로 전환
sudo -Es

# container 에 압축 풀기
# runc 기동시 rootfs 이름의 디렉토리를 찾는다.
cd ~
mkdir -p ./ysoftman_container/rootfs
tar xvf busybox_exported.tar -C ./ysoftman_container/rootfs

# runc 에서 사용할 명세서 생성
# 기본 명세의 config.json 파일이 생성된다.
# spec -> create a new specification file
cd ysoftman_container
tree -L 2
runc spec
ls config.json

# runc 로 ysoftman1 이름의 컨테이너 기동
runc run ysoftman1

# ysoftman1 컨테이너에서 namespace 확인
ls -ahl /proc/$$/ns

# 호스트 터미널 하나더 열고 root 계정으로 전환
sudo -Es

# 호스트에서 runc 같와 runc 가 실행한 sh 프로세스가 확인
ps -ef | grep -E "runc| sh$"

# 호스트에서 
# runc 네임스페이스 확인
lsns -p $(ps -ef | grep -v grep | grep "runc run ysoftman1" | awk '{print $2}')

# ysoftman1컨테이너(runc)가 실행한 sh pid 로 namespace 상태 확인
lsns -p $(ps --ppid=$(ps -ef | grep -v grep | grep "runc run ysoftman1" | awk '{print $2}') -o pid | grep -v PID)

# 호스트에서 runc 로 기동된 컨테이너 리스트 보기
runc list

# 테스트 결과

# runc help
checkpoint  checkpoint a running container
create      create a container
delete      delete any resources held by the container often used with detached container
events      display container events such as OOM notifications, cpu, memory, and IO usage statistics
exec        execute new process inside the container
kill        kill sends the specified signal (default: SIGTERM) to the container's init process
list        lists containers started by runc with the given root
pause       pause suspends all processes inside the container
ps          ps displays the processes running inside a container
restore     restore a container from a previous checkpoint
resume      resumes all processes that have been previously paused
run         create and run a container
spec        create a new specification file
start       executes the user defined process in a created container
state       output the state of a container
update      update container resource constraints
features    show the enabled features

#####

# 위 golang, runc, busybox 준비되어 있는 상태에서
# rootless container 생성해 보기
# root 계정이라면 일반 계정으로 나오기
exit

# ysoftman_rootless_container 에 압축풀기
cd ~
mkdir -p ./ysoftman_rootless_container/rootfs
tar xvf busybox_exported.tar -C ./ysoftman_rootless_container/rootfs

# rootless 옵션으로 runc 에서 사용할 명세서 생성
cd ysoftman_rootless_container
runc spec --rootless

# /tmp/runc 를 루트로 하는 ysoftman_rootless_container 실행
runc --root /tmp/runc run ysoftman_rootless_container

# 컨터이너에서 user id 와 네임스페이스 확인
id
ls -l /proc/$$/ns

# 호스트에서
# 루트 경로 및 state.json 확인해보기
tree /tmp/runc
cat /tmp/runc/ysoftman_rootless_container/state.json | jq . | head -10

# 호스트에서 runc 같와 runc 가 실행한 sh 프로세스가 확인
ps -ef | grep -E "runc| sh$"

# runc 네임스페이스 확인
lsns -p $(ps -ef | grep -v grep | grep "runc run ysoftman_rootless_container" | awk '{print $2}')

# ysoftman1컨테이너(runc)가 실행한 sh pid 로 namespace 상태 확인
lsns -p $(ps --ppid=$(ps -ef | grep -v grep | grep "runc run ysoftman_rootless_container" | awk '{print $2}') -o pid | grep -v PID)

# user 네이스페이스 id 가 ysoftman_rootless_container 가 호스트와 다르다.
# ysoftman_rootless_container 의 root 로 보이지만 호스트의 root는 아니다.

# 테스트 결과

user namespace 로 kubernetes 와 container 보안 향상

user namespace 로 kubernetes 와 container 보안 향상


!!! Caution 잘못된 번역이나 이해로 틀릴 수 있음!!! ^^;;

[user namespace 란?]
user namespace 는 user IDs, group IDs 를 격리(isolate)한다. 리눅스에서 모든 프로세스는 특정 userid(/etc/passwd에 정의)와 groupid(/etc/group에 정의)로 소유된다. user namespace 는 container 안에서는 host의 userid,groupid 의 일부만 볼 수 있도록 한다.
예를 들어 container 안에서의 root(user id 0)는 사실 호스트의 userid=100000 이다.

[user namespace and capabilities]
container 에서 root 로 보이지만 실제는 user namespace 로 구분되는 사용자다. 이와 관련된 linux kernel 히스토리를 살펴보자.

- linux 2.2 이전에는 실제 root(user id 0)는 1개 였다.
- linux 2.2 이후부터 root 권한이 여러개의 기능 구분되는 capabilities(https://man7.org/linux/man-pages/man7/capabilities.7.html)로 나뉘어졌다. ex) CAP_NET_ADMIN : network 설정권한, CAP_SYS_ADMIN : mount 설정권한
- linux 3.8 (2013년) 에 user namespace 가 소개되고 capabilities 는 더이상 글로벌하게 사용하지 않고 user namespace 의 context 에 의해서 해석된다.

예로 sshfs(fuse 로 ssh filesystem 에 마운트) 프로그램을 실행할때 CAP_SYS_ADMIN capability 가 요구된다. 이건 host 와 container 사이를 격리시키는 목적에 맞지 않게 container 에거 너무 막강한 권한을 부여하게 된다.
(참고로 FUSE:Filesystem in Userspace 로 user 레벨에서 파일시스템을 만들 수 있도록 해준다.)

container 가 새로운 user namespace 없이 기동된 상태에서 sshfs 성공하기 위핸선 CAP_SYS_ADMIN 을 허용할 수 밖에 없어 보안 취약점을 내포하고 있다.(호슽와 같은 name space 를 쓰면 sshfs 프로세스가 CAP_SYS_ADMIN 권한을 가지고 있어 container 안에서 host 로 나쁜짓을 할 수 있음?)

하지만 container 가 새로운 user namespace 로 격리돼 실행된다면 호스트 user namespace 의 CAP_SYS_ADMIN 없이도 container 내에서의 CAP_SYS_ADMIN 를 허용할 수 있다.(host user namespace 가 아닌 container user namespace 의 CAP_SYS_ADMIN 로 container 의 mount namespace 의 filesystem 에 마운트하는것이라 호스트에는 영향이 없다.)
container 내에서의 root 는 호스트의 root 가 아니기 때문에 host 에 영향을 줘서는 안된다.

[user namespace and filesystems]
linux 는 user namespace 에서의 mounting 안전을 고려해 FS_USERNS_MOUNT flag 를 마크해 filesystems 리스트를 유지한다. 초기화되지 않은 user namespace 에 새로운 마운트를 생성하려면 FS_USERNS_MOUNT 타입이어야 한다. linux 저장소에서 다음과 검색해보면 리스트 찾아 볼 수 있다. 
git grep -nw FS_USERNS_MOUNT

초기화 되지 않은 uesr namespace 는 초기에는 profs 와 sysfs 타입의 파일 시스템으로 제한되었는데 지금은 아래와 같은 파일 시스템이 허용된다. 이런 파일 시스템은 권한이 없는 사용자도 안전하게 사용할 수 있기 때문에 허용됐다.

파일 시스템         user namespace 에서 허용됨
procfs,sysfs --> linux 3.8, 2012
tmpfs        --> linux 3.9, 2013
cgroupfs     --> linux 4.6, 2016
FUSE         --> linux 4.18, 2018
overlay      --> linux 5.11, 2020
NFS,ext4,btrfs,etc  --> 허용 안됨

[impact on container security]
user namespace 는 호스트와 container 를 격리하는 보안 레이어다.
호스트의 runc(OCI(Open Container Initiative)를 따른 컨테이너 런타임)가 container 에 의해 overwritten 될 수 있는 취약점이 있었다. 이 취약점은  runc 바이너리는 root 소유이고 container 에 매핑되지 않았는데도, 컨테이너의 프로세스가 /proc/self/exe 를 통해 호스트의 runc 를 참조를 허용한다. 
flatcar container linux 는 호스트의 runc 바이너리를 읽기전용 파일시스템을 두어 완화했다.

[enabling user namespace for FUSE filesystem]
linux 커널은 파일이 변경되었다면 읽거나 쓰기전에 IMA(integrity Measurement Architecture) 서브시스템을 사용해 감지하고 감사(audit)한다.
FUSE 와 같은 파일시스템은 커널이 remeasure, reppraise, reaudit 없이 파일을 서빙할 수 있다. 
memfs FUSE driver 의 패치를 테스트 하기 위해 다음과 같은 시나오리가 있다고 하자.
1. 첫번째 요청에 FUSE 드라이버는 파일의 초기 내용으로 서빙된다.
2. IMA 파일에 대한 측정(measurement)을 제공한다.
3. 두번째 요청에 FUSE 드라이버는 같은 파일의 변경된 내용으로 서빙된다.
4. IMA 내용을 다시 측정하지 않아 내용 변경이 측정되지 않는다.

IMA 에 'force' 옵션을 주어 항상 remeasure, reppraise, reaudit 하도록 하는 패치를 생각해보자.
커널이 아무 변경도 감지하지 못했는데 모든 요청마다 강제로 측정이 수행된다. FUSE 에서 다르게 동작하는 부분이 다른 파일스템들에서 모두 알수는 없어, 이로 인해 'force' 옵션이 잘못된 레이어에 추가될 수 있다. 이 문제를 해결하기 위해 IMA 캐싱 관련 옵션 수행하는 FS_NO_IMA_CACHE(v1,v2,v3,v4) flag 를 제공한다. IMA 는 FS_NO_IMA_CACHE flat 를 체크한 후 'force' 이 있으면 사용한다. 이런 방법으로 IMA 은 다른 파일 시스템들 모두를 알 필요가 없다.
하지만 IMA 'force' 옵션은 모든 문제를 해결하지는 못하고 아직 중요사항들은 검증되지 않았다. IMA 관리자들에 의해 linux 4.17 에는 다음과 같은 flag가 구현되어졌다.
- SB_I_IMA_UNVERIFIABLE_SIGNATURE flag : 중요 검증되지 않았음을 표시하는
- SB_I_UNTRUSTED_MOUNTER flag : 신뢰하지 않는 user namespace 에 마운트 되었을 때

[bring user namespace to kubernetes]
kubernetes(k8s)에서는 user namespace 기능을 사용할 수 없다. 2016년 관련 개선을 논의하기 시작했다.
첫번째 시도로 k8s 는 default 가 아닌 Container Runtime Interface(CRI)를 사용해 kubelet 는 CRI 의 gRPC interface 로 컨테이너와 통신하고 주요 목적으로 user namespace 를 제공한다.

kubelet 이 container runtime 와 통신하는 방법

- kubelet docker api 로 통신 (deprecated)
Kubelet --> docker --> containers

- kubelet CRI의 gRPC 로 통신, CRI shim(CRI protocol 처리)
Kubelet --(CRI)--> CRI shim --> container runtime --> containers

- CRI protocol처리하는 containerd/cri 로 컴파일된 containerd 를 사용
Kubelet --(CRI)--> containerd/cri|containerd --(OCI spec)--> runc --> containers

[CRI changes for user namespaces]
user namespace 지원을 위한 CRI 변경 아이디어

1. kubelet 이 sandbox(infrastructure container) pod 시작을 위해 RunPodSandBox()로 container runtime 에 요청하면 pod 안에 container 들의 namespace 를 공유
2. kubelet 이 sandbox 에 CreateContainer(), pod 의 각 container들은 sandbox 를 참조하고 있음.

이렇게 해서 pod 의 컨테이너들은 use namespace 를 공유한다.

IPC 와 network namespace 도 pod 레벨에서 공유된다. IPC, network 는 pod 의 user namespacce 의 의해 소유되고 이 네임스페이스에서의 capabilities 가 허용된다.
pod 의 각 container 의 mount namespace 도 user namespace 의 의해 소유된다. 
(pod 에 network, ipc, user namespace 를 두면 호스트와는 격리된체 pod 내 container 들은 자유롭게 통신할 수 있다.)

[kubernetes volumes]
container 들이 같은 volume에 접근할때 다음 시나리오를 고려해 보자.

1. container1 에서 NFS 에 파일을 쓴다. 이 파일은 uid=10000(container1 mapping 되있음)
2. container2 에서 NFS 의 파일을 읽는다. container2에 uid=10000이 mapping 되어 있지 않아 uid=65534(nobody)로 보여진다. 

이 문제를 해결하기 위해 다음과 같은 방법을 사용한다.

1. 모든 pod 에 매핑되어 있는 uid 를 사용한다. 컨테이너들간의 격리는 줄어들지만 user namespace 사용하지 않는것 보단 안전하다.
2. 각 pod 에 매핑되어 다른 uid 를 사용하고 실제 파일을 사용할때 마다 uid를 convert 할 수 있는 메커니즘(mechanism)기능을 추가한다. linux 커널에는 shiftfs, fsid mapping, new mout api 와 같은 메커니즘을 제공한다. 하지만 아직까지는 linux upstream 제품에는 없다.

오늘날 volume 사용하지 않는 작업이 많이 있고 이는 user namespace 에 있어서 이익이다. 완벽한 해결책을 찾으려고 k8s 에 구현중인 user namespace 를 막지는 말자.

[conclusion]
user namespace 는 linux 에서 container 들을 더 안전하게 사용할 수 있는 유용한 레이어를 제공한다. 이는 지난 몇몇 취약점들을 완화하는 것으로 입증됐다. volume 제공시 단점으로 어려움을 겪고 있지만 linux kernel 에서 개발중이니 앞으로 많이 개선될것으로 기대한다.
언젠가 kubernetes 가 user namespace 를 지원을 완료하면 kubernetes 는 호스트와 컨테이너들 간에 더욱 안전하게 격리 효과를 볼 것이다. 이는 좀더 많은 권한으로 운영되는 container 들에서의 새로운 상황에도 적용된다. user namespace 없이 뭔가를 한다는것 아주 위험하다.

docker user namespace remap

# 기본적으로 docker 실행시 root(uid=0, root 이름이 아니더라도 커널은 uid 로 구분하기 때문에 uid=0이면 root다) 로 실행되는데,
# 다음과 같이 docker 실행시 호스트의 root 권한의 디렉토리를 바인딩(마운트)하면
# docker container 안에서 호스트의 root 파일을 변경할 수 있어 위험하다.
docker run -v /etc:/host_etc -it ubuntu

# container 에서 사용자 id 를 보면 root 다.
id
uid=0(root) gid=0(root) groups=0(root)

# container 에서 host 파일을 수정할 수 있다.
echo "hacker ysoftman ..!" >> /host_etc/ysoftman.txt

# user namespace 를 확인해보면
# host 의 user namespace 와
ls -ahl /proc/$$/ns/user
lrwxrwxrwx 1 root root 0 Dec 15 03:33 /proc/897/ns/user -> 'user:[4026531837]'

# docker container 의 user namespace 가 같다.(공유)
ls -ahl /proc/$$/ns/user
lrwxrwxrwx 1 root root 0 Dec 15 03:36 /proc/1/ns/user -> 'user:[4026531837]'

# 이를 방지하기해 docker 실행시 생성되는 사용자 gid,uid 범위를 제한할 수 있다.
# host 에서 dockremap 사용자를 추가
sudo useradd dockremap

# host 에서 dockremap 의 uid,gid 20000~30000 범위 설정
sudo vi /etc/subuid
dockremap:200000:300000

sudo vi /etc/subgid
dockremap:200000:300000

# docker.service 파일을 생성하고 다음과 같이 설정하자
sudo vi /etc/systemd/system/docker.service
[Service]
ExecStart=/usr/bin/dockerd --userns-remap=default

# docker daemon 재시작
# 참고로 stop docker 시 다음 에러 발생시 docker.socket 도 중지한다.
# sudo systemctl stop docker
# Warning: Stopping docker.service, but it can still be activated by:
#  docker.socket
sudo systemctl daemon-reload
sudo systemctl stop docker.socket
sudo systemctl stop docker
sudo systemctl start docker
sudo systemctl status docker

# 다시 docker container 를 실행
docker run -v /etc:/host_etc -it ubuntu

# 이제는 container 에서 host 파일을 수정할 수 없다.
echo "hacker ysoftmancd ..!" >> /host_etc/ysoftman.txt
bash: /host_etc/ysoftman.txt: Permission denied

# host 에서 ps 를 해보면 docker container bash 가 20000번대의 uid로 실행되는것을 알 수 있다.
ps -ef | grep bash
200000    1408  1379  0 03:26 pts/0    00:00:00 bash

# docker container 에서 id 를 보면 여전히 root 로 보이지만
# host 와 user namespace 를 다르다. 
id
uid=0(root) gid=0(root) groups=0(root)

# user namespace 를 확인해보면
# host 의 user 네임스페이스와 
ls -ahl /proc/$$/ns/user
lrwxrwxrwx 1 root root 0 Dec 15 03:33 /proc/897/ns/user -> 'user:[4026531837]'

# docker container 의 user 네임스페이스들 다르게 된걸 알 수 있다.
ls -ahl /proc/$$/ns/user
lrwxrwxrwx 1 root root 0 Dec 15 03:32 /proc/1/ns/user -> 'user:[4026532195]'

container overlay filesystem

# union filesystem mount 는 n개의 파일시스템을 하나로 마운트하는 것으로
# overlayFS, overlayFS2, AUFS 등이 있다.

# overlayFS 는 다음과 같은 layer 구조다.
# 아래 layer 파일은 상위 layer 에 투영되어 사용자는 최종 merged view 에서 모든 이전 layer 들의 파일이 있다.
# 같은 파일이라면 상위 layer 쪽이 선택된다.
# overlayFS2 는 다음과 같은 layer 로 구성된다.
[merged dir] - container mount 로 보여지는 내용으로, 최종 통합된 레이어
c.txt(from lower dir2) b.txt(from upper dir) a.txt(from lower dir1)

[upper dir] - container 에 변경 사항을 기록 하는 레이어
b.txt

[lower dir2] - 기존 이미지 readonly
c.txt b.txt

[lower dir1] - 기존 base 이미지 readonly
a.txt

# 위 4개의 layer 외에 추가로 work dir 이 있다.
[work dir] - atomic 보장을 위해, merged 에 반영되기전에 사용하는 임시 공간

# overlay filesystem 마운트 테스트
# 마운트 테스트에 사용할 디렉토리 생성
mkdir ysoftman_overlay1
cd ysoftman_overlay1
mkdir image1 image2 upper work merge
touch image1/a.txt image2/{b.txt,c.txt}

# -o 옵션으로 각 layer 에 해당하는 디렉토리 설정
# workdir 는 merged 에 반영전 준비를 위한 공간
# 최종 merged view layer 는 merge 디렉토리에 마운트 된다.
mount -t overlay overlay \
-o lowerdir=image2:image1,upperdir=upper,workdir=work \
merge

# 마운트 후 work 디렉토리를 보면 변경된 내용을 파악할 수 있다.
tree -I work
.
├── image1
│   └── a.txt
├── image2
│   ├── b.txt
│   └── c.txt
├── merge
│   ├── a.txt
│   ├── b.txt
│   └── c.txt
├── upper
└── work
    └── work

# 파일을 삭제하면 upper에 whiteout(파일 변경되었음을 표시)된다.
rm -rf merge/a.txt
ls -ahl upper/a.txt
c--------- 1 root root 0, 0 Dec  1 05:00 upper/a.txt
tree -I work
.
├── image1
│   └── a.txt
├── image2
│   ├── b.txt
│   └── c.txt
├── merge
│   ├── b.txt
│   └── c.txt
└── upper
    └── a.txt # whiteout


#####


# docker image layer 정보 확인
# 최신 nginx 도커 이미지 다운로드
docker pull nginx:latest

# layer id 확인
docker image inspect nginx:latest | jq '.[].RootFS'

# docker storage driver, root dir 확인해
docker info | grep -i -E "storage driver|root dir"

# (layer 정보를 가지고 있는)layerdb id 확인
cd /var/lib/docker/image/overlay2/layerdb
tree ./sha256/ -L 1

# layer id 확인
ls sha256/*/diff | awk '{system("cat "$0"; echo")}'

# 이 layer 의 부모 layer 를 가진 layerdb id 가 기록되어 있다.
ls sha256/*/parent | awk '{system("cat "$0"; echo")}'

# layer 가 저장된 경로 id 확인
# /var/lib/docker/overlay2/xxx 디렉토리 이름을 나타낸다.
ls sha256/*/cache-id | awk '{system("cat "$0"; echo")}'


#####


# nginx image layer 로 container 띄우기
# nginx GraphDriver(layer) 확인
docker image inspect nginx | jq '.[].GraphDriver'

# LowerDir 값중 마지막 부분은 최상위 부모 이미지로 내용을 확인해 볼 수 있다.
tree -L 1 /var/lib/docker/overlay2/1431186159df61d78822ed287754ce0e079739695e8966adb816f509687a1d92/diff

# nginx overlayFS 가 마운트될 디렉토리 생성
mkdir nginx_overlay
cd nginx_overlay
mkdir upper work merge

# nginx overlay 마운트
# lower 는 기존 이미지 사용
# upper,worker 는 생성한 디렉토리로 연결
# 모든 layer 가 오버레이된 디렉토리 merge 연결
mount -t overlay overlay \
-o "lowerdir=/var/lib/docker/overlay2/cfe31bc0b1bcc056093f9d1b73a090c81f4ce284eccf37e50344258a434fa5b1/diff:/var/lib/docker/overlay2/a9bcb65c3bfbad6d4fb692677b94fcda85b8856e4836490cc987296a5f902f4a/diff:/var/lib/docker/overlay2/3d6e60f9c348c81d24ecafbfb23a6e531541778efc36d41dd064c1e8b20dd4ad/diff:/var/lib/docker/overlay2/6d60e98403eadb29449726645f338d8a47dd8cd6fcfa22b6565d5c46d83519bc/diff:/var/lib/docker/overlay2/1431186159df61d78822ed287754ce0e079739695e8966adb816f509687a1d92/diff,\
upperdir=upper,\
workdir=work" \
merge

# nginx 마지막 layer 에서 nginx 실행 커맨드 파악
docker history nginx:latest --no-trunc | sed -n '1,2p' | tr -d ' '

# -m 새로운 마운트 네임스페이스 시작
unshare -m

# 새로운 마운트 네임스페이스에서 chroot 커맨드로 ./merge 를 root 디렉토리로 설정
chroot ./merge
# 또는 pivot_root 로 root 를 ./merge 로 변경
cd merge
mkdir put_old
pivot_root . put_old

# nginx 백그라운드로 실행하고 access 로그를 확인
nginx -g "daemon off;" & tail -F /var/log/nginx/access.log

# 새 터미널을 열고 nginx 동작 확인
curl localhost

# 위 과정 실행 결과

# unshare 종료하고, nginx 프로세스 마운트 해제
exit
killall nginx
umount /tmp/nginx_overlay/merge

container 에 없는 iptables 커맨드 실행하기

# container 에 없는 iptables 커맨드 실행하기

# 방법1
# container 시작시 호스트 볼륨 연결하기
# 이미 컨테이너가 존재하면 삭제(--rm)
# 호스트의 root path 를 컨테이너의 /host 로 연결하고, (--volume /:/new_root)
# 컨테이너에게 확장권한을 부여한다.(--privileged)
# 컨테이너 실행시 /new_root 로 root 를 변경한다.
docker run -it \
--name=ysoftman
--hostname=ysoftman \
--rm \
--volume /:/new_root \
--privileged  \
busybox \
chroot /new_root

# 이제 컨테이너 내에서 호스트의 커맨드가 실행된다.
iptables -t filter -S


# 방법2
# 컨테이너 내에서 --pid=host 로 호스트 PID namespace 를 사용한다.
# 호스트의 pid 1(system) 대상으로(-t) -m(--mount[=file], 파일을 명시하지 않으면 target process 의 mount namespace 조인)
docker run -it \
--name=ysoftman \
--hostname=ysoftman \
--rm \
--privileged \
--pid=host \
busybox \
nsenter -t 1 -m sh

# 이제 컨테이너 내에서 호스트의 mount 정보로 호스트의 커맨드가 실행된다.
iptables -t filter -S


# 방법3
# 그냥 컨테이너 실행
docker run -it --name=ysoftman --hostname=ysoftman busybox

# 터미널을 하나 더 열고 호스트에서
# 위 컨테이너의 pid 의 network namespace 조인해 iptables 명령 실행한다.
# -t(--target) pid -n(--net[=file], 파일을 명시하지 않으면 target process의 네트워크 네임스페이스로 조인)
nsenter -t $(docker inspect --format {{.State.Pid}} ysoftman) -n iptables -t filter -S

pid 네임스페이스에 관한 이슈와 해결책

[들어 가기전]
고아(orphan) process : 부모 프로세스가 자식 프로세스보다 먼저 종료되면, 자식 프로세스는 고아 프로세스가 된다. 고아 프로세스는 init(pid 1)에서 거둬 들여, 고아의 부모 프로세스는 init(pid 1)이 된다. 고아 프로세스가 종료되면 init 이 종료 상태를 파악해 좀비 프로세스가 되는것을 막는다.

좀비(zombie/defunct) process : 자식 프로세스는 종료되었지만 부모 프로세스가 wait()등으로 자식 프로세스의 종료 상태를 파악하지 못해 자식 프로세스를 거둬 드릴 수 없는 경우로 process table 에만 남아 있어 kill -9 좀비프로세스PID 로도 삭제되지 않는다.
좀비 프로세스를 삭제 방법
- kill -CHLD PPID (부모 프로세스가 CHLD 시그널을 처리할 수 있는 경우에만 가능)
- kill -KILL PPID (좀비 프로세스의 부모프로세스를 제거한다. 모든 child 가 죽기 때문에 신중해야함)

pid 네임스페이스에 관한 이슈와 해결책을 보고 정리해 봤다

!!! Caution 잘못된 번역이나 이해로 틀릴 수 있음!!! ^^;;


[linux 의 pid(process identifier)]

user space 의 root 프로세스는 init (pid 1, 이것의 부모는 pid 0 으로 커널)이다.
1 번 프로세스가 죽으면 커널이 패닉을 발생해 시스템을 재부팅해야 한다.


[linux namespace 생성]

리눅스 네임스페이스들(uts(host, domain 구분), network, mount, ipc, cgroup(cpu,memory,disk,network 등 리소스 구분), pid)는 unshare 시스템콜로 생성한다. unshare 하면 바로 새로운 네임스페이스로 들어간다.
하지만 pid 네임스페이스의 경우 unshare 로 바로 pid 네임스페이스로 들어가지 않고, fork 로 pid 네임스페이스에 pid 1(init)번를� 만든다. 이 1번 프로세스는 pid 네임스페이스 밖(호스트)에서는 다른 pid 값이다.


[네임스페이스에서의 pid 1]

1. pid 네임스페이스의 init 프로세스는 기본 signal 핸들러가 없어, HUP, INT, QUIT, TERM, KILL .. 등의 시그널 처리를 하지 못한다.(별다른 옵션 없이 docker 컨테이너를 수행하면 ctrl-c 가 동작하지 않는다. 그래서 docker kill 등을 사용한다.)

2. pid 네임스페이스의 init 외 다른 프로세스가 자식 프로세스보다 먼저 죽으면 그 자식 프로세스의 부모는 init (pid 1)이 되고, init 은 프로세스들이 exit 상태를 수집해 커널이 process table 에서 정리할 수 있도록 한다.

3. pid 네임스페이스의 init(pid 1)이 죽으면, 같은 네임스페이스의 모든 프로세스가 강제로 제거되며, pid 네임스페이스는 clean up 된다.


[docker 의 실수]

docker(runc:lightweight docker runtime)는 컨테이너의 entrypoint 를 pid 1 로 하는 새로운 pid 네임스페이스를 만든다. pid 1번에서 signal 처리가 되어 있지 않으면 ctrl-c 와 같은 signal 이 동작하지 않는다. 손자 프로세스가 종료(exit)되기 전에 죽은 자식 프로세스를 fork 하게 되면 zombie(좀비) 프로세스가 컨테이너에 쌓이게 되고, 결국 process table 을 꽉 채우게되는 상황이 발생할 수 있다.
이런 문제를 피하기 위해 fork-exec 를 할 수 있는 특별할 application process 를 실행할 수 있다. 이는 컨테이너가 복잡해지지만 실제 호스트(컨테이너가 아닌)의 init 처럼 만들 수 있다. 사람들은 종속성 격리의 이점을 희생하더라도 여러개의 apt 를 실행(dockerfile 에서 apt install a b c 컨테이너 내에서 패키지를 설치)을 포함시키는 경향이 있다.


[Rkt 해결책]

Rkt(rkt is an application container engine) 로 위 문제를 다소 해결할 수 있다. Rkt 는 사용자가 만든 시작 프로세스(container process)가 init 이 아니라고 가정하고 init (systemd) 를 생성하고 systemd 로 컨테이너에서 동작하는 filesystem 네임스페이스를 만들고 구동시킨다. systemd 는 pid 1번이 되고, (사용자가 만든) container process 는 pid 2번으로 동작한다. 만약 container process 가 init 이라면 이것도 pid 2 로 동작해 또 다른 이슈가 될 수 있다.


[간단한 대체 방법]

fork 후 pid 네임스페이스에서 container process 를 바로 실행하는 대신, spawner 가 다시 fork 한다. 이 두번째 fork 는 container spawner 가 pid 1이 되도록 하고, 모든 child 프로세스들에 signal을 전달할 수 있는 signal handler 를 구성한다. 이렇게 하면 child 프로세스가 죽을때 까지 좀비 프로세스를 거둬들일 수 있고, 이 시점에 container process 의 종료 상태를 수집해 컨테이너화 시스템에 전달한다. 이것은 ctrl-c 와 같은 signal 을 처리할 수 있고 zombie 프로세스도 적절히 거둬들 수 있다는 의미다.
docker 1.13 부터 가능하며 --init 플래그를 사용하면 된다.


[하나의 pod 내에서의 다중 컨테이너]

pod 는 네임스페이스를 공유 하는 컨테이너들의 집합이다. rkt 에서는 파일시스템 네임스페이스를 제외한 모든 네임스페이스들이 공유된다.
앞서 pid 네임스페이스 이슈로 kubernetes(k8s) 는 pod 내 컨테이너들간에 pid 네임스페이스를 공유 하지 않는다. 때문에 같은 pod 내의 container 들 끼리 signal 주고 받을 수 없고, 앞서 언급한 init 문제를 가지고 있어 container processor 는 pid 1로 실행된다.
rkt 방법을 사용하면 컨테이너 안에 init 프로세스를 만들지 않아도 통신이나 signal 을 주고받을 수 있는 컨테이너들을 쉽게 생성할 수 있다. 하지만 이미 존재하는 pod 에 컨테이너를 추가하는 상황에서는 바로 되지는 않는다.


[pod 에 컨테이너 추가하기]

k8s 는 pod sandbox 라는 개념이 있다. k8s 컨테이너 런타임은 컨테이너 시작에 앞서 리소스를 할당하는데, 이는 특히 네트워킹에 유용하고 존재하는 pod 에 컨테이너를 추가하는데도 적용할 수 있다. sandbox pod 를 먼저 생성 후 컨테이너를 하나씩 추가하는 경우, 왜 나중에 컨테이너 추가하는게 허용되지 않을까? 이는 데이터베이스 백업이나 로그 수집과 같은 주기적인 작업에 특히 유용할 것이다.
rkt(에서는 container 를 app 으로 부른다.)는 실험적으로 컨테이너와 독립적으로 pod 생성을 허용하여, 추후에라도 pod 에서 컨테이너를 추가,삭제할 수 있다. 별도 실행되는 유닛(프로세스?)없이 systemd 로 가능하게 한다. 새로운 app(container) 시작 요청이 있으면 pod 의 systemd 와 통신한다. 이러한 모델로 init 프로세스는 다음과 같은 추가적인 특권(privilege)을 가진다.
- 호스트 파일시스템 네임스페이스에 접근할 수 있어서 컨테이너 시작을 위한 파일시스템 네임스페이스를 생성할 수 있다.
- 각 app 시작에 필요한 privilege 들을 알 수 없어 전체 privilege 을 유지해야 한다.
- 컨테이너상의 모든 실행중인 프로세스를 볼 수 있다.
non-sandbox 모델이라면 init 프로세스는 child 프로세스를 시작한 다음 compromise 영향을 최소화 하기 위해 privilege 를 삭제한다.


[sandboxes 와 pid namespaces]

init, sandboxes, pid namespace 를 이용한 방법들 각각 약간의 결점(단점)이 있고, 다음 옵션을 사용할 수 있다.
1. pid namespace 가 sandbox 와 같이 생성되지 않을때, 각 컨테이너는 자신의만의 pid 네임스페이스를 가진다. k8s 에서 사용하는 방식이며, signal 처리를 위한 간단한 init 전략이다. 중요 단점으로는 pod 내 프로세스들간 signal 을 통신을 할 수 없다.
2. pid namespace 가 sandbox 와 같이 생성되지 않을때, sandbox 에서 처음으로 시작되는 컨테이너에 pid namespace 가 생성된다. 이 상태에서는 프로세스간 signal을 주고 받을 수 있다. 단점은 첫번째 프로세스가 pod 의 master 가 되고 이 master 프로세스가 죽으면 컨테이너 내 모든 프로세스는 커널에 의해 종료된다. 때문에 master 프로세스는 pod lifecycle 동안 살아 있어야 한다.
3. pid namespace 가 sandbox와 함께 생성될때, 다른 프로세스를 실행할 수 있는 smart init 프로세스를 포함한다. 이것은 rkt app sandbox 가 동작하는 방식이다. 단점으로는 init 프로세스가 너무 많은 privilege 를 가져 그 만큼 보안 공격에 취약할 수 있다.
4. pid namespace 가 sandbox와 함께 생성될때, sandbox 는 signal을 처리하고 zombie 프로세스를 회수하는 간단한 init 프로세스를 포함한다. 다른 프로세스들은 pid namespace 로 들어갈 수 있지만 부모 프로세스들은 네임스페이스 밖에 있다. init 프로세스는 새로운 컨테이너를 시작하지 않기 때문에, privilege 나 호스트 파일시스템 액세스를 유지할 필요가 없다. 단점은 네임스페이스 내의 각 프로세스들은 pid 0인 부모를 가져 일반적인 process tree 가 깨진다.
5. pid namespace 와 init 이 정확히 위 4번 옵션과 같을때, 다른 프로세스들은 pid namespace 에 들어가고 daemonize 된다. 커널은 위 4번에 pid 0으로 깨진 process tree 에서의 부모를 init 으로 돌릴 것이다. 단점은 daemonize 후 밖에 있는 새로운 프로세스를 모니터링 하기 어렵다. containerizationo system 은 간단하게 프로세스를 waiting 하는 대신 pid 를 통해 프로세스를 추적하도록 강요된다.

(필자는) 위 옵션 중 4번, 5번을 선호한다. 사실 기대하는 프로세스의 lifetime 에 따라 하나를 선택하면 된다. 위 옵션들은 프로세스를 오래 실행하기 위한 좋은 방법들이고 특히 프로세스를 spawner 로 daemonize 프로세스를 종료하는 docker 환경에 좋은 솔루션이다. 만약 간단하게 끝나는 프로세스라면 4번 옵션을 사용하고 프로세스를 pid process tree 와 분리해 유지하면 아주 쉽다. 
어떤 작업들은 k8s 가 init 처럼 동작하는 pause container를 생성하는것과 비슷하다. k8s 에 pid namespace 공유가 지원되면 5번 옵션도 가능할것이다.


[결론]

pid namespace 에는 꽤 숨겨진 복잡도가 존재한다. 오늘날 containerization 시스템은 중요한 단점과 이걸 피할 수 있는 방법들에 따라 선택한다. docker 의 싱글 컨테이너의 단점은 잘 이해되고 적당한 해결방법이 있는 반면, 컨테이너 spawner 가 init 처럼 동작하게 허용되면 컨테이너 빌더 작업이 단순화된다.
그룹 컨테이너의 경우 init 분리 방식이 rkt 가 docker 보다 우수하다. rkt 방식은 현재 k8s pod 모델에서 가능하지 않는 signal 을 통한 프로세스간 통신을 허용한다. 하지만 지연된 시작 컨테이너가 포함되기 때문에 rkt 방식도 몇가지 단점을 보인다.
지연된 시작 컨테이너에 가장 강력한 해결 방법은 pid namespace 와 함께 간단한 init 프로세스를 시작하는것이다. 하지만 새로운 컨테이너를 생성하려면 container spawner 를 통해야 한다. 이는 init 프로세스가 privileges 삭제를 허용하여, 공격을 차단한다. spawner 는 새 프로세스를 daemonize 하고 process tree 를 일관되게 유지하거나 새 프로세스의 부모로 남아 프로세스 관리를 단순화할 수 있다.


brew unkown command cask

맥 GUI 앱을 설치 할때 사용하는 cask 옵션이 언제부터인가 동작하지 않는다.
brew cask install docker
Error: Unknown command: cask

brew 2.6.0 부터 cask 가 deprecated 되었고, 이제는 --cask 로만 사용할 수 있다.

다음과 같이 사용하자.
brew install --cask docker

docker pull forbidden 해결하기

# private registry 를 사용하는데 docker pull 하면, 다음과 같은 에러가 발생한다.
docker pull ysoftman.test.com/drone/drone:2
Error response from daemon: Get https://ysoftman.test.com/: Forbidden

# docker login 을 해도 forbidden 에러가 발생한다.
sudo docker login -u "ysoftman" -p "password" ysoftman.test.com

# docker daemon 정보를 보면 프록시가 설정되어 있고,
# ysoftman.test.com 이 프록시를 사용해 접근이 안되고 있었다.
docker system info | grep -i proxy
HTTP Proxy: http://ysoftman.proxy.test
HTTPS Proxy: http://ysoftman.proxy.test
No Proxy: localhost,127.0.0.1,aaa

# 프록시를 사용하지 않도록 http-proxy 파일을 수정한다.
sudo vi /etc/systemd/system/docker.service.d/http-proxy.conf
No Proxy: localhost,127.0.0.1,aaa,ysoftman.test.com

# 변경된 설정 파일을 reload 하기 위해서 daemon-reload 을 실행해야 한다.
sudo systemctl daemon-reload

# docker daemon(service) 재시작
sudo systemctl restart docker

# 이제 docker daemon 정보를 보면 no proxy 에 추가된 것을 확인할 수 있고
# docker pull ysoftman.test.com/drone/drone:2 도 잘된다.

k8s sandbox OCI runtime create failed 에러

# k8s cronjob 수행으로 jobs -> pod 생성시 가끔 pod warning 이 발생한다.
# pod 내 container command(작업)는 정상적으로 수행되지만 아래와 같은 에러가 간혹 발생한다.
# 그런데 같은 클러스터에서 수행되는 다른 cronjob 은 에러가 발생하지 않는다.
# 참고로 kubernetes 버전은 1.15.5 이다.
# describe 로 pod Event 를 보면
kubectl describe pod ysoftman-pod-xxx(가칭)

... 생략 ...

Events:
  Type    Reason     Age    From               Message
  ----    ------     ----   ----               -------
  Normal  Scheduled  3m54s  default-scheduler  Successfully assigned ysoftman-xxx/ysoftman-pod-xxx to ysoftman-worker-1
  Normal  Pulling    3m51s  kubelet,ysoftman-worker-1            Pulling image "ysoftman-test:test"
  Normal  Pulled     3m51s  kubelet,ysoftman-worker-1            Successfully pulled image "ysoftman-test:test"
  Normal  Created    3m51s  kubelet,ysoftman-worker-1            Created container ysoftman-pod
  Normal  Started    3m51s  kubelet,ysoftman-worker-1            Started container ysoftman-pod
  Normal  SandboxChanged 3m50s  kubelet,ysoftman-worker-1          Pod sandbox changed, it will be killed and re-created.
  Warning  FiledCreatePodSandBox 3m50s  kubelet,ysoftman-worker-1  Failed created pod sandbox... rpc error: code = Unknown desc = failed to start sandbox container for pod "ysoftman-pod-xxx": Error response from daemon: 
  OCI runtime create failed: container_linux.go:367: starting container process caused process_linux.go:495: container init caused: Running hook #0:: error running hook: exit status 1,stdout: , stderr: time="xxx" level=fatal msg="no such file or directory": unknown
  # 또는
  OCI runtime create failed: container_linux.go:349: starting container process caused "process_linux.go:319: getting the final child's pid from pipe caused \"EOF\"": unknown

# 위와 같은 이슈가 moby(docker 에서 만든 컨테이너 open source)에
# https://github.com/moby/moby/issues/40835 로 등록되어 있고 아직 리졸브되지 않았다.

# sandbox는 gke(goole kubernetes engin)가 pod내에서 실행되는 컨테이너가 보안의 위협이 되지 않도록 k8s 가 생성해 관리하는 컨테이너라고 한다.
# 에러 메시지(getting the final child's pid from pipe caused \"EOF\"": unknown. )의 뜻을 보면
# OCI(Open Container Initiative)를 준수하는 runC(OCI 를 따르는 컨테이너 런타임)이 pod->container 생성시
# 어떤 이유로 pid 를 할당받지 못하는건데, moby 이슈에 제기된 원인을 보면 크게 다음과 같다.

# 1. cpu, memory 리소스 부족 -> 노드별로 cpu, mem 상태는 넉넉한것으로 보인다.
# 2. 커널 파라메터 max pid, namespace 수 늘리기 -> 요것도 아직 그리 부족해 보이진 않고, 커널 파라메터까지 수정하는 경우는 거의 없어야 할것 같다.

# 테스트1
# 1번을 보면 현재 ysoftman-pod 의 cpu, mem 사용량은 크지 않은것 같고,
# 혹시 잦은 pod 생성 에 따른 문제는 아니진 해서 테스트로 1분 간격으로 실행하는 크론잡을 돌려 봤다.
# 하루동안 돌려봤지만, interval 1분이라 빨리 재현될줄 알았는데 pod 내 sandbox 생성 에러가 없다.

# 테스트2
# pod -> container 도 잘 수행되지만, sandbox 생성시 사용자의 컨테이너를 찾을 수 없어 발생할 수 있을것 같다.
# ysoftman-pod 의 컨테이너 커맨드가 빨리 끝나서 sandbox 컨테이너 생성시 참고하는 ysoftman-pod 컨테이너가 없어
# no such file or directory 또는 다른 에러(pid EOF 로 찾을 수 없다등)이 발생하는 것 같아
# 다음과 같이 1초내로 빨리 종료되는 크론잡 컨테이너를 설정했다.

... 생략 ...

containers:
- name: ysoftman-cronjob-test
  image: busybox
  imagePullPolicy: Always
  command:
  - /bin/sh
  - -c
  - date; echo "wait for 0s"; i=1; while [ $i -le 1 ]; do echo $i; sleep 1; i=$((i+1)); done; echo "job completed\!"
  resources:

# 위와 같이 설장하고 1분마다 크론잡이 실행되면, 100%는 아니지만 에러가 재현됐다.
# 실제 ysoftman-pod 는 사용자 업데이트를 가져오는 잡으로 대부분 업데이트 내용이 없어서 1~2초내로 바로 종료되는 컨테이너였다.

# 해결방법
# ysoftman-pod container 에 sleep 10초 주어 sandbox 생성시 사용자 컨테이너를 참조할 수 있는 시간을 주니 더이상 발생하지 않았다.