CFNカスタムリソースでRDS DBの初期セットアップ

CFNカスタムリソースでRDSのDBを初期化する

CloudFormationカスタムリソースを使って、RDS DBの初期セットアップを実行する

CloudFormationでRDSリソース作成時に、DBの初期化(DBやテーブルの作成、テストレコードの追加等)も併せて実行することを考えます。
今回はCloudFormationカスタムリソースを使用して、DBを初期化します。

構築する環境

Diagram of initialize RDS DB with CFN Custom Resource.

主に4つのリソースを作成します。

1つ目はRDS DBインスタンスです。
今回はMySQLタイプのDBインスタンスを作成します。

2つ目はEC2インスタンスです。
DBインスタンスに接続するクライアントとして使用します。
インスタンスは最新版のAmazon Linux 2です。

3つ目はLambda関数です。
この関数がCloudFormationスタック作成時に自動的に実行されるように、CloudFormationカスタムリソースに関数を関連づけます。
この関数の働きは、DBインスタンスを初期化することです。
関数のランタイム環境はPython3.8です。

4つ目はSSMパラメータストアです。
Lambda関数で実行するSQL文を、パラメータストアに文字列として保存します。
なお初期化処理で実行する内容は、以下のAWS公式ページを参考にしました。

https://aws.amazon.com/jp/getting-started/hands-on/boosting-mysql-database-performance-with-amazon-elasticache-for-redis/3/

CloudFormationテンプレートファイル

上記の構成をCloudFormationで構築します。
以下のURLにCloudFormationテンプレートを配置しています。

https://github.com/awstut-an-r/awstut-fa/tree/main/062

テンプレートファイルのポイント解説

SSMパラメータストア

Resources:
  SQLParameter:
    Type: AWS::SSM::Parameter
    Properties:
      Name: !Ref Prefix
      Type: String
      Value: |
        CREATE database tutorial;
        USE tutorial;
        CREATE TABLE planet (id INT UNSIGNED AUTO_INCREMENT, name VARCHAR(30), PRIMARY KEY(id));
        INSERT INTO planet (name) VALUES ("Mercury");
        INSERT INTO planet (name) VALUES ("Venus");
        INSERT INTO planet (name) VALUES ("Earth");
        INSERT INTO planet (name) VALUES ("Mars");
        INSERT INTO planet (name) VALUES ("Jupiter");
        INSERT INTO planet (name) VALUES ("Saturn");
        INSERT INTO planet (name) VALUES ("Uranus");
        INSERT INTO planet (name) VALUES ("Neptune");
Code language: YAML (yaml)

Lambda関数で実行するSQL文を、SSMパラメータストアに登録します。
DBおよびテーブルを作成後、テストレコードを保存します。

CloudFormationカスタムリソース

まずカスタムリソースで実行するLambda関数を確認します。

Resources:
  Function:
    Type: AWS::Lambda::Function
    Properties:
      Environment:
        Variables:
          DB_ENDPOINT_ADDRESS: !Ref DBInstanceEndpointAddress
          DB_ENDPOINT_PORT: !Ref MySQLPort
          DB_PASSWORD: !Ref DBMasterUserPassword
          DB_USER: !Ref DBMasterUsername
          REGION: !Ref AWS::Region
          SQL_PARAMETER: !Ref SQLParameter
      Code:
        ZipFile: |
          import boto3
          import cfnresponse
          import mysql.connector
          import os

          db_endpoint_port = os.environ['DB_ENDPOINT_PORT']
          db_endpoint_address = os.environ['DB_ENDPOINT_ADDRESS']
          db_password = os.environ['DB_PASSWORD']
          db_user = os.environ['DB_USER']
          region = os.environ['REGION']
          sql_parameter = os.environ['SQL_PARAMETER']

          CREATE = 'Create'
          response_data = {}

          def lambda_handler(event, context):
            try:
              if event['RequestType'] == CREATE:
                client = boto3.client('ssm', region_name=region)
                response = client.get_parameter(Name=sql_parameter)
                sql_statements = response['Parameter']['Value']

                conn = mysql.connector.connect(
                  host=db_endpoint_address,
                  port=db_endpoint_port,
                  user=db_user,
                  password=db_password
                  )
                cur = conn.cursor()

                for sql in sql_statements.splitlines():
                  print(sql)
                  cur.execute(sql)

                cur.close()
                conn.commit()

              cfnresponse.send(event, context, cfnresponse.SUCCESS, response_data)

            except Exception as e:
              print(e)
              cfnresponse.send(event, context, cfnresponse.FAILED, response_data)
      FunctionName: !Sub "${Prefix}-function"
      Handler: index.lambda_handler
      Layers:
        - !Ref LambdaLayer
      Runtime: !Ref Runtime
      Role: !GetAtt FunctionRole.Arn
      Timeout: !Ref Timeout
      VpcConfig:
        SecurityGroupIds:
          - !Ref FunctionSecurityGroup
        SubnetIds:
          - !Ref PrivateSubnet
