发布于 2026-01-06 8 阅读
0

使用 Amazon ECS 和 ECS Anywhere 创建多架构 CI/CD 解决方案 DEV 的全球展示挑战赛,由 Mux 呈现:展示你的项目!

使用 Amazon ECS 和 ECS Anywhere 创建多架构 CI/CD 解决方案

由 Mux 主办的 DEV 全球展示挑战赛:展示你的项目!

请您填写这份简短的调查问卷,告诉我如何才能改进类似这样的文章。前 20 位完成问卷的参与者将获得 25 美元的 AWS 抵用金——快来参与调查吧!

各组织正尽可能快地将工作负载迁移到云端。虽然大多数应用程序都可以轻松迁移到云端,但由于低延迟或数据主权方面的要求,某些应用程序仍需保留在本地。

无论工作负载位于何处,组织都希望能够一次开发,然后使用一套通用的 API 以敏捷和一致的方式将工作负载部署到云端或本地,从而进行管理和操作。

此外,企业需要能够在其各个环境中充分利用在技能、技术和流程方面的投资。这促使许多企业寻求混合云架构,以整合其本地和云端运营,从而支持广泛的用例。

本文将探讨如何将应用程序部署到您自己的环境和 AWS 云平台。我们将以一个简单的 Docker 化 Spring Boot 应用程序为例,演示如何自动化构建和部署该应用程序,使其能够在任何位置以及多种处理器架构上运行。为此,我将配置 Amazon ECS 和 ECS Anywhere,它们允许您在任何位置运行 ECS 应用程序。

我们将介绍如何设置一个简单的混合环境,将我的家庭网络作为您自己的本地环境的代理,并设置站点到站点 VPN 和软件组件,使我们能够将容器化应用程序无缝部署到我们的云服务器和本地服务器上。

它最终的样子是这样的:

拱

它可以帮助您解决以下类型的使用场景和问题:

  • 我构建应用程序时会考虑使其能够在不同的架构类型上运行,这为我提供了更多选择和方案。
  • 简化并集中管理我的应用程序构建,这样我就可以一次构建,部署到任何架构的任何位置。
  • 管理我在 AWS 或我自己的环境中运行的容器化应用程序(通过 ECS Anywhere)。
  • 为可能需要在本地运行的应用程序提供额外的部署选项

这篇文章将分为两部分。

第一部分将着眼于设置多架构构建管道,该管道将获取我们的示例 Spring Boot 应用程序,然后构建并将其部署到运行混合 x86 和基于 AWS Graviton2/arm 实例类型的一些目标集群。

第二部分将着眼于扩展此模型,使用 ECS Anywhere 设置混合环境,然后使用该环境无缝部署同一应用程序。

你需要准备什么

您可以继续阅读本文,我已经发布了所有脚本和代码,您可以自行复现。GitHub代码库链接在此

您还需要一个具备以下条件的开发环境:

  • 需要拥有相应权限级别(管理员)的 AWS 账户,以及已配置的 CodeCommit。
  • AWS CLI
  • Docker 运行在本地环境中
  • AWS CDK 已安装并运行(cdk --version 版本至少应为 1.110.1)

【编辑:我已经移除了 Docker Hub 的要求并进行了简化,现在我们只使用 Amazon ECR。】

成本

我运行这个演示版大约一周了,每天花费大约 22 美元。但我估计,如果你先把它搭建好运行,然后再探索并移除所有功能,成本会低得多。这些费用不包括你本地环境可能产生的任何成本。

最后,我会讲解如何清除所有物品,作为清理工作的一部分。

第零部分:申请

我们将使用的应用程序是一个简单的 Spring Boot 应用程序,它会根据底层架构显示不同的徽标。

该应用程序已容器化,以下是 Dockerfile。

FROM amazoncorretto:11 as builder

COPY . /srv
WORKDIR /srv
RUN ./mvnw package spring-boot:repackage


FROM amazoncorretto:11

COPY --from=builder /srv/target/spring-boot-demo-1.0.0.jar /srv/
EXPOSE 8080

ENTRYPOINT ["java","-jar", "/srv/spring-boot-demo-1.0.0.jar"]

Enter fullscreen mode Exit fullscreen mode

当您通过网络浏览器访问该应用程序时,它会显示有关其运行所依赖的底层系统的信息,并根据进程架构改变图像。

我们可以在本地开发环境中运行此应用程序,稍后我们将看到,这样您就可以像在开发工作流程中通常那样看到它的运行情况。

第一部分:搭建多架构 CI/CD 流水线

概述

我们将使用基础设施即代码 (IaC) 来搭建环境,在本演练中,我将使用 AWS CDK。这将实现以下功能:

  • 通过安装脚本设置初始源代码和容器仓库。
  • 搭建网络基础设施,包括我们稍后会用到的站点到站点 VPN。
  • 设置 Amazon ECS 集群,部署我们的应用程序
  • 部署一个 CI/CD 流水线,以便在检测到应用程序变更时自动重建和重新打包应用程序,并将最新版本部署到我们的 Amazon ECS 集群。

这就是它最终的样子。

拱

运行安装脚本

[1] 在您的开发机器上检出存储库。

在下面的示例中,我切换到我的主文件夹下一个名为 blog-demo 的单独文件夹后,检出存储库 [cd ~/blog-demo]

cd ~
mkdir blog-demo
cd ~/blog-demo
git clone https://github.com/094459/blog-multi-arch-springboot.git
cd blog-multi-arch-springboot/demo-multiarch-springboot-multiarch
Enter fullscreen mode Exit fullscreen mode

[2] 查看 setup.sh 脚本,修改脚本顶部的参数,使其适用于您的环境。您必须修改 AWS 默认区域和 AWS 账户,其他参数可以保留,但都是可以自定义的。所有更改都需要更新到我们稍后将使用的 CDK 应用程序中。

在接下来的讲解中,我将使用这些数值。

AWS_DEFAULT_REGION={your AWS region}
AWS_ACCOUNT={your AWS account number}
AWS_ECR_REPO=demo-multiarch-springboot-ecsanywhere
AWS_CC_REPO=demo-multiarch-springboot-ecsanywhere
COMMIT_HASH="abcdef"
Enter fullscreen mode Exit fullscreen mode

[3] 运行 setup.sh 脚本,该脚本将执行以下操作:1/创建代码提交存储库,2/创建 ECR 存储库,以及 3/在 ECR 存储库中构建和发布示例 Spring Boot 应用程序。

我们将此应用程序容器化,因为我们将在创建 Amazon ECS 集群时使用此容器。

运行此脚本可能需要 10-15 分钟(或更长时间,具体取决于您的网络速度),但您应该会看到类似这样的内容(您的具体信息将与以下内容不同)。

...
...
The push refers to repository [704533066374.dkr.ecr.eu-west-1.amazonaws.com/demo-multiarch-springboot-ecsanywhere-test]
05aeb36b29bc: Pushed
# create AWS ECR Repo
91a6dcd86b53: Pushed
ec1372e9de86: Pushed
abcdef-arm64: digest: sha256:f51d9da34442fb53f67439752754fd36e02406d061db0a574429e6ecf94d9689 size: 954
Created manifest list 704533066374.dkr.ecr.eu-west-1.amazonaws.com/demo-multiarch-springboot-ecsanywhere-test:abcdef
{
   "schemaVersion": 2,
   "mediaType": "application/vnd.docker.distribution.manifest.list.v2+json",
   "manifests": [
      {
         "mediaType": "application/vnd.docker.distribution.manifest.v2+json",
         "size": 954,
         "digest": "sha256:28ad0b732b826e8a27575715b3d922c4ce0065749be31af7325ab5f96c5fba9b",
         "platform": {
            "architecture": "amd64",
            "os": "linux"
         }
      },
      {
         "mediaType": "application/vnd.docker.distribution.manifest.v2+json",
         "size": 954,
         "digest": "sha256:f51d9da34442fb53f67439752754fd36e02406d061db0a574429e6ecf94d9689",
         "platform": {
            "architecture": "arm64",
            "os": "linux"
         }
      }
   ]
}
Enter fullscreen mode Exit fullscreen mode

现在我们的容器化应用程序已存储在 Amazon ECR 中,我们将在构建 Amazon ECS 集群时使用它。

