UnSimple image builder

Unsimple image builder

보통 gii source 기반 이미지 빌드는 Jenkins 빌드파이프라인이나, Argocd 등을 이용하지만 임의로 이미지 빌드 등이 필요한 경우 수동 처리의 귀찮음을 해결하기 위한 도구.

쉽게 말해 매번 docker build -t registry.address/project/category/image_name:tag-tagnumber 명령을 입력하기 귀찮아서 만든 도구.

대강 구성도

                         ┌─────────────────────────────────────────────┐
                         │          Git Image WebBuilder               │
                         │          :8888 (Flask + gunicorn)           │
                         │                                             │
                         │  ┌─────────┐ ┌─────────┐ ┌───────────────┐ │
                         │  │ Sources │ │ Builder │ │   Scripts     │ │
                         │  │         │ │         │ │               │ │
                         │  │Registry │ │Build    │ │Registered     │ │
                         │  │Git Svr  │ │Push     │ │Ad-hoc (SSH)   │ │
                         │  └─────────┘ │Rollout  │ └───────────────┘ │
                         │              └────┬────┘                   │
                         │   ┌───────────────┼───────────────┐        │
                         │   │  SQLite DB    │  Build Logs   │        │
                         │   │  (builder.db) │  (data/)      │        │
                         │   └───────────────┴───────────────┘        │
                         └──────┬──────────┬──────────┬───────────────┘
                                │          │          │
                 ┌──────────────┘          │          └──────────────┐
                 │                         │                         │
                 ▼                         ▼                         ▼
    ┌────────────────────┐   ┌──────────────────────┐   ┌──────────────────┐
    │   Git Server       │   │   Docker Daemon      │   │  K8s Cluster     │
    │                    │   │                      │   │                  │
    │  Forgejo / GitHub  │   │  docker build        │   │  kubectl rollout │
    │  Gitea / GitLab    │   │  docker push         │   │  restart         │
    │                    │   │                      │   │  status / pods   │
    │  SSH clone ──┐     │   │         │            │   │                  │
    │  HTTP clone ─┤     │   │         ▼            │   └──────────────────┘
    └──────────────┘     │   │  ┌──────────────┐    │
                         │   │  │  Registry    │    │
                         │   │  │  Harbor /    │    │
                         │   │  │  DockerHub   │    │
                         │   │  └──────────────┘    │
                         │   └──────────────────────┘
                         │
                ┌────────┴─────────┐
                │  Build Pipeline  │
                │                  │
                │  1. git clone    │
                │  2. Dockerfile ? │
                │  3. docker login │
                │  4. docker build │
                │  5. docker push  │
                │  6. rollout      │
                │     (optional)   │
                └──────────────────┘

다운로드

사용법

기본적으로 git에 등록된 branch를 Unsimple image builder로 클론.
클론된 파일의 Dockerfile을 기반으로 이미지를 빌드한다.
연결된 image registry에 자동 push, 등록된 k8s 클러스터의 디플로이먼트(deployment, statefulset) rollout 명령 까지 날릴 수 있다.

docker를 이용한 openVPN 설정

컨테이너가 아닌 네이티브 설치는 다음 포스트를 참조
openVPN 서버구축 #1. 개요
openVPN 서버구축 #2. 서버 설치 및 설정

docker-compose.yaml 생성

version: '2'
services:
  openvpn:
    cap_add:
     - NET_ADMIN
    image: kylemanna/openvpn
    container_name: openvpn
    ports:
     - "1194:1194/udp" 
    restart: always
    volumes:
     - ./openvpn-data/conf:/etc/openvpn #.openvpn-data/conf 디렉토리를 생성해야 한다.

설정파일 및 인증서 초기화

docker-compose run --rm openvpn ovpn_genconfig -u udp://VPN.YOUR_HOST.NAME

