avatar

Đăng vào

Docker Anti Patterns

Tác giả

Docker Anti Pattern

Container đang bùng nổ và trở thành một công nghệ được sử dụng rộng rãi trong những năm gần đây. Ngay cả khi bạn chưa tin rằng Kubernetes sẽ là công nghệ tốt trong tương lai, thì việc sử dụng Docker thôi cũng mang lại nhiều giá trị. Container có thể đơn giản hóa cả việc triển khai và các CI/CD pipelines.

Trang web chính thức của Docker trình bày về các phương pháp tốt nhất của Docker và thường nó sẽ có tính kỹ thuật cao, tập trung nhiều hơn vào cấu trúc của Dockerfile thay vì thông tin chung về cách sử dụng container. Một người mới sử dụng Docker đến một lúc nào đó sẽ hiểu về các layer của Docker, cách chúng được lưu vào bộ nhớ cache và cách tạo Docker image nhỏ. Việc sử dụng multi-stage builds cũng không quá khó.

Tuy nhiên, vấn đề chính của việc sử dụng container là các công ty không có khả năng nhìn vào bức tranh lớn hơn và đặc biệt là tính chất bất biến của container/image. Một số công ty đặc biệt cố gắng chuyển đổi các quy trình dựa trên VM hiện có của họ thành các thùng chứa với kết quả không rõ ràng. Có rất nhiều thông tin về các chi tiết low-level của container (cách tạo và chạy chúng), nhưng rất ít thông tin về các phương pháp tốt nhất ở high-level.

Để thu hẹp khoảng cách của các tài liệu này, tôi sẽ trình bày danh sách các phương pháp tốt nhất của Docker ở high-level. Hy vọng rằng, bài viết sẽ cung cấp cho bạn một số hiểu biết về cách bạn nên sử dụng container.

Dưới đây là danh sách đầy đủ các phương pháp không tốt mà chúng ta sẽ kiểm tra:

Mục lục

Anti-pattern 1 - Coi Docker container như máy ảo (VM)

Trước khi đi vào một số ví dụ thực tế hơn, chúng ta hãy tìm hiểu lý thuyết cơ bản trước. Các container không phải là máy ảo. Thoạt nhìn, chúng có thể trông như chúng hoạt động giống VM nhưng sự thật lại hoàn toàn khác với những câu hỏi như:

  1. Làm cách nào để cập nhật các ứng dụng đang chạy bên trong container?
  2. Làm cách nào để ssh vào Docker container?
  3. Làm cách nào để lấy nhật ký/tệp từ container?
  4. Làm cách nào để áp dụng các bản sửa lỗi bảo mật bên trong container?
  5. Làm cách nào để chạy nhiều chương trình trong một container?

Tất cả những câu hỏi này đều đúng về mặt kỹ thuật, và những người đã trả lời chúng cũng đã đưa ra những câu trả lời chính xác về mặt kỹ thuật.

Làm cách nào để tôi có thể bỏ tất cả các phương pháp, quy trình áp dụng với VM để thay đổi quy trình làm việc với các container không trạng thái, bất biến, tồn tại trong thời gian ngắn (stateless, immutable, short-lived) thay vì các máy ảo có trạng thái, hoạt động lâu dài, có thể thay đổi (stateful, long-running, mutable)?

Nhiều công ty ngoài kia đang cố gắng sử dụng lại các phương pháp/công cụ/kiến thức tương tự từ máy ảo cho container. Một số công ty thậm chí chưa hoàn thành việc chuyển đổi từ bare-metal sang VM khi các container xuất hiện.

Việc học lại điều gì đó là rất khó. Hầu hết những người bắt đầu sử dụng container đều xem chúng ban đầu như một lớp trừu tượng bổ sung bên trên các phương pháp hiện có của họ:

Containers không phải VMs

Containers không phải VMs (Nguồn: codefresh.io)

Trong thực tế, containers yêu cầu một cái nhìn hoàn toàn khác và thay đổi các quy trình hiện có.

Container yêu cầu một cách suy nghĩ mới

Container yêu cầu một cách suy nghĩ mới (Nguồn: codefresh.io)

Không có cách khắc phục nào dễ dàng cho anti-pattern này ngoài việc học và hiểu về bản chất của các containers, các khối xây dựng và lịch sử của chúng.

Nếu bạn thường xuyên thấy mình muốn ssh đến các container đang chạy để "nâng cấp" chúng hoặc lấy nhật ký/tệp theo cách thủ công, bạn chắc chắn đang sử dụng Docker sai cách và bạn cần đọc thêm về cách các container hoạt động.

Anti-pattern 2 - Tạo Docker image không trong suốt

