LoginSignup
0
0

AWS Certificate Manager でサーバー証明(ELB使用)&aws-cli

Last updated at Posted at 2024-05-28

構成

今回は、AWS Certification Manager(ACM)を使用してWebサーバーの証明書を作成してみます。
構成は以下の通り。ALBを使用するパターンで考え得る限り最もチープな構成で構築しています。

image.png

  • プロバイダで独自ドメインを取得する(example.com)
  • Route 53にホストゾーンを作成する(プロバイダで取得したexample.comドメイン)
  • プロバイダのドメイン設定で、DNSサーバーにRoute 53のホストゾーンのDNSサーバーを設定する
  • ACMにサーバー証明書(www.example.com)を作成する
  • EC2インスタンスにWebサーバーを構築する
  • ALBを作成し、ターゲットグループにEC2インスタンスを登録(ALBの作成時に最低2つのアベイラビリティゾーンをマッピングする必要があるが、片方にEC2があればとりあえず動作する)
  • ALBのリスナーはHTTPSとし、ACMのサーバー証明書を設定する
  • Route 53のホストゾーンにwww.example.comのレコードを作成し、ルーティング先をALBのDNS名とする(Route53の設定画面ではALBへのエイリアスとして設定する)

独自ドメインの取得

インターネットプロバイダ等でドメインを取得します。
ここでは、example.comを取得したと仮定します。(実際にはexample.comは取得できないはずです)

Route53のホストゾーン作成

awsのRoute 53サービスを開き、ホストゾーンを作成します。
ここで、先に取得したドメイン(example.com)のホストゾーンを作成します。

image.png

作成したホストゾーンのNSレコードを確認します。awsのDNSサーバーが登録されています。

image.png

プロバイダにAWSのネームサーバーを登録

Route53に生成されたホストゾーンのNSレコードにあるDNSサーバーを、プロバイダに登録します。
以下の画面は お名前.com のネームサーバー設定画面ですが、他社でも同じような設定項目があるでしょう。

スクリーンショット 2024-05-28 22.15.30.png

Webサーバーの構築

ここでは簡単にするため、EC2インスタンス起動時のユーザーデータを使用してWebサーバーを構築します。
ユーザーデータは、サーバー起動時に実行されるスクリプトで、ここにapacheのインストールとコンテンツの作成等を記述しておけば、起動後すぐにWebサーバーとして稼働します。オートスケーリングの場合、s3ストレージ上にコンテンツを置いておいてここにダウンロードのスクリプトを書いたりします。

Amazon Linux のAMIを選択

スクリーンショット 2024-05-27 19.59.50.png

インスタンスタイプを選択

スクリーンショット 2024-05-27 20.00.14.png

キーペアは無しでも大丈夫

スクリーンショット 2024-05-27 20.00.31.png

セキュリティグループはHTTPを許可するように

スクリーンショット 2024-05-27 20.01.07.png

ユーザーデータを入力するため「高度な詳細」を展開

スクリーンショット 2024-05-27 20.01.42.png

ユーザーデータを入力する

apacheのインストールとindex.htmlの生成をサーバー起動時に行います。
スクリーンショット 2024-05-27 20.28.16.png

Certificate Managerでサーバー証明書作成

ようやく本題ですが、サーバー証明書を作成します。
サーバー名は独自ドメインとして取得したexample.comに属するFQDNとします。

証明書をリクエスト

スクリーンショット 2024-05-27 21.05.19.png

パブリック証明書をリクエスト

スクリーンショット 2024-05-27 21.06.45.png

FQDNは自ドメインのサブドメイン

スクリーンショット 2024-05-27 21.07.26.png

検証方法と暗号アルゴリズムの指定

スクリーンショット 2024-05-27 21.07.59.png

サーバーの検証

実はここの仕組みがまだよく理解できていないのですが、取得した証明書の検証を行わないと有効になりません。前段でDNS検証を選択しているので、Route 53のホストゾーンにCNAMEレコードを置けばOKです。
CNAMEレコードは、「Route 53でレコードを作成」というボタンをクリックすると作成できます。
スクリーンショット 2024-05-27 21.11.27.png
スクリーンショット 2024-05-27 21.16.51.png

追加されたCNAMEレコード

スクリーンショット 2024-05-27 21.21.01.png

ロードバランサー(ALB)の作成

Application load balancerを作成します。
ここで、リスナータイプをHTTPSとしてサーバー証明書をこの前に作成したACMの証明書にします。
セキュリティグループはあらかじめ作成しておきました。anywhereからのhttpsを許可しています。
スクリーンショット 2024-05-27 21.27.10.png
スクリーンショット 2024-05-27 21.27.33.png
スクリーンショット 2024-05-27 21.28.11.png

