Hướng Dẫn Kết Hợp Bazel Với Docker
Chào mừng bạn đến với pikoTutorial tiếp theo! Trong một bài viết gần đây, tôi đã hướng dẫn cách xây dựng và chạy các container Docker bằng cách sử dụng CMake. Hôm nay, chúng ta sẽ xem xét cách thực hiện điều tương tự nhưng bằng cách sử dụng Bazel. Dưới đây là cấu trúc dự án của chúng ta:
project/
├── app1/
│ ├── BUILD
│ ├── main.py
│ ├── run_container.sh
│ ├── some_config.json
├── app2/
│ ├── BUILD
│ ├── main.py
│ ├── run_container.sh
│ ├── some_config.json
├── MODULE.bazel
└── WORKSPACE
Lưu ý: Ngược lại với cấu trúc dự án được sử dụng trong bài viết về CMake, cây thư mục này không chứa bất kỳ Dockerfile nào. Điều này là do Bazel hỗ trợ xây dựng các hình ảnh tuân thủ OCI, độc lập với công cụ cụ thể nào (như Docker). Tuy nhiên, ở bước cuối cùng, tôi sẽ sử dụng Docker để chạy container.
Tệp MODULE.bazel
Đầu tiên, chúng ta cần xác định các phụ thuộc của quá trình xây dựng. Chúng chứa các quy tắc mà tôi sẽ sử dụng trong các tệp Bazel:
# phụ thuộc cần thiết để tạo một nhị phân python và một lớp ứng dụng python trong hình ảnh
bazel_dep(name = "aspect_rules_py", version = "1.4.0")
# phụ thuộc cần thiết để xây dựng hình ảnh OCI
bazel_dep(name = "rules_oci", version = "2.2.6")
# phụ thuộc cần thiết cho quy tắc pkg_tar mà chúng ta sẽ sử dụng để tạo
# một lớp hình ảnh bổ sung
bazel_dep(name = "rules_pkg", version = "1.1.0")
Tệp WORKSPACE
Tiếp theo là tệp WORKSPACE. Một số thao tác (như kéo một hình ảnh cơ sở) chỉ có thể được thực hiện trong giai đoạn tải không gian làm việc, vì vậy chúng phải có trong tệp WORKSPACE.
load("@rules_oci//oci:dependencies.bzl", "rules_oci_dependencies")
rules_oci_dependencies()
load("@rules_oci//oci:repositories.bzl", "oci_register_toolchains")
oci_register_toolchains(name = "oci")
load("@rules_oci//oci:pull.bzl", "oci_pull")
oci_pull(
name = "python_base",
image = "python",
tag = "3.9-slim",
platforms = ["linux/amd64"],
)
Trong bài viết này, có hai ứng dụng Python, vì vậy tôi kéo một hình ảnh cơ sở chuyên dụng cho Python, nhưng nếu bạn sử dụng ngôn ngữ khác, đây là nơi bạn chỉ định hình ảnh cơ sở phù hợp.
Các tệp app1/main.py và app2/main.py
Không có gì đặc biệt ở đây, chỉ là một số mã Python in nội dung của tệp cấu hình để kiểm tra xem container hoạt động hay không:
import json
with open("some_config.json", "r") as file:
print(f"Configuration from app 1: {json.load(file)}")
Tệp some_config.json là một từ điển với một khóa:
{
"Hello": "World"
}
Tương tự, nội dung của app2/some_config.json là:
{
"Bye": "World"
}
Các tệp BUILD
Cả hai tệp app1/BUILD và app2/BUILD có cấu trúc tương tự với điểm khác biệt duy nhất là tên ứng dụng:
load("@aspect_rules_py//py:defs.bzl", "py_binary", "py_image_layer")
load("@rules_oci//oci:defs.bzl", "oci_image", "oci_load")
load("@rules_pkg//:pkg.bzl", "pkg_tar")
# tạo một nhị phân ứng dụng Python
py_binary(
name = "main",
srcs = ["main.py"],
)
# tạo một lớp ứng dụng Python trong hình ảnh
py_image_layer(
name = "application_layer",
binary = ":main",
)
# tạo một lớp bổ sung trong hình ảnh
pkg_tar(
name = "configuration_layer",
srcs = ["some_config.json"],
)
# tạo hình ảnh OCI
oci_image(
name = "image_definition",
base = "@python_base",
entrypoint = ["/app1/main"],
tars = [":configuration_layer", ":application_layer"],
)
# tải hình ảnh vào runtime cục bộ
oci_load(
name = "image",
image = ":image_definition",
repo_tags = ["image_app_1:latest"],
)
Với tệp BUILD như vậy, tôi đã định nghĩa một hình ảnh OCI hai lớp mẫu có thể được tải vào runtime cục bộ (và có thể truy cập ví dụ với Docker).
Xây dựng các hình ảnh
Bây giờ khi mọi thứ đã sẵn sàng, bạn có thể xây dựng hình ảnh app1 bằng cách gọi:
bazel run //app1:image
Điều này sẽ tạo ra đầu ra sau:
INFO: Analyzed target //app1:image (152 packages loaded, 3970 targets configured).
INFO: Found 1 target...
Target //app1:image up-to-date:
bazel-bin/app1/image.sh
INFO: Elapsed time: 5.106s, Critical Path: 4.20s
INFO: 40 processes: 25 internal, 14 linux-sandbox, 1 local.
INFO: Build completed successfully, 40 total actions
INFO: Running command line: bazel-bin/app1/image.sh
6c4c763d22d0: Loading layer [==================================================>] 28.23MB/28.23MB
41757dc445c9: Loading layer [==================================================>] 3.512MB/3.512MB
529e75018436: Loading layer [==================================================>] 14.93MB/14.93MB
678221e973fe: Loading layer [==================================================>] 249B/249B
c025797afb0a: Loading layer [==================================================>] 10.24kB/10.24kB
7e7cd3ed4a69: Loading layer [==================================================>] 2.483MB/2.483MB
f465bed610cd: Loading layer [==================================================>] 23.19MB/23.19MB
7141b744acb3: Loading layer [==================================================>] 1.103MB/1.103MB
Loaded image: image_app_1:latest
Và tương tự cho hình ảnh app2:
bazel run //app2:image
Lưu ý cho người mới bắt đầu: hãy chú ý rằng quy tắc
oci_load
mà tôi đã sử dụng để định nghĩa các mục tiêu//app1:image
và//app2:image
phải được gọi bằngbazel run
. Gọibazel build
trên các mục tiêu này sẽ không tải hình ảnh vào runtime cục bộ.
Sau khi quá trình xây dựng hoàn tất, bạn có thể chạy:
docker images
để kiểm tra rằng 2 hình ảnh mới đã xuất hiện:
REPOSITORY TAG IMAGE ID CREATED SIZE
image_app_1 latest 95ef84a311e5 2 minutes ago 214MB
image_app_2 latest 8d757bc74a7e 2 minutes ago 214MB
Bạn có thể chạy một trong số chúng bằng cách gọi, ví dụ:
docker run --rm --name container_app_1 image_app_1
Và bạn sẽ thấy rằng không chỉ ứng dụng của chúng ta hoạt động trong container, mà nó còn có quyền truy cập vào tệp cấu hình đã được thêm vào hình ảnh của chúng ta trong lớp cấu hình:
Configuration from app 1: {'Hello': 'World'}
Chạy Container Docker với Bazel
Nhưng tại sao lại dừng lại ở đây? Chúng ta có một lớp trừu tượng đẹp cho việc tạo hình ảnh (dưới dạng mục tiêu Bazel //app1:image
), vì vậy hãy thêm một lớp trừu tượng tương tự cho việc chạy container từ cấp độ Bazel như thế này:
bazel run //app1:container
Để thực hiện điều đó, cần có một kịch bản bọc nào đó sẽ được sử dụng trong tệp BUILD của Bazel, hãy gọi nó là run_container.sh:
#!/bin/bash
docker run --rm --name container_app_1 image_app_1
Sau đó, nó có thể được sử dụng trong quy tắc sh_binary
trong tệp app1/BUILD:
sh_binary(
name = "container",
srcs = ["run_container.sh"],
data = [":image"],
)
Bây giờ, khi bạn gọi:
bazel run //app1:container
Bazel sẽ xử lý việc chạy container đã chỉ định:
INFO: Analyzed target //app1:container (0 packages loaded, 24 targets configured).
INFO: Found 1 target...
Target //app1:container up-to-date:
bazel-bin/app1/container
INFO: Elapsed time: 0.275s, Critical Path: 0.09s
INFO: 5 processes: 5 internal.
INFO: Build completed successfully, 5 total actions
INFO: Running command line: bazel-bin/app1/container
Configuration from app 1: {'Hello': 'World'}
Lưu ý cho người mới bắt đầu: mục tiêu
//app1:container
sẽ không phát hiện bất kỳ thay đổi nào được thực hiện trong hình ảnh (ví dụ: trong mã ứng dụng), vì vậy với thiết lập như vậy, nếu bạn muốn chạy phiên bản mới nhất của hình ảnh, bạn phải đảm bảo rằngbazel run //app1:image
đã được gọi trước khi chạybazel run //app1:container
.
Các Thực Hành Tốt Nhất
- Tổ chức mã: Hãy chắc chắn rằng mã của bạn được tổ chức tốt để dễ duy trì và mở rộng.
- Sử dụng phiên bản mới nhất: Luôn cập nhật các phụ thuộc của bạn để tận dụng các tính năng và cải tiến hiệu suất mới nhất.
- Kiểm tra kỹ lưỡng: Thực hiện kiểm tra với từng container để đảm bảo rằng mọi thứ hoạt động như mong đợi.
Các Cạm Bẫy Thường Gặp
- Bỏ qua phụ thuộc: Đảm bảo rằng tất cả các phụ thuộc cần thiết đều được xác định trong tệp MODULE.bazel.
- Gọi sai quy tắc: Đảm bảo gọi đúng các quy tắc của Bazel để tránh lỗi không mong muốn.
Mẹo Tối Ưu Hiệu Suất
- Sử dụng caching: Bazel hỗ trợ caching, giúp tăng tốc độ xây dựng cho các lần chạy sau.
- Tối ưu hóa hình ảnh: Giảm kích thước hình ảnh bằng cách loại bỏ các tệp không cần thiết.
Giải Quyết Sự Cố
- Lỗi không tìm thấy hình ảnh: Kiểm tra xem hình ảnh đã được tải thành công vào runtime chưa.
- Lỗi trong mã ứng dụng: Sử dụng logging để kiểm tra các lỗi trong mã ứng dụng.
Câu Hỏi Thường Gặp (FAQ)
1. Bazel là gì?
Bazel là một công cụ xây dựng mạnh mẽ hỗ trợ xây dựng và kiểm thử phần mềm.
2. Tại sao sử dụng Bazel với Docker?
Việc kết hợp Bazel và Docker giúp tối ưu hóa quy trình xây dựng và triển khai ứng dụng.
3. Có thể sử dụng Bazel cho ngôn ngữ khác không?
Có, Bazel hỗ trợ nhiều ngôn ngữ lập trình khác nhau.
Hy vọng rằng bài viết này sẽ giúp bạn hiểu rõ hơn về cách kết hợp Bazel với Docker. Nếu bạn có bất kỳ câu hỏi nào, đừng ngần ngại để lại ý kiến dưới bài viết này!