iT邦幫忙

第 11 屆 iT 邦幫忙鐵人賽

2

今天要介紹的是使用 WebJobLogic App 製作定時排程器,本來想和爬蟲一起寫,不過爬蟲內容太多,寫完後發現篇幅太長,所以最後決定分成兩篇,這篇先介紹 WebJob 和 Logic App,下一篇在把爬蟲結合進來。

Σ( ° △ °|||)

今天要做什麼?

  • 首先建立 Webjob 專案,並介紹 .NET Core 3.0 的 Generic Host 寫法

  • 接著將專案部屬到 Azure 上,這裡會使用 Pipelines 和網站一同部屬,不會因為加入了 WebJob,就破壞了原來自動化 CI/CD 的部分

  • 最後使用 Logic App 呼叫 WebJob,完成定時排程器功能

什麼是 WebJob?

WebJob 是 App Service 的一個子功能,可以用來執行背景程式,有三種執行方式,手動觸發Http 觸發排程觸發,本來我想直接使用排程功能,不過免費方案的 App Service 幾分鐘無人使用,資源就會被回收,WebJob 也會跟著一起,所以其實只剩 Http 觸發 這個選項可選。

免費方案的 App Service 因為無法啟用 always on 模式,網站幾分鐘無人使用,資源就會被回收

什麼是 Logic App?

Logic App 是 Azure 上的一項雲端服務,可以用來做 排程流程控制自動化系統間的協調,等等...功能非常強大,介面也做得很好,推薦大家使用看看。

不過不是免費的,採用執行次數計費,費用可以參考:
https://azure.microsoft.com/zh-tw/pricing/details/logic-apps/

評估後如果每天只執行一次,費用相當便宜,所以最後決定使用 Logic App 搭配 WebJob,等有空再找其它免費服務替代。

開始之前

這篇內容會接續之前介紹的東西,想了解完整過程的讀者可以先看。
[Day03] 將 Line Bot 部屬到 Azure 上 - 使用 Azure DevOps Pipelines


新增 Webjob 專案

新增專案 iBot.Crawler,類型選擇 主控台應用程式 (.NET Core)

開發 Webjob 用一般的 Console 程式就可以

https://ithelp.ithome.com.tw/upload/images/20191224/20106865n5Vy0VG5UA.jpg

需要安裝的套件 (Nuget):

  • Microsoft.Extensions.Hosting
  • Microsoft.Extensions.DependencyInjection
  • MySql.Data.EntityFrameworkCore

程式使用 .NET Core 3.0 的 Generic Host 寫法,可以參考: .NET 泛型主機

首先在 Program 內,使用 DI 註冊上一篇建立的 CoreDbContext 類別

public class Program
{
    public static void Main(string[] args)
    {
        CreateHostBuilder(args).Build().Run();
    }

    public static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args)
            .ConfigureAppConfiguration((hostContext, config) =>
            {
                //將 appsettings.json 加入 Configuration
                config.AddJsonFile("appsettings.json", optional: true);
            })
            .ConfigureServices((hostContext, services) =>
            {
                //將 CoreDbContext 註冊 DI
                services.AddDbContext<CoreDbContext>(options =>
                {
                    options.UseMySQL(hostContext.Configuration.GetConnectionString("SQLConnectionString"));
                });
                services.AddHostedService<Worker>();
            });
}

新增 Worker.cs 繼承 IHostedService,程式邏輯會寫在這裡。

IHostedService 介面有兩個方法需要實做:

  • StartAsync: 應用程式開始時會執行的方法
  • StopAsync: 應用程式結束時會執行的方法

我們只會用到第一個方法,第二個回傳 CompletedTask 就好。

public class Worker : IHostedService
{
    public Task StartAsync(CancellationToken cancellationToken)
    {
        //程式
    }

    public Task StopAsync(CancellationToken cancellationToken)
    {
        return Task.CompletedTask;
    }
}

好,到這邊設定完成,接下來要開始寫程式了!!

目前專案目錄

