Day 18 – 使用 CDK 控制 Relational Database Service(RDS)

我們 EC2、LB、ASG 與 CloudFront 都說明了剩下一個架站機乎一定要用到的服務還沒講到那就是 AWS 提供的資料庫 RDS

https://ithelp.ithome.com.tw/upload/images/20201004/20117701StJQWB7sCd.jpg

AWS RDS

在 AWS RDS 裡面除了我們常用的 MySQL、MariaDB 與 PostgreSQL 還有 Oracle 與 Microsoft SQL Server 可以選擇,而其實 AWS 還有自己研發一個資料庫叫做 Amazon Aurora Amazon Aurora 的特色是他可以同時相容 MySQL 和 PostgreSQL,官方文件說在速度上比 MySQL 資料庫快五倍的速度,比標準 PostgreSQL 資料庫快三倍的速度而且價格是 1/10

https://ithelp.ithome.com.tw/upload/images/20201002/20117701lq8QzcEe9L.png

建立測試使用的 RDS 機器 MySQL

以網站來說通常比較常使用 MySQL,所以這邊使用 MySQL 做介紹

建立 RDS

首先我們先來理解最簡單建立 RDS 的方法,而且為了方便好測試我們先把它放在 Public Subnet 其實密碼管理在 AWS 比較推薦使用 AWS Secrets Manager,不過這邊因為使用明碼直接說明比較簡單,所以就先直接放入密碼,AWS Secrets Manager 就放到後面的章節說明吧! 千萬要記得正式使用請勿把資料庫放在 Public Subnet

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

const rdsSecurityGroup = new ec2.SecurityGroup(this, "RdsSecurityGroup", {
  vpc,
});
rdsSecurityGroup.addIngressRule(
  ec2.Peer.anyIpv4(),
  ec2.Port.tcp(3306),
  "allow public mysql access"
);

const instance = 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.T3,
    ec2.InstanceSize.MICRO
  ),
  allocatedStorage: 10,
  credentials: {
    username: "admin",
    password: cdk.SecretValue.plainText("password"),
  },
  vpcSubnets: {
    subnetType: ec2.SubnetType.PUBLIC,
  },
  securityGroups: [rdsSecurityGroup],
});

new cdk.CfnOutput(this, "RDS", {
  value: instance.dbInstanceEndpointAddress,
});

使用以上的方法就可以建立一個 MySQL 版本 8.0.19 的資料庫拉!

測試一下

  • CDK output
CdkRdsStack.RDS = cd1c72041shs1y1.cub7t8ijymvz.us-west-2.rds.amazonaws.com
  • 使用指令測試 MySQL 連線
$ mysql -h cd1c72041shs1y1.cub7t8ijymvz.us-west-2.rds.amazonaws.com -u admin -p    1
Enter password:
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 14
Server version: 8.0.19 Source distribution

Copyright (c) 2000, 2020, Oracle and/or its affiliates. All rights reserved.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

mysql> quit
Bye

建立正式使用的 RDS 機器

已正式來說比較標準的架構我們會把 RDS 放在 private subnet 畢竟資料庫平常可以存取的只有我們內網的機器可以存取

建立 RDS

以實際使用來說通常會把 EC2 放在 public subnet 而 RDS 放在 private subnet 來保護它,所以我們這次的測試會使用 public 的 EC2 安裝 mysql client 來測試連線 private subnet 的 RDS

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

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

const ec2Instance = new ec2.Instance(this, "Instance", {
  vpc,
  instanceType: ec2.InstanceType.of(
    ec2.InstanceClass.T3,
    ec2.InstanceSize.NANO
  ),
  machineImage: ec2.MachineImage.latestAmazonLinux({
    generation: ec2.AmazonLinuxGeneration.AMAZON_LINUX_2,
  }),
  securityGroup: mySecurityGroup,
  vpcSubnets: {
    subnetType: ec2.SubnetType.PUBLIC,
  },
  keyName: "KeyPair",
});
new cdk.CfnOutput(this, "EC2PublicDns", {
  value: ec2Instance.instancePublicDnsName,
});
new cdk.CfnOutput(this, "EC2PublicIp", {
  value: ec2Instance.instancePublicIp,
});

const instance = 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.T3,
    ec2.InstanceSize.MICRO
  ),
  allocatedStorage: 10,
  credentials: {
    username: "admin",
    password: cdk.SecretValue.plainText("password"),
  },
});
instance.connections.allowFrom(ec2Instance, ec2.Port.tcp(3306));

new cdk.CfnOutput(this, "RDS", {
  value: instance.dbInstanceEndpointAddress,
});

登入 EC2 安裝 MySQL Client 登入測試

  1. 下載 rpm 並且安裝
$ sudo yum install -y https://dev.mysql.com/get/mysql80-community-release-el7-3.noarch.rpm
  1. 安裝 MySQL Client
$ sudo yum install -y mysql-community-client
  1. 使用 MySQL Client 連線 RDS
$ mysql -h cd1ayqd9sllf992.cub7t8ijymvz.us-west-2.rds.amazonaws.com -u admin -p
Enter password:
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 20
Server version: 8.0.19 Source distribution

Copyright (c) 2000, 2020, Oracle and/or its affiliates. All rights reserved.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

mysql>

以上就可以看到一個比較好的範例

建立測試 RDS 機器使用 Amazon Aurora

建立 RDS

這邊因為目前滿少使用的所以就簡單帶過拉 ~

const cluster = new rds.DatabaseCluster(this, "Database", {
  engine: rds.DatabaseClusterEngine.auroraMysql({
    version: rds.AuroraMysqlEngineVersion.VER_2_09_0,
  }),
  instances: 1,
  instanceProps: {
    instanceType: ec2.InstanceType.of(
      ec2.InstanceClass.BURSTABLE3,
      ec2.InstanceSize.SMALL
    ),
    vpc,
  },
  credentials: {
    username: "admin",
    password: cdk.SecretValue.plainText("password"),
  },
});

new cdk.CfnOutput(this, "RDS", {
  value: cluster.clusterEndpoint.hostname,
});

Amazon Linux 2 科普小教室

  • 如果今天想要在 Amazon Linux 2 裡面安裝 rpm 檔案要怎麼選擇版本呢?

我們可以到看看這份文件 How do I enable the EPEL repository for my Amazon EC2 instance running CentOS, RHEL, or Amazon Linux? 裡面有提到,如果是 Amazon Linux 2 請安裝 RHEL 7 版本

在系統裡面安裝 MySQL Client

以目前 8.0 來說可以先參考文件 Installing MySQL on Linux Using the MySQL Yum Repository 就可以知道怎麼安裝了

簡單處理

  1. 安裝 rpm
$ sudo yum install mysql80-community-release-el7-{version-number}.noarch.rpm
  1. 安裝 MySQL client
$ sudo yum install -y mysql-community-client

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

Day 17 – 使用 CDK 控制 Auto Scaling groups – 我要製作一個容量無限大的服務!

今天要來介紹 Auto Scaling groups 通常我們簡稱 ASG,這是一個專門用來處理自動擴展的服務,它可以控制我們的 EC2 scale-in 與 scale-out

https://ithelp.ithome.com.tw/upload/images/20201003/20117701BGo0iLDDCO.jpg

在雲端服務上常常使用自動擴展機制來增加或減少我們的容量,當使用服務的人數變多我們會開始做 scale-out 而人數變少就會開始做 scale-out 就可以理論上的達成系統容量無限的需求,不過在系統架構中並不是無限地增加機器就可以達到增加容量,還是會一直遇到不同的 bottleneck(瓶頸)

增加 Auto Scaling groups

基礎 Auto Scaling groups 介紹

簡單的 ASG 其實只要指定 VPC、instance type 與 machine image 就可以了,不過我想要在文裡面介紹怎麼與前幾天的 EC2 加上 User Data 做一個組合技

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

new autoscaling.AutoScalingGroup(this, 'ASG', {
  vpc,
  instanceType: ec2.InstanceType.of(ec2.InstanceClass.BURSTABLE2, ec2.InstanceSize.MICRO),
  machineImage: new ec2.AmazonLinuxImage() // get the latest Amazon Linux image
});

Auto Scaling groups 與 User Data 結合

所以就在基礎的 Auto Scaling groups 設定裡面做了一些改變,加入建立 EC2 Instance 的參數

  • securityGroup
  • vpcSubnets
  • keyName

然後還有一個是 maxCapacity 為什麼要建立這個參數呢?因為就算 AWS 的機器是無限的錢包應該不會是無限的吧 XD

這邊在提醒大家一件事情 EC2 的數量在每個帳號都是有限制的可以在 Service Quotas 裡面看到它

const asg = new autoscaling.AutoScalingGroup(this, "ASG", {
  vpc,
  instanceType: ec2.InstanceType.of(
    ec2.InstanceClass.T3,
    ec2.InstanceSize.NANO
  ),
  machineImage: ec2.MachineImage.latestAmazonLinux({
    generation: ec2.AmazonLinuxGeneration.AMAZON_LINUX_2,
  }),
  securityGroup: mySecurityGroup,
  vpcSubnets: {
    subnetType: ec2.SubnetType.PUBLIC,
  },
  keyName: "KeyPair",
  minCapacity: 1,
  maxCapacity: 5,
});

設定 Auto Scaling groups 縮放條件

在自動縮放一定要設計條件,通常大家比較會設定的是 CPU 例如我們希望 CPU 70% 開始縮放就可以設定如下

autoScalingGroup.scaleOnCpuUtilization('KeepSpareCPU', {
  targetUtilizationPercent: 70
});

再來還有像是設定 Incoming Bytes 與 Outcoming Bytes 的方法可以用

autoScalingGroup.scaleOnIncomingBytes('LimitIngressPerInstance', {
    targetBytesPerSecond: 10 * 1024 * 1024 // 10 MB/s
});
autoScalingGroup.scaleOnOutcomingBytes('LimitEgressPerInstance', {
    targetBytesPerSecond: 10 * 1024 * 1024 // 10 MB/s
});

另外還有像是 Request Count 一般我們可能會稱這個數值是 RPS,所以以範例來說就是 1000 RPS 會縮放

autoScalingGroup.scaleOnRequestCount('LimitRPS', {
    targetRequestsPerSecond: 1000
});

程式總結

我們把整個程式整理一下目前擁有的功能

  • VPC
  • ASG
    • 最大擴展數量 5
    • 最小機器數量 1
    • 設定 RPS 大於 1 時擴展
  • EC2
    • 設定 User Data 部署 LAMP
  • ALB
const vpc = new ec2.Vpc(this, "VPC", {
  maxAzs: 3,
  natGateways: 0,
});

const mySecurityGroup = new ec2.SecurityGroup(this, "SecurityGroup", {
  vpc,
  allowAllOutbound: true,
});

const asset = new assets.Asset(this, "Asset", {
  path: path.join(__dirname, "../", "ec2-config", "configure.sh"),
});

const alb = new elbv2.ApplicationLoadBalancer(this, "ALB", {
  vpc,
  internetFacing: true,
});
new cdk.CfnOutput(this, "ApplicationLoadBalancerDNS", {
  value: alb.loadBalancerDnsName,
});

const albListener = alb.addListener("Listener", {
  port: 80,
  open: true,
});

const asg = new autoscaling.AutoScalingGroup(this, "ASG", {
  vpc,
  instanceType: ec2.InstanceType.of(
    ec2.InstanceClass.T3,
    ec2.InstanceSize.NANO
  ),
  machineImage: ec2.MachineImage.latestAmazonLinux({
    generation: ec2.AmazonLinuxGeneration.AMAZON_LINUX_2,
  }),
  securityGroup: mySecurityGroup,
  vpcSubnets: {
    subnetType: ec2.SubnetType.PUBLIC,
  },
  keyName: "KeyPair",
  minCapacity: 0,
  maxCapacity: 5,
});

const localPath = asg.userData.addS3DownloadCommand({
  bucket: asset.bucket,
  bucketKey: asset.s3ObjectKey,
});
asg.userData.addExecuteFileCommand({
  filePath: localPath,
  arguments: "--verbose -y",
});
asset.grantRead(asg.role);

albListener.connections.allowTo(asg, ec2.Port.tcp(80));

albListener.addTargets("Targets", {
  healthCheck: {
    enabled: true,
    path: "/phpinfo.php",
  },
  port: 80,
  targets: [asg],
});

asg.scaleOnRequestCount("LimitRPS", {
  targetRequestsPerSecond: 1,
});

部署測試

部署完成先到 Auto Scaling groups 看看部署結果可以看到

  • Min 設定為 1
  • Max 設定為 5 https://ithelp.ithome.com.tw/upload/images/20201001/20117701ULo8yEyZug.png

之後我們對 ASG 網頁給一點流量,基本上就是手動重新整理就好

