為了實(shí)現(xiàn)CI/CD,先來定制一個(gè)Docker鏡像
背景
計(jì)劃把手頭的項(xiàng)目逐步改造為基于Docker容器的方式發(fā)布,同時(shí),項(xiàng)目中已經(jīng)采用了云廠商提供的CI/CD自動(dòng)化發(fā)布流水線。因此,為配合CI/CD操作,需要先針對項(xiàng)目構(gòu)建一些發(fā)布的腳本,通過腳本來操作Docker鏡像定制、Docker的啟動(dòng)和停止。
在閱讀和實(shí)踐本篇文章之前,如果你還未搭建Docker的環(huán)境,可參考上篇文章《??Linux安裝Docker完整教程??》,先把整個(gè)環(huán)境搭建起來,同時(shí)熟悉一下Docker的基本操作命令。
這篇文章就配合具體的實(shí)踐案例來為大家講講如何定制一個(gè)Docker鏡像,并通過腳本來執(zhí)行鏡像的構(gòu)建、項(xiàng)目的發(fā)布、容器的啟動(dòng)與停止等。
什么是Dockerfile?
Dockerfile是一個(gè)用來構(gòu)建鏡像的文本文件,文本內(nèi)容包含了一條條構(gòu)建鏡像所需的指令和說明。常見的指令比如有:FROM、RUN、ADD、COPY、CMD、ENV等。
在鏡像構(gòu)建時(shí),需要注意的一點(diǎn)是:鏡像的構(gòu)建是一層層構(gòu)建的,前一層是后一層的基礎(chǔ)。每一層構(gòu)建完就不會再發(fā)生改變,后一層上的任何改變只發(fā)生在自己這一層。
像上面提到的指令,每一次操作都會構(gòu)建一層。比如刪除前一層的文件,在最終容器運(yùn)行時(shí),雖然看不到這個(gè)文件,但是實(shí)際上該文件會一直跟隨鏡像。因此,在構(gòu)建鏡像時(shí),需要額外小心,每一層盡量只包含該層需要添加的東西,任何額外的東西應(yīng)該在該層構(gòu)建結(jié)束前清理掉。
另外,為了減少構(gòu)建層的數(shù)量,在編寫Dockerfile文件時(shí)盡量將多層的指令合并成一層執(zhí)行,比如兩個(gè)RUN命令可以通過&&將其合并成一條。
不建議的鏡像制作方式
制作Docker鏡像通常有兩種方式:基于docker commit和基于Dockerfile的形式。
Docker提供了一個(gè) docker commit 命令,可以將容器的存儲層保存下來成為鏡像。換句話說,就是在原有鏡像的基礎(chǔ)上,再疊加上容器的存儲層,并構(gòu)成新的鏡像。后續(xù)運(yùn)行這個(gè)新鏡像時(shí),就會擁有原有容器最后的文件變化。
docker commit的方式除了學(xué)習(xí)之外,還可以用于一些特殊的場景,比如被入侵后保存現(xiàn)場等。但是不要使用 docker commit 定制鏡像,定制鏡像應(yīng)該使用 Dockerfile 來完成。
這是因?yàn)樵谑褂胐ocker commit制作鏡像時(shí),除了我們想要修改的內(nèi)容(文件)之外,該命令還會修改一些其他的文件,而且所有對鏡像的操作都是黑箱操作,生成的鏡像也被稱為黑箱鏡像。
除了制作鏡像的人知道執(zhí)行過什么命令、怎么生成的鏡像,別人根本無從得知。即使制作鏡像的人,一段時(shí)間后可能也無法記清具體的操作。這種黑箱鏡像的維護(hù)工作是非常痛苦的。
另外,如果使用 docker commit 制作鏡像,以及后期修改的話,每一次修改都會讓鏡像更加臃腫一次,所刪除的上一層的東西并不會丟失,會一直如影隨形的跟著這個(gè)鏡像,即使根本無法訪問到。這會讓鏡像更加臃腫。
因此,這里我們不采用 docker commit 的方式制作鏡像,如果大家感興趣的話,可以在網(wǎng)絡(luò)上查詢一下該方式的制作流程。本文重點(diǎn)介紹基于 Dockerfile 的方式來制作鏡像,下面就以實(shí)例展示一下如何構(gòu)建一個(gè)Docker鏡像。
Dockerfile指令編寫
在/opt目錄下創(chuàng)建一個(gè)業(yè)務(wù)目錄/opt/channel/docker(這里部署的項(xiàng)目為渠道項(xiàng)目,取名channel),在該目錄下存放Dockerfile、待發(fā)布的jar包等資源文件。
$ cd /opt/channel/docker
$ touch Dockerfile
上述指令先進(jìn)入/opt/channel/docker目錄、創(chuàng)建了一個(gè)空的Dockerfile(文本)文件。
編輯Dockerfile內(nèi)容如下:
FROM java:8
COPY ./hqy-service-channel.jar ./app.jar
ENV spring.profiles.active prod
EXPOSE 8190
ENTRYPOINT ["java", "-jar","-Duser.timezone=GMT+08", "./app.jar"]
Dockerfile中涉及到FROM、COPY、ENV、EXPOSE、ENTRYPOINT五個(gè)指令,下面逐一講解。
FROM指令
所謂制作鏡像,就是在已經(jīng)存在的鏡像的基礎(chǔ)上進(jìn)行定制?;A(chǔ)鏡像是必須指定的,而 FROM 就是指定基礎(chǔ)鏡像,因此一個(gè) Dockerfile 中 FROM 是必備的指令,并且必須是第一條指令。
這里的FROM java:8,也就是采用openjdk在Docker鏡像源中的鏡像,版本為8。可以通過search命令查看一下這個(gè)鏡像:
[docker]# docker search java
NAME DESCRIPTION STARS OFFICIAL AUTOMATED
node Node.js is a JavaScript-based platform for s… 11734 [OK]
tomcat Apache Tomcat is an open source implementati… 3368 [OK]
openjdk "Vanilla" builds of OpenJDK (an open-source … 3362 [OK]
java DEPRECATED; use "openjdk" (or other JDK impl… 1976 [OK]
第4個(gè)name為java的便是,為了方便后面操作,這里直接將鏡像pull到本地。
docker pull java:8
查看本地pull之后,本地的鏡像列表:
[root@iZ2zehx0enix3i0aiea7p0Z docker]# docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
java 8 d23bdf5b1b1b 5 years ago 643MB
后續(xù)執(zhí)行鏡像制作時(shí)便以該鏡像為基礎(chǔ)進(jìn)行構(gòu)建。
COPY指令
COPY,復(fù)制指令,從上下文目錄中復(fù)制文件或者目錄到容器里指定路徑。
COPY ./hqy-service-channel.jar ./app.jar
其中第一個(gè)參數(shù)為源文件路徑,第二個(gè)參數(shù)為容器內(nèi)目標(biāo)文件路徑。這里是將當(dāng)前目錄下的Spring Boot項(xiàng)目jar包hqy-service-channel.jar,復(fù)制到容器內(nèi)并命名為app.jar。在執(zhí)行創(chuàng)建鏡像命令之前,需要把項(xiàng)目jar包放到Dockerfile同級目錄下。
ENV指令
ENV指令,用于設(shè)置環(huán)境變量,定義了環(huán)境變量,那么在后續(xù)的指令中,就可以使用這個(gè)環(huán)境變量。
基本格式為:
ENV <key> <value>
ENV <key1>=<value1> <key2>=<value2>...
第一個(gè)參數(shù)為變量key,第二個(gè)參數(shù)為變量值,這里用于設(shè)置SpringBoot項(xiàng)目的配置文件的profile為prod(生產(chǎn)配置文件)。
EXPOSE指令
EXPOSE指令,僅僅只是聲明端口。作用是幫助鏡像使用者理解這個(gè)鏡像服務(wù)的守護(hù)端口,以方便配置映射。另外,在運(yùn)行時(shí)使用隨機(jī)端口映射時(shí),也就是 docker run -P 時(shí),會自動(dòng)隨機(jī)映射 EXPOSE 的端口。
基本格式:
EXPOSE <端口1> [<端口2>...]
這里采用了8190端口。
ENTRYPOINT指令
ENTRYPOINT指令,類似于CMD指令,但其不會被docker run的命令行參數(shù)指定的指令所覆蓋,而且這些命令行參數(shù)會被當(dāng)作參數(shù)送給 ENTRYPOINT指令指定的程序。在執(zhí)行docker run時(shí)可以指定ENTRYPOINT運(yùn)行所需的參數(shù)。
ENTRYPOINT ["<executeable>","<param1>","<param2>",...]
這里使用ENTRYPOINT指令來執(zhí)行jar -jar啟動(dòng)SpringBoot項(xiàng)目。
RUN指令
RUN指令雖然在實(shí)例中沒用到,但也是非常常見的一個(gè)指令,于執(zhí)行后面跟著的命令行命令,有以下兩種格式。
shell 格式:
RUN <命令行命令>
# <命令行命令> 等同于,在終端操作的 shell 命令。
exec格式:
RUN ["可執(zhí)行文件", "參數(shù)1", "參數(shù)2"]
# 例如:
# RUN ["./test.php", "dev", "offline"] 等價(jià)于 RUN ./test.php dev offline
經(jīng)過上述一系列的操作,Dockerfile文件編寫完畢。在構(gòu)建命令時(shí)值得注意的是:按照Docker最佳實(shí)踐的要求,容器不應(yīng)該向其存儲層內(nèi)寫入任何數(shù)據(jù),容器存儲層要保持無狀態(tài)化。所有的文件寫入操作,都應(yīng)該使用數(shù)據(jù)卷(Volume)、或者綁定宿主目錄,在這些位置的讀寫會跳過容器存儲層,直接對宿主(或網(wǎng)絡(luò)存儲)發(fā)生讀寫,其性能和穩(wěn)定性更高。
構(gòu)建鏡像
上面準(zhǔn)備好了Dockerfile文件,再把對應(yīng)的jar包放在指定的位置,可在Dockerfile文件的目錄執(zhí)行構(gòu)建命令,比如:
docker build -t channel .
其中-t channel指定了構(gòu)建鏡像的名稱,當(dāng)然也可以同時(shí)指定版本編號-t channel:v1。后面的“.”指的是當(dāng)前目錄。
執(zhí)行效果如下:
[docker]# docker build -t channel .
Sending build context to Docker daemon 82.31MB
Step 1/5 : FROM java:8
---> d23bdf5b1b1b
Step 2/5 : COPY ./hqy-service-channel.jar ./app.jar
---> 10cb376c7572
Step 3/5 : ENV spring.profiles.active test
---> Running in ca70651b21b6
Removing intermediate container ca70651b21b6
---> ec420f94df51
Step 4/5 : EXPOSE 8190
---> Running in 318e718d552a
Removing intermediate container 318e718d552a
---> 6746bad4a990
Step 5/5 : ENTRYPOINT ["java", "-jar","-Duser.timezone=GMT+08", "./app.jar"]
---> Running in 135de4d42ec8
Removing intermediate container 135de4d42ec8
---> 1720afb4fec7
Successfully built 1720afb4fec7
Successfully tagged channel:latest
執(zhí)行docker images可查看到鏡像構(gòu)建完畢:
[docker]# docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
channel latest 1720afb4fec7 51 seconds ago 725MB
java 8 d23bdf5b1b1b 5 years ago 643MB
后續(xù)便可以通過docker run命令來啟動(dòng)容器了。
這里為了方便CI/CD操作,我們可以通過腳本來完成整個(gè)容器停止、容器移除、鏡像的移除、鏡像的重新制作以及容器的重新啟動(dòng),這樣CI/CD的系統(tǒng)只用調(diào)用對應(yīng)的腳本即可。
示例腳本start.sh如下:
# 停止容器
docker stop channel
echo "停止容器success!"
# 移除容器
docker rm channel
echo "移除容器success!"
# 移除鏡像
docker rmi channel
echo "移除鏡像success!"
# 制作鏡像
docker build -t channel /opt/channel/docker/
echo "制作鏡像success!"
# 啟動(dòng)容器
docker run -d --name channel -p 8190:8190 -v /opt/channel/logs/:/opt/channel/logs/ channel channel:latest
echo "啟動(dòng)success!"
執(zhí)行上述腳本之后,查看容器執(zhí)行結(jié)果:
[bin]# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
e9eff75cdb6f channel "java -jar -Duser.ti…" 30 seconds ago Up 28 seconds 0.0.0.0:8190->8190/tcp channel
可以看到容器已經(jīng)成功啟動(dòng)。當(dāng)重新構(gòu)建新的jar包時(shí),只需對目錄中的jar包進(jìn)行替換,然后再執(zhí)行一遍start.sh命令即可。
小結(jié)
本文帶大家以具體的實(shí)例演示了如何制作Docker鏡像,在制作Docker鏡像過程中需要注意的事項(xiàng),以及制作之后用于CI/CD的腳本編寫。大家可參考以上實(shí)例,根據(jù)自己的業(yè)務(wù)場景所需進(jìn)行對應(yīng)的改造。