
Hugo を使った CloudFront + S3 のブログサイトを構築してみた 〜 GitHub Actions で CI/CD 付き
この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。
AWS と GitHub Actions の勉強を兼ねて静的なブログサイトを構築・公開してみませんか?という試みで、Hugo と CloudFront + S3 で構成のブログサイト作ってみました。
手を動かしてみると得られるものも多いかと思います。
- AWS リソースの構築は CloudFormation
- 静的サイトジェネレーターは Hugo
- ブログ記事の投稿から Web 公開までは GitHub Actions(CI/CD)
構築するもの
以下の構成を作成します。ドメイン名の取得と、ACM と証明書発行は事前に済ますことを前提とします。
以下のディレクトリ構成で進めます。後々sample-blogディレクトリに Hugo で使うファイルを保存することになります。
$ tree -L 2
.
├── cloudformation
│ ├── cloudfront-s3.yaml
│ ├── github-oidc.yaml
│ └── iam-role.yaml
└── sample-blog
├── archetypes
├── config.toml
├── content
├── data
├── layouts
├── public
├── resources
├── static
└── themes
今回構築に使用したファイルは以下においてあります。
bigmuramura/hugo-cloudfront-s3
AWS 側の準備
静的サイトを公開するための CloudFront + S3 の構築と、GihHub Actions で CI/CD を行うために必要な権限を作成します。
CloudFront + S3
CloudFront(CDN)+ S3 構成にカスタムドメインでアクセスできる環境を Cloudformation で作成します。
前提条件
詳細は以下リンクの紹介している構成と同じですのでご参照ください。
あとで CloudFront Functions を追加することになるのですが当初デプロイしたコードを載せておきます。最初から完成形のコードを使いたい方は後半に改めてコードを載せておりますのでそちらをご利用ください。
折りたたみ
AWSTemplateFormatVersion: "2010-09-09"
Description: Cloudfront and S3
Parameters:
ProjectName:
Description: Project Name
Type: String
Default: unnamed
Environment:
Description: Environment
Type: String
Default: dev
AllowedValues:
- prod
- dev
S3BucketName:
Description: Bucket name for static site
Type: String
AliasName:
Description: Alias for CloudFront
Type: String
HostedZoneId:
Description: Route53 Host Zone ID
Type: String
CertificateId:
Description: ACM Certificate ID must be us-east-1 region
Type: String
Metadata:
AWS::CloudFormation::Interface:
ParameterGroups:
- Label:
default: Common Settings
Parameters:
- ProjectName
- Environment
- Label:
default: SSL Settings
Parameters:
- AliasName
- HostedZoneId
- CertificateId
Resources:
# ------------------------------------------------------------------------------------ #
# S3
# ------------------------------------------------------------------------------------ #
# Static site bucket
S3Bucket:
Type: AWS::S3::Bucket
Properties:
BucketName: !Sub ${ProjectName}-${Environment}-${S3BucketName}-${AWS::AccountId}
OwnershipControls:
Rules:
- ObjectOwnership: "BucketOwnerEnforced"
PublicAccessBlockConfiguration:
BlockPublicAcls: True
BlockPublicPolicy: True
IgnorePublicAcls: True
RestrictPublicBuckets: True
VersioningConfiguration:
Status: Enabled
BucketEncryption:
ServerSideEncryptionConfiguration:
- ServerSideEncryptionByDefault:
SSEAlgorithm: "AES256"
BucketKeyEnabled: false
LifecycleConfiguration:
Rules:
- Id: AbortIncompleteMultipartUpload
AbortIncompleteMultipartUpload:
DaysAfterInitiation: 7
Status: "Enabled"
- Id: NoncurrentVersionExpiration
NoncurrentVersionExpiration:
NewerNoncurrentVersions: 3
NoncurrentDays: 1
Status: Enabled
# Bucket Policy for CloudFront
BucketPolicy:
Type: AWS::S3::BucketPolicy
Properties:
Bucket: !Ref S3Bucket
PolicyDocument:
Statement:
- Action: s3:GetObject
Effect: Allow
Resource: !Sub arn:aws:s3:::${ProjectName}-${Environment}-${S3BucketName}-${AWS::AccountId}/*
Principal:
AWS: !Sub arn:aws:iam::cloudfront:user/CloudFront Origin Access Identity ${CloudFrontOriginAccessIdentity}
# ------------------------------------------------------------------------------------ #
# CloudFront
# ------------------------------------------------------------------------------------ #
CloudFrontDistribution:
Type: "AWS::CloudFront::Distribution"
Properties:
DistributionConfig:
PriceClass: PriceClass_All
Origins:
- DomainName: !Sub ${ProjectName}-${Environment}-${S3BucketName}-${AWS::AccountId}.s3.${AWS::Region}.amazonaws.com
Id: !Sub "S3origin-${ProjectName}-${Environment}-${S3BucketName}-${AWS::AccountId}"
S3OriginConfig:
OriginAccessIdentity: !Sub "origin-access-identity/cloudfront/${CloudFrontOriginAccessIdentity}"
DefaultRootObject: index.html
DefaultCacheBehavior:
TargetOriginId: !Sub "S3origin-${ProjectName}-${Environment}-${S3BucketName}-${AWS::AccountId}"
Compress: true
ViewerProtocolPolicy: redirect-to-https
# CachePolicyId: 658327ea-f89d-4fab-a63d-7e88639e58f6 # CachingOptimized
CachePolicyId: 4135ea2d-6df8-44a3-9df3-4b5a84be39ad # CachingDisabled
AllowedMethods:
- GET
- HEAD
CachedMethods:
- GET
- HEAD
ForwardedValues:
Cookies:
Forward: none
QueryString: false
Logging:
Bucket: !GetAtt S3BucketLogs.DomainName
IncludeCookies: false
Aliases:
- !Ref AliasName
ViewerCertificate:
SslSupportMethod: sni-only
MinimumProtocolVersion: TLSv1.2_2021
AcmCertificateArn: !Sub "arn:aws:acm:us-east-1:${AWS::AccountId}:certificate/${CertificateId}"
HttpVersion: http2
IPV6Enabled: true
Enabled: true
# OAI
CloudFrontOriginAccessIdentity:
Type: AWS::CloudFront::CloudFrontOriginAccessIdentity
Properties:
CloudFrontOriginAccessIdentityConfig:
Comment: Unique Domain Hosting Environment
# CloudFront log bucket
S3BucketLogs:
Type: AWS::S3::Bucket
DeletionPolicy: Retain
UpdateReplacePolicy: Retain
Properties:
BucketName: !Sub ${ProjectName}-${Environment}-${S3BucketName}-cloudfrontlogs-${AWS::AccountId}
OwnershipControls:
Rules:
- ObjectOwnership: "ObjectWriter"
PublicAccessBlockConfiguration:
BlockPublicAcls: True
BlockPublicPolicy: True
IgnorePublicAcls: True
RestrictPublicBuckets: True
VersioningConfiguration:
Status: Enabled
BucketEncryption:
ServerSideEncryptionConfiguration:
- ServerSideEncryptionByDefault:
SSEAlgorithm: "AES256"
BucketKeyEnabled: false
LifecycleConfiguration:
Rules:
- Id: AbortIncompleteMultipartUpload
AbortIncompleteMultipartUpload:
DaysAfterInitiation: 7
Status: "Enabled"
- Id: CurrentVersionExpiration
ExpirationInDays: 180
Status: "Enabled"
- Id: NoncurrentVersionExpiration
NoncurrentVersionExpiration:
NoncurrentDays: 30
Status: "Enabled"
# Alias Record
Route53RecordSet:
Type: AWS::Route53::RecordSet
Properties:
Name: !Sub ${AliasName}
HostedZoneId: !Sub ${HostedZoneId}
Type: A
AliasTarget:
DNSName: !GetAtt CloudFrontDistribution.DomainName
HostedZoneId: Z2FDTNDATAQYW2 # fixed
AliasNameやHostedZoneIdなど環境依存のパラメータはご自身の環境に合わせて変更してください。rain コマンドでデプロイした引数(Cloudformation のパラメータ)を参考に記します。
$ rain deploy ./cloudformation/cloudfront-s3.yaml hugo-cloudfront-s3-stack --params ProjectName=hugo,Environment=dev,S3BucketName=hugo-static-web-site,AliasName=hugo.ohmura.classmethod.info,HostedZoneId=Z04083533GATCXXXXXXX,CertificateId=d6627991-XXXX-4f69-XXXX-ba004cXXXXXX
rain コマンドについては以下の記事をご参照ください。
OpenID Connect ID Provider
GitHub Actions 用に AWS アカウント1つにつき、OpenID Connect ID Provider 設定が1つ必要になります。
IAM ロールを利用した GitHub Actions をすでに実行している AWS アカウントであればスキップしてください。初回の場合は規定の設定を作成しましょう。
規定の設定を作成します。
AWSTemplateFormatVersion: "2010-09-09"
Description: GitHub OIDC Provider
Resources:
GithubOidc:
Type: AWS::IAM::OIDCProvider
Properties:
Url: https://token.actions.githubusercontent.com
ClientIdList:
- sts.amazonaws.com
ThumbprintList:
- 6938fd4d98bab03faadb97b34396831e3780aea1
rain コマンドでデプロイしました。
$ rain deploy ./cloudformation/github-oidc.yaml github-oidc-stack
IAM ロール
CloudFront + S3 構成の S3 バケットへ GitHub Actions からファイル転送するための権限(IAM ロール)を作成します。
AWSTemplateFormatVersion: "2010-09-09"
Description: IAM Role for Hugo
Parameters:
ProjectName:
Description: Project Name
Type: String
Default: unnamed
Environment:
Description: Environment
Type: String
Default: dev
AllowedValues:
- prod
- dev
GitHubUserName:
Type: String
RepositoryName:
Type: String
DeployTargetS3BucketName:
Type: String
Metadata:
AWS::CloudFormation::Interface:
ParameterGroups:
- Label:
default: Common Settings
Parameters:
- ProjectName
- Environment
Resources:
IAMRole:
Type: AWS::IAM::Role
Properties:
RoleName: !Sub ${ProjectName}-${Environment}-S3AccessRole
AssumeRolePolicyDocument:
Statement:
- Effect: Allow
Action: sts:AssumeRoleWithWebIdentity
Principal:
Federated: !Sub "arn:aws:iam::${AWS::AccountId}:oidc-provider/token.actions.githubusercontent.com"
Condition:
StringLike:
token.actions.githubusercontent.com:sub: !Sub repo:${GitHubUserName}/${RepositoryName}:*
ManagedPolicyArns:
- !Ref S3AccessPolicy
S3AccessPolicy:
Type: "AWS::IAM::ManagedPolicy"
Properties:
ManagedPolicyName: !Sub ${ProjectName}-${Environment}-S3AccessPolicy
Path: "/"
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: "Allow"
Action:
- "s3:ListBucket"
- "s3:PutObject"
Resource:
- !Sub arn:aws:s3:::${DeployTargetS3BucketName}
- !Sub arn:aws:s3:::${DeployTargetS3BucketName}/*
Outputs:
Role:
Value: !GetAtt IAMRole.Arn
GitHubUserNameや S3 バケット名など環境依存のパラメータはご自身の環境に合わせて変更してください。rain コマンドでデプロイした引数(Cloudformation のパラメータ)を参考に記します。
$ rain deploy ./cloudformation/iam-role.yaml hugo-iamrole-githubactions-stack --params GitHubUserName=bigmuramura,RepositoryName=hugo-cloudfront-s3,DeployTargetS3BucketName=hugo-dev-hugo-static-web-site-12345789012,ProjectName=hugo,Environment=dev
以上で AWS 側の準備は完了です。CloudFront + S3 構成の静的コンテンツ配信するガワを作成し、GitHub Actions から AWS へアクセスすための認証(OpenID Connect provider)、権限(IAM ロール)を用意しました。
Hugo の準備
静的サイトジェネレーターの Hugo を使って静的サイトを作成します。Quick Start に沿って進めてローカル環境でブログ記事を書き、記事をプレビュー画面で確認するまでを行うこととします。CI/CD まで完成したらおしゃれなテーマへの変更などカスタマイズ作業もはかどりますので Web 公開できる環境を目指しましょう。
Hugo のコンテナイメージがあったので使ってみました。
klakegg/hugo - Docker Image | Docker Hub
現時点の最新版を利用します。初期セットアップをコンテナ内から実施します。
$ docker run --rm -it -v $(pwd):/src klakegg/hugo:0.101.0-ext-alpine shell
初期セットアップ
hugo new site hogeを実行するとhogeディレクトリが作成されます。作成したディレクトリに移動してから、テーマをサブモジュールで追加します。
$ hugo new site sample-blog $ cd sample-blog $ git submodule add https://github.com/theNewDynamic/gohugo-theme-ananke.git themes/ananke $ echo theme = \"ananke\" >> config.toml
git submodule addコマンドによりルートディレクトリに.gitmodulesが追加(新規作成)されます。
[submodule "sample-blog/themes/ananke"] path = sample-blog/themes/ananke url = https://github.com/theNewDynamic/gohugo-theme-ananke.git
新規ブログ記事を作成します。コンテナは一度終了して作成コマンドで生成された Markdown 形式のファイルをお好きなエディタで開いて編集します。
$ hugo new posts/my-first-post.md $ exit
デフォルトのテンプレートから生成された素っ気ないファイルを編集しました。
--- title: "こんにちは" date: 2022-07-23T03:58:27Z draft: false --- ## テスト投稿 こんにちは
サーバー起動
改めて Hugo コンテナを起動して新規投稿記事のプレビューを確認します。
docker run --rm -it \ -v $(pwd):/src \ -p 1313:1313 \ klakegg/hugo:0.101.0 \ server
http://localhost:1313 で TOP ページにアクセスできます。
テスト投稿した記事も確認できました。
以上で Hugo の準備は完了です。AWS へのデプロイは GitHub Actions から実行します。
GitHub Actions の準備
GitHub Actions を利用して、Build(静的ファイルの生成)と、Deploy(S3 へファイルを転送)を実行する GitHub Actions のワークフロー(hugo.yml)を作成します。
$ tree .github
.github
└── workflows
└── hugo.yml
ワークフローの解説
環境依存の変更箇所とポイントを簡単に説明します。
name: Hugo
on:
push:
branches:
- main
paths:
- "sample-blog/**"
defaults:
run:
working-directory: sample-blog
env:
AWS_REGION: ap-northeast-1
AWS_ROLE_ARN: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/hugo-dev-S3AccessRole
AWS_TARGET_S3_BUCKET: ${{ secrets.AWS_TARGET_S3_BUCKET }}
jobs:
deploy:
runs-on: ubuntu-latest
permissions:
id-token: write
contents: read
steps:
- name: Checkout
uses: actions/checkout@v3
with:
submodules: recursive # Fetch Hugo themes (true OR recursive)
fetch-depth: 0 # Fetch all history for .GitInfo and .Lastmod
- name: Setup Hugo
uses: peaceiris/actions-hugo@v2
with:
hugo-version: "0.101.0"
- name: Build
run: hugo --minify
- name: Configure AWS credentials from IAM Role
uses: aws-actions/configure-aws-credentials@v1
with:
role-to-assume: ${{ env.AWS_ROLE_ARN }}
aws-region: ${{ env.AWS_REGION }}
- name: Upload file to S3
run: aws s3 sync --delete public s3://${{ env.AWS_TARGET_S3_BUCKET }}
GitHub Secrets
AWS アカウント ID はシークレットに登録しました。S3 バケット名もシークレットに登録したのは、S3 バケット名の重複回避 のために AWS アカウント ID を付与していたためです。
AWS_ACCOUNT_IDAWS_TARGET_S3_BUCKET
ワークフロー実行のトリガー
mainブランチにマージ(push)かつ、Hugo のブログ用のディレクトリ(sample-blog)に変更があったときをトリガーとしました。
モノレポ構成で CloudFormation 用のコードも同レポジトリに保存している都合、CloudFormation テンプレートの修正時し、main ブランチにマージしたときも Hugo のビルド・デプロイが走らないためにです。
ブログ用のディレクトリ名は各環境によって変わるため変更してください。
on:
push:
branches:
- main
paths:
- "sample-blog/**"
作業ディレクトリ名指定
ブログ用のディレクトリ名は各環境によって変わるため変更してください。作業用ディレクトリとしてブログ用ディレクトリ(sample-blog)を指定しています。
defaults:
run:
working-directory: sample-blog
環境変数の設定
AWS_ROLE_ARN:は CloudFormation で作成した IAM ロールの ARN に変更してください。シークレットに登録した値と組み合わせて環境変数を登録しました。
env:
AWS_REGION: ap-northeast-1
AWS_ROLE_ARN: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/hugo-dev-S3AccessRole
AWS_TARGET_S3_BUCKET: ${{ secrets.AWS_TARGET_S3_BUCKET }}
OpenID Connect 設定
GitHub Actions 上に AWS に対して永続的な認証情報(IAM アクセスキー)を持たせず、よりセキュアに OpenID Connect を利用した短期間有効な認証方法を利用します。
以下のドキュメントを参考にしました。
Configuring OpenID Connect in Amazon Web Services - GitHub Docs
id-token: write: OIDC JWT ID Token をリクエストするために必要contents: read:actions/checkoutを利用するために必要
deploy:
runs-on: ubuntu-latest
permissions:
id-token: write
contents: read
Hugo Build 設定
Markdown 形式で執筆した記事を含め Webサイトに必要な静的ファイルを hugo コマンドで生成します。
まず、actions/checkout を利用してソースコードをチェックアウトします。
submodules: recursive: Hugo のテーマ取得のために必要fetch-depth: 0: 全タグ、全ブランチの全履歴を取得
Hugo の Build には peaceiris/actions-hugo を利用しました。設定方法は Getting started を参考にしています。
hugo-version: "0.101.0": Hugo のバージョンはローカルで確認済みのバージョンに合わせて指定してくださいrun: hugo --minify: HTML, xml, jsなどファイルを圧縮(不要な改行やインデントを削除)して生成- public ディレクトリ配下に必要な静的ファイルが保存されます。
- 参考: hugo | Hugo
- name: Checkout
uses: actions/checkout@v3
with:
submodules: recursive # Fetch Hugo themes (true OR recursive)
fetch-depth: 0 # Fetch all history for .GitInfo and .Lastmod
- name: Setup Hugo
uses: peaceiris/actions-hugo@v2
with:
hugo-version: "0.101.0"
- name: Build
run: hugo --minify
GitHub Actions から AWS へのアクセス権限取得
AWS へのアクセス権限(時限制)を手に入れ、静的サイト用の S3 バケットに GitHub Actions からファイル転送できる準備をします。
aws-actions/configure-aws-credentialsを利用します。GitHub OCID プロバイダーを使用した短期間有効なクレデンシャルを取得し、CloudFormation で作成した特定の S3 バケットへファイル転送できる IAM ロールを引き受けます(Assume Role)。
- name: Configure AWS credentials from IAM Role
uses: aws-actions/configure-aws-credentials@v1
with:
role-to-assume: ${{ env.AWS_ROLE_ARN }}
aws-region: ${{ env.AWS_REGION }}
aws s3 sync オプション
public ディレクトリに保存されている静的ファイルを S3 バケットへ転送します。
--delete- 同期元(GitHub Actions)にないファイルは同期先(S3 バケット)から削除します
- S3 バケットではバージョニングを有効にしているため完全に削除されるわけではありません
- name: Upload file to S3
run: aws s3 sync --delete public s3://${{ env.AWS_TARGET_S3_BUCKET }}
説明が長くなりましたが以上です。
AWS でブログを公開
Hugo の準備と、GitHub Actions の準備で作成したローカルのファイル類をコミットしてプッシュして、mainブランチにマージすると GitHub Actions が実行され S3 バケットに静的ファイルが配置されます。
これで終わりですと言いたかったのですがサブページを表示できないという事に気づきました。
サブページが表示できない
独自ドメイン名でアクセスするとローカルで確認したと同じ TOP ページにアクセスできます。
しかし、ルート以外のサブディレクトリでは index.html が補完されないため記事のリンクをクリックしても表示できません。パスの末尾にindex.htmlを手動で入力するとサブページを表示できます。
原因と対応方法
原因
CloudFront のデフォルトルートオブジェクトの設定で index.htmlを指定しているため、ルートディレクトリ(TOP ページ)は問題ないのですが、サブディレクトリ(投稿記事など)ではindex.htmlを返してくれません。ということをすっかり忘れていました。
対応方法
CloudFront Functions でindex.htmlを補完してあげます。スクリプトは公式ドキュメントで公開されていたためそのまま利用します。他には Lambda@Edge を使う、S3 バケットの静的ホスティングを有効化する方法もあります。
Add index.html to request URLs that don’t include a file name - Amazon CloudFront
CloudFront Functions のコードも含め Cloudformation テンプレートで作成し、CloudFront へ紐付けるコードを追加しました。
AWSTemplateFormatVersion: "2010-09-09"
Description: Cloudfront and S3 with CloudFront Functions
Parameters:
ProjectName:
Description: Project Name
Type: String
Default: unnamed
Environment:
Description: Environment
Type: String
Default: dev
AllowedValues:
- prod
- dev
S3BucketName:
Description: Bucket name for static site
Type: String
AliasName:
Description: Alias for CloudFront
Type: String
HostedZoneId:
Description: Route53 Host Zone ID
Type: String
CertificateId:
Description: ACM Certificate ID must be us-east-1 region
Type: String
Metadata:
AWS::CloudFormation::Interface:
ParameterGroups:
- Label:
default: Common Settings
Parameters:
- ProjectName
- Environment
- Label:
default: SSL Settings
Parameters:
- AliasName
- HostedZoneId
- CertificateId
Resources:
# ------------------------------------------------------------------------------------ #
# S3
# ------------------------------------------------------------------------------------ #
# Static site bucket
S3Bucket:
Type: AWS::S3::Bucket
Properties:
BucketName: !Sub ${ProjectName}-${Environment}-${S3BucketName}-${AWS::AccountId}
OwnershipControls:
Rules:
- ObjectOwnership: "BucketOwnerEnforced"
PublicAccessBlockConfiguration:
BlockPublicAcls: True
BlockPublicPolicy: True
IgnorePublicAcls: True
RestrictPublicBuckets: True
VersioningConfiguration:
Status: Enabled
BucketEncryption:
ServerSideEncryptionConfiguration:
- ServerSideEncryptionByDefault:
SSEAlgorithm: "AES256"
BucketKeyEnabled: false
LifecycleConfiguration:
Rules:
- Id: AbortIncompleteMultipartUpload
AbortIncompleteMultipartUpload:
DaysAfterInitiation: 7
Status: "Enabled"
- Id: NoncurrentVersionExpiration
NoncurrentVersionExpiration:
NewerNoncurrentVersions: 3
NoncurrentDays: 1
Status: Enabled
# Bucket Policy for CloudFront
BucketPolicy:
Type: AWS::S3::BucketPolicy
Properties:
Bucket: !Ref S3Bucket
PolicyDocument:
Statement:
- Action: s3:GetObject
Effect: Allow
Resource: !Sub arn:aws:s3:::${ProjectName}-${Environment}-${S3BucketName}-${AWS::AccountId}/*
Principal:
AWS: !Sub arn:aws:iam::cloudfront:user/CloudFront Origin Access Identity ${CloudFrontOriginAccessIdentity}
# ------------------------------------------------------------------------------------ #
# CloudFront
# ------------------------------------------------------------------------------------ #
CloudFrontDistribution:
Type: "AWS::CloudFront::Distribution"
Properties:
DistributionConfig:
PriceClass: PriceClass_All
Origins:
- DomainName: !Sub ${ProjectName}-${Environment}-${S3BucketName}-${AWS::AccountId}.s3.${AWS::Region}.amazonaws.com
Id: !Sub "S3origin-${ProjectName}-${Environment}-${S3BucketName}-${AWS::AccountId}"
S3OriginConfig:
OriginAccessIdentity: !Sub "origin-access-identity/cloudfront/${CloudFrontOriginAccessIdentity}"
DefaultRootObject: index.html
DefaultCacheBehavior:
TargetOriginId: !Sub "S3origin-${ProjectName}-${Environment}-${S3BucketName}-${AWS::AccountId}"
Compress: true
ViewerProtocolPolicy: redirect-to-https
# CachePolicyId: 658327ea-f89d-4fab-a63d-7e88639e58f6 # CachingOptimized
CachePolicyId: 4135ea2d-6df8-44a3-9df3-4b5a84be39ad # CachingDisabled
AllowedMethods:
- GET
- HEAD
CachedMethods:
- GET
- HEAD
ForwardedValues:
Cookies:
Forward: none
QueryString: false
# CloudFront Function Association
FunctionAssociations:
- EventType: viewer-request
FunctionARN: !GetAtt CloudFrontFunction.FunctionMetadata.FunctionARN
Logging:
Bucket: !GetAtt S3BucketLogs.DomainName
IncludeCookies: false
Aliases:
- !Ref AliasName
ViewerCertificate:
SslSupportMethod: sni-only
MinimumProtocolVersion: TLSv1.2_2021
AcmCertificateArn: !Sub "arn:aws:acm:us-east-1:${AWS::AccountId}:certificate/${CertificateId}"
HttpVersion: http2
IPV6Enabled: true
Enabled: true
# OAI
CloudFrontOriginAccessIdentity:
Type: AWS::CloudFront::CloudFrontOriginAccessIdentity
Properties:
CloudFrontOriginAccessIdentityConfig:
Comment: Unique Domain Hosting Environment
# CloudFront log bucket
S3BucketLogs:
Type: AWS::S3::Bucket
DeletionPolicy: Retain
UpdateReplacePolicy: Retain
Properties:
BucketName: !Sub ${ProjectName}-${Environment}-${S3BucketName}-cloudfrontlogs-${AWS::AccountId}
OwnershipControls:
Rules:
- ObjectOwnership: "ObjectWriter"
PublicAccessBlockConfiguration:
BlockPublicAcls: True
BlockPublicPolicy: True
IgnorePublicAcls: True
RestrictPublicBuckets: True
VersioningConfiguration:
Status: Enabled
BucketEncryption:
ServerSideEncryptionConfiguration:
- ServerSideEncryptionByDefault:
SSEAlgorithm: "AES256"
BucketKeyEnabled: false
LifecycleConfiguration:
Rules:
- Id: AbortIncompleteMultipartUpload
AbortIncompleteMultipartUpload:
DaysAfterInitiation: 7
Status: "Enabled"
- Id: CurrentVersionExpiration
ExpirationInDays: 180
Status: "Enabled"
- Id: NoncurrentVersionExpiration
NoncurrentVersionExpiration:
NoncurrentDays: 30
Status: "Enabled"
# Alias Record
Route53RecordSet:
Type: AWS::Route53::RecordSet
Properties:
Name: !Sub ${AliasName}
HostedZoneId: !Sub ${HostedZoneId}
Type: A
AliasTarget:
DNSName: !GetAtt CloudFrontDistribution.DomainName
HostedZoneId: Z2FDTNDATAQYW2 # fixed
# ------------------------------------------------------------------------------------ #
# CloudFront Functions
# ------------------------------------------------------------------------------------ #
CloudFrontFunction:
Type: "AWS::CloudFront::Function"
Properties:
Name: "add-index-function"
FunctionCode: |
function handler(event) {
var request = event.request;
var uri = request.uri;
// Check whether the URI is missing a file name.
if (uri.endsWith('/')) {
request.uri += 'index.html';
}
// Check whether the URI is missing a file extension.
else if (!uri.includes('.')) {
request.uri += '/index.html';
}
return request;
}
FunctionConfig:
Comment: "Add index.html to the path"
Runtime: "cloudfront-js-1.0"
AutoPublish: true
パラメータに変更はないため最初に作成した rain コマンドを再入力して更新をかけました。
$ rain deploy ./cloudformation/cloudfront-s3.yaml hugo-cloudfront-s3-stack --params ProjectName=hugo,Environment=dev,S3BucketName=hugo-static-web-site,AliasName=hugo.ohmura.classmethod.info,HostedZoneId=Z04083533GATUFF6GHTWT,CertificateId=d6627991-XXXX-4f69-XXXX-ba004cXXXXXX
CloudFunctions がデプロイされると、投稿した記事も表示できるようになりました。
CI/CD を体感
2回目のテスト投稿を通して全体の動きをみてみましょう。
ブランチを切って記事を執筆します。
$ git checkout -b good-afternaoon
ブログ用のディレクトリでコンテナを起動し hugo コマンドで新規投稿記事の雛形を作成します。
$ cd sample-blog $ docker run --rm -it -v $(pwd):/src klakegg/hugo:0.101.0-ext-alpine shell $ hugo new posts/good-afternoon.md $ exit
エディタで投稿記事を書きます。初回は「こんにちは」でしたの「こんばんは」とだけ記した記事を作成しました。
--- title: "こんばんは" date: 2022-07-23T15:20:58Z draft: false --- ## テスト投稿2 こんばんは
ローカルサーバで投稿記事を確認します。
$ docker run --rm -it -v $(pwd):/src -p 1313:1313 klakegg/hugo:0.101.0 server
表示に問題ありませんでしたのでコミットしてプッシュします。
$ git add . $ git commit -m "テスト投稿2本目" $ git push origin good-afternoon
GitHub を確認して、main ブランチにマージしました。
GitHub Actions で設定したワークフローが動きだしました。
30秒もしないで終わりました。Hugo で静的ファイル生成してから S3 へファイル転送しているのですけど早いですね。
独自ドメインの CloudFront へアクセスすると2本目のテスト投稿記事を TOP ページから確認できました。
今回は新規投稿記事をすぐに確認できるよう CloudFront のキャッシュ設定を無効化していました。ブログサイトとして運用するときはキャッシュを有効化するとよいでしょう。
# CachePolicyId: 658327ea-f89d-4fab-a63d-7e88639e58f6 # CachingOptimized
CachePolicyId: 4135ea2d-6df8-44a3-9df3-4b5a84be39ad # CachingDisabled
テスト投稿2を開いてもちゃんと見ることができました。
ローカルでブログを書いて GitHub にプッシュすれば自動的に Web 公開できる環境ができました。
さらに勉強したいときは
S3 バケットのアクセスログを取得設定を追加したり、CloudFront のアクセスログを Athena で検索してみたり、CloudFront に AWS WAF(ランニングコストに注意)をつけてみたりするとより AWS を堪能できるかと思います。Hugo のテーマをとっかえひっかえしているときの方が楽しい気はするので Hugo で調べてみてください。
- サーバーアクセスログを使用したリクエストのログ記録 - Amazon Simple Storage Service
- Amazon Athena で CloudFront のアクセスログを集計する | DevelopersIO
- AWS WAFをCloudFrontに導入する | DevelopersIO
おわりに
Hugo + GitHub Actions + CloudFront + S3 の組み合わせは見かけますが、求めている構成のものが見当たらなかったので書きました。想定よりずいぶんと長くなりまして、CloudFront + S3 の構築から Hugo の設定まで通して書いている記事が少ない理由がわかった気がします。
昔、個人サイトを Hugo で作ったときは AWS 力が低くて CloudFront + S3 構成に CI/CD まで作れませんでした。なので Netlify を使って GitHub からのお手軽 CI/CD 構成にしています。




























