Computer >> Hướng Dẫn Máy Tính >  >> Phần Mềm >> Máy Ảo

Làm chủ Mạng Container Docker: Hướng dẫn Chi tiết từng Bước

Đã đến lúc khám phá thêm những điều kỳ diệu của Docker. Chúng ta đã có hai hướng dẫn trước đó, một tập trung vào giới thiệu rất kỹ lưỡng, nơi chúng ta học về công nghệ, cách chạy dịch vụ và lộ cổng, cách commit và xây dựng image với Dockerfile, và một vài thủ thuật khác. Sau đó, chúng ta đã sử dụng supervisord như một thay thế cho init script và systemd.

Hôm nay, chúng ta sẽ tìm hiểu về mạng. Làm thế nào để kết nối với container của chúng ta, làm thế nào để truy cập host từ bên trong các instance được tạo ra, và quan trọng nhất, làm thế nào để kết nối từ container này sang container khác mà không cần biết bất cứ điều gì về topology trước hand. Điều này hứa hẹn sẽ thú vị. Giống như hai hướng dẫn trước, chúng ta sẽ tiến hành từng bước một và giải thích mọi thứ chi tiết. Theo chân mình nhé.

Mục lục

  1. Thiết lập
  2. Mạng Host đến Container
    1. Tóm tắt
  3. Mạng Container đến Host
  4. Mạng Container đến Container
  5. Link Container!
    1. File hosts
    2. Kết nối đến server, sử dụng tên của nó
  6. Đọc thêm
  7. Kết luận

Thiết lập

Trước tiên, hãy tạo một số container. Chúng ta đã có sẵn các image từ lần test trước, nên không cần bắt đầu từ đầu. Chúng ta sẽ chạy ba container, mỗi container có dịch vụ SSH và Apache riêng. Bỏ qua khía cạnh bảo mật, vì chúng ta đang tập trung vào phần mạng.

Một việc chúng ta sẽ làm ở đây là đặt tên cho các container để dễ dàng nhận diện, sử dụng tùy chọn --name. Ví dụ:

docker run -d -ti -p 22 -p 80 --name net3 image-4:latest

Hãy xem địa chỉ IP của ba instance này:

[root@localhost ~]# docker inspect net1 | grep -i ipaddr
"IPAddress": "172.17.0.5",

[root@localhost ~]# docker inspect net2 | grep -i ipaddr
"IPAddress": "172.17.0.6",

[root@localhost ~]# docker inspect net3 | grep -i ipaddr
"IPAddress": "172.17.0.7",

Đây là cấu hình ban đầu cho bài test. Chúng ta có ba container, tất cả chạy trên cùng một host, với các dịch vụ và cổng dịch vụ đã lộ ra. Bây giờ, chúng ta có thể bắt đầu làm những điều thú vị hơn.

Mạng Host đến Container

Đây là phần dễ dàng, và nói thật, chúng ta đã làm điều này trong bài tập trước. Chúng ta đã kết nối với container bằng SSH, và kiểm tra cổng bằng telnet. Netcat (nc) cũng hoạt động tốt, hãy tắt cảnh báo bảo mật đi. Có hai cách để kết nối. Thứ nhất, bạn có thể truy cập container trực tiếp bằng địa chỉ IP của chúng, như đã làm trước đó.

ssh 172.17.0.5
The authenticity of host '172.17.0.5 (172.17.0.5)' can't be established. ECDSA key fingerprint is 00:4b:de:91:60:e5:22:cc:f7:89:01:19:3e:61:cb:ea.
Are you sure you want to continue connecting (yes/no)? yes
Warning: Permanently added '172.17.0.5' (ECDSA) to the list of known hosts.
root@172.17.0.5's password:

Thứ hai, bạn cũng có thể truy cập các cổng được map trên host tương ứng với các cổng dịch vụ bên trong container. Nếu chỉ nhìn vào thông tin PORTS trong output của docker ps:

0.0.0.0:49162->22/tcp, 0.0.0.0:49161->80/tcp net1
0.0.0.0:49163->22/tcp, 0.0.0.0:49164->80/tcp net2
0.0.0.0:49165->22/tcp, 0.0.0.0:49166->80/tcp net3

Trong ví dụ trên, đối với container tên net1, cổng 22 được map đến cổng 49162 trên host, có nghĩa là interface docker0, mà mặc định được gán mạng 172.17.42.1/16. Thực tế, ta thấy:

telnet 172.17.42.1 49163
Trying 172.17.42.1...
Connected to 172.17.42.1.
Escape character is '^]'.
SSH-2.0-OpenSSH_6.6.1

