この記事は全七回の連載予定です!
- 第一回: インポート戦略編<このページ>
- 第二回: リソースのインポート-ネットワーク、IAM編
- 第三回: Terraform環境別設定管理術
- 第四回: リソースのインポート-ECS、タスク定義管理編※執筆中
- 第五回: リソースのインポート-RDS、S3、CloudFront編※執筆中
- 第六回: リソースのインポート-Route53、SES、SNS編※執筆中
- 第七回: 総括※執筆中
背景
当社が保守運用を行なっているとあるSaaSにて、運用中のAWS本番環境があります。納期とノウハウの都合から、GUIで環境を構築していました。
今後の拡大を視野に入れると、このままではインフラ管理コストの増大および設定ミスによる本番ワークロードへの障害懸念がありました。
運用期間が長くなればなるほどIaC化が難しくなると考え、思い切ってTerraformへの移行を決めました。
なお主な構成はこんな感じです
- バックエンド: Laravel (PHP)
- フロントエンド: React + TypeScript
- データベース: PostgreSQL
- ファイルストレージ: S3
- メールサーバー: SES
- WEB,APPサーバー: ECS
- DBサーバー: RDS
対象読者
- 既に稼働中のAWS環境からTerraform管理を進めたい方
- terraform importを使った移行を計画している方
この記事からわかること
- 既存AWS環境をTerraformにインポートする6フェーズ戦略
- 本番環境特有の命名規則問題への対処法
- 安全にインポート作業を進めるためのスクリプト設計
- 環境差分を吸収するTerraformコードの書き方
この記事では説明しないこと
- Terraformの基本的な使い方
- 個別のインポートコマンドの詳細(次回以降で解説)
おことわり
- プロジェクト名はダミーで「Wish」としています。本当の名称は明かせないので...ご了承ください
- ARN等も適当な値に置き換えています
- セキュリティ上、インフラ構成は実物から変えている箇所があります
方針検討
大きく分けて一からインフラ設定コードを書くのか、既存の環境から読み込むかと、CloudFormationなのかTerraformなのかを検討しました。
-
新環境を構築して切り替え
- メリット: 既存環境への誤操作リスクがなく安全性が高い
- デメリット: 旧環境と新環境の差分があったときに検出が結局目視、一時的にコスト倍増。DNS切り替えやデータ移行といった作業コストもある
-
CloudFormationで既存リソースを取り込む
- メリット: CloudFormationに自動スキャン機能あり、無停止移行、既存環境をそのまま活用
- デメリット: ハードコーディングされるため開発環境やステージングに使えるようにするのが結局大変。CloudFormationがモジュール化に強くなく、部分的にIaC管理するのがしんどそう
-
terraform importで既存リソースを取り込む
- メリット: 無停止移行、既存環境をそのまま活用
- デメリット: 作業量が多い、エラー対応が必要、使用している全ての設定に対応しているか不明
1は信頼性の面から却下。2と3は迷いましたが、最終的にモジュール化に強いTerraformを使用することにしました。
当初から想定された課題:
AWSコンソール(GUI)で手動作成された本番環境は数ヶ月前から運用されており、以下の特徴がありました。
-
統一されていないリソースの命名規則
- 理想:
${project_name}-${environment}-${リソース名と一目で役割がわかる名前}
- 現実:
-
wish-env
(環境変数格納用S3バケット。本番?ステージング?dev?) -
RDS-ECS``ECS-RDS
(セキュリティグループ。どっちがどっち?)
-
- 理想:
-
大文字小文字の混同
- 理想: 正式なプロジェクト名
Wish
で統一 - 現実: リソース名にWishとwishが混在
- 理想: 正式なプロジェクト名
-
ベストプラクティスに則っていない権限設計
- 理想: ECSタスク実行ロール
- 現実: アプリケーションコンテナからS3やSESへのアクセスに、IAMユーザーのアクセスキー使用
これらの違いにより、単純にTerraformで定義すると既存リソースが再作成されてしまう(つまり、本番環境の破壊の)リスクがありました。
6フェーズに分けてimportすることに
まず、CloudFormationの自動スキャン機能を使い、全リソースを対象にCloudFormationテンプレートを生成しました。
これは後々Tarreformでimportする際に、対象のリソースに漏れがないか、importと実際の設定に誤りがないか確認するために使用しました。実際の管理には使用しません。
続けてインフラ構成を見ながらリソースの依存関係を分析し、以下の6つに分割しました。
段階的にimportして、それぞれ実際に本番と別のAWSアカウントに対して実行していく戦略を採ります。
なお実際にはこの6つでは収まらず、後から追加したりしたのですが、計画段階では以下の通りでした。
Phase 1: VPCとネットワーク基盤
# VPC
terraform import aws_vpc.main vpc-0123456789abcdef0
# Internet Gateway
terraform import aws_internet_gateway.main igw-0123456789abcdef1
# Subnets
terraform import 'aws_subnet.public["ap-northeast-1a"]' subnet-0123456789abcdef2
terraform import 'aws_subnet.public["ap-northeast-1c"]' subnet-0123456789abcdef3
...
Phase 2: セキュリティグループとIAM
# Security Groups
terraform import aws_security_group.alb sg-0123456789abcdef4
terraform import aws_security_group.ecs sg-0123456789abcdef5
# IAM Roles
terraform import aws_iam_role.ecs_task_execution_role arn:aws:iam::123456789012:role/wish_ecs_task_execution_role
...
Phase 3: RDS、S3、VPCエンドポイント
# RDS Cluster
terraform import aws_rds_cluster.main wish-prod-dbcluster
terraform import aws_rds_cluster_instance.main wish-prod-dbcluster-instance-1
# S3 Buckets
terraform import aws_s3_bucket.public wish-prod-public
...
Phase 4: ECS、ALB、CloudWatchログ
# ECS Cluster
terraform import aws_ecs_cluster.main wish
# CloudWatch Log Groups
terraform import 'aws_cloudwatch_log_group.ecs_services["web"]' /ecs/Wish/web
...
Phase 5: CloudFront
terraform import aws_cloudfront_distribution.main E1ABCDEFGHIJKLM
...
Phase 6: SES、SNS
terraform import aws_ses_configuration_set.main wish-prod
terraform import aws_sns_topic.alarm arn:aws:sns:ap-northeast-1:123456789012:wish-alarm
...
環境差分を吸収するコード設計
「課題」に記載した通り、本番環境の命名規則にはいけてない点があります。せっかくなのでこれから作る環境は理想の命名規則にします。
ここで問題となるのは、Tarraformではリソース名が異なるとリソースが再作成されることです。何も考えず本番にtarraform applyをするとDBが消えます(汗)
そこで本番は既存の名前を維持するよう、import後に三項演算子で条件分岐させます。
# variables.tf
variable "environment" {
description = "Environment name"
type = string
}
# main.tf
locals {
is_production = var.environment == "production"
# 本番環境は既存のいけてない名前「wish-env」を維持
s3_env_bucket_name = local.is_production ? "wish-env" : "${var.project_name}-${var.environment}-env-files"
}
resource "aws_s3_bucket" "public" {
bucket = local.s3_public_bucket_name
# ...
}
安全対策:terraform-safe.sh
複数のAWS Profileを切り替えながらterraformコマンドを実行していると、新しい環境にapplyしてたつもりが本番だった!という事態が生じかねません。
適用先のAWSアカウントが間違っていたら適用前に想定外の差分がでて気づくのですが、万一があってからでは遅いため、簡易的なシェルスクリプトを組んで予防します。
terraformコマンドは直接実行せず、terraform-safe.sh経由で実行しました。
#!/bin/bash
# terraform-safe.sh
# プロファイル名とワークスペース名の一致を確認
AWS_PROFILE_NAME=$(aws configure get profile)
WORKSPACE=$(terraform workspace show)
if [[ "$AWS_PROFILE_NAME" != "$WORKSPACE" ]]; then
echo "Error: Profile name does not match workspace name"
exit 1
fi
# 本番環境の場合は警告表示
if [[ "$WORKSPACE" == "production" ]]; then
echo "WARNING: You are operating on PRODUCTION environment!"
read -p "Are you sure? (yes/no): " confirm
if [[ "$confirm" != "yes" ]]; then
exit 1
fi
fi
# terraformコマンドを実行
terraform "$@"
まとめ
移行戦略を決定し、本番環境に対する事故防止策も決まりました。次はPhase 1: VPCとネットワーク基盤から実際にimportしていきます。
第二回: リソースのインポート-ネットワーク、IAM編へつづく