0%

/images/20190430/0.jpg

前言

公司慢慢在導入 Docker 等相關 Container 技術,但因為剛起步所以很多 CI/CD 流程都還在優化建構中,前幾天為了能讓 Jenkins 順利重新部屬新版 Container 搞得焦頭爛額,乾脆把這些過程記錄下來


部屬流程

/images/20190430/1.png

這個服務是由三個 Component 來組成,分開維護開發,換句話說如果有任何一個組件更新,整個服務都需要更新並重新部屬,目前的流程是各個 Component 更新後都會觸發 CD 流程將完成品打包放到 S3。

而我這次做得線就是 Docker Image 那條,當有任何 Component 更新了都會觸發,我會將 S3 的各個 Component 最新版抓下來後整理,接著再打包一版 Build Docker Image 會需要的原料放到 S3。

接著 Jenkins 接手將 Docker Image Artifact 抓下來開始 build => push => 換掉遠端機器正在跑的 Container。


設定權限

所以第一步是釐清 Jenkins 所使用的 user 是否有執行 docker 相關指令的權限,後面都簡稱這位使用者為 ciuser

因為 docker 為 service 等級的服務,預設是 admin 才能使用相關指令,所以理所當然地馬上卡關

/images/20190430/2.png

要讓使用者有執行 docker 相關指令的權限有兩種方法

  1. 賦予該 User Admin 的權限
  2. 開一個群組,並設定 docker daemon ,賦予該群組權限

顯然 (1) 不太可能(雖然最簡單),所以這邊採取 (2) 的方法

*** Windows 上右鍵 > Computer Management**

/images/20190430/3.png

*** Local Users and Groups >> Groups **

/images/20190430/4.png

*** New Group >> 建立一個群組 >> 將 ciuser 加進去**

/images/20190430/5.png

*** 到 C:\ProgramData\docker\config 修改 daemon.json 檔案**(如果沒有這個檔案,請自己建一個)

將剛剛的群組加進去

1
2
3
{
"group": "docker-users"
}

*** 重新啟動 docker service ** (這邊需要 admin 權限)

1
2
# Powershell
$ Restart-Service docker

做到這邊你在用 ciuser 下 docker command 應該就可以動了

/images/20190430/6.png

PS. 如果還不行,請先登出 ciuser 再登入,因為將使用者加入一個群組或賦予權限,重登才會生效

到這邊應該已經打通 ciuser 可以執行 docker command 的權限了,接著在 Jenkins 設定 build 、 push Image的流程測試會發現依然錯誤

error during connect: Get http://%2F%2F.%2Fpipe%2Fdocker_engine/v1.40/containers/json: open //./pipe/docker_engine: The system cannot find the file specified. In the default daemon configuration on Windows, the docker client must be run elevated to connect. This error may also indicate that the docker daemon is not running.

但同樣的指令不透過 Jenkins 執行,而是用遠端登入直接執行會過,這個問題想了很久還是搞不懂為什麼,明明使用的 User 相同 …

但找到了一篇文章也提到相同的事情並提供了解決方案,主要是需要調整 docker_engine ACL 設定,但因為過程太麻煩,作者很貼心的還提供了小套件可以使用

1
2
3
4
# Powershell
$ Install-Module -Name dockeraccesshelper
$ Import-Module dockeraccesshelper
$ Add-AccountToDockerAccess "ciuser"

再次執行 Jenkins 就真的成功了


設定 Jenkins

Build Image 、 Push Image 這些流程就直接省略了,重點是最後一步怎麼觸發遠端機器更新 Container 。

Docker Compose 提供 --host ,讓你可以操作遠端機器的 Container

/images/20190430/7.png

但前提是你必須先去該機器開啟 2376 port 的防火牆,還有設定 docker daemon config,跟上面那段一樣檔案,只是上次設定的是 jenkins 機器,這次是跑 Container 的機器

路徑 : C:\ProgramData\docker\config\daemon.json

