Một ứng dụng Linux 15-year tuổi vẫn chạy logic kinh doanh cốt lõi không phải là vấn đề cần giải quyết. Nó là tài sản mà không ai muốn đụng tới vì không ai còn hiểu hết mọi thứ nó làm nữa.
Bản thân ứng dụng hiếm khi là rủi ro. Môi trường xung quanh nó mới là rủi ro. Hệ điều hành cũ, các gói không được hỗ trợ, bước cấu hình thủ công và phụ thuộc không được ghi chép có thể biến mỗi lần triển khai thành một canh bạc nhỏ.
Một số tổ chức tìm đến đối tác phần mềm để hiện đại hóa hệ thống cũ mà không thay thế nó để quản lý rủi ro đó trực tiếp. Với những đội ngũ muốn container hóa ứng dụng Linux cũ nội bộ, container hóa có thể cung cấp một điểm khởi đầu có kiểm soát bằng cách đóng gói ứng dụng cùng các phụ thuộc runtime cần thiết vào một môi trường cô lập, tái tạo được.
Các bước dưới đây hướng dẫn quy trình theo thứ tự: đánh giá xem ứng dụng có phù hợp không, lập bản đồ phụ thuộc thực tế, xây dựng và kiểm tra một container hoạt động, rồi triển khai dần vào sản xuất.
Docker được dùng làm ví dụ ở đây, nhưng bạn cũng có thể dùng Podman.
Xác nhận ứng dụng có phù hợp không
Trước khi viết bất cứ thứ gì, hãy kiểm tra ứng dụng với hai danh sách.
Ứng dụng phù hợp thường có:
- Ranh giới ứng dụng rõ ràng
- Phụ thuộc tái tạo được (bạn có thể liệt kê những gì nó cần, dù danh sách dài)
- Giao diện mạng chuẩn (TCP, HTTP, hoặc tương tự)
- Yêu cầu lưu trữ quản lý được
- Không có phụ thuộc phần cứng bất thường
- Runtime chạy mà không cần đặc quyền host đặc biệt
Ứng dụng khó thường liên quan đến hành vi kernel cụ thể, driver phần cứng chuyên dụng, độ nhạy độ trễ cực cao, giấy phép gắn với máy vật lý, hoặc logic gắn trực tiếp với hệ điều hành host.
Nếu ứng dụng của bạn rơi phần lớn vào danh sách thứ hai, container hóa sẽ tốn nhiều công sức hơn lợi ích nó mang lại. Xác nhận danh sách nào mô tả hệ thống của bạn trước khi tiếp tục.
Kiểm kê mọi phụ thuộc
Bắt đầu với một checklist: phiên bản OS, runtime, thư viện dùng chung, gói đã cài, biến môi trường, tệp cấu hình, đường dẫn filesystem, cơ sở dữ liệu, API bên ngoài, cổng mạng, chứng chỉ, bí mật, cron jobs, tiến trình nền và vị trí lưu trữ.
Sau đó xác nhận checklist với những gì ứng dụng thực sự làm, dùng công cụ quan sát hành vi thực tế thay vì dựa vào trí nhớ hoặc tài liệu cũ.
Tìm mọi tệp tiến trình chạm vào khi chạy:
strace -f -e trace=open,openat -p <PID> 2>&1 | grep -v ENOENT
Lệnh này in ra mọi tệp ứng dụng mở, kể cả những tệp không ai ghi chú. Chạy nó trong lúc hoạt động bình thường, không chỉ lúc khởi động, vì cron jobs và tác vụ nền thường chạm vào các đường dẫn khác với tiến trình chính.
Tìm mọi kết nối mạng và file handle đang mở:
lsof -p <PID>
Lệnh này hiển thị socket đang mở, tệp đang mở và đường dẫn của chúng trong một lần. Hãy cross-reference dữ liệu này với checklist của bạn. Bất cứ thứ gì trong danh sách chưa được ghi chép là phụ thuộc ẩn.
Tìm những gì binary liên kết tới:
ldd /path/to/binary
Lệnh này liệt kê các thư viện dùng chung mà ứng dụng yêu cầu lúc runtime. Thư viện nào hiển thị "not found" ở đây là thư viện bạn cần cài trong image container.
strace tạo thêm overhead đáng kể và có thể làm chậm tiến trình khi đang đính kèm, vì vậy hãy giới hạn dùng nó trong những cửa sổ ngắn trên production. Nếu bạn cần quan sát hệ thống production đang chạy lâu hơn, công cụ dựa trên eBPF như bpftrace thu thập cùng dữ liệu syscall và truy cập tệp với overhead thấp hơn nhiều, vì nó chạy trong kernel thay vì chặn từng lời gọi.
Bốn loại chiếm hầu hết các bất ngờ mà bước này tiết lộ:
- Giả định filesystem. Đọc hoặc ghi vào
/etc/,/opt/,/var/lib/, hoặc/tmp/mà giả định các đường dẫn đó luôn tồn tại. - Giả định quyền. Chạy với quyền root, hoặc mong đợi một UID hoặc GID cụ thể mà người dùng mặc định của container không có.
- Giả định mạng. Gọi tới
localhostcho một dịch vụ từng chạy trên cùng máy vật lý, hoặc tham chiếu đến hostname cố định hoặc IP tĩnh. - Giả định OS. Phụ thuộc vào hành vi kernel cụ thể, timezone, locale, hoặc tiện ích hệ thống không được cài mặc định trong image tối giản.
Khi đầu ra của strace, lsof và ldd không còn phát hiện điều gì mới, việc kiểm kê đã đủ để bắt đầu xây dựng.
Chọn image nền
FROM ubuntu:latest không tự động là điểm khởi đầu đúng. Image nền là filesystem và bộ công cụ khởi đầu mà container xây dựng trên đó.
Khớp image nền với những gì bạn tìm thấy ở Bước 2, không phải với bản mới nhất:
# Check the glibc version the old server uses
ldd --version
# Check the OpenSSL version
openssl version
# Compare against what's available in a candidate base image
docker run --rm ubuntu:24.04 bash -c "ldd --version && openssl version"
Nếu các phiên bản gần nhau, image nền đó là điểm khởi đầu hợp lý. Nếu chúng cách nhau vài phiên bản chính, hãy mong đợi các thư viện mà kiểm tra ldd của bạn tìm thấy sẽ hoạt động khác, và lên kế hoạch kiểm tra cụ thể khi container chạy.
Ubuntu 26.04 LTS là bản phát hành hiện tại, nhưng nó không tự động là lựa chọn đúng cho việc di chuyển hệ thống cũ. Với ứng dụng 15-year tuổi, ubuntu:24.04 thường là điểm khởi đầu an toàn hơn. Đây vẫn là bản LTS được hỗ trợ đầy đủ. Bạn có thể chuyển sang 26.04 sau, khi container đã ổn định, nếu bạn muốn bộ công cụ mới hơn.
Viết Dockerfile đầu tiên và mong đợi nó thất bại
Bắt đầu với tối thiểu cần thiết để chạy ứng dụng, dựa trực tiếp vào kiểm kê ở Bước 2:
FROM ubuntu:24.04
# Install the runtime and OS packages identified during dependency analysis
RUN apt-get update && apt-get install -y \
libssl3 \
libpq5 \
<other-packages-from-your-inventory> \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY ./legacy-app /app/
EXPOSE 8080
CMD ["/app/legacy-app"]
Build và chạy nó:
docker build -t legacy-app:test .
docker run --rm -p 8080:8080 legacy-app:test
Tại thời điểm này, hãy mong đợi các lỗi cụ thể, có thể chẩn đoán được thay vì một lần khởi động sạch. Trạng thái container thành công không có nghĩa là ứng dụng hoạt động, vì health check chỉ xác nhận tiến trình đã khởi động.
Các lỗi first-run phổ biến và ý nghĩa của chúng:
Sửa từng lỗi một, build lại và chạy lại. Vòng lặp này, không phải Dockerfile ban đầu, là nơi hầu hết công việc thực sự diễn ra.
Đưa state ra ngoài container
Filesystem của container là tạm thời theo mặc định. Bất cứ thứ gì được ghi bên trong sẽ biến mất khi container bị xóa.
Xác định những gì ứng dụng thực sự cần lưu trữ (tệp tải lên, báo cáo được tạo, log, tệp cơ sở dữ liệu nếu ứng dụng không dùng cơ sở dữ liệu bên ngoài) và mount cụ thể những thứ đó:
docker run -d \
-v /host/data/uploads:/app/uploads \
-v /host/data/reports:/app/reports \
-p 8080:8080 \
legacy-app:test
Đừng mount toàn bộ filesystem máy chủ cũ như một lối tắt. Chỉ mount các đường dẫn cụ thể được xác định ở Bước 2 là cần lưu trữ. Mỗi mount bổ sung là một đường dẫn mà container phụ thuộc vào việc tồn tại đúng trên host, và mỗi cái nên là một lựa chọn có chủ đích.
Tách các tiến trình nền
Một hệ thống 15-year tuổi thường, trên thực tế, là một máy chủ chạy ứng dụng cộng với worker, cron jobs, script dọn dẹp và log rotation cùng lúc, vì không ai từng tách chúng ra.
Liệt kê mỗi tiến trình nền mà kiểm kê ở Bước 2 tìm thấy, và gán cho nó một trong các loại sau:
# Example docker-compose.yml separating responsibilities
services:
app:
image: legacy-app:latest
ports:
- "8080:8080"
worker:
image: legacy-app:latest
command: ["/app/run-worker.sh"]
cron:
image: legacy-app:latest
command: ["/app/run-cron.sh"]
Một mẫu container phổ biến là cấp cho mỗi trách nhiệm được quản lý độc lập một container riêng. Điều này giúp kiểm soát vòng đời, mở rộng, lỗi và giám sát dễ dàng hơn. Gộp các trách nhiệm được quản lý độc lập vào một container có thể tái tạo một số khớp nối vận hành mà bạn đang cố gắng loại bỏ.
Tăng cường bảo mật container trước khi đưa vào sản xuất
Container không tự động sửa các vấn đề bảo mật. Nó cô lập một tiến trình, nhưng tiến trình vẫn mang những rủi ro trước đó.
Chạy với người dùng non-root:
RUN groupadd -r appuser && useradd -r -g appuser appuser
USER appuser
Đặt giới hạn tài nguyên rõ ràng. Nếu không có, container có thể tiêu thụ nhiều CPU hoặc bộ nhớ host hơn dự kiến và ảnh hưởng đến các workload khác.
docker run -d \
--memory="512m" \
--cpus="1.0" \
legacy-app:production
Quét image để tìm lỗ hổng đã biết trước khi triển khai:
docker scout cves legacy-app:production
Lệnh này yêu cầu tài khoản Docker. Chạy docker login trước nếu bạn chưa xác thực CLI. Bạn cũng có thể dùng công cụ quét image ưa thích để xác định lỗ hổng đã biết trước khi triển khai.
Di chuyển bí mật ra khỏi image và biến môi trường, đưa vào trình quản lý bí mật phù hợp với nền tảng của bạn. Chỉ publish các cổng mà ứng dụng thực sự cần, và thực thi truy cập mạng qua container runtime, reverse proxy, tường lửa hoặc nền tảng orchestration.
Xác thực so với hệ thống cũ
Ứng dụng cũ là đường cơ sở cho hành vi đúng. Container phải khớp với nó, không chỉ khởi động thành công.
Kiểm tra cùng các quy trình làm việc trên cả hai hệ thống và so sánh:
Chạy so sánh tải cơ bản để phát hiện suy giảm hiệu suất sớm:
# Example using Apache Bench against both systems
ab -n 1000 -c 10 http://legacy-server:8080/
ab -n 1000 -c 10 http://localhost:8080/
Đừng chuyển sang rollout cho đến khi mọi quy trình làm việc áp dụng đã được xác thực và mọi khác biệt so với đường cơ sở cũ đều được hiểu rõ.
Bạn cũng có thể dùng công cụ load-testing phù hợp với workload thực tế của ứng dụng và so sánh thời gian phản hồi, throughput, CPU, bộ nhớ và tỷ lệ lỗi giữa phiên bản cũ và phiên bản container hóa.
Triển khai dần dần
Chuyển toàn bộ lưu lượng production từ hệ thống cũ sang hệ thống mới trong một khoảnh khắc là cách triển khai rủi ro nhất.
Chạy cả hai hệ thống song song sau một reverse proxy và chuyển lưu lượng dần:
upstream backend {
server legacy-server:8080 weight=9;
server container-app:8080 weight=1;
}
Tăng trọng số của container khi sự tự tin tăng lên, và theo dõi tỷ lệ lỗi cũng như thời gian phản hồi ở mỗi bước.
Nếu ứng dụng đang được tách thành các phần nhỏ hơn theo thời gian thay vì di chuyển như một khối, mẫu strangler fig áp dụng: một proxy trước cả hai hệ thống định tuyến mỗi yêu cầu đến ứng dụng cũ hoặc thành phần mới cụ thể đã thay thế chức năng đó. Cách này cho phép bạn di chuyển từng khả năng một. Nó hữu ích khi bạn phân rã chức năng dần dần, không phải khi bạn di chuyển một ứng dụng nguyên vẹn vào container như một bước duy nhất.
Trước bất kỳ cutover nào, hãy viết ra thành công và thất bại trông như thế nào, bạn sẽ giám sát gì, cách bạn chuyển lưu lượng trở lại nếu cần, và ai có thẩm quyền đưa ra quyết định đó.
Một cuộc di chuyển an toàn không phải là nơi không thể thất bại. Nó là nơi thất bại có một đường đi có kiểm soát, pre-defined để quay về trạng thái hoạt động.
Container hóa sửa được gì, và không sửa được gì
Container hóa thường cải thiện cô lập phụ thuộc, tái tạo môi trường, nhất quán triển khai, tính di động, tốc độ rollback và tích hợp CI/CD.
Nó không sửa kiến trúc tồi, nợ kỹ thuật hiện có, logic kinh doanh lỗi thời, độ phủ kiểm thử kém, thiết kế nguyên khối, vấn đề cơ sở dữ liệu tiềm ẩn, hoặc quy tắc kinh doanh chưa từng được ghi chép.
Hiểu sự khác biệt này ngăn chặn một sai lầm phổ biến: coi container hóa như sự thay thế cho công việc kiến trúc mà nó chưa bao giờ có ý định làm.
Khi nào nên container hóa trước, và khi nào không
Container hóa trước khi ứng dụng hoạt động và môi trường là vấn đề thực sự, khi phụ thuộc có thể tái tạo đáng tin cậy, khi workload không yêu cầu truy cập host bất thường, và khi viết lại toàn bộ sẽ đưa vào nhiều rủi ro hơn là loại bỏ.
Cân nhắc một hướng khác khi kiến trúc chính là nút thắt, khi ứng dụng phụ thuộc vào hành vi OS không còn được hỗ trợ, khi truy cập phần cứng là thiết yếu cho cách nó hoạt động, khi thay thế hoàn toàn sẽ rẻ hơn, hoặc khi các yêu cầu kinh doanh mà nó được xây dựng đã thay đổi cơ bản.
Không có câu trả lời đúng phổ quát. Lựa chọn đúng phụ thuộc vào điều kiện nào trong số này thực sự mô tả hệ thống của bạn.
Container hóa là một bước, không phải đích đến
Một container hoạt động là nền tảng, không phải đích đến. Nó thường trở thành cơ sở cho tự động hóa CI/CD, kiểm thử tốt hơn, quan sát được cải thiện và công việc hiện đại hóa tiếp theo, nếu và khi điều đó trở nên cần thiết.
Một ứng dụng Linux cũ không cần phải viết lại trước khi có thể hiện đại hóa. Container hóa có thể là bước đầu tiên có kiểm soát hướng tới một môi trường tái tạo hơn, di động hơn và dễ bảo trì hơn, mà không đụng đến logic kinh doanh mà production đang phụ thuộc.



