テックブログ2022-07-14

LAPRAS Tech Blogのアーキテクチャ(インフラ編)

yktakaha4

LAPRAS株式会社 でSREをしている、yktakaha4 ともうします🐧
今回は本ブログ初めての記事を記念(?)して、LAPRAS Tech Blogの公開基盤のアーキテクチャについてご紹介したいと思います

どのような基盤を作るか


そもそもこのブログは、弊社のDevRel活動の一貫でおこなっている LAPRASモブプロ生配信 という動画の一企画として作り始められたものでした



この動画の中ではブログの骨子をNext.js + TDDで作るというところまでで時間切れとなってしまい、残りのタスクは持ち越しとなったのですが、
私の会社での主な役割がSRE兼インフラエンジニアという感じだったこともあり、改めて公開基盤について検討することとしました

構築にあたっては、以下のようなことを考えました

  • OSやミドルウェアのアップデートといった作業を極力おこなわずに済む技術を使いたい
  • 構築後改修の機会があまりなさそうなので、時間が経ってから構成がわからなくならないようにしたい
  • 低コストに運用したい


メインプロダクトである LAPRASLAPRAS SCOUT はKubernetes上で稼働していますが、今回は静的ページを可用性高く配信することが最終的な目的のため、より適した配信方法を選定したいと考えていました
また、このテックブログ開発の意図として 普段使っていない技術の素振り をするということも元々あったので(例えば、弊社はVue.jsをメインで使っていますが本ブログはReactベースのNext.jsを使っています)、インフラ担当としても気になっていたサービスを利用する機会にできるとよさそうです🐄

作ったもの


最終的な配信基盤のアーキテクチャは以下のようになりました
ページの公開には AWS AmplifyTerraform で管理し、記事データの保持は microCMS 、更新ワークフローは GitHub Actions を利用しています



ひとつずつ紹介していきます

AWS Amplify + Terraform


Amplifyはわずかな設定のみでWebアプリケーションを構築できるAWSサービスです
様々な機能が提供されていますが、今回は静的ページの公開ということで特に ホスティング関連の機能 を利用しています

なお、選定にあたっては以下も検討しました

  • Vercel
    • Next.jsの開発元ということもあり機能的にも充分に思えましたが、コスト面を考えたときにAWS Amplifyの方が優位性があった
  • Amplify CLIから作成
    • 弊社ではインフラのコード管理にTerraformを全面的に利用しており、また 公式のプロバイダ でも対応していたことから利用する積極的理由がなかった
  • CloudFront + S3をTerraformで作成
    • Amplifyも裏では同等の基盤で配信をおこなっているらしいが、Amplify固有の機能(BASIC認証が簡単な設定でつけられたり、ブランチ毎に固有の環境を自動生成してくれたり…など)が魅力的だった


Terraformの設定のうち、関連するものを抜粋します

resource "aws_amplify_app" "blog" {
  name       = "blog"
  repository = "https://github.com/xxx/blog"
  platform   = "WEB"

  enable_branch_auto_build    = true
  enable_branch_auto_deletion = true

  enable_auto_branch_creation = true
  auto_branch_creation_patterns = [
    "**",
  ]

  auto_branch_creation_config {
    stage                       = "DEVELOPMENT"
    enable_auto_build           = true
    enable_pull_request_preview = true
    enable_basic_auth           = true
    basic_auth_credentials      = base64encode("${var.branch_basic_auth_user_name}:${var.branch_basic_auth_password}")
    environment_variables = {
      ENV = "develop"
    }
  }

  custom_rule {
    source = "/<*>"
    status = "404"
    target = "/"
  }

  access_token = var.github_personal_access_token
  lifecycle {
    ignore_changes = [
      # 初期構築時に利用
      # 更新はAmplifyのGUIからOAuth認証にて実施してください
      # https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/amplify_app#repository-with-tokens
      access_token,
      # blogのリポジトリ側のamplify.ymlが自動的に参照される
      build_spec,
    ]
  }

  # 本番環境はBASIC認証不要
  enable_basic_auth      = false
  basic_auth_credentials = base64encode("${var.branch_basic_auth_user_name}:${var.branch_basic_auth_password}")
}

resource "aws_amplify_branch" "main" {
  app_id      = aws_amplify_app.blog.id
  branch_name = "main"

  stage = "PRODUCTION"

  environment_variables = {
    ENV = "production"
  }
}


上述の設定で xxx/blog リポジトリの main ブランチの内容が本番環境、それ以外のブランチが開発環境となります
加えて、本番環境以外にはBASIC認証を設定しているのと、存在しないURLにアクセスがあった場合に自動的にトップページにリダイレクトします

GitHub上にfeatureブランチがpushされたり、メインブランチへのマージがおこなわれると、



Amplify側にブランチ状態が同期され、自動的に環境へのデプロイが走ります
いい感じ🐏



