Igor Sokolov's Blog

IT blog

How to start you own blog using Hugo and Amazon S3

Posted at — May 8, 2019

This tutorial will walk you through how to setup and deploy Hugo blog in AWS using all features like HTTP/2, HTTPS only access, CDN and many others.

At the end you will have:

Before to jump into steps a couple word about Hugo. Hugo is a static website generator. That’s one of quite a big number of generator nowadays (see for example a post on Medium with an overview actual for 2019). What makes static website generators different from the traditional CMS platforms like WorkPress or Drupal is that you typically need only static hosting (read no Application Server) to keep your website running.

If you haven’t purchased domain it’s the time look at NameCheap or GoDaddy. From here and below assume the blog will be hosted at example.com domain, so please replace it down the road with your domain name.

  1. Install Hugo(see Quick Start).

  2. Choose the theme (for example, I’ve selected the current using filter all themes with a ‘blog’ tag)

  3. Now create a new directory for the blog:

    $ hugo new site example
    Congratulations! Your new Hugo site is created in /Users/user/Documents/hugo/example.
    ...
    
  4. It makes sense to initialize git repository even before any website is published (make sure git is installed and configured). This is also required if you want to use git submodule tool for themes. To do that type in console in the website root directory:

    $ git init
    Initialized empty Git repository in /Users/user/Documents/hugo/example/.git/
    
  5. You’ll probably want to create a .gitignore file right away (in order to ask git to ignore all generated by Hugo file). For this purpose we can use a template from GitHub:

    $ wget -O .gitignore https://raw.githubusercontent.com/github/gitignore/master/community/Golang/Hugo.gitignore
    --2019-05-09 09:25:52--  https://raw.githubusercontent.com/github/gitignore/master/community/Golang/Hugo.gitignore
    Resolving raw.githubusercontent.com... 151.101.200.133
    Connecting to raw.githubusercontent.com|151.101.200.133|:443... connected.
    HTTP request sent, awaiting response... 200 OK
    ...
    $ git add .gitignore
    

    if you use macOS the you would need add .DS_Store to the just downloaded file:

    $ echo ".DS_Store" >> .gitignore
    
  6. Then download the chosen theme (for instance, I picked this minimal style theme):

    $ git submodule add https://github.com/vividvilla/ezhil.git themes/ezhil
    
  7. Update config.toml as it suggested on the theme page.

  8. I prefer using Visual Studio Code and I wasn’t surprised to see an extension for Hugo. After its installation there is a nice option to invoke major Hugo commands right from VS Code: Cmd+Shift+P (called Command Palette) then start typing ‘hugo’.

  9. Create the first blog file calling ‘Hugofy: New Post’ from Command Palette with a name hugo-post.md (you don’t need to specify posts folder as it is required for CLI command)

  10. Write a text under the header. If you experience an issue with markdown code blocks in a list just don’t forget to tab the whole block as in example here.

  11. Now, start the Hugo server with drafts enabled:

    $ hugo server -D
                    | EN  
    +------------------+----+
    Pages            |  7  
    Paginator pages  |  0  
    Non-page files   |  0  
    Static files     |  4  
    Processed images |  0  
    Aliases          |  0  
    Sitemaps         |  1  
    Cleaned          |  0  
    
    Total in 13 ms
    Watching for changes in /Users/user/Documents/hugo/example/{content,data,layouts,static,themes}
    Watching for config changes in /Users/user/Documents/hugo/example/config.toml
    Environment: "development"
    Serving pages from memory
    Running in Fast Render Mode. For full rebuilds on change: hugo server --disableFastRender
    Web Server is available at http://localhost:1313/ (bind address 127.0.0.1)
    Press Ctrl+C to stop
    
  12. Open browser with URL http://localhost:1313/ as it was suggested above and check the website works.

  13. Now it is time to add files to git repository:

    $ git status
    On branch master
    
    No commits yet
    
    Changes to be committed:
    (use "git rm --cached <file>..." to unstage)
    
        new file:   .gitmodules
        new file:   themes/ezhil
    
    Untracked files:
    (use "git add <file>..." to include in what will be committed)
    
        .gitignore
        archetypes/
        config.toml
        content/
    
    $ git add .gitignore archetypes/* config.toml content/*
    $ git status
    On branch master
    
    No commits yet
    
    Changes to be committed:
    (use "git rm --cached <file>..." to unstage)
    
        new file:   .gitignore
        new file:   .gitmodules
        new file:   archetypes/default.md
        new file:   config.toml
        new file:   content/post/hugo-post.md
        new file:   themes/ezhil
    
  14. And commit:

    $ git commit -m "Initial commit"
    [master (root-commit) 4ec9563] Initial commit
    6 files changed, 155 insertions(+)
    create mode 100644 .gitignore
    create mode 100644 .gitmodules
    create mode 100644 archetypes/default.md
    create mode 100644 config.toml
    create mode 100644 content/post/hugo-post.md
    create mode 160000 themes/ezhil
    
  15. Request an SSL certificate. Follow the AWS docs to request a public certificate for your domain. Some important highlights are:

    • Add example.blog and *.example.blog to the certificate.
    • Use DNS validation (rather than email validation)

    A critical point is from ACM service documentation:

    To use an ACM Certificate with CloudFront, you must request or import the certificate in the US East (N. Virginia) region.

    i.e., change region to US East N. Virginia if needed (top right corner within the AWS admin console).

  16. Many others guidance cross the Internet usually suggest creation manually S3 and CloudFront, of course it is not a big deal however the current guidance make leverage capabilities of a great tool s3_website tool.

  17. First it should be installed s3_website and configuration files created in the project root directory:

    $ gem install s3_website
    $ s3_website cfg create
    
  18. Then update just created s3_website.yaml file to be as presented below:

    s3_id: <%= ENV['S3_ID'] %>
    s3_secret: <%= ENV['S3_SECRET'] %>
    s3_bucket: <%= ENV['S3_BUCKET'] %>
    
    # Below are examples of all the available configurations.
    # See README for more detailed info on each of them.
    
    site: public
    
    index_document: index.html
    error_document: 404.html
    
    max_age: 120
    gzip:
    - .html
    - .css
    - .md
    - .cs
    - .xml
    - .json
    #   - .md
    # gzip_zopfli: true
    
    cache_control:
    "css/*": public, max-age=3600
    "font/*": public, max-age=3600
    "js/*": public, max-age=3600
    
    # See http://docs.aws.amazon.com/general/latest/gr/rande.html#s3_region for valid endpoints
    s3_endpoint: us-east-1
    
    # ignore_on_server: that_folder_of_stuff_i_dont_keep_locally
    
    exclude_from_upload:
    - .DS_Store # delete macOS stuff
    
    #   - those_folders_of_stuff
    #   - i_wouldnt_want_to_upload
    s3_reduced_redundancy: true
    
    #cloudfront_distribution_id: <%= ENV['CLOUDFRONT_ID'] %>
    
    cloudfront_distribution_config:
    default_cache_behavior:
        min_ttl: <%= 60 * 60 * 24 %>
    http_version: http2
    aliases:
    quantity: 1
    items:
        - example.com
        - www.example.com
    
    # cloudfront_invalidate_root: true
    
    cloudfront_wildcard_invalidation: true
    
    # concurrency_level: 5
    
    # redirects:
    #   index.php: /
    #   about.php: about.html
    #   music-files/promo.mp4: http://www.youtube.com/watch?v=dQw4w9WgXcQ
    
    # routing_rules:
    #   - condition:
    #       key_prefix_equals: blog/some_path
    #     redirect:
    #       host_name: blog.example.com
    #       replace_key_prefix_with: some_new_path/
    #       http_redirect_code: 301
    

    What the suggested configuration does:

    • move all sensitive information out of the file (see ENV[‘key’] macros)
    • explain where to find the generated static website (site attribute )
    • ask to use gzip for files defines by their extensions (gzip attribute)
    • provide custom HTTP Cache Control configuration for browsers (cache_control)
    • ignore macOS autogenerated files (exclude_from_upload)
    • the reduced redundancy S3 storage type is used because all files can be easily restored from git/Hugo commands so high durability is not required but it might save a couple of cents (s3_reduced_redundancy).
    • adjust the default CloudFront cache behavior (min_ttl)
    • HTTP/2 is enabled (http_version)
  19. The next is to create AIM user with Programmatic access for s3_website, since the aim is to delegate S3 and CloudFront creation to s3_website AIM user should have full access to S3 bucket and CloudFront. A guidance Creating an IAM User in Your AWS Account can be used.

  20. Create .env file in the project directory and fill it with content below and AIM user credentials:

    S3_ID=<client_ID>
    S3_SECRET=<client_secret>
    S3_BUCKET=example.com
    CLOUDFRONT_ID=<leave empty for now>
    

    Since the file contains sensitive information it should be also add to .gitignore:

    $ echo ".env" >> .gitignore
    
  21. Now it is time to create magically S3 bucket and CloudFront distribution:

    $ s3_website cfg apply
    Applying the configurations in s3_website.yml on the AWS services ...
    Created bucket example.com in the us-east-1 Region
    Bucket example.com now functions as a website
    No redirects to configure for example.com bucket
    Bucket example.com is now readable to the whole world
    Do you want to deliver your website via CloudFront, Amazon’s CDN service? [y/N]
    y
    The distribution <CF Distribution ID> at <CF prefix ID>.cloudfront.net now delivers the origin help42.blog.s3-website-us-east-1.amazonaws.com
        Please allow up to 15 minutes for the distribution to initialize
        For more information on the distribution, see https://console.aws.amazon.com/cloudfront
    Added setting 'cloudfront_distribution_id: <CF Distribution ID>' into ./s3_website.yml
    Applied custom distribution settings:
        default_cache_behavior:
        min_ttl: 86400
        http_version: http2
    
  22. Open AWS console and update just created CloudFront distribution to redirect from HTTP => HTTPS and use generated above SSL certificate from AWS ACM.

  23. Go back to .env file and replace the placeholder with created CloudFront distribution ID.

  24. Everything ready to publish the blog:

    $ s3_website push
    [info] Deploying /Users/user/Documents/hugo/example/public/* to example.com
    [warn] Overriding the max_age setting with the cache_control setting
    [warn] Overriding the max_age setting with the cache_control setting
    [warn] Overriding the max_age setting with the cache_control setting
    [warn] Overriding the max_age setting with the cache_control setting
    [succ] Created index.xml (max-age=120 | application/rss+xml | gzip | REDUCED_REDUNDANCY)
    [succ] Created js/main.js (public, max-age=3600 | application/javascript | REDUCED_REDUNDANCY)
    [succ] Created sitemap.xml (max-age=120 | application/xml | gzip | REDUCED_REDUNDANCY)
    [succ] Created post/index.xml (max-age=120 | application/rss+xml | gzip | REDUCED_REDUNDANCY)
    [succ] Created index.html (max-age=120 | text/html; charset=utf-8 | gzip | REDUCED_REDUNDANCY)
    [succ] Created tags/hugo/index.html (max-age=120 | text/html; charset=utf-8 | gzip | REDUCED_REDUNDANCY)
    [succ] Created tags/index.html (max-age=120 | text/html; charset=utf-8 | gzip | REDUCED_REDUNDANCY)
    [succ] Created tags/index.xml (max-age=120 | application/rss+xml | gzip | REDUCED_REDUNDANCY)
    [succ] Created post/hugo-post/index.html (max-age=120 | text/html; charset=utf-8 | gzip | REDUCED_REDUNDANCY)
    [succ] Created css/normalize.css (public, max-age=3600 | text/css; charset=utf-8 | gzip | REDUCED_REDUNDANCY)
    [succ] Created css/main.css (public, max-age=3600 | text/css; charset=utf-8 | gzip | REDUCED_REDUNDANCY)
    [succ] Created post/index.html (max-age=120 | text/html; charset=utf-8 | gzip | REDUCED_REDUNDANCY)
    [succ] Created tags/hugo/index.xml (max-age=120 | application/rss+xml | gzip | REDUCED_REDUNDANCY)
    [succ] Created js/feather.min.js (public, max-age=3600 | application/javascript | REDUCED_REDUNDANCY)
    [info] Summary: Created 14 files. Transferred 83.6 kB, 266.0 kB/s.
    [info] Successfully pushed the website to http://example.com.s3-website-us-east-1.amazonaws.com
    
  25. Open the website https://example.blog/ and check if it works.

  26. Commit to git what it is done for now:

    $ git status
    On branch master
    Changes not staged for commit:
    (use "git add <file>..." to update what will be committed)
    (use "git checkout -- <file>..." to discard changes in working directory)
    
        modified:   .gitignore
    
    Untracked files:
    (use "git add <file>..." to include in what will be committed)
    
        .vscode/
        s3_website.yml
    
    no changes added to commit (use "git add" and/or "git commit -a")   
       
    $ git add .vscode/ s3_website.yml
    $ git commit -m "Configured s3_website tool"
    
  27. Go to Google Analytics homepage and setup a new property according to the Help page. After creation you will be automatically redirected to Tracking Info / Tracking Code page. The value Tracking ID should be copied and used in config.toml file.

  28. Go to Disqus homepage and configure an instance using a guide: How to install Disqus on Hugo?. Update config.toml using Shortname from Configure Disqus for Your Site page.

  29. Go to Google Search Console and register your property. The recommended way to proof ownership is to use DNS validation: just copy required TXT value and go to Route53, create another record set with TXT type and paste the value from Google Search Console. It might take a while (about 5 minutes in my case) to propagate DNS records.

  30. To kick off Google crawler to index the web site go to Sitemaps navigation menu item and type the path to sitemap.xml generated by Hugo: https://example.com/sitemap.xml

comments powered by Disqus