NTT DATA TECH
📝

Markdownで書いた社内ドキュメント、どう共有してる? AWSで構築するセキュアな自動公開パイプライン

に公開

はじめに

はじめまして。NTTデータの奥村です。

最近は内部向けのちょっとしたドキュメントをMarkdown形式で記載することは結構あるのかなと思います。GitHubなどのリポジトリサービスやNotionなどのドキュメント共有サービスを全員が利用できればよいのですが、コストや組織のポリシーなどの関係でそうしたサービスが利用できないケースだと、関係者限定でMarkdownのファイルを見やすい形で共有するのに困ることがあります。
本記事では、上記のようなケースで役に立つ仕組みをAmazon Web Services (AWS)を利用して構築する方法を紹介させていただきます。

全体構成

全体構成
まず、ドキュメント執筆者は何らかのリポジトリサービスにMarkdownファイルを配置します。次に、CI/CDサービスのCodePipelineがビルドサービスのCodeBuildを呼び出し、静的サイトジェネレーターを実行してHTMLファイルを作成します。最後にs3 syncコマンドで、S3バケットにHTMLファイルを格納するという流れです。
S3バケットに格納されたファイルはCloudFrontにより、ドキュメント参照者へ公開されます。ただし、AWS WAFによるIPアドレス制限を行い、CloudFront Functionsによる簡易な基本認証を実装し、セキュリティレベルを上げる対策を行っています。

こういった構成を紹介する記事は過去にもいくつかあるとは思うのですが、この構成を作るにあたって論点となる箇所について、整理していきたいと思います。

論点

静的サイトジェネレーター

HTMLを生成する静的サイトジェネレーターは多数あり、どのツールを利用するかは最初に悩むポイントになるかと思います。ReactやVueなどのJavaScriptフレームワークと親和性が高い高機能な静的サイトジェネレーターもありますが、今回はMarkdownファイルをHTMLファイルへ変換するという機能だけに絞って、できるだけシンプルに実現できるものを2つ紹介させていただきます。
この要件でまず最初に候補に挙がるのはHugoかと思います。HugoはGo言語で作られた高速な静的サイトジェネレーターで、コンパイル済みのバイナリ実行ファイルをダウンロードし、それを実行するだけでMarkdownからHTMLへの変換ができるという非常にお手軽なところが特徴的です。他の静的サイトジェネレーターと比べてもビルドスピードが非常に高速な点も大きな魅力です。
もう一つ紹介したいのが、MkDocsです。MkDocsは、Python言語で作られたシンプルな静的サイトジェネレーターです。特にMaterial for MkDocsというテーマと組み合わせて利用されることが多く、簡単に高機能なデザインのサイトを構築することができます。MkDocsで特徴的なのが、静的サイトジェネレーターにHTMLファイルへの変換対象であることを伝えるFront Matter(例:ファイルの先頭に--- title: 記事タイトル ---のように記述するメタデータ)と呼ばれるMarkdownファイルの先頭に追記する記述が不要であることです。多くの静的サイトジェネレーターではFront Matterの記述が推奨されていますが、MkDocsではそれが不要です。MkDocsでは素のMarkdownファイルをそのまま配置するだけで自動的にHTMLファイルに変換してくれます。

それぞれの静的サイトジェネレーターで必要なファイル構成について紹介します。

Hugo

Hugoでのフォルダ構成例
.
├── archetypes
│   └── default.md
├── content
├── hugo.toml
├── layouts
│   ├── _default
│   │   ├── _markup
│   │   │   └── render-link.html
│   │   ├── list.html
│   │   └── single.html
│   └── index.html
├── public
└── themes
    └── book

テーマを利用することを前提にすれば、contentフォルダと設定ファイルを利用することも可能ですが、後述するリンク記法のため、「layouts/_default/_markup/render-link.html」のファイルは必要になると筆者は考えています。

  • archetypes/default.md:hugoコマンドでMarkdownファイルを作成するときに、ファイルの先頭に挿入されるFront Matterを定義するファイル
  • content:Markdownファイルを配置するフォルダ
  • hugo.toml:Hugoの設定ファイル
  • layouts:生成されるHTMLのテンプレートファイル
  • layouts/_default/_markup:HTMLファイル生成時の処理を定義するフォルダ
  • public:生成されたHTMLファイルの配置先フォルダ
  • themes:Webサイトのデザインを定義したテーマ関連のファイル配置場所
hugo.tomlの例
baseURL = 'https://CloudFrontのFQDN'
languageCode = 'ja-jp'
title = 'タイトル'
theme = 'book'

MkDocs

続いてMkDocsのフォルダ構成例についてみていきましょう。Hugoと比較して非常にシンプルなフォルダ構成となっていることがお分かりになるかと思います。

MkDocsでのフォルダ構成例
.
├── docs
└── mkdocs.yml
  • docs:Markdownファイルを配置するフォルダ
  • mkdocs.yml:MkDocsの設定ファイル
mkdocs.ymlの例
site_name: サイト名
theme:
  name: material             
  language: ja

Markdownファイル間のリンク記法

もう一つ、両者の使い勝手を大きく分けるのが、Markdownファイル間のリンクの書き方です。
ドキュメントを作成する際には、他のページへリンクを張ることが頻繁にあります。この内部リンクの挙動に、HugoとMkDocsでは明確な違いがあります。
MkDocsの場合、特別な設定をしなくても、Markdownファイルへの相対リンクを自動的に解決してくれます。例えば、docsフォルダ内にpage-a.mdとpage-b.mdがある場合、page-a.mdからpage-b.mdへは以下のように直感的にリンクを記述できます

markdownファイル
これはページAです。
[ページBへ](./page-b.md)

