Kerry 的筆記本
  • Table of contents
  • Kerry的Mac裝機必要
  • ASP.NET Core 教育訓練文件
    • .NET 9 OpenAPI 介紹與教學
    • 目錄
    • ASP.NET Core Authentication系列(一)理解Claim, ClaimsIdentity, ClaimsPrincipal
    • ASP.NET Core Authentication系列(三)Cookie選項
    • ASP.NET Core Authentication系列(二)實現認證、登錄和註銷
    • ASP.NET Core Authentication系列(四)基於Cookie實現多應用間單點登錄(SSO)
    • ASP.NET Core Consul 教學
    • ASP.NET Core Hangfire 排程管理
    • ASP.NET Core KeyCloak 實作
    • ASP.NET Core NLog-依照Environment使用Nlog.Config檔案
    • ASP.NET Core NLog-如何使用 NLog 將 log 寫到檔案
    • ASP.NET Core Nlog-發送訊息到ElasticSearch
    • 目錄
    • ASP.NET Core Quartz.NET 管理介面
    • ASP.NET Core RDLC 報表設計
    • ASP.NET Core SFTP (使用第三方套建 SSH.Net) - 類別庫為案例
    • ASP.NET Core 中使用 HttpReports 進行接口統計,分析, 可視化, 監控,追踪等
    • ASP.NET 使用 MassTransit 與 RabbitMQ,實現事件發佈、訂閱
    • Asp.Net Core 分散式Session – 使用 Redis
    • ASP.NET Core 前台會員修改個人資料
    • ASP.NET Core 前台會員忘記密碼與重設密碼
    • ASP.NET Core 前台會員登入
    • ASP.NET Core 前台會員註冊
    • ASP.NET Core 呼叫 API 發生 CORS 錯誤
    • ASP.NET Core 如何套網頁設計樣版
    • ASP.NET Core 客製化Model Validation 預設錯誤訊息
    • ASP.NET Core 後台查詢頁面教學
    • ASP.NET Core 網站生命週期
    • ASP.NET Feature Management 使用說明與教學
    • ASP.NET RulesEngine 介紹
    • ASP.NET WinForms APP 程式安裝檔
    • LinePay 支付完成後返回 LINE 應用而不跳出外部瀏覽器
    • EntityFramework
      • EF Core Migrations 完整教學手冊
      • EntityFramework Core DB Migrations
      • 使用 Entity Framework Core (EF Core) 的 Migrations 功能進行版本控制
    • NET 6
      • .NET 6 Autofac範例
      • .NET 6 Automapper範例
      • .NET 6 BenchmarkDotNet範例
      • .NET 6 Bogus範例
      • .NET 6 Dapper範例
      • .NET 6 Dapper語法說明
      • .NET 6 EFCore範例
      • .NET 6 EFCore語法說明
      • .NET 6 EPPlus圖表範例
      • .NET 6 EPPlus範例
      • .NET 6 Hangfire範例
      • .NET 6 HttpClient單元測試範例
      • .NET 6 MailKit前置作業
      • .NET 6 MailKit範例
      • .NET 6 Moq範例
      • .NET 6 NLog範例
      • .NET 6 NLog進階範例
      • .NET 6 Serilog範例
      • .NET 6 Serilog進階範例
      • .NET 6 Telegram.Bot前置作業
      • .NET 6 Telegram.Bot範例
      • .NET 6 Text.Json範例
      • .NET 6 swagger授權
      • .NET 6 swagger範例
      • .NET 6 xUnit範例
      • .NET 6 取得appsettings檔案內容
      • .NET 6 更改回傳Json時為大駝峰命名
      • .NET 6 解決System.Text.Json序列化後會將所有非ASCII轉為Unicode
    • WDMIS
      • CORS
      • FeatureManagement
      • Serilog
      • Spectre.Console
      • 資料模型實戰:從 MSSQL 設計到 .NET 8 WebAPI 實作(以刀具管理為例)
  • Azure
    • 如何在 ASP.NET CORE 5.0 WEB 應用程序中實現 AZURE AD 身份驗證
    • Azure App Configuration 使用教學
    • Azure Blob Storage
    • Azure DevOps 持續整合(CI) + Artifacts
  • CSharp
    • ASP.NET await 與 wait 的差異
    • AutoMapper —— 類別轉換超省力
    • C# 中的 HTTPClient — 入門指南
    • C# 正則表達式:從零到英雄指南
    • C# 集合, List<> 取交集、差集、聯集的方法
    • C#單元測試教學
    • CORS 介紹與設定方式
    • CSharp Coding Conventions
    • Using jQuery Unobtrusive AJAX in ASP.NET Core Razor Pages
    • 深入Dapper.NET源碼
    • 菜雞與物件導向
      • 菜雞與物件導向 (0): 前言
      • 菜雞與物件導向 (1): 類別、物件
      • 菜雞與物件導向 (10): 單一職責原則
      • 菜雞與物件導向 (11): 開放封閉原則
      • 菜雞與物件導向 (12): 里氏替換原則
      • 菜雞與物件導向 (13): 介面隔離原則
      • 菜雞與物件導向 (14): 依賴反轉原則
      • 菜雞與物件導向 (15): 最少知識原則
      • 菜雞與物件導向 (2): 建構式、多載
      • 菜雞與物件導向 (3): 封裝
      • 菜雞與物件導向 (4): 繼承
      • 菜雞與物件導向 (5): 多型
      • 菜雞與物件導向 (6): 抽象、覆寫
      • 菜雞與物件導向 (7): 介面
      • 菜雞與物件導向 (8): 內聚、耦合
      • 菜雞與物件導向 (9): SOLID
      • 菜雞與物件導向 (Ex1): 小結
  • DBeaver
    • 如何強制讓 DBeaver 在 Mac 上使用英文介面
  • DesignPattern
    • OAuth
    • Repository 模式 (Repository Pattern)
    • Single Sign On 實作方式介紹 (CAS)
    • 【SOP製作教學】新手適用,SOP範例、流程圖、製作流程全公開!
    • 【SOP製作教學】流程圖教學、重點範例、BPMN符號介紹!
    • 【SOP製作教學】流程圖符號整理、BPMN2.0進階符號教學!
    • 多奇數位 C# 程式碼撰寫規範 (C# Coding Guideline)
    • 軟體分層設計模式 (Software Layered Architecture Pattern)
    • 開源程式碼檢測平台 SonarQube
    • 菜雞新訓記
      • 菜雞新訓記 (0): 前言
      • 菜雞新訓記 (1): 使用 Git 來進行版本控制吧
      • 菜雞新訓記 (2): 認識 Api & 使用 .net Core 來建立簡單的 Web Api 服務吧
      • 菜雞新訓記 (3): 使用 Dapper 來連線到資料庫 CRUD 吧
      • 菜雞新訓記 (4): 使用 Swagger 來自動產生可互動的 API 文件吧
      • 菜雞新訓記 (5): 使用 三層式架構 來切分服務的關注點和職責吧
      • 菜雞新訓記 (6): 使用 依賴注入 (Dependency Injection) 來解除強耦合吧
      • 菜雞新訓記 (7): 使用 Fluent Validation 來驗證參數吧
  • DevOps
    • Repository 模式 (Repository Pattern)
    • pipeline工具研究
    • 單例模式 (Singleton Pattern)
    • 單元測試
    • 軟體分層設計模式 (Software Layered Architecture Pattern)
    • 雙重檢查鎖定模式 (Double-Checked Locking Pattern)
  • Docker
    • Docker 中部署 .NET 8 Web App 並支援 HTTPS
    • Docker指令大全
    • 第七章 安裝Nomad
    • Docker - 第三章 | 安裝 MSSQL
    • Docker - 第九章 | 安裝 datalust seq
    • 第二章 docker-compose 教學
    • Docker - 第五章 | 安裝 Redis
    • 第八章 安裝SonarQube
    • Docker - 第六章 | 安裝RabbitMQ
    • 第十一章 安裝 VtigerCRM
    • 第十二章 安裝KeyCloak
    • Docker - 第十章 | 安裝 Redmine
    • 第四章 安裝MySQL
    • Docker Desktop (含更改 Docker Image 路徑)
  • Git
    • Git Flow 指令大全(完整指令整理) 🚀
    • Git 安裝及配置SSH Key
    • Git 建立到上傳
    • 將現有專案的遠端儲存庫直接更改為新的儲存庫
    • Git 流程規劃
    • Git 語法大全
    • 30 天精通 Git 版本控管
      • 30 天精通 Git 版本控制
        • 第 01 天:认识 Git 版本控制
        • 第 02 天:在 Windows 平台必装的三套 Git 工具
        • 第 03 天:建立仓库
        • 第 04 天:常用的 Git 版本控制指令
        • 第 05 天:了解仓库、工作目录、物件与索引之间的关系
        • 第 06 天:解析 Git 资料结构 - 物件结构
        • 第 07 天:解析 Git 资料结构 - 索引结构
        • 第 08 天:关于分支的基本观念与使用方式
        • 第 09 天:比对文件与版本差异
        • 第 10 天:认识 Git 物件的绝对名称
        • 第 11 天:认识 Git 物件的一般参照与符号参照
        • 第 12 天:认识 Git 物件的相对名称
        • 第 13 天:暂存工作目录与索引的变更状态
        • 第 14 天: Git for Windows 选项设定
        • 第 15 天:标签 - 标记版本控制过程中的重要事件
        • 第 16 天:善用版本日志 git reflog 追踪变更轨迹
        • 第 17 天:关于合并的基本观念与使用方式
        • 第 18 天:修正 commit 过的版本历史记录 Part 1
        • 第 19 天:设定 .gitignore 忽略清单
        • 第 20 天:修正 commit 过的版本历史记录 Part 2
        • 第 21 天:修正 commit 过的版本历史记录 Part 3
        • 第 22 天:修正 commit 过的版本历史记录 Part 4 (Rebase)
        • 第 23 天:修正 commit 过的版本历史记录 Part 5
        • 第 24 天:使用 GitHub 远端仓库 - 入门篇
        • 第 25 天:使用 GitHub 远端仓库 - 观念篇
        • 第 26 天:多人在同一个远端仓库中进行版控
        • 第 27 天:通过分支在同一个远端仓库中进行版控
        • 第 28 天:了解 GitHub 的 fork 与 pull request 版控流程
        • 第 29 天:如何将 Subversion 项目汇入到 Git 仓库
        • 第 30 天:分享工作中几个好用的 Git 操作技巧
      • zh-tw
        • 第 01 天:認識 Git 版本控管
        • 第 02 天:在 Windows 平台必裝的三套 Git 工具
        • 第 03 天:建立儲存庫
        • 第 04 天:常用的 Git 版本控管指令
        • 第 05 天:了解儲存庫、工作目錄、物件與索引之間的關係
        • 第 06 天:解析 Git 資料結構 - 物件結構
        • 第 07 天:解析 Git 資料結構 - 索引結構
        • 第 08 天:關於分支的基本觀念與使用方式
        • 第 09 天:比對檔案與版本差異
        • 第 10 天:認識 Git 物件的絕對名稱
        • 第 11 天:認識 Git 物件的一般參照與符號參照
        • 第 12 天:認識 Git 物件的相對名稱
        • 第 13 天:暫存工作目錄與索引的變更狀態
        • 第 14 天: Git for Windows 選項設定
        • 第 15 天:標籤 - 標記版本控制過程中的重要事件
        • 第 16 天:善用版本日誌 git reflog 追蹤變更軌跡
        • 第 17 天:關於合併的基本觀念與使用方式
        • 第 18 天:修正 commit 過的版本歷史紀錄 Part 1
        • 第 19 天:設定 .gitignore 忽略清單
        • 第 20 天:修正 commit 過的版本歷史紀錄 Part 2
        • 第 21 天:修正 commit 過的版本歷史紀錄 Part 3
        • 第 22 天:修正 commit 過的版本歷史紀錄 Part 4 (Rebase)
        • 第 23 天:修正 commit 過的版本歷史紀錄 Part 5
        • 第 24 天:使用 GitHub 遠端儲存庫 - 入門篇
        • 第 25 天:使用 GitHub 遠端儲存庫 - 觀念篇
        • 第 26 天:多人在同一個遠端儲存庫中進行版控
        • 第 27 天:透過分支在同一個遠端儲存庫中進行版控
        • 第 28 天:了解 GitHub 的 fork 與 pull request 版控流程
        • 第 29 天:如何將 Subversion 專案匯入到 Git 儲存庫
        • 第 30 天:分享工作中幾個好用的 Git 操作技巧
  • Hands-On Labs - LineBotSDK 實作手札 (C#, .net core)
    • 00. 如何申請LINE Bot
    • CLI
      • 使用CLI來發送新的Channel Access Token(LINE Bot)
      • 使用CLI免費發送LINE Notify通知
    • basic
      • 如何發送LINE訊息(Push Message)
      • 如何發送LINE Template Messages
      • 如何發送ImageMap訊息
      • 如何發送Flex Message
      • 如何在訊息後面加上QuickReply快捷選項
    • liff
      • Lab 21: 建立第一個LIFF應用
    • webhook
      • 如何建立可Echo的基本LINE Bot
      • 如何在WebHook中取得用戶個人資訊(名稱、頭像、狀態)
      • 如何在WebHook中取得用戶上傳的圖片(Bytes)
  • Markdown
    • Markdown Cheatsheet 中文版
    • Markdown語法大全
    • 使用HackMD建立書本目錄
    • 使用HackMD建立簡報
  • SAP ABAP
    • ABAP開發環境和總體介紹
    • SAP MM模塊常用表總結
    • SAP QM數據庫表清單
    • SAP欄位與表的對應關係
  • SQL Server
    • [SQL SERVER] Like in
    • SQL Server 中,移除資料庫中所有的關聯限制
    • SQL Server 刪除資料庫中所有資料表
    • SQL Server View、Function 及 Stored Procedure 定義之快速備份
    • SSMS v18 清除登入畫面中,下拉選單歷史紀錄
    • [MS SQL]如何透過Database Mail進行郵件發送
    • [SQL SERVER]撰寫Stored Procedure小細節
    • 使用 Data Migration Assistant 移轉 SQL Server 資料庫與帳戶
    • 使用SSIS創建同步資料庫數據任務
  • Tools
    • 免費 FTP 伺服器 FileZilla Server 安裝教學 (新版設定)
  • VisualStudio
    • .NET CLI 指令碼介紹
    • Visual Studio 使用 Git 版本控制
    • 使用 Visual Studio 2022 可透過 .editorconfig 鎖定文字檔案的儲存編碼格式分享
  • Web API
    • ASP.NET Core 6 Web API 進行 JWT 令牌身份驗證
    • [ASP.NET Core]如何使用SwaggerAPI說明文件
    • ASP.NET Core Web Api實作JWT驗證筆記
    • ECFIT API 範例
    • JWT Token Authentication And Authorizations In .Net Core 6.0 Web API
    • 微服務架構 - 從狀態圖來驅動 API 的設計
  • Windows
    • [C#] 伺服器監控常用語法 (事件檢視器、CPU 硬碟使用率、程式執行狀況)
    • Configure IIS Web Server on Windows Server 2019
    • Log Paser Studio 分析 IIS W3C Log
    • Windows Server 2019 如何安裝 IIS 運行 ASP.NET 專案
    • 如何檢查安裝在 IIS 上的 .NET Core Hosting Bundle 版本
    • [IIS] 如何解決網站第一個請求 Request 特別慢 ?
    • IIS 不停機更版設置
    • SQL Server 2019 Standard 繁體中文標準版安裝
    • WINDOWS共用資料夾的網路認證密碼放在哪?如何清除?
    • 如何設定 ASP.NET CORE 網站應用程式持續執行在 IIS 上
  • 專案管理
    • SSDLC (Secure Software Development Life Cycle)
    • 系統開發原則
    • MIS及專案管理-使用Redmine
      • 第10章 - [日常管理]MIS部門週會工作進度追蹤
      • 第11章 - [日常管理]MIS部門主管月會報告管理
      • 第12章 - [日常管理]機房工作日誌
      • 第13章 - [日常管理]MIS部門耗用工時及工作進度檢討
      • 第14章 - [日常管理]MIS文件知識庫
      • 第15章 - [日常管理]整理及管理分享
      • 第16章 - [異常管理]使用者問題回報系統
      • 第17章 - [異常管理]資安事件及異常紀錄
      • 第18章 - [異常管理]整理及管理分享
      • 第19章 - [變革管理]MIS的專案及專案管理五大階段
      • 第1章 - [MIS及專案管理]中小企業MIS的鳥事
      • 第20章 - [變革管理]MIS的新專案管理:起始階段
      • 第21章 - [變革管理]MIS的新專案管理:規劃階段
      • 第22章 - [變革管理]MIS的新專案管理:執行階段
      • 第23章 - [變革管理]MIS的新專案管理:監控階段
      • 第24章 - [變革管理]MIS的新專案管理:結束階段
      • 第25章 - [變革管理]整理及管理分享
      • 第26章 - [ISMS管理]ISMS平台整體規劃
      • 第27章 - [ISMS管理]ISMS文管中心
      • 第28章 - [ISMS管理]ISMS表單紀錄的管理
      • 第29章 - [ISMS管理]整理及管理分享
      • 第2章 - [MIS及專案管理]專案管理的概念及MIS應用
      • 第30章 - 初心、來時路及感謝:系列文章總結回顧
      • 第3章 - [MIS及專案管理]管理工具的選擇
      • 第4章 - [Redmine]Redmine的安裝及設定
      • 第5章 - [Redmine]Redime系統邏輯說明
      • 第6章 - [Redmine]自行建立及維護表單
      • 第7章 - [Redmine]專案版面的規劃
      • 第8章 - [日常管理]AR管理
      • 第9章 - [日常管理]資訊服務申請
  • 微服務架構
    • DDD + CQRS + MediatR 專案架構
    • 微服務架構 #2, 按照架構,重構系統
    • 淺談微服務與網站架構的發展史
    • API First Workshop 設計概念與實做案例
      • API First #1 架構師觀點 - API First 的開發策略 - 觀念篇
      • API First #2 架構師觀點 - API First 的開發策略 - 設計實做篇
    • 基礎建設 - 建立微服務的執行環境
      • Part #1 微服務基礎建設 - Service Discovery
      • Part #2 微服務基礎建設 - 服務負載的控制
      • Part #3 微服務基礎建設 - 排隊機制設計
      • Part #4 可靠的微服務通訊 - Message Queue Based RPC
      • Part #5 非同步任務的處理機制 - Process Pool
    • 實做基礎技術 API & SDK Design
      • API & SDK Design #1, 資料分頁的處理方式
      • API & SDK Design #2, 設計專屬的 SDK
      • API & SDK Design #3, API 的向前相容機制
      • API & SDK Design #4, API 上線前的準備 - Swagger + Azure API Apps
      • API & SDK Design #5 如何強化微服務的安全性 API Token JWT 的應用
    • 建構微服務開發團隊
      • 架構面試題 #1, 線上交易的正確性
      • 架構面試題 #2, 連續資料的統計方式
      • 架構面試題 #3, RDBMS 處理樹狀結構的技巧
      • 架構面試題 #4 - 抽象化設計;折扣規則的設計機制
    • 架構師觀點 - 轉移到微服務架構的經驗分享
      • Part #1 改變架構的動機
      • Part #2 實際改變的架構案例
    • 案例實作 - IP 查詢服務的開發與設計
      • 容器化的微服務開發 #1 架構與開發範例
      • 容器化的微服務開發 #2 IIS or Self Host
  • 系統評估
    • RPA 與 WebAPI 評估
    • 數位轉型:從現有系統到數位化的未來
    • 數位轉型:從現有系統到數位化的未來
  • 面試
    • CV_黃子豪_2024
    • HR 問題集
    • .NET 工程師 面試問題集
    • 資深工程師 問題集
    • 資深開發人員 / 技術主管
    • 題目
Powered by GitBook
On this page
  • 容器化的微服務開發 #1 架構與開發範例
  • 容器驅動開發 (Container Driven Development)
  • 建立微服務 Solution: IP2C
  • Project: IP2C.WebAPI
  • Project: IP2C.Worker
  • Test Run: IP2C Services (WebAPI + Worker) on Local PC
  • 小結
  1. 微服務架構
  2. 案例實作 - IP 查詢服務的開發與設計

容器化的微服務開發 #1 架構與開發範例

Previous案例實作 - IP 查詢服務的開發與設計Next容器化的微服務開發 #2 IIS or Self Host

Last updated 1 year ago

架構與開發範例

容器驅動開發 (Container Driven Development)

別以為一個 “IP 查詢服務”,就真的只是一個服務而已。既然微服務的設計準則包含這條: “能獨立自主運作的服務”,那麼你要開發 的就不只 webapi 這麼一個而已。我先把需求列一下:

  1. High Availability + Scalability

  2. Auto Update IP Database

  3. Include Client SDK (with client side cache)

  4. DX (Developer Experience, 要對使用我的服務的開發者友善)

綜合這些需求,我規劃了這樣的架構,如果都能實作出來,應該就能同時滿足上述這幾項了吧? 既然 container 這麼好用,能透過 容器化解決的事情我就不用自己做了。在能夠極度利用容器化的優點為前提,來看看這份設計圖:

img
  • Deployment diagram of IP2C service

很典型的架構,前端用一組 reverse proxy, 來把流量平均分配到後端的多組 IP2C.WebAPI, 來達到 HA + Scale out 的要求。 然而 IP query 最重要的就是資料庫的更新,因此另外安排了一個 IP2C.Worker, 定期到官網的下載頁面定期更新資料檔。下載後會自動 解壓縮,同時先對這個資料檔做好基本的單元測試,通過後才會部署到 share storage,供其他的 IP2C.WebAPI 使用。同時為了 方便其他的 developer 充分使用我的 IP2C service, 我也打算提供一組 SDK, 方便他們直接呼叫我的服務。這 IP2C.SDK 除了方便呼叫 IP2C.WebAPI 之外,我也在 SDK 內部實作上加了 client side cache, 同一個 IP 在時間內查詢多次, 則會直接透過 cache 傳回。

微服務化的應用程式開發,總是會有很多獨立的 projects, 開發測試的過程中會不斷面臨部署,測試,修改,部署… 的循環。過程中 若沒有做好 DevOps 的搭配,生產力會大幅地降低。因此我自己對於 DevOps 的流程我也做了簡單的 CI / CD 需求規劃:

  1. Code 修改完成 Push 到 Git Repository 之後,觸發 CI 進行編譯、單元測試

  2. 通過測試與編譯的 code, 則將 IP2C.WebAPI 與 IP2C.Worker 先建置成 docker image, 放到 docker registry

  3. SDK 則在編譯之後,自動進行 nuget package 與 nuget push ,將 SDK 包裝成 Nuget 套件,放上 nuget server 發行這份 SDK

  4. 決定要上版的時候,就透過預先編好的 docker compose 定義檔,從 docker registry 更新部署的版本

這兩部分都完成後,各位可以盤點一下,是否原先的需求都已經達成了? 整個藍圖設計好之後,接下來就是一步一步完成他了。 DevOps 的部分,我實際是使用 GitLab 的 CI-Runner, 簡單好用, 複雜度跟功能的平衡做得還不錯。不過這部分我會略過, 先用一個簡單的 build script 替代。以後有機會的話再來介紹這段。這次就跳過 DevOps, 後面的示範會直接半自動的部署到 Azure 上面現成的環境,篇幅有限,請見諒 :D

建立微服務 Solution: IP2C

這篇既然是 Hands-On Labs, 就直接來寫 code 吧! 這次的 solution 裡面有這些東西,先說明一下:

/build.cmd build script, 用來完成整個 solution 的編譯與發行 (to docker registry & nuget server)。 若你有採用任何 CI 的系統,可以將 script 的內容搬過去。我自己是用 GitLab, 可以在 CI runner 內沿用這個 build script。 這次範例我把重點擺在 code 上面,CI/CD 就用 build script + 手動部署容器來替代。

/docker-compose.yml 要部署到執行環境使用的 docker-compose 設定檔。

/IP2C.NET 從 Darkthread GitHub 直接 fork 過來的 project, 搜尋 ip-database csv 用的高效率 C# library

/IP2C.WebAPI 使用 IP2C.NET 開發的 WebAPI, 提供 REST API,可以使用 ~/api/ip2c/{ip value} 的格式來查詢 ip address 所屬的國家。

/IP2C.Worker .NET Console Application, 啟動之後會開始計時,每隔一段固定時間後就自動到 IP2C 官方網站下載新的資料檔,在更新檔案 前會自動解壓縮 + 測試檔案內容是否正確。

/IP2C.SDK 透過 REST API 呼叫 IP2C.WebAPI 的 .NET class library, 除了簡化呼叫的程序之外,也加上了 cache (client side)。

接下來逐一看看每個 project 做了什麼特別的動作…

Project: IP2C.WebAPI

這是服務的主體,對外提供查詢 IP 的 REST API。用 ASP.NET WebAPI2 開發。這個專案專注的部分很單純,就是提供查詢 IP 所屬 國家的 REST API。查詢格式是: /api/ip2c/134744072 (數字部分是 IPV4 的 4bytes 數值轉成 int, 範例是 8.8.8.8, 換成 int 是 0x08080808, 也就是十進位的 134744072)。

查詢的結果用 Json 傳回,格式如下:

{
    "CountryName": "United States",
    "CountryCode": "US"
}

至於查詢的來源資料檔,直接放在 ~/App_Data/ipdb.csv, 大小約 12mb 左右。查詢的核心邏輯,直接採用 Darkthread 提供的 IP2C.NET 這個 .NET 含式庫。

規格講完了,Code 其實也沒啥值得一看的 XD。 主要的 code 就只有這段,IP2CController:

public class IP2CController : ApiController
{
    // 傳回對應的 ip address 所屬的國家代碼與名稱 (json)
    public object Get(uint id)
    {
        IPCountryFinder ipcf = this.LoadIPDB();

        string ipv4 = this.ConvertIntToIpAddress(id);
        string countryCode = ipcf.GetCountryCode(ipv4);

        return new
        {
            CountryName = ipcf.ConvertCountryCodeToName(countryCode),
            CountryCode = countryCode
        };
    }

    // 將代表 ipv4 的 int value 轉成 ipv4 的 string value
    private string ConvertIntToIpAddress(uint ipv4_value)
    {
        return string.Format(
            "{0}.{1}.{2}.{3}",
            (ipv4_value >> 24) & 0x00ff,
            (ipv4_value >> 16) & 0x00ff,
            (ipv4_value >> 08) & 0x00ff,
            (ipv4_value >> 00) & 0x00ff);
    }

    private IPCountryFinder LoadIPDB()
    {
        string cachekey = "storage:ip2c";

        IPCountryFinder result = MemoryCache.Default.Get(cachekey) as IPCountryFinder;
        if (result == null)
        {
            string filepath = HostingEnvironment.MapPath("~/App_Data/ipdb.csv");

            var cip = new CacheItemPolicy();
            cip.ChangeMonitors.Add(new HostFileChangeMonitor(new List<string> { filepath }));

            result = new IPCountryFinder(filepath);
            MemoryCache.Default.Add(
                cachekey,
                result,
                cip);
        }

        return result;
    }
}

沒幾行 code, 解釋就省了,接著來看對應的 dockerfile:

FROM microsoft/aspnet

WORKDIR c:/inetpub/wwwroot/
COPY . .

EXPOSE 80
VOLUME ["c:/inetpub/wwwroot/App_Data"]
  1. 建置 container image 時,用 microsoft/aspnet 這個 image 為基礎。 這是 microsoft 預先準備好,包含 IIS + ASP.NET runtime 的 image。

  2. docker build 的過程中, 用 COPY 指令將編譯好的 webapp 檔案,複製到 container 內的 c:/inetpub/wwwroot

  3. 宣告這個 container 將會用到 port 80 將來在部署時,IT人員可以自由將之對應到其他 port

  4. 宣告這個 container 將允許將外部的 volume 掛載到 c:/inetpub/wwwroot/app_data 將來在部署時,IT人員可以選擇將這個目錄對應到其他 storage 上

在這個專案內,我已經預先放了一份格式正確的 ipdb.csv 檔案了。如果你不介意這是不是最新的話,其實你只要這個 project 就可 已執行 IP 查詢服務的 WebAPI 了。我直接附上 build 的指令 (DOS Prompt 命令提示字元,非 powershell):

: build solutions in release mode
"c:\Program Files (x86)\MSBuild\14.0\Bin\MSBuild.exe" /p:Configuration=Release /p:DeployOnBuild=true

: build webapi docker image
pushd .
cd IP2C.WebAPI\obj\Release\Package\PackageTmp
docker build -t ip2c/webapi:latest .
popd

如果執行成功,就會完成 project 的編譯,也會完成 docker image 的建置。 用這道指令就可以啟用這個 container 了:

docker run -d -p 8000:80 ip2c/webapi

這時,別的電腦就能透過 8000 port 存取你 PC 內的 webapi 了。由於 winnat 的限制,不支援 nat loopback。你如果本機要使用 的話,得查詢這個 container 在 nat 後面取得的 ip address, 用這個 ip 的 80 port 就可以使用 webapi.

到目前為止,我們已經完成 ASP.NET WebAPI 的容器化了。如果你的開發機器是 windows 10 pro / enterprise, 或是 windows server 2016, 同時也已經啟用 windows container 支援功能的話,你不需要安裝 IIS 或是 IIS express 就可以直接在本機測試了。 其實很簡單一行指令就可以完成了,不過實際最大的效益現在還看不出來。我們這個 lab 繼續做下去…

Project: IP2C.Worker

這個專案的目的,就只有一個,負責定期更新 WebAPI 服務需要的 ipdb.csv 檔案內容。期望的功能有:

  1. 排程啟動後能進入監控模式,在指定的時間到達時自動進行檔案更新 (實際上是每天執行,這邊我為了方便測試是每三分鐘執行一次)

  2. 第一次執行,若找不到舊版的檔案,則立刻先用預先下載的檔案替代更新。 (隨 code 附上的 ipdb.csv, 非最新的檔案)

  3. 能夠透過 logs 查詢執行的成果

  4. (option) 能透過後臺管控系統,隨時接受指令立即更新 (這功能其實我有實作,不過扯到後台有點囉嗦,這次範例我先跳過)

在過去,這類程式寫起來其實也不難,但是為了讓他定期自動執行,其實有點麻煩,很多瑣事要另外處理。舉例來說,專案類型 要嘛就 windows service, 寫好後要安裝時還要註冊到 server 上,透過 windows 的服務管理員啟動。不然就是寫成 console app, 透過 windows 工作排程器,時間到了啟動它。

不過不管用哪個方法,其實都有點麻煩… 這邊我要介紹的是,如果你要在 container 內做這件事,遠比你想像的簡單! container 本身就已經可以被當成一個服務了 (daemon), 因此你唯一要做的就是,寫個 console app, 啟動後就按照上述的需求。把他 安排到 dockerfile 內的 entrypoint, 讓 container 一啟動就執行它就可以了。實際部署之後,IT 人員只要控制這個 container 是否 啟動或是停止就好,完全不需要 developer 開發時花心思處理這些細節。

說到這裡,我想本部落格的讀者應該都知道怎麼寫吧? 一樣沒幾行的 code, 直接來看看:

class Program
{
    static void Main(string[] args)
    {
        bool isWatchMode = true;
        string filepath = Path.Combine(
            Path.GetDirectoryName(Assembly.GetEntryAssembly().Location),
            "data\\ipdb.csv");

        if (isWatchMode)
        {
            // watch mode
            DateTime start = new DateTime(2000, 1, 1, 15, 40, 0);
            TimeSpan period = TimeSpan.FromMinutes(3.0);

            while (true)
            {
                TimeSpan wait = TimeSpan.FromMilliseconds(period.TotalMilliseconds - (DateTime.Now - start).TotalMilliseconds % period.TotalMilliseconds);
                Console.WriteLine("wait: {0} (until: {1})", wait, DateTime.Now.Add(wait));
                Task.Delay(wait).Wait();
                UpdateFile(@"http://software77.net/geo-ip/?DL=1", filepath);
            }
            
        }
        else
        {
            // update once
            UpdateFile(@"http://software77.net/geo-ip/?DL=1", filepath);
        }
    }


    static void UpdateFile(string url, string file)
    {
        Console.WriteLine("-update file: {0}", DateTime.Now);

        string temp = Path.ChangeExtension(file, ".temp");
        string back = Path.ChangeExtension(file, ".bak");

        if (Directory.Exists(Path.GetDirectoryName(file)) == false)
        {
            Directory.CreateDirectory(Path.GetDirectoryName(file));
        }

        if (File.Exists(temp)) File.Delete(temp);

        if (DownloadAndExtractGZip(url, temp))
        {
            if (TestFile(temp))
            {
                if (File.Exists(back)) File.Delete(back);
                if (File.Exists(file)) File.Move(file, back);
                File.Move(temp, file);
            }
            else
            {
                // test file, file incorrect
            }
        }
        else
        {
            // download fail.
        }
    }

    static bool TestFile(string file)
    {
        IPCountryFinder finder = new IPCountryFinder(file);

        // add test case here
        if (finder.GetCountryCode("168.95.1.1") != "TW") return false;


        return true;
    }

    static bool DownloadAndExtractGZip(string url, string file)
    {
        using (var client = new HttpClient())
        {
            HttpResponseMessage rsp = client.GetAsync(url).Result;

            if (rsp.StatusCode == HttpStatusCode.OK)
            {
                Stream source = rsp.Content.ReadAsStreamAsync().Result;

                GZipStream gzs = new GZipStream(source, CompressionMode.Decompress);
                FileStream fs = File.OpenWrite(file);

                int count = 0;
                byte[] buffer = new byte[4096];
                while((count = gzs.Read(buffer, 0, buffer.Length)) > 0)
                {
                    fs.Write(buffer, 0, count);
                }
                gzs.Close();
                fs.Close();

                source.Close();

                return true;
            }
        }

        return false;
    }
}

程式碼稍長了一點,不過都很簡單。接著來看看對應的 dockerfile:

FROM microsoft/dotnet-framework:latest

WORKDIR   c:/IP2C.Worker
COPY . .

VOLUME ["c:/IP2C.Worker/data"]
ENTRYPOINT IP2C.Worker.exe

dockerfile 也是簡單到爆,選定 base image, 複製檔案, 啟動檔案 (加上正確參數) 就結束了。

想要測試看看這個 container 是否能正常啟動? build 成功之後用這段指令啟動它 (互動模式):

docker run ip2c/worker

為了方便展示,我 source code 裡面定義的執行頻率比較密集,每三分鐘就會跑一次。你可以看到類似這樣的 message:

D:\CodeWork\github.com\IP2C.NET.Service>docker run ip2c/worker
wait: 00:02:24.7680000 (until: 5/24/2017 1:01:00 AM)
-update file: 5/24/2017 1:01:00 AM
wait: 00:02:52.6580000 (until: 5/24/2017 1:04:00 AM)
-update file: 5/24/2017 1:04:00 AM
wait: 00:02:55.3320000 (until: 5/24/2017 1:06:59 AM)

在同個 container 用 docker exec ... 多開一個 shell (cmd.exe), 進去 container 的目錄,你可以看到檔案真的有下載成功:

C:\IP2C.Worker>dir data
 Volume in drive C has no label.
 Volume Serial Number is AC75-48E4

 Directory of C:\IP2C.Worker\data

05/24/2017  01:04 AM    <DIR>          .
05/24/2017  01:04 AM    <DIR>          ..
05/24/2017  01:01 AM        12,449,488 ipdb.bak
05/24/2017  01:04 AM        12,449,488 ipdb.csv
               2 File(s)     24,898,976 bytes
               2 Dir(s)  88,298,643,456 bytes free

C:\IP2C.Worker>

不過實際執行,不能用互動模式啊! 只要啟動指令調整一下,加個 -d (–daemon) 就可以變成背景服務:

docker run -d ip2c/worker

背景模式就看不到輸出的訊息了… 這也很簡單,用 docker logs 指令就可以 (其中 5edf24fae8cf 是 container 的 ID):

D:\>docker ps -a
CONTAINER ID        IMAGE               COMMAND                   CREATED             STATUS                      PORTS                  NAMES
5edf24fae8cf        ip2c/worker         "cmd /S /C 'IP2C.W..."    47 hours ago        Exited (255) 46 hours ago                          kind_noyce
714dd0e424f7        ip2c/webapi         "C:\\ServiceMonitor..."   47 hours ago        Up 47 hours                 0.0.0.0:8000->80/tcp   tender_brown

D:\>docker logs 5edf24fae8cf
wait: 00:01:10.0480000 (until: 5/24/2017 1:19:00 AM)
-update file: 5/24/2017 1:19:00 AM
wait: 00:02:48.9170000 (until: 5/24/2017 1:22:00 AM)
-update file: 5/24/2017 1:22:00 AM
wait: 00:02:55.0310000 (until: 5/24/2017 1:24:59 AM)
-update file: 5/24/2017 1:25:00 AM
wait: 00:02:54.0370000 (until: 5/24/2017 1:28:00 AM)
-update file: 5/24/2017 1:28:00 AM
wait: 00:02:55.7030000 (until: 5/24/2017 1:30:59 AM)
-update file: 5/24/2017 1:31:00 AM
wait: 00:02:54.4140000 (until: 5/24/2017 1:33:59 AM)
-update file: 5/24/2017 1:34:00 AM

體會到了嗎? 容器的機制其實替你解決了不少麻煩事,從服務的開發,執行環境的控制,服務啟動及停止等等的控制,到 LOG 的收集 處理,甚至到所有不同應用程式的 image 建置與部署通通解決了,這些才是 developer 最討厭的部分啊! docker 能漂亮的搞定 這些問題,又把工具鏈跟生態圈弄得很好,才會在短短的四年就爆紅..

容器技術優異的地方在於,用通用一致的 image 格式,把你的程式與執行環境都放到裡面,精準地控制了執行環境,解決執行時 對環境的要求,工程師們以後不用再把 “在我電腦上是好的啊..” 這句話掛在嘴邊了 XD

封裝成 image 後,要執行時只要透過 docker run 就能啟動了。啟動時 docker 允許你調整 container 對外溝通的幾個 管道,這部分就是部署人員要接手的部分了。Docker 統一管控 container 的對外溝通管道有:

  • network (ipaddress, ports)

  • volume (folder, file)

  • environment variable

舉例來說,HTTP REST 都會需要 TCP 80 port, 你在打包 container image 時不用擔心這個,實際部署時,docker run 指令 允許你把實際的 port (ex: 8080) 轉接給 container 內的 port 80。

同樣的道理也可應用在 volume 的對應上。你可以把實際 host server 上的磁碟目錄 (ex: d:\data) 掛到 container 內 (ex: c:\inetpub\wwwroot\app_data)。對 container 內來說,他就只是個單純的 symbol link 而已。如果你把實體的目錄同時掛載 到多個 container 內的話,那這效用就相當於你開了共享目錄,同時給多個 container 使用。這遠比開網路芳鄰簡單太多了。這些 環境的控制,都只要交給部署的 IT 人員決定就夠了,完全不用勞煩 developer 。

這邊最巧妙的地方,就是容器本身就已經有 start / stop 等等狀態了,容器本身可以用 daemon (-d) 模式啟動,這時整個容器 就相當於標準的 windows service 了。你完全不需要替背景服務這件事寫任何額外的 code, 你只需要像這個例子一樣,單純寫個 console app 就夠了。這簡單的程度跟我們當年在學 hello world 差不多容易啊..

不過,跟前面 webapi 一樣。到目前為止用容器化部署的效益還看不大出來,後面完成後再來看…

Test Run: IP2C Services (WebAPI + Worker) on Local PC

現在,我們要把兩個 container 一起啟動了。這邊啟動的方式有點不同,兩個 container 能共同合作的前提,是要有共享的目錄。 我的作法是兩個 container 都先定義了一個 volume 的掛載點,啟動的時候我在 docker engine host 上建立一個 folder, 把這個 folder 同時掛上兩個 container 就搞定了。

我準備拿 d:\codework\data 當作範例,依序執行下列指令:

D:\>md data

D:\>docker run -d -p 8000:80 -v d:\data:c:/inetpub/wwwroot/App_Data ip2c/webapi
714dd0e424f7b57996b5e62f0ef0b457bdf0280cdc9e8160b2f7862257717b1b

D:\>docker run -d -v d:\data:c:/IP2C.Worker/data ip2c/worker
5edf24fae8cf793736307344e84ec95b49c6deafc3c8a034ce5efadfdf54f409

D:\>

剛啟動時,要等一會兒,讓 worker 更新資料檔之後就可以使用了。簡單用瀏覽器測試:

我測試的是 Google DNS 的 IP (8.8.8.8),用十六進位表示這 IP 對應的 int 數值是: 0x08080808, 拿小算盤敲一敲, 換成十進位就是 134744072 … 湊成 URL 就可以直接丟給 webapi 查詢了。

我們先用 docker logs 這指令,看看 worker 執行的 logs (console app 顯示在 stdout 的訊息,都可以這樣查看):

D:\CodeWork\github.com\IP2C.NET.Service\IP2C.WebAPI\obj\Release\Package\PackageTmp>docker logs 5e
wait: 00:01:10.0480000 (until: 5/24/2017 1:19:00 AM)
-update file: 5/24/2017 1:19:00 AM
wait: 00:02:48.9170000 (until: 5/24/2017 1:22:00 AM)
-update file: 5/24/2017 1:22:00 AM
wait: 00:02:55.0310000 (until: 5/24/2017 1:24:59 AM)

D:\CodeWork\github.com\IP2C.NET.Service\IP2C.WebAPI\obj\Release\Package\PackageTmp>

看來更新的很順利。這時我們再多開個 cmd.exe 分別進去兩個 container 看看更新的狀況。先看 worker:

C:\IP2C.Worker>dir
 Volume in drive C has no label.
 Volume Serial Number is AC75-48E4

 Directory of C:\IP2C.Worker

05/24/2017  12:51 AM    <DIR>          .
05/24/2017  12:51 AM    <DIR>          ..
05/24/2017  01:17 AM    <SYMLINKD>     data [\\?\ContainerMappedDirectories\97D5871C-3BA8-4288-A465-6D2DE1164A85]
05/23/2017  11:34 PM               223 dockerfile
05/24/2017  12:35 AM             6,656 IP2C.Net.dll
05/24/2017  12:35 AM            13,824 IP2C.Net.pdb
05/24/2017  12:35 AM             7,168 IP2C.Worker.exe
05/21/2017  04:01 AM               189 IP2C.Worker.exe.config
05/24/2017  12:35 AM            13,824 IP2C.Worker.pdb
               6 File(s)         41,884 bytes
               3 Dir(s)  21,131,309,056 bytes free

C:\IP2C.Worker>dir data
 Volume in drive C has no label.
 Volume Serial Number is AC75-48E4

 Directory of C:\IP2C.Worker\data

05/24/2017  01:25 AM    <DIR>          .
05/24/2017  01:25 AM    <DIR>          ..
05/24/2017  01:22 AM        12,449,488 ipdb.bak
05/24/2017  01:25 AM        12,449,488 ipdb.csv
               2 File(s)     24,898,976 bytes
               2 Dir(s)  1,479,074,295,808 bytes free

C:\IP2C.Worker>

再來看看 webapi:

C:\inetpub\wwwroot>dir
 Volume in drive C has no label.
 Volume Serial Number is AC75-48E4

 Directory of C:\inetpub\wwwroot

05/24/2017  01:12 AM    <DIR>          .
05/24/2017  01:12 AM    <DIR>          ..
05/24/2017  01:12 AM    <SYMLINKD>     App_Data [\\?\ContainerMappedDirectories\8287B602-8FB3-460B-9405-BC1D770E9D5B]
05/24/2017  12:35 AM    <DIR>          bin
05/24/2017  01:12 AM               125 dockerfile
05/21/2017  03:59 AM               106 Global.asax
05/21/2017  02:18 PM               916 packages.config
05/24/2017  12:35 AM             1,633 web.config
               4 File(s)          2,780 bytes
               4 Dir(s)  21,123,481,600 bytes free

C:\inetpub\wwwroot>dir App_Data
 Volume in drive C has no label.
 Volume Serial Number is AC75-48E4

 Directory of C:\inetpub\wwwroot\App_Data

05/24/2017  01:25 AM    <DIR>          .
05/24/2017  01:25 AM    <DIR>          ..
05/24/2017  01:22 AM        12,449,488 ipdb.bak
05/24/2017  01:25 AM        12,449,488 ipdb.csv
               2 File(s)     24,898,976 bytes
               2 Dir(s)  1,479,074,295,808 bytes free

C:\inetpub\wwwroot>

看到了嗎? 跟前面不同的是,簡單的透過 volume 將 host 的 d:\data 目錄,分別掛上兩個 container 的 data folder,就完成 在兩個 container 之間共享目錄的功能了。這遠比正常的 server 要開分享目錄,再用網路磁碟,或是 iSCSI 等等機制掛上網路磁碟機, 然後還要再去搞定存取網路磁碟機的安全問題跟帳號設定… ,container 的做法簡單有效的多了。每次我搞分享目錄到這些服務上,都會 被帳號問題跟 ACL 搞得很毛… 現在這些問題都被容器技術解決了。

Container Driven Development 對開發人員最大的效益 (我自己觀點),就是: 你會發現你幾乎不需要浪費力氣,寫不需要的 code, 因為 容器化的技術都極大化的替你解決這些瑣事了。在過去正式上線的系統遠遠不能這樣搞,有一堆其他的東西要寫,例如要寫成 windows service 就有上百行的 code 要先填好, 其他如安裝程式,或是要在 server 上設定排程等等,現在通通不用了。

容器化的部署,現在看到效益了嗎? 這還只是在開發機器上面自己測試而已。你會發現容器技術的精神在於,把所有的 application 都包成一樣的 格式,用一樣的方式啟動執行。因此開發人員只要想辦法把 image 準備好,剩下的部署任務就交給 IT 人員。部署人員只要按照架構圖,把容器 按照規劃的方式啟動 (掛上 network port, 掛上 volume, 指定 environment variables) 就可以了,可大可小。最小如現在在開發 PC 上面測試功能,最大可以用同樣方法部署到 docker swarm cluster 上,將 WebAPI 多開幾個 instance 做效能擴充 (scale out), 應付 大量的使用者。這些極端的使用狀況,使用的通通都是一樣的 code, 一樣的 image !

小結

看到這邊就很 high 要馬上動手寫 code 了嗎? 別急… 後面還有! 請期待續集 :D

下一篇要說明 SDK 的處理,Reverse Proxy 的設置,以及 docker compose 的應用。

img

/IP2CTest 從 Darkthread GitHub 上提供的 直接 fork 過來的 project, IP2C.NET 的單元測試

這整個 solution 大致上就包含這些內容。對我寫的 Demo Code 有興趣的可以直接到 GitHub 參考我的 。覺得不錯的就給個星星吧 :D

Dockerfile 是 Docker Engine 替你建置專屬的 container image 參考的藍圖。只要你的程式內容有修正,透過這個 dockerfile 就能在短短幾秒內替你重新建立一份新的 container image。這流暢的建置過程,搭配公用的 registry () 服務, 是 docker 能夠成功推廣 immutable server 概念的主要原因。這 dockerfile 裡面定義了幾件事:

關於 winnat + windows container 的問題,我在這篇 “” 有說明,需要的可以參考!

更新的程序要確保檔案的正確性,別讓服務掛掉; 下載後需要解壓縮 (提供 .gz 下載),同時用 IP2C.NET 嘗試查詢幾筆 IP address 當作單元測試,通過之後備份原本的檔案,再替換上新的檔案。

至於 logs ? 這就更容易了。過去我們都會用 或是 Apache 這類 logging framework, 來簡化你的 log 程序。現在更無腦了, 你甚麼都不用管,只要用最古早的 printf 大法… 啊,不對,Console.WriteLine(…) 的方式輸出訊息到 stdout 就可以了。一樣,以後 IT人員想要調閱這些 logs 怎麼辦? 用 docker logs 這指令就可以了。

img
Source Code
Source Code
hub.docker.com
掃雷回憶錄 - Windows Container Network & Docker Compose
官方
NLog
Log4Net
容器化的微服務開發 #1