## 開發環境 Docker 化
## &
## GitLab CI
###### 投影片: [hackmd.io/@taichunmin/r1E6Kl6_z](https://hackmd.io/@taichunmin/r1E6Kl6_z)
###### [#Agile.Taichung](https://agile-taichung.kktix.cc/)
Note:
$('code').each(function () {
let a = $(this).html();
$(this).html(a.replace(/&/g, '&'));
})
---
### Who am I?
* 戴均民
* 現職: 微程式 DevOps 組
* [COSCUP 2016 工作坊 講師](http://coscup.org/2016/schedules.html#H23)
* https://github.com/taichunmin
---
## 真實案例
### 大學社團教 git
---
#### 問題
* 每個人都要有 git 環境
* 電腦有還原卡只能夠現場裝
* 網路很慢一起抓檔案絕對超慢
* 課堂時間有限沒時間裝環境
---
#### 於是就想用 Docker 來處理
* 用 docker 建立每個人獨立的環境
* 學弟用 ssh 連線軟體連線到 docker
---
#### 該怎麽開很多 Docker 互相連接?
* 需要在主機上開很多個 Docker
* Docker 之間要能夠有網路互相連接
* 要能夠方便知道 SSH 的 Port
---
### `docker run --link` ?

---
## docker-compose
#### 取代 `docker run` 的工具
#### 能夠 **快速** 開很多服務
#### 支援 `.env` 環境變數
---
## DEMO

----
## 開啟一臺虛擬機器
* [DigitalOcean](https://cloud.digitalocean.com/login)
* 系統: Ubuntu 16.04.3 x64
* 規格: USD$ 5/month
* 地區: Singapore
* Add SSH keys
* 機器名稱
----
## 設定 DNS
* [CloudFlare](https://www.cloudflare.com/a/login)
* 複製 IP 填到 DNS 中
* 不要過 CloudFlare CDN
----
## 設定 ssh config
```
Host do
HostName do.taichunmin.tk
User root
IdentityFile ~/.ssh/taichunmin@gmail.com.key
```
----
## 安裝 git-it-docker
```shell
git clone https://github.com/taichunmin/git-it-course-docker.git
cd git-it-course-docker
bash ./install.sh
```
---
###### `docker-compose up -d`
###### 一次開啟多個 Docker 服務
---
## DEMO

###### `docker-compose up -d --scale client = 3`
---
## 常用指令
<ul>
<li class="fragment" data-fragment-index="1"><code>docker-compose ps</code><br>查看 ports 和正在執行的服務</li>
<li class="fragment" data-fragment-index="2"><code>docker-compose pull</code><br>更新 image</li>
<li class="fragment" data-fragment-index="3"><code>docker-compose exec [服務] [指令]</code><br>執行指令</li>
<li class="fragment" data-fragment-index="4"><code>docker-compose logs [服務]</code><br>查看 example 的 stdout</li>
<li class="fragment" data-fragment-index="5"><code>docker-compose down</code><br>關閉機器</li>
</ul>
---
### DEMO: 假設有人忘記 client 密碼
1. 使用 `docker-compose ps` 看第幾個 client
2. 使用 `docker-compose exec --index=2 client bash`
3. 執行 `passwd` 強制改密碼
4. `exit` 退出
---
### 該怎麼寫 docker-compose
<ol>
<li class="fragment" data-fragment-index="1">規劃架構</li>
<li class="fragment" data-fragment-index="2">定義 <code>Dockerfile</code> 建立<br>或是使用現有的 image</li>
<li class="fragment" data-fragment-index="3">以 <code>docker-compose.yml</code><br>定義有什麼 Docker 服務</li>
<li class="fragment" data-fragment-index="4">使用 <code>docker-compose up -d</code><br>來啟動機器</li>
</ol>
---
## 架構
```mermaid
graph LR
Dashboard --- |"讀寫資料庫<br />(backend)"| Redis
Dashboard -.-> |"查看儀錶板<br />(Port 80)"| 瀏覽器(瀏覽器)
Client-1 --> |"回報解題狀況<br />(frontend)"| Dashboard
Client-2 --> |"回報解題狀況<br />(frontend)"| Dashboard
Client-N --> |"回報解題狀況<br />(frontend)"| Dashboard
SSH-1(SSH-1) -.-> |"Port 32768"| Client-1
SSH-2(SSH-2) -.-> |"Port 32769"| Client-2
SSH-N(SSH-N) -.-> |"Port N"| Client-N
```
Note:
學弟妹透過 ssh 軟體連線至 client
client 每一段時間透過 frontend 網路溝通 dashboard
瀏覽器連線至 dashboard 查看儀錶板
dashboard 透過 backend 連線至 redis 讀寫資料
---
## dashboard 的設定
```yaml
version: '2'
services:
dashboard: # 儀錶板
image: taichunmin/git-it-course-docker:dashboard
networks:
- frontend # 給瀏覽器和 client 連線用
- backend # 連線資料庫用
volumes: # 掛載外部的檔案到 docker 容器中
# 為了取得所有 client 對外的 SSH port
- /var/run/docker.sock:/var/run/docker.sock
# 儀錶板的程式碼
- ./webapp:/var/www/html
# 登入 Shell 後的歡迎訊息
- ./dashboard/motd:/etc/motd:ro
ports: # port 轉發設定
- "80:80" # 把主機的 80 對應到 docker 的 80
restart: always # 異常結束自動重啟
```
---
## client 的設定
```yaml
version: '2'
services:
client: # git-it 環境
image: taichunmin/git-it-course-docker:client
networks:
- frontend # 給 client 連線到 dashboard 用
depends_on: # 相依的服務,compose 會一起啟用這個服務
- dashboard
volumes:
# 登入 Shell 後的歡迎訊息
- ./client/motd:/etc/motd:ro
ports: # port 轉發設定
- "22" # 把主機的隨機 port 對應到 docker 的 22
```
---
## redis 的設定
```yaml
version: '2'
services:
redis:
# 使用 Docker Hub 上面提供的 redis
image: redis:latest
networks:
- backend # 給 dashboard 連線用
restart: always # 異常結束自動重啟
```
---
### docker-compose 的服務
### 該怎麼連線到其他服務?
---
#### 只要使用 `docker-compose.yml`
#### 裡面的服務名稱就可以連線了
---
### dashboard 連線到 redis

---
### client 連線到 dashboard

---
## DEMO
#### 多個 client 會如何?
```shell
# docker-compose up -d --scale client=5
# docker-compose exec dashboard bash
apt-get update
apt-get install -y dnsutils
dig +short client
```
---
# Dockerfile
<ol>
<li class="fragment" data-fragment-index="1">Dockerfile 裡面就是創建<br>image 所需要的全部步驟</li>
<li class="fragment" data-fragment-index="2">我們可以使用 <code>docker build</code><br> 來創建映像檔</li>
</ol>
---
## 怎麽寫 Dockerfile
<ol>
<li class="fragment" data-fragment-index="1">選擇從哪個 image 繼承</li>
<li class="fragment" data-fragment-index="2">安裝所需軟體 (需改成不會詢問的指令)</li>
<li class="fragment" data-fragment-index="3">修改設定檔</li>
<li class="fragment" data-fragment-index="4">設定容器的啟動指令</li>
</ol>
---
## 寫 Dockerfile 的小訣竅
* 先開啟你所繼承的 image<br>`docker run -it ubuntu:latest bash`
* 手動執行指令來安裝環境<br>再把指令寫入 `Dockerfile` 裡面
---
## client 的 Dockerfile
* [client 的 Dockerfile](https://github.com/taichunmin/git-it-course-docker/blob/master/client/Dockerfile)
* [dashboard 的 Dockerfile](https://github.com/taichunmin/git-it-course-docker/blob/master/dashboard/Dockerfile)
---
## Dockerfile
## [Best Practices](https://docs.docker.com/develop/develop-images/dockerfile_best-practices/)
---
## 使用 `.dockerignore`
* `docker build` 會把目前目錄丟給 dockerd
* 語法跟 `.gitignore` 一樣<br>避免把沒用的檔案傳給 dockerd
---
### 避免安裝沒用到的軟體
---
### 一個容器最好只有一個用途
* 例如: 資料庫、網站服務 應該分開
* 增加容器重複利用的可能性
* 使用 docker network 來互相連接
---
## 對指令的多行參數排序
* 如 apt-get 將每個套件獨立一行
```Dockerfile
RUN apt-get update && apt-get install -y --no-install-recommends \
curl \
git \
nano \
openssh-server \
vim
```
---
## 關於 FROM
* 盡量使用 Docker Hub 官方維護的 image<br><br>例: 一個 node.js 寫的程式<br>最好從 `node:latest` 繼承<br>而不是從 `ubuntu` 繼承
---
## 關於 FROM
* 想要把 image 最小化<br>從 [Alpine image](https://hub.docker.com/_/alpine/) 繼承就對了<br>(目前這個 image 小於 5 MB)<br>很多官方 image 都有 alpine 版本
---
## 關於 LABEL
* 在 Docker 1.10 以前務必把所有 LABEL 寫在同一行
* 在 Docker 1.10 以後分開寫比較好看
---
## 關於 RUN
* 盡量將 RUN 的指令寫在一起<br>避免產生多餘的 layer
* 用反斜線 `\` 換行,增加可讀性
---
## 關於 apt-get
* 務必將 `apt-get update`<br>和 `apt-get install -y` 寫在同一行<br>避免 `apt-get update` 誤用 cache
```Dockerfile
RUN apt-get update && apt-get install -y \
package-bar \
package-baz \
package-foo
```
---
## 關於 apt-get
* 確保 `apt-get install -y` 或類似的指令<br>要加上 `-y` 這種自動安裝不詢問的選項
* 用完 `apt-get` 後記得刪除多餘的檔案<br>`rm -rf /var/lib/apt/lists/*`
* 由於官方的 Debian 和 Ubuntu 已經會<br>[幫你在 docker 執行 `apt-get clean`](https://github.com/moby/moby/blob/03e2923e42446dbb830c654d0eec323a0b4ef02a/contrib/mkimage/debootstrap#L82-L105)<br>所以可以省略
---
## 關於 apt-get
```Dockerfile
RUN apt-get update && apt-get install -y \
aufs-tools \
automake \
build-essential \
curl \
dpkg-sig \
libcap-dev \
libsqlite3-dev \
mercurial \
reprepro \
ruby1.9.1 \
ruby1.9.1-dev \
s3cmd=1.1.* \
&& rm -rf /var/lib/apt/lists/*
```
---
## 關於 CMD
* 盡量用陣列語法<br>`CMD ["apache2","-DFOREGROUND"]`
---
## 關於 EXPOSE
* 應該用每個服務預設的 port
- 例如 Apache 是 80,Mongo 是 27017
* 避免其他 image 在連接上出問題<br>因為 link 會幫你加類似這樣的環境變數<br>`MYSQL_PORT_3306_TCP`
---
## 關於 ENV
* 把可執行檔透過 ENV 設定至 `$PATH` 中
* 把軟體版本指定至 ENV 中
```Dockerfile
ENV PG_MAJOR 9.3
ENV PG_VERSION 9.3.4
```
---
## 關於 ADD 或 COPY
* 由於 `ADD` 會有一些額外的行為<br>所以能用 `COPY` 就不要用 `ADD`
* `ADD` 會幫你解壓縮 linux 的壓縮檔<br>(只支援 `gzip`, `bzip2` 或 `xz`)<br>(對! 就是沒有 `zip`)
---
## 關於 VOLUMN
* 下列檔案應該使用 VOLUMN 寫出來
* 資料庫的資料存放區
* 服務的設定檔
* 服務執行過程會建立的檔案<br>(log, cache)
* 由於虛擬的檔案系統效能沒有 VOLUMN 好<br>應該盡量使用 VOLUMN
---
## 關於 WORKDIR
* 應使用 WORKDIR 避免一直用 `cd` 切換資料夾
```Dockerfile
RUN cd … && do-something
```
---
## GitLab CI

---
### 由於很多公司都喜歡用 GitLab
---
#### 所以跟 GitLab 整合度很高的
#### GitLab CI 是一個很棒的選擇
---
## 你只要在你的專案中
---
## 新增 `.gitlab-ci.yml`
---
## 然後再新增 Runner
---
## 就能夠跑 CI/CD 了
---
### 讓 GitLab CI
### 幫我 build Dockerfile
### 並且放到 GitLab Registry
### [docker-compose-git](https://gitlab.com/taichunmin/docker-compose-git)
---
## Dockerfile
```Dockerfile
FROM docker:latest
LABEL maintainer="taichunmin@gmail.com"
RUN apk --no-cache add bash git openssh py2-pip && \
pip install docker-compose
```
---
## 加上 `.gitlab-ci.yml`
```yaml
image: docker:latest
variables:
IMAGE_TAG: "registry.gitlab.com/taichunmin/docker-compose-git:latest"
services:
- docker:dind
stages:
- build
before_script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
build:
stage: build
tags:
- docker
script:
- docker build -t $IMAGE_TAG .
- docker run --rm $IMAGE_TAG docker-compose -v
- docker run --rm $IMAGE_TAG git --version
- docker push $IMAGE_TAG
```
----
## image & services
* image 是主要的執行環境<br>GitLab CI 會幫你把程式碼下載到 WORKDIR
* services 則是需要 link 到<br>主要執行環境的 docker image<br>但是裡面不會有程式碼<br>通常拿來是掛資料庫使用
---
## stages
代表這份 GitLab CI 要跑那些階段
```yaml
stages:
- build
XXX_job: # 這行 job 的名字單純辨識用
stage: build
```
---
## before_script
設定所有 job 執行前所需執行的共通指令
```yaml
before_script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
```
* `$CI_REGISTRY_USER`,<br>`$CI_REGISTRY_PASSWORD`,<br>`$CI_REGISTRY`<br>皆是 [GitLab CI 給予的環境變數](https://docs.gitlab.com/ce/ci/variables/)
---
## variables
自訂的變數,方便在 CI/CD 流程中使用<br>在此的 `IMAGE_TAG` 是 GitLab<br>內建的 Registry 所提供的 push 網址
```yaml
variables:
IMAGE_TAG: "registry.gitlab.com/taichunmin/docker-compose-git:latest"
```
---
## tags
指定 GitLab CI Runner 必須要有什麼 tag
所以我們挑 docker image 為基準的 Runner
```yaml
XXX_job:
tags:
- docker
```

---
## script
指定 GitLab CI 要在這個 job 中執行什麼指令<br>只要有指令異常結束 (回傳值非 0)<br>則這個 job 就等同執行失敗
```yaml
build:
script:
- docker build -t $IMAGE_TAG .
- docker run --rm $IMAGE_TAG docker-compose -v
- docker run --rm $IMAGE_TAG git --version
- docker push $IMAGE_TAG
```
---
### 當你設定完 `.gitlab-ci.yml` 後
---
### 每當你 commit + push
---
### 就會開始執行 CI
---

---
## 結合 docker-compose
## 一次 build 多個 image
---
## 一個公司的新專案
---
## 我把開發環境<br>全部都 docker 化了
---
#### 但是每次要 build Docker 都要很久
---
#### 於是就讓 GitLab CI 幫我 build
---
#### 以下是這個專案的 Docker 部分節錄
[mp-minioasis-docker](https://gitlab.com/taichunmin/mp-minioasis-docker/)
---
## QA 時間
## 請多指教
{"metaMigratedAt":"2023-06-14T15:51:08.457Z","metaMigratedFrom":"YAML","title":"開發環境 Docker 化 & GitLab CI","breaks":true,"slideOptions":"{\"transition\":\"slide\",\"theme\":\"moon\"}","contributors":"[{\"id\":\"0d9a5e06-1f92-4142-b9df-fed4c8873573\",\"add\":72,\"del\":67}]"}