Tại sao bạn muốn dùng phương pháp thứ hai, vì nó gây nhầm lẫn và khó nhớ hơn? Câu trả lời là, nếu bạn không dùng dynamic mapping, mà thay vào đó chỉ định các cổng host cố định, bạn sẽ có một bản đồ topology rất rõ ràng cho mạng container của mình. Hơn nữa, nếu bạn muốn container có thể truy cập từ bên ngoài host, bạn sẽ cần IP forwarding và NAT. Nếu dùng interface docker0 để giao tiếp, bạn sẽ cần ít rule iptables hơn, đơn giản hơn.

Bạn có thể kiểm tra bằng cách chạy lệnh iptables -L, và xem chain DOCKER. Các rule mới sẽ được thêm tự động mỗi khi bạn chạy container với tùy chọn -p.

Chain DOCKER (1 references)
target prot opt source destination
ACCEPT tcp -- anywhere 172.17.0.5 tcp dpt:http
ACCEPT tcp -- anywhere 172.17.0.5 tcp dpt:ssh
ACCEPT tcp -- anywhere 172.17.0.6 tcp dpt:ssh
ACCEPT tcp -- anywhere 172.17.0.6 tcp dpt:http
ACCEPT tcp -- anywhere 172.17.0.7 tcp dpt:ssh
ACCEPT tcp -- anywhere 172.17.0.7 tcp dpt:http
ACCEPT tcp -- anywhere 172.17.0.8 tcp dpt:ssh
ACCEPT tcp -- anywhere 172.17.0.8 tcp dpt:http

Tuy nhiên, nếu bạn không có bất kỳ giới hạn bảo mật nào, phương pháp dynamic có thể khá hữu ích, vì bạn không cần biết mapping cổng khi khởi động container. Bạn có thể lấy thông tin real-time bằng cách parse output của docker ps. Nhưng đối với các dịch vụ, thường hữu ích là đặt địa chỉ IP tĩnh và cổng cố định.

Tóm tắt

Vì hơi gây nhầm lẫn, hãy tóm gọn lại nhé. Mọi Docker host sẽ có một bridge tên docker0 hoặc tương tự, thường với subnet /16, dù bạn có thể đổi tên, subnet hoặc cả network. Đằng sau nó, bạn có thể có một loạt container, mỗi cái có IP riêng và một hoặc nhiều cổng lộ ra.

Nếu bạn muốn kết nối đến các container và cổng liên quan, bạn có vài lựa chọn. Bạn có thể kết nối trực tiếp, nhưng khi đó cần IP forwarding, NAT và rule firewall cho từng cái, và cần biết địa chỉ IP.

Nếu bạn muốn kết nối qua interface host (docker0) và phương pháp cổng host, thì bạn không cần biết container chạy như thế nào. Bạn chỉ cần biết mapping giữa cổng host và cổng container. Nói cách khác, nếu muốn container có thể truy cập giữa nhiều node host, bạn chỉ cần làm cho interface host (docker0) hiển thị với các host khác. Điều này làm cho IP forwarding, NAT và rule firewall trở nên đơn giản. Ví dụ:

0.0.0.0:30001->22/tcp net1
0.0.0.0:30007->22/tcp net4
0.0.0.0:30008->22/tcp net5

Ví dụ trên cho phép kết nối SSH của ba container khác nhau. Nếu dùng mapping one-to-one, tức là cùng cổng host và cổng container (ví dụ 22:22), bạn thực chất bị giới hạn chỉ một container với một dịch vụ lắng nghe trên đó. Điều này giống như router nhà bạn. Bạn sẽ phải dùng các cổng khác nhau cho container để dùng mapping one-to-one. Nhưng điều này gây nhầm lẫn, vì bạn mong đợi dịch vụ chạy trên các cổng dự đoán được.

Với ví dụ trên, ta thấy có thể có ba container dùng SSH. Nội bộ, từ góc độ container, luôn là cổng 22. Từ bên ngoài, số cổng vừa xác định container vừa xác định dịch vụ. Và đây trở thành thông tin duy nhất bạn cần biết, không cần lo lắng về những gì xảy ra bên hậu trường.

Điều này có nghĩa là bạn có thể gán động cổng host khi tạo container, dùng logic script nào đó, sau đó dùng thông tin đó để xác định cổng dịch vụ. Do đó, mạng host-to-container trở nên đơn giản và thanh lịch. Bạn cũng có thể kết nối từ host này sang host khác, lại nữa, không cần biết topography container trước.

Mạng Container đến Host

Đây là phần thú vị hơn. Container của bạn có thể phục vụ mục đích lâu dài, do đó có thể phải chạy một thời gian. Lúc này, bạn có thể phải bảo trì container, nghĩa là upgrade, patching, cập nhật file hosts, v.v. Bạn cũng có thể dùng công cụ quản lý cấu hình như Chef, Puppet hay Cfengine.

