michimani.net

ACM での SSL 証明書発行を AWS CLI でスクリプト化する

2020-12-10

AWS Certificate Manager で SSL 証明書を発行する手順は何度も繰り返し実行するものではないため、 IaC に載せずに手作業で作成している方は少なくないと思います。手作業とはいえ毎回マネジメントコンソールでポチポチやるのは面倒なので、 AWS CLI を使ってスクリプト化してみました。

目次

ACM で SSL 証明書を発行するまでの手順

ACM で SSL 証明書 (Certificate) を発行するには、下記の手順を踏むことになります。

  1. ACM で Certificate の発行を リクエスト する
  2. メールまたは DNS で ドメイン認証 する

ドメイン認証にはメールによる認証と DNS による認証があります。 DNS による認証の場合、認証に必要なレコードを DNS に登録します。(DNS として Route 53 を使う場合、マネジメントコンソールからはボタン一つでレコードの登録ができます)

手順としては多くないですが、今回は AWS CLI を使ってシェルスクリプト化することで、より簡潔に Certificate の発行をやってみたいと思います。

前提

前提として、対象のドメインは Route 53 で管理されているものとします。また、ドメイン認証は DNS で行います。

AWS CLI でやってみる

今回操作するサービスは AWS Certificate Manager と Amazon Route 53 です。(ドメイン認証を DNS で行うため)

なので、 AWS CLI のコマンドとしては acmroute53 を使います。スクリプト化する前に、前項で挙げた手順の 1 と 2 をそれぞれ AWS CLI で実行します。

AWS CLI のバージョンは、2020/12/09 時点で最新の 2.1.8 を使います。(v1 の最新は 1.18.192)

$ aws --version
aws-cli/2.1.8 Python/3.7.4 Darwin/19.6.0 exe/x86_64 prompt/off

1. ACM で Certificate の発行をリクエストする

使用するコマンドは acm request-certificate です。

$ aws acm request-certificate help
...
SYNOPSIS
            request-certificate
          --domain-name <value>
          [--validation-method <value>]
          [--subject-alternative-names <value>]
          [--idempotency-token <value>]
          [--domain-validation-options <value>]
          [--options <value>]
          [--certificate-authority-arn <value>]
          [--tags <value>]
          [--cli-input-json | --cli-input-yaml]
          [--generate-cli-skeleton <value>]
...

必須なのは対象のドメイン名を指定する --domain-name ですが、今回は認証方法を指定する --validation-method オプションで DNS による認証であることを指定します。ここを省略するとメールによる認証となります。

今回は Route 53 のホストゾーンとして登録されている michimani.net のサブドメインである curry.michimani.net に対する SSL 証明書を発行してみようと思います。

$ HOSTED_DOMAIN="michimani.net"
$ TARGET_DOMAIN="curry.${HOSTED_DOMAIN}"
$ aws acm request-certificate \
--domain-name ${TARGET_DOMAIN} \
--validation-method DNS

{
    "CertificateArn": "arn:aws:acm:ap-northeast-1:************:certificate/d8009e70-XXXX-XXXX-XXXX-XXXXXXXXXXXX"
}

今回は特にリージョンを指定せずにプロファイルの default のリージョンに対して実行しましたが、発行した Certificate を CloudFornt で使用する場合は バージニア北部 us-east-1 に対して実行する必要があります。

リクエストした Certificate の ARN は以降の手順で使用するので、変数に設定しておきます。(上記のコマンド実行時に、出力を変数に設定してもよいです)

$ CERT_ARN=$( \
  aws acm list-certificates \
  --query "CertificateSummaryList[?DomainName=='${TARGET_DOMAIN}'].CertificateArn" \
  --output text ) \
&& echo ${CERT_ARN}

arn:aws:acm:ap-northeast-1:************:certificate/d8009e70-XXXX-XXXX-XXXX-XXXXXXXXXXXX

2. DNS でドメイン認証する

DNS でドメイン認証するには、認証用のレコード情報が必要になります。マネジメントコンソールで操作する場合はボタン一つで Route 53 に登録することができますが、 AWS CLI だとそうはいきません。認証用のレコード情報は Certificate が持っているので、 acm describe-certificate コマンドで取得します。

