AWS

Material for MkDocs をAWS CDKとGitHub Actionsで公開する

yassan

当社は夏合宿が無事に終了し、ひと夏の思い出となってしまいました。やっさんでございます。

この度、Immersion Daysコンテンツを作成するのに利用しているMaterial for MKDocsの公開環境をAWS CDKとGitHub Actionsで共通化しましたので、その内容をご紹介します。

当社におけるMaterial for MKDocsの利用シーンについて

当社はAmazon Web Services様(以下AWS)のImmersion Daysコンテンツを作成する機会が度々あります。Immersion Daysコンテンツはスライド資料はもちろんのこと、ワークショップコンテンツを準備し参加者の皆様に実際にAWSを使って頂く機会を提供しております。

当社ではワークショップコンテンツの手順書を作成する際にオープンソースソフトウェア(OSS)のMaterial for MKDocsを利用しておりまして、手順書を公開する環境として Amazon CloudFront と CloudFront Functions を活用しております。

Material for MKDocs 公開環境の共通化

上記の利用シーンの背景がありワークショップコンテンツの構築頻度が増えておりまして、IaC化とGitHub Actionsによる自動デプロイの仕組みを用いることで公開環境の共通化を図りました。本記事ではこの公開環境の共通化について具体的に紹介します。

サンプルのソースコードはGitHubにございます。

AWS CDKを利用した公開環境のプロビジョニング

CloudFrontを活用した手順書の公開環境はAWS CDKによって簡単にデプロイ出来るようにしました。AWS CDKで構築するAWSリソースは以下が含まれます。

AWS リソース種別説明
Amazon S3 バケットコンテンツのオリジンを保存するS3バケット。
MKDocsでビルドされた静的ファイルを配置します。
Amazon S3 バケットポリシーCloudFrontのディストリビューションから
アクセス可能となるよう設定するバケットポリシー。
Amazon CloudFront オリジンアクセスコントロールCloudFrontからS3のオリジンを
参照できるようにする設定。
Amazon CloudFront ディストリビューションCloudFrontのメインリソース。ファンクションの
紐づけやデフォルトのビヘイビアを含みます。
Amazon CloudFront ファンクション限定された方が閲覧可能とするため
Basic認証の設定と、index.html の
パス置換のために利用します。
AWS CDKでプロビジョニングするAWSリソース一覧

重要な箇所について以下に解説いたします。

Amazon S3 バケット

S3バケットはパブリックアクセスをブロックしCloudFront経由からのみアクセスできるようにします。

const originS3Bucket = new aws_s3.Bucket(this, 'MKDocsOriginS3Bucket', {
      bucketName: `${cdk.Stack.of(this).account}-mkdocs-origin`,
      websiteIndexDocument: 'index.html',
      blockPublicAccess: aws_s3.BlockPublicAccess.BLOCK_ALL,
      removalPolicy: RemovalPolicy.DESTROY,
    });

Amazon CloudFront オリジンアクセスコントロール

今回はオリジンアクセスアイデンティティ(OAI)ではなく、オリジンアクセスコントロール(OAC)を利用しました。以下はS3バケットをオリジンにするためのOACになります。

const originAccessControl = new aws_cloudfront.CfnOriginAccessControl(this, 'MKDocsOriginAccessControl', {
      originAccessControlConfig: {
        name: 'MKDocsOriginAccessControlForOriginS3Bucket',
        originAccessControlOriginType: 's3',
        signingBehavior: 'always',
        signingProtocol: 'sigv4',
        description: 'Access Control',
      },
    });

また、CloudFront ディストリビューションへの紐づけも容易でして、作成したCloudFront ディストリビューションに後付で付与しました。以下が記述例になります。

const cfnDistribution = distribution.node.defaultChild as aws_cloudfront.CfnDistribution
    cfnDistribution.addPropertyOverride('DistributionConfig.Origins.0.OriginAccessControlId', originAccessControl.getAtt('Id'))

Amazon CloudFront ディストリビューション

CloudFront ディストリビューションでは、コードの4行目でS3バケットをオリジンに指定します。また、8行目でBasic認証のCloudFront ファンクションの割り当て、16行目でHTTPをHTTPSにリダイレクトするビューワープロトコルポリシーも設定しています。

    const distribution = new aws_cloudfront.Distribution(this, 'MKDocsDistribution', {
      defaultRootObject: 'index.html',
      defaultBehavior: {
        origin: new aws_cloudfront_origins.S3Origin(originS3Bucket),
        functionAssociations : [
          {
            eventType: FunctionEventType.VIEWER_REQUEST,
            function: new aws_cloudfront.Function(this, 'MKDocsBasicAuthFunction', {
              functionName: `mkdocs-basic-authentication`,
              code: aws_cloudfront.FunctionCode.fromFile({
                filePath: "basic_auth_function/index.js"
              }),
            }),
          },
        ],
        viewerProtocolPolicy: ViewerProtocolPolicy.REDIRECT_TO_HTTPS
      },
    });

Amazon CloudFront ファンクション

CloudFront ファンクションでは、Basic認証パス置換をビューワーリクエストに対して行います。CloudFront ファンクションのコードは以下の通りです。尚、Basic認証のユーザー名とパスワードはBase64でエンコードしております。

なぜパス置換が必要なのか。
Cloud Front の オリジンS3 における index.html の扱いについて

CloudFrontはオリジンのルートディレクトリにある index.html は認識しますが、階層構造のルートディレクトリより先の階層の index.html は認識してくれません。階層のみのURLでアクセスするとオブジェクトが存在しない旨のエラーが表示されます。

MKDocsはビルドするとindex.html が各階層に出力されるため、上記のエラーが発生します。