Câu hỏi lớn đầu tiên: container có thể liên lạc với thế giới bên ngoài không? Có. Trong hướng dẫn giới thiệu, chúng ta đã tải cập nhật CentOS trực tiếp từ repo chính thức, nên biết phần này hoạt động tốt. Làm thế nào để ping docker0:

Câu hỏi thứ hai: có thể truy cập interface docker0, RỒI kết nối đến các cổng host của nó không? Giả sử chúng ta đã có thông tin cần thiết bên trong container.

telnet 172.17.42.1 49162
Trying 172.17.42.1...
telnet: connect to address 172.17.42.1: No route to host

Điều này sẽ không hoạt động, vì chúng ta chưa cấu hình IP forwarding trên host. Hơn nữa, sẽ cần rule firewall để cho phép traffic cần thiết. Ở đây hơi phức tạp, vì nếu bạn không quen với iptables, bạn sẽ gặp khó khăn.

echo 1 > /proc/sys/net/ipv4/ip_forward

Và các rule iptables - một bộ khá lỏng lẻo, nhé (trên host):

iptables -t filter -A FORWARD -d 172.17.0.0/16 \
-o docker0 -j ACCEPT
iptables -t filter -A FORWARD -s 172.17.0.0/16 \
-i docker0 -j ACCEPT
iptables -t filter -A FORWARD -i docker0 -o docker0 -j ACCEPT
iptables -t nat -A POSTROUTING -s 172.17.0.0/16 \
! -d 172.17.0.0/16 -p tcp -j MASQUERADE --to-ports 1016-65535 \
iptables -t nat -A POSTROUTING -s 172.17.0.0/16
! -d 172.17.0.0/16 -p udp -j MASQUERADE --to-ports 1016-65535 \
iptables -t nat -A POSTROUTING -s 172.17.0.0/16
! -d 172.17.0.0/16 -j MASQUERADE

Giải pháp đơn giản nhất chỉ là tắt firewall - hoặc cho phép all. Nghĩa là chạy lệnh cần thiết, có thể là systemctl stop firewalld, iptables -F hoặc tương đương. Trong một số tình huống, điều này có thể không khả thi, vì bạn cần bảo mật thêm. Tuy nhiên, điều này cũng liên quan đến usecase đầu tiên chúng ta đã thảo luận. Bây giờ, tưởng tượng bạn phải tạo rule thủ công cho từng container và dịch vụ liên quan.

Với các rule mạng đúng chỗ, bạn sẽ có thể kết nối đến các cổng host, điều này thực chất có nghĩa là nói chuyện với các container khác, dù không có gì trong địa chỉ IP hoặc cổng cụ thể yêu cầu target phải là container khác. Hơn nữa, bạn cũng có thể kết nối đến các thiết bị mạng khác trên host, thậm chí cả remote host.

Mạng Container đến Container

Giống bài hát Ashes to Ashes của David Bowie, chỉ không cool bằng. Đây có vẻ như senario hữu ích nhất. Lại có hai lựa chọn. Chúng ta có thể thử kết nối trực tiếp đến container khác, hoặc thử dùng các cổng được map động của docker0. Chúng ta sẽ "lừa" một chút. Chúng ta sẽ dùng host và cổng host làm gateway vào container. Phương pháp đầu tiên khá đơn giản. Hãy thử ping net2 và net3 từ net1.

Hoạt động bình thường. Nhưng vấn đề là, chúng ta biết địa chỉ IP từ thế giới bên ngoài! Chúng ta chưa lấy thông tin này từ bên trong container. Thực tế, vì lý do bảo mật và base container là image khá nhỏ, bạn không có công cụ mạng mạnh nào sẵn sàng. Bạn thậm chí không có tiện ích ifconfig. Hơn nữa, bạn không thể thao tác iptables bên trong container:

iptables -L
iptables v1.4.21: can't initialize iptables table `filter': Permission denied (you must be root)
Perhaps iptables or your kernel needs to be upgraded.

Do đó, phương pháp thứ hai, truy cập docker0 và kết nối đến cổng host có thể không thực thi được, vì chúng ta không thể thao tác rule firewall host từ bên trong container. Đây là lý do chúng ta sẽ thảo luận phương pháp thứ ba.

Link Container!

Cách đúng đắn để làm điều này là dùng tính năng --link. Nghĩa là, chúng ta sẽ spawn instance mới đồng thời link chúng đến các container hiện có. Chức năng link sẽ chèn hostname và IP address của container được link vào environment và file /etc/hosts của instance mới. Có thể nghe hơi nhầm lẫn, nên hãy minh họa:

docker run -d -ti -p 80 --name web image-4:latest
8e689d8ae3ef43eeab3bfa9cf523e1cc6658dae5fc665f8cbf889db17db46c26

