0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

はじめてのRDS

0
Last updated at Posted at 2026-09-19

RDS で MySQL を1台立てて、自宅の iMac の Python からつなぎ、テーブルを作って読み書きして、消すところまでやりました。AWS 側の部品は全部 Terraform で作ります。はじめてのTerraform の続きです。

1. この記事でやること

先に、やってみて分かったことを2つ書きます。

分かったこと
1 RDS は、別々のアベイラビリティゾーン1のサブネット2を2つ以上渡さないと作れない。本体は1台なのに、置き場所の候補は2か所以上要る
2 パスワードは Terraform のファイルに書かず、環境変数で渡せる。それでも terraform.tfstate の中には平文で入る

やった順番は、Terraform に部品を3つ書き足す → apply → Python からつなぐ → テーブルを作って読み書き → destroy です。かかった時間とお金の数字は、途中に挟むと話が切れるので、9章にまとめて置きました。

前提を先に書いておきます。

  • Terraform と AWS CLI のインストール・AWS の認証・リージョン(東京)の設定は、前回までの記事で済ませてあります
  • 作業フォルダも前回と同じ ~/Documents/aws-terraform-study で、terraform init 済みです。新しいフォルダで始める場合は、provider の設定と terraform init が先に要ります
  • 専門用語の説明は巻末にまとめました。本文の脚注番号を押すと説明へ飛び、説明の末尾の記号で元の場所へ戻れます

2. RDS とは何か

RDS(Relational Database Service)は、AWS がデータベースサーバーを1台貸すサービスです。EC2 と違って OS のユーザーはもらえず、見えるのはデータベースの口(MySQL なら 3306 番ポート)だけです。

EC2 に自分で MySQL を入れる形と比べると、仕事の分担がこう変わります。

仕事 EC2 に自分で入れる RDS
OS の管理・パッチ 自分 AWS 側が持つ
MySQL のインストール 自分 作成時に選ぶだけ
バックアップの仕組み 自分で組む 設定1つ(今回は切った)
SQL を打つ・テーブル設計 自分 自分

今回選んだものは次の4つです。

項目 選んだもの
エンジン MySQL 8.0
インスタンスクラス(サーバーの大きさ) db.t4g.micro(いちばん小さい)
ディスク gp23 の 20GB(これより小さくはできない)
リージョン 東京(ap-northeast-1)

3. Terraform に書き足した3つの部品

EC2 の回で作った main.tf に、RDS 用の部品を3つ足します。セキュリティグループ、DB サブネットグループ、RDS 本体です。

3-1. セキュリティグループ — 3306 番を自宅からだけ

