【AWS】社内ツールをオンプレからAWSに移行した話
概要
先日、社内でのタスクチケット管理で使用していたRedmineをオンプレ環境からAWSに移行させた。
今回はどのようにオンプレからAWSへの移行を実施したのか書いていく。
移行前の課題
- 自社でオンプレサーバーの面倒を見るのが大変
- DBのバックアップが取れていない
、、、などなど、上げればキリがないが、代表的な課題は上記だった。
移行元であるオンプレの構成
幸いにも、サーバーにはDockerでインストールされており、実際のRedmineやMySQLはコンテナで動いていた。
また、Redmineの画像や動画などはAWSのS3で管理されていた。
(移行先のAWSとは異なる別プロジェクトだったが)
最終的な移行先のAWS構成
-
自社でオンプレサーバーの面倒を見るのが大変
→ サーバーレスにしよう -
DBのバックアップが取れていない
→ マネージドサービスで自動的にバックアップしてくれるものにしよう
という方針で、下記のように構成した。
【使用したAWSリソース】
リソース | 用途 | 選定理由 |
---|---|---|
ECS(Fargate) | サーバー | EKSほど複雑なシステムも必要なかったので、Fargateを選択。 |
NAT Gateway | FargateとS3などの通信 | サーバーはまるっとPrivate Subnetに置きたかったので、インターネットへの外向き通信用に建てた。今思えばPrivate Link使えば料金安く済んだかも、、 |
ALB | ルーティング | L7レベルでルーティングをする必要があったので選択。 |
RDS | データーベース | こちらも自前でDBサーバー作るのが面倒だったのと、障害等でデータが吹っ飛んだ時にすぐに復旧が出来るのでマネージドサービスにした。社内ツールなので、1時間ほどダウンタイムが許容出来たので、料金を抑える意味でもマルチAZ構成にはしなかった。かつ、QPSもそこまで高くないので、レプリケーションなども特にしていない。 |
ElastiCache | キャッシュサーバー | Redmineのチケット参照などレスポンスが早くなるので導入。こちらもマネージドの方が楽だったのでElastiCacheにした。 |
SES | SMTPサーバー | チケットの通知等でメールを使用したい、、という要件があったので導入。 |
ECR | コンテナリポジトリ | Redmineのイメージにpluginや、GitHubSecrets経由で秘匿情報も入れていたので、なるべく保管時に暗号化してくれたり、HTTPS通信で転送してくれたりなどセキュアなコンテナレジストリサービスを使いたかった。 |
S3 | ストレージサービス | Redmineのチケットの画像や動画を管理するのにS3が好都合だった。Redmineのプラグインで楽に送信も出来る。 |
DataSync | データ転送 | オンプレ環境からAWSに画像と動画を転送する際に、aws s3 syncコマンドより圧倒的に料金がかからなかったので選んだ。1GB/6円という破格の値段だった。 |
WAF | IP制限 | 誰でも社内ツールを見れる状態、、というのはセキュリティ上よろしくないので、会社のグローバルIPならアクセス可能、、などがしたかった。 |
CloudWatch | ログ | サーバーが立ち上がっているか、DataSyncの転送が終わっているかどうかを確認したかった。 |
【その他】
- GitHubActions (Dockerイメージのビルド & ECRへのPush)
- Terraform (IaC)
移行先のAWSの構築方法
Terraformのディレクトリ構成はこんな感じ。
modules/xxの配下に
- main.tf (リソース記述)
- variables.tf (外から渡す変数を定義)
- output.tf (外へ渡す変数を定義)
を置いている。
modules
|_ alb
|_ alb_target_group
|_ datasync_s3
|_ ecr
|_ ecs
|_ memcached_cluster
|_ rds_instance
|_ s3_bucket
|_ waf_acl
|_ waf_ip_set
products
|_ redmine
|_ main.tf
ECR
特にmodule化はせず、そのままのresourceを使用。
resource "aws_ecr_repository" "ecr" {
name = "redmine"
# イメージタグは上書き可能
image_tag_mutability = "MUTABLE"
# push時に脆弱性スキャンを行う
image_scanning_configuration {
scan_on_push = true
}
}
GitHubActions
ビルドしたいブランチで、
$ git tag v1.0.0
$ git push origin v1.0.0
を行うと、Actionsが走ってイメージがビルドされるようにした。
尚、OIDCでAssumeRoleする形でGitHub Actions → AWSヘのアクセスをしている。
下記の記事通りにやれば動くはず。
GitHub Actions OIDCでconfigure-aws-credentialsでAssumeRoleする
RoleのARNとかは適宜GithubActionsのSecrets管理で。
name: build-and-push
# タグのpushで走らせる
on:
push:
tags:
- 'v*'
permissions:
id-token: write
contents: read
pull-requests: write
jobs:
build-and-push:
runs-on: ubuntu-18.04
steps:
- name: Check out source code
uses: actions/checkout@v1
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v1
with:
role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
aws-region: ap-northeast-1
- name: Login to Amazon ECR
id: login-ecr
uses: aws-actions/amazon-ecr-login@v1
- name: Build and push docker image
env:
ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
ECR_REPOSITORY: ${{ secrets.AWS_ECR_REPO_NAME }}
run: |
IMAGE_TAG=$(echo ${{ github.ref }} | sed -e "s#refs/tags/##g")
docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG .
docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
ACM
コンソールからポチポチした。
3分くらいで出来たので、特に難しくなかった。
下記の記事を参考↓
ACMで証明書のインポートする
ALB
data "aws_subnet" "public_subnet" {
id = var.public_subnet_ids[0]
}
data "aws_acm_certificate" "acm" {
domain = var.acm_domain
most_recent = true
}
# publicとprivateは同じVPCなの前提
locals {
vpc_id = data.aws_subnet.public_subnet.vpc_id
}
# ---------- ALB ----------
resource "aws_lb" "alb" {
load_balancer_type = "application"
name = "${var.name}-alb"
internal = false
security_groups = [aws_security_group.alb.id]
subnets = var.public_subnet_ids
}
resource "aws_security_group" "alb" {
name = var.name
vpc_id = local.vpc_id
description = "Managed by Terraform"
tags = {
Name = "${var.name}-alb"
}
}
resource "aws_security_group_rule" "alb_ingress_https" {
# インターネット → セキュリティグループ内のリソースのアクセスを許可する
type = "ingress"
security_group_id = aws_security_group.alb.id
from_port = "443"
to_port = "443"
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
resource "aws_security_group_rule" "alb_ingress_http" {
# インターネット → セキュリティグループ内のリソースのアクセスを許可する(HTTPリダイレクト用)
type = "ingress"
security_group_id = aws_security_group.alb.id
from_port = "80"
to_port = "80"
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
resource "aws_security_group_rule" "alb_egress" {
# セキュリティグループ内のリソース → インターネットへのアクセスを許可する
type = "egress"
security_group_id = aws_security_group.alb.id
from_port = "443"
to_port = "443"
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
# ---------- listener ----------
resource "aws_lb_listener" "alb_listener_https" {
# HTTPSでのアクセスを受け付ける
load_balancer_arn = aws_lb.alb.arn
port = "443"
protocol = "HTTPS"
certificate_arn = data.aws_acm_certificate.acm.arn
# デフォルトアクションがないとApply出来ないのでとりあえず固定レスポンス返す
# todo: 404を返すようにする
default_action {
type = "fixed-response"
fixed_response {
content_type = "text/plain"
message_body = "Fixed response content"
status_code = "200"
}
}
tags = {}
}
resource "aws_alb_listener" "alb_listener_http" {
# HTTPでのアクセスを受け付けて、HTTPSにリダイレクトさせる
load_balancer_arn = aws_lb.alb.arn
port = "80"
protocol = "HTTP"
default_action {
type = "redirect"
redirect {
port = "443"
protocol = "HTTPS"
status_code = "HTTP_301"
}
}
}
variable "public_subnet_ids" {
description = "パブリックサブネットのID配列"
default = []
type = list(string)
}
variable "name" {
description = "ALB名"
type = string
}
variable "acm_domain" {
description = "ACMで管理している証明書のドメイン"
type = string
}
output "arn" {
description = "albのARN"
value = aws_lb.alb.arn
}
output "alb_security_group_id" {
description = "albのセキュリティグループID"
value = aws_security_group.alb.id
}
output "alb_listener_arn" {
description = "ALBのlistenerのARN"
value = aws_lb_listener.alb_listener_https.arn
}
ALB - TargetGroup
resource "aws_alb_listener_rule" "listener_rule" {
listener_arn = var.alb_listener_arn
priority = var.alb_listener_priority
# lb_target_groupへルーティング
condition {
host_header {
values = [var.target_host]
}
}
action {
type = "forward"
target_group_arn = aws_lb_target_group.lb_target.arn
}
}
resource "aws_lb_target_group" "lb_target" {
name = var.target_group_name
port = var.target_port
protocol = var.target_protocol
vpc_id = var.target_vpc_id
target_type = var.target_type
health_check {
interval = 60
path = var.health_check_path
port = "traffic-port"
protocol = "HTTP"
timeout = 30
unhealthy_threshold = 5
# 200番ならOK
matcher = 200
}
}
variable "alb_listener_arn" {
description = "ALBリスナーのARN"
type = string
}
variable "target_host" {
description = "ルーティング対象となるホスト名"
type = string
}
variable "target_group_name" {
description = "target_group名"
type = string
}
variable "target_port" {
description = "ルーティング対象のポート番号"
default = 80
}
variable "target_protocol" {
description = "ルーティング対象への通信プロトコル"
default = "HTTP"
}
variable "target_vpc_id" {
description = "ルーティング対象の位置するVPCID"
type = string
}
variable "target_type" {
description = "ルーティング対象の種類(instance,ip,alb,lambdaなど)"
default = "ip"
}
variable "health_check_path" {
description = "ヘルスチェックのパス"
default = "/"
}
variable "alb_listener_priority" {
description = "リスナールールの優先度"
default = 1
}
output "arn" {
description = "target_groupのARN"
value = aws_lb_target_group.lb_target.arn
}
ECS(Fargate)
ECS Execでコンテナの中に入れるようにしてる。
(デバッグする際にかなり便利なのでオススメ)
data "aws_subnet" "private_subnet" {
id = var.private_subnet_id
}
locals {
vpc_id = data.aws_subnet.private_subnet.vpc_id
}
# ---------- ECS Task Role with SSM Get Parameter Policy ----------
data "aws_iam_policy_document" "ecs_exec" {
version = "2012-10-17"
statement {
actions = [
"ssmmessages:CreateControlChannel",
"ssmmessages:CreateDataChannel",
"ssmmessages:OpenControlChannel",
"ssmmessages:OpenDataChannel"
]
resources = ["*"]
}
}
resource "aws_iam_role" "ecs" {
name = var.name
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Action = "sts:AssumeRole"
Effect = "Allow"
Principal = {
Service = "ecs-tasks.amazonaws.com"
}
}
]
})
}
resource "aws_iam_policy" "ecs_exec" {
name_prefix = "${var.name}-ecs-ecs-exec"
policy = data.aws_iam_policy_document.ecs_exec.json
}
resource "aws_iam_role_policy_attachment" "ecs" {
role = aws_iam_role.ecs.name
policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy"
}
resource "aws_iam_role_policy_attachment" "ecs_exec" {
role = aws_iam_role.ecs.name
policy_arn = aws_iam_policy.ecs_exec.arn
}
resource "aws_iam_role_policy_attachment" "s3" {
role = aws_iam_role.ecs.name
policy_arn = "arn:aws:iam::aws:policy/AmazonS3FullAccess"
}
# ---------- ECS Task Execution Role ----------
# https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/developerguide/task_execution_IAM_role.html
resource "aws_iam_role" "ecs_task_execution" {
name = "${var.name}-ecsTaskExecutionRole"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Action = "sts:AssumeRole"
Effect = "Allow"
Principal = {
Service = "ecs-tasks.amazonaws.com"
}
}
]
})
}
resource "aws_iam_role_policy_attachment" "ecs_task_execution" {
role = aws_iam_role.ecs_task_execution.name
policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy"
}
resource "aws_iam_role_policy_attachment" "ecr_power_user" {
role = aws_iam_role.ecs_task_execution.name
policy_arn = "arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryPowerUser"
}
# ---------- Fargate ECS Cluster ----------
resource "aws_security_group" "ecs" {
name = "${var.name}-ecs"
vpc_id = local.vpc_id
description = "Managed by Terraform"
tags = {
Name = "${var.name}-ecs"
}
}
resource "aws_security_group_rule" "ecs_ingress_alb_security" {
# ECSへのアクセスを許可する(セキュリティグループ)
type = "ingress"
security_group_id = aws_security_group.ecs.id
source_security_group_id = var.from_security_group_id
from_port = var.from_port
to_port = var.to_port
protocol = "TCP"
}
resource "aws_security_group_rule" "ecs_egress" {
# ECS → NAT経由でのインターネットアクセスを許可する
type = "egress"
security_group_id = aws_security_group.ecs.id
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
resource "aws_ecs_cluster" "cluster" {
name = "${var.name}-cluster"
}
resource "aws_ecs_task_definition" "task_definition" {
family = "${var.name}-task"
requires_compatibilities = ["FARGATE"]
network_mode = "awsvpc"
cpu = var.container_cpu
memory = var.container_memory
task_role_arn = aws_iam_role.ecs.arn
execution_role_arn = aws_iam_role.ecs_task_execution.arn
container_definitions = jsonencode(var.container_definitions)
}
resource "aws_ecs_service" "service" {
launch_type = "FARGATE"
name = "${var.name}-service"
cluster = aws_ecs_cluster.cluster.id
task_definition = aws_ecs_task_definition.task_definition.arn
platform_version = var.fargate_platform_version
desired_count = var.task_count
# デバッグしたいのでECS Execを有効に
enable_execute_command = true
network_configuration {
# プライベートサブネット指定
subnets = [data.aws_subnet.private_subnet.id]
security_groups = [
aws_security_group.ecs.id,
]
assign_public_ip = false
}
load_balancer {
target_group_arn = var.lb_target_group_arn
container_name = var.name
container_port = 80
}
depends_on = [
aws_ecs_task_definition.task_definition,
]
}
# ---------- CloudWatch Log Group ----------
resource "aws_cloudwatch_log_group" "log_group" {
name = var.name
retention_in_days = var.log_retention_in_days
}
variable "name" {
description = "ECSクラスタやコンテナ名"
type = string
}
variable "private_subnet_id" {
description = "プライベートサブネットのID"
type = string
}
variable "from_port" {
description = "リクエストを許可するポート番号"
default = 80
}
variable "from_security_group_id" {
description = "リクエストを許可するセキュリティグループのID"
type = string
}
variable "to_port" {
description = "リクエストを受け付けるポート番号"
default = 80
}
variable "fargate_platform_version" {
description = "Fargateのプラットフォームバージョン"
type = string
}
variable "task_count" {
description = "Fargateのタスク数"
default = 1
}
variable "lb_target_group_arn" {
description = "LBのtarget_groupのARN"
type = string
}
variable "container_definitions" {
description = "コンテナ定義"
default = []
}
variable "log_retention_in_days" {
description = "ログの保持期間"
default = 0
}
variable "container_cpu" {
description = "コンテナのCPU"
default = 256
}
variable "container_memory" {
description = "コンテナのメモリ"
default = 512
}
output "ecs_security_group_id" {
description = "ecsのセキュリティグループID"
value = aws_security_group.ecs.id
}
ElastiCache
Memcachedを使うかRedisを使うかでresourceも異なったので、memcached_clusterという命名にしてる。
data "aws_subnet" "private_subnet" {
id = var.private_subnet_id
}
# ---------- Memcached Cluster ----------
resource "aws_security_group" "memcached" {
name = "${var.name}-memcached"
vpc_id = data.aws_subnet.private_subnet.vpc_id
description = "Managed by Terraform"
tags = {
Name = "${var.name}-memcached"
}
}
resource "aws_security_group_rule" "memcached_ingress" {
# memcachedへのアクセスを許可する(セキュリティグループ)
type = "ingress"
security_group_id = aws_security_group.memcached.id
source_security_group_id = var.from_security_group_id
from_port = var.from_port
to_port = var.to_port
protocol = "TCP"
}
resource "aws_elasticache_subnet_group" "memcached_subnets" {
name = "${var.name}-subnets"
subnet_ids = [var.private_subnet_id]
}
resource "aws_elasticache_cluster" "memcached_cluster" {
cluster_id = "${var.name}-cluster"
engine = "memcached"
node_type = var.node_type
num_cache_nodes = var.num_cache_nodes
parameter_group_name = "default.memcached1.6"
port = var.to_port
security_group_ids = [aws_security_group.memcached.id]
subnet_group_name = aws_elasticache_subnet_group.memcached_subnets.name
}
variable "name" {
description = "elasticacheのクラスタ名"
default = ""
}
variable "private_subnet_id" {
description = "設置するプライベートサブネットのID"
default = ""
}
variable "node_type" {
description = "ノードスペック"
default = ""
}
variable "num_cache_nodes" {
description = "ノード数"
default = 0
}
variable "from_port" {
description = "リクエストを許可するポート番号"
default = ""
}
variable "from_security_group_id" {
description = "リクエストを許可するセキュリティグループのID"
default = ""
}
variable "to_port" {
description = "リクエストを受け付けるポート番号"
default = ""
}
RDS
Redmineに使ったイメージが、起動時にRDBにデータベースとテーブルが
- なければ勝手に作成
- あれば何もしない
という挙動だったので、特にデータベースはTerraformで管理していない。
#--------------------------------------------------------------
# Security group
#--------------------------------------------------------------
resource "aws_security_group" "rds-sg" {
name = "${var.instance_name}-rds-sg"
description = "RDS service security group for rds-sg"
vpc_id = var.vpc_id
tags = {
Name = "rds-sg of mysql"
}
}
resource "aws_security_group_rule" "rds-sg-rule" {
security_group_id = aws_security_group.rds-sg.id
type = "ingress"
from_port = 3306
to_port = 3306
protocol = "tcp"
# 指定したプライベートのサブネットからのアクセスを許可する
cidr_blocks = [for cidr_block in var.ingress_cidr_blocks : cidr_block]
}
#--------------------------------------------------------------
# Subnet group
#--------------------------------------------------------------
resource "aws_db_subnet_group" "rds-subnet-group" {
name = "${var.instance_name}-rds-subnet-group"
description = "rds subnet group"
subnet_ids = [for id in var.subnet_ids : id]
}
#--------------------------------------------------------------
# RDS
#--------------------------------------------------------------
resource "aws_db_instance" "db" {
engine = "mysql"
engine_version = "5.7"
identifier = var.instance_name
instance_class = var.instance_class
# 汎用SSD
storage_type = "gp2"
parameter_group_name = "default.mysql5.7"
final_snapshot_identifier = "final-snapshot"
allocated_storage = var.allocated_storage
max_allocated_storage = var.max_allocated_storage
username = var.master_username
password = var.master_password
backup_retention_period = var.backup_retention_period
backup_window = var.backup_window
maintenance_window = var.maintenance_window
vpc_security_group_ids = [aws_security_group.rds-sg.id]
db_subnet_group_name = aws_db_subnet_group.rds-subnet-group.name
apply_immediately = var.apply_immediately
}
variable "subnet_ids" {
description = "RDSを配置するサブネットIDリスト (RDSを使うために最低2つ必要)"
default = []
type = list(string)
}
variable "ingress_cidr_blocks" {
description = "RDSへのアクセスを許可するCIDRブロックを指定"
default = []
type = list(string)
}
variable "vpc_id" {
description = "RDSを配置するVPCのID"
default = ""
}
variable "instance_name" {
description = "インスタンスの名前"
default = ""
}
variable "instance_class" {
description = "インスタンスクラス"
default = ""
}
variable "allocated_storage" {
description = "RDSの最低ストレージ"
default = ""
}
variable "max_allocated_storage" {
description = "RDSのMAXのストレージ"
default = ""
}
variable "master_username" {
description = "RDSのマスターユーザー名"
type = string
}
variable "master_password" {
description = "RDSのマスターパスワード"
type = string
}
variable "backup_retention_period" {
description = "バックアップの保持期間(日)。0〜35が指定可能。"
default = 7
}
variable "backup_window" {
description = "RDSのバックアップ実行時間帯(UTC)。デフォルトはmaintenance_window以外の時間帯。maintenance_windowと被らないように注意。"
default = "19:01-17:59"
}
variable "maintenance_window" {
description = "RDSのメンテナンス実行時間帯(UTC)。デフォルトは日曜03:30 ~ 04:00にしてる。"
default = "Sat:18:30-Sat:19:00"
}
variable "apply_immediately" {
description = "RDSへの設定変更を即時反映するかどうかの設定。即時反映しない場合は、次回のメンテナンスウィンドウで反映される。"
default = false
}
output "rds_address" {
description = "RDSのホストネーム"
value = aws_db_instance.db.address
}
S3
resource "aws_s3_bucket" "bucket" {
bucket = var.name
acl = var.acl
tags = {
Name = var.name
}
}
resource "aws_s3_bucket_public_access_block" "public_access_block" {
bucket = aws_s3_bucket.bucket.id
block_public_acls = var.is_block_public_acls
block_public_policy = var.is_block_public_policy
ignore_public_acls = var.is_ignore_public_acls
restrict_public_buckets = var.is_restrict_public_buckets
}
variable "name" {
description = "バケット名"
default = ""
}
variable "acl" {
description = "aclの設定"
default = "private"
}
variable "is_block_public_acls" {
default = true
}
variable "is_block_public_policy" {
default = true
}
variable "is_ignore_public_acls" {
default = true
}
variable "is_restrict_public_buckets" {
default = true
}
output "bucketname" {
description = "バケット名"
value = aws_s3_bucket.bucket.bucket
}
WAF ACL
resource "aws_wafv2_web_acl" "acl" {
name = var.name
scope = "REGIONAL"
# デフォでRule以外のIPは弾く
default_action {
block {}
}
dynamic "rule" {
for_each = toset(var.allow_ip_sets)
content {
name = rule.value.name
priority = rule.value.priority
action {
allow {}
}
statement {
ip_set_reference_statement {
arn = rule.value.arn
}
}
visibility_config {
cloudwatch_metrics_enabled = false
metric_name = "${rule.value.name}-rule-firewall"
sampled_requests_enabled = false
}
}
}
visibility_config {
cloudwatch_metrics_enabled = false
metric_name = "${var.name}-firewall"
sampled_requests_enabled = false
}
}
resource "aws_wafv2_web_acl_association" "association" {
resource_arn = var.association_resource_arn
web_acl_arn = aws_wafv2_web_acl.acl.arn
}
variable "name" {
description = "ACLの名前"
type = string
}
variable "allow_ip_sets" {
description = "許可するIPSetのリスト"
type = list(map(any))
}
variable "association_resource_arn" {
description = "ACLと紐付けるリソースのARN(ALBやAPI Gatewayとか)"
type = string
}
WAF IPSet
resource "aws_wafv2_ip_set" "ip_set" {
for_each = var.name_map
name = each.key
scope = "REGIONAL"
ip_address_version = "IPV4"
addresses = each.value
}
variable "name_map" {
# key→名前、value→アドレス
description = "IPセットの名前とアドレスの組み合わせ"
type = map(any)
}
DataSync
# ---------- source iam policy ----------
resource "aws_iam_role" "datasync_source" {
name = "${var.name}_datasync_source"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Action = "sts:AssumeRole"
Effect = "Allow"
Principal = {
Service = "datasync.amazonaws.com"
}
}
]
})
}
data "aws_iam_policy_document" "datasync_source_policy_document" {
version = "2012-10-17"
statement {
effect = "Allow"
actions = [
"s3:GetBucketLocation",
"s3:ListBucket",
"s3:ListBucketMultipartUploads",
]
resources = ["arn:aws:s3:::${var.source_bucket_name}"]
}
statement {
effect = "Allow"
actions = [
"s3:AbortMultipartUpload",
"s3:DeleteObject",
"s3:GetObject",
"s3:ListMultipartUploadParts",
"s3:PutObjectTagging",
"s3:GetObjectTagging",
"s3:PutObject",
]
resources = ["arn:aws:s3:::${var.source_bucket_name}/*"]
}
}
resource "aws_iam_policy" "datasync_source_policy" {
name_prefix = aws_iam_role.datasync_source.name
policy = data.aws_iam_policy_document.datasync_source_policy_document.json
}
resource "aws_iam_role_policy_attachment" "datasync_source_attachement" {
role = aws_iam_role.datasync_source.name
policy_arn = aws_iam_policy.datasync_source_policy.arn
}
# ---------- destination iam policy ----------
resource "aws_iam_role" "datasync_destination" {
name = "${var.name}_datasync_destination"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Action = "sts:AssumeRole"
Effect = "Allow"
Principal = {
Service = "datasync.amazonaws.com"
}
}
]
})
}
data "aws_iam_policy_document" "datasync_destination_policy_document" {
version = "2012-10-17"
statement {
effect = "Allow"
actions = [
"s3:GetBucketLocation",
"s3:ListBucket",
"s3:ListBucketMultipartUploads",
]
resources = ["arn:aws:s3:::${var.destination_bucket_name}"]
}
statement {
effect = "Allow"
actions = [
"s3:AbortMultipartUpload",
"s3:DeleteObject",
"s3:GetObject",
"s3:ListMultipartUploadParts",
"s3:PutObjectTagging",
"s3:GetObjectTagging",
"s3:PutObject",
]
resources = ["arn:aws:s3:::${var.destination_bucket_name}/*"]
}
}
resource "aws_iam_policy" "datasync_destination_policy" {
name_prefix = aws_iam_role.datasync_destination.name
policy = data.aws_iam_policy_document.datasync_destination_policy_document.json
}
resource "aws_iam_role_policy_attachment" "datasync_destination_attachement" {
role = aws_iam_role.datasync_destination.name
policy_arn = aws_iam_policy.datasync_destination_policy.arn
}
# ---------- main resource ----------
resource "aws_datasync_location_s3" "source" {
s3_bucket_arn = "arn:aws:s3:::${var.source_bucket_name}"
subdirectory = var.source_subdirectory
s3_config {
bucket_access_role_arn = aws_iam_role.datasync_source.arn
}
}
resource "aws_datasync_location_s3" "destination" {
s3_bucket_arn = "arn:aws:s3:::${var.destination_bucket_name}"
subdirectory = var.destination_subdirectory
s3_config {
bucket_access_role_arn = aws_iam_role.datasync_destination.arn
}
}
resource "aws_cloudwatch_log_group" "log_group" {
name = "${var.name}_datasync_log_group"
retention_in_days = 0
}
resource "aws_datasync_task" "task" {
name = var.name
source_location_arn = aws_datasync_location_s3.source.arn
destination_location_arn = aws_datasync_location_s3.destination.arn
cloudwatch_log_group_arn = aws_cloudwatch_log_group.log_group.arn
options {
bytes_per_second = -1
posix_permissions = "NONE"
uid = "NONE"
gid = "NONE"
# デフォルトだとログを送ってくれないので明示的に指定
# TRANSFERだとファイルごとにロクでそうで料金が心配なのでBASICにする
log_level = "BASIC"
}
}
variable "name" {
description = "datasync関連のリソースのprefix"
default = ""
}
variable "source_bucket_name" {
description = "datasyncの移行元バケット名"
default = ""
}
variable "source_subdirectory" {
description = "datasyncの移行元サブディレクトリ"
default = ""
}
variable "source_bucket_access_role_arn" {
description = "datasyncの移行元バケットへアクセスするためのロール"
default = ""
}
variable "destination_bucket_name" {
description = "datasyncの移行先バケット名"
default = ""
}
variable "destination_subdirectory" {
description = "datasyncの移行先サブディレクトリ"
default = ""
}
variable "destination_bucket_access_role_arn" {
description = "datasyncの移行先バケットへアクセスするためのロール"
default = ""
}
main.tf
実際に各々のmoduleを呼び出すと下記のような感じになる。
data "aws_subnet" "private-a" {
id = "subnet-xx"
}
data "aws_subnet" "private-c" {
id = "subnet-xx"
}
# 事前にSecretはコンソールで登録しておく
data "aws_secretsmanager_secret" "prd_redmine_secret" {
arn = "arn:aws:secretsmanager:xxx"
}
data "aws_secretsmanager_secret_version" "prd_redmine_secret_version" {
secret_id = data.aws_secretsmanager_secret.prd_redmine_secret.id
}
data "aws_region" "current" {}
locals {
vpc_id = data.aws_subnet.private-a.vpc_id
datasync_source_bucket_name = "xxx-redmine"
datasync_destination_bucket_name = "xxx-redmine"
prd_secret_map = jsondecode(data.aws_secretsmanager_secret_version.prd_redmine_secret_version.secret_string)
}
# ---------- ECR ----------
module "ecr" {
source = "../../modules/ecr"
name = "redmine"
}
# ---------- RDS ----------
module "rds_instance" {
source = "../../modules/rds_instance"
# RDSを配置するサブネットのID
subnet_ids = [
data.aws_subnet.private-a.id,
data.aws_subnet.private-c.id
]
# RDSへのアクセスを許可するCIDR
ingress_cidr_blocks = [
data.aws_subnet.private-a.cidr_block,
data.aws_subnet.private-c.cidr_block
]
vpc_id = local.vpc_id
instance_name = "{DBのname}"
instance_class = "db.t3.micro"
master_username = local.prd_secret_map["REDMINE_USER"]
master_password = local.prd_secret_map["REDMINE_PASSWORD"]
allocated_storage = 20
max_allocated_storage = 1000
apply_immediately = true
}
# ---------- ALB ----------
module "alb" {
source = "../../modules/alb"
name = "redmine"
public_subnet_ids = [
"subnet-xx",
"subnet-xx"
]
acm_domain = "*.xx.com"
}
module "alb_target_group" {
source = "../../modules/alb_target_group"
# ホスト名
target_host = "redmine.xx.com"
# 対象ALBのlistenerのARNを指定
alb_listener_arn = module.alb.alb_listener_arn
alb_listener_priority = 2
target_group_name = "redmine-target-group"
target_port = 80
target_protocol = "HTTP"
target_vpc_id = local.vpc_id
target_type = "ip"
health_check_path = "/"
}
module "waf_acl" {
source = "../../modules/waf_acl"
name = "redmine_firewall"
# ACLとALBを紐付ける
association_resource_arn = module.alb.arn
allow_ip_sets = [
{
name = "会社",
priority = 1,
arn = "arn:aws:wafv2:xx"
},
]
depends_on = [
module.alb
]
}
# ---------- ECS クラスタ ----------
module "ecs" {
source = "../../modules/ecs"
name = "redmine"
# private-a
private_subnet_id = "subnet-xx"
# ALBのsecurity_group_id
from_security_group_id = module.alb.alb_security_group_id
# ecs_serviceに紐付けるLBのtarget_groupのARN
lb_target_group_arn = module.alb_target_group.arn
fargate_platform_version = "1.4.0"
task_count = 1
log_retention_in_days = 30
# オンプレの時のスペックに合わせる
container_cpu = 2048
container_memory = 6144
container_definitions = [
{
name = "redmine"
image = "${module.ecr.ecr_repository_url}:v0.0.1"
essential = true
portMappings = [
{ "protocol" : "tcp", "containerPort" = 80 }
]
logConfiguration = {
logDriver = "awslogs"
options = {
awslogs-group = "redmine"
awslogs-region = data.aws_region.current.name
awslogs-stream-prefix = "redmine"
}
}
environment = [
{ "name" : "TZ", "value" : "Asia/Tokyo" },
{ "name" : "DB_ADAPTER", "value" : "mysql2" },
{ "name" : "DB_HOST", "value" : "${module.rds_instance.rds_address}" },
{ "name" : "DB_PORT", "value" : "${local.prd_secret_map["RDS_PORT"]}" },
{ "name" : "DB_NAME", "value" : "${local.prd_secret_map["REDMINE_DB"]}" },
{ "name" : "DB_USER", "value" : "${local.prd_secret_map["REDMINE_RDS_MASTER_USER"]}" },
{ "name" : "DB_PASS", "value" : "${local.prd_secret_map["REDMINE_RDS_MASTER_PASSWORD"]}" },
{ "name" : "DB_ENCODING", "value" : "utf8mb4" },
{ "name" : "REDMINE_PORT", "value" : "${local.prd_secret_map["REDMINE_PORT"]}" },
]
}
]
}
# ---------- S3(Redmineの画像管理) ----------
module "s3_bucket" {
source = "../../modules/s3_bucket"
name = "xxx-redmine"
}
# ---------- Elasticache(Memcached) ----------
module "memcached_cluster" {
source = "../../modules/memcached_cluster"
name = "redmine"
private_subnet_id = data.aws_subnet.private-a.id
node_type = "cache.t4g.micro"
num_cache_nodes = 1
from_security_group_id = module.ecs.ecs_security_group_id
from_port = ${local.prd_secret_map["MEMCACHED_PORT"]}
to_port = ${local.prd_secret_map["MEMCACHED_PORT"]}
}
# ---------- Datasync(S3データの移行) ----------
module "datasync" {
source = "../../modules/datasync_s3"
name = "redmine"
source_bucket_name = "xxx-redmine"
source_subdirectory = "/xxx"
destination_bucket_name = "xxx-redmine"
destination_subdirectory = "/xxx"
}
# CloudWatch Logs のリージョンレベルのポリシー(特定のリソースには結びつかない)
# これがないとDataSync → CloudWatchにログが送れない
data "aws_iam_policy_document" "log_publishing_policy" {
version = "2012-10-17"
statement {
sid = "DataSyncLogsToCloudWatchLogs"
effect = "Allow"
principals {
identifiers = ["datasync.amazonaws.com"]
type = "Service"
}
actions = [
"logs:PutLogEvents",
"logs:CreateLogStream"
]
resources = ["arn:aws:logs:*"]
}
}
resource "aws_cloudwatch_log_resource_policy" "log_publishing_policy" {
policy_document = data.aws_iam_policy_document.log_publishing_policy.json
policy_name = "log-publishing-policy"
}
# ---------- WAF IP Set ----------
module "waf_ip_set" {
source = "../../modules/waf_ip_set"
name_map = {
"会社" : [
"xxx.xxx.x.xxx/32",
],
}
}
実際の移行手順
移行する必要があったのは下記の2つだった。
①MySQLに入っているRedmineのユーザーデータ
こちらはシンプルにmysqldumpを行った。
mysqldumpは軽い処理ではないので、事前にサーバーのCPU使用率やメモリ負荷を調べておくこと。
- オンプレ環境からmysqldumpでデータを取ってくる
- 事前に踏み台サーバーを建てる
- 踏み台サーバーにscpでダンプファイルを転送
- 踏み台サーバーにsshして、RDSに対してダンプを投入
# sshポートフォワード (MySQL)
$ ssh -NL 12345:{オンプレのIP}:3306 {sshユーザー名}@{オンプレのIP}
# 別タブで実行
$ mysqldump -u {ユーザー名} -p{パスワード} -h 127.0.0.1 -P 12345 --single-transaction --compress --skip-column-statistics {DBのname} > ~/Desktop/redmine.sql
# jump-server(踏み台サーバー)にダンプファイルを転送
$ scp -i jump-server.pem redmine.sql ec2-user@{踏み台サーバーのPublicIP}:
# jump-server(踏み台サーバー)にssh
$ ssh -i jump-server.pem ec2-user@{踏み台サーバーのPublicIP}
# ダンプを投入する。
$ mysql -h xxx.rds.amazonaws.com -u {ユーザー名} -p{パスワード} {DBのname} < redmine.sql
②S3に入っている画像、動画
DataSyncを採用したので、コンソールからポチるだけだった。
10TB近くあったが、40分程で転送は終了した。
DataSync自体が
- 1GBあたり料金が6円
- 1秒あたり3~4GBの転送速度
なので、採用してよかった。
実際に移行してみた感想
ハードだったが、楽しかった
インフラ構築以外にもやる事が結構あり、例えば
-
オンプレ時代の構成の把握/調査
→ オンプレ時代のドキュメントがなかった為 -
移行実施日時を社内、社外に通知周りのディレクション
→ 社内DNSサーバーの設定を切り替える都合で、業務時間内に2時間程度止めて移行する必要があった且つ、Redmine自体が外部の協力会社の人も使っていた。各プロジェクトのPMと協力して外部の会社にメールを送ってもらって認知してもらった。
などのディレクション、舵取り周りも行った。
(中々大変だったが、楽しかった。)
計画のマイルストーンを建てて良かった
基本的にこういう社内ツール系のタスクは明確な期限がなく、後ろ倒しにどんどん出来てしまう。
後ろ倒しにするといつまで経っても終わらないので、しっかりと期限を決めて「何月に何が終わっているか」を明確化して、作業を進める事が重要だなと改めて感じた。
最終的に通常業務と並行する形で3ヶ月という期間で移行を完了させられたのは、1月にマイルストーンを建てて置いたのが大きかったと思う。
1月: 移行先のAWS環境構築
2月: 移行先のAWS環境のデバッグ
各プロジェクト/外部会社への周知
3月: 移行テスト、移行実施
とにかくオーナーシップが大事
移行は1週間で終わるタスクでもなく、大体長丁場 & 日の目を見るまでに時間がかかるタスクになる。
精神論にはなってしまうが、「最後まで責任持ってやり遂げてやる!」 というオーナーシップを高く持って、遂行するのが大事だなって改めて思った。
Discussion