Một Dockerfile phải minh bạch và độc lập. Nó phải mô tả rõ ràng tất cả các thành phần của một ứng dụng. Bất kỳ ai cũng có thể nhận được cùng một Dockerfile và tạo lại cùng một image. Các bước "magic" nên được tránh.

Dưới đây là một ví dụ không tốt:

FROM alpine:3.4

RUN apk add --no-cache \
      ca-certificates \
      pciutils \
      ruby \
      ruby-irb \
      ruby-rdoc \
      && \
    echo http://dl-4.alpinelinux.org/alpine/edge/community/ >> /etc/apk/repositories && \
    apk add --no-cache shadow && \
    gem install puppet:"5.5.1" facter:"2.5.1" && \
    /usr/bin/puppet module install puppetlabs-apk

# Install Java application
RUN /usr/bin/puppet agent --onetime --no-daemonize

ENTRYPOINT ["java","-jar","/app/spring-boot-application.jar"]

Đừng hiểu lầm, tôi thích Puppet vì nó là một công cụ tuyệt vời (hoặc Ansible hoặc Chef cho vấn đề đó). Việc lạm dụng nó để triển khai ứng dụng có thể dễ dàng với máy ảo, nhưng với container thì điều đó thật tệ hại.

Trước hết, nó làm cho Dockerfile này phụ thuộc vào vị trí của host. Bạn phải xây dựng nó trên một máy tính có quyền truy cập vào máy chủ Puppet trên môi trường production. Máy trạm của bạn có quyền truy cập vào máy Puppet trên môi trường production không? Nếu có, máy trạm của bạn có thực sự nên quyền truy cập vào máy chủ Puppet trên môi trường production không?

Nhưng vấn đề lớn nhất là Docker image này không thể được tạo lại dễ dàng. Nội dung của nó phụ thuộc vào những gì máy chủ Puppet có tại thời điểm build ban đầu. Nếu bạn xây dựng cùng một Dockerfile vào một ngày khác, bạn có thể nhận được một image hoàn toàn khác. Và nếu bạn không có quyền truy cập đến máy chủ Puppet hoặc máy Puppet không hoạt động, bạn thậm chí không thể build image ngay từ đầu. Bạn thậm chí không biết phiên bản của ứng dụng là gì nếu bạn không có quyền truy cập vào các puppet script.

Nhóm tạo Dockerfile này đã hơi lười biếng. Đã có một puppet script để cài đặt ứng dụng trong máy ảo. Dockerfile chỉ để làm điều tương tự (xem anti-pattern trước).

Cách khắc phục ở đây là có các Dockerfiles tối giản mô tả rõ ràng những gì chúng làm. Đây là ví dụ tương tự tự với một Dockerfile "nghiêm chỉnh".

FROM openjdk:8-jdk-alpine

ENV MY_APP_VERSION="3.2"

RUN apk add --no-cache \
ca-certificates

WORKDIR /app
ADD  http://artifactory.mycompany.com/releases/${MY_APP_VERSION}/spring-boot-application.jar .

ENTRYPOINT ["java","-jar","/app/spring-boot-application.jar"]

Chú ý rằng:

  • Không có sự phụ thuộc vào cơ sở hạ tầng puppet. Dockerfile có thể được xây dựng trên bất kỳ máy tính nào có quyền truy cập vào kho lưu trữ nhị phân.
  • Các phiên bản của phần mềm được xác định rõ ràng.
  • Rất dễ dàng để thay đổi phiên bản của ứng dụng bằng cách chỉ chỉnh sửa Dockerfile (thay vì các puppet script).
  • Đây chỉ là một ví dụ rất đơn giản. Tôi đã thấy nhiều Dockerfiles phụ thuộc vào các công thức "magic" với các yêu cầu đặc biệt về thời gian và địa điểm chúng có thể được tạo ra. Vui lòng không viết Dockerfiles của bạn theo cách này, vì các nhà phát triển (và những người khác không có quyền truy cập vào tất cả các hệ thống) sẽ gặp khó khăn lớn trong việc tạo Docker image trong môi trường local của họ.

Một giải pháp thay thế tốt hơn sẽ là nếu Dockerfile tự biên dịch mã nguồn Java (sử dụng multi-stage builds). Điều đó sẽ cung cấp cho bạn khả năng thể hiện rõ hơn về những gì đang xảy ra trong Docker image.

Anti-pattern 3 - Tạo các Dockerfiles có ảnh hưởng tới bên ngoài

Hãy tưởng tượng rằng bạn là kỹ sư vận hành/SRE đang làm việc tại một công ty rất lớn, nơi sử dụng nhiều ngôn ngữ lập trình.Sẽ rất khó để trở thành một chuyên gia về tất cả các ngôn ngữ lập trình và các hệ thống build sử dụng ở đó.