$ aws acm describe-certificate \
--certificate-arn ${CERT_ARN}

{
    "Certificate": {
        "CertificateArn": "arn:aws:acm:ap-northeast-1:************:certificate/d8009e70-XXXX-XXXX-XXXX-XXXXXXXXXXXX",
        "DomainName": "curry.michimani.net",
        "SubjectAlternativeNames": [
            "curry.michimani.net"
        ],
        "DomainValidationOptions": [
            {
                "DomainName": "curry.michimani.net",
                "ValidationDomain": "curry.michimani.net",
                "ValidationStatus": "PENDING_VALIDATION",
                "ResourceRecord": {
                    "Name": "_6709bba0b171XXXXXXXXXXXXXXXXXXXX.curry.michimani.net.",
                    "Type": "CNAME",
                    "Value": "_2166df0b8981XXXXXXXXXXXXXXXXXXXX.wggjkglgrm.acm-validations.aws."
                },
                "ValidationMethod": "DNS"
            }
        ],
        "Subject": "CN=curry.michimani.net",
        "Issuer": "Amazon",
        "CreatedAt": "2020-12-09T07:04:37+09:00",
        "Status": "PENDING_VALIDATION",
        "KeyAlgorithm": "RSA-2048",
        "SignatureAlgorithm": "SHA256WITHRSA",
        "InUseBy": [],
        "Type": "AMAZON_ISSUED",
        "KeyUsages": [],
        "ExtendedKeyUsages": [],
        "RenewalEligibility": "INELIGIBLE",
        "Options": {
            "CertificateTransparencyLoggingPreference": "ENABLED"
        }
    }
}

Certificate.DomainValidationOptions[0].ResourceRecord がドメイン認証用のレコード情報です。 Type は CNAME で固定なので、 Name と Value を変数に設定します。一度に 2 つの値を設定する良い方法が思い浮かばないので、 2 回に分けて設定します。なにか良い方法があれば教えて下さい。

$ VALIDATION_RECORD_NAME=$( \
  aws acm describe-certificate \
  --certificate-arn ${CERT_ARN} \
  --query "Certificate.DomainValidationOptions[0].ResourceRecord.Name" \
  --output text) \
&& VALIDATION_RECORD_VALUE=$( \
  aws acm describe-certificate \
  --certificate-arn ${CERT_ARN} \
  --query "Certificate.DomainValidationOptions[0].ResourceRecord.Value" \
  --output text) \
&& echo "
VALIDATION_RECORD_NAME  = ${VALIDATION_RECORD_NAME}
VALIDATION_RECORD_VALUE = ${VALIDATION_RECORD_VALUE}"

VALIDATION_RECORD_NAME  = _6709bba0b171XXXXXXXXXXXXXXXXXXXX.curry.michimani.net.
VALIDATION_RECORD_VALUE = _2166df0b8981XXXXXXXXXXXXXXXXXXXX.wggjkglgrm.acm-validations.aws.

この情報を Route 53 に登録します。その際、ホストゾーンに登録しているドメインの HostedZoneId が必要になるので、 route53 list-hosted-zones コマンドの出力から変数に設定しておきます。

$ HOSTED_ZONE_ID=$( \
  aws route53 list-hosted-zones \
  --query "HostedZones[?Name=='${HOSTED_DOMAIN}.'].Id" \
  --output text) \
&& echo ${HOSTED_ZONE_ID}

/hostedzone/Z39XXXXXXXXXX

Route 53 の RecordSet の作成には route53 change-resource-record-sets コマンドを使用します。

$ route53 change-resource-record-sets help
...
SYNOPSIS
            change-resource-record-sets
          --hosted-zone-id <value>
          --change-batch <value>
          [--cli-input-json | --cli-input-yaml]
          [--generate-cli-skeleton <value>]
...

このコマンドは作成時だけでなく、変更・削除の際にも使用します。どのアクションを実行するかはオプション内の値で指定します。

今回は RecordSet の作成なので、次のように実行します。