# 아래는 수행 결과
reating network "vpn_default" with the default driver
Pulling openvpn (kylemanna/openvpn:)...
....중략....
Creating vpn_openvpn_run ... done
Processing PUSH Config: 'block-outside-dns'
Processing Route Config: '192.168.254.0/24'
Processing PUSH Config: 'dhcp-option DNS 8.8.8.8'
Processing PUSH Config: 'dhcp-option DNS 8.8.4.4'
Processing PUSH Config: 'comp-lzo no'
Successfully generated config
Cleaning up before Exit ...
docker-compose run --rm openvpn ovpn_initpki

# 아래는 수행 결과
Creating vpn_openvpn_run ... done

init-pki complete; you may now create a CA or requests.
Your newly created PKI dir is: /etc/openvpn/pki

Using SSL: openssl OpenSSL 1.1.1g  21 Apr 2020

Enter New CA Key Passphrase: CA_KEY의_Passphrase
Re-Enter New CA Key Passphrase: CA_KEY의_Passphrase
Generating RSA private key, 2048 bit long modulus (2 primes)
................................+++++
......................................................+++++
e is 65537 (0x010001)
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Common Name (eg: your user, host, or server name) [Easy-RSA CA]:vpn.haedongg.net

CA creation complete and you may now import and sign cert requests.
Your new CA certificate file for publishing is at:
/etc/openvpn/pki/ca.crt

Using SSL: openssl OpenSSL 1.1.1g  21 Apr 2020
Generating DH parameters, 2048 bit long safe prime, generator 2
This is going to take a long time
..............................................................+...........................................+..........................................................
...중략...
........................................................+..+....................+................................................................+....+..++*++*++*++*

DH parameters of size 2048 created at /etc/openvpn/pki/dh.pem

Using SSL: openssl OpenSSL 1.1.1g  21 Apr 2020
Generating a RSA private key
.............+++++
............................+++++
writing new private key to '/etc/openvpn/pki/easy-rsa-74.akeLei/tmp.JBdIKe'
-----
Using configuration from /etc/openvpn/pki/easy-rsa-74.akeLei/tmp.BfJlok
Enter pass phrase for /etc/openvpn/pki/private/ca.key:
Check that the request matches the signature
Signature ok
The Subject's Distinguished Name is as follows
commonName            :ASN.1 12:'vpn.haedongg.net'
Certificate is to be certified until Jul 30 00:51:51 2025 GMT (825 days)

Write out database with 1 new entries
Data Base Updated

Using SSL: openssl OpenSSL 1.1.1g  21 Apr 2020
Using configuration from /etc/openvpn/pki/easy-rsa-149.FPcifA/tmp.cjaMHC
Enter pass phrase for /etc/openvpn/pki/private/ca.key: 위_11_12_라인에서_입력했던_Passphrase

An updated CRL has been created.
CRL file: /etc/openvpn/pki/crl.pem

디렉토리 권한 변경

sudo chown -R $(whoami): ./openvpn-data

서버 프로세스 실행

docker-compose up -d openvpn

클라이언트 사용자 생성 및 인증서 생성

# 비밀번호화 함께 생성
docker-compose run --rm openvpn easyrsa build-client-full 사용자_이름

# 비밀번호 없이 생성
docker-compose run --rm openvpn easyrsa build-client-full 사용자_이름 nopass

# 인증서 파일 출력, 이 파일을 클라이언트 사용자에게 전달하면 된다.
docker-compose run --rm openvpn ovpn_getclient 사용자_이름 > 사용자_이름.ovpn

클라이언트 사용자 제거

# Keep the corresponding crt, key and req files.
docker-compose run --rm openvpn ovpn_revokeclient $CLIENTNAME

# Remove the corresponding crt, key and req files.
docker-compose run --rm openvpn ovpn_revokeclient $CLIENTNAME remove

docker image에 ssh 서비스 추가 (멀티 서비스 띄우기)

docker 컨테이너는 마지막 CMD 혹은 ENTRYPOINT 명령 하나로 컨테이너를 시작한다.
즉, 아래와 같이 docker file을 생성해도 ssh와 httpd가 모두 실행되지 않는다는 이야기이다.

