Google Code Prettify

2022年12月20日 星期二

Azure DevOps: CI Tutorial

  • 登入 Azure DevOps

Azure DevOps 的網址是 https://dev.azure.com,注意網頁有兩個【Sign in】,右上角會登入到 Azure portal,要使用左下角的【Sign in to Azure DevOps】才能正確登入到 Azure DevOps。

  • 建立組織

在 Azure DevOps 裡,最上層的結構是 Organization (組織),在 Ogranization 下會有許多 Project (專案)。所以,使用 Azure DevOps,第一個步驟就是【New organization】。

按了上圖的【New ogranization】會出現如下的彈出畫面,只要輸入組織名稱,並選擇這個組織下的 project 要放在那裡的機房,然後按【Continue】。


  • 建立專案
如下,可以看到左邊選單出現上面建立的 Organization。這時候可以為這個組織建立第一個 project。
如下圖,輸入 project 名稱後按【Create project】。

  • 專案版面簡介

建立 project 後會出現如下畫面,右上角的【Invite】按下去,可以邀請專案成員,左邊選單的 Repos,是 git repo,用來版控程式碼,左邊選單的 Pipeline,可用來建立 CI 流程。

  • 程式碼版控 - 加入公鑰

點選左邊選單的「Repos」,選擇通訊協定為 SSH,然後按【Manage SSH Keys】。
出現如下畫面後按【New Key】。
popup 出如下畫面,加一把公鑰,這裡為公鑰命名為 demo-key,按【Add】。

  • 匯入程式碼

我程式早寫好,所以偷懶的按上面紅色的複製,並且在本機一個空目錄 git clone 一份,把 .git 版控設定建立好,然後把我的程式碼搬進該目錄後,commit、push 到 Azure DevOps 的 git repos,push 進去後,可以看到如下的結果。

  • 建立 CI pipeline

如上,點選左邊選單的【Pipelines】,再點選中間的【Create Pipeline】,可以建立 CI pipeline。當出現如下畫面,選擇「Azure Repos Git」,因為在上一個步驟我們是把程式碼版控在 Azure Repos Git。
如下,「spring-security-authentication」是前面建立的專案,裡面已經有程式碼,點選進入下一步驟。
剛剛匯入的程式,根目錄有一個 build.gradle,是以 Gradle 進行 artifacts 管控的專案,所以,這裡就選擇用 Gradle,這樣 pipeline 會在 build 的過程採用 Gradle。

底下是 azure-pipelines.yml 的內容,這當然是可以依需要修改,我把 jdk 版本由 1.8 改成 17,然後按【Save and run】。

當彈出如下畫面時,commit message 就隨意填一些讓自己看的懂的訊息,然後按【Save and run】。
為什麼會出現下面的錯誤訊息呢? 因為這個 organization 沒有綁定到某個 Azure 的訂閱帳號!

  • 組織綁定 Billing


在組織層級,左邊選單選「Billing」出現右邊的畫面,按【Set up billing】。出現如下畫面後按【Save】。

上面的【Save】按了之後會出現如下畫面,紅框的值預設是 0,必須給它們一點錢才能 run,這裡先設定為 20 美元,然後按最下面的【Save】。

  • 測試 CI pipeline


回到專案的 Pipelines 畫面,可以看到唯一的一條 pipeline 及最後一次執行結果,選擇這個 pipeline,會進入下面的畫面,如果要看 pipeline 的資訊,可以先點選 pipeline 後再按【Run】,這裡直接按右上的【Run pipeline】即可。

出現如下畫面,按【Run】。

執行 CI 的過程會如下,Jobs 的 Status 會一直變,如果想看執行細節,可以按紅色。

因為前面我們選擇這是個 Gradle 專案,CI 就會用 Gradle 執行,包含所有單元測試,測試沒問題會包成 jar 檔。

Job 執行完,Status 顯示 Success,表示所有單元測試都通過,也成功包成 jar 檔。如果這時想看過程的細節,點前面的那個綠色勾勾。

2022年12月2日 星期五