https://ithelp.ithome.com.tw/upload/images/20191224/20106865Iq9gQ7h6WJ.jpg

※ 專案內的 appsettings.json 是空的,後面會使用 Pipelines 在部屬時,將 iBotTest 內的 appsettings.json 複製到 iBot.Crawler,這樣才方便管理。


撰寫測試程式

接下來寫一段程式,測試資料庫是否可以正常連線,將取得的結果輸出到 Log 上

Webjob 可以使用 Console.WriteLine() 輸出 Log

public Task StartAsync(CancellationToken cancellationToken)
{
    Task.Run(() =>
    {
        //使用新執行續執行
        ExecuteAsync().ConfigureAwait(false).GetAwaiter().GetResult();
        //結束後關閉視窗
        _lifeTime.StopApplication();
    });
    return Task.CompletedTask;
}

public async Task ExecuteAsync()
{
    var iTHome = await _db.ITHomes.FirstOrDefaultAsync();

    Console.WriteLine($"{iTHome.Name} {iTHome.Url}");
}

public Task StopAsync(CancellationToken cancellationToken)
{
    return Task.CompletedTask;
}

結果的部分要等部屬後才能看。

為什麼要用 Task.Run() 開一個新的執行續?

其實可以不用,不過這麼做是個好習慣,為什麼這麼說呢?

回來看 IHostedService 內的兩個方法:

  • StartAsync 程式啟動時執行
  • StopAsync 程式結束時執行

雖然說 StopAsync 會在程式結束時執行,不過前提是 StartAsync 已執行完畢。

也就是說,假設程式的工作量很大,我們設計了一個取消機制,使用者關閉程式後,會取消未完成的工作。

如果沒有另開執行續,會導致 StartAsync 一直無法結束,後果就是 StopAsync 內取消工作的邏輯永遠不會被執行。

所以比較好的作法是,另外開一個執行續執行我們的程式,讓 StartAsync 可以馬上返回,不過程式內我並沒有設計取消機制,只是借題和大家分享這個概念。

詳情可以參考:
在微服務中使用 IHostedService 和 BackgroundService 類別實作背景工作

下面這段程式的作用是?

.ConfigureAwait(false).GetAwaiter().GetResult();

在 Console 專案下,await 會用新的執行續返回 (假設方法內有新開執行續),而 WPF 或 WinForm 這類的專案,則會用 UI 執行續返回。

設定 ConfigureAwait(false) 則一律不使用 UI 執行續返回,這樣可以避免死結、互相等待,等情況發生,不過因為 Console 沒有 UI 執行續,所以這裡加與不加都可以。

GetAwaiter().GetResult() 用來將非同步方法轉成同步。

想深入研究的讀者可以參考這篇:
.NET 非同步程式小技巧:GetAwaiter().GetResult() 與 Result 的差異


使用 Azure DevOps Pipelines 部屬 Webjob

如果 App Service 部屬有使用 CI/CD,Azure 介面上的新增功能會無法使用,如下圖

本來想偷懶,現在只好認命去研究如何使用 Pipelines 部署了。 Σ( ° △ °|||)

https://ithelp.ithome.com.tw/upload/images/20191224/201068651tXdlJHkaH.jpg

如有設定部署表單原始檔控制,即無法從入口網站新增 WebJob。

※ 這邊會接續第三篇的設定。
[Day03] 將 Line Bot 部屬到 Azure 上 - 使用 Azure DevOps Pipelines

首先開啟 Azure DevOps Pipelines 的 Builds 功能頁。

原來 Builds 的設定如下:

  • Restore: 載入需要的資源
  • Delete files: 刪除專案內空的 appsettings.json
  • Download secure file: 載入正式的 appsettings.json 到暫存空間
  • Copy files: 將暫存區的 appsettings.json 複製到專案目錄下
  • Build: 建置專案
  • Test: 執行相關單元測試
  • Publish: 發行專案
  • Publish Artifact: 將發行後的檔案移動到 Azure Pipelines

