定期的に最新データに生まれ変わる開発用DBを作成する


#目的
以下の図のようにAWS上で、開発者全員で使える開発DBを作成する。
![スクリーンショット 2021-09-18 18.16.47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/664524/aa4dd4c8-4b2a-2a91-6a71-3d1b100802b9.png)

## 開発DBで行いたいこと
■ 本番に近いデータで開発のテストを行いたい。
  ◦ メリット
   ・ 想定外のデータの存在によるバグの発生をなくす。
   ・ パフォーマンスの悪い箇所に気づきやすくする。

■ 本番に近いデータで不具合の再現などを行いたい。
  ◦ メリット　
   ・ 本番データでしか発生しない不具合の調査スピードを上げる。
   ・ 本番に近い自由にいじれるデータがあれば、
     新しく入ってきたエンジニアのキャッチupや、ビジネスサイドとのコミニュケーションに役立つ。

#実現方法
単純に本番DBから開発用DBを作る方法は以下の二つが挙げられる。
① 新しくDBを立ち上げて本番DBからダンプし、マスキング。
② AWSを使ってるなら、RDS or EC2のスナップショット,AMIから復元、マスキング。　

どちらも作業自体は、数十分もかからないと思うが
その数十分を後回しにしてしまい、気づいたら数年経ってました。
なんてことは人手不足の企業ではよくあることだと思う。

そこで今回は、定期的に本番DBから開発DBを立ち上げる作業を自動化するところまで行う。

弊社の本番DBは、RDSにまだ移行できておらずEC2上で稼働していて、
毎晩本番DBのEC2からAMIを作成しバックアップをとっている。
これを利用した②の方法を採用する。
②の作業をAWS Lamdbaを使って自動化する。

定期的に開発DB新しくを新しくする頻度だが、
とりあえず週一で運用してみる、以下図のように毎週土曜日に開発DBを新しくする。

開発DBに追加したテーブル、テストデータが消えてしまうというデメリットは、
現在既に運用中の別のDBを使うことで補う。


![スクリーンショット 2021-09-18 20.22.01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/664524/a2f3876e-58da-f774-ef82-ac3caba26dbb.png)



##システム構成

![スクリーンショット 2021-09-18 23.39.02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/664524/76a2433d-9885-2a5f-f72c-af3b06b3494e.png)


上記の図のような構成の開発DBを週一回lamdbaで新しくする。
Cloud Watch Eventsを使うと、簡単にlamdbaを定期実行できる。
![スクリーンショット 2021-09-18 21.02.19.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/664524/a4ac1707-d61b-c7fc-79cb-f39f665ce01f.png)

### Lamdbaの処理の流れ

Lamdbaの行う処理は大まかに以下のようになる。

① 過去の開発DBを削除
　　
② バックアップされた最新のAMIから新たな開発DB立ち上げ

③ 外部からのアクセスを遮断するようなSecurity Groupを開発DBにアタッチ

④ SSM経由で開発DBにマスキングを行うコマンドを実行

⑤ 踏み台サーバーからもアクセスできるようになSecurity Groupを開発DBにアタッチ

⑥ privateドメインの向き先をnew開発DBのprivateIPに変更。

## 各処理の詳細

Lamdbaの実装は、rubyで行う。
以下参考
https://docs.aws.amazon.com/sdk-for-ruby/v3/api/

### ① 過去の開発DBを削除　　
過去の開発DBを削除する。
インスタンスの識別は、Nametagで行う。
今回は、Name:exmapleDevDB　とする。

```ruby

def terminate_old_dev_db(ec2_client=Aws::EC2::Client.new( region: '*****'))
   resp =  ec2_client.describe_instances({
             filters: [{ name: "tag:Name", 
                      values: ["exmapleDevDB"] } ]  
            })
   #何らかの理由で既に停止済みの場合returnする。          
   if resp.reservations[0].instances[0].count.positive? &&
      resp.reservations[0].instances[0].state.name == 'terminated'
      puts 'The instance is already terminated.'
      return true
   end
   instance_id = resp.reservations[0].instances[0].instance_id
   ec2_client.terminate_instances(instance_ids: [instance_id])
   #インスタンスが終了するまで処理を遅延させる。　
   ec2_client.wait_until(:instance_terminated, instance_ids: [instance_id])
   puts 'Instance terminated.'
   return true
      
end
```
### ② バックアップされた最新のAMIから新たな開発DB立ち上げ
弊社では、別で本番のDBのAMIをバックアップするLamdbaが動いてるので、
そこで作られるAMIを探して、そこからインスタンスを立ち上げる。
また、インスタンスが初期化されるまで処理を遅延させるのは、
インスタンスが初期化してからでないと、④のコマンドが実行できない為である。

```ruby
ec2_client=Aws::EC2::Client.new( region: '*****')
ec2_resource = Aws::EC2::Resource.new(region: '*****')

image_id = latest_pro_db_image(ec2_client)
create_dev_db(ec2_resource, ec2_client, image_id)
```