ビルドすると、このリンクは自動的にhref="../page-b.html"のような適切なHTMLリンクに変換されます。
一方、Hugoはデフォルトではこの自動変換を行いません。同じように記述しても、リンク先はhref="./page-b.md"のまま出力されてしまい、多くの場合リンク切れとなります。
この問題に対応するため、Hugoでは主に2つの方法があります。

  1. render-link.html(Render Hooks)を利用する
    layouts/_default/_markup/render-link.html というテンプレートファイルを作成し、リンクの描画処理をカスタマイズする方法です。このファイルに「リンク先が.mdで終わっていたら、URLをHugoが解決できる形式に書き換える」という処理を記述することで、MkDocsと同様の書き心地を実現できます。
    GitHubなどのリポジトリサービス上でもリンクが正しく機能し、ローカルのMarkdownエディタでもプレビューしやすくなるため、素のMarkdown記法のままドキュメントを管理したい場合には、この設定が事実上必須となります。
  2. ショートコード(ref / relref)を利用する
    Hugoが推奨する公式な方法で、以下のようにHugo独自の記法でリンクを記述します。
markdownファイル
[ページBへ]({{< relref "page-b.md" >}})

この方法のメリットは、リンク先のファイルが存在しない場合にビルドエラーで教えてくれるため、リンク切れを確実に防げる点です。しかし、この記法はHugo専用であり、他のツールでは解釈できません。

S3へのデプロイ方法

CodePipelineを利用する場合は、CodeBuildでビルドしたファイルをアーティファクト用のS3バケットに配置し、CodeDeployで最終成果物用のS3バケットへデプロイする構成を検討することが多いと思います。ただし今回のようなMarkdownからHTMLに変換するという単純な処理の場合は、わざわざアーティファクト用のS3バケットを介さずに、直接最終成果物用のS3バケットへ配置する構成のほうがシンプルです。この場合はCodeBuildでS3バケットへ配置することになります。しかし、CodeBuildのアーティファクト出力機能では、意図しないフォルダ構造で出力される場合があり、S3バケットのルートディレクトリにファイルを配置するには一手間必要です。そのため、AWS CLIのaws s3 syncコマンドを使い、CodeBuildからS3バケットへ配置することを考えます。
パイプライン
CodeBuildのアーティファクト出力機能

buildspec.ymlの例
phases:
  build:
    commands:
      - hugo
  post_build:
    commands:
      - aws s3 sync ./public s3://your-bucket-name --delete # --deleteオプションにより、コピー元にないファイルはS3バケットから削除され、常に最新の状態が保たれます。

セキュリティ対策

今回はちょっとした内部利用を想定しているため、どの程度のセキュリティ対策が必要かも一つの検討ポイントになるかと思います。安全に公開するため、以下の対策を実施することを推奨します。

  • IPアドレス制限
  • Basic認証

今回はAWS WAFによってIPアドレス制限を行い、CloudFront FunctionsでBasic認証を行う方法を紹介します。公式ドキュメントの手順に従い、IP Setのルールを追加し、Default web ACL action for requests that don't match any rulesをBlockに設定することで、許可したIPアドレスからのみの通信に制限することが出来ます。
WAFによるIPアドレス制限の設定例

またCloudFront Functionsでは下記のようなコードで認証をかけることが可能です。今回は簡単のためにユーザ名とパスワードをBase64エンコードした値を設定していますが、セキュリティレベルを上げたいケースでは、CloudFront Functionsではなく、Lambda@Edgeを利用し、AWS Secrets Managerと連携することも検討してください。(補足:Basic認証は手軽な一方、認証情報がコード内に含まれ、Base64エンコードも容易にデコード可能です。より高いセキュリティが求められる場合は、Lambda@EdgeとSecrets Managerの連携が推奨されます。)
CloudFront Functionsはネットワークアクセスが制限されているため、APIを利用した各AWSサービスへの接続が原則できませんので、ご注意ください。

basic-auth
function handler(event) {
    var request = event.request;
    var headers = request.headers;
    var uri = request.uri;

    // --- 設定ここから ---
    // 'username:password' をBase64エンコードした値を設定
    // 例: 'user:pass' -> 'dXNlcjpwYXNz'
    var expectedAuthHeader = 'Basic dXNlcjpwYXNz'; 
    // --- 設定ここまで ---

    if (typeof headers.authorization === 'undefined' || headers.authorization.value !== expectedAuthHeader) {
        var response = {
            statusCode: 401,
            statusDescription: 'Unauthorized',
            headers: {
                'www-authenticate': { value: 'Basic realm="Restricted Area"' }
            }
        };
        // 認証失敗なら、ここで処理を終了して401応答を返す
        return response;
    }
    return request;
}

この作成したCloudFront FunctionsをViewer Request(CloudFrontがユーザーからのリクエストを受け取った直後のイベント)に設定することで、オリジン(S3)にリクエストが到達する前の、よりユーザーに近い段階で認証をかけることができます。

まとめ

本記事では内部向けのMarkdownファイルを公開するための仕組みとその仕組みの中で論点となる箇所について説明いたしました。本記事が皆様のお役に立てば幸いです。

仲間募集中です!

NTTデータ クラウド&データセンタ事業部では、以下の職種を募集しています。

  1. プライベートクラウドコンサル/エンジニア
  2. デジタルワークスペース構築/新規ソリューション開発におけるプロジェクトリーダー
  3. IT基盤(パブリッククラウド、プライベートクラウド)エンジニア
  4. パブリッククラウド/プライベートクラウドを用いた大規模プロジェクトをリードするインフラエンジニア

ソリューション紹介

  1. クラウドプロフェッショナルサービス/クラウドインテグレーションサービス/クラウドマネージドサービス/パートナークラウドサービス
  2. OpenCanvas
NTT DATA TECH
NTT DATA TECH

Discussion