Heroku是一个平台即服务,使开发者能够完全在云中构建、运行和操作应用程序。
Heroku使创建虚拟机来托管应用程序和部署网站等事情变得简单。
Heroku提供的一些功能实际上可以用其他工具轻松创建。
在这篇文章中,你将学习如何创建一个非常简单的网络应用,允许用户通过点击按钮来配置虚拟机和部署静态网站,所有这些都托管在亚马逊网络服务。
你将学习如何用代码配置基础设施,然后你将能够把你学到的东西应用到你自己的应用程序中。
这篇文章是我们刚刚在freeCodeCamp.org YouTube频道上发布的完整课程的补充,该课程将教你如何使用Python以编程方式配置基础设施。
你可以在下面或freeCodeCamp.org YouTube频道上观看该课程(1.5小时的观看时间)。
配置基础设施与平台工程有关。一个平台工程团队通过规划、设计和管理其云平台来为一个组织服务。而这往往可以通过编程来完成。
我在本课程中讲授的工具不仅仅可以用于配置虚拟机和部署网站。它们可以用于平台工程,使云平台的管理更加简单。
自动化API
这门课程的重点是Pulumi的自动化API。Pulumi为freeCodeCamp提供了一笔资金,使本课程成为可能。
Pulumi的开源基础设施即代码SDK使你能够使用许多不同的编程语言,在任何云上创建、部署和管理基础设施。
他们的自动化API使得使用Pulumi引擎以编程方式提供基础设施成为可能。基本上,它使编写一个程序变得简单,可以在各种不同的云平台上自动创建虚拟机、数据库、VPC、静态网站等。
我将向你展示如何使用Flask和Python在后端创建我们的Heroku克隆网络应用。然而,你不需要知道如何使用Flask和Python来跟随。此外,我向你展示的一切也可以用许多其他不同的框架和编程语言来完成,无论你使用什么网络框架,许多步骤都是一样的。
我们的应用程序将在AWS上配置资源,但Pulumi使其能够简单地在大多数主要的云供应商上配置资源,而且不需要对代码进行太多的更新就能使用不同的供应商。
感谢Komal Ali,他创建的代码是我在本课程中的代码的基础。
创建Heroku克隆系统
Pulumi CLI
首先,确保你已经安装了Pulumi CLI。根据你的操作系统,安装的方式是不同的。
如果你有MacsOS和Homebrew,你可以使用命令brew install pulumi 。
如果你有Windows和Chocolatey,你可以使用命令choco install pulumi 。
AWS CLI
这个项目也使用了AWS,所以你必须确保你有一个AWS账户,并且设置了CLI并进行了验证。
你可以在这里注册一个免费的AWS账户:https://aws.amazon.com/free/
了解如何为你的操作系统安装AWS CLI:https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2.html
对于MacOS,你可以使用这些命令:
curl "https://awscli.amazonaws.com/AWSCLIV2.pkg" -o "AWSCLIV2.pkg"
sudo installer -pkg AWSCLIV2.pkg -target /
对于Windows来说,有一些额外的步骤,你应该只按照这里的指示来做。
接下来,你需要从AWS获得一个访问密钥ID和秘密访问密钥。按照亚马逊的指示来获取这些。
现在在命令行中运行以下程序:
aws configure
在提示时输入你的访问密钥ID和秘密访问密钥。你可以保持 "默认区域名称 "和 "默认输出格式 "为无。
设置项目
一个Pulumi项目只是一个包含一些文件的目录。你有可能手工创建一个新的项目。然而,pulumi new 命令可以自动完成这一过程。
pulumi new
如果这是你第一次使用Pulumi,你会被要求输入一个访问代码或登录。要获得一个访问代码,请到app.pulumi.com/account/tok…
pulumi new 命令允许你在许多模板中进行选择。选择 "aws-python "模板。你可以选择其他选项的默认值。
这个命令已经创建了我们需要的所有文件,初始化了一个名为dev (我们项目的一个实例)的新栈。
每个Pulumi程序都被部署到一个堆栈。堆栈是一个孤立的、可独立配置的Pulumi程序的实例。堆栈通常用于表示开发的不同阶段。在这种情况下,我们称它为 "dev"。
现在我们需要为我们的项目安装另外两个依赖项,即
venv/bin/pip install flask requests
创建Flask应用程序
我们现在可以开始为我们的Flask网络应用创建文件了。
Pulumi创建了一个名为"__main__.py "的文件。把这个文件的名字改为 "app.py",因为这是一个Flask应用,Flask会寻找一个有这个名字的文件。
该文件中有一些代码,我们最终会使用与之类似的代码,但现在我们只需将 "app.py "中的代码替换为以下内容,以测试flask是否正常。
from flask import Flask
app = Flask(__name__)
@app.route('/')
def hello_world():
return 'Hello World'
if __name__ == '__main__':
app.run()
这是最基本的Flask网络应用,我们可以通过在终端运行以下命令来测试它。
env/bin/flask run
然后我们可以在网页浏览器中访问正在运行的应用程序,网址是"http://127.0.0.1:5000/"。
如果你进入该网址,它应该显示 "Hello World"。如果是这样,Flask就能正常工作,所以我们可以将 "app.py "文件更新为以下内容。
import os
from flask import Flask, render_template
import pulumi.automation as auto
def ensure_plugins():
ws = auto.LocalWorkspace()
ws.install_plugin("aws", "v4.0.0")
def create_app():
ensure_plugins()
app = Flask(__name__, instance_relative_config=True)
app.config.from_mapping(
SECRET_KEY="secret",
PROJECT_NAME="herocool",
PULUMI_ORG=os.environ.get("PULUMI_ORG"),
)
@app.route("/", methods=["GET"])
def index():
return render_template("index.html")
from . import sites
app.register_blueprint(sites.bp)
from . import virtual_machines
app.register_blueprint(virtual_machines.bp)
return app
我们将Pulumi的自动化框架作为一个变量导入,称为auto 。然后在ensure_plugins 函数中,我们可以访问LocalWorkspace 。一个Workspace 是包含单个Pulumi项目的执行环境。工作空间用于管理执行环境,提供各种实用工具,如插件安装、环境配置以及创建、删除和列出堆栈。
因为我们在本教程中部署了AWS资源,所以我们必须在Workspace 内安装AWS提供者插件,这样Pulumi程序在执行过程中就可以得到它。
create_app 函数是Flask在我们启动flask应用时要运行的。我们首先运行ensure_plugins 函数,然后创建Flask应用。app.config.from_mapping() 函数被Flask用来设置应用程序将使用的一些默认配置。这不会是一个生产就绪的应用程序,但如果你曾经部署一个Flask应用程序,请确保改变秘密密钥。其他变量是由pulumi使用的。
文件的其余部分是Flask应用所共有的。我们设置默认路由(@app.route("/", methods=["GET"]) )来渲染模板 "index.html"(我们仍然需要创建这个模板。
最后,该文件导入site和virtual_machines文件,并将其注册为蓝图。
Flask中的蓝图是一种组织一组相关视图和其他代码的方式。与其直接在应用程序中注册视图和其他代码,不如在蓝图中注册它们。然后,当蓝图在工厂函数中可用时,它就被注册到应用程序中。
基本上,我们使用蓝图,这样我们就可以在其他文件中为我们的网络应用定义额外的路由,而我们仍然需要创建这些文件。
创建模板
说到创建其他文件,让我们来创建一些。创建一个名为 "templates "的目录,然后在该目录中创建一个名为 "index.html "的文件。
添加以下代码。
{% extends "base.html" %}
{% block content %}
<div class="row row-cols-1 row-cols-md-2 g-4">
<div class="col">
<div class="card">
<div class="card-body">
<h5 class="card-title">Static Websites</h5>
<p class="card-text">Deploy your own static website in seconds!</p>
<a href="{{ url_for("sites.list_sites") }}" class="btn btn-primary">Get started</a>
</div>
</div>
</div>
<div class="col">
<div class="card">
<div class="card-body">
<h5 class="card-title">Virtual Machines</h5>
<p class="card-text">Set up a virtual machine for development and testing.</p>
<a href="{{ url_for("virtual_machines.list_vms") }}" class="btn btn-primary">Get started</a>
</div>
</div>
</div>
</div>
{% endblock %}
我不会对这段代码进行太多详细说明。它是基本的HTML,但大括号内的任何内容都是一个表达式,将被输出到最终的文档中。Flask使用Jinja模板库来渲染模板。你会注意到,它动态地渲染了一个网站和虚拟机的列表。很快我们将创建Python代码,使用Pulumi获得这些列表。
但接下来创建一个名为 "base.html "的文件。你会注意到,"index.html "扩展了 "base.html"。这基本上将是我们所有页面共享的标题。
添加这段代码。
<!DOCTYPE html>
<head>
<title>Herocool - {% block title %}{% endblock %}</title>
<link rel="stylesheet" href="{{ url_for("static", filename="bootstrap.min.css") }}">
<script src="{{ url_for("static", filename="bootstrap.min.js") }}"></script>
</head>
<body>
<div class="container p-2">
<header class="d-flex flex-wrap justify-content-center py-3 mb-4 border-bottom">
<a href="{{ url_for("index") }}" class="d-flex align-items-center mb-3 mb-md-0 me-md-auto text-dark text-decoration-none">
<span class="fs-3">Herocool</span>
</a>
{% block nav %}{% endblock %}
</header>
</div>
<section class="container-md px-4">
<div>
<header class="row gy-4">
<span class="fs-4">{% block header %}{% endblock %}</span>
{% for category, message in get_flashed_messages(with_categories=true) %}
<div class="alert alert-{{ category }}" role="alert">{{ message }}</div>
{% endfor %}
</header>
</div>
{% block content %}{% endblock %}
</section>
</body>
现在,我们将快速创建其余的HTML模板,然后创建Python文件,在我们的应用程序中做真正的重活。
在我们的 "templates "目录中,再创建两个名为 "sites "和 "virtual_machines "的目录。在这两个目录中分别创建同样的三个文件。"index.html", "create.html", 和 "update.html"。
下面是要添加到每个文件的代码。
sites/index.html
{% extends "base.html" %}
{% block nav %}
<ul class="nav nav-pills">
<li class="nav-item fs-6"><a href="{{ url_for("sites.create_site") }}" class="nav-link active">Create static site</a></li>
</ul>
{% endblock %}
{% block header %}
{% block title %}Site Directory{% endblock %}
{% endblock %}
{% block content %}
<table class="table">
<tbody>
{% if not sites %}
<div class="container gy-5">
<div class="row py-4">
<div class="alert alert-secondary" role="alert">
<p>No websites are currently deployed. Create one to get started!</p>
<a href="{{ url_for("sites.create_site") }}" class="btn btn-primary">Create static site</a>
</div>
</div>
</div>
{% endif %}
{% for site in sites %}
<tr>
<td class="align-bottom" colspan="4">
<div class="p-1">
<a href="{{ site["url"] }}" class="fs-5 align-bottom" target="_blank">{{ site["name"] }}</a>
</div>
</td>
<td>
<div class="float-end p-1">
<form action="{{ url_for("sites.delete_site", id=site["name"]) }}" method="post">
<input class="btn btn-sm btn-danger" type="submit" value="Delete">
</form>
</div>
<div class="float-end p-1">
<form action="{{ url_for("sites.update_site", id=site["name"]) }}" method="get">
<input class="btn btn-sm btn-primary" type="submit" value="Edit">
</form>
</div>
<div class="float-end p-1">
<a href="{{ site["console_url"] }}" class="btn btn-sm btn-outline-primary" target="_blank">View in console</a>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endblock %}
sites/create.html
{% extends "base.html" %}
{% block nav %}
<ul class="nav nav-pills">
<li class="nav-item fs-6"><a href="{{ url_for("sites.list_sites") }}" class="nav-link">Back to site directory</a></li>
</ul>
{% endblock %}
{% block header %}
{% block title %}Create new static site{% endblock %}
{% endblock %}
{% block content %}
<section class="p-2">
<form method="post">
<div class="mb-3">
<label for="site-id" class="form-label">Name</label>
<input type="text" class="form-control" name="site-id" id="site-id" aria-describedby="nameHelp" required>
<div id="nameHelp" class="form-text">Choose a unique name as a label for your website</div>
</div>
<div class="mb-3">
<label for="file-url" class="form-label">File URL</label>
<input type="text" class="form-control" id="file-url" name="file-url">
</div>
<div class="mb-3">
<strong>OR</strong>
</div>
<div class="mb-3">
<label for="site-content" class="form-label">Content</label>
<textarea class="form-control" name="site-content" id="site-content" rows="5"></textarea>
</div>
<button type="submit" class="btn btn-primary">Create</button>
</form>
</section>
{% endblock %}
sites/update.html
{% extends "base.html" %}
{% block nav %}
<ul class="nav nav-pills">
<li class="nav-item fs-6"><a href="{{ url_for("sites.list_sites") }}" class="nav-link">Back to site directory</a></li>
</ul>
{% endblock %}
{% block header %}
{% block title %}Update site '{{ name }}'{% endblock %}
{% endblock %}
{% block content %}
<section class="p-2">
<form method="post">
<div class="mb-3">
<label for="file-url" class="form-label">File URL</label>
<input type="text" class="form-control" name="file-url" id="file-url">
</div>
<div class="mb-3">
<strong>OR</strong>
</div>
<div class="mb-3">
<label for="site-content" class="form-label">Content</label>
<textarea class="form-control" name="site-content" id="site-content" rows="5">{{ content }}</textarea>
</div>
<button type="submit" class="btn btn-primary">Update</button>
</form>
</section>
{% endblock %}
virtual_machines/index.html
{% extends "base.html" %}
{% block nav %}
<ul class="nav nav-pills">
<li class="nav-item fs-6"><a href="{{ url_for("virtual_machines.create_vm") }}" class="nav-link active">Create VM</a></li>
</ul>
{% endblock %}
{% block header %}
{% block title %}Deployed Virtual Machines{% endblock %}
{% endblock %}
{% block content %}
<table class="table">
<tbody>
{% if not vms %}
<div class="container gy-5">
<div class="row py-4">
<div class="alert alert-secondary" role="alert">
<p>No virtual machines are currently deployed. Create one to get started!</p>
<a href="{{ url_for("virtual_machines.create_vm") }}" class="btn btn-primary">Create VM</a>
</div>
</div>
</div>
{% endif %}
{% for vm in vms %}
<tr>
<td class="align-bottom" colspan="4">
<div class="p-1">
<pre> ssh -i ~/.ssh/id_rsa.pem ec2-user@{{ vm["dns_name"] }} </pre>
</div>
</td>
<td>
<div class="float-end p-1">
<form action="{{ url_for("virtual_machines.delete_vm", id=vm["name"]) }}" method="post">
<input class="btn btn-sm btn-danger" type="submit" value="Delete">
</form>
</div>
<div class="float-end p-1">
<form action="{{ url_for("virtual_machines.update_vm", id=vm["name"]) }}" method="get">
<input class="btn btn-sm btn-primary" type="submit" value="Edit">
</form>
</div>
<div class="float-end p-1">
<a href="{{ vm["console_url"] }}" class="btn btn-sm btn-outline-primary" target="_blank">View in console</a>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endblock %}
虚拟机器/创建.html
{% extends "base.html" %}
{% block nav %}
<ul class="nav nav-pills">
<li class="nav-item fs-6"><a href="{{ url_for("virtual_machines.list_vms") }}" class="nav-link">Back to Virtual Machines directory</a></li>
</ul>
{% endblock %}
{% block header %}
{% block title %}Create new virtual machine{% endblock %}
{% endblock %}
{% block content %}
<section class="p-2">
<form method="post">
<div class="mb-3">
<label for="vm-id" class="form-label">Name</label>
<input type="text" class="form-control" name="vm-id" id="vm-id" aria-describedby="nameHelp" required>
<div id="nameHelp" class="form-text">Choose a unique name as a label for your virtual machine</div>
</div>
<div class="mb-3">
<label for="vm-id" class="form-label">Instance Type</label>
<select name="instance_type" class="form-control" id="instance_type">
{% for instance_type in instance_types %}
<option value="{{ instance_type }}" {% if instance_type == curr_instance_type %} selected {% endif %}>
{{ instance_type }}
</option>
{% endfor %}
</select>
</div>
<div class="mb-3">
<label for="vm-keypair" class="form-label">Public Key</label>
<textarea class="form-control" name="vm-keypair" id="vm-keypair-content" rows="5" aria-describedby="keypairHelp"></textarea>
<div id="keypairHelp" class="form-text">The public key to use to connect to the VM </div>
</div>
<button type="submit" class="btn btn-primary">Create</button>
</form>
</section>
{% endblock %}
虚拟机器/更新.html
{% extends "base.html" %}
{% block nav %}
<ul class="nav nav-pills">
<li class="nav-item fs-6"><a href="{{ url_for("virtual_machines.list_vms") }}" class="nav-link">Back to Virtual Machines directory</a></li>
</ul>
{% endblock %}
{% block header %}
{% block title %}Update Virtual Machine '{{ name }}'{% endblock %}
{% endblock %}
{% block content %}
<section class="p-2">
<form method="post">
<div class="mb-3">
<label for="vm-id" class="form-label">Instance Type</label>
<select name="instance_type" class="form-control" id="instance_type">
{% for instance_type in instance_types %}
<option value="{{ instance_type }}" {% if instance_type == curr_instance_type %} selected {% endif %}>
{{ instance_type }}
</option>
{% endfor %}
</select>
</div>
<div class="mb-3">
<label for="vm-keypair" class="form-label">Public Key</label>
<textarea class="form-control" name="vm-keypair" id="vm-keypair-content" rows="5">{{ public_key }}</textarea>
</div>
<button type="submit" class="btn btn-primary">Update</button>
</form>
</section>
{% endblock %}
用Python创建功能
现在是使用Python创建Heroku克隆的功能的时候了。app.py "文件从我们仍然需要创建的文件中导入。在与 "app.py "相同的目录下,创建 "sites.py"。
在 "sites.py "中添加以下内容。
import json
import requests
from flask import (
current_app,
Blueprint,
request,
flash,
redirect,
url_for,
render_template,
)
import pulumi
import pulumi.automation as auto
from pulumi_aws import s3
bp = Blueprint("sites", __name__, url_prefix="/sites")
这些只是我们对Flask和Pulumi需要的基本导入,包括导入自动化框架。
接下来,在该文件中添加以下代码。这个函数根据调用者传入的内容来定义我们的Pulumi s3静态网站。这使我们能够根据用户定义的POST体的值动态地部署网站。
def create_pulumi_program(content: str):
# Create a bucket and expose a website index document
site_bucket = s3.Bucket(
"s3-website-bucket", website=s3.BucketWebsiteArgs(index_document="index.html")
)
index_content = content
# Write our index.html into the site bucket
s3.BucketObject(
"index",
bucket=site_bucket.id,
content=index_content,
key="index.html",
content_type="text/html; charset=utf-8",
)
# Set the access policy for the bucket so all objects are readable
s3.BucketPolicy(
"bucket-policy",
bucket=site_bucket.id,
policy=site_bucket.id.apply(
lambda id: json.dumps(
{
"Version": "2012-10-17",
"Statement": {
"Effect": "Allow",
"Principal": "*",
"Action": ["s3:GetObject"],
# Policy refers to bucket explicitly
"Resource": [f"arn:aws:s3:::{id}/*"],
},
}
)
),
)
# Export the website URL
pulumi.export("website_url", site_bucket.website_endpoint)
pulumi.export("website_content", index_content)
你在代码末尾看到的pulumi.export ,目的是为了导出一个命名的堆栈输出。输出的值被附加到程序的堆栈资源中。稍后你将看到我们如何访问这些被导出的数据。
到目前为止,没有任何东西在调用我们刚刚创建的函数。在这一点上,我们将创建URL端点,可以用来调用该函数并创建网站。
首先,我们将为/new URL添加代码,它将创建一个新的网站。
@bp.route("/new", methods=["GET", "POST"])
def create_site():
"""creates new sites"""
if request.method == "POST":
stack_name = request.form.get("site-id")
file_url = request.form.get("file-url")
if file_url:
site_content = requests.get(file_url).text
else:
site_content = request.form.get("site-content")
def pulumi_program():
return create_pulumi_program(str(site_content))
try:
# create a new stack, generating our pulumi program on the fly from the POST body
stack = auto.create_stack(
stack_name=str(stack_name),
project_name=current_app.config["PROJECT_NAME"],
program=pulumi_program,
)
stack.set_config("aws:region", auto.ConfigValue("us-east-1"))
# deploy the stack, tailing the logs to stdout
stack.up(on_output=print)
flash(
f"Successfully created site '{stack_name}'", category="success")
except auto.StackAlreadyExistsError:
flash(
f"Error: Site with name '{stack_name}' already exists, pick a unique name",
category="danger",
)
return redirect(url_for("sites.list_sites"))
return render_template("sites/create.html")
对于一个GET请求,这个路由渲染了 "create.html "模板。对于一个POST请求,它创建一个新的网站,并重定向到/ 路由的网站列表。
你会注意到,有一个stack 。作为提醒,每个Pulumi程序都被部署到一个堆栈。堆栈是一个孤立的、可独立配置的Pulumi程序的实例。在这段代码中,堆栈的名称是基于用户在表格中输入的ID。还有一个项目名称,以及程序,也就是我们之前讨论的代码块。
一旦堆栈被创建,我们可以对Stack ,包括更新、预览、刷新、销毁、导入和导出等命令。在这段代码中,我们使用stack.up() ,以up栈的日期。并且我们传入一个.callback函数给stack.up() ,用于标准输出。
接下来添加根URL的代码,将列出网站。
@bp.route("/", methods=["GET"])
def list_sites():
"""lists all sites"""
sites = []
org_name = current_app.config["PULUMI_ORG"]
project_name = current_app.config["PROJECT_NAME"]
try:
ws = auto.LocalWorkspace(
project_settings=auto.ProjectSettings(
name=project_name, runtime="python")
)
all_stacks = ws.list_stacks()
for stack in all_stacks:
stack = auto.select_stack(
stack_name=stack.name,
project_name=project_name,
# no-op program, just to get outputs
program=lambda: None,
)
outs = stack.outputs()
if 'website_url' in outs:
sites.append(
{
"name": stack.name,
"url": f"http://{outs['website_url'].value}",
"console_url": f"https://app.pulumi.com/{org_name}/{project_name}/{stack.name}",
}
)
except Exception as exn:
flash(str(exn), category="danger")
return render_template("sites/index.html", sites=sites)
一个Workspace 是包含一个Pulumi项目、一个程序和多个堆栈的执行环境。所以我们首先获得访问ws = auto.LocalWorkspace() 。
然后我们list_stacks() 。这只是让我们访问堆栈的名称,所以我们必须使用auto.select_stack() ,以获得对堆栈的访问。我们特别想要stack.outputs() 。这将是我们输出到堆栈的内容,包括website_url 。
然后,我们将每个网站的信息附加到sites 列表中,该列表在前台用于显示网站列表。
现在,添加/update路线的代码。
@bp.route("/<string:id>/update", methods=["GET", "POST"])
def update_site(id: str):
stack_name = id
if request.method == "POST":
file_url = request.form.get("file-url")
if file_url:
site_content = requests.get(file_url).text
else:
site_content = str(request.form.get("site-content"))
try:
def pulumi_program():
create_pulumi_program(str(site_content))
stack = auto.select_stack(
stack_name=stack_name,
project_name=current_app.config["PROJECT_NAME"],
program=pulumi_program,
)
stack.set_config("aws:region", auto.ConfigValue("us-east-1"))
# deploy the stack, tailing the logs to stdout
stack.up(on_output=print)
flash(f"Site '{stack_name}' successfully updated!",
category="success")
except auto.ConcurrentUpdateError:
flash(
f"Error: site '{stack_name}' already has an update in progress",
category="danger",
)
except Exception as exn:
flash(str(exn), category="danger")
return redirect(url_for("sites.list_sites"))
stack = auto.select_stack(
stack_name=stack_name,
project_name=current_app.config["PROJECT_NAME"],
# noop just to get the outputs
program=lambda: None,
)
outs = stack.outputs()
content_output = outs.get("website_content")
content = content_output.value if content_output else None
return render_template("sites/update.html", name=stack_name, content=content)
你会注意到,/update路由与/new路由非常相似。因为它使用相同的stack_name ,所以不会创建一个新的网站。
这个函数中的一个新东西是,代码得到了网站的内容,以返回到前台模板中。
最后,为一个/delete路由添加以下代码。
@bp.route("/<string:id>/delete", methods=["POST"])
def delete_site(id: str):
stack_name = id
try:
stack = auto.select_stack(
stack_name=stack_name,
project_name=current_app.config["PROJECT_NAME"],
# noop program for destroy
program=lambda: None,
)
stack.destroy(on_output=print)
stack.workspace.remove_stack(stack_name)
flash(f"Site '{stack_name}' successfully deleted!", category="success")
except auto.ConcurrentUpdateError:
flash(
f"Error: Site '{stack_name}' already has update in progress",
category="danger",
)
except Exception as exn:
flash(str(exn), category="danger")
return redirect(url_for("sites.list_sites"))
在这个删除函数中,我们首先获得对堆栈的访问权,然后销毁并删除堆栈。仅仅通过调用stack.destroy() 函数就可以删除AWS上的资源。
现在,在与 "app.py "相同的目录下,创建 "virtual_machines.py"。
在 "virtual_machines.py "中添加以下内容。这一次,我们将一次性地添加它。
from flask import (Blueprint, current_app, request, flash,
redirect, url_for, render_template)
import pulumi
import pulumi_aws as aws
import pulumi.automation as auto
import os
from pathlib import Path
bp = Blueprint("virtual_machines", __name__, url_prefix="/vms")
instance_types = ['c5.xlarge', 'p2.xlarge', 'p3.2xlarge']
def create_pulumi_program(keydata: str, instance_type=str):
# Choose the latest minimal amzn2 Linux AMI.
# TODO: Make this something the user can choose.
ami = aws.ec2.get_ami(most_recent=True,
owners=["amazon"],
filters=[aws.GetAmiFilterArgs(name="name", values=["*amzn2-ami-minimal-hvm*"])])
group = aws.ec2.SecurityGroup('web-secgrp',
description='Enable SSH access',
ingress=[aws.ec2.SecurityGroupIngressArgs(
protocol='tcp',
from_port=22,
to_port=22,
cidr_blocks=['0.0.0.0/0'],
)])
public_key = keydata
if public_key is None or public_key == "":
home = str(Path.home())
f = open(os.path.join(home, '.ssh/id_rsa.pub'), 'r')
public_key = f.read()
f.close()
public_key = public_key.strip()
print(f"Public Key: '{public_key}'\n")
keypair = aws.ec2.KeyPair("dlami-keypair", public_key=public_key)
server = aws.ec2.Instance('dlami-server',
instance_type=instance_type,
vpc_security_group_ids=[group.id],
key_name=keypair.id,
ami=ami.id)
pulumi.export('instance_type', server.instance_type)
pulumi.export('public_key', keypair.public_key)
pulumi.export('public_ip', server.public_ip)
pulumi.export('public_dns', server.public_dns)
@bp.route("/new", methods=["GET", "POST"])
def create_vm():
"""creates new VM"""
if request.method == "POST":
stack_name = request.form.get("vm-id")
keydata = request.form.get("vm-keypair")
instance_type = request.form.get("instance_type")
def pulumi_program():
return create_pulumi_program(keydata, instance_type)
try:
# create a new stack, generating our pulumi program on the fly from the POST body
stack = auto.create_stack(
stack_name=str(stack_name),
project_name=current_app.config["PROJECT_NAME"],
program=pulumi_program,
)
stack.set_config("aws:region", auto.ConfigValue("us-east-1"))
# deploy the stack, tailing the logs to stdout
stack.up(on_output=print)
flash(
f"Successfully created VM '{stack_name}'", category="success")
except auto.StackAlreadyExistsError:
flash(
f"Error: VM with name '{stack_name}' already exists, pick a unique name",
category="danger",
)
return redirect(url_for("virtual_machines.list_vms"))
current_app.logger.info(f"Instance types: {instance_types}")
return render_template("virtual_machines/create.html", instance_types=instance_types, curr_instance_type=None)
@bp.route("/", methods=["GET"])
def list_vms():
"""lists all vms"""
vms = []
org_name = current_app.config["PULUMI_ORG"]
project_name = current_app.config["PROJECT_NAME"]
try:
ws = auto.LocalWorkspace(
project_settings=auto.ProjectSettings(
name=project_name, runtime="python")
)
all_stacks = ws.list_stacks()
for stack in all_stacks:
stack = auto.select_stack(
stack_name=stack.name,
project_name=project_name,
# no-op program, just to get outputs
program=lambda: None,
)
outs = stack.outputs()
if 'public_dns' in outs:
vms.append(
{
"name": stack.name,
"dns_name": f"{outs['public_dns'].value}",
"console_url": f"https://app.pulumi.com/{org_name}/{project_name}/{stack.name}",
}
)
except Exception as exn:
flash(str(exn), category="danger")
current_app.logger.info(f"VMS: {vms}")
return render_template("virtual_machines/index.html", vms=vms)
@bp.route("/<string:id>/update", methods=["GET", "POST"])
def update_vm(id: str):
stack_name = id
if request.method == "POST":
current_app.logger.info(
f"Updating VM: {stack_name}, form data: {request.form}")
keydata = request.form.get("vm-keypair")
current_app.logger.info(f"updating keydata: {keydata}")
instance_type = request.form.get("instance_type")
def pulumi_program():
return create_pulumi_program(keydata, instance_type)
try:
stack = auto.select_stack(
stack_name=stack_name,
project_name=current_app.config["PROJECT_NAME"],
program=pulumi_program,
)
stack.set_config("aws:region", auto.ConfigValue("us-east-1"))
# deploy the stack, tailing the logs to stdout
stack.up(on_output=print)
flash(f"VM '{stack_name}' successfully updated!",
category="success")
except auto.ConcurrentUpdateError:
flash(
f"Error: VM '{stack_name}' already has an update in progress",
category="danger",
)
except Exception as exn:
flash(str(exn), category="danger")
return redirect(url_for("virtual_machines.list_vms"))
stack = auto.select_stack(
stack_name=stack_name,
project_name=current_app.config["PROJECT_NAME"],
# noop just to get the outputs
program=lambda: None,
)
outs = stack.outputs()
public_key = outs.get("public_key")
pk = public_key.value if public_key else None
instance_type = outs.get("instance_type")
return render_template("virtual_machines/update.html", name=stack_name, public_key=pk, instance_types=instance_types, curr_instance_type=instance_type.value)
@bp.route("/<string:id>/delete", methods=["POST"])
def delete_vm(id: str):
stack_name = id
try:
stack = auto.select_stack(
stack_name=stack_name,
project_name=current_app.config["PROJECT_NAME"],
# noop program for destroy
program=lambda: None,
)
stack.destroy(on_output=print)
stack.workspace.remove_stack(stack_name)
flash(f"VM '{stack_name}' successfully deleted!", category="success")
except auto.ConcurrentUpdateError:
flash(
f"Error: VM '{stack_name}' already has update in progress",
category="danger",
)
except Exception as exn:
flash(str(exn), category="danger")
return redirect(url_for("virtual_machines.list_vms"))
这个文件和 "sites.py "文件之间有很多相似之处。一个虚拟机还有相当多的设置必须设置,Pulumi能够创建我们想要的确切类型的虚拟机。
我们可以让用户在网络界面上定制一切,但我们只让用户选择实例类型。
一个虚拟机需要的东西是一个公钥/私钥对。在AWS上创建一个虚拟机时,你必须给出一个公钥。然后,为了访问该虚拟机,你必须拥有私钥。
你可以在你的终端中创建密钥。运行以下命令。
ssh-keygen -m PEM
以后在测试应用程序时,你必须打开公钥文件,这样你就可以复制密钥并将其粘贴到网站上。
现在用你刚刚创建的文件切换目录。无论你是在MacOS还是在Windows上,这个目录都应该是一样的。
在你的终端上输入。
cd /Users/[username]/.ssh
AWS需要该文件是.pem格式的,这就是为什么我们在上面用 "PEM "创建它。现在让我们重命名该文件,使其具有正确的扩展名。你需要将名为id_rsa 的文件的名称改为id_rsa.pem 。
在macOS上,你可以用这个命令重命名。
mv id_rsa id_rsa.pem
在Windows上,使用。
rename id_rsa id_rsa.pem
当运行Flask应用程序时,你可能需要输入你刚刚创建的公钥。你可以在任何文本编辑器中打开id_rsa.pub ,以便复制文本。如果你有vim,你可以用这个命令来打开文件。
vim /Users/beau/.ssh/id_rsa.pub
测试应用程序
现在是时候测试一下这个应用程序了。在终端上,运行这个命令。
FLASK_RUN_PORT=1337 FLASK_ENV=development FLASK_APP=__init__ PULUMI_ORG=[your-org-name] venv/bin/flask run
现在你可以试用该应用程序并创建网站和虚拟机。确保在创建后删除它们,这样AWS就不会继续向你收取资源费用。
总结
你现在应该知道足够的知识,开始使用Pulumi的自动化API在你自己的应用程序中配置基础设施。如果你想逐步了解如何做本文中的事情,请查看视频教程。