https://ithelp.ithome.com.tw/upload/images/20201001/201177012mGzjgKSlE.png

因為我們設定 RPS 大於 1 就會擴展,所以很快就可以看到 ASG 把 Desired 做變動 下圖設定為 5,是因為我在測試的時候不小心給太多流量,不然理論上應該會依造流量的需求慢慢增加

注意一下 ASG 會有一個反應時間並不會流量馬上進去就有反應因此如果沒有反應需要等它一下

https://ithelp.ithome.com.tw/upload/images/20201001/20117701x4MDzAEjac.png

在 Auto Scaling groups 的 Instance management 可以看到受這個 ASG 管理的 EC2 有哪些

https://ithelp.ithome.com.tw/upload/images/20201001/201177013cnHYaxlmv.png

而在 Activity history 可以看到 ASG 被觸發的詳細紀錄

https://ithelp.ithome.com.tw/upload/images/20201001/20117701XDiq8fAJ66.png

另外還可以到 EC2 看看機器的狀態,像是以下圖來看就可以看到 EC2 的機器正在 Initalizing

https://ithelp.ithome.com.tw/upload/images/20201001/20117701XSSCdntkvD.png

竟然我們的機器掛在 ASG 後面,當然也需要來看看 Target groups 可以看到機器很完美的都註冊上去了

https://ithelp.ithome.com.tw/upload/images/20201001/20117701KdPWHG5kff.png

在 Target groups 上面也可以看到註冊的 Healthy Hosts 從原本的 1 台變成了 5 台

https://ithelp.ithome.com.tw/upload/images/20201001/2011770182F0VYAkH9.png

過了 5 min 後因為沒有流量再進去了就可以看到 ASG 自動幫我們把 Desited 設定成 2,並且狀態變成了 Updating capacity

https://ithelp.ithome.com.tw/upload/images/20201001/20117701U004WR0yYz.png

觀察一下 Targets 就會看到機器開始 draining

https://ithelp.ithome.com.tw/upload/images/20201001/20117701g3sYJQ5xYh.png

機器雖然在 draining 不過機器不會馬上被收掉,要等到完全脫離 Targets 才會慢慢的被移除

https://ithelp.ithome.com.tw/upload/images/20201001/20117701Mfjv9tTiBw.png

再過一段時間再看看 EC2 的 Instance 就會發現有的機器狀態開始變成了 Shutting-down 了

https://ithelp.ithome.com.tw/upload/images/20201001/201177011jwzRuA3BQ.png

再過一段時間 ASG 的 Desited capacity 變成了 2

https://ithelp.ithome.com.tw/upload/images/20201001/201177015hZzmia3p9.png

Targets 也變成了 2

https://ithelp.ithome.com.tw/upload/images/20201001/20117701fB0W2gc2mm.png

再過許久就會看到整個 ASG 狀態被還原到最初的狀態

https://ithelp.ithome.com.tw/upload/images/20201001/20117701Xg7P3w4VEb.png


今天的文章比平常多了更多的圖片,不知道這樣大家是不是會比較喜歡,如果可以就在下面給點回應吧!

Day 16 – 要串接 API 除了 API Gateway 你還有另外的選擇 Application Load Balancer

不知道大家還記不記得在 Day 6 – AWS CDK 部署 Lambda 與 API Gateway 服務 (上) 的時候有說過要介紹 Lambda 串接 ALB,那時候因為還沒介紹 VPC,而我們已經介紹過 VPC 了,所以今天就來介紹它吧!

https://ithelp.ithome.com.tw/upload/images/20201002/20117701HtwmQ7qpm1.jpg

創建 Application Load Balancer 串接 Lambda

準備 Lambda

這次的 Lambda 與上次的大同小異,主要在於直接印出寫 API 的資訊

  • Method
  • Path
  • Query

大家可以直接用這個網頁做 Debug 或是開發

檔案:lambda/index.js

exports.handler = async function (event) {
  console.log("request:", JSON.stringify(event, undefined, 2));

  return {
    statusCode: 200,
    headers: { "Content-Type": "text/html" },
    body:
      `<meta charset="utf-8">` +
      `<h1><a href="https://ithelp.ithome.com.tw/users/20117701/ironman/3734" target="_blank">用 CDK 定義 AWS 架構 AWS Lambda!</a></h1>` +
      `<p><a href="https://blog.clarence.tw/" target="_blank">https://blog.clarence.tw/</a></p>` +
      `<p>Method: ${event.httpMethod}</p>` +
      `<p>Path: ${event.path}</p>` +
      `<p>Query: ${JSON.stringify(
        event.queryStringParameters,
        undefined,
        2
      )}</p>`,
  };
};

準備 VPC 與 Lambda Target

在這邊 VPC 建立方法與之前教大家 EC2 建立機器的 VPC 相同,主要不同的地方在於我們把 targets 放入 new targets.LambdaTarget(lambdaFunction)

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

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

const lb = new elbv2.ApplicationLoadBalancer(this, "ALB", {
  vpc,
  internetFacing: true,
});
new cdk.CfnOutput(this, "ALBDomain", {
  value: lb.loadBalancerDnsName,
});

const listener = lb.addListener("Listener", { port: 80 });
listener.addTargets("LambdaTarget", {
  targets: [new targets.LambdaTarget(lambdaFunction)],

  healthCheck: {
    enabled: true,
  },
});

測試看看 API 吧!

我們這次拿到的 ALB Domain:http://cdklb-albae-1ixf52wqbsc7b-2018343058.us-west-2.elb.amazonaws.com/

拿到的資訊如下:

  • 用 CDK 定義 AWS 架構 AWS Lambda!
  • https://blog.clarence.tw/
  • Method: GET
  • Path: /
  • Query: {}

https://ithelp.ithome.com.tw/upload/images/20200930/20117701IU2ijBKA50.png

結合 Lambda API 與 EC2 API

使用了 Lambda 串接 ALB 大家有注意到可以玩什麼不一樣的變化嗎?

就是可以把前天教的 ALB 串接 EC2 加上 ALB 串接 Lambda 做一個組合技拉! 如此應該滿好玩的!因為大家就可以是情況幫它們拆負載或是把用 Lambda 比較好寫的服務用 Lambda 寫,想要用 EC2 組合的服務放機器,做很多種變化。

準備 CDK 腳本

話不多說現在就教大家怎麼做吧! 準備一下目標,我們希望目標 path 是 /api* 的全部導入 Lambda 而其他都給 EC2 如此我們會有兩個 Target 並且要切 path,而在 ALB 裡面如果設定 conditions 就需要給 priority

conditions

先來介紹一下 conditions

  • hostHeaders:用於設定 host 條件
  • httpHeader:用於設定 header 條件
  • httpRequestMethods:用於設定 method 條件
  • pathPatterns:用於設定 path 條件
  • queryStrings:用於設定 query 條件
  • sourceIps:用於設定 source ip 條件
const listener = lb.addListener("Listener", { port: 80 });
listener.addTargets("LambdaTarget", {
  priority: 1,
  conditions: [elbv2.ListenerCondition.pathPatterns(["/api*"])],

  targets: [new targets.LambdaTarget(lambdaFunction)],

  healthCheck: {
    enabled: true,
  },
});

listener.addTargets("EC2Target", {
  healthCheck: {
    enabled: true,
    path: "/phpinfo.php",
  },
  port: 80,
  targets: [new targets.InstanceTarget(ec2Instance)],
});

組合一下

拆散解釋大家可能會看不懂,所以我們還是把它整個整理一下吧!

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

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

const asset = new assets.Asset(this, "Asset", {
  path: path.join(__dirname, "../", "ec2-config", "configure.sh"),
});

const ec2Instance = new ec2.Instance(this, "Instance", {
  vpc,
  instanceType: ec2.InstanceType.of(
    ec2.InstanceClass.T3,
    ec2.InstanceSize.NANO
  ),
  machineImage: ec2.MachineImage.latestAmazonLinux({
    generation: ec2.AmazonLinuxGeneration.AMAZON_LINUX_2,
  }),
  vpcSubnets: {
    subnetType: ec2.SubnetType.PUBLIC,
  },
  keyName: "KeyPair",
});
const localPath = ec2Instance.userData.addS3DownloadCommand({
  bucket: asset.bucket,
  bucketKey: asset.s3ObjectKey,
});
ec2Instance.userData.addExecuteFileCommand({
  filePath: localPath,
  arguments: "--verbose -y",
});
asset.grantRead(ec2Instance.role);

const lb = new elbv2.ApplicationLoadBalancer(this, "ALB", {
  vpc,
  internetFacing: true,
});
new cdk.CfnOutput(this, "ALBDomain", {
  value: lb.loadBalancerDnsName,
});

const listener = lb.addListener("Listener", { port: 80 });
listener.addTargets("LambdaTarget", {
  priority: 1,
  conditions: [elbv2.ListenerCondition.pathPatterns(["/api*"])],

  targets: [new targets.LambdaTarget(lambdaFunction)],

  healthCheck: {
    enabled: true,
  },
});

listener.addTargets("EC2Target", {
  healthCheck: {
    enabled: true,
    path: "/phpinfo.php",
  },
  port: 80,
  targets: [new targets.InstanceTarget(ec2Instance)],
});
listener.connections.allowTo(ec2Instance, ec2.Port.tcp(80));

測試看看 API 與 EC2 的結合吧!

  • 可以看到我們的 PATH 可以直接在 Lambda 的 event.path 取得它
    • URL: http://cdklb-albae-1ixf52wqbsc7b-2018343058.us-west-2.elb.amazonaws.com/api
    • Method: GET
    • Path: /api
    • Query: {}

https://ithelp.ithome.com.tw/upload/images/20200930/20117701l2np9lHq8f.png

  • 就算是 query string 也可以取得呦!
    • URL: http://cdklb-albae-1ixf52wqbsc7b-2018343058.us-west-2.elb.amazonaws.com/api/api?target=ec2
    • Method: GET
    • Path: /api?target=ec2
    • Query: {"target": "ec2"}

https://ithelp.ithome.com.tw/upload/images/20200930/20117701u6zVm9NtXF.png

  • 看一下 root URL 吧!
    • URL: http://cdklb-albae-1ixf52wqbsc7b-2018343058.us-west-2.elb.amazonaws.com/

https://ithelp.ithome.com.tw/upload/images/20200930/20117701qnRwYT1G64.png

參考資料

  • https://docs.aws.amazon.com/zh_tw/elasticloadbalancing/latest/application/load-balancer-listeners.html

以上為今日的 Application Load Balancer 與 Lambda 整合介紹,希望有幫助到大家

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

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

Day 15 – 使用 CDK 控制 Elastic Load Balancing – Network Load Balancer

在負載平衡中 AWS 提供了我們三種負載平衡 Application Load Balancer(ALB)、Network Load Balancer(NLB) 與 Classic Load Balancer,而 Classic Load Balancer 屬於上一代的負載平衡所以並不會提到它

https://ithelp.ithome.com.tw/upload/images/20201001/20117701qD5IUlduzr.jpg

而 Network Load Balancer 與 Application Load Balancer 最大的不同點在於 Network Load Balancer 為第四層(Transport Layer) 服務而 Application Load Balancer 處於 第七層(Application Layer) 服務,如果以我們目前介紹的用法來出其實沒有太大的差別,因為我們都是把它當 HTTP 的負載平衡器來使用,不過還是先體驗一下吧!

建立 Load Balancing

建立 Network Load Balancer(NLB)

竟然說到 NLB 當然是先建立它拉!

const lb = new elbv2.NetworkLoadBalancer(this, "LB", {
  vpc,
  internetFacing: true,
});
new cdk.CfnOutput(this, "NetworkLoadBalancerDNS", {
  value: lb.loadBalancerDnsName,
});

新增 Listener

建立一個 port 80 的 Listener

const listener = lb.addListener("Listener", {
  port: 80,
});

新增 Target 到 Listener

這邊分為兩個建立方法,大家可以評估自己的需求來選用想要用的方法

使用 Instance type 方法建立

Target 設定為 Instance 與 IP 最大的不同在於 Security Group 的設定

使用 Instance Type 可以想像所有的流量會從 ALB 外面直接流進機器,而 NLB 不像是 ALB 一樣擁有 Security Group 因此無法使用 Security Group 對接的方法來保護機器,所以說對應到機器 Security Group 的 Port 需要全開,不然會沒有辦法通

listener.addTargets("Targets", {
  port: httpPort,
  targets: [
    new targets.InstanceTarget(ec2Instance),
  ],
});

使用 IP type 方法建立