Code language: YAML (yaml)

Lambda関数で実行するコードをインライン表記で定義します。
詳細につきましては、以下のページをご確認ください。

あわせて読みたい
CloudFormationでLambdaを作成する3パータン(S3/インライン/コンテナ) 【CloudFormationでLambdaを作成する】 CloudFormationでLambdaを作成する場合、大別すると以下の3パターンあります。 S3バケットにコードをアップロードする インライ...

cfnresponseモジュールを使用して、関数をLambda-backedカスタムリソースとして実装します。
詳細につきましては、以下のページをご確認ください。

あわせて読みたい
CloudFormationカスタムリソース入門 【CloudFormationカスタムリソースの挙動を確認する構成】 CloudFormationの機能の1つにカスタムリソースがあります。 カスタムリソースを使用すると、テンプレートにカ...

今回の構成では、Lambda関数をVPC内に配置します。
VpcConfigプロパティで、関数を配置するサブネットや、適用するセキュリティグループを指定します。

実行するコードの内容ですが、以下の通りです。

  1. CloudFormationテンプレートで定義した環境変数を、os.environにアクセスして取得する。
  2. Boto3でSSM用クライアントオブジェクトを作成する。
  3. クライアントオブジェクトを使用して、SSMパラメータストアにアクセスし、先述のSQL文を取得する。
  4. MySQLクライアントを使用してDBインスタンスに接続し、SQLを1文ずつ実行する。

DBインスタンス接続用のMySQLクライアントですが、MySQL Connectorを使用します。

https://dev.mysql.com/doc/connector-python/en/

今回はLambdaレイヤーとしてMySQL Connectorパッケージを用意します。
Lambdaレイヤーの詳細につきましては、以下のページをご確認ください。

あわせて読みたい
CFNでLambdaレイヤー作成 【CloudFormationでLambdaレイヤー作成】 本ページでは、CloudFormationでLambdaレイヤーを作成する方法を確認します。 Lambda レイヤーは、Lambda 関数で使用できるラ...

ちなみに関数用のIAMロールは以下の通りです。

Resources:
  FunctionRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Action: sts:AssumeRole
            Principal:
              Service:
                - lambda.amazonaws.com
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole
      Policies:
        - PolicyName: GetSSMParameterPolicy
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Effect: Allow
                Action:
                  - ssm:GetParameter
                Resource:
                  - !Sub "arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/${SQLParameter}"
Code language: YAML (yaml)

AWS管理ポリシーであるAWSLambdaVPCAccessExecutionRoleに加えて、SSMパラメータストアからパラメータを取得する権限を与えます。

続いてCloudFormationカスタムリソース本体を確認します。

Resources:
  CustomResource:
    Type: Custom::CustomResource
    Properties:
      ServiceToken: !Ref FunctionArn
Code language: YAML (yaml)

先述のLambda関数を指定します。