```ruby
def latest_pro_db_image(ec2_client)
    image_name = "**********_#{date.to_s}" 
    resp = ec2_client.describe_images({
                   owners: ['********'],
                     filters: [
                         { name: "name",
                          values: [image_name]}]
            })   
    image_id = resp.images[0].image_id 
end

def create_dev_db(ec2_resource, ec2_client, image_id)
  instance = create_instances(ec2_resource)
  instance_id = instance.first.id
  # インスタンスが起動するまで処理を遅延
  ec2_client.wait_until(:instance_running, instance_ids: [instance_id]) 
  # インスタンスが初期化されるまで処理を遅延
  wait_until_instance_has_inited(ec2_client, instance_id)
  attach_tag(instance)
  return instance_id
end

def create_instances(ec2_resource)
  instance = ec2_resource.create_instances(
    block_device_mappings: [
     { 
      device_name: "/dev/sda1",
      ebs: { delete_on_termination: true} 
     }
    ],
    image_id: image_id,
    min_count: 1,
    max_count: 1,
    key_name: '*****',
    instance_type: '****',
    security_group_ids: ['sg-*****','sg-*****'],
    subnet_id: '******',
  )
end

def wait_until_instance_has_inited(ec2_client, instance_id)
  ec2info = ec2_client.describe_instance_status({"instance_ids" => [instance_id]})
  instance_status = ec2info.instance_statuses[0].instance_status.details[0].status rescue nil
  system_status  = ec2info.instance_statuses[0].system_status.details[0].status  rescue nil

  polls = 0
  while instance_status != "passed" || system__status != "passed" do
    ec2info = ec2_client.describe_instance_status({"instance_ids" => [instance_id]})
    instance_status = ec2info.instance_statuses[0].instance_status.details[0].status rescue nil
    system_status  = ec2info.instance_statuses[0].system_status.details[0].status  rescue nil
    polls += 1
    raise "instance status is still bad" if polls >= 40
    sleep (15)
  end
  
end

def attach_tag(instance)
  instance.batch_create_tags(
    tags: [
      {
        key: 'Name',
        value: 'exmapleDevDB'
      }
    ]
  )
end
```

### ③ 外部からのアクセスを遮断するようなSecurity Groupを開発DBにアタッチ
マスキング前のデータに開発者がアクセスできないように対策する必要がある。
外部からのアクセスを遮断するようなSecurity Group を作成して、
以下メソッドにidをハードコーディングする。

```ruby
def block_access
 instance.modify_attribute({groups: [sg_id]})
end
```
### ④ SSM経由で開発DBにマスキングを行うコマンドを実行
SSMは、SSHログインなしでec2でコマンドを実行できるサービスである。

以下記事でlamdbからSSMを使いec2でコマンドを実行する方法をまとめている。
https://qiita.com/iizukapynyo/items/d7e2d71284cc5c9f4d75

マスキングのsqlをshでラップしてコマンドで実行できる形にする。

しかし、本番DBサーバーに個人情報を全て消してしまうスクリプトを置くなんて怖すぎるので
githubにあげて、毎度cloneしてくる。
sqlをRDBMSに向けて実行するshスクリプトでも、**本番に向けては実行できない**ような制御を入れる。 

なおSSMで実行したコマンドの実行タイムアウト時間は一時間なので
マスキングがそれを超える場合は、ドキュメントを変更する必要がある。

実はここで結構詰まった。。。
以下の記事が参考になりました。ありがとうございました！
https://qiita.com/quwtoy/items/0c2dce3897b3a83c7216

```ruby
ec2_client=Aws::EC2::Client.new( region: '*****')
def send_mask_command_to_dev_db(client)
    commands = ["git clone https://***** && sh /***/mask.sh"]
    
    resp = client.send_command({ instance_ids: [instance_id],
                                 document_name: "AWS-RunShellScript",
                                 comment: "Comment",
                                 parameters: {
                                  "commands" => commands
                                 } })
    return resp                             
    
end
```



以下⑤,⑥はLamdbaの実行時間は15分が限界のため、
別の処理に分ける必要がある。
⑤,⑥を実行するトリガーは例えば、以下のA,B

A. ④のマスキング終了後shからcurlコマンド実行　→ API GateWay　→ lamdba
B. マスキングの実行時間を測定し、バッファを持たせて時間差で実行。

今回は時間がなかったので、Bで実装したが、
マスキングの実行時間は日々増えていくので、Aで実装する必要がある。
追ってAの流れで実行できるように対応を入れる。　

### ⑤ 踏み台サーバーからもアクセスできるようになSecurity Groupを開発DBにアタッチ
③と同様、適切なSecurity Groupをアタッチ。


### ⑥ privateドメインの向き先をnew開発DBのprivateIPに変更。
これで踏み台サーバー経由で開発DBにアクセスできる。
開発者は、踏み台サーバーから同じドメインでアクセスすればいいので
毎週設定を変える必要はない。

弊社ではDockerを使ってrailsアプリケーションを開発している。
dokcer上のrailsから踏み台サーバー経由でdbにアクセスする方法は以下にまとめました。
https://qiita.com/iizukapynyo/items/4da57ce2d4cb8b061b13

```ruby
route_client=Aws::EC2::Client.new( region: '*****')

def chage_local_domain_target(route_client,new_private_ip)
  resp = route_client.change_resource_record_sets({
  change_batch: {
    changes: [
      {
        action: "UPSERT", 
        resource_record_set: {
          name: "dev-db.exmaple", 
          resource_records: [
            {
              value: new_private_ip , 
            }, 
          ], 
          type: "A",
          ttl: 300
        }, 
      }, 
    ]
  }, 
  hosted_zone_id: "*********", 
})
end
```



##あとがき

リフレッシュする期間などは、運用しながら柔軟に変更していく。
なお開発DBを本番と同期させる方法として以下cookpad社の記事のように
レプリケーションを用いて常に同期させる、という方法もあるようだ。

しかしこの方法では、
弊社のアプリケーションでは
本番環境、開発環境の二箇所からの変更で整合性の無いデータが出来上がってしまう場合があると考え採用しなかった。

https://techlife.cookpad.com/entry/2014/10/03/110806