Techfirm Cloud Architect Blog

テックファーム株式会社クラウドインフラグループのブログ

ACMにインポートする証明書をTerraformで管理する

ACMの証明書は無償で簡単に発行できて便利ですが、DV(ドメイン認証)証明書になります。
OV(企業認証)証明書やEV証明書を利用したい場合は別に購入してインポートする必要があります。

今回は、外部認証局から証明書を取得してACMへインポートするためのTerraformテンプレートを紹介します。

証明書利用手順

認証局から発行された証明書を利用するためには以下のような手順で行います。

  1. 秘密鍵とCSR(証明書署名要求)を作成する
  2. CSRを認証局へ送り証明書を発行してもらう
  3. 秘密鍵、発行された証明書、対応する中間証明書をACMにインポートする

このうち、1と3をTerraformで実施します。

Terraformテンプレート

今回のTerraformテンプレートは以下の通りです。

/*************************************************************************
*
* Settings
*
*************************************************************************/
locals {
  # 証明書
  csr_certs = {
    "example_2310" = {
      subject = {CN="example.com"}
    }
    "example_2410" = {
      subject   = {CN="example.com", O="Techfirm"}
      dns_names = ["example.com", "test1.example.com"]
    }
  }

  # 証明書ファイル配置ディレクトリ
  cert_dir = "certs"

  # 発行された証明書は上記ディレクトリに以下ファイル名で配置すること
  #  証明書ファイル:     <キー名>.crt
  #  中間証明書ファイル: <キー名>_chain.pem
}

/*************************************************************************
*
* PivateKey and CSR
*
*************************************************************************/
# Private Key
resource "tls_private_key" "csr_certs" {
  for_each = local.csr_certs

  algorithm   = lookup(each.value, "algorithm", "ECDSA")
  ecdsa_curve = lookup(each.value, "ecdsa_curve", "P384")
  rsa_bits    = lookup(each.value, "rsa_bits", null)
}
# CSR
resource "tls_cert_request" "csr_certs" {
  for_each = local.csr_certs

  private_key_pem = tls_private_key.csr_certs[each.key].private_key_pem
  dns_names       = lookup(each.value, "dns_names", null)
  subject {
    common_name         = each.value["subject"]["CN"]
    organization        = lookup(each.value["subject"], "O", null)
    organizational_unit = lookup(each.value["subject"], "OU", null)
    country             = lookup(each.value["subject"], "C", null)
    province            = lookup(each.value["subject"], "ST", null)
    locality            = lookup(each.value["subject"], "L", null)
    postal_code         = lookup(each.value["subject"], "PC", null)
    street_address      = lookup(each.value["subject"], "STREET", null)
    serial_number       = lookup(each.value["subject"], "SERIAL", null)
  }
}

/*************************************************************************
*
* Import Certificate
*
*************************************************************************/
locals {
  csr_cert_exists = compact([
    for k,v in local.csr_certs: fileexists("${local.cert_dir}/${k}.crt") ? k : null
  ])
}
# ACM
resource "aws_acm_certificate" "csr_certs" {
  for_each = toset(local.csr_cert_exists)

  private_key       = tls_private_key.csr_certs[each.key].private_key_pem
  certificate_body  = file("${local.cert_dir}/${each.key}.crt")
  certificate_chain = (
    fileexists("${local.cert_dir}/${each.key}_chain.pem") 
                                  ? file("${local.cert_dir}/${each.key}_chain.pem") : null
  )
}

/*************************************************************************
*
* Output
*
*************************************************************************/
output "csr" {
  value = {
    for k,v in tls_cert_request.csr_certs: k => trimspace(v.cert_request_pem)
  }
}

テンプレートの説明

秘密鍵の管理

証明書の秘密鍵はtlsプロバイダのtls_private_keyで作成できます。
ドキュメントには、秘密鍵は暗号化されずにStateファイルに保存されるために勧められないと記載があります。
しかし、どちらにしてもACMへのインポートをTerraformで行った時点で秘密鍵はStateファイルに記載されます。

tls_private_keyリソースを使うメリットは、秘密鍵を別ファイルで管理する必要がないことです。
秘密鍵はStateファイルの外に出す必要がなく、Stateファイルの管理を厳重にすれば人の目に触れることはありません。

秘密鍵を生成しているのは以下の部分で、ECDSA P384の秘密鍵を生成しています。
(証明書によってアルゴリズムを変更することもできるようにしています)

resource "tls_private_key" "csr_certs" {
  for_each = local.csr_certs

  algorithm   = lookup(each.value, "algorithm", "ECDSA")
  ecdsa_curve = lookup(each.value, "ecdsa_curve", "P384")
  rsa_bits    = lookup(each.value, "rsa_bits", null)
}

