﻿# Angular 結合 nginx 反向代理的 Docker 容器化封裝技巧

在微服務架構下，部署 Angular 應用程式與後端 Web API 應該切割成兩個不同的微服務。但具體的實現方式，通常會透過容器化技術來達成，實作上相對容易許多。像我們公司的特定專案，就是透過 Kubernetes 叢集化技術來實現這個目的。這篇文章，我將分享如何將 Angular 這類 SPA 框架，如何透過 nginx 反向代理與 Docker 容器化技術，徹底做到微服務化。將前後端分離的結果，可以幫助我們簡化 CI/CD 流程，同時降低系統之間的耦合性。

在整個實作的過程中，我會以 Angular 6 當作範例，並透過 Angular CLI 建立專案，最後透過 Docker 進行容器化封裝。

### 安裝 Angular CLI 工具

```bash
npm i -g @angular/cli
```

### 建立 Angular 專案範本

```bash
ng new demo1
```

### 設定 ng serve 專用的 proxy 代理伺服器

這裡我打算用網路上的一個 [Dog API](https://dog.ceo/dog-api/) 當作我們的後端 Web API 服務。我將會使用 [https://dog.ceo/api/breeds/image/random](https://dog.ceo/api/breeds/image/random) 當作我們主要實作用的 API，所以我在開發環境下，就可以透過建立 `proxy.config.json` 設定檔，來做到開發環境的 API 反向代理功能。

```json
{
  "/api": {
    "target": "https://dog.ceo",
    "changeOrigin": true,
    "secure": false
  }
}
```

接著我們修改 `package.json` 檔案，將 `"start"` 腳本設定修改為 `"ng serve --proxy-config proxy.config.json"`，並執行 `npm start` 啟動 Angular 開發伺服器 (Angular Live Development Server)。

```bash
npm start
```

啟動之後，你可以嘗試連到 [http://localhost:4200/api/breeds/image/random](http://localhost:4200/api/breeds/image/random) 看是否能得到遠端 API 執行的結果：

```json
{"status":"success","message":"https:\/\/images.dog.ceo\/breeds\/papillon\/n02086910_7405.jpg"}
```

👉 參考文章: [如何在 Angular CLI 建立的 Angular 2 開發環境呼叫遠端 RESTful APIs  
](/post/2017/02/05/Setup-proxy-to-backend-in-Angular-CLI.aspx)👉 此時還無須設定 nginx 反向代理伺服器，因為 Angular CLI 建立的專案中已經內建 Proxy 功能。

### 修改 Angular 應用程式，呼叫 API 顯示照片

編輯 `src\app\app.module.ts` 並匯入 `HttpClientModule` 模組：

```bash
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { HttpClientModule } from "@angular/common/http";

import { AppComponent } from './app.component';

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    HttpClientModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }
```

編輯 `src\app\app.component.ts` 並實作 API 呼叫程式：

```typescript
import { Component, OnInit } from '@angular/core';
import { HttpClient } from '@angular/common/http';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {
  title = 'demo1';
  imgUrl = '/assets/Spinner-1s-200px.svg';

  constructor(private httpClient: HttpClient) {}

  ngOnInit(): void {
    this.load_image();
  }

  load_image() {
    this.httpClient.get('/api/breeds/image/random').subscribe((v: any) => {
      this.imgUrl = v.message;
    });
  }
}
```

編輯 `src\app\app.component.html` 並顯示圖片在畫面上：

```html
<div style="text-align:center">
  <h1>
    Welcome to {{ title }}!
  </h1>
  <img width="300" [src]="imgUrl" (click)="load_image()">
</div>
```

### 建立 nginx 站台設定檔

我的 `nginx.site.template` 檔案內容如下，其中的 `${BACKEND_URI}` 將會透過「環境變數」自動替換成後端 Web API 的 URI 位址。

另外，由於 `dog.ceo` 這個域名的 DNS 實在太慢了，這會導致 nginx 會一直出現 **502 Bad Gateway** 的錯誤。所以我才特別加上一條 `resolver 8.8.8.8 valid=30s ipv6=off;` 設定。這條設定在使用 docker-compose 或 Kubernetes 部署的時候可以放心移除。

最後，有個 `proxy_ssl_server_name` 設定也很重要。當你的後端 Web API 使用 SNI SSL 的時候，如果不設定為 `on` 就一定會掛掉。(很雷)

```nginx
server {
    listen 80;
    root  /usr/share/nginx/html;

    # The dog.ceo domain is very slow that will lead nginx resolver not working.
    resolver 8.8.8.8 valid=30s ipv6=off;

    location ~/api(.*)$ {

        proxy_pass ${BACKEND_URI}/api$1;

        # if BACKEND_URI is using TLS/SSL with SNI, this is important!
        proxy_ssl_server_name on;

        proxy_redirect off;
        proxy_set_header Host $proxy_host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Host $server_name;
    }
}
```

### 建立 Dockerfile 與 .dockerignore 檔案

請將以下兩個檔案建立在 Angular 專案根目錄下。

**.dockerignore**

注意：`node_modules` 一定要排除。

```dockerignore
node_modules
npm-debug.log
Dockerfile*
docker-compose*
.dockerignore
.git
.gitignore
README.md
LICENSE
.vscode
```

**Dockerfile**

我使用了 [multi-stage builds](https://docs.docker.com/develop/develop-images/multistage-build/) 建置技巧，先透過 `node:10` 建置 Angular 應用程式，然後再將建置過的 Angular 應用程式複製到透過 `nginx:1.15.2-alpine` 建立的容器中，最後再透過 `nginx.site.template` 置換成 `/etc/nginx/conf.d/default.conf` 站台設定檔。

這邊最重要技巧是 `envsubst` 的使用方式，透過這個工具，可以輕易的將設定檔範本中特定變數 ( `$VARNAME` , `${VARNAME}` )，替換成目前「同名環境變數的值」。不過執行的時候要注意，你必須在參數列上加上你想替換的變數有哪些 (`${BACKEND_URI}`)，否則執行的過程會將所有範本中的 `$VARNAME` 全部移除。

最後一段的 `nginx -g 'daemon off;'` 是因為 nginx 預設執行時，會跑在 daemon 模式，將這個模式關閉，就可以讓 nginx 跑在前景，讓 Docker Engine 監視這個程序，如果 nginx 意外被關閉的話，Container 就會自動停止！

```dockerfile
FROM node:10 AS node-builder
WORKDIR /src
COPY . .
RUN npm install
RUN npm run build -- --prod --output-path dist

FROM nginx:1.15.2-alpine
COPY --from=node-builder /src/dist /usr/share/nginx/html
COPY nginx.site.template /etc/nginx/conf.d/
CMD envsubst '${BACKEND_URI}' < /etc/nginx/conf.d/nginx.site.template > /etc/nginx/conf.d/default.conf && exec nginx -g 'daemon off;'
```

### 建置 Docker 映像檔

```bash
docker build -t myapp:ng6-nginx .
```

### 執行 Docker 容器

請注意：執行 `docker run` 的時候，一定要加入 `-e` 參數，帶入 `BACKEND_URI` 環境變數，並設定 `SCHEME://HOSTNAME:PORT` 進去，這樣才能成功執行 nginx 反向代理功能。部署到 Kubernetes 的時候，也要記得綁定環境變數才行！

其中 `myapp:ng6-nginx` 只是一個 `ImageName:Tag` 而已，你可以任意修改。

```bash
docker run -d -p 8088:80 -e BACKEND_URI=https://dog.ceo myapp:ng6-nginx
```

連到 `http://localhost:8088/` 就可以看到 Angular 應用程式成功執行，並且成功呼叫後端 API 顯示狗狗圖片。

連到 `http://localhost:8088/api/breeds/image/random` 就可以看到後端 API 可以透過 nginx 的反向代理設定正常取得資料。

**相關連結**

-   [本篇文章完整範例程式](https://github.com/doggy8088/angular-nginx-with-proxy-docker-sample)
-   [Using environment variables in nginx configuration](https://github.com/docker-library/docs/tree/master/nginx#using-environment-variables-in-nginx-configuration)
-   [bash - is there an escape character for envsubst? - Stack Overflow](https://stackoverflow.com/a/24964184/910074)
-   [Full Example Configuration | NGINX](https://www.nginx.com/resources/wiki/start/topics/examples/full/)
-   [NGINX caching proxy fails with SSL23*GET*SERVER\_HELLO:sslv3 alert handshake failure - Stack Overflow](https://stackoverflow.com/questions/25329941/nginx-caching-proxy-fails-with-ssl23-get-server-hellosslv3-alert-handshake-fail)
-   [chimurai/http-proxy-middleware: The one-liner node.js http-proxy middleware for connect, express and browser-sync](https://github.com/chimurai/http-proxy-middleware)
-   [nginx | Docker Documentation](https://hub.docker.com/_/nginx)
-   [Define a Command and Arguments for a Container - Kubernetes](https://kubernetes.io/docs/tasks/inject-data-application/define-command-argument-container/#running-a-command-in-a-shell)
-   [library/nginx - Docker Hub](https://hub.docker.com/r/library/nginx/tags/)
