애플리케이션을 컨테이너화 시키기 위해서는 docker 를 쓰는 시나리오에는 일반적으로 docker가 설치된 환경에서 dockerfile 을 만들어 그것을 이미지로 빌드하는 것이 보통이다.
구글에서 나온 jib 와 maven 플러그인, 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 등의 시스템과 연동을 해서 어떻게 쓰는 게 최적일지 조금 더 고민을 해 봐야겠다.
'dev > Java&Spring' 카테고리의 다른 글
Java Authentication and Authorization Service (JAAS) - 요약 (0) | 2019.08.18 |
---|---|
Spring Cloud Gateway 2.1.0RELEASE 레퍼런스 (2) | 2019.05.12 |
netflix hystrix-dashboard 가 뜨지 않을 때 (0) | 2019.05.08 |
Spring에서 Client Authentication (two-way TLS/SSL) 구현하기 (1) | 2019.04.07 |
Spring에서 insecure SSL 요청(RestTemplate, WebClient) (1) | 2019.04.06 |