1
2
3
{
"hosts": ["tcp://0.0.0.0:2376", "npipe://"]
}

都開好後回頭在 Jenkins 的流程中埋下最後一段

1
2
3
$ docker-compose -H machineIp:2376 pull

$ docker-compose -H machineIp:2376 up -d

結語

雖然只是短短的一篇,但是整個設定的過程其實卡很久,尤其是 windows 的權限不是很熟常常會把自己搞得很亂,但為了團隊能接手維運,這些過程又勢必有人需要去踩過一次,以前都覺得 DevOps 的 Dev 才是主力(自己大部分經歷也都是待在開發團隊),對於維運一直都沒有很深入去了解並感受他們的痛。

但當角色從 Developer 漸漸轉成 Infra ,不同的角度去看這些事情有了完全不同的感受,維運真的才是整個軟體生命週期佔最大的部分,一套系統你可能開發個 1 ~ 3 年,但產品的維運可能是數十年,怎麼讓一套機制落地,讓人為介入越少越好,要考慮的面向真的比過去單純開發差非常多,果然還有得學阿。

/images/20190422/0.png

前言

剛開始使用 Docker 的時候,我常常都是使用別人已經包好的 Image,但難免會有不符使用需要客製的時候,又或是將公司的某些服務做成 Docker Image ,再從多台機器上 Pull Docker Image 直接執行,所以 Docker Build 幾乎是玩 Docker 必學的技巧了

從 Docker Hub 尋找適合的 Image 來使用

/images/20190422/1.png

前陣子將公司一些服務包成 Docker Image 並部署執行

/images/20190422/2.png


製作第一個 Docker Image

下面的範例會透過一個簡單的範例來解說包 Docker Image 的過程,希望最後的成果是,我們透過執行自製的 Docker Image 就跑起來一個 ASP.Net 的網站


開新專案

/images/20190422/1.png

/images/20190422/1.png

直接用 VS 執行起來就是一個最原始的 MVC 網站

/images/20190422/1.png


Dockerfile

Dockerfile 是 Build Docker Image 的根本,這個檔案會告訴 Docker 該把什麼東西放進 Image ? 步驟是什麼 ? 怎麼啟動 ?


先新增一個 Docker File 到剛剛的專案中

/images/20190422/6.png

調整屬性

/images/20190422/7.png

這樣如果發行這個專案,dockerfile 就會一起過去

/images/20190422/8.png

/images/20190422/9.png


Dockerfile 內容

1
2
3
FROM mcr.microsoft.com/dotnet/framework/aspnet:latest

COPY . /inetpub/wwwroot

From : 表示你這個 Image 是以哪一個當作 Base,以這次的案例為例,我們選擇微軟官方提供的 mcr.microsoft.com/dotnet/framework/aspnet Image 作為 base,因為它已經幫我們安裝好 IIS 、.net Framework,這樣我們只要專注在我們開發的程式即可。

COPY : dockerfile 所屬資料夾所有的內容複製到 container 裡面的 c:/inetpub/wwwroot,也就是 IIS 的預設目錄


Docker build

1
$ docker build -t <ImageName>:<Tag> <Dockerfile Path>
1
$ docker build -t my_first_docker_image:latest -t my_first_docker_image:1.0.0 .

docker build 時我為這個 Image 下了兩個 tag,分別為 latest、1.0.0

/images/20190422/10.png


Docker Run

執行剛剛我們自建的 Image,並且將本機的 9999 port mapping 到 container 內的 80 port

1
$ docker run -d -p 9999:80 my_first_docker_image:latest

這樣就可以看到網站啦

/images/20190422/11.png


小結

這邊只是小小演示如何 Build 一個最簡單的 docker image,其實 Dockerfile 還有很多可以發揮的地方,如果有需要,可以參考以下官方文件

/images/20190323/0.png

之前在將公司某支程式移植到 Windows Container 時碰到一個問題,該程式會透過網路磁碟 (SMB) Eamil Template 來套版並寄出,但將網路磁碟透過 Volume 的方式 Mount 到 Container 卻抓不到檔案,查了一下才知道原來需要用 SmbGlobalMapping 掛載的網路磁碟才行

