今天開始進入使用 Gitlab pipeline deploy 的部份,我們要把「手動 build docker image 並 push 上 ECR repository」這部份自動化、交給 Gitlab pipeline 做。不過使用 Gitlab pipeline 前,要先準備一台機器好安裝 Gitlab Runner。

需要一台機器?我們不是準備用 AWS 嗎?到 AWS EC2 開台機器吧!EC2 是個可以讓我們啟動機器的服務。機器需要有個網路環境才能啟動,所以我們今天先設置好它需要的網路環境,也就是 VPC 與 Subnet。(有種要做的事的 stack 一直被 push 的 fu 絕對不是錯覺!)

設置 AWS 費用通知

開始前先保護一下自己的荷包~(這很重要!)

前兩天提到除非還有免費使用期並且使用 free tier 的 resource,不然使用 AWS 服務會被收費、會被收費、會被收費(很重要說三次),要注意自己的資源使用狀況與費用(不然荷包會哭哭)。AWS 有個功能可以在花費達到設定的金額時寄 email 通知,可以用這個功能來注意費用有沒有超過太多。

這個功能是 Billing 裡 Cost Management 的 Budgets,用 root email 登入可以進到 Billing,從左邊選單進到 Budgets:

image.png

點選右上角 Create budget。

Read more »

2024 寫在最前面:這是去年(2023)IT 鐵人賽的文章,來個舊文重貼~


開賽啦~開賽啦~這 30 天會帶大家從地面飛上雲端!呃不是,是從本機建立 Laravel 的 Docker image 開始,一步步透過 Gitlab Pipeline 建立將 image deploy 上 AWS 的 CD 流程,再利用 AWS 的 container 服務 ECS 來運行 Laravel 的 web application。

另外,作為工程師,懶是最大的美德,能電腦做的事情就不要自己做,能用程式跑的東西就不要自己手動按,所以我們也會使用 infrastructure as code 的工具 Terraform 來維護以及持續進化各項基礎建設。(重點是雲裡那麼多複雜的設定,沒有程式碼筆者根本記不起來)

Read more »

動機

消除 duplicated member。

要了解 member 被使用的方式,如果被使用的方式類似,就可以移到 super class。除了移除 duplicated member 之外,也可以把 subclass 使用 member 的行為移到 supoer class(如果使用方式相似)。

作法

  1. 檢查候選 member 的所有使用方,確定它們用同樣的方式使用 member
  2. 如果 member 名稱彼此不同,使用 Rename Field 讓它們的名稱一致
  3. 在 super class 建立一個新 member
    這個新 member 要可以被 subclass 使用(一般語言通常是 protected)
  4. 刪除 subclass 的 member
  5. 測試

PhpStorm 操作

用 Docker Compose 啟動程式所需的 service 並且以 docker compose 的 container 執行 test。

Docker

先在 project 裡增加 Dockerfile

1
2
3
4
5
6
7
8
9
10
FROM php:8.0-cli

RUN apt-get update && \
apt-get install -y \
libpq-dev \
libzip-dev \
libpng-dev \
libjpeg62-turbo-dev && \
docker-php-ext-configure gd --with-jpeg && \
docker-php-ext-install pdo pdo_pgsql gd

這邊會安裝 libzip-devlibpng-devlibjpeg62-turbo-dev 並設定與安裝 php extension gd 是為了 image upload 的 test。

build 出 image 並且上 tag:

1
2
$ docker build .
$ docker tag <image_id> cjwind/phpstorm-php8.0-cli

Docker Compose

增加 docker-compose.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
version: "3"
services:
php:
image: cjwind/phpstorm-php8.0-cli:latest # docker image
deploy:
replicas: 1
volumes:
- ".:/opt" # mapping . in host to /opt in container
database:
image: postgres:12.3
container_name: "postgres"
deploy:
replicas: 1
environment:
- POSTGRES_PASSWORD=<your_password>

.env.testing

1
2
3
4
5
6
DB_CONNECTION=pgsql
DB_HOST=postgres # container name of database
DB_PORT=5432
DB_DATABASE=DB
DB_USERNAME=USERNAME
DB_PASSWORD=PASSWORD

PhpStorm Setting

Docker

因為 OS 是 Linux 所以選 Unix socket。

Troubleshooting