Target 設定為 IP Type 的好處在於所有的流量會經由內網丟進去,所以在機器 Security Group 的地方就可以選用 VPC CIDR 的方法指定,以安全性來說可以阻止使用者使用機器的 Public IP 連線或許有的人比較喜歡此方法。 有人可能會詢問那這樣內部的機器是不是就沒有辦法知道 client 的 IP 呢?如果要支援大家可以開啟 proxy protocol version 2 的支援就可以了,不過此 Protocol 需要軟體支援大家需要特別注意!

listener.addTargets("Targets", {
  port: httpPort,
  targets: [
    new targets.IpTarget(ec2Instance.instancePrivateIp),
  ],
});

詳細的 Security 規則大家可以參考 AWS 文件

整理整個服務的 Code

只要使用以上兩個步驟就可以把整個 Network Load Balancer 完成,感覺是不是很快呢 XD 因為這邊的步驟怕大家搞混,所以我們把 Instance type 建立方法與 IP type 建立方法獨立成兩種方法讓大家比較好理解

使用 Instance type 方法建立

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

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

mySecurityGroup.addIngressRule(
  ec2.Peer.anyIpv4(),
  ec2.Port.tcp(80),
  "allow public http access"
);

const asset = new assets.Asset(this, "Asset", {
  path: path.join(__dirname, "../", "ec2-config", "configure.sh"),
});

const ec2Instance = new ec2.Instance(this, "Instance", {
  vpc,
  instanceType: ec2.InstanceType.of(
    ec2.InstanceClass.T3,
    ec2.InstanceSize.NANO
  ),
  machineImage: ec2.MachineImage.latestAmazonLinux({
    generation: ec2.AmazonLinuxGeneration.AMAZON_LINUX_2,
  }),
  securityGroup: mySecurityGroup,
  vpcSubnets: {
    subnetType: ec2.SubnetType.PUBLIC,
  },
  keyName: "KeyPair",
});
new cdk.CfnOutput(this, "EC2PublicDns", {
  value: ec2Instance.instancePublicDnsName,
});
new cdk.CfnOutput(this, "EC2PublicIp", {
  value: ec2Instance.instancePublicIp,
});

const localPath = ec2Instance.userData.addS3DownloadCommand({
  bucket: asset.bucket,
  bucketKey: asset.s3ObjectKey,
});
ec2Instance.userData.addExecuteFileCommand({
  filePath: localPath,
  arguments: "--verbose -y",
});
asset.grantRead(ec2Instance.role);

const httpPort = 80;
const lb = new elbv2.NetworkLoadBalancer(this, "LB", {
  vpc,
  internetFacing: true,
});
new cdk.CfnOutput(this, "NetworkLoadBalancerDNS", {
  value: lb.loadBalancerDnsName,
});

const listener = lb.addListener("Listener", {
  port: httpPort,
});

listener.addTargets("Targets", {
  port: httpPort,
  targets: [
    new targets.InstanceTarget(ec2Instance),
  ],
});

使用 IP type 方法建立

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

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

mySecurityGroup.addIngressRule(
  ec2.Peer.ipv4(vpc.vpcCidrBlock),
  ec2.Port.tcp(80),
  "allow vpc cidr http access"
);

const asset = new assets.Asset(this, "Asset", {
  path: path.join(__dirname, "../", "ec2-config", "configure.sh"),
});

const ec2Instance = new ec2.Instance(this, "Instance", {
  vpc,
  instanceType: ec2.InstanceType.of(
    ec2.InstanceClass.T3,
    ec2.InstanceSize.NANO
  ),
  machineImage: ec2.MachineImage.latestAmazonLinux({
    generation: ec2.AmazonLinuxGeneration.AMAZON_LINUX_2,
  }),
  securityGroup: mySecurityGroup,
  vpcSubnets: {
    subnetType: ec2.SubnetType.PUBLIC,
  },
  keyName: "KeyPair",
});
new cdk.CfnOutput(this, "EC2PublicDns", {
  value: ec2Instance.instancePublicDnsName,
});
new cdk.CfnOutput(this, "EC2PublicIp", {
  value: ec2Instance.instancePublicIp,
});

const localPath = ec2Instance.userData.addS3DownloadCommand({
  bucket: asset.bucket,
  bucketKey: asset.s3ObjectKey,
});
ec2Instance.userData.addExecuteFileCommand({
  filePath: localPath,
  arguments: "--verbose -y",
});
asset.grantRead(ec2Instance.role);

const httpPort = 80;
const lb = new elbv2.NetworkLoadBalancer(this, "LB", {
  vpc,
  internetFacing: true,
});
new cdk.CfnOutput(this, "NetworkLoadBalancerDNS", {
  value: lb.loadBalancerDnsName,
});

const listener = lb.addListener("Listener", {
  port: httpPort,
});

listener.addTargets("Targets", {
  port: httpPort,
  targets: [
    new targets.IpTarget(ec2Instance.instancePrivateIp),
  ],
});

測試 NLB 網址

目前拿到的

  • CdkEc2Stack.LoadBalancerDNS: http://CdkEc-LB8A1-11UM2UIFJMS5H-446ba4836b259a16.elb.us-west-2.amazonaws.com/

測試 Test Page

打開它試試看 https://ithelp.ithome.com.tw/upload/images/20200929/201177010EWKEhmVm0.png

測試 phpinfo.php

http://cdkec-albae-1qhysttihzwc-231561288.us-west-2.elb.amazonaws.com/phpinfo.php

https://ithelp.ithome.com.tw/upload/images/20200929/20117701nwoy0p1IgZ.png

結論

基本上與昨天的結果會是一樣的,因為測試的範例是使用 HTTP 網頁作為範例,所以比較沒有感覺

而 Network Load Balancer 主要是用來解決需要處理第四層以上的服務所生的,想要理解更多可以查看一下 AWS Console 打開 Listeners 可以看到除了 TCP 還可以支援 TLS、UDP 或是同時處理 TCP 與 UDP 的服務 https://ithelp.ithome.com.tw/upload/images/20200929/20117701sBEAt55WxC.png

TLS 支援

以 TLS 來說我們可能有一個服務是 RTSP 而我們的程式可能因為某些原因不方便直接讓程式支援 TLS 可以又必須支援 RTSP over SSL 的時候所使用

UDP 支援

以 UDP 來說可能是 DNS 服務或是 QUIC

TCP 與 UDP

可能類似 DNS


以上為今日的 Network Load Balancer 介紹,希望有幫助到大家

Day 14 – 使用 CDK 控制 Elastic Load Balancing – Application Load Balancer

說到部署機器怎麼可以少了 Load Balancing (LB) 呢?今天就來介紹 LB 吧!

https://ithelp.ithome.com.tw/upload/images/20200930/20117701GtFoof8DD0.jpg

在 CDK 的文件上面都是建議使用 Auto Scaling Group (ASG) 來設定,因為這樣才是一個高可用性架構,不過我覺得可以從單獨一個服務開始說起,畢竟單獨解釋一個服務會比較好理解,況且我知道很多公司的服務其實一台機器就可以頂住了 XD

建立 Load Balancing

建立 ALB

這次新增了一個新的 package 是 @aws-cdk/aws-elasticloadbalancingv2 它用來處理 Elastic Load Balancing internetFacing 預設是 false,意思是創建一個內網的 Load Balancing 而我們這邊目前需要的是外網的所以要把它設定成 true 使用 CfnOutput 把 Load Balancing 的 DNS 印出來

import * as elbv2 from "@aws-cdk/aws-elasticloadbalancingv2";

const alb = new elbv2.ApplicationLoadBalancer(this, "ALB", {
  vpc,
  internetFacing: true,
});
new cdk.CfnOutput(this, "LoadBalancerDNS", {
  value: alb.loadBalancerDnsName,
});

新增 Listener

目前只有一個 Listener 使用 port 80,因為目前的服務是要給 HTTP 的

const listener = alb.addListener("Listener", {
  port: 80,
  open: true,
});

新增 Target 到 Listener

以昨天的服務來說我們有一個 /phpinfo.php 目前先用它來做 healthCheck 頁面之後會再修正它,用這個頁面來做其實不太正確不過就先頂著用吧 XD 另外加入 ec2Instance 當我們的 Target,如此設定就可以讓我們的 ALB 後面有一個 Target Group 而裡面有一台 ec2Instance 了 這邊需要注意一下我們的 targets 要使用 aws-elasticloadbalancingv2-targets package 來作為我們的 targets,因為 elbv2.InstanceTarget 要棄用了,所以不要使用 new elbv2.InstanceTarget(ec2Instance.instanceId) 呦!

import * as targets from "@aws-cdk/aws-elasticloadbalancingv2-targets";

listener.addTargets("Targets", {
  healthCheck: {
    enabled: true,
    path: "/phpinfo.php",
  },
  port: 80,
  targets: [new targets.InstanceTarget(ec2Instance)],
});

新增 Security Group

這邊需要注意一下預設的 ALB 會把 outbound 完全丟掉,所以需要特別新增 listener 到 EC2 的 Security Group

listener.connections.allowTo(ec2Instance, ec2.Port.tcp(httpPort));

ALB Security Group 如何限制 outbound

在閱讀 CDK Source Code 的時候發現一個很有趣的東西如果要阻止所有流量的時候會建立一條規則

  • IP: 255.255.255.255/32 – 實際上沒有任何機器可以擁有此 IP
  • Protocol: ICMP
  • Port 252 – 實際上沒有定義的 Type

可以查看 Wiki 網際網路控制訊息協定

const MATCH_NO_TRAFFIC = {
  cidrIp: '255.255.255.255/32',
  description: 'Disallow all traffic',
  ipProtocol: 'icmp',
  fromPort: 252,
  toPort: 86,
};

https://github.com/aws/aws-cdk/blob/v1.64.1/packages/@aws-cdk/aws-ec2/lib/security-group.ts##L508-L523

整理一遍整個服務的 Code

最後我們一樣把整個 Source Code 整理一次給大家,讓大家方便好部署 XD

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

// 設定 Security Group
const mySecurityGroup = new ec2.SecurityGroup(this, "SecurityGroup", {
  vpc,
  allowAllOutbound: true,
});
mySecurityGroup.addIngressRule(
  ec2.Peer.anyIpv4(),
  ec2.Port.tcp(22),
  "allow public ssh access"
);

// 設定上傳的 User Data 腳本
const asset = new assets.Asset(this, "Asset", {
  path: path.join(__dirname, "../", "ec2-config", "configure.sh"),
});

// 設定 EC2 機器
const ec2Instance = new ec2.Instance(this, "Instance", {
  vpc,
  instanceType: ec2.InstanceType.of(
    ec2.InstanceClass.T3,
    ec2.InstanceSize.NANO
  ),
  machineImage: ec2.MachineImage.latestAmazonLinux({
    generation: ec2.AmazonLinuxGeneration.AMAZON_LINUX_2,
  }),
  securityGroup: mySecurityGroup,
  vpcSubnets: {
    subnetType: ec2.SubnetType.PUBLIC,
  },
  keyName: "Clarence_KeyPair",
});
new cdk.CfnOutput(this, "EC2PublicDns", {
  value: ec2Instance.instancePublicDnsName,
});
new cdk.CfnOutput(this, "EC2PublicIp", {
  value: ec2Instance.instancePublicIp,
});

const localPath = ec2Instance.userData.addS3DownloadCommand({
  bucket: asset.bucket,
  bucketKey: asset.s3ObjectKey,
});
ec2Instance.userData.addExecuteFileCommand({
  filePath: localPath,
  arguments: "--verbose -y",
});
asset.grantRead(ec2Instance.role);

// 設定 Load Balancer
const httpPort = 80;
const alb = new elbv2.ApplicationLoadBalancer(this, "ALB", {
  vpc,
  internetFacing: true,
});
new cdk.CfnOutput(this, "LoadBalancerDNS", {
  value: alb.loadBalancerDnsName,
});

const listener = alb.addListener("Listener", {
  port: httpPort,
  open: true,
});
listener.connections.allowTo(ec2Instance, ec2.Port.tcp(httpPort));

listener.addTargets("Targets", {
  healthCheck: {
    enabled: true,
    path: "/phpinfo.php",
  },
  port: httpPort,
  targets: [new targets.InstanceTarget(ec2Instance)],
});

測試 ALB 網址

我目前拿到的

  • CdkEc2Stack.LoadBalancerDNS: http://cdkec-albae-1qhysttihzwc-231561288.us-west-2.elb.amazonaws.com/

測試 Test Page

打開它試試看 https://ithelp.ithome.com.tw/upload/images/20200927/20117701n9hbrNUmIH.png

測試 phpinfo.php

http://cdkec-albae-1qhysttihzwc-231561288.us-west-2.elb.amazonaws.com/phpinfo.php

https://ithelp.ithome.com.tw/upload/images/20200927/20117701Jy0wVjDCNf.png

結論

以上是今天的 ALB 介紹希望有幫助到大家,話說我有在網路上找了找好像目前沒有人寫過單一台 Instance 接在 ALB 後面的文,今天就介紹給大家啦!

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

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

