CloudFormationを使ってAmazon RDSのインスタンスをスナップショットから立ち上げてみました

2023.12.19

初めに

Amazon RDSはスナップショットの復元は別アカウントからのデータ移行や、バックアップからの復元、様々なケースで利用されます。

個人的にはバックアップからの復元や既存ののデータを利用した一時検証環境などを使うケースで利用することが多く、その関係IaCで管理する環境に含めることが少なく今まで手動で立ち上げしかやったことのない対応でした。

直近別環境から既存RDSのスナップショットを利用して新規環境を立ち上げる対応でスナップショットから立ち上げる対応をCloudFormatinoで書く機会があったので備忘録として残しておきます。

別アカウントからのスナップショット復元

本筋からは少し外れますが別のアカウントからのスナップショットを利用する場合は、そのアカウントからスナップショットを共有してもらう必要があります。

なお自動バックアップでは共有できないようなので共有が必要な場合は自動バックアップとは別に手動でスナップショットを取得し、そちらを共有する必要があります。

その他詳細は上記AWSドキュメントページをご参照ください。

スナップショットからの立ち上げ方

今回は非Aurora環境から試しますがこの場合はAWS::RDS::DBInstanceDBSnapshotIdentifierに対してスナップショットのARNを指定することでそのスナップショットからのインスタンスの立ち上げが可能です。

Auroraを利用する場合はAWS::RDS::DBClusterSnapshotIdentifierとなりそうですが、DBInstanceの方にもDBClusterSnapshotIdentifierというパラメータがあるのでこの辺りの指定は改めて機会がありましたら確認してみます。

DBSnapshotIdentifierの指定変更に関する注意事項

AWS::RDS::DBInstanceに記載がありますが一度指定した後の変更には注意が必要です。

After you restore a DB instance with a DBSnapshotIdentifier property, you can delete the DBSnapshotIdentifier property. When you specify this property for an update, the DB instance is not restored from the DB snapshot again, and the data in the database is not changed. However, if you don't specify the DBSnapshotIdentifier property, an empty DB instance is created, and the original DB instance is deleted. If you specify a property that is different from the previous snapshot restore property, a new DB instance is restored from the specified DBSnapshotIdentifier property, and the original DB instance is deleted.

つまりのところDBSnapshotIdentifierでしているする復元元のスナップショットの識別子(ARN)の値が指定された場合は(そのスナップショットで再作成が必要なため)置換処理がかかるようです。
ただし指定がある状態から指定を削除した場合は特に何も処理が行われない形になるとのことです(現状維持)。

削除保護をつけておくことで防止はできますが誤って指定を変えてしまった場合に元のDBを吹き飛ばす可能性があるので一応注意しましょう。

テンプレート

CloudFormationテンプレートは以下のようになります。
今回はDBSnapshotArnを空とすることでDBSnapshotIdentifierAWS::NoValue(すなわちパラメータ無し)として扱われるようになっております。

今回の対応は以下の流れで対応を行います。
CloudFormationで新規立ち上げ->DB内容変更->スナップショット作成->DB内容変更->スナップショット指定追加(スナップショット復元)->スナップショット指定削除(空のDB作成)

VPC等一部のリソースは毎回作成するのは煩雑でベースになるものを事前にあるのでそちらを利用しています。バージョンのベタ書き等がありますがその点は目を瞑っていただければと思います。

AWSTemplateFormatVersion: 2010-09-09
Parameters:
  Env:
    Type: String
  Prefix:
    Type: String
  DeleteProtection:
    Type: String
    AllowedValues:
      - True
      - False
  EnableMultiAZ:
    Type: String
    AllowedValues:
      - True
      - False
  VPCId:
    Type: String
  RDSSGId:
    Type: String
  SubnetGroupId:
    Type: String
  DBSnapshotId:
    Type: String
  DBInstanceClass:
    Type: String
  RDSStorageSize:
    Type: Number
Conditions:
  SpecifySnapshot: !Not [ !Equals [ !Ref DBSnapshotId, ""]]