以下這段是先將要用到的網路磁碟透過 New-SmbGlobalMapping 指令掛成 T:

1
2
3
4
5
6
7
$ $User="yourUSerAccount"

$ $PWD = ConvertTo-SecureString -String "yourPassword" -AsPlainText -Force

$ $Credential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $User, $PWD

$ New-SmbGlobalMapping -LocalPath T: -RemotePath \\my-company-Filer\Files -Credential $Credential

然後在透過 Volume 即可

1
$ docker run -it -v T:\:C:\Files myContainer

不過還要特別注意的是,在 Windows Server (版本 1709),SMB 全域對應不支援 DFS、DFSN、DFSR 共用。

所以如果是 DFS 那就只好先強制指到某一台,期待哪一天 Windows Container 可以支援了

/images/20190321/0.jpg

整理一下最近在學習的 Road map,大致上圍繞著微服務

/images/20190315/0.jpg

最近經過大師的指導後,對於 Yield Return 又或是迭代器有更深的感受,先來看一段以前常常會寫的程式

1
2
3
4
5
6
7
8
9
10
11
12
13
void Main()
{
var users = GetUser(idList);
//過濾黑名單
var notInBlackList = users.Where(x=> !x.InBlackList);

//往下做其他邏輯
}

public List<User> GetUser(List<int> ids)
{
//透過 ID 取得會員的詳細資料
}

如果 Id 一次有一百萬筆,為了避免擊沉 Database 做了分批查詢也是很合理的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void Main()
{
var users = GetUser(idList);

//過濾黑名單
var notInBlackList = users.Where(x=> !x.InBlackList);

//往下做其他邏輯
}

public List<User> GetUser(List<int> ids)
{
var Result = new List<User>();
// 每批搜尋 1000筆
var batchCount = 1000;

//計算出搜尋次數後分批查詢
for (int i = 0; i < 搜尋次數; i++)
{
Result.AddRange(
UserRepository.Get(ids.Skip(i * batchCount).Take(batchCount)));
}
}

當然也可以從呼叫端分批處理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void Main()
{
// 每批搜尋 1000筆
var batchCount = 1000;
for (int i = 0; i < 搜尋次數; i++)
{
var users = GetUser(idList);

//過濾黑名單
var notInBlackList = users.Where(x=> !x.InBlackList);

//往下做其他邏輯
}
}

public List<User> GetUser(List<int> ids)
{
//透過 ID 取得會員的詳細資料
}

如果需要再更複雜一些,要依據使用者的設定濾掉一些人,最終要將處理過程都填回 DB ,以便追蹤那些人是因為那些條件被過濾掉的 (不然被客戶抱怨沒收到通知,不知道往哪找),這時候程式就會變成這樣….

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
void Main()
{
// 每批搜尋 1000筆
var batchCount = 1000;

var temp = new List<User>()
for (int i = 0; i < 搜尋次數; i++)
{
var users = GetUser(idList);

var notInBlackList = users.Where(x=> !x.InBlackList);

var wantToReceiveThisMessage = notInBlackList.Where(x => x.BlockMessageType != ThisMessageType);

//統計誰在黑名單被過濾掉
//統計誰不想收到這種類型的 DB

//可能批次更新是 5000 筆最有效率
temp.Add(InBlackList);
temp.Add(DontWantToReceiveThisMessage);

if (temp.Count > 5000)
{
// Insert Log
LogRepository.Add(temp);
temp.Clear();
}
}
}

public List<User> GetUser(List<int> ids)
{
//透過 ID 取得會員的詳細資料
}

條件可以無限增長上去,程式也會變得越來越複雜,當然這時候可以採取一些物件導向的設計將功能職責拆開,不過這不是這篇的重點所以跳過。

更符合語意的寫法

如果有用過 FluentAssertionNsubstitute 等套件,應該會發現它 API 設計得非常好讀好懂

