暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

Grails 5 With Docker

MicroGrails 2021-12-13
873

Grails 5
已经发布有一阵时间了,作为一个小众却不失大雅的全栈框架,在 springboot
groovy
的加持下,一直以卓越的效率而著称,是一款名副其实的 不打断你思路
的趁手框架。

Docker
应该不用介绍了。

本文产生于一次需求(临时要码个证书管理,请无视),以 Docker
方式部署的,在部署过程中,萌生出 Grails 怎么做 Docker 最帅气
的想法,因此,有了此篇备忘。

文章比较长,建议点击 在看
收藏
分享

第一部分 准备工作


1. 确认环境

采用当前主流的 JDK 11 + Grails 5

[aaron@micrograils.com workspaces]$ grails -version
| Grails Version: 5.0.2
| JVM Version: 11.0.13
[aaron@micrograils.com workspaces]$ 

2. 创建项目

仅为了演示,所以没有任何逻辑(不过这点也正说明 Grails
的骚气:创建完项目就可以运行了)

[aaron@micrograils.com workspaces]$ grails create-app com.micrograils.helloworld
Resolving dependencies......
...
| Application created at /home/aaron/workspaces/helloworld
[aaron@micrograils.com workspaces]$ 

3. 编译打包

[aaron@micrograils.com workspaces]$ cd helloworld/
[aaron@micrograils.com helloworld]$ grails package
...
BUILD SUCCESSFUL in 18s
9 actionable tasks: 1 executed, 8 up-to-date
| Built application to build/libs using environment: production

[aaron@micrograils.com helloworld]$ ls build/libs/
helloworld-0.1-plain.war  helloworld-0.1.war
[aaron@micrograils.com helloworld]$ 

3. 部署验证

无。

因为咱们刚刚做的是一个“空”项目,没啥好验证的,非要验证你是不相信 spring 咋滴?

第二部分 镜像制作

Grails
框架自身的三个特征,

  1. 是个 java
    项目;
  2. 是个 springboot
    项目;
  3. 是个 grails
    项目;

(这不是废话么?)

因此,Grails
项目的 Docker
镜像也有三类制作方式:

  1. java
    的方式;
  2. spirngboot
    的方式;
  3. grails
    的方式;

(这不又是废话么?)(为什么要用“又”?)

1. Java 方式

所谓 java
方式,其实就是指的传统、通用的 Docker
镜像制作办法:写 Dockerfile
build
run
,不涉及项目具体框架与结构。

1.1 编写 Dockerfile

build/
下,新建一个 Dockerfile.j
文件(名字有个 .j
是为了方便后面跟其他方式做区分):

(正式项目建议放在 src/main/docker
下)

FROM openjdk:11
LABEL maintainer="aaron#micrograils.com"
EXPOSE 8080