Đây là một trong những lợi thế của việc áp dụng container. Bạn có thể tải xuống bất kỳ tệp Dockerfile nào từ bất kỳ nhóm phát triển nào và build tệp đó mà không cần quan tâm đến các ảnh hưởng bên ngoài nó có thể tạo ra (vì không nên có bất kỳ ảnh hưởng nào).

Việc build Docker image phải là một hoạt động lý tưởng, không quan trọng việc bạn build cùng một Dockerfile một lần hay một nghìn lần, hoặc nếu bạn xây dựng nó trên máy chủ CI trước và sau đó trên máy trạm của bạn.

Tuy nhiên, có một số Dockerfiles ngoài đó trong giai đoạn build sẽ:

  1. Thực hiện các git commit hoặc các thao tác khác với git
  2. Dọn dẹp hoặc xáo trộn cơ sở dữ liệu
  3. Gọi các dịch vụ bên ngoài với các thao tác POST/PUT (thay đổi dữ liệu)

Container cung cấp khả năng cách ly (isolation) với đến hệ thống tệp của host nhưng không có gì đảm bảo rằng Dockerfile sẽ không có chỉ thị RUN gửi một HTTP request cùng payload đến mạng nội bộ của bạn.

Đây là một ví dụ đơn giản trong đó một Dockerfile thực hiện việc đóng gói (hành động an toàn) và công khai (hành động không an toàn) một ứng dụng npm trong một lần chạy.

FROM node:9
WORKDIR /app

COPY package.json ./package.json
COPY package-lock.json ./package-lock.json
RUN npm install
COPY . .

RUN npm test

ARG npm_token

RUN echo "//registry.npmjs.org/:_authToken=${npm_token}" > .npmrc
RUN npm publish --access public

EXPOSE 8080
CMD [ "npm", "start" ]

Dockerfile này gây nhầm lẫn giữa hai mối quan tâm khác nhau, phát hành một phiên bản của ứng dụng và tạo Docker image cho nó. Có thể đôi khi hai hành động này thực sự xảy ra cùng một lúc, nhưng đây không phải là lý do để làm một Dockerfile trở nên không gọn gàng với các ảnh hưởng phụ.

Docker KHÔNG phải là một hệ thống CI nói chung và nó chưa bao giờ giống như vậy. Đừng lạm dụng Dockerfile như những tập lệnh bash. Dù việc có những ảnh hưởng phụ trong container run time là điều chấp nhận được nhưng những ảnh hưởng đó được tạo ra khi trong container build time thì không.

Giải pháp là đơn giản hóa các tệp Dockerfile của bạn và đảm bảo rằng chúng chỉ chứa các logic đơn giản như:

  • Clone mã nguồn
  • Download các dependency
  • Biên dịch và đóng gói mã Nguồn
  • Xử lý/Thu gọn/Biến đổi các tài nguyên cục bộ (bên trong container)
  • Chạy các scripts và sửa các tệp trên hệ thống file của container

Ngoài ra, hãy nhớ rằng cách Docker cache các layer của filesystem. Docker giả định rằng nếu một layer và các layer trước đó chưa "thay đổi" thì chúng có thể được sử dụng lại từ bộ nhớ cache. Nếu các lệnh Dockerfile của bạn có các ảnh hưởng phụ, về cơ bản, bạn đã phá vỡ cơ chế cache của Docker.

FROM node:10.15-jessie

RUN apt-get update && apt-get install -y mysql-client && rm -rf /var/lib/apt

RUN mysql -u root --password="" < test/prepare-db-for-tests.sql

WORKDIR /app

COPY package.json ./package.json
COPY package-lock.json ./package-lock.json
RUN npm install
COPY . .

RUN npm integration-test

EXPOSE 8080
CMD [ "npm", "start" ]

Giả sử rằng bạn cố gắng xây dựng tệp Dockerfile này và các unit-test của bạn không thành công. Bạn thực hiện thay đổi đối với mã nguồn và build lại. Docker sẽ giả định rằng layer xoá DB đã "chạy" và nó sẽ sử dụng lại bộ nhớ cache. Vì vậy, các unit-test của bạn bây giờ sẽ chạy trong một DB chưa được xoá và chứa dữ liệu từ lần chạy trước.

Trong ví dụ này, Dockerfile rất nhỏ và sẽ dễ dàng xác định vị trí câu lệnh có ảnh hưởng phụ (lệnh mysql) và di chuyển nó đến đúng vị trí để sửa bộ nhớ cache của layer, cố gắng tìm đúng thứ tự của câu lệnh RUN rất khó nếu bạn không biết câu nào có ảnh hưởng phụ và câu nào không.

