AWS CDK(Python)だけでWordPressサイトをデプロイする(ELB,EC2,EFS,RDS)

もくじ

はじめに

#2022/9/23一部修正しました。
#User DataをStackにベタ書きしていましたが、Shell(.sh)ファイルにして読み込むようにしました。

やること

こんにちは。あつしです。最近はIaC最高!って感じです。

今回はAWS CDKを使って、ほぼ一発でWordPressサイトを構築してみます。

概要

パブリックサブネットにALBを配置してユーザーからのリクエストを受け付けます。

その後ろにプライベートサブネット(NATでインターネット接続可能)を作成し、EC2とEFSを配置します。EFSにWordPressモジュールを配置し、EC2からマウントして使用します。これによりEC2をステートレスにすることができ、Auto Scalingで負荷に応じてスケールアウトすることができるようにします。

Apache、PHPなどのインストールと、WordPressモジュールのダウンロードはすべてEC2のユーザーデータで行います(かなり強引ですが)。インストールが完了したら、そのEC2からカスタムAMIを作成します。最終的にはカスタムAMIからEC2が起動するように設定します。

プライベートサブネット(インターネット接続不可)も作成し、こちらにはRDS MySQLを配置します。AZ障害に備えてマルチAZ構成とします。

細かい設計は後述します。

ゴール

ゴールは以下のことが達成されることです。

  1. ELBのDNS名でアクセスしてWordPressサイトが表示されること。
  2. WordPressの管理画面にログインできること。
  3. カスタムAMIからEC2が起動してマルチAZ構成となっていること。
  • ボリュームが多いため、記事を分割してお送りします。
  • 本ページでは構成図と設計をご紹介します。

設計

本件の設計をご説明します。

構成図

構成図を示します。完成形は以下のようになっています。

  • EC2を配置しているプライベートサブネットは「プライベートサブネット(APP)」と記載。
  • RDSを配置しているプライベートサブネットは「プライベートサブネット(DB)」と記載。

AWSリソース

使用するAWSリソースと設計の概要を説明します。

AWSリソース説明
VPCCIDRは10.0.0.0/16。サブネットは/24で分割。
AZは2つ。
●パブリックサブネット✕2
●プライベートサブネット(NATでインターネット接続可能)✕2
 →「プライベートサブネット(APP)」と記載します。
●プライベートサブネット(インターネット接続不可)✕2
 →「プライベートサブネット(DB)」と記載します。
Nat Gateway1つ作成(普段は使用しないため)。EC2がインターネットへ接続するときに使用する。
WordPressサーバーが完成した後は削除する。WordPressやプラグインをアップデートするときは都度作成する。
セキュリティグループ以下の5つを作成。
●ELB用
●EC2用
●EFS用
●RDS用
●セッションマネージャーエンドポイント用
Systems ManagerセッションマネージャーでEC2に接続するため作成。EC2にポート22は開けない。
エンドポイントセッションマネージャーでEC2に接続するため以下を作成。
●ec2messages.[region].amazonaws.com
●ssm.[region].amazonaws.com
●ssmmessages.[region].amazonaws.com
IAMロールEC2に付与するロールを作成。以下のポリシーをアタッチ。
●AmazonSSMManagedInstanceCore
 →セッションマネージャー使用のため。
●AmazonElasticFileSystemReadOnlyAccess
 →EFSのファイルシステムID取得のため(詳細は次回の記事)
●SecretsManagerReadWrite
 →Secrets ManagerからRDS認証情報を取得するため(詳細は次回の記事)
●ElasticLoadBalancingReadOnly
 →ELBのDNS名取得のため(詳細は次回の記事)
EFSWordPressのモジュールを保存。EC2からマウントして使用。これにより、EC2をステートレスな状態にする。
※プライベートサブネット(APP)に配置。インターネットから接続不可。
RDSMySQL 5.7.37を使用。
AZ障害に備えて、マルチAZの高可用性な設計とする。
※プライベートサブネット(DB)に配置。インターネットから接続不可。
Secrets ManagerRDSの認証情報を保存。
EC2起動テンプレートから起動する。
Auto Scalingグループに組み込みマルチAZ構成とする。負荷に応じてスケールアウトする。
※プライベートサブネット(APP)に配置。WordPressのアップデートなど必要なときだけNat Gatewayを通してインターネットに接続する。
ELBALBを使用。
HTTPでユーザーからのリクエストを受け付ける。(証明書は使用しません)

