ปัญหาของการ build Gradle project ใน Docker เมื่อเชื่อมต่อผ่าน private repository ที่ใช้ SSL

Nont Banditwong
4 min readMar 4, 2023

--

สองสามวันก่อนผมพยายาม build Gradle project ที่เขียนด้วย Kotlin โดย build ผ่าน gitlab runner บน Docker ซึ่งใน environment ที่ใช้อยู่ Gradle ไม่สามารถออกไป internet ตรงๆเพื่อไปเอา plugin และ artifacts ต่างๆได้ต้องเชื่อมต่อผ่าน private repository ที่สร้างขึ้นจาก Nexus Repository วิธีการ configure ใน Gradle ให้ใช้ private repository สามารถศึกษาได้จาก https://docs.gradle.org/current/userguide/plugins.html#sec:custom_plugin_repositories

build script กับ private repository สามารถใช้งานได้นอก gitlab runner ซึ่งอยู่ใน Docker container คือ build ใน Linux ปกติ โดยเมื่อสั่ง gradle build ก็จะทำงานจนเสร็จได้ jar ออกมาไม่มี error อะไร

แต่เมื่อใช้ gitlab-ci ทำการ build ใน gitlab runner เมื่อถึงขั้นตอนที่สั่ง gradle build รอไปสักพักสุดท้ายจะเกิด Error “could not resolve plugin artifact”

FAILURE: Build failed with an exception.

* Where:
Build file '/app/build.gradle.kts' line: 3

* What went wrong:
Plugin [id: 'org.springframework.boot', version: '2.7.9'] was not found in any of the following sources:

- Gradle Core Plugins (plugin is not in 'org.gradle' namespace)
- Plugin Repositories (could not resolve plugin artifact 'org.springframework.boot:org.springframework.boot.gradle.plugin:2.7.9')
Searched in the following repositories:
companyrepo(https://repository.company.com/repository/maven-public/)
Gradle Central Plugin Repository

ตอนแรกถอดใจกับ gitlab-ci ไปแล้ว เลยมาเขียน Dockerfile และทำ multi-stage build คือเปลี่ยนจากการสั่ง gradle build ใน Linux เข้าไปอยู่ใน Docker แล้วเวลา build ก็สั่ง

docker build -t registry.company.com/kotlin-ping:0.0.1-SNAPSHOT .

ตัวอย่าง Dockerfile

FROM registry.company.com/gradle:8.0.1-jdk11 as builder

WORKDIR /app
COPY gradle gradle
COPY build.gradle.kts settings.gradle.kts gradlew ./
COPY src src

RUN gradle build --no-daemon build -x test

# Stage 2: Create a lightweight image with the compiled application
FROM registry.company.com/adoptopenjdk/openjdk11:jre-11.0.18_10
WORKDIR /app

COPY --from=builder /app/build/libs/kotlin-ping-0.0.1-SNAPSHOT.jar app.jar

CMD ["java", "-jar", "app.jar"]

ถ้าเราใช้ multi-stage builds แบบนี้จะเจอ error เหมือนกัน ดังนั้นสิ่งที่ต่างกันน่าจะเป็นแค่การ run gradle build ใน และนอก Docker container สิ่งที่นึกออกในตอนแรกคือ private repository ซึ่งใช้ self-signed certificate ของบริษัท ถ้าไม่ทำอะไรเลย connection จากใน container น่าจะไม่ trust domain https://repository.company.com

สิ่งที่ลองทำเพิ่มเติมคือ copy Root/Intermediate CA bundled (ในที่นี้ใช้ไฟล์ชื่อ company-ca.crt) เข้ามาใน container ด้วย (ใน Linux ที่ใช้ทดสอบ run gradle build ในตอนแรกมี CA certificate นี้ติดตั้งอยู่แล้ว)

FROM registry.company.com/gradle:8.0.1-jdk11 as builder

WORKDIR /app
COPY gradle gradle
COPY build.gradle.kts settings.gradle.kts gradlew ./
COPY src src
COPY company-ca.crt /usr/local/share/ca-certificates/company-ca.crt
RUN chmod 644 /usr/local/share/ca-certificates/company-ca.crt && update-ca-certificates
RUN curl -s https://repository.company.com

# Stage 2: Create a lightweight image with the compiled application
FROM registry.company.com/adoptopenjdk/openjdk11:jre-11.0.18_10
WORKDIR /app

# comment ด้านล่างนี้ไว้ก่อนเพราะไม่ได้ build จริง
#COPY --from=builder /app/build/libs/kotlin-ping-0.0.1-SNAPSHOT.jar app.jar

CMD ["java", "-jar", "app.jar"]

ผลที่ได้คือ สามารถ curl ไปที่ https://repository.company.com ได้โดยที่ curl trust domain นี้แล้ว ดังนั้นในระดับ container น่าจะรู้จัก Root CA นี้แล้ว

note: ต้องใส่ DOCKER_BUILDKIT=0 ไว้หน้า docker build เพื่อจะได้เห็น output ของ curl

$ DOCKER_BUILDKIT=0 docker build -t registry.company.com/kotlin-ping:0.0.1-SNAPSHOT .
DEPRECATED: The legacy builder is deprecated and will be removed in a future release.
BuildKit is currently disabled; enable it by removing the DOCKER_BUILDKIT=0
environment-variable.

Sending build context to Docker daemon 24.23MB
Step 1/10 : FROM registry.company.com/gradle:8.0.1-jdk11 as builder
---> d6da68edd221
Step 2/10 : WORKDIR /app
---> Using cache
---> 0d9f7a6edc17
Step 3/10 : COPY gradle gradle
---> Using cache
---> 9e1c0ef8ff8b
Step 4/10 : COPY build.gradle.kts settings.gradle.kts gradlew ./
---> Using cache
---> db72b5956762
Step 5/10 : COPY src src
---> Using cache
---> 611d307f35fe
Step 6/10 : COPY company-ca.crt /usr/local/share/ca-certificates/company-ca.crt
---> 21f6a54d2ba2
Step 7/10 : RUN chmod 644 /usr/local/share/ca-certificates/company-ca.crt && update-ca-certificates
---> Running in a74de2b2124b
Updating certificates in /etc/ssl/certs...
rehash: warning: skipping ca-certificates.crt,it does not contain exactly one certificate or CRL
1 added, 0 removed; done.
Running hooks in /etc/ca-certificates/update.d...
done.
Removing intermediate container a74de2b2124b
---> 88e3136e645e
Step 8/10 : RUN curl -s https://repository.company.com
---> Running in d108b0460169

<!DOCTYPE html>
<html lang="en">
<head>
<title>Nexus Repository Manager</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<meta name="description" content="Nexus Repository Manager"/>
<meta http-equiv="X-UA-Compatible" content="IE=edge"/>
...

แต่พอแก้ Dockerfile ให้ไปทำคำสั่ง gradle build เหมือนเดิมก็เจอ error could not resolve plugin artifact เดิมกลับมา

ความหวังสุดท้ายก้อนคิดว่าจะเลิกทำแล้วไป build ใน Linuxแทน Docker คือสร้าง truststore แล้ว import Root CA เข้าไป — ถ้าคนมีความรู้หน่อยควรจะตรวจสอบว่า JVM มองเห็น Root CA ของเราหรือเปล่า :-) ตอนนั้นลืมเลยใช้วิธีวัดดวงเอา

