yu nkt’s blog

nkty blog

I'm an enterprise software and system architecture. This site dedicates sharing knowledge and know-how about system architecture with me and readers.

jibでDockerfileを作らずにコンテナをビルドする

f:id:yunkt:20180826150614p:plain

背景

Dockerのようなコンテナは、"プロセスの仮想化"であって、VMの代わりではありません。インフラの管理や、リソースの管理という次元の話ではないのです。 "常駐プロセス"を仮想的に動かすもの、という制限すらありません。

だからこそ、コンテナは、DevOpsの文脈において、コードを書くアプリ開発者に大きく関わる事なのです。 アプリ開発者は作ったコードをコンテナに入れて、すぐに使えるようにしておくのです。 成果物はそのコンテナ自身も含まれ、毎週の打ち合わせでは、すぐに動く成果物でレビューが出来るのです。

課題

ただ、動く成果物を用意すると言っても、Javaデベロッパーにとってどうでもいい(あまり時間を使いたくない)話がつきまといます。

  • Docker daemonの管理
  • Dockerfileの作成・管理
  • イメージサイズを減らすための検討をする
  • 適切なベースイメージを選択する
  • .dockerignoreファイルを作成する
  • Mavenプラグインでコンテナビルドするように修正する

今のDockerでは、Javaデベロッパーがこれらをやらなければなりません。 とても面倒です。 Javaデベロッパーは、このようなことは気にせず、極上のアプリを作る事に専念すべきです。

Jib

そこでGoogleがJibというものを開発しました。 Jibが何か、内部でどんな動きがされているかは、Googleのブログを見た方が正確でしょう。

cloudplatform.googleblog.com

より詳しく背景や内容を見たい場合は、発表資料が良いと思います。

speakerdeck.com

ここでは、具体的に使い方を見ていきましょう。

準備

まず、以下のものはインストールします。

Gradleを利用した日本語での解説ページが見つからなかったので、Gradleでやってみましょう。

あと、これから以降で入力するコマンドは、全てsuper user状態で入力しています。 その理由は、Dockerコンテナをビルドするときに、Docker daemonを利用する必要があり、そこでsuper user権限が必要なためです。

使い方

ローカルにDockerイメージを作成して保存してみましょう。

まず、サンプル用に、Spring bootのひな形プロジェクトを持ってきます。 Spring Initializrを利用しましょう。 Spring Initializrの説明は、GitHubのREADMEに詳しく記載されています。

# curl https://start.spring.io/starter.tgz -d dependencies=web,actuator \
-d language=java -d type=gradle-project -d baseDir=jib-sample-app | tar -xzvf -
# cd jib-sample-app
# vim build.gradle

恐らく、build.gradleには、このように書かれています。

buildscript {
        ext {
                springBootVersion = '2.0.4.RELEASE'
        }
        repositories {
                mavenCentral()
        }
        dependencies {
                classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
        }
}

apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management'


group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = 1.8

repositories {
        mavenCentral()
}


