IAM認証を使用して、EC2(Linux)/LambdaからRDSに接続する

IAM認証を使用して、EC2(Linux)/LambdaからRDSに接続する

RDSが提供する機能の1つに、IAM認証があります。

AWS Identity and Access Management (IAM) データベース認証を使用して、DB インスタンスを認証できます。IAM データベース認証には、MariaDB、MySQL、および PostgreSQL を使用します。この認証方法では、DB インスタンスに接続するときにパスワードを使用する必要はありません。代わりに、認証トークンを使用します。

MariaDB、MySQL、および PostgreSQL の IAM データベース認証

本ページでは、EC2インスタンスおよびLambda関数から、IAM認証でRDSにアクセスする方法を確認します。

構築する環境

Diagram of connecting to RDS from EC2(Linux) / Lambda using IAM authentication.

DBインスタンスを作成します。
IAM認証を有効化します。
エンジンはMySQLです。

プライベートサブネットにEC2インスタンスおよびLambda関数を作成します。
EC2インスタンスは最新版のAmazon Linux2、Lambda関数はPython3.8とします。
両リソースはDBインスタンスにIAM認証で接続するクライアントとして使用します。

CloudFormationカスタムリソースに関連づけるLambda関数を2種類作成します。
1種類目はLambda関数用のLambdaレイヤーを作成します。
2種類目はDBインスタンスの初期化処理を行います。
カスタムリソースに関連づけることによって、CloudFormationスタック作成時に、自動的に関数を実行するように設定します。

CloudFormationテンプレートファイル

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

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

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

RDS

DBインスタンス

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

ポイントはEnableIAMDatabaseAuthenticationプロパティです。
本プロパティに「true」を設定することで、DBインスタンスでIAM認証が有効化されます。

Lambda関数を使用した初期化処理