VPCエンドポイント

今回の構成では、複数のVPCエンドポイントを作成します。
ここではSSM用VPCエンドポイントに注目します。

Resources:
  SSMEndpoint:
    Type: AWS::EC2::VPCEndpoint
    Properties:
      PrivateDnsEnabled: true
      SecurityGroupIds:
        - !Ref EndpointSecurityGroup
      ServiceName: !Sub "com.amazonaws.${AWS::Region}.ssm"
      SubnetIds:
        - !Ref PrivateSubnet
      VpcEndpointType: Interface
      VpcId: !Ref VPC
Code language: YAML (yaml)

VPC内に配置したLambda関数からSSMパラメータストアに接続するために、このVPCエンドポイントを使用します。
VPCエンドポイントを作成することで、インターネットを経由することなく、VPC外のAWSリソースに接続することができます。

(参考)RDS DBインスタンス

Resources:
  DBInstance:
    Type: AWS::RDS::DBInstance
    Properties:
      AllocatedStorage: !Ref DBAllocatedStorage
      AvailabilityZone: !Sub "${AWS::Region}${AvailabilityZone}"
      DBInstanceClass: !Ref DBInstanceClass
      DBInstanceIdentifier: dbinstance
      DBSubnetGroupName: !Ref DBSubnetGroup
      Engine: !Ref DBEngine
      EngineVersion: !Ref DBEngineVersion
      MasterUsername: !Ref DBMasterUsername
      MasterUserPassword: !Ref DBMasterUserPassword
      VPCSecurityGroups:
        - !Ref DBSecurityGroup
Code language: YAML (yaml)

DBインスタンスです。
CloudFormationカスタムリソースを使用して、DBを初期化するためには、特別な設定は不要です。

(参考)EC2インスタンス

Resources:
  Instance:
    Type: AWS::EC2::Instance
    Properties:
      IamInstanceProfile: !Ref InstanceProfile
      ImageId: !Ref ImageId
      InstanceType: !Ref InstanceType
      NetworkInterfaces:
        - DeviceIndex: 0
          SubnetId: !Ref PrivateSubnet
          GroupSet:
            - !Ref InstanceSecurityGroup
      UserData: !Base64 |
        #!/bin/bash -xe
        yum update -y
        yum install -y mariadb
Code language: YAML (yaml)

ユーザーデータでインスタンスの初期化処理を定義します。
EC2インスタンスからMySQLタイプのDBインスタンスに接続するために、MySQLクライアントパッケージをインストールします。
詳細につきましては、以下のページをご確認ください。

あわせて読みたい
AL2で全RDSに接続する方法まとめ 【Amazon Linux 2からRDSの全DBエンジンに接続する方法】 2022年現在、RDSは以下の7種類のDBエンジンを提供しています。 Aurora(PostgreSQL) Aurora(MySQL) PostgreSQL ...

環境構築

CloudFormationを使用して、本環境を構築し、実際の挙動を確認します。

事前準備

事前準備として、Lambdaレイヤー用のデプロイパッケージを用意します。
具体的には、以下のコマンドを実行します。

$ mkdir python

$ pip3 install mysql-connector-python -t ./python

$ zip -r layer.zip python
Code language: Bash (bash)

作成したデプロイパッケージをS3バケットにアップロードします。

CloudFormationスタックを作成し、スタック内のリソースを確認する

CloudFormationスタックを作成します。
スタックの作成および各スタックの確認方法については、以下のページをご確認ください。

あわせて読みたい
CloudFormationのネストされたスタックで環境を構築する 【CloudFormationのネストされたスタックで環境を構築する方法】 CloudFormationにおけるネストされたスタックを検証します。 CloudFormationでは、スタックをネストす...

各スタックのリソースを確認した結果、今回作成された主要リソースの情報は以下の通りです。

  • RDS DBインスタンス:dbinstance
  • RDS DBインスタンスのエンドポイント:dbinstance.cl50iikpthxs.ap-northeast-1.rds.amazonaws.com
  • EC2インスタンス:i-0b3d18375ca48f06b
  • Lambda関数:fa-062-function
  • SSMパラメータストアのパラメータ:fa-062

