1. 程式人生 > >老司機實戰Windows Server Docker:5 Windows Server Dockerfile葵花寶典

老司機實戰Windows Server Docker:5 Windows Server Dockerfile葵花寶典

前面兩篇(簡單運維1簡單運維2)介紹了一些Windows Server Docker相關的基本運維知識。今天這一篇,Windows Server Dockerfile葵花寶典,涵蓋了許多典型場景的Windows Server下的Dockerfile例項,並且每一個都包含可直接執行的程式碼例項,完全開源,並且新示例持續新增中。也希望大家能一起貢獻經驗。

示例原始碼

所有示例均經過Windows Server 2016環境實際測試。如果你需要了解如何配置一個Windows Server Docker的測試環境,可以參考本系列的第一篇

咱們從一些基礎的Dockerfile命令說起:

ENTRYPOINT和CMD

FROM microsoft/windowsservercore

#usually, ENTRYPOINT should be fixed and CMD to be overridden when necessary
#always use the exec form instead of shell mode for ENTRYPOINT and CMD commands and params

ENTRYPOINT ["ping", "-n", "3"]
CMD ["baidu.com"]

#to build, execute: docker build -t entrypoint_and_cmd .
#to override CMD, execute: docker run --rm entrypoint_and_cmd 360.cn

每一個Dockerfile必須的兩個命令是FROM命令,和ENTRYPOINT/CMD。FROM命令無需多說,就是指定當前docker image的爹是誰。這裡重點說說,ENTRYPOINT和CMD命令。ENTRYPOINT和CMD都可以用來指定docker容器例項啟動時執行的啟動程式,這兩個命令可以分別使用,但是一般推薦組合使用。

以上面的示例為例,這裡組合使用了ENTRYPOINT和CMD,執行時的效果是什麼呢?

如果我們在命令列執行docker run entrypoint_and_cmd啟動這個docker容器,它就相當於執行了下面這一行命令:

ping -n 3 baidu.com

而如果我們用docker run entrypoint_and_cmd 360.cn啟動這個容器,則相當於執行了下面這條命令:

ping -n 3 360.cn

也就是說,通過CMD指定的部分,在執行docker run時,如果在引數中的image名字後面指定了一些引數,這些引數會覆蓋Dockerfile中定義的CMD後面的命令。

限於篇幅,本文中演示的示例,都是解決相同問題的最佳實踐或者推薦實踐。但是不會擴充套件講太多為什麼這是最佳實踐,但是我會附上一些參考資料,感興趣的朋友可以自行閱讀。以上面的ENTRYPOINT和CMD的區別問題,更多延伸內容,請參考這篇文章:Dockerfile: ENTRYPOINT vs CMD

ADD和COPY

FROM microsoft/windowsservercore

#on windows, ADD & COPY almost the same

ADD test_folder /test_folder_add
COPY test_folder /test_folder_copy

ADD test_folder /test_folder_add2
COPY test_folder /test_folder_copy2

#unlike on linux, ADD doesn't support uncompressing zip files on windows

ADD test_folder.zip /
COPY test_folder.zip /

ADD test_folder.zip /test_folder_add2.zip
COPY test_folder.zip /test_folder_copy2.zip

#how to ADD or COPY files containing space in the name

ADD ["file name with space.txt", "/"]
COPY ["file name with space.txt", "/file name with space copy.txt"]

#overwriting files also works

ADD test.txt /test_overwrite_add.txt
ADD test_overwrite_add.txt /test_overwrite_add.txt

COPY test.txt /test_overwrite_copy.txt
COPY test_overwrite_copy.txt /test_overwrite_copy.txt

#add files with wildchar also works

ADD wildchar* /
RUN del wildchar*
COPY wildchar* /

#the only obvious difference of ADD and COPY on windows is, you can use ADD for downloading files, but not COPY

ADD https://github.com/jquery/jquery/archive/3.2.1.zip /
ADD https://github.com/jquery/jquery/archive/3.2.1.zip /3.2.1_2.zip

ENTRYPOINT ["cmd", "/c", "dir"]

#to build, execute: docker build -t add_and_copy .
#to run, execute: docker run --rm add_and_copy

ADD和COPY也是幾乎每個Dockerfile都會用到的命令,它們的作用其實非常類似,就是將檔案或者目錄從docker client執行的機器複製到正在編譯的docker image中。這兩個命令只有一些細小差別:

  • ADD命令可用於從一個URL下載檔案,而COPY命令不行;
  • 在Linux下的docker中,ADD命令還有一個特殊能力,就是如果ADD一個zip壓縮包的話,docker build時它能做自動解壓縮,但是在Windows Docker下,沒有這個效果;

合併和減少RUN命令

FROM microsoft/windowsservercore

#run 2 commands in sequence even if the firts one fails, but not the second fails
RUN cd notexists & md folder1

#run 2 commands in sequence only if both succeed
RUN md folder2 && md folder3

