AWS CDKを活用したRDS IAM認証設定とメンテナンス用EC2作成の自動化

はじめに

先日、RDSに対してIAM認証の設定をしたのですが、手作業で作業をやったこともあり思いのほか手こずったので、CDKをつかって実装してみることにしました。

今回実装したのは、IAM認証を有効にしたRDS(PostgreSQL)とそれをメンテナンスするプライベートサブネットにおいたEC2を作るCDK(TypeScript)です。

実装例

VPC関連設定

何はともあれVPCとサブネットを作成します。

    const vpc = new Vpc(this, 'Vpc', {
      vpcName: 'MyVPC',
      maxAzs: 2,
      natGateways: 0,
      subnetConfiguration: [
        {
          // EC2用
          cidrMask: 24,
          name: 'ec2',
          subnetType: SubnetType.PRIVATE_ISOLATED
        },
        {
          // RDS用
          cidrMask: 24,
          name: 'rds',
          subnetType: SubnetType.PRIVATE_ISOLATED
        }
      ]
    });

今回はNATGatewayを作りませんでしたが、作る場合は、natGateways:の記述を削除したのち、subnetConfigurationにパブリックサブネットを追記する必要があります。

NATGatewayを作らない場合、EC2に対してソフトのインストールやパッチ適用はどうするのか不安に思われる方もいるかもしれませんが、以下のre:PostにあるようにS3バケットにパッチが含まれているので標準の範囲内であればNATGatewayがなくても運用できます。

repost.aws

後々の処理でEC2用とRDS用のサブネットを指定するために以下の定数を設定しておきます。

    const ec2Subnet = vpc.selectSubnets({
      subnetGroupName: 'ec2'
    });
    const dbSubnet = vpc.selectSubnets({
      subnetGroupName: 'rds'
    });

次にセキュリティグループを作成します。EC2からSSM用のエンドポイントに向けてHTTPSを通す必要があるのでその許可設定を行なっておきます。

    // EC2用セキュリティグループ
    const ec2SecurityGroup = new SecurityGroup(this, 'EC2SecurityGroup', {
      securityGroupName: 'EC2SecurityGroup',
      vpc: vpc,
      allowAllOutbound: true
    });

    // SSMエンドポイント用セキュリティグループ
    const ssmSecurityGroup = new SecurityGroup(this, 'SSMSecurityGroup', {
      securityGroupName: 'SSMSecurityGroup',
      vpc: vpc,
      allowAllOutbound: true
    });

    ssmSecurityGroup.addIngressRule(ec2SecurityGroup, Port.HTTPS);

エンドポイントの設定を行います。必要なものは以下のre:Postを参考に。

repost.aws

    vpc.addInterfaceEndpoint('SSMEndpoint', {
      service: InterfaceVpcEndpointAwsService.SSM,
      securityGroups: [ssmSecurityGroup],
      subnets: ec2Subnet
    });
    vpc.addInterfaceEndpoint('SSMMessagesEndpoint', {
      service: InterfaceVpcEndpointAwsService.SSM_MESSAGES,
      securityGroups: [ssmSecurityGroup],
      subnets: ec2Subnet
    });
    vpc.addInterfaceEndpoint('EC2MessagesEndpoint', {
      service: InterfaceVpcEndpointAwsService.EC2_MESSAGES,
      securityGroups: [ssmSecurityGroup],
      subnets: ec2Subnet
    });

パッチ取得用のGatewayタイプのVPCエンドポイントも忘れずに作成しておきます。

    // GatewayタイプのVPCエンドポイントを作成
    vpc.addGatewayEndpoint('S3Endpoint', {
      service: GatewayVpcEndpointAwsService.S3,
      subnets: [ec2Subnet],
    });

IAMロール

手作業でちょっとハマったのがIAMロール設定です。AmazonSSMManagedInstanceCoreを設定します。また、アクセスログの保存用にCloudWatch Logsに対する権限も必要です。

    const ec2Role = new Role(this, "EC2Role", {
      assumedBy: new ServicePrincipal("ec2.amazonaws.com"),
      roleName: "EC2Role",
      description: "IAM Role for EC2"
    });
    // IAMロールにSSMのポリシーをアタッチ
    ec2Role.addManagedPolicy(
      ManagedPolicy.fromAwsManagedPolicyName(
        "AmazonSSMManagedInstanceCore"
      )
    );
    // アクセスログの保存用にCloudWatch Logsのアクセスポリシーをアタッチ
    ec2Role.addManagedPolicy(
      ManagedPolicy.fromAwsManagedPolicyName(
        "CloudWatchLogsFullAccess"
      )
    );