ネットワークマッピングで二つのサブネットにチェックを入れる

スクリーンショット 2024-05-27 21.28.59.png

HTTPSを許可するセキュリティグループをあらかじめ作っておいてあります(なければ「新しいセキュリティグループを作成」をクリックして作る)

スクリーンショット 2024-05-27 21.32.49.png

リスナーをHTTPSにして「ターゲットグループの作成」をクリック

スクリーンショット 2024-05-27 21.33.39.png

ターゲットグループの作成

本来は2つのAZの両方にインスタンスが起動されている(またはAutoscalingで2つ以上のインスタンスを起動する)べきですが、固定で起動している1インスタンスをターゲットグループに登録します。
スクリーンショット 2024-05-27 21.35.28.png
プロトコルがHTTPSになっているのでHTTPに変更します。
スクリーンショット 2024-05-27 22.01.31.png

スクリーンショット 2024-05-27 21.36.56.png
スクリーンショット 2024-05-27 21.37.56.png

ALBにターゲットグループを設定

ターゲットグループの作成が完了したら、ALBの作成ウィザードに戻ってターゲットグループ更新ボタン(右端にある丸矢印)をクリックし、リストからターゲットグループを選択します。
スクリーンショット 2024-05-27 21.39.37.png

ACMの証明書を選択

セキュアリスナーの設定で、ACMの証明書を選択します。
スクリーンショット 2024-05-27 21.42.44.png
スクリーンショット 2024-05-27 21.49.46.png

Route53のレコード作成

ホストゾーンにwww.example.comのAレコードを作成します。このホスト名はACMのサーバー証明書と合わせる必要があります。
スクリーンショット 2024-05-27 21.51.50.png

確認

以上でwebサーバの起動、ロードバランサ、DNSレコードの設定、サーバー証明書の設定ができました。
https://www.example.comにアクセスすると、Amazonに署名されたサーバー証明書が使用されていることが確認できます。
スクリーンショット 2024-05-27 22.27.15.png
スクリーンショット 2024-05-27 22.29.09.png

aws-cliによるバッチ処理化

ここまでの流れをaws-cliでやってみます。

ユーザーデータ用シェル

userdata.webserver.sh
#!/bin/bash

sudo yum -y install httpd
sudo yum -y update
sudo systemctl enable httpd
sudo systemctl start httpd

cat << EOF > /var/www/html/index.html
<!DOCTYPE html>
<html>
<head>
    <title>My Website</title>
</head>
<body>
    <h1>Welcome to my website</h1>
    <p>Content goes here</p>
</body>
</html>
EOF

リソース生成・起動シェル

だいぶ長くなってしまいましたが、インスタンス起動、証明書生成・有効化、ALB起動、DNS登録までやっています。

up.sh
#!/bin/bash

####################################
# VPC
VPCID=vpc-1a2b3c4d
# インスタンスを起動するサブネット
SUBNETID=subnet-abcd1234
# ロードバランシングするサブネット
SUBNETS="subnet-abcd1234 subnet-1234aaaa"
# インスタンス定義
INSTANCENAME=MyWebServer
USERDATA=userdata.webserver.sh
INSTANCETYPE=t2.micro
AMI=ami-02a405b3302affc24       # Amazon Linux 2023
# セキュリティグループ名
SGHTTP=sgHTTP
SGHTTPS=sgHTTPS

# サーバー証明書関連
FQDN=www.example.com
HOSTZONE=ABCDEFG12345678

# ロードバランサ関連定義
ALBNAME=exampleALB
TGNAME=exampleTG
ALBHOSTZONE=Z14GRHDCWA56QT      # ALBのDNSが登録されているホストゾーン(リージ ョンごとに決まっている)
####################################

# セキュリティグループを作成する
SG_ID=$(aws ec2 create-security-group \
        --group-name $SGHTTP \
        --description "allow HTTP" \
        --vpc-id $VPCID \
        --query 'GroupId' \
        --output text)
aws ec2 authorize-security-group-ingress \
        --protocol tcp \
        --port 80 \
        --cidr 0.0.0.0/0 \
        --group-id $SG_ID --no-cli-pager
SGHTTPS_ID=$(aws ec2 create-security-group \
        --group-name $SGHTTPS \
        --description "allow HTTPS" \
        --vpc-id $VPCID \
        --query 'GroupId' \
        --output text)
