Thứ Ba, 31 tháng 8, 2021

Bài 4: Giới thiệu về dockerfile, volume và network

Xây dựng môi trường hỗ trợ làm việc nhóm với Docker

Bài 4: Giới thiệu về dockerfile, volume và network

Nguyễn Lê Nhật Trường

 

Mục đích: Tiếp theo bài viết về “Tổng quan về docker”, chúng ta sẽ đi tìm hiểu về cách hoạt động của Dockerfile, volume và network trong docker. Bên cạnh đó, chúng ta sẽ xem qua một vài ví dụ thực tế về việc sử dụng các thành phần này để giải quyết các vấn đề thường gặp.

Để thuận lợi cho việc trình bày, chúng ta sẽ định nghĩa một số khái niệm dùng chung

Khái niệm

Diển giải

Máy host

Máy tính/máy chủ/máy ảo đang cài đặt ứng dụng docker

Image

Docker image

Instruction

Các keyword đại diện cho các xử lý trong dockerfile nhằm hướng dẩn Docker tự build image.

Arguments

Các dữ liệu hoặc khai báo cấu hình cho các instruction

writable layer

Vùng nhớ được docker container tạo ra để lưu trữ các file bị thay đổi hoặc khởi tạo mới trong quá trình chạy của container đó.

Snapshot

Hành động biến một container thành một image bằng cách biến writable layer của container đó thành một layer mới cho image mới.

BuildContext

Thư mục chứa các file và folder cần thiết dùng cho việc build image

AUFS

Advance multi-layered Unification File System. Định dạng dữ liệu của docker-image và docker-container.

 

1.       Dockerfile

Trong bài 3, chúng ta đã được nhắc đến 2 cách để tạo ra Docker image là tạo snapshot từ một container hoặc sử dụng Dockerfile. Trong phần này, chúng ta sẽ tìm hiểu sâu hơn về các khái niệm được sử dụng trong Dockerfile và cách sử dụng Dockerfile một cách hiệu quả.

a.       Cấu trúc tổ chức của file docker (format)

Dockerfile là một file text chứa các instructions (chỉ dẩn) nhằm thực giúp Docker tự động build một Docker image.

Cấu trúc của một instruction trong dockerfile bao gồm 2 thành phần:

·         INSTRUCTION: tên của instruction nhằm yêu cầu Docker thực hiện một tác vụ trong quá trình build image

·         Arguments: các arguments được dùng cho instruction tương ứng

Mỗi dockerfile phải bắt đầu bằng instruction FROM. Chỉ có instruction ARG và các comment được phép đặt trước FROM.

b.      Cách sử dụng

·         Để yêu cầu Docker build image từ file dockerfile, chúng ta dùng tập lệnh build để bắt đầu.

·         Cú pháp thường dùng cho tập lệnh build sẽ là:

Tên argument

Bắt buộc

Mặc định

Ghi chú

-t image-name

Optional

Nếu không chỉ định, mặc định image sẽ chỉ dùng mã hash để đại diện. Nếu chỉ định tên nhưng không có tag, tag mặt định là latest.

Có thể định nghĩa nhiều image-name và tag-name cho cùng một image. Lúc này, chúng ta sẽ khai báo nhiều cặp argument -t trong dòng lệnh.

-f path/to/dockerfile

Optional

Mặc định docker sẽ tìm file có tphần mở rộng là Dockerfile trong thư mục hiện hành.

 

path/to/buildContext

Required

 

Nếu chọn thư mục hiện hành làm buildContext thì dùng path “.”

·         Ngoài ra còn một số argument khác, quý vị có thể tham khảo ở đây tùy theo mục đích của công việc.

·         Toàn bộ quá trình build image sẽ được diển ra trong Docker daemon. Điều này có nghĩa là toàn bộ file và folder trong buildContext sẽ được tiến hành copy vào trong Docker daemon để chuẩn bị cho quá trình build. Vì lý do đó, sử dụng tập lệnh build, cần chú ý đối tượng buildContext. Các file và folder trong buildContext chỉ nên chứa các file cần thiết cho quá trình build image, càng nhiều file thừa sẽ khiến quá trình build kéo dài không cần thiết.

