AWSプロバイダー@Terraform¶
はじめに¶
本サイトにつきまして、以下をご認識のほど宜しくお願いいたします。
AWSプロバイダーとは¶
TerraformがAWSリソースのAPIと通信可能にする。
これにより、Terraformを使用してAWSリソースを作成できるようになる。
ACM¶
SSL証明書のリクエスト¶
*実装例*
Eメール検証の場合を示す。
# ---------------------------------------------
# For www domain
# ---------------------------------------------
resource "aws_acm_certificate" "www_an1" {
domain_name = var.route53_domain_www
subject_alternative_names = [
"*.${var.route53_domain_www}"
]
validation_method = "EMAIL"
tags = {
Name = "prd-foo-www-an1-cert"
}
lifecycle {
create_before_destroy = true
}
}
*実装例*
DNS検証の場合を示す。
# ---------------------------------------------
# For www domain
# ---------------------------------------------
resource "aws_acm_certificate" "www_an1" {
domain_name = var.route53_domain_www
subject_alternative_names = [
"*.${var.route53_domain_www}"
]
validation_method = "DNS"
tags = {
Name = "prd-foo-www-an1-cert"
}
lifecycle {
create_before_destroy = true
}
}
検証の実行¶
*実装例*
Eメール検証の場合を示す。
# ---------------------------------------------
# For www domain
# ---------------------------------------------
# 後述の説明を参考にせよ。(1)
resource "aws_acm_certificate_validation" "www_an1" {
certificate_arn = aws_acm_certificate.www_an1.arn
}
*実装例*
DNS検証の場合を示す。
# ---------------------------------------------
# For www domain
# ---------------------------------------------
# 後述の説明を参考にせよ。`(2)`
resource "aws_acm_certificate_validation" "www_an1" {
certificate_arn = aws_acm_certificate.www_an1.arn
validation_record_fqdns = [for record in var.www_an1_route53_record : record.fqdn]
}
(1)
AWS以外でドメインを購入した場合は注意¶
AWS以外でドメインを購入した場合はAWS以外で作業になる。
SSL証明書のDNS検証時に、ドメインを購入したサービスが管理するドメインレジストラに、Route53のNSレコード値を登録する。
(2)
検証のためにメール再送が必要¶
SSL証明書のEメール検証時に、ドメインの所有者にメールが送信されないことがある。
送信されなかった場合は、メールの再送を実行する。
(*) SSL証明書の検証方法を変更する¶
もしコンソール画面からSSL証明書の検証方法を変更する場合、検証方法の異なるSSL証明書を作成してこれに切り替えたうえで、古いSSL証明書を削除する必要がある。
これに合わせて、Terraformでもリリースを二回に分ける。
AMI¶
まとめ¶
*実装例*
AMIを検索した上で、AWSから特定のAMIを取得する。
# ---------------------------------------------
# For bastion
# ---------------------------------------------
data "aws_ami" "bastion" {
# 後述の説明を参考にせよ。(1)
most_recent = false
# 後述の説明を参考にせよ。(2)
owners = ["amazon"]
filter {
name = "name"
values = ["amzn-ami-hvm-2018.03.0.20201028.0-x86_64-gp2"]
}
filter {
name = "image-id"
values = ["ami-040c9333a9c90b2b6"]
}
}
# 後述の説明を参考にせよ。(3)
data "aws_ami" "backuped" {
most_recent = true
owners = ["self"]
filter {
name = "name"
values = ["AwsBackup_*"]
}
filter {
name = "tag:Env"
values = ["prd"]
}
}
(1)
取得するAMIのバージョンを固定¶
取得するAMIが常に最新になっていると、EC2が再作成されなねない。
そこで、特定のAMIを取得できるようにしておく。
most_recent
は無効化しておき、特定のAMIをフィルタリングする。
(2)
AWS Backupで作成したAMIを参照¶
AWS BackupでEC2のAMIを作成している場合に、フィルターの条件を使用して、AMIを参照する。
API Gateway¶
まとめ¶
*実装例*
# ---------------------------------------------
# RESTful API
# ---------------------------------------------
resource "aws_api_gateway_rest_api" "foo" {
name = "prd-foo-api-for-foo"
description = "The API that enables two-way communication with prd-foo"
# VPCリンクのプロキシ統合のAPIを定義したOpenAPI仕様
# 後述の説明を参考にせよ。(1)
body = templatefile(
"${path.module}/open_api.yml",
{
api_gateway_vpc_link_foo_id = aws_api_gateway_vpc_link.foo.id
nlb_dns_name = var.nlb_dns_name
}
)
endpoint_configuration {
types = ["REGIONAL"]
}
lifecycle {
ignore_changes = [
policy
]
}
}
# ---------------------------------------------
# Deployment
# ---------------------------------------------
resource "aws_api_gateway_deployment" "foo" {
rest_api_id = aws_api_gateway_rest_api.foo.id
# 後述の説明を参考にせよ。(1)
triggers = {
redeployment = sha1(aws_api_gateway_rest_api.foo.body)
}
lifecycle {
create_before_destroy = true
}
}
# ---------------------------------------------
# Stage
# ---------------------------------------------
resource "aws_api_gateway_stage" "foo" {
deployment_id = aws_api_gateway_deployment.foo.id
rest_api_id = aws_api_gateway_rest_api.foo.id
stage_name = var.environment
}
(1)
OpenAPI仕様のインポートと差分認識¶
あらかじめ用意したOpenAPI仕様のyaml
ファイルをbody
オプションのパラメーターとし、これをインポートすることにより、APIを定義できる。
yaml
ファイルに変数を渡すこともできる。
APIの再デプロイのトリガーとして、redeployment
パラメーターにbody
パラメーターのハッシュ値を渡すようにする。
これにより、インポート元のyaml
ファイルに差分があった場合、Terraformがredeployment
パラメーターの値の変化を識別できるようになり、再デプロイを実行できる。
(*) ステージ名を取得する方法はない¶
API Gatewayのステージ名を参照するためには、resourceを使用する必要があり、dataではこれを取得できない。
もしステージをコンソール画面上から作成している場合、ステージのARNを参照できないため、ARNを自力で作る必要がある。
API Gatewayの各ARNについては、以下のリンクを参考にせよ。
*実装例*
WAFにAPI Gatewayを紐付けるために、ステージのARNが必要である。
これは自力で作る。
# ---------------------------------------------
# Web ACLアソシエーション
# ---------------------------------------------
resource "aws_wafv2_web_acl_association" "api_gateway" {
resource_arn = "${var.api_gateway_rest_arn}/stages/prd"
web_acl_arn = aws_wafv2_web_acl.api_gateway.arn
}
CloudWatchログ¶
まとめ¶
resource "aws_cloudwatch_log_group" "ecs_service_container_datadog" {
name = "/prd-foo-ecs-service/container/datadog/log"
}
(1)
ECSサービス名をルートとした命名¶
同じAWSアカウントの異なるECSサービスを作成する場合がある。
この場合、コンテナ名が重複することになるため、CloudWatchログのロググループはECSサービスをルートとして命名する必要がある。
CloudFront¶
まとめ¶
*実装例*
resource "aws_cloudfront_distribution" "this" {
price_class = "PriceClass_200"
web_acl_id = var.cloudfront_wafv2_web_acl_arn
aliases = [var.route53_domain_foo]
comment = "prd-foo-cf-distribution"
enabled = true
# 後述の説明を参考にせよ。(1)
retain_on_delete = true
viewer_certificate {
acm_certificate_arn = var.foo_acm_certificate_arn
ssl_support_method = "sni-only"
minimum_protocol_version = "TLSv1.2_2019"
}
logging_config {
bucket = var.cloudfront_s3_bucket_regional_domain_name
include_cookies = true
}
restrictions {
geo_restriction {
restriction_type = "none"
}
}
...
}
(1)
削除保持機能¶
Terraformでは、retain_on_delete
で設定できる。
固有の設定で、AWSに対応するものは無い。
originブロック¶
Origins画面に設定するオリジンを定義する。
*実装例*
resource "aws_cloudfront_distribution" "this" {
...
# オリジン (ここではS3としている)
origin {
domain_name = var.s3_bucket_regional_domain_name
origin_id = "S3-${var.s3_bucket_id}"
s3_origin_config {
origin_access_identity = aws_cloudfront_origin_access_identity.s3_foo.cloudfront_access_identity_path
}
}
...
}
resource "aws_cloudfront_distribution" "this" {
...
# オリジン (ここではALBとしている)
origin {
domain_name = var.alb_dns_name
origin_id = "ELB-${var.alb_name}"
custom_origin_config {
origin_ssl_protocols = ["TLSv1.2"]
origin_protocol_policy = "match-viewer"
origin_read_timeout = 30
origin_keepalive_timeout = 5
http_port = var.alb_listener_port_http
https_port = var.alb_listener_port_https
}
}
...
}
ordered_cache_behaviorブロック¶
Behavior画面に設定するオリジンにルーティングするパスを定義する。
*実装例*
resource "aws_cloudfront_distribution" "this" {
...
ordered_cache_behavior {
path_pattern = "/images/*"
target_origin_id = "S3-${var.s3_bucket_id}"
viewer_protocol_policy = "redirect-to-https"
allowed_methods = ["GET", "HEAD", "OPTIONS", "PUT", "POST", "PATCH", "DELETE"]
cached_methods = ["GET", "HEAD"]
min_ttl = 0
max_ttl = 31536000
default_ttl = 86400
compress = true
forwarded_values {
query_string = true # クエリストリングのキャッシュ
cookies {
forward = "none" # Cookieのキャッシュ
}
}
}
...
}
default_cache_behavior¶
Behavior画面に設定するオリジンにルーティングする標準パスを定義する。
*実装例*
resource "aws_cloudfront_distribution" "this" {
default_cache_behavior {
target_origin_id = "ELB-${var.alb_name}"
viewer_protocol_policy = "redirect-to-https"
allowed_methods = ["GET", "HEAD", "OPTIONS", "PUT", "POST", "PATCH", "DELETE"]
cached_methods = ["GET", "HEAD"]
min_ttl = 0
max_ttl = 31536000
default_ttl = 86400
compress = true
forwarded_values {
query_string = true # クエリストリングのキャッシュ
headers = ["*"] # ヘッダーのキャッシュ
cookies {
forward = "all" # Cookieのキャッシュ
}
}
}
...
}
ECR¶
ライフサイクルポリシー¶
ECRに紐付けられる、コンテナイメージの有効期間を定義するポリシー。
コンソール画面から入力できるため、基本的にポリシーの実装は不要であるが、TerraformなどのIaCツールでは必要になる。
{
"rules":
[
{
"rulePriority": 1,
"description": "Keep last 10 images untagged",
"selection":
{
"tagStatus": "untagged",
"countType": "imageCountMoreThan",
"countNumber": 10,
},
"action": {"type": "expire"},
},
{
"rulePriority": 2,
"description": "Keep last 10 images any",
"selection":
{
"tagStatus": "any",
"countType": "imageCountMoreThan",
"countNumber": 10,
},
"action": {"type": "expire"},
},
],
}
ECS¶
まとめ¶
*実装例*
# ---------------------------------------------
# ECS Service
# ---------------------------------------------
resource "aws_ecs_service" "this" {
name = "prd-foo-ecs-service"
cluster = aws_ecs_cluster.this.id
launch_type = "FARGATE"
platform_version = "1.4.0"
desired_count = var.ecs_service_desired_count
deployment_maximum_percent = 200
deployment_minimum_healthy_percent = 100
# 後述の説明を参考にせよ。(1)
health_check_grace_period_seconds = 330
# 後述の説明を参考にせよ。(2)
task_definition = "${aws_ecs_task_definition.this.family}:${max(aws_ecs_task_definition.this.revision, data.aws_ecs_task_definition.this.revision)}"
network_configuration {
security_groups = [var.ecs_security_group_id]
subnets = [var.private_a_app_subnet_id, var.private_c_app_subnet_id]
assign_public_ip = false
}
load_balancer {
target_group_arn = var.alb_target_group_arn
container_name = "nginx"
container_port = 80
}
load_balancer {
target_group_arn = var.nlb_target_group_arn
container_name = "nginx"
container_port = 80
}
depends_on = [
# 後述の説明を参考にせよ。(3)
var.alb_listener_https,
var.nlb_listener
]
lifecycle {
ignore_changes = [
# ※後述の説明を参考にせよ(4)
desired_count,
]
}
}
(1)
ヘルスチェック待機時間¶
ECSタスクの起動が完了する前にサービスがロードバランサ-のヘルスチェックを検証し、Unhealthyと誤認してしまうため、ECSタスクの起動完了を待機する。
例えば、ロードバランサ-が30秒間隔でヘルスチェックを実行する場合は、30秒単位で待機時間を増やし、適切な待機時間を見つけるようにする。
(2)
実インフラのリビジョン番号の追跡¶
アプリケーションのデプロイによって、実インフラのECSタスク定義のリビジョン番号が増加するため、これを追跡可能にする。
(3)
ALB/NLBリスナーの作成を待機¶
Terraformは、特に依存関係を実装しない場合、『ターゲットグループ ➡︎ ALB/NLB ➡︎ リスナー』の順でresource
ブロックを作成する。
問題として、ALB/NLBやリスナーの作成が終了する前に、ECSサービスの作成が始まってしまう。
ALB/NLBの作成 (※リスナーも含む可能性) が完全に完了しない状態では、ターゲットグループはECSサービスに紐付けらず、これが完了する前にECSサービスがターゲットグループを参照しようとするため、エラーになってしまう。
リスナーの後にECSサービスを作成するようにし、『ターゲットグループ ➡︎ ALB/NLB ➡︎ リスナー ➡︎ ECSサービス』の順でresource
ブロックを作成可能にする。
(4)
AutoScalingによるECSタスク数の増減を無視¶
AutoScalingによって、ECSタスク数が増減するため、これを無視する。
(*) ECSタスク定義の更新¶
TerraformでECSタスク定義を更新すると、現在稼働しているECSで稼働しているECSタスクはそのままに、新しいリビジョン番号のECSタスク定義を作成する。
コンソール画面の『新しいリビジョンの作成』と同じ挙動である。
実際にECSタスクが増えていることは、サービスに紐付くECSタスク定義一覧から確認できる。
次のデプロイ時に、このECSタスクが使用される。
(*) サービスのデプロイの削除時間¶
ECSサービスの削除には『ドレイニング』の時間が発生する。
約2
分30
秒かかるため、気長に待つこと。
(*) ローリングアップデート¶
terraform apply
コマンドで、新しいリビジョン番号のECSタスク定義を作成すると、これを使用してローリングアップデートが自動的に実行されることに注意する。
ただし、ローリングアップデートの仕組み上、新しいECSタスクのヘルスチェックが失敗すれば、既存のECSタスクは停止せずにそのまま稼働するため、安心ではあるが。
(*) ECSコンテナ名¶
コンテナ名は、役割名 (app、web、monitoringなど) ではなく、ベンダー名 (laravel、nginx、datadogなど) とする。
ただし、AWS FireLensコンテナはlog_routerとしなければならない仕様であり、ベンダー名を使用できない場合は役割名になることを許容する。
EC2¶
まとめ¶
*実装例*
# ---------------------------------------------
# For bastion
# ---------------------------------------------
resource "aws_instance" "bastion" {
ami = "*****"
instance_type = "t2.micro"
vpc_security_group_ids = ["*****"]
subnet_id = "*****"
associate_public_ip_address = true
# ※後述の説明を参考にせよ(1)
key_name = "prd-foo-bastion"
disable_api_termination = true
tags = {
Name = "prd-foo-bastion"
}
# ※後述の説明を参考にせよ(2)
depends_on = [var.internet_gateway]
}
(1)
キーペアはコンソール上で設定¶
誤って削除しないように、またコードに秘密鍵の内容をハードコーディングしないように、キーペアはコンソール画面で作成した後、key_name
でキー名を指定する。
(2)
Internet Gatewayの後に作成¶
Internet Gatewayの後にEC2を作成可能にする。
EKS¶
まとめ¶
EKSのNodeグループをマネージドとする場合、起動テンプレートも合わせて設定する必要がある。
resource "aws_eks_node_group" "this" {
cluster_name = "foo-eks-cluster"
node_role_arn = ""
subnet_ids = ""
scaling_config {
min_size = 3
max_size = 5
desired_size = 4
}
node_group_name = ""
ami_type = ""
release_version = ""
version = ""
capacity_type = ""
disk_size = ""
force_update_version = ""
instance_types = ""
labels = ""
# 起動テンプレートを紐づける
launch_template = {
id = ""
version = ""
}
remote_access = {
ec2_ssh_key = ""
source_security_group_ids = ""
}
taint = {
key = ""
value = ""
effect = ""
}
update_config {
max_unavailable_percentage = ""
max_unavailable = ""
}
timeouts {
create = ""
update = ""
delete = ""
}
lifecycle {
create_before_destroy = true
ignore_changes = [
# ※後述の説明を参考にせよ(1)
scaling_config[0].desired_size,
]
}
tags = {
Name = ""
}
}
(1)
Node数の増減は無視¶
EKSでは、cluster-autoscalerを使用して、Nodeをスケーリングさせる。
この時のNode数の増減を無視できるようにしておく。
IAMユーザー¶
カスタマー管理ポリシーを持つロール¶
事前に、tpl形式のカスタマー管理ポリシーを定義しておく。
作成済みのIAMロールに、aws_iam_policy
リソースを使用して、AWS管理ポリシーをIAMユーザーに紐付ける。
*実装例*
ローカルマシンからAWS CLIコマンドを実行する必要がある場合、コマンドを特定の送信元IPアドレスを特定のものに限定する。
事前に、list型でIPアドレスを定義する。
# ---------------------------------------------
# IP addresses
# ---------------------------------------------
global_ip_addresses = [
"*.*.*.*/32",
"*.*.*.*/32"
]
また事前に、指定した送信元IPアドレス以外を拒否するカスタマー管理ポリシーを定義する。
{
"Version": "2012-10-17",
"Statement": {
"Effect": "Deny",
"Action": "*",
"Resource": "*",
"Condition": {
"NotIpAddress": {
"aws:SourceIp": ${global_ip_addresses}
}
}
}
}
コンソール画面で作成済みのIAMユーザーの名前を取得する。
tpl形式のポリシーにlist型の値を渡す時、JSONを文字列に変換するjsonencode
関数を使用する必要がある。
# ---------------------------------------------
# For IAM User
# ---------------------------------------------
data "aws_iam_user" "aws_cli_command_executor" {
user_name = "aws_cli_command_executor"
}
resource "aws_iam_policy" "aws_cli_command_executor_ip_address_restriction" {
name = "prd-aws-cli-command-executor-ip-address-restriction-policy"
description = "Allow global IP addresses"
policy = templatefile(
"${path.module}/policies/customer_managed_policies/aws_cli_command_executor_ip_address_restriction_policy.tpl",
{
global_ip_addresses = jsonencode(var.global_ip_addresses)
}
)
}
AWS管理ポリシー¶
IAMユーザーにAWS管理ポリシーを紐付ける。
*実装例*
# ---------------------------------------------
# For IAM User
# ---------------------------------------------
resource "aws_iam_user_policy_attachment" "aws_cli_command_executor_s3_read_only_access" {
user = data.aws_iam_user.aws_cli_command_executor.user_name
policy_arn = "arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess"
}
IAMロール¶
信頼ポリシーを持つロール¶
コンソール画面でロールを作成する場合は意識することはないが、特定のresource
ブロックにロールを紐付けるためには、ロールに信頼ポリシーを組み込む必要がある。
事前に、tpl形式の信頼ポリシーを定義しておく。
aws_iam_role
リソースを使用して、IAMロールを作成すると同時に、これに信頼ポリシーを紐付ける。
*実装例*
事前に、ECSタスクのための信頼ポリシーを定義する。
{
"Version": "2012-10-17",
"Statement":
[
{
"Effect": "Allow",
"Principal": {"Service": "ecs-tasks.amazonaws.com"},
"Action": "sts:AssumeRole",
},
],
}
ECSタスクロールとECSタスク実行ロールに信頼ポリシー紐付ける。
# ---------------------------------------------
# IAM Role For ECS Task Execution
# ---------------------------------------------
resource "aws_iam_role" "ecs_task_execution" {
name = "prd-foo-ecs-task-execution-role"
description = "The role for prd-foo-ecs-task"
assume_role_policy = templatefile(
"${path.module}/policies/trust_policies/ecs_task_policy.tpl",
{}
)
}
# ---------------------------------------------
# IAM Role For ECS Task
# ---------------------------------------------
resource "aws_iam_role" "ecs_task" {
name = "prd-foo-ecs-task-role"
description = "The role for prd-foo-ecs-task"
assume_role_policy = templatefile(
"${path.module}/policies/trust_policies/ecs_task_policy.tpl",
{}
)
}
*実装例*
事前に、Lambda@Edgeのための信頼ポリシーを定義する。
{
"Version": "2012-10-17",
"Statement":
[
{
"Sid": "",
"Effect": "Allow",
"Principal":
{"Service": ["lambda.amazonaws.com", "edgelambda.amazonaws.com"]},
"Action": "sts:AssumeRole",
},
],
}
Lambda実行ロールに信頼ポリシー紐付ける。
# ---------------------------------------------
# IAM Role For Lambda@Edge
# ---------------------------------------------
# ロールに信頼ポリシーを紐付けします。
resource "aws_iam_role" "lambda_execute" {
name = "prd-foo-lambda-execute-role"
assume_role_policy = templatefile(
"${path.module}/policies/lambda_execute_role_trust_policy.tpl",
{}
)
}
インラインポリシーを持つロール¶
事前に、tpl形式のインラインポリシーを定義しておく。
aws_iam_role_policy
リソースを使用して、インラインポリシーを作成すると同時に、これにインラインポリシーを紐付ける。
*実装例*
事前に、ECSタスクに必要最低限の権限を与えるインラインポリシーを定義する。
{
"Version": "2012-10-17",
"Statement":
[{"Effect": "Allow", "Action": ["ssm:GetParameters"], "Resource": "*"}],
}
ECSタスクロールとECSタスク実行ロールにインラインポリシー紐付ける。
# ---------------------------------------------
# IAM Role For ECS Task
# ---------------------------------------------
resource "aws_iam_role_policy" "ecs_task" {
name = "prd-foo-ssm-read-only-access-policy"
role = aws_iam_role.ecs_task_execution.id
policy = templatefile(
"${path.module}/policies/inline_policies/ecs_task_policy.tpl",
{}
)
}
AWS管理ポリシーを持つロール¶
事前に、tpl形式のAWS管理ポリシーを定義しておく。
aws_iam_role_policy_attachment
リソースを使用して、実インフラにあるAWS管理ポリシーを作成済みのIAMロールに紐付ける。
ポリシーのARNは、AWSのコンソール画面を確認する。
*実装例*
# ---------------------------------------------
# IAM Role For ECS Task Execution
# ---------------------------------------------
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"
}
カスタマー管理ポリシーを持つロール¶
事前に、tpl形式のインラインポリシーを定義しておく。
aws_iam_role_policy
リソースを使用して、カスタマー管理ポリシーを作成する。
aws_iam_role_policy_attachment
リソースを使用して、カスタマー管理ポリシーを作成済みのIAMロールに紐付ける。
*実装例*
事前に、ECSタスクに必要最低限の権限を与えるカスタマー管理ポリシーを定義する。
{
"Version": "2012-10-17",
"Statement":
[
{
"Effect": "Allow",
"Action": ["logs:CreateLogStream", "logs:PutLogEvents"],
"Resource": ["arn:aws:logs:*:*:*"],
},
],
}
ECSタスクロールにカスタマー管理ポリシー紐付ける。
# ---------------------------------------------
# IAM Role For ECS Task
# ---------------------------------------------
resource "aws_iam_policy" "ecs_task" {
name = "prd-foo-cloudwatch-logs-access-policy"
description = "Provides access to CloudWatch Logs"
policy = templatefile(
"${path.module}/policies/customer_managed_policies/cloudwatch_logs_access_policy.tpl",
{}
)
}
resource "aws_iam_role_policy_attachment" "ecs_task" {
role = aws_iam_role.ecs_task.name
policy_arn = aws_iam_policy.ecs_task.arn
}
サービスリンクロール¶
サービスリンクロールは、AWSリソースの作成時に自動的に作成され、紐付けられる。
そのため、Terraformの管理外である。
aws_iam_service_linked_role
リソースを使用して、手動で作成できるが、数が多く実装の負担にもなるため、あえて管理外としても問題ない。
*実装例*
サービス名を指定して、ApplicationAutoScalingのサービスリンクロールを作成する。
# ---------------------------------------------
# IAM Role For ECS Service
# ---------------------------------------------
# Service Linked Role
resource "aws_iam_service_linked_role" "ecs_service_auto_scaling" {
aws_service_name = "ecs.application-autoscaling.amazonaws.com"
}
# ---------------------------------------------
# Output IAM Role
# ---------------------------------------------
output "ecs_service_auto_scaling_iam_service_linked_role_arn" {
value = aws_iam_service_linked_role.ecs_service_auto_scaling.arn
}
ApplicationAutoScalingにサービスリンクロールを紐付ける。
手動でも設定できるが、Terraformの管理外で自動的に紐付けられるため、あえて妥協しても良い。
# ---------------------------------------------
# Application Auto Scaling For ECS
# ---------------------------------------------
resource "aws_appautoscaling_target" "ecs" {
service_namespace = "ecs"
resource_id = "service/prd-foo-ecs-cluster/prd-foo-ecs-service"
scalable_dimension = "ecs:service:DesiredCount"
max_capacity = 4
min_capacity = 2
# この設定がなくとも、サービスリンクロールが自動的に作成され、AutoScalingに紐付けられる。
role_arn = var.ecs_service_auto_scaling_iam_service_linked_role_arn
}
リスナーとターゲットグループ¶
まとめ¶
*実装例*
# ---------------------------------------------
# NLB target group
# ---------------------------------------------
resource "aws_lb_target_group" "this" {
name = "prd-foo-nlb-tg"
port = 80
protocol = "TCP"
vpc_id = "vpc-*****"
deregistration_delay = "60"
target_type = "ip"
# ※後述の説明を参考にせよ(1)
slow_start = "0"
# ※後述の説明を参考にせよ(2)
health_check {
protocol = "HTTP"
healthy_threshold = 3
path = "/healthcheck"
}
# stickiness ※後述の説明を参考にせよ(3)
# https://registry.terraform.io/providers/hashicorp/aws/3.16.0/docs/resources/lb_target_group#stickiness
lifecycle {
create_before_destroy = false
}
}
(1)
NLBはスロースタートに非対応¶
NLBに紐付くターゲットグループはスロースタートに非対応のため、これを明示的に無効化する必要がある。
(2)
NLBヘルスチェックには設定できる項目が少ない¶
ターゲットグループの転送プロトコルがTCPの場合は、設定できないヘルスチェックオプションがいくつかある。
ヘルスチェックプロトコルがHTTPまたはHTTPSの時のみ、パスを設定できる。
(3)
NLBスティッキーネスは明示的に無効化¶
スティッキネス機能を無効化する場合、AWSプロバイダーのアップグレード時に問題が起こらないように、このブロックを実装しないようにする。
リンクのNOTE文を参考にせよ。
(*) ターゲットグループの削除時にリスナーを先に削除できない。¶
リスナーがターゲットグループに依存しているが、Terraformがターゲットグループの削除時にリスナーを先に削除しようとしない。
そのため、以下のようなエラーが発生する。
Error deleting Target Group: ResourceInUse: Target group 'arn:aws:elasticloadbalancing:ap-northeast-1:<AWSアカウントID>:targetgroup/*****-tg/*****' is currently in use by a listener or a rule
status code: 400, request id: *****
このエラーが発生した場合、コンソール画面上でリスナーを削除したうえで、もう一度terraform apply
コマンドを実行する。
RDS (Aurora) の場合¶
まとめ¶
*実装例*
# ---------------------------------------------
# RDS Cluster
# ---------------------------------------------
resource "aws_rds_cluster" "this" {
engine = "aurora-mysql"
engine_version = "5.7.mysql_aurora.2.08.3"
cluster_identifier = "prd-foo-rds-cluster"
# 後述の説明を参考にせよ。(1)
master_username = var.rds_db_master_username_ssm_parameter_value
master_password = var.rds_db_master_password_ssm_parameter_value
port = var.rds_db_port_ssm_parameter_value
database_name = var.rds_db_name_ssm_parameter_value
vpc_security_group_ids = [var.rds_security_group_id]
db_subnet_group_name = aws_db_subnet_group.this.name
db_cluster_parameter_group_name = aws_rds_cluster_parameter_group.this.id
storage_encrypted = true
backup_retention_period = 7
preferred_backup_window = "00:00-00:30"
copy_tags_to_snapshot = true
final_snapshot_identifier = "final-db-snapshot"
skip_final_snapshot = false
enabled_cloudwatch_logs_exports = ["audit", "error", "general", "slowquery"]
preferred_maintenance_window = "sun:01:00-sun:01:30"
# 後述の説明を参考にせよ。(2)
apply_immediately = true
# 後述の説明を参考にせよ。(3)
availability_zones = ["${var.region}${var.vpc_availability_zones.a}", "${var.region}${var.vpc_availability_zones.c}"]
deletion_protection = true
lifecycle {
ignore_changes = [
# 後述の説明を参考にせよ。(4)
availability_zones,
# 後述の説明を参考にせよ。(5)
engine_version
]
}
}
# ---------------------------------------------
# RDS Cluster Instance
# ---------------------------------------------
resource "aws_rds_cluster_instance" "this" {
# 後述の説明を参考にせよ。(6)
for_each = var.vpc_availability_zones
engine = "aurora-mysql"
engine_version = "5.7.mysql_aurora.2.08.3"
identifier = "prd-foo-rds-instance-${each.key}"
cluster_identifier = aws_rds_cluster.this.id
db_subnet_group_name = aws_db_subnet_group.this.id
db_parameter_group_name = aws_db_parameter_group.this.id
monitoring_interval = 60
monitoring_role_arn = var.rds_iam_role_arn
auto_minor_version_upgrade = var.rds_auto_minor_version_upgrade
preferred_maintenance_window = "sun:01:00-sun:01:30"
apply_immediately = true
# 後述の説明を参考にせよ。(7)
instance_class = var.rds_instance_class[each.key]
# 後述の説明を参考にせよ。(8)
# preferred_backup_window
}
# 後述の説明を参考にせよ。(9)
locals {
rds_cluster_vpc_availability_zones_a = aws_rds_cluster_instance.this[var.vpc_availability_zones.a]
}
resource "aws_rds_cluster_instance" "read_replica" {
count = 1
engine = local.rds_cluster_vpc_availability_zones_a.engine
engine_version = local.rds_cluster_vpc_availability_zones_a.engine_version
identifier = "prd-foo-rds-instance-read-replica-${count.index + 1}"
instance_class = local.rds_cluster_vpc_availability_zones_a.instance_class
cluster_identifier = local.rds_cluster_vpc_availability_zones_a.cluster_identifier
db_subnet_group_name = local.rds_cluster_vpc_availability_zones_a.db_subnet_group_name
db_parameter_group_name = local.rds_cluster_vpc_availability_zones_a.db_parameter_group_name
monitoring_interval = local.rds_cluster_vpc_availability_zones_a.monitoring_interval
monitoring_role_arn = local.rds_cluster_vpc_availability_zones_a.monitoring_role_arn
auto_minor_version_upgrade = local.rds_cluster_vpc_availability_zones_a.auto_minor_version_upgrade
preferred_maintenance_window = local.rds_cluster_vpc_availability_zones_a.preferred_maintenance_window
apply_immediately = local.rds_cluster_vpc_availability_zones_a.apply_immediately
}
# ---------------------------------------------
# RDS Subnet Group
# ---------------------------------------------
resource "aws_db_subnet_group" "this" {
name = "prd-foo-rds-subnet-gp"
description = "The subnet group for prd-foo-rds"
# 後述の説明を参考にせよ。(10)
subnet_ids = [var.private_a_datastore_subnet_id, var.private_c_datastore_subnet_id]
lifecycle {
create_before_destroy = true
}
}
(1)
パラメーターストア¶
Terraformに値をハードコーディングしたくない場合は、パラメーターストアで値を管理し、これをdata
ブロックで取得する。
(2)
メンテナンスウインドウ時に変更適用¶
メンテナンスウインドウ時の変更適用をTerraformで行う場合、一段階目にapply_immediately
オプションをfalse
に変更してterraform apply
コマンドを実行することにより、二段階目に修正をterraform apply
コマンドを実行する。
(3)
DBクラスターにはAZが3
個必要¶
DBクラスターでは、レプリケーションのために、3
個のAZが必要である。
そのため、指定したAZが2つであっても、コンソール画面上で3
個のAZが自動的に設定される。
Terraformがこれを認識しないように、ignore_changes
引数でAZを指定しておく必要がある。
(4)
インスタンスを配置するAZは選択できない¶
事前にインスタンスにAZを表す識別子を入れたとしても、Terraformはインスタンスを配置するAZを選択できない。
そのため、AZと識別子の関係が逆になってしまうことがある。
多くの場合、 c
ゾーンのインスタンスが最初に作成されるため、インスタンスのゾーン名と配置されるa
/c
ゾーンが逆になる。
その場合は、デプロイ後に手動で名前を変更すれば良い。
この変更は、Terraformが差分として認識しないので問題ない。
(5)
エンジンバージョンのアップグレードは画面から¶
運用でTerraformでエンジンバージョンをアップグレードすることに抵抗感がある場合、コンソール画面からアップグレードをTerraformで無視すると良い。
ただし、画面からの変更後にTerraformのコードも変更しておく必要がある。
Terraformを書き換えなくとも問題は起こらないが、Terraformのコードと実インフラが乖離してしまう。
(6)``for_each
引数を使用して¶
Auroraでは、クラスターにインスタンスを1つだけ紐付けると、プライマリーインスタンスとして作成される。
また以降インスタンスを紐付けると、リードレプリカとして自動的に作成されていく。
AZのマップデータに対してfor_each
引数を使用することにより、各AZに最低1つのインスタンスを配置するように設定できる。
(7)
インスタンスタイプは別々に設定する¶
インスタンスタイプにfor_each
引数で値を渡さない場合、各DBインスタンスのインスタンスタイプを同時に変更することになる。
この場合、インスタンスのフェイルオーバーを使用できず、ダウンタイムを最小化できない。
そのため、for_each
引数を使用して、DBインスタンスごとにインスタンスタイプを設定する。
インスタンスごとに異なるインスタンスタイプを設定する場合は、for_each
引数で割り当てる値の順番を考慮する必要があるため、配置されているAZを事前に確認する必要がある。
(8)
インスタンスにバックアップウインドウは設定しない¶
DBクラスターとDBインスタンスの両方に、preferred_backup_window
オプションを設定できるが、RDSインスタンスに設定してはいけない。
(9)
リードレプリカの追加¶
クラスターに count
引数で量産したインスタンスを紐付ける。
count
引数は本来非推奨であるが、同じ設定のインスタンスを単に量産するだけなため、許容する。
(10)
マルチAZを有効化する¶
Auroraでは、紐付けられたサブネットグループが複数のAZのサブネットで構成されている場合、各インスタンスを自動的にAZに配置するようになっている。
そのため、サブネットグループに複数のサブネットを紐付けるようにする。
RDS (Global) の場合¶
グローバルクラスター¶
グローバルクラスターは、状況によって実装方法が少し異なる。
実装方法で依存関係の向きを変えているが、使っているAWSリソースのAPIは同じである。
そのため、変更前後でterraform plan
コマンドで差分は同じになる。
既存のクラスターからグローバルクラスターを作成する¶
既存のクラスターからグローバルクラスターを作成する。
aws_rds_global_cluster
ブロックが aws_rds_cluster
ブロック依存している。
これにより、スタンドアローンを作成した上で、これをグローバルクラスターに昇格させられる。
この依存関係の方向は、新規にグローバルクラスターを作成させる場合と逆である。
さらに、作成したグローバルクラスターにセカンダリークラスターを追加する。
『既存のクラスターからグローバルクラスターを作成する』と『グローバルクラスターを新規作成する』の実装方法を組み合わせた方法である。
resource "aws_rds_global_cluster" "foo" {
...
// 東京リージョンのみで作成する
count = var.region == "ap-northeast-1" ? 1 : 0
// 既存クラスターからグローバルクラスターを作成するために、グローバルクラスターが既存クラスターに依存するように設定する
// aws_rds_cluster には、同時に設定できない global_cluster_identifier という対になるオプションがあり、大阪リージョンではそちらを使用する
// 東京リージョンでは、aws_rds_global_cluster の source_db_cluster_identifier を使用する
// @see https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/rds_global_cluster#new-global-cluster-from-existing-db-cluster
source_db_cluster_identifier = aws_rds_cluster.foo.arn
global_cluster_identifier = "<東京リージョンのプライマリークラスター名>"
force_destroy = true
lifecycle {
ignore_changes = [
// グローバルクラスターは、既存クラスターからデータベース名の設定を継承するため、変更を無視する
database_name,
]
}
...
}
// プライマリークラスターとセカンダリークラスター兼用
resource "aws_rds_cluster" "foo" {
// グローバルクラスターに依存しないように、設定値をハードコーディングする
// 大阪リージョンのセカンダリークラスターでは、東京リージョンから各種設定を継承するため、設定が不要である
database_name = var.region == "ap-northeast-1" ? "foo" : ""
master_username = var.region == "ap-northeast-1" ? "admin" : ""
master_password = var.region == "ap-northeast-1" ? "password" : ""
// 大阪リージョンのセカンダリークラスターでは、データのソースを東京リージョンとする
source_region = var.region == "ap-northeast-1" ? "" : var.region
// グローバルクラスターに大阪リージョンを追加する
// aws_rds_global_cluster には、同時に設定できない source_db_cluster_identifier という対になるオプションがあり、東京リージョンではそちらを使用する
// 大阪リージョンでは、aws_rds_cluster の global_cluster_identifier を使用するため、global_cluster_identifier の値は空文字とする
// @see https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/rds_global_cluster#new-mysql-global-cluster
global_cluster_identifier = var.region == "ap-northeast-1" ? "<東京リージョンのプライマリークラスター名>" : ""
engine = "<エンジン>"
engine_version = "<エンジンバージョン>"
lifecycle {
ignore_changes = [
// @see https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/rds_global_cluster#new-global-cluster-from-existing-db-cluster
global_cluster_identifier
]
}
...
}
グローバルクラスターを新規作成する¶
グローバルクラスターを新規作成する。
同時に、プライマリークラスターとセカンダリークラスターを新規作成する
aws_rds_cluster
ブロックが aws_rds_global_cluster
ブロックに依存している。
この依存関係の方向は、既存のクラスターをグローバルクラスターに昇格させる場合と逆である。
resource "aws_rds_global_cluster" "foo" {
...
// 東京リージョンのみで作成する
count = var.region == "ap-northeast-1" ? 1 : 0
global_cluster_identifier = "<東京リージョンのプライマリークラスター名>"
engine = "aurora-mysql"
engine_version = "5.7.mysql_aurora.2.07.5"
...
}
// プライマリークラスターとセカンダリークラスター兼用
resource "aws_rds_cluster" "foo" {
...
database_name = "foo"
master_username = "admin"
master_password = "password"
engine = aws_rds_global_cluster.foo.engine
engine_version = aws_rds_global_cluster.foo.engine_version
global_cluster_identifier = aws_rds_global_cluster.foo.id
...
}
アップグレードする¶
aws_rds_cluster
ブロックがaws_rds_global_cluster
ブロックに依存している。
この依存関係の方向は、既存のクラスターをグローバルクラスターに昇格させる場合と逆である。
resource "aws_rds_global_cluster" "foo" {
...
// 東京リージョンのみで作成する
count = var.region == "ap-northeast-1" ? 1 : 0
global_cluster_identifier = "<東京リージョンのプライマリークラスター名>"
engine = "aurora-mysql"
engine_version = "5.7.mysql_aurora.2.07.5"
...
}
// プライマリークラスターとセカンダリークラスター兼用
resource "aws_rds_cluster" "foo" {
...
database_name = "foo"
master_username = "admin"
master_password = "password"
engine = aws_rds_global_cluster.foo.engine
engine_version = aws_rds_global_cluster.foo.engine_version
global_cluster_identifier = aws_rds_global_cluster.foo.id
lifecycle {
ignore_changes = [
engine_version
]
}
...
}
Route53¶
まとめ¶
*実装例*
# ---------------------------------------------
# For foo domain
# ---------------------------------------------
resource "aws_route53_zone" "foo" {
name = var.route53_domain_foo
}
resource "aws_route53_record" "foo" {
zone_id = aws_route53_zone.foo.id
name = var.route53_domain_foo
type = "A"
alias {
name = var.alb_dns_name
zone_id = var.alb_zone_id
evaluate_target_health = false
}
}
**実装例***
# ---------------------------------------------
# For foo domain
# ---------------------------------------------
resource "aws_route53_zone" "foo" {
name = var.route53_domain_foo
}
resource "aws_route53_record" "foo" {
zone_id = aws_route53_zone.foo.id
name = var.route53_domain_foo
type = "NS"
ttl = 30
# NSレコードのリストを出力する
records = aws_route53_zone.foo.name_servers
# AWSが自動的に作成するNSレコードとTerraformによるそれが衝突するため、Terraform側が上書き可能にする
allow_overwrite = true
}
ルートテーブル¶
メインルートテーブルは自動作成¶
Terraformを使用してVPCを作成した時、メインルートテーブルが自動的に作成される。
そのため、これはTerraformの管理外である。
S3¶
バケットポリシー¶
S3紐付けられる、自身へのアクセスを制御するためにインラインポリシーのこと。
定義したバケットポリシーは、aws_s3_bucket_policy
リソースでロールに紐付けできる。
ALBアクセスログ¶
ALBがバケットにログを書き込めるように、『ELBのサービスアカウントID』を許可する必要がある。
*実装例*
# ---------------------------------------------
# S3 bucket policy
# ---------------------------------------------
# S3にバケットポリシーを紐付けします。
resource "aws_s3_bucket_policy" "alb" {
bucket = aws_s3_bucket.alb_logs.id
policy = templatefile(
"${path.module}/policies/alb_bucket_policy.tpl",
{}
)
}
ALBのアクセスログを送信するバケット内には、自動的に『/AWSLogs/
そのため、『arn:aws:s3:::<バケット名>/*
』の部分を最小権限として、『arn:aws:s3:::<バケット名>/AWSLogs/<AWSアカウントID>/;*
』にしても良い。
東京リージョンのELBサービスアカウントIDは『582318560864
』である。
{
"Version": "2012-10-17",
"Statement":
[
{
"Effect": "Allow",
"Principal": {"AWS": "arn:aws:iam::582318560864:root"},
"Action": "s3:PutObject",
"Resource": "arn:aws:s3:::<バケット名>/*",
},
],
}
NLBアクセスログ¶
ALBがバケットにログを書き込めるように、『delivery.logs.amazonaws.com
』からのリクエストを許可する必要がある。
*実装例*
# ---------------------------------------------
# S3 bucket policy
# ---------------------------------------------
# S3にバケットポリシーを紐付けします。
resource "aws_s3_bucket_policy" "nlb" {
bucket = aws_s3_bucket.nlb_logs.id
policy = templatefile(
"${path.module}/policies/nlb_bucket_policy.tpl",
{}
)
}
NLBのアクセスログを送信するバケット内には、自動的に『/AWSLogs/<AWSアカウントID>
』の名前でディレクトリを作成する。
そのため、『arn:aws:s3:::<バケット名>/*
』の部分を最小権限として、『arn:aws:s3:::<バケット名>/AWSLogs/<AWSアカウントID>/;*
』にしても良い。
{
"Version": "2012-10-17",
"Statement":
[
{
"Sid": "AWSLogDeliveryWrite",
"Effect": "Allow",
"Principal": {"Service": "delivery.logs.amazonaws.com"},
"Action": "s3:PutObject",
"Resource": "arn:aws:s3:::<バケット名>/*",
"Condition": {
# 完全一致
"StringEquals": {"s3:x-amz-acl": "bucket-owner-full-control"},
},
},
{
"Sid": "AWSLogDeliveryAclCheck",
"Effect": "Allow",
"Principal": {"Service": "delivery.logs.amazonaws.com"},
"Action": "s3:GetBucketAcl",
"Resource": "arn:aws:s3:::<バケット名>",
},
],
}
AWS Systems Manager¶
まとめ¶
# ---------------------------------------------
# For RDS
# ---------------------------------------------
output "rds_db_name_ssm_parameter_value" {
sensitive = true # 後述の説明を参考にせよ。(1)
value = data.aws_ssm_parameter.rds_db_name.value
}
output "rds_db_master_password_ssm_parameter_value" {
sensitive = true
value = data.aws_ssm_parameter.rds_db_master_password.value
}
output "rds_db_master_username_ssm_parameter_value" {
sensitive = true
value = data.aws_ssm_parameter.rds_db_master_username.value
}
output "rds_db_port_ssm_parameter_value" {
sensitive = true
value = data.aws_ssm_parameter.rds_db_port.value
}
(1)
terraform plan`コマンド時に非表示¶
CIのterraform plan
コマンド時に値が公開されないようにoutput
ブロックでsensitive
オプションを有効化する。
VPC¶
まとめ¶
# 後述の説明を参考にせよ。(1)
vpc_availability_zones = { a = "a", c = "c" }
vpc_cidr = "*.*.*.*/23"
vpc_subnet_public_cidrs = { a = "*.*.*.*/27", c = "*.*.*.*/27" }
vpc_subnet_private_datastore_cidrs = { a = "*.*.*.*/27", c = "*.*.*.*/27" }
vpc_subnet_private_app_cidrs = { a = "*.*.*.*/25", c = "*.*.*.*/25" }
# ---------------------------------------------
# VPC
# ---------------------------------------------
resource "aws_vpc" "this" {
cidr_block = var.vpc_cidr
enable_dns_support = true
enable_dns_hostnames = true
tags = {
Name = "${var.environment}-${var.service}-vpc"
}
}
# ---------------------------------------------
# Internet Gateway
# ---------------------------------------------
resource "aws_internet_gateway" "this" {
vpc_id = aws_vpc.this.id
tags = {
Name = "${var.environment}-${var.service}-igw"
}
}
# ---------------------------------------------
# パブリックサブネット
# ---------------------------------------------
resource "aws_subnet" "public" {
for_each = var.vpc_availability_zones
vpc_id = aws_vpc.this.id
cidr_block = var.vpc_subnet_public_cidrs[each.key]
availability_zone = "${var.region}${each.value}"
map_public_ip_on_launch = true
tags = {
Name = format(
"${var.environment}-${var.service}-pub-%s-subnet",
each.value
)
}
}
# ---------------------------------------------
# プライベートサブネット
# ---------------------------------------------
# App subnet
resource "aws_subnet" "private_app" {
for_each = var.vpc_availability_zones
vpc_id = aws_vpc.this.id
cidr_block = var.vpc_subnet_private_app_cidrs[each.key]
availability_zone = "${var.region}${each.value}"
tags = {
Name = format(
"${var.environment}-${var.service}-pvt-%s-app-subnet",
each.value
)
}
}
# Datastore subnet
resource "aws_subnet" "private_datastore" {
for_each = var.vpc_availability_zones
vpc_id = aws_vpc.this.id
cidr_block = var.vpc_subnet_private_datastore_cidrs[each.key]
availability_zone = "${var.region}${each.value}"
tags = {
Name = format(
"${var.environment}-${var.service}-pvt-%s-datastore-subnet",
each.value
)
}
}
# ---------------------------------------------
# ルートテーブル (パブリック)
# ---------------------------------------------
resource "aws_route_table" "public" {
vpc_id = aws_vpc.this.id
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.this.id
}
tags = {
Name = "${var.environment}-${var.service}-pub-rtb"
}
}
# ---------------------------------------------
# ルートテーブル (プライベート)
# ---------------------------------------------
resource "aws_route_table" "private_app" {
for_each = var.vpc_availability_zones
vpc_id = aws_vpc.this.id
route {
cidr_block = "0.0.0.0/0"
nat_gateway_id = aws_nat_gateway.this[each.key].id
}
tags = {
Name = format(
"${var.environment}-${var.service}-pvt-%s-app-rtb",
each.value
)
}
}
# ---------------------------------------------
# ルートテーブルアソシエーション (パブリック)
# ---------------------------------------------
resource "aws_route_table_association" "public" {
for_each = var.vpc_availability_zones
subnet_id = aws_subnet.public[each.key].id
route_table_id = aws_route_table.public.id
}
# ---------------------------------------------
# ルートテーブルアソシエーション (プライベート)
# ---------------------------------------------
resource "aws_route_table_association" "private_app" {
for_each = var.vpc_availability_zones
subnet_id = aws_subnet.private_app[each.key].id
route_table_id = aws_route_table.private_app[each.key].id
}
# ---------------------------------------------
# NAT Gateway
# ---------------------------------------------
resource "aws_nat_gateway" "this" {
for_each = var.vpc_availability_zones
subnet_id = aws_subnet.public[each.key].id
allocation_id = aws_eip.nat_gateway[each.key].id
tags = {
Name = format(
"${var.environment}-${var.service}-%s-ngw",
each.value
)
}
depends_on = [aws_internet_gateway.this]
}
# ---------------------------------------------
# Elastic IP
# ---------------------------------------------
resource "aws_eip" "nat_gateway" {
for_each = var.vpc_availability_zones
vpc = true
tags = {
Name = format(
"${var.environment}-${var.service}-ngw-%s-eip",
each.value
)
}
depends_on = [aws_internet_gateway.this]
}
(1)
冗長化されたAWSリソースをfor_each関数で作成¶
AZを上長化している場合、VPC内のサブネットと関連のAWSリソース (ルートテーブル、NAT Gateway、Elastic IPなど) も冗長化することになる。
各AZをキーとするマップ型で定義しておいた変数をfor_each
引数に渡し、AWSリソースをAZごとに作成する。
VPC endpoint¶
まとめ¶
# ---------------------------------------------
# VPC endpoint
# ---------------------------------------------
resource "aws_vpc_endpoint" "cloudwatch_logs" {
vpc_id = aws_vpc.this.id
subnet_ids = [aws_subnet.private_app[var.vpc_availability_zones.a].id, aws_subnet.private_app[var.vpc_availability_zones.c].id]
vpc_endpoint_type = "Interface"
service_name = "com.amazonaws.${var.region}.logs"
private_dns_enabled = true
security_group_ids = [var.cloudwatch_logs_endpoint_security_group_id]
tags = {
Name = "${var.environment}-${var.service}-cw-logs-ep"
}
}
resource "aws_vpc_endpoint" "ecr_api" {
vpc_id = aws_vpc.this.id
subnet_ids = [aws_subnet.private_app[var.vpc_availability_zones.a].id, aws_subnet.private_app[var.vpc_availability_zones.c].id]
vpc_endpoint_type = "Interface"
service_name = "com.amazonaws.${var.region}.ecr.api"
private_dns_enabled = true
security_group_ids = [var.ecr_endpoint_security_group_id]
tags = {
Name = "${var.environment}-${var.service}-ecr-api-ep"
}
}
resource "aws_vpc_endpoint" "ecr_dkr" {
vpc_id = aws_vpc.this.id
subnet_ids = [aws_subnet.private_app[var.vpc_availability_zones.a].id, aws_subnet.private_app[var.vpc_availability_zones.c].id]
vpc_endpoint_type = "Interface"
service_name = "com.amazonaws.${var.region}.ecr.dkr"
private_dns_enabled = true
security_group_ids = [var.ecr_endpoint_security_group_id]
tags = {
Name = "${var.environment}-${var.service}-ecr-dkr-ep"
}
}
resource "aws_vpc_endpoint" "s3" {
vpc_id = aws_vpc.this.id
route_table_ids = [aws_route_table.private_app[var.vpc_availability_zones.a].id, aws_route_table.private_app[var.vpc_availability_zones.c].id]
vpc_endpoint_type = "Gateway"
service_name = "com.amazonaws.${var.region}.s3"
tags = {
Name = "${var.environment}-${var.service}-s3-ep"
}
}
resource "aws_vpc_endpoint" "ssm" {
vpc_id = aws_vpc.this.id
subnet_ids = [aws_subnet.private_app[var.vpc_availability_zones.a].id, aws_subnet.private_app[var.vpc_availability_zones.c].id]
vpc_endpoint_type = "Interface"
service_name = "com.amazonaws.${var.region}.ssm"
private_dns_enabled = true
security_group_ids = [var.ssm_endpoint_security_group_id]
tags = {
Name = "${var.environment}-${var.service}-ssm-ep"
}
}
resource "aws_vpc_endpoint" "ssmmessages" {
vpc_id = aws_vpc.this.id
subnet_ids = [aws_subnet.private_app[var.vpc_availability_zones.a].id, aws_subnet.private_app[var.vpc_availability_zones.c].id]
vpc_endpoint_type = "Interface"
service_name = "com.amazonaws.${var.region}.ssmmessages"
private_dns_enabled = true
security_group_ids = [var.ssmmessages_endpoint_security_group_id]
tags = {
Name = "${var.environment}-${var.service}-ssmmessages-ep"
}
}
WAF¶
ruleブロック¶
*実装例*
API Gateway用のWAFに、特定のユーザーエージェントを拒否するルールを設定する。
resource "aws_wafv2_web_acl" "api_gateway" {
rule {
name = "block-user-agents"
priority = 0
statement {
regex_pattern_set_reference_statement {
# 別ディレクトリのmain.tfファイルに分割した正規表現パターンセットを参照する。
arn = var.wafv2_regex_pattern_set_regional_block_user_agents_arn
field_to_match {
# ヘッダーを検証する。
single_header {
name = "user-agent"
}
}
text_transformation {
priority = 0
type = "NONE"
}
}
}
action {
block {}
}
visibility_config {
cloudwatch_metrics_enabled = true
metric_name = "APIGatewayWAFBlockUserAgentsRule"
sampled_requests_enabled = true
}
}
default_action {
allow {}
}
visibility_config {
cloudwatch_metrics_enabled = true
metric_name = "APIGatewayALBWAFRules"
sampled_requests_enabled = true
}
...
}
*実装例*
API Gateway用のWAFに、特定のグローバルIPアドレスを拒否するルールを設定する。
resource "aws_wafv2_web_acl" "api_gateway" {
rule {
name = "block-global-ip-addresses"
priority = 0
statement {
ip_set_reference_statement {
# 別ディレクトリのmain.tfファイルに分割したIPアドレスセットを参照する。
arn = var.waf_blocked_global_ip_addresses
}
}
action {
block {}
}
visibility_config {
cloudwatch_metrics_enabled = true
metric_name = "APIGatewayWAFBlockGlobalIPAddressesRule"
sampled_requests_enabled = true
}
}
default_action {
allow {}
}
visibility_config {
cloudwatch_metrics_enabled = true
metric_name = "APIGatewayWAFRules"
sampled_requests_enabled = true
}
...
}
*実装例*
API Gateway用のWAFに、SQLインジェクションを拒否するマネージドルールを設定する。
resource "aws_wafv2_web_acl" "api_gateway" {
rule {
name = "block-sql-injection"
priority = 0
statement {
# マネージドルールを使用する。
managed_rule_group_statement {
vendor_name = "AWS"
name = "AWSManagedRulesSQLiRuleSet"
}
}
override_action {
count {}
}
visibility_config {
cloudwatch_metrics_enabled = true
metric_name = "APIGatewayWAFBlockSQLInjectionRule"
sampled_requests_enabled = true
}
}
default_action {
allow {}
}
visibility_config {
cloudwatch_metrics_enabled = true
metric_name = "APIGatewayWAFRules"
sampled_requests_enabled = true
}
...
}
*実装例*
ALB用のWAFに、APIキーまたはBearerトークンをOR条件ルールを設定する。
あくまで例としてで、本来であれば、異なるルールとした方が良い。
resource "aws_wafv2_web_acl" "api_gateway" {
# x-api-keyヘッダーにAPIキーを含むリクエストを許可します。
rule {
name = "allow-request-including-api-key"
priority = 3
statement {
or_statement {
# APIキーを持つのリクエストを許可します。
statement {
byte_match_statement {
positional_constraint = "EXACTLY"
search_string = var.waf_api_key_ssm_parameter_value
field_to_match {
single_header {
name = "x-api-key"
}
}
text_transformation {
priority = 0
type = "NONE"
}
}
}
# Bearerトークンを持つリクエストを許可します。
statement {
byte_match_statement {
positional_constraint = "EXACTLY"
search_string = var.waf_bearer_token_ssm_parameter_value
field_to_match {
single_header {
name = "authorization"
}
}
text_transformation {
priority = 0
type = "NONE"
}
}
}
}
}
action {
allow {}
}
visibility_config {
cloudwatch_metrics_enabled = true
metric_name = "APIGatewayWAFAllowRequestIncludingAPIKeyRule"
sampled_requests_enabled = true
}
}
...
}
IPセットの依存関係¶
WAFのIPセットと他設定の依存関係に癖がある。
新しいIPセットへの付け換えと古いIPセットの削除を同時にデプロイしないようにする。
もし同時に行った場合、Terraformは古いIPセットの削除処理を先に実行するが、これはWAFに紐付いているため、ここでエラーが起こってしまう。
そのため、IPセットを新しく再設定する場合は、以下の通り2
個の段階に分けてデプロイする。
補足として、IPセットの名前を変更する場合は、更新処理ではなく削除を伴う再作成処理が実行されるため注意する。
(1) 新しいIPセットのresourceを実装し、ACLに紐付け、デプロイする。
(2)古いIPセットのresourceを削除し、デプロイする。
もし、これを忘れてしまった場合は、画面上で適当なIPセットに付け換えて、削除処理を実行可能にする。
Terraform管理外のAWSリソース¶
判断基準¶
以下の理由で、個人的には、一部のAWSリソースではTerraformを使用しない方が良い。
代わりに、コンソール画面やAWS CLIでAWSリソースで構築する。
-
ビジネスロジックを持つAWSリソースでは、継続的な改善のサイクルが早い (変更の要望頻度が高い) ため、リリースまでに時間がかかるTerraformで管理すると、このサイクルを阻害してしまう:
-
API Gateway、IAMユーザー/グループ、IAMユーザー/グループに関連するロール/ポリシーなど
-
セキュリティを含むAWSリソースでは、Terraformのリポジトリで機密な変数やファイルを管理するわけにはいかず、また
tfstate
ファイルに書き込まれないようにする: -
EC2の秘密鍵、パラメーターストアなど
-
Terraformによる初期作成時に必要であり、それがないとそもそも
terraform apply
コマンドできない: -
Terraform用IAMユーザー、tfstateを管理するS3バケットなど
-
これに関しては、CloudFormationで作成しても良い。
-
Terraformの誤操作で削除してはいけないAWSリソースでは、Terraformで管理しないことにより、削除を防げる:
-
tfstateを管理するS3バケットなど
-
Terraformで特定のAWSリソースを作成すると、それに伴って自動的に作成されてしまう:
-
ENIなど
-
そもそもAWSがAPIを公開していないことが理由で、Terraformで実装できない:
- Chatbotなど
詳細¶
AWSリソース | 管理外の部分 | 管理外の理由 |
---|---|---|
ACM | 全て | terraform apply コマンド中に承認作業が発生し、プロビジョニングが止まってしまうため。ただし、承認を自動で実行できるオプションを使用する場合には、Terraformで管理しても良い。 |
API Gateway、紐付くVPCリンク | 全て | ビジネスロジックを持ち、変更の要望頻度が高い。バックエンドチームがスムーズにAPIを作成できるようになる。 |
Chatbot | 全て | AWSがAPIを公開していないため、Terraformで作成できない。 |
DynamoDB | 全て | state.lock ファイルをテーブルとして管理する。 |
EC2 | 秘密鍵 | Terraformで作成する時にGitHubで秘密鍵を管理する必要があるため、セキュリティ上の理由で却下する。 |
ENI | 全て | 特定のAWSリソース (ALB、セキュリティグループなど) の作成に伴って、自動的に作成されるため、Terraformで管理できない。 |
EventBridge | StepFunctionsGetEventsForECSTaskRule | Step FunctionsでECS RunTaskの『ECSタスクが完了するまで待機』オプションを選択すると自動的に作成されるため、Terraformで管理できない。このルールは、ECSのECSタスクの状態がSTOPPEDになったことを検知し、StepFunctionsに通知してくれる。STOPPED は、ECSタスクが正常に停止 (完了?) した状態を表す。 |
Global Accelerator | セキュリティグループ | リソースを作成するとセキュリティグループが自動作成されるため、セキュリティグループのみTerraformで管理できない。 |
IAMユーザー | 全て | ビジネスロジックを持ち、変更の要望頻度が高い。 |
IAMユーザーグループ | 全て | ビジネスロジックを持ち、変更の要望頻度が高い。 |
IAMロール | ユーザーに紐付くロール | ビジネスロジックを持ち、変更の要望頻度が高い。 |
サービスリンクロール | サービスリンクロールは自動的に作成されるが、これが行われる前に事前にTerraformで作成でき、以下のリンクにて各AWSリソースにサービスリンクロールが存在しているのか否かを確認できる。しかし、数が多く、また初回作成時のみしかエラーは起こらないため、サービスリンクロールはTerraformで作成しないようにする。 - https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_aws-services-that-work-with-iam.html |
|
IAMポリシー | ビジネスロジックを持ち、変更の要望頻度が高い。ただし、IPアドレス制限ポリシーなど、自動化した方が便利になる場合はこの限りではない。 | |
RDS | admin以外のユーザー | 個別のユーザー作成のために、mysql providerを使用する必要がある。ただし、moduleディレクトリ配下にprovider.tf ファイルを配置する必要があるため、ディレクトリ構成規約に難がある。 |
Route53 | NSレコード | ホストゾーンを作成すると、レコードとして、NSレコード値が自動的に設定される。これは、Terraformの管理外である。 |
S3 | tfstate ファイルの管理バケット |
・リモートバックエンドとしてtfstate ファイルを格納するため、Terraformのデプロイより先に存在している必要がある。・Terraformで誤って削除してしまわないようにする。 ・ terraform apply -destroy コマンドの実行時に、他のAWSリソースを削除する前にtfstate ファイルのS3バケットを先に削除してしまう。 |
SES | 全て | terraform apply コマンド中に承認作業が発生し、プロビジョニングが止まってしまうため。ただし、承認を自動で実行できる場合には、Terraformで管理しても良い。 |
パラメーターストア | 全て | セキュリティを含むAWSリソースでは、Terraformのリポジトリで機密な変数やファイルを管理するわけにはいかず、またtfstate ファイルに書き込まれてしまうため。パラメーターストアの代わりに、キーバリュー型ストア (例:SOPS、kubesec、Hashicorp Vault) を使用しつつ、暗号化された状態でリポジトリで管理しても良い。 |
AWS CLIによるセットアップ例¶
▼ S3バケット¶
以下のコマンドを実行し、tfstate
ファイル用のS3バケットを作成する。
AWS_REGION="ap-northeast-1"
PREFIX=foo
ENV=prd
BUCKET_NAME=${ENV}-${PREFIX}-tfstate
if aws s3 ls | grep --silent ${BUCKET_NAME}; then
echo "${BUCKET_NAME} already exists"
exit 1
fi
aws s3api create-bucket \
--region ${AWS_REGION} \
--bucket ${BUCKET_NAME} \
--create-bucket-configuration LocationConstraint=${AWS_REGION}
aws s3api put-public-access-block \
--region ${AWS_REGION} \
--bucket ${BUCKET_NAME} \
--public-access-block-configuration BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true
aws s3api put-bucket-versioning \
--region ${AWS_REGION} \
--bucket ${BUCKET_NAME} \
--versioning-configuration Status=Enabled
aws s3api put-bucket-encryption \
--region ${AWS_REGION} \
--bucket ${BUCKET_NAME} \
--server-side-encryption-configuration '
{
"Rules": [
{
"ApplyServerSideEncryptionByDefault": {
"SSEAlgorithm": "AES256"
}
}
]
}'
▼ DynamoDB¶
DynamoDBでは、state.lock
ファイルをテーブルとして管理できる。
以下のコマンドを実行し、state.lock
ファイル用のDynamoDBを作成する。
AWS_REGION="ap-northeast-1"
PREFIX=foo
ENV=prd
TABLE_NAME=${ENV}-${PREFIX}-state-lock
aws dynamodb create-table\
--region ${AWS_REGION} \
--table-name ${TABLE_NAME} \
--attribute-definitions AttributeName=LockID,AttributeType=S \
--key-schema AttributeName=LockID,KeyType=HASH \
--provisioned-throughput ReadCapacityUnits=1,WriteCapacityUnits=1
tfstate
ファイルに関連するAWSリソースをTerraformで管理する¶
▼ S3バケットの場合¶
tfstate
ファイルを管理するS3バケットをTerraformで管理しないプラクティスが一般的ではあるが、実はTerraformで管理する裏技がある。
それは、tfstate
ファイルのS3バケットのみを、別のローカルバックエンドのtfsftate
ファイルで管理する方法である。
このローカルバックエンドのtfstate
ファイルが管理するS3バケットをリモートバックエンドとして、メインのtfstate
ファイルを配置する。
もちろん、ローカルバックエンドのtfstate
ファイルがS3バケットに配置されたメインのtfstate
ファイルを検知しないように、無視する必要がある。
terraform {
backend "local" {}
required_version = "1.3.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 4.0"
}
}
}
provider "aws" {
region = "ap-northeast-1"
}
resource "aws_s3_bucket" "tfstate" {
bucket = "tfstate-bucket"
}
resource "aws_s3_bucket_versioning" "tfstate" {
bucket = aws_s3_bucket.tfstate.id
versioning_configuration {
status = "Enabled"
}
}
resource "aws_s3_bucket_public_access_block" "tfstate" {
bucket = aws_s3_bucket.tfstate.id
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}
resource "aws_s3_bucket_server_side_encryption_configuration" "tfstate" {
bucket = aws_s3_bucket.tfstate.id
rule {
apply_server_side_encryption_by_default {
kms_master_key_id = aws_kms_key.tfstate.arn
sse_algorithm = "aws:kms"
}
bucket_key_enabled = true
}
}
resource "aws_kms_key" "tfstate" {
}
▼ DynamoDBの場合¶
state.lock
ファイルを管理するDynamoDBをTerraformで管理しないプラクティスが一般的ではあるが、実はTerraformで管理する裏技がある。
resource "aws_dynamodb_table" "tfstate" {
name = "tfstate-lock-table"
billing_mode = "PAY_PER_REQUEST"
hash_key = "LockID"
attribute {
name = "LockID"
type = "S"
}
}
複数のAWSリソースに共通のプラクティス¶
環境変数¶
▼ AZに関するマップ型データ¶
AZのデータ自体をマップ型データで用意しておく。
また、AZごとに異なる値を設定できるように、その他のデータではAZ名をキー名としたマップデータを定義しておく。
availability_zones = { a = "a", c = "c" }
# ---------------------------------------------
# RDS
# ---------------------------------------------
rds_instance_class = { a = "db.r6g.xlarge", c = "db.r6g.xlarge" }
# ---------------------------------------------
# VPC
# ---------------------------------------------
vpc_cidr = "*.*.*.*/23"
vpc_subnet_private_datastore_cidrs = { a = "*.*.*.*/27", c = "*.*.*.*/27" }
vpc_subnet_private_app_cidrs = { a = "*.*.*.*/25", c = "*.*.*.*/25" }
vpc_subnet_public_cidrs = { a = "*.*.*.*/27", c = "*.*.*.*/27" }
削除保護機能のあるAWSリソース¶
削除保護設定のあるAWSリソースに癖がある。
削除保護の無効化とresource
ブロックの削除を同時にデプロイしないようにする。
もし同時に行った場合、削除処理を先に実行するが、削除は保護されたままなため、エラーになってしまう。
エラーになってしまう。
そのため、このAWSリソースを削除する時は、以下の通り2
個の段階に分けてデプロイする。
(1)
-
削除保護を無効化 (
false
) に変更し、デプロイする。 (2)
-
コードを削除し、デプロイする。
もし、これを忘れてしまった場合は、画面上で削除処理を無効化し、削除処理を実行可能にする。
AWSリソース名 | Terraform上での設定名 |
---|---|
ALB | enable_deletion_protection |
EC2 | disable_api_termination |
RDS | deletion_protection |