CDKバージョン

CDKはPythonを使用します。CDKとPythonのバージョンは以下のようになっています。

% python --version
Python 3.9.13 
% cdk --version   
2.28.1 (build d035432)

AWS CDKコード(Network、Storage)

AWS CDKでコーディングしていきます。

コーディング規約(変数名など)

簡単ですが、変数名とAWSリソースに付与する名前について、本件のコーディング規約を記載します。

  • 変数名
    wp_[リソース名](_[リソース名])
    ※例1)VPC:wp_vpc
    ※例2)EC2セキュリティグループ:wp_sg_ec2
     
  • AWSリソース名
    wp-dev-[リソース名](_[リソース名]_[リソース名])
    ※例1)VPC:wp-dev-vpc
    ※例2)EC2セキュリティグループ:wp-dev-sg-ec2

フォルダ構成

フォルダ構成は以下のようになっています。

├── Network
│   └── Network_stack.py
│
├── Storage
│    │── Storage_stack.py
│
├── Database
│    └── Database_stack.py
│
├── Computing
│       │── Computing_stack.py
│       └── User_data.sh
│
app.py

スタックを4つに分けています。

  1. Network_stack.py
  2. Storage_stack.py
  3. Database_stack.py
  4. Computing_stack.py

これを順番にデプロイしていくことでWordPressサイトが構築されます。
※User_data.shはEC2で初回起動時に実行するユーザーデータです。Computing_stack.pyから読み込まれます。

今回は❶と❷のスタックを説明します。

コード量が多いため、❸と❹は次回の記事で説明します。

  • ❸、❹のDatabaseスタックとComputingスタックの解説記事はこちら

エントリポイント(app.py)

まずエントリポイントを簡単に説明します。

  • app.py
#!/usr/bin/env python3
import os

import aws_cdk as cdk

from Network.Network_stack import NetworkStack
from Storage.Storage_stack import StorageStack
from Database.Database_stack import DatabaseStack
from Computing.Computing_stack import ComputingStack

my_account = "XXXXXXXXXXXX"
my_region = "ap-northeast-1"

envJP = cdk.Environment(account=my_account, region=my_region)

app = cdk.App()
NetworkStack(app, "Network", env=envJP)
StorageStack(app, "Storage", env=envJP)
DatabaseStack(app, "Database", env=envJP)
ComputingStack(app, "Computing", env=envJP)

app.synth()

コード解説(app.py)

my_account変数にAWSアカウントNoを入れて、my_region変数にデプロイしたいAWSリージョンを入れます。

それを各スタックに渡してデプロイするようにしています。

Networkスタック(Network_stack.py)

1番最初にデプロイするスタックです。

  • Network_stack.py
from aws_cdk import (
    Stack,
    aws_ec2 as ec2,
    aws_iam as iam
)
from constructs import Construct