エラーを解消するために、CloudFront ファンクションの20-24行目の箇所で、アクセス時に index.html を付与するよう変更しています。

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

    // echo -n user:pass | base64
    var authString = "Basic dXNlcjpwYXNz";

    if (
        typeof headers.authorization === "undefined" ||
        headers.authorization.value !== authString
    ) {
        return {
            statusCode: 401,
            statusDescription: "Unauthorized",
            headers: { "www-authenticate": { value: "Basic" } }
        };
    }

    if (uri.endsWith('/')) {
        request.uri += 'index.html';
    }
    else if (!uri.includes('.')) {
        request.uri += '/index.html';
    }

    return request;
}

GitHub Actionsを活用した継続的デプロイ

AWS CDKによって、公開環境をプロビジョニングする準備が出来ました。次に、GitHub Actionsによってデプロイを自動化します。

OIDCを利用したAWSアクセスキーの秘匿化

現在はGitHub ActionsからAWSにアクセスするためのアクセスキーとシークレットキーの取得はOpenID Connect(OIDC)を利用することが望ましいです。

OIDCを利用することでAWSにアクセスするためのアクセスキーの管理から開放されます。本記事ではGitHub ActionsのSecretsにOIDCのためのIAMロールのArnを指定することで実現しています。

アマゾン ウェブ サービスでの OpenID Connect の構成

必要な環境変数

以下の環境変数をGitHub ActionsのリポジトリSecretsとして設定します。

Secrets名説明
S3_BUCKET_NAMEAWS CDKで作成したオリジンのS3のバケット名
AWS_REGIONAWSのリージョン
AWS_OIDC_IAM_ROLE_ARNAWSアカウントで準備したOIDCを許可する
IAM RoleのロールArn
SLACK_WEBHOOK_URLSlackに通知するためのWebhook URL
SLACK_CHANNEL_IDSlackに通知するための通知先のチャンネルID
GitHub Actionsに設定するリポジトリのSecrets

Material for MKDocsのコンテナイメージを活用した継続的デプロイ

GitHub ActionsのワークフローにはMaterial for MKDocsのコンテナイメージを利用します。コンテナイメージを利用するメリットとして、 Pythonのrequirements.txt のバージョンに固定されることなく、最新のMaterial for MKDocsを利用してビルド可能であるため、機能が改善されたバージョンをデプロイすることが出来ます。

※逆の意味ではMaterial for MKDocsのバージョンを固定していないため、予期せぬ表示崩れが発生する可能性があります。後方互換性を重視するのであればコンテナイメージのタグを固定することが望ましいです。

ワークフローのコード例は以下のようになります。6~10行目でSecrets情報を参照しております。23~28行目ではコンテナイメージを利用してMKDocsをビルドしています。30~34行目ではOIDCを利用してAWSの認証情報取得を行っております。36~38行目ではビルドされたコンテンツをS3バケットに同期しています。

name: "Manual Deploy"

on: workflow_dispatch

env:
  BUCKET_NAME : ${{ secrets.S3_BUCKET_NAME }}
  AWS_REGION : ${{ secrets.AWS_REGION }}
  AWS_ROLE_ARN: ${{ secrets.AWS_OIDC_IAM_ROLE_ARN }}
  SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK_URL }}
  SLACK_CHANNEL_ID: ${{ secrets.SLACK_CHANNEL_ID }}

permissions:
  id-token: write
  contents: read

jobs:
  build_and_push:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v2

      - name: Build
        uses: addnab/docker-run-action@v3
        with:
          image: squidfunk/mkdocs-material:latest
          options: -v ${{ github.workspace }}:/docs
          run: mkdocs build

      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v1
        with:
          role-to-assume: ${{ env.AWS_ROLE_ARN }}
          aws-region: ${{ env.AWS_REGION }}

      - name: Sync html to s3
        run: |
          aws s3 sync ./site s3://${{ env.BUCKET_NAME }}/ --delete --quiet

      - name: Slack Notification on Success
        if: success()
        uses: rtCamp/action-slack-notify@v2
        env:
          SLACK_CHANNEL: ${{ env.SLACK_CHANNEL_ID }}
          SLACK_TITLE: MKDocs - Manual Deploy job Success
          SLACK_COLOR: good
          SLACK_MESSAGE: Successfully executed the Manual Deploy job.
          SLACK_USERNAME: MKDocs - Manual Deploy job Success

      - name: Slack Notification on Failure
        uses: rtCamp/action-slack-notify@v2
        if: failure()
        env:
          SLACK_CHANNEL: ${{ env.SLACK_CHANNEL_ID }}
          SLACK_TITLE: MKDocs - Manual Deploy job Failure
          SLACK_COLOR: danger
          SLACK_MESSAGE: <!channel> The Manual Deploy job failed to execute.
          SLACK_USERNAME: MKDocs - Manual Deploy job Failure

今回はCloudFrontのキャッシュ削除(Invalidation)は行っておりませんが、デプロイ時にキャッシュを削除し最新のコンテンツを取得できるにしたい場合はキャッシュ削除の実行を推奨いたします。

以下画像のように、Material for MKDocsのデプロイに成功しました。

また、Slackにも成功が通知され、コンテンツも公開されました。

まとめ

本記事ではAWS CDKとCloudFrontを用いたMaterial for MKDocsの公開環境をご紹介しました。Material for MKDocsはエンジニアが慣れ親しんでいるMarkdown記法で記述できることがメリットの一つとしてあります。

ご興味がありましたら、是非とも本記事の公開環境の構築をお試し下さい。

AUTHOR
Yasuyuki Sato
Yasuyuki Sato
記事URLをコピーしました