1
2
3
customer.Active.Should().BeTrue(because, becauseArgs);

theObject.Should().NotBeSameAs(otherObject);

讓程式不再是一段一段的,而是一看程式就能理解這邊在做什麼,而且也因為如此讓每個方法的職責更清楚單一,所以我們可以將上述的程式改成

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
void Main()
{
//從使用端傳來一大批 Ids List
Ids.GetUser().NotInBlackList().WantToReceiveThisMessage(ThisMessageType).Record();
}

public static IEnumerable<User> GetUser(this IEnumerable<int> ids)
{
var temp = new List<int>();
foreach (var id in ids)
{
temp.Add(id);
// 假設每 1000 筆是有最好的搜尋效率
if (temp.Count == 1000)
{
foreach (var user in UserRepository.Get(temp))
{
yield return user;
}
temp.Clear();
}
}

if (temp.Count > 0 )
{
foreach (var user in UserRepository.Get(temp))
{
yield return user;
}
}
yield break;
}

public static IEnumerable<User> NotInBlackList(this IEnumerable<User> users)
{
foreach (var user in users)
{
if (user.InBlackList == false)
{
yield return user;
}
}
yield break;
}

public static IEnumerable<User> NotInBlackList(this IEnumerable<User> users, MessageType type)
{
foreach (var user in users)
{
if (user.BlockMessageType != type)
{
yield return user;
}
}
yield break;
}

public static IEnumerable<User> Record(this IEnumerable<User> users)
{
var temp = new List<User>();
foreach (var user in users)
{
temp.Add(user);
if (temp.Count == 5000)
{
// Insert Log
LogRepository.Add(temp);
temp.Clear();
}
yield return user;
}

if (temp.Count > 5000)
{
LogRepository.Add(temp);
}

yield break;
}

如果還需要紀錄每個過程的分別過濾的狀態,只需要在方法開個統計的物件參數,在每筆過程中做紀錄即可,不只讓整個程式可讀性更高、內聚更強,之後如果不想要紀錄或不想要過濾黑名單,也可以很快的調整完成

1
Ids.GetUser().WantToReceiveThisMessage(ThisMessageType);

/images/20190301/0.png

這個部落格是用 hexo 來製作的,優點是可以用 Markdown 寫法,且很多套件支援,讓我這個美感小白痴也可以輕鬆弄出富有質感的部落格(自己說)。

但也因為這樣,每次寫完文章都需要下 command 來編譯部落格、發佈到 Github ,讓我覺得不夠自動化

1
2
$ hexo g 
$ hexo deploy

剛好最近在玩 AWS ,就想說來弄一台 t2.small Centos 機器來做 CI 機器好了

/images/20190301/1.png


AWS EC2


Launch Instance

AMI : CentOS 7

/images/20190301/2.png

Type : t2.small

/images/20190301/3.png

原本用 t2.micro ,但經過測試在 Jenkins 執行 Job 的時候常常因為記憶體不足就當掉了,改 t2.small 後穩定許多

因為我之前已經有其他台 EC2 ,所以這邊我選擇用已經產過的 ssh key 來當作連線這台機器的 Key

/images/20190301/4.png


Security Group

SSH 連線需要開 22 Port,先確認一下 Security Group 有開啟

/images/20190301/5.png

用剛剛產出的 pem 檔連線到機器

1
$ ssh -i ~/.ssh/{yourSSH.pem} centos@{yourEc2IP}

如果登入成功應該會看到這樣

/images/20190301/6.png

如果遇到 Permission denied ,你需要先調整剛剛下載下來的 pem 檔的權限

1
$ chmod 400 yourSSH.pem

@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@ WARNING: UNPROTECTED PRIVATE KEY FILE! @
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
Permissions 0644 for ‘amazonec2.pem’ are too open.
It is recommended that your private key files are NOT accessible by others.
This private key will be ignored.
bad permissions: ignore key: amazonec2.pem
Permission denied (publickey).


