基于Terraform在AWS ECS中构建Jenkins持续集成体系

  之前我们旧的 Jenkins 集群跑在 AWS EC2 上,近期由于大量新增 Job 以及大量构建任务的并行,导致集群资源吃紧,不得不新增更多的 Slave 来应对。为了降低成本,同时获得更好的资源弹性,Alliot 打算基于 Terraform 在 AWS ECS 中构建新的 Jenkins 持续集成体系。容器提供了更细的资源粒度,拥有更好的资源弹性和资源利用率。

需求说明

  新建 ECS 集群,并在 ECS(Amazon Elastic Container Service) 集群中部署 Jenkins master,使用 ECS fargate 作为 Slave Executor 执行任务。

需求构思

本文尽可能的使用 Terraform 来实现资源的管理,尽可能的 Infrastructure As Code。在平时的学习过程中,通过 Terraform 与 AWS Console 以及 AWS CLI 的配合使用,也能更快的熟悉 AWS 的细节。

  计划使用 AWS ECS fargate 运行 Jenkins master 节点,通过 AWS EFS(Amazon Elastic File System) 作为共享存储,实现配置持久化。
  Jenkins master 通过 Amazon Elastic Container Service (ECS) / Fargate 插件,调度 ECS 资源动态的完成 slave 的管理(master 常驻,而 slave 是动态创建与销毁的,这个过程由 master 来控制)。
  ECS 负载与 EFS 实例均部署在 Private subnet,通过统一的 NAT Gateway 作为出口。 ECS Service 通过 ALB 向公网暴露 WebUI 提供服务(当然,生产环境搭配安全组白名单食用)。Jenkins master 与 Jenkins slave 之间,利用 AWS service-discovery 走 VPC 内部通信。
  架构大致如下图所示:
terraform-aws-ecs-jenkins-01

  综上, 我们需要的资源有:
网络资源: VPC(1 个),VPC 启用三个可用区,每个可用区包含 Privcate、Public subnet 各一个。
计算资源: ECS Cluster 一个,Networing 指定上述 VPC 的三个 Private subnet。ALB 一个(由于需要暴露在公网,所以可用区应位于 Public subnet),用于暴露 Jenkins WebUI。
存储资源: EFS 实例一个,Networking 同上;CloudWatch LogGroup 一个;ECR 一个。
IAM role: 供 ECS service 与 task 来访问 EFS 以及向 CloudWatch 输出日志。
其他: ECS task 实例之间走 Private subnet 进行通讯,但 ECS 不能像 kubernetes 那样能够直接通过内部 DNS 进行通讯,要想达到同样的目的,需要依赖 Service Discovery 来创建内部 DNS 记录,通过内部 DNS 来进行 task 实例间调用 。

本文代码已开源,仓库地址:
https://github.com/AlliotTech/terraform-aws-ecs-jenkins
注意: 由于每个人的现网环境现网资源不一定相同,因此 Alliot 在代码中尽可能的不依赖现网的资源,所以代码不一定能够完整的契合每个人的需求,各位同学也可以考虑分模块按需取用。

下面是各资源模块的讲解。

前置条件

AWS 账号一个(废话)
域名一个
AWS Certificate Manager 托管的该域名的 SSL 证书一个

IAM 配置

  AWS 拥有非常完整细致的权限管控模式,IAM 规则可以细分到每个资源每个操作。这里我们只能是尽量遵循 “权限最小化” 的原则,但可能依旧存在权限给的稍大的情况,各位依据自己的场景来调整权限松紧。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