·         Để bỏ qua các file và folder không liên quan đến việc build image trong buildContext, chúng ta có thể định nghĩa các file và folder này trong file .dockerignore. Đây là file text được đặt trong thư mục hiện hành khi chúng ta chạy lệnh docker build.

·         Docker sử dụng AUFS (Use the AUFS storage driver | Docker Documentation) để lưu trữ  image và hỗ trợ cơ chế caching trong quá trình build từ dockerfile. Trong lần build đầu tiên, các thay đổi sau khi thực thi instruction của mỗi lớp sẽ được cache lại trong các lớp của image. Từ lần build thứ 2, Docker sẽ kiểm tra sự thay đổi về mặt khai báo của dockerfile ở layer đó hoặc nếu layer đó đang dùng instruction COPY/ADD thì sẽ kiểm tra xem file được chỉ định có thay đổi không. Nếu không ghi nhận thay đổi thì Docker sẽ bỏ qua chỉ thị đó và lấy layer được cache trước đó làm commit cho lần build hiện tại. Nếu ghi nhận được thay đổi thì sẽ tiến hành bỏ qua cache của các instructions từ hiện tại trở xuống.

·         Quá trình build  image sẽ trả qua các bước cơ bản sau:

o   Load dockerfile được chỉ định

o   Kiểm tra cú pháp của file dockerfile đã load

o   Copy các file và folder trong đường dẫn của buildContext vào Docker daemon

o   Thực thi các instructions có trong dockerfile từ trên xuống dưới

0  Pull image của FROM nếu không tìm thấy image ở local

0  Với các instruction khác, nếu không bỏ qua cache thì docker sẽ kiểm tra xem có tồn tại cache ở layer hiện tại không, nếu có thì layer cha có bị thay đổi hay không. Nếu có cache và layer cha không thay đổ thì sẽ bỏ qua instruction hiện tại.

0  Thực hiện commit các thay đổi nếu có sự thay đổi của một layer mới dựa trên layer cha.

o   Khi đã hoàn tất việc thực thi các instructions trong dockerfile, Docker sẽ trả về ID của layer cuối cùng. Cùng lúc đó, các image-name và tag-name được chỉ định sẽ được gán cho ID này.

·         Chúng ta có thể thấy docker ứng dụng cơ chế caching để tăng tốc việc build một image. Như vậy việc sắp xếp thứ tự các instructions bên trong một dockerfile có ảnh hưởng lớn đến thời gian build của image. Chúng ta nên phân loại các instruction ít bị thay đổi như thư viện, tools và các tài nguyên ít bị thay đổi lên trên để tận dụng lợi thế của cache.

c.       Các lệnh cơ bản thường dùng

Sau đây sẽ là một số các instruction thường dùng trong dockerfile, ngoài ra quý vị có thể tham khảo các instruction khác của dockerfile tại đây.

·         FROM

FROM được dùng để chỉ định base-image được dùng để build image cũng như khởi chạy các instructions bên dưới nó.

Trong file dockerfile, FROM có thể được khai báo nhiều lần nhằm build nhiều image một lúc hoặc chia các state (giai đoạn) để quá trình build thuận lợi hơn.

Có thể xem FROM như là một cách khai báo state, chúng ta có thể dùng cặp argument AS <name> để định danh cho state và tái sử dụng trong các state tiếp theo.

Chúng ta có thể không chỉ định tag cho các image trong FROM, khi này Docker sẽ sử dụng tag mặc định là latest.

·         WORKDIR

Đây là instruction cho phép chỉ định thư mục hiện hành của docker image cho các instruction bên dưới nó như RUN, CMD, ENTRYPOINT, COPY và ADD.

Việc chỉ định thư mục hiện hành này sẽ cho phép các chỉ thị trên sử dụng relative path (đường dẫn tương đối) thay vì absolute path (đường dẫn tuyệt đối), tạo sự thống nhất và dể dàng chỉnh sửa trong tương lai.

Chúng ta có thể dùng WORKDIR nhiều lần trong một state, những instruction bên dưới có thể dùng đường dẫn tương đối so với instruction trước nó.

·         RUN