Jenkins


安裝

基本上我完全是按照上面的文章教學一步一步完成的,在 Linux 還不是很熟的情況下,這邊只記下流水帳,詳細建議參考上述文章。

  1. Install the OpenJDK 8 package

    1
    $ sudo yum install java-1.8.0-openjdk-devel
  2. Import the GPG key

    1
    $ curl --silent --location http://pkg.jenkins-ci.org/redhat-stable/jenkins.repo | sudo tee /etc/yum.repos.d/jenkins.repo
  3. Add the Jenkins repository to your system

    1
    $ sudo rpm --import https://jenkins-ci.org/redhat/jenkins-ci.org.key
  4. Install Jenkins

    1
    $ sudo yum install jenkins
  5. Start the Jenkins service

    1
    $ sudo systemctl start jenkins
  6. Check Jenkins Status

1
$ systemctl status jenkins

/images/20190301/7.png

  1. Enable the Jenkins service to start on system boot

    1
    $ sudo systemctl enable jenkins

設定

這時候透過瀏覽器連線 Jenkins,應該會得到 Timeout 的回應

1
網址:http://yourEc2PublicIP:8080

/images/20190301/8.png

原因是機器的 Security Group 並沒有開放 80 Port 可以連進來,所以需要設定一下

/images/20190301/9.png

再次連線應該就可以看到設定畫面了

/images/20190301/10.png


第一次設定需要透過機器取得密碼

1
$ sudo cat /var/lib/jenkins/secrets/initialAdminPassword

/images/20190301/11.png

將這段密碼貼上去後就可以進入下一步

/images/20190301/12.png

選擇 Install Suggested plugins

/images/20190301/13.png

設定 Admin User

/images/20190301/14.png

/images/20190301/15.png

Hexo


安裝 Nodejs 、 NPM

為了能在 Jenkins Build Hexo ,必須先安裝好 NodeJs 並安裝 Hexo

這邊一樣只節錄流水帳,詳細說明建議參考原文

  1. Add NodeSource yum repository

    1
    $ curl -sL https://rpm.nodesource.com/setup_10.x | sudo bash -
  2. Install NodeJs and NPM

    1
    $ sudo yum install nodejs
  3. Check

    1
    2
    $ node --version
    $ npm --version

Install Git

1
2
$ sudo yum install git
$ git --version

Build Hexo

因為我原本就已經有 Hexo Blog Repository,所以我該先讓 Jenkins 機器能夠從 pull blog source

  1. 建立 ssh key

    1
    $ ssh-keygen

    /images/20190301/16.png

  2. Add this ssh public key to github

    1
    $ cat ~/.ssh/id_rsa.pub

    將這整段 Public Key 加到 Github

    /images/20190301/17.png

    /images/20190301/18.png

  3. Install Hexo

    1
    $ sudo npm install hexo-cli -g
  4. Test git clone and hexo build

    1
    2
    3
    4
    5
    $ git clone git@github.com:yourRepository/yourRepository.github.io.source.git
    $ cd yourRepository
    $ npm install
    $ hexo g
    $ hexo server

    如果到這邊都正常,基本上 Jenkins 機器已經有能力幫你 Build Hexo了


Create Jenkins Job

  1. 新增作業

    /images/20190301/19.png

  2. 原始碼管理

    /images/20190301/20.png

  3. 設置 Credentials(點選 Add > Jenkins)

    將 Private Key 貼進去

    1
    $ cat ~/.ssh/id_rsa

    /images/20190301/21.png

    連線就正常了

    /images/20190301/22.png

  4. 建置觸發程序

    /images/20190301/23.png

  5. 建置

    /images/20190301/24.png


Github Webhook

這邊是要設定,當 Github 發現 Repository 被更新時,主動打個 Request 觸發剛剛建立好的 Jenkins

  1. 先進到 Your Repository > Settings > Webhooks

    /images/20190301/25.png

  2. Add Webhooks

    1
    http://yourEc2PublicIP:8080/github-webhook/

    請特別注意!! github-webhook/ 後面這個斜線一定要,不然會 302 錯誤

    /images/20190301/26.png