https://ithelp.ithome.com.tw/upload/images/20191224/20106865yDz7c6sK6W.jpg


如果使用介面手動新增,會看到 Webjob 是部屬在 App_Data\jobs\Triggered 目錄下。

D:\home\site\wwwroot\App_Data\jobs\Triggered>

https://ithelp.ithome.com.tw/upload/images/20191224/20106865zowMXJA9ys.jpg

知道位置後,思路就清楚了,只要在部屬過程中,想辦法將 Webjob 程式放進指定目錄下,就能同時部署兩者。

1. 新增 Delete files 刪除專案內空白的 appsettings.json

https://ithelp.ithome.com.tw/upload/images/20191224/20106865WMNpgeYzac.jpg

2. 新增 Copy files 將 appsettings.json 複製到專案目錄 (共用設定檔)

https://ithelp.ithome.com.tw/upload/images/20191224/20106865fAWfiN44eo.jpg

3. 將原 Build 項目,改為只建置原專案

**/*.csproj 改成 **/iBotTest.csproj

https://ithelp.ithome.com.tw/upload/images/20191224/201068653DMV8w71Qm.jpg

4. 將原 Publish 項目,的 Zip Published Projects 選項取消

這樣才能在下一步將 Webjob 放進指定目錄。

https://ithelp.ithome.com.tw/upload/images/20191224/20106865yI90BIKdoR.jpg

5. 新增 Publish 項目發行 Webjob 專案,並將結果放進指定目錄

--configuration $(BuildConfiguration) --output "$(build.artifactstagingdirectory)\App_Data\jobs\Triggered\dotnetcore-webjob"

https://ithelp.ithome.com.tw/upload/images/20191224/20106865VuAf5qihW8.jpg

6. 新增 Zip 項目,將發行結果壓縮成 .zip 檔

https://ithelp.ithome.com.tw/upload/images/20191224/20106865Gya45jJ9qx.jpg

整理一下:

紅色為新增或修改部分。

  • Restore: 載入需要的資源
  • Delete files: 刪除專案內空的 appsettings.json
  • Delete files (Webjob): 刪除專案內空的 appsettings.json
  • Download secure file: 載入正式的 appsettings.json 到暫存空間
  • Copy files: 將暫存區的 appsettings.json 複製到專案目錄下
  • Copy files (Webjob): 將暫存區的 appsettings.json 複製到專案目錄下
  • Build: 建置專案
  • Test: 執行相關單元測試
  • Publish: 發行專案
  • Publish (Webjob): 發行專案
  • Zip: 將發行結果壓縮成 .zip 檔
  • Publish Artifact: 將發行後的檔案移動到 Azure Pipelines

https://ithelp.ithome.com.tw/upload/images/20191224/20106865s7i9M4IUhc.jpg

結果

部屬成功後可以看到清單內多了 dotnetcore-webjob

https://ithelp.ithome.com.tw/upload/images/20191224/20106865ZTfrV6PCJn.jpg


※ 補充說明:

為什麼最後需要將發行結果,壓縮成 zip?

開啟 Releases 管理畫面,找到發行設定,可以看到 Pipelines 會去發行資料夾下找 .zip 副檔名的檔案,用這個檔案去部屬。

https://ithelp.ithome.com.tw/upload/images/20191224/20106865T46ZH1L4KT.jpg

如何查看發行結果?

這個功能還蠻方便的,可以在建置結束後查看發行目錄。

https://ithelp.ithome.com.tw/upload/images/20191224/20106865gDFnm2NCLF.jpg

選擇 View contents 可以看到檔案目錄。

https://ithelp.ithome.com.tw/upload/images/20191224/20106865MA5wLhKcRe.jpg


測試 Webjob

部屬成功後測試執行結果如何。

開啟 Webjob 視窗,點選 執行 按紐,成功後可以看到完成狀態。

https://ithelp.ithome.com.tw/upload/images/20191225/20106865fOxIGhxgfj.jpg

點選旁邊的 紀錄,可以看到剛剛 Console.WriteLine 的內容。