Day 13 – 使用 CDK 部署 EC2 讓 User Data 與 S3 結合我的機器也要自動部署!

今天來說明我們想要一台擁有 LAMP 的機器要怎麼讓 CDK 幫我們自動部署起來吧!

https://ithelp.ithome.com.tw/upload/images/20200929/2011770199UlrctO6e.jpg

建立 HTTP Server

寫安裝腳本檔

要建立 HTTP Server 首先需要先準備一個安裝腳本,而這個腳本需要請 User Data 幫我們跑執行指令 先建立一個資料夾 ec2-config 在裡面新增一個檔案 configure.sh,先對腳本做一個說明

  • 第三行:更新 yum
  • 第四行:安裝 LAMP
  • 第五行:安裝 Apache 與 maria DB
  • 第六行:啟動 Apache
  • 第七行:設定開機啟動 Apache
  • 第八行:把 ec2-user 加入 apache 群組
  • 第九行:設定 /var/www 目錄擁有者
  • 第十行:設定 /var/www 目錄權限
  • 第十一行:設定所有資料夾目錄權限
  • 第十二行:設定所有檔案權限
  • 第十三行:設定 phpinfo 檔案

以功能來看此腳本做了

  • 更新機器
  • 安裝必要程式
  • 設定檔案目錄權限
  • 新增一個 phpinfo.php 網頁
#!/bin/bash
uname -a
yum update -y
amazon-linux-extras install -y lamp-mariadb10.2-php7.2 php7.2
yum install -y httpd mariadb-server
systemctl start httpd
systemctl enable httpd
usermod -a -G apache ec2-user
chown -R ec2-user:apache /var/www
chmod 2775 /var/www
find /var/www -type d -exec chmod 2775 {} \;
find /var/www -type f -exec chmod 0664 {} \;
echo "<?php phpinfo(); ?>" > /var/www/html/phpinfo.php

上傳腳本到 S3

這次使用一個新的 CDK package 來完成這件事情,以下的功能就是上傳剛剛建立的 ec2-config/configure.sh 安裝腳本,並顯示上傳結果。

import * as assets from "@aws-cdk/aws-s3-assets";

const asset = new assets.Asset(this, "Asset", {
  path: path.join(__dirname, "../", "ec2-config", "configure.sh"),
});
new cdk.CfnOutput(this, "S3BucketName", { value: asset.s3BucketName });
new cdk.CfnOutput(this, "S3ObjectKey", { value: asset.s3ObjectKey });
new cdk.CfnOutput(this, "S3HttpURL", { value: asset.httpUrl });
new cdk.CfnOutput(this, "S3ObjectURL", { value: asset.s3ObjectUrl });

EC2 執行 User Data

在處理 User Data 我們需要處理兩個部分

  1. 從 S3 下載腳本
  2. 使用 User Data 執行腳本

從 S3 下載腳本

在 CDK 裡面有提供 User Data 可以直接下載 S3 檔案的 function,所以我們只需要填入 bucket 在上一步的 asset 有使用的 asset.bucket 與 bucketKey 一樣在上一部 asset 有使用過的 asset.s3ObjectKey,最後不要忘記給 Instance role 不然檔案會無法下載

const localPath = ec2Instance.userData.addS3DownloadCommand({
  bucket: asset.bucket,
  bucketKey: asset.s3ObjectKey,
});
asset.grantRead(ec2Instance.role);

使用 User Data 執行腳本

讓 EC2 User Data 直接執行上傳檔案的 localPath

ec2Instance.userData.addExecuteFileCommand({
  filePath: localPath,
  arguments: "--verbose -y",
});

整體部署腳本整合

部署 Web APP

在這邊我們使用單獨一台機器來做示範,所以並還沒有加入昨天說的 Bastion Host 而這次部署的內容含有:

  • VPC
  • SecurityGroup
    • 開啟 22 Port
    • 開啟 80 Port
  • 上傳 ec2-config/configure.sh 提供給 User Data 使用
  • 架設 EC2
    • 等級 t3.nano
    • 使用 Image: AMAZON LINUX 2
    • Subnet: Public
    • User Data: 執行 configure.sh
const vpc = new ec2.Vpc(this, "VPC", {
  maxAzs: 3,
  natGateways: 0,
});

const mySecurityGroup = new ec2.SecurityGroup(this, "SecurityGroup", {
  vpc,
  allowAllOutbound: true,
});
mySecurityGroup.addIngressRule(
  ec2.Peer.anyIpv4(),
  ec2.Port.tcp(22),
  "allow public ssh access"
);
mySecurityGroup.addIngressRule(
  ec2.Peer.anyIpv4(),
  ec2.Port.tcp(80),
  "allow public http access"
);

const asset = new assets.Asset(this, "Asset", {
  path: path.join(__dirname, "../", "ec2-config", "configure.sh"),
});

const ec2Instance = new ec2.Instance(this, "Instance", {
  vpc,
  instanceType: ec2.InstanceType.of(
    ec2.InstanceClass.T3,
    ec2.InstanceSize.NANO
  ),
  machineImage: ec2.MachineImage.latestAmazonLinux({
    generation: ec2.AmazonLinuxGeneration.AMAZON_LINUX_2,
  }),
  securityGroup: mySecurityGroup,
  vpcSubnets: {
    subnetType: ec2.SubnetType.PUBLIC,
  },
  keyName: "KeyPair",
});
new cdk.CfnOutput(this, "EC2PublicDns", {
  value: ec2Instance.instancePublicDnsName,
});
new cdk.CfnOutput(this, "EC2PublicIp", {
  value: ec2Instance.instancePublicIp,
});

const localPath = ec2Instance.userData.addS3DownloadCommand({
  bucket: asset.bucket,
  bucketKey: asset.s3ObjectKey,
});
ec2Instance.userData.addExecuteFileCommand({
  filePath: localPath,
  arguments: "--verbose -y",
});
asset.grantRead(ec2Instance.role);

測試網頁

測試 Apache

在剛剛 CDK 的 output 找到 EC2PublicDns 看到測試 Web 頁面

CdkEc2Stack.EC2PublicDns = ec2-54-213-111-80.us-west-2.compute.amazonaws.com

https://ithelp.ithome.com.tw/upload/images/20200926/201177016mwfvWj5H1.png

測試 PHP

輸入測試 phpinfo 的網址測試 PHP 是否正常執行

http://ec2-54-213-111-80.us-west-2.compute.amazonaws.com/phpinfo.php

https://ithelp.ithome.com.tw/upload/images/20200926/20117701hvXeRTmeWO.png

https://ithelp.ithome.com.tw/upload/images/20200926/20117701QALHGEVkVJ.png 如果打開網頁直接出現了文字代表 PHP 安裝部分有問題請修改 AMI 為 AMAZON LINUX 2,這點要注意一下

結論

如此就擁有一台自動部署 lamp (Linux + Apache + MariaDB + PHP) 的機器了 終於說到 DevOps 的重點拉!(つ> _◕)つ︻╦̵̵͇̿̿̿̿╤───

Day 12 – 使用 CDK 部署 Bastion Host 防禦主機!

要說明自動部署前我們應該要先說明怎麼好好保護我們的內網機器而說到內網機器就會說到我們的 Bastion Host,畢竟大家都不會想要我們的機器在外面裸奔吧 XD 所以在介紹自動部署之前我們先來介紹 Bastion Host 中文可能稱它為防禦主機或是堡壘主機

https://ithelp.ithome.com.tw/upload/images/20200928/201177014SQspjkASO.jpg

Bastion Host

一般來說比較偷懶的時候會在 Application 的 EC2 上面設定一個 Static IP 白名單 22 Port 來保護我們的機器而這其實不太安全,所以比較好的做法其實是設定一台 Bastion Host 來當我們的跳板機,所有的連線會經由這台跳板機連到內部的 private 網路主機,這其實是相對安全的一個方法,以架構來看我們會希望它是這個架構。

https://ithelp.ithome.com.tw/upload/images/20200928/20117701HMzU5AKzFO.png (圖片來源:AWS Security Blog

而我們今天就要來教大家如何使用 CDK 架設一台 Bastion Host

使用 SSH 直接連接 Bastion Host

不知道大家有沒有發現前幾天的教學文其實是在教大家如何建立自己的 Bastion Host,而 CDK 其實有一個簡單的 function 可以簡單的建立 Bastion Host

在這邊先把 SSH 開成 any 有需要設定 static ip 可以使用host.allowSshAccessFrom(ec2.Peer.ipv4('192.192.192.192/32'));

這樣就不用寫昨天的一堆程式只要一行就可以達成拉!

const host = new ec2.BastionHostLinux(this, "BastionHost", {
  vpc,
  subnetSelection: { subnetType: ec2.SubnetType.PUBLIC },
});
host.allowSshAccessFrom(ec2.Peer.anyIpv4());

使用 AWS Systems Manager 的 Session Manager 連接 Bastion Host

另外如果使用 Session Manager 連入 Bastion 其實可以更簡單的部署 Bastion,看起來是不是又更短了呢!

const host = new ec2.BastionHostLinux(this, "BastionHost", {
  vpc,
});

Session Manager 使用本機 SSH 連線

相信大家平常習慣連接 EC2 Instance 機器方法是 SSH,而 Session Manager 也支援直接使用 SSH 的方法,在這裡我們可以使用工具 aws-ssm-ec2-proxy-command 來簡單達成此功能

安裝 SSH Proxy Command

  • 先下載 aws-ssm-ec2-proxy-command.sh 把它放入 ~/.ssh/aws-ssm-ec2-proxy-command.sh
  • 加入執行權限 chmod +x ~/.ssh/aws-ssm-ec2-proxy-command.sh

設定 SSH Config

編輯 ~/.ssh/config 加入

host i-* mi-*
  IdentityFile ~/.ssh/id_rsa
  ProxyCommand ~/.ssh/aws-ssm-ec2-proxy-command.sh %h %r %p ~/.ssh/id_rsa.pub
  StrictHostKeyChecking no

如此我們就可以使用 ssh <INSTACEC_USER>@<INSTANCE_ID> 連接主機拉! 假設我們的機器 ID 是 i-0de45ffd579418348 就可以使用

$ ssh -A ec2-user@i-0de45ffd579418348

分析一下 aws-ssm-ec2-proxy-command 這隻腳本

其實它與我們昨天教大家登入 SSM 指令最大的不同在於

aws ec2-instance-connect send-ssh-public-key  \
  --instance-id "$ec2_instance_id" \
  --instance-os-user "$ssh_user" \
  --ssh-public-key "file://$ssh_public_key_path" \
  --availability-zone "$instance_availability_zone"

此指令會把我們設定的 ~/.ssh/id_rsa.pub 上傳上去主機裡面,所以當我們下次使用 SSH 指令的時候就可以直接登入主機啦!

連接到 private network 的 App Host

預設設定 App Host 會放入本機的 SSH Key 並且只有 Bastion Host 可以連入,這樣我們就可以先跳到 Bastion Host 再連到 App Host 拉! https://ithelp.ithome.com.tw/upload/images/20200926/20117701t6d1mfFudM.png

參考資料

  • https://globaldatanet.com/blog/ssh-and-scp-with-aws-ssm
  • https://github.com/qoomon/aws-ssm-ec2-proxy-command
  • https://aws.amazon.com/tw/blogs/aws/new-port-forwarding-using-aws-system-manager-sessions-manager/
  • https://aws.amazon.com/tw/blogs/security/how-to-record-ssh-sessions-established-through-a-bastion-host/

Day 11 – 使用 CDK 部署 EC2 我需要為機器加入 Role Policy 與執行 User Data

使用 EC2 一定要介紹到 Role Policy 與 User Data,在 AWS 因為服務很多相關的服務如果都使用 IAM Key 去處理會比較麻煩而且久了沒有換可能會有資安風險,這時候就可以使用 Role Policy 來幫我們做到這件事情,而機器啟動後通常會想要跑一些自己的腳本來設定一下機器,要達成這件事情就需要用到 User Data 拉!因此今天就來介紹他們。

https://ithelp.ithome.com.tw/upload/images/20200927/20117701o94CjDzA4q.jpg

複習 EC2 部署程式

昨天還沒帶大家看部署的結果,今天先來部署程式並帶大家看一下昨天部署的結果後面會用這段程式繼續延伸

const vpc = new ec2.Vpc(this, "VPC", {
  natGateways: 0,
});

const mySecurityGroup = new ec2.SecurityGroup(this, "SecurityGroup", {
  vpc,
  description: "Allow ssh access to ec2 instances from anywhere",
  allowAllOutbound: true,
});
mySecurityGroup.addIngressRule(
  ec2.Peer.anyIpv4(),
  ec2.Port.tcp(22),
  "allow public ssh access"
);