錯誤排除

看似一切美好又順利的把 Webhooks 接了起來,但觸發 Jenkins 後發現以下錯誤

1
2
3
INFO  Deploying: git
Permission denied (publickey).
fatal: Could not read from remote repository.

為什麼 ? 剛剛不是遠端登進去都試過跟 Git 之前的操作沒問題嗎?

原來是因為剛剛的 SSH Key 是建立在 centos 這個登入帳號底下,Jenkins 是透過自己的帳號權限在執行,所以我們必須將剛剛的SSH Key 搬過去給你,並賦予它權限

1
2
$ cp ~/.ssh/id_rsa.pub /var/lib/jenkins/.ssh/
$ cp ~/.ssh/id_rsa /var/lib/jenkins/.ssh/

移過去後還需要將檔案擁有者改成 Jenkins,或是你可以針對 Jenkins 這跟帳號設定檔案權限,我這邊選擇直接把擁有者改成 Jenkins

1
2
$ sudo chown jenkins /var/lib/jenkins/.ssh/id_rsa.pub
$ sudo chown jenkins /var/lib/jenkins/.ssh/id_rsa

確認權限都正確就大功告成啦!!

/images/20190301/27.png


現在你看到的這篇,就是透過 Jenkins 自動建置部署上來的


/images/20190221/02/1.jpg

學習 Docker 最先碰到的困擾應該都是究竟該如何偵錯 ,畢竟 Docker Run Container 如果沒有下一些指令,通常都是執行完就砍掉,什麼都沒留下,不像在本機可以透過開發的 IDE、Log … 等手段來 Debug,所以這邊就寫下一些我較常使用的偵錯方式


Run

如果 Build Docker Image 時就有指定 CMD 或是 ENTRYPOINT,那 Container Run 起來後跑完就關掉了,這時候可以透過 Run 的最後一個參數來覆蓋過原本的 CMD 指令。

1
$ docker run -it mcr.microsoft.com/windows/nanoserver:1809 cmd.exe

這邊的 cmd.exe 就是要覆蓋 Build Image 所指定的指令,讓你可以進入 Container 的 terminal 中執行你想做的指令


Volume

因為 Container 每次執行都是全新的,所以導致 Log 不易保留,但我們可以 Volume 的方式將 Host Folder 與 Container 內的 Log Folder Mount ,這樣就可以將 Container 內的 Log File 寫出來,達到持久化的效果。

/images/20190221/02/2.png

(圖片來源 : Docker Docs )

接著用簡單的範例來展示一下

1
$ docker run -it mcr.microsoft.com/windows/nanoserver:1809 cmd.exe

進到 Container 後,隨便寫個檔案

1
2
3
4
$ mkdir log
$ cd log
$ echo "hi" >> test.txt
$ dir

/images/20190221/02/3.png

可以看到在 Container 內的確長出了 C:\log 資料夾,並且裡面有個 test.txt 的檔案,檔案裡面寫著 “hi”。但這時候你在 host 機器應該找不到對應的檔案,因為 Container 是彼此獨立且隔離的。

這時候離開 Container 再重啟一次,會發現剛剛 log 的 Folder 已經消失,因為這個 Container 是全新的

1
2
3
$ exit
$ docker run -it mcr.microsoft.com/windows/nanoserver:1809 cmd.exe
$ dir

/images/20190221/02/4.png


Volume 使用方式

1
docker run -v HostFolderPath:ContainerFolderPath 

加上 Volume 參數再重新執行一次剛剛的步驟

1
$ docker run -it -v c:\files:c:\log mcr.microsoft.com/windows/nanoserver:1809 cmd.exe

將 Host 機器的 c:\files Folder Mount 到 Container 內的 c:\log 資料夾,所以在 Container 內新增檔案寫 Log 也會同時寫出來

/images/20190221/02/5.png

