dev/Java&Spring

jib로 springboot 애플리케이션 컨테이너화 + registry 등록

lugi 2019. 9. 30. 02:14

애플리케이션을 컨테이너화 시키기 위해서는 docker 를 쓰는 시나리오에는 일반적으로 docker가 설치된 환경에서 dockerfile 을 만들어 그것을 이미지로 빌드하는 것이 보통이다.

 

구글에서 나온 jibmaven 플러그인, gradle 플러그인을 활용하면 애플리케이션의 빌드 과정에서 자동으로 컨테이너 이미지를 만들고, 그것을 registry 에 등록할 수 있다.

 

jib란 docker daemon 없이도 Java 애플리케이션을 Docker 혹은 OCI 규격의 컨테이너 이미지를 만들어 주는 도구이며, maven 플러그인 및 gradle 으로 제공되어 Dockerfile 에 대한 별도의 지식 없이도 애플리케이션을 이미지로 만들 수 있다. 또한 JAR를 single layer 로 빌드하는 것이 아니라, Application을 종속성, 리소스, 클래스 등으로 좀 더 세분화 한 Layer로 빌드하여 코드 변경시 증분만을 변경할 수 있다고 하니, Java 애플리케이션을 컨테이너화 하는데 매우 적절한 전략이라고 볼 수 있다.

 

이 포스트에서는 간략하게

1. private registry 를 만들고

2. 기존의 spring boot 애플리케이션을 jib로 컨테이너화해서

3. 구축한 private registry에 push하기

4. dockerhub에 push하기

를 해 보려고 한다.

 

private registry에 별로 연관이 없으신 분은 1과 관련된 부분은 생략하고 아래부터 보셔도 될 것 같다.

 

왜 내 블로그는 좋고 잘 갖춰진 public 환경을 놔두고 맨날 직접 설치 아니면 환경 구축이 꼭 끼는가... 에 대해서 말씀드려보자면, 일하는 환경이 public에 무언가를 업로드 할 수 있는 정책이 아니고, 망 분리 환경도 가지고 있어서 환경 구축의 비중이 높아서 그렇다. 나도 좀 더 편하게 일하고 싶은 작은 소망이 있다...

 

일단 docker와 docker-compose는 사용자의 환경에 구성되어 있다고 가정한다.

(테스트 환경은 CENTOS 7.4 + Docker CE 19.03 이다)

 

registry image 를 받아서 구동하면 간단하게 private registry를 구동할 수 있다. 다만 운영 환경에서 registry는 HTTPS 환경에서 구동할 것을 강하게 권장하고 있고, HTTPS가 아닐 경우에 다른 연계 과정에서 별도의 옵션을 요구하거나 경고를 보는 일이 잦기 때문에 private registry를 구성하고 시작할 것이다.

 

이 단계의 순서는 다음과 같다.

1. self-signed 인증서 만들기

2. nginx 를 docker 로 구성하고 1)에서 만든 인증서를 통해 HTTPS proxy를 구동하기

3. private registry 를 구성하기

 

private registry를 생성하고 nginx를 proxy로 사용하는 방법은 이 링크이 링크에서 소개하는 방법을 기반으로 한다.

Self-signed 인증서 만들기

HTTPS 기반으로 서버를 구동하기 위해서는 인증서와 KEY가 있어야 한다. 자기 자신이 서명한 Self-signed 인증서를 만들 것이다. Self-signed 인증서를 만드는 방법은 OPENSSL 로 할 수도 있고 JAVA 의 경우 KEYTOOLS 를 이용할 수도 있다. 나는 JAVA가 편하니까 KEYTOOLS 로 해 보려고 한다.

# user home에 auth 디렉토리 생성 후 이동
mkdir ~/auth
cd ~/auth

# JKS keystore 생성
keytool -genkey -alias gnu-server-key -keyalg RSA -keypass server -storepass server -keystore server.jks -validity 365 -keysize 2048 -dname "CN=localhost,OU=gnu,O=gnu,L=gnu,S=gnu,C=KR"