docker inspect web | grep -i ipaddr
"IPAddress": "172.17.0.9",

nc -w1 -v 172.17.0.9 80
Ncat: Version 6.40 ( https://nmap.org/ncat )
Ncat: Connected to 172.17.0.9:80.

Chúng ta có một container đang chạy, và nó được thiết kế làm Web server. Bây giờ, chúng ta muốn client có thể truy cập nó. Chúng ta muốn dùng tên server, thay vì địa chỉ IP, vì có thể không có kiến thức trước về topography. Thêm vào đó, tên rất hữu ích vì cho phép thay đổi layout mạng. Hãy tạo một client container và link nó đến Web server:

docker run -ti --name client --link web:web image-4:latest /bin/bash

Đây là những gì chúng ta có. Chúng ta tạo link source:target. Nói cách khác, server (container) tên web sẽ hiển thị bên trong container mới tên client là web. Nếu bạn spawn container và attach vào nó, bạn có thể kiểm tra environment bằng lệnh env trong BASH shell:

WEB_PORT_80_TCP_PORT=80
WEB_PORT_80_TCP=tcp://172.17.0.9:80
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
PWD=/
WEB_PORT_22_TCP_ADDR=172.17.0.9
container_uuid=558a9db0-6d1e-d9d2-b62d-a8dbfbad0b2d
WEB_PORT_22_TCP=tcp://172.17.0.9:22
SHLVL=1
HOME=/root
WEB_NAME=/client/web
WEB_PORT_80_TCP_PROTO=tcp
WEB_PORT_80_TCP_ADDR=172.17.0.9
LESSOPEN=||/usr/bin/lesspipe.sh %s
WEB_PORT=tcp://172.17.0.9:22
_=/usr/bin/env

Vì mapping tên giống nhau có thể gây nhầm lẫn, hãy thử mapping khác:

docker run -ti --name client --link web:dedoimedo image-4:latest /bin/bash

Sau đó ta được:

Tùy chọn, nếu bạn spawn container chỉ cần làm worker nhanh, bạn có thể cân nhắc tạo chúng với tùy chọn run --rm. Đối với tác vụ ngắn hạn, không lý do gì phải giữ file container trên disk, và bạn có thể khiến Docker tự động dọn dẹp chúng.

Đọc tài liệu chính thức:

Mặc định hệ thống file của container tồn tại ngay cả sau khi container thoát. Điều này khiến việc debug dễ dàng hơn (vì bạn có thể kiểm tra trạng thái cuối) và bạn giữ lại tất cả dữ liệu mặc định. Nhưng nếu bạn chạy các tiến trình foreground ngắn hạn, các hệ thống file container này có thể tích tụ. Nếu bạn muốn Docker tự động dọn dẹp container và xóa hệ thống file khi container thoát, bạn có thể thêm cờ --rm.

docker run --rm -ti --name client --link web:dedoimedo image-4:latest /bin/bash

File hosts

Điều tốt thứ hai về chức năng link là container mới sẽ tự động điền file /etc/hosts với các tên cần thiết, nên bạn sẽ được "đối xử" tốt. Nó sẽ trông như bên dưới, với các entry IPv6 đã bỏ:

[root@e1e92f6918dd /]# cat /etc/hosts
172.17.0.11 e1e92f6918dd
127.0.0.1 localhost
172.17.0.9 dedoimedo 8e689d8ae3ef web

Kết nối đến server, sử dụng tên của nó

Vì đã có phân giải tên host và cài đặt environment, mọi thứ trở nên giống như bất kỳ mạng nào khác. Rất đơn giản, dễ nhớ và quan trọng nhất, có thể script.

Đọc thêm

Nếu bạn nghĩ chưa đủ vui, thì:

Hướng dẫn mạng nâng cao Docker

Kết luận

Xong rồi. Một chủ đề "kinh dị" khác được giải mã. Thực tế, tôi tin rằng thách thức lớn nhất bạn sẽ gặp khi làm việc với Docker container là cấu hình rule firewall chứ không phải bất cứ điều gì cốt lõi về mảnh mạng của công nghệ này. Nhưng khi bạn nắm được cách làm, nó không quá khó hay phức tạp.

Chúng ta đã thấy cách khởi động container và lộ cổng, thực hành mạng host-to-container, container-to-host và container-to-container, thiết lập forwarding, và dùng link để làm cho cuộc sống dễ dàng và dự đoán được hơn. Tóm lại, đây là một hướng dẫn bận rộn nhưng hy vọng thực tế. Lại nữa, nếu bạn có chủ đề khác, đừng ngần ngại gợi ý. Và chúng ta kết thúc ở đây.

P.S. Nếu bạn thích bài viết này, thì hãy cho Dedoimedo một chút tình yêu!

Chúc vui.