class NetworkStack(Stack):

    def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None:
        super().__init__(scope, construct_id, **kwargs)

        ###############################################
        ############### Network Section ###############
        ###############################################
        # VPC & Subnet
        wp_vpc = ec2.Vpc(self, "Vpc",
            vpc_name="wp-dev-vpc",
            cidr="10.0.0.0/16",
            max_azs=2,
            nat_gateways=1,
            subnet_configuration=[
                ec2.SubnetConfiguration(
                    name="wp-dev-public",
                    cidr_mask=24,
                    subnet_type=ec2.SubnetType.PUBLIC
                ),
                ec2.SubnetConfiguration(
                    name="wp-dev-private-with-nat",
                    cidr_mask=24,
                    subnet_type=ec2.SubnetType.PRIVATE_WITH_NAT
                ),
                ec2.SubnetConfiguration(
                    name="wp-dev-private-isolated",
                    cidr_mask=24,
                    subnet_type=ec2.SubnetType.PRIVATE_ISOLATED
                )
            ]
        )


        ######################################################
        ############### Security Group Section ###############
        ######################################################
        # Security Group(ELB)
        wp_sg_elb = ec2.SecurityGroup(self, "SgELB",
            vpc=wp_vpc,
            security_group_name="wp-dev-sg-elb",
            description="Security Group for ELB",
            allow_all_outbound=False
        )
        
        # Security Group(EC2)
        wp_sg_ec2 = ec2.SecurityGroup(self, "SgEC2",
            vpc=wp_vpc,
            security_group_name="wp-dev-sg-ec2",
            description="Security Group for EC2",
            allow_all_outbound=False
        )

        # Security Group(EFS)
        wp_sg_efs = ec2.SecurityGroup(self, "SgEFS",
            vpc=wp_vpc,
            security_group_name="wp-dev-sg-efs",
            description="Security Group for EFS",
            allow_all_outbound=False
        )

        # Security Group(RDS)
        wp_sg_rds = ec2.SecurityGroup(self, "SgRDS",
            vpc=wp_vpc,
            security_group_name="wp-dev-sg-rds",
            description="Security Group for RDS",
            allow_all_outbound=False
        )

        # Security Group(SSM)
        wp_sg_ssm = ec2.SecurityGroup(self, "SgSSM",
            vpc=wp_vpc,
            security_group_name="wp-dev-sg-ssm",
            description="Security Group for SSM",
            allow_all_outbound=False
        )


        ################################################
        ############### Endpoint Section ###############
        ################################################
        # SSM Endpoint
        ec2.InterfaceVpcEndpoint(self, "SSMEndpoint",
            vpc=wp_vpc,
            service=ec2.InterfaceVpcEndpointAwsService.SSM,
            security_groups=[wp_sg_ssm],
            subnets=ec2.SubnetSelection(subnet_type=ec2.SubnetType.PRIVATE_WITH_NAT)
        )
        # SSM_MESSAGES Endpoint
        ec2.InterfaceVpcEndpoint(self, "SSM_MESSAGESEndpoint",
            vpc=wp_vpc,
            service=ec2.InterfaceVpcEndpointAwsService.SSM_MESSAGES,
            security_groups=[wp_sg_ssm],
            subnets=ec2.SubnetSelection(subnet_type=ec2.SubnetType.PRIVATE_WITH_NAT)
        )
        # EC2_MESSAGES Endpoint
        ec2.InterfaceVpcEndpoint(self, "EC2_MESSAGESEndpoint",
            vpc=wp_vpc,
            service=ec2.InterfaceVpcEndpointAwsService.EC2_MESSAGES,
            security_groups=[wp_sg_ssm],
            subnets=ec2.SubnetSelection(subnet_type=ec2.SubnetType.PRIVATE_WITH_NAT)
        )


        ###########################################
        ############### IAM Section ###############
        ###########################################
        # Role(EC2)
        wp_role_ec2 = iam.Role(self, "RoleEC2",
            role_name="wp-dev-role-ec2",
            description="Role for EC2",
            assumed_by=iam.ServicePrincipal("ec2.amazonaws.com")
        )

        # Add Policy To Role(EC2)
        wp_role_ec2.add_managed_policy(
            iam.ManagedPolicy.from_aws_managed_policy_name(managed_policy_name="AmazonSSMManagedInstanceCore")
        )
        wp_role_ec2.add_managed_policy(
            iam.ManagedPolicy.from_aws_managed_policy_name(managed_policy_name="AmazonElasticFileSystemReadOnlyAccess")
        )
        wp_role_ec2.add_managed_policy(
            iam.ManagedPolicy.from_aws_managed_policy_name(managed_policy_name="SecretsManagerReadWrite")
        )
        wp_role_ec2.add_managed_policy(
            iam.ManagedPolicy.from_aws_managed_policy_name(managed_policy_name="ElasticLoadBalancingReadOnly")
        )