RUN dùng để thực thi một command hoặc khởi chạy 1 file thực thi trong quá trình build.

Có 2 cách sử dụng RUN như bên trên.

o   Cách đầu tiên thì command sẽ được thực thi trên trình thực thi command mặc định của hệ điều hành như của /bin/sh -c Linus và cmd /S /C của Windows.

o   Cách thứ 2 là để khai báo một file thực thi kèm với các param của nó. Lưu ý đây là cú pháp khai báo mảng JSON, chỉ được sử dụng dấu ngoặc kép để bao các arguments.

Với instruction này thì layer cache của nó sẽ bị bỏ qua bởi các instruction COPY hoặc ADD nằm trước nó. Nếu các layer nằm trước không thay đổi thì khi thực thi RUN, Docker sẽ chủ đông dùng cache cho layer hiện tại.

Lưu ý: Khi build, Docker sẽ không chạy các CMD hoặc ENTRYPOINT của base-image. Do đó, các command hoặc file thực thi phụ thuộc vào các instruction trên sẽ gặp lỗi trong quá trình build.

·         ADD

ADD dùng để copy file, folder trong contextBuild hoặc download một file bằng URL vào thư mục đích (dest)

Chúng ta có 2 cách sử dụng ADD như hình trên:

o   Cách đầu tiên, mỗi argument sẽ cách nhau bằng một khoảng trắng, argument cuối cùng sẽ là đường dẩn đích.

o   Cách thứ 2 là khai báo theo cú pháp của mảng JSON.

Chúng ta không thể copy các file hoặc folder nằm ngoài thư mục contextBuild

Nếu src là đường dẩn tới một folder thì Docker chỉ copy nội dung của folder đó chứ không copy bản thân folder đó.

Nếu dest là một đường dẩn không tồn tại trong image, Docker sẽ tiến hành khởi tạo toàn bộ các folder chưa tồn tại trong đường dẩn đó.

·         COPY

COPY tương tự với ADD, dùng để copy file và folder từ contextBuild và trong thư mục đích (dest) trong image.

Cách sử dụng đối với COPY cũng tương tự như cách sử dụng của ADD.

COPY không hỗ trợ tải file từ URL.

COPY cho phép copy file từ các state trước đó thông qua argument –from=<state-name>

 

·         CMD

CMD dùng để chỉ định command, file thực thi hoặc các parameter dùng cho ENTRYPOINT khi start container.

Có 3 cách sử dụng CMD như trên hình:

o   Khai báo mảng JSON, bắt đầu với file thực thi và các param dành cho file đó

o   Khai báo mảng JSON các string parameters dành cho ENTRYPOINT

o   Khai báo command được thực thi trên trình thực thi command mặc định của hệ điều hành như của /bin/sh -c Linus và cmd /S /C của Windows.

Mỗi state chỉ nên có một instruction CMD, nếu có nhiều hơn một instruction thì Docker sẽ ghi nhận instruction cuối cùng.

CMD và RUN đều thực thi các command hoặc các file thực thi. Tuy nhiên CMD sẽ thực thi khi container được khởi chạy, còn RUN sẽ được thực thi trong quá trình build. Cần lưu ý điều này để tránh nhầm lẫn không đáng có.

·         ENTRYPOINT

ENTRYPOINT dùng để chỉ định command hoặc file thực thi khi start 1 container.

Có 2 cách để sử dụng ENTRYPOINT như trên hình:

o   Khai báo mảng JSON chứa file thực thi và các param dành cho file đó

o   Khai báo command được thực thi trên trình thực thi command mặc định của hệ điều hành như của /bin/sh -c Linus và cmd /S /C của Windows.

Chúng ta có thể kết hợp sử dụng CMD và ENTRYPOINT để cấu hình container một cách linh động hơn.

Các kết hợp giữa ENTRYPOINT và CMD sẽ có kết quả như bên dưới:

 

d.      Multi state

Thường thì 1 image chỉ chứa các resource cần thiết cho việc thực thi của container. Vì lý do đó, việc build image thông thường sẽ giống việc đặt các resource executable vào bên trong môi trường thực thi. Ví dụ như file dockerfile bên dưới:

Trong file dockerfile này, chúng ta chỉ việc copy file war vào trong tomcat server, khi start container lên thì tomcat sẽ cung cấp môi trường để thực thi nội dung trong file war. Điều này đồng nghĩa với việc chúng ta sẽ cần phải build ra file war trước sau đó mới đặt file war này vào trong image.

Giống như việc thực thi ứng dụng, việc build ứng dụng cũng đòi môi trường build phức tạp, cấu hình nhiều công cụ hỗ trợ. Đôi lúc môi trường và các công cụ hỗ trợ build sẽ không dùng lại ở môi trường thực thi của ứng dụng.

Ví dụ 1: chúng ta xây dựng 1 single page application bằng react và sẽ được deploy vào tomcat chung với phần backend java. Như vậy môi trường để build react app buộc phải có NodeJs, các dependency cần thiết nhưng môi trường để thực thi thì lại không cần NodeJS.

Vì vậy, chúng ta nên tận dụng docker để phục vụ mục đích build ra các artifact/application. Multi-state trong docker ra đời để giải quyết nhu cầu này. Cụ thể, khi định nghĩa quy trình build trong Dockerfile, chúng ta có thể định nghĩa nhiều state, mỗi state dùng FROM để khai báo 1 base-image được dùng ở state đó. Kết quả build của các state này có thể copy qua lại với nhau hoặc kế thừa và tạo ra một state mới dựa trên state đã build.

Ví dụ  2: chúng ta có file dockerfile như sau

Chúng ta có thể thấy có 2 state được dùng trong trường hợp này:

State đầu tiên build file war với sự hộ trợ của maven

State thứ 2 sử dụng file war đã build xong ở state đầu và copy vào tomcat server.

Sử dụng lại yêu cầu trong ví dụ 1, chúng ta sẽ giải quyết vấn đề trong ví dụ 1 và dùng lại trong ví dụ 2 như sau:

Chúng ta có state 1 để build frontend thành các file html, css và js tĩnh bằng node js, dùng state 2 để build api service bằng maven và ở state cuối chúng ta build image có tomcat serve các file html, css, js và api java trong gói war.

2.       Quản lý về data trong Docker

a.       Tổng quan về lưu trữ data trong docker

Các file được khởi tạo và cập nhật bên trong một container sẽ được lưu bên trong 1 writable-layer bằng cơ chế copy-on-write. Các file và folder trong layer này có các đặc điểm sau:

·         Không được lưu trữ dài hạn, layer này sẽ bị xóa nếu container của nó bị xóa khỏi Docker host.

·         Khó để chia sẻ các file này giữa các container với nhau

·         Các layer này bị gắn liền với Docker host đang quản lý container của nó.

·         Việc khác biệt về định dạng dữ liệu bên trong layer này (AUFS) với máy host sẽ khiến cho việc đưa file từ máy host vào trong container trở nên khó khăn hơn.

Chính vì các lý do trên, Docker hỗ trợ thêm cơ chế mounting để cho phép người dùng tách phần persist data (dữ liệu dài hạn) ra khỏi writable-layer của container, từ đó không bị ảnh hưởng bởi life circle của container.

b.      Phương pháp mount dữ liệu

Docker hỗ trợ 2 cơ chế mount dữ liệu chính là volume và bind mount.

·         Volume: Tạo 1 vùng nhớ được quản lý bởi Docker, do Docker quy định nơi lưu trữ. Việc truy cập dữ liệu vào các vùng nhớ này từ các process/application không thuộc Docker sẽ gặp nhiều khó khăn

·         Bind mount: Cho phép ánh xạ việc lưu trữ dữ liệu vào một thư mục của máy host. Việc truy cập dữ liệu bởi các process/application không thuộc Docker sẽ dể dàng hơn.

Nếu dùng docker trên Linux thì có thêm cơ chế thứ 3 là tmpfs mount. Cơ chế này không lưu trữ các dữ liệu dài hạn trên ổ cứng mà sẽ lưu trên RAM. Cơ chế cho phép tuy xuất nhanh các dữ liệu, không ảnh hưởng đến dung lượng tổng thể của writable-layer. Tuy nhiên vùng nhớ tmpfs sẽ mất đi nếu chúng ta stop container. Bên cạnh đó, vùng nhớ của tmpfs không thể chia sẽ giữa các container. Vì vậy, dữ liệu được mount dạng này nên là non-persisted data hoặc sensitive data của riêng một container.

