摘要:Docker
整理Docker网络管理知识,包括Docker网络基础,宿主机和容器互相访问,容器间网络通信,跨机器访问容器服务
(1)Docker网络类型
Docker提供了5种网络类型,其中常见的两种:bridge及host
Bridge
Bridge(网桥)是Docker默认使用的网络类型,用于同一主机上的docker容器相互通信,网络中的所有容器可以通过IP互相访问,连接到同一个网桥的docker容器可以相互通信。Bridge网络通过网络接口docker0
与主机桥接,启动docke时就会自动创建,新创建的容器默认都会自动连接到这个网络,可以在主机上通过ifconfig docker0
查看到该网络接口的信息
root@ubuntu:~# ifconfig docker0
docker0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500
inet 172.17.0.1 netmask 255.255.0.0 broadcast 172.17.255.255
Host
Host模式下,容器的网络接口不与宿主机网络隔离。在容器中监听相应端口的应用能够直接被从宿主机访问,即不需要做端口映射。host网络仅支持Linux,host网络没有与宿主机网络隔离,可能引发安全隐患或端口冲突
(2)宿主机访问容器应用
这种是最常见的场景,使用容器部署应用,给其他机器提供服务,此时容器和宿主机IP是通的,可以直接使用容器的虚拟IP+端口进行访问容器服务,如果想使用宿主机的IP访问到容器服务,需要将宿主机的某端口映射到容器的服务端口,即对外暴露端口供宿主机和其他机器访问,如果不发布端口,外界将无法访问这些容器
使用容器的虚拟IP访问
服务准备,现有一个flask api,允许其他IP远程链接,服务端口为5000
import json
import datetime
import traceback
from flask import Flask, jsonify, request
app = Flask(__name__)
@app.route("/search", methods=["POST"])
def api():
res = {}
try:
data = request.get_json(force=True)
ent = data["ent"]
dt = data.get("dt", datetime.datetime.today().strftime("%Y-%m-%d"))
res = {"msg": "success", "code": 200, "data": json.dumps({"ent": ent, "dt": dt}, ensure_ascii=False)}
code = 200
except Exception as e:
res = {"meg": "fail", "code": 400, "trace": traceback.format_exc()}
code = 400
app.logger.info(res)
return jsonify(res), code
if __name__ == '__main__':
app.run("0.0.0.0", 5000, debug=False)
对这个Python api服务使用docker部署成服务,此时可以使用docker inspect
来查看容器的虚拟IP为172.17.0.2
root@ubuntu:~/docker_test/p_test# docker inspect -f {{".NetworkSettings.IPAddress"}} 9ed6d53f5999
172.17.0.2
测试在宿主机请求容器的服务api是否能够返回数据
>>> import requests
>>> res = requests.post("http://172.17.0.2:5000/search", json={"ent": "a"})
>>> res.json()
{'code': 200, 'data': '{"ent": "a", "dt": "2021-07-08"}', 'msg': 'success'}
结果是可以正常访问,但是实际场景中不使用虚拟IP直接访问,原因是
-
虚拟IP不固定
:容器启动分配的虚拟IP是不固定的,每次启动容器都需要重新查看一次容器IP -
跨机器无法访问
:容器值和所在的宿主机和其他容器IP互通,跨机器无法ping通在这个机器上生成的容器虚拟IP
bridge模式
在bridge网络模式下,连接到同一bridge网络的容器可以相互访问彼此任意一个端口,,在docker run中设定参数指定端口,有-p
,-P
两种方式
-
-p
:指定端口映射-p 宿主机端口:容器端口
,指定宿主机的端口映射到容器端口,会让容器暴露出一个指定端口供宿主机访问,直接在docker run中指定即可 -
-P
:对于已经暴露的容器端口,随机
给出一个宿主机端口进行映射,随机端口范围是32769-60999,需要配合Dockerfile中的EXPOSE
一起使用,在启动容器后查看容器的port信息获得随机值
给flask api构建Dockerfile,其中不指定EXPOSE暴露端口
FROM python:3.6
MAINTAINER xiaogp
ENV PIPURL=https://mirrors.aliyun.com/pypi/simple/
COPY . /home
RUN pip install -i ${PIPURL} flask --default-timeout=1000
WORKDIR /home
CMD ["python", "api.py"]
构建镜像
root@ubuntu:~/docker_test/Pp_test# docker build -t xiaogp/p_test:v1 .
root@ubuntu:~/docker_test/Pp_test# docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
xiaogp/p_test v1 98b4d1d5d440 6 minutes ago 886MB
启动镜像指定-p
将宿主机端口映射到容器端口实现宿主机访问容器应用进行数据交互,并查看容器的port详情
root@ubuntu:~/docker_test/Pp_test# docker run -p 5000:5000 --rm -d 98b4d1d5d440
root@ubuntu:~/docker_test/Pp_test# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
12f2d9124300 98b4d1d5d440 "python api.py" 6 seconds ago Up 4 seconds 0.0.0.0:5000->5000/tcp upbeat_jemison
此时访问0.0.0.0:5000即可在外部网络访问容器应用api实现数据交互
>>> import requests
>>> res = requests.post("http://127.0.0.1:5000/search", json={"ent": "123"})
>>> res.json()
{'code': 200, 'data': '{"ent": "123", "dt": "2021-07-04"}', 'msg': 'success'}
如果使用-P
则不能达到任何效果,因为容器内没有暴露任何端口,api应用的默认5000端口也不能自动暴露出来
root@ubuntu:~/docker_test/Pp_test# docker run -P --rm -d 98b4d1d5d440
7267db6adb6cf5a3645e4bd55d9c7e6d57d50498d3ba35b4f3efb596f095e3fb
root@ubuntu:~/docker_test/Pp_test# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
7267db6adb6c 98b4d1d5d440 "python api.py" 10 seconds ago Up 8 seconds funny_mcclintock
可见port信息为空,外网无法调用容器api应用,正确的使用方式是在Dockerfile中指定EXPOSE暴露一个5000端口出来,然后-P随机映射一个宿主机端口
FROM python:3.6
MAINTAINER xiaogp
ENV PIPURL=https://mirrors.aliyun.com/pypi/simple/
COPY . /home
RUN pip install -i ${PIPURL} flask --default-timeout=1000
WORKDIR /home
EXPOSE 5000
CMD ["python", "api.py"]
重新构建镜像和启动容器,可以查看到ports信息0.0.0.0:49153->5000/tcp,49153是随机分配的宿主机端口
root@ubuntu:~/docker_test/Pp_test# docker build -t xiaogp/p_test:v2 .
root@ubuntu:~/docker_test/Pp_test# docker run -P --rm -d 82bbc6bd3778
8b127ae48ef94ee736ae1183f6862b6af5a1fcfc08253d100a8c2d4eac6ab6a5
root@ubuntu:~/docker_test/Pp_test# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
8b127ae48ef9 82bbc6bd3778 "python api.py" 3 seconds ago Up 2 seconds 0.0.0.0:49153->5000/tcp sharp_vaughan
调用容器api测试,可以调用成功
>>> res = requests.post("http://127.0.0.1:49153/search", json={"ent": "123"})
>>> res.json()
{'code': 200, 'data': '{"ent": "123", "dt": "2021-07-04"}', 'msg': 'success'}
在指定了EXPOSE之后依然可以使用-p
强制指定宿主机的端口,还不是-P
随机分配,-p相当于先指定容器暴露某个端口,再指定某个宿主机端口和容器端口进行映射
root@ubuntu:~/docker_test/Pp_test# docker run -p 5000:5000 --rm -d 82bbc6bd3778
418599b4cb619de3bfc2ba5f049093a97a0cfc8d01cb8075fdcee39d91722a2a
root@ubuntu:~/docker_test/Pp_test# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
418599b4cb61 82bbc6bd3778 "python api.py" 5 minutes ago Up 5 minutes 0.0.0.0:5000->5000/tcp eloquent_lehmann
另外如果使用EXPOSE则暴露的端口必须和api的端口一致,否则api的端口外部网络还是无法访问,但是-p则可以自由地设置宿主机端口和容器端口
Host模式
此模式下可以直接在宿主机访问容器中监听的端口,不需要做端口映射,比如在Dockerfile中不指定EXPOSE,不显式的暴露端口
FROM python:3.6
MAINTAINER xiaogp
ENV PIPURL=https://mirrors.aliyun.com/pypi/simple/
COPY . /home
RUN pip install -i ${PIPURL} flask --default-timeout=1000
WORKDIR /home
CMD ["python", "api.py"]
在docker run中设置--net=host
来设置
root@ubuntu:~/docker_test/Pp_test# docker build -t xiaogp/p_test:v3 .
root@ubuntu:~/docker_test/Pp_test# docker run --net=host --rm -d ca47c8bec4d1
792912903b80e877159bdfc54315cba16f355b38fec8a37e41345ad67c8f96cf
root@ubuntu:~/docker_test/Pp_test# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
792912903b80 ca47c8bec4d1 "python api.py" 6 seconds ago Up 1 second elastic_wiles
此时查看不到端口信息,因为没有申明端口映射也没有暴露端口,但是依旧可以直接在宿主机使用容器中api的默认5000端口访问
>>> res = requests.post("http://127.0.0.1:5000/search", json={"ent": "123"})
>>> res.json()
{'code': 200, 'data': '{"ent": "123", "dt": "2021-07-04"}', 'msg': 'success'}
>>> res = requests.post("http://192.168.1.28:5000/search", json={"ent": "123"})
>>> res.json()
{'code': 200, 'data': '{"ent": "123", "dt": "2021-07-04"}', 'msg': 'success'}
(3)容器访问外部网络
Bridge
在网桥bridge模式下docker0网络的默认网关就是宿主机IP,linux下docker0的网关地址为172.17.0.1,在容器中使用该IP地址即可访问宿主机上的各种服务
测试游一下在容器中部署一个一个flask API服务,API需要访问宿主机的MySQL数据库,先创建数据库表,注释掉MySQL的bind-address或者修改为0.0.0.0允许远程链接的IP连入
mysql> select * from student;
+----+------+-------------+
| id | name | phone |
+----+------+-------------+
| 1 | ? | 13852517263 |
| 2 | a | 13874530293 |
| 3 | b | 13874530293 |
| 4 | e | 13879930293 |
+----+------+-------------+
# vim /etc/mysql/mysql.conf.d/mysqld.cnf
bind-address = 0.0.0.0
创建一个API实现查询MySQL返回数据的功能
import json
from flask import Flask, jsonify, request
import pymysql
import yaml
CONFIG = yaml.load(open("./etc/config.yml"), Loader=yaml.FullLoader)
print(CONFIG)
app = Flask(__name__)
def get_mysql_data(name: str):
res = []
conn = pymysql.connect(**CONFIG)
cursor = conn.cursor()
cursor.execute("SELECT phone FROM student WHERE name='{}'".format(name))
data = cursor.fetchall()
for line in data:
res.append({"phone": line[0]})
return res
@app.route("/student", methods=["POST"])
def get_data_api():
res = {}
try:
data = request.get_json(force=True)
name = data["name"]
data = get_mysql_data(name)
res = jsonify({"code": 200, "msg": "success", "data": json.dumps(data, ensure_ascii=False)})
except Exception as e:
print(e)
res = jsonify({"code": 400, "msg": "fail", "trace": e.args})
return res
if __name__ == '__main__':
app.run("0.0.0.0", 5001, debug=False)
构建镜像
root@ubuntu:~/docker_test/bridge_test# cat Dockerfile
FROM python:3.7
MAINTAINER xiaogp
ENV PIPURL=https://mirrors.aliyun.com/pypi/simple/
COPY . /home
WORKDIR /home
RUN pip install -r requirements.txt -i ${PIPURL}
CMD ["python", "api.py"]
root@ubuntu:~/docker_test/bridge_test# tree
.
├── api.py
├── Dockerfile
├── etc
│ └── config.yml
└── requirements.txt
root@ubuntu:~/docker_test/bridge_test#docker build -t xiaogp/bridge_test .
在/etc/config.yml下存储了MySQL的链接信息,其中host是docker0的网关地址172.17.0.1,不能是127.0.0.1,在容器中127.0.0.1指向容器自己,最后把config和相关的目录一起挂载到容器下运行
root@ubuntu:~/docker_test/bridge_test/etc# cat config.yml
host: 172.17.0.1
port: 3306
database: test
user: xiaogp
password: gp123456
root@ubuntu:~/docker_test/bridge_test# docker run --rm -p 5001:5001 -v `pwd`:/home a80d59e81ee8
测试接口返回数据正常
>>> res = requests.post("http://127.0.0.1:5001/student", json={"name": "b"})
>>> res.json()
{'code': 200, 'data': '[{"phone": "13874530293"}]', 'msg': 'success'}
如果将配置中的host改为本地回环地址127.0.0.1则报错MySQL拒绝链接
# 修改host为127.0.0.1
>>> res = requests.post("http://127.0.0.1:5001/student", json={"name": "b"})
>>> res.json()
{'code': 400, 'msg': 'fail', 'trace': [2003, "Can't connect to MySQL server on '127.0.0.1' ([Errno 111] Connection refused)"]}
Host
在Host模式下,容器直接使用Host宿主机网络,此时容器的localhost就是宿主机的localhost,可以直接使用本地调用服务的IP和端口,在启动时设置--net=host
来进行设定,此时config可以直接指定本地回环地址127.0.0.1
# 修改config的host
host: 127.0.0.1
port: 3306
database: test
user: xiaogp
password: gp123456
root@ubuntu:~/docker_test/bridge_test# docker run --rm -v `pwd`:/home --net=host a80d59e81ee8
>>> res = requests.post("http://127.0.0.1:5001/student", json={"name": "b"})
>>> res.json()
{'code': 200, 'data': '[{"phone": "13874530293"}]', 'msg': 'success'}
(3)容器间互相访问
使用容器虚拟IP直接访问
在同一个网桥下的所有容器IP是相通的,在获得容器IP之后,可以在某个容器中指定访问其他容器的IP和端口进行访问,现将宿主机的MySQL搬到容器中,在另一个容器中启动一个API访问MySQL,先下载一个MySQL镜像然后启动容器MySQL服务
root@ubuntu:~# docker pull mysql:5.7
容器启动脚本,端口映射到本机的3307
root@ubuntu:~/docker/mysql# cat run.sh
#!/bin/bash
cd /home/docker/mysql
docker run -d -p 3307:3306 -e MYSQL_ROOT_PASSWORD=gp123456 --name mysql mysql:5.7
MySQL远程链接脚本
root@ubuntu:~/docker/mysql# cat connect.sh
#!/bin/bash
mysql -h127.0.0.1 -P3307 -uroot -pgp123456
测试在宿主机远程链接容器的MySQL服务,并在其中插入几条测试数据
root@ubuntu:~/docker/mysql# bash connect.sh
mysql: [Warning] Using a password on the command line interface can be insecure.
Welcome to the MySQL monitor. Commands end with ; or \g.
Your MySQL connection id is 7
mysql>
下一步查看MySQL的容器IP为172.17.0.3
root@ubuntu:~/docker/mysql# docker inspect -f {{".NetworkSettings.IPAddress"}} mysql
172.17.0.3
修改api config中的host为MySQL容器的IP为172.17.0.3
root@ubuntu:~/docker_test/bridge_test/etc# cat config.yml
host: 172.17.0.3
port: 3306
database: test
user: root
password: gp123456
重新启动api容器
root@ubuntu:~/docker_test/bridge_test# docker run --rm -p 5001:5001 -v `pwd`:/home a80d59e81ee8
测试接口可以正常且正确的返回数据
>>> res = requests.post("http://127.0.0.1:5001/student", json={"name": "a"})
>>> res.json()
{'code': 200, 'data': '[{"phone": "13876545372"}]', 'msg': 'success'}
使用--link
如果是以虚拟IP进行访问,IP是经常会变化的,因此另一种方式是采用link
来为容器起个名字,link就是类似使用了容器的IP地址,无需知道其真实IP,使用link后的别名,这样解决了IP常发生变化而导致的问题
link的格式如下,别名alias可选
--link <name or id>:alias
将需要被连接的容器叫做源容器,另一个连接源容器的叫做接受容器,接受容器中可以直接ping通link指定的name和alias,相当于此时name和alias就是MySQL容器的虚拟IP
修改flask api的config的mysql host为mysql_host
root@ubuntu:~/docker_test/bridge_test/etc# cat config.yml
host: mysql_host
port: 3306
database: test
user: root
password: gp123456
此时再次挂载配置文件启动容器,指定--link将MySQL容器的虚拟IP以容器名并配以别名绑定到api容器一起启动
root@ubuntu:~/docker_test/bridge_test# docker run --rm -d -p 5001:5001 -v `pwd`:/home --link mysql:mysql_host a80d59e81ee8
810a6c982a5b709ce104fbdf89268e2243760e70b9eeae26b117255e1a267c6d
测试api是否能够正常访问ok
>>> res = requests.post("http://127.0.0.1:5001/student", json={"name": "a"})
>>> res.json()
{'code': 200, 'data': '[{"phone": "13345434986"}]', 'msg': 'success'}
此时进入接受容器,发现在/etc/hosts下已经增加了MySQL容器name,容器id,link的alias的IP地址映射172.17.0.3 mysql_host 5fc7595d085e mysql
root@ubuntu:~/docker_test/bridge_test/etc# docker exec -it 810a6c982a5b /bin/bash
root@810a6c982a5b:/home# cat /etc/hosts
127.0.0.1 localhost
::1 localhost ip6-localhost ip6-loopback
fe00::0 ip6-localnet
ff00::0 ip6-mcastprefix
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters
172.17.0.3 mysql_host 5fc7595d085e mysql
172.17.0.4 810a6c982a5b