# PKCS#12 로 변환
keytool -importkeystore -srckeystore server.jks -srcstorepass server -destkeystore server.p12 -deststoretype pkcs12 -deststorepass server

# PKCS#12 파일을 X.509 형식으로 변환
openssl pkcs12 -in server.p12 -out server.pem -passin pass:server -passout pass:server

# key 추출
openssl rsa -in server.pem -out server_nopass.key -passin pass:server

# 인증서 추출
openssl x509 -in server.pem >> server_nopass.crt

~/auth 경로에 수행 결과로 생기는 파일은 아래와 같다

-rw-r--r--. 1 root root 2213 Sep 29 11:53 server.jks
-rw-r--r--. 1 root root 1212 Sep 29 11:54 server_nopass.crt
-rw-r--r--. 1 root root 1675 Sep 29 11:54 server_nopass.key
-rw-r--r--. 1 root root 2583 Sep 29 11:54 server.p12
-rw-r--r--. 1 root root 3421 Sep 29 11:54 server.pem

이 중에서 server_nopass.crt 와 server_nopass.key 를 nginx 의 인증서 및 key로 사용할 것이다.

 

private registry를 위한 nginx 파일 구성

private registry 는 외부에서 접속하기 위해 HTTPS 를 통한 접속을 요구한다. 그래서 docker 컨테이너에 nginx 를 띄우고 그것을 nginx proxy로 쓸 것이다. 또한 private registry 에 접속하기 위해서 username 과 password basic auth를 설정할 것이다.

 

일단 nginx 에서 사용할 basic auth의 username / password 데이터를 ~/auth 에 생성한다.

username 은 user, password는 password로 설정하였다.

# private registry 에서 사용할 basic auth ID/PW 생성
docker run --rm --entrypoint htpasswd registry -Bbn user password > ~/auth/nginx.htpasswd

수행 결과로 못 보던 파일이 한 개 추가된 것을 알 수 있다.

-rw-r--r--. 1 root root   67 Sep 29 12:00 nginx.htpasswd
-rw-r--r--. 1 root root 2213 Sep 29 11:57 server.jks
-rw-r--r--. 1 root root 1212 Sep 29 11:57 server_nopass.crt
-rw-r--r--. 1 root root 1675 Sep 29 11:57 server_nopass.key
-rw-r--r--. 1 root root 2583 Sep 29 11:57 server.p12
-rw-r--r--. 1 root root 3421 Sep 29 11:57 server.pem

이제 nginx의 환경 구성 파일인 nginx.conf 를 설정할 것이다. 이 설정은 nginx로 들어오는 요청을 registry로 proxy pass 해주는 역할을 한다.

# nginx 설정
cat >> nginx.conf <<'EOF'
events {
    worker_connections  1024;
}

