AWSの静的サイトホスティングパターン構築用CloudFormation作ってみた

やりつくされた感のあるAWSの静的サイトホスティングパターンのCloudFormationでやってみる系のやつです。まぁ先人が色々としてくださっているので今更ネタです。

そしてAWSは5年ほど触ってますが、いまだに初心者を脱出できていないですし、コピペしかできないへっぽこです。なんで急にCloudFormationやってみようと思ったかというと静的サイトホスティング二重化Terraformで一本でまとめてやりたいなぁと思っていて、その練習がてらどうせならCloudFormation勉強してみようと思ってやってみました。

2021年1月31時点に構築しました。

aws-cli/1.18.39 Python/3.7.7 Linux/5.4.0-64-generic botocore/1.15.3

◆作成したもの

1.ディレクトリ構成

.
|-- create_stacks.sh
|-- delete_stacks.sh
`-- yml
    |-- acmsetting.yml
    |-- makehostedzone.yml
    `-- static-site.yml

2.シェルスクリプト群

「create_stacks.sh sample.com」とか「delete_stacks.sh」で起動すると一気に静的サイトホスティングのCloudFormationテンプレート呼び出してくれます。

・create_stacks.sh

#!/bin/bash
SLEEP_TIME=30
R53_YML_NAME="makehostedzone.yml"
R53_STACK_NAME="route53set"
ACM_YML_NAME="acmsetting.yml"
ACM_STACK_NAME="acmbuild"
STATIC_YML_NAME="static-site.yml"
STATIC_STACK_NAME="staticsitebuild"
BREAK_WORD="CREATE_COMPLETE"