https://ithelp.ithome.com.tw/upload/images/20191225/20106865AQz5Fu9Ct6.jpg


使用 Logic App 定時呼叫 Webjob

正如開頭所說,App Service 免費方案無法啟動 Webjob 排程功能,因此我另外找了 Logic App 服務替代。

新增 Logic Apps,被歸類在物聯網分類。

https://ithelp.ithome.com.tw/upload/images/20191224/20106865ldhnie73AJ.jpg

開啟 邏輯應用程式設計工具,新增三個項目。

  1. 週期: 每天凌晨 12:00 執行一次
  2. HTTP: 呼叫 Line Bot 喚醒網站
  3. HTTP: 呼叫 Webjob 程式

本來想直接呼叫 Webjob 省略第二步,可是發現只呼叫 Webjob 不會讓 MySQL 啟動,必需透過主網站才行。

https://ithelp.ithome.com.tw/upload/images/20191224/20106865nPsbRBSE5m.jpg

1. 週期

如果選擇時區,則時間後不加 Z,反之,
例如: 如果我不選擇時區,時間需設為 2019-12-17T00:00:00Z

這裡的 Z 指的是航海時間,詳情可以參考微軟文件:
https://docs.microsoft.com/zh-tw/azure/connectors/connectors-native-recurrence

https://ithelp.ithome.com.tw/upload/images/20191224/20106865g74isqZ2Ju.jpg

2. HTTP

https://ithelp.ithome.com.tw/upload/images/20191224/20106865TZ3ofAl04u.jpg

這裡我特別開了一個 API 用來喚醒網站。

[HttpGet("wakeUp")]
public string WakeUp()
{
    return "I wake up!!";
}

3. HTTP

方法選擇 POST,驗證選擇 基本,接著填入 Webjob 相關連線資訊。

https://ithelp.ithome.com.tw/upload/images/20191224/20106865vUeI5ZfWfa.jpg

連線資訊可以在 Webjob屬性 視窗中找到。

https://ithelp.ithome.com.tw/upload/images/20191225/20106865n6eaRl72ab.jpg

Logic Apps 設定完後記得儲存。


結果

選擇 回合觸發程序,執行成功可以看到綠色勾勾。

https://ithelp.ithome.com.tw/upload/images/20191224/20106865GDqruVtCpS.jpg

「定時排程器」 完成 ~ 。:.゚ヽ(*´∀`)ノ゚.:。


結語

本來已經發文了,後來覺得內容太多,無法聚焦,忍痛將一篇分成兩篇,重寫了一部份,希望對閱讀舒適度有幫助。

幫大家回顧一下這篇的內容:

  • 首先新增 Webjob 專案,並使用 Generic Host 寫法
  • 接著寫了測試連接資料庫的程式
  • 完成後將專案使用 Pipelines 部屬到 Azure 上
  • 測試 Webjob 並了解查看 Log 的方法
  • 使用 Logic App 定時呼叫 Webjob,完成了「定時排程器」

下一篇會完成整個功能,今天就到這裡,感謝大家觀看。

╰ ( ´ ▽ ` ) ╯

參考文章

Dotnet Core 2.1 Webjob (.net core) Azure DevOps CI CD Pipeline
[NETCore] ASP.NET Core 建立排程服務 - 使用 Generic Host 搭配 Quartz.Net - Part 1
Generic Host - 微軟文件
ASP.NET Core 3 系列 - 程式生命週期 (Application Lifetime)
.NET 非同步程式小技巧:GetAwaiter().GetResult() 與 Result 的差異
谈谈.NET Core中基于Generic Host来实现后台任务
Building .NET Core WebJob Console App CI/CD Using Azure DevOps Pipelines


上一篇
[Day07] 使用 EF Core 讀取 Azure 上的 MySQL 資料庫
下一篇
[Day09] LINE Bot 爬蟲實作 - 使用 HttpClient 和 Regex
系列文
Line Bot 心得分享 LineMessagingApi + LUIS + BotFramework27

尚未有邦友留言

立即登入留言