新 EC2 T4g Instance – AWS Graviton2 – 免費試用

今天在 AWS 官方 Facebook 的 Service Update 影片看到有新的 Instance type T4g 推出

新機器等級 T4g

看起來是一個主打 ARM 架構的 Instance 目前還沒測試過效能,不過可以嘗試看看有心得再跟大家分享

免費額度

2020 年 9 月 – 2020 年 12 月 31 日所有 AWS 客戶都會自動註冊一個 type t4g.micro 的免費試用版。

Day 30 – 使用 CDK 創建 Open Unlight 遊戲

https://ithelp.ithome.com.tw/upload/images/20201015/20117701P7SaYXBQqw.png

今天是鐵人賽最後一天,我要來跟大家介紹一個遊戲 Open Unlight 它是一個在 2019 年 7 月 27 日 Open Source 的一個遊戲,剛開源的時候因為沒有安裝說明文件而且有缺少檔案,所以不容易部署在當年的 2019 年 8 月 8 日由 Open Unlight 團隊修改成 legacy-unlight-docker 繼續維護 它應該算是全世界少數 Open Source Server-Client 架構的遊戲之一,Open Source 的遊戲實在是太少了 XD 而為什麼要介紹它呢?因為今天的題目就是教大家用 CDK 把 Unlight 遊戲部署起來呀!

Unlight 遊戲 Server 部署文件

我們今天目標以一台 EC2 在裡面跑 Docker 的架構把整個遊戲伺服器創建起來

建置前我們先看一下 Legacy Unlight Docker (中文) 遊戲專案介紹在文件裡面可以看到遊戲的部署分為 Server 端與 Client 端,而 Server 端有預編譯 Image 可以使用,但是 Client 端沒有需要自己手動編譯,而且 Client 端編譯的時候需要準備字型檔

快速建置指令

而 Open Unlight 其實提供專屬的 UnlightCLI 指令可以幫助快速建立系統,不一定需要用 Legacy Unlight Docker 從 Source Code 編譯

下載指令

指令下載方法

$ gem install unlight-cli

啟動專案

啟動遊戲方法

$ unlight init

導讀完應該就大概知道怎麼把遊戲建立起來了吧!那我們就來寫今天的 CDK 吧!

Unlight 遊戲 CDK

CDK 部分只有寫到 Server 端部署完成,Client 端還是需要進入機器手動編譯的,這點需要注意一下。

建立 Default VPC

今天只有一台 EC2 就直接使用 Default VPC 吧!

const vpc = ec2.Vpc.fromLookup(this, "VPC", {
  isDefault: true,
});

建立 EC2

系統安裝腳本準備

今天建立的 EC2 主要讓 User Data 幫我們把整個環境建立起來並執行遊戲,它會開啟 14 台遊戲主機 1 台資料庫與 1 台 cache 伺服器

更新系統

yum update -y

安裝 vim ruby Nginx

amazon-linux-extras install vim ruby2.4 nginx1 epel -y

安裝 git htop

yum install git htop -y

安裝 docker

amazon-linux-extras install -y docker

下載 docker-compose

curl -L https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m) -o /usr/local/bin/docker-compose

加入 docker-compose 執行權限

chmod +x /usr/local/bin/docker-compose

啟動 docker

systemctl restart docker

設定開機啟動 docker

systemctl enable docker

設定 ec2-user 使用 docker

usermod -a -G docker ec2-user

安裝 Nginx

amazon-linux-extras install nginx1 -y

啟動 Nginx

systemctl restart nginx

設定開機啟動 Nginx

systemctl enable nginx

下載 unlight-cli

gem install unlight-cli -N --clear-sources --minimal-deps

執行 unlight init 啟動遊戲

unlight init

CDK 建立 EC2

EC2 部分我們把上面討論的 User Data 放進去讓它開機的時候執行,這是因為服務需求比較大我們開的機器等級是 t3a.medium 更小台應該會跑不動

const instance = new ec2.Instance(this, "Instance", {
  vpc,
  instanceType: ec2.InstanceType.of(
    ec2.InstanceClass.T3A,
    ec2.InstanceSize.MEDIUM
  ),
  machineImage: ec2.MachineImage.latestAmazonLinux({
    generation: ec2.AmazonLinuxGeneration.AMAZON_LINUX_2,
  }),
  vpcSubnets: {
    subnetType: ec2.SubnetType.PUBLIC,
  },
  keyName: "KeyPair",
});
instance.userData.addCommands(
  `yum update -y`,
  `amazon-linux-extras install vim ruby2.4 nginx1 epel -y`,
  `yum install tmux git htop -y`,
  `amazon-linux-extras install -y docker`,
  "curl -L https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m) -o /usr/local/bin/docker-compose",
  `chmod +x /usr/local/bin/docker-compose`,
  `systemctl restart docker`,
  `systemctl enable docker`,
  `usermod -a -G docker ec2-user`,
  `amazon-linux-extras install nginx1 -y`,
  `systemctl restart nginx`,
  `systemctl enable nginx`,
  `gem install unlight-cli -N --clear-sources --minimal-deps`,
  `unlight init`
);
new cdk.CfnOutput(this, "instance", {
  value: instance.instancePublicDnsName,
});

設定防火牆

如果要開設給大家玩記得要把 22 port 保護起來呦!一樣我們為了方便 debug 所以先把 22 port 全開

Unlight 遊戲使用的 Port 為 11000 ~ 13000,相信大家應該不會想用前面幾篇教的方法然後用 for 迴圈跑吧 XD

所以今天說明一個之前沒講過用來控制 Security Group 方法 tcpRange 就可以一行幫我們完成 Port 區段的開設

instance.connections.allowFrom(ec2.Peer.anyIpv4(), ec2.Port.tcp(22));
instance.connections.allowFrom(
  ec2.Peer.anyIpv4(),
  ec2.Port.tcpRange(11000, 13000)
);

以上就是今天的 CDK 教學!就這麼簡單!

Unlight 遊戲 Server 部署測試

寫完後我們就來執行今天的 CDK 吧!cdk deploy 把它執行下去!

查看系統是否有正常跑起來