なお、SSMにてアクセスログを保存させる設定もCDKで実装しようかなと思っていたのですが、現状ではコンソール上でしか設定できなさそうでした。

EC2を作成

ここまで準備できたら、EC2の作成は非常に簡素な設定となります。

    const ec2 = new Instance(this, "MyEC2", {
      instanceType: InstanceType.of(
        InstanceClass.T3, InstanceSize.MICRO
      ),
      machineImage: MachineImage.latestAmazonLinux2023(),
      vpc: vpc,
      vpcSubnets: ec2Subnet,
      role: ec2Role,
    });

あらかじめインストールするソフトが決まっているのであれば、以下の記述を追加して、EC2インスタンス作成時にuserDataプロパティに指定しても良いかもしれません。

    const userData = UserData.forLinux({
      shebang: '#!/bin/bash',
    });
    userData.addCommands(
      'dnf install -y mariadb105',
      'dnf install -y postgresql15'
    );

RDS

RDSの作成において特筆すべき点は2点。一つはiamAuthenticationプロパティをtrueにすることです。

    const rdsInstance = new DatabaseInstance(this, "MyRDSInstance", {
      instanceIdentifier: "MyRDSInstancePostgreSQL",
      vpc: vpc,
      engine: DatabaseInstanceEngine.POSTGRES,
      instanceType: InstanceType.of(InstanceClass.T3, InstanceSize.MICRO),
      databaseName: "foobar",
      multiAz: false,
      subnetGroup: dbSubnetGroup,
      securityGroups: [rdsSG],
      iamAuthentication: true,
      credentials: Credentials.fromSecret(secret)
    });

二つ目は、ポリシーの追加です。

以前は以下のようにPolicyStatementを書いていたのですが...

    ec2Role.addToPolicy(
      new PolicyStatement({
        actions: [
          "rds-db:connect"
        ],
        resources: [
          `arn:aws:rds-db:${Aws.REGION}:${Aws.ACCOUNT_ID}:dbuser:${rdsInstance.instanceResourceId}/myiam_db_user`
        ]
      })
    );

今はgrantConnectというメソッドを利用するとよいです(myiam_db_userがIAM認証対象ユーザー)。

docs.aws.amazon.com

rdsInstance.grantConnect(ec2Role, "myiam_db_user");

最後にEC2からRDSへの通信許可を設定してあげます。

    rdsInstance.connections.allowDefaultPortFrom(ec2, "allow connect from ec2");

CDKでやる内容は以上で終わりです。

RDSでの設定

RDSでの設定は以下のことをやります。

  • IAM認証対象ユーザーを作る
  • 権限付与
  • 接続テスト

IAM認証対象ユーザーの作成はRDSに対して上記で作成したEC2からpsqlで接続して、以下のコマンドを打ちます。

foobar=> CREATE USER myiam_db_user;
CREATE ROLE
foobar=> GRANT rds_iam TO myiam_db_user;
GRANT ROLE
foobar=>

権限付与は必要なものを適切に。

foobar=> ALTER DEFAULT PRIVILEGES
IN SCHEMA public
GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO myiam_db_user;
ALTER DEFAULT PRIVILEGES
foobar=>

あとはIAM認証対象ユーザーでログインできるか試みます。

sh-5.2$ export RDSHOST=RDSのエンドポイント
sh-5.2$ export PGPASSWORD="$(aws rds generate-db-auth-token --hostname $RDSHOST --port 5432 --region ap-northeast-1 --username myiam_db_user)"
sh-5.2$ psql "host=$RDSHOST port=5432 dbname=foobar user=myiam_db_user password=$PGPASSWORD"
psql (15.12, server 17.2)
WARNING: psql major version 15, server major version 17.
         Some psql features might not work.
SSL connection (protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384, compression: off)
Type "help" for help.

foobar=>

考察

RDSに対してIAM認証の設定を実施することをCDKにて実装してみました。単純にIAM認証のプロパティをtrueにするだけでなく、メンテナンス用のEC2を用意して安全にメンテナンスできるようにしているのが工夫点です。このとき、VPCエンドポイントの設定が地味に面倒なのですが、CDKで実装しておけば間違いも起きにくいと考えます。

また、RDSに対するrds-db:connectアクションの設定がgrantConnectメソッドで実装できるようになっており、CDK活用のメリットがまた一つ加わったと考えています。