반응형

스프링 부트 기반으로 개발한 어플리케이션을 컨테이너 이미지로 만들 때 이미지 사이즈를 줄이는 방법에 대해서 알아보자.

먼저, sample source 를 다운 받는다.

$ git clone https://github.com/seungkyua/springboot-docker.git
$ cd springboot-docker

다운 받은 소스에서 mvn 으로 package 를 빌드한다.

$ mvn package

빌드 후에 소스의 디렉토리 구조는 다음과 같다.

$ tree . -L 2
.
├── LICENSE
├── README.md
├── docker
│   ├── Dockerfile
│   ├── Dockerfile2
│   └── Dockerfile3
├── logs
│   └── access_log.log
├── pom.xml
├── scripts
│   └── run.sh
├── src
│   └── main
├── target
│   ├── classes
│   ├── example-0.0.1-SNAPSHOT.jar
│   ├── example-0.0.1-SNAPSHOT.jar.original
│   ├── generated-sources
│   ├── maven-archiver
│   └── maven-status
└── work
    └── Tomcat

오늘은 자바 소스는 상관없이 도커 이미지 빌드에 대해서만 설명하므로 Dockerfile 만 살펴보자.

컨테이너 이미지 만들기 - 기본편

docker/Dockerfile2 을 보면 아래와 같다.

FROM openjdk:17.0.2-jdk-slim
MAINTAINER Ahn Seungkyu

ARG JAR_FILE=example-0.0.1-SNAPSHOT.jar

RUN mkdir -p /app
WORKDIR /app

COPY target/${JAR_FILE} /app/app.jar
COPY scripts/run.sh .
ENTRYPOINT ["./run.sh"]

base 이미지로 openjdk:17.0.2-jdk-slim 이미지를 사용하였고 target 아래의 빌드된 jar 파일을 복사하여 실행하는 방법이다.

실행 명령어는 scripts/run.sh 파일을 보면 알 수 있다.

#!/bin/sh

java ${JAVA_OPTS} -jar app.jar ${@}

JAVA_OPTS 는 환경 변수 이므로 컨테이너를 실행할 때 해당 값을 전달하는 것이 가능하다.

이제 컨테이너 이미지를 빌드하면 그 사이즈는 다음과 같다.

$ docker build -t seungkyua/springboot-docker -f docker/Dockerfile2 .

------- output -----------
[+] Building 2.6s (11/11) FINISHED                                                                                                                                                                                                               docker:desktop-linux
 => [internal] load build definition from Dockerfile2                                                                                                                                                                                                            0.0s
 => => transferring dockerfile: 442B                                                                                                                                                                                                                             0.0s
 => [internal] load .dockerignore                                                                                                                                                                                                                                0.0s
 => => transferring context: 2B                                                                                                                                                                                                                                  0.0s
 => [internal] load metadata for docker.io/library/openjdk:17.0.2-jdk-slim                                                                                                                                                                                       2.2s
 => [auth] library/openjdk:pull token for registry-1.docker.io                                                                                                                                                                                                   0.0s
 => CACHED [1/5] FROM docker.io/library/openjdk:17.0.2-jdk-slim@sha256:aaa3b3cb27e3e520b8f116863d0580c438ed55ecfa0bc126b41f68c3f62f9774                                                                                                                          0.0s
 => [internal] load build context                                                                                                                                                                                                                                0.0s
 => => transferring context: 291B                                                                                                                                                                                                                                0.0s
 => [2/5] RUN mkdir -p /app                                                                                                                                                                                                                                      0.1s
 => [3/5] WORKDIR /app                                                                                                                                                                                                                                           0.0s
 => [4/5] COPY target/example-0.0.1-SNAPSHOT.jar /app/app.jar                                                                                                                                                                                                    0.1s
 => [5/5] COPY scripts/run.sh .                                                                                                                                                                                                                                  0.0s
 => exporting to image                                                                                                                                                                                                                                           0.1s
 => => exporting layers                                                                                                                                                                                                                                          0.1s
 => => writing image sha256:6d6ba6764805971eef0532e21ec28feb6308ddb04bb650a7d087ab689d0d65be                                                                                                                                                                     0.0s
 => => naming to docker.io/seungkyua/springboot-docker
$ docker image ls 
REPOSITORY                    TAG       IMAGE ID       CREATED          SIZE
seungkyua/springboot-docker   latest    6d6ba6764805   54 seconds ago   473MB

이미지 크기가 473M 로 만들어졌다. 어플리케이션 jar 파일의 크기가 68M 이므로 JVM 을 포함하는 이미지가 405M 의 크기가 된다는 의미이다.

보통 이미지를 작게하기 위해서 base 이미지 태그로 alpine 이나 slim 을 많이 사용한다. 여기서 slim 을 사용했는데도 이 정도 크기라면 사이즈가 작다고 할 수 없다.

더 작은 사이즈를 만들기 위해서 alpine 이미지를 찾아서 적용해 보자.

base 이미지를 amazoncorretto 로 변경하고 다시 사이즈를 비교해 보자.