Azure: Network Security Group

  • NSG (Network Security Group, 網路安全性群組) 是一種網路流量篩選器,可以和 VM 的虛擬網卡或 VNet 的子網路結合。

  • NSG 中包含防火牆的對內、對外規則,修改規則只會影響形成的新連線。 建立新規則或在網路安全性群組中更新現有規則時,它只會套用至新的流程和新連線。 現有的工作流程連線不會隨著新的規則更新。

  • 建立 VM 時,Azure 預設會建立一個相對應的 NSG,如果要改變 VM 對防火牆等網路設定,要修改 NSG,不能用傳統上管理網路的指令直接修改。如下圖,當建立一個命名為 VM4 的 VM 時,系統自動建立了一個 NSG 掛在該 VM 上,如果要修改 VM4 的網路設定,要修改 VM4-nsg 這個 NSG 裡的設定,千萬不要在 VM4 裡下傳統網路管理指令。


    • 說明上圖的幾個項目:

    1. Inbound Security Rules 是由外向內連線的規則,Outbound Security Rules 是由內向外連線的規則。

    2. 每個連線要通過防火牆,會比對上面的條件,由 Priority 小的開始比對,符合了就可以通行,不再往下比對。

    3. Name 可以隨便取,可讀性高就好,但是如上圖,有預先設定好幾個了,如下:

       規則說明
      1AllowVNetInBound
      允許來自同一 VNet 或對等互連 VNet 的對內流量
      2AllowAzureLoadBalancerInBound
      允許來自任何 Azure 負載平衡器 IP 位址的對內檢測流量
      3DenyAllInbound
      特殊規則,確保任何前面的規則未能比對出來的對內流量都會被擋下。
      4AllowVNetOutBound
      允許源於此一 VNet 或前往對等互連 VNet 的對外流量
      5AllowInternetOutBound
      允許此一 VNet 內的 VM 前往 Internet
      6DenyAllOutBound
      特殊規則,確保任何前面的規則未能比對出的對外流量都會被擋下。
      預設規則無法移除,但可以建立較高優先順序的規則來覆寫預設規則。
    4. 在 Source 和 Destination 欄位,有如下標籤方便設定 NSG 規則:
    5. Service Tag說明
      1VirtualNetwork
      所有的虛擬網路定址空間皆包含在內,包含本地及對等互連的 VNet。
      2Internet
      所有公共 IP 位址,不論是 Azure 或外界持有皆然。
      3AzureLoadBalancer
      你所部署的 Azure 負載平衡器的虛擬 IP 位址
      4AzureCloud
      Azure 的公共 IP 位址空間
      5Storage
      Azure 儲存體服務的 IP 位址空間
  • 如下圖,VM 3、VM 4 在同一個 subnet,如果它們的網路設定一樣,想要統一管理,可以把它們預設綁在 VM 上的 NSG 移除,並設定一個新的 NSG 綁在 subnet 3 上。(在 example 中會說明怎麼做)

example 
  1. create NSG
    進入如上畫面,按【Create】後,進入如下畫面,填入 NSG 名稱。
    按【Review + create】後進入下面畫面,再按【Create】即成功建立一個 NSG。

  2. 設定 NSG
    如上,剛剛才建立的 subnet3-nsg 已經有預設建了一些 Inbound security rules 和 Outbound security rules,要很嚴謹的話,通常會把這些都刪除,只加上自己要的規則。
    因為我們希望可以 ssh 到 VM 3、VM 4,如下圖在「Inbound security rules」中按【Add】。
    popup 出如下畫面後,在「Service」下拉選單選擇「SSH」。
    按下【Add】後,會出現如下畫面,已經加入新的規則,因為 Priority 100,所以排最上面。

  3. 掛載 subnet-nsg 到 subnet

    如上圖,在「Virtual networks」中找到 VNet2,選擇「Subnets」,裡面只有一個 subnet3,點選後出現 subnet3 的規則選項,在「Network security group」下拉選單中選擇「subnet3-nsg」後按【Save】。
  4. 修改 VM 4 的 Networking

    查看 VM 4 的網路設定,原本的 VM4-nsg 及剛設定的 subnet3-nsg 同時存在,從 subnet3 外連線進去或由 VM 4 連出 subnet3 會同時受這兩個規則限制,與我們想要的不同,要移除 VM4-nsg。
    點選上面紅框中的 vm491-z1 會進到下圖,目前的 NSG 是 VM4-nsg,要將它卸載,下拉選單改選為 None 後按【Save】。
    再重新觀察 VM4 下的 Netowkring,如下圖,只剩下 subnet3-nsg。
    VM4-nsg 現在不想用了,可以刪除,如下圖按【Delete】。