這邊應該會注意到,因為 Volume 了 Container 內原本不存在的 log 資料夾,所以一進去的時候它就長出來了,並不需要特別另外建立。


EXEC

有時候 Container 內執行的是常駐程式,它可能會持續執行直到任務完成,如果又沒有將 Log 寫出來的必要,其實很難知道它目前的狀況為何,這時候 EXEC 就派上用場了,它可以讓你進入一個正在執行中的 Container 中。

一樣用個簡單的範例來演練一下

先 Run 一個 Container 起來,並且一樣寫一個 log 下來

1
2
3
4
5
$ docker run -it mcr.microsoft.com/windows/nanoserver:1809 cmd.exe
$ mkdir log
$ cd log
$ echo "hi" >> test.txt
$ dir

/images/20190221/02/6.png

這時候開另一個 terminal,先查詢剛剛的 Container ID

1
$ docker ps

/images/20190221/02/7.png

透過 EXEC 指定要執行哪個 Container

1
$ docker exec -it d24e6db4de85 cmd.exe

/images/20190221/02/8.png

可以看到一進去就已經有剛剛我們寫下的 test.txt,表示這是同一個 Container,或是可以透過 hostname 來做驗證,可以得到相同的 ID

/images/20190221/02/9.png

/images/20190221/0.png

一開始以為 Docker For Windows 跟 Windows Containers on Windows Server 是一樣的東西,結果在建置 CI 機器時碰到一些問題才發現原來是不同東西啊


Docker for Windows

Docker for Windows 底層是透過 Hyper-V 來乘載 Docker Engine,也因為需要多一層 Hyper-V ,所以執行效率比之後出的 Docker on Windows Server 直接原生 Docker Engine 效率來說差了一截,但也因為有 Hyper-V 的協助下,可切換成能執行 Linux Container 的模式。

/images/20190221/1.png

Windows 10 Professional 版本就可以安裝 ( 因為Professional 版本才有 Hyper-V )

/images/20190221/2.png

圖片來源 : Microsoft


Windows Containers on Windows Server

Windows Server 2016 或更新的版本才可使用,直接在 OS 上執行 Docker Engine,所以效能比需要 Hyper-V 的 Docker for Windows 好上一大截,如果要跑 Linux 類的 Container 也可以透過 --isolation=hyperv 的方式,不過有個限制是這個方法不能在 VM 中用,也就是說如果你今天是用雲端的 Windows Server,本身已經是在 VM 裡面了,就無法在裡面執行 isolation

/images/20190221/3.png

圖片來源 : MSDN

目前因為公司內部很多既有專案都是用 .Net Framework 開發的,所以轉移到 Container 也是選擇用 Windows Containers on Windows Server,畢竟如果是 Production Service,效能還是非常重要的一個考量


/images/20190220/0.png

情境

Windows Container 執行起來的時候,DNS Server 預設會吃 Host 的設定,但第一組會擺 Default Gateway 的 IP

/images/20190220/1.png

但會碰到當在解析 IP 時,只會走第一組 Name Server 去問,如果問不到並不會問第二組,導致解析失敗

/images/20190220/2.png

(需明確指定 Name Server 才能查的到)

聽說這也是 Windows Container 才會碰到的問題,用 Linux 的同事們表示黑人問號 ???


解法

DotNet 人有 DotNet 人的玩法,果然找到有人有相同的問題

解法就是當 Container 執行起來的時候,用 Powershell 去改 DNS Server 設定 ….

恩 …. 就改吧

1
$ powershell  Set-DnsClientServerAddress -InterfaceAlias vEthernet* -ServerAddresses 10.2.x.x,10.2.x.x

執行完後重新查詢網路設定會看到 DNS Server 第一組已經不是 Default Gateway

1
$ ipconfig /all

/images/20190220/4.png

這時候在重新查一次 IP ,不用特別指定 Name Server 就正確了

/images/20190220/3.png

同場加映

這邊特別介紹 dns_search 這個 docker 參數

/images/20190220/5.png