aws ec2 authorize-security-group-ingress \
        --protocol tcp \
        --port 443 \
        --cidr 0.0.0.0/0 \
        --group-id $SGHTTPS_ID \
        --no-cli-pager

# EC2インスタンスを起動する
INSTANCEID=$(aws ec2 run-instances \
        --tag-specifications "ResourceType=instance,Tags=[{Key=Name,Value=$INSTANCENAME}]" \
        --image-id $AMI \
        --count 1 \
        --instance-type $INSTANCETYPE \
        --subnet-id $SUBNETID \
        --security-group-ids $SG_ID \
        --user-data file://$USERDATA \
        --query 'Instances[0].InstanceId' \
        --output text)

count=0
while [ $count -lt 20 ]; do
        # インスタンスの状態を取得する
        STATUS=$(aws ec2 describe-instance-status \
                --instance-id ${INSTANCEID} \
                --query 'InstanceStatuses[0].InstanceState.Name' \
                --output text)
        if [ "${STATUS}" = "running" ]; then
                break;
        fi
        echo "インスタンス(${INSTANCEID})状態:${STATUS}"
        sleep 10
        count=$((count+1))
done

echo "インスタンス ${INSTANCEID} を起動しました。"

# Amazon Certificate Manager(ACM)に証明書をリクエストする
CERTARN=$(aws acm request-certificate \
        --domain-name $FQDN \
        --validation-method DNS \
        --query 'CertificateArn' \
        --output text)

echo "証明書を生成しました:${CERTARN}"

# 証明書を作成してから検証用の情報が生成されるまで少し時間がかかる

count=0

while [ $count -lt 10 ]; do
  # 証明書の検証用のCNAME値
  VALID_NAME=$(aws acm describe-certificate \
          --certificate-arn $CERTARN \
          --query 'Certificate.DomainValidationOptions[0].[ResourceRecord.Name]' \
          --output text)
  VALID_VALUE=$(aws acm describe-certificate \
          --certificate-arn $CERTARN \
          --query 'Certificate.DomainValidationOptions[0].[ResourceRecord.Value]' \
          --output text)
  if [ $VALID_NAME != "None" ] && [ $VALID_VALUE != "None" ]; then
    break;
  fi
  echo "VALID_NAME=$VALID_NAME , VALID_VALUE=$VALID_VALUE"
  count=$((count+1))
done

# Route53 ホストゾーンにCNAMEレコードを作成する
aws route53 change-resource-record-sets \
        --no-cli-pager \
        --hosted-zone-id $HOSTZONE \
        --change-batch "
{
  \"Changes\": [
    {
      \"Action\": \"UPSERT\",
      \"ResourceRecordSet\": {
        \"Name\": \"$VALID_NAME\",
        \"Type\": \"CNAME\",
        \"TTL\":300,
        \"ResourceRecords\": [ 
          {
            \"Value\": \"$VALID_VALUE\"
          } 
        ]
      }
    }
  ]
}"

# 検証が終わるまで少し待つ
count=0
while [ $count -lt 20 ]; do
        CERTSTATUS=$(aws acm describe-certificate \
                --certificate-arn $CERTARN \
                --query 'Certificate.Status' \
                --output text)

        if [ "${CERTSTATUS}" = "ISSUED" ]; then
                echo "証明書の発行が完了しました。"
                break
        fi
        echo "証明書ステータス:${CERTSTATUS}"
        sleep 10
        count=$((count+1))
done

# TargetGroupを作成する
TGARN=$(aws elbv2 create-target-group \
        --name $TGNAME \
        --protocol HTTP \
        --port 80 \
        --vpc-id $VPCID \
        --query 'TargetGroups[0].TargetGroupArn' \
        --output text)
# EC2インスタンスをターゲットとして割り当てる
aws elbv2 register-targets \
        --target-group-arn $TGARN \
        --targets Id=$INSTANCEID

echo "ターゲットグループを生成しました:${TGARN}"

# ALBを生成する
ALBARN=`aws elbv2 create-load-balancer \
        --name $ALBNAME \
        --subnets $SUBNETS \
        --security-groups $SGHTTPS_ID \
        --scheme internet-facing \
        --type application \
        --ip-address-type ipv4 \
        --query 'LoadBalancers[0].LoadBalancerArn' \
        --output text`

# ALBにリスナーとターゲットグループを割り当てる
aws elbv2 create-listener \
        --load-balancer-arn $ALBARN \
        --protocol HTTPS --port 443  \
        --ssl-policy ELBSecurityPolicy-2016-08 \
        --certificates CertificateArn=$CERTARN \
        --default-actions Type=forward,TargetGroupArn=$TGARN \
        --no-cli-pager