#run 2 commands in sequence only if at least one succeeds
RUN md folder4 || cd notexists

#if one line of RUN is too long, breakdown into multiple lines with \ at the end
#so that it is more friendly for code review
RUN echo 1 \
         2 \
         3 

ENTRYPOINT ["cmd", "/c", "dir"]

#to build, execute: docker build -t merge_and_reduce_run .
#to run CMD, execute: docker run --rm merge_and_reduce_run

在執行docker build的時候,我們一定能注意到它有step 1,step 2……每個step對於docker build來說,實際上就是建立一個臨時的docker image,只執行了一個RUN,所以,如果我們不注意,將每個細小的步驟都寫一個RUN的話,最後就會發現,我們的Dockerfile的build變得非常慢,因為步驟太多了。所以,我們應該合理地合併沒必要分開RUN的命令。

但是,合併的兩個命令到一個RUN裡,其實沒表面那麼簡單。以上面的示例為例,前面三個主要是說如何合併多個windows cmd命令:

  • &連線的兩個命令代表即使前一個命令執行失敗,只要後一個命令執行成功,這個RUN就不算失敗;
  • &&連線的兩個命令代表兩個命令都必須執行成功,整個RUN才成功;
  • ||連線的兩個命令代表只要任意一個執行成功,就算整個RUN成功;

注意,所謂RUN成功指的是,如果RUN失敗的話,docker build就會中斷執行。因此,我們要根據實際情況,只允許可以失敗的命令失敗,確保重要的命令必須成功。

當然,除了合併windowns cmd命令,我們也可以合併多個powershell命令到一個RUN,例如:

RUN powershell -command "command1;command2;command3"

或者,如果有比較複雜的多個命令,我們最好把多個命令寫成一個.cmd或者.ps1指令碼,這樣,Dockerfile就只需要一個RUN了。

另外,對於一行RUN太長的情況,最好通過""分割成多行書寫,這主要是為了方便做程式碼的Review,這樣在diff工具裡,能更清晰的顯示到底改了什麼引數。

到目前為止的sample,其實都不不算Windows Docker特定的案例,相比Linux下,其實都很類似,屬於基礎中的基礎。下面,我們就開始介紹一些Windows下特有的案例:

unzip

原始碼:unzip

FROM microsoft/windowsservercore

COPY test_folder.zip /

#unzip
RUN powershell -Command "expand-archive -Path 'c:\test_folder.zip' -DestinationPath 'c:\'"

ENTRYPOINT ["cmd", "/c", "dir"]

#to build, execute: docker build -t unzip .
#to run CMD, execute: docker run --rm unzip

前面我們說到,ADD命令在Windows Docker下不支援解壓縮zip檔案。那麼,在Windows下如何解壓縮呢?最簡單的方法,就是使用Expand-Archive這個powershell命令。

set_hosts

FROM microsoft/iis

#install ASP.NET 4.5
RUN dism /online /enable-feature /all /featurename:IIS-ASPNET45 /NoRestart

#deploy webapp
COPY iis-demo /inetpub/wwwroot/iis-demo
RUN /windows/system32/inetsrv/appcmd.exe add app /site.name:"Default Web Site" /path:"/iis-demo" /physicalPath:"c:\inetpub\wwwroot\iis-demo"

#set entrypoint script
COPY scripts /scripts
ENTRYPOINT ["C:\\scripts\\SetHostsAndStartMonitoring.cmd"]

#to build, execute: docker build -t set_hosts .
#to run, execute: docker run --rm --env-file ./hosts.env set_hosts

前面的文章中,我們提到過Windows下的Docker目前不支援docker run的--add-host引數。所以,這裡我們提供了一個基於環境變數,這裡用一個環境變數檔案(./hosts.env ),設定Windows系統的hosts檔案的方法。其中讀取環境變數並設定hosts的程式碼,其實就是下面的powershell指令碼:

If ($env:HOSTS) {
  $hosts = $env:HOSTS.Replace(",", "`r`n");
  $hosts | Set-Content "C:\Windows\System32\drivers\etc\hosts"
  "Applied hosts: `r`n" + $hosts;
}

gacutil

FROM microsoft/windowsservercore

#copy minimized gacutil binary to container
COPY tools.zip /
RUN powershell -Command "expand-archive -Path 'c:\tools.zip' -DestinationPath 'c:\'"

#install a DLL to GAC with gacutil
COPY Microsoft.VisualStudio.QualityTools.UnitTestFramework.dll /
RUN /tools/gacutil.exe /i Microsoft.VisualStudio.QualityTools.UnitTestFramework.dll

ENTRYPOINT ["cmd", "/c", "/tools/gacutil.exe", "/l", "Microsoft.VisualStudio.QualityTools.UnitTestFramework"]

#to build, execute: docker build -t gacutil .
#to run, execute: docker run --rm gacutil

