---
title: Kubernetes 的實務應用 (Part 1?) | Nginx+PHP
tags: 學習日誌, Tutorial, 開發文件, K8s, PKS, Demo, PHP, AlfieYFC
description:
image: https://i.imgur.com/glNdcA3.png
robot: noindex, nofollow
lang: zh
slideOptions:
transition: fade
theme: dark
---
{%hackmd BkVfcTxlQ %}
###### tags: `學習日誌` `Tutorial` `開發文件` `K8s` `PKS` `Demo` `PHP` `Nginx` `AlfieYFC`
Kubernetes 的實務應用 (Part 1?) | Nginx+PHP
===
2019年初至今,在工作上其實為了客戶做了不少客製化的 Demo 情境,甚至撰寫技術文章。苦於這些文章都是針對客戶情境寫的,有些資訊可能不方便公開透露,不然真的是很想跟大家分享這些實務上有感的 Demo :laughing::laughing: 於是我開始想,辛苦做那麼多卻不能分享出來,幹嘛不把客戶相關的軟體或文字內容拿掉、改掉就好 :no_mouth: 所以就坐下來開始拼貼改寫這些文章啦~~~
本篇作為這一系列的開端 _~(視情況再決定有沒有Part2)~_,還是先從入門的簡單範例開始吧!
Demo 情境說明
===
本篇將開發一個簡單的 [PHP](https://hub.docker.com/_/php) Hello World 容器應用,在頁面被 Client 端(`curl` 或瀏覽器)存取時,會在 Client 端及 Server 端皆產生 Log 訊息。該應用會被分別改寫成三支獨立卻互相雷同的 Web,並且用 [Kubernetes Ingress](https://kubernetes.io/docs/concepts/services-networking/ingress/) 和一個 [Nginx](https://hub.docker.com/_/nginx) 前端應用,各自實現「服務重導」的功能;Ingress 的實作設計類似 [Kairen 大神這一篇](https://k2r2bai.com/2018/07/19/kubernetes/k8s-external-dns/),跟我一樣喜歡拜神的朋友,可以參訪 Kairen 的部落格喔。
寫得這麼玄一定很多人看不懂,下面給一張圖片應該清楚一些:

:::info
另外補充,本篇所有實作都是在 [Pivotal Container Service (PKS)](https://pivotal.io/platform/pivotal-container-service) 環境執行,但是我會在內文較著重於 Kubernetes 原生的功能應用。如果透過本篇你產生出對 PKS 的管理功能有所好奇、疑問之處,歡迎[隨時聯絡我](https://www.facebook.com/alfieyfc)或者[迎棧科技](https://www.inwinstack.com/) (加減替公司工商一下 :joy: )
:::
應用開發
---
首先開發三支互相雷同的 Hello World 應用。
第一支命名為 `Happiness`,設定粉紅色背景,並產生 Log `Hello, World of Happiness`。
為了區別 Client 端及 Server 端的 Log,我們在日誌最後分別加上 `@ Client` 和 `@ Server` 文字,輔助辨別及驗證。
開發目錄展開如下:
```
./
|-------- Happiness/
| |-------- Dockerfile
| |
| |-------- src/
| |-------- conf/
| | |-------- php.ini
| |
| |-------- html/
| |-------- index.php
|
|-------- Sadness/
|
|-------- Darkness/
```
PHP 原始碼如下:
```php=
## Happiness/src/html/index.php
<html>
<head>
<title>Pink Happiness</title>
</head>
<?php echo "<body style='background-color:pink'>"; ?>
<?php
$message_server = "Hello, World of Happiness @ Server side!!!";
error_log($message_server);
echo '<p>Hello World</p>';
?>
<?php
function debug_to_console( $data ) {
$output = $data;
if ( is_array( $output ) )
$output = implode( ',', $output);
echo "<script>console.log( 'Debug Objects: " . $output . "' );</script>";
}
$message_client = "Hello, World of Happiness @ Client side!!!";
debug_to_console($message_client);
?>
</body>
</html>
```
而另外兩支 Hello World 僅將 Title、log 訊息以及背景顏色更改
- Blue Sadness
>> 以下僅列出有修改的原始碼行位 [color=red]
>
> `<title>Blue Sadness</title>`
> `<?php echo "<body style='background-color:blue'>"; ?>`
> `$message_server = "Hello, World of Sadness @ Server side!!!"`
> `$message_client = "Hello, World of Sadness @ Client side!!!"`
- Black Darkness
>> 以下僅列出有修改的原始碼行位 [color=red]
>
> `<title>Black Darkness</title>`
> `<?php echo "<body style='background-color:black'>"; ?>`
> `$message_server = "Hello, World of Darkness @ Server side!!!"`
> `echo '<p style="color=white">Hello World</p>';`
> `$message_client = "Hello, World of Darkness @ Client side!!!"`
打包 Docker 映像檔
---
由於本篇僅為 Demo 用途,因此 Dockerfile 盡可能簡化:
```dockerfile=
# Dockerfile
FROM php:7.2-apache
COPY ./html/index.php /var/www/html/index.php
COPY ./conf/php.ini /usr/local/etc/php/php.ini
```
須注意的是,這個 `php.ini` 檔有特別更改過 `error_log` 欄位,以確保 `index.php` 中的 `error_log($message_server);` 日誌會被產生並顯示在 stderr:
> ```
> ; Log errors to specified file. PHP's default behavior is to leave this value empty.
> ; http://php.net/error-log
> ; Example:
> ;error_log = php_errors.log
> error_log = /dev/stderr
> ```
> [color=orange]
將 `Dockerfile`、`php.ini` 和 `index.php` 放到相應的目錄位置:

利用 Docker 指令打包這些 PHP 應用:
```bash
docker build -t harbor.pks.inwinstack.com/demo-public/php-hello:7.2-apache-pink ./Happiness/
docker build -t harbor.pks.inwinstack.com/demo-public/php-hello:7.2-apache-blue ./Sadness/
docker build -t harbor.pks.inwinstack.com/demo-public/php-hello:7.2-apache-black ./Darkness/
```
:::info
上述指令中 `-t` 參數所輸入映像檔的完整名稱,請參考以下規則:
> `<registry-fqdn>`/`<project-name>`/`<repo-name>`:`<tag>`
:::


上傳映像檔
---
利用 Docker 指令上傳映像檔:
```bash
docker login harbor.pks.inwinstack.com
docker push harbor.pks.inwinstack.com/demo-public/php-hello:7.2-apache-pink
docker push harbor.pks.inwinstack.com/demo-public/php-hello:7.2-apache-blue
docker push harbor.pks.inwinstack.com/demo-public/php-hello:7.2-apache-black
```

部署應用
===
應用設定檔(YAML)
---
首先是 `happiness.yaml`~~_~(因為我喜歡先甘後苦再被黑)~_~~,Deployment YAML 編輯如下:
```yaml=
# Happiness/happiness.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: happiness-deploy
spec:
replicas: 1
selector:
matchLabels:
app: happiness
template:
metadata:
labels:
app: happiness
spec:
containers:
- name: php
image: harbor.pks.inwinstack.com/demo-public/php-hello:7.2-apache-pink
ports:
- containerPort: 80
```
把 `sadness.yaml` 和 `darkness.yaml` 的 YAML 也一併撰寫,其內容跟上方幾乎一樣,只是改了應用名稱和映像檔 tag 而已。
```yaml=
# Sadness/sadness.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: sadness-deploy
spec:
replicas: 1
selector:
matchLabels:
app: sadness
template:
metadata:
labels:
app: sadness
spec:
containers:
- name: php
image: harbor.pks.inwinstack.com/demo-public/php-hello:7.2-apache-blue
ports:
- containerPort: 80
```
```yaml=
# Darkness/darkness.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: darkness-deploy
spec:
replicas: 1
selector:
matchLabels:
app: darkness
template:
metadata:
labels:
app: darkness
spec:
containers:
- name: php
image: harbor.pks.inwinstack.com/demo-public/php-hello:7.2-apache-black
ports:
- containerPort: 80
```
這時你的目錄展開應該長這樣:

部署應用
---
利用 `kubectl apply` 指令將容器應用部署至你的 Kubernetes 叢集:
```bash
kubectl apply -f Happiness/happiness.yaml
kubectl apply -f Sadness/sadness.yaml
kubectl apply -f Darkness/darkness.yaml
```

建立服務
---
任何 Kubernetes 應用皆需要建立 `Service` 來提供對外服務,利用 `kubectl expose` 指令將服務產生:
```bash
kubectl expose deploy happiness-deploy --name happiness-svc --type LoadBalancer
kubectl expose deploy sadness-deploy --name sadness-svc --type LoadBalancer
kubectl expose deploy darkness-deploy --name darkness-svc --type LoadBalancer
```

查看應用
---
利用 `kubectl get` 指令,確認應用服務成功在 K8s 建立:
```bash
kubectl get po,svc -o wide
```

:::warning
上圖 LoadBalancer type Service 中的 `EXTERNAL-IP` 欄位,獲取自 NSX-T 的 loadbalancer-ip-pool。其中 `100.64.x.x` 為 NSX-T 內部存取溝通轉址使用,而上圖看到的 `192.168.21.x` 則是可以「對外連線」的 IP 位址;這個 IP 位址因應實際環境設定而將不同。
> **這是 PKS + NSX-T on vSphere 特有的機制**
> [color=red]
:::
馬上用 Client 端的 Chrome 存取這三個 Service 試試!
> 記得打開「開發人員工具」,才看得到 Client 端的 console log:
> `Hello, World of ____ @ Client side!!!`



回到指令介面,用 `curl` 存取看看:

查看日誌
===
利用 `kubectl log` 指令查看各個 Pod 的 log,可以看到 apache2 server 啟動過程的日誌以及剛剛透過 Chrome 和 `curl` 存取的紀錄,也確實在每次存取時產生了 `Hello, World of ____ @ Server side!!!` 的 error_log(下圖==反白內容==)。

使用 `kubectl log` 指令對 `Sadness` 和 `Darkness` 應用查看 Pod 的 log,也應該要得到相同的結果。
Ingress 服務代理(方法一)
===
Ingress 是 Kubernetes 自帶的服務代理工具,將後端的容器服務 (Kubernetes Service) 以 HTTP 或 HTTPS 協定提供外部存取;想進一步瞭解可以參考[官方文件](https://kubernetes.io/docs/concepts/services-networking/ingress/)。
前面我們利用指令產生了 LoadBalancer 類型的 Service,而往往 LoadBalancer 中的 `EXTERNAL-IP` 數量有限,要想讓其對網際網路提供服務存取,通常也會產生相應的成本考量。因此,在實務上經常見到用 ClusterIP 類型的 Service 作為 Ingress 後端,並且僅以 Ingress 對外提供存取,由 Ingress 重新導向正確的「後端服務」。

為了模擬實務情境,重新建立 ClusterIP 類型的 Service:
```bash
kubectl delete svc --all
kubectl expose deploy happiness-deploy --name happiness-svc --type ClusterIP
kubectl expose deploy sadness-deploy --name sadness-svc --type ClusterIP
kubectl expose deploy darkness-deploy --name darkness-svc --type ClusterIP
```

這樣一來,瀏覽器理應無法存取這些服務了 (ClusterIP 是不對外提供連線的)。

建立 Ingress
---
Ingress YAML 內容如下:
```yaml=
# hello-ingress.yaml
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: hello-ingress
spec:
rules:
- host: happiness.pks.inwinstack.com
http:
paths:
- path: /
backend:
serviceName: happiness-svc
servicePort: 80
- host: sadness.pks.inwinstack.com
http:
paths:
- path: /
backend:
serviceName: sadness-svc
servicePort: 80
- host: darkness.pks.inwinstack.com
http:
paths:
- path: /
backend:
serviceName: darkness-svc
servicePort: 80
```
利用 `kubectl apply` 指令建立 Ingress,並確認其 `EXTERNAL-IP` 位址:
```bash
kubectl apply -f hello-ingress.yaml
kubectl get ing
kubectl describe ing hello-ingress
```

DNS 解析
---
前面在 Ingress 定義了三個應用的網域名稱 Host,我們利用 `curl` 來驗證看看,使用同一個 IP 位址,是否能透過 Host 欄位存取到相應正確的服務?
```bash
curl -H "Host: happiness.pks.inwinstack.com" 192.168.21.2
curl -H "Host: sadness.pks.inwinstack.com" 192.168.21.2
curl -H "Host: darkness.pks.inwinstack.com" 192.168.21.2
```

透過驗證我們發現,只要 HTTP GET Header 中的資訊帶有 Host 欄位,並與 Ingress 的 Host 名稱相符,便會被重新導向相呼應的 backend Kubernetes Service。欲使用瀏覽器來實際驗證,則需要在 Client 端的 DNS 解析這些 Host 名稱到 Ingress IP 位址。為了達到此目的,或許很多人會直接編輯 `/etc/hosts` 檔案內容,在 Client 系統直接解析 Hostname。

而今天我想介紹一個很方便的 Chrome extension「[Host Switch Plus](https://chrome.google.com/webstore/detail/host-switch-plus/bopepoejgapmihklfepohbilpkcdoaeo)」。這個工具真的超級方便,直接在 Chrome 上面隨時新增、設定你想暫時解析的網域名稱,隨時用不到了就可以關閉起來。
如下圖所示,利用 Host Switch Plus 解析了這些網域名稱後,在網址列輸入前往,即可存取相應的容器應用了!

~~*~偷偷置入我的網站~*~~




Nginx Pod 服務代理(方法二)
===
雖然 Kubernetes Ingress 其實也是在底層用 Nginx 來實作,但是在特定情況下,Dev/Ops 團隊可能會想要用獨立的 Nginx 容器應用來實現服務代理(例如:自定義 access_log 或 error_log 的蒐集),因此本篇另外也實作一個 Nginx Pod 來達到第二種服務代理方式。

建立 Nginx
---
```bash
mkdir nginx
```
建立一個 nginx 目錄,並新增一個檔案 `nginx.conf`:
``` conf=
# nginx/nginx.conf
user nginx;
worker_processes 1;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
log_format main ' - [] "" '
' "" '
'"" ""';
access_log /var/log/nginx/access.log main;
server {
listen 80;
location /happiness/ {
proxy_pass http://happiness-svc.hello-demo/;
}
location /sadness/ {
proxy_pass http://sadness-svc.hello-demo/;
}
location /darkness/ {
proxy_pass http://darkness-svc.hello-demo/;
}
location / {
root /usr/share/nginx/html/;
}
}
sendfile on;
#tcp_nopush on;
keepalive_timeout 65;
#gzip on;
include /etc/nginx/conf.d/*.conf;
}
```
:::info
`nginx.conf` 檔案中,我們將 proxy_pass 參數定義為與其 path 相應的 Service 名稱。這利用了 kubedns 的功能來實現,因此如果你的 Kubernetes 叢集沒有實作 kubedns 的話,可以將 proxy_pass 參數改為 ClusterIP 位址:
```conf
location /path/ {
proxy_pass http://<ClusterIP>;
}
```
:::
打包 Docker 映像檔
---
由於本篇僅為 Demo 用途,因此 Dockerfile 盡可能簡化:
```dockerfile=
# nginx/Dockerfile
FROM nginx:1.15.9
COPY ./nginx.conf /etc/nginx/nginx.conf
```
利用 Docker 指令打包 Nginx 應用:
``` bash
docker build -t harbor.pks.inwinstack.com/demo-public/nginx:1.15.9-hello ./nginx/
docker push harbor.pks.inwinstack.com/demo-public/nginx:1.15.9-hello
```

應用設定檔(YAML)
---
```yaml=
# nginx/nginx.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deploy
spec:
replicas: 1
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: harbor.pks.inwinstack.com/demo-public/nginx:1.15.9-hello
ports:
- containerPort: 80
```
利用 `kubectl apply` 指令將容器應用部署至你的 Kubernetes 叢集,並用 `kubectl expose` 來建立對外提供的服務:
```bash
kubectl apply -f nginx/nginx.yaml
kubectl expose deploy nginx-deploy --type=LoadBalancer
kubectl get svc -o wide
```

存取驗證
---
先在指令介面用 `curl` 存取看看,會發現我們設置的 `proxy_pass` 確實被重導到其他服務去了(但是 `curl` 看不到重導後的結果):
```bash
curl 192.168.21.11
curl 192.168.21.11/happiness
```

試著在瀏覽器上存取,Nginx 首頁存取成功!

`proxy_pass` 存取後端服務也沒有問題!



---
結語
===
在此篇中我們開發了一個簡單的 Hello World 網頁應用,部署到 K8s,然後用 LoadBalancer 類型 Service 提供外部存取,並且存取該網頁應用時能夠查看到 Client 端及 Server 端的 Log。
我們進一步地將 Service 改為 ClusterIP 類型,並且利用 Ingress 和 Nginx Pod 兩種方法實現服務代理。而在 Nginx Pod 段落中,我們 expose 的是 LoadBalancer 類型 Service,大家不仿也可以試試將 Nginx 以 ClusterIP 類型產生 Service,再利用 Ingress 代理 Nginx 讓外部存取;如下圖:

事實上服務對外提供存取的方式有很多,而本篇介紹的也僅是入門實作中較常見且易懂的應用架構、方法,希望對大家有所幫助囉!
---