CSR(証明書署名要求)

CSRはtlsプロバイダのtls_cert_requestで作成できます。

resource "tls_cert_request" "csr_certs" {
  for_each = local.csr_certs

  private_key_pem = tls_private_key.csr_certs[each.key].private_key_pem
  dns_names       = lookup(each.value, "dns_names", null)
  subject {
    common_name         = each.value["subject"]["CN"]
    organization        = lookup(each.value["subject"], "O", null)
    organizational_unit = lookup(each.value["subject"], "OU", null)
    country             = lookup(each.value["subject"], "C", null)
    province            = lookup(each.value["subject"], "ST", null)
    locality            = lookup(each.value["subject"], "L", null)
    postal_code         = lookup(each.value["subject"], "PC", null)
    street_address      = lookup(each.value["subject"], "STREET", null)
    serial_number       = lookup(each.value["subject"], "SERIAL", null)
  }
}

dns_namesはCSRのSubject Alternative Nameフィールドに記述されます。
複数ドメインに対して有効な証明書を発行するときに利用します。

tls_cert_requestでは、CSRはファイルに出力される等はなくStateファイルに保持されます。
認証局へ送るために、Output等を利用してCSRの内容を確認できるようにしておく必要があります。

output "csr" {
  value = {
    for k,v in tls_cert_request.csr_certs: k => trimspace(v.cert_request_pem)
  }
}

証明書の更新

IaCのコードでは、リソースの更新時に間違えて古いリソースを消してしまうことに注意する必要があります。

証明書更新のさいに、新しい秘密鍵とCSRを生成したタイミングで古い秘密鍵は失いたくありません。
証明書を更新した後に問題が発覚した場合、迅速に元の証明書へ戻す必要もあります。

このテンプレートでは、最初から複数の証明書を管理することを考えています。
ファイル先頭にあるlocal変数csr_certsで作成する証明書を設定しています。

locals {
  # 証明書
  csr_certs = {
    "example_2310" = {
      subject = {CN="example.com"}
    }
    "example_2410" = {
      subject   = {CN="example.com", O="Techfirm"}
      dns_names = ["example.com", "test1.example.com"]
    }
  }
}

上記の例では、CN=example.comの証明書を2つ作成します。
キーとなるexample_2310example_2410は内部管理用の名前であって証明書の内容とは関係ありません。
証明書更新のさいは、キーのみ変えた同じ内容のものを追加することで同じ内容のCSRを作り直せます。
新しいCSRは秘密鍵も作り直されたものになります。

証明書インポート

証明書が発行されたらcertsディレクトリの下に発行された証明書と中間証明書を配置します。
ファイル名はlocal.csr_certsのキー名を含むものになります。

  • 証明書本体: <キー名>.crt(例: example_2410.crt
  • 中間証明書: <キー名>_chain.pem(例: example_2410_chain.pem

Terraformテンプレートでは、上記の証明書本体のファイルがあるかどうかを判定しています。
以下の部分が、ファイルが存在するキー名のリストを作成している部分です。

locals {
  csr_cert_exists = compact([
    for k,v in local.csr_certs: fileexists("${local.cert_dir}/${k}.crt") ? k : null
  ])
}

for文では、証明書本体のファイルが存在する場合はキー名をリストに設定し、ファイルがない場合はnullを設定しています。
compact関数がリストの中からnullを取り除き、最終的にファイルが存在するキー名のリストができあがります。

あとは、作成したリストからaws_acm_certificateリソースを作成するだけです。

resource "aws_acm_certificate" "csr_certs" {
  for_each = toset(local.csr_cert_exists)

  private_key       = tls_private_key.csr_certs[each.key].private_key_pem
  certificate_body  = file("${local.cert_dir}/${each.key}.crt")
  certificate_chain = (
    fileexists("${local.cert_dir}/${each.key}_chain.pem") 
                                  ? file("${local.cert_dir}/${each.key}_chain.pem") : null
  )
}

秘密鍵はtls_private_keyリソースから直接PEM形式で取得できます。
証明書と中間証明書は配置されたファイルをfile関数で読み込みます。

local変数csr_certから定義を消すか証明書ファイルを削除してterraform applyすればACMの証明書も削除されます。

最後に

Terraformで秘密鍵とCSRを生成し、発行された証明書をインポートするテンプレートを作成しました。
コードにより秘密鍵やCSRを管理することができ、秘密鍵のファイル管理も不要になります。
証明書管理の手法を検討するときの参考になればと思います。