構成
今回は、AWS Certification Manager(ACM)を使用してWebサーバーの証明書を作成してみます。
構成は以下の通り。ALBを使用するパターンで考え得る限り最もチープな構成で構築しています。
- プロバイダで独自ドメインを取得する(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)のホストゾーンを作成します。
作成したホストゾーンのNSレコードを確認します。awsのDNSサーバーが登録されています。
プロバイダにAWSのネームサーバーを登録
Route53に生成されたホストゾーンのNSレコードにあるDNSサーバーを、プロバイダに登録します。
以下の画面は お名前.com のネームサーバー設定画面ですが、他社でも同じような設定項目があるでしょう。
Webサーバーの構築
ここでは簡単にするため、EC2インスタンス起動時のユーザーデータを使用してWebサーバーを構築します。
ユーザーデータは、サーバー起動時に実行されるスクリプトで、ここにapacheのインストールとコンテンツの作成等を記述しておけば、起動後すぐにWebサーバーとして稼働します。オートスケーリングの場合、s3ストレージ上にコンテンツを置いておいてここにダウンロードのスクリプトを書いたりします。
Amazon Linux のAMIを選択
インスタンスタイプを選択
キーペアは無しでも大丈夫
セキュリティグループはHTTPを許可するように
ユーザーデータを入力するため「高度な詳細」を展開
ユーザーデータを入力する
apacheのインストールとindex.htmlの生成をサーバー起動時に行います。
Certificate Managerでサーバー証明書作成
ようやく本題ですが、サーバー証明書を作成します。
サーバー名は独自ドメインとして取得したexample.comに属するFQDNとします。
証明書をリクエスト
パブリック証明書をリクエスト
FQDNは自ドメインのサブドメイン
検証方法と暗号アルゴリズムの指定
サーバーの検証
実はここの仕組みがまだよく理解できていないのですが、取得した証明書の検証を行わないと有効になりません。前段でDNS検証を選択しているので、Route 53のホストゾーンにCNAMEレコードを置けばOKです。
CNAMEレコードは、「Route 53でレコードを作成」というボタンをクリックすると作成できます。
追加されたCNAMEレコード
ロードバランサー(ALB)の作成
Application load balancerを作成します。
ここで、リスナータイプをHTTPSとしてサーバー証明書をこの前に作成したACMの証明書にします。
セキュリティグループはあらかじめ作成しておきました。anywhereからのhttpsを許可しています。
ネットワークマッピングで二つのサブネットにチェックを入れる
HTTPSを許可するセキュリティグループをあらかじめ作っておいてあります(なければ「新しいセキュリティグループを作成」をクリックして作る)
リスナーをHTTPSにして「ターゲットグループの作成」をクリック
ターゲットグループの作成
本来は2つのAZの両方にインスタンスが起動されている(またはAutoscalingで2つ以上のインスタンスを起動する)べきですが、固定で起動している1インスタンスをターゲットグループに登録します。
プロトコルがHTTPSになっているのでHTTPに変更します。
ALBにターゲットグループを設定
ターゲットグループの作成が完了したら、ALBの作成ウィザードに戻ってターゲットグループ更新ボタン(右端にある丸矢印)をクリックし、リストからターゲットグループを選択します。
ACMの証明書を選択
Route53のレコード作成
ホストゾーンにwww.example.comのAレコードを作成します。このホスト名はACMのサーバー証明書と合わせる必要があります。
確認
以上でwebサーバの起動、ロードバランサ、DNSレコードの設定、サーバー証明書の設定ができました。
https://www.example.comにアクセスすると、Amazonに署名されたサーバー証明書が使用されていることが確認できます。
aws-cliによるバッチ処理化
ここまでの流れをaws-cliでやってみます。
ユーザーデータ用シェル
#!/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登録までやっています。
#!/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
リソース削除シェル
これも作っておかないと、リソース削除を忘れてしまうので。
#!/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 等を使った方が良さそうです。