通过用户数据引导 Amazon EC2 实例以运行 Python Web 应用程序

使用借助于 Amazon CDK 创建的 CI/CD 管道,将 Python Web 应用程序部署到运行 Nginx 和 uWSGI 的 EC2 实例上。
发布时间:2023 年 4 月 21 日
EC2
CDK
CodePipeline
CodeDeploy
CodeBuild
Python
教程
亚马逊云科技

在服务器上使用 NginxuWSGI 手动设置和配置 Python Web 应用程序运行所需的软件包可能非常耗时,而且很难在没有任何错误的情况下完成。而且如果可以实现自动化,为什么还要那么辛苦呢?我们可以使用 Amazon CDK 来设置用户数据脚本和基础设施以预配置 EC2 实例,从而将耗时的手动过程变得轻而易举。在本教程中,我们会将 bash 脚本与 Amazon CodeDeploy 结合使用以安装和配置 Nginx 及 uWSGI、为 uWSGI 设置 systemd 服务并使用 CDK 复制我们的应用程序。然后,我们将从 GitHub 存储库部署基于 Python 的 Web 应用程序。我们将介绍如何:

  • 创建 AWS CDK 堆栈(包括 Amazon EC2 实例和 CI/CD 管道)以及使其正常运行所需的资源。
  • 在 EC2 实例首次启动时通过创建用户数据资产来安装软件包。
  • 使用 CI/CD 管道来测试、部署和配置 Web 应用程序。
Olawale Olaleye
亚马逊云科技使用经验
200 - 中级
完成所需时间
60 分钟
所需费用
前提条件
示例代码

本教程中使用的示例代码来自 GitHub

上次更新时间
2024 年 1 月 29 日

简介

为了部署此 Web 应用程序,我们将使用 Amazon CDK 创建和部署底层基础设施。此基础设施将由 EC2 实例、VPC、CI/CD 管道以及使其正常运行所需的附带资源(安全组和 IAM 权限)组成。

设置 CDK 项目

首先,我们检查 CDK 版本是否为最新版本 — 本指南使用 CDK v2。如果您仍在使用 v1,请通读迁移文档。若要检查版本,请运行以下命令:

cdk --version

# 2.122.0 (build 7e77e02)

如果您看到输出结果中显示 1.x.x,或者只想确保您使用的是最新版本,请运行以下命令:

npm install -g aws-cdk

现在,我们将使用所选语言 TypeScript 来创建框架 CDK 应用程序:

mkdir ec2-cdk
cd ec2-cdk
cdk init app --language typescript

# Output:

Applying project template app for typescript
# Welcome to your CDK TypeScript project

This is a blank project for CDK development with TypeScript.

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 w3c-hr-time@1.0.2: Use your platform's native performance.now() and performance.timeOrigin.
npm notice 
npm notice New patch version of npm available! 8.19.2 → 8.19.3
npm notice Changelog: https://github.com/npm/cli/releases/tag/v8.19.3
npm notice Run npm install -g npm@8.19.3 to update!
npm notice 
✅ All done!

为资源堆栈创建代码

CDK 会将文件夹名称用于它生成的文件。在本教程中,我们将使用 ec2-cdk。如果您以不同的方式命名目录,请将其替换为所用文件夹的名称。若要开始添加基础设施,请前往文件 lib/ec2-cdk-stack.ts。我们将在这里为您要创建的资源堆栈编写代码。

资源堆栈是一组云基础设施资源(在您的特定情况下,全都是亚马逊云科技资源),这些资源将预配到特定的账户。可以在堆栈中配置用来预配这些资源的账户和区域 — 我们将在稍后介绍。

在此资源堆栈中,您将创建以下资源:

  • IAM 角色此角色将分配给 EC2 实例,以允许 EC2 实例调用其他亚马逊云科技服务。
  • EC2 实例:EC2 实例:您将用于托管 Web 应用程序的虚拟机。
  • 安全组:允许对您的 Web 应用程序发出入站请求的虚拟防火墙。
  • 密钥管理器密钥:此处将用来存储 Github 令牌,该令牌将在密钥管理器验证管道身份时使用。
  • 创建 CI/CD 管道此管道将由 Amazon CodePipelineAmazon CodeBuildAmazon CodeDeploy 组成。

创建 EC2 实例

在这一部分中,我们将创建 EC2 实例及其所需的资源。学习本教程的过程中会有代码检查点,我们将在该检查点处展示完整文件的外观。我们建议您在按照分步说明操作时,键入或复制并粘贴示例代码块以确保您了解每个代码块的用途。

首先,我们将为您的 EC2 实例创建所需的 IAM 角色。此角色旨在为您的实例授予与 Amazon Systems Manager 和 Amazon CodeDeploy 进行交互所需的权限。这对于本教程中的后续操作非常重要。第一步是确保将以下模块导入主堆栈中。(lib/ec2-cdk-stack.ts):

