Với blog này, chúng ta sẽ cùng nhau tìm hiểu một số best parctices về docker, tập trung chủ yếu với việc đóng gói triển khai các ứng dụng JAVA.
1. Giới thiệu
Nếu đã từng làm việc hoặc có kiến thức về docker, việc viết Dockerfiles thực tế là khá đơn giản: bạn chỉ cần lên Google và tìm kiếm một template dockerfile vừa ý và tự tùy chỉnh theo yêu cầu của mình. Tuy nhiên, có một thực tế là hầu hết các template mà bạn tìm được từ internet chỉ phù hợp khi sử dụng trên môi trường phát triển thôi, chứ mang sang môi trường ứng dụng thì sẽ gặp rất nhiều vấn đề liên quan, đặc biệt trong đó là vấn đề bảo mật.
Do đó, thông qua bài viết này, mình muốn chia sẻ một số phương pháp, ví dụ mẫu mà bạn có thể áp dụng khi viết Dockerfiles khi triển khai trên các môi trường thực tế
Template docekerfile cho ứng dụng Java được xây dựng cơ bản như sau
FROM eclipse-temurin:17
RUN mkdir /opt/app
ARG JAR_FILE
ADD target/${JAR_FILE} /opt/app/app.jar
CMD ["java", "-jar", "/opt/app/app.jar"]
Chi tiết dockerfile cụ thể như sau:
- FROM: eclipse-temurin:17 là docker base image
- RUN: tạo thư mục cài đặt trong container
- ARG: gán tham số JAR_FILE ứng với tên của jar file, điều này giúp bạn dễ dàng thay đổi tên ứng dụng một cách linh hoạt sau này
- ADD: câu lệnh đẩy file jar vào trong docker image
- CMD: câu lệnh sẽ được khởi chạy khi ứng dụng được start, trong trường hợp này là câu lệnh start ứng dụng Java
Trong phần tiếp theo mình sẽ đưa ra các hướng dẫn chi tiết khi xây dựng dockerfile, hướng đến cấu trúc tốt nhất khi triển khai thực tế.
2. Yêu cầu kiến thức
Kiến thức cần thiết:
- Kiến thức cơ bản về Linux
- Kiến thức về Java và Java Spring boot
- Kiến thức cơ bản về Docker
3. Ứng dụng demo
Chúng ta sẽ cần một ứng dụng Java Spring Boot để phục vụ cho việc demo các ví dụ. Ứng dụng này được khởi tạo tích hợp sẵn với Spring Web dependency.
Đảm bảo ứng dụng có thể khởi chạy băng câu lệnh sau tại thư mục root của sourcecode
mvn spring-boot:run
Để thuận tiện cho việc tạo Docker image, chúng ta tích hợp thêm plugin dockerfile-maven-plugin của Spotify vào file pom của project.
<plugin>
<groupId>com.xenoamess.docker</groupId>
<artifactId>dockerfile-maven-plugin</artifactId>
<version>1.4.25</version>
<configuration>
<repository>mydeveloperplanet/dockerbestpractices</repository>
<tag>${project.version}</tag>
<buildArgs>
<JAR_FILE>${project.build.finalName}.jar</JAR_FILE>
</buildArgs>
</configuration>
</plugin>
Plugin này sẽ giúp bạn dễ dàng config và tạo mới Docker image thông qua câu lệnh Maven đơn giản
Bước 1: Build file jar
mvn clean verify
Bước 2: Build Docker images
mvn dockerfile:build
Bước 3: Thử chạy Docker images
docker run --name dockerbestpractices -p 8080:8080 mydeveloperplanet/dockerbestpractices:0.0.1-SNAPSHOT
Bước 4: Kiểm tra response từ ứng dụng
curl http://172.17.0.3:8080/hello
Hello Docker!
4. Best practices
4.1 Nên sử dụng image nào?
Quay lại dockerfile template ở mục 1, dễ dàng nhận thấy image được sử dụng ở đây là eclipse-temurin:17. Đây là một image đã được dựng sẵn. Vậy chính xác loại của image này là gì, nó được xây dựng dựa trên nền tảng nào, có đáp ứng được được yêu cầu triển khai không? Chúng ta cần tìm hiểu rõ.
Kiểm tra cách mà image này được xây dựng bằng các thao tác sau
- Truy cập vào trang Dockerhub
- Tìm kiếm eclipse-termurin
- Mở tab Tags
- Tìm kiếm tag version 17;
- Sắp xếp theo thứ tự từ A-Z;
- Click chọn vào tag 17 và xem trang mô tả chi tiết về image.
Nhìn vào nội dung được mô tả về các layer và so sánh với một image khác là tag 17-jre, dễ dàng nhận thấy tag 17 được xây dựng bao gồm cả bộ JDK trong khi tag 17-jre chỉ có mỗi bộ JRE. Đứng từ quan điểm chỉ cần các thành phần cần thiết để chạy được ứng dụng, rõ ràng chỉ 17-jre là đủ, bạn không cần cả bộ JDK để chạy ứng dụng với môi trường production. Bên cạnh đó, việc sử dụng cả bộ JDK còn tiềm tàng nguy cơ bảo mật bởi vì các công cụ phát triển có thể dễ dàng tương tác khi bạn sử dụng JDK. Hơn thế nữa, việc kích cỡ đóng gói của tag-17 lên tới 235MB trong khi 17-jre chỉ có 89MB cũng là một điểm cộng khi triển khai.
Trong trường hợp muốn giảm tối đa kích cỡ của image hơn nữa, đáp ứng việc phân phối bản build một cách nhanh chóng, bạn có thể sử dụng dạng slimmed image. Trong trường hợp bài viết này là 17-jre-alpine. Kích cỡ đóng gói của image này chỉ 59MB, nhỏ hơn 30MB so với 17-jre.
Một điểm nữa cần chú ý chính là version của image mà bạn sử dụng. Trong ví dụ ở trên, tags version của image được trỏ về lastest version ( bản mới nhất). Điều này có thể bình thường đối với môi trường phát triển - nơi mà bạn có thể thử nghiệm mọi thứ mới nhất, nhưng đối với môi trường production, tốt nhất vẫn nên sử dụng một phiên bản đã được kiểm chứng, ví dụ: 17.0.5_8-jre-alpine. Và để đảm bảo an toàn hơn nữa, ta hoàn toàn có thể bổ sung thêm mã SHA256 vào sau image tag version để yêu cầu docker xác thực phiên bản image. Mã SHA256 này có thể tìm thấy tại trang mô tả chi tiết của image.
Cách bổ sung mã SHA256
Trong dockerfile trước khi bổ sung:
FROM eclipse-temurin:17
Bổ sung thêm mã:
FROM eclipse-temurin:17.0.5_8-jre-alpine@sha256:02c04793fa49ad5cd193c961403223755f9209a67894622e05438598b32f210e
Sau khi đã thực hiện các thay đổi, tiến hành build lại image và kiểm tra kết quả. Dễ dàng nhận thấy kích cỡ đóng gói của image đã giảm từ 475MB xuống còn 188MB
docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
mydeveloperplanet/dockerbestpractices 0.0.1-SNAPSHOT 0b8d89616602 3 seconds ago 188MB
4.2 Đừng chạy với quyền root
Mặc định nếu không thay đổi gì, ứng dụng sẽ chạy với quyền root trong nội tại container. Nghe có vẻ bình thường nhưng thực tế điều này dẫn đến nhiều rủi ro mà bạn không hề mong muốn gặp phải. Do đó, tốt hơn là nên định nghĩa một quyền người dùng riêng cho ứng dụng của bạn.
Để thêm mới một người dùng, bạn cần tác động vào dockerfile. Trong dockerfile, chúng ta thêm group javauser và người dùng javauser. Người dùng (user) javauser được định nghĩa là một người dùng hệ thống (system user) và không thể login.
RUN addgroup --system javauser && adduser -S -s /usr/sbin/nologin -G javauser javauser
Một số tham số bạn có thể bổ sung thêm để cấu hình các thông số của người dùng:
- -h dir : Đường dẫn thư mục home mặc định
- -g GECOS : thuộc tính GECOS
- -s SHELL_dir: đường dẫn login shell
- -g GRP : group của người dùng
- -S : đánh dấu đây là người dùng hệ thống (system user)
- -D : không gán mật khẩu
- -H : không tạo thư mục home mặc định
- -u UID: id của người dùng
- -k SKEL_path : đường dẫn thư mục skeleton (/etc/skel)
Bạn cũng nên thay đổi quyền sở hữu (owner) của thư mục /opt/apt gán cho javauser:
RUN chown -R javauser:javauser /opt/app
Cuối cùng, chắc chắn rằng javauser thực sư được sử dụng trong container bằng cách kiểm tra với lệnh "user". Dockerfile hoàn chỉnh lúc này sẽ có cấu trúc như sau:
FROM eclipse-temurin:17.0.5_8-jre-alpine@sha256:02c04793fa49ad5cd193c961403223755f9209a67894622e05438598b32f210e
RUN mkdir /opt/app
RUN addgroup --system javauser && adduser -S -s /usr/sbin/nologin -G javauser javauser
ARG JAR_FILE
ADD target/${JAR_FILE} /opt/app/app.jar
RUN chown -R javauser:javauser /opt/app
USER javauser
CMD ["java", "-jar", "/opt/app/app.jar"]
Để test image mới này, chú ý bạn cần stop và remove container mà mình đang chạy từ trước nhé.
docker stop dockerbestpractices
docker rm dockerbestpractices
Tiến hành build và chạy lại container ứng dụng. Kiểm tra dòng đầu tiên trong của log ứng dụng, bạn sẽ thấy ứng dụng lúc này đã được chạy bằng quyền user javauser.
2022-11-26 09:06:45.227 INFO 1 --- [ main] m.MyDockerBestPracticesPlanetApplication : Starting MyDockerBestPracticesPlanetApplication v0.0.1-SNAPSHOT using Java 17.0.5 on ab1bcd38dff7 with PID 1 (/opt/app/app.jar started by javauser in /)
4.3 Sử dụng WORKDIR
Trong ví dụ về dockerfile ở trên, đường dẫn /opt/app được tạo ra để chứa file jar của ứng dụng. Thực tế sau này, đường dẫn này cũng sẽ được sử dụng tiếp cho các cấu hình khác liên quan của ứng dụng, việc lặp lại nó nhiều lần có thể xảy ra rủi ro nhầm lẫn trong lúc khai báo cấu hình dockerfile (thiếu đường dẫn/ sai đường dẫn chả hạn). Do đó, để tránh các rủi ro này, chúng ta có thể sử dụng cơ chế WORKDIR của doceker đê cấu hình. Khi sử dụng WORKDIR, nếu đường dẫn của bạn chưa tồn tại, nó sẽ được tự động tạo ra. Kể từ sau khi đã khai báo WORKDIR, mọi câu lệnh đều sẽ được thực hiện trong đường dẫn WORKDIR mà bạn khai báo.
FROM eclipse-temurin:17.0.5_8-jre-alpine@sha256:02c04793fa49ad5cd193c961403223755f9209a67894622e05438598b32f210e
WORKDIR /opt/app
RUN addgroup --system javauser && adduser -S -s /usr/sbin/nologin -G javauser javauser
ARG JAR_FILE
ADD target/${JAR_FILE} app.jar
RUN chown -R javauser:javauser .
USER javauser
CMD ["java", "-jar", "app.jar"]
4.4 Sử dụng ENTRYPOINT
Dockerfile có 2 cú pháp khác nhau là CMD và ENTRYPOINT. Nếu bạn muốn tìm hiểu rõ hơn, có thể xem blog này.
- ENTRYPOINT: Được sử dụng khi xây dựng các image docker có thể thực thi (executable) bằng cách sử dụng các lệnh thực thi
- CMD: Được sử dụng khi muốn cung cấp các tham số mặc định có thể ghi đè bằng dòng lệnh khi thực hiện
Do đó, trong trường hợp muốn khởi chạy một ứng dụng Java, tránh cho việc ghi đè tham số làm thay đổi các cấu hình, ta nên sử dụng ENTRYPOINT
Đổi dòng cuối cùng của dockerfile như sau:
ENTRYPOINT ["java", "-jar", "app.jar"]
4.5 Sử dụng COPY thay vì ADD
Lệnh COPY và ADD trông thì có vẻ giống nhau. Tuy nhiên, COPY thường được ưu tiên hơn ADD bởi vì COPY chỉ làm những gì mà bạn yêu cầu nó xử lý - sao chép tệp vào trong image. TRong khi đó, ADD có thêm một số tính năng bổ sung, chẳng hạn như thêm tệp từ những remote resource. Tải về những thứ cần thiết, hạn chế sử dụng các tài nguyên remote sẽ giúp bạn giảm thiểu các yếu tố rủi ro về An toàn thông tin trên môi trường production.
Trong dockerfile ở trên:
ADD target/${JAR_FILE} app.jar
Đổi thành
COPY target/${JAR_FILE} app.jar
4.6 Sử dụng dockerignore
Để tránh vô tình add thêm các file không cần thiết vào trong Docker image, ta có thể sử dụng .dockerignore file để hạn chế. Cơ bản chức năng của .dockerignore file là bạn có thể chỉ định các tệp nào có thể gửi đến Docker daermon hoặc có thể sử dụng trong image của bạn. Tốt hơn cả, bạn nên bỏ qua tất cả các định dạng tệp không liên quan và chỉ ra rõ ràng các tệp mà bạn cho phép.
Ví dụ bạn cần file jar để có thể chạy được ứng dụng. Do đó các tệp jar cần được cho phép thêm vào trong docker image:
**/**
!target/*.jar
4.7 Chạy Docker deamon không cần quyền root (rootless)
Mặc định khi cài đặt, Docker deamon được chạy với quyền root. Tuy nhiên, điều này gây ra một số vấn đề bảo mật đến hệ thống. Kể từ phiên bản Docker v20.10, bạn đã có thể cấu hình chạy Docker với tư cách là người dùng không phải root. Tham khảo link sau để biết thêm chi tiết
5. Kết luận
Xây dựng docker từ dockerfile thực tế khá dễ dàng, tuy nhiên để cấu hình một cách chính xác thì sẽ cần thêm nhiều nỗ lực tìm hiểu, thử nghiệm. Hy vọng bài viết này mang lại nhiều điều hữu ích cho bạn.
Xin cảm ơn!