echo "ロードバランサを生成しました:${ALBARN}"

# ALBのDNS名を取得
ALBDNS=`aws elbv2 describe-load-balancers \
        --names $ALBNAME \
        --query 'LoadBalancers[0].DNSName' \
        --output text`

# Route53にレコードを追加する
aws route53 change-resource-record-sets \
        --no-cli-pager \
        --hosted-zone-id $HOSTZONE \
        --change-batch "
{
  \"Changes\": [
    {
      \"Action\": \"UPSERT\",
      \"ResourceRecordSet\": {
        \"Name\": \"$FQDN\",
        \"Type\": \"A\",
        \"AliasTarget\": {
          \"HostedZoneId\": \"$ALBHOSTZONE\",
          \"DNSName\": \"$ALBDNS\",
          \"EvaluateTargetHealth\": false
        }
      }
    }
  ]
}"

# ロードバランサの状態を待つ
count=0
while [ $count -lt 20 ]; do
        ALBSTATUS=$(aws elbv2 describe-load-balancers \
                --names $ALBNAME \
                --query 'LoadBalancers[0].State.Code' \
                --output text)
        TGSTATUS=$(aws elbv2 describe-target-health \
                --target-group-arn $TGARN \
                --output text)

        echo "ロードバランサ:${ALBSTATUS}"
        echo "ターゲットグループ:${TGSTATUS}"

        if [ ${ALBSTATUS} = "active" ] ; then
                echo "起動完了"
                break
        fi
        sleep 10
        count=$((count+1))
done
exit 0

リソース削除シェル

これも作っておかないと、リソース削除を忘れてしまうので。

down.sh
#!/bin/bash

####################################
# VPC
VPCID=vpc-1a2b3c4d
# インスタンスを起動するサブネット
SUBNETID=subnet-abcd1234
# ロードバランシングするサブネット
SUBNETS="subnet-abcd1234 subnet-1234aaaa"
# インスタンス定義
INSTANCENAME=MyWebServer
USERDATA=userdata.webserver.sh
INSTANCETYPE=t2.micro
AMI=ami-02a405b3302affc24       # Amazon Linux 2023
# セキュリティグループ名
SGHTTP=sgHTTP
SGHTTPS=sgHTTPS

# サーバー証明書関連
FQDN=www.example.com
HOSTZONE=ABCDEFG12345678

# ロードバランサ関連定義
ALBNAME=exampleALB
TGNAME=exampleTG
ALBHOSTZONE=Z14GRHDCWA56QT      # ALBのDNSが登録されているホストゾーン(リージ ョンごとに決まっている)
####################################
# EC2のインスタンスIDを取得
INSTANCE_ID=$(aws ec2 describe-instances \
        --filters "Name=tag:Name,Values=$INSTANCENAME" \
        --query 'Reservations[*].Instances[*].InstanceId' \
        --output text)

if [ -z "${INSTANCE_ID}" ] || [ "${INSTANCE_ID}" = "None" ] ; then
        echo "EC2がありません。"
else
        # EC2インスタンスを終了
        aws ec2 terminate-instances --instance-ids ${INSTANCE_ID} \
                --no-cli-pager
        count=0
        while [ $count -lt 20 ]; do
                STATUS=$(aws ec2 describe-instance-status \
                        --instance-id ${INSTANCE_ID} \
                        --query 'InstanceStatuses[0].InstanceState.Name' \
                        --output text)
                if [ "${STATUS}" = "terminated" ] || [ "${STATUS}" = "None" ]; then
                        echo "EC2インスタンス${INSTANCE_ID}を終了しました。"
                        break;
                fi
                echo "インスタンス状態:${STATUS}"
                count=$((count+1))
                sleep 10
        done
fi

sleep 10

# ALBのDNS名を取得
ALBDNS=`aws elbv2 describe-load-balancers \
        --names $ALBNAME \
        --query 'LoadBalancers[0].DNSName' \
        --output text`

if [ -z "${ALBDNS}" ] || [ "${ALBDNS}" = "None" ] ; then
        echo "ALBがありません。"