http {

  upstream docker-registry {
    server registry:5000;
  }

  ## Set a variable to help us decide if we need to add the
  ## 'Docker-Distribution-Api-Version' header.
  ## The registry always sets this header.
  ## In the case of nginx performing auth, the header is unset
  ## since nginx is auth-ing before proxying.
  map $upstream_http_docker_distribution_api_version $docker_distribution_api_version {
    '' 'registry/2.0';
  }

  server {
    listen 443 ssl;
    server_name localhost;

    # SSL
    ssl_certificate /etc/nginx/conf.d/server_nopass.crt;
    ssl_certificate_key /etc/nginx/conf.d/server_nopass.key;

    # Recommendations from https://raymii.org/s/tutorials/Strong_SSL_Security_On_nginx.html
    ssl_protocols TLSv1.1 TLSv1.2;
    ssl_ciphers 'EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH';
    ssl_prefer_server_ciphers on;
    ssl_session_cache shared:SSL:10m;

    # disable any limits to avoid HTTP 413 for large image uploads
    client_max_body_size 0;

    # required to avoid HTTP 411: see Issue #1486 (https://github.com/moby/moby/issues/1486)
    chunked_transfer_encoding on;

    location /v2/ {
      # Do not allow connections from docker 1.5 and earlier
      # docker pre-1.6.0 did not properly set the user agent on ping, catch "Go *" user agents
      if ($http_user_agent ~ "^(docker\/1\.(3|4|5(?!\.[0-9]-dev))|Go ).*$" ) {
        return 404;
      }

      # To add basic authentication to v2 use auth_basic setting.
      auth_basic "Registry realm";
      auth_basic_user_file /etc/nginx/conf.d/nginx.htpasswd;

      ## If $docker_distribution_api_version is empty, the header is not added.
      ## See the map directive above where this variable is defined.
      add_header 'Docker-Distribution-Api-Version' $docker_distribution_api_version always;

      proxy_pass                          http://docker-registry;
      proxy_set_header  Host              $http_host;   # required for docker client's sake
      proxy_set_header  X-Real-IP         $remote_addr; # pass on real client's IP
      proxy_set_header  X-Forwarded-For   $proxy_add_x_forwarded_for;
      proxy_set_header  X-Forwarded-Proto $scheme;
      proxy_read_timeout                  900;
    }
  }
}
EOF

위의 내용을 긁어서 실행시키면 역시나 nginx.conf가 추가 되었다. 이때 주의해야 하는 것은, 바로 위의 쉘스크립트의 상단에 보면 cat >> nginx.conf <<'EOF'  라는 라인이 있는데 이 때 '' 으로 감싼 'EOF'가 아닌 그냥 EOF를 쓰면 아래에 나올 $들을 변수로 인식해서 시스템에 설정된 환경 변수를 가지고 오려고 시도하다 공백이 들어간다는 점이다. 'EOF' 로 꼭 감싸주자.

 

server_name 은 localhost 를 사용하고 있고, #SSL 라인의 아래에 두 라인을 보면 생성한 crt 파일과 key 파일을 사용하고 있다. 나머지는 docker 공홈의 예제와 크게 다르지 않다.

 

수행 결과 아래와 같이 nginx.conf 파일이 추가 되었다.

-rw-r--r--. 1 root root 2285 Sep 29 12:03 nginx.conf
-rw-r--r--. 1 root root   67 Sep 29 12:00 nginx.htpasswd
-rw-r--r--. 1 root root 2213 Sep 29 11:57 server.jks
-rw-r--r--. 1 root root 1212 Sep 29 11:57 server_nopass.crt
-rw-r--r--. 1 root root 1675 Sep 29 11:57 server_nopass.key
-rw-r--r--. 1 root root 2583 Sep 29 11:57 server.p12
-rw-r--r--. 1 root root 3421 Sep 29 11:57 server.pem

nginx 와 private registry 구동

nginx proxy가 완성되었으니 private registry와 nginx를 구동시켜보자. 이는 docker-compose 로 해보려 한다.

 

아래는 docker-compose.yml 이다.

nginx:
  image: "nginx:alpine"
  ports:
    - 5043:443
  links:
    - registry:registry
  volumes:
    - ~/auth:/etc/nginx/conf.d
    - ~/auth/nginx.conf:/etc/nginx/nginx.conf:ro

registry:
  image: registry
  volumes:
    - ~/data:/var/lib/registry

nginx 컨테이너를 구동하고 외부의 5043 포트를 컨테이너 내부의 443(HTTPS)포트로 포워딩시키며  nginx container 내부에서 registry 컨테이너를 registry 라는 호스트명으로 연결 시켰다. 그리고 호스트에 설정된 ~/auth 디렉토리를 컨테이너의 /etc/nginx/conf.d 디렉토리로 사용하고, ~/auth/nginx.conf 를 /etc/nginx/nginx.conf 로 사용할 것이다.

그리고 private registry는 ~/data 를 컨테이너 내부의 /var/lib/registry 에서 push 된 이미지의 저장소로 사용할 것이다. 이 디렉토리도 만들어주자.

