TerraformでACM PCAのルート証明書に対してACMからリクエストしてサーバ証明書を発行する処理を自動化する

# はじめに
[以前の記事](https://qiita.com/neruneruo/items/b20b91265b5258069332)で、オレオレ証明書のACMへの設定方法を紹介したが、この方法だとルート証明書の認証局がないため、場合によってはHTTPS接続でエラーになってしまう。
AWSでは、AWS Private Certificate Authorityを使うことで、お手軽に認証局の機能をお任せすることができるので、TerraformのIaCで自動化してサクッと構築したうえで、ACMと連動させられるようにしておこう。

# AWS Private Certificate Authority
##  プライベート認証局の作成
まずは、Private Certificate Authorityの作成からだ。
```local.acm_pca_common_name```には自分の取得しているドメインを設定しておこう。
```permanent_deletion_time_in_days```は削除後にどれだけの期間、取消可能として残しておくかのパラメータだ。今回はお試しで作っているので、最短の7日を設定している。
なお、```usage_mode```を```SHORT_LIVED_CERTIFICATE(有効期間の短い証明書)```に設定すると、ACMからのリクエストがエラーになるので注意が必要だ。

```HCL
resource "aws_acmpca_certificate_authority" "example" {
  type       = "ROOT"
  usage_mode = "GENERAL_PURPOSE"

  certificate_authority_configuration {
    key_algorithm     = "RSA_2048"
    signing_algorithm = "SHA512WITHRSA"

    subject {
      common_name = local.acm_pca_common_name
    }
  }

  enabled                         = true
  permanent_deletion_time_in_days = 7
}
```

## CA証明書のインストール
プライベート認証局を作ったら、CA証明書をインストールする。
特に難しいことはないが、```validity(CA証明書の有効期間)```が、ACMの証明書発行のリクエストの期間よりも短いとエラーになることは留意しておこう。
また、```template_arn```はTerraformのパラメータ上は必須ではないが、今回のケースでは付与していないとエラーになる。

```HCL
resource "aws_acmpca_certificate" "example_root" {
  certificate_authority_arn   = aws_acmpca_certificate_authority.example.arn
  certificate_signing_request = aws_acmpca_certificate_authority.example.certificate_signing_request
  signing_algorithm           = "SHA256WITHRSA"

  template_arn = "arn:aws:acm-pca:::template/RootCACertificate/V1"

  validity {
    type  = "YEARS"
    value = 10
  }
}

resource "aws_acmpca_certificate_authority_certificate" "example" {
  certificate_authority_arn = aws_acmpca_certificate_authority.example.arn

  certificate       = aws_acmpca_certificate.example_root.certificate
  certificate_chain = aws_acmpca_certificate.example_root.certificate_chain
}
```

## ルート証明書をファイル出力する場合
ACMで設定するだけの場合は不要だが、ルート証明書を他のサーバに設定する場合はPEM形式のファイルが必要になるので、```local_file```で出力しておこう。

```HCL
resource "local_file" "example_root_cert_pem" {
  filename = "${path.module}/certificate/example_root.pem"
  content  = aws_acmpca_certificate_authority.example.certificate
}
```

# ACM
## サーバ証明書のリクエスト
ACMは、ACM PCAに証明書のリクエストを送るだけであれば、以下で良い。とてもお手軽だ。

```HCL
resource "aws_acm_certificate" "example" {
  domain_name               = local.acm_cert_domain_name
  certificate_authority_arn = aws_acmpca_certificate_authority.example.arn
}
```

## サーバ証明書のエクスポート
証明書のエクスポートはTerraformのAWS Providerでは機能提供されていないため、```null_resource```改め```terraform_data```でAWS CLIを実行する。

```HCL
resource "terraform_data" "export_cert" {
  provisioner "local-exec" {
    command = <<-EOF
      mkdir -p ./certificate &&
      aws acm export-certificate --certificate-arn ${aws_acm_certificate.example.arn} --passphrase $(echo -n 'example' | openssl base64 -e) > ./certificate/tmp.json
    EOF
  }
}
```

エクスポートにはパスフレーズが必要だが、パスフレーズはBASE64エンコードされている必要があるので、煩雑だが上記のようにしておく。また、出力はサーバ証明書と秘密鍵と証明書チェーンがまとめて出てくるので、一旦JSONの状態のまま一時ファイルに吐き出す。

## サーバ証明書、秘密鍵ファイルの作成
ということで、JSONのエクスポートファイルをjqコマンドで吐き出しておく。
なお、GitLabのように、証明書ファイルのパスフレーズを入力する機能がないサーバで扱う場合は、下記の通りOpenSSLのコマンドでパスフレーズを削除した秘密鍵ファイルを作っておく。パスフレーズの削除が不要な場合は、```PrivateKey```の値を直接example.keyに出力しておこう。

```HCL
resource "terraform_data" "parse_json_cert" {
  depends_on = [terraform_data.export_cert]

  provisioner "local-exec" {
    command = <<-EOF
      jq -r '.Certificate' ./certificate/tmp.json > ./certificate/example.pem &&
      jq -r '.PrivateKey' ./certificate/tmp.json > ./certificate/example_pass.key &&
      openssl rsa -in ./certificate/example_pass.key -out ./certificate/example.key
    EOF
  }
}
```

# 確認
サーバ証明書が発行されたら、以下のように中身を確認してみる。
ちゃんと証明書からルート証明書をたどれることが分かる。
これで、プライベート認証局で証明をしてくれるサーバ証明書の発行ができるようになった！

```shell-session
$ openssl x509 -text -noout -in ./certificate/example.pem 
Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number:
            6d:dc:6a:89:d8:dc:28:26:47:0a:ee:2f:52:34:bd:0f
        Signature Algorithm: sha256WithRSAEncryption
        Issuer: CN = ★ルート証明書のCommon Name
        Validity
            Not Before: Feb 18 06:14:05 2024 GMT
            Not After : Mar 19 07:14:04 2025 GMT
        Subject: CN = ★サーバ証明書のCommon Name
        Subject Public Key Info:
            Public Key Algorithm: rsaEncryption
                RSA Public-Key: (2048 bit)
                Modulus:
                    00:c1:5d:9e:ac:17:f3:b4:15:30:42:dc:9f:e5:c2:
                    ae:51:16:df:67:d5:3c:78:c2:52:27:b2:98:95:e5:
                    68:66:29:df:49:3a:52:7a:d4:50:6b:91:30:40:1d:
                    71:4b:c6:1b:13:79:df:93:36:3f:fd:2c:fe:b4:a3:
                    9f:23:70:89:1a:bd:c2:39:44:cc:9e:67:90:7a:6f:
                    b2:82:ce:b3:91:d6:5a:c9:7b:2e:21:4c:73:f2:16:
                    46:45:c3:2c:16:98:15:ac:84:e1:c3:ad:83:e5:7b:
                    e7:90:18:b0:56:47:aa:0b:e1:ba:95:cb:a8:46:5d:
                    8a:b7:15:2d:27:bd:2d:78:ba:79:b6:0e:22:91:13:
                    44:83:51:51:b0:ae:bf:43:3e:e2:db:de:c7:89:c0:
                    a6:04:76:93:90:cc:9e:30:72:52:60:70:83:f2:58:
                    37:08:9c:8d:62:c2:56:6a:06:15:13:86:e1:2a:0a:
                    a4:57:c3:47:f8:2e:a2:55:a2:21:f3:c2:6b:73:d5:
                    15:93:e9:75:bc:47:c5:82:35:88:51:49:7a:e6:f9:
                    85:2c:9e:e4:92:55:cd:23:4b:3e:14:9e:ae:4c:b6:
                    3f:cd:53:11:49:a6:db:bc:0b:00:d8:06:f9:8e:2a:
                    d8:79:e7:9a:e4:c5:77:35:75:b2:c0:fb:ee:2d:93:
                    4a:0f
                Exponent: 65537 (0x10001)
        X509v3 extensions:
            X509v3 Subject Alternative Name: 
                DNS:★ドメイン名
            X509v3 Basic Constraints: 
                CA:FALSE
            X509v3 Authority Key Identifier: 
                keyid:53:A0:C6:14:D1:1C:C1:3F:0C:50:9E:B2:22:B0:A0:DF:6E:E3:22:CD

            X509v3 Subject Key Identifier: 
                B2:65:1F:22:A1:6D:3D:CB:04:00:F9:2C:33:A5:ED:3E:F3:29:6B:4A
            X509v3 Key Usage: critical
                Digital Signature, Key Encipherment
            X509v3 Extended Key Usage: 
                TLS Web Server Authentication, TLS Web Client Authentication
    Signature Algorithm: sha256WithRSAEncryption
         4d:d5:82:03:9f:d5:b0:8c:29:53:90:a5:68:0a:48:53:36:9e:
         db:95:40:ff:4f:04:4e:af:58:b3:ca:e7:96:5d:c5:f3:19:a0:
         00:12:d5:95:5b:90:17:ff:33:d4:6a:ed:40:f5:41:56:59:40:
         c2:84:21:90:a1:36:c9:77:fc:a8:c4:34:66:95:8c:88:ca:8a:
         03:c8:e6:b3:96:1c:da:e0:17:b0:46:dc:01:ec:24:3a:34:13:
         28:ff:47:e5:9f:ff:4a:8d:02:57:95:01:90:f4:9a:17:98:53:
         6e:76:98:0b:2b:6b:5d:54:51:89:c8:26:aa:b7:af:e6:60:9d:
         83:2c:49:24:d8:65:74:be:2b:ad:0f:b9:cf:91:d5:31:ad:06:
         df:77:ef:bf:d5:98:d5:56:38:85:cc:05:70:93:52:9c:e1:17:
         ea:6c:18:ea:25:e4:d0:ef:40:8b:3b:fd:01:33:bb:08:d0:c4:
         54:d7:da:bd:b8:52:ca:e2:c0:11:89:64:ac:6e:25:d3:b4:9a:
         b9:46:d5:12:3a:0e:02:36:be:9a:5c:d5:e1:d7:ec:d6:54:8d:
         4d:da:c5:93:65:d6:ae:50:eb:87:f6:62:8a:18:eb:1c:ab:27:
         e8:57:83:91:f3:77:af:8c:26:4a:b8:7f:fe:fb:3e:ce:cf:ab:
         9e:43:b1:b6
```