Dockerfiles của bạn sẽ đơn giản hơn nhiều nếu tất cả các hành động mà chúng thực hiện ở chế độ chỉ đọc và với phạm vi cục bộ.

Anti-pattern 4 - Nhầm lẫn giữa image được sử dụng để phát triển với những image được sử dụng để triển khai

Trong bất kỳ công ty nào đã sử dụng container, thường có hai danh mục Docker image riêng biệt. Đầu tiên, có những image được sử dụng làm artifact để triển khai trên các máy chủ sản xuất.

Các image dùng để triển khai phải chứa:

  1. Mã ứng dụng ở dạng đã được rút gọn/biên dịch kèm với các phụ thuộc cho run time của nó.
  2. Không có gì khác, thực sự không có thêm bất cứ thứ gì khác.

Loại thứ hai là các image được sử dụng cho các hệ thống CI/CD hoặc dành cho các kỹ sư phát triển, cần có:

  1. Mã nguồn ở dạng ban đầu (VD: chưa được thu gọn)
  2. Trình biên dịch/bộ thu nhỏ/bộ chuyển đổi (Compilers/minifiers/transpilers)
  3. Framework kiểm thử/các công cụ báo cáo
  4. Quét bảo mật, quét chất lượng, máy phân tích tĩnh (Security scanning, quality scanning, static analyzers)
  5. Các công cụ tích hợp với cloud
  6. Các tiện ích khác cần thiết cho CI/CD pipeline

Rõ ràng là các danh mục container image này nên được tách biệt vì chúng có các mục đích khác nhau. Image được triển khai tới máy chủ sản xuất phải ở mức tối giản, an toàn và chắc chắn. Image được sử dụng trong quy trình CI/CD thì không bao giờ nên được triển khai ở bất cứ đâu nên chúng có yêu cầu ít nghiêm ngặt hơn nhiều (về kích thước và bảo mật).

Tuy nhiên, vì một số lý do, mọi người không phải lúc nào cũng hiểu sự khác biệt này. Tôi đã thấy một số công ty cố gắng sử dụng cùng một image Docker cho cả quy trình phát triển và triển khai. Hầu như các tiện ích và framework không liên quan cuối cùng lại xuất hiện trong Docker image của môi trường sản xuất.

Có chính xác 0 lý do cho việc Docker image ở môi trường sản xuất cần có git, framework kiểm thử hoặc trình biên dịch/trình thu nhỏ.

Việc sử dụng một artifact chung để triển khai luôn là giữa các môi trường để đảm bảo rằng những gì bạn đang kiểm thử cũng là những gì bạn sẽ triển khai (sẽ được nói rõ hơn ở phần sau) là một điều nên làm. Nhưng cố gắng cố gắng sử dụng những gì ở môi trường local trong quá trình phát triển cho môi trường sản xuất thì không tốt một chút nào.

Tóm lại, hãy cố gắng hiểu vai trò của các Docker image mà bạn sử dụng. Mỗi image nên có một vai trò duy nhất. Nếu bạn đang chuyển các framework/thư viện kiểm thử đến môi trường sản xuất thì bạn đang làm sai. Bạn cũng nên dành một chút thời gian để tìm hiểu và sử dụng multi-stage builds

Anti-pattern 5 - Sử dụng các image khác nhau cho từng môi trường (qa, stage, production)

Một trong những lợi thế quan trọng nhất của việc sử dụng container là thuộc tính bất biến của chúng. Điều này có nghĩa là Docker image chỉ nên được xây dựng một lần và sau đó được chuyển tới các môi trường khác nhau cho đến khi nó tới môi trường sản xuất.

Sử dụng cùng một docker image

Bởi vì với các image giống nhau sẽ được chuyển tới các môi trường dưới dạng một thực thể duy nhất, bạn có được đảm bảo rằng những gì bạn đang thử nghiệm trong một môi trường giống với môi trường khác.

Tôi thấy rất nhiều công ty xây dựng các artifact khác nhau cho từng môi trường của họ với các phiên bản hoặc cấu hình khác nhau.

Sử dụng image khác nhau cho từng môi trường

Điều này sẽ phát sinh vấn đề vì không có gì đảm bảo rằng các image “đủ giống nhau” và đảm bảo chúng hoạt động giống nhau. Nó cũng mở ra rất nhiều khả năng lạm dụng, nơi các kỹ sư phát triển/vận hành đang lén sử dụng các công cụ gỡ lỗi bổ sung trong các image của môi trường không sản xuất, tạo ra sự khác nhau thậm chí còn lớn hơn giữa các image cho các môi trường khác nhau.

