HugoとAWSを使ってブログを構築したので、その方法を書いていきます。

目次

Hugo

Quick Start を参考にしながら進めていきます。

インストール

バイナリを使用してHogoをインストールします。Hugo Releases から自分の環境に合ったものをダウンロードします。
私の場合は hugo_extended_0.65.3_Windows-64bit.zip をダウンロードしました。ダウンロード後、フォルダを解凍し、解凍したフォルダの名前をbinに変更します。新しいフォルダ(Hugo)をローカルディスク(C:)に作成し、先ほど解凍したフォルダ(bin)をHugoフォルダに入れます。後は、C:¥Hugo¥binを環境変数のPathに追加すれば、インストールは完了です。

サイトを作成

$ cd C:¥Hugo
$ hugo new site quickstart

コマンドプロンプトで上記のコマンドを実行します。実行するとconfigファイル等が入ったquickstartフォルダが作成されます。
コマンドの quickstart の部分は、フォルダ名になるので、任意の名前を入力します。

テーマを追加

$ cd quickstart
$ git clone https://github.com/dim0627/hugo_theme_robust.git themes/hugo_theme_robust
$ git init

$ echo theme = "hugo_theme_robust" >> config.toml

テーマを追加することにより、Hugo Themes のような、様々なテンプレートを使用することができます。 今回は、私のブログでも使用している hugo_theme_robust を追加します。

記事を追加

$ hugo new post/my-first-post.md

Markdownファイルが、quickstartフォルダ内のpostフォルダに作成されます。Markdownファイルを編集することにより、記事の内容を変更できます。

draft: true
        ↓
draft: false

記事をウェブサイトで公開するために、Markdownファイル先頭部分を上記に変更します。

ローカルで確認

$ hugo server -D

Hugoサーバーを起動し、ブラウザから http://localhost:1313/ にアクセスして動作を確認します。特に問題がなければ、Hugoの準備は一旦完了です。

Route53でドメインを取得

下記を参考にして、ウェブサイトを公開するためのドメインを取得します。

Route 53 でドメインを取得・購入する(2019版) | Developers.IO

ドメインを取得したら、Hugoに戻ってquickstartフォルダ内のconfig.tomlファイルを編集します。

baseURL = "http://example.org/"
                     ↓
baseURL = "https://取得したドメイン名/"

AWS Certificate ManagerでSSL/TLS証明書を発行

CloudFrontに設定するSSL/TLS証明書を発行します。

マネージメントコンソールから Certificate Manager を選択 → バージニア北部リージョンであることを確認 → 証明書のプロビジョニング「今すぐ始める」をクリック → 「パブリック証明書のリクエスト」を選択 → 「証明書のリクエスト」をクリック → ドメイン名を入力 → 「次へ」をクリック → 「DNSの検証」を選択 → 「次へ」をクリック → 「確認」をクリック → 「確認とリクエスト」をクリック → 「Route53でのレコードの作成」をクリック → 「作成」をクリック

S3とCloudFrontを作成

S3とCloudFrontは、CloudFormationで一気に作ってしまいます。

AWSTemplateFormatVersion: 2010-09-09

Metadata:
  AWS::CloudFormation::Interface:
    ParameterGroups:
      - Label:
          default: S3 and CloudFront Configuration
        Parameters:
          - S3BucketName
          - WebsiteDomainName
          - AcmCertificateArn

Parameters:
# バケット名を入力
  S3BucketName:
    Type: String

# ドメイン名を入力
  WebsiteDomainName:
    Type: String

# AcmCertificateArnを入力
  AcmCertificateArn:
    Type: String

Resources:
# ログ用のバケットを作成
  AccessLogS3Bucket:
    Type: "AWS::S3::Bucket"
    Properties:
      BucketName: !Sub ${S3BucketName}-log
      AccessControl: LogDeliveryWrite
      LifecycleConfiguration:
        Rules:
          - Id: !Sub ${S3BucketName}-log-lifecycle-rule
            Status: Enabled
            ExpirationInDays: 30

# Hugo用のバケットを作成
  HugoS3Bucket:
    Type: "AWS::S3::Bucket"
    Properties:
      AccessControl: PublicRead
      BucketName: !Ref S3BucketName
      LoggingConfiguration:
        DestinationBucketName: !Ref AccessLogS3Bucket

