🤖

TerraformでLet's EncryptのDNS challenge してみる for AWS

2021/04/04に公開
1

https://registry.terraform.io/providers/vancluever/acme/latest/docs/resources/certificate

完成形をお求めの方

certbotとか使ってもできるんですが、なんとなく terraform にしてみました。

ただ、このプロバイダはちょっと古いのかサンプルのままだと動きません。
↓のような感じに修正して無事動きました。
詳細は別途説明しますが、とりあえず動かしたい人は以下のterraformを参考にしてください。v0.14.8 使用。

terraform {
  required_providers {
    acme = {
      source  = "vancluever/acme"
    }
  }
}

provider "acme" {
  server_url = "https://acme-staging-v02.api.letsencrypt.org/directory"
}

resource "tls_private_key" "private_key" {
  algorithm = "RSA"
}

resource "acme_registration" "reg" {
  account_key_pem = tls_private_key.private_key.private_key_pem
  email_address   = "nekoneko@example.com"
}

resource "acme_certificate" "certificate" {
  account_key_pem           = acme_registration.reg.account_key_pem
  common_name               = "hogehoge.example.com"
  subject_alternative_names = ["example.com"]

  dns_challenge {
    provider = "route53"
    config = {
      AWS_ACCESS_KEY_ID     = "YOUR_KEY"
      AWS_SECRET_ACCESS_KEY = "YOUR_SECRET_KEY"
    }
  }
}

server_url はステージングのものなので、本番用でしたい場合は、 https://acme-v02.api.letsencrypt.org/directory で。

ハマった部分を解説

サンプルのまま terraform v0.14.8 で terraform init をやってみたところ

# terraform init

Initializing the backend...

Initializing provider plugins...
- Finding latest version of hashicorp/tls...
- Finding latest version of hashicorp/acme...
- Installing hashicorp/tls v3.1.0...
- Installed hashicorp/tls v3.1.0 (signed by HashiCorp)

Error: Failed to query available provider packages

Could not retrieve the list of available versions for provider hashicorp/acme:
provider registry registry.terraform.io does not have a provider named
registry.terraform.io/hashicorp/acme

If you have just upgraded directly from Terraform v0.12 to Terraform v0.14
then please upgrade to Terraform v0.13 first and follow the upgrade guide for
that release, which might help you address this problem.

Did you intend to use vancluever/acme? If so, you must specify that source
address in each module which requires that provider. To see which modules are
currently depending on hashicorp/acme, run the following command:
    terraform providers

そういえば、0.13 で プロバイダの呼び方周りに変更が入ってて、たぶんそれについてふれられています。
https://www.terraform.io/upgrade-guides/0-13.html#explicit-provider-source-locations

サンプルを見る限り、最新のフォーマットに対応していないことは明らかだったので
account_key_pem = "${acme_registration.reg.account_key_pem}" のところとか)、0.12くらいでもう一度やってみる。

0.12.30

terraform init

Initializing the backend...

Initializing provider plugins...
- Checking for available provider plugins...
- Downloading plugin for provider "acme" (terraform-providers/acme) 2.3.0...
- Downloading plugin for provider "tls" (hashicorp/tls) 3.1.0...

The following providers do not have any version constraints in configuration,
so the latest version was installed.

To prevent automatic upgrades to new major versions that may contain breaking
changes, it is recommended to add version = "..." constraints to the
corresponding provider blocks in configuration, with the constraint strings
suggested below.

* provider.acme: version = "~> 2.3"
* provider.tls: version = "~> 3.1"


Warning: registry.terraform.io: For users on Terraform 0.13 or greater, this provider has moved to vancluever/acme. Please update your source in required_providers.


Terraform has been successfully initialized!

You may now begin working with Terraform. Try running "terraform plan" to see
any changes that are required for your infrastructure. All Terraform commands
should now work.

If you ever set or change modules or backend configuration for Terraform,
rerun this command to reinitialize your working directory. If you forget, other
commands will detect it and remind you to do so if necessary.

For users on Terraform 0.13 or greater, this provider has moved to vancluever/acme. ほうほうほう。なるほど。これ読んで完全に理解しました。

つまり、

terraform {
  required_providers {
    acme = {
      source  = "vancluever/acme"
    }
  }
}

のように書けば 0.13以降でもおkということか。

では、AWS側でIAMユーザを作って…

Route53のFullAccess 付けるのもどうかと思うんでちゃんと設定したいなと思ったけど、何が必要かわからないのでちょっとずつ実行してみるw

Error: error creating certificate: error: one or more domains had a problem:
[hogehoge.example.com] [hogehoge.example.com] acme: error presenting token: 2 errors occurred:
        * route53: AccessDenied: User: arn:aws:iam::905502778046:user/hogehoge is not authorized to perform: 
route53:ListResourceRecordSets 
Error: error creating certificate: error: one or more domains had a problem:
[hogehoge.example.com] [hogehoge.example.com] acme: error presenting token: 2 errors occurred:
        * route53: failed to determine hosted zone ID: AccessDenied: User: arn:aws:iam::905502778046:user/hogehoge is not authorized to perform:
route53:ListHostedZonesByName

みたいに表示されたので、 route53:ListResourceRecordSetroute53:ListHostedZonesByName

しかし、その後もなんかうまくいかず…

$ TF_LOG=DEBUG terraform apply

してデバッグ情報出してみたところ

2021-03-21T18:06:53.171Z [INFO]  plugin.terraform-provider-acme_v2.3.0: 2021/03/21 18:06:53 [DEBUG] lego: [hogehoge.example.com] acme: cleaning up failed: 2 errors occurred:
        * route53: time limit exceeded: last error: failed to query change status: AccessDenied: User: arn:aws:iam::905502778046:user/hogehoge is not authorized to perform: route53:GetChange

route53:GetChange も必要なことがわかりました。

今見てみたらこうなってたんだけど、 ChangeResourceRecordSets はどこから出てきたw
(必要です)

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "VisualEditor0",
            "Effect": "Allow",
            "Action": [
                "route53:GetChange",
                "route53:ChangeResourceRecordSets",
                "route53:ListResourceRecordSets",
                "route53:ListHostedZonesByName"
            ],
            "Resource": "*"
        }
    ]
}

nginx に読み込ませる前準備

最終的にはこれで作ったのを nginx で読ませたかったので出力。

output "full_cert" {
  value = "${acme_certificate.certificate.certificate_pem}${acme_certificate.certificate.issuer_pem}"
}

output "prikey" {
  value = acme_certificate.certificate.private_key_pem
}

中間証明書は別にしてもいいけど面倒なんでくっつける派。

ただ、これを output すると

$ terraform output full_cert
<<EOT
-----BEGIN CERTIFICATE-----
(snip)
-----END CERTIFICATE-----

EOT

となるので、

$ terraform output -json full_cert | jq -r

としてやるといい感じに。
改行のない文字列も "STRING" のようにダブルクォーテーションがついてしまうので、-json つけて jq してやるといいかと思います。

ただ、これそのまま $ terraform output -json full_cert | jq -r > FILENAME ってするとエラーになるので

$ terraform output -json full_cert | jq . -r > FILENAME

としました。

file provider 使ってもいいけど、作ったあとに取り出して何か別途するっていうのはありそうなんで、output にしました。

outdir=/etc/nginx/ssl/files
date=$(date +%Y%m%d)
terraform output -json full_cert | jq . -r > ${outdir}/${date}_cert_full.pem
terraform output -json prikey | jq . -r > ${outdir}/${date}_private.key

ln -snf ${outdir}/${date}_cert_full.pem /etc/nginx/ssl/live/cert_full.pem
ln -snf ${outdir}/${date}_private.key /etc/nginx/ssl/live/privete_key.pem

あとは、こんな感じでてきとーにスクリプト書いといて、更新があったら実行。
期限が切れる一ヶ月前くらいから terraform plan で差分出るのかな?
その辺検証できてないので、できたら追記します。

2021年6月14日追記

おお、差分が出ていますので更新!

# terraform plan
tls_private_key.private_key: Refreshing state... [id=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX]
acme_registration.reg: Refreshing state... [id=https://acme-v02.api.letsencrypt.org/acme/acct/XXXXXXXXXX]
acme_certificate.certificate: Refreshing state... [id=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX]

An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
  ~ update in-place

Terraform will perform the following actions:

  # acme_certificate.certificate will be updated in-place
  ~ resource "acme_certificate" "certificate" {
      ~ certificate_domain           = "hogehoge.example.com" -> (known after apply)
      ~ certificate_p12              = (sensitive value)
      ~ certificate_pem              = <<-EOT
(snip)

2021年6月30日追記
v0.14.9だかv0.14.10あたりでセンシティヴな内容は sensitive=true しないとエラーになるようになりました。このままだoutputがちゃんとと動きません。v0.14.8を使うか、べつの方法で取り出してください。

Discussion

yu_s_1985yu_s_1985

certbot以外のものを使って証明書更新を行う手段を検討していたので参考になりました。
更新が必要なタイミングにplanを実行すると差分が出るというのが良いですね。

ドキュメントを読むと、必要な権限は書いてくれています。
https://registry.terraform.io/providers/vancluever/acme/latest/docs/guides/dns-providers-route53

{
   "Version": "2012-10-17",
   "Statement": [
       {
           "Sid": "",
           "Effect": "Allow",
           "Action": [
               "route53:GetChange",
               "route53:ChangeResourceRecordSets",
               "route53:ListResourceRecordSets"
           ],
           "Resource": [
               "arn:aws:route53:::hostedzone/*",
               "arn:aws:route53:::change/*"
           ]
       },
       {
           "Sid": "",
           "Effect": "Allow",
           "Action": "route53:ListHostedZonesByName",
           "Resource": "*"
       }
   ]
}