Thay vì cố gắng đảm bảo rằng các image khác nhau của bạn giống nhau nhất có thể, việc sử dụng một image duy nhất cho tất cả các giai đoạn vòng đời phần mềm sẽ dễ dàng hơn nhiều.

Lưu ý rằng hoàn toàn bình thường nếu các môi trường khác nhau sử dụng các cài đặt khác nhau (VD: các biến bí mật và biến cấu hình) như chúng ta sẽ thấy ở phần sau của bài viết này. Tuy nhiên, mọi thứ khác phải hoàn toàn giống nhau.

Anti-pattern 6 - Tạo Docker image trên máy chủ sản xuất

Docker registry đóng vai trò là danh mục các ứng dụng hiện có có thể được sử dụng để triển khai bất kỳ lúc nào cho bất kỳ môi trường nào. Nó cũng đóng vai trò là trung tâm của các assets trong ứng dụng với các metadata cùng với các phiên bản của một ứng dụng. Sẽ rất dễ dàng để chọn một tag cụ thể của Docker image và triển khai nó tới bất kỳ môi trường nào.

Một trong những cách linh hoạt nhất để sử dụng Docker registry là promote các Docker image giữa các registry. Một tổ chức có ít nhất registry (môi trường phát triển và môi trường sản xuất). Docker image nên được tạo một lần (xem anti-pattern trước) và được đặt ở registry cho môi trường phát triển. Sau đó, khi image vượt qua các bài kiểm tra tích hợp, quét bảo mật và các kiểm tra chất lượng, nó có thể được promote vào Docker registry của môi trường sản xuất để được gửi đến máy chủ sản xuất hoặc Kubernetes cluster.

Cũng có thể có các tổ chức khác nhau có các Docker registry theo khu vực/vị trí hoặc theo bộ phận. Điểm chính ở đây là tiêu chuẩn cho việc triển khai Docker image sẽ bao gồm Docker registry. Các Docker registry vừa đóng vai trò là kho lưu trữ nội dung ứng dụng vừa là nơi lưu trữ trung gian trước khi một ứng dụng được triển khai vào môi trường sản xuất.

Tuy nhiên có một thực hành không tốt là việc loại bỏ hoàn toàn các Docker registry khỏi vòng đời của image và đẩy mã nguồn trực tiếp đến các máy chủ sản xuất.

Xây dựng image trong máy chủ sản xuất

Máy chủ sản xuất sử dụng git pull để lấy mã nguồn và sau đó xây dựng Docker để tạo một image nhanh chóng và chạy nó (thường với docker-compose hoặc các orchestration khác). "Phương pháp triển khai" này về cơ bản sử dụng anti-pattern cùng một lúc!

Quá trình triển khai này gặp phải rất nhiều vấn đề, bắt đầu từ vấn đề bảo mật. Máy chủ sản xuất không được có quyền truy cập vào kho git repositories của bạn. Nếu một công ty nghiêm túc về bảo mật, mô hình này thậm chí sẽ không được thông qua tại nhóm bảo mật. Máy chủ sản xuất không có bất cứ lý do nào để cài đặt git. Git (hoặc bất kỳ hệ thống kiểm soát phiên bản nào khác) là một công cụ dành cho sự cộng tác của nhóm phát triển và không phải là một giải pháp phân phối.

Nhưng vấn đề quan trọng nhất là với "phương pháp triển khai" này, bạn bỏ qua hoàn toàn Docker registry. Bạn không còn biết Docker image nào được triển khai trên máy chủ của mình vì không còn trung tâm lưu giữ Docker images nữa.

Phương pháp triển khai này có thể hoạt động tốt với một start up, nhưng sẽ nhanh chóng trở nên kém hiệu quả khi các bản cài đặt trở nên lớn hơn. Bạn cần tìm hiểu cách sử dụng Docker registry và những lợi ích mà chúng mang lại (cũng liên quan đến việc quét bảo mật các containers).

Sử dụng Docker registry

Docker registry có một API rất rõ ràng và có một số sản phẩm mã nguồn mở có thể được sử dụng để thiết lập registry trong tổ chức của bạn.

Cũng lưu ý rằng với Docker registry mã nguồn của bạn nằm an toàn phía sau tường lửa và không bao giờ rời khỏi nó.

Anti-pattern 7 - Làm việc với git hash thay vì Docker image

