Terraform、AWS DMSによるデータベース移行

# はじめに
この技術記事では、Terraform、AWS DMS (Database Migration Service)を使用して、データベース移行を安全かつ効率的に実行する方法を説明します。

Terraformを利用したAWS DMSは、安全かつ効率的なデータベース移行を実現し、コードによるバージョン管理と踏み台サーバーによるRDSインスタンスへの安全な接続、DMSレプリケーション機能による高可用性とスケーラビリティにより、時間と労力を大幅に削減します。

![aws-1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/731268/0a192f9d-3192-b7e2-7ff7-993e481e6a89.png)


この技術記事で解説するコードは、以下の Github リポジトリで公開されています。

https://github.com/sugiyama404/practice_dms

また、以下の記事を参考にしました。

https://qiita.com/neruneruo/items/d327e2e0bf504c4c8c43

https://cloud5.jp/create-dms-handson/#--2

# 使用する AWS サービス

DMS (Database Migration Service)
EC2 (Elastic Compute Cloud)
IAM (Identity and Access Management)
RDS (Relational Database Service)

# 構成概要

+ 踏み台サーバー: EC2 インスタンスを踏み台サーバーとして構築し、RDS インスタンスへの SSH 接続に使用します
+ DMS レプリケーションインスタンス: DMS レプリケーションインスタンスを作成し、ソースデータベースとターゲットデータベース間のデータ移行を管理します
+ ネットワーク: サブネットグループとセキュリティグループを作成し、各コンポーネント間の安全な通信を確保します
+ データ移行タスク: レプリケーショントーナメントとテーブルマッピングを設定し、移行するデータと方法を定義します

# コード

## iam
AWS DMSの通信に必要なIAMロールを作成します。

```terraform:infra/modules/iam/aws_iam_role_policy_attachment.tf
resource "aws_iam_role_policy_attachment" "dms_policy" {
  role       = aws_iam_role.main_role.name
  policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonDMSVPCManagementRole"
}
```

```terraform:infra/modules/iam/aws_iam_role.tf
resource "aws_iam_role" "main_role" {
  name = "dms-vpc-role"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Sid    = ""
        Action = "sts:AssumeRole"
        Effect = "Allow"
        Principal = {
          Service = "dms.amazonaws.com"
        }
        Action = "sts:AssumeRole"
      },
    ]
  })

  tags = {
    Name = "${var.app_name}-app-iam-role"
  }
}
```

## network
TerraformでAWS DMSのsecurity_group設定を正確に行うことは、安全かつスムーズなデータベース移行を実現するために不可欠です。設定に誤りがあると、DMSはエラーを出力し、移行作業が中断してしまう可能性があります。

```terraform:infra/modules/network/security_group.tf
# SecurityGroup for DMS
resource "aws_security_group" "dms_sg" {
  name   = "dms-sg"
  vpc_id = aws_vpc.main.id
  tags = {
    Name = "${var.app_name}-dms-sg"
  }
}

# SecurityGroup for opmng
resource "aws_security_group" "opmng_sg" {
  name   = "opmng-sg"
  vpc_id = aws_vpc.main.id
  tags = {
    Name = "${var.app_name}-opmng-sg"
  }
}

# SecurityGroup for RDS
resource "aws_security_group" "rds_source_sg" {
  name   = "rds-source-sg"
  vpc_id = aws_vpc.main.id
  tags = {
    Name = "${var.app_name}-rds-source-sg"
  }
}

resource "aws_security_group" "rds_target_sg" {
  name   = "rds-target-sg"
  vpc_id = aws_vpc.main.id
  tags = {
    Name = "${var.app_name}-rds-target-sg"
  }
}
```

```terraform:infra/modules/network/security_group_rule.tf
locals {
  opmng_sg_id_list = [
    { type = "ingress", port = "22" },
    { type = "egress", port = "80" },
    { type = "egress", port = "443" },
  ]
}

# SecurityGroupRules for dms
resource "aws_security_group_rule" "dms_out_tcp3306" {
  type              = "egress"
  from_port         = var.db_ports[0].internal
  to_port           = var.db_ports[0].external
  protocol          = var.db_ports[0].protocol
  cidr_blocks       = ["0.0.0.0/0"]
  security_group_id = aws_security_group.dms_sg.id
}

# SecurityGroupRules for opmng
resource "aws_security_group_rule" "opmng_web" {
  for_each = { for i in local.opmng_sg_id_list : i.port => i }

  type              = each.value.type
  from_port         = tonumber(each.value.port)
  to_port           = tonumber(each.value.port)
  protocol          = "tcp"
  cidr_blocks       = ["0.0.0.0/0"]
  security_group_id = aws_security_group.opmng_sg.id
}

resource "aws_security_group_rule" "opmng_out_db" {
  type              = "egress"
  from_port         = var.db_ports[0].internal
  to_port           = var.db_ports[0].external
  protocol          = var.db_ports[0].protocol
  cidr_blocks       = ["0.0.0.0/0"]
  security_group_id = aws_security_group.opmng_sg.id
}

# SecurityGroupRules for source db
resource "aws_security_group_rule" "dbsource_in_tcp3306_from_opmng" {
  type                     = "ingress"
  from_port                = var.db_ports[0].internal
  to_port                  = var.db_ports[0].external
  protocol                 = var.db_ports[0].protocol
  source_security_group_id = aws_security_group.opmng_sg.id
  security_group_id        = aws_security_group.rds_source_sg.id
}

resource "aws_security_group_rule" "dbsource_in_tcp3306_from_dms" {
  type                     = "ingress"
  from_port                = var.db_ports[0].internal
  to_port                  = var.db_ports[0].external
  protocol                 = var.db_ports[0].protocol
  source_security_group_id = aws_security_group.dms_sg.id
  security_group_id        = aws_security_group.rds_source_sg.id
}

# SecurityGroupRules for target db
resource "aws_security_group_rule" "dbtarget_in_tcp3306_from_opmng" {
  type                     = "ingress"
  from_port                = var.db_ports[0].internal
  to_port                  = var.db_ports[0].external
  protocol                 = var.db_ports[0].protocol
  source_security_group_id = aws_security_group.opmng_sg.id
  security_group_id        = aws_security_group.rds_target_sg.id
}

resource "aws_security_group_rule" "dbtarget_in_tcp3306_from_dms" {
  type                     = "ingress"
  from_port                = var.db_ports[0].internal
  to_port                  = var.db_ports[0].external
  protocol                 = var.db_ports[0].protocol
  source_security_group_id = aws_security_group.dms_sg.id
  security_group_id        = aws_security_group.rds_target_sg.id
}

```

