AWS CDKを用いてDrupalサイトを自動でデプロイしてみた

  •  
 
トビウオ2024年3月28日 - 14:58 に投稿

紹介記事のタイトル:AWS CDKを用いてDrupalサイトを自動でデプロイしてみた

概要

ご存じの方も多いと思いますが、AWS (Amazon Web Services) は世界的なクラウドサービスの一つです。提供するサービスの内容をザックリ言うと「インフラ」…… 仮想マシン、仮想ネットワーク、データベース、ストレージなど。IT 環境構築に必要なリソース (クラウドリソース) を、オンライン経由でスピーディーに調達することができます。

ここで興味深いのは、この「調達」操作について、主に2種類の手段が存在することです。

  • AWSのWebサイトにアクセスし、Webフォームを操作することでクラウドリソースを取得し、設定を行う
  • 提供されているコマンドラインツールを使用し、コマンドもしくはプログラミング言語により、自動的にクラウドリソースを生成する

前者は手軽かつ直感的で、細かな設定も確認しやすいです。後者は「設定内容」を「シェルスクリプト」ないし「プログラミング言語」として定義できるため、自動化による利便性と厳密性を得ることができます。

今回は、後者で使われるツールの一つ…… AWS CDK について取り上げ、それを用いたWebサイト構築について学びます。

※記事の性質上、「前者の方法でWebサイト構築することはできる」ものと仮定して話を勧めます

AWS CDKとは?

AWS CDK (AWS Cloud Development Kit) は、各種プログラミング言語 (JavaScript, Python, Java, C# など) を使い、AWS のクラウドリソースを自動生成するためのフレームワークです。使用するツールとしては、主に次の2つになります。

  • AWS CDK のライブラリ
    • AWS のクラウドリソースを生成するためのライブラリコード
    • 各種プログラミング言語に対応している
    • AWS CDK CLI が処理するためのコードを書くために必要
  • AWS CDK CLI
    • AWS 向けのコマンドラインツールの1つ
    • AWS CDK 開発向けの環境を構築できる
    • AWS CDK として書かれたコードを実行することもできる

より詳しく書くと、次のようなステップを踏むことで、「プログラミング言語で書かれたコード」が「AWS 上のクラウドリソース」になります。

  1. AWS CDK CLIがプログラムコードを読み取って実行する
  2. 実行処理により、AWS CloudFormation で使用するためのYAMLファイルが生成される
  3. AWS CloudFormation も AWSが提供するクラウドサービスの一つで、「YAMLファイルなどの設定ファイルからクラウドリソースを作成する」ことができる
  4. AWS CDK CLI が YAMLファイルを送信し、AWS 上にクラウドリソースが生成される

このため、コーディングで発生するエラーは主に、「YAMLファイルが作成できない」か「送信したYAMLファイルからクラウドリソースを作成できない」に大別されます。どちらの段階で躓いているかを見極めることが、AWS CDK を使った開発における重要ポイントになります。

今回作成するもの

今回は、記事タイトル通り、Drupal という CMS を用いたWebサイトづくりを行います。今回はDockerを使用せず行うため、最低限必要なものを列挙すると、次のようになります。

  • AWS VPC (Virtual Private Cloud)
    • AWS の各種リソースが利用できる、仮想のネットワーク空間を作成できる
    • Subnet という単位で分割したり、外部からアクセスするための Endpoint を設置したりできる
  • Amazon EC2 (Elastic Compute Cloud)
    • 仮想環境上で動作するコンピューター (サーバー) を作成できる
    • コア数やメモリ量など、その規模感は様々
    • 受領課金制なので、高性能なサーバーほどお金が掛かることになる
    • 負荷に合わせて自動でサーバーを増減することもできる (Auto Scaling)
  • Amazon RDS (Relational Database Service)
    • オンライン経由でアクセスできるデータベースを用意できる
    • デフォルトでも高い冗長性とセキュリティが確保されている
    • MySQL や PostgreSQL など、様々なデータベースエンジンに対応

環境を構築しよう

公式のチュートリアルが丁寧で分かりやすいのですが、概略だけ書くとこんな感じ。

  1. Node.js をインストールし、npmが使用できるようにする
  2. AWS CDK をインストールする
    • npm install -g aws-cdkとコマンドを打つことで導入できる
  3. AWS CDK のプロジェクトを作成する
    • 例えば TypeScript で書く場合は、cdk init app --language typescriptとコマンドを打つ

プログラミングしてクラウドリソースを作成しよう

実際に書く際は、ステッブバイステップで進めるとよいでしょう。つまり、少し書く→デプロイしてみる→思った通りのリソースができているか確認する→必要なら修正する、を繰り返せばいいわけです。

また、AWS CDK における各リソースを定義する手段としては、主に「L1コンストラクト」または「L2コンストラクト」が使われます。前者はよりローエンドな記述が可能なものの、その分記述が長くなってややこしいので、可能なら後者で書くほうが望ましいです。公式資料だとこのあたり、それ以外だとこのあたりの記事が分かりやすかったです。

1. VPCを作成する

import * as cdk from 'aws-cdk-lib';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import { Construct } from 'constructs';

const MAX_AZS = 2;

export class AwsCdkSampleStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    // VPC
    const vpc = new ec2.Vpc(this, 'Vpc', {
      subnetConfiguration: [
        {
          cidrMask: 24,
          name: 'Public',
          subnetType: ec2.SubnetType.PUBLIC,
        },
        {
          cidrMask: 24,
          name: 'Private',
          subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS,
        }
      ],
      maxAzs: MAX_AZS,
    });
  }
}