const ec2Instance = new ec2.Instance(this, "Instance", {
  vpc,
  instanceType: ec2.InstanceType.of(
    ec2.InstanceClass.T3,
    ec2.InstanceSize.NANO
  ),
  machineImage: new ec2.AmazonLinuxImage(),
  securityGroup: mySecurityGroup,
  vpcSubnets: {
    subnetType: ec2.SubnetType.PUBLIC,
  },
  keyName: "KeyPair",
});
  • 檢查一下 AWS console,會看到 Instance 部署在 Public subnet https://ithelp.ithome.com.tw/upload/images/20200924/20117701XOOoO0CrJo.png

  • 再來看一下 CDK 部署的 SG

    • Inbound 0.0.0.0 開啟 22 port 的 TCP
    • Outbound 0.0.0.0 全部都開啟 https://ithelp.ithome.com.tw/upload/images/20200924/20117701NR2LiOJXyZ.png

使用 SSH 測試登入

到 command line 使用 SSH 登入,因為我們部署的機器是 Amazon Linux 它預設的 user 名稱是 ec2-user,如果使用的是 Ubuntu 預設的登入帳號就是 ubuntu 了

不同版本的機器可能登入的 user 預設帳號不同,大家需要注意一下

https://ithelp.ithome.com.tw/upload/images/20200924/20117701prpcJtS60H.png

部署支援 SSM 的 EC2

應該不少朋友之前有用過 SSM 登入機器,它可以讓使用者不用先放入 Public Key 由 AWS 代勞在需要使用的時候放入臨時的 Public Key 來登入,如此不僅不用保管 SSH key 也可以減少資安風險 現在來教大家如何使用 CDK 部署一台可以支援 SSM 的 instance,要部署我們需要先準備

  1. Role Policy
  2. 安裝 SSM agent

Role Policy

以需求來看我們啟動 SSM 需要以下 action

  • ssmmessages:*
  • ssm:UpdateInstanceInformation
  • ec2messages:*

通常最麻煩的地方是要收集 action 收集好就把它用 array 的方法填入就 OK 了! 其實與以前準備好 action 填到 AWS Console 差不多,現在有了 CDK 就連 AWS Console 都不用進去拉!是不是超方便的 ~

ec2Instance.addToRolePolicy(
  new iam.PolicyStatement({
    actions: [
      "ssmmessages:*",
      "ssm:UpdateInstanceInformation",
      "ec2messages:*",
    ],
    resources: ["*"],
  })
);

User data

準備安裝腳本

以這次的目標需要安裝 SSM agent 我們把指令準備好,安裝的方法可以參考 AWS 文件 How do I install AWS Systems Manager Agent (SSM Agent) on an Amazon EC2 Linux instance at launch?

yum install -y https://s3.amazonaws.com/ec2-downloads-windows/SSMAgent/latest/linux_amd64/amazon-ssm-agent.rpm

使用 CDK 執行安裝腳本

使用 addUserData 填入執行的字串就 OK 了!如果有多個指令也可以使用 && 把它串起來執行

ec2Instance.addUserData(
  "yum install -y https://s3.amazonaws.com/ec2-downloads-windows/SSMAgent/latest/linux_amd64/amazon-ssm-agent.rpm"
);

結合昨天的程式

Role Policy 與 User Data 都準備好就把它合併到昨天的程式裡面

const ec2Instance = new ec2.Instance(this, "Instance", {
  vpc,
  instanceType: ec2.InstanceType.of(
    ec2.InstanceClass.T3,
    ec2.InstanceSize.NANO
  ),
  machineImage: new ec2.AmazonLinuxImage(),
  securityGroup: mySecurityGroup,
  vpcSubnets: {
    subnetType: ec2.SubnetType.PUBLIC,
  },
  keyName: "KeyPair",
});

ec2Instance.addToRolePolicy(
  new iam.PolicyStatement({
    actions: [
      "ssmmessages:*",
      "ssm:UpdateInstanceInformation",
      "ec2messages:*",
    ],
    resources: ["*"],
  })
);

ec2Instance.addUserData(
  "yum install -y https://s3.amazonaws.com/ec2-downloads-windows/SSMAgent/latest/linux_amd64/amazon-ssm-agent.rpm"
);

使用 AWS Console 測試 SSM 登入

  1. 右上角的 Connect 點選

https://ithelp.ithome.com.tw/upload/images/20200925/20117701EYWS1MUcWd.png

  1. 選擇 Session Manager 然後點選 Connect https://ithelp.ithome.com.tw/upload/images/20200925/20117701fylZZNJbrh.png

  2. 如此就可以在網頁執行 SSH 功能拉! https://ithelp.ithome.com.tw/upload/images/20200925/20117701NkUXexi67y.png

使用 Command 測試 SSM 登入

還記得前面說的吧?我們不僅可以使用 AWS Console 登入 SSM 其實也可以使用自己的 Command Line 登入 AWS 機器,首先需要先準備 SSM Tool 並且準備 Instance ID 以我這次部署的 Instance 來說我的 Instance ID 是 i-06af4a77c0be9b4ad 就輸入

$ aws ssm start-session --target i-06af4a77c0be9b4ad

可以看到我使用 SSM agent 登入機器拉!雖然我登入完就輸入 exit 離開了 XD https://ithelp.ithome.com.tw/upload/images/20200925/20117701XPEZQcU1f4.png

以上是今日的 Role Policy 與 User Data,因為準備花了比較長的時間所以內容比較少請大家見諒

Day 10 – 使用 CDK 部署 EC2

今天終於要來講部署 EC2 的方法拉!不過在講 EC2 之前要先介紹 VPC 畢竟沒有 VPC 就沒有地方可以放機器拉 ~

https://ithelp.ithome.com.tw/upload/images/20200926/20117701uFwSRxkx2E.jpg

CDK 建立 VPC

如果要在 CDK 新增一個簡單的 VPC 非常簡單只要簡單的一行即可,創建完的預設

  • CIDR:10.0.0.0/16
  • AZ:預設為 3
  • NAT gateway:預設為每個 AZ 各一個
const vpc = new ec2.Vpc(this, 'VPC');

Subnet 分為三種

  • Public (開放 subnet): Public subnet 直接連接 Internet 希望直接可以取得 public IP 請將機器放在這個 subnet
  • Private (私人 subnet): 無法直接使用 Internet 預設會在每個 subnet 創建一個 NAT gateway

    NAT gateway 是需要付費的這點需要請大家注意一下

  • Isolated (隔離 subnet): subnet 不會連接 Internet 也不會連接 NAT gateway 一個隔離的 subnet,它只能連到 VPC 裡面的機器或是被 VPC 裡面的其他機器連

https://ithelp.ithome.com.tw/upload/images/20200924/20117701sSpUEmrb65.png

https://ithelp.ithome.com.tw/upload/images/20200924/20117701Cw5F5rGkiC.png

NAT gateway

如果沒有 NAT gateway 需求可以把 natGateways 設定為 0,不過這樣就不會創建 Private subnet

const vpc = new ec2.Vpc(this, "VPC", {
  natGateways: 0,
});

https://ithelp.ithome.com.tw/upload/images/20200924/20117701qXi9oSWz67.png

可以檢查一下 NAT Gateway 並不會看到有服務

如果想要有 Private subnet 又不想要 NAT gateway 這麼多可以把 natGateways 設定為 1 他們就會自動共用 NAT gateway

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

https://ithelp.ithome.com.tw/upload/images/20200924/201177019agU6wIgRu.png

可以檢查一下 NAT Gateway 會看到有服務

CDK 建立 EC2

創建簡單的 EC2 只要以下程式就可以創建,不過目前這台機器沒有開放 SG,而且放在 Isolated Subnet 或是 Private Subnet 因此一般來說是摸不到它的

const vpc = new ec2.Vpc(this, 'VPC');

const ec2Instance = new ec2.Instance(this, "Instance", {
  vpc,
  instanceType: ec2.InstanceType.of(
    ec2.InstanceClass.T3,
    ec2.InstanceSize.NANO
  ),
  machineImage: new ec2.AmazonLinuxImage(),
});

https://ithelp.ithome.com.tw/upload/images/20200924/20117701yDWLyEx1WS.png

但是如果需求是 IAM image 開起來就可以用的需求就只要修改一下 machineImage 就可以了

設定一台外網可以連入的 EC2

要達成外網可以連入的需求我們需要完成幾件事情

  • SG 設定 22 port 可以連入
  • EC2 要在 Public Subnet
  • EC2 需要含有 Public Key

通常我們應該會把 SSH port 設定成只有某個 Static 可以連入,不過因為範例的關係就先全開。範例的 keyName 為 KeyPair,大家平常應該會有自己習慣的 key pair name 記得修改他!

const vpc = new ec2.Vpc(this, "VPC", {
  natGateways: 0,
});

const mySecurityGroup = new ec2.SecurityGroup(this, "SecurityGroup", {
  vpc,
  description: "Allow ssh access to ec2 instances from anywhere",
  allowAllOutbound: true,
});
mySecurityGroup.addIngressRule(
  ec2.Peer.anyIpv4(),
  ec2.Port.tcp(22),
  "allow public ssh access"
);

const ec2Instance = new ec2.Instance(this, "Instance", {
  vpc,
  instanceType: ec2.InstanceType.of(
    ec2.InstanceClass.T3,
    ec2.InstanceSize.NANO
  ),
  machineImage: new ec2.AmazonLinuxImage(),
  securityGroup: mySecurityGroup,
  vpcSubnets: {
    subnetType: ec2.SubnetType.PUBLIC,
  },
  keyName: "KeyPair",
});

以上為今天介紹的基本 EC2 機器介紹,如此就可以自行創建簡單的 EC2 了

Day 9 – 部署靜態網頁 (番外篇)

在昨天的靜態網頁教學中還沒說明限制的部分,今天準備了幾個問題來回答

https://ithelp.ithome.com.tw/upload/images/20200925/20117701j0DMhuAFMC.jpg

問題列表

  • 在昨天的教學文件裡面有提到如果要使用 acm.Certificate 需要切換 Region 到 us-east-1 這是為什麼呢?
  • 我們竟然都使用 CDK 了,那可不可以讓 ACM 在 us-east-1 而其他服務在不同區呢?

在昨天的教學文件裡面有提到如果要使用 acm.Certificate 需要切換 Region 到 us-east-1 這是為什麼呢?

這是因為 CloudFront 使用 ACM 有限制條件,我們可以在 AWS 文件裡面找到 如果要使用 CloudFront 必須使用 US East (N. Virginia) 區域的憑證

我們竟然都使用 CDK 了,那可不可以讓 ACM 在 us-east-1 而其他服務在不同區呢?

類似的問題其實在 2020/07/28 有被提出過 [aws-certificatemanager] Create certificate in us-east-1 and use it in a different region #9274

簡單說明一下這則 issue 的內容

這位作者想要創建 AWS Cognito 的服務而他的 CDK stack 在 eu-central-1,如果要使用自定義網域憑證需要在 us-east-1 要如何分享憑證到 AWS Cognito.

AWS 人員給予的回應簡述

  1. 使用兩個不同的 CDK stack 來處理
  2. 使用 acm.CertificateValidation 來處理

方法一:以目前來說不可行,目前在 Certificates ARN 還沒有解法,可以在此 commit 看到詳細說明 方法二:限制只有使用 Route53 的使用者可以使用此解法

談討為什麼 acm.Certificate 不可以指定 Region 而 acm.CertificateValidation 可以指定

首先先看 CDK 文件可以看到 Certificate 的 class 裡面沒有可以填入 region 的地方,而 DnsValidatedCertificate 有一個 Construct Props region

分析 Certificate 實作

我們到 Github 看一下 Certificate 的實作方法 首先看到 import 的地方只有 route53, core 與 certificatemanager.generated

特別說明一下 certificatemanager.generated 代表 AWS CloudFormation-only 或稱 L1 也就是它是原生的 CloudFormation 可參考文件

因此可以知道 Certificate 只有使用原生的 CloudFormation

import * as route53 from '@aws-cdk/aws-route53';
import { Construct, IResource, Resource, Token } from '@aws-cdk/core';
import { CfnCertificate } from './certificatemanager.generated';
import { apexDomain } from './util';

分析 DnsValidatedCertificate 實作

先看到 Github DnsValidatedCertificate 的實作方法,一樣我們可以從 import 先做概略分析它有一個 lambda 代表這邊的功能有人會用它實作

import * as path from 'path';
import * as iam from '@aws-cdk/aws-iam';
import * as lambda from '@aws-cdk/aws-lambda';
import * as route53 from '@aws-cdk/aws-route53';
import * as cdk from '@aws-cdk/core';
import { CertificateProps, ICertificate } from './certificate';