FROM amazoncorretto:17-alpine
MAINTAINER Ahn Seungkyu

ARG JAR_FILE=example-0.0.1-SNAPSHOT.jar

RUN mkdir -p /app
WORKDIR /app

COPY target/${JAR_FILE} /app/app.jar
COPY scripts/run.sh .
ENTRYPOINT ["./run.sh"]
$ docker build -t seungkyua/springboot-docker -f docker/Dockerfile2 .

$ docker image ls
REPOSITORY                    TAG       IMAGE ID       CREATED          SIZE
seungkyua/springboot-docker   latest    ed08524545ba   31 seconds ago   358MB
<none>                        <none>    6d6ba6764805   7 minutes ago    473MB

조회되는 이미지를 보면 base 이미지만을 바꿨을 뿐인데 358M 로 줄어들었다.

컨테이너 이미지 만들기 - 멀티 스테이지

컨테이너 이미지 사이즈를 줄이기 위해서 멀티 스테이지를 써야 한다는 말을 들어봤을 것이다. 여기서도 사이즈를 줄이기 위해 멀티 스테이지를 사용해 보자.

docker/Dockerfile3 를 보면 멀티 스테이지를 어떻게 구성하는지 알 수 있다.

# syntax=docker/dockerfile:1
FROM maven:3.8.5-openjdk-17 as build
MAINTAINER Ahn Seungkyu

WORKDIR /app
COPY . /app
RUN --mount=type=cache,target=/root/.m2 mvn -Dmaven.wagon.http.ssl.insecure=true -Dmaven.wagon.http.ssl.allowall=true package

FROM amazoncorretto:17-alpine
MAINTAINER Ahn Seungkyu

ARG JAR_FILE=example-0.0.1-SNAPSHOT.jar

WORKDIR /app
COPY --from=build /app/target/${JAR_FILE} /app/app.jar
COPY scripts/run.sh /app/
ENTRYPOINT ["./run.sh"]

빌드하는 이미지와 런타임에서 실행되는 이미지가 나눠져 있다.

# syntax=docker/dockerfile:1 는 이미지 안에서 소스 빌드를 할 때 디펜던시가 있는 파일을 매번 가져오지 말고 한 번만 가져와서 캐싱하여 효율적으로 사용하고자 할 때 사용한다.

--mount=type=cache,target=/root/.m2 는 로컬에 저장해서 재활용하자는 의미이고 -Dmaven.wagon.http.ssl.insecure=true -Dmaven.wagon.http.ssl.allowall=true 메이븐으로 패키지할 때 jvm 이 ssl 인증서 없이 사용하기 위해서 추가되었다.

첫번째는 어플리케이션을 빌드하기 위해 필요한 이미지 정의이고 두번째는 빌드된 어플리케이션을 복사하는 이미지 정의로 두번째는 앞서 기본편에서 설명한 것과 동일하다.

jar 파일만 빌드해서 복사하는 구조라 실제로 이미지 차이는 없을 것이다. 다만, 빌드를 이미지를 만들 때 로컬 환경 구성 없이도 만들 수 있다는 장점이 있다.

$ docker build -t seungkyua/springboot-docker -f docker/Dockerfile3 .

$ docker image ls
REPOSITORY                    TAG       IMAGE ID       CREATED          SIZE
seungkyua/springboot-docker   latest    33ed3cbf0300   31 seconds ago   358MB
<none>                        <none>    ed08524545ba   4 minutes ago    358MB
<none>                        <none>    6d6ba6764805   7 minutes ago    473MB

컨테이너 이미지 만들기 - alpine 에 추가

여전히 사이즈가 358M 로 작지 않은 사이즈이다.

이제 기본 alpine 이미지에 JRE 와 어플리케이션을 설치하는 방법으로 이미지 사이즈를 줄여보자.

docker/Dockerfile 을 살펴보자.

FROM amazoncorretto:17-alpine3.18 as builder-jre

RUN apk add --no-cache --repository=http://dl-cdn.alpinelinux.org/alpine/edge/main/ binutils=2.41-r0

RUN $JAVA_HOME/bin/jlink \
         --module-path "$JAVA_HOME/jmods" \
         --verbose \
         --add-modules ALL-MODULE-PATH \
         --strip-debug \
         --no-man-pages \
         --no-header-files \
         --compress=2 \
         --output /jre

#=========================================================================

# syntax=docker/dockerfile:1
FROM maven:3.8.5-openjdk-17 as build
MAINTAINER Ahn Seungkyu

WORKDIR /app
COPY . /app
RUN --mount=type=cache,target=/root/.m2 mvn -Dmaven.wagon.http.ssl.insecure=true -Dmaven.wagon.http.ssl.allowall=true package

#=========================================================================

FROM alpine:3.18.4
MAINTAINER Ahn Seungkyu

ENV JAVA_HOME=/jre
ENV PATH="$JAVA_HOME/bin:$PATH"
ARG JAR_FILE=example-0.0.1-SNAPSHOT.jar

COPY --from=builder-jre /jre $JAVA_HOME