dependencies {
        compile('org.springframework.boot:spring-boot-starter-actuator')
        compile('org.springframework.boot:spring-boot-starter-web')
        testCompile('org.springframework.boot:spring-boot-starter-test')

これに、binary pluginを追加します。

buildscript {
        ext {
                springBootVersion = '2.0.4.RELEASE'
        }
        repositories {
                mavenCentral()
        }
        dependencies {
                classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
        }
}

plugins {
  id 'com.google.cloud.tools.jib' version '0.9.0'
}

apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management'


group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = 1.8

repositories {
        mavenCentral()
}


dependencies {
        compile('org.springframework.boot:spring-boot-starter-actuator')
        compile('org.springframework.boot:spring-boot-starter-web')
        testCompile('org.springframework.boot:spring-boot-starter-test')

あとは、gradleコマンドで、追加したプラグインを利用して、Dockerイメージをビルドしましょう。

# gradle jibDockerBuild

ビルドが成功したら、docker imagesで、Docker daemonに保存されたDockerイメージを参照してみましょう。

REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
demo                0.0.1-SNAPSHOT      c88b80900e92        292 years ago       136MB

保存されていました。REPOSITORYは、demoになっています。 demoというワードがbuild.gradleには存在していなかったので、どこから引っ張られたのか調べてみました。

# grep -r demo .
./settings.gradle:rootProject.name = 'demo'
./src/main/java/com/example/demo/DemoApplication.java:package com.example.demo;
./src/test/java/com/example/demo/DemoApplicationTests.java:package com.example.demo;
Binary file ./.gradle/4.9/taskHistory/taskHistory.bin matches
Binary file ./build/classes/java/main/com/example/demo/DemoApplication.class matches

settings.gradle:rootProject.nameで指定されたディレクトリ名が、REPOSITORY名になります。

TAGは簡単で、build.gradleに記述されたversionの値です。

CREATEDが、謎ですね笑

作成したコンテナの利用

Googleのブログなどにも、上までの使い方しか書かれていませんが、作ったアプリは使ってなんぼです。

# docker run -p 8080:8080 --name jib_demo demo:0.0.1-SNAPSHOT

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v2.0.4.RELEASE)

2018-08-26 05:43:06.634  INFO 1 --- [           main] com.example.demo.DemoApplication         : Starting DemoApplication on dd72c49cdd03 with PID 1 (/app/classes started by root in /)
2018-08-26 05:43:06.639  INFO 1 --- [           main] com.example.demo.DemoApplication         : No active profile set, falling back to default profiles: default
2018-08-26 05:43:06.809  INFO 1 --- [           main] ConfigServletWebServerApplicationContext : Refreshing org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext@568bf312: startup date [Sun Aug 26 05:43:06 GMT 2018]; root of context hierarchy
2018-08-26 05:43:10.362  INFO 1 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port(s): 8080 (http)
2018-08-26 05:43:10.429  INFO 1 --- [           main] o.apache.catalina.core.StandardService   : Starting service [Tomcat]
2018-08-26 05:43:10.434  INFO 1 --- [           main] org.apache.catalina.core.StandardEngine  : Starting Servlet Engine: Apache Tomcat/8.5.32
2018-08-26 05:43:10.468  INFO 1 --- [ost-startStop-1] o.a.catalina.core.AprLifecycleListener   : The APR based Apache Tomcat Native library which allows optimal performance in production environments was not found on the java.library.path: [/usr/java/packages/lib/amd64:/usr/lib/x86_64-linux-gnu/jni:/lib/x86_64-linux-gnu:/usr/lib/x86_64-linux-gnu:/usr/lib/jni:/lib:/usr/lib]
2018-08-26 05:43:10.698  INFO 1 --- [ost-startStop-1] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
2018-08-26 05:43:10.699  INFO 1 --- [ost-startStop-1] o.s.web.context.ContextLoader            : Root WebApplicationContext: initialization completed in 3898 ms
2018-08-26 05:43:11.873  INFO 1 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean   : Mapping filter: 'characterEncodingFilter' to: [/*]
2018-08-26 05:43:11.878  INFO 1 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean   : Mapping filter: 'webMvcMetricsFilter' to: [/*]
2018-08-26 05:43:11.878  INFO 1 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean   : Mapping filter: 'hiddenHttpMethodFilter' to: [/*]
2018-08-26 05:43:11.878  INFO 1 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean   : Mapping filter: 'httpPutFormContentFilter' to: [/*]
2018-08-26 05:43:11.878  INFO 1 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean   : Mapping filter: 'requestContextFilter' to: [/*]
2018-08-26 05:43:11.878  INFO 1 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean   : Mapping filter: 'httpTraceFilter' to: [/*]
2018-08-26 05:43:11.878  INFO 1 --- [ost-startStop-1] o.s.b.w.servlet.ServletRegistrationBean  : Servlet dispatcherServlet mapped to [/]
2018-08-26 05:43:12.108  INFO 1 --- [           main] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/**/favicon.ico] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2018-08-26 05:43:12.440  INFO 1 --- [           main] s.w.s.m.m.a.RequestMappingHandlerAdapter : Looking for @ControllerAdvice: org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext@568bf312: startup date [Sun Aug 26 05:43:06 GMT 2018]; root of context hierarchy
2018-08-26 05:43:12.595  INFO 1 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error],produces=[text/html]}" onto public org.springframework.web.servlet.ModelAndView org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController.errorHtml(javax.servlet.http.HttpServletRequest,javax.servlet.http.HttpServletResponse)
2018-08-26 05:43:12.597  INFO 1 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error]}" onto public org.springframework.http.ResponseEntity<java.util.Map<java.lang.String, java.lang.Object>> org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController.error(javax.servlet.http.HttpServletRequest)
2018-08-26 05:43:12.665  INFO 1 --- [           main] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/webjars/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2018-08-26 05:43:12.666  INFO 1 --- [           main] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2018-08-26 05:43:13.176  INFO 1 --- [           main] o.s.b.a.e.web.EndpointLinksResolver      : Exposing 2 endpoint(s) beneath base path '/actuator'
2018-08-26 05:43:13.192  INFO 1 --- [           main] s.b.a.e.w.s.WebMvcEndpointHandlerMapping : Mapped "{[/actuator/health],methods=[GET],produces=[application/vnd.spring-boot.actuator.v2+json || application/json]}" onto public java.lang.Object org.springframework.boot.actuate.endpoint.web.servlet.AbstractWebMvcEndpointHandlerMapping$OperationHandler.handle(javax.servlet.http.HttpServletRequest,java.util.Map<java.lang.String, java.lang.String>)
2018-08-26 05:43:13.198  INFO 1 --- [           main] s.b.a.e.w.s.WebMvcEndpointHandlerMapping : Mapped "{[/actuator/info],methods=[GET],produces=[application/vnd.spring-boot.actuator.v2+json || application/json]}" onto public java.lang.Object org.springframework.boot.actuate.endpoint.web.servlet.AbstractWebMvcEndpointHandlerMapping$OperationHandler.handle(javax.servlet.http.HttpServletRequest,java.util.Map<java.lang.String, java.lang.String>)
2018-08-26 05:43:13.199  INFO 1 --- [           main] s.b.a.e.w.s.WebMvcEndpointHandlerMapping : Mapped "{[/actuator],methods=[GET],produces=[application/vnd.spring-boot.actuator.v2+json || application/json]}" onto protected java.util.Map<java.lang.String, java.util.Map<java.lang.String, org.springframework.boot.actuate.endpoint.web.Link>> org.springframework.boot.actuate.endpoint.web.servlet.WebMvcEndpointHandlerMapping.links(javax.servlet.http.HttpServletRequest,javax.servlet.http.HttpServletResponse)
2018-08-26 05:43:13.312  INFO 1 --- [           main] o.s.j.e.a.AnnotationMBeanExporter        : Registering beans for JMX exposure on startup
2018-08-26 05:43:13.420  INFO 1 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8080 (http) with context path ''
2018-08-26 05:43:13.429  INFO 1 --- [           main] com.example.demo.DemoApplication         : Started DemoApplication in 7.652 seconds (JVM running for 8.559)
2018-08-26 05:44:36.546  INFO 1 --- [nio-8080-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring FrameworkServlet 'dispatcherServlet'
2018-08-26 05:44:36.546  INFO 1 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : FrameworkServlet 'dispatcherServlet': initialization started
2018-08-26 05:44:36.578  INFO 1 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : FrameworkServlet 'dispatcherServlet': initialization completed in 31 ms

では、ブラウザから、http://(ホスト名):8080にアクセスしてみましょう。

f:id:yunkt:20180826144945p:plain

恐らく、Spring Bootのエラーページが出力されているものと思われます。

自動的に、Dockerfileに記載すべきENTRYPOINTの設定まで行われているのでしょう。