gacutil是常用的通過命令列註冊.NET DLL到GAC的工具。但是這個工具包含在.Net Framework SDK中,並不包含於.NET Framework的分發庫中。而為了註冊幾個DLL而讓docker容器裡面安裝一個臃腫的.NET SDK實在有點難受。因此,這個示例包含了從.NET Framework SDK中抽取出來的單獨的gacutil工具,只有94k大小。

enable_eventlog

FROM microsoft/windowsservercore

#enable eventlog
RUN powershell.exe -command Set-ItemProperty HKLM:\SYSTEM\CurrentControlSet\Control\WMI\Autologger\EventLog-Application Start 1

# list latest 10 system eventlog
CMD powershell -command "Get-EventLog system -newest 10 | Format-List"

#to build, execute: docker build -t enable_eventlog .
#to run, execute: docker run --rm enable_eventlog

這個太簡單了,不多解釋了,就是通過powershell設定了一個登錄檔值。

enable_wcf

FROM microsoft/iis

#install ASP.NET 4.5
RUN dism /online /enable-feature /all /featurename:IIS-ASPNET45 /NoRestart

#install WCF features, here we only enabled http and tcp protocol
#for the full list of features you can enable, execute "dism /online /get-features" on a windows server machine
RUN dism /online /enable-feature /featurename:WCF-HTTP-Activation45 /all
RUN dism /online /enable-feature /featurename:WCF-TCP-Activation45 /all

#enable WCF protocols
RUN /windows/system32/inetsrv/appcmd.exe set config -section:system.applicationHost/sites /[@0].[@0].enabledProtocols:"http,net.tcp"

ENTRYPOINT ["c:\\ServiceMonitor.exe", "w3svc"]

#to build, execute: docker build -t enable_wcf .
#to run, execute: docker run --rm enable_wcf

在遠古的SOA時代(好像就在眼前,悲傷),WCF是.NET下最熱門的技術之一,現在是江河日下了。不過,如何通過命令列enable WCF並設定IIS裡的protocols呢?
另外,提一下,上面這行ENTRYPOINT ["c:\ServiceMonitor.exe", "w3svc"]其實不是必須的,因為microsoft/iis這個image其實預設就是執行的這個ENTRYPOINT。這其實是在監控w3svc這個Windows Service的執行,我們當然也可以用它來監控我們自己的Windows Service的,這裡順便提一下。

set_iis_ssl

FROM microsoft/iis

#install ASP.NET 4.5
RUN dism /online /enable-feature /all /featurename:IIS-ASPNET45 /NoRestart

#setup SSL in IIS
ADD iis-test.pfx \iis-test.pfx
ADD SetSSL.ps1 \SetSSL.ps1

#set entrypoint script
ADD SetSSLAndStart.cmd \SetSSLAndStart.cmd
ENTRYPOINT ["C:\\SetSSLAndStart.cmd"]

#to build, execute: docker build -t set_iis_ssl .
#to run, execute: docker run --rm -e "SSL_PASS=test" -p 443:443 set_iis_ssl

通過命令列設定IIS的SSL,這個可能幹過的不多,但現在SSL幾乎是大多數主流網站的標配了,在docker容器裡部署網站,也是必不可少的。其中,主要的邏輯,在SetSSLAndStart.cmd中呼叫SetSSL.ps1執行:

cmd /c "certutil -p %SSL_PASS% -importPFX c:\iis-test.pfx"
$addr=[System.Net.Dns]::GetHostAddresses([System.Environment]::MachineName).IPAddressToString
$env:ssl_addr=$addr[1]+":443"
cmd /c 'netsh http add sslcert ipport=%ssl_addr% certstorename=MY certhash=C97E4D29C00D9B250EADCFE27D50F09FA76599B0 appid="{4dc3e181-e14b-4a21-b022-59fc669b0914}"'
New-WebBinding -name "Default Web Site" -Protocol https  -HostHeader * -Port 443 -SslFlags 1

需要注意的是,這裡SSL_PASS包含的證書密碼,是讀取的一個環境變數。因為,pfx格式的證書中包含非常重要的私鑰,我們不可以將密碼寫在指令碼中,必須在docker run的時候傳入。另外,netsh http add sslcert的引數中,certhash這個引數的值,這裡hardcode了iis-test.pfx這個證書的hash值,如果你要安裝的是你自己的證書,需要用你自己證書的hash值替換。對於已經安裝於當前機器的自定義證書,我們可以通過下面的powershell命令,列出所有的證書和它們的hash值:

Get-ChildItem cert:\LocalMachine\My

再有,這裡的SSL我們是設定到機器當前的ip,除了將SSL通過ipport引數繫結到ip,我們也可以通過hostnameport將SSL繫結到hostname,具體請參考netsh http add sslcert的相關文件。另外,這個示例的部分程式碼參考了這篇文章

本篇完!不過,本文今後還會持續更新,後面有新的Windows Server Dockerfile的武功心法,我會陸續補充到這篇文章,大家也可以關注相關的Github Repo獲取更新。