$ aws route53 change-resource-record-sets \
--hosted-zone-id ${HOSTED_ZONE_ID} \
--change-batch \
"{
  \"Changes\": [
    {
      \"Action\": \"CREATE\",
      \"ResourceRecordSet\": {
        \"Name\": \"${VALIDATION_RECORD_NAME}\",
        \"Type\": \"CNAME\",
        \"TTL\": 300,
        \"ResourceRecords\": [{\"Value\": \"${VALIDATION_RECORD_VALUE}\"}]
      }
    }
  ]
}"

{
    "ChangeInfo": {
        "Id": "/change/C0057826YCD9M3MBZEJV",
        "Status": "PENDING",
        "SubmittedAt": "2020-12-08T22:33:51.455000+00:00"
    }
}

実行結果として、リソースの変更情報が出力されます。実行直後は Status が PENDING になっていますが、体感としてはすぐに INSYNC (変更完了) になります。一応 route53 get-change コマンドを使ってステータスを確認することはできます。

$ aws route53 get-change \
--id /change/C0057826YCD9M3MBZEJV

{
    "ChangeInfo": {
        "Id": "/change/C0057826YCD9M3MBZEJV",
        "Status": "INSYNC",
        "SubmittedAt": "2020-12-08T22:33:51.455000+00:00"
    }
}

3. ドメイン認証ステータスを確認

最後に、ドメイン認証のステータスを確認します。確認には acm describe-certificate コマンドを使います。先ほどの実行結果でもわかるように Certificate.DomainValidationOptions[0].ValidationStatus にドメイン認証のステータスが含まれているので、その部分だけ出力します。

$ aws acm describe-certificate \
--certificate-arn ${CERT_ARN} \
--query "Certificate.DomainValidationOptions[0].ValidationStatus" \
--output text

SUCCESS

実行タイミングにもよりますが、認証が成功していれば SUCCESS と出力されます。

シェルスクリプト化する

上記でやった手順をスクリプト化します。と言っても、ほとんどコピペしてつなげただけです。

#!/bin/bash

set -e

if [ $# != 3 ] || [ $1 = "" ] || [ $2 = "" ] || [ $3 = "" ]; then
  echo -e "Three parameters are required

  1st - string: Hosted Domain Name on Route 53 (e.g. example.com)
  2nd - string: Domain Name for Certificate (e.g. sub.mexample.com)
  3rd - string: Target Region  (e.g. us-east-1)

  example command
  \t sh ./issue-certificate.sh example.com sub.example.com"
  exit
fi

HOSTED_DOMAIN=$1
TARGET_DOMAIN=$2
REGION=$3
NONE="None"

# request certificate
echo "Request certificate for '${TARGET_DOMAIN}' to ACM."
CERT_ARN=$( \
  aws acm request-certificate \
  --domain-name ${TARGET_DOMAIN} \
  --validation-method DNS \
  --region ${REGION} \
  --output text) \
&& sleep 5 \
&& echo -e "\t CERT_ARN = ${CERT_ARN}"


# create domain validation record set
echo "Create record set to validate domain in Route 53."
VALIDATION_RECORD_NAME=$( \
  aws acm describe-certificate \
  --certificate-arn ${CERT_ARN} \
  --query "Certificate.DomainValidationOptions[0].ResourceRecord.Name" \
  --region ${REGION} \
  --output text) \
&& echo -e "\t VALIDATION_RECORD_NAME = ${VALIDATION_RECORD_NAME}"

VALIDATION_RECORD_VALUE=$( \
  aws acm describe-certificate \
  --certificate-arn ${CERT_ARN} \
  --query "Certificate.DomainValidationOptions[0].ResourceRecord.Value" \
  --region ${REGION} \
  --output text) \
&& echo -e "\t VALIDATION_RECORD_VALUE = ${VALIDATION_RECORD_VALUE}"

HOSTED_ZONE_ID=$( \
  aws route53 list-hosted-zones \
  --query "HostedZones[?Name=='${HOSTED_DOMAIN}.'].Id" \
  --output text) \
&& echo -e "\t HOSTED_ZONE_ID = ${HOSTED_ZONE_ID}"

if [ $VALIDATION_RECORD_NAME == $NONE ] || [ $VALIDATION_RECORD_VALUE == $NONE ] || [ $HOSTED_DOMAIN == $NONE ]; then
   echo "Failed to get the parameters required for domain validation."
   exit
fi

CHANGE_ID=$( \
  aws route53 change-resource-record-sets \
  --hosted-zone-id ${HOSTED_ZONE_ID} \
  --change-batch \
  "{
    \"Changes\": [
      {
        \"Action\": \"CREATE\",
        \"ResourceRecordSet\": {
          \"Name\": \"${VALIDATION_RECORD_NAME}\",
          \"Type\": \"CNAME\",
          \"TTL\": 300,
          \"ResourceRecords\": [{\"Value\": \"${VALIDATION_RECORD_VALUE}\"}]
        }
      }
    ]
  }" \
  --query "ChangeInfo.Id" \
  --output text) \
&& echo -e "\t Change ID : ${CHANGE_ID}\n"