# 저장소로 사용할 디렉토리 생성
mkdir ~/data

# docker-compose 실행
docker-comppose up -d

# 실행결과
docker ps
CONTAINER ID        IMAGE                 COMMAND                  CREATED             STATUS              PORTS                               NAMES
be12c6fdb25f        nginx:alpine          "nginx -g 'daemon of…"   24 seconds ago      Up 22 seconds       80/tcp, 0.0.0.0:5043->443/tcp       private-registory_nginx_1
42df526e8f2f        registry              "/entrypoint.sh /etc…"   25 seconds ago      Up 23 seconds       5000/tcp                            private-registory_registry_1

docker-compose 의 수행결과 nginx 와 registry 컨테이너가 구동되었고, 외부의 5043 포트는 내부의 443포트로 포워딩되고 있다. private registry 구성이 끝났다.

 

기존에 존재하는 Spring boot 애플리케이션의 컨테이너화 및 push

기존에 해 오던대로라면 이 애플리케이션을 컨테이너화 하기 위해 base image 를 만들거나 결정하고, dockerfile을 작성한 후 docker가 설치된 환경에서 빌드를 하거나 하는 과정을 거쳐야 했다. jib 플러그인을 사용하면 그럴 필요 없이, pom,xml 에 간단한 몇 가지 구성을 해주는 것만으로 해결이 된다.

 

샘플로 사용할 프로젝트는 글쓴이의 github에 기존에 존재하였던 deploy-test를 사용하였다.

이 프로젝트의 pom.xml 파일에 아래와 같이 jib maven plugin을 포함시켰다.

이 프로젝트는 9909 포트로 웹 요청을 받아들이는 간단한 애플리케이션이다.

<build>
  <plugins>
    <plugin>
      <groupId>com.google.cloud.tools</groupId>
      <artifactId>jib-maven-plugin</artifactId>
      <version>1.6.1</version>
      <configuration>
        <to>
        	<image>192.168.0.51:5043/gnu/deploy-test</image>
        </to>
      	<container>
          <ports>
              <port>9909</port>
          </ports>
      	</container>
      </configuration>
    </plugin>
  <!-- 이하 생략 -->
  <plugins>
<build>

위의 환경 설정은 base image를 설정하지 않았기 때문이 이 이미지를 base로 사용한다. 해당 이미지는 java를 구동하기 위한 매우 가벼운 이미지이다. 만약 별도의 base image 설정이 필요하다면 이 내용을 참고로 하여 원격 registry 혹은 현재 docker daemon에 포함된 이미지 혹은 tar 파일을 base로 사용하도록 설정할 수 있다.

 

<to></to> 섹션에서 image 가 빌드 후 push될 대상을 지정하였으며, 이 부분이 위에서 구성한 private registry 이다. 다만 private registry 를 대상으로 사용하기 위해서 별도의 설정이 필요한데, 이는 아래에서 설명하겠다.

dockerfile 에 개별적으로 설정하는 각종 환경변수, 포트, 볼륨등의 설정과 관련된 것들은 <container></container> 섹션에서 지정할 수 있다. 이 부분에 대해서는 여기를 참조한다.

 

jib가 제공하는 phase와 goal은 아래의 3가지이다.

  • jib:build : image 를 빌드한다, <to> 의 경로로 push한다.
  • jib:dockerBuild : image를 빌드한다, docker daemon에 image를 등록한다. docker가 실행환경에 있어야 한다.
  • jib:buildTar : image를 빌드한다. tar 파일로 만든다. docker CLI에서 load하여 사용할 수 있다.

글쓴이의 윈도우 개발 환경에서 일단 docker 에서 사용할 수 있는 tarBall image를 만들어보겠다. 물론 로컬 환경에 docker는 깔려있지 않다.

 

위와 같이 maven 실행 환경을 clean compile jib:buildTar 로 주고 실행하였다.

빌드가 수행되고 나니

