像 K8S 一样部署 EC2:Launch Template + Auto Scaling 实践

目录:
AWS概述
EMR Serverless
AWS VPC及其网络
关于AWS网络架构的思考
AWS IRSA 原理与使用
AWS S3 和 Lambda 使用
LaunchTemplate + AutoScalingGroup 实践


一般来说在企业生产环境中,业务服务都是部署在云上的 k8s 中的,比如 AWS 的 EKS。服务的流量在 VPC 子网中通过路由表指向 NAT Gateway,经此可以流向公网。但是这种方式也不是对所有的服务都适用,比如当并发连接很高时,NAT 受限于性能存在并发问题

除此之外,NAT 的流量费用也很高,如果服务是音/视频类型的服务,通过 NAT 会产生海量的流量费。

对于上述场景,可以通过在公有子网中创建 EC2,将服务部署在 EC2 中或者将流量出口代理到公网 EC2 的方式来避免使用 NAT。

但是在公有子网中部署代理服务时,就无法像在 EKS 中一样进行容器化部署和管理了,而是需要另外一套自动化部署和管理的方式。

本文将以 squid 反向代理为例,介绍如何在公有子网中部署和管理服务。

AWS Infrastructure

假设 VPC 和公有子网已经创建好,只需要在其中创建其他 AWS 资源就行了。

创建用户和角色

首先需要创建一个 service group,比如 UserAdminGroup,其 policy 可以通过 Admin 访问策略 和 Deny 访问策略组合而成。Admin 访问策略允许所有操作:

复制代码
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": "*",
            "Resource": "*"
        }
    ]
}

但这个权限太大了,比较危险,因此可以设置一些 Deny 策略,具体 Deny 操作结合实际情况来设置。

有了用户组之后创建一个 service user,比如 proxy-ci-bot,将该用户指向用户组。

虽然该用户拥有的权限很大,但是 AWS 中推荐使用 Role,而不是 User。需要创建一个 operation 的 Role,信任 proxy-ci-bot 这个 User。

后续通过用户来 AssumeRole 时,使用的是这个 Role 的权限,因此 operation 的角色权限需要设置得大一点。

IAM User 代表的是"人",Role 代表的是"能力"。人的权限是长期的,直接使用不安全。在实际使用时,应该从 Role 来临时获取能力(AssumeRole)。AdminUser 是特殊用户,所以才设置了很大的权限。

创建 EC2 模板

在公有子网中部署代理服务时,可以直接部署到 EC2 中。首先创建一个 EC2 模板,用于生成 AMI(Amazon Machine Image)镜像。

创建 EC2 实例

在 AWS EC2 控制台上直接点击就可以创建,具体过程比较简单,注意点如下:

  • 操作系统建议选最新的 Amazon Linux
  • Key pair 不需要,后面会通过 SSM 登录,安全性更高。
  • Network settings 中需要指定安全组,控制 EC2 的访问途径。
  • Advanced network configuration 中需要开启 Auto-assign public IP,这样启动的实例才有公网 ip;Delete on termination 也可以勾选 Yes。

创建好 EC2 模板实例后,该 EC2 暂时不能登录上。需要创建 SSM Role,此过程就要用到前面创建的operation 角色了。SSM Role 创建过程如下(注意账号和 profile 替换):

复制代码
aws sts assume-role --role-arn "arn:aws-cn:iam::123456789012:role/product/operation" --role-session-name assume-session --profile proxy-ops
aws iam create-role --role-name SSM-Role --assume-role-policy-document file://trust.json --profile proxy-ops
aws iam create-instance-profile --instance-profile-name SSM-Role --profile proxy-ops
aws iam add-role-to-instance-profile --role-name SSM-Role --instance-profile-name SSM-Role --profile proxy-ops
aws iam attach-role-policy --role-name SSM-Role --policy-arn=arn:aws-cn:iam::123456789012:policy/Role-SSM --profile proxy-ops
aws iam attach-role-policy --role-name SSM-Role --policy-arn=arn:aws-cn:iam::aws:policy/AmazonS3FullAccess --profile proxy-ops
 aws iam attach-role-policy --role-name SSM-Role --policy-arn=arn:aws-cn:iam::aws:policy/AmazonSSMFullAccess --profile proxy-ops
aws iam attach-role-policy --role-name SSM-Role --policy-arn=arn:aws-cn:iam::aws:policy/ElasticLoadBalancingFullAccess --profile proxy-ops
aws iam attach-role-policy --role-name SSM-Role --policy-arn=arn:aws-cn:iam::aws:policy/AutoScalingConsoleReadOnlyAccess --profile proxy-ops