Hệ quả của hai anti-pattern trước đó là khi bạn sử dụng container, Docker registry của bạn sẽ trở thành single point of truth cho mọi thứ. Mọi người nên nói về Docker tags và image được promote. Nhóm phát triển và nhóm vận hành nên sử dụng container làm ngôn ngữ chung của họ. Thứ được chuyển giao giữa các nhóm phải là một container chứ không phải một git hash.

Nói về git hash

Điều này trái ngược với cách cũ sử dụng git hash như là một artifact được promote. Mã nguồn tất nhiên là rất quan trọng, nhưng việc xây dựng lại nhiều lần cùng một hash để promote là một sự lãng phí tài nguyên (xem thêm anti-pattern 5). Một số công ty nghĩ rằng các container chỉ nên được xử lý nhóm vấn hành còn nhóm phát triển chỉ làm việc với mã nguồn. Điều này khác xa sự thật. Container là cơ hội hoàn hảo cho nhóm phát triển và nhóm điều hành làm việc cùng nhau.

Nói về container

Lý tưởng nhất là nhóm vận hành thậm chí không nên quan tâm đến những gì đang diễn ra với git repo của một ứng dụng. Tất cả những gì họ cần biết là liệu Docker image mà họ có trong tay đã sẵn sàng để đưa vào môi trường sản xuất hay chưa. Họ không nên bị bắt phải xây dựng lại một git hash để có được Docker image giống như nhóm phát triển đang sử dụng trong môi trường tiền sản xuất.

Bạn có thể hiểu liệu mình có phải là nạn nhân của anti-pattern này hay không bằng cách hỏi nhóm vận hành trong tổ chức của bạn. Nếu họ bị buộc phải làm quen với các ứng dụng nội bộ như hệ thống xây dựng hoặc framework kiểm thử, những thứ thường không liên quan đến runtime của ứng dụng, họ sẽ có những công việc nặng nề mà không cần thiết hàng ngày.

Anti-pattern 8 - Hard-code các giá trị bí mật và cấu hình trong Docker image

Anti-pattern này có liên quan chặt chẽ với Anti-pattern 5 (các image khác nhau trên mỗi môi trường). Trong hầu hết các trường hợp khi tôi hỏi các công ty tại sao họ cần các image khác nhau cho qa/stage/prod, câu trả lời thông thường là chúng chứa các cấu hình và giá trị bí mật khác nhau.

Điều này không chỉ phá vỡ lời hứa chính của Docker (triển khai những gì bạn đã thử nghiệm) mà còn làm cho tất cả các đường ống CI/CD trở nên rất phức tạp vì chúng phải quản lý bí mật/cấu hình trong thời gian xây dựng (build-time)

Tất nhiên, chống lại mô hình ở đây là hard-code các cấu hình. Các ứng dụng không được có cấu hình nhúng cùng với chúng. Điều này không mới với bất kỳ ai đã quen thuộc với 12-factor apps

Cấu hình hard-code tại thời điểm xây dựng

Các ứng dụng của bạn nên tìm nạp cấu hình trong thời gian chạy (run-time) thay vì thời gian xây dựng (build-time). Docker image phải là cấu hình bất khả tri (agnostic). Chỉ cấu hình trong thời gian chạy mới được "đính kèm" vào container. Đối với cấu hình thời gian chạy (configmaps, zookeeper, consul, v.v.) và bí mật (vault, keywhiz , tâm sự, cerberus).

Đang tải cấu hình trong thời gian chạy Đang tải cấu hình trong thời gian chạy Nếu image Docker của bạn có các IP và / hoặc thông tin xác thực được mã hóa cứng, bạn chắc chắn đang làm sai.

Các ứng dụng của bạn nên tìm nạp cấu hình trong thời gian chạy thay vì thời gian xây dựng. image Docker phải là cấu hình bất khả tri. Chỉ trong khi cấu hình thời gian chạy mới được "đính kèm" vào container. Có nhiều giải pháp cho việc này và hầu hết các hệ thống phân cụm hoặc hệ thống triển khai đều có thể hoạt động với các giải pháp cho cấu hình trong thời gian chạy (configmaps, Zookeeper, consul, v.v.) hay giá trị bí mật (vault, keywhiz, confidant, cerberus).

Đang tải cấu hình trong thời gian chạy

Nếu image Docker của bạn có các IP hoặc/và thông tin xác thực được hard-code, bạn chắc chắn đang làm sai.

Anti-pattern 9 - Tạo Dockerfile làm quá nhiều thứ

Tôi đã xem qua các bài báo đề xuất rằng Dockerfile nên được sử dụng như một giải pháp CI cho người nghèo. Đây là một ví dụ thực tế về một Dockerfile duy nhất.