jib-image.tar 파일이 생성되었다. 이 파일을 docker가 설치된 환경으로 옮겨 docker load --input jib-image.tar 로 사용할 수 있다. 망 분리 환경이며, 별도의 내부 registry 가 구성되지 않은 환경에서는 이 파일을 쓸 수 있을 것이다.

 

이번에는 빌드한 이미지를 원격 registry 에 push 해 보겠다. 별도의 docker가 없어도 개발 환경에서 바로 image를 원격 registry 에 push 할 수 있다.

 

maven 커맨드를 clean compile jib:build로 주었다. 이렇게 하면 <to> 섹션에 설정한 registry 로 빌드 작업 후 push를 시도한다. 다만 registry 에는 username/password로 인증이 걸려 있으며, 사설 레지스트리이다. 그렇기 때문에 빌드 시에 별도의 VM args를 옵션으로 줘야한다.

 

화면에서는 약간 잘렸는데, VM Options에

-Djib.to.auth.username=user

-Djib.to.auth.password=password

-Djib.allowInsecureRegistries=true

3가지 옵션을 주었다.

username과 password는 상단의 nginx proxy 설정시 basic auth 로 주었던 username과 password이다.

allowInsecureRegistries는 사설 인증서 기반 혹은 HTTP 기반으로 운영되는 registry 와의 통신을 허가하는 옵션이다.

 

해당 옵션들은 Authentication methods 섹션의 설명을 참조하여 다른 Credential Helper를 쓸 수도 있고, plugin 설정에 명시적으로 입력할 수도 있다. 다만 인증과 관련된 정보가 소스 코드에 직접 반영되는 것은 바람직하지 않은 것이라고 생각하여 별도의 옵션으로 분리 시켰다.

allowInsecureRegistries 도 Extended Usage를 참고하여 plugin 설정에 직접 명시할 수 있다. 다만 이 부분도, 공인 registry와 테스트용 내부 registry를 분리하거나 하며 쓸 경우에 별도의 프로파일로 분리하여도 되지만, VM Options 로 분리하는 것이 편리할 것 같아 별도로 명시하였다.

 

이 goal을 수행한 결과 

# build 전
curl https://user:password@localhost:5043/v2/_catalog -k
{"repositories":[]}

# build 후
curl https://user:password@localhost:5043/v2/_catalog -k
{"repositories":["gnu/deploy-test"]}

private registry 에 gnu/deploy-test 이미지가 등록되었다.

 

이번에는 docker 가 구동 중인 Linux 환경에서 jib:dockerBuild 로 빌드해보겠다.

mvn clean compile jib:dockerBuild