ARG APPLICATION_USER=appuser
RUN adduser --no-create-home -u 1000 -D $APPLICATION_USER

RUN mkdir /app && chown -R $APPLICATION_USER /app

USER 1000

COPY --chown=1000:1000 --from=build /app/target/${JAR_FILE} /app/app.jar
COPY scripts/run.sh /app/

WORKDIR /app
EXPOSE 8080

ENTRYPOINT ["./run.sh"]

이미지가 3개로 구성되어 있다.

첫번째는 작은 사이즈의 jre 를 만드는 이미지, 두번째는 어플리케이션을 빌드하는 이미지, 마지막으로 세번째는 작은 alpine 베이스 이미지에 jre 와 빌드된 어플리케이션 jar 파일을 복사하는 이미지이다.

첫번째 이미지 만드는 부분에서 binutils 을 설치하는데 다운 받을 리파지토리를 지정하여 에러가 없게 한다. binutils 는 jre 을 만들 때 strip-debug 옵션을 사용하기 위해서 설치한다.

세번째 이미지 만드는 부분에서 사용자를 추가하는 로직이 있는데 이는 보안상 이미지내 실행 프로세스를 root 가 아닌 지정된 사용자로 하기 위해서 일반적으로 추가한다.

이미지 사이즈는 다음과 같다.

$ docker build -t seungkyua/springboot-docker -f docker/Dockerfile .

$ docker image ls                                                   
REPOSITORY                    TAG       IMAGE ID       CREATED          SIZE
seungkyua/springboot-docker   latest    f5dc2f994864   28 seconds ago   168MB
<none>                        <none>    ed08524545ba   24 minutes ago   358MB
<none>                        <none>    6d6ba6764805   31 minutes ago   473MB

이미지가 최종적으로 168M 로 줄어들었다.

이제 실제 이미지가 정상적으로 실행되는지 확인해 보자.

mysql 을 띄우고 어플리케이션을 띄운 후에 curl 로 데이터를 입력해 본다.

1. mysql 실행

$ mkdir -p ~/.docker-data/mysql

$ docker run --cap-add=sys_nice -d --restart=always -p 3306:3306 -e MYSQL_ROOT_PASSWORD=password \
-v ~/.docker-data/mysql:/var/lib/mysql \
--name mysql-ask mysql:8.0.34 \
--character-set-server=utf8mb4 \
--collation-server=utf8mb4_unicode_ci

2. database 생성

$ docker exec -it mysql-ask bash
# mysql -uroot -ppassword
mysql> create database if not exists order_service;

3. 어플리케이션 실행

$ docker run -d --name springboot --rm -p 19090:19090 --link mysql-ask:localhost seungkyua/springboot-docker

4. 데이터 조회

$ curl -X POST http://127.0.0.1:19090/orders \
   -H "Content-Type: application/json" \
   -d '{"customerId": 1, "orderTotal": 12.23}'

--- output ---
{"orderId":1}

정상적으로 어플리케이션이 실행되어 동작하는 것을 확인할 수 있다.

반응형
Posted by seungkyua@gmail.com
,
반응형

컨테이어가 사용하는 네트워크는 처음 접할 때는 어렵기 마련이다. 그러나 리눅스 네트워크 네임스페이스를 공부하고 리눅스 명령어로 이를 구현해 보면 이해가 훨씬 쉽다.

 

Single Network Namespace

구현하고자 하는 목표는 아래 다이어그램과 같다.

red 네트워트 네임스페이스를 만들고 인터페이스를 만들어서 ip 를 할당하고 노드에서 ping 을 통해 통신이 가능한지 확인한다.

 

 

 

 

아래의 명령어로 컨테이너 네트워크를 만들어 본다.

 

1. 새로운 네임스페이스 생성 (이름: red)

$ sudo ip netns add red

 

2. veth pair 생성

$ sudo ip link add veth1-r type veth peer veth2-r

 

3. veth2-r 을 red 네임스페이스로 이동

$ sudo ip link set veth2-r netns red

 

4. red 네임스페이스에 존재하는 veth2-r 인터페이스에 ip 주소를 세팅

$ sudo ip netns exec red ip address add 192.168.0.1 dev veth2-r

 

5. red 네임스페이스에 존재하는 veth2-r 인터페이스 up

$ sudo ip netns exec red ip link set dev veth2-r up

 

6. 노드에 존재하는 veth1-r 인터페이스 up

$ sudo ip link set dev veth1-r up

 

7. red 네임스페이스에 존재하는 loopback 인터페이스 up

$ sudo ip netns exec red ip link set lo up

 

8. 노드의 라우팅 테이블 세팅

subnet mask 가 32 이므로 바로 해당 ip 에 대해서만 veth1-r 로 향하도록 라우팅을 세팅했다. veth1-r 은 veth2-r 과 링크가 연결되어 있으므로 192.168.0.1 을 목적지로 하는 네트워크 패킷들은 red 네임스페이스 안에 존재하는 veth2-r 인터페이스로 향하게 된다.