if [ $# != 1 ]; then
    echo 'Empty Domain! Please [./create_stacks.sh yourdomain]'
    exit 1
fi
domain=$1

echo '*** Created route53 hostedzone ***'
aws cloudformation create-stack --stack-name ${R53_STACK_NAME} \
--template-body file://`pwd`/yml/${R53_YML_NAME} \
--parameters ParameterKey=DomainName,ParameterValue=${domain}
while true
do
  sleep ${SLEEP_TIME}
  cloudformationstatus=$(aws cloudformation describe-stacks --stack-name ${R53_STACK_NAME})
  stat=$(echo ${cloudformationstatus} | jq -r ".Stacks[0].StackStatus")
  if [ "$stat"=${BREAK_WORD} ]; then
    break
  fi
done

#Get hostedzoneId
route53result=$(aws route53 list-hosted-zones-by-name --dns-name ${domain})
hosted=$(echo ${route53result} | jq -r ".HostedZones[0].Id")
hz=$(echo ${hosted} | sed -e "s!/hostedzone/!!g")

echo '*** Create ACM ***'
if [ "$hz" = "" ]; then
    echo 'Fail get HostedZoneId'
    exit 1
fi
aws cloudformation create-stack \
--region us-east-1 \
--stack-name ${ACM_STACK_NAME} \
--template-body file://`pwd`/yml/${ACM_YML_NAME} \
--parameters ParameterKey=DomainName,ParameterValue=${domain} ParameterKey=HostedZone,ParameterValue=${hz}

echo '*** Please Setting Your Domain Nameserve ***'
route53records=$(aws route53 list-resource-record-sets --hosted-zone-id /hostedzone/${hz})
array=$(echo ${route53records} | jq -r ".ResourceRecordSets[0].ResourceRecords[].Value")
for i in ${array[@]}
do
  echo ${i}
done

#Check Certificate
while true
do
  sleep ${SLEEP_TIME}
  cloudformationstatus=$(aws cloudformation describe-stacks --region us-east-1 --stack-name ${ACM_STACK_NAME})
  stat=$(echo ${cloudformationstatus} | jq -r ".Stacks[0].StackStatus")
  if [ "$stat"=${BREAK_WORD} ]; then
    break
  fi
done
cloudformationstatus=$(aws cloudformation describe-stacks --region us-east-1 --stack-name ${ACM_STACK_NAME})
acmarn=$(echo ${cloudformationstatus} | jq -r ".Stacks[0].Outputs[].OutputValue")

echo '*** Create S3 and CloudFront ***'
aws cloudformation create-stack \
--stack-name ${STATIC_STACK_NAME} \
--template-body file://`pwd`/yml/${STATIC_YML_NAME} \
--parameters ParameterKey=SystemName,ParameterValue=prd ParameterKey=HostDomain,ParameterValue=${domain} ParameterKey=ACMCertificate,ParameterValue=${acmarn}

echo 'Static Site Build Completed!'

・delete_stacks.sh

#!/bin/bash
R53_STACK_NAME="route53set"
ACM_STACK_NAME="acmbuild"
STATIC_STACK_NAME="staticsitebuild"
BREAK_WORD="CREATE_COMPLETE"
SLEEP_TIME=30

if [ $# != 1 ]; then
    echo 'Empty Domain! Please [./deletestack.sh yourdomain]'
    exit 1
fi
domain=$1
BUCKET_NAME=prd-${domain}-logs

echo '*** Delete S3 object ***'
objectdel=$(aws s3 rm s3://${BUCKET_NAME} --recursive)
aws cloudformation delete-stack --stack-name ${STATIC_STACK_NAME}

echo '*** Checked DNS Records ***'
route53result=$(aws route53 list-hosted-zones-by-name --dns-name ${domain})
hosted=$(echo ${route53result} | jq -r ".HostedZones[0].Id")
hz=$(echo ${hosted} | sed -e "s!/hostedzone/!!g")

while true
do
  route53records=$(aws route53 list-resource-record-sets --hosted-zone-id /hostedzone/${hz})
  array=$(echo ${route53records} | jq -r ".ResourceRecordSets[].Type")
  if [[ $(printf '%s\n' "${array[@]}" | grep -qx "A"; echo -n ${?} ) -eq 0 ]]; then
    sleep ${SLEEP_TIME}
  else
    break
  fi
done

echo '*** Delete ACM ***'
aws cloudformation delete-stack --region us-east-1 --stack-name ${ACM_STACK_NAME}

echo '*** Delete Route53 ***'
route53records=$(aws route53 list-resource-record-sets --hosted-zone-id /hostedzone/${hz})
RESOURCE_VALUE=$(echo ${route53records} | jq -c -r '.ResourceRecordSets[]| if .Type == "CNAME" then .ResourceRecords[].Value else empty end')
DNS_NAME=$(echo ${route53records} | jq -c -r '.ResourceRecordSets[] | if .Type == "CNAME" then .Name else empty end')
RECORD_TYPE="CNAME"
TTL=$(echo ${route53records} | jq -c -r '.ResourceRecordSets[]| if .Type == "CNAME" then .TTL else empty end')
JSON_FILE=`mktemp`

(
cat < $JSON_FILE

echo "Deleting DNS Record set"
aws route53 change-resource-record-sets --hosted-zone-id ${hz} --change-batch file://$JSON_FILE
aws cloudformation delete-stack --stack-name ${R53_STACK_NAME}

echo 'Delete Complete!'

3.CloudFormationテンプレートYAML

Route53のホストゾーン登録するテンプレートです。

・makehostedzone.yml

AWSTemplateFormatVersion: "2010-09-09"
Parameters:
  DomainName:
    Type: String
    Default: sample.com
Resources:
  DNS:
    Type: "AWS::Route53::HostedZone"
    Properties:
      Name: !Ref DomainName
      HostedZoneConfig:
        Comment: !Join
          - " "
          - ["My hosted zone for", !Ref DomainName]
      HostedZoneTags:
        - Key: Name
          Value: !Sub ${DomainName}-Route53HostedZone

ACMを登録するテンプレートです。

・acmsetting.yml

AWSTemplateFormatVersion: "2010-09-09"
Parameters:
  SystemName:
    Type: String
    Default: mysystem
  DomainName:
    Type: String
    Default: devstatic
  HostedZone:
    Type: String
    Default: ZXXXXXXXXXXX
Resources:
  Certificate:
    Type: AWS::CertificateManager::Certificate
    Properties:
      DomainName: !Ref DomainName
      DomainValidationOptions:
        - DomainName: !Ref DomainName
          HostedZoneId: !Ref HostedZone
      SubjectAlternativeNames:
        - !Sub '*.${DomainName}'
      ValidationMethod: DNS
Outputs:
  ACMCertificateARN:
    Value: !Ref Certificate
    Export:
      Name: !Sub ${SystemName}-CertificateARN

S3+CloudFrontのテンプレートです。Route53のAレコード追加もします。

・static-site.yml

AWSTemplateFormatVersion: '2010-09-09'
Description: "aws static site cloudfront and s3"
Parameters:
  SystemName:
    Type: String
    Default: mysystem
  HostDomain:
    Type: String
    Default: sample.com
  HostedZoneID:
    Type: String
    Default: Z2FDTNDATAQYW2
  ACMCertificate:
    Type: String
    Default: arn:aws:acm:us-east-1:XXXXX:certificate/XXXXXXXX
Resources:
  S3BucketLogs:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Sub ${SystemName}-${HostDomain}-logs
      AccessControl: LogDeliveryWrite
      BucketEncryption:
        ServerSideEncryptionConfiguration:
          - ServerSideEncryptionByDefault:
              SSEAlgorithm: AES256
  S3BucketWebStatic:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Sub ${SystemName}-${HostDomain}
      BucketEncryption:
        ServerSideEncryptionConfiguration:
          - ServerSideEncryptionByDefault:
              SSEAlgorithm: AES256
      LoggingConfiguration:
        DestinationBucketName: !Ref S3BucketLogs
        LogFilePrefix: 'origin/'
  BucketPolicyWebStatic:
    Type: AWS::S3::BucketPolicy
    Properties:
      Bucket: !Ref S3BucketWebStatic
      PolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Action:
              - s3:GetObject
            Effect: Allow
            Resource: !Sub '${S3BucketWebStatic.Arn}/*'
            Principal:
              CanonicalUser: !GetAtt CloudFrontOriginAccessIdentity.S3CanonicalUserId
  CloudFrontOriginAccessIdentity:
    Type: AWS::CloudFront::CloudFrontOriginAccessIdentity
    Properties:
      CloudFrontOriginAccessIdentityConfig:
        Comment: "CloudFront OAI for Static Web Site"
  CloudFrontDistribution:
    Type: AWS::CloudFront::Distribution
    Properties:
      DistributionConfig:
        Aliases:
          - !Sub ${HostDomain}
          - !Sub www.${HostDomain}
        CustomErrorResponses:
          - ErrorCachingMinTTL: 10
            ErrorCode: 403
            ResponseCode: 200
            ResponsePagePath: /404.html
        DefaultCacheBehavior:
          Compress: true
          DefaultTTL: 0
          ForwardedValues:
            QueryString: true
          MaxTTL: 0
          MinTTL: 0
          TargetOriginId: 'WebStatic-S3'
          # httpの通信はhttpsにリダイレクトする
          ViewerProtocolPolicy: 'redirect-to-https'
        Enabled: true
        HttpVersion: 'http2'
        DefaultRootObject: 'index.html'
        IPV6Enabled: true
        Logging:
          Bucket: !GetAtt S3BucketLogs.DomainName
          IncludeCookies: false
          Prefix: 'cdn/'
        Origins:
          # 作成直後にStatusCode 307とならないように、Region指定の方式
          # https://aws.amazon.com/jp/premiumsupport/knowledge-center/s3-http-307-response/
          - DomainName: !Join ["", [!Ref S3BucketWebStatic, !Sub ".s3-${AWS::Region}.amazonaws.com"]]
            Id: 'WebStatic-S3'
            S3OriginConfig:
              OriginAccessIdentity:
                !Join ['', ['origin-access-identity/cloudfront/', !Ref CloudFrontOriginAccessIdentity]]
        PriceClass: 'PriceClass_All'
        ViewerCertificate:
          SslSupportMethod: sni-only
          AcmCertificateArn: !Ref ACMCertificate
  ZoneApexRecord:
    Type: 'AWS::Route53::RecordSetGroup'
    Properties:
      HostedZoneName: !Sub "${HostDomain}."
      RecordSets:
        - Name: !Ref HostDomain
          Type: A
          AliasTarget:
            HostedZoneId: !Ref HostedZoneID
            DNSName: !GetAtt CloudFrontDistribution.DomainName
  HostRecord:
    Type: 'AWS::Route53::RecordSetGroup'
    Properties:
      HostedZoneName: !Sub "${HostDomain}."
      RecordSets:
        - Name: !Sub www.${HostDomain}
          Type: A
          AliasTarget:
            HostedZoneId: !Ref HostedZoneID
            DNSName: !GetAtt CloudFrontDistribution.DomainName
Outputs:
  S3BucketLogsName:
    Description: "S3 Bucket for keeping access logs."
    Value: !Ref S3BucketLogs
  S3BucketWebStaticName:
    Description: "S3 Bucket for deploying static site."
    Value: !Ref S3BucketWebStatic
  CloudFrontDomainName:
    Description: "CloudFront DomainName."
    Value: !GetAtt  CloudFrontDistribution.DomainName
  ZoneApexRecordName:
    Description: "Route53 ZoneApex RecordSet."
    Value: !Ref ZoneApexRecord
  HostRecordName:
    Description: "Route53 Host RecordSet."
    Value: !Ref HostRecord

今度はGoogleのCloud Deployment Manager を使ってGoogleCloudのテンプレートやってみようかなと思ったりしています。いつになることやら・・・

没になったけどfreenomのNameServerにShellキックで登録してやりたかった。リセラーしかAPIで登録できないみたい。備忘録で置いときます。使ってもエラーなっちゃうし。

#!/bin/bash
array=($(aws route53 create-hosted-zone --name yourdomain --caller-reference `date +%Y-%m-%d_%H-%M-%S` | jq -r ".DelegationSet.NameServers[]"))

nsstr='&nameserver='
cmd1='curl -X POST https://api.freenom.com/v2/domain/register.xml -d '
cmd2=' "tohonokai.cf'
frontcmd="${cmd1}${cmd2}"

for i in ${array[@]}
do
  cmd="${cmd}${nsstr}${i}"
done
backcmd='&email=yourmail@example.com&password=yourpassword&domaintype=FREE"'
fullcmd="${frontcmd}${cmd}${backcmd}"
eval $fullcmd

ほとんど参考サイトのコピペです。これではだめです。もっと修行しないといけない。


◆参考サイト

・CloudFormation


・S3+CloudFront


・Route53


・ACM


・FreenomAPI



◆GitHub

とりあえずGitHubに上げているので公開しときます。

コメント

このブログの人気の投稿

証券外務員1種勉強(計算式暗記用メモ)

GASでGoogleDriveのサブフォルダとファイル一覧を出力する

マクロ経済学(IS-LM分析)