2022年11月1日 星期二

Azure: Virtual Network

  • Azure 的 Virtual Network (虛擬網路, VNet) 彼此間是隔離的,預設狀況下無法相互通訊,不同 VNet 間要通訊,需要架設 vNet peering 或 VPN,VNet 內可用的 IP 區段可以參考 IPv4 內部 IP
  • 一個 VNet 只能位於一個 Azure Region,不能跨 Region。
  • 每個 VNet 裡可以切割 subnet (子網段),每個子網預設會保留 5 個 IP,以 192.168.30.0/24 為例,Azure 保留的除了第1個 IP - 192.168.30.0 這是網段識別碼及 192.168.30.255 是網段的廣播識別碼,這是 Internet 的基本規定,Azure 另外保留了 3 個給 Azure 的服務用,分別為 192.168.30.1、192.168.30.2、192.168.30.3。
  • 子網段最小為 /29,僅 3 個 IP 可以用,最大為 /8,有 2^24 - 5 個 IP 可以用,計算的方式是以 32 減該數字,/29 就只有 2^3 - 5 = 3 個 IP。
  • 單一 region 最多可定義 1000 個 VNet,每個 VNet 最多可以有 3000 個 subnet。
  • 同一 VNet 裡的 subnet 間可以互相通訊。

example

  • create VNet


為了驗證上面的基本觀念,這裡會建立兩個 VNet,如上圖所示,VNet1 會有兩個 subnet,VNet2 只會有一個,三個 VM 都用 Linux,方面透過 ssh 登入到另一台 VM,以驗證 VM 間是否預設可通訊? 同一個 VM 裡的不同 subnet 是否預設可通訊?
在「Virutal networks」中按【Create】建立 Virutal Network,會出現如下畫面,因為台灣沒有 Azure 機房,通常會選日本的機房,這裡選 Japan East (關東) 機房

上圖按【Next:IP Address >】後出現如下畫面,依上面的網路圖填入 VNet1 的網段,並加入兩個 subnet (按【Add subnet】)。
按上圖的【Next:Security >】,並選擇「Tags」,這只是為了方便管理,把 VNet1、VNet2 及相關的 VM 都加入同樣的 tag,搜尋比較方便,然後按【Review + create】。

當出現如下畫面,表示剛的設定 Azure 初步檢查過沒問題,接著按【Create】即會產生 VNet1 及其內的兩個 subnet。用相同的辦法產生 VNet2 及其內的 subnet3。

  • create VM

接下來要建立如上架構圖的三個 VM,並套用已建立好的三個 subnet,以下以建立 VM3 為例。

我已經建立好 VM1、VM2,方法和 VM3 一樣,首先按【Create】,出現如下的下拉選單,選「Azure virtual machine」

出現如下的畫面,並面有許多 tab,也就是有許多類別的選項要設定,在 Basics 頁面就相當長,底下分三部份看。如下紅框,填入 VM 名稱,並選擇 region,要注意不要選中國、香港,這些地方的資料有可能會被當地國政府要求讀取。

底下比較沒有什麼需要特別說明,依自己的需求選擇機器型號。

因為是 Linux,用 SSH 登入,可以用帳密,只是比較麻煩,這裡選擇用「SSH publick key」,因為我已經在建立 VM1、VM2 時有產生好 key pair,在「SSH public key source」中選擇「Use existing public key」,並且在「SSH public key」貼入我之前產生的 public key,當然,VM 的防火牆要開放 22 port。

上面都選完後按【Next: Disks >】,「OS disk type」選擇 HHD 比較便宜,預設是 SSD。

按【Next: Networking > 】後,出現如下畫面,依架構圖的規劃選擇相關的網路設定,並開放 public IP 的 22 port,這樣才能從自己的電腦 ssh 進 VM。

到這都選擇好,按【Review + create】建立 VM,會出現如下的 preivew 結果,再按【Create】就會建立 VM了。

如下圖,共建立了三個 VM,最重要的是紅框的「Public IP address」。

  • 測試