$ sudo ip route add 192.168.0.1/32 dev veth1-r

 

9. red 네임스페이스의 디폴트 라우팅을 세팅

red 네임스페이스 안에서 디폴트 게이트웨이를 veth2-r 인터페이스의 ip address 로 세팅

$ sudo ip netns exec red ip route add default via 192.168.0.1 dev veth2-r

 

10. 네트워크 통신 확인

$ ip netns
red (id: 0)

$ sudo ip netns exec red ip address
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host
       valid_lft forever preferred_lft forever
74: veth2-r@if75: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
    link/ether 52:42:40:a9:86:8b brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet 192.168.0.1/32 scope global veth2-r
       valid_lft forever preferred_lft forever
    inet6 fe80::5042:40ff:fea9:868b/64 scope link
       valid_lft forever preferred_lft forever

$ ping 192.168.0.1
PING 192.168.0.1 (192.168.0.1) 56(84) bytes of data.
64 bytes from 192.168.0.1: icmp_seq=1 ttl=64 time=0.057 ms
64 bytes from 192.168.0.1: icmp_seq=2 ttl=64 time=0.046 ms
64 bytes from 192.168.0.1: icmp_seq=3 ttl=64 time=0.043 ms

 

 

Single Network, 2 Namespaces

이번에는 하나의 노드에 2개의 네임스페이스가 있는 경우를 살펴보자. 보통 컨테이너는 하나의 노드에 2개 이상 멀티로 띄는 경우가 많기 때문에 이 번 케이스가 대부분의 컨테이너 네트워크에 맞다고 보면 된다.

 

 

 

1개의 네임스페이스와 다른 점은 ip 대역을 지정하고 (일반적으로 subnet mask 24 혹은 25) 리눅스 브릿지로 이를 연결하는 것이다. 그리고 브릿지에 ip 주소를 할당한 후 네임스페이스의 네트워크에서 사용할 디폴트 게이트웨이로 지정하여, 네임스페이스 내에서는 외부로 나가는 패킷을 리죽스 브릿지로 통하게 한다.

 

1. 네임스페이스 생성

$ sudo ip netns add red
$ sudo ip netns add blue

 

2. veth pair 생성

$ sudo ip link add veth1-r type veth peer name veth2-r
$ sudo ip link add veth1-b type veth peer name veth2-b

 

3. veth pair 를 네임스페이스로 이동

$ sudo ip link set veth2-r netns red
$ sudo ip link set veth2-b netns blue

 

4. 네트워크 네임스페이스 내에 있는 인터페이스에 ip 주소 세팅

$ sudo ip netns exec red ip address add 192.168.0.2/24 dev veth2-r
$ sudo ip netns exec blue ip address add 192.168.0.3/24 dev veth2-b

 

5. 네트워크 네임스페이스 내에 있는 인터페이스를 up

$ sudo ip netns exec red ip link set dev veth2-r up
$ sudo ip netns exec blue ip link set dev veth2-b up

 

6. 리눅스 브릿지 생성

 $ sudo ip link add name br0 type bridge

 

7. 네트워크 네임스페이스 인터페이스를 브릿지에 연결

$ sudo ip link set dev veth1-r master br0
$ sudo ip link set dev veth1-b master br0

 

8. 네트워크 네임스페이스 인터페이스를 up

$ sudo ip link set dev veth1-r up
$ sudo ip link set dev veth1-b up

 

9. 브릿지에 ip 주소 할당

$ sudo ip address add 192.168.0.1/24 dev br0

 

10. 브릿지 up

$ sudo ip link set dev br0 up

 

red 네임스페이스에서 blue 네임스페이스로 ping 테스트

$ sudo ip netns exec red ping 192.168.0.3
PING 192.168.0.3 (192.168.0.3) 56(84) bytes of data.

ping 은 통신이 안되고 있다. 원인이 무엇일까?

위의 통신은 아래의 경로를 지난다.

red 네임스페이스 -> root 1번 프로세스의 노드 네임스페이스 -> blue 네임스페이스

즉, red 네임스페이스의 iptables, 노드 네임스페이스의 iptables, blue 네임스페이스의 iptables 를 모두 지난다. 그러므로 각 iptables 의 filter 체인을 모두 확인해야 한다.

 

  1. red 네임스페이스의 iptables 은 밖으로 나가는 출발점 이고, 응답을 받아야 하므로 OUTPUT filter 체인과 INPUT 체인이 열려 있는지를 확인한다.
  2. 노드 네임스페이스의 iptables 는 네트워크를 경유하는 곳이므로 FORWARD filter 체인이 열려있는지 확인한다. (지금의 경우에만 그렇고 서버에 접속하고 여러 작업을 하므로 INPUT, OUTPUT 이 모두 열려 있어야 한다.)
  3. blue 네임스페이스는 iptables 는 패킷을 받고 응답을 줘야 하므로 INPUT filter 체인과 OUTPUT 체인이 열려있는지 확인한다.
## red 네임스페이스
$ sudo ip netns exec red iptables -S
-P INPUT ACCEPT
-P FORWARD ACCEPT
-P OUTPUT ACCEPT