resource "aws_iam_policy" "jenkins_ecs_policy" {
name = var.jenkins_ecs_policy_name
description = "Policy for jenkins ecs task"

policy = <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "elasticfilesystem:*",
"Resource": "*"
},
{
"Effect": "Allow",
"Action": [
"cloudwatch:*",
"logs:*",
"ecr:*",
"iam:PassRole",
"ecs:*"
],
"Resource": "*"
},
{
"Effect": "Allow",
"Action": [
"ssmmessages:CreateControlChannel",
"ssmmessages:CreateDataChannel",
"ssmmessages:OpenControlChannel",
"ssmmessages:OpenDataChannel"
],
"Resource": "*"
},
{
"Effect": "Allow",
"Action": [
"ecs:RegisterTaskDefinition",
"ecs:ListClusters",
"ecs:DescribeContainerInstances",
"ecs:ListTaskDefinitions",
"ecs:DescribeTaskDefinition",
"ecs:DeregisterTaskDefinition",
"ecs:ListContainerInstances",
"ecs:DescribeClusters"
],
"Resource": "*"
}
]
}
EOF
tags = merge(
var.tags
)
}

resource "aws_iam_role" "jenkins_ecs_role" {
name = var.jenkins_ecs_role_name
description = "iam role for ecs jenkins task"

assume_role_policy = <<EOF
{
"Version": "2008-10-17",
"Statement": [
{
"Sid": "",
"Effect": "Allow",
"Principal": {
"Service": "ecs-tasks.amazonaws.com"
},
"Action": "sts:AssumeRole"
}
]
}
EOF

depends_on = [
aws_iam_policy.jenkins_ecs_policy
]
tags = merge(
var.tags
)
}

resource "aws_iam_role_policy_attachment" "jenkins_ecs_policy_attachment" {
role = aws_iam_role.jenkins_ecs_role.name
policy_arn = aws_iam_policy.jenkins_ecs_policy.arn

depends_on = [
aws_iam_policy.jenkins_ecs_policy,
aws_iam_role.jenkins_ecs_role
]
}

VPC

  为了方便起见,我们直接为其单独起了一个 VPC,并启用了 Internet-gateway,这里直接使用 aws 官方的 module 来实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
locals {
name_prefix = "${var.vpc_name}-${var.environment}"
}

# https://github.com/terraform-aws-modules/terraform-aws-vpc
module "vpc" {
source = "terraform-aws-modules/vpc/aws"

name = local.name_prefix
cidr = var.vpc_cidr

azs = ["${var.region}a", "${var.region}b", "${var.region}c"]
public_subnets = ["${var.public_subnet_1_cidr}", "${var.public_subnet_2_cidr}", "${var.public_subnet_3_cidr}"]
private_subnets = ["${var.private_subnet_1_cidr}", "${var.private_subnet_2_cidr}", "${var.private_subnet_3_cidr}"]

public_subnet_tags = {
Name = "${local.name_prefix}-public-subnets"
"kubernetes.io/role/elb" = "1"
}
private_subnet_tags = {
Name = "${local.name_prefix}-private-subnets"
}

enable_dns_hostnames = true
enable_dns_support = true
enable_nat_gateway = true
single_nat_gateway = true
one_nat_gateway_per_az = false
create_database_subnet_group = false

tags = merge(
var.tags,
{ Name = local.name_prefix }
)
}

安全组规则

  这里可以根据自己的环境需求来修改,此处对内网环境规则比较开放:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
# 内网安全组直接全部放开了,生产环境请根据实际情况进行修改  
resource "aws_security_group" "ecs_jenkins_service_sg" {
name = "${var.ecs_service_name}_sg"
description = "Allow ALL traffic in private subnet for ECS internal service"
vpc_id = module.vpc.vpc_id


ingress = [
{
description = "Allow all private network inbound"
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
ipv6_cidr_blocks = []
self = false
security_groups = []
prefix_list_ids = []
}
]
egress {
cidr_blocks = ["0.0.0.0/0"]
description = "Allow all outbound"
from_port = 0
protocol = "-1"
to_port = 0
}
tags = merge(
var.tags
)
depends_on = [
module.vpc
]
}

