AWS Cloud Development Kit(CDK)(Python)でVPCエンドポイント+NAT構成+EC2 AutoRecoveryを作る

2020.08.25

どーもsutoです。

前回は3部構成でAWS Cloud Development Kit(CDK)を使ってPythonコードVPC+ノートブック構築をやってみました。

作成したノートブックは直接インターネット接続可能な状態で構築しましたが、今回はそれをプライベート通信Onlyの環境で作ってみようと思います。ついでにNATインスタンスにAuto Recovery設定の追加やっちゃいます。

コードは前回記事でデプロイした構成を編集するかたちで作成していきます。前回までのコードの内容は以下をご参照ください。

はじめに

今回やること

  1. ノートブック〜S3バケットの通信をVPCエンドポイント経由にする
  2. ノートブック〜外部通信(インターネット)をNATインスタンス経由にする
  3. NATインスタンスCloudwatch「Auto Recovery」を設定

デプロイ前の注意点

既存でデプロイしているプロジェクトを更新する場合は、サブネットのCIDRが競合してデプロイ時にエラーとなる場合がありますので、その際は、一旦destroyして再度作り直す必要があります。そうなると各インスタンスも一度Terminatedされて再作成となるので必ずリソースのバックアップを取っておきましょう。

CDK環境にSagemakerモジュール追加

前回作成したsage-nwプロジェクトの仮想環境ディレクトリ内に入っている状態からスタートします。

~ sage-nw % source .env/bin/activate
(.env) ~ sage-nw %

setup.pyの「install_requires」部分を編集して必要なモジュールをインストールします。

    install_requires=[
        "aws-cdk.core",
        "aws-cdk.aws-ec2",
        "aws-cdk.aws-sagemaker",
        "aws_cdk.aws_lambda",
        "aws_cdk.aws_events",
        "aws_cdk.aws_events_targets",
        "aws_cdk.aws_cloudwatch",
        "boto3",
        ],
pip install -r requirements.txt

コードを記述

「sage_nw」フォルダに移動してsage_nw_stack.pyを書き換えていきます。

コードの主な変更点は、

  • ec2.VpcでサブネットタイプをISOLATEDPRIVATEに変更、NATとエンドポイント の指定
  • NATインスタンスタイプを定義
  • Cloudwatch設定を追加

書き換え後のsage_nw_stack.pyは以下となります。

from aws_cdk import (
    core,
    aws_ec2 as ec2,
    aws_cloudwatch as cw
    )
from sagemaker_stack import SagemakerStack
from lambda_stack import LambdaStack

class SageNwStack(core.Stack):

    def __init__(self, scope: core.Construct, id: str, **kwargs) -> None:
        super().__init__(scope, id, **kwargs)
    
        # The code that defines your stack goes here
        # 変数の宣言
        vpc_cidr = '10.1.0.0/16'
        subnet_mask = 24

        # 新規VPC作成(NAT構成)
        natinstance_type = ec2.InstanceType('t3.micro')
        nat_instance = ec2.NatProvider.instance(instance_type=natinstance_type)
        vpc = ec2.Vpc(
            self,
            id="Sage-vpc",
            cidr=vpc_cidr,
            nat_gateways=1,
            max_azs=2,
            subnet_configuration=[
                ec2.SubnetConfiguration(
                    cidr_mask=subnet_mask, name="public", subnet_type=ec2.SubnetType.PUBLIC,
                ),
                ec2.SubnetConfiguration(
                    cidr_mask=subnet_mask, name="private", subnet_type=ec2.SubnetType.PRIVATE,
                ),
            ],
            gateway_endpoints={
                "S3": ec2.GatewayVpcEndpointOptions(
                    service=ec2.GatewayVpcEndpointAwsService.S3,
                    subnets=[{"subnet_type": ec2.SubnetType.PRIVATE}]
                ),
            },
            nat_gateway_provider=nat_instance,
        )

        security_group = ec2.SecurityGroup(
            self,
            id="Sage-sg",
            vpc=vpc,
            security_group_name="test-sg"
        )
        security_group.add_ingress_rule(
            peer=ec2.Peer.ipv4(vpc_cidr),
            connection=ec2.Port.all_traffic(),
        )

        subnet = vpc.private_subnets[0].subnet_id
        sg = [security_group.security_group_id]

        # ノートブックインスタンス作成
        sagemaker = SagemakerStack(
            self,
            "Notebook",
            subnetid = subnet,
            sgid = sg,
        )
        # Lambda作成
        lambda_autostop = LambdaStack(
            self,
            "AutoStop",
        )

        # NATインスタンスIDを取得
        natinstance_id = vpc.public_subnets[0].node.find_child('NatInstance').instance_id
        # Cloudwatch作成
        alarm = cw.CfnAlarm(
            self,
            id='EC2AutoRecovery',
            comparison_operator='GreaterThanThreshold',
            threshold=0,
            period=60,
            evaluation_periods = 5,
            statistic='Minimum',
            alarm_name='nat-autorecovery',
            namespace='AWS/EC2',
            metric_name='StatusCheckFailed_System',
            alarm_actions=["arn:aws:automate:ap-northeast-1:ec2:recover"],
            dimensions=[
                {
                    "name": 'InstanceId',
                    "value": natinstance_id
                }
            ],
        )

ec2.NatProviderの宣言だけで、AWSが提供する最新のNATインスタンス用AMIを指定できるのは便利ですね。そこからNATインスタンスのIDを取得する必要があるので、.find_childを使ってサブネット内の対象インスタンスを検索しています。

※余談ですが前回記事ではVPCの最大AZ数を指定していなかったので、今回のコードでは”2”を指定しています。

ノートブックインスタンスの通信もVPCエンドポイント およびNATインスタンス経由とするため、sagemaker_stack.pyで直接インターネット接続の設定をDisabledに変更します。

internet_access = 'Disabled'

また、今回のようにCDKからAMIイメージを取得する場合、スタックレベルでアカウント/リージョンを明示的に指定する必要がありますので、app.pyを以下のように書き換えます。

#!/usr/bin/env python3
import os
from aws_cdk import core

from sage_nw.sage_nw_stack import SageNwStack

app = core.App()
SageNwStack(app, "sage-nw", env=core.Environment(
    account=os.environ["CDK_DEFAULT_ACCOUNT"],
    region=os.environ["CDK_DEFAULT_REGION"]))

app.synth()

これでAWS CLI(.aws/config)に保存しているデフォルトの認証情報を取得できます。スイッチロール先のアカウントにデプロイさせたい場合は、cdkコマンド実行時に--profileオプションで指定すればOKです。

スタックの確認とデプロイ

cdk synthcdk diffコマンドでデプロイする内容を表示して確認します。

cdk synth
cdk diff sage-nw --profile suto

確認が終わったら以下コマンドでスタックを更新してみます。

(.env) ~ sage-nw % cdk deploy sage-nw --profile suto

スタックが更新されてリソースが作成されていることを確認できました。

まとめ

今回はNATインスタンス、VPCエンドポイント 、CloudWatch設定を構築するコードを書きました。CloudWatchではAlarmMetricsを使った記述もできますが、アラームアクションの作り込みがちょっと難しかったのでCfnAlarmを使いました。Cfnシリーズは最低限記述するコードが多少長くなるデメリットがある一方で、CloudFormationの定義と比較ができてわかりやすいというメリットがあります。

これまで使ってきて、AWS CDKはCLIでCloudFormationのコマンドをたたくよりもスタックの管理がラクだと感じているためどんどん使っていきたいと思います。