## 노드 네임스페이스
$ sudo iptables -S
-P INPUT ACCEPT
-P FORWARD DROP
-P OUTPUT ACCEPT
...

## blue 네임스페이스
$ sudo ip netns exec blue iptables -S
-P INPUT ACCEPT
-P FORWARD ACCEPT
-P OUTPUT ACCEPT

노드 네임스페스의 FOWARD filter 가 DROP 으로 되어 있다. 이를 ACCEPT 로 변경해 준다.

$ sudo iptables -P FORWARD ACCEPT

한가지가 더 있다. 다음과 같이 sysctl 로 forward 를 열어줘야 한다.

아래의 값이 0 일 경우 이를 1로 변경해 준다.

$ sudo sysctl -a | grep -i net.ipv4.ip_forward
net.ipv4.ip_forward = 0

$ sudo vi /etc/sysctl.d/99-sysctl.conf
net.ipv4.ip_forward=1

$ sudo sysctl -p

이제 red 네임스페이스에서 blue 네임스페이스로 ping 테스트를 다시 하면 성공함을 알 수 있다.

$ sudo ip netns exec red ping 192.168.0.3
PING 192.168.0.3 (192.168.0.3) 56(84) bytes of data.
64 bytes from 192.168.0.3: icmp_seq=1 ttl=64 time=0.077 ms
64 bytes from 192.168.0.3: icmp_seq=2 ttl=64 time=0.063 ms
64 bytes from 192.168.0.3: icmp_seq=3 ttl=64 time=0.062 ms

 

 

red, blue 네임스페이스에서 외부로의 ping 테스트

노드 내부 간의 ping 테스트는 성공했으나 외부 통신은 여전히 실패한다.

$ sudo ip netns exec red ping 8.8.8.8
ping: connect: Network is unreachable

이유는 간단하다. red 네임스페이스에서 밖으로 나가는 route 경로를 지정하지 않았기 때문이다. 디폴트 게이트웨이를 아래와 같이 각각 추가해 준다.

$ sudo ip netns exec red ip route add default via 192.168.0.1 dev veth2-r
$ sudo ip netns exec blue ip route add default via 192.168.0.1 dev veth2-b

그리고 중요한 내용 하나가 더 있다.

외부에서는 node 까지는 알 수 있지만 실제로 내부의 red 네임스페이스에 대해서는 알지 못한다.

그렇기 때문에 네임스페이스 안에서 노드 밖으로 패킷이 나갈 때는 source address 를 node 의 address 로 변경해줘야 한다. 이를 masquerade 라고 하며 아래와 같이 노드에서 iptables 의 nat 테이블에 등록하면 된다.

$ sudo iptables -t nat -A POSTROUTING -s 192.168.0.0/24 ! -o br0 -j MASQUERADE

이제 다시 외부 통신을 테스트 해보면 성공함을 알 수 있다.

$ sudo ip netns exec red ping 8.8.8.8
PING 8.8.8.8 (8.8.8.8) 56(84) bytes of data.
64 bytes from 8.8.8.8: icmp_seq=1 ttl=103 time=32.6 ms
64 bytes from 8.8.8.8: icmp_seq=2 ttl=103 time=32.6 ms
64 bytes from 8.8.8.8: icmp_seq=3 ttl=103 time=32.6 ms

$ sudo ip netns exec blue ping 8.8.8.8
PING 8.8.8.8 (8.8.8.8) 56(84) bytes of data.
64 bytes from 8.8.8.8: icmp_seq=1 ttl=103 time=32.5 ms
64 bytes from 8.8.8.8: icmp_seq=2 ttl=103 time=32.6 ms
64 bytes from 8.8.8.8: icmp_seq=3 ttl=103 time=32.5 ms

 

참고: https://www.youtube.com/watch?v=6v_BDHIgOY8&t=1561s (Container Networking From Scratch - Kristen Jacobs)

반응형
Posted by seungkyua@gmail.com
,
반응형

일반적으로 go 실행 프로그램을 도커 이미지를 만드는 경우는 많은데 plugin 을 이미지로 만드는 경우는 드문 것 같다. 이번에는 kustomize 의 plugin 을 go 로 작성하여 plugin 이미지를 만들어 본다. (kustomize plugin 을 만드는 방법이 아니라 kustomize plugin 을 도커 이미지로 만드는 내용이다.)

kustomize plugin 소스 다운로드

TACO 에서는 Kubernetes Resource yaml 파일을 kustomize 를 사용하여 만든다. 일반 kubernetes resource type (정확히는 HelmRelease type 임)이 아니라서 이를 위한 plugin 을 만들어 제공하고 있다.

github 으로 부터 소스를 다운 받는다.

$ git clone https://github.com/openinfradev/kustomize-helm-transformer.git

2 step building Dockerfile 작성

이미지 사이즈를 줄이기 위해서 2 step 으로 빌드하는 Dockerfile 을 작성한다.

아래는 Dockerfile2 파일의 첫번째 부분이다.

