原文:
towardsdatascience.com/easy-cdk-deep-learning-ec2-instances-f30fbeec1ea3
·发表于数据科学前沿 ·阅读时间 10 分钟·2023 年 1 月 18 日
--
今天我们要构建的内容的大致概述。
作为数据科学家,我经常需要在云中运行 GPU 作业,而我对点击 UI 的操作深恶痛绝。过去,当我需要一个 GPU 实例进行单次模型训练时,我不得不费力地在 AWS 的 UI 中进行导航。但现在不再是这样!现在,创建一个了解如何安装和实例化所需所有内容的 EC2 堆栈非常简单。在本教程中,我们将使用 AWS CDK 创建一个 EC2 实例。我们会将这个 EC2 实例保护在 VPC 的私有子网中,仅允许入站流量。然后我们将使用 AWS 安全会话管理器(SSM)连接到这个实例。这很酷,因为过去如果你想让你的机器处于一个完全私密的网络中,你需要一个堡垒主机,而有了 SSM,这不再是必需的!本教程的所有代码可以在这里找到。完成这些步骤后,你将能够运行几个命令,拥有一个完全功能的数据科学准备好的安全 EC2 实例,并配备 JupyterLab 接口。
本教程的所有代码可以在这里找到。
CDK 是 AWS 创建的云开发工具包。它允许我们使用 Python 或 TypeScript 等语言在云中创建资源。使用起来非常简单,而且免费,不像Pulumi(我在生产环境中使用的服务)。然而,CDK 只是 CloudFormation 的一个封装,因此你会继承这个系统的所有限制。
如果你对了解 CDK 多一些感兴趣,我建议你查看这个 教程。它会指导你完成设置 CDK 和创建一个非常简单的 API 所需的一切。如果你还没有读过,这值得一读。我假设你已经完成了该教程的 New Project
部分,并在本地机器上设置了 CDK。从现在开始,我们将重点关注创建 EC2 实例。
创建一个项目文件夹
mkdir ds-ec2 && cd ds-ec2
进入新文件夹后,我们可以实例化我们的项目。
cdk init --language=python
这将创建一堆文件,但实际上你只需要关注两个文件,即 app.py 和 ds_ec2_stack.py。
app.py 定义了 CDK 应用程序,并且是部署的入口点。如果你想部署多个互联的堆栈,这就是需要编辑的地方。不过,我们这里不会做太多的操作。
ds_ec2_stack.py 是我们定义堆栈和资源的地方。这是整个系统的工作马,所以让我们开始编辑这个文件并添加我们想要的内容!
激活你的环境并安装 CDK 库。
source .venv/bin/activate
pip install -r requirements.txt
如果你不在乎 CDK 的细节和发生了什么,可以跳到部署堆栈部分并直接克隆仓库。
否则,下面的代码需要:
我们为所有资源创建一个 VPC。
# Create a VPC to control the network our instance lives on.
vpc = ec2.Vpc(self, "ds-vpc", cidr="10.0.0.0/16")
我们为这个实例创建一个 IAM 角色,这使我们能够添加一个托管策略,赋予实例通过 SSM 连接的能力。
# Create a session manager role so we can connect without SSH.
role = iam.Role(
self,
"ds-ec2-role",
assumed_by=iam.ServicePrincipal("ec2.amazonaws.com"),
role_name="ds-ec2-role",
)
# Provide access to SSM for secure communication with the instance.
role.add_managed_policy(
iam.ManagedPolicy.from_aws_managed_policy_name(
"AmazonSSMManagedInstanceCore",
)
)
我们希望我们的实例位于私有子网中,并只允许入站流量。这有助于安全性,因为实例中的任何内容都不能离开我们的网络。
# Create a security group that only allows inbound traffic.
security_group = ec2.SecurityGroup(
self,
"ds-security-group",
vpc=vpc,
allow_all_outbound=True,
security_group_name="ds-security-group",
)
使用 CDK 的一个优点是我们可以设置有关安装 Python 依赖项的自定义逻辑。我已经设置了安装我使用的许多常用库,因此一旦我连接到实例,它就可以立即使用。我们可以使用 CDK 来做到这一点,通过 ec2.MultipartUserData()。这只是一个传递用户数据到实例的高级方式,以便它为你运行一些配置。
# Create initializatoin commands for non GPU instances
multipart_user_data = ec2.MultipartUserData()
python_other_pkgs = []
env_activate_cmd = "python3.8 -m "
install_python = ec2.UserData.for_linux()
# Install python3.8 on the instance
install_python.add_commands("sudo yum update & sudo amazon-linux-extras install -y python3.8 ")
# Activate python3.8 and install the CPU version of torch.
install_python.add_commands(
f"{env_activate_cmd} pip install torch --extra-index-url https://download.pytorch.org/whl/cpu" # noqa: E501
)
# Add the commands to the multipart user data and execute them.
multipart_user_data.add_part(ec2.MultipartBody.from_user_data(install_python))
在项目中创建一个 requirements.txt
文件,位于 app.py 旁边。对我来说,我的文件如下:
matplotlib
seaborn
numpy
datasets
pandas
scikit-learn
boto3
nltk
transformers
spacy
jupyter
jupyterlab
然后加载 requirements.txt
文件,以便我们知道要安装哪些软件包。
# Create a list of all requirements we want installed on our instance.
with open("requirements.txt", "r") as f:
python_pkgs = [x.strip() for x in f.readlines()]
# Install python dependencies.
pkgs_to_install = " ".join(python_pkgs)
install_dependencies = ec2.UserData.for_linux()
install_dependencies.add_commands(f"{env_activate_cmd} pip install {pkgs_to_install}")
multipart_user_data.add_part(ec2.MultipartBody.from_user_data(install_dependencies))
EC2 实例上的默认磁盘空间有点小,通常我希望在实例上加载几 GB 的数据来进行工作。这会附加一个 EBS 卷以提供额外的磁盘空间。
# Increase the disk space on the device
root_volume = ec2.BlockDevice(device_name="/dev/xvda", volume=ec2.BlockDeviceVolume.ebs(60))
如果我们运行的是 CPU 实例,我们可以使用 AmazonLinux
# Create a generic machine image for use with CPU.
image = ec2.MachineImage.latest_amazon_linux(generation=ec2.AmazonLinuxGeneration.AMAZON_LINUX_2)
如果我们运行 GPU,我们需要使用 generic_linux MachineImage 并传递深度学习实例的 AMI。例如,如果在 us-west-2 部署,我们可以使用 ami-0a83cbb39db39cb3e。
# Create a Machine Image with the specified AMI.
image = ec2.MachineImage.generic_linux({region: os.getenv("AWS_AMI")})
最后一步是使用我们到目前为止创建的所有内容实例化 ec2 实例。
ec2.Instance(
self,
"ds-instance",
role=role,
instance_type=ec2.InstanceType(instance_type),
machine_image=image,
vpc=vpc,
security_group=security_group,
user_data=multipart_user_data,
block_devices=[root_volume],
)
我使用的最终堆栈在这里。你会注意到有一些额外的逻辑。这个逻辑检查所需的实例是 GPU 还是 CPU,然后选择正确的堆栈组件。
from aws_cdk import Stack, aws_ec2 as ec2
from aws_cdk import aws_iam as iam
from constructs import Construct
import os
from ds_ec2.utils import (
get_instance_type,
get_machine_image,
is_gpu,
)
def is_gpu(instance_type: str) -> bool:
"""Check to see if an instance type is gpu enabled."""
return "p" in instance_type or "g5" in instance_type or "g4" in instance_type
class DsEc2Stack(Stack):
def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None:
"""
Create a single EC2 instance with the libraries need to do data science work.
This ec2 instance understands whether it has a gpu or not and installs the
correct libraries. If not instance_type is provided defaults to c4.2xlarge.
"""
super().__init__(scope, construct_id, **kwargs)
# Get the current region to deploy to
region = os.getenv("CDK_DEFAULT_REGION")
# Get the instance type from the environment. If none then defaults c2.4xlarge.
if "INSTANCE_TYPE" in os.environ:
instance_type = os.getenv("INSTANCE_TYPE")
else:
instance_type = "c4.2xlarge"
# Create a VPC to control the network our instance lives on.
vpc = ec2.Vpc(self, "ds-vpc", cidr="10.0.0.0/16")
# Create a session manager role so we can connect without SSH.
role = iam.Role(
self,
"ds-ec2-role",
assumed_by=iam.ServicePrincipal("ec2.amazonaws.com"),
role_name="ds-ec2-role",
)
# Provide access to SSM for secure communication with the instance.
role.add_managed_policy(
iam.ManagedPolicy.from_aws_managed_policy_name(
"AmazonSSMManagedInstanceCore",
)
)
# Create a security group that only allows inbound traffic.
security_group = ec2.SecurityGroup(
self,
"ds-security-group",
vpc=vpc,
allow_all_outbound=True,
security_group_name="ds-security-group",
)
# Create initializatoin commands for non GPU instances
multipart_user_data = ec2.MultipartUserData()
# Create a list of all requirements we want installed on our instance.
with open("requirements.txt", "r") as f:
python_pkgs = [x.strip() for x in f.readlines()]
# Check if the instance is a GPU if it isn't we want to install python
# and install the cpu version of pytorch. Otherwise we want to activate
# the GPU enabled version of pytorch in the AMI.
if not is_gpu(instance_type):
python_other_pkgs = []
env_activate_cmd = "python3.8 -m "
install_python = ec2.UserData.for_linux()
install_python.add_commands("sudo yum update & sudo amazon-linux-extras install -y python3.8 ")
install_python.add_commands(
f"{env_activate_cmd} pip install torch --extra-index-url https://download.pytorch.org/whl/cpu" # noqa: E501
)
# Install python.
multipart_user_data.add_part(ec2.MultipartBody.from_user_data(install_python))
# Increase the disk space on the device.
root_volume = ec2.BlockDevice(device_name="/dev/xvda", volume=ec2.BlockDeviceVolume.ebs(25))
# Create a generic machine image for use with CPU.
image = ec2.MachineImage.latest_amazon_linux(generation=ec2.AmazonLinuxGeneration.AMAZON_LINUX_2)
else:
python_other_pkgs = []
# The deep learning AMI's have python installed we need to activate it.
env_activate_cmd = "source activate pytorch; "
# Increase the disk space on the device
root_volume = ec2.BlockDevice(device_name="/dev/xvda", volume=ec2.BlockDeviceVolume.ebs(60))
# Create a Machine Image with the specified AMI.
image = ec2.MachineImage.generic_linux({region: os.getenv("AWS_AMI")})
# Install python dependencies.
pkgs_to_install = " ".join(python_pkgs + python_other_pkgs)
install_dependencies = ec2.UserData.for_linux()
install_dependencies.add_commands(f"{env_activate_cmd} pip install {pkgs_to_install}")
multipart_user_data.add_part(ec2.MultipartBody.from_user_data(install_dependencies))
# Create an EC2 instance ami-01dafd60663418a6f 1.12.0
image = get_machine_image(instance_type, region)
ec2.Instance(
self,
"ds-instance",
role=role,
instance_type=ec2.InstanceType(instance_type),
machine_image=image,
vpc=vpc,
security_group=security_group,
user_data=multipart_user_data,
block_devices=[root_volume],
)
要部署你的堆栈运行,你需要设置两个环境变量。INSTANCE_TYPE
和 AWS_AMI
,如果你使用 GPU 的话。如果你想使用默认设置,你不需要设置这两个变量。作为 GPU 实例部署的示例,请运行:
export INSTANCE_TYPE="instance-type"; export AWS_AMI="aws_ami_id"; cdk deploy
这将通过 CloudFormation 创建我们迄今为止指定的所有资源。一旦完成,你可以登录到控制台查看你的实例。
从第二列中获取实例 ID,你稍后需要用它来通过 SSM 连接到实例。
SSM 允许我们使用 AWS IAM 角色来管理对实例的访问。这很不错,因为我们不必打开任何 ssh 端口,可以将实例完全放置在私有子网中,这在安全性方面非常好。为了使 SSM 能够工作,我们需要用以下行修改我们的 .ssh/config
文件:
# SSH over Session Manager
host i-* mi-*
ProxyCommand sh -c "aws ssm start-session --target %h --document-name AWS-StartSSHSession --parameters 'portNumber=%p'"
不用太担心这个复杂的命令。它基本上允许 SSM 使用安全外壳 (SSH) 连接到远程实例,而无需打开入站端口。现在要连接到实例,我们可以运行:
aws ssm start-session --target instance_id --region=our_region
如果你想避免使用 UI,你可以使用以下命令从 AWS 获取所有 EC2 实例 ID。
aws ec2 describe-instances --region=your-region --query "Reservations[].Instances[?State.Name == 'running'].InstanceId[]" --output text
JupyterLab 提供了一个相当不错的界面,用于在这些远程服务器上工作。我喜欢设置一个实验室会话,然后通过 SSM 连接到它。在你的远程实例上创建一个屏幕来保持 jupyter 会话,以便以后易于查找和管理。
screen -S jupyter
确保你在一个具有读写权限的文件夹中,默认情况下你的 EC2 实例会把你放在 /usr/bin
。如果你运行以下命令,这将把你放到你的用户文件夹中,该文件夹应该具有 Jupyter 的读写权限。
cd /home/ssm-user
现在在你的屏幕内创建 JupyterLab 实例。此命令将在端口 8123 上创建一个 JupyterLab 会话。我们稍后将使用此端口通过 SSM 连接。
jupyter-lab --no-browser --ip=0.0.0.0 --port=8123
现在退出这个连接(ctrl + d 应该有效),在你的本地机器上运行:
aws ssm start-session --target instance_id --region out_region --document-name AWS-StartPortForwardingSession --parameters '{"portNumber":["8123"],"localPortNumber":["8123"]}'
确保此命令中的端口与之前的端口匹配。这将通过 SSM 打开一个通道到你的实例,并转发所有流量到端口 8123. 现在,只需在浏览器中访问 localhost:8123 即可,瞧! 你已连接到你的远程实例,并可以在其中进行所有有趣的 jupyterlab 操作。
连接到实例时你应该看到的内容。
一旦你完成所有操作并且想要销毁你的实例,只需运行 cdk destroy
。此命令将清理你在该堆栈中部署的所有内容。
希望这能帮助你轻松地开始和管理 EC2 实例。我发现这是一种非常好的与 EC2 实例交互的方式。你需要记住一些事情。这个脚本创建了一个 VPC。通常,AWS 限制了你在一个区域中可以拥有的自定义 VPC 的数量为 5,因此,如果你想创建多个实例,为每个实例生成一个 VPC 是相当低效的(我的意思是不要这么做……)。这里的目标是个人开发,而不是一个大型共享团队。如果你希望我写另一篇关于创建共享 VPC 的文章,请在评论中告诉我。
下一步,尝试从 AWS 自动获取 AMI,而不是硬编码。尝试使用 CfnParameters 而不是环境变量来指定实例类型。尝试创建一个共享 VPC 实例。所有这些都是改善上述代码的好项目。祝构建愉快!