resource "aws_security_group" "rds" {
  name        = "aws-study-tf-rds-sg"
  description = "mysql from my home only"

  ingress {
    description = "mysql"
    from_port   = 3306
    to_port     = 3306
    protocol    = "tcp"
    cidr_blocks = [var.my_ip]
  }

  egress {
    description = "all outbound"
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

EC2 用のセキュリティグループ(22番だけ開けたもの)がすでにあるので、そこに 3306 番を足せば行数は減ります。分けたのは、同じグループに足すと EC2 を立てたときに 3306 番も一緒に開くからです。穴の一覧は用途ごとに別の部品にしておくと、あとで消すときも1つずつ消せます。

var.my_ip には自宅のグローバル IP が入ります(渡し方は4章で。この記事では例として 203.0.113.45 を使います)。

3-2. DB サブネットグループ — EC2 との一番の違い

RDS には、EC2 のときには無かった部品が1つ要ります。「どのサブネットに置いてよいか」の一覧、DB サブネットグループです。

# 既定の VPC を AWS に聞く。作るのではなく読むだけ
data "aws_vpc" "default" {
  default = true
}

# その VPC の中にある、AZ ごとの既定サブネットを AWS に聞く。東京は 1a/1c/1d の3つ
data "aws_subnets" "default" {
  filter {
    name   = "vpc-id"
    values = [data.aws_vpc.default.id]
  }
  filter {
    name   = "default-for-az"
    values = ["true"]
  }
}

# RDS を置いてよい場所の一覧。RDS は 2つ以上の AZ を渡さないと作れない
resource "aws_db_subnet_group" "study" {
  name        = "aws-study-tf-db-subnet-group"
  description = "subnets for the study RDS"
  subnet_ids  = data.aws_subnets.default.ids
}

RDS 本体は1台しか作らないのに、別々のアベイラビリティゾーン(AZ)のサブネットを2つ以上渡さないと、作成そのものを断られます。EC2 は「このサブネットに置く」と1か所指せば済んでいました。

理屈はこうです。RDS は故障や保守のときに別の AZ へ動かせる作りになっていて、その引っ越し先を最初から申告しておく決まりになっています。実際に配置されるのは1か所だけです。

もう1つ、サブネットの ID(subnet-0a1b2c3d… のような値)を直接書かず、既定の VPC4 から data で聞く形にしました。data は「作る」ではなく「読む」だけの書き方です。ID はアカウントごとに違うので、直接書くとそのままでは他の人の環境で動きません。

3-3. RDS 本体

# RDS 本体。MySQL 8.0 を1台。⛔ ここから先は apply すると課金が始まる
resource "aws_db_instance" "study" {
  identifier     = "aws-study-tf-db"
  engine         = "mysql"
  engine_version = "8.0"
  instance_class = "db.t4g.micro"

  # ディスクの大きさ。20GB より小さくはできない
  allocated_storage = 20
  storage_type      = "gp2"

  # 中に最初から作っておくデータベースの名前
  db_name  = "studydb"
  username = "admin"
  password = var.db_password

  # 3-1 で作ったセキュリティグループと、3-2 で作ったサブネットの一覧を指す
  vpc_security_group_ids = [aws_security_group.rds.id]
  db_subnet_group_name   = aws_db_subnet_group.study.name

  # iMac から直接つなぐので、インターネット側に口を出す
  publicly_accessible = true

  # 学習用なので節約する。バックアップを取らない・AZ をまたいで二重化しない
  backup_retention_period = 0
  multi_az                = false

  # destroy のとき「最後のバックアップを取りますか」で止まらないようにする
  skip_final_snapshot = true
  apply_immediately   = true
}

行ごとの意味です。

  • db_name = "studydb" — RDS というサーバーの中に、最初から1つ作っておくデータベース(テーブルの入れ物)の名前です。サーバー本体も「データベース」と呼ぶので言葉が二重になりますが、6章でつなぐ先はこの studydb です
  • engine_version = "8.0" — 細かい版(8.0.何とか)まで書かず、系列だけ指定しました。どの版が選ばれたかは5章で分かります
  • publicly_accessible = true — 自宅から直接つなぐための設定です。仕事のデータベースなら false にして、同じ VPC の中のサーバーからだけつなぐ形にします。今回は学習用で、3306 番は自宅の IP からしか開いていないので、この形にしました
  • backup_retention_period = 0multi_az = false — 自動バックアップと2台構成を切りました。どちらも切るとそのぶん安くなります
  • skip_final_snapshot = true — これを書かないと、destroy が「最後のスナップショット5の名前を指定してください」というエラーで止まります。止まって調べている間も課金は続くので、学習用では最初から true にしておきます

あわせて、apply が終わったときにつなぎ先が画面に出るよう、outputs.tf に出力を足しておきます。5章と6章で使います。

# RDS のつなぎ先。6章でここへ pymysql からつなぐ
output "db_endpoint" {
  description = "RDS のエンドポイント(ホスト名:ポート)"
  value       = aws_db_instance.study.endpoint
}

4. パスワードをファイルに残さない

RDS には管理者パスワードが要ります。password = "..." と直接書けば動きますが、そのファイルを git に入れた瞬間、履歴にパスワードが残ります。今回はこう書きました。

# RDS の管理者パスワード。⛔ default を書かない(書くとファイルに残る)
# 渡し方:ターミナルで export TF_VAR_db_password='…' してから terraform を打つ
variable "db_password" {
  description = "RDS の master パスワード(8文字以上・/ ' \" @ と空白は使えない)"
  type        = string
  sensitive   = true
}

ポイントは2つです。

  1. default を書かない。 Terraform は、値の無い変数を TF_VAR_変数名 という環境変数から探します。ファイルには「パスワードという変数がある」ことしか残りません
  2. sensitive = true を付ける。 plan や apply の画面に値が出ず、(sensitive value) と表示されます

パスワードの実物は、ホームディレクトリ直下のファイルに置きました。git の管理外で、読めるのは自分だけです。

echo '(パスワード)' > ~/.aws-study-db-password
chmod 600 ~/.aws-study-db-password
export TF_VAR_db_password="$(cat ~/.aws-study-db-password)"

3-1 の var.my_ip(自宅のグローバル IP)も同じ形です。variables.tf にはこう書いてあります。

# ssh と MySQL を許す IP。⛔ default を書かない(書くとファイルに IP が残る)
# 渡し方:export TF_VAR_my_ip="$(curl -s https://checkip.amazonaws.com)/32"
variable "my_ip" {
  description = "自宅のグローバル IP を /32 で(例: 203.0.113.45/32)"
  type        = string
  sensitive   = true
}

こちらは値を控えておく必要すらなく、毎回その場で調べて渡します。回線が変わっても書き直しが要りません。値には末尾の /326 まで含めます。

export TF_VAR_my_ip="$(curl -s https://checkip.amazonaws.com)/32"

ただし、1か所だけ残ります。terraform.tfstate の中には平文のまま入ります。tfstate は Terraform が「いま何を作ってあるか」を控えるファイルで、パスワードもそのまま書かれます。ここは仕組み上避けられないので、tfstate を必ず .gitignore に入れます。

なお、パスワードに使えない文字があります(/ ' " @ と空白)。variables.tf の description に書いておくと、次に打つときに思い出せます。

5. apply する — 今回は3つだけ作る

terraform plan を打つと、こう出ました。

Plan: 10 to add, 0 to change, 0 to destroy.

足したのは3つなのに、10個作ろうとしています。理由は、前回の学習の最後に terraform destroy で全部消していたからです。Terraform は main.tf と tfstate(いま有るものの控え)を見比べて差分を作るので、控えが空だと EC2 も S3 も「まだ無いもの」として全部作りに行きます。

今回 EC2 は使わないので、-target で RDS の3つに絞りました。

terraform apply \
  -target=aws_security_group.rds \
  -target=aws_db_subnet_group.study \
  -target=aws_db_instance.study

これで Plan: 3 to add になります。代わりに、警告が2本出ます。

Warning: Resource targeting is in effect
Warning: Applied changes may be incomplete

意味は「一部だけ作ったので、main.tf に書いてある残りは置き去りです」です。-target は普段使いの道具ではない、と Terraform 自身が言っています。今回は「EC2 を置き去りにする」のが目的なので、この警告は想定どおりです。なお、この記事の部品だけを新しい main.tf に書いた場合は最初から 3 to add になり、-target は要りません。

apply の画面では、こういう行が延々と流れます。

aws_db_instance.study: Still creating... [01m40s elapsed]
aws_db_instance.study: Still creating... [01m50s elapsed]

RDS の作成は EC2 とは桁が違って、数分かかります(実測は9章)。終わると、つなぎ先が表示されます。

db_endpoint = "aws-study-tf-db.xxxxxxxxxxxx.ap-northeast-1.rds.amazonaws.com:3306"

この db_endpoint の値がエンドポイント7で、6章のつなぎ先になります。

版は engine_version = "8.0" とだけ書きましたが、選ばれたのは 8.0.46 でした。6章でつないで SELECT VERSION() を打つと確かめられます。

6. Python からつなぐ

Mac 側の準備は pymysql(Python から MySQL につなぐ部品)を1つ入れるだけです。ここで今回最初のエラーが出ました。

pip3 install pymysql
error: externally-managed-environment

Homebrew で入れた Python は、pip での直接の追加を断る作りになっています(PEP 668 という決まりです)。以前 brew install awscli が Python ごと入れ替えて、関係ないスクリプトが動かなくなったことがあったので(はじめてのAWS CLI の回)、この断りはそういう事故の予防だと考えて、venv(プロジェクト専用の Python 置き場)を作る形にしました。

cd ~/Documents/aws-terraform-study
python3 -m venv venv
venv/bin/pip install pymysql

venv はこのフォルダの中で完結し、Mac 本体の Python には何も足しません。以後、このプロジェクトでは python3 ではなく venv/bin/python3 を打ちます。

つなぐスクリプト(db_connect.py)です。冒頭の説明コメントだけ省いて載せます。パスワードは4章と同じファイルから読み、スクリプトには書きません。

import os
import pymysql

pw_file = os.path.expanduser("~/.aws-study-db-password")
conn = pymysql.connect(
    host=os.environ["DB_HOST"],
    port=3306,
    user="admin",
    password=open(pw_file).read().strip(),
    database="studydb",
    connect_timeout=10,
)

with conn.cursor() as cur:
    cur.execute("SELECT VERSION()")
    print("MySQL の版        :", cur.fetchone()[0])
    cur.execute("SELECT DATABASE()")
    print("いま見ている DB   :", cur.fetchone()[0])
    cur.execute("SELECT CURRENT_USER()")
    print("名乗っている利用者:", cur.fetchone()[0])
    cur.execute("SELECT NOW()")
    print("RDS 側の現在時刻  :", cur.fetchone()[0])

conn.close()
print("つながった ✅")

つなぎ先は Terraform の出力から環境変数で渡します。

export DB_HOST="$(terraform output -raw db_endpoint | cut -d: -f1)"
venv/bin/python3 db_connect.py

cut -d: -f1 は、出力の末尾に付く :3306 を切り落としてホスト名だけにするためです。pymysql にはホスト名とポート番号を別々に渡します。

MySQL の版        : 8.0.46
いま見ている DB   : studydb
名乗っている利用者: admin@%
RDS 側の現在時刻  : 2026-09-19 12:02:27
つながった ✅

admin@%% は「どの場所からつないできても」を表す MySQL の書き方です。

打ったのは夜の21時02分なのに、RDS の時計は12時02分です。RDS の MySQL は、時刻の設定が最初は UTC(日本より9時間前)になっています。壊れているのではなく既定値です。時刻を入れる列を使うときは、どちらの時計で入るのかを先に確かめておくと混乱しません。

7. テーブルを作って、入れて、読む

ここからは SQL です。mysql コマンド(クライアント)は Mac に入れていません。brew install mysql-client は依存に Python を含んでいて、6章に書いた入れ替えと同じことが起きうるからです。SQL はすべて、6章と同じ形の Python スクリプトから cur.execute("CREATE TABLE ...") のように流します。計測結果を入れる小さなテーブルを1つ作りました。

7-1. CREATE TABLE — 書いていないものが3つ足される

CREATE TABLE IF NOT EXISTS bench_result (
  id          INT            NOT NULL AUTO_INCREMENT,
  measured_on DATE           NOT NULL,
  machine     VARCHAR(50)    NOT NULL,
  item        VARCHAR(100)   NOT NULL,
  elapsed_ms  DECIMAL(10, 1) NOT NULL,
  note        VARCHAR(200),
  created_at  DATETIME       NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (id)
)

列の指定で使ったのは3つです。AUTO_INCREMENT は番号を自動で振る指定(この id を主キーにしました)、DECIMAL(10, 1) は小数1桁まで入る数値、DEFAULT CURRENT_TIMESTAMP は行を入れた時刻が自動で入る指定です。

作ったあとに SHOW CREATE TABLE で定義を読み返すと、書いた覚えの無いものが3つ付いていました。

) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci

ENGINE(テーブルの保存方式)、CHARSET(文字コード)、COLLATE(並べ替えの規則)です。どれも既定値が自動で入り、定義に明示された形で保存されます。裏を返すと、文字コードを utf8mb4 以外にしたいなら自分で書く必要がある、ということでもあります。

7-2. INSERT — commit を忘れると消える

次にデータを入れます。中身は、これまでの回で控えた計測メモ(ollama を入れるのにかかった時間など)5件です。

SQL = ("INSERT INTO bench_result (measured_on, machine, item, elapsed_ms, note) "
       "VALUES (%s, %s, %s, %s, %s)")
with conn.cursor() as cur:
    cur.executemany(SQL, ROWS)   # ROWS は5件の計測メモのリスト

commit は「ここまでの変更を確定する」合図です。これをわざと書かずに接続を閉じる実験をしました。結果はこうです。

同じ接続の中からは5件見えているのに、つなぎ直すと0件です。見えていたのに、無かったことになります。

原因を調べると、MySQL サーバー側の設定(@@GLOBAL.autocommit)は 1(自動 commit する)でした。ところが自分の接続の設定(@@SESSION.autocommit)を見ると 0 です。確かめ方も SQL で、SELECT @@GLOBAL.autocommit, @@SESSION.autocommit を打つと 1 と 0 が並んで返ります。pymysql が、接続するときに自動 commit を切っています。サーバーの既定だけ調べて「MySQL は自動 commit だから書かなくていい」と覚えると、Python から使ったときにデータが消えます。conn.commit() を明示的に書くのが確実です。

commit し直して入れた5件の id は、1〜5 ではなく 6〜10 になっています。捨てられた5件のぶんも AUTO_INCREMENT の番号は進んだままで、戻りません。id の欠番は異常ではない、ということです。

7-3. SELECT — 読み返して件数を確かめる

読み返しのスクリプトは5つの SELECT を流します。出力の抜粋です(②は全件の一覧、③は machine ごとの集計、⑤は次に振られる id 番号で、ここでは省略します)。

① 全部で何件か        : 5 件(入れたのは5件)

④ 時間のかかった順に上位3件(LIMIT)
   1位  terraform apply(RDS 本体)     357000.0 ms
   2位  ollama 本体を入れる              10000.0 ms
   3位  llama3.2:3b を落とす             10000.0 ms

COUNT・ORDER BY・GROUP BY・LIMIT がひととおり動くことを確かめて、SQL の部は終わりです。

8. 消す

destroy には -target を付けません。全部消すときに絞る理由が無く、絞らないほうが消し残しも出ないからです。

terraform destroy

3-3 で skip_final_snapshot = true を書いておいたので、スナップショットの名前を聞かれずにそのまま消えました。

消えたことは、画面の Destroy complete だけでなく、AWS に聞き直して確かめます。

aws rds describe-db-instances --query 'DBInstances[].DBInstanceIdentifier' --output text
aws rds describe-db-snapshots --query 'DBSnapshots[].DBSnapshotIdentifier' --output text

2本とも何も表示されなければ、本体もスナップショットも残っていません。スナップショットまで見るのは、本体を消してもスナップショットには保存料がかかるからです(今回は backup_retention_period = 0 なので最初からありません)。

9. かかった時間とお金

本文で後回しにした数字を、ここに全部並べます。

時間

何に 実測
terraform apply(RDS 本体) 5分57秒
(比較)EC2 を apply したとき 32秒
terraform destroy(RDS 本体) 1分51秒

作るのに6分、消すのに2分です。EC2 の感覚で待っていると「失敗したのでは」と不安になりますが、この長さが正常です。

お金

単価はこうです(AWS の料金表からの概算。為替と料金改定で変わります)。

何に 概算
db.t4g.micro 1時間 約4.5円
ディスク gp2 20GB 1時間 約0.6円
合計 1時間 約5円

今回、apply の開始から destroy の完了までは 44分22秒で、約3.8円でした。この記事を同じ順でなぞって、SQL の練習をゆっくりやっても、1時間・5円ほどで収まる計算です。

気をつける点が2つあります。

  • -target で絞らずに 10 to add のまま apply していたら、EC2(m7i-flex.large・1時間 約19円)も立って、概算で5倍かかっていました
  • 消し忘れて1か月立てたままにすると、1時間約5円 × 24時間 × 30日 = 月およそ3,600円です。学習用なら、使うたびに作って消すほうが安くつきます

10. つまずいたところ

つまずき 分かったこと
1 pip3 install が断られた Homebrew の Python は pip の直接追加を断る(PEP 668)。venv を作ればフォルダの中で完結する
2 plan が 10 to add だった tfstate が空だと、Terraform は main.tf の全部を作りに行く。今回使うものだけ -target で絞れる
3 INSERT したデータが消えた pymysql は接続時に自動 commit を切る。conn.commit() を明示的に書く
4 NOW() が9時間ずれていた RDS の MySQL は既定が UTC。壊れているのではない

EC2 のときと勝手が違ったのは、サブネットグループ(置き場所の候補を2つ以上)と、作成・削除に数分かかることの2つでした。逆に、Terraform の書き方そのものは EC2 と同じ型で通りました。resource を書いて plan で差分を見て apply する、の繰り返しです。

  1. アベイラビリティゾーン(AZ)。同じリージョンの中で、建物単位に分かれたデータセンターのまとまり。東京リージョンには 1a・1c・1d の3つがあります。

  2. サブネット。VPC の中をさらに区切った区画。既定の VPC には AZ ごとに1つずつあります。

  3. gp2。EBS(AWS のディスク)の種類の1つで、標準的な SSD です。

  4. VPC(Virtual Private Cloud)。AWS の中に作る、自分用のネットワークの区画。アカウントには「既定の VPC」が最初から1つ用意されています。

  5. スナップショット。その時点のデータベースの中身をまるごと控えたもの。ここから元に戻せます。残しておくと保存料がかかります。

  6. /32。IP アドレスの範囲を表す書き方(CIDR 記法)で、/32 は「この1つの IP だけ」という意味です。

  7. エンドポイント。RDS につなぐためのホスト名。裏側の IP アドレスは AWS の都合で変わることがあるため、この名前のほうでつなぎます。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?