なお、本来は PRに対してプレビュー画面へのリンクがコメントされる 仕様のようなのですが、少し試したもののうまく動作してくれなかったので、少々強引ですがGitHub ActionsからPR作成時にコメントすることにしました
ワークフローは以下のような感じです

name: Notify Amplify Branch
on:
  pull_request:
    types:
      - opened
jobs:
  notify_amplify_branch:
    timeout-minutes: 3
    runs-on: ubuntu-20.04
    steps:
      - uses: actions/github-script@v3
        env:
          BRANCH_NAME: ${{ github.head_ref }}
        with:
          github-token: ${{secrets.GITHUB_TOKEN}}
          script: |
            const appId = "xxxxxxxx";
            const branchName = process.env.BRANCH_NAME.replace(/\//g, "-");
            const commentBody = [
              "🐧検証環境は以下から確認できます(ビルド完了まで表示されません)",
              `https://${branchName}.${appId}.amplifyapp.com/`,
              "",
              "🍉ビルドの実行状況は以下から確認してください",
              `https://ap-northeast-1.console.aws.amazon.com/amplify/home?region=ap-northeast-1#/${appId}/${branchName}`,
            ].join("\n");

            await github.issues.createComment({
              issue_number: context.issue.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
              body: commentBody,
            })


デプロイのワークフローについては、 専用の書式のyamlファイルを定義する 必要があります
Terraform側のリソース設定で定義することもできますが、blogのソースコードが入っているリポジトリの直下に amplify.yml というファイル名で格納しておくと、自動的にそちらを読んでくれます
内容としてはそれほど難しいことはなく、Next.jsであれば next buildnext export などのコマンドを発行し、 artifacts.baseDirectory に最終的に環境にデプロイしたいファイル群の格納されているディレクトリを指定すれば、自動的にその内容をホスティングしてくれます

version: 1
frontend:
  phases:
    preBuild:
      commands:
        - yarn install --frozen-lockfile
    build:
      commands:
        - yarn build
        - yarn export
  artifacts:
    baseDirectory: out
    files:
      - "**/*"
  cache:
    paths:
      - node_modules/**/*


また、今回ブログにカスタムドメインを設定していますが、これについてもTerraformで実現できます
aws_amplify_domain_association というリソースがあるのでこちらを使います

variable "lapras_tech_blog_target_domain_name" {
  type = string
  # Amplifyのドメイン管理画面から指定されたものを設定
  default = "xxxxx.cloudfront.net"
}

resource "aws_route53_record" "tech_blog" {
  zone_id = var.lapras_com_zone_id
  name    = "tech-blog"
  type    = "CNAME"
  ttl     = "300"

  records = [
    var.lapras_tech_blog_target_domain_name,
  ]
}

resource "aws_route53_record" "www_tech_blog" {
  zone_id = var.lapras_com_zone_id
  name    = "www.tech-blog"
  type    = "CNAME"
  ttl     = "300"

  records = [
    var.lapras_tech_blog_target_domain_name,
  ]
}

resource "aws_amplify_domain_association" "blog" {
  # 本リソースの作成時に Route53に _xxxxxxxx.tech-blog.lapras.com のCNAMEレコードが自動作成されている
  # tfファイルの削除をしてもこちらは消滅しないようなので、もしも削除の必要が発生した場合は手で消すこと
  app_id                = aws_amplify_app.blog.id
  domain_name           = "tech-blog.lapras.com"
  wait_for_verification = true

  sub_domain {
    branch_name = aws_amplify_branch.main.branch_name
    prefix      = ""
  }

  sub_domain {
    branch_name = aws_amplify_branch.main.branch_name
    prefix      = "www"
  }
}


microCMS


公開基盤ができたので、次はブログ記事データの管理です
本ブログは ブログ外部で公開しているアプトプットへのリンク集本ブログのみで閲覧できるオリジナル記事 を公開していますが、オリジナル記事の管理についてmicroCMSを利用しています

microCMSは、今回のようなブログの記事管理に限らず、汎用的なCRUDができるAPIをノーコードで作成できるサービスです
無料のHobbyプランであっても商用利用が可能であることから、なにかを小さく始めるのにとても適しているように感じました



記事の執筆用ページについても簡単にご紹介します
WPのような リッチエディタでの記事執筆 にも対応しており、非エンジニアでない方でも直感的に執筆することができそうです🦨



また、今回利用しているGitHub Actionsとの親和性も高く、Webhookによって repository_dispatchイベント を発火させることができます
これについては後節で説明します

CloudFrontによる画像キャッシュ


microCMSは記事データに限らず画像等のバイナリファイルを公開する機能がありますが、従量課金の データ転送量制限 があるため、手前にCDNを挟めば転送量超過を抑止できそうです
今回は、microCMSに格納している画像データをCloudFront経由で配信するようにTerraformを設定しました

resource "aws_cloudfront_distribution" "assets" {
  enabled     = true
  comment     = "assets.tech-blog.lapras.com"
  price_class = "PriceClass_200"
  aliases     = ["assets.tech-blog.lapras.com"]

  origin {
    origin_id   = "images.microcms-assets.io"
    domain_name = "images.microcms-assets.io"
    # xxxxx はアカウントごとに固定IDとなる模様
    origin_path = "/assets/xxxxx"

    custom_origin_config {
      origin_protocol_policy = "https-only"
      http_port              = "80"
      https_port             = "443"
      origin_ssl_protocols   = ["TLSv1.2"]
    }
  }

  default_cache_behavior {
    allowed_methods  = ["GET", "HEAD", "OPTIONS"]
    cached_methods   = ["GET", "HEAD", "OPTIONS"]
    target_origin_id = "images.microcms-assets.io"
    compress         = true

    forwarded_values {
      # 画像APIにより返却されるオブジェクトが変わりうるため、クエリストリング毎にキャッシュする
      # https://document.microcms.io/image-api/introduction
      query_string = true

      cookies {
        forward = "none"
      }
    }

    viewer_protocol_policy = "redirect-to-https"
    min_ttl                = 86400
    default_ttl            = 86400
    max_ttl                = 86400
  }

# その他、アクセスログやACMの設定など

}


画像にアクセスしてみると、ちゃんと Hit from cloudfront と出ておりキャッシュできていそうです


GitHub Actions


最後に、GitHub Actionsを用いた記事の更新方法について触れます

前述した通り、ブログの内容としては公開アウトプットとmicroCMSで管理しているオリジナル記事があるので、それぞれ記事が生まれるライフサイクルが違います
公開アウトプットについては各サービスのRSSフィードに対して定期的にアクセスして取得する必要があり、対してmicroCMSの記事については、更新のタイミングで環境に反映したいです

これについて、今回はブログ記事の元となる情報をリポジトリにコミットするワークフローを作って、以下をトリガーに実行する仕様としました

  • 0時頃に日次実行
  • microCMSから記事更新時に実行
  • GitHubのActionsの画面より手動実行


ワークフローのサンプルを示します
yarn build:posts コマンドを実行すると、posts/ ディレクトリに置かれた記事情報JSONファイルが更新されるイメージです

name: Update posts

on:
  schedule:
    - cron: "5 15 * * *"
  workflow_dispatch:
  repository_dispatch:

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

jobs:
  update_posts:
    timeout-minutes: 10
    runs-on: ubuntu-20.04
    steps:
      - uses: actions/checkout@v2
      - uses: borales/actions-yarn@v3.0.0
        with:
          cmd: install
      - run: yarn build:posts
        env:
          GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }}
          MICROCMS_API_KEY: ${{ secrets.MICROCMS_API_KEY }}
      - run: |
          echo "----- set config -----"
          git remote set-url origin "https://$GH_USER:$GH_PAT@github.com/xxx/blog.git"
          git config --local user.email "$GH_EMAIL"
          git config --local user.name "$GH_USER"
          echo "----- check diff -----"
          if [[ "$(git diff -s --exit-code posts/ || echo "diff")" = "diff" ]]; then
            echo "----- push commit -----"
            git add posts/
            git commit -m "Update posts"
            git push origin HEAD
          fi
        env:
          GH_USER: xxx
          GH_PAT: ${{ secrets.GH_PAT }}
          GH_EMAIL: xxx@example.com