Resources:
  #----------------------------------
  #----- RDS Instance
  #----------------------------------
  DBInstance: 
    Type: AWS::RDS::DBInstance
    Properties:
      Engine: postgres
      MasterUsername: postgres
      AllocatedStorage: !Ref RDSStorageSize
      StorageType: gp3
      # 後述しますがこの設定が原因で後々失敗します
      DBInstanceIdentifier: !Sub ${Env}-${Prefix}
      MasterUserPassword: !Sub "{{resolve:secretsmanager:${RDSMasterUserPassword}:SecretString:password::}}"
      AvailabilityZone: !Sub ${AWS::Region}a
      DBInstanceClass: !Ref DBInstanceClass
      AutoMinorVersionUpgrade: False
      EnablePerformanceInsights: True
      MultiAZ: !Ref EnableMultiAZ
      CACertificateIdentifier: "rds-ca-rsa4096-g1"
      CopyTagsToSnapshot: True
      DBSubnetGroupName: !Ref SubnetGroupId
      OptionGroupName: !Ref RDSOptionGroup
      DBSnapshotIdentifier:
        Fn::If:
          - SpecifySnapshot
          - !Ref DBSnapshotId
          - !Ref AWS::NoValue
      VPCSecurityGroups: 
        - !Ref RDSSGId
      DBParameterGroupName: !Ref InstanceParameterGroup
      DeletionProtection: !Ref DeleteProtection
      Tags: 
        - Key: Environment
          Value: !Ref Env
  RDSMasterUserPassword:
    Type: AWS::SecretsManager::Secret
    Properties:
      Name: !Sub ${Env}-${Prefix}-master-password
      GenerateSecretString:
        GenerateStringKey: password
        PasswordLength: 20
        ExcludePunctuation: True
        SecretStringTemplate: '{}'
  #----------------------------------
  #----- Parameter Group
  #----------------------------------
  InstanceParameterGroup:
      Type: AWS::RDS::DBParameterGroup
      Properties: 
        DBParameterGroupName: !Sub ${Env}-${Prefix}-instance-pg
        Description: "-"
        Family: postgres15
        Tags: 
          - Key: Environment
            Value: !Ref Env
  #----------------------------------
  #----- Option Group
  #----------------------------------
  RDSOptionGroup:
    Type: AWS::RDS::OptionGroup
    Properties: 
      EngineName: postgres
      MajorEngineVersion: 15
      OptionGroupName: !Sub ${Env}-${Prefix}-og
      OptionGroupDescription: "-"
      Tags: 
        - Key: Environment
          Value: !Ref Env

新規立ち上げ〜データ投入〜スナップショット作成

まずはスナップショットなしで立ち上げを行います。
先ほどのテンプレートでスタックを立ち上げる際にDBSnapshotIdを空、後々再作成を行う関係でDeleteProtectionをfalseにして削除保護をつけないようにして起動します。

起動後接続を行い適当なテストデータを作っておきます。

postgres=> select version();
                                                 version
---------------------------------------------------------------------------------------------------------
 PostgreSQL 15.4 on x86_64-pc-linux-gnu, compiled by gcc (GCC) 7.3.1 20180712 (Red Hat 7.3.1-12), 64-bit
(1 row)

postgres=> CREATE TABLE history(id serial, text text, create_date timestamp DEFAULT now());
CREATE TABLE
postgres=> INSERT INTO history(text) SELECT md5(generate_series::text) FROM generate_series(1,20);
INSERT 0 20
postgres=> SELECT * FROM history;
 id |               text               |        create_date
----+----------------------------------+----------------------------
  1 | c4ca4238a0b923820dcc509a6f75849b | 2023-12-19 08:41:54.345255
  2 | c81e728d9d4c2f636f067f89cc14862c | 2023-12-19 08:41:54.345255
  3 | eccbc87e4b5ce2fe28308fd9f2a7baf3 | 2023-12-19 08:41:54.345255
  4 | a87ff679a2f3e71d9181a67b7542122c | 2023-12-19 08:41:54.345255
  5 | e4da3b7fbbce2345d7772b0674a318d5 | 2023-12-19 08:41:54.345255
  6 | 1679091c5a880faf6fb5e6087eb1b2dc | 2023-12-19 08:41:54.345255
  7 | 8f14e45fceea167a5a36dedd4bea2543 | 2023-12-19 08:41:54.345255
  8 | c9f0f895fb98ab9159f51fd0297e236d | 2023-12-19 08:41:54.345255
  9 | 45c48cce2e2d7fbdea1afc51c7c6ad26 | 2023-12-19 08:41:54.345255
 10 | d3d9446802a44259755d38e6d163e820 | 2023-12-19 08:41:54.345255
 11 | 6512bd43d9caa6e02c990b0a82652dca | 2023-12-19 08:41:54.345255
 12 | c20ad4d76fe97759aa27a0c99bff6710 | 2023-12-19 08:41:54.345255
 13 | c51ce410c124a10e0db5e4b97fc2af39 | 2023-12-19 08:41:54.345255
 14 | aab3238922bcc25a6f606eb525ffdc56 | 2023-12-19 08:41:54.345255
 15 | 9bf31c7ff062936a96d3c8bd1f8f2ff3 | 2023-12-19 08:41:54.345255
 16 | c74d97b01eae257e44aa9d5bade97baf | 2023-12-19 08:41:54.345255
 17 | 70efdf2ec9b086079795c442636b55fb | 2023-12-19 08:41:54.345255
 18 | 6f4922f45568161a8cdf4ad2299f6d23 | 2023-12-19 08:41:54.345255
 19 | 1f0e3dad99908345f7439f8ffabdffc4 | 2023-12-19 08:41:54.345255
 20 | 98f13708210194c475687be6106a3b84 | 2023-12-19 08:41:54.345255