FROM golang:1.14-alpine3.13 AS builder
LABEL AUTHOR Seungkyu Ahn (seungkyua@gmail.com)

RUN apk update && apk add build-base

ENV HOME /root
ENV GO111MODULE on
ENV GOROOT /usr/local/go
ENV GOPATH $HOME/golang

RUN mkdir -p $HOME/.config/kustomize/plugin/openinfradev.github.com/v1/helmvaluestransformer

WORKDIR $HOME
COPY . $HOME/kustomize-helm-transformer

RUN cat kustomize-helm-transformer/README.md | grep -m 1 "* kustomize" | sed -nre 's/^[^0-9]*(([0-9]+\.)*[0-9]+).*/\1/p' > .kustomize_version
RUN go get sigs.k8s.io/kustomize/kustomize/v3@v$(cat $HOME/.kustomize_version)
RUN mv $GOPATH/bin/kustomize /usr/local/bin/

WORKDIR $HOME/kustomize-helm-transformer/plugin/openinfradev.github.com/v1/helmvaluestransformer/
RUN GOOS=linux GOARCH=amd64 go build -buildmode plugin -o HelmValuesTransformer.so
RUN mv HelmValuesTransformer.so $HOME/
  1. go build 를 위해서 build-base 를 설치한다.
  2. kustomize plugin 을 위해서 $HOME/.config/kustomize/plugin 디렉토리 아래 module 명 디렉토리를 만든다.
  3. kustomize version 을 알아내기 위해서 README.md 파일을 파싱한다.
  4. go get 으로 해당 버전의 kustomize 를 다운받아 /usr/local/bin 에 설치한다.
  5. go build -buildmode plugin 으로 파일을 빌드한다. -o 옵션을 사용하면 원하는 파일명으로 만들 수 있다. go build 명령어 외에 go test 를 통해 unit test 를 수행하면서 so 파일을 생성할 수 도 있다.

다음은 Dockerfile2 의 두번째 부분이다.

FROM alpine:edge
LABEL AUTHOR Seungkyu Ahn (seungkyua@gmail.com)

