🪜
ECSタスク上でTerraform × MySQL Providerを実行できる環境を作ってみる
概要
TerraformでMySQL Providerを用いてデータベースやユーザ管理をしており、terraform apply
はローカル環境から踏み台サーバ(EC2)をポートフォワーディングでRDSに接続して実行しているとします。
その場合、ポートフォワーディング時にSSHでの通信を介すため、よりセキュアな環境でMySQL Providerを使いたいです。
また、EC2ではなくECS Fargateを使用することで運用コストの削減も図りたいです。
そうした課題を解決するため、今回はTerraform, MySQLプロバイダーを利用できるECSコンテナ環境を構築してみます。
いざ構築
構成図
今回はECS + 関連リソース(ECR, IAM)を構築します。
Terraformでリソース構築
resource "aws_ecr_repository" "bastion" {
name = "bastion-container"
}
resource "aws_ecs_cluster" "bastion" {
name = "bastion-cluster"
}
resource "aws_ecs_task_definition" "bastion_task" {
family = "bastion-task"
requires_compatibilities = ["FARGATE"]
network_mode = "awsvpc"
cpu = "256"
memory = "512"
execution_role_arn = aws_iam_role.ecs_task_execution_role.arn
task_role_arn = aws_iam_role.ecs_task_role.arn
container_definitions = templatefile("${path.module}/container_definition.json", {
ecr_image_url = aws_ecr_repository.bastion.repository_url
log_group = aws_cloudwatch_log_group.bastion.name
})
runtime_platform {
operating_system_family = "LINUX"
}
}
# ECSサービス
resource "aws_ecs_service" "bastion_service" {
name = "bastion-service"
cluster = aws_ecs_cluster.bastion.id
task_definition = aws_ecs_task_definition.bastion_task.arn
desired_count = 1
launch_type = "FARGATE"
network_configuration {
subnets = [data.aws_subnet.selected.id]
security_groups = [aws_security_group.bastion_sg.id]
assign_public_ip = true # NATゲートウェイを使用する場合はfalseでOK
}
enable_execute_command = true
}
# セキュリティグループ
resource "aws_security_group" "bastion_sg" {
name = "bastion-sg"
vpc_id = data.aws_vpc.selected.id
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}
# Log Group (Optional)
resource "aws_cloudwatch_log_group" "bastion" {
name = "/ecs/bastion"
retention_in_days = 7
}
コンテナ定義
[
{
"name": "bastion",
"image": "${ecr_image_url}:latest",
"essential": true,
"command": [
"sleep",
"infinity"
],
"logConfiguration": {
"logDriver": "awslogs",
"options": {
"awslogs-group": "${log_group}",
"awslogs-region": "ap-northeast-1",
"awslogs-stream-prefix": "bastion",
"awslogs-create-group": "true"
}
}
}
]
IAMロール (タスクロール・タスク実行ロール)
# ECS タスクロール
resource "aws_iam_role" "ecs_task_role" {
name = "bastion-ecs-task-role"
assume_role_policy = jsonencode({
Version = "2012-10-17",
Statement = [{
Effect = "Allow",
Principal = {
Service = "ecs-tasks.amazonaws.com"
},
Action = "sts:AssumeRole"
}]
})
}
# ReadOnlyAccess ポリシー付与
resource "aws_iam_role_policy_attachment" "ecs_task_readonly" {
role = aws_iam_role.ecs_task_role.name
policy_arn = "arn:aws:iam::aws:policy/ReadOnlyAccess"
}
# CloudWatch Logsポリシー
resource "aws_iam_policy" "cloudwatch_logs" {
name = "ecs-task-logs-policy"
policy = jsonencode({
Version = "2012-10-17",
Statement = [
{
Effect = "Allow",
Action = [
"logs:CreateLogStream",
"logs:PutLogEvents",
"logs:DescribeLogStreams"
],
Resource = "${aws_cloudwatch_log_group.bastion.arn}:*"
}
]
})
}
resource "aws_iam_role_policy_attachment" "cloudwatch_logs_attach" {
role = aws_iam_role.ecs_task_role.name
policy_arn = aws_iam_policy.cloudwatch_logs.arn
}
# Terraform State更新・参照ポリシー
resource "aws_iam_policy" "ecs_task_s3_terraform" {
name = "ecs-task-tfstate-policy"
policy = jsonencode({
Version = "2012-10-17",
Statement = [
{
Effect = "Allow",
Action = [
"s3:GetObject",
"s3:PutObject",
"s3:ListBucket"
],
Resource = ["arn:aws:s3:::arie-terraform-states", "arn:aws:s3:::arie-terraform-states/*"]
}
]
})
}
resource "aws_iam_role_policy_attachment" "ecs_task_s3_attach" {
role = aws_iam_role.ecs_task_role.name
policy_arn = aws_iam_policy.ecs_task_s3_terraform.arn
}
# タスク実行ロール
resource "aws_iam_role" "ecs_task_execution_role" {
name = "bastion-ecs-task-execution-role"
assume_role_policy = jsonencode({
Version = "2012-10-17",
Statement = [{
Effect = "Allow",
Principal = {
Service = "ecs-tasks.amazonaws.com"
},
Action = "sts:AssumeRole"
}]
})
}
resource "aws_iam_role_policy_attachment" "ecs_task_exec_attach" {
role = aws_iam_role.ecs_task_execution_role.name
policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy"
}
# Task RoleにSSMのポリシーをアタッチする
resource "aws_iam_role_policy_attachment" "ecs_task_ssm_attach" {
role = aws_iam_role.ecs_task_role.name
policy_arn = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"
}
タスク実行ロールはAWS管理のAmazonECSTaskExecutionRolePolicyを利用しておけばOK
タスクロールにはstateを保存しているS3バケット内のオブジェクト参照・追加できる権限をアタッチする必要があります。
MySQL Providerを利用する上ではその他の権限は不要です。
Dockerfile
今回はAmazon Linux 2023のイメージを使用
FROM amazonlinux:2023
ENV HOME=/root
RUN yum install -y \
git \
tar \
gzip \
make \
unzip \
shadow-utils && \
usermod -aG wheel root && \
git clone https://github.com/tfutils/tfenv.git ~/.tfenv && \
echo 'export PATH="$HOME/.tfenv/bin:$PATH"' >> ~/.bashrc && \
echo 'eval "$(tfenv init -)"' >> ~/.bashrc
# tfenv useを使う際にfindコマンドが必要
RUN dnf install -y findutils
WORKDIR /root/bastion-container
CMD [ "sleep", "infinity" ]
terraform apply
してみる
ECSコンテナ上で構築が完了してECSタスクが起動できたら、SessionManagerでECSコンテナに接続→terraform実行を試してみようと思います。(AWS SSM Agentは導入済みとします)
ECS Execを実行
TASK_ID=xxxxxxxx
AWS_REGION=ap-northeast-1
aws ecs execute-command \
--cluster bastion-cluster \
--region ${AWS_REGION} \
--task ${TASK_ID} \
--container bastion \
--interactive \
--command "/bin/bash"
tfenvの設定
# 好きなバージョンをインストールする
tfenv install 1.12.0
tfenv use 1.12.0
mysql構築用のterraformファイルを作成
# SecretsManagerにMySQLの接続情報を保存するものとします。
resource "aws_secretsmanager_secret" "mysql" {
name = "mysql-secrets"
}
data "aws_secretsmanager_secret_version" "secret-version" {
secret_id = aws_secretsmanager_secret.mysql.id
}
provider "mysql" {
endpoint = jsondecode(data.aws_secretsmanager_secret_version.secret-version.secret_string)["MYSQL_HOST"]
username = jsondecode(data.aws_secretsmanager_secret_version.secret-version.secret_string)["MYSQL_USERNAME"]
password = jsondecode(data.aws_secretsmanager_secret_version.secret-version.secret_string)["MYSQL_PASSWORD"]
}
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
}
mysql = {
source = "petoju/mysql"
version = "3.0.23"
}
}
backend "s3" {
bucket = "{Stateを保存しているS3バケット名}"
region = "ap-northeast-1"
key = "mysql/terraform.tfstate"
}
}
resource "mysql_database" "app" {
name = "sample_db"
}
terraform apply
してApply CompleteできればOK
試してみた中での気づきメモ
- CloudShellでも良さそうだが今回はFargateを採用
- CloudShellのストレージ容量は1GBなので, terraformの実行環境としては物足りないため
- mysql providerは
mysql_native_password
プラグインを使っているぽい。- 現行のAurora MySQL 3系だとデフォルトの認証プラグインは
mysql_native_password
なので特に問題なさそう - RDS for MySQLは
caching_sha2_password
なので、認証プラグインの設定を変更する必要がありそう - EC2でMySQL 8.4をホスティングして検証した際はmy.cnfを編集してデフォルト認証プラグインを変更する必要があった。
- 現行のAurora MySQL 3系だとデフォルトの認証プラグインは
Discussion