まずはVPCを作成します。L2コンストラクトを使用しているため、これだけの記述で次のようなリソース群が自動で作成されます。

  • VPC
  • VPC内のサブネット (Public, Private)
    • それぞれのサブネットは、多くとも、MAX_AZSで指定した数だけの Availability Zone に分散される

2. セキュリティーグループを作成する

const PORT_HTTP = 80;

const securityGroup = new ec2.SecurityGroup(this, 'SecurityGroup', {
    vpc,
});
securityGroup.addIngressRule(ec2.Peer.anyIpv4(), ec2.Port.tcp(PORT_HTTP));

次にセキュリティーグループを作成します。ここでは、HTTPポート(PORT_HTTP番)を開放するための設定を行っています。セキュリティーグループはデフォルト設定だと、アウトバウンド側 (そのセキュリティーグループを設定したクラウドリソースから、セキュリティーグループ外部への通信) への通信が許可されているので、インバウンド側 (セキュリティーグループ外部からの通信) の設定だけを行えばよいです。

3. EC2インスタンスを作成する

const INSTANCE_TYPE = 't2.micro';
const INSTANCE_AMI = ec2.MachineImage.latestAmazonLinux2();

// UserData for EC2
const userData = ec2.UserData.forLinux({shebang: '#!/bin/bash'});
const script = readFileSync('./lib/resources/user-data.sh', 'utf8');

// EC2 Instance
new ec2.Instance(this, 'Instance', {
    vpc,
    instanceType: new ec2.InstanceType(INSTANCE_TYPE),
    machineImage: INSTANCE_AMI,
    securityGroup,
    vpcSubnets: {
    subnets: vpc.publicSubnets,
    },
    userData
});

次にEC2インスタンスを作成します。user-data.shの中身は後述しますが、つまり「指定したスクリプト (ユーザーデータ) が前処理として実行される仮想マシン (サーバー)」を作成しているわけですね。

また、先ほど設定したセキュリティーグループを設定しておくことで、外部からEC2インスタンスに、セキュリティーグループで許可した手段 (ここでは80番ポート) 以外からアクセスできないようになっています。

4. RDSを作成する

const PORT_MYSQL = 3306;
const RDS_INSTANCE_TYPE = 't2.micro';
const RDS_ENGINE = rds.DatabaseInstanceEngine.mysql({version: rds.MysqlEngineVersion.VER_8_0});
const MULTI_AZ = true;
const RDS_USER_NAME = 'username';
const RDS_USER_PASSWORD = 'password';

// Add RDS instance (MySQL 8.0)
const rdsSecurityGroup = new ec2.SecurityGroup(this, 'RDSSecurityGroup', {
    vpc,
});
rdsSecurityGroup.addIngressRule(securityGroup, ec2.Port.tcp(PORT_MYSQL));
new rds.DatabaseInstance(this, 'RDS', {
    engine: RDS_ENGINE,
    instanceType: new ec2.InstanceType(RDS_INSTANCE_TYPE),
    vpc,
    vpcSubnets: {
    subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS,
    },
    securityGroups: [rdsSecurityGroup],
    removalPolicy: cdk.RemovalPolicy.DESTROY,
    multiAz: MULTI_AZ,
    credentials: rds.Credentials.fromPassword(
    RDS_USER_NAME,
    cdk.SecretValue.unsafePlainText(RDS_USER_PASSWORD)
    ),
});

最後に RDS を作成します。RDS の項目は多岐にわたるため、必要最小限だけ記述してもこれだけの行数になってしまいますが、Web フォーム上での作業と違い、「一度記述すれば後は実行するだけ」にできるのは、AWS CDK の大きなメリットですね。

※今回は説明を簡単にするため、パスワードをベタ書きしていますが、例えばAWS Secrets Managerを使って安全に管理することもできます

まとめ

手動だと煩雑になりがちなシステム構築も、AWS CDKを用いることにより、自動で作成/削除できるようになります。文法を覚えるのが少々大変なものの、Webフォームを比べるとずっと効率的なので、ぜひ使いこなしていきたいですね。

おまけ:user-data.shの中身

