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

2020 12th 鐵人賽

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

https://i1.wp.com/ithelp.ithome.com.tw/upload/images/20201007/20117701sUxMGXcnPF.jpg?w=640&ssl=1

建立一個 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://i2.wp.com/ithelp.ithome.com.tw/upload/images/20201004/20117701acEN8tNE6U.png?w=640&ssl=1

Github: web-app-env

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

https://i2.wp.com/ithelp.ithome.com.tw/upload/images/20201004/20117701iFAeqpLPXM.png?w=640&ssl=1

建立 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://i1.wp.com/ithelp.ithome.com.tw/upload/images/20201004/20117701hpI5aFkOFd.png?w=640&ssl=1

設定 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 的服務,希望有幫助到大家 ~