...전략...
# ssh 시작
CMD ["/usr/sbin/sshd", "-D"]
# httpd 시작
CMD ["/usr/local/apache2/bin/httpd", "-DFOREGROUND"]

하지만 편의상 컨테이너에 ssh로 접속하고, 이를 통해 컨테이너 내부 상황을 확인하거나 하고 싶다면 ssh를 추가해줘야 한다.
이를 위해 실행할 서비스만큼 스크립트를 생성하고 wrapper 스크립트를 만들어서 이들을 실행해줘야 한다. 아래 스크립트와 dockerfile을 참고해서 이미지를 만들면 된다.

## ssh를 실행하기 위한 스크립트 
## ssh_start.sh
#!/bin/bash
/usr/sbin/sshd -D

## apache를 실행하기 위한 스크립트
## httpd_start.sh
#!/bin/bash
/usr/local/apache2/bin/httpd -DFOREGROUND

## wrapper script
## wrapper.sh
#!/bin/bash
set -m
/ssh_start.sh &
/httpd_start.sh &
fg %1
## dockerfile 예시
## httpd 이미지에 ssh를 추가하여 연결하고 싶은 경우에 대한 예시
# base가 될 이미지
FROM httpd

#아래 작업들을 수행할 계정 (apt 등은 root 권한이 필요하다)
USER root

ENV DEBIAN_FRONTEND=noninteractive \
    TZ=Asia/Seoul