วิธี import Root CA จากไฟล์ company-ca.crt ไปสร้างเป็น truststore ไฟล์ชื่อ company-ca.truststore.jks keytool จะให้ตั้ง password สำหรับ truststore ในตัวอย่างนี้ใช้ว่า changeit

keytool -import -file company-ca.crt -alias CA -keystore company-ca.truststore.jks

ใน Dockerfile ให้ copy truststore เข้าไปใน container ด้วยคำสั่ง COPY company-ca.truststore.jks company-ca.truststore.jks

ใน gradle build ให้เพิ่ม parameter -Djavax.net.ssl.trustStore=./company-ca.truststore.jks -Djavax.net.ssl.trustStorePassword=changeit เพื่อกำหนด truststore ให้กับ gradle

FROM registry.company.com/gradle:8.0.1-jdk11 as builder

WORKDIR /app
COPY gradle gradle
COPY build.gradle.kts settings.gradle.kts gradlew ./
COPY src src
COPY company-ca.crt /usr/local/share/ca-certificates/company-ca.crt
COPY company-ca.truststore.jks company-ca.truststore.jks
RUN chmod 644 /usr/local/share/ca-certificates/company-ca.crt && update-ca-certificates
RUN gradle build --no-daemon build -x test -Djavax.net.ssl.trustStore=./company-ca.truststore.jks -Djavax.net.ssl.trustStorePassword=changeit

# Stage 2: Create a lightweight image with the compiled application
FROM registry.company.com/adoptopenjdk/openjdk11:jre-11.0.18_10
WORKDIR /app

#COPY --from=builder /app/build/libs/kotlin-ping-0.0.1-SNAPSHOT.jar app.jar

#CMD ["java", "-jar", "app.jar"]

Bingo! ผลคือสามารถ build ได้จนเสร็จ

ส่วนตัวยังไม่แน่ใจว่าทำไม gradle ไม่ไปเอา Root CA certificate จาก system ของ container มาใช้เหมือนที่ curl ทำ แต่ที่มันทำงานได้ใน Linux ปกติน่าจะเกิดจากใน Linux ผมมี Root CA Certificate ติดตั้งเอาไว้อยู่แล้ว ถึงมาติดตั้ง openjdk และ Gradle ในขั้นตอนการติดตั้งตัว openjdk อาจจะไปกวาดเอา Root Ca Certificate เหล่านั้นเข้ามาใน truststore ของ JVM ก็เป็นไปได้ อันนี้ยังไม่มีเวลาทดสอบ

ส่วนใน container ผมใช้ Gradle image ซึ่งมี JVM และ Gradle ติดตั้งไว้ก่อนที่จะ import Root CA Certificate ของบริษัทเข้าไป และจริงๆแล้ว JVM มันอาจจะไม่ได้สนใจ CA Certificate ของ Operating system เลยด้วยซ้ำ แต่อ่านจาก keystore ของ JVM เอง อันนี้ถ้ามีเวลาเดี๋ยวไปหาข้อมูลเพิ่มเติม

--

--

Nont Banditwong

Cloud Engineering Specialist, Software Developer, System Engineer, Photographer and Learner