再來我們往下看到 L76-L94 就可以看到這邊有一個 Lambda 的呼叫檔案位置在

path.resolve(__dirname, '..', 'lambda-packages', 'dns_validated_certificate_handler', 'lib')

也就是 dns_validated_certificate_handler/lib/index.js 而輸入 region 的位置在 L241 因此使用 DnsValidatedCertificate 才可以輸入 region 位置,不然依造目前 CloudFormation 的特性是沒辦法跨 region 的

今日主要帶大家簡單的看一下 CDK Source code 有個體驗,希望今天的分享有幫到大家

Day 8 – 部署靜態網頁

前面說了這麼多的 API 部署,差不多也該來個網頁了,今天就來教大家如何部署一個靜態網頁吧!

https://ithelp.ithome.com.tw/upload/images/20200924/20117701Rqhfw3U79A.jpg

創建靜態網頁專案

創建專案

$ mkdir cdk-static-site && cd cdk-static-site
$ cdk init --language typescript

準備我們需要的 package

要創建一個靜態網頁我們需要幾個服務,我們先把它列出來

  • s3: 存放靜態網頁的地方
  • ACM: 網頁需要 HTTPS 憑證
  • CloudFront: 使用者開啟網頁第會先到 CDN 再讓 CDN 去 S3 取得靜態網頁檔 都想好了就先把我們需要的 package 裝起來吧!每次要裝的 npm package 其實都滿多的,這邊來教大家一個快速裝 npm package 的方法 我們因為的專案都是來自於 @aws-cdk 只有後面的專案名稱不一樣就可以用快速指令把它串接起來,如此就可以一行指令快速的把 package 裝起來了
$ npm install @aws-cdk/{aws-s3,aws-s3-deployment,aws-certificatemanager,aws-cloudfront}

準備 S3 Bucket

用過 S3 的朋友都知道如果要把東西放到 S3 要先創建 S3 Bucket,在這邊我以 "static.cdk.clarence.tw" 為 S3 Bucket 名稱同時也是網址名稱來創建 因為我們是要創建靜態網頁 Bucket 所以有幾個參數需要準備:

  • 必填欄位:
    • websiteIndexDocument: 網頁的起始檔案通常都是 index.html
    • publicReadAccess: 網頁要可以 public 存取,如果不行 public 就不用看網頁了對吧 XD
  • 選填欄位:
    • websiteErrorDocument: 網頁錯誤時預設檔案名稱
  • 開發欄位:
    • removalPolicy: 填入 cdk.RemovalPolicy.DESTROY 在我們執行 destory 的時候就會順便把它移除掉比較方便 debug,不建議放在正式專案大家要記得啊!
const siteDomain = "static.cdk.clarence.tw";

const siteBucket = new s3.Bucket(this, "SiteBucket", {
  bucketName: siteDomain,
  websiteIndexDocument: "index.html",
  websiteErrorDocument: "error.html",
  publicReadAccess: true,

  removalPolicy: cdk.RemovalPolicy.DESTROY,
});

建立 ACM

因為創建 CloudFront 需要放入 ACM 因此這邊有兩個選擇

  1. 使用 acm.Certificate 創建新的 ACM
  2. 使用 Certificate.fromCertificateArn 給予 ACM 的 arn 創建 ACM 物件

1. 使用 acm.Certificate

這邊要請大家注意因為 CloudFront 只可以使用 us-east-1 的 ACM 因此如果專案不是部署在 us-east-1 就只能使用方法二了

const cert = new acm.Certificate(this, "Certificate", {
  domainName: "static.cdk.clarence.tw",
  validation: acm.CertificateValidation.fromDns(),
});

2. 使用 Certificate.fromCertificateArn

const cert = acm.Certificate.fromCertificateArn(
  this,
  "Certificate",
  "arn:aws:acm:us-east-1:888888888888:certificate/9d2306e2-420d-48df-b934-1bd21bf1ce73"
);

創建 CloudFront 並且與 S3 串接

在這邊我們使用 CloudFrontWebDistribution 來部署我們的 CloudFront 首先要注意的是 viewerCertificate 因為我們這次的範例是要使用自己的網域所以才要填這個參數,如果未來是想要使用 CloudFront 網域可以不用填,這邊還要注意的是 aliases 它本身是一個 Array 因此可以填入多個的如果有多網域需求可以在這邊增加 接下來是 s3BucketSource 填入我們的 S3 Bucket 即可

const distribution = new cloudfront.CloudFrontWebDistribution(
  this,
  "Distribution",
  {
    viewerCertificate: cloudfront.ViewerCertificate.fromAcmCertificate(
      cert,
      {
        aliases: [siteDomain],
        securityPolicy: cloudfront.SecurityPolicyProtocol.TLS_V1, // default
        sslMethod: cloudfront.SSLMethod.SNI, // default
      }
    ),
    originConfigs: [
      {
        s3OriginSource: {
          s3BucketSource: siteBucket,
        },
        behaviors: [{ isDefaultBehavior: true }],
      },
    ],
  }
);

上傳打包檔案到 S3 Bucket

不知道在前面的準備 package 步驟大家有沒有發現我偷渡了一個 package 這邊有一個很好用的 package aws-s3-deployment,使用此 package 只要在我們的專案旁邊建立一個資料夾把網頁放進去剩下的事情就交給 BucketDeployment 處理就好了,這邊範例我創建了一個叫做 website-dist 的資料夾來放 Demo 網頁

new s3deploy.BucketDeployment(this, "DeployWebsite", {
  sources: [s3deploy.Source.asset("./website-dist")],
  destinationBucket: siteBucket,
  distribution,
});

測試網頁

為了方便講解我這邊也提供一個範例網頁給大家測試一下

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>第 12 屆 iT 邦幫忙鐵人賽!</title>
  </head>
  <body>
    <p>用 CDK 定義 AWS 架構 AWS Lambda!</p>
  </body>
</html>

部署 CDK

第一次在 us-east-1 執行

建立 ACM 有提到 CloudFront 只能吃 us-east-1,所以特別為之前專案不是在 us-east-1 使用的朋友建立一個解說 第一次在 us-east-1 使用 CDK 的朋友需要先執行 cdk bootstrap 在這邊教大家如果只有此次專案想要執行在不同 region 可以使用 AWS_DEFAULT_REGION 這個專用的 env 來指定,後面再加上要執行的指令即可

$ AWS_DEFAULT_REGION=us-east-1 cdk bootstrap
$ AWS_DEFAULT_REGION=us-east-1 cdk deploy

如果出現

❌  CdkStaticSiteStack failed: Error: This stack uses assets, so the toolkit stack must be deployed to the environment (Run "cdk bootstrap aws://unknown-account/unknown-region")

就是因為還沒執行過 cdk bootstrap

之前專案都在 us-east-1 執行

直接執行

$ cdk deploy

看一下結果吧!

https://ithelp.ithome.com.tw/upload/images/20200922/20117701O79tNUhi5M.png

今日主題教大家如何部署靜態網頁希望有幫助到大家,其實在實作這個專案的時候踩了很多雷會在下一篇分享給大家,然後還會帶大家看看 ACM 實作的方法與目前 CDK 的一些限制,希望大家會喜歡 ~

Day 7 – AWS CDK 部署 Lambda 與 API Gateway 服務 (下)

https://ithelp.ithome.com.tw/upload/images/20201024/20117701QzNO37YYln.jpg

昨天說明了 Lambda 串接 API Gateway 今天就依照昨天的內容繼續延伸吧!

以下有幾個大家可能會有的問題今天來回答他們

  • 問題一:ACM 我們之前應該就建立過了這個 ACM 想要繼續用舊的可以嗎?
  • 問題二:Lambda 除了用檔案方法部署之外我可以把 Lambda function 直接放在程式裡面嗎? 這樣就不用先上傳到 S3 了
  • 問題三:我們是不是可以在 Lambda 裡面寫我們對應的 API 程式呢?
  • 問題四:全部程式都放在同一個文件裡面好像不太好管理,可以拆開來嗎?

問題一:ACM 我們之前應該就建立過了這個 ACM 想要繼續用舊的可以嗎?

AWS CDK 印出 output

在說明如何如何使用舊的 ACM 之前先來說明如何在 CDK 印出 output 吧! 要印 output 就要使用 CfnOutput 這個 function 它是 CDK core 的基礎功能,使用方法如下:

const cert = new acm.Certificate(this, "Certificate", {
    domainName: "*.cdk.clarence.tw",
    validation: acm.CertificateValidation.fromDns(),
});
new cdk.CfnOutput(this, "ACM-ARN", {
    value: cert.certificateArn,
});

詳細說明可以看官方文件

那事不宜遲我們來印印看吧!

$ cdk deploy 
CdkLambdaStack: deploying...
[0%] start: Publishing 4570c9a06764894ceb3b53aa789576d8258408a69cfd3cf13dc7337b49bb50bd:current
[100%] success: Published 4570c9a06764894ceb3b53aa789576d8258408a69cfd3cf13dc7337b49bb50bd:current
CdkLambdaStack: creating CloudFormation changeset...


 ✅  CdkLambdaStack

Outputs:
CdkLambdaStack.AcmArn = arn:aws:acm:us-west-2:888888888888:certificate/e2f36ef1-6b06-4cb3-9128-6de273275f0d
CdkLambdaStack.Endpoint8024A810 = https://eli6fm96r2.execute-api.us-west-2.amazonaws.com/prod/

Stack ARN:
arn:aws:cloudformation:us-west-2:888888888888:stack/CdkLambdaStack/f95906d0-fa8f-11ea-ae89-0a43e2aa5bac

如此我們就知道 AcmArn 了

CDK 使用 Arn ID 建立 API Gateway certificate

我們來複習一下昨天建立 ACM 的方法

const cert = new acm.Certificate(this, "Certificate", {
  domainName: "*.cdk.clarence.tw",
  validation: acm.CertificateValidation.fromDns(),
});

我們來把 cert 換成用 Arn ID 來定義,假設我們的 ACM ID 為 arn:aws:acm:us-west-2:888888888888:certificate/bd476304-de15-4feb-b5f4-136fa60cae59 使用 function fromCertificateArn 定義

const cert = acm.Certificate.fromCertificateArn(
  this,
  "Certificate",
  "arn:aws:acm:us-west-2:888888888888:certificate/bd476304-de15-4feb-b5f4-136fa60cae59"
);
new cdk.CfnOutput(this, "AcmArn", {
  value: cert.certificateArn,
});

跑一下 deploy 可以看到我們上面指定的 AcmArn 與 Output 的 AcmArn是一樣的,那就是成功拉!

$ cdk deploy
CdkLambdaStack: deploying...
[0%] start: Publishing 4570c9a06764894ceb3b53aa789576d8258408a69cfd3cf13dc7337b49bb50bd:current
[100%] success: Published 4570c9a06764894ceb3b53aa789576d8258408a69cfd3cf13dc7337b49bb50bd:current
CdkLambdaStack: creating CloudFormation changeset...
[██████████████████████████████████████████████████████████] (3/3)




 ✅  CdkLambdaStack

Outputs:
CdkLambdaStack.AcmArn = arn:aws:acm:us-west-2:888888888888:certificate/bd476304-de15-4feb-b5f4-136fa60cae59
CdkLambdaStack.Endpoint8024A810 = https://eli6fm96r2.execute-api.us-west-2.amazonaws.com/prod/

Stack ARN:
arn:aws:cloudformation:us-west-2:888888888888:stack/CdkLambdaStack/f95906d0-fa8f-11ea-ae89-0a43e2aa5bac

詳細說明可以看官方文件

問題二:Lambda 除了用檔案方法部署之外我可以把 Lambda function 直接放在程式裡面嗎? 這樣就不用先上傳到 S3

CDK Lambda 使用 inline code

使用 inline code 有一個 4KiB 的限制,也可能比較不好 debug,不過還是介紹給有需要使用的朋友 用法是呼叫 lambda.Code.fromInline 把 Code 轉成 String 塞進去即可,這邊為了方便閱讀所以使用了換行符號,如果是 Code 很短的朋友直接一行解決也是 OK 的呦!

const main = new lambda.Function(this, "lambda", {
  runtime: lambda.Runtime.NODEJS_10_X,
  handler: "index.handler",
  code: lambda.Code.fromInline(
    ' \
  exports.handler = async function (event) { \
    console.log("request:", JSON.stringify(event, undefined, 2)); \
    return { \
      statusCode: 200, \
      headers: { "Content-Type": "text/html" }, \
      body: `<meta charset="utf-8"><h1>第 12 屆 iT 邦幫忙鐵人賽 用 CDK 定義 AWS 架構 AWS Lambda!</h1>`, \
    }; \
  }; \
  '
  ),
});

Lambda 開啟 X-Ray