if [ $? == 0 ]; then
  echo -e "\nFinished to request certificate and create record set to validate domain.
Please run command bellow to check validation status.

  aws acm describe-certificate \\
  --certificate-arn ${CERT_ARN} \\
  --query \"Certificate.DomainValidationOptions[0].ValidationStatus\" \\
  --region ${REGION} \\
  --output text"
else
  echo -e "\nFailed to issue certificate."
fi

最初の Certificate のリクエストのあとに sleep を入れている理由は、リクエスト直後だと acm describe-certificate コマンドで Certificate の情報を取得できない可能性があるからです。その場合、ドメイン認証用の Name または Value が None となり、 route53 change-resource-record-sets コマンドを実行時に下記のようなエラーとなり失敗してしまいます。

An error occurred (InvalidChangeBatch) when calling the ChangeResourceRecordSets operation: [RRSet with DNS name none. is not permitted in zone michimani.net.]

実行してみる

このシェルスクリプトでは、実行時の引数として 3 つの値を指定します。

上記の例の値で実行してみます。

$ ./issue-certificate.sh michimani.net sub.michimani.net ap-northeast-1
Request certificate for 'sub.michimani.net' to ACM.
	 CERT_ARN = arn:aws:acm:ap-northeast-1:************:certificate/ac2c1f33-96ea-XXXX-XXXX-XXXXXXXXXX
Create record set to validate domain in Route 53.
	 VALIDATION_RECORD_NAME = _d82165d09e36XXXXXXXXXXXXXXXXXXXX.sub.michimani.net.
	 VALIDATION_RECORD_VALUE = _e1a9a5b806b2XXXXXXXXXXXXXXXXXXXX.wggjkglgrm.acm-validations.aws.
	 HOSTED_ZONE_ID = /hostedzone/Z39XXXXXXXXXX
	 Change ID : /change/C06129382Z4LWAZ4JDF96


Finished to request certificate and create record set to validate domain.
Please run command bellow to check validation status.

  aws acm describe-certificate \
  --certificate-arn arn:aws:acm:ap-northeast-1:************:certificate/ac2c1f33-96ea-XXXX-XXXX-XXXXXXXXXX \
  --query "Certificate.DomainValidationOptions[0].ValidationStatus" \
  --region ap-northeast-1 \
  --output text

最後に Certificate の認証ステータスを確認するコマンドが出力されるので、それを実行してステータスを確認します。

$ aws acm describe-certificate \
--certificate-arn arn:aws:acm:ap-northeast-1:************:certificate/ac2c1f33-96ea-XXXX-XXXX-XXXXXXXXXX \
--query "Certificate.DomainValidationOptions[0].ValidationStatus" \
--region ap-northeast-1 \
--output text

SUCCESS

まとめ

なんとなく手作業で作成している ACM の Certificate を AWS CLI でスクリプト化してサクッと作成できるようにしてみた話でした。

各プロダクト・サービスごとに一回しかやらない作業とは言え、スクリプト化されているとかなり楽です。AWS CLI を使えば、各 API を組み合わせて一連の操作をスクリプト化することにより、独自のコマンドのようなものが簡単に作成できます。特に、今回作成したような、コードで管理するほどでもないリソースを作成するような場面で AWS CLI は活きるなと思いました。

シェルスクリプトがうまく動かないとか、こうしたほうがいいとか、ご意見あれば下記の gist にコメントを頂けると幸いです。


comments powered by Disqus