AWS Management Consoleから各リソースを確認します。
まずCloudFormationカスタムリソースを確認します。

Result of Custom Resource 1.
Result of Custom Resource 2.

Lambda関数とカスタムリソース本体が正常に作成されていることがわかります。

次にSSMパラメータストアに保存されている値を確認します。

SSM Parameter Store.

DB初期化用のSQL文が保存されていることがわかります。

動作確認

準備が整いましたので、実際の動作を確認します。

Lambda関数の実行結果

まずLambda関数の実行結果を、CloudWatch Logsのロググループで確認します。

Result of Custom Resource 3.

ログから、SSMパラメータストアに保存されていたSQLが、1文ずつ実行されていることがわかります。
つまりDB初期化処理が正常に実行されたということです。

そしてCloudFormationカスタムリソースとして、関数が「SUCCESS」を返していることもわかります。
つまり関数はカスタムリソースとして正常に動作したということです。

EC2インスタンス

次にEC2インスタンスに接続し、DBの初期化状況を確認します。

インスタンスへのアクセスはSSM Session Managerを使用します。

% aws ssm start-session --target i-0b3d18375ca48f06b

Starting session with SessionId: root-0b2e1f71f0d2aea98

sh-4.2$
Code language: Bash (bash)

SSM Session Managerの詳細につきましては、以下のページをご確認ください。

あわせて読みたい
LinuxインスタンスにSSM Session Manager経由でアクセスする 【LinuxインスタンスにSSM Session Manager経由でアクセスする】 EC2インスタンスにSSM Session Manager経由でアクセスする構成を確認します。 Session Manager は完全...

まずユーザーデータによるEC2インスタンスの初期化処理の実行状況を確認します。

sh-4.2$ yum list installed | grep mariadb
mariadb.aarch64                       1:5.5.68-1.amzn2               @amzn2-core
mariadb-libs.aarch64                  1:5.5.68-1.amzn2               installed

sh-4.2$ mysql -V
mysql  Ver 15.1 Distrib 5.5.68-MariaDB, for Linux (aarch64) using readline 5.1
Code language: Bash (bash)

正常にMySQLクライアントパッケージがインストールされていることがわかります。

このクライアントパッケージを使用して、DBインスタンスに接続します。

sh-4.2$ mysql -h dbinstance.cl50iikpthxs.ap-northeast-1.rds.amazonaws.com -P 3306 -u testuser -p
Enter password:
Welcome to the MariaDB monitor.  Commands end with ; or \g.
Your MySQL connection id is 20
Server version: 8.0.28 Source distribution

Copyright (c) 2000, 2018, Oracle, MariaDB Corporation Ab and others.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

MySQL [(none)]>
Code language: Bash (bash)

正常に接続することができました。

DBを選択します。

MySQL [(none)]> USE tutorial;
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A

Database changed
MySQL [tutorial]>
Code language: Bash (bash)

DBを変更することができました。
CloudFormationカスタムリソースによる初期化処理が正常に実行されているということです。

最後に全レコードを取得します。

MySQL [tutorial]> select * from planet;
+----+---------+
| id | name    |
+----+---------+
|  1 | Mercury |
|  2 | Venus   |
|  3 | Earth   |
|  4 | Mars    |
|  5 | Jupiter |
|  6 | Saturn  |
|  7 | Uranus  |
|  8 | Neptune |
+----+---------+
8 rows in set (0.01 sec)
Code language: Bash (bash)

テストレコードが返ってきました。
正常にCREATE TABLE文やINSERT文が実行されたということです。

以上の通り、CloudFormationカスタムリソースによって、DBインスタンスを初期化することができました。

まとめ

CloudFormationカスタムリソースを使用して、DBを初期化する方法をご紹介しました。