# Run Sonar analysis
FROM newtmitch/sonar-scanner AS sonar
COPY src src
RUN sonar-scanner
# Build application
FROM node:11 AS build
WORKDIR /usr/src/app
COPY . .
RUN yarn install \
 yarn run lint \
 yarn run build \
 yarn run generate-docs
LABEL stage=build
# Run unit test
FROM build AS unit-tests
RUN yarn run unit-tests
LABEL stage=unit-tests
# Push docs to S3
FROM containerlabs/aws-sdk AS push-docs
ARG push-docs=false
COPY --from=build docs docs
RUN [[ "$push-docs" == true ]] && aws s3 cp -r docs s3://my-docs-bucket/
# Build final app
FROM node:11-slim
EXPOSE 8080
WORKDIR /usr/src/app
COPY --from=build /usr/src/app/node_modules node_modules
COPY --from=build /usr/src/app/dist dist
USER node
CMD ["node", "./dist/server/index.js"]

Dù thoạt nhìn, Dockerfile này có thể trông giống như một cách sử dụng tốt của multi-stages build, nhưng về cơ bản nó là sự kết hợp của các anti-pattern trước đó.

Nó giả định sự hiện diện của một máy chủ SonarQube (anti-pattern 2). Nó có những tác dụng phụ tiềm ẩn vì nó có thể đẩy file lên S3 (anti-pattern 3). Nó vừa đóng vai trò là image dùng để phát triển vừa là image dùng để triển khai (anti-pattern 4).

Docker không phải là một hệ thống CI. Công nghệ container có thể được sử dụng như một phần của đường ống CI/CD, nhưng kỹ thuật này là một cái gì đó hoàn toàn khác. Đừng nhầm lẫn các lệnh cần chạy trong Docker container với các lệnh cần chạy trong một CI build job.

Tác giả của Dockerfile này cho rằng bạn nên sử dụng các đối số để tương tác với các nhãn và thực hiện/bỏ qua các giai đoạn build cụ thể (VD: bạn có thể tắt sonar). Nhưng cách tiếp cận này chỉ là thêm phức tạp hơn thay vì làm mọi thứ đơn giản, tiện lợi hơn.

Cách khắc phục Dockerfile này là chia nhỏ thành 5 Dockerfile khác. Một được sử dụng để triển khai ứng dụng và tất cả các tệp khác là các bước khác nhau trong đường ống CI/CD của bạn. Một Dockerfile duy nhất nên có một mục đích/mục tiêu duy nhất.

Anti-pattern 10 - Tạo tệp Docker quá đơn giản

Bởi vì container cũng bao gồm các phần dependency của chúng, chúng thường được dùng để cô lập các phiên bản cho các thư viện và framework cho các ứng dụng. Các nhà phát triển đã quá quen với vấn đề khi cố gắng cài đặt nhiều phiên bản khác nhau của cùng một công cụ trên máy trạm của họ. Docker hứa hẹn sẽ giải quyết vấn đề này bằng cách cho phép bạn mô tả trong Dockerfile của bạn chính xác những gì ứng dụng của bạn cần và không có gì khác.

Nhưng nó chỉ đúng nếu bạn thực sự tuân thủ nó. Là một kỹ sư vận hành, bạn không thực sự quan tâm đến công cụ lập trình bạn sử dụng trong image Docker của mình. Tôi có thể tạo image Docker của một ứng dụng Java, sau đó là Python một và sau đó là một NodeJs, mà không thực sự có môi trường phát triển cho từng ngôn ngữ trên máy tính xách tay của tôi.

Tuy nhiên, nhiều công ty vẫn xem Docker là một định dạng gói và chỉ sử dụng nó để đóng gói một artifact/ứng dụng hoàn chỉnh đã được tạo bên ngoài container. Anti-pattern này rất nổi tiếng với các tổ chức sử dụng Java rộng rãi và ngay cả tài liệu chính thức dường như cũng quảng bá nó.

Đây là Dockerfile được đề xuất từ hướng dẫn chính thức của Spring Boot Docker.

FROM openjdk:8-jdk-alpine
VOLUME /tmp
ARG JAR_FILE
COPY ${JAR_FILE} app.jar
ENTRYPOINT ["java","-Djava.security.egd=file:/dev/./urandom","-jar","/app.jar"]

Dockerfile này chỉ đóng gói một tệp jar hiện có. Tệp Jar được tạo như thế nào? Không ai biết. Nó không được mô tả trong Dockerfile. Nếu tôi là người vận hành, tôi buộc phải cài đặt tất cả các thư viện phát triển Java chỉ để tạo Dockerfile này. Nếu bạn làm việc trong một tổ chức làm việc với nhiều ngôn ngữ lập trình, quá trình này nhanh chóng vượt ra khỏi tầm kiểm soát không chỉ đối với các kỹ sư vận hành mà còn đối với build nodes.