用 putty 金鑰連線進上面三個 VM …

連入後要觀察 VM 裡的 IP,登入後先執行如下指令,安裝 ifconfig 等網路指令。

sudo apt install net-tools

接著在 VM1 執行ifconfig, 再看一下前面的架構圖,VM1 的 subnet 1 是 10.20.2.0/24,正如基本觀念裡說的,10.20.2.0 是網段識別碼,10.20.2.1、10.20.2.2、10.20.2.3 前三個 IP 是保留給 Azure 使用,所以分配到下一個 IP 10.20.2.4。

經過 ifconfig 查詢,三個 VM 的 IP 如下:
  • VM1: 10.20.2.4

  • VM2: 10.20.1.4

  • VM3: 192.168.30.12

接著在 VM1 裡 ping 另兩個 IP,會發現 VM2 有回應,VM3 沒有回應,表示 VM1 只和 VM2 網路有連通。在 VM2、VM 3 也用 ping 試看看,可以了解基本觀念裡所說的,同一個 VNet 網路互通,不同 VNet 網路預設不通。


2022年7月23日 星期六

Lambda / Stream / Function

項目日期
*
  Lambda / Stream / Function
1Java Lambda: getting started
2020/11/01
2Java Stream: getting started
2020/10/03
3Java Stream: Creation
2020/10/04
4Java Stream: operation
2020/11/02
5Java Stream: Collections
2020/11/07
6Consumer、Supplier、Predicate、Function
2022/07/23

Consumer、Supplier、Predicate、Function

 Java 要逐漸導入 Functional Programming,多了 Lambda、Stream 及現在要說明的 java.util.function 這個 package 下的四個 interface。