# 为公网 ALB 流量入口白名单,这里也是0.0.0.0/0 全开的,强烈建议生产环境限制来源IP或直接使用内网 ALB !!!!
resource "aws_security_group" "jenkins_master_public_lb_sg" {
name = "public_${var.public_load_balancer_name}_sg"
description = "Whitelist for Jenkins master WebUI in public network alb"
vpc_id = module.vpc.vpc_id


ingress {
description = "Allow 443"
from_port = 443
to_port = 443
protocol = "TCP"
cidr_blocks = ["0.0.0.0/0"]
ipv6_cidr_blocks = []
self = false
}

ingress {
description = "Allow 80"
from_port = 80
to_port = 80
protocol = "TCP"
cidr_blocks = ["0.0.0.0/0"]
ipv6_cidr_blocks = []
self = false
}

egress {
cidr_blocks = ["0.0.0.0/0"]
description = "Allow all outbound"
from_port = 0
protocol = "-1"
to_port = 0
}
tags = merge(
var.tags
)
depends_on = [
module.vpc
]
}

EFS

  EFS 即 AWS 托管的 NFS 服务,AWS 相比国内常用的 NFS NAS 服务,拥有更细致的权限管控机制,动手实操的过程中,会发现直接将 EFS 作为 NFS Volume 挂给 Jenkins container 后,会出现挂载的Volume 无法读写的情况。这是由于 Jenkins 官方的 docker 镜像采用 jenkins 用户(non-root) 作为run user,而我们在 task-definition 中配置的 EFS Volume 默认采用 root 身份挂载,所以 jenkins 用户无法向挂载的目录中读写文件。 解决这个问题有两种方式:
第一种: 重新打包官方的 docker 镜像,改用 root 身份来运行容器(或者直接在 task-difinition 中指定使用特权模式启动容器)

第二种(推荐): 使用 AWS EFS 的 Access Point 来提供挂载 Volume。 Access Point 可以为文件请求指定用户、权限以及根目录。

创建资源

  可用区选择: 由于此 EFS 实例仅用于 ECS,所以选择三个私有 subnet 可用区。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
# 创建 EFS
resource "aws_efs_file_system" "jenkins_ecs" {

performance_mode = "maxIO"
encrypted = false
throughput_mode = "bursting"

tags = merge(
{ Name = var.efs_name },
var.tags
)
depends_on = [
module.vpc
]
}

# mount target 类似可用区性质
resource "aws_efs_mount_target" "efs_mount" {
count = length(module.vpc.private_subnets)
file_system_id = aws_efs_file_system.jenkins_ecs.id
subnet_id = module.vpc.private_subnets[count.index]
security_groups = [aws_security_group.ecs_jenkins_service_sg.id]
depends_on = [
aws_efs_file_system.jenkins_ecs
]
}

# 创建 access point,指定根路径,由于我们jenkins容器默认使用jenkins用户启动,uid gid 均为 1000,因此需要在此指定,否则容器将无法正常写入这个Volume
resource "aws_efs_access_point" "jenkins_master" {
file_system_id = aws_efs_file_system.jenkins_ecs.id

posix_user {
gid = 1000
uid = 1000
}

root_directory {
path = "/var/jenkins_home"
creation_info {
owner_gid = 1000
owner_uid = 1000
permissions = "0777"
}
}
depends_on = [
aws_efs_file_system.jenkins_ecs
]
}

Cloudwatch

  CloudWatch 提供了各项 AWS 计算服务的集成,包括像 lambda 等。这里我们需要先创建 log group 再为其创建 log steam 供 ECS 写入日志。

1
2
3
4
5
6
7
8
9
10
11
12
13
resource "aws_cloudwatch_log_group" "jenkins_ecs_log_group" {
name = "/aws/ecs/${var.ecs_cluster_name}/${var.ecs_service_name}"
retention_in_days = 7
tags = merge(
var.tags
)
}

resource "aws_cloudwatch_log_stream" "jenkins_ecs_log_stream" {
name = var.ecs_service_name
log_group_name = aws_cloudwatch_log_group.jenkins_ecs_log_group.name
}