如果 connection 連不上,先檢查有沒有啟動 Docker daemon,有的話看看 user 是不是在 docker 這個 group 裡,沒有在 group 裡就把 user 加進 docker group、logout、login、重新啟動 PhpStorm 再試一次。

CLI Interpreter

按左上角的 + 選 From Docker

使用 Docker-compose 並且設定 config file 以及 service。設完可以看到第一張圖的樣子。

CLI Interpreter 選擇剛剛新增的 interpreter。

Run/Debug Configurations

service

按左上角的 + 選 Docker-compose

設定 Server 以及 Compose file path。

Run service

可以在 service window 看到跑起來的 container。

點 container 可以看到 log 啊 properties 等等資訊。

test

設定 test,使用 default interpreter。

Run test 可以看到是用 docker compose 執行。

About Docker Network

這裡只記錄一點點 docker network 有關的東西。

在 default 下,docker compose 裡的 container 的 network 是 bridge mode。

可以用 docker network ls 看到現在有哪些 network。

再用 docker network inspect NETWORK_NAME 可以看到該 network 的詳細資料,在其中 Containers 可以看到 container 被分配的 IP address。

Ref:https://ithelp.ithome.com.tw/articles/10206725

來了解一下 C 語言~

Generate raw machine code

讓我們從一個簡單的 C 程式開始,看看會轉成什麼樣的組合語言:

1
2
3
int foo() {
return 0xbaba;
}

用以下指令 compile:

1
$ i686-linux-gnu-gcc -fno-pie -ffreestanding -c basic.c -o basic.o

這會產生 object file basic.o。比起直接 compile 出可以執行的 machine code,compiler 會輸出的是帶有 meta 資訊的 machine code,meta 資訊保留了這些 code 最後會如何連接在一起的資訊。這麼做的好處是這段程式碼可以被 relocate 到其他 binary file 裡,因為在 object file 裡的 address 都是相對位置而非絕對位置。

freestanding 是沒有 standard library 存在而且程式的 entry 也不需要是 main() 的環境。因為是開發 kernel,所以當然沒有 standard library,也要自己想辦法跳到 kernel 的 main() 開始執行。-ffreestanding 是要讓 compiler 產生在 freestanding 環境的 code。Ref

至於 -fno-pie 則是讓 compiler 不要產生 position independent executables 的 code。PIE 是什麼可以參考這篇。在我們要寫 kernel 的環境下,不需要 PIE 的功能。另一方面,啟動 PIE 就要有 Global Offset Table(GOT)才能 link,我們當然沒做 GOT 啊~

我們可以用以下指令來看看 object file 的內容:

1
2
3
4
5
6
7
8
9
10
11
12
13
$ i686-linux-gnu-objdump -d basic.o

basic.o: file format elf32-i386


Disassembly of section .text:

00000000 <foo>:
0: 55 push %ebp
1: 89 e5 mov %esp,%ebp
3: b8 ba ba 00 00 mov $0xbaba,%eax
8: 5d pop %ebp
9: c3 ret

要產生出真正可以執行的 code,我們需要 linker 來把所有 object file link 起來。link 過程中會把相對 address 轉換成絕對 address。例如 call <function_foo_label> 會被轉換成 call 0x123450x12345 是 label function_foo_label 最後在檔案中的 offset。

這裡只有一個檔案,我們只需要用以下 command 來產生 raw machine code:

1
$ i686-linux-gnu-ld -o basic.bin -Ttext 0x0 --oformat binary basic.o

跟 compiler 一樣,linker 可以 output 出很多種格式的檔案。其中一種是帶有 meta 資訊的可執行檔,這種檔案可以在 OS 上執行,這些 meta 資訊會描述這些 code 要如何被 load 到 memory 中,也可能帶有方便 debug 的資訊。

那因為我們是要寫 OS,所以不要這些 meta 資訊,免得 CPU 以為這些是可以執行的指令。所以在上面的指令裡,我們要指定 output format 是 raw binary

至於 -Ttext 0x0,它的效果跟前面組語裡寫的 org 是一樣的,表示程式會被 load 到的 memory 位置(也可以說是在 memory 中的整體 offset)。

這樣我們就得到前面 function 的 raw machine code 了!來反組譯看看它的內容:

1
2
3
4
5
6
7
8
$ ndisasm -b 32 basic.bin

00000000 55 push ebp
00000001 89E5 mov ebp,esp
00000003 B8BABA0000 mov eax,0xbaba
00000008 5D pop ebp
00000009 C3 ret
...

可以看到進入 function 時會先 push ebp 到 stack 裡保存,等到 function 要 return 前恢復,這樣回到 caller 才不會搞亂 caller 的 register 內容。

接著將 esp 的值 assign 給 ebp,也就是將 stack base pointer 設定成目前 stack top 的位置,這麼做就是在原本的 stack 上再做出一個 stack 給目前這個 function 使用,這個動作是在為目前的 function 設定它的 stack frame。stack frame 裡會儲存 function 的 local variable。

setup 好 stack frame 後,0xbaba 被 assign 給 register eax,這是因為 register eax 是 C 語言用來放 return value 的 register,caller 會預期 callee 的 return value 會放在 eax

Local Variables

接下來我們寫這樣一個 function:

1
2
3
4
int foo() {
int x = 0xbaba;
return x;
}

將它 compile 成 raw binary 再反組譯,得到:

1
2
3
4
5
6
7
00000000  55                push ebp
00000001 89E5 mov ebp,esp ; setup stack frame, stack bottom = stack top
00000003 83EC10 sub esp,byte +0x10 ; allocate 16 bytes on the top of stack
00000006 C745FCBABA0000 mov dword [ebp-0x4],0xbaba ; 將資料存到 stack 上
0000000D 8B45FC mov eax,[ebp-0x4] ; 將 local variable 的值放進 eax 作為 return value
00000010 C9 leave ; restore stack for caller
00000011 C3 ret

一開始先在 stack 保存 ebp,接著 setup stack frame,再來由 sub esp,byte +0x10 在 stack top 保留 16 byte 的空間。但我們明明是要儲存 4 byte 的 int,為什麼需要保留 16 byte 呢?這是因為 CPU 在 memory 的 data 有對齊(alignment)的時候,會有比較好的效能,這是 CPU 最佳化的一個方式。所以雖然我們只要存 4 byte 的資料,但為了讓資料能對齊,會採用最大的 datatype width 16 byte 來放每個 stack 中的 element。dword 表示 double word,也就是 4 bytes。

最後的 leave 是用來恢復 stack 的,等同以下:

1
2
mov esp, ebp		; restore stack top
pop ebp ; restore stack bottom

Calling Functions

再來讓我們看看 C 語言在 call function 時會做些什麼事:

1
2
3
4
5
6
7
int callee(int arg) {
return arg;
}

void caller() {
callee(0xdede);
}

compile 後再反組譯:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
; callee()
00000000 55 push ebp
00000001 89E5 mov ebp,esp ; setup stack frame
00000003 8B4508 mov eax,[ebp+0x8] ; 往 stack 底部多 8 byte 取得參數資料
00000006 5D pop ebp
00000007 C3 ret

; caller()
00000008 55 push ebp
00000009 89E5 mov ebp,esp ; setup stack frame
0000000B 68DEDE0000 push dword 0xdede ; push data into stack
00000010 E8EBFFFFFF call 0x0 ; call callee()
00000015 83C404 add esp,byte +0x4
00000018 90 nop
00000019 C9 leave
0000001A C3 ret

callee()mov eax, [ebp+0x8] 取得 caller 傳給它的參數。因為 stack 在 memory 中是往 address 小的位置長的,所以加上 8 byte 實際上是往「stack 的底部再往下 8 byte」的位置,也就是 caller push 參數的位置。

C 語言預設的 calling convention 是將 argument 以相反的順序 push 到 stack,所以第一個 argument 會在 stack top。

Pointers

在解讀一個 memory 位置上的資料時,除了 memory address 之外,還需要知道這個資料有多長,是 1 byte?2 byte?還是 4 byte?否則會解讀錯誤。

C 語言的 pointer,在 32 bit 下,其值的長度都是 32 bit。而它指到的 memory address 內的資料的長度則可以從 pointer 的 type 知道。例如 *int 就是個指向 4 byte 資料的 pointer。

C 語言裡我們會用 *char 來表示 string,這是因為 string 是一個 char 的 array,我們不知道它確切上有多長。