import { readFileSync } from 'fs';
import { Vpc, SubnetType, Peer, Port, AmazonLinuxGeneration, 
 AmazonLinuxCpuType, Instance, SecurityGroup, AmazonLinuxImage,
 InstanceClass, InstanceSize, InstanceType
} from 'aws-cdk-lib/aws-ec2';
import { Role, ServicePrincipal, ManagedPolicy } from 'aws-cdk-lib/aws-iam';

然后添加以下各行来创建角色并附加所需的托管 IAM 策略:

const webServerRole = new Role(this, "ec2Role", {
 assumedBy: new ServicePrincipal("ec2.amazonaws.com"),
 });

 // IAM policy attachment to allow access to
 webServerRole.addManagedPolicy(
 ManagedPolicy.fromAwsManagedPolicyName("AmazonSSMManagedInstanceCore")
 );

 webServerRole.addManagedPolicy(
 ManagedPolicy.fromAwsManagedPolicyName("service-role/AmazonEC2RoleforAWSCodeDeploy")
 );

下一步是创建我们的 EC2 实例所在的 VPC。我们要创建的 VPC 仅包含三个公有子网,因此不会包含 NAT 网关或私有子网。

// This VPC has 3 public subnets, and that's it
 const vpc = new Vpc(this, 'main_vpc',{
 subnetConfiguration: [
 {
 cidrMask: 24,
 name: 'pub01',
 subnetType: SubnetType.PUBLIC,
 },
 {
 cidrMask: 24,
 name: 'pub02',
 subnetType: SubnetType.PUBLIC,
 },
 {
 cidrMask: 24,
 name: 'pub03',
 subnetType: SubnetType.PUBLIC,
 }
 ]
 });

我们还需要能通过 http(端口 80)访问我们的实例。为了使流量通过此端口,我们需要创建一个安全组来设置防火墙规则。我们将设置端口 80,以允许 HTTP 流量从互联网上的任何位置访问实例。

// Security Groups
 // This SG will only allow HTTP traffic to the Web server
 const webSg = new SecurityGroup(this, 'web_sg',{
 vpc,
 description: "Allows Inbound HTTP traffic to the web server.",
 allowAllOutbound: true,
 });

 webSg.addIngressRule(
 Peer.anyIpv4(),
 Port.tcp(80)
 );

现在,我们已准备好使用预构建的 Amazon Machine Image (AMI) — 在本教程中,我们将使用面向 X86_64 CPU 架构的 Amazon Linux 2023 AMI。我们还将传递先前创建的 IAM 角色和 VPC,以及要在其上运行的实例类型(本例中的实例类型为具有 1 个 vCPU 和 1 GB 内存的 t2.micro)。如果您在一个较新的亚马逊云科技区域中按照本教程操作,则 t2.micro 类型可能不可用。在此情况下,改为使用 t3.micro 类型即可。若要查看所有不同的实例类型,请参阅 EC2 实例类型页

// the AMI to be used for the EC2 Instance
 const ami = new AmazonLinuxImage({
 generation: AmazonLinuxGeneration.AMAZON_LINUX_2023,
 cpuType: AmazonLinuxCpuType.X86_64,
 });

 // The actual Web EC2 Instance for the web server
 const webServer = new Instance(this, 'web_server',{
 vpc,
 instanceType: InstanceType.of(
 InstanceClass.T2,
 InstanceSize.MICRO,
 ),
 machineImage: ami,
 securityGroup: webSg,
 role: webServerRole,
 });

最后,我们要附加用户数据并使用特定标签来标记实例。用户数据用于引导 EC2 实例并在实例首次启动时安装特定的应用程序包。Systems Manager 稍后会使用标签对要部署的实例进行标记。

下面是我们将附加到 EC2 实例的用户数据 bash 脚本。请确保此代码存储在名为 configure_amz_linux_sample_app.sh 的文件中,该文件位于 CDK 应用程序根目录下的 assets 目录中。

#!/bin/bash -xe
# Install OS packages
yum update -y
yum groupinstall -y "Development Tools"
amazon-linux-extras install -y nginx1
yum install -y nginx python3.11 python3.11-pip python3.11-devel ruby wget
python3.11 -m pip install pipenv wheel
python3.11 -m pip install uwsgi

# Code Deploy Agent
cd /home/ec2-user
wget https://aws-codedeploy-us-west-2.s3.us-west-2.amazonaws.com/latest/install
chmod +x ./install
./install auto

现在,使用 CDK 附加用户数据脚本并为实例添加标签:

// User data - used for bootstrapping
 const webSGUserData = readFileSync('./assets/configure_amz_linux_sample_app.sh','utf-8');
 webServer.addUserData(webSGUserData);
 // Tag the instance
 cdk.Tags.of(webServer).add('application-name','python-web')
 cdk.Tags.of(webServer).add('stage','prod')

为了轻松追踪 EC2 实例的 IP 地址,我们还将对输出结果进行配置:

// Output the public IP address of the EC2 instance
 new cdk.CfnOutput(this, "IP Address", {
 value: webServer.instancePublicIp,
 });

现在,我们已经定义了用于创建 EC2 实例、VPC、具有入站访问规则的安全组以及 IAM 角色的 Amazon CDK 堆栈,并将该堆栈作为 IAM 实例配置文件附加到 EC2 实例。此外,我们还标记了 EC2 实例并向其附加了用户数据脚本。


✅ ✅ ✅ 检查点 1 ✅ ✅ ✅
 

您的 lib/ec2-cdk-stack.ts 文件应如下所示:

import * as cdk from 'aws-cdk-lib';
import { readFileSync } from 'fs';
import { Construct } from 'constructs';

import { Vpc, SubnetType, Peer, Port, AmazonLinuxGeneration, 
 AmazonLinuxCpuType, Instance, SecurityGroup, AmazonLinuxImage,
 InstanceClass, InstanceSize, InstanceType
} from 'aws-cdk-lib/aws-ec2';

import { Role, ServicePrincipal, ManagedPolicy } from 'aws-cdk-lib/aws-iam';

export class Ec2CdkStack extends cdk.Stack {
 constructor(scope: Construct, id: string, props?: cdk.StackProps) {
 super(scope, id, props);
 // IAM
 // Policy for CodeDeploy bucket access
 // Role that will be attached to the EC2 instance so it can be
 // managed by AWS SSM
 const webServerRole = new Role(this, "ec2Role", {
 assumedBy: new ServicePrincipal("ec2.amazonaws.com"),
 });

 // IAM policy attachment to allow access to
 webServerRole.addManagedPolicy(
 ManagedPolicy.fromAwsManagedPolicyName("AmazonSSMManagedInstanceCore")
 );
 
 webServerRole.addManagedPolicy(
 ManagedPolicy.fromAwsManagedPolicyName("service-role/AmazonEC2RoleforAWSCodeDeploy")
 );

 // VPC
 // This VPC has 3 public subnets, and that's it
 const vpc = new Vpc(this, 'main_vpc',{
 subnetConfiguration: [
 {
 cidrMask: 24,
 name: 'pub01',
 subnetType: SubnetType.PUBLIC,
 },
 {
 cidrMask: 24,
 name: 'pub02',
 subnetType: SubnetType.PUBLIC,
 },
 {
 cidrMask: 24,
 name: 'pub03',
 subnetType: SubnetType.PUBLIC,
 }
 ]
 });

 // Security Groups
 // This SG will only allow HTTP traffic to the Web server
 const webSg = new SecurityGroup(this, 'web_sg',{
 vpc,
 description: "Allows Inbound HTTP traffic to the web server.",
 allowAllOutbound: true,
 });

 webSg.addIngressRule(
 Peer.anyIpv4(),
 Port.tcp(80)
 );
 
 // EC2 Instance
 // This is the Python Web server that we will be using
 // Get the latest AmazonLinux 2 AMI for the given region
 const ami = new AmazonLinuxImage({
 generation: AmazonLinuxGeneration.AMAZON_LINUX_2023,
 cpuType: AmazonLinuxCpuType.X86_64,
 });

 // The actual Web EC2 Instance for the web server
 const webServer = new Instance(this, 'web_server',{
 vpc,
 instanceType: InstanceType.of(
 InstanceClass.T2,
 InstanceSize.MICRO,
 ),
 machineImage: ami,
 securityGroup: webSg,
 role: webServerRole,
 });

 // User data - used for bootstrapping
 const webSGUserData = readFileSync('./assets/configure_amz_linux_sample_app.sh','utf-8');
 webServer.addUserData(webSGUserData);
 // Tag the instance
 cdk.Tags.of(webServer).add('application-name','python-web')
 cdk.Tags.of(webServer).add('stage','prod')

 // Output the public IP address of the EC2 instance
 new cdk.CfnOutput(this, "IP Address", {
 value: webServer.instancePublicIp,
 });
 }
}

设置 GitHub

现在,我们将把示例应用程序分叉到自己的 GitHub 账户,并配置要由 CI/CD 管道使用的 Github 令牌。

最佳做法是使用令牌(而非密码)通过 GitHub API 或命令行访问 GitHub 账户。更多信息请参阅创建个人访问令牌