# ssh server 설치
### Ubuntu
RUN apt-get update                                            &&\
    apt-get install -y openssh-server                         &&\
    apt-get install -y curl vi vim tcpdump net-tools          &&\  #필요한 경우 설치한다.
    apt-get clean                                             &&\
    rm -rf /var/lib/apt/lists/*    /tmp/*    /var/tmp/*

RUN mkdir /var/run/sshd
RUN echo 'root:password' | chpasswd
RUN sed -i 's/#PermitRootLogin prohibit-password/PermitRootLogin yes/' /etc/ssh/sshd_config
RUN sed -i 's/#PasswordAuthentication yes/PasswordAuthentication yes/' /etc/ssh/sshd_config
RUN sed -i 's/#X11Forwarding no/X11Forwarding yes/' /etc/ssh/sshd_config
RUN echo "export VISIBLE=now" >> /etc/profile


# 사용자 추가
RUN useradd haedong -G sudo -m -d /home/haedong -s /bin/bash
RUN echo 'haedong:password'| chpasswd
RUN echo alias "ls='ls --color=auto'" >> ~/.bash_profile
RUN echo alias "ll='ls -lha" >> ~/.bash_profile

RUN mkdir /var/run/sshd

# 외부로 연결할 포트
EXPOSE 22 80

# 서비스 시작을 위한 스크립트
RUN mkdir -p /root/scripts
COPY ssh_start.sh /root/scripts/ssh_start.sh
COPY httpd_start.sh /root/scripts/httpd_start.sh
COPY wrapper.sh /root/scripts/wrapper.sh

CMD /root/scripts/wrapper.sh

컨테이너 빌드 및 시작

# image build
docker build -t httpd:new .

# 컨테이너 시작
docker run -d -p 2222:20 -p 8888:80 httpd:new

Redmine : docker-compose.yaml

Redmine 이 무엇인지는 링크 참조.

docker-compose.yaml

version: '3.1'

services:

  redmine:
    container_name: redmine
    image: redmine
    restart: always
    ports:
      - 8080:3000
    environment:
      REDMINE_DB_MYSQL: db
      REDMINE_DB_PASSWORD: fpemakdls
      REDMINE_DB_DATABASE: redmine
      REDMINE_DB_ENCODING: utf8
      #REDMINE_NO_DB_MIGRATE: true
    volumes:
      - ./volumes/redmine/usr/src/redmine/files:/usr/src/redmine/files
      - ./volumes/redmine/usr/src/redmine/plugins:/usr/src/redmine/plugins
      - ./volumes/redmine/usr/src/redmine/public/themes:/usr/src/redmine/public/themes

  db:
    container_name: mariadb
    image: mariadb
    restart: always
    environment:
      MYSQL_ROOT_PASSWORD: fpemakdls
      MYSQL_DATABASE: redmine
    ports:
      - 3306:3306
    volumes:
      - ./volumes/mysql/var/lib/mysql:/var/lib/mysql
    command:
      - --character-set-server=utf8mb4
      - --collation-server=utf8mb4_unicode_ci

디렉토리 생성

mkdir  -p  ./volumes/redmine/usr/src/redmine/files
mkdir      ./volumes/redmine/usr/src/redmine/plugins
mkdir  -p  ./volumes/redmine/usr/src/redmine/public/themes
mkdir  -p  ./volumes/mysql/var/lib/mysql

Nginx Proxy Manager : docker-compose.yaml

Nginx Proxy Manager가 무엇인지는 링크를 참조.

docker-compose.yaml

version: '3'
services:
  app:
    image: 'jc21/nginx-proxy-manager:latest'
    ports:
      - '80:80'                  # 일반 http 용 port
      - '81:81'                  # 관리 페이지 용 port
      - '443:443'                # https 서비스 용 port
    environment:
      DB_MYSQL_HOST: "db" 
      DB_MYSQL_PORT: 3306
      DB_MYSQL_USER: "npm" 
      DB_MYSQL_PASSWORD: "npm" 
      DB_MYSQL_NAME: "npm" 
    volumes:
      - ./data:/data
      - ./letsencrypt:/etc/letsencrypt
  db:
    image: 'jc21/mariadb-aria:latest'
    environment:
      MYSQL_ROOT_PASSWORD: 'npm'
      MYSQL_DATABASE: 'npm'
      MYSQL_USER: 'npm'
      MYSQL_PASSWORD: 'npm'
    volumes:
      - ./data/mysql:/var/lib/mysql
#      - ./etc/my.cnf.d:/etc/my.cnf.d       mariadb(mysql을 사용할 때도 동일) 설정을 변경하고자 할 때 생성하고 설정 파일을 넣는다.

디렉토리 생성

docker-compose.yaml 파일이 존재하는 디렉토리에 아래와 같이 세 개의 디렉토리를 생성한다.


mkdir ./data
mkdir ./letsencrypt
mkdir -p ./data/mysql
mkdir -p ./etc/my.cnf.d

dockerfile 예시 및 이미지 빌드 – ssh 서버 및 편의 패키지 포함

일반적으로 개발사 등에서 제공되는 docker image 들은 운영에 필요한 최소한의 바이너리들만 담는 경우가 많기 때문에 컨테이너에 문제가 생기거나, 컨테이너 내부의 프로세스를 확인하고자 하는 경우에 ps 명령이 없어서 프로세스 확인이 불가능하거나, ping 명령이 없어서 연결 여부 확인이 안된다거나 하는 경우가 있을 수 있다. 또 docker 명령을 이용해 컨테이너에 진입하여 작업을 하는 것이 이래저래 불편해서 ssh의 아쉬움을 느끼는 경우도 왕왕 있을 것이다.

이 포스트에서는 특정한 도커 이미지에 운영에 편의를 위한 패키지들을 설치하고 SSH 서버도 추가하여 이미지를 새로 만들 것이다.

dockerfile 예제

실제 docker image build를 위한 예제이다.
세부 내용은 주석을 참고하면된다.

# base가 될 이미지 
# bitnami/jupyter-base-notebook
# 최신의 이미지에 append 하는 형태로 빌드하고자 한다면 latest tag 를 붙여준다.
# FROM haedongg.net/library/jupyter/pyspark-notebook:latest
FROM haedongg.net/library/jupyter/pyspark-notebook:spark-3.2.0

# 레이블. 없어도 된다. 
LABEL comment "essential tools & configuration"

# 아래 작업들을 수행할 계정 
# USER ACCOUNT 다음 라인은 별도로 USER를 변경하는 작업이 없다면 모두 ACCOUNT 계정으로 작업이 수행된다.
USER root

ENV DEBIAN_FRONTEND=noninteractive \
    TZ=Asia/Seoul


# 편의를 위해 기본적으로 필요한 것들
# 프록시를 사용하는 경우 apt-get 앞에 다음을 추가한다.
# RUN sudo https_proxy=PROXY_SERVER_HOST:PORT http_proxy=PROXY_SERVER_HOST:PORT
RUN apt-get update
RUN apt-get install -y openssh-server sudo procps curl vim git openssh-client telnet net-tools tcpdump htop --no-install-recommends 
# 컨테이너 이미지 파일의 크기를 줄이기 위해 apt 수행 중 생성된 임시 파일들을 삭제해준다.
RUN apt-get clean && \
    rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*


#### 여기부터 SSH 
RUN mkdir /var/run/sshd

# root password 변경, $PASSWORD를 변경한다.
RUN echo 'root:$PASSWORD' |  chpasswd

# ssh 설정 변경
# root 계정으로의 로그인을 허용한다. 아래 명령을 추가하지 않으면 root 계정으로 로그인이 불가능하다. 
RUN sed -ri 's/^#?PermitRootLogin\s+.*/PermitRootLogin yes/' /etc/ssh/sshd_config
# 응용 프로그램이 password 파일을 읽어 오는 대신 PAM이 직접 인증을 수행 하도록 하는 PAM 인증을 활성화
RUN sed -ri 's/UsePAM yes/#UsePAM yes/g' /etc/ssh/sshd_config

