通过用户数据引导 Amazon EC2 实例以运行 Python Web 应用程序
在服务器上使用 Nginx 和 uWSGI 手动设置和配置 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 应用程序。
- 注册 / 登录 亚马逊云科技账户
- 已安装 CDK:请访问 Amazon CDK 入门指南,了解更多信息。
本教程中使用的示例代码来自 GitHub
简介
为了部署此 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 CodePipeline、Amazon CodeBuild 和 Amazon 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 账户。更多信息请参阅创建个人访问令牌。
将令牌保存在安全的位置以供日后使用。我们会将此令牌用于以下两个目的:
- 为暂存代码、提交代码、将代码从本地存储库推送到 GitHub 存储库提供身份验证。您还可以使用 SSH 密钥来实现此目的。
- 将 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 应用程序。