将令牌保存在安全的位置以供日后使用。我们会将此令牌用于以下两个目的:

  1. 为暂存代码、提交代码、将代码从本地存储库推送到 GitHub 存储库提供身份验证。您还可以使用 SSH 密钥来实现此目的。
  2. 将 GitHub 连接到 CodePipeline,连接后,每当新代码提交到 GitHub 存储库时就会自动触发管道执行。

该令牌的作用域应当为 repo(用于读取存储库)和 admin:repo_hook(如果您打算使用 webhook,则默认为 true),如下图所示。

现在,为了让 Amazon CodePipeline 从该 GitHub 存储库中读取数据,我们需要对刚创建的 GitHub 个人访问令牌进行配置。此令牌应当以明文密钥(而非 JSON 密钥)形式按照与 github-oauth-token 完全相同的名称存储在 Amazon Secrets Manager 中。

在以下命令中将 GITHUB_ACCESS_TOKEN 替换为您的明文密钥和 REGION,然后运行它:

aws secretsmanager create-secret \ 
 --name github-oauth-token \ 
 --description "Github access token for cdk" \ 
 --secret-string GITHUB_ACCESS_TOKEN \ 
 --region REGION

如需更多帮助,请参阅创建和检索密钥

最后,让我们继续将示例应用程序存储库分叉到自己的 GitHub 账户中。从现在开始,我们采用这种方式与此应用程序进行交互。有关将存储库分叉的更多信息可以在此处找到。

创建 CI/CD 管道

这一步需要创建 CI/CD 管道。此 CI/CD 管道将负责在我们的 EC2 实例上测试、部署和配置 Web 应用程序。该管道包含以下三个阶段:1/ 源 - 管道在此阶段会从我们之前分叉的 GitHub 存储库中提取要提交的内容;2/ 构建 - 我们在此阶段使用 unittest Python 单元测试框架测试应用程序代码;3/ 部署 - 使用 Amazon CodeDeploy 在 EC2 实例上部署和配置 Web 应用程序。让我们回到 CDK。

首先,让我们将其他模块导入我们的主 CDK 堆栈文件 lib/ec2-cdk-stack.ts 中:

import { Pipeline, Artifact } from 'aws-cdk-lib/aws-codepipeline';
import { GitHubSourceAction, CodeBuildAction, CodeDeployServerDeployAction } from 'aws-cdk-lib/aws-codepipeline-actions';
import { PipelineProject, LinuxBuildImage } from 'aws-cdk-lib/aws-codebuild';
import { ServerDeploymentGroup, ServerApplication, InstanceTagSet } from 'aws-cdk-lib/aws-codedeploy';
import { SecretValue } from 'aws-cdk-lib';

现在让我们创建管道及其阶段,该操作只是定义管道以及不同阶段/时期的结构:

// CodePipeline
 const pipeline = new Pipeline(this, 'python_web_pipeline',{
 pipelineName: 'python-webApp',
 crossAccountKeys: false, // solves the encrypted bucket issue
 });

 // STAGES
 // Source Stage
 const sourceStage = pipeline.addStage({
 stageName: 'Source',
 })
 
 // Build Stage
 const buildStage = pipeline.addStage({
 stageName: 'Build',
 })
 
 // Deploy Stage
 const deployStage = pipeline.addStage({
 stageName: 'Deploy',
 })

我们将从 Source 阶段开始,因为我们会在此阶段将管道连接到 GitHub,以便 GitHub 可以检索已提交且要通过管道进行传递的代码。这里需要注意一些重要事项:请确保在 Amazon Secrets Manager 中将 GitHub 令牌设置为密钥(参见上述步骤),并确保对 owner 参数进行更改,使其与您的 GitHub 用户名相匹配:

// Source action
 const sourceOutput = new Artifact();
 const githubSourceAction = new GitHubSourceAction({
 actionName: 'GithubSource',
 oauthToken: SecretValue.secretsManager('github-oauth-token'), // MAKE SURE TO SET UP BEFORE
 owner: 'darko-mesaros', // THIS NEEDS TO BE CHANGED TO YOUR OWN USER ID
 repo: 'sample-python-web-app',
 branch: 'main',
 output: sourceOutput,
 });

 sourceStage.addAction(githubSourceAction);

在 Build 阶段:我们实际上并不构建任何内容,而是测试代码。在此阶段,我们将针对代码运行单元测试(稍后会设置),如果测试成功,则会继续进入下一个阶段。

// Build Action
 const pythonTestProject = new PipelineProject(this, 'pythonTestProject',{
 environment: {
 buildImage: LinuxBuildImage.AMAZON_LINUX_2_5
 }
 });

 const pythonTestOutput = new Artifact();

 const pythonTestAction = new CodeBuildAction({
 actionName: 'TestPython',
 project: pythonTestProject,
 input: sourceOutput,
 outputs: [pythonTestOutput]
 });

 buildStage.addAction(pythonTestAction);