このワークフローの動作によってリポジトリのメインブランチに記事情報をコミットすることで、Amplify側で自動デプロイを走らせる構造になっています
開発当初は posts/ ディレクトリの内容はリポジトリに入れないように考えていたのですが、
ローカル環境環境の検討やデプロイ中のAPIの一時的なエラーに対して対処する…といったことをしているうちに、リポジトリに含めてしまった方がメリットが高いという結論になりこのような設計になりました

ポイントとしては、(例えばmicroCMSで公開操作が連続した場合などで)ワークフローが並列実行されると困るため、 concurrency の設定をおこなっています
これによって、近いタイミングでジョブが並列実行されそうになったとき、古い方の実行がただちにキャンセルされコミットの書き合いが起きる…といったことを防げます


今後の改善


現時点で、例えば以下のようなことには対応できていないので、順次やっていきたいと思います

  • 画像配信の効率化・可用性向上
    • microCMSの、画像APIを利用 してより最適化された画像を配信できるようにする
    • (外部サービスの影響を受けづらくするという観点から、)画像を一度S3に保存した上でCloudFront経由で配信する
  • 下書き情報の表示
    • microCMSには画面プレビュー機能があるので、動的にコンテンツ表示可能なエンドポイントを作成すれば下書き状態の記事を即時確認できそう
  • いい加減にエイヤでやっちゃった部分のfix
    • 前述したAmplifyから通知が来ない問題とか...


また、弊社ではエンジニアを募集していますので、ご興味があればぜひカジュアル面談しましょう

それでは🐇

このエントリーをはてなブックマークに追加