AWS
NodeJs
Github
WebApp
Deploy
最近剛好手上有一個專案要佈建Production版了,有鑑於之前佈署Dev環境的時候沒有仔細記錄,導致那時候雖然拼拼湊湊的把環境架好了,卻還是一知半解,所以這次希望可以好好寫一篇,讓未來的自己能夠有取回記憶的方法。
首先簡單描述一下完整的應用程式包含哪些部分,可以參考下圖
可以看到前端會有兩個網頁,分別是給一般用戶看得Web,用Next js實作,可以支援SSR(Server Side Rendering),與給管理者看的Admin,用一般的React去做靜態網頁,他們都連到同一個API Server,再統一由API Server去跟Database溝通存取資料,有一個額外的功能是Admin要支援可以將依些靜態資源存到雲端上,並從Web那邊可以存取,所以額外添加了一個存貯空間。另外還有一些塗上沒有畫出來的,像是掛網域、SSL憑證…等等。
所以統整起來,要佈署這個應用上AWS,主要會需要以下幾個東西:
以下就來一項一項把他們建立起來吧!
首先建一個虛擬機給我們的API Server,這裡要跑Node 14環境,我們會用到AWS的Elastic Beanstalk,登入到AWS中控台,再搜尋框找EB
,就可以進到Elastic Beanstalk的主控版。
EB其實不是虛擬基本體(?),真正要跑我們NodeJs的是EC2(Elastic Compute Cloud),EB根據官方的說法,他可以自動地幫我們scale up,像是做load balance,根據當前不同的負載程度,多開不同數量的EC2,並同時管理App的健康程度,也可以將一組環境變數,同時套用到多台EC2,總之就是很方便(X)
點右上角的Create a new environment
來新增一個環境
這邊因為要建API Server,所以選Web Server Environment,下一步就是設定一些名字,基本環境,平台這邊就選Node14,Code的話,等等會用別的方式從Github上匯進來,所以先選Sample就可以了
接下來我們要做更多設定,點Configure more options
,進來後我們要設定兩個地方,Software和Capacity
再Software的部分,主要是先將environment variable打進去,這邊就要一筆一筆的從dev環境複製過來…好累…怎麼沒有匯出匯入功能。
再來是Capacity的部分,這邊我們要打開Load balanced,但網站上線初期應該不會有太多流量,所以Min跟Max都設1就好了,雖然說這樣其實就失去了Load balanced的功能,不過開這個還有另外一個目的,就是之後要掛SSL,都選好了之後就可以按開始建立環境,之後大概等個5分鐘,環境就建好了
再來建Database,在搜尋框找RDS
,進到頁面後點左側 Database
就可以進到管理DB的Dashboard,這邊可以看到已經建立的DB的一些相關資訊,按右上的Create database
開始建立新的DB
前面是一些基本設定,像是名字、引擎、登入帳密…等
在Instance這邊,一樣由於剛上線不會有很高的Data吞吐量,所以選擇了最小的DB instance,class要選Burstable,意思是一開始只有基本的性能,但可以依據當前的需求短暫進入爆發模式(?)
而storage的部分,有3種type可以選,分別是
根據官方描述,io1主要是用在有大量I/O需求的應用上,而Magnetic則是已經過時(?)的方案,所以我們就選最一般性的gp2,空間大小也先配置為最小的20GB,並把autoscaling勾起來,日後如果資料變多,可以讓AWS幫我們自動增加大小
再來是連線的設定,這邊我們要設定讓剛剛建起來的API Server可以連到這個DB,由於安全性的關係,AWS中大部分的功能都會被放在不同的VPC(virtual private cloud)中,不同的VPC間彼此是不能互聯的,所以為了要讓EB可以access到DB(單押),這裡需要設定RDS跟剛剛建立的EB在同一個VPC底下
繼續設定connectivity,Public access預設是不能選yes的,這邊要等到建立好之後再進去設定,但根據Info裡面的說明,即便開啟了Public access,還是要通過所在VPC的防火牆設定,所以不是隨隨便便就能從外部連進來DB的(好安全),可是一開始我們會需要連進來Insert一些初始資料,所以為了要能夠從外部連線,我們要在Additional VPC security group這邊assign給他一個publicAccessTODB
的group,這個group不是預設的,是之前為了dev環境連進DB所新增的,這邊我們可以先跳離開一下設定畫面,去建一個給production用的VPC security group
一樣在新分頁登入AWS,搜尋VPC
進到VPC的主控版,底下會有一大堆可以設定的東東,找到Security Groups,點進去,從右上角新增security group
這邊設定好group的名稱、描述、與上層VPC後,底下要新增一個Inbound rule,點Add rule,設定type為custom TCP,監聽的port是MySQL的3306,source可以設為Anywhere IPv4,這邊IP會被設定為0.0.0.0,即任何人都可以連線進來,當然還是要輸入帳號密碼,但這樣其實有點不夠安全,比較好的設定是選custom,然後輸入可以連線的IP,不過前提是要有固定IP,不然就是每次要連線的時候,再登入修改IP
建立好了group就可以回來繼續我們的設定,可以點connectivity區塊右上角的重整按紐,在不會把剛剛輸入清空的情況下,重新載入列表,就可以選取剛剛建立好的database-public-access
這個group,接者設定一些Additional configuration,這邊可以設定初始的database名稱,還有開啟備份。
按下create database後,他會跳回dashboard,但這時DB還沒建立起來,status顯示為creating
等個差不多10分鐘,就可以看到他的status轉成Available了,這時候點進去database-prod裡面,可以在底下看到它的endpoint
但這時候我們還沒開public access,所以是沒辦法從外部連進去的,於是選起來我們剛剛建立好的DB,點下Modify,進去將public access打開,就可以快快樂樂的從外部連線進去了…嗎?
當然沒有這麼容易!明明public access也開了,security group也加了,怎麼沒辦法連進DB呢?仔細看了官方的故障診斷,原來是剛剛我們設定connect to EC2的時候,會同時幫我們建立一個subnet,這是屬於RDS與EB間的小圈圈,外人是沒辦法連進去的,為了成為第三者(X),我們還需要做一些額外設定。
先記住詳細資訊圖中的subnets有哪些,再到VPC的主控台底下進入Subnets的Dashboard,可以看到剛剛建RDS的時候一併加入的RDS-Pvt-subnet-*
點進去設定底下的route table,按Edit route table association
在上面的route table ID裡面應該會看到有一個主要的route table,點選它就會將0.0.0.0/0綁訂到我們的getway上了
同樣把另一個subnet也設定好route table就可以從外部連進來DB了。
Amplify是AWS一個比較新的服務(至少界面上看起來比較新XD),他主打簡單快速的建造、佈署、hosting、管理一個無伺服器(serverless)APP,但同時也支援單純的Host App,由於他的設定非常的簡單,也同時支援SSR或是單純靜態網頁的hosting,所以就選用這個服務來作為我們Nextjs與React App的平台。此外Amplify也可以直接連結到Github,比起以前需要自己寫Github Action,雖然這需要讓AWS在你的Github帳號上裝一些連動的功能,但簡化了CD(continuous deployment)很多。
自動佈署的部分等一下會設定要連動到哪一個branch,每當該branch有push新的commit時,就會自動觸發CD,更新APP的版本。為此,我們會需要在Github上面開一個新的branch。
這裡先說個題外話,為了要分離開發環境,確保production的運作是沒有問題的,我們在開發的時候會有幾個主要的branch:
在local開發完功能或修好bug的時候會提交PR到main,經過code review後就可以merge進main裡面了,這時候會主動更新dev環境的APP。一段時間就會將dev環境的code merge到staging中,這裡其實算是進production前的一個緩衝區,將這段時間內所開發的功能進行一次總測試,確認沒有任何bug之後就會將stagin的code推到production,在正式環境中開始提供這些功能。但多開一個環境就要多花錢(X),為了省錢省事(X),我們把dev拿掉,改為定期更新staging,當要發布新版本前,可以減緩開發新功能的branch,等確定staging都沒有問題後,再將它推到production branch,可以參考下圖。
為了做到上述的功能,我們需要建立兩個新的branch,release-staging
和release-production
,建立branch本身沒什麼問題,在repo的branch管理頁面中按New branch就可以了。但為了防止我們不小心(?),傳了壞掉的code上去把它們用壞,所以必須要加上branch protection rule,來阻止直接推上production或staging的branch。
進到Settings這個tab,在左側導航列找到branches,可以看到如上的設定畫面,在branch protection rules那邊點選Add rule,開始設定保護規則。首先設定要保護branch的名稱,這邊可以用regex,我們就設為release-*
,這樣就可以一次把rule設定到兩個branch上面,而底下的設定都不需要打開,這是確保這些branches不能被push。
branches建好了,也保護起來了,但我們要怎麼把main上面的code推進去呢?在上述的保護規則下,是沒有辦法在本地端merge完在堆上去的,加上前面有提到我們要定時把main的code merge進staging,這裡就可以用到github action來幫我們完成這些事。
在Actions這個tab底下,可以看到我們建立的workflow以及每次執行的結果,要建立workflow有兩種方法,一個是直接在這個頁面點New workflow,然後用線上編輯器編輯,另外也可以在local端將workflow檔案放進 _WORKING_DIR_/.github/workflows
資料夾底下,再推上去,這邊只是簡單的建立一個merge的workflow就採用線上編輯器了。
進到New workflow之後,會先有一個頁面讓你選擇模板,這邊就先按Skip this and set up a workflow yourself 跳過設定進入編輯器,這裡我們先來做CD-staging
Github Action的設定檔都是yaml的格式,一開始可以定義這個action的名字,在on
的部分,是設定這個action在什麼情況下會觸發,schedule是排程,以POSIX cron syntax
來設定,從左到右每位數字分別代表:
可以用萬用字元*
代表該時段的所有值,這裡我們設定0 8 * * 2
即是指每周二的早上8點(UTC)會執行這個action。這邊為了保留手動執行的彈性,也有另外設定了workflow_dispatch
這個條件,這可以讓我們直接從actions page手動執行底下的jobs。
而在jobs設定的部分,可以指定工作名稱、要執行的環境(runs-on
)、與該工作的每一步驟(steps
),第一步驟我們用github官方寫好的package來checkout到main,第二步驟則是用其他人寫好的package將main這個branch merge到release-staging這個branch,其中的github_token
是你repo的token,這個package需要用這個token才能執行merge的動作,不過它本來就存在於環境變數github.token
中了,所以無需做額外設定。
編輯完成後,按右上角的Start commit,然後輸入commit訊息,選Commit directly to the main branch.就可以直接把這個workflow寫入到.github/workflows/CD-staging.yml
底下了。完成後回到Actions page,選擇我們剛剛建立的workflow,因為有設定workflow_dispatch
這個條件,所以會看到有Run workflow的按鈕選單出現,點開按Run workflow,就可以手動觸發我們上面設定的jobs了。
等一小段時間(真的很快),就可以看到workflow順利跑完,這其實也不會出錯,畢竟只是把一個branch merge到另一個而已,這時候到release-staging這個branch看他的commit,可以看到一個由 @github-actions
github-actions[bot] 所推的commit,到這邊我們staging的CD在Github上的部分已經完成了,剩下的要去AWS設定。
與建立CDstaging的流程一樣,我們幫production也建一個merge的workflow
這邊我們只有開啟手動執行的選項,另外在jobs的部分,除了merge-branch,還額外裝了別人寫的用來自動推版本號tag的package,他在每次執行的時候都會將minor的版號推進1,如過這次有任何的commit裡面含有#major
則會推進主版號,若有#patch
則是推進最後一位。
寫入main之後一樣可以在Actions page看到這個workflow,手動執行後,進到tag頁面,就可以看到他幫我們推進了副版號到v0.1.0
接下來就要回到Amplify那邊去設定要同步更新的branch。
一樣前往AWS console,搜尋Amplify來到他的主控台,點選New App,在選單中找到host web app,開始設定。一進來首先要設定這個APP要跑的code從哪裡來,我們在From your existing code這裡選github然後按continue
這時候如果還沒有再Github裝AWS的APP的話,他會跳轉去安裝授權,如果已經有AWS APP,他就會主動列處擬在Github那邊授權可以操作的Repo,選好Repo之後,底下的Branch選擇我們剛剛建立的release-production,接者下一步。
這邊則是設定一些初始化的細節,前面有提到Amplify主要是hosting靜態網頁,但他可以透過lambda function在需要的時候去執行server的部分,這裡因為我們有用到SSR,所以必須要給他一個role去跑server的部分,底下的build and test setting是在Amplify抓完最新的code之後要怎麼build的指令,通常他都會從我們的package.json自動判斷,不太需要額外設定。
底下打開Advanced settings,這邊我們可以預先將environment variable輸入,另外我們也可以指定Next.js跟Node.js的版本,下一步就是review所有設定,確定沒問題之後就可以save and deploy了。接下來Ampliyfy就會主動去我們githun上抓release-production這個branch的code下來,然後build,在deploy出去。
在App settings的Domain management管立頁面可以看到,現在我們的URL是一串Amplify隨機生成的亂碼,當然不能讓客戶用這奇奇怪怪記不起來的網址連入我們的APP,所以必須給他一個好聽(?)的網址。按右上角Add domain進入設定
如果之前有在Route 53設定過你的Domain,當點下Domain的欄位的時候,就會跳出可選的Domain讓你設定,輸入完Domain後按Configure domain,底下的sub-domain欄位就會跑出來,預設是會有一個root的domain,另一個是www開頭的domain,這邊我把www的subdomain拿掉,只留root的domain,每一列後面則是可以選要連結到哪一個branch,這個只有在該APP有連結到多個branch的時候才有意義,通常是在dev環境中想要先看看某個featurebranch實際上會跑怎樣才去設定的。
按下Save後,可以看到AWS會主動幫我們去申請SSL憑證,然後回來設定SSL相關的config,可以說是真 • 一鍵安裝。而如果一開始沒有在Route 53設定過Domain的話可以參考下面一段,先去Route 53設定Domain。
搜尋Route 53到他的主控台,點選左邊的host zone,再按Create hosted zone,輸入你的Domain name然後下一步。
建立好host zone之後點進去在設定record的地方,這邊可以看到很多的records,那些route到cloudfront.net的record就是Amplify幫我們建立的,可以把那些URL導到Amplify放靜態網頁的地方。而第2列的NS Record是用來將DNS要求導向AWS的DNS Server。這邊可以提一下,當初這個Domain name是在Go Daddy買的,他有隨附(?)他們的DNS server,所以AWS這邊生成之後要再將這些DNS server的網址填到Go Daddy那邊。其他的CNAME大部分是用來驗證你擁有這個Domain。
基本上Route 53設定好的話在Amplify那邊就可以直接選取Domain了
其實這部分跟前面完全一樣,就是在Github開新的Branch,然後回到Amplify,建立新的APP,連結到repo下的release-production branch,完成後設定Domain就可以了。
到這邊,我們前端的網頁都已經成功佈署了,也都會根據github上release的branch去自動更新(CD),但API Server的部分還沒有辦法主動從github那邊抓到code來更新。這邊就可以用到AWS的另一個功能 –- CodePipeline,他可以連結到Github,然後主動抓某個branch上面的code下來,然後Deploy到EB上,其實就很像前面Amplify幫我們做的事,雖然也可以用github action做到鑄件事,但既然AWS有提供的話就還是用吧XD
一樣搜尋code pipeline到他的主控台,在左側選單找到pipelines然後點create pipeline開始設定
第一步輸入完名字後,底下需要指派一個role給pipeline去執行fetch和deploy的動作,這邊因為之前已經有一個role給dev了,所以就直接用Existing service role,如果還沒有任何相關的role,也可以選New service role讓他幫你新增。
第二步是連接source,這邊選Github(version 2),接著選以連接的github 帳號,如果還沒連接,也可以點旁邊的connect to github,讓AWS在你的github帳號上裝一些APP已取得授權。然後選API的repo和release-production這個branch。
第三步的 build stage可以跳過,因為我們code clone過來,EB就會幫我們build好啟動了。
第四部選擇deploy的目的地,這邊就選一開始我們在EB中開的production環境與APP,下一步review完設定後就可以順利create pipeline。
建立後稍微等一下就可以看到pipeline成功把source的code deploy到EB上了,這時候回去EB看,可以發現中間的Running Version已經變成code-pipeline-...
的版本,代表我們成功把release-production上的code放進EB中了,之後branch如果有任何變動也會觸發pipeline deploy。
接下來就是要把我們的Domain與SSL憑證掛到EB上的API server,在Amplify那邊,可以直接在domain設定那邊設定好domain,他會直接幫我們建立route 53上的record與掛上SSL憑證,但是在EB這裡沒有這麼方便,需要手動設定。
首先來到route 53這邊,在我們domain name的host zone底下新增一筆record。設定api server的URL,Record type選A,底下的traffic原本只能輸入要導向的ip位置,但把alias勾起來之後,就可以直接設定導向到EB底下的環境。
再來設定SSL憑證的部分,正如前面講的,我們現在開load balancer主要是為了掛SSL上去,所以進到我們production EB的環境中,在左邊Configuration的設定頁面中,找到Load balancer,案edit開始編輯。
在最上面設定listeners的部分,增加一個監聽443port,且protocal為https的listener,並把流量導向default process,也就是在port 80上的API server,新增完成後記得按底下的Apply套用設定。
接下我們要開一個可以上傳靜態資源的雲端存貯空間,搜尋S3到他的主控台,按create bucket,輸入名字與選擇地區後,需要把底下的public access打開,因為這些上傳的靜態資源(e.g.: 圖片)需要可以從外部存取到,記得底下的warning要勾起來。
建立好後點進去到permission的標籤底下,先編輯Bucket policy,這是用來設定bucket中那些資源可以被public access,設定如下:
其中${arn}
的部分要換成你的bucket的arn,這讓所有在/public
資料夾底下的檔案都可以被public read,接者設定底下CORS的部分,因為S3沒有設置網域,到時候的存取不管是從前端還是admin,都是跨網域的,所以要把CORS給開起來,設定如下:
Lambda 是一項運算服務,可讓您執行程式碼,而無需佈建或管理伺服器。
以上是來自官方的說明,那我們這邊要用到lambda的部分主要是希望當有檔案上傳到S3的時候,可以主動通知API server去更新DB的資料。一樣到Lambda的主控台然後Create function。基本設定如下
建立好function之後,點Add triger,添加觸發function執行的條件,source選S3,bucket選剛剛建的assets bucket,底下的event type選put,設定好後按Add,這樣有任何檔案上傳到S3的時候就會觸發lambda function執行了。再同樣設定一遍All object delete的trigger。
接者點到Code的tab底下,輸入以下程式碼:
其中export.handler
所assign的function即是trigger觸發時所執行的function,trigger會傳一個event給它,裡面包含了event type(put, delete),與key(即檔案路徑),這邊我們額外寫了一個function去post這些資料給API server。
存檔後,記得按Deploy,再去Configuration底下的Environment variable中設定API_URL就可以了。這邊小小的紀錄一下踩到的雷,再用https(或http)這個module的時候,他hostname那邊是不可以有協定名稱的,即,不能是https開頭,結尾也不可以是/
,這邊弄了好久想說怎麼server一直沒有收到通知,去跑了lambda內建的test才發現有error。