こんにちは、アプリケーション開発を担当している田中です。オンプレミス環境からAWS環境へバックエンドAPIを移行しました。移行対象のコマンド数が100本ほどあり、リスク軽減のため段階的に移行をしたので、どう段階移行を実現したのかを書いておこうと思います。
要件
現行環境(オンプレ)と新環境(AWS)を並行稼働し、APIコマンドを段階的に移行する移行はURLパスごとのかたまりで行う※sample-api-app.io/{機能名}/* 程度の単位でいくつかに分割して順次移行ホワイトリスト形式のIPアドレスベースのアクセス制限を行う(現行踏襲)IPアクセス制限はURLパス単位に設定できるようにする
前提
現行環境(オンプレ)と新環境(AWS)間は、インターナルなネットワークで接続が可能手前にクラウドWAFがあり、このWAFにより接続元IPがヘッダー情報に格納されるため、アクセス制限の際はヘッダーからIPを参照する
段階移行のイメージ
構築手順
1. URLパスベースの振り分け
振り分けはALB(Application Load Balancer)のリスナールールで行います。ALBのパスベースでの振り分け設定方法は「ALB パスベースルーティング」などでネット検索するとたくさん出てくるので、コンソールからの設定方法は割愛します。のちほどterraformのサンプルコードを記載しているので参考にしてみてください。以下2つのリスナールールを用意し、それぞれに条件を追加します。
■80ポートのリスナールール
HTTPS(443)へリダイレクトする
■443ポートのリスナールール現行環境(オンプレ)VMのIP群と、新環境(AWS)のECSタスクのIP群のターゲットグループをそれぞれ作成した上で以下の条件をリスナールールに追加します。
新環境(AWS)への振り分け対象URLパスパターンの場合、新環境のターゲットグループへ振り分け1つのリスナールールに付与できる条件は最大5つなので、5つ以上のパスパターンを振り分ける場合は上記と同様のリスナールールを追加すべてのアクセス(/*)を現行環境(オンプレ)のターゲットグループへ振り分け(この条件を最も低い優先度で登録し、上記条件に一致しないパスをすべてオンプレへ転送します)
2. IPアドレスでのアクセス制限
IPアドレスによるアクセス制限の方式は、以下3つの方式から検討し、AWS WAFによるアクセス制限を採用しました。
AWS WAFこれを採用した◯ALBリスナールール1ルールにつき5条件までしか設定できず、適用するパスを限定する条件設定と組み合わせると複雑になってしまう△セキュリティグループヘッダー情報を条件に制限がかけられないため要件を満たせない✕IPベースのアクセス制限方式を検討
3. terraformサンプル
弊社ではAWSリソースをterraformで実装してコード管理しています。以下にterraformで実装した際のサンプルコードを掲載します。今回はVPCやサブネット、ECSリソースは別のモジュールで作成するものとして割愛しています。また、CodeDeployによるBlue/Greenデプロイを想定してBlue/Greenそれぞれのリスナーを用意しています。
ディレクトリ構成
main.tf
modules
├alb
│ ├alb.tf
│ ├target_group.tf
│ ├variables.tf
│ └outputs.tf
│
├waf
│ ├waf.tf
│ └variables.tf
│
main.tf
locals {
application_name = "sample-api-app"
}
# --------------------------------------------------------
# Application Load Balancer
# --------------------------------------------------------
module "alb" {
source = "../../modules/alb"
application_name = local.application_name
private_subnet_ids = module.network.vpc.private_subnets # 別途moduleを作って作成してください
certificate_arn = "arn:aws:acm:ap-northeast-1:xxxxxxxxxxxx:certificate/uuuuuuuu-uuuu-iiii-dddd-000000000000"
security_group_id = module.security_groups.alb.id # 別途moduleを作って作成してください
vpc_id = module.network.vpc.vpc_id # 別途moduleを作って作成してください
aws_health_check_path = "/aws-health-check-path" # アプリケーションに合わせて設定
onpre_health_check_path = "/onpre-health-check-path" # アプリケーションに合わせて設定
routing_paths1 = ["/aaa*", "/bbb*", "/ccc*", "/ddd*", "/eee*"]
listener_rule_priority1 = 1
routing_paths2 = ["/fff*", "/ggg*", "/hhh*", "/iii*", "/jjj*"]
listener_rule_priority2 = 2
}
# --------------------------------------------------------
# AWS WAF
# --------------------------------------------------------
module "waf" {
source = "../../modules/waf"
application_name = local.application_name
alb_arn = module.alb.aws_alb_arn
allowed_ip_address = [
"xxx.xxx.xxx.xxx/32",
"yyy.yyy.yyy.yyy/30"
]
ip_header_name = "X-WAF-Connecting-IP" # 手前のクラウドWAFで接続元IPアドレスが格納されるヘッダーのパラメータ名
}
modules/alb
variable "application_name" {
type = string
description = "アプリケーション名"
}
variable "private_subnet_ids" {
type = list(string)
}
variable "certificate_arn" {
type = string
description = "証明書のARN"
}
variable "security_group_id" {
type = string
}
variable "vpc_id" {
type = string
}
variable "aws_health_check_path" {
type = string
}
variable "onpre_health_check_path" {
type = string
}
variable "routing_paths1" {
type = list(string)
}
variable "listener_rule_priority1" {
type = number
}
variable "routing_paths2" {
type = list(string)
}
variable "listener_rule_priority2" {
type = number
}
locals {
# ALBの名前は32文字まで
alb_name = "${substr(var.application_name, 0, 28)}-alb"
}
# --------------------------------------------------------
# ALB
# --------------------------------------------------------
resource "aws_lb" "alb" {
name = local.alb_name
internal = true
load_balancer_type = "application"
security_groups = [var.security_group_id]
subnets = var.private_subnet_ids
enable_deletion_protection = false
drop_invalid_header_fields = true
idle_timeout = 300
xff_header_processing_mode = "preserve"
tags = {
Name = "${var.application_name}-alb"
}
}
# --------------------------------------------------------
# Listener
# --------------------------------------------------------
# Blue/Greenデプロイ用にそれぞれのlistenerを作成する
# http(80, 8080) -> https(443, 8443) にリダイレクトする
resource "aws_lb_listener" "blue_https" {
load_balancer_arn = aws_lb.alb.arn
port = 443
protocol = "HTTPS"
ssl_policy = "ELBSecurityPolicy-TLS13-1-2-2021-06"
certificate_arn = var.certificate_arn
default_action {
type = "fixed-response"
fixed_response {
content_type = "text/plain"
message_body = "404: page not found"
status_code = "404"
}
}
}
resource "aws_lb_listener" "blue_http" {
load_balancer_arn = aws_lb.alb.arn
port = 80
protocol = "HTTP"
default_action {
type = "redirect"
redirect {
port = "443"
protocol = "HTTPS"
status_code = "HTTP_301"
}
}
}
resource "aws_lb_listener" "green_https" {
load_balancer_arn = aws_lb.alb.arn
port = 8443
protocol = "HTTPS"
ssl_policy = "ELBSecurityPolicy-TLS13-1-2-2021-06"
certificate_arn = var.certificate_arn
default_action {
type = "fixed-response"
fixed_response {
content_type = "text/plain"
message_body = "404: page not found"
status_code = "404"
}
}
}
resource "aws_lb_listener" "green_http" {
load_balancer_arn = aws_lb.alb.arn
port = 8080
protocol = "HTTP"
default_action {
type = "redirect"
redirect {
port = "8443"
protocol = "HTTPS"
status_code = "HTTP_301"
}
}
}
# --------------------------------------------------------
# Listener Rule
# --------------------------------------------------------
# Blue/Green x AWS/オンプレごとにリスナールールを作成する
# AWS環境へ振り分けるルールは、パスパターン5つまでしか設定できないため、パスパターンの数に合わせてルールを増やす
# http(80, 8080)はhttps(443, 8443)にリダイレクトするため、httpsのルールのみを追加すればOK
# これらのルールはAWSへ完全移行後に削除します
resource "aws_lb_listener_rule" "aws_blue_listener_rule1" {
listener_arn = aws_lb_listener.blue_https.arn
priority = var.listener_rule_priority1
action {
type = "forward"
target_group_arn = aws_lb_target_group.blue.arn
}
condition {
path_pattern {
values = var.routing_paths1
}
}
lifecycle {
ignore_changes = [action]
}
tags = {
Name = "${var.application_name}-aws-rule1"
}
}
resource "aws_lb_listener_rule" "aws_blue_listener_rule2" {
listener_arn = aws_lb_listener.blue_https.arn
priority = var.listener_rule_priority2
action {
type = "forward"
target_group_arn = aws_lb_target_group.blue.arn
}
condition {
path_pattern {
values = var.routing_paths2
}
}
lifecycle {
ignore_changes = [action]
}
tags = {
Name = "${var.application_name}-aws-rule2"
}
}
resource "aws_lb_listener_rule" "aws_green_listener_rule1" {
listener_arn = aws_lb_listener.green_https.arn
priority = var.listener_rule_priority1
action {
type = "forward"
target_group_arn = aws_lb_target_group.green.arn
}
condition {
path_pattern {
values = var.routing_paths1
}
}
lifecycle {
ignore_changes = [action]
}
tags = {
Name = "${var.application_name}-aws-rule1"
}
}
resource "aws_lb_listener_rule" "aws_green_listener_rule2" {
listener_arn = aws_lb_listener.green_https.arn
priority = var.listener_rule_priority2
action {
type = "forward"
target_group_arn = aws_lb_target_group.green.arn
}
condition {
path_pattern {
values = var.routing_paths2
}
}
lifecycle {
ignore_changes = [action]
}
tags = {
Name = "${var.application_name}-aws-rule2"
}
}
resource "aws_lb_listener_rule" "onpre_blue_listener_rule" {
listener_arn = aws_lb_listener.blue_https.arn
priority = 100 # 最も低い優先度で設定する
action {
type = "forward"
target_group_arn = aws_lb_target_group.onpre.arn
}
condition {
path_pattern {
values = ["/*"]
}
}
tags = {
Name = "${var.application_name}-onpre-rule"
}
}
resource "aws_lb_listener_rule" "onpre_green_listener_rule" {
listener_arn = aws_lb_listener.green_https.arn
priority = 100 # 最も低い優先度で設定する
action {
type = "forward"
target_group_arn = aws_lb_target_group.onpre.arn
}
condition {
path_pattern {
values = ["/*"]
}
}
tags = {
Name = "${var.application_name}-onpre-rule"
}
}
# --------------------------------------------------------
# Target Group
# --------------------------------------------------------
# AWS環境のBlue、Greenおよび、オンプレ環境の3つのターゲットグループを作成する
# AWSへの完全移行後にオンプレ環境のターゲットグループは削除する
resource "aws_lb_target_group" "blue" {
name_prefix = "a-tg1-"
port = 80
protocol = "HTTP"
vpc_id = var.vpc_id
target_type = "ip"
health_check {
path = var.aws_health_check_path
protocol = "HTTP"
timeout = 30
interval = 60
matcher = 200
healthy_threshold = 3
unhealthy_threshold = 3
}
tags = {
Name = "${var.application_name}-aws-albtg1"
}
lifecycle {
create_before_destroy = true
}
}
resource "aws_lb_target_group" "green" {
name_prefix = "a-tg2-"
port = 80
protocol = "HTTP"
vpc_id = var.vpc_id
target_type = "ip"
health_check {
path = var.aws_health_check_path
protocol = "HTTP"
timeout = 30
interval = 60
matcher = 200
healthy_threshold = 3
unhealthy_threshold = 3
}
tags = {
Name = "${var.application_name}-aws-albtg2"
}
lifecycle {
create_before_destroy = true
}
}
resource "aws_lb_target_group" "onpre" {
name_prefix = "tg-op-"
port = 80
protocol = "HTTP"
vpc_id = var.vpc_id
target_type = "ip"
health_check {
path = var.onpre_health_check_path
protocol = "HTTP"
timeout = 30
interval = 60
matcher = 200
healthy_threshold = 3
unhealthy_threshold = 3
}
tags = {
Name = "${var.application_name}-onpre-albtg"
}
lifecycle {
create_before_destroy = true
}
}
resource "aws_lb_target_group_attachment" "onpre" {
for_each = toset(["xxx.xxx.xxx.xxx", "yyy.yyy.yyy.yyy"]) # オンプレ環境WebサーバーのIPアドレス
target_group_arn = aws_lb_target_group.onpre.arn
target_id = each.value
availability_zone = "all" # オンプレ環境のプライベートIPはVPC外にあるためallを指定
port = 80
}
output "aws_alb_arn" {
value = aws_lb.alb.arn
}
modules/waf
variable "application_name" {
type = string
description = "アプリケーション名"
}
variable "allowed_ip_address" {
type = list(string)
description = "アクセス許可するIPアドレスのリスト"
}
variable "alb_arn" {
type = string
description = "作成したALBのARN"
}
variable "ip_header_name" {
type = string
description = "接続元IPが格納されているヘッダー名"
}
resource "aws_wafv2_ip_set" "_" {
name = "${var.application_name}-waf-ipset"
description = "Allowed ip set for ${var.application_name}"
scope = "REGIONAL"
ip_address_version = "IPV4"
addresses = var.allowed_ip_address
}
resource "aws_wafv2_web_acl" "_" {
name = "${var.application_name}-waf-acl"
description = "ACL for ${var.application_name}"
scope = "REGIONAL"
default_action {
block {}
}
rule {
name = "WAFIPsetRule"
priority = 1
action {
allow {}
}
statement {
ip_set_reference_statement {
arn = aws_wafv2_ip_set._.arn
ip_set_forwarded_ip_config {
header_name = var.ip_header_name
fallback_behavior = "MATCH"
position = "ANY"
}
}
}
visibility_config {
cloudwatch_metrics_enabled = false
metric_name = "${var.application_name}-waf-ipset"
sampled_requests_enabled = false
}
}
visibility_config {
cloudwatch_metrics_enabled = false
metric_name = "${var.application_name}-waf-acl"
sampled_requests_enabled = true
}
}
resource "aws_wafv2_web_acl_association" "_" {
resource_arn = var.alb_arn
web_acl_arn = aws_wafv2_web_acl._.arn
}
まとめ
AWSへの移行はアプリケーション開発者でインフラリソースの管理をよしなに行い、インフラチームとの調整は最初の導通確認のみで以降はほぼ調整不要でした。今後も調整コストが減り、プロジェクトのコントロールがとても楽になる実感がありました。
「段階的な移行」と言うと関係者が増えてリリース計画が複雑化するため構えてしまう印象がありますが、これであればかなりハードルが下がりますね。AWS強い。