インスタンスの種類や、動作させたいアプリなどにより、このあたりの記述はいかようにも変化します。重要なのは、まるでDockerfileを弄っているかのように、各種コマンドを駆使したスクリプトでインスタンスを初期化できることです。

#!/bin/bash

yum -y update
yum -y install httpd
yum -y install mysql

amazon-linux-extras install php8.2 -y
yum -y install php-xml php-gd php-mbstring

# Memo: User Dataで書かれたコードはrootユーザーで実行されるため、$HOMEを適宜設定しないと、composerが正常にインストールできなかった
export HOME=/home/ec2-user
cd $HOME
curl -sS http://getcomposer.org/installer | php
mv composer.phar /usr/local/bin/composer

composer create-project drupal/recommended-project sample_site
mv sample_site /var/www/html
chown -R apache:apache /var/www/html/sample_site

new_document_root="/var/www/html/sample_site/web"
apache_config_file="/etc/httpd/conf/httpd.conf"
php_ini_file="/etc/php.ini"

sed -i "s|DocumentRoot \"/var/www/html\"|DocumentRoot \"$new_document_root\"|g" $apache_config_file
sed -i "s|<Directory \"/var/www/html\">|<Directory \"$new_document_root\">|g" $apache_config_file
sed -i '$a\\nTimeout 300' $apache_config_file
sed -i "s|;mbstring.language = Japanese|mbstring.language = Japanese|g" $php_ini_file

systemctl enable httpd.service
systemctl start httpd.service

おまけ:AWS CDKの全文コード

勉強時に使用したコードですが、AWS側の更新などにより、そのままでは動かないこともあるかもしれません。AWS CDK は様々な言語で記述できるものの、私はTypeScriptをよく使っています。

import * as cdk from 'aws-cdk-lib';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import * as rds from 'aws-cdk-lib/aws-rds';
import { Construct } from 'constructs';
import { readFileSync } from 'fs';

const INSTANCE_TYPE = 't2.micro';
const INSTANCE_AMI = ec2.MachineImage.latestAmazonLinux2();
const RDS_ENGINE = rds.DatabaseInstanceEngine.mysql({ version: rds.MysqlEngineVersion.VER_8_0 });
const RDS_INSTANCE_TYPE = 't2.micro';
const RDS_USER_NAME = 'admin';
const RDS_USER_PASSWORD = 'password';
const PORT_SSH = 22;
const PORT_HTTP = 80;
const PORT_MYSQL = 3306;
const MAX_AZS = 2;
const MULTI_AZ = true;

export class AwsCdkSampleStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    // VPC
    const vpc = new ec2.Vpc(this, 'Vpc', {
      subnetConfiguration: [
        {
          cidrMask: 24,
          name: 'Public',
          subnetType: ec2.SubnetType.PUBLIC,
        },
        {
          cidrMask: 24,
          name: 'Private',
          subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS,
        }
      ],
      maxAzs: MAX_AZS,
    });

    // Security Group for EC2
    const securityGroup = new ec2.SecurityGroup(this, 'SecurityGroup', {
      vpc,
    });
    securityGroup.addIngressRule(ec2.Peer.anyIpv4(), ec2.Port.tcp(PORT_HTTP));

    // UserData for EC2
    const userData = ec2.UserData.forLinux({shebang: '#!/bin/bash'});
    const script = readFileSync('./lib/resources/user-data.sh', 'utf8');

    // EC2 Instance
    new ec2.Instance(this, 'Instance', {
      vpc,
      instanceType: new ec2.InstanceType(INSTANCE_TYPE),
      machineImage: INSTANCE_AMI,
      securityGroup,
      vpcSubnets: {
        subnets: vpc.publicSubnets,
      },
      userData
    });

    // Add RDS instance (MySQL 8.0)
    const rdsSecurityGroup = new ec2.SecurityGroup(this, 'RDSSecurityGroup', {
      vpc,
    });
    rdsSecurityGroup.addIngressRule(securityGroup, ec2.Port.tcp(PORT_MYSQL));
    new rds.DatabaseInstance(this, 'RDS', {
      engine: RDS_ENGINE,
      instanceType: new ec2.InstanceType(RDS_INSTANCE_TYPE),
      vpc,
      vpcSubnets: {
        subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS,
      },
      securityGroups: [rdsSecurityGroup],
      removalPolicy: cdk.RemovalPolicy.DESTROY,
      multiAz: MULTI_AZ,
      credentials: rds.Credentials.fromPassword(
        RDS_USER_NAME,
        cdk.SecretValue.unsafePlainText(RDS_USER_PASSWORD)
      ),
    });
  }
}

コメントを追加

プレーンテキスト

  • HTMLタグは利用できません。
  • 行と段落は自動的に折り返されます。
  • ウェブページのアドレスとメールアドレスは自動的にリンクに変換されます。
CAPTCHA
この質問はあなたが人間の訪問者であるかどうかをテストし、自動化されたスパム送信を防ぐためのものです。