Tôi sử dụng Java làm ví dụ nhưng anti-pattern này cũng xuất hiện trong các tình huống khác. Dockerfiles không hoạt động nếu bạn không "cài đặt npm" trên máy của bạn trong lần đầu tiên là một sự cố rất phổ biến.

Giải pháp cho anti-pattern này cũng giống như đối với anti-pattern 2 (các tệp Docker không độc lập). Hãy đảm bảo rằng các tệp Dockerfiles của bạn mô tả toàn bộ quy trình của một thứ gì đó. Các nhà vận hành/SRE của bạn sẽ yêu bạn hơn nữa nếu bạn tuân thủ điều này. Trong trường hợp của ví dụ Java trước Dockerfile nên được sửa đổi như sau:

FROM openjdk:8-jdk-alpine
COPY pom.xml /tmp/
COPY src /tmp/src/
WORKDIR /tmp/
RUN ./gradlew build
COPY  /tmp/build/app.war /app.jar
ENTRYPOINT ["java","-Djava.security.egd=file:/dev/./urandom","-jar","/app.jar"]

Dockerfile này mô tả chính xác cách ứng dụng được tạo và có thể được chạy bởi bất kỳ ai trên bất kỳ máy trạm nào mà không cần cài đặt Java cục bộ. Bạn có thể cải thiện Dockerfile này hơn nữa với multi-stage builds (bài tập cho bạn đó!).

Tổng kết

Rất nhiều công ty gặp khó khăn khi áp dụng container vì họ cố gắng vận dụng các phương thức hiện có với VM vào các container. Tốt nhất là dành một chút thời gian để nghĩ lại những lợi thế mà container có và hiểu cách bạn có thể tạo quy trình của mình từ đầu với kiến thức mới về công nghệ này.

Trong hướng dẫn này, tôi đã trình bày một số phương pháp không tốt khi làm việc/sử dụng container và giải pháp cho từng phương pháp.

  1. Cố gắng sử dụng các phương pháp của VM trên container -> Cố gắng hiểu container là gì.
  2. Tạo tệp Docker không trong suốt -> Viết Dockerfile từ đầu thay vì sử dụng lại các tập lệnh hiện có.
  3. Tạo Dockerfiles có tác dụng phụ bên ngoài -> Chuyển tác dụng phụ sang CI/CD của bạn và giữ cho Dockerfiles không gây ra tác dụng phụ.
  4. Image được sử dụng để triển khai bị nhầm lẫn với những image được sử dụng để phát triển -> Không đưa các công cụ phát triển và framework kiểm thử vào máy chủ sản xuất.
  5. Xây dựng image khác nhau cho mỗi môi trường -> Chỉ xây dựng image một lần và promote image đó trên các môi trường khác nhau
  6. Kéo mã từ máy chủ sản xuất bằng git và xây dựng image on-the-fly -> Sử dụng Docker registry
  7. Giao tiếp giữa các nhóm sử dụng git hash thay vì docker image -> Sử dụng/giao tiếp bằng image container giữa các nhóm
  8. Hard-code các giá trị bí mật trong container image -> Tạo image một lần duy nhất và inject cấu hình/giá trị bí mật tại run-time
  9. Sử dụng Docker làm CI/CD -> Sử dụng Docker làm artifact triển khai và chọn giải pháp CI/CD cho CI/CD
  10. Coi container là một phương pháp đóng gói tiện lợi -> Tạo Dockerfiles tự biên dịch/đóng gói mã nguồn từ đầu.

Xem xét quy trình làm việc của bạn, hỏi kỹ sư phát triển (nếu bạn là kỹ sư vận hành) hoặc kỹ sư vận hành (nếu bạn là kỹ sư phát triển) và cố gắng tìm xem liệu công ty của bạn có đang làm một hoặc nhiều anti-pattern này hay không.

Bạn có biết bất kỳ thực hành container tốt/xấu nào khác không? Nếu có, hãy để lại comment ở cuối bài nhé!

Tham khảo

VNTechies Dev Blog

Kho tài nguyên mã nguồn mở với sứ mệnh đào tạo kiến thức, định hướng nghề nghiệp cho cộng đồng Cloud ☁️ DevOps 🚀

Facebook page

Tham gia group VNTechies - Cloud ☁️ / DevOps 🚀 nếu bạn muốn giao lưu với cộng đồng và cập nhật các thông tin mới nhất về Cloud và DevOps.

Discord banner

Anh chị em hãy follow/ủng hộ VNTechies để cập nhật những thông tin mới nhất về Cloud và DevOps nhé!