因為公司測試機都有加入 AD 群組,所以伺服器別名即便不寫全名還是可以查的到,但在 Container 內就不是這麼回事了,必須寫完整的全名不可,原本以為要改 Config ,請大家都寫完整,還好同事提醒有這個參數可以使用。

只要在 Docker Run 時或是 Docker Compose 設定加上 Domain Name,這樣 Container 找不到時就會加上 Domain Name 在完整搜尋一次

1
dns_search: your.domain

這參數又拯救了 IT 狗的一天…

/images/20190219/0.png

情境

通常在使用 EC2 的建議上都是希望將機器掛上 IAM Role 的權限,在機器上面執行相關 AWS 資源時就可以透過該角色的權限來執行,避免將 Access Toke 、Secret Key 寫到程式中去跑。

但如果將程式包到 Container 裡面去執行時卻發現,Linux Container 可以吃到 Host 的 IAM Role 權限,而 Windows Container 卻不行,為何 ?


原因

原來 AWS 之所以能夠讓機器能知道執行權限為何是否過一個叫 Metadata Service 的服務來達成

1
$ curl 169.254.169.254

當在 EC2 上執行該指令應該能看到基本的回應

/images/20190219/1.png

連線到 http://169.254.169.254/latest/meta-data/ 也可以看到一些對應的 Metadata 設定,而這個 Metadata Service IP 很巧的在 Azure 上也一模一樣 XD


所以要能夠透過 IAM Role 的權限來執行 AWS 相關資源,169.254.169.254 就勢必要能通,接著將 Windows Container Run起來並執行相同指令會發現得到 Timeout 的回應

/images/20190219/2.png


Windows Container 預設是走 NAT 的網路模式,如果你在 Host 與 Windows Container 內列出 Routing Table IPv4 的資料會發現,169.254.169.254 Gateway 是走同一條

1
$ route print -4

/images/20190219/3.png

這也是導致 Container 內得不到回應的原因

/images/20190219/11.png

1
$ ipconfig /all

/images/20190219/4.png

應該透過 NAT Router default gateway 才出的去

/images/20190219/12.png


解法

先看看預設 Container 用的網路是如何設定的

1
$ docker network ls

/images/20190219/5.png

1
$ docker network inspect nat

/images/20190219/6.png

可以看到紅框處並沒有指定 Default Gateway,所以每次執行 Container 時都是動態分配 Subnet 與 Default Gateway,這樣讓我們設定上會有困難


自建一組網路給 Container 使用

可以透過 Docker network create 來預先建立一組規範好的網路,並讓 Container 起起來的時候指定吃這組設定

1
$ docker network create --driver nat --gateway 172.17.0.1 --subnet 172.17.0.0/20 mynetwork

/images/20190219/7.png

1
$ docker run -it --rm --network mynetwork mycontainer:latest

檢視 Container 起來後網路設定狀況,可以看到 Default Gateway 已經被定下來了

/images/20190219/8.png

接著執行

1
$ route -p add 169.254.169.254 172.17.0.1

/images/20190219/9.png

/images/20190219/10.png

至於 route add 怎麼在 Container 起起來時自動執行那就是另一個課題了,這邊不討論

Docker compose 動態建網路給 Container 使用

第二種方法是透過 Docker compose 執行時動態建立一組網路來使用,方法大同小異,只是透過 Docker Compose 來設定而已

1
2
3
4
5
6
7
8
9
10
11
12
13
14
version: '2.1'
services:
mycontainer:
image: mycontainer:latest
networks:
- mynetwork
networks:
mynetwork:
driver: nat
ipam:
config:
- subnet: 172.17.0.0/20
gateway: 172.17.0.1

結語

最近跟著架構師還有一群很厲害的同事做新的專案,發現自己對於 Infra 與網路的知識真的相當不足,所以在Docker 一些設定與建置上常常卡的亂七八糟,也趁著這個機會把以前一堆已經還老師的知識又惡補了一翻,希望能越來越順利