[INFO] Scanning for projects...
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] Building deploy-test 0.0.1-SNAPSHOT
[INFO] ------------------------------------------------------------------------
[INFO]
[INFO] --- maven-clean-plugin:3.1.0:clean (default-clean) @ deploy-test ---
[INFO] Deleting /root/deploy-test-project/target
[INFO]
[INFO] --- maven-resources-plugin:3.1.0:resources (default-resources) @ deploy-test ---
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] Copying 1 resource
[INFO] Copying 0 resource
[INFO]
[INFO] --- maven-compiler-plugin:3.8.0:compile (default-compile) @ deploy-test ---
[INFO] Changes detected - recompiling the module!
[INFO] Compiling 1 source file to /root/deploy-test-project/target/classes
[INFO]
[INFO] --- jib-maven-plugin:1.6.1:dockerBuild (default-cli) @ deploy-test ---
[INFO]
[INFO] Containerizing application to Docker daemon as 192.168.0.51:5043/gnu/deploy-test...
[INFO]
[INFO] Container entrypoint set to [java, -cp, /app/resources:/app/classes:/app/libs/*, com.gnu.deploy.DeployTestApplication]
[INFO]
[INFO] Built image to Docker daemon as 192.168.0.51:5043/gnu/deploy-test
[INFO] Executing tasks:
[INFO] [==============================] 100.0% complete
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 29.882s
[INFO] Finished at: Sun Sep 29 16:38:08 UTC 2019
[INFO] Final Memory: 28M/71M
[INFO] ------------------------------------------------------------------------

 

해당 작업은 docker 가 설치된 local에서 일어나기 때문에 별도의 Auth 정보 입력이 필요 없다.

docker images

REPOSITORY                          TAG                 IMAGE ID            CREATED             SIZE
jenkinsci/blueocean                 latest              4dcbbe4ee9fa        2 days ago          553MB
nginx                               alpine              4d3c246dfef2        4 days ago          21.2MB
registry                            latest              f32a97de94e1        6 months ago        25.8MB
192.168.0.51:5043/gnu/deploy-test   latest              73942b8bfde9        49 years ago        145MB

docker images 수행 결과 192.168.0.51:5043/gnu/deploy-test 이미지가 생성되었다. 생성날짜가 49년 전인 것은 별도의 설정이 없을 경우 image의 Creation time을 Unix timestamp 0 (=1970.1.1. 00:00:00) 으로 설정하기 때문이다. 이 시간을 변경하고 싶다면 여기를 참조하여 변경하자.

 

그럼 이제 jib로 빌드된 docker image 를 docker run -d -p9909:9909 192.168.0.51:5043/gnu/deploy-test 명령어로 한 번 구동시킨 뒤에 접속을 해 보자 /echo/ 뒤에 오는 PathVariable 을 그대로 출력하게 되어 있는 애플리케이션이다.

 

잘 뜬다.

 

Dockerhub에 push하기

docker hub로 push를 하는 방법은 <to></to> 섹션에서 <image> 의 값을

<plugin>
  <groupId>com.google.cloud.tools</groupId>
  <artifactId>jib-maven-plugin</artifactId>
  <version>1.6.1</version>
  <configuration>
    <to>
    	<image>registry.hub.docker.com/webfuel/deploy-test</image>
    </to>
    <container>
      <ports>
          <port>9909</port>
      </ports>
    </container>
  </configuration>
</plugin>

registry.hub.docker.com/[Username]/[Image-name] 으로 주면 된다. 사실 굳이 이것만 해도 쓰는데 지장은 없을 것 같은데 굳이 private registry 까지 적은 이유는 왜인지 모르겠지만 docker hub으로의 push가 너무너무너무 느리기 때문이다. 내 컴퓨터가 문제인지 docker hub쪽이 문제인지는 좀 살펴보아야 할 것 같다.

또한 dockerhub의 주소를 jib 공식 문서에는 docker.io 로 주면 된다고 나와 있는데, 몇 번 시도를 해 보니 registry.hub.docker.com으로 주소를 줬을 때는 가끔 push가 성공했고, docker.io 로 줬을 경우에 401 Error가 나서 권한 문제인가? 싶었는데, 아무래도 registry.hub.docker.com 에서도 401 Error 가 가끔 나는 것을 보면 시간이 너무 오래 걸리면 연결이 끊기면서 발생하는 에러인 것 같다. Timeout을 늘려보면 되지 않을까? 싶기도 하지만, 이만큼 오래 걸리는 건 별로 정상적인 상황이 아닌 것 같아서 일단 나중에 살펴보려고 하고 있다. Credential Helper 를 통한 방법을 쓰면 좀 나을지 그것도 나중에 살펴보려고 한다.

 

결론은 Dockerfile 에 대한 작성이나 별다른 작업이 없어도 개발 환경에서 구성해둔 것만 가지고도 Docker image를 build 할 수 있다는 것은 나름 장점인 것 같다. jib:build 만 돌려서 해당 환경에서 바로 kubectl 로 push 도 가능할 것 같은데 그것은 다시 한 번 해 봐야할 것 같다. 전체적으로 jenkins 등의 시스템과 연동을 해서 어떻게 쓰는 게 최적일지 조금 더 고민을 해 봐야겠다.