Deploy 阶段(最后一个阶段):此阶段将使用 CodeDeploy 在 EC2 实例上部署和配置 Web 应用程序。为此,我们需要安装 CodeDeploy 代理并在实例上运行该代理(我们之前通过用户数据执行过此操作),并且还需要通知 CodeDeploy 要针对哪些实例进行部署。我们将在此操作中使用标签。回顾本教程的前面部分,我们用特定标签标记了 EC2 实例。现在,我们通过 CodeDeploy 使用这些标签来确定实例目标,并部署代码。

// Deploy Actions
 const pythonDeployApplication = new ServerApplication(this,"python_deploy_application",{
 applicationName: 'python-webApp'
 });

 // Deployment group
 const pythonServerDeploymentGroup = new ServerDeploymentGroup(this,'PythonAppDeployGroup',{
 application: pythonDeployApplication,
 deploymentGroupName: 'PythonAppDeploymentGroup',
 installAgent: true,
 ec2InstanceTags: new InstanceTagSet(
 {
 'application-name': ['python-web'],
 'stage':['prod', 'stage']
 })
 });

 // Deployment action
 const pythonDeployAction = new CodeDeployServerDeployAction({
 actionName: 'PythonAppDeployment',
 input: sourceOutput,
 deploymentGroup: pythonServerDeploymentGroup,
 });

 deployStage.addAction(pythonDeployAction);
 
✅ ✅ ✅ 检查点 2 ✅ ✅ ✅
 

我们现已对 ​CDK 应用程序的所有代码完成更改,lib/ec2-cdk-stack.ts 文件应如下所示:

import * as cdk from 'aws-cdk-lib';
import { readFileSync } from 'fs';
import { Construct } from 'constructs';

import { Vpc, SubnetType, Peer, Port, AmazonLinuxGeneration, 
 AmazonLinuxCpuType, Instance, SecurityGroup, AmazonLinuxImage,
 InstanceClass, InstanceSize, InstanceType
} from 'aws-cdk-lib/aws-ec2';

import { Role, ServicePrincipal, ManagedPolicy } from 'aws-cdk-lib/aws-iam';
import { Pipeline, Artifact } from 'aws-cdk-lib/aws-codepipeline';
import { GitHubSourceAction, CodeBuildAction, CodeDeployServerDeployAction } from 'aws-cdk-lib/aws-codepipeline-actions';
import { PipelineProject, LinuxBuildImage } from 'aws-cdk-lib/aws-codebuild';
import { ServerDeploymentGroup, ServerApplication, InstanceTagSet } from 'aws-cdk-lib/aws-codedeploy';
import { SecretValue } from 'aws-cdk-lib';