创建 ALB

  由于我们 ECS 部署在内网 VPC, 又要提供对外的访问入口,因此必须使用 ALB(application loadblancer)作为入口。(NLB 作为四层负载均衡器,无法设置安全组规则,而 ALB 可以,因此我们也能对流量入口的 inbound 流量做一定的白名单限制):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
resource "aws_lb" "jenkins_master" {
name = var.public_load_balancer_name
internal = false
load_balancer_type = "application"
security_groups = [aws_security_group.jenkins_master_public_lb_sg.id]
subnets = [module.vpc.public_subnets[0], module.vpc.public_subnets[1], module.vpc.public_subnets[2]]

# 这里为了后面 destroy 方便,把删除保护关闭了, 生产可以打开。
# enable_deletion_protection = true
enable_deletion_protection = false

tags = merge(
var.tags
)
depends_on = [
module.vpc
]
}

# 创建target group,此处的健康检查必须配置,否则后面的 ECS task 会因为 loadbalancer 健康检查不通过而被 kill,出现不断重启的情况。
resource "aws_lb_target_group" "ecs_jenkins_master" {
name = var.lb_target_group_name
port = 80
protocol = "HTTP"
target_type = "ip"
vpc_id = module.vpc.vpc_id
health_check {
interval = 60
path = "/login"
port = 8080
protocol = "HTTP"
timeout = 10
unhealthy_threshold = 5
healthy_threshold = 5
matcher = "200"
}
tags = merge(
var.tags
)

depends_on = [
aws_lb.jenkins_master
]
}


# ALB 的 443 入口直接转发到上面的 target group. 这里必须为其指定域名证书
resource "aws_lb_listener" "jenkins_master" {
load_balancer_arn = aws_lb.jenkins_master.arn
port = 443
protocol = "HTTPS"
certificate_arn = data.aws_acm_certificate.jenkins.arn
default_action {
target_group_arn = aws_lb_target_group.ecs_jenkins_master.arn
type = "forward"
}
tags = merge(
var.tags
)
depends_on = [
aws_lb.jenkins_master
]
}

# 80 301 重定向到 443 端口
resource "aws_lb_listener" "redirect_to_443" {
load_balancer_arn = aws_lb.jenkins_master.arn
port = "80"
protocol = "HTTP"

default_action {
type = "redirect"

redirect {
port = "443"
protocol = "HTTPS"
status_code = "HTTP_301"
}
}
tags = merge(
var.tags
)
depends_on = [
aws_lb.jenkins_master
]
}

# 如果不需要https,那可以不配置域名证书,将上面两个 listener 注释掉,启用下面这个 80 listener(不推荐)
# if you don't need to use HTTPS as the ingress:
/* resource "aws_lb_listener" "jenkins_master" {
load_balancer_arn = aws_lb.jenkins_master.arn
port = 80
protocol = "HTTP"
default_action {
target_group_arn = aws_lb_target_group.ecs_jenkins_master.arn
type = "forward"
}
tags = merge(
var.tags
)
depends_on = [
aws_lb.jenkins_master
]
} */

ECS

  一切前置要求的资源已经准备就绪,现在我们来到主角,创建 jenkins master。

创建 ECS cluster

1
2
3
4
5
6
resource "aws_ecs_cluster" "jenkins" {
name = var.ecs_cluster_name
tags = merge(
var.tags
)
}

创建 task-definition

  这个可以类比理解为 k8s 中的 deployment 资源,编排容器。可以看到,我们在此为容器指定了 NFS volume, 容器端口以及健康检查等:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