這四個 interface 都標注有 @FunctionalInterface,這表示這些 interface 只能剛剛好有一個未實作的 method! 要特別注意,Java 8 開始,interface 的 method 是可以實作非僅有宣告。

  1. Consumer
  2. @FunctionalInterface
    public interface Consumer<T> {
        
        void accept(T t);
    
        default Consumer<T> andThen(Consumer<? super T> after) {
            Objects.requireNonNull(after);
            return (T t) -> { accept(t); after.accept(t); };
        }
    }
    上面是 Consumer 本身的程式碼,這個 interface 有兩個 method,僅有一個未實作的 method - accept。從名稱猜測,可以知道這是個消費者,即實作 Consumer interface 的類別要透過 accept 吃進資料並進行處理。至於 andThen 這個 method 並沒有強制要用,當有使用時,會在 accept 執行完後接著執行。
    @DisplayName("ConsumerTest")
    @SpringBootTest
    @Slf4j
    class ConsumerTest {
    
        @Test
        void accept() {
            Consumer<Integer> display = a -> log.debug(a.toString());
     
            display.accept(10);
     
            Consumer<List<Integer> > modify = list ->
            {
                for (int i = 0; i < list.size(); i++)
                    list.set(i, 2 * list.get(i));
            };
     
            Consumer<List<Integer> >
                dispList = list -> list.stream().forEach(a -> log.debug(a + " "));
     
            List<Integer> list = new ArrayList<Integer>();
            list.add(2);
            list.add(1);
            list.add(3);
     
            modify.accept(list);
     
            dispList.accept(list);
        }
    
        @Test
        void andThen() {
            Consumer<List<Integer> > modify = list ->
            {
                for (int i = 0; i < list.size(); i++)
                    list.set(i, 2 * list.get(i));
            };
     
            Consumer<List<Integer> >
                dispList = list -> list.stream().forEach(a -> log.debug(a + " "));
     
            List<Integer> list = new ArrayList<Integer>();
            list.add(2);
            list.add(1);
            list.add(3);
     
            modify.andThen(dispList).accept(list);
        }
    }
    
    
    上面單元測試程式,執行後第一個 method (accept) 會輸出 10 4 2 6,第二個 method (andThen) 會輸出 4 2 6,程式說明如下:
    (1) 第一個單元測試 - accept()
    • 第 8 行: 實做一個命名為 display 的 Consumer 實做類別,實做出 accept() method,很簡單的將收到的值輸出。
    • 第 10 行: 將 10 傳入給 display 這個 Consumer 實做類別,程式會輸出 10。
    • 第 12~16 行: 實做一個命名為 modify 的 Consumer 實做類別,這個類別會接收一個 List<Integer>,並將 List 的每個值乘以 2。
    • 第 18 行: 實做一個命名為 dispList 實做類別,這個類別會接收一個 List<Integer>,並將 List 裡所有值輸出。
    • 第 21~24 行: 產生一個 3 個元素的 List,值分別為2、1、3。
    • 第 26 行: modify 將傳入的 List 裡的元素都乘以 2。
    • 第 28 行: dispList 將傳入的 List 裡的元素依序輸出為 4 2 6。
    (2) 第二個單元測試 - andThen()
    • 第 33~37 行: 實做一個命名為 modify 的 Consumer 實做類別,這個類別會接收一個 List<Integer>,並將 List 的每個值乘以 2。
    • 第 39 行: 實做一個命名為 dispList 實做類別,這個類別會接收一個 List<Integer>,並將 List 裡所有值輸出。
    • 第 42~45 行: 產生一個 3 個元素的 List,值分別為2、1、3。
    • 第 47 行: 會先執行 accept 將值乘 2,接著執行 andThen 將值輸出。

  3. Supplier
  4. @FunctionalInterface
    public interface Supplier<T> {
        T get();
    }
    

    Supplier 是生產者,上面是 Supplier 的程式碼,只有一個 method - get(),讓外界可以透過這個 method 取得 Supplier 實做類別裡的值。
    @DisplayName("SupplierTest")
    @SpringBootTest
    @Slf4j
    class SupplierTest {
    
        @Test
        void random() {
            Supplier<Double> randomValue = Math::random;
      
            log.debug(randomValue.get().toString());
        }
    }
    
    上面單元測試程式執行後,輸出 0.4077113851887808,這是個隨機值,程式說明如下:
    • 第 8 行: 實做一個命名為 rendomValue 的實做類別,覆寫 get() method 為 Math:: random。
    • 第 10 行: 從 randomValue 中取出值並輸出。

  5. Predicate
  6. @FunctionalInterface
    public interface Predicate<T> {
    
        boolean test(T t);
    
        default Predicate<T> and(Predicate<? super T> other) {
            Objects.requireNonNull(other);
            return (t) -> test(t) && other.test(t);
        }
    
        default Predicate<T> negate() {
            return (t) -> !test(t);
        }
    
        default Predicate<T> or(Predicate<? super T> other) {
            Objects.requireNonNull(other);
            return (t) -> test(t) || other.test(t);
        }
    
        static <T> Predicate<T> isEqual(Object targetRef) {
            return (null == targetRef)
                    ? Objects::isNull
                    : object -> targetRef.equals(object);
        }
    
        @SuppressWarnings("unchecked")
        static <T> Predicate<T> not(Predicate<? super T> target) {
            Objects.requireNonNull(target);
            return (Predicate<T>)target.negate();
        }
    }
    Predicate 的 test method 是用來測試邏輯運算是 true 或 false,其餘 method 用來和 test 搭配出更複雜的邏輯判斷。
    @DisplayName("PredicateTest")
    @SpringBootTest
    @Slf4j
    class PredicateTest {
    
        @Test
        void greaterThanFive() {
    	Predicate<Integer> predicate = (i) -> i > 5;
    
            Stream<Integer> stream = Stream.of(1, 23, 3, 4, 5, 56, 6, 6);
            List<Integer> list = stream.filter(predicate).collect(Collectors.toList());
            list.forEach(item -> log.debug(Integer.toString(item)));
        }
    
        @Test
        void betweenTenAndTwenty() {
    	Predicate<Integer> greaterThanTen = (i) -> i > 10;
    
            Predicate<Integer> lowerThanTwenty = (i) -> i < 20; 
            boolean result = greaterThanTen.and(lowerThanTwenty).test(15);
            log.debug("result = " + result);
     
            boolean result2 = greaterThanTen.and(lowerThanTwenty).negate().test(15);
            log.debug("result2 = " + result2);
        }
    }
    
    上面單元測試程式,執行後第一個 method (greaterThanFive) 會輸出 23 56 6 6,第二個 method (betweenTenAndTwenty) 會輸出 result = true、result2 = false,程式說明如下:
    • 第 8 行: 實做一個 Predicate 類別,測試傳入值是否大於 5。
    • 第 10 行: 產生一個有 8 個數字的串流。
    • 第 11 行: 利用第 8 行實做的 Predicate 類別過瀘第 10 行的串流資料,過瀘出來的資料放入一個 List。
    • 第 12 行: 輸出 List 裡的值,可以看到只輸出大於 5 的元素。
    • 第 17、19 行: 實做兩個 Predicate 類別,用來篩選大於 10 及小於 20 的值。
    • 第 20 行: 用 greaterThanTen 搭配 Predicate 介面中的 and method 整合 lowerThanTwenty 篩選同時滿足大於 10 及小於 20 兩個條件的值。
    • 第 23 行: 將第 20 行的結果透過 Predicate 介面中提供的 negate method 將值反向,所以原本是 true,反向變 false。 
    上面程式第 8、17、19 宣告的是 Integer 型別,實際傳入的是 int 型別,在 Java 中執行時,會先把 int 轉型成 Integer,在效能上會比較差, 為了避免這個問題,Java 另外提供了 IntPredicate,以第 17 行來說,可以改成如下:
    IntPredicate greaterThanTen = (i) -> i > 10;
    
    這樣避免了轉型,速度會快些。
  7. Function
  8. @FunctionalInterface
    public interface Function<T, R> {
        R apply(T t);
    
        default <V> Function<V, R> compose(Function<? super V, ? extends T> before) {
            Objects.requireNonNull(before);
            return (V v) -> apply(before.apply(v));
        }
    
        default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {
            Objects.requireNonNull(after);
            return (T t) -> after.apply(apply(t));
        }
    
        static <T> Function<T, T> identity() {
            return t -> t;
        }
    }
    
    Function 的 apply method 類似於 Stream 的 map,將 T 經運算後傳出 R。
    @DisplayName("FunctionTest")
    @SpringBootTest
    @Slf4j
    class FunctionTest {
        @Test
        void apply() {
    	Function<Integer, Double> half = a -> a / 2.0;
    	log.debug(half.apply(10).toString());
        }
    }
    
    上面程式執行後傳回 5.0,程式說明如下:
    • 第 7 行: 實傳一個 Function 類別,將傳入值除以 2 傳回。
    • 第 8 行: 傳入 10,傳回 5.0。
    上面程式也有轉型的問題,要避開這個問題,程式可以改成如下:
    IntToDoubleFunction half = a -> a / 2.0;
    log.debug(Double.toString(half.applyAsDouble(10)));
    