(20 rows)

マネジメントコンソールから手動でスナップショットを取得します。

後ほどCloudFormationのパラメータのDBSnapshotId(と言いつつARN)を入れるのでスナップショットのARNを控えておきます。

今回は同一アカウントのため前述のような別アカウント共有は行いません。

データ変更〜スナップショット復元

上記のスナップショットで戻したことが確認できるようにさらにデータを追加しておきます。

postgres=> INSERT INTO history(text) SELECT md5(generate_series::text) FROM generate_series(1,20);
INSERT 0 20
postgres=> SELECT * FROM history offset 20;
 id |               text               |        create_date
----+----------------------------------+----------------------------
 21 | c4ca4238a0b923820dcc509a6f75849b | 2023-12-19 08:51:18.906937
 22 | c81e728d9d4c2f636f067f89cc14862c | 2023-12-19 08:51:18.906937
 23 | eccbc87e4b5ce2fe28308fd9f2a7baf3 | 2023-12-19 08:51:18.906937
 24 | a87ff679a2f3e71d9181a67b7542122c | 2023-12-19 08:51:18.906937
 25 | e4da3b7fbbce2345d7772b0674a318d5 | 2023-12-19 08:51:18.906937
 26 | 1679091c5a880faf6fb5e6087eb1b2dc | 2023-12-19 08:51:18.906937
 27 | 8f14e45fceea167a5a36dedd4bea2543 | 2023-12-19 08:51:18.906937
 28 | c9f0f895fb98ab9159f51fd0297e236d | 2023-12-19 08:51:18.906937
 29 | 45c48cce2e2d7fbdea1afc51c7c6ad26 | 2023-12-19 08:51:18.906937
 30 | d3d9446802a44259755d38e6d163e820 | 2023-12-19 08:51:18.906937
 31 | 6512bd43d9caa6e02c990b0a82652dca | 2023-12-19 08:51:18.906937
 32 | c20ad4d76fe97759aa27a0c99bff6710 | 2023-12-19 08:51:18.906937
 33 | c51ce410c124a10e0db5e4b97fc2af39 | 2023-12-19 08:51:18.906937
 34 | aab3238922bcc25a6f606eb525ffdc56 | 2023-12-19 08:51:18.906937
 35 | 9bf31c7ff062936a96d3c8bd1f8f2ff3 | 2023-12-19 08:51:18.906937
 36 | c74d97b01eae257e44aa9d5bade97baf | 2023-12-19 08:51:18.906937
 37 | 70efdf2ec9b086079795c442636b55fb | 2023-12-19 08:51:18.906937
 38 | 6f4922f45568161a8cdf4ad2299f6d23 | 2023-12-19 08:51:18.906937
 39 | 1f0e3dad99908345f7439f8ffabdffc4 | 2023-12-19 08:51:18.906937
 40 | 98f13708210194c475687be6106a3b84 | 2023-12-19 08:51:18.906937
(20 rows)

postgres=> SELECT COUNT(*) FROM history;
 count
-------
    40
(1 row)

先ほど作成したスナップショットのARNをDBSnapshotIdに指定し同一のスタックをアップデートします。

置換条件がConditionになっているのでこの時点では発生するかは断定できません。

アップデートすると失敗してしまいました。
完全に失念していましたがこれはDBInstanceIdentifierを指定していることで置換処理ができないためのエラーとなります。
(RDSの仕組み上同一アカウントリージョン上に同名のDBが作れない為)

識別子のために固定の名前をつけていることも多いかと思いますのでCloudFormationでDBSnapshotIdentifierを差し替えるような型値前提としている場合はDBInstanceIdentifierを未指定にするようにして対応しましょう。