## RDS

Terraformモジュールの出力で、aws_db_instance.source-db.address からホスト名のみを取得するには、以下のコードを使用できます。

```terraform:infra/modules/rds/outputs.tf
output "sorce_db_address" {
  value = split(":", "${aws_db_instance.source-db.address}")[0]
}
```

## DMS
DMS インスタンスは、Database Migration Service のインスタンスのことを指します。こちらは、データベース間のデータ移行を管理するサービスです。

### インスタンス

DMS レプリカは、ソースデータベースのデータを複製し、ターゲットデータベースに同期するための論理的な構成です。
vpc_security_group_ids を設定しないと、RDS と通信できず、データ移行が実行できません。

```terraform:infra/modules/dms/aws_dms_replication_instance.tf
resource "aws_dms_replication_instance" "main" {
  replication_instance_id    = "main-dms-replication"
  engine_version             = "3.5.1"
  replication_instance_class = "dms.t3.micro"
  allocated_storage          = 50
  publicly_accessible        = true
  multi_az                   = false
  availability_zone          = "ap-northeast-1a"

  apply_immediately            = true
  auto_minor_version_upgrade   = false
  preferred_maintenance_window = "sun:10:30-sun:14:30"
  replication_subnet_group_id  = aws_dms_replication_subnet_group.main.id
  vpc_security_group_ids       = [var.sg_dms_id]

  tags = {
    Name = "${var.app_name}-main"
  }
}
```

```terraform:infra/modules/dms/aws_dms_replication_subnet_group.tf
resource "aws_dms_replication_subnet_group" "main" {
  replication_subnet_group_description = "replication subnet group"
  replication_subnet_group_id          = "dms-replication-subnet-group"
  subnet_ids = [
    "${var.subnet_private_subnet_1a_id}",
    "${var.subnet_private_subnet_1c_id}",
  ]

  tags = {
    Name = "${var.app_name}-main"
  }

  depends_on = [var.iam_role_policy_attachment_dms_policy]
}
```

### タスク
DMS エンドポイントは、ソースデータベースとターゲットデータベースへの接続情報を設定するための構成要素です。
```terraform:infra/modules/dms/aws_dms_endpoint.tf
# source endpoint
resource "aws_dms_endpoint" "source_endpoint" {
  endpoint_id   = "source-endpoint"
  endpoint_type = "source"
  engine_name   = "mysql"

  username      = var.db_username
  password      = var.db_password
  port          = 3306
  server_name   = var.sorce_db_address
  database_name = var.db_name

  tags = {
    Name = "${var.app_name}-source-endpoint"
  }
}

# target endpoint
resource "aws_dms_endpoint" "target_endpoint" {
  endpoint_id   = "target-endpoint"
  endpoint_type = "target"
  engine_name   = "mysql"

  username      = var.db_username
  password      = var.db_password
  port          = 3306
  server_name   = var.target_db_address
  database_name = var.db_name

  tags = {
    Name = "${var.app_name}-target-endpoint"
  }
}
```

DMS タスクは、レプリカとエンドポイントを使用して、実際にデータ移行を実行するための設定です。

```terraform:infra/modules/dms/aws_dms_replication_task.tf
resource "aws_dms_replication_task" "main" {
  replication_task_id      = "replication-main-task"
  target_endpoint_arn      = aws_dms_endpoint.target_endpoint.endpoint_arn
  source_endpoint_arn      = aws_dms_endpoint.source_endpoint.endpoint_arn
  replication_instance_arn = aws_dms_replication_instance.main.replication_instance_arn
  migration_type           = "full-load"
  table_mappings           = file("./modules/dms/dms_table_mapping.json")

  tags = {
    Name = "${var.app_name}-main"
  }
}
```

schema-nameはデータベース名です。

```terraform:infra/modules/dms/dms_table_mapping.json
{
    "rules": [
        {
            "rule-type": "selection",
            "rule-id": "1",
            "rule-name": "1",
            "object-locator": {
                "schema-name": "todoproject",
                "table-name": "%"
            },
            "rule-action": "include"
        }
    ]
}
```

# 結果

sourceDBのデータ

![Screenshot 2024-04-09 at 16.55.25.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/731268/2a218611-32f4-81f1-f2ca-f51280dec24d.png)

AWSコンソール

![Screenshot 2024-04-10 at 9.10.10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/731268/c90ac806-48b5-90ca-12ee-849c59c92079.png)

targetDBのデータ
バッチリ移行できてます。

![Screenshot 2024-04-10 at 9.12.22.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/731268/32a0b1fe-3d9f-3f21-ef3b-755e326e7d36.png)