2022年7月6日 星期三

jasypt 加解密

 在 application.yml 中如果有資料庫連線設定,可能會如下:

spring:
  datasource:
    url: jdbc:mysql://127.0.0.1:3306/AUTH
    username: root
    password: ENC(wwCYqJ+PMxrCAY0aPs8v91/oykkFVyux)
    driver-class-name: com.mysql.cj.jdbc.Driver
  jpa:
    database-platform: org.hibernate.dialect.MySQLDialect
上面密碼的部份是用 jasypt 加密,通常不會在設定檔直接寫明碼。那麼,怎麼初始化 jasypt 函式庫呢? 
  1. build.gradle
  2. implementation 'com.github.ulisesbocchio:jasypt-spring-boot-starter:3.0.4'  
    
    加入如上 jar 檔。
  3. application.yml
  4. jasypt:
      encryptor:
        password: ${JASYPT_ENCRYPTOR_PASSWORD}
        algorithm: PBEWithMD5AndDES
        iv-generator-classname: org.jasypt.iv.NoIvGenerator
    
    ${JASYPT_ENCRYPTOR_PASSWORD} 會從環境變數取得加解密的 key,要注意最後一行,在 jasypt-spring-boot-starter 2.x 版不需要這個設定,到 3.x 版才需要。
  5. @EnableEncryptableProperties
  6. @EnableEncryptableProperties
    @SpringBootApplication
    public class DemoSecurityApplication {
    
    	public static void main(String[] args) {
    		SpringApplication.run(DemoSecurityApplication.class, args);
    	}
    }
    
    在 @SpringBootApplication 或 @Configuration 所在的 class 上加上 @EnableEncryptableProperties 啟用 jasypt。