export class Ec2CdkStack extends cdk.Stack {
 constructor(scope: Construct, id: string, props?: cdk.StackProps) {
 super(scope, id, props);
 // IAM
 // Policy for CodeDeploy bucket access
 // Role that will be attached to the EC2 instance so it can be
 // managed by AWS SSM
 const webServerRole = new Role(this, "ec2Role", {
 assumedBy: new ServicePrincipal("ec2.amazonaws.com"),
 });

 // IAM policy attachment to allow access to
 webServerRole.addManagedPolicy(
 ManagedPolicy.fromAwsManagedPolicyName("AmazonSSMManagedInstanceCore")
 );
 
 webServerRole.addManagedPolicy(
 ManagedPolicy.fromAwsManagedPolicyName("service-role/AmazonEC2RoleforAWSCodeDeploy")
 );

 // VPC
 // This VPC has 3 public subnets, and that's it
 const vpc = new Vpc(this, 'main_vpc',{
 subnetConfiguration: [
 {
 cidrMask: 24,
 name: 'pub01',
 subnetType: SubnetType.PUBLIC,
 },
 {
 cidrMask: 24,
 name: 'pub02',
 subnetType: SubnetType.PUBLIC,
 },
 {
 cidrMask: 24,
 name: 'pub03',
 subnetType: SubnetType.PUBLIC,
 }
 ]
 });

 // Security Groups
 // This SG will only allow HTTP traffic to the Web server
 const webSg = new SecurityGroup(this, 'web_sg',{
 vpc,
 description: "Allows Inbound HTTP traffic to the web server.",
 allowAllOutbound: true,
 });
 
 webSg.addIngressRule(
 Peer.anyIpv4(),
 Port.tcp(80)
 );
 
 // EC2 Instance
 // This is the Python Web server that we will be using
 // Get the latest AmazonLinux 2 AMI for the given region
 const ami = new AmazonLinuxImage({
 generation: AmazonLinuxGeneration.AMAZON_LINUX_2023,
 cpuType: AmazonLinuxCpuType.X86_64,
 });

 // The actual Web EC2 Instance for the web server
 const webServer = new Instance(this, 'web_server',{
 vpc,
 instanceType: InstanceType.of(
 InstanceClass.T3,
 InstanceSize.MICRO,
 ),
 machineImage: ami,
 securityGroup: webSg,
 role: webServerRole,
 });

 // User data - used for bootstrapping
 const webSGUserData = readFileSync('./assets/configure_amz_linux_sample_app.sh','utf-8');
 webServer.addUserData(webSGUserData);
 // Tag the instance
 cdk.Tags.of(webServer).add('application-name','python-web')
 cdk.Tags.of(webServer).add('stage','prod')
 
 // Pipeline stuff
 // CodePipeline
 const pipeline = new Pipeline(this, 'python_web_pipeline', {
 pipelineName: 'python-webApp',
 crossAccountKeys: false, // solves the encrypted bucket issue
 });

 // STAGES
 // Source Stage
 const sourceStage = pipeline.addStage({
 stageName: 'Source',
 });
 
 // Build Stage
 const buildStage = pipeline.addStage({
 stageName: 'Build',
 });
 
 // Deploy Stage
 const deployStage = pipeline.addStage({
 stageName: 'Deploy',
 });

 // Add some action
 // Source action
 const sourceOutput = new Artifact();
 const githubSourceAction = new GitHubSourceAction({
 actionName: 'GithubSource',
 oauthToken: SecretValue.secretsManager('github-oauth-token'), // SET UP BEFORE
 owner: 'darko-mesaros', // THIS NEEDS TO BE CHANGED TO THE READER
 repo: 'sample-python-web-app',
 branch: 'main',
 output: sourceOutput,
 });

 sourceStage.addAction(githubSourceAction);

 // Build Action
 const pythonTestProject = new PipelineProject(this, 'pythonTestProject', {
 environment: {
 buildImage: LinuxBuildImage.AMAZON_LINUX_2_5
 }
 });
 
 const pythonTestOutput = new Artifact();
 const pythonTestAction = new CodeBuildAction({
 actionName: 'TestPython',
 project: pythonTestProject,
 input: sourceOutput,
 outputs: [pythonTestOutput]
 });

 buildStage.addAction(pythonTestAction);

 // Deploy Actions
 const pythonDeployApplication = new ServerApplication(this,"python_deploy_application", {
 applicationName: 'python-webApp'
 });

 // Deployment group
 const pythonServerDeploymentGroup = new ServerDeploymentGroup(this,'PythonAppDeployGroup', {
 application: pythonDeployApplication,
 deploymentGroupName: 'PythonAppDeploymentGroup',
 installAgent: true,
 ec2InstanceTags: new InstanceTagSet(
 {
 'application-name': ['python-web'],
 'stage':['prod', 'stage']
 })
 });

 // Deployment action
 const pythonDeployAction = new CodeDeployServerDeployAction({
 actionName: 'PythonAppDeployment',
 input: sourceOutput,
 deploymentGroup: pythonServerDeploymentGroup,
 });

 deployStage.addAction(pythonDeployAction);

 // Output the public IP address of the EC2 instance
 new cdk.CfnOutput(this, "IP Address", {
 value: webServer.instancePublicIp,
 });
 }
}

用于测试和部署的其他文件

为了正确测试和部署应用程序,我们需要向先前分叉的示例存储库添加一些额外的内容。CodeBuild 和 CodeDeploy 服务会使用这些文件。此外,我们还将编写一个简单的 Python 单元测试。让我们从这个开始吧。

若要创建我们的测试,请在示例应用程序的根目录下创建一个 tests 目录,并向其中添加以下 test_sample.py 文件:

import unittest
from application import application

class TestHello(unittest.TestCase):

 def setUp(self):
 application.testing = True
 self.application = application.test_client()

 def test_hello(self):
 rv = self.application.get('/')
 self.assertEqual(rv.status, '200 OK')

if __name__ == '__main__':
 import xmlrunner
 unittest.main(testRunner=xmlrunner.XMLTestRunner(output='test-reports'))
 unittest.main()

此测试将运行 Flask 应用程序并查看它是否返回 200 HTTP 状态码。就这么简单。除了此文件以外,让我们在同一目录下创建一个仅用于后续操作的 __init__.py 文件。该文件可以为空,因此您只需使用以下命令创建它:

touch tests/__init__.py

我们现已准备好创建 buildspec.yml 文件。CodeDeploy 会将该文件用作某些操作的指令集,在构建代码时需要执行这些操作。在本例中,我们通过该文件指示了如何运行测试。在示例应用程序的根目录下,添加包含以下内容的 buildspec.yml 文件:

version: 0.2