應該滿多使用 Lambda 的朋友會使用 X-Ray 服務的,要開啟只要把 tracing 開啟即可

const main = new lambda.Function(this, "lambda", {
  runtime: lambda.Runtime.NODEJS_10_X,
  handler: "index.handler",
  code: lambda.Code.fromAsset("lambda"),
  tracing: lambda.Tracing.ACTIVE,
});

問題三:我們是不是可以在 Lambda 裡面寫我們對應的 API 程式呢?

為了好解說我們把 API 分成幾個不一樣的 path:

  • root path 顯示:第 12 屆 iT 邦幫忙鐵人賽!
  • ironman path 顯示:用 CDK 定義 AWS 架構 AWS Lambda!
  • 其他頁面:跳轉到我的 Blog

修改一下 Lambda Function

完成以上條件的程式可以這樣寫,我們修改一下 lambda/index.js

exports.handler = async function (event) {
  console.log("request:", JSON.stringify(event, undefined, 2));
  switch (event.path) {
    case "/":
      switch (event.httpMethod) {
        case "GET":
          return {
            statusCode: 200,
            headers: { "Content-Type": "text/html" },
            body: `<meta charset="utf-8"><h1>第 12 屆 iT 邦幫忙鐵人賽!</h1>`,
          };
      }
    case "/ironman":
      switch (event.httpMethod) {
        case "GET":
          return {
            statusCode: 200,
            headers: { "Content-Type": "text/html" },
            body: `<meta charset="utf-8"><h1>用 CDK 定義 AWS 架構 AWS Lambda!</h1>`,
          };
      }
    default:
      return {
        statusCode: 301,
        headers: {
          Location: "https://blog.clarence.tw/",
        },
      };
  }
};

執行一下 cdk deploy,看一下結果 查看 https://api.cdk.clarence.tw/ https://ithelp.ithome.com.tw/upload/images/20200920/2011770118IMSg6yzx.png

查看 https://api.cdk.clarence.tw/ironman https://ithelp.ithome.com.tw/upload/images/20200920/20117701fu5Ww1gLxF.png

其他頁面因為會直接跳轉所以我們用 curl 來表示

$ curl -sID -X https://api.cdk.clarence.tw/blog
HTTP/2 301
date: Sun, 20 Sep 2020 15:56:34 GMT
content-type: application/json
content-length: 0
location: https://blog.clarence.tw/
x-amzn-requestid: c3e2f2d3-6db0-4f81-be69-3fce13a9957f
x-amz-apigw-id: TLBD9FYePHcF7QA=
x-amzn-trace-id: Root=1-5f677bb2-360d545301670034714adbfe;Sampled=0

問題四:全部程式都放在同一個文件裡面好像不太好管理,可以拆開來嗎?

使用 API Gateway 接 Lambda

介紹了這麼多單個 Lambda 寫 API 的方法,現在來說明怎麼把 Lambda 拆開的方法

準備多個 Lambda Function

修改一下原本的 lambda/index.js 讓我們比較好辨識目前的網頁

exports.handler = async function (event) {
  console.log("request:", JSON.stringify(event, undefined, 2));
  return {
    statusCode: 200,
    headers: { "Content-Type": "text/html" },
    body: `<meta charset="utf-8"><h1>第 12 屆 iT 邦幫忙鐵人賽!</h1>`,
  };
};

新增一個新的 Lambda Function lambda/ironmanFunc.js 來增加頁面

exports.handler = async function (event) {
  console.log("request:", JSON.stringify(event, undefined, 2));
  return {
    statusCode: 200,
    headers: { "Content-Type": "text/html" },
    body: `<meta charset="utf-8"><h1>用 CDK 定義 AWS 架構 AWS Lambda!</h1>`,
  };
};

修改 cdk-lambda-stack.js

原本的 CDK stack Lambda Function 只有定義一個,現在我們再多新增一個 ironmanFunc 第 28 行:使用 GET Method 串接到原本的 main function 第 29 行:新增一個 12th 的 path 第 30 行:使用 GET method 串接新的 ironmanFunc

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

const cert = acm.Certificate.fromCertificateArn(
  this,
  "Certificate",
  "arn:aws:acm:us-west-2:888888888888:certificate/bd476304-de15-4feb-b5f4-136fa60cae59"
);
new cdk.CfnOutput(this, "AcmArn", {
  value: cert.certificateArn,
});
const api = new apigw.LambdaRestApi(this, "Endpoint", {
  handler: main,
  domainName: {
    domainName: "api.cdk.clarence.tw",
    certificate: cert,
  },
  proxy: false,
});
api.root.addMethod("GET", new apigw.LambdaIntegration(main));
const ironman = api.root.addResource("12th");
ironman.addMethod("GET", new apigw.LambdaIntegration(ironmanFunc));

deploy 一下測試一下效果

先到 https://api.cdk.clarence.tw/ 看一下 https://ithelp.ithome.com.tw/upload/images/20200920/20117701tzHjED0Xbd.png

再到 https://api.cdk.clarence.tw/12th 果然如預期 https://ithelp.ithome.com.tw/upload/images/20200920/20117701u8BlMNaPH8.png

今天說明的 CDK 使用 Lambda、API Gateway 的延伸,希望對大家都有收穫,我們明天再見 ~

Day 6 – AWS CDK 部署 Lambda 與 API Gateway 服務 (上)

今天我們來部署一個 API Gateway 串接 Lambda 的服務,讓我們直接來 serverless 一下!

https://ithelp.ithome.com.tw/upload/images/20201024/20117701QzNO37YYln.jpg

建立 Lambda

創建新的 CDK Project

我們這次的專案叫它 cdk-lambda,首先我們先創建資料夾然後 init 一下專案

$ mkdir cdk-lambda && cd cdk-lambda
$ cdk init --language typescript
Applying project template app for typescript
# Welcome to your CDK TypeScript project!

This is a blank project for TypeScript development with CDK.

The `cdk.json` file tells the CDK Toolkit how to execute your app.

## Useful commands

 * `npm run build`   compile typescript to js
 * `npm run watch`   watch for changes and compile
 * `npm run test`    perform the jest unit tests
 * `cdk deploy`      deploy this stack to your default AWS account/region
 * `cdk diff`        compare deployed stack with current state
 * `cdk synth`       emits the synthesized CloudFormation template

Initializing a new git repository...
Executing npm install...
npm WARN deprecated [email protected]: request has been deprecated, see https://github.com/request/request/issues/3142
npm WARN deprecated [email protected]: request-promise-native has been deprecated because it extends the now deprecated request package, see https://github.com/request/request/issues/3142
npm WARN deprecated [email protected]: this library is no longer supported
npm WARN deprecated [email protected]: https://github.com/lydell/resolve-url#deprecated
npm WARN deprecated [email protected]: Please see https://github.com/lydell/urix#deprecated
npm notice created a lockfile as package-lock.json. You should commit this file.
npm WARN [email protected] No repository field.
npm WARN [email protected] No license field.

✅ All done!

與之前的 CDK Project 創建方法做比較

可以看到這次沒有加入 template 指令,所以在 cdk-lambda.stack.ts 裡面沒有內容只有一行註解,移除註解就可以開始我們今天的主題了!

https://ithelp.ithome.com.tw/upload/images/20200919/20117701GbnkomaadV.png

創建 Lambda function 主程式

  • 接下來我們創建一個放 lambda function 的資料夾
$ mkdir lambda
  • 放入 lambda/index.js 測試檔案
exports.handler = async function (event) {
  console.log("request:", JSON.stringify(event, undefined, 2));
  return;
};
  • 加入 cdk-lambda.stack.ts 放入 lambda
import * as cdk from "@aws-cdk/core";
import * as lambda from "@aws-cdk/aws-lambda";

export class CdkLambdaStack extends cdk.Stack {
  constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    const main = new lambda.Function(this, "lambda", {
      runtime: lambda.Runtime.NODEJS_10_X,
      handler: "index.handler",
      code: lambda.Code.fromAsset("lambda"),
    });
  }
}
  • 安裝套件
$ npm i @aws-cdk/aws-lambda
  • 執行 deploy
$ cdk deploy
  • 開啟 AWS Console 檢查一下看一下我們的 Lambda 是否有成功部署上去! https://ithelp.ithome.com.tw/upload/images/20200920/20117701o9dCUQzGvW.png

準備 API Gateway 要用的 Lambda Function

第一次使用 CDK 部署 Lambda 來測試看看改變我們的 Lambda Function 會不會同步幫我們修改吧! 此次的修改內容因應要串接 API Gateway 做了一點改變 如果想要進一步知道其他的串接參數可以直接閱讀官方文件在 API Gateway 中設定整合回應

exports.handler = async function (event) {
  console.log("request:", JSON.stringify(event, undefined, 2));
  return {
    statusCode: 200,
    headers: { "Content-Type": "text/html" },
    body: `<h1>第 12 屆 iT 邦幫忙鐵人賽 用 CDK 定義 AWS 架構 AWS Lambda!</h1>`,
  };
};

在部署之前先查看一下 cdk diff 可以發現我們更新了 index.js CDK 會幫我們重新上傳檔案到 S3 並且更新它 這段繁瑣的功能 CDK 完美的幫我們處理好了 ~~ 可以從 Parameters 與 Resources 看到有 S3 的新增檔案與移除

$ cdk diff

因為剛開始使用我們還是重新整理一下 AWS Console 看看吧! https://ithelp.ithome.com.tw/upload/images/20200920/201177010v7nnBdvdN.png

加入 API Gateway

修改 lib/cdk-lambda-stack.ts

我絕對不會說我本來是想要串 ALB 可是突然想到要先講 VPC 才可以創 ALB 因此決定還是先寫 API Gateway 要使用 API Gateway 串接 Lambda 可以直接使用 CDK 的 LambdaRestApi 來完成 是不是非常簡單又快速呢!這邊如果手動可是還要在 AWS Console 點個好幾下呢!

import * as cdk from "@aws-cdk/core";
import * as lambda from "@aws-cdk/aws-lambda";
import * as apigw from '@aws-cdk/aws-apigateway';

export class CdkLambdaStack extends cdk.Stack {
  constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    const main = new lambda.Function(this, "lambda", {
      runtime: lambda.Runtime.NODEJS_10_X,
      handler: "index.handler",
      code: lambda.Code.fromAsset("lambda"),
    });
    new apigw.LambdaRestApi(this, 'Endpoint', {
      handler: main
    });
  }
}

檢查一下 diff 可以看到新增了 API Gateway 的 Resources

$ cdk diff

安裝套件

$ npm i @aws-cdk/aws-apigateway

使用部署指令完成後可以看到網頁了!

$ cdk deploy

部署完成後測試

網頁亂碼

我們來看一下網頁,哇!竟然是亂碼,其實是我們忘記指定它是 UTF-8 了來修正一下程式 https://ithelp.ithome.com.tw/upload/images/20200920/2011770168RYYmfbLl.png

修正網頁

其實這邊的問題主要是在 HTML 語法的部分跟 Lambda 比較沒有關係,不過還是介紹一下 XD

exports.handler = async function (event) {
  console.log("request:", JSON.stringify(event, undefined, 2));
  return {
    statusCode: 200,
    headers: { "Content-Type": "text/html" },
    body: `<meta charset="utf-8"><h1>第 12 屆 iT 邦幫忙鐵人賽 用 CDK 定義 AWS 架構 AWS Lambda!</h1>`,
  };
};

再部署一次就正常了 https://ithelp.ithome.com.tw/upload/images/20200920/201177018WFxePIQre.png

讓 API Gateway 支援 customer domain

目前使用的網址是 API Gateway 的 domain 覺得不太美觀來幫它換一下! 首先我們先在 LambdaRestApi 指定 domain name 如 21-24 行使用 api.cdk.clarence.tw 在 API Gateway 使用 Customer Domain 需要使用 HTTPS 因此還需要設定 ACM 來新增 ACM 使用 *.cdk.clarence.tw 如 15-18 行

import * as cdk from "@aws-cdk/core";
import * as lambda from "@aws-cdk/aws-lambda";
import * as apigw from "@aws-cdk/aws-apigateway";
import * as acm from "@aws-cdk/aws-certificatemanager";

export class CdkLambdaStack extends cdk.Stack {
  constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    const main = new lambda.Function(this, "lambda", {
      runtime: lambda.Runtime.NODEJS_10_X,
      handler: "index.handler",
      code: lambda.Code.fromAsset("lambda"),
    });
    const cert = new acm.Certificate(this, "Certificate", {
      domainName: "*.cdk.clarence.tw",
      validation: acm.CertificateValidation.fromDns(),
    });
    new apigw.LambdaRestApi(this, "Endpoint", {
      handler: main,
      domainName: {
        domainName: "api.cdk.clarence.tw",
        certificate: cert,
      },
    });
  }
}