Resources:
  SQLParameter:
    Type: AWS::SSM::Parameter
    Properties:
      Name: !Sub "${Prefix}-customresource-03"
      Type: String
      Value: !Sub |
        CREATE USER ${DBIamUsername} IDENTIFIED WITH AWSAuthenticationPlugin AS 'RDS';
        GRANT SELECT ON *.* TO 'iamuser'@'%';
        USE ${DBName};
        CREATE TABLE ${DBTableName} (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");

  Function:
    Type: AWS::Lambda::Function
    Properties:
      Architectures:
        - !Ref Architecture
      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 = int(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}-customresource-03"
      Handler: !Ref Handler
      Layers:
        - !Ref LambdaLayer
      Runtime: !Ref Runtime
      Role: !GetAtt FunctionRole.Arn
      Timeout: !Ref Timeout
      VpcConfig:
        SecurityGroupIds:
          - !Ref FunctionSecurityGroup
        SubnetIds:
          - !Ref FunctionSubnet
Code language: YAML (yaml)

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

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

VPC内に本関数を配置し、DBインスタンスにアクセスして、DBの初期化処理を行います。
詳細につきましては、以下のページをご確認ください。

あわせて読みたい
CFNカスタムリソースでRDS DBの初期セットアップ 【CloudFormationカスタムリソースを使って、RDS DBの初期セットアップを実行する】 CloudFormationでRDSリソース作成時に、DBの初期化(DBやテーブルの作成、テストレコ...

実行するSQL文はSSMパラメータストアに保存し、これを関数が参照します。
SQL文の中で、特に1〜2行目がポイントです。

1行目はIAM認証によってDBインスタンスにアクセスするユーザを作成するコマンドです。
2行目は作成したユーザにデータベースを操作する権限を付与するコマンドです。

残りのコマンドはテストレコードを書き込む処理を行います。
以下のページの内容を参考にしました。

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

EC2インスタンス

Resources:
  Instance:
    Type: AWS::EC2::Instance
    Properties:
      IamInstanceProfile: !Ref InstanceProfile
      ImageId: !Ref ImageId
      InstanceType: !Ref InstanceType
      NetworkInterfaces:
        - DeviceIndex: 0
          SubnetId: !Ref InstanceSubnet
          GroupSet:
            - !Ref InstanceSecurityGroup
      UserData: !Base64 |
        #!/bin/bash -xe
        yum update -y
        yum install -y mariadb
        curl -OL https://s3.amazonaws.com/rds-downloads/rds-combined-ca-bundle.pem
Code language: YAML (yaml)

ユーザデータでインスタンスの初期化処理を定義します。
初期化処理に関する詳細は以下のページをご確認ください。

あわせて読みたい
Linuxインスタンスの初期化方法4選 【Linuxインスタンスを初期化する4つの方法】 EC2インスタンスの起動時に初期化処理を実行する方法を考えます。 EC2インスタンスを構築時に初期化する以下の4つの手法を...

ポイントは3行目と4行目です。

前者はMySQLクライアントをインストールするコマンドです。
Amazon Linux 2において、各種RDSに接続する方法に関しては、以下のページをご確認ください。

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

後者はIAM認証に必要な証明書をダウンロードするコマンドです。
前提として、IAM認証を行う場合、SSL通信が必須となります。

データベースとの間で送受信されるネットワークトラフィックは、Secure Socket Layer (SSL) または Transport Layer Security (TLS) を使用して暗号化されます。

MariaDB、MySQL、および PostgreSQL の IAM データベース認証

そしてSSL通信を行うためには、証明書を用意する必要があります。

SSL/TLS 接続は、クライアントと DB インスタンスの間を移動するデータを暗号化することによって、1 つのセキュリティ層を提供します。サーバー証明書を使用すると、Amazon RDS DB インスタンスへの接続が確立されていることを検証することによって、追加のセキュリティレイヤーが提供されます。これを行うには、プロビジョニングしたすべての DB インスタンスに自動インストールされたサーバー証明書をチェックします。

SSL/TLS を使用した DB インスタンスへの接続の暗号化

使用可能な証明書は以下のページにまとめられています。

https://docs.aws.amazon.com/ja_jp/AmazonRDS/latest/UserGuide/UsingWithRDS.SSL.html

今回は全リージョンの証明書バンドルであるglobal-bundle.pemを使用します。

以下が本インスタンス用のIAMロールです。

Resources:
  InstanceRole:
    Type: AWS::IAM::Role
    DeletionPolicy: Delete
    Properties:
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Action: sts:AssumeRole
            Principal:
              Service:
                - ec2.amazonaws.com
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore
      Policies:
        - PolicyName: RDSIamAthenticationPolicy
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Effect: Allow
                Action:
                  - rds-db:connect
                Resource:
                  - !Sub "arn:aws:rds-db:${AWS::Region}:${AWS::AccountId}:dbuser:${DBInstanceResourceId}/${DBIamUsername}"
Code language: YAML (yaml)

後述する動作確認時に、SSM Session Managerで本インスタンスに接続するため、AWS管理ポリシーAmazonSSMManagedInstanceCoreをアタッチします。

以下のページを参考にして、インラインポリシーにIAM認証用の設定を行います。

https://docs.aws.amazon.com/ja_jp/AmazonRDS/latest/UserGuide/UsingWithRDS.IAMDBAuth.IAMPolicy.html

アクションにrds-db:connectを、リソースにDBインスタンス内のユーザ名を指定します。

Lambda関数

Resources:
  Function:
    Type: AWS::Lambda::Function
    Properties:
      Architectures:
        - !Ref Architecture
      Environment:
        Variables:
          DB_ENDPOINT_ADDRESS: !Ref DBInstanceEndpointAddress
          DB_ENDPOINT_PORT: !Ref MySQLPort
          DB_NAME: !Ref DBName
          DB_TABLENAME: !Ref DBTableName
          DB_USER: !Ref DBIamUsername
          REGION: !Ref AWS::Region
          SSL_CERTIFICATE: /opt/python/global-bundle.pem
      Code:
        ZipFile: |
          import boto3
          import datetime
          import json
          import mysql.connector
          import os

          db_endpoint_address = os.environ['DB_ENDPOINT_ADDRESS']
          db_endpoint_port = int(os.environ['DB_ENDPOINT_PORT'])
          db_name = os.environ['DB_NAME']
          db_tablename = os.environ['DB_TABLENAME']
          db_user = os.environ['DB_USER']
          region = os.environ['REGION']
          ssl_certificate = os.environ['SSL_CERTIFICATE']

          os.environ['LIBMYSQL_ENABLE_CLEARTEXT_PLUGIN'] = '1'

          client = boto3.client('rds', region_name=region)

          def lambda_handler(event, context):
              token = client.generate_db_auth_token(
                  DBHostname=db_endpoint_address,
                  Port=db_endpoint_port,
                  DBUsername=db_user,
                  Region=region)

              conn = mysql.connector.connect(
                  host=db_endpoint_address,
                  user=db_user,
                  password=token,
                  port=db_endpoint_port,
                  database=db_name,
                  ssl_ca=ssl_certificate)

              cur = conn.cursor()
              read_sql = 'select * from {tbl};'.format(tbl=db_tablename)
              cur.execute(read_sql)
              content = [record for record in cur]

              cur.close()
              conn.close()

              return {
                  'statusCode': 200,
                  'body': json.dumps(content, indent=2)
              }
      FunctionName: !Sub "${Prefix}-function"
      Handler: !Ref Handler
      Layers:
        - !Ref LambdaLayer1
        - !Ref LambdaLayer2
      Runtime: !Ref Runtime
      Role: !GetAtt FunctionRole.Arn
      Timeout: !Ref Timeout
      VpcConfig:
        SecurityGroupIds:
          - !Ref FunctionSecurityGroup
        SubnetIds:
          - !Ref FunctionSubnet
Code language: YAML (yaml)

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

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

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

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

  1. CloudFormationテンプレートで定義した環境変数を、os.environにアクセスして取得する。
  2. Boto3でRDS用クライアントオブジェクトを作成する。
  3. generate_db_auth_tokenメソッドを実行して、IAM認証用のトークンを取得する。
  4. mysql.connector.connectメソッドを実行して、IAM認証でDBインスタンスに接続する。
  5. SELECT文を実行して、テーブル内の全データを取得する。

ポイントはLambdaレイヤーです。
後述するCloudFormationカスタムリソースに関連づいたLambda関数で、自動的に作成します。
今回は2つのレイヤーを作成して、本関数に関連づけます。
一方はMySQL接続用パッケージ用で、もう一方はSSL通信用の証明書用のレイヤーです。

SSL通信用の証明書の設置場所もポイントです。

Lambda 関数にレイヤーを含めると、Lambda は関数実行環境で/optディレクトリにレイヤーコンテンツを抽出します。

関数からレイヤーコンテンツにアクセスする

そして本関数のランタイム環境はPython3.8ですから、/opt/pythonディレクトリに証明書が設置されることになります。

以下が本関数用の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: RDSIamAthenticationPolicy
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Effect: Allow
                Action:
                  - rds-db:connect
                Resource:
                  - !Sub "arn:aws:rds-db:${AWS::Region}:${AWS::AccountId}:dbuser:${DBInstanceResourceId}/${DBIamUsername}"
Code language: YAML (yaml)

VPC内で本関数を実行するために、AWS管理ポリシーAWSLambdaVPCAccessExecutionRoleをアタッチします。

加えてEC2インスタンスと同様に、IAM認証用の設定を行います。

本関数のFunction URLを有効化します。

Resources:
  FunctionUrl:
    Type: AWS::Lambda::Url
    Properties:
      AuthType: NONE
      TargetFunctionArn: !GetAtt Function.Arn

  FunctionUrlPermission:
    Type: AWS::Lambda::Permission
    Properties:
      Action: lambda:InvokeFunctionUrl
      FunctionName: !GetAtt Function.Arn
      FunctionUrlAuthType: NONE
      Principal: "*"
Code language: YAML (yaml)

詳細は以下のページをご確認します。

あわせて読みたい
CFNでLambda Function URL – 認証方式: NONE 【CloudFormationでLambda Function URLを作成する(NONEバージョン)】 2022年4月22日にLambda Function URLがリリースされました。 この新機能は、AWS Lambda サービス...

Lambdaレイヤー

Pythonパッケージ用レイヤー
Resources:
  RequirementsParameter:
    Type: AWS::SSM::Parameter
    Properties:
      Name: !Sub "${Prefix}-customresource-01"
      Type: String
      Value: |
        mysql-connector-python

  LambdaLayer:
    Type: AWS::Lambda::LayerVersion
    DependsOn:
      - CustomResource
    Properties:
      CompatibleArchitectures:
        - !Ref Architecture
      CompatibleRuntimes:
        - !Ref Runtime
      Content:
        S3Bucket: !Ref CodeS3Bucket
        S3Key: !Ref LayerS3Key
      Description: !Ref Prefix
      LayerName: !Sub "${Prefix}-customresource-01"

  CustomResource:
    Type: Custom::CustomResource
    Properties:
      ServiceToken: !GetAtt Function.Arn

  Function:
    Type: AWS::Lambda::Function
    Properties:
      Architectures:
        - !Ref Architecture
      Environment:
        Variables:
          LAYER_PACKAGE: !Ref LayerPackage
          REGION: !Ref AWS::Region
          REQUIREMENTS_PARAMETER: !Ref RequirementsParameter
          S3_BUCKET: !Ref CodeS3Bucket
          S3_BUCKET_FOLDER: !Ref Prefix
      Code:
        ZipFile: |
          import boto3
          import cfnresponse
          import os
          import pip
          import shutil
          import subprocess

          layer_package = os.environ['LAYER_PACKAGE']
          region = os.environ['REGION']
          requirements_parameter = os.environ['REQUIREMENTS_PARAMETER']
          s3_bucket = os.environ['S3_BUCKET']
          s3_bucket_folder = os.environ['S3_BUCKET_FOLDER']

          CREATE = 'Create'
          response_data = {}

          work_dir = '/tmp'
          requirements_file = 'requirements.txt'
          package_dir = 'python'

          requirements_path = os.path.join(work_dir, requirements_file)
          package_dir_path = os.path.join(work_dir, package_dir)
          layer_package_path = os.path.join(
            work_dir,
            layer_package
            )

          def lambda_handler(event, context):
            try:
              if event['RequestType'] == CREATE:
                ssm_client = boto3.client('ssm', region_name=region)
                ssm_response = ssm_client.get_parameter(Name=requirements_parameter)
                requirements = ssm_response['Parameter']['Value']
                #print(requirements)

                with open(requirements_path, 'w') as file_data:
                  print(requirements, file=file_data)

                pip.main(['install', '-t', package_dir_path, '-r', requirements_path])
                shutil.make_archive(
                  os.path.splitext(layer_package_path)[0],
                  format='zip',
                  root_dir=work_dir,
                  base_dir=package_dir
                  )

                s3_resource = boto3.resource('s3')
                bucket = s3_resource.Bucket(s3_bucket)

                bucket.upload_file(
                  layer_package_path,
                  '/'.join([s3_bucket_folder, layer_package])
                  )

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

            except Exception as e:
              print(e)
              cfnresponse.send(event, context, cfnresponse.FAILED, response_data)
      EphemeralStorage:
        Size: !Ref EphemeralStorageSize
      FunctionName: !Sub "${Prefix}-customresource-01"
      Handler: !Ref Handler
      Runtime: !Ref Runtime
      Role: !GetAtt FunctionRole.Arn
      Timeout: !Ref Timeout
Code language: YAML (yaml)

先述の関数では、MySQLに接続するために、mysql(mysql-connector-python)パッケージを使用しましたが、これをLambdaレイヤーとして用意します。

今回はCloudFormationカスタムリソースに関連づくLambda関数を使用して、自動的にLambdaレイヤーを作成します。

SSM Parameter Storeにダウンロードするパッケージのリストを登録します。
先述のパッケージを登録します。

詳細は以下のページをご確認ください。

あわせて読みたい
CFNカスタムリソースでLambdaレイヤーパッケージを準備する – Python版 【CloudFormationカスタムリソースを使って、Python用のLambdaレイヤーパッケージを自動的に作成・配置する】 以下のページでLambdaレイヤーの作成方法について取り上げ...
証明書用レイヤー
Resources:
  UrlsParameter:
    Type: AWS::SSM::Parameter
    Properties:
      Name: !Sub "${Prefix}-customresource-02"
      Type: String
      Value: |
        https://truststore.pki.rds.amazonaws.com/global/global-bundle.pem

  LambdaLayer:
    Type: AWS::Lambda::LayerVersion
    DependsOn:
      - CustomResource
    Properties:
      CompatibleArchitectures:
        - !Ref Architecture
      CompatibleRuntimes:
        - !Ref Runtime
      Content:
        S3Bucket: !Ref CodeS3Bucket
        S3Key: !Ref LayerS3Key
      Description: !Ref Prefix
      LayerName: !Sub "${Prefix}-customresource-02"

  CustomResource:
    Type: Custom::CustomResource
    Properties:
      ServiceToken: !GetAtt Function.Arn

  Function:
    Type: AWS::Lambda::Function
    Properties:
      Architectures:
        - !Ref Architecture
      Environment:
        Variables:
          LAYER_PACKAGE: !Ref LayerPackage
          REGION: !Ref AWS::Region
          URLS_PARAMETER: !Ref UrlsParameter
          S3_BUCKET: !Ref CodeS3Bucket
          S3_BUCKET_FOLDER: !Ref Prefix
      Code:
        ZipFile: |
          import boto3
          import cfnresponse
          import os
          import shutil
          import subprocess
          import urllib

          layer_package = os.environ['LAYER_PACKAGE']
          region = os.environ['REGION']
          urls_parameter = os.environ['URLS_PARAMETER']
          s3_bucket = os.environ['S3_BUCKET']
          s3_bucket_folder = os.environ['S3_BUCKET_FOLDER']

          CREATE = 'Create'
          response_data = {}

          work_dir = '/tmp'
          package_dir = 'python'

          package_dir_path = os.path.join(work_dir, package_dir)
          layer_package_path = os.path.join(
            work_dir,
            layer_package
            )

          ssm_client = boto3.client('ssm', region_name=region)
          s3_client = boto3.client('s3', region_name=region)

          def lambda_handler(event, context):
            try:
              if event['RequestType'] == CREATE:
                ssm_response = ssm_client.get_parameter(Name=urls_parameter)
                urls = ssm_response['Parameter']['Value']

                result = subprocess.run(
                  ['mkdir', package_dir_path],
                  stdout=subprocess.PIPE,
                  stderr=subprocess.PIPE
                )

                for url in urls.splitlines():
                  print(url)
                  file_name = os.path.basename(url)
                  download_path = os.path.join(package_dir_path, file_name)

                  data = urllib.request.urlopen(url).read()

                  with open(download_path, mode='wb') as f:
                    f.write(data)

                shutil.make_archive(
                  os.path.splitext(layer_package_path)[0],
                  format='zip',
                  root_dir=work_dir,
                  base_dir=package_dir
                )

                s3_client.upload_file(
                  layer_package_path,
                  s3_bucket,
                  os.path.join(s3_bucket_folder, layer_package)
                )

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

            except Exception as e:
              print(e)
              cfnresponse.send(event, context, cfnresponse.FAILED, response_data)
      EphemeralStorage:
        Size: !Ref EphemeralStorageSize
      FunctionName: !Sub "${Prefix}-customresource-02"
      Handler: !Ref Handler
      Runtime: !Ref Runtime
      Role: !GetAtt FunctionRole.Arn
      Timeout: !Ref Timeout
Code language: YAML (yaml)

先述の関数では、IAM認証を行うために証明書を使用しました。
これをLambdaレイヤーとして用意します。

こちらもCloudFormationカスタムリソースに関連づくLambda関数を使用して、自動的にLambdaレイヤーを作成します。

SSM Parameter StoreにダウンロードするファイルのURLリストを登録します。
先述の証明書のURLを登録します。

詳細は以下のページをご確認ください。

あわせて読みたい
CFNカスタムリソースを使ってLambdaレイヤーパッケージを準備する – 一般ファイル版 【CFNカスタムリソースを使ってLambdaレイヤーパッケージを準備する – 一般ファイル版】 以下のページで、Python用Lambdaレイヤーのパッケージを自動的に作成する方法を...

環境構築

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

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

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

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

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

  • EC2インスタンス:i-0a625917a26ccc6ae
  • Lambda関数:fa-128-function
  • Lambda関数のFunction URL:https://wndut2cyprxccm4cxjpm5or7bm0takbo.lambda-url.ap-northeast-1.on.aws/
  • Lambdaレイヤー1:fa-128-customresource-01
  • Lambdaレイヤー2:fa-128-customresource-02
  • DB初期化処理用Lambda関数:fa-128-customresource-03
  • DBインスタンス:fa-128-dbinstance
  • DBインスタンスのエンドポイント:fa-128-dbinstance.cl50iikpthxs.ap-northeast-1.rds.amazonaws.com

AWS Management Consoleから各リソースを確認します。

まずDBインスタンスを確認します。

Detail of RDS 1.

正常にDBインスタンスが作成されています。
そしてIAM認証が有効化されていることも確認できます。

Lambdaレイヤーを確認します。

Detail of Lambda 1.
Detail of Lambda 2.

確かに2つのレイヤーが作成されていることがわかります。

DBインスタンスの初期化処理用のLambda関数の実行結果を確認します。

Detail of Lambda 3.

確かにCloudFormationカスタムリソースによって、本関数が実行されました。
そして本関数内でDBの初期化処理用のSQL文が実行されたことがわかります。

動作確認

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

EC2インスタンスからDBインスタンスにIAM認証でアクセス

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

% aws ssm start-session --target i-0a625917a26ccc6ae
...
sh-4.2$Code language: Bash (bash)

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

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

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

sh-4.2$ sudo 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

sh-4.2$ ls -l /*.pem
-rw-r--r-- 1 root root 174184 May  3 00:27 /global-bundle.pemCode language: Bash (bash)

正常にMySQLクライアントパッケージがインストールされていることがわかります。
またSSL通信用の証明書についてもダウンロードされています。

確認が済みましたので、IAM認証でDBインスタンスにアクセスします。

sh-4.2$ mysql \
--host=fa-128-dbinstance.cl50iikpthxs.ap-northeast-1.rds.amazonaws.com \
--port=3306 \
--ssl-ca=/global-bundle.pem \
--default-auth=mysql_clear_password \
--user=iamuser \
--password=`aws rds generate-db-auth-token \
  --hostname fa-128-dbinstance.cl50iikpthxs.ap-northeast-1.rds.amazonaws.com \
  --port 3306 \
  --username iamuser \
  --region ap-northeast-1`
...
MySQL [(none)]>Code language: Bash (bash)

正常にアクセスすることができました。

アクセス用のコマンドは以下のページを参考にしました。

https://docs.aws.amazon.com/ja_jp/AmazonRDS/latest/UserGuide/UsingWithRDS.IAMDBAuth.Connecting.AWSCLI.html

なお今回はMariaDB用のクライアントパッケージを使用しているため、上記のページで言及されているenable-cleartext-pluginオプションが存在しません。
ですから代わりに「–default-auth=mysql_clear_password」オプションを使用します。

https://mariadb.com/kb/en/pluggable-authentication-overview/

なんかいろいろと
aws rds の iam 認証 を試してみた時のメモ。 - なんかいろいろと MySQL および Amazon Aurora に対する IAM データベース認証 - Amazon Relational Database Service という資料にある通り、AWSのRDSでは IAM 認証を利用してDBにアクセス...

試しにテストテーブルに対して、SELECT文を実行します。

MySQL [(none)]> use tutorial;
...
Database changed

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: SQL (Structured Query Language) (sql)

確かにSELECT文を実行することができました。

このようにIAM認証でEC2インスタンスからDBインスタンスにアクセスし、操作することができます。

Lambda関数からDBインスタンスにIAM認証でアクセス

Detail of Lambda 4.

上記の関数を実行して動作を確認します。
関数の実行は、Function URLにアクセスして行います。

$ curl https://wndut2cyprxccm4cxjpm5or7bm0takbo.lambda-url.ap-northeast-1.on.aws/
[
  [
    1,
    "Mercury"
  ],
  [
    2,
    "Venus"
  ],
  [
    3,
    "Earth"
  ],
  [
    4,
    "Mars"
  ],
  [
    5,
    "Jupiter"
  ],
  [
    6,
    "Saturn"
  ],
  [
    7,
    "Uranus"
  ],
  [
    8,
    "Neptune"
  ]
]Code language: Bash (bash)

正常に応答がありました。
DBインスタンスのテストテーブルの内容が返ってきました。

このようにIAM認証でLambda関数からDBインスタンスにアクセスし、操作することができます。

まとめ

EC2インスタンスおよびLambda関数から、IAM認証でRDSにアクセスする方法を確認しました。