resource "aws_ecs_task_definition" "jenkins_master" {
family = var.ecs_task_definitions_name
task_role_arn = aws_iam_role.jenkins_ecs_role.arn
execution_role_arn = aws_iam_role.jenkins_ecs_role.arn
network_mode = "awsvpc"
requires_compatibilities = [
"FARGATE"
]
cpu = var.jenkins_master_cpu_size
memory = var.jenkins_master_memory_size


volume {
name = "jenkins_home"

# use efs directly, this way must be run container as root user.
# ref: https://stackoverflow.com/questions/61967965/how-to-run-nonroot-user-in-ecs
/* efs_volume_configuration {
file_system_id = var.efs_id
root_directory = "/"
} */


# use efs access point.
efs_volume_configuration {
file_system_id = aws_efs_file_system.jenkins_ecs.id
transit_encryption = "ENABLED"
transit_encryption_port = 2999
root_directory = "/"
authorization_config {
access_point_id = aws_efs_access_point.jenkins_master.id
iam = "ENABLED"
}
}

}

container_definitions = jsonencode(
[
{
"name" : "jenkins-master",
"image" : var.jenkins_master_image,
"cpu" : var.jenkins_master_cpu_size,
"memory" : var.jenkins_master_memory_size,
"portMappings" : [
{
"containerPort" : 8080,
"hostPort" : 8080,
"protocol" : "tcp"
},
{
"containerPort" : 50000,
"hostPort" : 50000,
"protocol" : "tcp"
}
],
"essential" : true,
"environment" : [],
"mountPoints" : [
{
"sourceVolume" : "jenkins_home",
"containerPath" : "/var/jenkins_home"
}
],
"logConfiguration" : {
"logDriver" : "awslogs",
"options" : {
"awslogs-group" : "/aws/ecs/${var.ecs_cluster_name}/${var.ecs_service_name}"
"awslogs-region" : var.region,
"awslogs-stream-prefix" : var.ecs_service_name
}
},
"healthCheck" : {
"command" : [
"CMD-SHELL",
"curl -f http://localhost:8080/login || exit 1"
],
"interval" : 30,
"timeout" : 5,
"retries" : 3,
"startPeriod" : 120
}
}
]
)
tags = merge(
var.tags
)
depends_on = [
aws_ecs_cluster.jenkins
]
}

创建服务发现

  ECS 服务间的调用不能像 k8s 内部一样直接使用 FQDN 来访问,那当我们需要相互调用的时候总不能一直走公网吧。为此,AWS 为我们提供了服务发现,每个 service discovery namespace 可以创建一个私有的 DNS 服务,并通过指定 service discovery service 指定为哪些 ECS service 自动创建 DNS 解析。
  这个服务发现自动注册的过程大致为: ECS service(类比为 k8s 的 deployment 资源)会根据 task-definition 拉起 ECS task(类比为 k8s 的 pod 资源),ECS service 中指定 service discovery service 可以类似在 k8s 中创建 service。 service discovery 会为 ecs task 的 IP 自动创建 DNS 记录,这样我们就能通过固定的 FQDN 来访问 ECS service 了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# 这里会为我们创建一个 private host zoom,域为 ecs.local  
resource "aws_service_discovery_private_dns_namespace" "ecs" {
name = "ecs.local"
description = "ecs service discovery dns ns"
vpc = module.vpc.vpc_id
tags = merge(
var.tags
)
}

# 这里创建 service discovery service 供后面的 ecs service 注册
resource "aws_service_discovery_service" "jenkins_master" {
name = "jenkins-master"

dns_config {
namespace_id = aws_service_discovery_private_dns_namespace.ecs.id

dns_records {
ttl = 10
type = "A"
}

routing_policy = "MULTIVALUE"
}

health_check_custom_config {
failure_threshold = 1
}
tags = merge(
var.tags
)

depends_on = [
aws_service_discovery_private_dns_namespace.ecs
]
}

创建 ECS service

  ECS 可以看作把 k8s 的 deployment 与 service 资源做了封装。 ECS service 会根据指定的 task-definition 创建容器,并这些容器定义网络资源。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