else

        # Webサーバー(ALB)用DNSレコードを削除
        aws route53 change-resource-record-sets \
                --no-cli-pager \
                --hosted-zone-id $HOSTZONE \
                --change-batch "
{
  \"Changes\": [
    {
      \"Action\": \"DELETE\",
      \"ResourceRecordSet\": {
        \"Name\": \"$FQDN\",
        \"Type\": \"A\",
        \"AliasTarget\": {
          \"HostedZoneId\": \"$ALBHOSTZONE\",
          \"DNSName\": \"$ALBDNS\",
          \"EvaluateTargetHealth\": false
        }
      }
    }
  ]
}"

        # ALBを削除する

        # ALBのARNを取得
        ALBARN=$(aws elbv2 describe-load-balancers \
                --names $ALBNAME \
                --query 'LoadBalancers[0].LoadBalancerArn' \
                --output text)

        # ALBに関連付けられているリスナーのARNを取得
        LISTENERARN=$(aws elbv2 describe-listeners \
                --load-balancer-arn $ALBARN \
                --query 'Listeners[0].ListenerArn' \
                --output text)

        # リスナーを削除
        aws elbv2 delete-listener --listener-arn $LISTENERARN

        # TargetGroupのARNを取得
        TGARN=$(aws elbv2 describe-target-groups \
                --names $TGNAME \
                --query 'TargetGroups[0].TargetGroupArn' \
                --output text)

        # ターゲットグループを削除
        aws elbv2 delete-target-group --target-group-arn $TGARN

        # ALBを削除
        aws elbv2 delete-load-balancer --load-balancer-arn $ALBARN
        echo "ロードバランサー${ALBNAME}を削除しました"
fi

sleep 10
 
# ACMの証明書のARNを取得
CERTARN=$(aws acm list-certificates \
        --query 'CertificateSummaryList[?DomainName==`'$FQDN'`].CertificateArn' \
        --output text)
# 複数ある場合は先頭のを対象とする
CERTARN=$(echo $CERTARN | cut -d ' ' -f 1)

if [ -z "${CERTARN}" ] || [ "${CERTARN}" = "None" ]; then
        echo "証明書がありません。"
else

        echo "CERTARN=${CERTARN}"

        # 証明書の検証用のCNAME値
        VALID_NAME=$(aws acm describe-certificate \
                --certificate-arn $CERTARN \
                --query 'Certificate.DomainValidationOptions[0].[ResourceRecord.Name]' \
                --output text)
        VALID_VALUE=$(aws acm describe-certificate \
                --certificate-arn $CERTARN \
                --query 'Certificate.DomainValidationOptions[0].[ResourceRecord.Value]' \
                --output text)

        # Route53のレコード削除
        # 証明書検証用
        aws route53 change-resource-record-sets \
                --no-cli-pager \
                --hosted-zone-id $HOSTZONE \
                --change-batch "
        {
                \"Changes\": [{
                        \"Action\": \"DELETE\",
                        \"ResourceRecordSet\": {
                                \"Name\": \"$VALID_NAME\",
                                \"Type\": \"CNAME\",
                                \"TTL\":300,
                                \"ResourceRecords\": [ {
                                        \"Value\": \"$VALID_VALUE\"
                                } ]
                        }
                }]
        }"
        # ACMの証明書を削除
        aws acm delete-certificate --certificate-arn $CERTARN

        echo "${FQDN}の証明書を削除しました。"

fi

# セキュリティグループを削除できるようになるには少し時間がかかる
sleep 30

# セキュリティグループIDを取得する
SG_ID=$(aws ec2 describe-security-groups \
        --filters Name=group-name,Values=$SGHTTP \
        --query 'SecurityGroups[0].GroupId' \
        --output text)
if [ -z ${SG_ID} ] || [ ${SG_ID} = "None" ]; then
        echo "セキュリティグループ ${SGHTTP} がありません。"
else
        aws ec2 delete-security-group --group-id $SG_ID
        echo "セキュリティグループ${SGHTTP}を削除しました。"
fi

SGHTTPS_ID=$(aws ec2 describe-security-groups \
        --filters Name=group-name,Values=$SGHTTPS \
        --query 'SecurityGroups[0].GroupId' \
        --output text)
if [ -z ${SGHTTPS_ID} ] || [ ${SGHTTPS_ID} = "None" ]; then
        echo "セキュリティグループ ${SGHTTPS} がありません。"
else
        aws ec2 delete-security-group --group-id $SGHTTPS_ID
        echo "セキュリティグループ${SGHTTPS}を削除しました。"
fi

まとめ

もともと、Amazon Certificate Manager でサーバー証明書を作成しようと思い立ったのがきっかけで始めたのですが、思ったより手順が多くて大変でした。
CLIで何とか生成・削除ができることはわかったのですが、実用面を考えると Cloud Formation 等を使った方が良さそうです。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0