安裝套件

$ npm i @aws-cdk/aws-certificatemanager

修改完程式後我們來更新一下

$ cdk deploy

執行完成後到 AWS Console 看 API Gateway/Custom domain names 看一下是否設定完成 https://ithelp.ithome.com.tw/upload/images/20200920/20117701HnnsTzBIuz.png

確定完成後到我們的 DNS 設定一下 Domain 的 CNAME,接下來就可以使用新的網址打開網站拉! 是不是變得比較美觀 ~ https://ithelp.ithome.com.tw/upload/images/20200920/20117701cOXKj3bPP0.png

以上是今天的 CDK 使用 Lambda、API Gateway 與 ACM 解說,目前的排版可能有點亂我還在思考要怎麼做會比較好看請大家見諒 QQ

Day 5 – 執行 AWS CDK sample-app

昨天分析完了整個 sample-app 程式,今天就直接來執行它吧!

https://ithelp.ithome.com.tw/upload/images/20201024/20117701l2JAbJK5oH.jpg

今日的主題除了教大家怎麼把 sample-app 部署到 AWS 上面之外還會教大家怎麼簡單的修改 CDK 程式先體驗 CDK 的方便之處,那就來看看今天的內容吧!

執行 CDK sample-app

cdk bootstrap

第一次使用 CDK 先執行 bootstrap

如果之前有做過了就不需要再做了呦!

$ cdk bootstrap
 ⏳  Bootstrapping environment aws://888888888888/us-west-2...
 ✅  Environment aws://888888888888/us-west-2 bootstrapped (no changes).

cdk synth

看一下部署腳本 一開始使用 CDK 還是可以先看一下如果使用 CloudFormation 會怎麼寫的,看完就會這樣寫真的滿辛苦的

$ cdk synth
Resources:
  HelloCdkQueueB56C77B9:
    Type: AWS::SQS::Queue
    Properties:
      VisibilityTimeout: 300
    Metadata:
      aws:cdk:path: HelloCdkStack/HelloCdkQueue/Resource
  HelloCdkQueuePolicy027FC30A:
    Type: AWS::SQS::QueuePolicy
    Properties:
      PolicyDocument:
        Statement:
          - Action: sqs:SendMessage
            Condition:
              ArnEquals:
                aws:SourceArn:
                  Ref: HelloCdkTopic1F583424
            Effect: Allow
            Principal:
              Service: sns.amazonaws.com
            Resource:
              Fn::GetAtt:
                - HelloCdkQueueB56C77B9
                - Arn
        Version: "2012-10-17"
      Queues:
        - Ref: HelloCdkQueueB56C77B9
    Metadata:
      aws:cdk:path: HelloCdkStack/HelloCdkQueue/Policy/Resource
  HelloCdkQueueHelloCdkStackHelloCdkTopic850E0FBD36A066B9:
    Type: AWS::SNS::Subscription
    Properties:
      Protocol: sqs
      TopicArn:
        Ref: HelloCdkTopic1F583424
      Endpoint:
        Fn::GetAtt:
          - HelloCdkQueueB56C77B9
          - Arn
    Metadata:
      aws:cdk:path: HelloCdkStack/HelloCdkQueue/HelloCdkStackHelloCdkTopic850E0FBD/Resource
  HelloCdkTopic1F583424:
    Type: AWS::SNS::Topic
    Metadata:
      aws:cdk:path: HelloCdkStack/HelloCdkTopic/Resource
  CDKMetadata:
    Type: AWS::CDK::Metadata
    Properties:
      Modules: aws-cdk=1.63.0,@aws-cdk/aws-cloudwatch=1.63.0,@aws-cdk/aws-iam=1.63.0,@aws-cdk/aws-kms=1.63.0,@aws-cdk/aws-sns=1.63.0,@aws-cdk/aws-sns-subscriptions=1.63.0,@aws-cdk/aws-sqs=1.63.0,@aws-cdk/cloud-assembly-schema=1.63.0,@aws-cdk/core=1.63.0,@aws-cdk/cx-api=1.63.0,@aws-cdk/region-info=1.63.0,jsii-runtime=node.js/v12.16.3
    Condition: CDKMetadataAvailable
Conditions:
  CDKMetadataAvailable:
    Fn::Or:
      - Fn::Or:
          - Fn::Equals:
              - Ref: AWS::Region
              - ap-east-1
          - Fn::Equals:
              - Ref: AWS::Region
              - ap-northeast-1
          - Fn::Equals:
              - Ref: AWS::Region
              - ap-northeast-2
          - Fn::Equals:
              - Ref: AWS::Region
              - ap-south-1
          - Fn::Equals:
              - Ref: AWS::Region
              - ap-southeast-1
          - Fn::Equals:
              - Ref: AWS::Region
              - ap-southeast-2
          - Fn::Equals:
              - Ref: AWS::Region
              - ca-central-1
          - Fn::Equals:
              - Ref: AWS::Region
              - cn-north-1
          - Fn::Equals:
              - Ref: AWS::Region
              - cn-northwest-1
          - Fn::Equals:
              - Ref: AWS::Region
              - eu-central-1
      - Fn::Or:
          - Fn::Equals:
              - Ref: AWS::Region
              - eu-north-1
          - Fn::Equals:
              - Ref: AWS::Region
              - eu-west-1
          - Fn::Equals:
              - Ref: AWS::Region
              - eu-west-2
          - Fn::Equals:
              - Ref: AWS::Region
              - eu-west-3
          - Fn::Equals:
              - Ref: AWS::Region
              - me-south-1
          - Fn::Equals:
              - Ref: AWS::Region
              - sa-east-1
          - Fn::Equals:
              - Ref: AWS::Region
              - us-east-1
          - Fn::Equals:
              - Ref: AWS::Region
              - us-east-2
          - Fn::Equals:
              - Ref: AWS::Region
              - us-west-1
          - Fn::Equals:
              - Ref: AWS::Region
              - us-west-2

cdk deploy

開始執行部屬,執行此指令如果有修改權限都會再一次跟使用者確定修改的內容,確定請按 y

$ cdk deploy
This deployment will make potentially sensitive changes according to your current security approval level (--require-approval broadening).
Please confirm you intend to make the following modifications:

IAM Statement Changes
┌───┬───────────────────┬────────┬───────────────────┬───────────────────┬─────────────────────┐
│   │ Resource          │ Effect │ Action            │ Principal         │ Condition           │
├───┼───────────────────┼────────┼───────────────────┼───────────────────┼─────────────────────┤
│ + │ ${HelloCdkQueue.A │ Allow  │ sqs:SendMessage   │ Service:sns.amazo │ "ArnEquals": {      │
│   │ rn}               │        │                   │ naws.com          │   "aws:SourceArn":  │
│   │                   │        │                   │                   │ "${HelloCdkTopic}"  │
│   │                   │        │                   │                   │ }                   │
└───┴───────────────────┴────────┴───────────────────┴───────────────────┴─────────────────────┘
(NOTE: There may be security-related changes not in this list. See https://github.com/aws/aws-cdk/issues/1299)

Do you wish to deploy these changes (y/n)?  y
HelloCdkStack: deploying...
[██████████████████████████████████████████████████████████] (6/6)




 ✅  HelloCdkStack

Stack ARN:
arn:aws:cloudformation:us-west-2:888888888888:stack/HelloCdkStack/cbb80510-f9d0-11ea-b44e-0a8a148431ae

文字版

https://ithelp.ithome.com.tw/upload/images/20200921/20117701cfOJDLEjrf.png

圖片版

查看 CloudFormation 結果

在這邊可以很清楚的看到上面的 Stack ARN 與我們的 Stack ID 一樣,並且有部署成功 https://ithelp.ithome.com.tw/upload/images/20200919/20117701QYoa1USu1T.png

再來檢查一下 Resources 的部分 https://ithelp.ithome.com.tw/upload/images/20200919/20117701pvf2lZyz2S.png

到 SNS 看看新建的 Topic https://ithelp.ithome.com.tw/upload/images/20200919/20117701GU15q1SGFC.png

到 SQS 檢查 timeout 為 300 秒也就是 5 分鐘,並且訂閱了 SNS https://ithelp.ithome.com.tw/upload/images/20200919/20117701gvEhDKgfgJ.png

修改一下 hello-cdk-stack.ts

體驗一下如果把某些程式註解是否會真的幫我們移除對應的服務,把 13 ~ 16 行註解

import * as sns from "@aws-cdk/aws-sns";
import * as subs from "@aws-cdk/aws-sns-subscriptions";
import * as sqs from "@aws-cdk/aws-sqs";
import * as cdk from "@aws-cdk/core";

export class HelloCdkStack extends cdk.Stack {
  constructor(scope: cdk.App, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    const queue = new sqs.Queue(this, "HelloCdkQueue", {
      visibilityTimeout: cdk.Duration.seconds(300),
    });

    // const topic = new sns.Topic(this, 'HelloCdkTopic');

    // topic.addSubscription(new subs.SqsSubscription(queue));
  }
}

跑一下 cdk diff

執行一下 cdk diff 可以很清楚的看到什麼 Resources 被移除了

  • [-] AWS::SQS::QueuePolicy HelloCdkQueuePolicy027FC30A destroy
  • [-] AWS::SNS::Subscription HelloCdkQueueHelloCdkStackHelloCdkTopic850E0FBD36A066B9 destroy
  • [-] AWS::SNS::Topic HelloCdkTopic1F583424 destroy
$ cdk diff
Stack HelloCdkStack
IAM Statement Changes
┌───┬───────────────────┬────────┬───────────────────┬───────────────────┬─────────────────────┐
│   │ Resource          │ Effect │ Action            │ Principal         │ Condition           │
├───┼───────────────────┼────────┼───────────────────┼───────────────────┼─────────────────────┤
│ - │ ${HelloCdkQueue.A │ Allow  │ sqs:SendMessage   │ Service:sns.amazo │ "ArnEquals": {      │
│   │ rn}               │        │                   │ naws.com          │   "aws:SourceArn":  │
│   │                   │        │                   │                   │ "${HelloCdkTopic1F5 │
│   │                   │        │                   │                   │ 83424}"             │
│   │                   │        │                   │                   │ }                   │
└───┴───────────────────┴────────┴───────────────────┴───────────────────┴─────────────────────┘
(NOTE: There may be security-related changes not in this list. See https://github.com/aws/aws-cdk/issues/1299)

Resources
[-] AWS::SQS::QueuePolicy HelloCdkQueuePolicy027FC30A destroy
[-] AWS::SNS::Subscription HelloCdkQueueHelloCdkStackHelloCdkTopic850E0FBD36A066B9 destroy
[-] AWS::SNS::Topic HelloCdkTopic1F583424 destroy

文字版

https://ithelp.ithome.com.tw/upload/images/20200921/20117701F4ML0Q8SMv.png

圖片版

再跑一次 cdk deploy

看完之後我們來執行一次 cdk deploy,等執行成功就來檢查一下

$ cdk deploy
HelloCdkStack: deploying...
HelloCdkStack: creating CloudFormation changeset...
[██████████████████████████████████████████████████████████] (5/5)



 ✅  HelloCdkStack

Stack ARN:
arn:aws:cloudformation:us-west-2:888888888888:stack/HelloCdkStack/cbb80510-f9d0-11ea-b44e-0a8a148431ae

檢查一下 CloudFormation

可以發現剩下一個 SQS,代表有執行成功! https://ithelp.ithome.com.tw/upload/images/20200919/20117701Ekpmvfqodm.png

重新整理一下 SNS

剛剛的 SNS 也已經被移除掉了 https://ithelp.ithome.com.tw/upload/images/20200919/20117701ILg8dPhzct.png

檢查一下 SQS

確定沒有 SNS 的訂閱資料 https://ithelp.ithome.com.tw/upload/images/20200919/20117701hNWhnENec3.png

最後我們把整個環境移除掉吧!

今天的講解差不多結束了,我們來把整個 cdk destroy 掉 執行會出現警告直接按下 y 即可

$ cdk destroy
Are you sure you want to delete: HelloCdkStack (y/n)? y
HelloCdkStack: destroying...
9:20:24 AM | DELETE_IN_PROGRESS   | AWS::CloudFormation::Stack | HelloCdkStack
9:20:26 AM | DELETE_IN_PROGRESS   | AWS::SQS::Queue    | HelloCdkQueue

 ✅  HelloCdkStack: destroyed

文字版

https://ithelp.ithome.com.tw/upload/images/20200921/20117701uJnbCaDfrt.png

圖片版

搜尋一下剛剛的 CloudFormation

可以發現已經沒有任何東西拉! https://ithelp.ithome.com.tw/upload/images/20200919/201177016h9RkzmIau.png

以上是 AWS CDK sample-app 的執行與測試