resource "aws_ecs_service" "jenkins_master" {
name = var.ecs_service_name
cluster = aws_ecs_cluster.jenkins.id
desired_count = 1
enable_execute_command = true
platform_version = "LATEST"
task_definition = aws_ecs_task_definition.jenkins_master.arn
deployment_maximum_percent = 200
deployment_minimum_healthy_percent = 100
launch_type = "FARGATE"
# iam_role = var.ecs_task_role_arn
network_configuration {
assign_public_ip = false
security_groups = [
aws_security_group.ecs_jenkins_service_sg.id
]
subnets = tolist(module.vpc.private_subnets)
}

# 指定负载均衡器,该 service 的容器会自动注册到负载均衡器的指定 target group 中,因此前面 ALB 部分的健康检查至关重要。
load_balancer {
target_group_arn = aws_lb_target_group.ecs_jenkins_master.arn
container_name = "jenkins-master"
container_port = 8080
}

# 服务注册,此 ECS service 会自动注册到该 service discovery service,自动在 private host zoom 中为这些容器创建 A 记录。
service_registries {
registry_arn = aws_service_discovery_service.jenkins_master.arn
}
scheduling_strategy = "REPLICA"

tags = merge(
var.tags
)
depends_on = [
aws_ecs_task_definition.jenkins_master,
aws_lb_target_group.ecs_jenkins_master,
aws_efs_file_system.jenkins_ecs
]
}

使用 Terraform 创建资源

  如何为终端配置 terraform 所需的 aws 凭据这里不在赘述,请移步: 配置AWS profile

1
2
3
4
5
6
7
8
9
10
11
12
git clone https://github.com/AlliotTech/terraform-aws-ecs-jenkins  
cd terraform-aws-ecs-jenkins

# 参数调整: 修改 variables/prod.tfvars

# 创建资源
terraform apply -var-file=variables/prod.tfvars
# 等待plan结束后输入 yes 回车确认创建资源,这将花费大约两三分钟


# 销毁资源,此操作会销毁本次terraform创建的所有资源
# terraform destroy -var-file=variables/prod.tfvars

配置 jenkins

  在一切就绪后,我们可以从 cloudwatch 中找到 jenkins 初始化所需要的密码,访问前面 terraform appy 后输出的 alb_dns_name 来初始化 Jenkins,这里不再多赘述。

  我们想要使用 ECS fargate 作为我们的构建节点,需要用到 Jenkins 的 Amazon Elastic Container Service (ECS) / Fargate 插件。在 Jenkins 的插件中心安装完成后,我们进入到 Jenkins 的 “节点管理”,在左侧选择 ”Configure cloud“
具体配置如图:
terraform-aws-ecs-jenkins-02
terraform-aws-ecs-jenkins-03
terraform-aws-ecs-jenkins-04
terraform-aws-ecs-jenkins-05
terraform-aws-ecs-jenkins-06
terraform-aws-ecs-jenkins-07

测试流水线任务

  我们创建一个 job, 其 pipline 为:

1
2
3
4
5
6
7
8
9
10
11
12
13
pipeline {
agent {
label 'ecs-fargate'
}

stages {
stage('Hello') {
steps {
echo 'Hello World in agent'
}
}
}
}

  执行构建该 Job 后,我们去到 AWS ECS console,可以看到 Jenkins 自动为我们创建了 Jenkins agent 的 task-definition,并且根据 task-definition 创建了 task 来执行我们的构建任务,当构建任务完成后,便会自动销毁该 task。至此,我们的 Jenkins 体系就搭建完成了。

参考文档

https://docs.aws.amazon.com/AmazonECS/latest/developerguide/ecs-exec.html
https://docs.aws.amazon.com/zh_tw/AmazonECS/latest/developerguide/ecs-exec.html
https://docs.aws.amazon.com/AmazonECS/latest/APIReference/API_HealthCheck.html
https://hands-on.cloud/managing-amazon-ecs-using-terraform
https://faun.pub/accessing-efs-as-a-non-root-user-inside-ecs-container-using-efs-access-point-74bcd9eff04f
https://stackoverflow.com/questions/69488032/terraform-how-to-mount-efs-access-point-to-ec2
https://tomgregory.com/jenkins-jobs-in-aws-ecs-with-slave-agents/
https://docs.aws.amazon.com/AmazonECS/latest/APIReference/API_HealthCheck.html