RUN mkdir -p $HOME/.config/kustomize/plugin/openinfradev.github.com/v1/helmvaluestransformer
COPY --chown=0:0 --from=builder /root/*.so /root/.config/kustomize/plugin/openinfradev.github.com/v1/helmvaluestransformer/
COPY --chown=0:0 --from=builder /usr/local/bin/kustomize /usr/local/bin/kustomize
WORKDIR /

CMD ["/usr/local/bin/kustomize"]

base 이미지를 scratch 로 할 수 도 있지만 scratch 는 shell 명령을 쓸 수 없기 때문에 alpine 이미지를 사용하였다.

  1. kustomize plugin 을 위해서 $HOME/.config/kustomize/plugin 디렉토리 아래 module 명 디렉토리를 만든다.
  2. 빌드된 kustomize plugin so 파일을 복사한다.
  3. 다운받은 kustomize 파일을 복사한다.

추가 확인 사항

  1. kustomize 를 go get 으로 다운받으면 잘 동작하나 curl 로 바이너리만 받아서 실행하면 에러가 난다. 실제로 이 둘의 binary 사이즈에도 차이가 있는데 이는 추후 원인을 파악해 봐야 겠다.
  2. go build 에 앞서 go mod tidy 와 go mod vender 를 수행하면 이미지를 실행할 때 앞의 1번과 같은 에러가 난다. 이 부분도 추후 확인해야 할 사항이다.
반응형
Posted by 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 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 seungkyua@gmail.com
,
반응형

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 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 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 seungkyua@gmail.com
,
반응형
## https://github.com/kubernetes/kubernetes/tree/master/examples/volumes/cephfs

[ ceph-admin 노드에서 ]
$ ssh ceph@192.168.30.22

## kubes Pool 생성
$ ceph osd pool create kubes 128

## kube user 생성
$ ceph auth get-or-create client.kube mon 'allow r' \
osd 'allow class-read object_prefix rbd_children, allow rwx pool=kubes'


[client.kube]
    key = AQCt/BpYigJ7MRAA5vy+cl39EsKpY3C+tXEGrA==

## kube user 에 대한 secret key 생성 및 조회
$ ceph auth get-or-create client.kube
AQCt/BpYigJ7MRAA5vy+cl39EsKpY3C+tXEGrA==


## kube-node01, kube-node02 서버에 kube key 와 ceph.conf 추가
$ ssh stack@192.168.30.15 sudo mkdir -p /etc/ceph
$ ceph auth get-or-create client.kube | ssh stack@192.168.30.15 sudo tee /etc/ceph/ceph.client.kube.keyring
$ cat /etc/ceph/ceph.conf | ssh stack@192.168.30.15 sudo tee /etc/ceph/ceph.conf
$ ssh stack@192.168.30.15 sudo chown -R stack.stack /etc/ceph

$ ssh stack@192.168.30.16 sudo mkdir -p /etc/ceph
$ ceph auth get-or-create client.kube | ssh stack@192.168.30.16 sudo tee /etc/ceph/ceph.client.kube.keyring
$ cat /etc/ceph/ceph.conf | ssh stack@192.168.30.16 sudo tee /etc/ceph/ceph.conf
$ ssh stack@192.168.30.16 sudo chown -R stack.stack /etc/ceph



[ kube-node01, kube-node02 에 접속 ]
## ceph rbd client (ceph-common) 와 ceph fs client 설치 (ceph-fs-common)
$ sudo apt-get -y install ceph-common ceph-fs-common



########################################
## ceph rbd 로 연결하는 방식
########################################

## https://github.com/kubernetes/kubernetes/tree/master/examples/volumes/rbd
## https://github.com/ceph/ceph-docker/tree/master/examples/kubernetes

[ ceph-admin 노드에서 ]

## kube keyring 파일 넣기
$ sudo vi /etc/ceph/ceph.client.kube.keyring
[client.kube]
    key = AQCt/BpYigJ7MRAA5vy+cl39EsKpY3C+tXEGrA==


## rbd 이미지 생성
## http://karan-mj.blogspot.kr/2013/12/ceph-installation-part-3.html

$ rbd create ceph-rbd-test --pool kubes --name client.kube --size 1G -k /etc/ceph/ceph.client.kube.keyring

$ rbd list --pool kubes --name client.kube -k /etc/ceph/ceph.client.kube.keyring
$ rbd -p kubes ls


## Jewel 의 새기능은 현재 대부분의 OS 에서 mount 문제가 있어 image 기능을 제거 해야 함
$ rbd feature disable ceph-rbd-test fast-diff --pool kubes --name client.kube -k /etc/ceph/ceph.client.kube.keyring
$ rbd feature disable ceph-rbd-test deep-flatten --pool kubes --name client.kube -k /etc/ceph/ceph.client.kube.keyring
$ rbd feature disable ceph-rbd-test object-map --pool kubes --name client.kube -k /etc/ceph/ceph.client.kube.keyring
$ rbd feature disable ceph-rbd-test exclusive-lock --pool kubes --name client.kube -k /etc/ceph/ceph.client.kube.keyring

$ rbd info ceph-rbd-test --pool kubes --name client.kube -k /etc/ceph/ceph.client.kube.keyring
$ rbd --image ceph-rbd-test -p kubes info

$ rbd remove ceph-rbd-test --pool kubes --name client.kube -k /etc/ceph/ceph.client.kube.keyring


## secret yaml 을 만들기 위해 key 를 base64 로 인코딩 함
$ grep key /etc/ceph/ceph.client.kube.keyring |awk '{printf "%s", $NF}'|base64
QVFDdC9CcFlpZ0o3TVJBQTV2eStjbDM5RXNLcFkzQyt0WEVHckE9PQ==




[ kube-deploy 접속 ]

## secret key 를 pod 로 생성하여 접속
$ vi ~/kube/ceph-secret.yaml
apiVersion: v1
kind: Secret
metadata:
  name: ceph-secret
data:
  key: QVFDdC9CcFlpZ0o3TVJBQTV2eStjbDM5RXNLcFkzQyt0WEVHckE9PQ==

$ scp ~/kube/ceph-secret.yaml kube-master01:~/kube/.
$ ssh kube-master01 "kubectl create -f ~/kube/ceph-secret.yaml"
$ kubectl -s http://kube-master01:8080 get secrets


## rbd-with-secret pod 생성해서 rbd 활용
$ vi ~/kube/rbd-with-secret.yml
apiVersion: v1
kind: Pod
metadata:
  name: rbd2
spec:
  containers:
  - image: gcr.io/google_containers/busybox
    command:
    - sleep
    - "3600"
    imagePullPolicy: IfNotPresent
    name: rbd-rw-busybox
    volumeMounts:
    - mountPath: "/mnt/rbd"
      name: rbdpd
  volumes:
  - name: rbdpd
    rbd:
      monitors:
      - 192.168.30.23:6789
      - 192.168.30.24:6789
      - 192.168.30.25:6789
      pool: kubes
      image: ceph-rbd-test
      user: kube
      keyring: /etc/ceph/ceph.client.kube.keyring
      secretRef:
        name: ceph-secret
      fsType: ext4
      readOnly: false


$ scp ~/kube/rbd-with-secret.yml kube-master01:~/kube/.
$ ssh kube-master01 "kubectl create -f ~/kube/rbd-with-secret.yml"
$ kubectl -s http://kube-master01:8080 get pods




## rbd 연결 확인
$ kubectl -s http://kube-master01:8080 describe pods rbd2
$ kubectl -s http://kube-master01:8080 exec -it rbd2 -- df -h



[ kube-node02 접속하여 ]

$ docker ps
$ docker inspect --format '{{ .Mounts }}' 4c4070a1393b

## 혹은
$ mount |grep kub
/dev/rbd0 on /var/lib/kubelet/plugins/kubernetes.io/rbd/rbd/kubes-image-ceph-rbd-test type ext4 (rw,relatime,stripe=1024,data=ordered)
/dev/rbd0 on /var/lib/kubelet/pods/061973fc-a265-11e6-940f-5cb9018c67dc/volumes/kubernetes.io~rbd/rbdpd type ext4 (rw,relatime,stripe=1024,data=ordered)




[ kube-deploy 접속해서 ]

## secret key pod 를 사용하지 않고 keyring 으로만 rbd pod 생성
$ vi ~/kube/rbd.yml
apiVersion: v1
kind: Pod
metadata:
  name: rbd
spec:
  containers:
  - image: gcr.io/google_containers/busybox
    command:
    - sleep
    - "3600"
    imagePullPolicy: IfNotPresent
    name: rbd-rw-busybox
    volumeMounts:
    - mountPath: "/mnt/rbd"
      name: rbdpd
  volumes:
  - name: rbdpd
    rbd:
      monitors:
      - 192.168.30.23:6789
      - 192.168.30.24:6789
      - 192.168.30.25:6789
      pool: kubes
      image: ceph-rbd-test
      user: kube
      keyring: /etc/ceph/ceph.client.kube.keyring
      fsType: ext4
      readOnly: false


$ scp ~/kube/rbd.yml kube-master01:~/kube/.
$ ssh kube-master01 "kubectl create -f ~/kube/rbd.yml"
$ kubectl -s http://kube-master01:8080 get pods

## rbd 연결 확인

$ kubectl -s http://kube-master01:8080 exec -it rbd -- df -h 


반응형
Posted by seungkyua@gmail.com
,
반응형

kubernetes 의 Authentication 과 Authorization 활용


[ Authentication ]

  • client-ca-file 로 접속
  • static password file 사용
  • static token file 사용
  • OpenStack Keystone 사용
먼저 client-ca-file 은 Authorization 을 사용할 수 없으니 제외

static password 도 잘됨, 그러나 Authorization 을 연동해 보지는 않았음.

OpenStack Keystone 연계는 아직 알파버전이고 수정이 자주 일어나서 아직 소스까지 볼 단계는 아니라 생략.

static token 방식은 Authorization 과도 잘 연동되므로 이걸 활용



## uuid generate

$ cat /proc/sys/kernel/random/uuid



## {{uuid}} 는 위에서 제너레이션 된 값으로 대치

$ sudo vi /etc/default/kube-token

{{uuid}},admin,1

{{uuid}},ahnsk,2,"tfabric,group1"

{{uuid}},stack,3,tfabric



## api 서버에 token file 옵션 추가

$ sudo chown stack.root /etc/default/kube-token

--token-auth-file=/etc/default/kube-token \


$ sudo systemctl restart kube-apiserver.service


$ kubectl -s https://kube-master01:6443 --token={{uuid}} get node





[ Authorization ]

  • ABAC Mode
  • RBAC Mode
RBAC 는 아직 알파라 베타인 ABAC 를 활용

## 전체 admin : admin,   tfabric admin : ahnsk,      tfabric readOnly user : stack
kubectl 이 api version 을 체크하기 때문에 무조건 nonResourcePath 도 all 로 지정해야 함

$ sudo vi /etc/default/kube-rbac.json
{"apiVersion":"abac.authorization.kubernetes.io/v1beta1","kind":"Policy","spec":{"user":"system:serviceaccount:kube-system:default","namespace":"*","resource":"*","apiGroup":"*", "nonResourcePath": "*"}}
{"apiVersion": "abac.authorization.kubernetes.io/v1beta1", "kind": "Policy", "spec": {"user":"admin", "namespace": "*", "resource": "*", "apiGroup": "*", "nonResourcePath": "*" }}
{"apiVersion": "abac.authorization.kubernetes.io/v1beta1", "kind": "Policy", "spec": {"user":"ahnsk", "namespace": "tfabric", "resource": "*", "apiGroup": "*", "nonResourcePath": "*" }}
{"apiVersion": "abac.authorization.kubernetes.io/v1beta1", "kind": "Policy", "spec": {"user":"stack", "namespace": "tfabric", "resource": "*", "apiGroup": "*", "readonly": true, "nonResourcePath": "*"}}



kube-system 이 kube-apiserver 에 접근하기 위해서는 1라인이 반드시 있어야 함





$ sudo vi /etc/default/kube-apiserver

--authorization-mode=ABAC \

--authorization-policy-file=/etc/default/kube-rbac.json \


$ sudo systemctl restart kube-apiserver.service




$ cd ~/kube

$ vi busybox-tfabric.yaml

apiVersion: v1

kind: Pod

metadata:

  name: busybox

  namespace: tfabric

spec:

  containers:

  - image: gcr.io/google_containers/busybox

    command:

      - sleep

      - "3600"

    imagePullPolicy: IfNotPresent

    name: busybox

  restartPolicy: Always



$ kubectl -s https://kube-master01:6443 --token={{uuid}} --v=8 version


token 지정을 매번 하기 귀찮으니 config context 를 활용하는 것이 좋음.

이건 다음에....















반응형
Posted by seungkyua@gmail.com
,