phases:
 install:
 runtime-versions:
 python: 3.11
 commands:
 - echo Entered the install phase...
 - pip install pipenv
 - pipenv install
 finally:
 - echo This always runs even if the update or install command fails 
 build:
 commands:
 - echo Entered the build phase...
 - echo Build started on `date`
 - pipenv run python -m unittest # not an interactive session so we need to run
 finally:
 - echo This always runs even if the install command fails
 post_build:
 commands:
 - echo Entered the post_build phase...
 - echo Build completed on `date`

最后,让我们为 CodeDeploy 添加一些迫切需要的文件。与 CodeBuild 类似,CodeDeploy 将一个名为 appspec.yml 的文件用作有关如何将应用程序部署到其最终目标的指令集。除了此文件以外,我们将添加一些 shell 脚本,通过这些脚本可以在服务器上配置和启动应用程序。这是必需的,因为我们需要创建一个特定的 nginx 网站,并重新启动一些服务。但是,让我们首先在示例应用程序的根目录下创建 appspec.yml 文件,其中包含以下内容:

version: 0.0
os: linux
files:
 - source: /
 destination: /var/www/SampleApp
hooks:
 BeforeInstall:
 - location: scripts/setup_dirs.sh
 timeout: 300
 runas: root
 AfterInstall:
 - location: scripts/setup_services.sh
 - location: scripts/pipenv.sh
 timeout: 300
 runas: root
 ApplicationStart:
 - location: scripts/start_server.sh
 timeout: 300
 runas: root

正如您所看到的,我们在部署的不同阶段涉及 4 个不同的脚本。这是在部署代码前后正确设置 EC2 实例所必需的。这些脚本应存储在示例应用程序根目录下名为 scripts 的目录中。这些脚本应按如下方式命名,并且应包含以下内容:

setup_dirs.sh

#!/bin/bash -xe
mkdir -p /var/www/SampleApp
chown nginx:nginx /var/www
chown nginx:nginx /var/www/SampleApp

setup_services.sh

#!/bin/bash -xe
## Install uWSGI as a systemd service, enable it to run at boot, then start it
cp /var/www/SampleApp/sample-app.uwsgi.service /etc/systemd/system/mywebapp.uwsgi.service
mkdir -p /var/log/uwsgi
chown nginx:nginx /var/log/uwsgi
systemctl enable mywebapp.uwsgi.service

## Copy the nginx config file, then ensure nginx starts at boot, and restart it to load the config
cp /var/www/SampleApp/nginx-app.conf /etc/nginx/conf.d/nginx-app.conf
mkdir -p /var/log/nginx
chown nginx:nginx /var/log/nginx
systemctl enable nginx.service

pipenv.sh

#!/bin/bash -xe

chown nginx:nginx -R /var/www/SampleApp/
cd /var/www/SampleApp
pipenv install

start_server.sh

#!/bin/bash -xe
systemctl restart mywebapp.uwsgi.service
systemctl restart nginx.service

最后,按如下示例更新应用代码仓库根目录下的 Pipfile 文件。

[[source]]
name = "pypi"
url = "https://pypi.org/simple"
verify_ssl = true

[dev-packages]

[packages]
flask = "*"
boto3 = "*"
uwsgi = "*"

[requires]
python_version = "3.11"

所有这些文件创建完毕后,示例应用程序目录应如下所示:

├── application.config
├── application.py
├── appspec.yml
├── buildspec.yml
├── CODE_OF_CONDUCT.md
├── configure_amz_linux_sample_app.sh
├── CONTRIBUTING.md
├── Dockerfile
├── LICENSE
├── nginx-app.conf
├── Pipfile
├── README.md
├── sample-app.uwsgi.service
├── scripts
│   ├── pipenv.sh
│   ├── setup_dirs.sh
│   ├── setup_services.sh
│   └── start_server.sh
├── start.sh
├── static
│   ├── bootstrap
│   └── jquery
├── templates
│   └── index.html
└── tests
 ├── __init__.py
 ├── __pycache__
 └── test_sample.py

现在,请确保将对示例代码的更改添加、提交并推送到 GitHub 存储库,然后再继续执行下一步来部署基础设施。

引导 CDK

在部署 CDK 应用程序之前,我们需要在您要部署到的账户上配置 CDK。编辑 bin/ec2-cdk.ts 并取消对第 14 行的注释:

env: { account: process.env.CDK_DEFAULT_ACCOUNT, region: process.env.CDK_DEFAULT_REGION },

这将使用在 Amazon CLI 中配置的账户 ID 和区域 — 如果您尚未进行设置,请按照本教程中的相关部分进行操作。我们还需要在我们的账户中引导 CDK。为此,我们将为 CDK 创建所需的基础设施,以管理您账户中的基础设施,每个账户只需操作一次。如果您已完成引导,或者不确定是否已完成,只需再次运行相关命令。CDK 仅在需要时进行引导。若要引导 CDK,请运行 cdk bootstrap(您的账户 ID 会与下面的占位符 ID 有所不同):