DBInstanceIdentifierの指定を削除したテンプレートで再作成し(DBSnapshotIdの値は空)あらためて先ほどのDBSnapshotIdを指定する形で更新を行います。
(後で冷静に考えるとわざわざ一度消さずにDBInstanceIdentifierの指定を削除してアップデートで十分だったかもしれません)

スタックを更新してみると新しいリソースを作成するメッセージが含まれており置換処理が発生することがわかります。

作成中にDBインスタンスを見に行くと置換前後のインスタンス両方が確認できます。

SELECT文を実行して最初に取ったスナップショットの状態に戻っていることを確認します。

sh-4.2$ psql -h db-snapshot-dbinstance-yflw3q0pvlzb.xxxxxx.ap-northeast-1.rds.amazonaws.com -U postgres
Password for user postgres:
psql (14.8, server 15.4)
WARNING: psql major version 14, server major version 15.
         Some psql features might not work.
SSL connection (protocol: TLSv1.2, cipher: ECDHE-RSA-AES256-GCM-SHA384, bits: 256, compression: off)
Type "help" for help.

postgres=> select * from history;
 id |               text               |        create_date
----+----------------------------------+----------------------------
  1 | c4ca4238a0b923820dcc509a6f75849b | 2023-12-19 08:41:54.345255
  2 | c81e728d9d4c2f636f067f89cc14862c | 2023-12-19 08:41:54.345255
  3 | eccbc87e4b5ce2fe28308fd9f2a7baf3 | 2023-12-19 08:41:54.345255
  4 | a87ff679a2f3e71d9181a67b7542122c | 2023-12-19 08:41:54.345255
  5 | e4da3b7fbbce2345d7772b0674a318d5 | 2023-12-19 08:41:54.345255
  6 | 1679091c5a880faf6fb5e6087eb1b2dc | 2023-12-19 08:41:54.345255
  7 | 8f14e45fceea167a5a36dedd4bea2543 | 2023-12-19 08:41:54.345255
  8 | c9f0f895fb98ab9159f51fd0297e236d | 2023-12-19 08:41:54.345255
  9 | 45c48cce2e2d7fbdea1afc51c7c6ad26 | 2023-12-19 08:41:54.345255
 10 | d3d9446802a44259755d38e6d163e820 | 2023-12-19 08:41:54.345255
 11 | 6512bd43d9caa6e02c990b0a82652dca | 2023-12-19 08:41:54.345255
 12 | c20ad4d76fe97759aa27a0c99bff6710 | 2023-12-19 08:41:54.345255
 13 | c51ce410c124a10e0db5e4b97fc2af39 | 2023-12-19 08:41:54.345255
 14 | aab3238922bcc25a6f606eb525ffdc56 | 2023-12-19 08:41:54.345255
 15 | 9bf31c7ff062936a96d3c8bd1f8f2ff3 | 2023-12-19 08:41:54.345255
 16 | c74d97b01eae257e44aa9d5bade97baf | 2023-12-19 08:41:54.345255
 17 | 70efdf2ec9b086079795c442636b55fb | 2023-12-19 08:41:54.345255
 18 | 6f4922f45568161a8cdf4ad2299f6d23 | 2023-12-19 08:41:54.345255
 19 | 1f0e3dad99908345f7439f8ffabdffc4 | 2023-12-19 08:41:54.345255
 20 | 98f13708210194c475687be6106a3b84 | 2023-12-19 08:41:54.345255
(20 rows)

なお今回初めて知ったのですがリストア復旧の場合はモニタリングのセッティングの後にResetting-master-credentials-messageと言う処理が挟まりこの段階でマスターユーザの情報が変更されるようです。

スナップショット指定削除

DBSnapshotIdの指定を削除しDBSnapshotIdentifierの指定が有りから無しになった際の挙動を見ます。

この場合は特に置換が発生しないことが確認できます。

少しわかりづらいですが更新処理を行った19:32~3頃にはRDS側のイベントは特に発生しておらず、また先のスナップショットからの復元を行った際のログも残っております。

終わりに

終わりに今回はCloudFormationによるスナップショットの復元を試しつつ指定が変動した際の挙動を確認してみました。

一発目の立ち上げを行う場合には今回のような置換処理等が発生せずにスムーズに行えるため便利ではありそうですが、変更するような運用を取る際にはパラメータの指定には少し注意が必要そうです。

CloudFormationでスナップショットを利用したRDSインスタンスの立ち上げは可能ですが置換等を行う場合は条件によっては利用できないことがございますので利用シーンや差し替えのフローを整理の上、場合によっては手動での対応も視野に入れていただければと思います。