WORKDIR app
ARG WAR_FILE=libs/*.war
COPY ${WAR_FILE} application.war
ENTRYPOINT ["java""-jar""/app/application.war"]

FROM
不用多说了;

新版 Docker
中,MAINTAINER
已经被标注 deprecated
了,官方推荐用 LABEL maintainer=""
来代替;

其他几个也很好理解,都是常用命令。

1.2 构建镜像

(Grails 打包会生成两个文件, *-plain.war
*.war
,这里为了方便,先删掉了 -plain.war
。当然也可以不删掉,然后在 docker build
时,以参数的形式将 helloworld-0.1.war
传进去,参见前面的 Dockerfile.j
定义)

rm build/libs/helloworld-0.1-plain.war

build/
路径下执行:

sudo docker build -t micrograils/hello:0.1 -f Dockerfile.j .

[aaron@micrograils.com build]$ sudo docker build -t micrograils/hello:0.1 -f Dockerfile.j .
Sending build context to Docker daemon  92.57MB
Step 1/7 : FROM openjdk:11
...
Successfully built f413d0195c9a
Successfully tagged micrograils/hello:0.1
[aaron@micrograils.com build]$ 

确认一下:

[aaron@micrograils.com build]$ sudo docker images
REPOSITORY          TAG                IMAGE ID       CREATED          SIZE
micrograils/hello   0.1                f413d0195c9a   37 seconds ago   743MB
...

1.3 部署验证

sudo docker run -d -p 28080:8080 micrograils/hello:0.1

[aaron@micrograils.com build]$ sudo docker run -d -p 28080:8080 micrograils/hello:0.1
22cece7c6df7757c16cf8021fbd4c33738d005041ce7c61d09e316b894502ebd
[aaron@micrograils.com build]$ 

[aaron@micrograils.com build]$ sudo docker ps
CONTAINER ID   IMAGE                   COMMAND                  CREATED          STATUS          PORTS                                                   NAMES
22cece7c6df7   micrograils/hello:0.1   "java -jar /app/appl…"   34 seconds ago   Up 33 seconds   0.0.0.0:28080->8080/tcp, :::28080->8080/tcp             jovial_bartik
...

打开浏览器,验证是否可以正常访问。

OK,通常情况基于传统方式制作的 Docker 应该没啥特别注意的,可以理解为只是包了一个壳子而已。

2. springboot 方式

一个标准的 springboot
项目有多种 docker
镜像的做法:传统的 Dockerfile
方式(类似上面)、使用 spring boot 插件
 、使用 Google
Jib
等等,本文着重提及一下从 spring boot 2.3.*
开始支持的 分层
构建方式(Grails 5
是基于 Spring Boot 2.5
)。

补充说明:Grails 5
的其他基础组件版本为 Apache Groovy 3
Micronaut framework 3
Gradle 7
Spring framework 5.3
Spock 2.0

正如前面看到的,Grails 5
在打包时默认生成两个文件,其中我们使用到的 helloworld-0.1.war
,如果手动解包,则能看到相对于 helloworld-0.1-plain.war
多了 WEB-INF/lib/spring-boot-jarmode-layertools-*.jar
WEB-INF/layers.idx
这样两个文件,这两个文件的作用,就是用来支撑 spring boot
项目的 分层
构建。

2.1 Q:为什么要 分层
构建?

A:最直观的好处就是 性能
:我们都知道一个 Docker
镜像是由许多层组成的,镜像本质上是增量构建,后一层是在前一层基础上做封装。拉取镜像时,这些层会在主机缓存,而动则几百兆大小的容器镜像,真正属于工程代码的部分很少,因此,分层最大的好处是解耦
不变
的层(类似动静分离),进而提升未来因代码调整而带来的镜像更新影响范围(已缓存的前提下,只有变化的层才重新拉取);其他还有安全等方面因素的考量,这里不一一扩展。

我们看下怎么分层。

2.2 关于 jarmode

当前还在 build/
下,那么执行:

java -Djarmode=layertools -jar libs/helloworld-0.1.war

[aaron@micrograils.com build]$ java -Djarmode=layertools -jar libs/helloworld-0.1.war 
...
Usage:
  java -Djarmode=layertools -jar helloworld-0.1.war

Available commands:
  list     List layers from the jar that can be extracted
  extract  Extracts layers from the jar for image creation
  help     Help about any command
[aaron@micrograils.com build]$ 

可以看到有三个命令可用:查看(list)
提取(extract)
帮助(help)

list
一下:

java -Djarmode=layertools -jar libs/helloworld-0.1.war list

[aaron@micrograils.com build]$ java -Djarmode=layertools -jar libs/helloworld-0.1.war list
dependencies
spring-boot-loader
snapshot-dependencies
application
[aaron@micrograils.com build]$ 

其实这里列出的,就是前面提到的 layers.idx
所定义的内容,layers.idx
的完整内容如下:

- "dependencies":
  - "WEB-INF/lib/"
- "spring-boot-loader":
  - "org/"
- "snapshot-dependencies":
- "application":
  - "META-INF/"
  - "WEB-INF/classes/"
  - "WEB-INF/layers.idx"
  - "assets/"
  - "cafiles/"

OK,开始以 分层
的方式构建镜像。

2.3 编写 Dockerfile

基于 分层
方式构建镜像,官方的建议是分步构建,就是将原来 一次成型
的操作,分隔成 打基础
真正做
两个阶段,当然,配置文件还是一个。

在当面路径下 build/
新建一个 Dockerfile.s
文件:

FROM adoptopenjdk:11-jre-hotspot as builder
WORKDIR app
ARG JAR_FILE=libs/*.war
COPY ${JAR_FILE} application.war
RUN java -Djarmode=layertools -jar application.war extract

FROM adoptopenjdk:11-jre-hotspot
LABEL maintainer="aaron@micrograils.com"
EXPOSE 8080

WORKDIR app
COPY --from=builder app/dependencies/ ./
COPY --from=builder app/spring-boot-loader/ ./
COPY --from=builder app/snapshot-dependencies/ ./
COPY --from=builder app/application/ ./
ENTRYPOINT ["java""org.springframework.boot.loader.WarLauncher"]

第一个 FROM
(1~6),核心内容就是将 *.war
layers.idx
定义的层提取( extract
)数据,第二个 FROM
内容与传统 Dockerfile
做法一致,其中的四行 COPY
恰好对应着上面的 extract
,其他无需多讲。

2.4 构建镜像

(我已经删掉了 build/libs/helloworld-0.1-plain.war
了哈):

sudo docker build -t micrograils/hello:0.2 -f Dockerfile.s .

[aaron@micrograils.com build]$ sudo docker build -t micrograils/hello:0.2 -f Dockerfile.s .
Sending build context to Docker daemon  92.57MB
Step 1/14 : FROM adoptopenjdk:11-jre-hotspot as builder
...
Successfully built 38a1a2e4988d
Successfully tagged micrograils/hello:0.2
[aaron@micrograils.com build]$ 

2.5 部署验证

sudo docker run -d -p 28080:8080 micrograils/hello:0.2

倒数五个数,打开浏览器,咦?咋没动静?

好吧,看看日志:

sudo docker logs e7be8defdcbd

[aaron@micrograils.com build]$ sudo docker logs e7be8defdcbd
...

***************************
APPLICATION FAILED TO START
***************************

Description:

An attempt was made to call a method that does not exist. The attempt was made from the following location:

    javax.el.ELManager.getExpressionFactory(ELManager.java:61)

The following method did not exist:

    'javax.el.ExpressionFactory javax.el.ELUtil.getExpressionFactory()'

The method's class, javax.el.ELUtil, is available from the following locations:

    jar:file:/app/WEB-INF/lib/el-api-2.2.1-b04.jar!/javax/el/ELUtil.class
    jar:file:/app/WEB-INF/lib/javax.el-3.0.1-b12.jar!/javax/el/ELUtil.class
    jar:file:/app/WEB-INF/lib/javax.el-api-3.0.1-b06.jar!/javax/el/ELUtil.class

The class hierarchy was loaded from the following locations:

    javax.el.ELUtil: file:/app/WEB-INF/lib/el-api-2.2.1-b04.jar


Action:

Correct the classpath of your application so that it contains a single, compatible version of javax.el.ELUtil

搜嘎,典型的依赖冲突,经排查是 org.glassfish.web
依赖了 javax.el:el-api:2.2.1-b04
, 这个是用来将 Grails
部署到 GlassFish
的,明显用不上,干掉之~,重新打包后继续。

(编辑 build.gradle
,注释掉对应的依赖)

//    runtimeOnly "org.glassfish.web:el-impl:2.2.1-b05"

重新 build
run
,熟悉的大酒瓶子又出现在我们面前~

3. Grails 方式

终于到了最后一种了...一边敲一边验证结果真挺累的……

众所周知,Grails
自打有狗的那天起,就以提倡且践行 约定大于配置
而闻名,因此,符合 grails
风格的做法,通常是 你可以不知道在哪儿有,你猜他有他就有
。但是截至到我发稿,目前还没发现 Grails
官方的 Docker
指令,不过官方网站上基于 Grails 4
有一篇教程是关于 Docker
的,本文此部分也正是参考了官方的教程,结合新版本特性进行测验并形成备忘的。

核心思路是使用了一个 com.bmuschko:gradle-docker-plugin
的插件(github
上开源且 star
还不少,文档也很清晰简明),通过提供 gradle
task
来实现自动编译打包和创建镜像的。此插件包含三个插件:

  • DockerRemoteApiPlugin
  • DockerJavaApplicationPlugin
  • DockerSpringBootApplicationPlugin

Grails 4
的那篇教程是基于 DockerRemoteApiPlugin
的,本来我还想着是不是基于他的 DockerSpringBootApplicationPlugin
搞事情,但是看了下源码,关于 Dockerfile
的某些内容他已经固化到代码里了,于是最终也决定直接从 DockerRemoteApiPlugin
入手。

3.1 编写 docker.gradle

gradle/
下新建一个  docker.gradle
文件,用来提供命令行的 gradle buildImage
指令,其内容如下(约定大于配置
的前提下,可以认为是一劳永逸的配置):

[aaron@micrograils.com helloworld]$ cat gradle/docker.gradle 

import com.bmuschko.gradle.docker.tasks.image.Dockerfile
import com.bmuschko.gradle.docker.tasks.image.DockerBuildImage

buildscript {
    repositories {
        gradlePluginPortal()
        mavenCentral()
    }
    dependencies {
        classpath 'com.bmuschko:gradle-docker-plugin:7.1.0'
    }
}

apply plugin: com.bmuschko.gradle.docker.DockerRemoteApiPlugin

ext {
    dockerTag = "${project.group}/${project.name}:${project.version}".toLowerCase()
    dockerBuildDir = mkdir("${buildDir}/docker")
}

task prepareDocker(type: Copy, dependsOn: assemble) {
    description = 'Copy files from src/main/docker and application jar to Docker temporal build directory'
    group = 'Docker'
    
    from project.bootJar

    into dockerBuildDir
}
task createDockerfile(type: Dockerfile, dependsOn: prepareDocker) {
    description = 'Create a Dockerfile file'
    group = 'Docker'

    destFile = project.file("${dockerBuildDir}/Dockerfile")

    from 'adoptopenjdk:11-jre-hotspot'
    exposePort 8080
    workingDir '/app'
    copyFile bootJar.archiveName, 'application.jar'
    entryPoint 'java''-jar''/app/application.jar'
}
task buildImage(type: DockerBuildImage, dependsOn: createDockerfile) {
    description = 'Create Docker image to run the Grails application'
    group = 'Docker'

    inputDir = file(dockerBuildDir)
    images.add(dockerTag)
}

之后,在 build.gradle
中,添加对此文件的引用即可:

apply from: 'gradle/docker.gradle'

3.2 构建镜像

sudo -E $GRADLE_HOME/bin/gradle buildImage

注:这里我用了 sudo
, 原因是最终提交镜像的时候,需要 root
权限。

[aaron@micrograils.com helloworld]$ sudo -E $GRADLE_HOME/bin/gradle buildImage

>
 Task :buildImage
Building image using context '/home/aaron/workspaces/helloworld/build/docker'.
Using images 'com.micrograils/helloworld:0.1'.
Step 1/5 : FROM adoptopenjdk:11-jre-hotspot
...
BUILD SUCCESSFUL in 28s
14 actionable tasks: 14 executed
[aaron@micrograils.com helloworld]$ 

崭新的镜像就做好了:

[aaron@micrograils.com helloworld]$ sudo docker images
REPOSITORY                   TAG                IMAGE ID       CREATED          SIZE
com.micrograils/helloworld   0.1                6b9c8e23f6c9   18 seconds ago   327MB

3.3 部署验证

一路下来还算一帆风顺,略。

第三部分 一些补充


1. 关于 adoptopenjdk:11-jre-hotspot

项目都是同一个项目的前提下,FROM
不同的镜像,最终的文件大小差距较大:

REPOSITORYFROMSIZE
com.micrograils/helloworld:0.1
adoptopenjdk:11-jre-hotspot
327MB
micrograils/hello:0.2
adoptopenjdk:11-jre-hotspot
333MB
micrograils/hello:0.1
openjdk:11
743MB

而且现在主流也都是用 JDK11
了,这样这两天正火热的 log4j
风险跟你也没啥关系。

2. 镜像内部结构对比

REPOSITORY/app
com.micrograils/helloworld:0.1
application.jar
micrograils/hello:0.2
解压后的目录(参见layers.idx
)
micrograils/hello:0.1
application.war

1和3其实本质是一样的。

对于迭代部署,如果客观上有快速部署、批量拉起的需求,方式2会效率高些。

第四部分 参考资料

为了保证文章质量,本文相关的参考资料如下:

[Spring Boot Gradle Plugin Reference Guide] https://docs.spring.io/spring-boot/docs/current/gradle-plugin/reference/htmlsingle/

[Spring Boot with Docker] https://spring.io/guides/gs/spring-boot-docker/

[Creating Optimized Docker Images for a Spring Boot Application] https://reflectoring.io/spring-boot-docker/

[Grails as a Docker Container] https://guides.grails.org/grails-as-docker-container/guide/index.html

[Gradle Docker Plugin] https://bmuschko.github.io/gradle-docker-plugin/current/user-guide/


---- [我是分割线] ----

Q:怎样能第一时间收到推送?

A:点击文章右上角的 ...
找到公众号页面,继续点击右上角的 ...
后选择 设为星标
,这样就不会漏掉推送了。

转载请发邮件:aaron#micrograils.com


文章转载自MicroGrails,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论