cdk bootstrap

#output
⏳ Bootstrapping environment aws://0123456789012/<region>...
✅ Environment aws://0123456789012/<region> bootstrapped
Deploying the stack

完成引导后,我们就可以署所有基础设施了。运行以下命令:

cdk deploy

系统将向您显示以下输出结果和确认页面。由于我们的堆栈存在安全隐患,因此您将看到这些内容的摘要,并且需要先对其进行确认,然后再继续部署。如果您正在创建、修改或删除任何 IAM 策略、角色、组或用户,则当您更改任何防火墙规则时,将始终显示此摘要。

输入 y 以继续部署并创建资源。CLI 将显示部署进度,最后显示我们在 CDK 应用程序中定义的输出结果。

Do you wish to deploy these changes (y/n)? y
PythonEc2BlogpostStack: deploying...
[0%] start: Publishing afe67465ec62603d27d77795221a45e68423c87495467b0265ecdadad80bb5e2:current
[33%] success: Published afe67465ec62603d27d77795221a45e68423c87495467b0265ecdadad80bb5e2:current
[33%] start: Publishing 73887b77b71ab7247eaf6dc4647f03f9f1cf8f0da685460f489ec8f2106d480d:current
[66%] success: Published 73887b77b71ab7247eaf6dc4647f03f9f1cf8f0da685460f489ec8f2106d480d:current
[66%] start: Publishing 13138ebf2da51426144f6f5f4f0ad197787f52aad8b6ceb26ecff68d33cd2b78:current
[100%] success: Published 13138ebf2da51426144f6f5f4f0ad197787f52aad8b6ceb26ecff68d33cd2b78:current
Ec2CdkStack: creating CloudFormation changeset...

✅ PythonEc2BlogpostStack

✨ Deployment time: 27.74s

Outputs:
PythonEc2BlogpostStack.IPAddress = x.x.x.x
Stack ARN:
arn:aws:cloudformation:us-west-2:123456789000:stack/Ec2CdkStack/59f1e560-grunf-11ed-afno1-06f3bbc9cf63

✨ Total time: 29.11s

您的基础设施现已部署,实例正在启动,您可以使用底部的输出结果来确定 Web 服务器的 IP 地址。该应用程序需要进行部署,因此不能立即使用。若要检查部署的状态,请前往 Amazon CodePipeline 控制台并找到 python-webApp 管道。在管道信息页面上,您应当会看到类似下图的内容:

部署成功后(Deploy 阶段应为绿色),复制 EC2 实例的 IP 地址并将其粘贴到浏览器中,您的示例应用程序应启动并运行。恭喜您!您已使用 CI/CD 管道设置了一个在 EC2 实例上运行的 Python Web 应用程序,该管道可用来测试和部署所做的更改!

清理亚马逊云科技环境

您现在已完成本教程,但我们仍需要清除在学习本教程的过程中创建的资源。如果您的账户仍在免费套餐范围内,则不会产生任何月度费用。超出免费套餐后,每月费用约为 9.45 美元,或每小时 0.0126 美元。

若要删除我们创建的所有基础设施,请使用 cdk destroy 命令。该命令只会删除在学习本教程的过程中在 CDK 应用程序中创建的基础设施。您将看到一个确认消息:

cdk destroy

# Enter y to approve the changes and delete any stack resources.
PythonEc2BlogpostStack: destroying ...

✅ PythonEc2BlogpostStack: destroyed

如果输出结果中显示 PythonEc2BlogpostStack: destroyed,则说明您的资源已被删除。此外,还需要执行一个清除步骤:删除 CDK 用于上传脚本和示例应用程序的 S3 存储桶。作为安全预防措施,CDK 不会删除这些资源。为此,在浏览器中打开 S3 控制台,然后查找名称类似于 pythonec2blockpoststack-<randonmunbers>-us-east-1 的存储桶(您的存储桶名称将包含您的账号和不同的随机数,而不包含 123456789012)。如果您看到多个名称(通常是因为您先前用过 CDK 资产功能),则可以按 Creation Date 排序以查看最新创建的名称。打开该存储桶,确认您是否看到一个名为 python-webApp 的目录。选择所有目录,依次选择 actions -> delete,然后按照提示删除这些对象。最后,返回 S3 控制台,删除该存储桶。

总结

恭喜您!您已完成“在 Amazon EC2 上构建 Web 应用程序”教程,使用 CDK 预配了所有基础设施,而且配置了 EC2 实例,以便安装和配置操作系统软件包来运行示例 Python Web 应用程序。