# バケットポリシーを作成
  S3BucketPolicy:
    Type: "AWS::S3::BucketPolicy"
    Properties:
      Bucket: !Ref HugoS3Bucket
      PolicyDocument: 
        Statement:
          - 
            Action: 
              - s3:GetObject
            Effect: Allow
            Resource: !Sub arn:aws:s3:::${HugoS3Bucket}/*
            Principal: "*"

# CloudFrontを設定
  CloudFront:
    Type: "AWS::CloudFront::Distribution"
    Properties:
      DistributionConfig:
        Origins:
          - DomainName: !GetAtt HugoS3Bucket.DomainName
            Id: !Sub S3-Website-${HugoS3Bucket.DomainName}
            CustomOriginConfig:
                OriginProtocolPolicy: http-only
        Enabled: true
        DefaultRootObject: index.html
        Logging:
          IncludeCookies: false
          Bucket: !GetAtt AccessLogS3Bucket.DomainName
          Prefix: cloudfront/
        Aliases:
        - !Ref WebsiteDomainName
        DefaultCacheBehavior:
          AllowedMethods:
          - GET
          - HEAD
          CachedMethods:
          - GET
          - HEAD
          DefaultTTL: 604800
          MaxTTL: 1209600
          MinTTL: 0
          Compress: true
          TargetOriginId: !Sub S3-Website-${HugoS3Bucket.DomainName}
          ForwardedValues:
            QueryString: false
            Cookies:
              Forward: none
          ViewerProtocolPolicy: redirect-to-https
        PriceClass: PriceClass_200
        ViewerCertificate:
          AcmCertificateArn: !Ref AcmCertificateArn
          SslSupportMethod: sni-only
          MinimumProtocolVersion: TLSv1.1_2016
        HttpVersion: http2
              
Outputs:
# バケット名を表示
  BucketName:
    Value: !Ref HugoS3Bucket

# CloudFrontのドメイン名を表示
  DomainName:
    Value: !GetAtt CloudFront.DomainName

スタック作成時、パラメータでは3つの値を入力します。

  • S3BucketName = S3のバケット名
  • WebsiteDomainName = Route53で取得したドメイン名
  • AcmCertificateArn = AWS Certificate Managerで発行したSSL/TLS証明書のARN

出力では2つの値を表示します。

  • BucketName = Hugo用S3のバケット名
  • DomainName = CloudFrontのドメイン名

出力で表示されたDomainNameは、Route53でエイリアスレコードとして登録する必要があります。

マネージメントコンソールからRoute 53を選択 → ホストゾーンを選択 → ドメイン名を選択 → 「レコードセットに移動」をクリック → 「レコードセットの作成」をクリック → タイプは「A - IPv4アドレス」を選択、エイリアスは「はい」を選択、エイリアス先は「CloudFrontディストリビューション」を選択 → 「作成」をクリック

CodeCommit + CodeBuild + CodePipelineで自動化

  • AWS CodeCommit(AWSが提供するフルマネージド型のGitリポジトリサービス)
  • AWS CodeBuild(クラウドで動作する完全マネージド型のビルドサービス)
  • AWS CodePipeLine(完全マネージド型の継続的デリバリーサービス)

3つのサービスを利用して、Hugoで記事を作成したら、自動で公開できるようにします。

CodeCommit

まず、リポジトリを作成します。

次に、リポジトリに接続します。
リポジトリに接続するためには、CodeCommitのGit認証情報を生成する必要があります。
マネージメントコンソールからIAMユーザーにアクセスし、「AWS CodeCommit の HTTPS Git 認証情報」の「認証情報を生成」ボタンをクリックします。

リモートリポジトリを追加し、後は add → commit → push の流れです。pushするとWindowsの場合、Git Credential Manager for Windows の画面が表示されます。そこに先ほど生成した認証情報を入力すると、リモートリポジトリに接続できます。

あと、git add する前にhugo_theme_robustフォルダ内の .gitフォルダ(隠しフォルダ) は削除しておきます。

CodeBuild + CodePipeline

buildspec.ymlを作成します。
テンプレート内の bucket_name Distribution_ID に値を入力します。

  • bucket_name = Hugo用S3のバケット名
  • Distribution_ID = CloudFrontのID
version: 0.2

env:
  variables:
    hugo_version: "0.65.3"
    bucket_name: "***"
    Distribution_ID: "***************"

phases:
  install:
    runtime-versions:
      golang: 1.13
    commands:
      - curl -Ls https://github.com/gohugoio/hugo/releases/download/v${hugo_version}/hugo_extended_${hugo_version}_Linux-64bit.tar.gz -o /tmp/hugo.tar.gz
      - tar xf /tmp/hugo.tar.gz -C /tmp
      - mv /tmp/hugo /usr/bin/hugo
      - rm -rf /tmp/hugo*
  build:
    commands:
      - hugo
  post_build:
    commands:
      - aws s3 sync --delete public/ s3://${bucket_name}
      - aws cloudfront create-invalidation --distribution-id ${Distribution_ID} --paths "/*"

作成したら、quickstartフォルダに配置し、CodeCommitにpushしておきます。

└─quickstart
    │  buildspec.yml
    │  config.toml
    │
    ├─archetypes
    │
    ├─content
    │
    ├─data
    │
    ├─layouts
    ├─resources
    ├─static
    └─themes

次に、CloudFormationでCodeBuildとCodePipelineを作成します。

AWSTemplateFormatVersion: 2010-09-09

Metadata:
  AWS::CloudFormation::Interface:
    ParameterGroups:
      - Label:
          default: Build and Pipeline Configuration
        Parameters:
          - BuildProjectName
          - PipelineName
          - HugoS3BucketName
          - CodeCommitRepositoryName

Parameters: 
# ビルドプロジェクト名を入力
  BuildProjectName: 
    Type: String

# パイプライン名を入力
  PipelineName:
    Type: String

# Hugo用S3バケット名を入力
  HugoS3BucketName:
    Type: String

# CodeCommitのリポジトリ名を入力
  CodeCommitRepositoryName:
    Type: String

Resources:
# CodeBuild用IamRoleを作成
  BuildIamRole:
    Type: "AWS::IAM::Role"
    Properties:
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - codebuild.amazonaws.com
            Action: sts:AssumeRole
      Path: /
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/CloudFrontFullAccess
      Policies:
        - PolicyName: CodeBuild-Policy
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Effect: Allow
                Resource:
                  - !Sub arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/codebuild/${BuildProjectName}
                  - !Sub arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/codebuild/${BuildProjectName}:*
                Action:
                  - logs:CreateLogGroup
                  - logs:CreateLogStream
                  - logs:PutLogEvents
              - Effect: Allow
                Resource:
                  - !Sub arn:aws:s3:::codepipeline-${AWS::Region}-*
                Action:
                  - s3:PutObject
                  - s3:GetObject
                  - s3:GetObjectVersion
                  - s3:GetBucketAcl
                  - s3:GetBucketLocation
                  - s3:DeleteObject
              - Effect: Allow
                Resource:
                  - !Sub arn:aws:s3:::${HugoS3BucketName}
                  - !Sub arn:aws:s3:::${HugoS3BucketName}/*
                Action:
                  - s3:PutObject
                  - s3:GetObject
                  - s3:GetObjectVersion
                  - s3:GetBucketAcl
                  - s3:GetBucketLocation
                  - s3:DeleteObject
                  - s3:ListBucket
              - Effect: Allow
                Action:
                  - codebuild:CreateReportGroup
                  - codebuild:CreateReport
                  - codebuild:UpdateReport
                  - codebuild:BatchPutTestCases
                Resource:
                  - !Sub arn:aws:codebuild:${AWS::Region}:${AWS::AccountId}:report-group/${BuildProjectName}-*

# CodePipeline用IamRoleを作成
  PipelineIamRole:
    Type: "AWS::IAM::Role"
    Properties:
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - codepipeline.amazonaws.com
            Action: sts:AssumeRole
      Path: /
      Policies:
        - PolicyName: CodePipeline-Policy
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Effect: Allow
                Action:
                  - codecommit:CancelUploadArchive
                  - codecommit:GetBranch
                  - codecommit:GetCommit
                  - codecommit:GetUploadArchiveStatus
                  - codecommit:UploadArchive
                Resource: '*'
              - Effect: Allow
                Action:
                  - codebuild:BatchGetBuilds
                  - codebuild:StartBuild
                Resource: '*'
              - Effect: Allow
                Action:
                  - lambda:InvokeFunction
                  - lambda:ListFunctions
                Resource: '*'
              - Effect: Allow
                Action:
                  - iam:PassRole
                Resource: '*'
              - Effect: Allow
                Action:
                  - cloudwatch:*
                  - s3:*
                Resource: '*'

# CloudWatchEvent用IamRoleを作成
  CloudWatchEventIamRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - events.amazonaws.com
            Action: sts:AssumeRole
      Path: /
      Policies:
        - PolicyName: cwe-pipeline-execution
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Effect: Allow
                Action: codepipeline:StartPipelineExecution
                Resource: !Join 
                  - ''
                  - - 'arn:aws:codepipeline:'
                    - !Ref AWS::Region
                    - ':'
                    - !Ref AWS::AccountId
                    - ':'
                    - !Ref BlogPipeline

# CodeBuildを設定
  BlogBuild:
    Type: "AWS::CodeBuild::Project"
    Properties:
      Source: 
        Type: CODEPIPELINE
      Name: !Ref BuildProjectName
      Artifacts: 
        Type: CODEPIPELINE
      LogsConfig:
        CloudWatchLogs:
          Status: ENABLED
      ServiceRole: !GetAtt BuildIamRole.Arn
      Environment: 
        ComputeType: BUILD_GENERAL1_SMALL
        Image: aws/codebuild/standard:3.0
        Type: LINUX_CONTAINER

# CloudWatchEventルールを設定
  CloudWatchEventRule:
    Type: "AWS::Events::Rule"
    Properties:
      EventPattern:
        source:
          - aws.codecommit
        detail-type:
          - CodeCommit Repository State Change
        resources:
          - !Join 
            - ''
            - - 'arn:aws:codecommit:'
              - !Ref AWS::Region
              - ':'
              - !Ref AWS::AccountId
              - ':'
              - !Ref CodeCommitRepositoryName
        detail:
          event:
            - referenceCreated
            - referenceUpdated
          referenceType:
            - branch
          referenceName:
            - master
      Targets:
        - Arn: !Join 
          - ''
          - - 'arn:aws:codepipeline:'
            - !Ref AWS::Region
            - ':'
            - !Ref AWS::AccountId
            - ':'
            - !Ref BlogPipeline
          RoleArn: !GetAtt CloudWatchEventIamRole.Arn
          Id: codepipeline-BlogPipeline

# アーティファクト保存用バケットを作成
  PipelineArtifactStoreBucket:
    Type: "AWS::S3::Bucket"

# バケットポリシーを作成
  PipelineArtifactStoreBucektPolicy:
    Type: "AWS::S3::BucketPolicy"
    Properties:
      Bucket: !Ref PipelineArtifactStoreBucket
      PolicyDocument:
        Version: 2012-10-17
        Statement:
          - Sid: DenyUnEncryptedObjectUploads
            Effect: Deny
            Principal: '*'
            Action: s3:PutObject
            Resource: !Sub arn:aws:s3:::${PipelineArtifactStoreBucket}/*
            Condition:
              StringNotEquals:
                's3:x-amz-server-side-encryption': aws:kms
          - Sid: DenyInsecureConnections
            Effect: Deny
            Principal: '*'
            Action: s3:*
            Resource: !Sub arn:aws:s3:::${PipelineArtifactStoreBucket}/*
            Condition:
              Bool:
                'aws:SecureTransport': false

# CodePipelineを設定
  BlogPipeline:
    Type: "AWS::CodePipeline::Pipeline"
    Properties:
      Name: !Ref PipelineName
      RestartExecutionOnUpdate: false
      RoleArn: !GetAtt PipelineIamRole.Arn
      Stages:
        - Name: Source
          Actions:
            - Name: SourceAction
              ActionTypeId:
                Category: Source
                Owner: AWS
                Provider: CodeCommit
                Version: 1
              OutputArtifacts:
                - Name: SourceOutput
              Configuration:
                BranchName: master
                RepositoryName: !Ref CodeCommitRepositoryName
                PollForSourceChanges: false
              RunOrder: 1
        - Name: Build
          Actions:
            - Name: CodeBuild
              InputArtifacts:
                - Name: SourceOutput
              ActionTypeId:
                Category: Build
                Owner: AWS
                Version: 1
                Provider: CodeBuild
              Configuration:
                ProjectName: !Ref BuildProjectName
              RunOrder: 1
      ArtifactStore:
        Location: !Ref HugoS3BucketName
        Type: S3

スタック作成時、パラメータで4つの値を入力します。

  • BuildProjectName = CodeBuildのプロジェクト名
  • PipelineName = CodePipelineのパイプライン名
  • HugoS3BucketName = S3とCloudFrontを作成で入力したS3バケット名
  • CodeCommitRepositoryName = CodeCommitに作成したリポジトリ名

Lambda@Edgeを設定

最後にLambda@Edgeを設定します。こちらを設定しないと、サブフォルダにアクセスできません。

バージニア北部リージョンであることを確認 → ランタイムは「Node.js 10.x」を選択 → 「AWSポリシーテンプレートから新しいロールを作成」を選択 → ポリシーテンプレート - オプションから「基本的なLambda@Edgeのアクセス権限(CloudFrontトリガーの場合)」を選択 → 「関数の作成」をクリック

コードを下記のように編集し、「保存」をクリックします。

'use strict';
exports.handler = (event, context, callback) => {
     
    // Extract the request from the CloudFront event that is sent to Lambda@Edge 
    var request = event.Records[0].cf.request;
 
    // Extract the URI from the request
    var olduri = request.uri;
 
    // Match any '/' that occurs at the end of a URI. Replace it with a default index
    var newuri = olduri.replace(/\/$/, '\/index.html');
     
    // Log the URI as received by CloudFront and the new URI to be used to fetch from origin
    console.log("Old URI: " + olduri);
    console.log("New URI: " + newuri);
     
    // Replace the received URI with the URI that includes the index page
    request.uri = newuri;
     
    // Return to CloudFront
    return callback(null, request);
 
};

アクションから「Lambda@Edgeへのデプロイ」を選択します。

「Lambda@Edgeへのデプロイを確認」にチェックし、「デプロイ」をクリックすれば完了です。