Computer >> บทช่วยสอนคอมพิวเตอร์ >  >> ซอฟต์แวร์ >> เครื่องเสมือน

เชี่ยวชาญเครือข่ายคอนเทนเนอร์ Docker: บทช่วยสอนแบบขั้นตอน

ถึงเวลาสำรวจความมหัศจรรย์ของ Docker อีกครั้ง เราได้ผ่านบทช่วยสอนสองบทมาก่อนแล้ว บทแรกมุ่งเน้นการแนะนำอย่างครอบคลุม เรียนรู้เกี่ยวกับเทคโนโลยี วิธีรันบริการและเปิดพอร์ต การคอมมิตและสร้างอิมเมจด้วย Dockerfile รวมถึงเทคนิคอื่นๆ อีกมากมาย บทที่สองเราใช้ supervisord แทนสคริปต์ init และ systemd

วันนี้เราจะเรียนรู้เกี่ยวกับเครือข่าย (Networking) วิธีเชื่อมต่อกับคอนเทนเนอร์ วิธีเข้าถึงโฮสต์จากอินสแตนซ์ที่สร้างขึ้น และสำคัญที่สุดคือวิธีเชื่อมต่อจากคอนเทนเนอร์หนึ่งไปอีกคอนเทนเนอร์หนึ่งโดยไม่ต้องรู้ทopolojiล่วงหน้า เรื่องนี้น่าสนใจมาก คล้ายกับบทก่อนหน้าสองบท เราจะทำทีละขั้นตอนและอธิบายรายละเอียดครบทุกส่วน ตามมาถึง

สารบัญ

  1. การตั้งค่า (Setup)
  2. เครือข่ายโฮสต์ไปคอนเทนเนอร์ (Host to container networking)
    1. สรุป (Summary)
  3. เครือข่ายคอนเทนเนอร์ไปโฮสต์ (Container to host networking)
  4. เครือข่ายคอนเทนเนอร์ไปคอนเทนเนอร์ (Container to container networking)
  5. ลิงก์คอนเทนเนอร์ (Container links!)
    1. ไฟล์ hosts
    2. เชื่อมต่อเซิร์ฟเวอร์โดยใช้ชื่อ
  6. อ่านเพิ่มเติม (More reading)
  7. สรุป (Conclusion)

การตั้งค่า (Setup)

มาสร้างคอนเทนเนอร์หลายตัวกันก่อน เรามีอิมเมจจากการทดสอบครั้งก่อนแล้ว จึงไม่ต้องเริ่มจากศูนย์ เราจะรันคอนเทนเนอร์สามตัว แต่ละตัวมีบริการ SSH และ Apache ของตัวเอง ขอละเว้นด้านความปลอดภัยไปก่อน เพราะเราเน้นที่ส่วนเครือข่าย

สิ่งหนึ่งที่เราจะทำต่างจากครั้งก่อนคือ การตั้งชื่อคอนเทนเนอร์เพื่อให้ระบุง่ายขึ้น โดยใช้ตัวเลือก --name ตัวอย่างเช่น:

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

ลองดูที่อยู่ IP ของอินสแตนซ์ทั้งสาม:

[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",

นี่คือการตั้งค่าเริ่มต้นสำหรับการทดสอบ เรามีคอนเทนเนอร์สามตัว รันบนโฮสต์เดียวกัน พร้อมบริการและพอร์ตที่เปิดไว้ ตอนนี้เราสามารถเริ่มทำสิ่งน่าสนใจได้แล้ว

เครือข่ายโฮสต์ไปคอนเทนเนอร์ (Host to container networking)

ส่วนนี้ง่ายที่สุด และบอกตรงๆ เราทำไปแล้วในแบบฝึกหัดก่อนหน้า เราเชื่อมต่อคอนเทนเนอร์ด้วย SSH และทดสอบพอร์ตด้วย telnet netcat (nc) ก็ใช้ได้เช่นกัน มีสองวิธีในการเชื่อมต่อ:

วิธีที่ 1: เข้าถึงคอนเทนเนอร์โดยตรงผ่านที่อยู่ IP เหมือนที่เคยทำ

ssh 172.17.0.5
The authenticity of host '172.17.0.5 (172.17.0.5)' can't be established...
root@172.17.0.5's password: 

วิธีที่ 2: เข้าถึงพอร์ตที่แมปบนโฮสต์ซึ่งสอดคล้องกับพอร์ตบริการภายในคอนเทนเนอร์ ดูจากคอลัมน์ PORTS ในคำสั่ง 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

ในตัวอย่าง net1 พอร์ต 22 ถูกแมปไปพอร์ต 49162 บนโฮสต์ ซึ่งหมายถึงอินเทอร์เฟซ docker0 ที่มักมีเน็ตเวิร์ก 172.17.42.1/16 เราสามารถทดสอบได้:

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

ทำไมต้องใช้วิธีที่สองซับซ้อนกว่า? คำตอบคือ ถ้าไม่ใช้การแมปแบบไดนามิก แต่ระบุพอร์ตโฮสต์ที่แน่นอน จะได้ทopolojiชัดเจน นอกจากนี้ ถ้าต้องการให้คอนเทนเนอร์เข้าถึงได้จากภายนอกโฮสต์ จะต้องใช้ IP forwarding และ NAT หากใช้ docker0 จะต้องใช้กฎ iptables น้อยและง่ายกว่า

ตรวจสอบได้ด้วยคำสั่ง iptables -L ดู chain DOCKER กฎใหม่จะถูกเพิ่มอัตโนมัติเมื่อรันคอนเทนเนอร์ด้วย -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

อย่างไรก็ตาม หากไม่มีข้อจำกัดด้านความปลอดภัย วิธีไดนามิกก็มีประโยชน์ เพราะไม่ต้องรู้การแมปพอร์ตตอนสร้างคอนเทนเนอร์ สามารถดึงข้อมูลได้เรียลไทม์จาก docker ps แต่สำหรับบริการจริง มักควรกำหนด IP และพอร์ตคงที่

สรุป (Summary)

เนื่องจากส่วนนี้ค่อนข้างสับสน ให้สรุปสั้นๆ โฮสต์ Docker จะมีบริดจ์ชื่อ docker0 (หรือชื่อคล้ายกัน) รันอยู่ โดยปกติเป็น subnet /16 แต่อาจเปลี่ยนได้ ข้างหลังจะมีคอนเทนเนอร์จำนวนมาก แต่ละตัวมี IP ส่วนตัวและพอร์ตที่ expose

ถ้าต้องการเชื่อมต่อคอนเทนเนอร์และพอร์ต มีหลายทางเลือก: เชื่อมโดยตรง (ต้องมี IP forwarding, NAT, firewall rules และรู้ IP) หรือใช้วิธีพอร์ตโฮสต์ (ไม่ต้องรู้คอนเทนเนอร์รันยังไง แค่รู้การแมปพอร์ตโฮสต์ไปพอร์ตคอนเทนเนอร์) กล่าวคือ ถ้าต้องการให้คอนเทนเนอร์เข้าถึงได้ระหว่างโฮสต์หลายตัว 只要ทำให้อินเทอร์เฟซโฮสต์ (docker0) เห็นได้จากโฮสต์อื่น กฎ IP forwarding, NAT, firewall จะง่ายขึ้น

ตัวอย่างการแมปแบบหนึ่งต่อหนึ่ง (one-to-one mapping):

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

ตัวอย่างข้างต้นให้เชื่อมต่อ SSH ของคอนเทนเนอร์สามตัวได้ ภายในคอนเทนเนอร์เป็นพอร์ต 22 ตลอด แต่ภายนอกหมายเลขพอร์ตจะบ่งบอกทั้งตัวคอนเทนเนอร์และบริการ นี่คือข้อมูลเดียวที่ต้องรู้ โดยไม่ต้องสนใจสิ่งที่เกิดขึ้นเบื้องหลัง

หมายความว่าสามารถกำหนดพอร์ตโฮสต์แบบไดนามิกตอนสร้างคอนเทนเนอร์ด้วยสคริปต์ แล้วนำข้อมูลนั้นไประบุพอร์ตบริการ ดังนั้น เครือข่ายโฮสต์ไปคอนเทนเนอร์จึงกลายเป็นเรื่องง่ายและสง่างาม สามารถเชื่อมโฮสต์ไปโฮสต์อื่นได้เช่นกัน โดยไม่ต้องรู้ทopolojiคอนเทนเนอร์ล่วงหน้า

เครือข่ายคอนเทนเนอร์ไปโฮสต์ (Container to host networking)

ส่วนนี้น่าสนใจกว่า คอนเทนเนอร์อาจทำงานนาน จึงอาจต้องดูแลรักษา เช่น อัปเกรด แพตช์ อัปเดต hosts file หรือใช้เครื่องมือ configuration management เช่น Chef, Puppet, Cfengine

คำถามแรก: คอนเทนเนอร์ติดต่อโลกภายนอกได้ไหม? ได้ ในบทแนะนำ เราดาวน์โหลดอัปเดต CentOS จาก repo อย่างเป็นทางการได้ปกติ ลอง ping docker0:

(ผลลัพธ์การ ping)

คำถามที่สอง: เข้าถึงอินเทอร์เฟซ docker0 แล้วเชื่อมต่อพอร์ตโฮสต์ได้ไหม? สมมติว่ามีข้อมูลที่จำเป็นอยู่ในคอนเทนเนอร์แล้ว:

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

ไม่ได้ เพราะยังไม่ได้เปิด IP forwarding บนโฮสต์ และต้องมีกฎไฟร์วอลล์อนุญาตการส่งผ่าน หากไม่คุ้นเคย iptables จะลำบาก

เปิด IP forwarding:

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

กฎ iptables (ชุดที่ค่อนข้าง鬆弛):

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

ทางเลือกง่ายที่สุดคือปิดไฟร์วอลล์ (systemctl stop firewalld หรือ iptables -F) แต่บางสถานการณ์อาจไม่ได้ เพราะต้องการความปลอดภัย เพิ่มเติม เรื่องนี้เกี่ยวข้องกับกรณีแรกที่อธิบายไป 想像一下 ถ้าต้องสร้างกฎมือทีละคอนเทนเนอร์และบริการ...

ด้วยกฎเครือข่ายที่ถูกต้อง จะสามารถเชื่อมต่อพอร์ตโฮสต์ได้ ซึ่งหมายถึงการคุยกับคอนเทนเนอร์อื่น (แม้ IP/พอร์ตจะไม่บังคับว่าเป้าหมายต้องเป็นคอนเทนเนอร์) ยังสามารถเชื่อมต่ออุปกรณ์เครือข่ายอื่นบนโฮสต์ หรือแม้แต่โฮสต์ระยะไกลได้อีกด้วย

เครือข่ายคอนเทนเนอร์ไปคอนเทนเนอร์ (Container to container networking)

คล้ายเพลง Ashes to Ashes ของ David Bowie แต่ไม่ได้โค้ลเท่าตัว 這似乎是最有用的场景。同样有两个选项。我们可以尝试直接连接到其他容器,或者尝试使用 docker0 动态映射的端口。我们将作弊。我们将使用主机和主机端口作为进入容器的网关。第一种方法非常直接。让我们尝试从 net1 ping net2 和 net3。

(ผลลัพธ์การ ping - ทำงานได้โดยไม่มีปัญหา)

แต่ปัญหาคือ เรารู้ IP จากโลกภายนอก! ไม่ได้รับข้อมูลนี้จากภายในคอนเทนเนอร์ เนื่องจากเหตุผลด้านความปลอดภัยและขนาดอิมเมจฐานเล็ก ไม่มีเครื่องมือเครือข่ายมากมายโดยค่าเริ่มต้น ไม่มี ifconfig แม้แต่นั้น ไม่สามารถจัดการ iptables ภายในคอนเทนเนอร์ได้:

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.

ดังนั้น วิธีที่สอง (เข้าถึง docker0 และเชื่อมต่อพอร์ตโฮสต์) อาจไม่เป็นไปได้ เพราะไม่สามารถจัดการกฎไฟร์วอลล์โฮสต์จากภายในคอนเทนเนอร์ได้ นี่คือเหตุผลที่เราจะพูดถึงวิธีที่สาม

ลิงก์คอนเทนเนอร์ (Container links!)

วิธีที่ถูกต้องคือใช้ฟีเจอร์ --link หมายถึงการสร้างอินสแตนซ์ใหม่โดยลิงก์กับคอนเทนเนอร์ที่มีอยู่ ฟังก์ชันลิงก์จะแทรก hostname และ IP ของคอนเทนเนอร์ที่ลิงก์เข้าไปใน environment และไฟล์ /etc/hosts ของอินสแตนซ์ใหม่ อาจดูสับสน ลองดูตัวอย่าง:

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.

เรามีคอนเทนเนอร์รันอยู่ เป็นเว็บเซิร์ฟเวอร์ ตอนนี้ต้องการให้ไคลเอนต์เข้าถึงได้ โดยใช้ชื่อเซิร์ฟเวอร์ แทน IP เพราะอาจไม่รู้ทopolojiล่วงหน้า ชื่อยังช่วยให้เปลี่ยนเลย์เอาต์เครือข่ายได้ สร้างคอนเทนเนอร์ไคลเอนต์และลิงก์กับเว็บเซิร์ฟเวอร์:

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

ที่นี่เราทำลิงก์ source:target คือ เซิร์ฟเวอร์ชื่อ web จะมองเห็นภายในคอนเทนเนอร์ใหม่ชื่อ client เป็น web เข้าไปในคอนเทนเนอร์แล้วรัน env ใน 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

เนื่องจากการแมปชื่อเดียวกันอาจสับสน ลองแมปต่างกัน:

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

สำหรับคอนเทนเนอร์ที่เป็น worker ชั่วคราว อาจใช้ run --rm เพื่อให้ Docker ลบไฟล์ระบบอัตโนมัติเมื่อคอนเทนเนอร์จบ:

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

ไฟล์ hosts (Hosts file)

ประโยชน์อีกอย่างของลิงก์คือคอนเทนเนอร์ใหม่จะมี /etc/hosts ถูกเติมชื่ออัตโนมัติ:

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

เชื่อมต่อเซิร์ฟเวอร์โดยใช้ชื่อ (Connect to server, using its name)

ด้วยการ resolve ชื่อและ environment settings การเชื่อมต่อจึงง่าย คุ้นเคย และสำคัญคือ สคริปต์ได้ (scriptable)

(ตัวอย่างการเชื่อมต่อ)

อ่านเพิ่มเติม (More reading)

หากยังอยากสนุกเพิ่ม: Docker advanced networking guide

สรุป (Conclusion)

อีกหัวข้อที่น่ากลัวถูกถอดรหัสแล้ว ความท้าทายใหญ่สุดเมื่อทำงานกับ Docker คอนเทนเนอร์ คือการตั้งค่ากฎไฟร์วอลล์ มากกว่าสิ่งที่เป็นส่วนหนึ่งของเทคโนโลยีเครือข่าย แต่พอเข้าใจแล้ว ไม่ยากหรือซับซ้อน

เราได้ดูวิธีสร้างคอนเทนเนอร์และ expose พอร์ต ทำเครือข่ายโฮสต์-คอนเทนเนอร์ คอนเทนเนอร์-โฮสต์ คอนเทนเนอร์-คอนเทนเนอร์ ตั้งค่า forwarding และใช้ลิงก์เพื่อให้ชีวิตง่ายและคาดการณ์ได้ ทั้งหมดเป็นบทช่วยสอนที่วุ่นวายแต่เป็นประโยชน์ หากมีหัวข้ออื่นเสนอมาได้ครับ และจบแล้ว

P.S. ถ้าชอบบทความนี้ ให้ส่งความรักให้ Dedoimedo นะ!

Cheers.