注意带账号的 policy 是自定义的,其他的 policy 是官方自带的。其中 trust.json 如下:

复制代码
 {
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
                "Service": "ec2.amazonaws.com.cn"
            },
            "Action": "sts:AssumeRole"
        }
    ]
}

总之,创建好拥有对应权限的 SSM Role 之后,需要在 EC2 实例的 Security 设置中 Modify IAM Role,等几分钟后 EC2 就可以通过 Session Manager 登录了:

配置 EC2 实例

配置 squid

登录到 EC2 实例后,首先安装 squid

复制代码
sudo yum update -y
sudo yum install squid
# 查看版本
squid -v

squid 安装后配置文件位于 /etc/squid 目录下。不管是正向代理还是反向代理,都需要修改配置文件 squid.conf。通过 squid -k parse 命令可以验证配置语法是否正确。

初始化 squid 缓存:

复制代码
squid -z

接着做一些设置让 squid 能在 ec2 创建时自动启动:

复制代码
# 开机自启
systemctl enable squid
# 开启 squid
systemctl start squid
# 查看状态
systemctl status squid

以上配置完成后,EC2 中的 squid 就启动了,并且以后可以开机自启。

日志 rotate

squid 的日志轮转配置在 /etc/logrotate.d/squid 文件中,默认一星期轮转一次。如果日志太大,可以修改配置,比如:

复制代码
/var/log/squid/*.log {
    # weekly
    daily
    size 800M
    rotate 5 # 保留 5 份历史日志
    compress # 对历史日志进行压缩
    delaycompress
    notifempty
    missingok
    nocreate
    sharedscripts
    postrotate
      # Asks squid to reopen its logs. (logfile_rotate 0 is set in squid.conf)
      # errors redirected to make it silent if squid is not running
      /usr/sbin/squid -k rotate 2>/dev/null
    endscript
}

weekly 改成 daily,增加 size 800M,这样当日志达到 800M 或者超过 1 天时,就会创建一份新的日志。

日志轮转定时任务的设置在 /usr/lib/systemd/system/logrotate.timer 中,默认是每天执行一次。如果日志增长非常快,比如每小时 800M,那等到定时任务进行轮转时日志已经非常大了。因此可以修改定时任务的执行频率,比如改成每小时一次。

rotate 本身并没有什么性能消耗,但是对历史日志进行压缩时,会消耗 CPU 和 IO。

AutoScalingGroup 自动部署

生成 AMI

上面初始化了一个 EC2 模板实例,并启动了 squid 代理软件,已经可以提供代理服务了。接下来需要以此为基础创建一个 AMI(Amazon Machine Image):

复制代码
aws ec2 create-iamge --instance-id ${{instanceTemplateId}} --name "$amiName" --description "xxx" --tag-specifications "ResourceType=image,Tags=[{Key=Name,Value='$amiName'}]" --profile proxy-ops

这样就创建了一个 EC2 镜像,在控制台 EC2 -> AMI 可以查看。

创建 LaunchTemplate

有了 AMI,还需要创建一个 LaunchTemplate。选择上面创建的 AMI 作为软件系统,再选择某个 Instance Type 作为硬件系统,加上一些其他设置,就可以创建一个 LaunchTemplate 了。需要注意的点:

  • 这次需要 Key pair 了。在 Key pair 界面可以创建登录密钥。
  • Network settings 中需要指定安全组,控制 EC2 的访问途径。这一点非常重要,因为 EC2 放在了公网,其访问控制完全依赖于安全组。
  • Advanced network configuration 中需要开启 Auto-assign public IP,这样启动的实例才有公网 ip;Delete on termination 可以勾选 Yes。
  • Advanced details 中可以配置 instance profile,用于控制 EC2 的 AWS 权限(EC2 → Instance Profile → IAM Role → Policy)。

LaunchTemplate 创建好之后,就绑定了 AMI(软件) 、InstanceType (硬件)和网络层面的一些设置了。

创建 AutoScalingGroup

有了 LaunchTemplate,接着就要创建 AutoScalingGroup 了。

点击创建,将 LaunchTemplate 绑定到 AutoScalingGroup,再选择对应的 VPC、Subnet,下一步继续选择 Load balancing,开启 Health checks,下一步配置 Scaling limit 和策略,直到完成配置。

AutoScalingGroup 创建完成就会生效,它将以 launchTemplate 为启动参数来创建 EC2 实例,EC2 的安全组即 launchTemplate 中指定的安全组,EC2 所在的网络为 AutoScalingGroup 中指定的 VPC 和 Subnet。

ASG 会根据 Scaling limit 和 policy 来决定 EC2 的数量和扩缩容的策略。由于 ASG 绑定了 Load balancing,因此创建的 EC2 会自动注册到 Load balancer 的 targetGroup 中去。

CICD 实践

上面介绍了 ASG 部署的基本原理和操作,但这些操作都是手动完成的。在实际应用中,项目是会迭代更新的,需要自动化的 CICD。比如现在要在上述基础上改一下 squid 的配置文件,通过 CICD 如何实现呢?

下面以 github workflow 为例介绍如何实现 CICD。

自动更新 EC2 模板

首先可以在项目中创建一个 upgrade 目录,将最新的 squid.conf 放在该目录下。在 CICD 的 workflow ,例如 github action 中,将 upgrade 整个目录上传到 S3:

复制代码
- name: Upload upgrade data
run: |
  s3bucketName='proxy-upgrade-test'
  echo 'Begin upload upgrade data'
  aws s3 sync ./upgrade/ s3://${s3bucketName}/upgrade/

接着可以将 upgrade 文件夹全部下载到 EC2 模板实例上,并且执行更新操作:

复制代码
- name: Sync upgrade data
uses: debugger24/action-aws-ssm-run-command@v1
with:
  aws-region: ${{ inputs.aws-region }}
  instance-ids: ${{ inputs.instanceTemplateId }}
  commands: |
    echo "Download upgrade data from S3"
    aws s3 cp s3://${s3bucketName}/upgrade/ /etc/squid/upgrade/ --recursive
    /bin/sh /etc/squid/upgrade/update.sh

这里要用到 github 官方提供的 debugger24/action-aws-ssm-run-command@v1,这个 action 可以通过 AWS SSM 在 EC2 实例上远程执行命令,这也是为什么我们要在 EC2 模板上设置 SSM 访问的原因。

更新脚本 update.sh 负责替换配置文件:

复制代码
#!/usr/bin/env sh
set -eu

# update squid conf
mv /etc/squid/squid.conf /etc/squid/squid_old.conf
cp -r /etc/squid/upgrade/squid.conf /etc/squid/squid.conf
chown root:squid /etc/squid/squid.conf
squid -k reconfigure

这个脚本会以最新的配置文件替换旧版本的配置文件,并通过 squid -k reconfigure 重新加载配置,这样 EC2 模板就是最新的配置了。

实际上更新操作会复杂得多,但原理是一样的,update.sh 负责完整的更新流程,其中可以包含用其他脚本实现的子流程。

创建新的 AMI

在项目的 upgrade 目录下创建一个 version.txt 文件,用于存放版本信息:

复制代码
1.0.1/update squid conf

AMI 的版本和描述需要通过版本信息来获取:

复制代码
- name: Describe tags
  id: describe_tags
  run: |
    echo 'Begin describe tags'
    versionInfo=$(cat upgrade/version.txt)
    echo 'versionInfo : '$versionInfo
    latestVersion=$(echo $versionInfo | cut -d'/' -f1)
    echo "latestVersion=$latestVersion" >> $GITHUB_OUTPUT
    echo "latestVersionDesc=$(echo $versionInfo | cut -d'/' -f2)" >> $GITHUB_OUTPUT
    timeString=$(date "+%Y%m%d%H%M%S")
    echo "amiLatestVersion=${latestVersion}-${timeString}" >> $GITHUB_OUTPUT

通过这个 action 可以将版本号,版本描述以及 AMI 的版本信息保存到 github output 中。

接下来为 EC2 模板添加一些 tag,用于标识代理服务的版本信息:

复制代码
- name: Set tags for base instance
  run: |
    echo 'Begin set tags for base instance'
    aws ec2 create-tags --resources ${{ inputs.instanceTemplateId }} --tags Key='Version',Value='${{ steps.describe_tags.outputs.latestVersion }}' Key='Description',Value='${{ steps.describe_tags.outputs.latestVersionDesc }}'

上文介绍过通过 aws 命令手动创建 AMI,在 CICD 中可以将创建命令放到 github action 中:

复制代码
- name: Create AMI
  id: create_ami
  run: |
    echo 'Begin create AMI'
    amiName=${{ inputs.ami-name-prefix }}-${{ steps.describe_tags.outputs.amiLatestVersion }}
    createAMICommand=$(aws ec2 create-image --instance-id ${{ inputs.instanceTemplateId }} --name "$amiName" --description "${{ steps.describe_tags.outputs.latestVersionDesc }}" --tag-specifications "ResourceType=image,Tags=[{Key=Name,Value='$amiName'}]" --output json)
    echo "amiId=$(echo $createAMICommand | jq '.ImageId')" >> $GITHUB_OUTPUT
    echo 'amiId = '$amiId

这样就会基于最新的 EC2 模板创建一个新的 AMI,其名称格式为: {prefix}-1.0.1-20260210151033,其中 prefix 是自定义的名称前缀,1.0.1 为版本号,后缀是时间戳。该 action 每执行一次都会生成一个新的 AMI。

创建 LaunchTemplate

上文创建 LaunchTemplate 时绑定了手动创建的 AMI,但在 CICD 中 AMI 还没生成的话,launchTemplate 可以绑定 AWS 官方只包含纯净操作系统的 AMI,此时 launchTemplate 会有一个默认的初始版本 1。

LaunchTemplate 创建后 ID 格式如 lt-xxxxxx,CICD 中只需要更新其版本:

复制代码
- name: Create new launchTemplate version
  id: create_launchTemplate_version
  run: |
    echo 'Begin create new launchTemplate version'
  templateVersion=$(aws ec2 create-launch-template-version --launch-template-id "${{ inputs.launchTemplateId }}" --version-description "${{ steps.describe_tags.outputs.latestVersionDesc }}" --source-version ${{ inputs.sourceVersion }} --launch-template-data '{"ImageId":${{ steps.create_ami.outputs.amiId }}}' --output json)
    templateVersionNum=$(echo $templateVersion | jq '.LaunchTemplateVersion.VersionNumber')
    echo "templateVersionNum=$templateVersionNum" >> $GITHUB_OUTPUT
    echo 'latest launchTemplate version = ' $templateVersionNum

这个 action 中 inputs.launchTemplateIdlt-xxxxxxinputs.sourceVersion 为初始的 launchTempalte 版本 1,需要作为参数传入。这个 action 执行完之后,会创建一个新的 launchTemplate 版本 2。因为在创建时通过 --source-version 参数指定了默认版本 1,所以新版 launchTemplate 的 instanceType 继承于版本 1,而软件系统则是绑定的新的 AMI。

该 action 每执行一次都会生成一个新的 launchTemplate 版本,新版本号是 AWS 生成的自增值。

创建 AutoScalingGroup 并部署服务

AutoScalingGroup 手动创建完成后基本不用修改了。通过 start-instance-refresh 命令可以滚动更新 ec2 实例:

复制代码
- name: Start instance refresh
  run: |
    echo 'Begin instance refresh'
    echo "aws autoscaling start-instance-refresh --auto-scaling-group-name "${{ inputs.autoScalingGroupName }}" --desired-configuration '{"LaunchTemplate":{"LaunchTemplateId":"${{ inputs.launchTemplateId }}","Version":"${{ steps.create_launchTemplate_version.outputs.templateVersionNum }}"}}' --preferences '{"InstanceWarmup": 90, "MinHealthyPercentage": 100, "SkipMatching": true}' --output json"
    refreshAction=$(aws autoscaling start-instance-refresh --auto-scaling-group-name "${{ inputs.autoScalingGroupName }}" --desired-configuration '{"LaunchTemplate":{"LaunchTemplateId":"${{ inputs.launchTemplateId }}","Version":"${{ steps.create_launchTemplate_version.outputs.templateVersionNum }}"}}' --preferences '{"InstanceWarmup": 90, "MinHealthyPercentage": 100, "SkipMatching": true}' --output json)  
    echo 'Instance refreshAction: ' $refreshAction  # 结果示例: { "InstanceRefreshId": "xxxx-xx-xxx-xxx-xxxx" }
    if [[ $refreshAction != '' ]]; then
      refreshId=$(echo $refreshId | jq '.InstanceRefreshId' | tr -d '"') # 结果示例:xxxx-xx-xxx-xxx-xxxx
      echo "refreshId is ": $refreshId # refreshId 是 refresh 操作的标识符
      sleep 180
      for i in $(seq 1 20); do
        refreshStatus=$(aws autoscaling describe-instance-refreshes --auto-scaling-group-name ${{ inputs.autoScalingGroupName }} --instance-refresh-ids $refreshId --output json)
        echo 'Describe instance refresh status: ' $refreshStatus # 结果示例:Describe instance refresh status:  { "InstanceRefreshes": [ { "InstanceRefreshId": "xxx", "AutoScalingGroupName": "xxx", "Status": "InProgress", "StatusReason": "Waiting for instances to warm up before continuing. For example: i-xx is warming up.", ...  } ] }
        status=$(echo refreshStatus | jq -r '.InstanceRefreshes[0].Status')
        echo "Attempt times is: $i, status: $status" # 结果示例:Attempt times is: 1, status: InProgress
        if [[ status != 'Pending' ]] && [[ status != 'InProgress' ]]; then
            break
        fi
        sleep 60
      done
      if [[ $refreshStatus != 'Successful' ]]; then
        echo 'Last instance refresh status : ' $status
        echo "InstanceRefresh failed."
        exit 1
      fi
      echo 'InstanceRefresh success.'
    else
      echo "execute instance refresh cli result is null."
      exit 1
    fi

以上 action 执行滚动更新 ec2 的操作,命令 aws autoscaling start-instance-refresh 会返回 refresh 操作的唯一标识,然后根据唯一标识来获取滚动更新结果。由于命令中设置了 ec2 启动后的 warmUp 时间(90s),因此更新过程需要花费一定的时间,可以通过轮训的方式来查询最终结果。这里设置的轮询总时间差不多是 20 * 3m = 60min。

更新状态也可以在 ASG 控制台的 Instance refresh 选项中查看。

CICD 流程

CICD 的流程示意图如下:

EC2 模板在更新后即是目标软件系统,以此来构建 AMI,再根据 launchTemplate 源版本和 AMI 来生成最新的 launchTemplate 版本。在 ASG 中绑定该版本,作为启动参数来创建 EC2。LaunchTemplate 源版本的软件系统并不重要,但硬件系统即 InstanceType 是重要的。当 InstanceType 发生变更时,需要更换源版本。

动态扩缩容策略

ASG 的 Automatic scaling 选项中可以设置动态扩缩容策略。动态扩缩容策略有三种类型,分别是 Simple scalingStep scalingTarget tracking scaling 三种。

Target tracking scaling

目标跟踪策略是最简单的,只需要指定一个目标值就行,如 ASGAverageCPUUtilization 利用率 70%。Auto Scaling 会自动计算扩缩容动作,与 K8S 生态中的 hpa 类似。

Step scaling

步长策略基于 CloudWatch 告警触发,可以设置多个调整级别。

还是以 CPU 利用率来举例。先在 CloudWatch 中设置一个告警,当 ASG(管理的 EC2)的平均 CPU 利用率达到 70% 时触发。在创建 dynamic scaling policy 时,选择该告警,设置告警触发后的 action 为:

  • Add 1 capacity units when 70 <= cpu_usage_active < 80。
  • Add 2 capacity units when 80 <= cpu_usage_active < +infinity。

Simple scaling

在 Step scaling 中,可以根据告警指标的取值范围来设置不同的 action。但如果告警指标类型为离散的,就不适用了。比如告警指标只有两个取值,分别是 0 和 1。此时可以用 Simple scaling 来扩缩容。

以上三种策略中,只有目标跟踪策略不依赖于告警,其他两种都依赖于 CloudWatch 告警。自动扩缩容策略会持续采集指标值,并持续进行扩缩容判断。

但是自动扩缩容策略可以像 K8S 的 hpa 一样设置稳定时间窗口,也就是说,如果某个策略触发了,在稳定时间窗口内,该策略不会再次触发,只有等待稳定时间窗口过去后,如果指标值还会触发该策略,才会再次执行。

参考资料

1\]. https://docs.aws.amazon.com/autoscaling/ec2/userguide/create-asg-launch-template.html \[2\]. https://docs.github.com/en/actions

相关推荐
小锋学长生活大爆炸9 小时前
【教程】免Root在Termux上安装Docker
运维·docker·容器
进击切图仔9 小时前
常用 Docker 命令备份
运维·docker·容器
阿里云云原生12 小时前
巨人网络《超自然行动组》携手阿里云打造云原生游戏新范式
云原生
我在人间贩卖青春15 小时前
C++之STL容器
c++·容器·stl
71ber16 小时前
深入理解 HAProxy:四层/七层透传与高级 ACL 调度详解
linux·云原生·haproxy
A-刘晨阳16 小时前
K8S 之 DaemonSet
运维·云原生·容器·kubernetes·daemonset
小锋学长生活大爆炸17 小时前
【教程】查看docker容器的TCP连接和带宽使用情况
tcp/ip·docker·容器
ccino .18 小时前
【Drupal文件上传导致跨站脚本执行(CVE-2019-6341)】
运维·网络安全·docker·容器
sun032219 小时前
【Docker】构建镜像时使用的 Dockerfile ,以及其中的 MicroDNF
运维·docker·容器
2501_9481142420 小时前
资深程序员真实测评:9家中转API平台实战横评
微服务·云原生·架构