RUN mkdir /root/.ssh
EXPOSE 22
CMD ["/usr/sbin/sshd", "-D"]
####여기까지 SSH

# sudo 설정
# 사용자 추가, $USER 가 사용자 명, $PASSWORD 가 패스워드이다.
RUN adduser --disabled-password --gecos "" $USER &&\ 
    echo '$USER:$PASSWORD' | chpasswd  &&\ 
    adduser $USER sudo &&\ 
    echo 'USER ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers


# user 변경
USER $USER

# alias를 추가하고 싶은 경우
RUN echo "alias ll='ls -lha'"  >>  /home/$USER/.bashrc


# pip 패키지 인스톨
# 프록시를 사용한다면 RUN https_proxy=http://PROXY_SERVER_HOST:PORT http_proxy=PROXY_SERVER_HOST:PORT
RUN  pip --trusted-host pypi.org --trusted-host files.pythonhosted.org install --no-cache-dir s3fs py4j pytz toml xgboost==1.5.1 lightgbm==3.3.1 scikit-learn==1.0.1 mlflow mlflow-skinny sqlalchemy alembic sqlparse  tqdm boto3

# 설정된 home directory의 권한 부여
RUN chown -R $USER /home/$USER

 

Build

#  docker  image    build     -t       $TARGET_DOCKER_IMAGE_NAME:tag           $dockerfile_location
docker image build -t haedongg.net/library/new_haedong_image:v0.1   .
docker image tag haedongg.net/library/new_haedong_image:v0.1  haedongg.net/library/new_haedong_image:latest

_-t 옵션 및 tag 옵션으로 지정하는 이미지 이름은 build에 사용한 원래 이미지의 이름, 경로와는 상관이 없다. 내가 사용하고자 하는 이름을 지정하면 된다.
단, local 이미지가 아닌 harbor나 docker hub 등의 외부 리포지터리에 업로드 하고자 한다면 정확한 이름을 지정해줘야 push가 가능하다._

Kubernetes #4-3. Kubernetes 클러스터 구축 – worker node join (offline, 폐쇄망)