部署完後我們就可以登入進去輸入 docker ps 看看系統是否有跑起來拉! 如下資訊就可以看到系統是正常執行的情況

  • 14 台遊戲主機
  • 1 台資料庫
  • 1 台 cache 伺服器

       __|  __|_  )
       _|  (     /   Amazon Linux 2 AMI
      ___|\___|___|

https://aws.amazon.com/amazon-linux-2/
[ec2-user@ip-172-31-43-71 ~]$ docker ps
CONTAINER ID        IMAGE                              COMMAND                  CREATED             STATUS              PORTS                      NAMES
ae55da85c62f        openunlight/legacy-server:latest   "server xmlsocket"       4 minutes ago       Up 23 seconds       0.0.0.0:11999->11999/tcp   openunlight_xmlsocket_1
dbd1591f84b9        openunlight/legacy-server:latest   "server raid_rank 12…"   4 minutes ago       Up 23 seconds       0.0.0.0:12070->12070/tcp   openunlight_raid_rank_server_1
5d491c301bcb        openunlight/legacy-server:latest   "server data_lobby 1…"   4 minutes ago       Up 22 seconds       0.0.0.0:12032->12032/tcp   openunlight_data_server_1
92189a119d6e        openunlight/legacy-server:latest   "server lobby 12002"     4 minutes ago       Up 28 seconds       0.0.0.0:12002->12002/tcp   openunlight_lobby_server_1
ca1c6b1795ba        openunlight/legacy-server:latest   "server matching 120…"   4 minutes ago       Up 23 seconds       0.0.0.0:12018->12018/tcp   openunlight_match_server_1
d502e85a13de        openunlight/legacy-server:latest   "server game 12008"      4 minutes ago       Up 22 seconds       0.0.0.0:12008->12008/tcp   openunlight_game_server_1
a615ab3896ae        openunlight/legacy-server:latest   "server global_chat …"   4 minutes ago       Up 27 seconds       0.0.0.0:12020->12020/tcp   openunlight_global_chat_server_1
f1fc99c578dd        openunlight/legacy-server:latest   "server raid_data 12…"   4 minutes ago       Up 31 seconds       0.0.0.0:12100->12100/tcp   openunlight_raid_data_server_1
97eb265c132f        openunlight/legacy-server:latest   "server chat 12012"      4 minutes ago       Up 29 seconds       0.0.0.0:12012->12012/tcp   openunlight_chat_server_1
0d7d7fe736cc        openunlight/legacy-server:latest   "server raid 12050"      4 minutes ago       Up 28 seconds       0.0.0.0:12050->12050/tcp   openunlight_raid_server_1
7bf3e0aeaea9        openunlight/legacy-server:latest   "server raid_chat 12…"   4 minutes ago       Up 21 seconds       0.0.0.0:12090->12090/tcp   openunlight_raid_chat_server_1
7899ae49d6b2        openunlight/legacy-server:latest   "server authenticati…"   4 minutes ago       Up 22 seconds       0.0.0.0:12001->12001/tcp   openunlight_auth_server_1
92c136738e17        openunlight/legacy-server:latest   "server watch 12080"     4 minutes ago       Up 29 seconds       0.0.0.0:12080->12080/tcp   openunlight_watch_server_1
b4fd0bec724b        openunlight/legacy-server:latest   "server quest 12005"     4 minutes ago       Up 28 seconds       0.0.0.0:12005->12005/tcp   openunlight_quest_server_1
58625fbb5e55        mysql:5.7                          "docker-entrypoint.s…"   4 minutes ago       Up 26 seconds       3306/tcp, 33060/tcp        openunlight_db_1
c60470b7c2f3        memcached:1.5.16-alpine            "docker-entrypoint.s…"   5 minutes ago       Up 29 seconds       11211/tcp                  openunlight_memcached_1

部署與編譯 Client

如前面所說因為 Open Unlight 遊戲的 Client 需要依靠資料庫才可以被 build 出來,所以大家需要跟我一起進入機器手動把 Client 端 build 起來

字體檔

字體檔需要大家找一下把它放到 /home/ec2-user/OpenUnlight/fonts 裡面,要準備的字體如:

語言 必要字體
繁體中文 cwming.ttf (cwTeXMing 明體), wt004.ttf (王漢宗特明體), nbr.ttf

nbr.ttf 是一個名為 Bradley Gratis 的字體,但 Unlight 是有客製化過的。

編譯 Client

準備好字體檔後,就可以來跑 client 建置腳本了,其實只要把字體檔準備好差不多就好了,執行以下指令

$ export ROOT_DIR="/home/ec2-user/OpenUnlight"
$ git clone https://github.com/unlightcpa/Unlight-Images.git OpenUnlight/assets
$ mkdir ${ROOT_DIR}/OpenUnlight/dist
$ mkdir ${ROOT_DIR}/OpenUnlight/fonts

$ docker run --rm -v ${ROOT_DIR}/assets:/assets \
                  -v ${ROOT_DIR}/dist:/app/dist \
                  -v ${ROOT_DIR}/fonts:/app/fonts \
                  -e LANGUAGE=tcn \
                  openunlight/legacy-builder compile-client

結果會出現在 dist 資料夾裡面

$ ls -all OpenUnlight/dist/
total 17136
drwxrwxr-x 2 ec2-user ec2-user       43 Oct 14 17:22 .
drwxrwxr-x 5 ec2-user ec2-user       83 Oct 14 17:11 ..
-rw-r--r-- 1 root     root         1976 Oct 14 17:22 index.html
-rw-r--r-- 1 root     root     17539965 Oct 14 17:22 Unlight.swf

Host Client

有了 Unlight.swf 與 index.html 後就可以把檔案放到我們前面安裝的 Nginx 裡面拉! 它的結構需要如:

dist
├── Unlight.swf
├── index.html
└── public
    ├── config.xml
    ├── image/
    ├── news.xml
    └── sound/
  • image 與 sound
    • 在 OpenUnlight/assets 可以找到它
  • config.xml 與 news.xml

不過其實我有準備好腳本,執行它就可以了!

$ sudo cp /home/ec2-user/OpenUnlight/dist/index.html /home/ec2-user/OpenUnlight/dist/Unlight.swf /usr/share/nginx/html/
$ sudo cp -r /home/ec2-user/OpenUnlight/assets/public /usr/share/nginx/html/
$ sudo wget https://raw.githubusercontent.com/unlightcpa/Unlight/master/app/client/public/news.xml.orig -O /usr/share/nginx/html/news.xml
$ sudo wget https://raw.githubusercontent.com/unlightcpa/Unlight/master/app/client/public/config.xml.orig -O /usr/share/nginx/html/config.xml

在開始前需要記得修改 /usr/share/nginx/html/config.xml 把裡面的 localhost 修改成自己的 EC2 網域或是 IP

https://ithelp.ithome.com.tw/upload/images/20201015/20117701JIwwIvdaGO.png

小錯誤:架設起來的 index.html 會有一個 swfobject.js 找不到雖然不會影響開啟不過如果看到覺得煩可以修改一下到 CloudFlare CDN

https://cdnjs.cloudflare.com/ajax/libs/swfobject/2.2/swfobject.js

測試遊戲

超興奮的打開網址就可以看到登入介面囉!

https://ithelp.ithome.com.tw/upload/images/20201015/20117701lYnRGfRvOu.png

趕緊註冊一下遊戲帳號 https://ithelp.ithome.com.tw/upload/images/20201015/20117701ojDxbwFi4y.png

註冊成功,如此就可以確定資料庫串接也是正常的 https://ithelp.ithome.com.tw/upload/images/20201015/20117701WEZAZ8Owre.png

開始玩遊戲吧!(誤 https://ithelp.ithome.com.tw/upload/images/20201015/20117701yvpMM7KPKE.png

最後

謝謝大家這 30 天的陪伴,不管你是一開始就看我教學文的朋友或是看到分享才進來觀看的朋友,希望這 30 天的教學文對於一個剛學習 AWS CDK 的朋友有幫助,因為我知道中文教學文其實還是一個偏少的一個情況,所以寫中文就是希望讓母語中文的朋友在學習上可以減少阻礙,另外如果大家對我的系列文章有興趣,歡迎大家關注我的部落格謝謝大家拉 ~

Day 28 – 使用 CDK 創建 CloudWatch Alarm 的含圖告警同時發送到 LINE 與 Discord

昨天教大家怎麼簡單的在 LINE Notify 上面看到 CloudWatch 的 Alarm,不過這樣只有通知沒有目前的狀態圖對於平常判斷系統是否有問題好像少了什麼,所以今天來教大家怎麼讓 LINE Notify 上面除了可以看到 CloudWatch 的 Alarm 之外還可以看到 CloudWatch Graph

https://ithelp.ithome.com.tw/upload/images/20201012/20117701DXnf49N8qV.jpg

而今天的內容除了可以把訊息發到 LINE Notify 之外還加碼支援把訊息發送到 Discord

https://ithelp.ithome.com.tw/upload/images/20201012/20117701R5IkkogXCH.png

今日目標

  1. 更新 Lambda 程式
  2. Lambda 加入 Policy
  3. Lambda 加入 environment
  4. 加入壓測工具

創建一個 CloudWatch Alarm 的含圖告警

更新 Lambda 程式

先更新新版的 Lambda 程式,程式位置 GitHub

主要功能我都已經先寫好並且放到 GitHub 上面了,所以大家只要 pull 就可以取得新版程式了呦!

寫得很爛請大家見諒

$ cd lambda
$ git pull

Lambda 加入 Policy

我們需要讓 Lambda 可以吃到 CloudWatch 的資料,所以要修改一下它的權限,其實這邊權限還可以再給得更小不過今天就先開個 cloudwatch:Get* 吧 XD

myFunction.addToRolePolicy(
  new iam.PolicyStatement({
    actions: ["cloudwatch:Get*"],
    resources: ["*"],
  })
);

Lambda 加入 environment

還記得昨天的範例我們需要直接修改 "your-line-notify-token" 嗎?這個部分我做了一點改變,讓參數直接從 CDK 喂給 Lambda 的 environment

const myFunction = new lambda.Function(this, "Lambda", {
  handler: "index.handler",
  runtime: lambda.Runtime.NODEJS_10_X,
  code: lambda.Code.fromAsset("lambda"),
  environment: {
    LINE_NOTIFY_TOKEN: "your-line-notify-token",
    DISCORD_URL: "your-discord-webhook-url",
  },
});

取得 Discord Webhook 的方法可以參考官方文件

加入壓測工具

昨天的教學因為篇幅關係沒有介紹到如何使用人為方法給予機器負載做測試,今天來把這段補起來

我們平常使用的 AMI 為 AMAZON LINUX 2 所以以此介紹,首先我們需要先安裝 epel 不然我們沒有軟體包可以使用

$ sudo amazon-linux-extras install epel -y

如果對 epel 有問題可以參考 AWS 的文件 How do I enable the EPEL repository for my Amazon EC2 instance running CentOS, RHEL, or Amazon Linux?

安裝 stress-ng

這次我們選用 stress-ng 作為我們的測試工具,還是記得我們前面有教大家使用 User Data 嗎?我們就直接請 User Data 安裝這樣我們開啟系統的時候就可以直接使用了

$ sudo yum install stress-ng -y

製造 CPU Load

我們範例定義的 CPU Utilization threshold 為 5 基本上只要給他一點壓力 CloudWatch Alarm 就會有反應了,所以我們先給予他 CPU 40% 的壓力作為測試給予壓力的指令如:

$ stress-ng -c 1 -l 40
stress-ng: info:  [1084] defaulting to a 86400 second run per stressor
stress-ng: info:  [1084] dispatching hogs: 1 cpu
stress-ng: info:  [1084] successful run completed in 74.43s (1 min, 14.43 secs)

整理一下範例

今天的範例建立了

  • EC2
    • 放入 Default VPC 加速部署
    • 等級 t3a.nano
    • Security Group 22 全開
    • Image:AMAZON_LINUX_2
    • Subnet:PUBLIC
    • User Data:
      • amazon-linux-extras install epel -y
      • yum install stress-ng tmux htop -y
  • SNS
  • CloudWatch
  • Lambda
const vpc = ec2.Vpc.fromLookup(this, "VPC", {
  isDefault: true,
});

const securityGroup = new ec2.SecurityGroup(this, "SecurityGroup", {
  vpc,
  allowAllOutbound: true,
});
securityGroup.addIngressRule(ec2.Peer.anyIpv4(), ec2.Port.tcp(22));

const instance = new ec2.Instance(this, "Instance", {
  vpc,
  instanceType: ec2.InstanceType.of(
    ec2.InstanceClass.T3A,
    ec2.InstanceSize.NANO
  ),
  machineImage: ec2.MachineImage.latestAmazonLinux({
    generation: ec2.AmazonLinuxGeneration.AMAZON_LINUX_2,
  }),
  securityGroup,
  vpcSubnets: {
    subnetType: ec2.SubnetType.PUBLIC,
  },
  keyName: "KeyPair",
});
instance.userData.addCommands(
  `amazon-linux-extras install epel -y`,
  `yum install stress-ng tmux htop -y`
);
new cdk.CfnOutput(this, "instance", {
  value: instance.instancePublicDnsName,
});

const topic = new sns.Topic(this, "Topic");

const metric = new cloudwatch.Metric({
  namespace: "AWS/EC2",
  metricName: "CPUUtilization",
  dimensions: {
    InstanceId: instance.instanceId,
  },
  period: cdk.Duration.minutes(1),
});

const alarm = new cloudwatch.Alarm(this, "Alarm", {
  metric,
  threshold: 5,
  evaluationPeriods: 1,
});

alarm.addAlarmAction(new cw_actions.SnsAction(topic));

const myFunction = new lambda.Function(this, "Lambda", {
  handler: "index.handler",
  runtime: lambda.Runtime.NODEJS_10_X,
  code: lambda.Code.fromAsset("lambda"),
  environment: {
    LINE_NOTIFY_TOKEN: "your-line-notify-token",
    DISCORD_URL: "your-discord-webhook-url",
  },
});
myFunction.addToRolePolicy(
  new iam.PolicyStatement({
    actions: ["cloudwatch:Get*"],
    resources: ["*"],
  })
);

topic.addSubscription(new subscriptions.LambdaSubscription(myFunction));

參考資料

  • https://github.com/aws-samples/aws-cloudwatch-snapshot-graphs-alert-context

今天介紹如何部署一個 CloudWatch Alarm 的含圖告警,有機會我會在修改目前的程式讓它變得更靈活使用或是支援更多的通訊程式,希望對大家有幫助 ~

Day 27 – 使用 CDK 創建 CloudWatch 也能發送 Alarm 到 LINE 的系統

前幾天說了很多建置系統的方法但是對於監控系統沒有太多的說明,今天就來一個 CloudWatch 傳送 Alarm 到 LINE 的教學吧!先給大家看看今天的成品

https://ithelp.ithome.com.tw/upload/images/20201012/20117701qwE7eaxFmE.jpg

目標

今天的目標是要模擬我們開的 EC2 發生 CPU Utilization 過高觸發 CloudWatch Alarm 發送告警訊息給 LINE Notify 整個觸發流程是

  1. EC2 CPU Utilization 過高觸發 CloudWatch Alarm
  2. CloudWatch Alarm 觸發 SNS Action
  3. SNS Action 觸發 Lambda
  4. Lambda 發送 HTTP Request 把訊息帶給 LINE Notify

所以需要創建的服務會有

  1. EC2
  2. CloudWatch Alarm
  3. SNS Action
  4. Lambda

創建 SNS Topic

我們先創建 SNS

const topic = new sns.Topic(this, "Topic");

https://ithelp.ithome.com.tw/upload/images/20201012/20117701B2upcd4Jko.png

創建 Cloudwatch Metric

在創建 Cloudwatch Alarm 之前需要先把資料抓出來,所以先創建一個 Metric,指定我們要抓的資料是 AWS/EC2 以 CPU Utilization 為目標,並且指定 EC2 Instance Id

const metric = new cloudwatch.Metric({
  namespace: "AWS/EC2",
  metricName: "CPUUtilization",
  dimensions: {
    InstanceId: instance.instanceId,
  },
  period: cdk.Duration.minutes(1),
});

https://ithelp.ithome.com.tw/upload/images/20201012/20117701aPSjLirZVo.png

創建 Cloudwatch Alarm

準備好 Metric 後就可以把 Alarm 創建起來,範例定義 threshold 為 5

平常比較常用的應該是 85 上下取一個比較能判斷發生問題的值,不過我們為了方便測試以 5 為我們的閾並設定 period 為 1

const alarm = new cloudwatch.Alarm(this, "Alarm", {
  metric,
  threshold: 5,
  evaluationPeriods: 1,
});

https://ithelp.ithome.com.tw/upload/images/20201012/20117701lFyyaU430W.png

串接 Cloudwatch Alarm 與 SNS

設定完之後把 Alarm 與 SNS 連起來

import * as cw_actions from "@aws-cdk/aws-cloudwatch-actions";

alarm.addAlarmAction(new cw_actions.SnsAction(topic));

創建 Lambda 並取得發送 LINE Notify 方法

創建一個 Lambda 這邊我準備好了腳本大家只要把它 Clone 下來就好拉!

Github:https://github.com/clarencetw/sns-alarm.git

$ git clone https://github.com/clarencetw/sns-alarm.git lambda

這邊記得要切換 tag 到 v1.0.0 ,不然會與此篇範例教學不一樣呦!

$ git checkout v1.0.0

還有一個要注意的是要修改 "your-line-notify-token",此參數是 LINE Notify 專用的 Token 取得方法請參考我之前的文章

const myFunction = new lambda.Function(this, "Lambda", {
  handler: "index.handler",
  runtime: lambda.Runtime.NODEJS_10_X,
  code: lambda.Code.fromAsset("lambda"),
});

https://ithelp.ithome.com.tw/upload/images/20201012/201177019huM8wQUdD.png

SNS 訂閱 Lambda

topic 訂閱剛剛創建的 Lambda

topic.addSubscription(new subscriptions.LambdaSubscription(myFunction));

https://ithelp.ithome.com.tw/upload/images/20201012/20117701cGgggbMaoq.png

在 SNS 的 Subscriptions 可以看到 Endpoint 為 Lambda

https://ithelp.ithome.com.tw/upload/images/20201012/20117701lTa74wRqKa.png

可以在 CloudWatch 看到 SNS 的 Trigger

整理一下

如此就完成一個可以收到 CloudWatch 通知的系統拉!

這邊也跟大家說明一下之前的範例都是每次建立一個 VPC 這樣其實有點久,所以這次就使用 fromLookup 讓它尋找 Default VPC 可以讓整個建置的速度變快

const vpc = ec2.Vpc.fromLookup(this, "VPC", {
  isDefault: true,
});

const securityGroup = new ec2.SecurityGroup(this, "SecurityGroup", {
  vpc,
  allowAllOutbound: true,
});
securityGroup.addIngressRule(ec2.Peer.anyIpv4(), ec2.Port.tcp(22));

const instance = new ec2.Instance(this, "Instance", {
  vpc,
  instanceType: ec2.InstanceType.of(
    ec2.InstanceClass.T3A,
    ec2.InstanceSize.NANO
  ),
  machineImage: ec2.MachineImage.latestAmazonLinux({
    generation: ec2.AmazonLinuxGeneration.AMAZON_LINUX_2,
  }),
  securityGroup,
  vpcSubnets: {
    subnetType: ec2.SubnetType.PUBLIC,
  },
  keyName: "KeyPair",
});

const topic = new sns.Topic(this, "Topic");

const metric = new cloudwatch.Metric({
  namespace: "AWS/EC2",
  metricName: "CPUUtilization",
  dimensions: {
    InstanceId: instance.instanceId,
  },
  period: cdk.Duration.minutes(1),
});

const alarm = new cloudwatch.Alarm(this, "Alarm", {
  metric,
  threshold: 5,
  evaluationPeriods: 1,
});

alarm.addAlarmAction(new cw_actions.SnsAction(topic));

const myFunction = new lambda.Function(this, "Lambda", {
  handler: "index.handler",
  runtime: lambda.Runtime.NODEJS_10_X,
  code: lambda.Code.fromAsset("lambda"),
});

topic.addSubscription(new subscriptions.LambdaSubscription(myFunction));

今天介紹了一個使用 CloudWatch 發送 LINE Notify 的方法,其實網路上應該有滿多文章在教學這段的,不過這次的重點其實在於教大家怎麼使用 CDK 串接 CloudWatch、SNS 與 Lambda!希望對大家有幫助 ~

Day 26 – CDK builds Amazon Elastic Kubernetes Service (EKS)-Service

昨天介紹完了 Cluster 今天來介紹如何使用 CDK 建立 EKS Service 往常我們如果使用 kubectl 要新增服務或是控制都會需要用到 yaml 檔案,但今天我們使用 CDK 部署 EKS 不用使用到 yaml,那我們要怎麼控制 Kubernetes(K8S)呢? 我們用 typescript 寫 CDK 當然就是直接寫 JSON 拉!是不是很神奇呢?快點跟著我看下去吧!

https://ithelp.ithome.com.tw/upload/images/20201012/201177017ZDM9OEOb9.png

部署目標

在部署前我們先整理一下這次目標需要什麼

  1. 部署一個服務名稱:hello-kubernetes
  2. replicas 設定為:3
  3. Docker Image 使用:paulbouwer/hello-kubernetes:1.5
  4. Container 內部 port 為:8080
  5. 設定一個 LoadBalancer 外部 port 為 80 對接內部的 hello-kubernetes 服務

部署 EKS Service

定義 Deployment

整理好後我們先設定 Deployment 以部署 Kubernetes(K8S)的 YAML 來說,我們要處理 Container 名稱、數量、內部 Port

apiVersion: apps/v1
kind: Deployment
metadata:
  name: hello-kubernetes
spec:
  replicas: 3
  selector:
    matchLabels: hello-kubernetes
  template:
    metadata:
      labels: hello-kubernetes
    spec:
      containers:
        - name: hello-kubernetes
          image: 'paulbouwer/hello-kubernetes:1.5'
          ports:
            - containerPort: 8080

但我們說過我們這次的部署不需要 YAML 所以我們直接使用 TypeScript 來寫

const appLabel = { app: "hello-kubernetes" };

const deployment = {
  apiVersion: "apps/v1",
  kind: "Deployment",
  metadata: { name: "hello-kubernetes" },
  spec: {
    replicas: 3,
    selector: { matchLabels: appLabel },
    template: {
      metadata: { labels: appLabel },
      spec: {
        containers: [
          {
            name: "hello-kubernetes",
            image: "paulbouwer/hello-kubernetes:1.5",
            ports: [{ containerPort: 8080 }],
          },
        ],
      },
    },
  },
};

Deployment 部署方法可以參考 kubernetes Documentation

定義 Service

而在部署 Kubernetes(K8S)的 Service YAML,需要處理 LoadBalancer 外部 port 與內部 Service 的對應

apiVersion: v1
kind: Service
metadata:
  name: hello-kubernetes
spec:
  type: LoadBalancer
  ports:
    - port: 80
      targetPort: 8080
  selector: hello-kubernetes

想好後轉換成 TypeScript 的程式

const service = {
  apiVersion: "v1",
  kind: "Service",
  metadata: { name: "hello-kubernetes" },
  spec: {
    type: "LoadBalancer",
    ports: [{ port: 80, targetPort: 8080 }],
    selector: appLabel,
  },
};

Service 部署方法可以參考 kubernetes Documentation

設定 Manifest

往常使用 Kubernetes(K8S)我們需要使用 kubectl apply -f hello-kubernetes.yaml 而在 CDK 我們可以直接使用 addManifest 塞入 cluster 就做完了!

cluster.addManifest("mypod", service, deployment);

顯示 Load Balancer

在使用 Kubernetes(K8S 需要使用 kubectl get rc,services 取得我們 Service 的位置,那使用 CDK 可以直接讓它在部署的時候用 Output 直接吐出來!從此可以不用再使用 kubectl 拉!ヽ(✿゚▽゚)ノ

new cdk.CfnOutput(this, "LoadBalancer", {
  value: cluster.getServiceLoadBalancerAddress("hello-kubernetes"),
});

測試

說了這麼多我們來部署測試看看吧!雖然上面說我們不需要用到 kubectl 不過我們還是需要用它來進行測試確定系統有沒有如所想的部署執行一下 cdk deploy 等它 15 ~ 30 分鐘吧! ( • ̀ω•́ )

執行完後的 Output:

Outputs:
CdkEksStack.LoadBalancer = aba304eea204b4239abb905439ac8ec7-1947016485.us-west-2.elb.amazonaws.com
CdkEksStack.eksConfigCommandDB09280A = aws eks update-kubeconfig --name eksB49B8EA3-7fcd6a64ed934f00ae43ec1cb67a87fa --region us-west-2 --role-arn arn:aws:iam::869989823608:role/CdkEksStack-mastersRole634808EE-1I504Q4EPLS70
CdkEksStack.eksGetTokenCommand8952195F = aws eks get-token --cluster-name eksB49B8EA3-7fcd6a64ed934f00ae43ec1cb67a87fa --region us-west-2 --role-arn arn:aws:iam::869989823608:role/CdkEksStack-mastersRole634808EE-1I504Q4EPLS70

AWS Console 的 Load balancer

可以看到在 AWS Load balancer 建立了一個 classic 的服務而他的 DNS 如上面的 CDK Output https://ithelp.ithome.com.tw/upload/images/20201009/20117701vyK5sehqiW.png

而在 Instances 可以看到我們的 Cluster 機器確實註冊在這個 Load balancer 後面 https://ithelp.ithome.com.tw/upload/images/20201009/20117701oJz1LTTEKH.png

Tags 可以看到有些設定被定義 https://ithelp.ithome.com.tw/upload/images/20201009/20117701MCGv87KYvN.png

查看 Kubernetes replicas 數量

可以使用 kubectl get pods 看到我們的 replicas 確實有設定為 3

$ kubectl get pods
NAME                                READY   STATUS    RESTARTS   AGE
hello-kubernetes-5655b546f8-cm66l   1/1     Running   0          49m
hello-kubernetes-5655b546f8-l8xkj   1/1     Running   0          49m
hello-kubernetes-5655b546f8-msjh6   1/1     Running   0          49m
mypod                               1/1     Running   0          97m

查看 Kubernetes Services 的部署狀態

使用 kubectl get rc,services 查看我們的 Load Balancer 確實也與 AWS Console 顯示的一樣

$ kubectl get rc,services
NAME                       TYPE           CLUSTER-IP      EXTERNAL-IP                                                               PORT(S)        AGE
service/hello-kubernetes   LoadBalancer   172.20.200.52   aba304eea204b4239abb905439ac8ec7-1947016485.us-west-2.elb.amazonaws.com   80:30139/TCP   35m
service/kubernetes         ClusterIP      172.20.0.1      <none>                                                                    443/TCP        3h35m

瀏覽一下 Kubernetes 部署的網頁

瀏覽一下剛剛部署的 LoadBalancer 可以看到我們的 pod id 每次都不一樣,如剛剛部署的 replicas 數量會有三個輪流吃到我們的 request

https://ithelp.ithome.com.tw/upload/images/20201009/20117701I7raDeZn4h.png

https://ithelp.ithome.com.tw/upload/images/20201009/20117701ezeaxLkeAi.png

https://ithelp.ithome.com.tw/upload/images/20201009/20117701L7V9fMO1DA.png

查看 pod log

雖然我們這邊主要不是介紹 Kubernetes 不過順便提一下如果想要看到 pod 的 log 可以使用 kubectl logs 帶上 id 查看 Log

$ kubectl logs hello-kubernetes-5655b546f8-cm66l
npm info it worked if it ends with ok
npm info using [email protected]
npm info using [email protected]
npm info lifecycle [email protected]~prestart: [email protected]
npm info lifecycle [email protected]~start: [email protected]

> [email protected] start /usr/src/app
> node server.js

Listening on: http://hello-kubernetes-5655b546f8-cm66l:8080
::ffff:10.0.255.11 - - [09/Oct/2020:11:17:29 +0000] "GET / HTTP/1.1" 200 667 "-" "curl/7.64.1"
::ffff:10.0.255.11 - - [09/Oct/2020:11:17:40 +0000] "GET /favicon.ico HTTP/1.1" 404 150 "http://aba304eea204b4239abb905439ac8ec7-1947016485.us-west-2.elb.amazonaws.com/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.121 Safari/537.36"

今天介紹 CDK 可以神奇的處理很多原本要用 kubectl 才可以處理的事情是不是超神奇的啊!!!

Day 25 – CDK 建置 Amazon Elastic Kubernetes Service(EKS)- Cluster

最近談了這麼多的 ECS 來換換口味來個 Amazon Elastic Kubernetes Service 吧!通常我們簡稱它為 Amazon EKS 或是 AWS EKS 後面我們會直接使用 AWS EKS 稱呼它

https://ithelp.ithome.com.tw/upload/images/20201012/201177017ZDM9OEOb9.png

AWS EKS

發布時間

在一開始我們先來簡單介紹一下 AWS EKS 這個服務,它在 2017/11/29 發布預覽版,還記得那個時候剛發布就去申請了 preview,初期版本滿簡單的而且不能外掛任何的模組

Amazon Elastic Container Service for Kubernetes 簡介 (預覽)

經過了半年多在 2018/06/05 發布了正式版

Amazon Elastic Container Service for Kubernetes 現在普遍可用

在一年後 2019/06/03 開始支援 AWS Fargate

使用 Amazon EKS 和 AWS Fargate 執行無伺服器 Kubernetes Pod

在 2020/03/10 也開始更新到 1.15 版

Amazon EKS 現在支援Kubernetes 1.15 版 – AWS

而在 2020/04/30 更新到 Kubernetes 1.16 版

Amazon EKS 現在支援 Kubernetes 1.16 版

價格

在 2020/01/21 之前的價格是每小時 0.20 USD 發布訊息後由原本的 0.20 USD 降至每小時 0.10 USD 價格降低了 50%,其實這個價格說貴不貴說便宜也不便宜因為自己起一台 AWS c5.large 也需要 0.10 USD 而且還需要自己顧這台 EC2

Amazon EKS 宣佈價格降低 50%

使用 CDK 建立 AWS EKS

簡單介紹了 AWS EKS 的歷史那我們就來使用 AWS CDK 建立 AWS EKS 吧!

AWS EKS 架構

在說明之前我們先來看一下我們今天要完成的目標架構圖,今天要完成的地方是紅色框框裡面的部分,有 Amazon EKS 本身跟 Kubernetes Cluster 讓它跑在 EC2 上面

https://ithelp.ithome.com.tw/upload/images/20201009/20117701S4aWHPvxdf.jpg

出處 AWS re:Invent 2019: [REPEAT 1] Amazon EKS under the hood (CON421-R1) PPT YouTube

CDK 程式

其實要使用 CDK 建立一個 EKS 非常的簡單只要一行就可以把整個非常龐大的 K8S 啟動

const cluster = new eks.Cluster(this, "eks", {
  version: eks.KubernetesVersion.V1_17,
});

如此配置會在後面創建一個新的 VPC 與兩台 m5.large 的機器

https://docs.aws.amazon.com/cdk/api/latest/docs/aws-eks-readme.html#managed-node-groups

https://docs.aws.amazon.com/cdk/api/latest/docs/aws-eks-readme.html#masters-role

EKS 建立 master role

如此配置 EKS 沒有辦法訪問,所以我們建立一個 Role 它會把 IAM 與 system:masters 進行一個綁定,來修改一下程式

const mastersRole = new iam.Role(this, "mastersRole", {
  assumedBy: new iam.AccountRootPrincipal(),
});

const cluster = new eks.Cluster(this, "eks", {
  version: eks.KubernetesVersion.V1_17,
  mastersRole,
});

修改 spot cluster

我在測試比較喜歡用 spot 的機器畢竟 spot 的機器比較便宜,所以我在這邊先做一些修改加入 spot 的設定,如果之前有看我的 ECS 介紹就會覺得這邊很熟悉我們只要跟 auto scaling group 說明 spot 的 price 就會幫我們啟動一個 spot 的 cluster 了!

這邊要注意原本的 cluster 也會啟動機器所以要把 defaultCapacity 設定為 0 不然會跑出三台呦!

const cluster = new eks.Cluster(this, "eks", {
  version: eks.KubernetesVersion.V1_17,
  defaultCapacity: 0,
  mastersRole,
});
    
cluster.addAutoScalingGroupCapacity("spot", {
  spotPrice: "0.1094",
  instanceType: new ec2.InstanceType("t3.large"),
});

部署測試

而因為整個架構比較複雜這邊的部署要先有心理準備,大概需要 15 分鐘到 30 分鐘的時間

部署完成後 CDK 很貼心的準備好要怎麼登入 EKS 的指令直接服用就可以了!

Outputs:
CdkEksStack.eksConfigCommandDB09280A = aws eks update-kubeconfig --name eksB49B8EA3-7fcd6a64ed934f00ae43ec1cb67a87fa --region us-west-2 --role-arn arn:aws:iam::888888888888:role/CdkEksStack-mastersRole634808EE-1I504Q4EPLS70
CdkEksStack.eksGetTokenCommand8952195F = aws eks get-token --cluster-name eksB49B8EA3-7fcd6a64ed934f00ae43ec1cb67a87fa --region us-west-2 --role-arn arn:aws:iam::888888888888:role/CdkEksStack-mastersRole634808EE-1I504Q4EPLS70

設定 kubeconfig

$ aws eks update-kubeconfig --name eksB49B8EA3-7fcd6a64ed934f00ae43ec1cb67a87fa --region us-west-2 --role-arn arn:aws:iam::888888888888:role/CdkEksStack-mastersRole634808EE-1I504Q4EPLS70
Added new context arn:aws:eks:us-west-2:888888888888:cluster/eksB49B8EA3-7fcd6a64ed934f00ae43ec1cb67a87fa to /Users/clarence/.kube/config

使用 kubectl

如此執行完我們就可以使用 kubectl 指令拉!執行 get node 可以看到我們有一台機器受 EKS 管理

$ kubectl get node
NAME                                        STATUS   ROLES    AGE   VERSION
ip-10-0-255-11.us-west-2.compute.internal   Ready    <none>   14m   v1.17.11-eks-cfdc40

AWS EKS Console

我們先到 AWS Console 看看 Clusters 出現了我剛剛創建的 EKS Cluster https://ithelp.ithome.com.tw/upload/images/20201009/20117701EYE20zxyvF.png

也可以在 EC2 看到唯一的一台 EC2 機器 https://ithelp.ithome.com.tw/upload/images/20201009/201177014Bf5hGYho1.png

  • Details

    • EKS 的 Details 裡面有 API server endpoint 與其他詳細的 EKS 資料 https://ithelp.ithome.com.tw/upload/images/20201009/20117701HaHczZwh8e.png
  • Compute

    • 如下可以看到 EKS 的 Compute https://ithelp.ithome.com.tw/upload/images/20201009/20117701YbUQBw8eDy.png
  • Networking

    • 另外還有 Networking 裡面有 4 個 Subnets https://ithelp.ithome.com.tw/upload/images/20201009/20117701MzEUEhaKfz.png
  • Logging

    • 目前我們沒有開啟 EKS 的 Logging 所以裡面都是 Disable 的 https://ithelp.ithome.com.tw/upload/images/20201009/20117701HKlRaR35D4.png
  • Update history https://ithelp.ithome.com.tw/upload/images/20201009/20117701CFiUNdgNNk.png

  • Tags https://ithelp.ithome.com.tw/upload/images/20201009/20117701ryjOfB1h35.png

參考資料

  • https://aws.amazon.com/tw/eks/

今天介紹 EKS 叢集如果熟悉 K8S 的朋友其實只需要如此就可以開始用一般的方法使用拉!

Day 24 – CDK 建置 Amazon Elastic Container Service(ECS) Service – 一個 Container 服務多個 Port

前面都只有介紹一個 Container 服務一個 Port 那大家是不是有一個疑問如果我的 Container 是多個 Port 的服務有沒有辦法用 ECS 嗎?答案是可以的呦!平常看到的範例都是多個 Port 就要開多個 Container 那我可以節省成本只用一個 Container 就好嗎?答案也是可以的呦!

今天就來教大家怎麼達成這件事情吧!

https://ithelp.ithome.com.tw/upload/images/20201010/20117701uiabPDJKWN.jpg

一個可以服務多個 Port 的程式

因為要測試多 Port 服務所以我寫了一個測試程式提供給大家測試,它可以顯示目前連線的網址與 Port 這支程式開的 Port 為:80、8080、8000 與 8888

https://ithelp.ithome.com.tw/upload/images/20201008/20117701LC0DYkGTZ3.png

建立 Fargate Task Definition

我們這次的 Task 與之前差不多,主要的重點是這次我們宣告了多個 container port

const taskDefinition = new ecs.FargateTaskDefinition(this, "TaskDef", {
  memoryLimitMiB: 512,
  cpu: 256,
});
const container = taskDefinition.addContainer("container", {
  image: ecs.ContainerImage.fromDockerImageAsset(asset),
  memoryLimitMiB: 512,
});
container.addPortMappings(
  {
    containerPort: 80,
  },
  {
    containerPort: 8888,
  },
  {
    containerPort: 8080,
  },
  {
    containerPort: 8000,
  }
);

如此在 Task Definition 就可以看到我們宣告的 Port Mappings 表

https://ithelp.ithome.com.tw/upload/images/20201008/20117701nVmoYXK0sF.png

設定 ALB

我們在 ALB 上面 Listen 剛剛的 80、8080、8000 與 8888

這邊有一個新的設定是 defaultAction 我們希望他在沒有 Rules 的時候做什麼事情,在後面我特別保留了 8888 port 沒有設定,這樣就可以看到它是什麼樣子了!

要注意這個值一定要設定不然會有問題呦!

https://ithelp.ithome.com.tw/upload/images/20201008/20117701D0QeicV0rV.png

const lb = new elbv2.ApplicationLoadBalancer(this, "LB", {
  vpc,
  internetFacing: true,
});
const listener1 = lb.addListener("Listener1", {
  port: 80,
  defaultAction: elbv2.ListenerAction.fixedResponse(200, {
    contentType: elbv2.ContentType.TEXT_PLAIN,
    messageBody: "OK",
  }),
});
const listener2 = lb.addListener("Listener2", {
  port: 8080,
  defaultAction: elbv2.ListenerAction.fixedResponse(200, {
    contentType: elbv2.ContentType.TEXT_PLAIN,
    messageBody: "OK",
  }),
});
const listener3 = lb.addListener("Listener3", {
  port: 8888,
  protocol: elbv2.ApplicationProtocol.HTTP,
  defaultAction: elbv2.ListenerAction.fixedResponse(200, {
    contentType: elbv2.ContentType.TEXT_PLAIN,
    messageBody: "OK",
  }),
});
const listener4 = lb.addListener("Listener4", {
  port: 8000,
  defaultAction: elbv2.ListenerAction.fixedResponse(200, {
    contentType: elbv2.ContentType.TEXT_PLAIN,
    messageBody: "OK",
  }),
});

註冊 Targets

我們分別把 Container 的 port 都跟 Listener 對應保留剛剛說的 8888 沒有設定對應 這邊有一個重點是 containerName 一定要與建立 Fargate Task Definition時設定的一樣,因為他們是靠同一個名稱做連結的

listener1.addTargets("ECS1", {
  port: 80,
  targets: [
    service.loadBalancerTarget({
      containerName: "container",
      containerPort: 80,
    }),
  ],
});

https://ithelp.ithome.com.tw/upload/images/20201008/20117701tkNPEZACpe.png

listener2.addTargets("ECS2", {
  port: 8080,
  targets: [
    service.loadBalancerTarget({
      containerName: "container",
      containerPort: 8080,
    }),
  ],
});

https://ithelp.ithome.com.tw/upload/images/20201008/20117701dgoLMD4PhF.png

listener4.addTargets("ECS4", {
  port: 8000,
  protocol: elbv2.ApplicationProtocol.HTTP,
  targets: [
    service.loadBalancerTarget({
      containerName: "container",
      containerPort: 8000,
    }),
  ],
});

https://ithelp.ithome.com.tw/upload/images/20201008/20117701LHpLCjOMm0.png

targets 使用同一個 IP

由上面三張圖我們可以觀察到,我們的 ECS Container 都是同一台

觀察一下

我們可以回到 ECS 的 Service 看一下我們只有一個 Service,而且只有一個 Running 的 Task https://ithelp.ithome.com.tw/upload/images/20201008/201177010R5iTJb2EQ.png

再到 ECS Task 確定只有一個 Task 在跑

https://ithelp.ithome.com.tw/upload/images/20201008/20117701JgKDNzEkpc.png

測試一下

我們可以到 ALB 的網址 http://cdkec-lb8a1-iyxzyuebmegp-816266764.us-west-2.elb.amazonaws.com:80/ 看到服務正常並且有讀到 Port

https://ithelp.ithome.com.tw/upload/images/20201008/20117701J4D24OA6ht.png

ALB 的網址 http://cdkec-lb8a1-iyxzyuebmegp-816266764.us-west-2.elb.amazonaws.com:8080/ 看到服務正常並且有讀到 Port

https://ithelp.ithome.com.tw/upload/images/20201008/20117701K2mE20rMYL.png

ALB 的網址 http://cdkec-lb8a1-iyxzyuebmegp-816266764.us-west-2.elb.amazonaws.com:8000/ 也是一樣的情況

https://ithelp.ithome.com.tw/upload/images/20201008/20117701sq69d4Lf0N.png

ALB 的網址 http://cdkec-lb8a1-iyxzyuebmegp-816266764.us-west-2.elb.amazonaws.com:8888/ 因為我們剛剛做了一點手腳所以只有回應 Default 的 OK

https://ithelp.ithome.com.tw/upload/images/20201008/20117701NiJm9JpP0a.png


今天是測試如果一台機器需要服務多個 Port 的範例希望有幫助到大家!

Day 23 – CDK 建置 Amazon Elastic Container Service(ECS) Service – Fargate 與 EC2 混搭

最近討論了 EC2、Spot 與 Fargate 有沒有想過我們是不是可以來一個混搭的系統呢? 混搭系統可以讓我們的系統可以更有彈性的被控制,像是今天可能有顯卡運算資源機器的需求我們就把它掛在 EC2 上面,一般服務型態的服務可以開 EC2 與 EC2 Spot 各半,排成服務就跑在 Fargate 上,流量高的時段就開 EC2 Spot 的機器,這樣聽起來是不是很完美呢 XD ~ 所以今天就來說明怎麼建構一個如此有彈性的系統吧!

https://ithelp.ithome.com.tw/upload/images/20201009/20117701ZCj2l2U57Z.jpg

建立一個 EC2 與 Fargate 混搭的 ECS Cluster

在開始說明之前先整理一下我們的目標,我們需要擁有三種不同的機器分別是

  • On-Demand Instance:t3.micro
  • Spot Instance:t3.small
  • Fargate

模擬服務可以在不同的機器上面運作,所以需要開三種不同的 Task 分別是

  • EC2
  • EC2 Spot
  • Fargate

建立 ECS Cluster

首先處理 ECS Cluster 並且設定 On-Demand Instance 的 ASG 與 Spot Instance 的 ASG,分別對應到機器等級

  • On-Demand Instance:t3.micro
  • Spot Instance:t3.small

Fargate 不用自己控制機器所以這邊不用處理

const cluster = new ecs.Cluster(this, "EcsCluster", { vpc });

const autoScalingGroup = cluster.addCapacity(
  "DefaultAutoScalingGroupCapacity",
  {
    instanceType: ec2.InstanceType.of(
      ec2.InstanceClass.T3,
      ec2.InstanceSize.MICRO
    ),
    desiredCapacity: 1,
    machineImage: ecs.EcsOptimizedImage.amazonLinux2(),
  }
);
autoScalingGroup.scaleOnCpuUtilization("KeepCpuHalfwayLoaded", {
  targetUtilizationPercent: 50,
});

const spotAutoScalingGroup = cluster.addCapacity(
  "SpotAutoScalingGroupCapacity",
  {
    instanceType: ec2.InstanceType.of(
      ec2.InstanceClass.T3,
      ec2.InstanceSize.SMALL
    ),
    desiredCapacity: 1,
    machineImage: ecs.EcsOptimizedImage.amazonLinux2(),
    spotPrice: "0.0104",
    spotInstanceDraining: true,
  }
);
spotAutoScalingGroup.scaleOnCpuUtilization("KeepCpuHalfwayLoaded", {
  targetUtilizationPercent: 50,
});

建立完後可以在 ECS Instances 看到有兩種不同大小的機器註冊

https://ithelp.ithome.com.tw/upload/images/20201007/20117701DvR0CJmlbq.png

建立 Task Definition

使用 Constraints 約束 Task

因為要實現不同類型的機器跑不同的服務這邊介紹一個新的參數 Constraints 它可以用來對 Task 做一些約束像是機器等級系統種類是否有掛 EFS 等等或是一些自訂參數,但是目前 AWS CDK 還不支援設定設定 Attributes 所以自動參數的部分這次沒有辦法做給大家看有興趣可以參考 AWS 文件直接在 console 或是使用 command 實作

使用 TaskDefinition 定義共用 Task

除了 Constraints 這次範例還使用了一個新的 function TaskDefinition 它有一個很好用的屬性 compatibility 使用 EC2_AND_FARGATE 就可以建立一個不管是 EC2 或是 Fargate 都可以一起共用的 Task

AWS VPC

另外還有使用一個特別的 Network Mode AWS VPC 它可以把 ENI 網卡直接掛在 Container 上面,往常我們如果直接使用 EC2 部署沒有使用 Load Balance 是沒有辦法兩個 Container 使用同一個 port 的,畢竟在系統上只有一個 process 可以 bind 一個 port,而使用了 AWS VPC 就不會發生這個問題了,因為大家都可以直接使用自己的獨立網卡拉!

查看 ECS Instance Attributes

上面有提到要使用 Constraints 約束機器可是可以約束的條件要從哪裡找呢?我們可以到 AWS Console ECS 找到 EC2 Instances,在裡面選擇一台機器按下 Actions -> View/Edit Attributes 就可以看到了,如下兩張圖片是我們開設的機器 Attributes

https://ithelp.ithome.com.tw/upload/images/20201007/20117701m8ZbIua97v.png

如圖可以看到機器等級是t3.micro

https://ithelp.ithome.com.tw/upload/images/20201007/20117701kqrC7l7xgU.png

如圖可以看到機器等級是t3.small

設定 Fargate Task

Fargate 雖然不受 Cluster 管理不過我們一樣要設定 Task

const fargateTaskDefinition = new ecs.FargateTaskDefinition(
  this,
  "FargateTaskDef",
  {
    cpu: 256,
    memoryLimitMiB: 512,
  }
);

https://ithelp.ithome.com.tw/upload/images/20201007/20117701TCEDRvljgH.png

可以在 Task 看到 Network Mode 設定為 awsvpc

https://ithelp.ithome.com.tw/upload/images/20201007/2011770194n2yUnDrR.png

可以看到 Constraint 裡面是空的

設定限制機器等級 t3.micro 的 Task Definition

在這邊使用了上面說的 TaskDefinition 所以需要設定 compatibility 為 EC2,不然就要如下使用 Ec2TaskDefinition 不過其實都可以

在前面我們設定 On-Demand Instance 等級為 t3.micro,所以 placementConstraints 設定機器等級限制 t3.micro

const taskDefinition = new ecs.TaskDefinition(this, "TaskDef", {
  cpu: "256",
  memoryMiB: "512",
  compatibility: ecs.Compatibility.EC2,
  networkMode: ecs.NetworkMode.AWS_VPC,
  placementConstraints: [
    ecs.PlacementConstraint.memberOf(
      "attribute:ecs.instance-type == t3.micro"
    ),
  ],
});

https://ithelp.ithome.com.tw/upload/images/20201007/20117701PUwO0IUMFa.png

可以在 Task 看到 Network Mode 設定為 awsvpc

https://ithelp.ithome.com.tw/upload/images/20201007/20117701EFGTqgXzTD.png

可以在 Constraint 看到限制 type 等級 t3.micro

設定限制機器等級 t3.small 的 EC2 Task Definition

而 Spot Instance 等級為 t3.small,所以在 placementConstraints 設定機器等級限制 t3.small

const spotTaskDefinition = new ecs.Ec2TaskDefinition(this, "SpotTaskDef", {
  networkMode: ecs.NetworkMode.AWS_VPC,
  placementConstraints: [
    ecs.PlacementConstraint.memberOf(
      "attribute:ecs.instance-type == t3.small"
    ),
  ],
});

https://ithelp.ithome.com.tw/upload/images/20201007/20117701zE6oJ9ruIC.png

可以在 Task 看到 Network Mode 設定為 awsvpc

https://ithelp.ithome.com.tw/upload/images/20201007/20117701t89389R4YB.png

可以在 Constraint 看到限制 type 等級 t3.small

在 ECS Tasks 檢查設定

可以在 ECS Tasks 看到上面設定的所有 Task

https://ithelp.ithome.com.tw/upload/images/20201007/201177016GZ50ndNNr.png

建立 Container

這次範例使用一樣的 image 來建立範例,不過我們分別使用了不同的 Task Definition 來限制不同的機器種類

const fargateContainer = fargateTaskDefinition.addContainer(
  "WebContainer",
  {
    image: ecs.ContainerImage.fromRegistry("amazon/amazon-ecs-sample"),
    memoryLimitMiB: 512,
  }
);
fargateContainer.addPortMappings({
  containerPort: 80,
});

const container = taskDefinition.addContainer("WebContainer", {
  image: ecs.ContainerImage.fromRegistry("amazon/amazon-ecs-sample"),
  memoryLimitMiB: 512,
});
container.addPortMappings({
  containerPort: 80,
});

const spotContainer = spotTaskDefinition.addContainer("WebContainer", {
  image: ecs.ContainerImage.fromRegistry("amazon/amazon-ecs-sample"),
  memoryLimitMiB: 512,
});
spotContainer.addPortMappings({
  containerPort: 80,
});

設定 Service

建立三種不同的 Service 分別管理不同機器等級的 Task

const fatgetService = new ecs.FargateService(this, "FargateService", {
  cluster,
  taskDefinition: fargateTaskDefinition,
});

const ecsService = new ecs.Ec2Service(this, "Ec2Service", {
  cluster,
  taskDefinition,
});

const spotEcsService = new ecs.Ec2Service(this, "SpotEc2Service", {
  cluster,
  taskDefinition: spotTaskDefinition,
});

https://ithelp.ithome.com.tw/upload/images/20201007/201177018tbnpB3bLA.png

建立 ALB

為了方便測試來把三種 Service 加入一樣的 target 吧!如果混搭的目標是用不同的機器跑不同的 Service 就可以分別投入對應的 Load Balancer 拉!

const lb = new elbv2.ApplicationLoadBalancer(this, "LB", {
  vpc,
  internetFacing: true,
});
const listener = lb.addListener("Listener", { port: 80 });

listener.addTargets("ECS2", {
  port: 80,

  targets: [fatgetService, ecsService, spotEcsService],
});

測試

這次也是測試網頁而已就不多提了 XD https://ithelp.ithome.com.tw/upload/images/20201007/20117701H0SV5b4Cpc.png


今天是一個機器混搭的測試,如此設定就可以在 ECS 部署上有更多的彈性

Day 22 – CDK 建置 Amazon Elastic Container Service(ECS) Service – Fargate

今天來介紹 ECS 使用 Fargate 它是一個由 AWS 直接管理 Container 的服務,之前介紹的 EC2 使用 ECS 需要自己管理 Host 的部分,使用了 Fargate 連 Host 都不用自己管理了,聽起來是不是很吸引人呢 ヽ(●´∀`●)ノ

https://ithelp.ithome.com.tw/upload/images/20201008/20117701YXiGtZFHZF.jpg

ECS 使用 Fargate 的好處

不用管理 ECS agent

使用 Fargate 除了 Host 不用自己管理之外還有什麼好處呢?另外一個是 ECS agent 的部分,在之前的文章沒有介紹到其實 EC2 如果要跑 ECS 上面會跑有 ECS 的 Agent 而這個版本常常會更新(如左下角),如果更新就需要用右上角的 Update agent 去更新,在更新的時候其實 ECS 會把目前這台機器上所有的 Container 移走那可能就需要自己注意會不會有服務不穩定的問題,如果使用 Fargate 就不用擔心這件事情,因為 AWS 會處理好這件事情

https://ithelp.ithome.com.tw/upload/images/20201006/20117701eozvve49xX.png

不會有資源浪費

如果自己維護 ECS host 還會遇到像是一台機器可能有 100 vCPU 而我們今天全部的 ECS conatiner 只用到了 80 vCPU 剩下的 20 vCPU 還是需要付費的,而使用 Fargate 就沒有這個問題,因為 Fargate 計價是直接計算 vCPU 與記憶體計價的,用多少拿多少減少資源浪費

用 EC2 沒有好處嗎?

其實也不能說沒有好處,因為如果我們有特別需求需要進入 Host 查看東西或是自己有更多的調教 Host 方法這樣在 Fargate 機器上就沒有辦法實作,所以基本上還是看有需求決定應該怎麼選擇機器

CDK 使用 Fargate

定義 Cluster

就算使用 Fargate 我們還是需要一個 Cluster 來管理我們的 Fargate 機器

const cluster = new ecs.Cluster(this, "EcsCluster", { vpc });

定義 Fargate Task Definition

Fargate 需要定義自己的 Task Definition 與昨天的 EC2 一樣需要先定義 Task Definition

const taskDefinition = new ecs.FargateTaskDefinition(this, "TaskDef");

https://ithelp.ithome.com.tw/upload/images/20201006/20117701UFJ2oimJld.png

定義 Container

今天的測試一樣使用 amazon/amazon-ecs-sample 來當測試腳本

const container = taskDefinition.addContainer("WebContainer", {
  image: ecs.ContainerImage.fromRegistry("amazon/amazon-ecs-sample"),
  memoryLimitMiB: 512,
});
container.addPortMappings({
  containerPort: 80,
});

定義 Fargate Service

把剛剛定義好的 Cluster 與 Task Definition 組合成一個 Service

const fatgetService = new ecs.FargateService(this, "FargateService", {
  cluster,
  taskDefinition,
});

https://ithelp.ithome.com.tw/upload/images/20201006/20117701qQ10GKW9z0.png

定義 ALB

為了測試需要我們還是設定一個 ALB 好讓我們在外面可直接戳 DNS 拿到結果

const lb = new elbv2.ApplicationLoadBalancer(this, "LB", {
  vpc,
  internetFacing: true,
});
const listener = lb.addListener("Listener", { port: 80 });
listener.addTargets("ECS", {
  port: 80,
  targets: [fatgetService],
});

觀察一下 ECS Instances

我們可以看一下如果是使用 Fargate 的 task 就不會在 ECS Instances 的地方看到任何機器囉!

https://ithelp.ithome.com.tw/upload/images/20201006/20117701HCagSxayOU.png

整理一下

其實整個程式量真的很少大概 30 行就可以起一個可以用的 Web 服務而且是高可用性的真的很簡單 ~

const vpc = new ec2.Vpc(this, "Vpc", { maxAzs: 3, natGateways: 1 });

const cluster = new ecs.Cluster(this, "EcsCluster", { vpc });

const taskDefinition = new ecs.FargateTaskDefinition(this, "TaskDef");

const container = taskDefinition.addContainer("WebContainer", {
  image: ecs.ContainerImage.fromRegistry("amazon/amazon-ecs-sample"),
  memoryLimitMiB: 512,
});
container.addPortMappings({
  containerPort: 80,
});

const fatgetService = new ecs.FargateService(this, "FargateService", {
  cluster,
  taskDefinition,
});

const lb = new elbv2.ApplicationLoadBalancer(this, "LB", {
  vpc,
  internetFacing: true,
});
const listener = lb.addListener("Listener", { port: 80 });
listener.addTargets("ECS", {
  port: 80,
  targets: [fatgetService],
});

測試 ALB

最後我們還是測試一下 ALB 的結果,可想而知一定也是正常拉 XD https://ithelp.ithome.com.tw/upload/images/20201006/20117701qpFuuZq221.png


今天教大家實作 Fargate 機器,其實如果是小團隊我滿推薦使用 Fargate 的因為他的價錢其實跟實際開一台一樣等級的機器差不多,而且可以減少很多麻煩事 XD ~

Day 21 – CDK 建置 Amazon Elastic Container Service(ECS)Service – EC2 與 RDS

我們的 ECS Container 不會只有單存一個服務自己跑,通常都會需要與資料庫做一個連結,所以今天就來說明如何讓 ECS Container 與 RDS 連結

https://ithelp.ithome.com.tw/upload/images/20201007/20117701sUxMGXcnPF.jpg

建立一個 ECS 與 RDS 關聯的 Web 服務

建立 RDS

先建立一個 RDS 與前面不一樣的地方是不直接指定密碼,而是讓 RDS 直接產生密碼到 AWS Secrets Manager,如此是比較安全的做法因為我們的程式不會保管密碼,也不會再部署的過程中看到密碼,要取得密碼就需要經過 Secrets Manager

const rdsInstance = new rds.DatabaseInstance(this, "Database", {
  engine: rds.DatabaseInstanceEngine.mysql({
    version: rds.MysqlEngineVersion.VER_8_0_19,
  }),
  vpc,
  deleteAutomatedBackups: true,
  instanceType: ec2.InstanceType.of(
    ec2.InstanceClass.BURSTABLE3,
    ec2.InstanceSize.MICRO
  ),
  allocatedStorage: 10,
  credentials: {
    username: "admin",
  },
});

要取得資料庫需要經過 Secrets Manager 而取得的過程需要使用 secretArn

new cdk.CfnOutput(this, "DatabaseSecretArn", {
  value: rdsInstance.secret!.secretArn,
});

如此資料庫的地方就完畢了

建立 ECS Cluster

因為測試使用就建立一個 ASG 就好

const cluster = new ecs.Cluster(this, "EcsCluster", { vpc });

const autoScalingGroup = cluster.addCapacity(
  "DefaultAutoScalingGroupCapacity",
  {
    instanceType: ec2.InstanceType.of(
      ec2.InstanceClass.T3,
      ec2.InstanceSize.MICRO
    ),
    minCapacity: 1,
    desiredCapacity: 1,
    maxCapacity: 6,
    machineImage: ecs.EcsOptimizedImage.amazonLinux2(),
    spotPrice: "0.0136",
    spotInstanceDraining: true,
  }
);
autoScalingGroup.scaleOnCpuUtilization("KeepCpuHalfwayLoaded", {
  targetUtilizationPercent: 50,
});

建立 Docker

這邊我們使用 DockerImageAsset 來讓本機跑一個 Dockerfile 編譯一個 Docker Image 上傳到 ECR 上面

const asset = new DockerImageAsset(this, "BuildImage", {
  directory: path.join(__dirname, "../", "image"),
});

而這邊為了方便理解 Secrets Manager 是如何讓 ECS 吃到的密碼所以我寫了一個簡單的 Web,可以用來看所有 Docker 吃進去的 environment

所以只要建立一個資料夾 image 並且下載 web-app-env 就可以了

mkdir image 
git clone https://github.com/clarencetw/web-app-env.git

https://ithelp.ithome.com.tw/upload/images/20201004/20117701acEN8tNE6U.png

Github: web-app-env

上傳成功的 Docker 可以在 ECR 看到它

https://ithelp.ithome.com.tw/upload/images/20201004/20117701iFAeqpLPXM.png

建立 Task Definition

這次的 Task 主要定義了普通的 environment

  • NODE_ENV

與 secret 而 secret 可以使用 ecs.Secret.fromSecretsManager(rdsInstance.secret!) 可以直接解出整個 rdsInstance.secret 的 JSON,而我這邊就直接把 rdsInstance 裡面的 JSON 直接解出來在程式使用上比較方便

  • DB_ENGINE
  • DB_HOST
  • DB_PORT
  • DB_USERNAME
  • DB_PASSWORD
const taskDefinition = new ecs.Ec2TaskDefinition(this, "TaskDef");

const container = taskDefinition.addContainer("DefaultContainer", {
  image: ecs.ContainerImage.fromDockerImageAsset(asset),
  memoryLimitMiB: 16,
  logging: ecs.LogDrivers.awsLogs({ streamPrefix: "cdk-ecs" }),
  environment: {
    NODE_ENV: "production",
  },
  secrets: {
    DB_ENGINE: ecs.Secret.fromSecretsManager(rdsInstance.secret!, "engine"),
    DB_HOST: ecs.Secret.fromSecretsManager(rdsInstance.secret!, "host"),
    DB_PORT: ecs.Secret.fromSecretsManager(rdsInstance.secret!, "port"),
    DB_USERNAME: ecs.Secret.fromSecretsManager(
      rdsInstance.secret!,
      "username"
    ),
    DB_PASSWORD: ecs.Secret.fromSecretsManager(
      rdsInstance.secret!,
      "password"
    ),
  },
});
container.addPortMappings({
  containerPort: 80,
});

設定完的 Task 可以直接在 ECS 看到指定成 Secrets Manager

https://ithelp.ithome.com.tw/upload/images/20201004/20117701hpI5aFkOFd.png

設定 ECS Service

const ecsService = new ecs.Ec2Service(this, "Service", {
  cluster,
  taskDefinition,
});

設定 ALB 服務

其實我們的服務只要在 CDK 建立成 service 在 LB 就很好控制了,我們只要在 targets 把 ecsService 放進去就可以了

const lb = new elbv2.ApplicationLoadBalancer(this, "LB", {
  vpc,
  internetFacing: true,
});
const listener = lb.addListener("Listener", { port: 80 });
const targetGroup = listener.addTargets("ECS", {
  port: 80,
  targets: [ecsService],
});

設定 security group

這邊要注意一下我們需要允許 ECS Service 可以存取 RDS,如果沒有設定會不能連線的

rdsInstance.connections.allowFrom(ecsService, ec2.Port.tcp(3306));

整理一下

const vpc = new ec2.Vpc(this, "Vpc", { maxAzs: 3, natGateways: 1 });

const rdsInstance = new rds.DatabaseInstance(this, "Database", {
  engine: rds.DatabaseInstanceEngine.mysql({
    version: rds.MysqlEngineVersion.VER_8_0_19,
  }),
  vpc,
  deleteAutomatedBackups: true,
  instanceType: ec2.InstanceType.of(
    ec2.InstanceClass.BURSTABLE3,
    ec2.InstanceSize.MICRO
  ),
  allocatedStorage: 10,
  credentials: {
    username: "admin",
  },
});

const cluster = new ecs.Cluster(this, "EcsCluster", { vpc });

const autoScalingGroup = cluster.addCapacity(
  "DefaultAutoScalingGroupCapacity",
  {
    instanceType: ec2.InstanceType.of(
      ec2.InstanceClass.T3,
      ec2.InstanceSize.MICRO
    ),
    minCapacity: 1,
    desiredCapacity: 1,
    maxCapacity: 6,
    machineImage: ecs.EcsOptimizedImage.amazonLinux2(),
    spotPrice: "0.0136",
    spotInstanceDraining: true,
  }
);
autoScalingGroup.scaleOnCpuUtilization("KeepCpuHalfwayLoaded", {
  targetUtilizationPercent: 50,
});

const asset = new DockerImageAsset(this, "BuildImage", {
  directory: path.join(__dirname, "../", "image"),
});

const taskDefinition = new ecs.Ec2TaskDefinition(this, "TaskDef");

const container = taskDefinition.addContainer("DefaultContainer", {
  image: ecs.ContainerImage.fromDockerImageAsset(asset),
  memoryLimitMiB: 16,
  logging: ecs.LogDrivers.awsLogs({ streamPrefix: "cdk-ecs" }),
  environment: {
    NODE_ENV: "production",
  },
  secrets: {
    DB_ENGINE: ecs.Secret.fromSecretsManager(rdsInstance.secret!, "engine"),
    DB_HOST: ecs.Secret.fromSecretsManager(rdsInstance.secret!, "host"),
    DB_PORT: ecs.Secret.fromSecretsManager(rdsInstance.secret!, "port"),
    DB_USERNAME: ecs.Secret.fromSecretsManager(
      rdsInstance.secret!,
      "username"
    ),
    DB_PASSWORD: ecs.Secret.fromSecretsManager(
      rdsInstance.secret!,
      "password"
    ),
  },
});
container.addPortMappings({
  containerPort: 80,
});

const ecsService = new ecs.Ec2Service(this, "Service", {
  cluster,
  taskDefinition,
});

const lb = new elbv2.ApplicationLoadBalancer(this, "LB", {
  vpc,
  internetFacing: true,
});
const listener = lb.addListener("Listener", { port: 80 });
const targetGroup = listener.addTargets("ECS", {
  port: 80,
  targets: [ecsService],
});

rdsInstance.connections.allowFrom(ecsService, ec2.Port.tcp(3306));

今天的服務主要是教大家如何在 ECS 使用一個可以連接 RDS 的服務,希望有幫助到大家 ~

Day 20 – CDK 建置 Amazon Elastic Container Service(ECS)Service – EC2

昨天說明了如何控制 ECS 的 Cluster,今天介紹如何使用 ECS Service

https://ithelp.ithome.com.tw/upload/images/20201006/20117701iaCrWBv0mG.jpg

要跑服務在 ECS 上面就會使用到 ECS Service,因為我們需要一個服務去管理我們的 Task 讓它可以做到自動擴展或是死掉重啟等功能

今天目標是如何在 ECS 上面跑一個 amazon/amazon-ecs-sample 的 container,並且讓 ALB 的 80 port 可以直接使用這個 container 的 Service

使用 ECS 建立網頁服務

建立 Ec2TaskDefinition

在建立 Service 之前我們需要先建立一個 EC2 Task Definition,一個 Task 裡面可以有很多的 Container,感覺就像把多個 Container 服務做一個打包,而通常會把有關係的服務一起打包或是內部需要相互呼叫的服務

const taskDefinition = new ecs.Ec2TaskDefinition(this, "TaskDef");

在 EC2 Task Definition 建立 Container

以目前目標只需要在 Task Definition 加入一個 Container

image

image 使用前面說的 amazon/amazon-ecs-sample

如果想要知道這個 image 主要內容是什麼可以看一下 Github repo

memory limit

這裏設定為 16 M 可以抓一下平常需要用的記憶體大小來設定這個值

logging

我比較喜歡使用 AWS Logs 所以先介紹它,這邊需要設定一個 streamPrefix 設定一個好辨識服務的值

設定完後的 Log 在 CloudWatch 格式

prefix-name/container-name/ecs-task-id

詳細可以參考文件

其實我滿喜歡用 AWS Logs 的其中一個原因是可以直接在 ECS Service 下面就直接看到 Logs 對於 Debug 滿有幫助的

https://ithelp.ithome.com.tw/upload/images/20201003/20117701EsV4VrZ5Ic.png

另一個觀看 Logs 的方式是直接到 CloudWatch

大家可以觀察一下剛剛設定的 prefix-name 範例設定為 cdk-ecs 所以 Log 就變成了 cdk-ecs/DefaultContainer/a3d16bae-7c8f-4987-a1e7-ea67a6ac6a9c

https://ithelp.ithome.com.tw/upload/images/20201003/20117701ThwXXAjrh4.png

container port

我們需要先設定 container 內部 port 因為等等我們要設定 ALB 跟它做一個對應

const container = taskDefinition.addContainer("DefaultContainer", {
  image: ecs.ContainerImage.fromRegistry("amazon/amazon-ecs-sample"),
  memoryLimitMiB: 16,
  logging: ecs.LogDrivers.awsLogs({ streamPrefix: "cdk-ecs" }),
});
container.addPortMappings({
  containerPort: 80,
});

設定完後可以在 ECS Service 裡面看到它

https://ithelp.ithome.com.tw/upload/images/20201003/201177011YMYgugum0.png

設定 ECS Service

設定完 ECS Task Definition 後我們需要把 Cluster 與 Task 經由 Service 做一個連接,讓它成為一個 ECS Service

const ecsService = new ecs.Ec2Service(this, "Service", {
  cluster,
  taskDefinition,
});

部署之後就可以看到 Cluster 上面跑了一個 CdkEcsStack-Service 而它的 Task Definition 為 CdkEcsStackTaskDef

https://ithelp.ithome.com.tw/upload/images/20201003/20117701Lzspw8AepH.png

設定 ALB

最後設定 ALB 把整個服務串起來,設定方法與之前介紹 ALB 大同小異,大家忘記的可以回去參考一下之前的內容

const lb = new elbv2.ApplicationLoadBalancer(this, "LB", {
  vpc,
  internetFacing: true,
});
const listener = lb.addListener("Listener", { port: 80 });
const targetGroup = listener.addTargets("ECS", {
  port: 80,
  targets: [ecsService],
});

整理一下今天的內容

有一個地方忘記提到我們的 ECS Cluster 預設會部署在 private subnet 所以要設定 NAT Gateway 為 1 不然會有問題呦!

const vpc = new ec2.Vpc(this, "Vpc", { maxAzs: 3, natGateways: 1 });

const cluster = new ecs.Cluster(this, "EcsCluster", { vpc });

const autoScalingGroup = cluster.addCapacity(
  "DefaultAutoScalingGroupCapacity",
  {
    instanceType: ec2.InstanceType.of(
      ec2.InstanceClass.T3,
      ec2.InstanceSize.MICRO
    ),
    minCapacity: 1,
    desiredCapacity: 1,
    maxCapacity: 6,
    machineImage: ecs.EcsOptimizedImage.amazonLinux2(),
    spotPrice: "0.0136",
    spotInstanceDraining: true,
  }
);
autoScalingGroup.scaleOnCpuUtilization("KeepCpuHalfwayLoaded", {
  targetUtilizationPercent: 50,
});

const taskDefinition = new ecs.Ec2TaskDefinition(this, "TaskDef");

const container = taskDefinition.addContainer("DefaultContainer", {
  image: ecs.ContainerImage.fromRegistry("amazon/amazon-ecs-sample"),
  memoryLimitMiB: 16,
  logging: ecs.LogDrivers.awsLogs({ streamPrefix: "cdk-ecs" }),
});
container.addPortMappings({
  containerPort: 80,
});

const ecsService = new ecs.Ec2Service(this, "Service", {
  cluster,
  taskDefinition,
});

const lb = new elbv2.ApplicationLoadBalancer(this, "LB", {
  vpc,
  internetFacing: true,
});
const listener = lb.addListener("Listener", { port: 80 });
const targetGroup = listener.addTargets("ECS", {
  port: 80,
  targets: [ecsService],
});

測試服務

使用我們的 ALB 網址連進去就可以看到一個準備好的 Simple PHP App 測試頁面

https://ithelp.ithome.com.tw/upload/images/20201003/20117701sxxW5uAmmH.png


今天主要把整個 ECS 要部署一個服務的方法介紹一遍,希望可以幫助除了在寫 CDK 的朋友也可以幫助到正在學習 ECS 的朋友 XD ~

Day 19 – CDK 建置 Amazon Elastic Container Service(ECS)Cluster

今天來介紹一個新的服務 Amazon Elastic Container Service 通常簡稱它為 ECS,它是一個可以幫助使用者建置微服務叢集的服務,它可以同時使用三種不同的機器 Amazon EC2、AWS Fargate 與 Spot 做一個混搭,我覺得是一個滿用的服務,如果服務的大小沒有龐大到需要用到 Amazon Elastic Kubernetes Service(Amazon EKS)其實可以考慮使用 ECS 就好

https://ithelp.ithome.com.tw/upload/images/20201005/201177016kvIdww2oy.jpg

建立 ECS 叢集

今天會先用比較簡單的方法先說明 ECS 叢集

要建立 ECS 服務之前要先建立一個 ECS 叢集,可以把這個叢集當成是一個管理器而它不用另外付費這點與 EKS 不太一樣,而建立完成如下:

https://ithelp.ithome.com.tw/upload/images/20201003/20117701l5m2xwUVIS.png

使用 CDK 建立 ECS Cluster

其實 CDK 封裝的很簡單,只要實體化它就 OK 了!而命名方法一樣會由 CDK 會用我們的 id 去自動產生

import * as ecs from "@aws-cdk/aws-ecs";

const cluster = new ecs.Cluster(this, "EcsCluster", { vpc });

使用 EC2 建立 Auto Scaling Group

在 ECS 中我們可以用 ECS 裡面提供的 addCapacity 來建立我們的 EC2 機器,我們可以在裡面選擇機器的等級、數量與 Image 等 …… 另外還可以宣告它是 spot,像是此範例宣告的機器等級是 m3.micro,它每小時的價錢是 $0.0136 所以我這邊就設定他的 spot price 為 0.0136,在這邊 AWS 會使用目前最便宜的價錢來計算所以 spot price 只要使用一個大概價錢就好

const autoScalingGroup = cluster.addCapacity(
  "DefaultAutoScalingGroupCapacity",
  {
    instanceType: ec2.InstanceType.of(
      ec2.InstanceClass.T3,
      ec2.InstanceSize.MICRO
    ),
    minCapacity: 1,
    desiredCapacity: 1,
    maxCapacity: 6,
    machineImage: ecs.EcsOptimizedImage.amazonLinux2(),
    spotPrice: "0.0136",
    spotInstanceDraining: true,
  }
);

部署完後我們可以在 Launch configurations 看到目前的設定與 Spot price 的費用

https://ithelp.ithome.com.tw/upload/images/20201003/20117701nNUk4dvQxd.png

而如果想要看目前的價錢可以按 Copy launch configuration 就可以看到目前的 spot 價錢了

https://ithelp.ithome.com.tw/upload/images/20201003/20117701dtETxmkfwq.png

如此的設定其實沒有設定目前在什麼情況下會做擴展,所以我們還是需要對它設定一下,以目前的需求設定 CPU 50% 好了!

autoScalingGroup.scaleOnCpuUtilization("KeepCpuHalfwayLoaded", {
  targetUtilizationPercent: 50,
});

設定完後一樣可以在 Auto Scaling groups -> Automatic scaling 看到結果

https://ithelp.ithome.com.tw/upload/images/20201003/201177016Fx1IuAJsZ.png

ECS 的 User Data

另外可以在 ASG 看一下 ECS 預設的 User Data

https://ithelp.ithome.com.tw/upload/images/20201003/20117701snvrtW0eOH.png

#!/bin/bash
echo ECS_CLUSTER=CdkEcsStack-EcsCluster97242B84-TSqMfSQQWNA2 >> /etc/ecs/ecs.config
sudo iptables --insert FORWARD 1 --in-interface docker+ --destination 169.254.169.254/32 --jump DROP
sudo service iptables save
echo ECS_AWSVPC_BLOCK_IMDS=true >> /etc/ecs/ecs.config

使用 autoscaling 再建立一個擴展機制

除了原本 ECS 提供的方法之外我們其實還可以自己使用 autoscaling 建立一個擴展機制,建立多個的原因是可以能不同場合可以設定不同的機器大小或是不同條件來讓整個叢集可以更符合實際情況

const asg = new autoscaling.AutoScalingGroup(this, "ASG", {
  instanceType: ec2.InstanceType.of(
    ec2.InstanceClass.T3,
    ec2.InstanceSize.MICRO
  ),
  machineImage: ecs.EcsOptimizedImage.amazonLinux2(),
  updateType: autoscaling.UpdateType.REPLACING_UPDATE,
  minCapacity: 1,
  desiredCapacity: 1,
  maxCapacity: 6,
  vpc,
});

建立完後加入到 ECS

cluster.addAutoScalingGroup(asg);

可以在 Auto Scaling groups 看到設定

https://ithelp.ithome.com.tw/upload/images/20201003/20117701aBfOGaORsQ.png

Launch configurations 看到 Spot price 是空的,代表它不是 spot,通常我們不會把所有機器都做成 spot 畢竟 spot 機器有一定的機率會被抽走,這樣可能就會造成服務有中斷的風險

https://ithelp.ithome.com.tw/upload/images/20201003/20117701SAUOpc7392.png

ECS Instances

最後可以到 ECS Instances 看一下註冊上去的機器

https://ithelp.ithome.com.tw/upload/images/20201003/20117701zlxaD0jfhF.png

把整個 CDK 做一個整理

const vpc = new ec2.Vpc(this, "Vpc", { maxAzs: 3, natGateways: 1 });

const cluster = new ecs.Cluster(this, "EcsCluster", { vpc });

const autoScalingGroup = cluster.addCapacity(
  "DefaultAutoScalingGroupCapacity",
  {
    instanceType: ec2.InstanceType.of(
      ec2.InstanceClass.T3,
      ec2.InstanceSize.MICRO
    ),
    minCapacity: 1,
    desiredCapacity: 1,
    maxCapacity: 6,
    machineImage: ecs.EcsOptimizedImage.amazonLinux2(),
    spotPrice: "0.0136",
    spotInstanceDraining: true,
  }
);
autoScalingGroup.scaleOnCpuUtilization("KeepCpuHalfwayLoaded", {
  targetUtilizationPercent: 50,
});

const asg = new autoscaling.AutoScalingGroup(this, "ASG", {
  instanceType: ec2.InstanceType.of(
    ec2.InstanceClass.T3,
    ec2.InstanceSize.MICRO
  ),
  machineImage: ecs.EcsOptimizedImage.amazonLinux2(),
  updateType: autoscaling.UpdateType.REPLACING_UPDATE,
  desiredCapacity: 1,
  vpc,
});

cluster.addAutoScalingGroup(asg);

今天的說明是 ECS Cluster 希望有幫助到大家!

想要看更多嗎?歡迎到我的部落格參觀

文章內容主要是網路或是程式開發類型的文章