c.       Docker volume

Vùng nhớ của 1 volume sẽ có đặc điểm sau:

·         Được quản lý bởi Docker

·         Có thể thực thi các thao tác backup, restore, extract vùng nhớ này thông qua Docker CLI hoặc API

·         Tách biệt khỏi lifecircle của container và không có cơ chế tự động xóa, kể cả các volume không được sử dụng bởi bất kỳ container nào.

·         Có thể chia sẻ vùng nhớ này với nhiều container khác nhau, dù container đó run trên cùng hay khác Docker host.

·         Với docker volume vừa được khởi tạo, không chứa bất kỳ file hay folder nào, chúng ta có thể pre-populate các file nằm trong container làm dữ liệu ban đầu cho volume. Cụ thể hơn thì docker sẽ copy các file có sẵn trong thư mục được mount của container vào trong docker volume.

Chúng ta có thể tạo ra một anonymous volume để lưu trữ các persist data dành cho một container. Hoặc có thể dùng named volume để dể dàng khai báo sử dụng ở các container khác. Đối với anonymous volume, Docker sẽ tự động khởi sinh ra một tên unique để gán cho volume đó.

Để chia sẻ volume giữa các Docker host khác nhau, Docker hỗ trợ nhiều driver cho phép các volume đọc và ghi dữ liệu thông qua các hệ thống lưu trữ mạng hoặc các dịch vụ cloud Object Storage như S3 hoặc gcb.

Ví dụ thực tế: Trong các bài 1, chúng ta dùng docker-compose để cấu hình 1 volume nhằm lưu trữ lại persist data của sql server

Chúng ta có thể thấy, container của web-db service được mount dữ liệu trong thư mục /var/opt/mssql/data vào volume “db” với driver là local. Việc này sẽ giúp data của sql server được tách khỏi lifecircle của container web-db, giúp cho việc nâng cấp và bảo trì container không làm ảnh hưởng đến dữ liệu đang hoạt động.

Ví dụ thứ 2, chúng ta sẽ dùng bind mount và so sánh ưu điểm khi sử dụng với container.

·         Chúng ta thực hiện nạp docker container với service của MySQL như sau

o   Sau khi, MySQL được tạo lập thành công, chúng ta tạo schema và tạo table truy cập như ở bài 02 việc truy xuất dữ liệu ok

o   Sau đó, chúng ta xóa container đi và thực hiện việc chạy lại câu lệnh run. Sau khi ứng dụng khởi động lại, chúng ta nhận được báo lỗi từ chương trình như bên dưới:

o   Cho dù chúng ta, có chạy và nạp lại docker container như bước đầu tiên của ví dụ thì dữ liệu cũng sẽ bị xóa cùng với container trước đó. Các schema và data sẽ không được bảo toàn.

è Đây là ví dụ điển hình cho việc writable layer của docker là một non-persistent storage.

·         Chúng ta thực hiện tương tự nhưng bây giờ chúng ta mapping voloum với tham số -v như sau

o   Chúng ta đang dùng kỹ thuật bind mount để ánh xạ thư mục /var/lib/mysql của mySQL trong docker container ra bên ngoài. Chúng ta có kết quả thực thi thành công như sau

§  Chúng ta có thể truy vấn các container

§  Chúng ta thấy dữ liệu được mapping theo ổ đĩa chỉ định như sau

o   Chúng ta thực hiện thao tác dữ liệu, tạo schema, tạo bảng và thêm dữ liệu và chạy lại các ví dụ bài 2 thành công

o   Chúng ta thực hiện stop docker container đang được host MySQL vừa rồi

o   Sau đó, chúng ta thực hiện lại việc nạp docker container với bước đầu tiên này

§  Chúng ta nhìn thấy mã hash code được phát sinh mới hoàn toàn so với cái cũ

§  Chúng ta kiểm tra lại docker container đang host