[Kubernetes #1. 사전작업 (offline, 폐쇄망)]
[Kubernetes #2-2. 사전작업 – 컨테이너 런타임 (offline, 폐쇄망)]
[Kubernetes #2-2. 사전작업 – docker 설정 (offline, 폐쇄망)]
[Kubernetes #3. Kubernetes 바이너리 설치 (offline, 폐쇄망)]
[Kubernetes #4. Kubernetes 클러스터 구축 – image pull (offline, 폐쇄망)]
[Kubernetes #4-2. Kubernetes 클러스터 구축 – 단일 마스터노드 생성 (offline, 폐쇄망)]
[Kubernetes #4-3. Kubernetes 클러스터 구축 – worker node join (offline, 폐쇄망)]

Worker node cluster join

Dockerkubernetes 구성 요소를 설치한 뒤 이전 포스트에서 cluster 초기화 시 생성된 스크립트를 실행하면 worker node로써 설정이 된다.

kubeadm join 192.168.4.78:6443 --token abcdefg.8rpr4mfmetbabcde --discovery-token-ca-cert-hash sha256:3a12345caaef12334567890cd3953d1234c3904ab70a8b949f32d6df12345

[preflight] Running pre-flight checks
[preflight] Reading configuration from the cluster...
[preflight] FYI: You can look at this config file with 'kubectl -n kube-system get cm kubeadm-config -o yaml'
[kubelet-start] Writing kubelet configuration to file "/var/lib/kubelet/config.yaml"
[kubelet-start] Writing kubelet environment file with flags to file "/var/lib/kubelet/kubeadm-flags.env"
[kubelet-start] Starting the kubelet
[kubelet-start] Waiting for the kubelet to perform the TLS Bootstrap...

This node has joined the cluster:
* Certificate signing request was sent to apiserver and a response was received.
* The Kubelet was informed of the new secure connection details.

Run 'kubectl get nodes' on the control-plane to see this node join the cluster.

worker 노드에서도 kubernetes config 관련 명령을 수행해주면 kubectl 명령을 이용할 수 있다.

kubectl get nodes

NAME       STATUS     ROLES                  AGE    VERSION
centos7    NotReady   control-plane,master   104s   v1.23.5
centos71   NotReady   <none>                 36s    v1.23.5

master node 설정 후 확인한 것과 같이 STAUS는 NotReady 상태로 표시된다. 이는 kubernetes 네트워크 관련 배포가 필요한 것으로 이후 관련 인스턴스를 배포하면 Ready 상태로 표시된다.

 

cluster 생성 후 24시간 경과 시

kubeadm init 수행 시 생성된 join 스크립트의 token은 24시간의 만료 시간을 가진다. 즉 24시간이 지나면 생성된 join script, 정확히 말하자면 token이 만료되어 사용이 불가능하다.

따라서 24시간 이후에는 갱신된 token을 확인하고 새 token을 이용한 join 스크립트를 사용해야 한다.

  • token 확인: TTL(Time To Live) 값을 보면 22h시간 사용가능함을 알 수 있다.
kubeadm token list
TOKEN                     TTL         EXPIRES                USAGES                   DESCRIPTION                                                EXTRA GROUPS
abcdef.2zha2oqwp12abcd1   22h         2022-04-08T00:46:45Z   authentication,signing   The default bootstrap token generated by 'kubeadm init'.   system:bootstrappers:kubeadm:default-node-token
  • 해당 token에 해당하는 sha256 값 확인
openssl x509 -pubkey -in /etc/kubernetes/pki/ca.crt | openssl rsa -pubin -outform der 2>/dev/null | openssl dgst -sha256 -hex | sed 's/^.* //'
871c73bfcb0d0421f029faa7ba07201abf4f00abcdefghijklmnopqrstuvwxyz
  • 확인된 token, 과 sha256 값을 이용하여 join 명령을 수행한다.
kubeadm join 192.168.4.78:6443 --token abcdef.2zha2oqwp12abcd1 --discovery-token-ca-cert-hash sha256:871c73bfcb0d0421f029faa7ba07201abf4f00abcdefghijklmnopqrstuvwxyz