はじめに
年月が経つにつれ、開発環境に入っているレコードは汚くなっていきます。
結果として、取得したい内容によっては想定しない値が紐づくレコードも取得され、開発環境では検証にならないケースが出てきてしまいます。
開発環境にはデータが存在しなかったために、検証時にコーナーケースを拾えず不具合になってしまうこともあるかもしれません。
また、そのような開発環境のためにE2Eテストの自動化に悩む例もあるでしょう。
そのような問題に対してのソリューションとして、開発環境を日時で作り直すことが考えられます。
今回はAWS環境に構築されたAurora RDSに対して、CodeBuildを動かして日時で開発環境を作り直す方法を紹介します。
要件
- AWS環境
- Aurora RDSにレコードがあり、これをきれいにしたい
- MySQL
- 日時でDBの中身をきれいにしたい
- DBにはテーブルの作成と初期値の投入もしておいてほしい
完成像
- 日時でCodeBuildが動作し、下記処理が流れる
- DBが一度破壊され作り直される
- Djangoマイグレーションでテーブルが作られる
- Django loaddataで、fixtureで定義している初期データが依存関係を解決しつつ投入される
ディレクトリ構成
.
├─ buildspec.yml
├─ requirements.txt
│
├─infrastructure
│ ├─ cleanup-db-codebuild.yml
│ ├─ common.yml
│ ├─ db.yml
│ │
│ └─params
│ ├─ cleanup-db-codebuild.param.json
│ ├─ common-codebuild.param.json
│ └─ db.param.json
│
├─sql
│ └─ 000_drop_db_and_create_default_db.sql
│
└─src
├─ manage.py
│
├─cleanup_develop
│ ├─ settings.py
│ └─ ...(Djangoのプロジェクトを立ち上げたときに作られるファイル群)
│
└─defaultdb
├─ apps.py
├─ models.py
│
├─fixtures
│ ├─ article.json
│ ├─ tag.json
│ ├─ user.json
│ ├─ user_tag_relation.json
│ └─ __init__.py
│
├─management
│ │
│ └─commands
│ └─ load_fixtures.py
│
└─migrations
└─ ...(マイグレーションファイル群)
requirements.txt
Django
dj-database-url
mysqlclient
おまけ: venvつくるmake
venv:
python3 -m venv .venv
cat ./requirements.txt | grep -v mysql | xargs ./.venv/bin/pip install
Django
モデル定義、初期データ定義、初期データ投入プログラムの作成を行います。
ここでのやっていることは下記記事のDjango版になります。
テーブル定義
まず最初にテーブル定義を行います
今回はDjangoでモデルを定義して、このモデル定義からマイグレーションを当てることでテーブルを作ります。
Djangoはマイグレーションファイルを作成 (makemigrations) して、それをもとにDBへマイグレーションを行います (migrate) 。
src/defaultdb/models.py にモデル定義を記載します。
from django.db import models
# Create your models here.
class User(models.Model):
id = models.AutoField(primary_key=True)
created_at = models.DateTimeField(auto_now_add=True)
name = models.CharField(max_length=256)
code = models.CharField(max_length=256, unique=True)
class Meta:
db_table = "user"
class Tag(models.Model):
id = models.AutoField(primary_key=True)
created_at = models.DateTimeField(auto_now_add=True)
created_by = models.ForeignKey(User, on_delete=models.CASCADE)
name = models.CharField(max_length=256, unique=True)
is_official = models.BooleanField(default=False)
class Meta:
db_table = "tag"
class UserTagRelation(models.Model):
id = models.AutoField(primary_key=True)
created_at = models.DateTimeField(auto_now_add=True)
user = models.ForeignKey(User, on_delete=models.CASCADE)
tag = models.ForeignKey(Tag, on_delete=models.CASCADE)
class Meta:
constraints = [
models.UniqueConstraint(fields=['user', 'tag'], name='user_and_tag_uniq'),
]
db_table = "user_tag_relation"
class Article(models.Model):
id = models.AutoField(primary_key=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
title = models.CharField(max_length=256)
article_path = models.TextField()
user = models.ForeignKey(User, on_delete=models.CASCADE)
class Meta:
constraints = [
models.UniqueConstraint(fields=['user', 'title'], name='article_title_and_user_uniq'),
]
db_table = "article"
class ArticleTagRelation(models.Model):
id = models.AutoField(primary_key=True)
created_at = models.DateTimeField(auto_now_add=True)
article = models.ForeignKey(Article, on_delete=models.CASCADE)
tag = models.ForeignKey(Tag, on_delete=models.CASCADE)
class Meta:
constraints = [
models.UniqueConstraint(fields=['article', 'tag'], name='article_and_tag_uniq'),
]
db_table = "article_tag_relation"
マイグレーションファイルの作成
makemigrationsを用いて、マイグレーションファイルを作成します。
python manage.py makemigrations defaultdb
fixtures
初期データを作成します。
Djangoでは、loaddataを用いることで、指定形式のデータをコマンドで投入することができます。
下記の通り src/defaultdb/fixtures/ 配下にfixtureファイルを作成します。
article.json
[
{
"model": "defaultdb.Article",
"pk": 1,
"fields": {
"created_at": "1970-01-01T09:00:00",
"updated_at": "1970-01-01T09:00:00",
"title": "はじめまして",
"article_path": "path/to/initial",
"user_id": 1
}
}
]
tag.json
[
{
"model": "defaultdb.Tag",
"pk": 1,
"fields": {
"created_at": "1970-01-01T09:00:00",
"created_by": 1,
"name": "JavaScript",
"is_official": true
}
},
{
"model": "defaultdb.Tag",
"pk": 2,
"fields": {
"created_at": "1970-01-01T09:00:00",
"created_by": 1,
"name": "TypeScript",
"is_official": true
}
},
{
"model": "defaultdb.Tag",
"pk": 3,
"fields": {
"created_at": "1970-01-01T09:00:00",
"created_by": 1,
"name": "CoffeeScript",
"is_official": true
}
},
{
"model": "defaultdb.Tag",
"pk": 4,
"fields": {
"created_at": "1970-01-01T09:00:00",
"created_by": 1,
"name": "React",
"is_official": true
}
},
{
"model": "defaultdb.Tag",
"pk": 5,
"fields": {
"created_at": "1970-01-01T09:00:00",
"created_by": 1,
"name": "Angular",
"is_official": true
}
},
{
"model": "defaultdb.Tag",
"pk": 6,
"fields": {
"created_at": "1970-01-01T09:00:00",
"created_by": 1,
"name": "Vue",
"is_official": true
}
},
{
"model": "defaultdb.Tag",
"pk": 7,
"fields": {
"created_at": "1970-01-01T09:00:00",
"created_by": 1,
"name": "AWS",
"is_official": true
}
},
{
"model": "defaultdb.Tag",
"pk": 8,
"fields": {
"created_at": "1970-01-01T09:00:00",
"created_by": 1,
"name": "AWS lambda",
"is_official": true
}
},
{
"model": "defaultdb.Tag",
"pk": 9,
"fields": {
"created_at": "1970-01-01T09:00:00",
"created_by": 1,
"name": "EC2",
"is_official": true
}
},
{
"model": "defaultdb.Tag",
"pk": 10,
"fields": {
"created_at": "1970-01-01T09:00:00",
"created_by": 1,
"name": "S3",
"is_official": true
}
}
]
user.json
[
{
"model": "defaultdb.User",
"pk": 1,
"fields": {
"created_at": "1970-01-01T09:00:00",
"name": "管理者",
"code": "admin"
}
},
{
"model": "defaultdb.User",
"pk": 2,
"fields": {
"created_at": "1970-01-01T09:00:00",
"name": "フロント",
"code": "front"
}
},
{
"model": "defaultdb.User",
"pk": 3,
"fields": {
"created_at": "1970-01-01T09:00:00",
"name": "AWS",
"code": "aws"
}
}
]
user_tag_relation.json
[
{
"model": "defaultdb.UserTagRelation",
"pk": 1,
"fields": {
"created_at": "1970-01-01T09:00:00",
"user_id": 2,
"tag_id": 1
}
},
{
"model": "defaultdb.UserTagRelation",
"pk": 2,
"fields": {
"created_at": "1970-01-01T09:00:00",
"user_id": 2,
"tag_id": 2
}
},
{
"model": "defaultdb.UserTagRelation",
"pk": 3,
"fields": {
"created_at": "1970-01-01T09:00:00",
"user_id": 2,
"tag_id": 3
}
},
{
"model": "defaultdb.UserTagRelation",
"pk": 4,
"fields": {
"created_at": "1970-01-01T09:00:00",
"user_id": 2,
"tag_id": 4
}
},
{
"model": "defaultdb.UserTagRelation",
"pk": 5,
"fields": {
"created_at": "1970-01-01T09:00:00",
"user_id": 2,
"tag_id": 5
}
},
{
"model": "defaultdb.UserTagRelation",
"pk": 6,
"fields": {
"created_at": "1970-01-01T09:00:00",
"user_id": 2,
"tag_id": 6
}
},
{
"model": "defaultdb.UserTagRelation",
"pk": 7,
"fields": {
"created_at": "1970-01-01T09:00:00",
"user_id": 3,
"tag_id": 7
}
},
{
"model": "defaultdb.UserTagRelation",
"pk": 8,
"fields": {
"created_at": "1970-01-01T09:00:00",
"user_id": 3,
"tag_id": 8
}
},
{
"model": "defaultdb.UserTagRelation",
"pk": 9,
"fields": {
"created_at": "1970-01-01T09:00:00",
"user_id": 3,
"tag_id": 9
}
},
{
"model": "defaultdb.UserTagRelation",
"pk": 10,
"fields": {
"created_at": "1970-01-01T09:00:00",
"user_id": 3,
"tag_id": 10
}
}
]
依存関係順に投入するためのコマンドを作成
fixtureの投入される順番は下記にある通り、指定順になります。
辞書順が依存関係順である保証はないので、トポロジカルソートを用いて、依存関係順に並べ替えて投入します。
依存順に並べ替え、Djangoのloaddataコマンドを叩きます。
そのために、カスタムコマンド load_fixtures を作成します。
src/defaultdb/management/commands/load_fixtures.py に下記プログラムを作成します。
from collections import deque
import os
import pathlib
from django.core import management
from django.core.management.base import BaseCommand
from django.core.management.commands import loaddata
from django.db import connection
PATH = pathlib.Path(__file__).parent.resolve()
QUERY = """SELECT
TABLE_NAME AS table_name,
REFERENCED_TABLE_NAME AS reference_table_name
FROM
information_schema.KEY_COLUMN_USAGE"""
class Command(BaseCommand):
help = 'dependency_load_fixture'
def handle(self, *args, **options):
fixture_files = set()
for _root, _dirs, files in os.walk(PATH.parent.parent / 'fixtures'):
for file in files:
if file.endswith('.json'):
fixture_files.add(file.removesuffix('.json'))
dependency_map: dict[str, set[str]] = dict()
rows = self._get_table_dependency()
for row in rows:
s: set[str] = set()
if dependency_map.get(row['table_name']):
s = dependency_map.get(row['table_name'])
if row['reference_table_name'] is not None:
s.add(row['reference_table_name'])
dependency_map[row['table_name']] = s
sorted_list = self._topological_sort(dependency_map)
print(sorted_list)
fixture_target = [
table_name
for table_name in sorted_list
if table_name in fixture_files
]
management.call_command(loaddata.Command(), *fixture_target, verbosity=0)
def _get_table_dependency(self):
with connection.cursor() as cursor:
cursor.execute(QUERY)
columns = [col[0] for col in cursor.description]
rows = [dict(zip(columns, row)) for row in cursor.fetchall()]
return rows
def _topological_sort(self, dependency_map: dict[str, set[str]]) -> list[str]:
all_tables = set(dependency_map.keys())
# { テーブル名: 入次数 }
in_degree: dict[str, int] = dict()
# 依存先から依存しているテーブルの一覧を作成
graph = {table: [] for table in dependency_map.keys()}
# グラフと入次数の計算
for table, deps in dependency_map.items():
in_degree[table] = len(deps)
for dep in deps:
graph[dep].append(table)
# 現時点で入次数が 0 の頂点をすべてキューに追加する
queue = deque([table for table, degree in in_degree.items() if degree == 0])
# 結果(トポロジカル順序)を格納する配列
sorted_list: list[str] = []
while queue:
# 入次数が 0 の頂点を 1 つ取り出す
table = queue.popleft()
# トポロジカル順序に追加する
sorted_list.append(table)
# その頂点から出る各辺について
# その先の頂点の入次数を減らし、新たに 0 になったらキューに追加する
for dependent in graph[table]:
in_degree[dependent] -= 1
if in_degree[dependent] == 0:
queue.append(dependent)
# 閉路が存在する(頂点をすべて処理できていない)場合
if len(sorted_list) != len(all_tables):
closed_circuit = ', '.join([
table
for table, degree in in_degree.items()
if degree > 0
])
raise ValueError(f'{closed_circuit} で閉路を検出')
return sorted_list
繰り返しになりますが、やっていることは下記記事に書かれているものをDjangoに書き換えたものです。
ただしトポロジカルソートについては下記記事の形に直しています。
相変わらずテーブルのリレーションが循環参照する場合には耐えられません。
SQL
DBをリセットするために、一度DBごと壊して再度作成し直します。
sql/ 内を指定順で流すために、ファイルの順序に注意する必要があります。
000_drop_db_and_create_default_db.sql
とりあえずDBは一つ。
データベース名とユーザに注意しましょう
-- DROP DB
DROP DATABASE IF EXISTS `my_database`;
-- CREATE DEFAULT DB
CREATE DATABASE IF NOT EXISTS `my_database`;
GRANT ALL ON my_database.* TO 'my_user'@'%';
Infrastructure
下記をCFnで構築していきます。
common.ymldb.ymlcleanup-db-codebuild.yml
common.yml
VPC周りと共通SGを作成します。
CodeBuildからGitHubへのアクセスをするために、プライベートサブネットを作成します。
CidrBlockについては下記の推奨範囲を参考に振りました。
AWSTemplateFormatVersion: "2010-09-09"
Description: cleanup dev codebuild common resources
Resources:
VPC:
Type: AWS::EC2::VPC
Properties:
CidrBlock: 10.0.0.0/16
EnableDnsHostnames: true
EnableDnsSupport: true
Tags:
- Key: Name
Value: codebuild-vpc
InternetGateway:
Type: AWS::EC2::InternetGateway
AttachInternetGateway:
Type: AWS::EC2::VPCGatewayAttachment
Properties:
VpcId: !Ref VPC
InternetGatewayId: !Ref InternetGateway
PublicSubnetA:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref VPC
CidrBlock: 10.0.1.0/24
AvailabilityZone: !Select [ 0, !GetAZs '' ]
MapPublicIpOnLaunch: true
Tags:
- Key: Name
Value: public-subnet-a
PublicSubnetB:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref VPC
CidrBlock: 10.0.2.0/24
AvailabilityZone: !Select [ 1, !GetAZs '' ]
MapPublicIpOnLaunch: true
Tags:
- Key: Name
Value: public-subnet-b
PrivateSubnet:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref VPC
CidrBlock: 10.0.3.0/24
AvailabilityZone: !Select [ 0, !GetAZs '' ]
Tags:
- Key: Name
Value: private-subnet
PublicRouteTable:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref VPC
PublicRoute:
Type: AWS::EC2::Route
Properties:
RouteTableId: !Ref PublicRouteTable
DestinationCidrBlock: 0.0.0.0/0
GatewayId: !Ref InternetGateway
PublicSubnetARouteTableAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref PublicSubnetA
RouteTableId: !Ref PublicRouteTable
PublicSubnetBRouteTableAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref PublicSubnetB
RouteTableId: !Ref PublicRouteTable
NatEIP:
Type: AWS::EC2::EIP
Properties:
Domain: vpc
NatGateway:
Type: AWS::EC2::NatGateway
Properties:
AllocationId: !GetAtt NatEIP.AllocationId
SubnetId: !Ref PublicSubnetA
PrivateRouteTable:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref VPC
PrivateRoute:
Type: AWS::EC2::Route
Properties:
RouteTableId: !Ref PrivateRouteTable
DestinationCidrBlock: 0.0.0.0/0
NatGatewayId: !Ref NatGateway
PrivateSubnetRouteTableAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref PrivateSubnet
RouteTableId: !Ref PrivateRouteTable
CodeBuildSG:
Type: AWS::EC2::SecurityGroup
Properties:
GroupName:
!Join [
'-',
[
cleanup,
dev,
CodeBuild,
sg,
],
]
GroupDescription:
!Join [
'-',
[
cleanup,
dev,
CodeBuild,
sg,
],
]
VpcId: !Ref VPC
SecurityGroupIngress:
- CidrIp: 0.0.0.0/0
FromPort: 443
ToPort: 443
IpProtocol: tcp
Outputs:
VpcId:
Description: Cleanup Dev Common CodeBuild VPC ID.
Value: !Ref VPC
Export:
Name: VpcId
PublicSubnetAId:
Description: Cleanup Dev Common CodeBuild Public Subnet A ID.
Value: !Ref PublicSubnetA
Export:
Name: PublicSubnetAId
PublicSubnetBId:
Description: Cleanup Dev Common CodeBuild Public Subnet B ID.
Value: !Ref PublicSubnetB
Export:
Name: PublicSubnetBId
PrivateSubnetId:
Description: Cleanup Dev Common CodeBuild Private Subnet ID.
Value: !Ref PrivateSubnet
Export:
Name: PrivateSubnetId
CommonCodeBuildSGId:
Description: Cleanup Dev Common CodeBuild SG.
Value: !GetAtt CodeBuildSG.GroupId
Export:
Name: CommonCodeBuildSGId
db.yml
リードインスタンスを作成したかったので、インスタンスを複数作成しています。
下記特性の通りに DependsOn を用いて、ライターインスタンスをコントロールしています。
レプリケーションについても下記の通りAWS側でいい感じに設定されます。
Aurora プロビジョンド DB クラスターに 2 つめ、3 つめ、と DB インスタンスを作成すると、Aurora が、書き込み DB インスタンスからその他すべての DB インスタンスに、レプリケーションを自動でセットアップします。これらその他の他の DB インスタンスは、読み取り専用で、Aurora レプリカと呼ばれます。また、クラスター内で書き込みと読み取りの DB インスタンスを組み合わせる方法について説明する際は、リーダーインスタンスとも呼ばれます。
パブリックサブネットにDBを配置している理由
よろしくないのですが、パブリックサブネットにDBを配置しています。
踏み台サーバーの作成を横着して、手元のDBクライアントから直接DBにしたのでパブリックサブネットに配置しています。
パブリックサブネットに配置したDBにローカルのDBクライアントから接続する
コンソール上から下記を変更します。
- DBに設定しているSGに、3306ポートに自分のIPを許可する
- Auroraの設定からパブリックアクセスを可能にする
AWSTemplateFormatVersion: "2010-09-09"
Description: cleanup develop database
Parameters:
DBClusterScalingMaxCapacity:
Type: Number
Default: 64
Description: DB Cluster ServerlessV2ScalingConfiguration MaxCapacity
DBClusterScalingMinCapacity:
Type: Number
Default: 0
Description: DB Cluster ServerlessV2ScalingConfiguration MinCapacity
DBName:
Type: String
Description: Default DB Name
DBMasterId:
Type: String
Description: DB Master ID
DBMasterPassword:
Type: String
Description: DB Master Password
Resources:
DBSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: !Join [ "-", [ cleanup, dev, rdb, sg ] ]
VpcId: !ImportValue VpcId
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 3306
ToPort: 3306
SourceSecurityGroupId: !ImportValue CommonCodeBuildSGId
Tags:
- Key: Name
Value: !Join [ "-", [ cleanup, dev, rdb, sg ] ]
DBSubnetGroup:
Type: AWS::RDS::DBSubnetGroup
Properties:
DBSubnetGroupDescription: db-subnet
SubnetIds:
- !ImportValue PublicSubnetAId
- !ImportValue PublicSubnetBId
RDSCluster:
Type: AWS::RDS::DBCluster
Properties:
Engine: aurora-mysql
EngineVersion: 8.0.mysql_aurora.3.08.0
ServerlessV2ScalingConfiguration:
MaxCapacity: !Ref DBClusterScalingMaxCapacity
MinCapacity: !Ref DBClusterScalingMinCapacity
StorageEncrypted: true
VpcSecurityGroupIds:
- !Ref DBSecurityGroup
DBSubnetGroupName:
Ref: DBSubnetGroup
DatabaseName: !Ref DBName
MasterUsername: !Ref DBMasterId
MasterUserPassword: !Ref DBMasterPassword
DBClusterIdentifier: !Join [ "-", [ cleanup, dev, db ] ]
Tags:
- Key: Name
Value: !Join [ "-", [ cleanup, dev, db ] ]
DBServerPrimaryInstance:
Type: AWS::RDS::DBInstance
Properties:
Engine: aurora-mysql
EngineVersion: 8.0.mysql_aurora.3.08.0
AutoMinorVersionUpgrade: false
StorageEncrypted: true
DBInstanceClass: db.serverless
DBClusterIdentifier: !Ref RDSCluster
DBSubnetGroupName:
Ref: DBSubnetGroup
DBInstanceIdentifier: !Join [ "-", [ cleanup, dev, db, primary ] ]
Tags:
- Key: Name
Value: !Join [ "-", [ cleanup, dev, db, primary ] ]
DBServerReadReplicaInstance:
Type: AWS::RDS::DBInstance
DependsOn: DBServerPrimaryInstance
Properties:
Engine: aurora-mysql
EngineVersion: 8.0.mysql_aurora.3.08.0
AutoMinorVersionUpgrade: false
StorageEncrypted: true
DBInstanceClass: db.serverless
DBClusterIdentifier: !Ref RDSCluster
DBSubnetGroupName:
Ref: DBSubnetGroup
DBInstanceIdentifier: !Join [ "-", [ cleanup, dev, db, read, replica ] ]
Tags:
- Key: Name
Value: !Join [ "-", [ cleanup, dev, db, read, replica ] ]
Outputs:
MyRDSHostEndpoint:
Value: !GetAtt RDSCluster.Endpoint.Address
Export:
Name: MyRDSHostEndpoint
cleanup-db-codebuild.yml
ServiceRoleがかなり大雑把に設定しています。
私のGitHub情報をSecretsManagerに保存しているため、SecretsManager policyを入れています。
cronは深夜0時に流したかったけどUTCでの設定だったので、 cron(0 15 * * ? *) にしています。
適宜変更ください
- Imageは2025/06時点のものになります
- インスタンスサイズが小さいです
AWSTemplateFormatVersion: "2010-09-09"
Description: cleanup db codebuild resources
Parameters:
GitHubSourceRepository:
Type: String
Description: GitHub Repository Url.
DBName:
Type: String
Description: Default DB Name
DBMasterId:
Type: String
Description: DB Master ID
DBMasterPassword:
Type: String
Description: DB Master Password
Resources:
ServiceRole:
Type: AWS::IAM::Role
Properties:
RoleName: !Join [ "-", [ cleanup, db, codeBuild, serviceRole] ]
AssumeRolePolicyDocument:
Statement:
- Effect: Allow
Principal:
Service:
- codebuild.amazonaws.com
Action:
- sts:AssumeRole
Path: "/"
ManagedPolicyArns:
- "arn:aws:iam::aws:policy/CloudWatchFullAccess"
- "arn:aws:iam::aws:policy/AWSCodeBuildDeveloperAccess"
- "arn:aws:iam::aws:policy/AmazonRDSFullAccess"
- "arn:aws:iam::aws:policy/AmazonEC2FullAccess"
- "arn:aws:iam::aws:policy/SecretsManagerReadWrite"
- "arn:aws:iam::aws:policy/AmazonEventBridgeFullAccess"
CodeBuildProject:
Type: AWS::CodeBuild::Project
Properties:
Name: !Join [ "-", [ cleanup, db, CodeBuild ] ]
Source:
Location: !Ref GitHubSourceRepository
Type: GITHUB
GitCloneDepth: 1
BuildSpec: "buildspec.yml"
SourceVersion: main
Environment:
Type: LINUX_CONTAINER
ComputeType: BUILD_GENERAL1_SMALL
Image: aws/codebuild/standard:7.0
EnvironmentVariables:
- Name: MYSQL_HOST
Value: !ImportValue MyRDSHostEndpoint
- Name: MYSQL_DATABASE
Value: !Ref DBName
- Name: MYSQL_USER
Value: !Ref DBMasterId
- Name: MYSQL_PASSWORD
Value: !Ref DBMasterPassword
ServiceRole: !GetAtt ServiceRole.Arn
Artifacts:
Type: NO_ARTIFACTS
VpcConfig:
SecurityGroupIds:
- !ImportValue CommonCodeBuildSGId
Subnets:
- !ImportValue PrivateSubnetId
VpcId: !ImportValue VpcId
EventBridgeInvokeRole:
Type: AWS::IAM::Role
Properties:
RoleName: !Join [ "-", [ cleanup, db, event, invoke, role ] ]
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Principal:
Service:
- events.amazonaws.com
Action: "sts:AssumeRole"
Path: "/"
Policies:
- PolicyName: "AllowInvokeCodeBuild"
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Action:
- codebuild:StartBuild
Resource: !GetAtt CodeBuildProject.Arn
CodeBuildTriggerRule:
Type: AWS::Events::Rule
Properties:
Name: !Join [ "-", [ cleanup, db, codeBuild, trigger, rule ] ]
ScheduleExpression: "cron(0 15 * * ? *)"
State: ENABLED
Targets:
- Arn: !GetAtt CodeBuildProject.Arn
Id: "CodeBuildProjectTarget"
RoleArn: !GetAtt EventBridgeInvokeRole.Arn
buildspec.yml
SQLを順序で流して、Djangoマイグレートしてテーブルを作り直します。
最後にカスタムコマンドで作成した load_fixtures を流して完了です。
GitHubにデプロイしてCodeBuildが流れると、DBがきれいになります。
version: 0.2
phases:
install:
runtime-versions:
python: 3.13
commands:
- apt-get update
- apt-get install -y default-mysql-client
- pip install -r requirements.txt
build:
commands:
- for sql in $(ls sql/*.sql | sort); do
echo "Applying $sql";
mysql -h $MYSQL_HOST -u $MYSQL_USER -p$MYSQL_PASSWORD $MYSQL_DATABASE < $sql;
done
- cd src
- python manage.py migrate
- python manage.py load_fixtures
おわりに
ここまでの長文を読んでいただきありがとうございました。
あくまで開発環境をきれいにして検証をしやすくするための一案として捉えていただければと思います。
とはいえ、セキュリティ的にもコスト的にも甘い部分が多いです。
個人的に開発しているため見逃してください。
また、実サービスのDBはここまでシンプルではなく、もっと複雑かと思います。本記事での内容ですと、テーブルのリレーションの循環参照がまず大きな課題かと思います。
一度テーブルの参照をすべて切る、fixtureの投入をトポロジカルソートを行わずSQLの実行のように順序をあらかじめ定義する、など、暫定的な課題解決方法がありそうです。
これら含めて、私の中でまだ課題解決の目星はついていません。一緒に考えましょう。
これが完成すれば、日時で動作させるe2eであったり、検証しやすさの向上といった、本質的なDevOpsの実現をできる気がしています。
以上、現状での日時でDBの中身をきれいにするためのDevOps成果報告でした。
おわりだよー(o・∇・o)