[4] 现在我们要填充已创建的 CodeCommit 代码库。为此,我们将创建一个新的工作目录。

cd ~/blog-demo
git clone https://git-codecommit.eu-west-1.amazonaws.com/v1/repos/demo-multiarch-springboot-ecsanywhere-test demo-app
cd demo-app
cp -r ~/blog-demo/blog-multi-arch-springboot/demo-multiarch-springboot-multiarch/* .
Enter fullscreen mode Exit fullscreen mode

现在你的目录结构应该如下所示

├── cdk-app
│   └── ecs-anywhere
│       ├── app.py
│       ├── ecs_anywhere
│       │   ├── ecs_anywhere_cicd.py
│       │   ├── ecs_anywhere_ecs.py
│       │   ├── ecs_anywhere_pipe.py
│       │   ├── ecs_anywhere_repo.py
│       │   └── ecs_anywhere_vpc.py
│       └── requirements.txt
├── pipeline
│   ├── ecs-pipeline
│   └── eks-pipeline
├── setup.sh
└── springbootdemo
    ├── Dockerfile
    ├── mvnw
    ├── pom.xml
    ├── src
    │   └── main
    │       ├── java
    │       │   └── hello
    │       │       ├── Application.java
    │       │       └── HelloController.java
    │       └── resources
    │           └── static
    │               ├── css
    │               │   └── style.css
    │               └── images
    │                   ├── arm.png
    │                   ├── aws.png
    │                   └── intel.png
    ├── tf-any.json
    ├── tf.json
    └── tf.json.template
Enter fullscreen mode Exit fullscreen mode

[5] 提交此更改,然后发布到 CodeCommit 存储库。

git checkout -b main
git add .
git commit -m "Initial upload"
git push --set-upstream origin main
Enter fullscreen mode Exit fullscreen mode

注意!如果系统提示输入用户名/密码,则说明您可能尚未配置 CodeCommit Git 助手。请查看上面链接的文档。我的 .gitconfig 文件中有以下设置。

> [credential]
>   helper =
>   helper = !aws codecommit credential-helper $@
>   UseHttpPath = true
>
>[user]
>   email = xxxx@xxxx.com
>   name = xxxx
>
Enter fullscreen mode Exit fullscreen mode

[可选] 您可以将默认分支更改为 Main。通过控制台可以更轻松地完成此操作,您只需进入仓库(demo-multiarch-springboot-ecsanywhere-test),创建一个分支(main),然后进入设置并将其设置为默认分支即可。

我们现在有了这个应用程序,我们可以进行更新,更新后会触发我们的 CI/CD 流水线。现在我们可以进行设置了。

通过 AWS CDK 部署 AWS 资源

[7] 现在我们准备使用 AWS CDK 部署 AWS 资源。返回到我们检出的第一个文件夹,然后进入 CDK 应用所在的文件夹。我们需要调整 app.py 文件,其中包含用于自定义演示环境的参数。

cd ~/blog-demo/blog-multi-arch-springboot/demo-multiarch-springboot-multiarch/cdk-app/ecs-anywhere
Enter fullscreen mode Exit fullscreen mode

查看 app.py 文件时,您需要更改 env_EU 和 props。

from aws_cdk import core

from ecs_anywhere.ecs_anywhere_cicd import EcsAnywhereCICDStack
from ecs_anywhere.ecs_anywhere_vpc import EcsAnywhereVPCStack
from ecs_anywhere.ecs_anywhere_ecs import EcsAnywhereECSStack
from ecs_anywhere.ecs_anywhere_pipe import EcsAnywherePipeStack
from ecs_anywhere.ecs_anywhere_repo import EcsAnywhereLBStack

env_EU=core.Environment(region="eu-west-1", account="704533066374")

props = {
    'mydcexternalip': '79.67.110.xx',
    'mydcinternalcidr' : '192.168.1.0/24',
    'awsvpccidr':'10.0.0.0/16',
    'ecsclustername':'mydc-ecs',
    'ecr-repo': 'demo-multiarch-springboot-ecsanywhere',
    'code-repo' : 'demo-multiarch-springboot-multiarch',
    'image-tag' : 'abcdef',
    'home-pi' : '192.168.1.99'
    }

app = core.App()

mydc_vpc = EcsAnywhereVPCStack(
    scope=app,
    id="ecs-anywhere-vpc",
    env=env_EU,
    props=props
)

mydc_lb = EcsAnywhereLBStack(
    scope=app,
    id="ecs-anywhere-lb",
    env=env_EU,
    vpc=mydc_vpc.vpc,
    props=props
)

mydc_ecs_cicd = EcsAnywhereCICDStack(
    scope=app,
    id="ecs-anywhere-cicd",
    env=env_EU,
    vpc=mydc_vpc.vpc,
    props=props  
)

mydc_ecs_pipe = EcsAnywherePipeStack(
    scope=app,
    id="ecs-anywhere-pipe",
    env=env_EU,
    vpc=mydc_vpc.vpc,
    props=props  
)

mydc_ecs = EcsAnywhereECSStack(
    scope=app,
    id="ecs-anywhere-cfn",
    env=env_EU,
    vpc=mydc_vpc.vpc,
    props=props
)

app.synth()
Enter fullscreen mode Exit fullscreen mode

app.py 文件展示了构建此演示所需的各种部署堆栈。我们首先需要做的是设置正确的值。

env_EU=core.Environment(region="{your aws region}", account="{your aws account}")

props = {
    'mydcexternalip': '{your external router IP - you can find this via tools like what is my IP}',
    'mydcinternalcidr' : '{your home network CIDR e.g.192.168.1.0/24}',
    'awsvpccidr':'{your VPC CIDR range e.g 10.0.0.0/16}',
    'ecsclustername':'{name for your ecs cluster e.g. mydc-ecs}',
    'ecr-repo': '{name of the ECR repo - must match what you set in the setup.sh e.g. demo-multiarch-springboot-ecsanywhere}',
    'code-repo' : '{name of the CodeCommit repo - must match what you setup above, e.g. demo-multiarch-springboot-multiarch}',
    'image-tag' : '{initial hash for ECR repo - must match what you have setup above, e.g. abcdef}'
    }
    'home-pi' : '{the IP address of the Raspberry Pi you will be running your workloads on your local network}
Enter fullscreen mode Exit fullscreen mode

修改并保存此文件后,即可部署第一个堆栈。为确保一切运行正常,请键入以下命令:

cdk ls
Enter fullscreen mode Exit fullscreen mode

你应该会看到以下输出,这意味着我们准备就绪。

ecs-anywhere-cfn
ecs-anywhere-pipe
ecs-anywhere-vpc
ecs-anywhere-cicd
ecs-anywhere-lb
Enter fullscreen mode Exit fullscreen mode

部署 VPC 堆栈

[8] 我们将部署的第一个堆栈是 VPC,它将部署和配置所有网络组件。其中很多内容将在第二部分中使用。要进行部署,

cdk deploy ecs-anywhere-vpc
Enter fullscreen mode Exit fullscreen mode

如果成功,您应该会看到类似这样的信息。第二部分需要用到这些信息,所以请务必做好记录,虽然您也可以在 CloudFormation 控制台中查看这些信息。

 ✅  ecs-anywhere-vpc

Outputs:
ecs-anywhere-vpc.ExportsOutputRefmydcvpnvpcC99028A5E0C4BB86 = vpc-0e77901855dd34b64
ecs-anywhere-vpc.ExportsOutputRefmydcvpnvpcprivateSubnet1Subnet907447937118DADD = subnet-00d3e62f014db2eb2
ecs-anywhere-vpc.ExportsOutputRefmydcvpnvpcprivateSubnet2Subnet2F863D5508703380 = subnet-084ab056d6ab1ae1a
ecs-anywhere-vpc.ExportsOutputRefmydcvpnvpcpublicSubnet1Subnet93B9609B6FB2CB58 = subnet-0eb59347bf659e8d0
ecs-anywhere-vpc.ExportsOutputRefmydcvpnvpcpublicSubnet2Subnet252458EC3B8C529C = subnet-038234a8a8bffb90a
ecs-anywhere-vpc.VPCId = vpc-0e77901855dd34b64
ecs-anywhere-vpc.VPGId = vgw-0bc980838a7a37eda

Stack ARN:
arn:aws:cloudformation:eu-central-1:704533066374:stack/ecs-anywhere-vpc/a5f78e70-e28e-11eb-90fc-026316002c84
Enter fullscreen mode Exit fullscreen mode

部署 Amazon ECS 集群

[9] 下一步,我们将把应用程序部署到 Amazon ECS 集群中。这将创建两个 EC2 实例,一个使用 x86 架构,另一个使用 AWS Graviton2 实例类型。它还会设置应用程序负载均衡器并创建一个服务。完成后,我们就可以通过输出中显示的 DNS 名称访问该应用程序了。

cdk deploy ecs-anywhere-cicd
Enter fullscreen mode Exit fullscreen mode

您将看到一个审核屏幕,其中显示了此堆栈将部署的安全更改。回答“Y”以部署。稍后,如果部署成功,您将看到以下内容:

 ✅  ecs-anywhere-cicd

Outputs:
ecs-anywhere-cicd.LoadBalancerEndpoint = ecs-a-LB8A1-UQ0ZD3461WUB-1638849351.eu-central-1.elb.amazonaws.com

Stack ARN:
arn:aws:cloudformation:eu-central-1:704533066374:stack/ecs-anywhere-cicd/7004c1a0-e290-11eb-9334-0288a08f1e2c
Enter fullscreen mode Exit fullscreen mode

[10] 现在您可以通过浏览器访问该应用程序,使用输出的端点。在我的示例中,当我在浏览器中输入“ http://ecs-a-LB8A1-UQ0ZD3461WUB-1638849351.eu-central-1.elb.amazonaws.com ”并刷新几次后,我看到以下内容:

演示应用程序

如果我刷新页面几次,你应该会看到页面发生变化,因为你的请求是由运行在不同处理器架构上的不同容器实例来处理的。

演示

我们现在已经部署了应用程序,下一步是设置和部署 CI/CD,以便我们可以进行更改并自动化以下过程:1/构建应用程序的更新容器镜像,以及 2/将其部署到此集群。

部署 CI/CD 系统

[11] 我们可以通过以下命令从命令行部署 CI/CD 设置:

cdk deploy ecs-anywhere-pipeline
Enter fullscreen mode Exit fullscreen mode

您将看到一个审核屏幕,其中显示了此堆栈将部署的安全更改。回答“Y”以部署。稍后,如果部署成功,您将看到以下内容:

 ✅  ecs-anywhere-pipe

Outputs:
ecs-anywhere-pipe.CodeCommitOutput = https://git-codecommit.eu-central-1.amazonaws.com/v1/repos/demo-springboot-repo

Stack ARN:
arn:aws:cloudformation:eu-central-1:704533066374:stack/ecs-anywhere-pipe/30f90840-e29a-11eb-b20f-0ae3cbe3a3b6
Enter fullscreen mode Exit fullscreen mode

这样就完成了,现在您拥有了一个 CI/CD 设置,每次您更改代码时,它都会构建应用程序的 x86 和 arm 容器镜像。

CDK 应用程序会构建 CodePipeline 和 CodeBuild 步骤,确保定义了正确的权限,并将其与正确的源代码仓库集成以触发管道。但是,它不会配置这些步骤的具体功能,因此我们需要一些额外的 yml 文件来配置实际的构建步骤。

在源代码仓库中,您会找到一个名为“pipelines/ecs-pipeline”的目录,其中包含三个文件:armbuild.yml、amdbuild.yml 和 post_build.yml。这些文件用于创建自动化流程将执行的具体操作。它们使用环境变量,包括我们在代码中设置的一些变量,用于存储诸如 ECS 集群和服务名称、Docker Hub 用户名和密码以及容器仓库标签 ID 等值。

这是 post_build.yml 文件,它获取 x86 和 arm 构建的容器,创建容器清单文件,给它们贴上标签,将它们上传到 Amazon ECR 存储库,最后启动部署到我们的 ECS 集群。

version: 0.2

phases:
  pre_build:
    commands:
      - echo Logging in to Amazon ECR...
      - aws --version
      - $(aws ecr get-login --region $AWS_DEFAULT_REGION --no-include-email)
      - COMMIT_HASH=$(echo $CODEBUILD_RESOLVED_SOURCE_VERSION | cut -c 1-7)
      - IMAGE_TAG=${COMMIT_HASH:=latest}
      - ARM_TAG=${IMAGE_TAG}-arm64
      - AMD_TAG=${IMAGE_TAG}-amd64
      - echo $REPOSITORY_URI
      - echo $IMAGE_TAG
      - echo $AMD_TAG
      - echo $ARM_TAG
      - export DOCKER_CLI_EXPERIMENTAL=enabled
  build:
    commands:
      - echo Build started on `date`
      - echo Building the Docker manifest...
      - docker manifest create $REPOSITORY_URI:$IMAGE_TAG $REPOSITORY_URI:$ARM_TAG $REPOSITORY_URI:$AMD_TAG
      - docker manifest annotate --arch arm64 $REPOSITORY_URI:$IMAGE_TAG $REPOSITORY_URI:$ARM_TAG
      - docker manifest annotate --arch amd64 $REPOSITORY_URI:$IMAGE_TAG $REPOSITORY_URI:$AMD_TAG
      - docker manifest inspect $REPOSITORY_URI:$IMAGE_TAG
      - docker manifest push $REPOSITORY_URI:$IMAGE_TAG
  post_build:
    commands:
       - cd springbootdemo
       - sed -i "s|{ecr_image}|${REPOSITORY_URI}:${IMAGE_TAG}|g" tf.json
       - sed -i "s|{region}|$AWS_DEFAULT_REGION|g" tf.json
       - sed -i "s|{log_group}|$ECS_SN|g" tf.json
       - aws ecs register-task-definition --cli-input-json file://tf.json
       - TASK_DEFINITON="springboot-cicd"
       - TASK_REVISION=`aws ecs describe-task-definition --task-definition $TASK_DEFINITON | egrep "revision" | tr "/" " " | awk '{print $2}' | sed 's/,$//'`
       - aws ecs update-service --cluster $ECS_CLUSTER --service $ECS_SERVICE --task-definition $TASK_DEFINITON:${TASK_REVISION} 
       - aws ssm put-parameter --name "/demo/ecsanywhere/latestimage" --value ${IMAGE_TAG} --type "String" --overwrite

Enter fullscreen mode Exit fullscreen mode

让我们回顾一下目前为止我们所做的事情。

回顾我们刚才所做的事情

我们使用 AWS CDK 以及一些脚本来自动化将我们的示例应用程序部署和配置到 Amazon ECS 集群中,并设置了一个简单的 CI/CD 管道。

如果您前往 AWS 控制台并查看 CodePipeline 下的 Pipelines,您应该会看到现在有一​​个名为“ECSAnyWhere”(或者如果您修改了代码,则为您重命名的任何名称)的管道,其状态为成功。

管道

您可以查看构建日志,了解每个阶段的输出,从而更详细地了解正在发生的事情。

当我们对应用程序进行更改时,它会触发 CI/CD 系统,该系统会在 x86 和 arm 平台上构建应用程序,然后将其部署到 x86 和 Graviton2 实例类型上。请在本地检出应用程序仓库并进行一些更改。您可以修改的代码是 HelloController.java(它位于 springboot/src/main/java/hello 目录中),我建议您尝试更改版本号或显示的文本。

提交代码后,返回 CodePipeline 界面,您应该会看到流水线正在启动。代码更新并部署到 ECS 集群大约需要 5-10 分钟。完成后,您可以通过浏览器检查应用程序,查看更改是否成功,也可以查看日志(见下文)。

您可以通过 CloudFormation 控制台查看输出结果。此时会生成一个新的 CloudWatch 日志组,其中应该包含每个 ECS 实例的日志流(如果您遵循所有默认设置,则会有两个日志流)。打开这些日志流后,您应该会看到如下输出:

日志

接下来,我们将与我们的“本地”环境进行集成,就我而言,就是我的家庭网络。

第二部分:设置本地环境并与之集成

现在我们已经有了应用程序,下一步是将其与我们自己的环境集成。我将执行以下操作:

  1. 设置本地环境——配置站点到站点 VPN 以及所有其他所需的软件组件。
  2. 部署 AWS 资源——我们将设置一个 Amazon ECS 集群,该集群将使用我们刚刚自动化的应用程序。
  3. 集成本地环境和 AWS 云环境
  4. 运行我们的工作负载,并在云端和本地进行测试。

我在准备这篇文章的过程中阅读了一些很棒的博客文章,强烈推荐大家看看。首先是 Romain Jourdan 的《AWS VPC 与 Raspberry Pi 之间的安全连接》,但还有其他几篇也值得一提。比如 Nathan Peck 的这篇《使用 Amazon VPC 网络连接构建 Amazon ECS Anywhere 家庭实验室》,以及这篇《看,我可以在 Amazon ECS Anywhere 上运行容器——本地部署或云端部署!》。最后,这篇很棒的文章《将 Raspberry Pi 3 设置为 AWS VPN 客户网关》在我遇到 VPN 问题时帮我解决了难题。

如果您遇到困难或想更深入地了解此设置,请务必查看它们。

建筑学

我打算使用一台树莓派作为VPN网关,并将其设置在我的本地网络上。我将使用另一台树莓派来运行应用程序工作负载。

如果您使用的是其他 VPN 网关(例如,如果您使用的是 Unify 类型的解决方案,请查看上面这篇 Romain Jourdan 的博客文章)。

我在家里的路由器上唯一做的就是创建了一个防火墙DMZ,它将互联网的访问权限开放给单个主机(在我的例子中,是一台树莓派,我将在上面运行我的VPN网关)。本文不赘述其具体配置,但考虑到它是“暴露”的,所以这是一个强化/安全的配置。

更新本地树莓派

我首先要做的是将树莓派的固件和内核更新到最新版本。这超出了本文的讨论范围,但如果您在 VPN 设置过程中遇到缺少库的错误,很可能是库文件缺失导致的,因此请务必在继续操作之前更新所有组件。您应该在开始之前就完成此步骤。

我使用了两种不同的树莓派——VPN 网关使用的是较旧的树莓派 3 Rev B 型号(运行 Raspbian 系统),而工作负载使用的是较新的树莓派 4 型号(运行 Ubuntu 20.04.02 LTS 系统)。

这是我更新运行 VPN 网关的树莓派后的输出结果。

uname-a output
Linux dmcpi01 5.10.44-v7+ #1428 SMP Thu Jun 24 17:22:49 BST 2021 armv7l GNU/Linux
Enter fullscreen mode Exit fullscreen mode

这是我更新运行工作负载的树莓派后的输出结果。

uname -a
Linux ubuntu 5.4.0-1038-raspi #41-Ubuntu SMP PREEMPT Thu Jun 17 14:14:11 UTC 2021 aarch64 aarch64 aarch64 GNU/Linux
Enter fullscreen mode Exit fullscreen mode

在运行工作负载的树莓派上,我需要修改 /boot/firmware/cmdline.txt 文件,添加以下内容:cgroup_enable=memory

我的就是这样的

net.ifnames=0 dwc_otg.lpm_enable=0 console=serial0,115200 console=tty1 root=LABEL=writable rootfstype=ext4 elevator=deadline rootwait fixrtc cgroup_enable=memory
Enter fullscreen mode Exit fullscreen mode

更改完成后,需要重启系统。

设置用于运行本地 VPN 网关的本地 Raspberry Pi。

[1] 我需要安装一些额外的软件包和依赖项。在将作为 VPN 网关的 Raspberry Pi 上,我运行了以下命令

sudo apt-get update
sudo apt-get install strongswan raspberrypi-kernel-headers lsof
Enter fullscreen mode Exit fullscreen mode

我们可以通过运行以下命令来检查是否成功

sudo ipsec status
Enter fullscreen mode Exit fullscreen mode

这应该不会返回任何错误。现在可以连接到我们的 AWS VPN 了,我们接下来会创建 VPN,所以暂时先到这里,稍后再回来。

设置站点到站点 VPN

[2] 运行“cdk deploy ecs-anywhere-vpc”命令后,AWS 端的站点到站点 VPN 的所有配置都已完成。现在我们只需要配置本地 VPN 网关(Strongswan)来建立 IPsec 隧道。

在 AWS 控制台中,如果您转到 VPC,则可以看到站点到站点连接,它应该看起来像这样。

VPN关闭

这些信息已在 app.py 文件的参数部分提供。这里需要重点关注的是 CIDR 地址范围(必须与您的本地网络匹配——我的本地网络是 192.168.1.0/24)以及外部 IP 地址,AWS 端的 VPN 隧道将尝试连接到该地址。如果您不确定外部 IP 地址是什么,可以通过搜索“我的 IP 地址”来查找——但请确保您未连接到 VPN,否则您将获得不同的结果。

您需要点击“下载配置”(上方高亮显示/箭头所示),然后从下拉菜单中选择 STRONGSWAN。平台/软件选项只有一个,因此请点击“下载”按钮,即可下载一个文本文件。

强的

[3] 现在您需要按照树莓派本地网关上的该文本文件中提供的说明进行操作。

应该很简单,但可能需要 10-15 分钟才能完成。以下是我采取的步骤,供您参考:

  • 根据文档中说明的各种选项修改了 /etc/sysctl.conf 文件(然后通过 sudo sysctl -p 重新加载该文件)。
  • 创建了ipsec.conf文件,并添加了两个ipsec隧道的详细信息。
  • 创建了 ipsec.secrets 文件并添加了共享密钥。
  • 运行以下命令创建了隧道接口:
sudo ip link add Tunnel1 type vti local 192.168.1.6 remote 35.156.20.208 key 100
sudo ip addr add 169.254.100.230/30 remote 169.254.100.229/30 dev Tunnel1
sudo ip link set Tunnel1 up mtu 1419
sudo ip route add 10.0.0.0/16 dev Tunnel1 metric 100
sudo iptables -t mangle -A FORWARD -o Tunnel1 -p tcp --tcp-flags SYN,RST SYN -j TCPMSS --clamp-mss-to-pmtu
sudo iptables -t mangle -A INPUT -p esp -s 35.156.20.208 -d 79.67.110.164 -j MARK --set-xmark 100

sudo ip link add Tunnel2 type vti local 192.168.1.6 remote 52.58.162.102 key 200
sudo ip addr add 169.254.254.234/30 remote 169.254.254.233/30 dev Tunnel2
sudo ip link set Tunnel2 up mtu 1419
sudo ip route add 10.0.0.0/16 dev Tunnel2 metric 200
sudo iptables -t mangle -A FORWARD -o Tunnel2 -p tcp --tcp-flags SYN,RST SYN -j TCPMSS --clamp-mss-to-pmtu
sudo iptables -t mangle -A INPUT -p esp -s 52.58.162.102 -d 79.67.110.164 -j MARK --set-xmark 200
Enter fullscreen mode Exit fullscreen mode

如果此操作已成功完成,运行 ifconfig-a 命令后,您应该会看到两个新的接口:

Tunnel1   Link encap:IPIP Tunnel  HWaddr
          inet addr:169.254.100.230  P-t-P:169.254.100.229  Mask:255.255.255.252
          inet6 addr: fe80::5efe:c0a8:106/64 Scope:Link
          UP POINTOPOINT RUNNING NOARP  MTU:1419  Metric:1
          RX packets:0 errors:0 dropped:0 overruns:0 frame:0
          TX packets:0 errors:8 dropped:0 overruns:0 carrier:8
          collisions:0 txqueuelen:1000
          RX bytes:0 (0.0 B)  TX bytes:0 (0.0 B)

Tunnel2   Link encap:IPIP Tunnel  HWaddr
          inet addr:169.254.254.234  P-t-P:169.254.254.233  Mask:255.255.255.252
          inet6 addr: fe80::5efe:c0a8:106/64 Scope:Link
          UP POINTOPOINT RUNNING NOARP  MTU:1419  Metric:1
          RX packets:0 errors:0 dropped:0 overruns:0 frame:0
          TX packets:0 errors:3 dropped:0 overruns:0 carrier:3
          collisions:0 txqueuelen:1000
          RX bytes:0 (0.0 B)  TX bytes:0 (0.0 B)
Enter fullscreen mode Exit fullscreen mode

如果我们检查路由表,应该可以看到现在通过这些接口已经有了到达 AWS 上 VPC 的路径。

netstat -rn

Kernel IP routing table
Destination     Gateway         Genmask         Flags   MSS Window  irtt Iface
0.0.0.0         192.168.1.1     0.0.0.0         UG        0 0          0 wlan0
10.0.0.0        0.0.0.0         255.255.0.0     U         0 0          0 Tunnel1
10.0.0.0        0.0.0.0         255.255.0.0     U         0 0          0 Tunnel2
169.254.100.228 0.0.0.0         255.255.255.252 U         0 0          0 Tunnel1
169.254.254.232 0.0.0.0         255.255.255.252 U         0 0          0 Tunnel2
192.168.1.0     0.0.0.0         255.255.255.0   U         0 0          0 wlan0
Enter fullscreen mode Exit fullscreen mode

注意!此信息不会永久保存。如果您重启树莓派,则需要重复这些步骤。请阅读文本文件,了解如何保存配置的详细信息。

[4] 现在我们可以通过运行以下命令来启动本地 Strongswan VPN:

sudo ipsec start
Enter fullscreen mode Exit fullscreen mode

我们可以通过运行以下命令来检查:

sudo ipsec status
Enter fullscreen mode Exit fullscreen mode

现在我们应该看到 VPN 正在启动并与 AWS VPN 网关建立连接。

Security Associations (2 up, 0 connecting):
     Tunnel2[2]: ESTABLISHED 3 seconds ago, 192.168.1.6[80.42.49.11]...18.184.143.185[18.184.143.185]
     Tunnel2{2}:  INSTALLED, TUNNEL, ESP in UDP SPIs: c6777c18_i c9f1bf73_o
     Tunnel2{2}:   0.0.0.0/0 === 0.0.0.0/0
     Tunnel1[1]: ESTABLISHED 3 seconds ago, 192.168.1.6[80.42.49.11]...3.65.61.44[3.65.61.44]
     Tunnel1{1}:  INSTALLED, TUNNEL, ESP in UDP SPIs: c57f2982_i cbfd1699_o
     Tunnel1{1}:   0.0.0.0/0 === 0.0.0.0/0
Enter fullscreen mode Exit fullscreen mode

注意!如果您看到以下内容,则很可能是您输入的 ipsec.conf/ipsec.security 详细信息有误,请检查这些信息。

Security Associations (2 up, 0 connecting):
     Tunnel2[2]: CONNECTING, 192.168.1.6[%any]...52.58.162.102[%any]
     Tunnel1[1]: CONNECTING, 192.168.1.6[%any]...35.156.20.208[%any]

我们可以通过访问 AWS 控制台并再次检查隧道状态来验证一切是否正常运行。您应该看到 VPN 隧道现在已建立。

VPN启动

通往 AWS 的路线

最后还有一件事需要做。运行示例应用程序的树莓派目前还不知道如何通过 VPN 将流量路由到 AWS。我们需要添加一条静态路由:

sudo ip route add 10.0.0.0/16 via 192.168.1.6
Enter fullscreen mode Exit fullscreen mode

这将把所有发往 AWS 的流量通过我们刚刚创建的 Raspberry Pi VPN 网关 (192.168.1.6) 进行路由。

注意!这是非常重要的一步。如果工作负载机器没有返回路由,那么在本文后续部分创建指向该实例的负载均衡器时,您会遇到错误,因为树莓派不知道如何通过 VPN 路由流量。

现在我们已经完成了网络部分,可以开始设置本地工作负载了。

注意!在我编写这段代码时,我的宽带断了。恢复后,我的外部 IP 地址发生了变化。我不得不更新 app.py 文件中的 IP 地址,然后重新部署 ecs-anywhere-vpc 堆栈以更新所有内容。

配置和部署 AWS 资源

现在,我们的 AWS 环境和本地环境之间已经建立了安全连接。稍后我们会详细介绍这一点。接下来,我们将配置本地的 Raspberry Pi 工作负载机器,使其成为运行容器化应用程序并与 Amazon ECS 控制平面集成的主机。

这将涉及创建一个新的 ECS 集群,我们可以在其中注册这些“外部”资源,然后在这些本地资源(在我的例子中是 Raspberry Pi 工作负载机器)上设置软件,以便它可以由 Amazon ECS 管理。

注意!AWS 与本地环境之间需要建立连接,因为本地实例本质上是由 Amazon ECS 控制平面管理的。虽然它可以容忍一定的停机时间,但几个小时后,您的本地环境将会出现问题。因此,此方案(至少目前)不适用于断开连接或连接频率较低的环境。

[4] 现在我们需要回到我们在第一部分中运行 CDK 应用程序的开发机器上,并运行以下命令来部署一个新的 Amazon ECS 集群,我们将使用该集群来注册外部或混合 ECS Anywhere 实例。

cdk deploy ecs-anywhere-cfn
Enter fullscreen mode Exit fullscreen mode

系统会提示您查看安全设置,请回复“Y”,如果成功,您应该会看到类似以下的输出:


 ✅  ecs-anywhere-cfn

Outputs:
ecs-anywhere-cfn.ECSAnyWhereIamRole = ecs-anywhere-cfn-mydcecsroleE385F8F3-AI911P55T3JP
ecs-anywhere-cfn.ECSClusterName = mydc-ecs-extcluster

Stack ARN:
arn:aws:cloudformation:eu-central-1:704533066374:stack/ecs-anywhere-cfn/24fb74e0-e2fb-11eb-ae06-06c47aa6fac8
Enter fullscreen mode Exit fullscreen mode

我们稍后会用到 ecs-anywhere-cfn.ECSAnyWhereIamRole 的值。在我的示例中,它是 ecs-anywhere-cfn-mydcecsroleE385F8F3-AI911P55T3JP。

如果您访问 AWS 控制台,您现在将看到一个新的 ECS 集群出现(该集群的名称应与 ecs-anywhere-cfn.ECSClusterName 的输出名称匹配,因此在上面的示例中,该名称为 mydc-ecs-extcluster)。

设置本地树莓派,用于运行我们的 ECS Anywhere 工作负载

[5] 重新登录到您计划运行工作负载的本地 Raspberry Pi。您需要安装 AWS CLI,然后使用一组 AWS 凭证进行配置。我为此创建了一个新的 IAM 用户,但您可以自行决定是否创建。

sudo apt-get install awscli
aws configure
Enter fullscreen mode Exit fullscreen mode

[6] 现在我们需要将代理软件安装到这台 Raspberry Pi(工作负载)上。不过在此之前,我们需要获取一个激活码,我们将使用上面创建的 IAM 角色(CDK 应用程序的输出 - ecs-anywhere-ec2.ECSAnyWhereIamRole,在我的示例中为 ecs-anywhere-cfn-mydcecsroleE385F8F3-AI911P55T3JP)。

aws ssm create-activation --iam-role ecs-anywhere-cfn-mydcecsroleE385F8F3-AI911P55T3JP --region={region} | tee ssm-activation.json
Enter fullscreen mode Exit fullscreen mode

由此生成了以下输出。

{
    "ActivationId": "0c8840d6-cd71-482c-9ea5-3c8380cc325a",
    "ActivationCode": "v4PEBr7nyIQ6u55OaWs!"
}
Enter fullscreen mode Exit fullscreen mode

注意!请勿分享或以电子方式存储这些信息。我已经更改了这些数值,它们将无法使用。

稍后会用到这些值,所以我们先创建环境变量。

export id="0c8840d6-cd71-482c-9ea5-3c8380cc325a"
export code="v4PEBr7nyIQ6u55OaWs!"
Enter fullscreen mode Exit fullscreen mode

一旦你获得了这些值,时间就开始倒计时了。你只有很短的时间来运行脚本(具体时间取决于你如何配置 AWS 环境账户的超时时间)。要确定这是否是导致任何问题的原因,你会在日志中看到类似这样的错误信息。

> level=error time=2021-06-30T16:22:12Z msg="Unable to register as a container instance with ECS: ExpiredTokenException: The security token included in the request is expired\n\tstatus code: 400, request id: f21461d9-ba49-4097-a66c-a902747428c4" module=client.go
level=error time=2021-06-30T16:22:12Z msg="Error registering: ExpiredTokenException: The security token included in the request is expired\n\tstatus code: 400, request id: f21461d9-ba49-4097-a66c-a902747428c4" module=agent.go
> 
Enter fullscreen mode Exit fullscreen mode

[7] 运行以下命令,该命令将下载 ECS Anywhere 安装脚本,该脚本会执行三项操作:安装 ECS Agent、SSM Agent 和 Docker 引擎。

curl --proto "https" -o ~/ecs-anywhere-install.sh "https://amazon-ecs-agent.s3.amazonaws.com/ecs-anywhere-install-latest.sh"
Enter fullscreen mode Exit fullscreen mode

当我们运行 CDK 脚本来配置 ECS 集群时,它输出了 ECS 集群的名称,因此让我们将其分配给一个环境变量,因为下一步需要使用它。

export ecscluster="mydc-ecs-extcluster"
Enter fullscreen mode Exit fullscreen mode

[8] 现在我们可以运行安装脚本来安装和配置所需的软件组件。

sudo bash ~/ecs-anywhere-install.sh --region {your AWS region} --cluster $ecscluster --activation-id $id --activation-code $code
Enter fullscreen mode Exit fullscreen mode

这需要几分钟时间,因为它需要在树莓派上下载、安装和配置 SSM 和 ECS 代理。

如果看到以下错误,请确保您已正确设置 AWS 区域信息:

获取 Seelog 配置文件路径时出错:打开 /etc/amazon/ssm/seelog.xml:没有该文件或目录。
正在初始化新的 Seelog 日志记录器。
新的 Seelog 日志记录器创建完成。2021-07-12
12:46:23 警告:无法读取实例指纹文件:实例指纹不存在。2021-07-12
12:46:23 信息:未检测到初始指纹,正在生成指纹文件……
2021-07-12 12:46:25 错误:由于向 AWS SSM 注册实例时出错,注册失败。InvalidActivation:

您应该会得到类似以下的输出结果

...
...
Ping ECS Agent registered successfully! Container instance arn: "arn:aws:ecs:eu-west-1:704533066374:container-instance/ecs-anywhere-ec2-mydcecsclusterBB109425-r7l2mKClssuV/8dfb8700d9a1460dad403a321db6b5b9"

You can check your ECS cluster here https://console.aws.amazon.com/ecs/home?region=eu-west-1#/clusters/ecs-anywhere-ec2-mydcecsclusterBB109425-r7l2mKClssuV

# ok
##########################


##########################
This script installed three open source packages that all use Apache License 2.0.
You can view their license information here:
  - ECS Agent https://github.com/aws/amazon-ecs-agent/blob/master/LICENSE
  - SSM Agent https://github.com/aws/amazon-ssm-agent/blob/master/LICENSE
  - Docker engine https://github.com/moby/moby/blob/master/LICENSE
##########################
Enter fullscreen mode Exit fullscreen mode

刚才发生了什么?

在您的本地系统(例如我的 Raspberry Pi)上,脚本 (ecs-anywhere-install.sh) 会安装并配置 AWS SSM 代理和 ECS Anywhere 代理。该脚本还会安装运行容器应用程序所需的其他软件,例如 Docker。

如果我们进入 AWS 系统管理器 > 车队管理,现在应该可以看到我们的 Raspberry Pi 出现了。

ssm-舰队

ECS Agent 也已作为资源添加到我们创建的 ECS 集群中。如果我们打开 AWS 控制台并查看 ECS 集群,就会看到这个新资源已列出。

资源1

我们可以点击它来查看有关此实例提供的可用资源(内存/CPU/处理器等)的更多详细信息。

资源2

现在我们可以将应用程序部署到这个 ECS 集群,它将被部署到我们本地的 Raspberry Pi 上,然后可以通过本地网络访问(在我的例子中,这是http://192.168.1.99)。

将应用程序部署到本地 ECS Anywhere 集群

[9] 我们将通过命令行界面 (cli) 执行此操作,然后再修改 CI/CD 系统,以便在每次更新时自动执行此操作。

注意!首次启动任务时,树莓派需要下载容器镜像,因此应用程序启动时间会比较长(具体时间取决于您的网络连接速度)。如果您发现任务状态为“待处理”,则可能是此原因造成的。

首先,我们需要准备 task_definion.json 文件,我们将使用它来创建新版本的任务定义,该任务定义是在 AWS CDK ecs-anywhere-cfn 部署期间创建的。我们创建一个名为 tf-any.json 的文件,如下所示。

{
    "requiresCompatibilities": [
        "EXTERNAL"
    ],
    "containerDefinitions": [{
        "name": "springboot-remote",
        "image": "{ecr_image}",
        "memory": 256,
        "cpu": 256,
        "essential": true,
        "portMappings": [{
            "containerPort": 8080,
            "hostPort": 8080,
            "protocol": "tcp"
        }]
    }],
    "networkMode": "bridge",
    "family": "ecsanywhere"
}

Enter fullscreen mode Exit fullscreen mode

我们可以通过查看 ECR 存储库来了解最新的容器镜像。(当我们通过 CI/CD 实现自动化时,这些信息会作为环境变量传递,因此我们现在只需要在首次启动应用程序时执行此操作。)

export APP="704533066374.dkr.ecr.eu-central-1.amazonaws.com/demo-springboot-ecsanywhere:56beb49"
sed -i "s|{ecr_image}|$APP|g" tf-any.json
Enter fullscreen mode Exit fullscreen mode

注意!如果您使用的是 Mac,则需要使用“sed -i '.bak' "s|{ecr_image}|$APP|g" tf-any.json"

这将使用容器引用更新文件。现在我们可以注册一个新的任务定义。我将使用 post_build.yml 脚本中已有的内容。

LOCAL_ECS_CLUSTER="mydc-ecs-extcluster"
LOCAL_ECS_SERVICE="mydc-ecs-svc"
LOCAL_TASK_DEFINITON="ecsanywhere"
aws ecs register-task-definition --cli-input-json file://tf-any.json --region={the region where the ECS cluster is running}
Enter fullscreen mode Exit fullscreen mode

这将生成类似于以下内容的输出:

{
    "taskDefinition": {
        "taskDefinitionArn": "arn:aws:ecs:eu-central-1:704533066374:task-definition/ecsanywhere:5",
        "containerDefinitions": [
            {
                "name": "springboot-remote",
                "image": "704533066374.dkr.ecr.eu-central-1.amazonaws.com/demo-springboot-ecsanywhere:56beb49",
                "cpu": 256,
                "memory": 256,
                "portMappings": [
                    {
                        "containerPort": 8080,
                        "hostPort": 8080,
                        "protocol": "tcp"
                    }
                ],
                "essential": true,
                "environment": [],
                "mountPoints": [],
                "volumesFrom": []
            }
        ],
        "family": "ecsanywhere",
        "networkMode": "bridge",
        "revision": 5,
        "volumes": [],
        "status": "ACTIVE",
        "requiresAttributes": [
            {
                "name": "com.amazonaws.ecs.capability.ecr-auth"
            }
        ],
        "placementConstraints": [],
        "compatibilities": [
            "EXTERNAL",
            "EC2"
        ],
        "requiresCompatibilities": [
            "EXTERNAL"
        ]
    }
}
Enter fullscreen mode Exit fullscreen mode

现在我需要从输出结果中获取最新版本,然后用它来更新正在运行的服务。

LOCAL_TASK_REVISION=`aws ecs describe-task-definition --task-definition $LOCAL_TASK_DEFINITON --region={the region where the ECS cluster is running} | egrep "revision" | tr "/" " " | awk '{print $2}' | sed 's/,$//'`
Enter fullscreen mode Exit fullscreen mode

如果我们使用“echo $LOCAL_TASK_REVISION”,您将看到新的版本号,我们将在更新正在运行的服务时使用该版本号。现在我们已经具备了更新服务所需的一切,我们可以使用以下命令来更新服务:

aws ecs update-service --cluster $LOCAL_ECS_CLUSTER --service $LOCAL_ECS_SERVICE --task-definition $LOCAL_TASK_DEFINITON:${LOCAL_TASK_REVISION} --region={the region where the ECS cluster is running}

Enter fullscreen mode Exit fullscreen mode

这将得到类似这样的结果:

{
    "service": {
        "serviceArn": "arn:aws:ecs:eu-central-1:704533066374:service/mydc-ecs-extcluster/mydc-ecs-svc",
        "serviceName": "mydc-ecs-svc",
        "clusterArn": "arn:aws:ecs:eu-central-1:704533066374:cluster/mydc-ecs-extcluster",
        "loadBalancers": [],
        "serviceRegistries": [],
        "status": "ACTIVE",
        "desiredCount": 1,
        "runningCount": 1,
        "pendingCount": 0,
        "launchType": "EXTERNAL",
        "taskDefinition": "arn:aws:ecs:eu-central-1:704533066374:task-definition/ecsanywhere:5",
        "deploymentConfiguration": {
            "deploymentCircuitBreaker": {
                "enable": false,
                "rollback": false
            },
            "maximumPercent": 200,
            "minimumHealthyPercent": 100
        },
        "deployments": [
            {
                "id": "ecs-svc/2775386206525924918",
                "status": "PRIMARY",
                "taskDefinition": "arn:aws:ecs:eu-central-1:704533066374:task-definition/ecsanywhere:5",
                "desiredCount": 1,
                "pendingCount": 0,
                "runningCount": 0,
                "failedTasks": 0,
                "createdAt": 1626102200.686,
                "updatedAt": 1626102200.686,
                "launchType": "EXTERNAL",
                "rolloutState": "IN_PROGRESS",
                "rolloutStateReason": "ECS deployment ecs-svc/2775386206525924918 in progress."
            },
            {
                "id": "ecs-svc/7501839119011433935",
                "status": "ACTIVE",
                "taskDefinition": "arn:aws:ecs:eu-central-1:704533066374:task-definition/ecsanywhere:4",
                "desiredCount": 1,
                "pendingCount": 0,
                "runningCount": 0,
                "failedTasks": 2,
                "createdAt": 1626101923.328,
                "updatedAt": 1626101923.328,
                "launchType": "EXTERNAL",
                "rolloutState": "IN_PROGRESS",
                "rolloutStateReason": "ECS deployment ecs-svc/7501839119011433935 in progress."
            },
            {
                "id": "ecs-svc/5388566575203796106",
                "status": "ACTIVE",
                "taskDefinition": "arn:aws:ecs:eu-central-1:704533066374:task-definition/ecsanywhere:3",
                "desiredCount": 1,
                "pendingCount": 0,
                "runningCount": 1,
                "failedTasks": 0,
                "createdAt": 1626101139.148,
                "updatedAt": 1626101938.625,
                "launchType": "EXTERNAL",
                "rolloutState": "COMPLETED",
                "rolloutStateReason": "ECS deployment ecs-svc/5388566575203796106 completed."
            }
        ],
...
...
Enter fullscreen mode Exit fullscreen mode

本地访问应用程序

[10] 现在,您可以通过网络浏览器在 8080 端口访问本地网络上的应用程序。当我在我的网络上访问它时,我得到了以下结果。

本地应用

既然我们知道它有效,我们就可以自动化这些步骤并将其纳入 CI/CD 系统中。

更新 CI/CD 以进行本地部署

为此,我们只需更新 CI/CD 工作流中的 post_build 步骤。容器已经构建/打包完成,因此我们只需添加以下步骤:

#       - sed -i "s|{ecr_image}|${REPOSITORY_URI}:${IMAGE_TAG}|g" tf-any.json
#       - LOCAL_ECS_CLUSTER="mydc-ecs-extcluster"
#       - LOCAL_ECS_SERVICE="mydc-ecs-svc"
#       - aws ecs register-task-definition --cli-input-json file://tf-any.json
#       - LOCAL_TASK_DEFINITON="ecsanywhere"
#       - LOCAL_TASK_REVISION=`aws ecs describe-task-definition --task-definition $LOCAL_TASK_DEFINITON | egrep "revision" | tr "/" " " | awk '{print $2}' | sed 's/,$//'`
#       - aws ecs update-service --cluster $LOCAL_ECS_CLUSTER --service $LOCAL_ECS_SERVICE --task-definition $LOCAL_TASK_DEFINITON:${LOCAL_TASK_REVISION} 

Enter fullscreen mode Exit fullscreen mode

这些内容已存在于示例文件中,但被注释掉了。这是因为在设置远程集群之前,post_build 脚本会失败。现在我们可以启用这些内容(如果您更改了任何名称,请确保更新上面的值)。

为此,我们检出代码仓库,进行修改,然后提交更改。提交后,这将触发构建过程。我们将进行的更改如下:

  • 将新的 tf-any.json 文件上传到 springboot 文件夹——每次构建新的容器镜像时,该文件都会更新,然后用于注册新任务。
  • 更新 post_build.yml 文件,使其包含上述步骤,更新 tf-any.json 文件,然后触发部署。

修改代码中的以下行,增加数字,以便可以看到已部署新版本。

str = str.concat("<p style='text-align:center;font-family:Arial'>Version : 25 </p><br>\n");
Enter fullscreen mode Exit fullscreen mode

提交后,您应该会看到流水线触发构建过程。请务必查看 CodePipeline 中的日志,特别是构建后日志。您会看到我们添加到构建脚本中的新行,这些新行会运行更新并将部署到通过 ECS Anywhere 管理的本地 Raspberry Pi。

大约 5-10 分钟后,刷新浏览器查看本地和 AWS 负载均衡上的版本,它们应该都更新到同一版本。但是等等,它们并没有更新。AWS 上的 ECS 集群已经更新,但本地集群还没有。这是怎么回事?

本地集群与 AWS 集群

本地 ECS 集群尚未更新的原因是,没有其他实例可供部署应用程序。我们在 AWS 上有两个实例,每次部署一个,验证后移除旧实例。而我们的本地实例只有一个 Raspberry Pi。

我们需要停止正在运行的任务。ECS Anywhere 代理会检测到该任务未运行,然后启动一个新实例,并选择我们刚刚创建的最新版本。为此,请在 AWS 控制台中,转到 Amazon ECS 集群视图,选择本地集群(我的集群名为 mydc-ecs-extcluster),然后单击“任务”选项卡。选中正在运行的任务旁边的复选框,然后选择“停止”,忽略警告。

如果您尝试通过浏览器访问该应用程序,将会收到错误提示。目前我们没有正在运行的容器来处理请求。大约 1-2 分钟后,ECS 服务会检测到没有正在运行的任务,并启动一个新的任务。任务启动并运行后,刷新页面,您应该会看到它运行的是相同的版本。

增加本地容量

我们可以通过向由 ECS Anywhere 管理的本地 ECS 集群添加更多本地实例来轻松解决此问题。

重复上述步骤,我添加了另一台本地计算机——这次是我的主 Ubuntu 桌面系统,它运行在一台老旧的 x86 机器上。可以看到添加成功,它现在显示在 ECS 实例选项卡下,是一个全新的 ECS 实例。

额外实例

现在启动新构建时,它会在空闲实例上配置任务,检查任务是否已启动并运行,然后再从旧应用程序中清除会话。

供应

如果需要,我们可以增加此集群支持的实例数量(当我们通过 CDK 应用程序设置它时,我们定义“期望计数”为 1)。如果我们回到那段代码,将数字更新为 2,然后重新部署。

        service = ecs.CfnService(
            self,
            f"{props['ecsclustername']}-svc",
            service_name=f"{props['ecsclustername']}-svc",
            cluster=f"{props['ecsclustername']}-extcluster",
            launch_type="EXTERNAL",
            desired_count=2,
            task_definition="ecsanywhere"
            #task.to_string()
        )
Enter fullscreen mode Exit fullscreen mode

现在我们可以看到,我们有这样一个集群,其中包含正在运行的任务,分布在由 ECS Anywhere 管理的两个不同的(本地)ECS 实例上。

更新

现在它们两个都运行起来了。

跑步
跑步2

我们快完成了,只剩最后一件事了。

通过 AWS 负载均衡器访问应用程序

如果现在能够通过云端的负载均衡器访问应用程序,那就太好了。为什么要这样做呢?或许我们想利用云端应用程序负载均衡器,而不是部署和管理自己的负载均衡器。这样一来,我们就可以部署更多本地机器,然后 ECS Anywhere 可以将这些应用程序部署到这些实例上。无需再手动停止任务。

我们可以通过添加一些 CDK 代码来实现自动化,并在 props 文件中添加一个新项,这样我们就不需要硬编码条目了。

        local_lb_security_group = ec2.SecurityGroup(
            self,
            "Load Balance internal Springboot http access",
            vpc=vpc
        )

        local_lb_security_group.add_ingress_rule(
            ec2.Peer.any_ipv4(),
            ec2.Port.tcp(80)
        )
        local_lb_security_group.add_egress_rule(
            ec2.Peer.ipv4(f"{props['mydcinternalcidr']}"),
            ec2.Port.tcp(8080)
        )

        lb = elbv2.ApplicationLoadBalancer(
            self,
            "LB",
            vpc=vpc,
            internet_facing=True,
            security_group=local_lb_security_group
        )

        listener = lb.add_listener(
            "Listener",
            port=80,
            open=True
        )

        remotepi = elbv2.IpTarget(
            f"{props['home-pi']}",
            port=8080,
            availability_zone="all")

        listener.add_targets(
            "Target",
            port=8080,
            targets=[remotepi]
        )
Enter fullscreen mode Exit fullscreen mode

首先,我们运行

cdk deploy ecs-anywhere-lb
Enter fullscreen mode Exit fullscreen mode

几分钟后,您应该会看到以下输出。

ecs-anywhere-lb
ecs-anywhere-lb: deploying...
ecs-anywhere-lb: creating CloudFormation changeset...


 ✅  ecs-anywhere-lb

Outputs:
ecs-anywhere-lb.PiRemoteLB = ecs-a-LB8A1-UMQHFMJZEX80-435054691.eu-central-1.elb.amazonaws.com

Stack ARN:
arn:aws:cloudformation:eu-central-1:704533066374:stack/ecs-anywhere-lb/017fbe20-e3ca-11eb-b32e-06f34beb5820

Enter fullscreen mode Exit fullscreen mode

你会看到它输出了已创建的负载均衡器的 DNS 名称,我们可以将其输入到浏览器中。当我们在浏览器中输入“ecs-a-LB8A1-UMQHFMJZEX80-435054691.eu-central-1.elb.amazonaws.com”时,就能再次看到我们漂亮的应用程序了。

本地应用

注意!我们目前只添加了一个本地节点,但我们可以添加两个,这将在下一节中看到。

改进

恭喜,您已阅读完本文及相关教程。一如既往,这仅仅是个开始,我希望您能花时间深入研究代码,并在此基础上进行扩展和改进,以满足您自身的使用需求。

我通过控制台成功完成了一件事,但还没弄明白如何通过 CDK 应用程序完成,那就是将本地树莓派集成到第一个 ECS 集群中。为此,我首先卸载了上一步:

cdk destroy ecs-anywhere-lb
Enter fullscreen mode Exit fullscreen mode

Once this finished, I then went to the AWS console, found the Target Group that had the existing EC2 instances registered (one running on x86 instance types, the other AWS Graviton2) and then registered manually my local Raspberry Pi (using the settings of the VPC created as part of this application, the IP address of the local Raspberry Pi 192.168.1.99, and the other local machine 192.168.1.110, the port they are listening to - 8080. I then added this to the cluster.

When that completed, it came back as unhealthy. Why?

This is to be expected, and shows the security groups working. The original ECS Cluster has a security group setup that controls access from the Application Load Balancer to the EC2 instances, on the target port (8080). That was for the subnet that those instances were on, not my local network (which is on a 192.168.1.0/24 CIDR).

I needed to add a new rule that would allow the Application Load Balancer to health check and route traffic, so I added a new rule (in my instance, updating sg-027005ab1cded3bce to enable TCP port 8080 access to the CIDR 192.168.1.0/24 range.

Once that was completed, I now had three healthy targets.

健康

And now when refreshing the original link, we can see we have all four different hosts servicing requests.

演示

Managing your local ECS Anywhere cluster

I thought it would be useful to share some of the things I found useful when administering the local ECS Anywhere instance.

Managing the agent

To shutdown, restart and get a status you can use the following commands

sudo systemctl stop ecs
sudo systemctl status ecs
sudo systemctl start ecs
Enter fullscreen mode Exit fullscreen mode

Example output

● ecs.service - Amazon Elastic Container Service - container agent
     Loaded: loaded (/lib/systemd/system/ecs.service; enabled; vendor preset: enabled)
     Active: active (running) since Mon 2021-06-28 12:37:53 UTC; 2 days ago
       Docs: https://aws.amazon.com/documentation/ecs/
   Main PID: 2214 (amazon-ecs-init)
      Tasks: 8 (limit: 4435)
     Memory: 12.0M
     CGroup: /system.slice/ecs.service
             └─2214 /usr/libexec/amazon-ecs-init start
Enter fullscreen mode Exit fullscreen mode

Log files

If you want to find the log files, then these are located in /var/log/ecs

  • ecs-agent.log
  • ecs-init.log

You can view the ECS agent initialisation logs as well as the operational logs.

Other key files

The name of the ECS Cluster can be found in /etc/ecs/ecs.config. You may need to update/remove this as you experiment.

If you are trying this and run into the following error:

level=critical time=2021-06-30T15:52:29Z msg="Data mismatch; saved cluster 'dc_pi_ecs_cluster' does not match configured cluster 'ecs-anywhere-ec2-mydcecsclusterBB109425-3HBnIv5IpvQm'. Perhaps you want to delete the configured checkpoint file?" module=agent.go
Enter fullscreen mode Exit fullscreen mode

When the agent starts (perhaps because this is a subsequent installation or you have changed values) then you may need shutdown your ECS agent, move/rename the following file /var/lib/ecs/data/agent.db and then restart to recover.

You may also need to move/rename the /var/lib/amazon/ssm if you get issues around the agent failing to install on subsequent attempts. If you are seeing errors like this in your hibernate.log

2021-06-30 18:30:37 ERROR Health ping failed with error - error occurred in RequestManagedInstanceRoleToken: AccessDeniedException: Authentication failed
    status code: 400, request id: afbe8882-27ed-49fa-92a9-1c3582b12f11
Enter fullscreen mode Exit fullscreen mode

The shut down your agent/uninstall/move or rename that directory and re-run the script.

Cleaning up

Removing the AWS resources

We can clean up what we have created by running the following commands:

cdk destroy ecs-anywhere-lb
cdk destroy ecs-anywhere-cfn
cdk destroy ecs-anywhere-pipe
cdk destroy ecs-anywhere-cicd
cdk destroy ecs-anywhere-vpc
Enter fullscreen mode Exit fullscreen mode

Once this has completed, you will then manually need to delete the following AWS resources:

  • AWS CodeCommit and ECR repositories as these will not be deleted
  • AWS System Manager Parameter Store values for the Docker Hub user name and password and the ECS Cluster details

Cleaning up your local environment

sudo systemctl stop amazon-ssm-agent
sudo systemctl stop ecs

Enter fullscreen mode Exit fullscreen mode

Remove the ECS Anywhere and SSM agents

sudo apt remove amazon-ecs-init
sudo apt remove amazon-ssm-agent
Enter fullscreen mode Exit fullscreen mode

Remove the Strongswan software from the Pi VPN Gateway

sudo apt remove strongswan
Enter fullscreen mode Exit fullscreen mode

你还应该删除安装软件包时可能不会删除的 /etc/ecs.config 文件,以及清理 /var/lib/ecs 文件夹。

现在您的环境应该已与 AWS Systems Manager 和 Amazon ECS 断开连接。

结论

在这篇文章中,我向您展示了一种可以自动创建容器化应用程序的多种架构和部署选项的方法,并与 ECS Anywhere 集成,从而为您提供更多部署这些工作负载的选择。

这些脚本还有很多可以改进的地方,但我希望它们能帮助您在自动化部署流程时节省一些时间。我希望在后续文章中继续完善这篇文章,添加/更新现有的 CDK 应用程序,以涵盖以下内容:

  • 增强应用程序的安全性,自动配置 SSL 证书和友好的 DNS 名称
  • 集成我们可能需要在应用程序中使用的其他应用程序或服务
  • 请问您想看到什么?

最后,如果您能就本次教程提供一些简短的反馈意见,我将不胜感激。请点击此处填写这份简短的调查问卷

如果您觉得这篇文章有用,或者想了解您打算如何使用这种配置,请告诉我。您是否正在考虑将工作负载迁移到 ARM 处理架构?您是否正在探索混合部署方案,即将单个应用程序部署到不同的环境中?

用心制作❤️

文章来源:https://dev.to/aws/creating-a-multi-architecture-ci-cd-deployment-for-amazon-ecs-and-ecs-anywhere-15o3