§  Docker container mới được thiết lập. Tuy nhiên, chúng ta chạy ứng dụng ở bài số 2 hay kết nối truy cập dữ liệu vẫn ok, dữ liệu hoàn toàn không bị mất

à Như vậy, chúng ta đã hoàn tất bind mount dữ liệu của container MySQL vào một persistent storage trên máy host.

3.       Network

Mặc định, các container được isolate (cô lập) với nhau và isolate với máy host chạy Docker. Để các container này có thể kết nối với nhau, kết nối với máy host hoặc kết nối với các thành phần khác trong mạng nội bộ, chúng ta tiến hành kết nối các container này vào các network của Docker.

Giống như volume, network là một resource được khởi tạo và quản lý bởi Docker. Trong mỗi network, chúng ta có thể định nghĩa các driver và các cấu hình về DNS, IP, MAC address cho các container tùy theo nhu cầu sử dụng.

Các driver thường dùng cho network gồm:

·         Bridge: đây là driver mặc định khi khởi tạo một network. Network dùng driver này sẽ hướng đến kết nối các container nằm trong cùng một Docker host.

·         Host: Đây là driver cho phép container có thể sử dụng trực tiếp network của máy host, từ đó loại bỏ tính isolate của container với máy host.

·         Overlay: driver cho phép kết nối các container ở nhiều Docker host khác nhau. Driver này được dùng nhiều để các service giao tiếp với nhau trong swarm mode (chúng ta sẽ đi chi tiết đến swarm mode trong các bài viết sau).

·         Macvlan: cho phép ta gán MAC address cho một container, từ đó cho phép container đó xuất hiện như một máy vật lý trong local network của máy host.

·         None: khóa tất cả các kết nối của container với các network. Thường dùng để disable networking cho một container bất kỳ.

Ví dụ về bridge:

Giả sử, chúng ta có một Docker host được cài đặt để vận hành 2 hệ thống:

·         Gitlab để quản lý source code được setup thành một container standalone

·         Local server chứa server nginx, database và private registry cho docker được setup bằng docker-compose

Các hệ thống này được cài đặt trước như bên dưới:

Mục tiêu là có thể dùng nginx trong local server để định hướng các request gọi vào Docker host đến server gitlab thông qua domain name, tận dụng port 80 của Docker host. Bây giờ, chúng ta tiến hành tạo một network và connect từng thành phần vào trong network này.

Chúng ta tạo network bằng câu lệnh

docker network create -d bridge local_network

Sau đó, chúng ta connect container standalone của gitlab vào netwok bằng câu lệnh

docker network connect –alias gitlab.local local_network gitlab

Lúc này, các container vào cùng network local_network có thể kết nối với container gitlab thông qua domain name gitlab.local

Tiếp đến, chúng ta tiến hành kết nối local server vào chung network bằng cấu hình network trong file docker-compose như bên dưới

Chúng ta có thể thấy cấu hình network local trong file docker-compose là network kết nối với local_network. Sau khi chỉnh sửa file docker-compose, chúng ta dùng lệnh docker-compose up -d để cập nhật lại cấu hình cho các container này.

Chúng ta có thể dùng lệnh bên dưới để kiểm tra các container đã kết nối vào local_network:

docker network inspect local_network

Chúng ta kiểm tra trong phần JSON được trả về, mục Containers sẽ chứa danh sách các container đã kết nối thành công vào local_network

Cuối cùng, chúng ta bổ sung cấu hình cho nginx để định hướng request gọi đến port 80 của nginx dựa trên domain git.domain.com về container gitlab như bên dưới, sau đó khởi động lại nginx

Như vậy, chúng ta đã tìm hiểu xong về cơ chế hoạt động, khái niệm và các ví dụ trong việc sử dụng dockerfile, docker volume và docker network.

Trong bài tiếp theo chúng ta sẽ vận dụng các kiến thức đã đi qua trong bài 3 và 4 để thực hành với việc nhúng docker vào web project được build bằng maven và sử dụng hệ quản trị cơ sở dữ liệu là MySQL.

Rất mong sự góp ý chân thành và chia sẻ của quí vị về loạt series này.

 

Không có nhận xét nào:

Đăng nhận xét