コード解説(Network_stack.py)

  • 17〜39行目
    VPCを作成しています。
    max_azs=2でAZを2つに指定しています。
    nat_gateways=1
    でNat Gatewayを1つに指定しています。
    subnet_configurationでサブネットの定義を記述しています。ec2.SubnetType.PUBLICでパブリックサブネットを使用すること、ec2.SubnetType.PRIVATE_WITH_NATでプライベートサブネット(NATでインターネット接続可能)を使用すること、ec2.SubnetType.PRIVATE_ISOLATEDでプライベートサブネット(インターネット接続不可)を使用することを記述しています。それぞれのサブネットマスクは/24です。
     
  • 46〜83行目
    セキュリティグループを作成しています。(ELB、EC2、EFS、RDS、セッションマネージャーのセキュリティグループをそれぞれ作成しています)
    allow_all_outbound=Falseでアウトバウンドはすべてブロックするようにしています。
     
  • 90〜109行目
    セッションマネージャーでEC2に接続するための、エンドポイントを作成しています。
    security_groups に先ほど作成したセッションマネージャー用のセキュリティグループを渡しています。
    subnetsにプライベートサブネット(APP)を指定しています。(EC2と同じサブネットを指定)
     
  • 116〜120行目
    EC2にアタッチするIAMロールを作成しています。
     
  • 123〜134行目
    IAMロールにポリシーを追加しています。
    AmazonSSMManagedInstanceCoreはセッションマネージャーでEC2にアクセスするために必要です。

    以下のポリシーはEC2のユーザーデータでaws cliを実行して情報を取得するためにアタッチしています。(ユーザーデータについては次回の記事で触れます)
    AmazonElasticFileSystemReadOnlyAccessはEFSのファイルシステムIDを取得するため必要です。
    SecretsManagerReadWriteはRDSのシークレット情報を取得するために必要です。
    ElasticLoadBalancingReadOnlyはALBのDNS名を取得するために必要です。

Storageスタック(Storage_stack.py)

EFSのデプロイが記述されたスタックです。

  • Storage_stack.py
from aws_cdk import (
    Stack,
    aws_ec2 as ec2,
    aws_efs as efs
)
from constructs import Construct
import aws_cdk as cdk

class StorageStack(Stack):

    def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None:
        super().__init__(scope, construct_id, **kwargs)

        ##############################################
        ############### Import Section ###############
        ##############################################
        # Import VPC
        wp_vpc = ec2.Vpc.from_lookup(self, "Vpc",
            vpc_name="wp-dev-vpc"
        )

        # Import Security Group(EFS)
        wp_sg_efs = ec2.SecurityGroup.from_lookup_by_name(self, "SgEFS",
            vpc=wp_vpc,
            security_group_name="wp-dev-sg-efs"
        )

        # Import Security Group(EC2)
        wp_sg_ec2 = ec2.SecurityGroup.from_lookup_by_name(self, "SgEC2",
            vpc=wp_vpc,
            security_group_name="wp-dev-sg-ec2"
        )


        ###############################################
        ############### Storage Section ###############
        ###############################################
        # EFS File System
        wp_efs = efs.FileSystem(self, "Efs",
            vpc=wp_vpc,
            encrypted=True,
            file_system_name="wp-dev-efs",
            security_group=wp_sg_efs,
            vpc_subnets=ec2.SubnetSelection(subnet_type=ec2.SubnetType.PRIVATE_WITH_NAT),
            removal_policy=cdk.RemovalPolicy.DESTROY
        )


        ######################################################
        ############### Add Allow Rule Section ###############
        ######################################################
        # Add Allow Connection to EFS
        wp_efs.connections.allow_from(wp_sg_ec2, ec2.Port.tcp(2049))

コード解説(Storage_stack.py)

  • 18〜20行目
    Networkスタックで作成したVPCをfrom_lookupでStorageスタックにインポートしています。
     
  • 23〜32行目
    Networkスタックで作成したセキュリティグループ(EFSとEC2)をfrom_lookup_by_nameでインポートしています。
     
  • 39〜46行目
    EFSファイルシステムを作成しています。
    security_groupにインポートしたEFSのセキュリティグループを渡しています。
    vpc_subnetsでプライベートサブネット(APP)を指定しています(EC2と同じサブネット)。
    CDKでdestroyしたときにファイルシステムが削除されるように、removal_policycdk.RemovalPolicy.DESTROYを渡しています。
     
  • 53行目
    EFSに対してEC2からの2049ポートでのアクセスを許可しています。

次回は

次回はDatabaseスタックとComputingスタックの解説をします。

1 2 3

この記事が気に入ったら
フォローしてね!

よかったらシェアしてね!

コメント

コメントする

コメントは日本語で入力してください。(スパム対策)

CAPTCHA

もくじ
閉じる