michimani.net

Gatsby で静的サイトを作ってみた - 自動デプロイ編 -

2018-10-23

前回 まで、実際に公開するところまでできました。
公開するにはビルド後の public ディレクトリを S3 にアップロードする必要がありますが、そのあたりも自動化したいところです。

今回は、AWS のサービスを使って自動化を実現します。

使う AWS のサービス

使うのは次のサービスです。

自動デプロイの流れ

自動デプロイの流れは次のようになります。

  1. 記事を作成して CodeCommit に作成したリポジトリに push
  2. Lambda で Algolia のインデックスを削除
  3. CodeBuild でビルド、S3 へのソースの配置
  4. Lambda で実行結果を通知

これらの流れを CodePipeline で繋げます。
CodePipeline の設定の最終形は下図のようになります。

/images/2018-10-23_16.43.05.png

流れをひとつずつみていきます。

1. 記事を作成して CodeCommit に作成したリポジトリに push

これは特筆することはないですね。記事ファイルを追加して commit → push するだけです。

2. Lambda で Algolia のインデックスを削除

これですが、Algolia へのインデックス登録は、npm run build つまり gatdby build を実行した際に登録されます。
その際、同じページであっても オブジェクトID が異なると別ページとして登録されることになります。このオブジェクトIDはビルド時の環境によって決まるもので、今回のように 仮想環境でビルドを実行する場合はビルドのたびに値が変わり、結果的にインデックスの重複がどんどん増えてしまいます。
これを回避するために、ビルド前に既存のインデックスを全て削除します。

としては、Lambda で下記のようなスクリプトを作って Algolia の API を実行します。

from algoliasearch import algoliasearch
import boto3

def lambda_handler(event, context):

    try:
        ALGOLIA_APP_ID = 'your_algolia_app_id'
        ALGOLIA_ADMIN_API_KEY = 'your_algolia_admin_api_key'
        ALGOLIA_INDEX_NAME = 'your_algolia_index_name'
        algolia = algoliasearch.Client(ALGOLIA_APP_ID, ALGOLIA_ADMIN_API_KEY)
        index = algolia.init_index(ALGOLIA_INDEX_NAME)
        res = index.clear_index()

        job_id = event["CodePipeline.job"]["id"]
        put_job_success(job_id)
    except Exception as e:
        put_job_failer(job_id, e.reason)

    return res

def put_job_success(job_id):
    pipeline = boto3.client('codepipeline')
    pipeline.put_job_success_result(
        jobId=job_id,
        currentRevision={
            'revision': 'revision_string',
            'changeIdentifier': 'changeIdentifier_string'
        }
    )

def put_job_failer(job_id, message):
    pipeline = boto3.client('codepipeline')
    pipeline.put_job_failure_result(
        jobId=job_id,
        failureDetails={
            'type': 'JobFailed',
            'message': message
        }
    )

ここで注意が必要なのが、 algoliasearch というライブラリが Lambda のランタイム上には存在しないということです。
これを解決するためには、ローカル環境で algoliasearch をローカル (グローバルではなく という意味で) にインストールして、上記のスクリプトと含めて zip 化して Lambda にアップロードします。下記のサイトを参考にしました。

3. CodeBuild でビルド、S3 へのソースの配置

続いて、CodeBuild でビルドして、そのまま S3 への配置まで行います。
CodeBuild のコンソール画面からビルドプロジェクトを作成します。

プロジェクト名

任意に入力します。

ソース

ソースプロバイダに CodeCommit を指定して、対象のリポジトリを指定します。

環境

マネージド型イメージで OS は Ubuntu、ランタイムは aws/codebuild/nodejs:10.1.0 を選択します。

サービスロールでは、下記のような権限を持つロールを作成して選択します。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Resource": [
                "arn:aws:logs:ap-northeast-1:12345678XXXX:log-group:/aws/codebuild/{ビルドプロジェクト名}",
                "arn:aws:logs:ap-northeast-1:12345678XXXX:log-group:/aws/codebuild/{ビルドプロジェクト名}:*"
            ],
            "Action": [
                "logs:CreateLogGroup",
                "logs:CreateLogStream",
                "logs:PutLogEvents"
            ]
        },
        {
            "Effect": "Allow",
            "Resource": [
                "arn:aws:s3:::codepipeline-ap-northeast-1-*"
            ],
            "Action": [
                "s3:PutObject",
                "s3:GetObject",
                "s3:GetObjectVersion",
                "s3:ListBucket"
            ]
        },
        {
            "Effect": "Allow",
            "Resource": [
                "arn:aws:codecommit:ap-northeast-1:12345678XXXX:{リポジトリ名}"
            ],
            "Action": [
                "codecommit:GitPull"
            ]
        },
        {
            "Effect": "Allow",
            "Resource": [
                "arn:aws:s3:::{対象のS3バケット名}/*"
            ],
            "Action": [
                "s3:PutObject"
            ]
        }
    ]
}

Buildspec

ビルドに関する設定です。
リポジトリの直下に buildspec.yml を配置しても良いのですが、今回はビルドプロジェクト内で指定します。内容は下記のような形です。

version: 0.2

phases:
  install:
    commands:
      - touch .npmignore
  pre_build:
    commands:
       - npm install
  build:
    commands:
       - echo GOOGLE_ANALYTICS_ID=UA-123456789-0 > .env
       - echo ALGOLIA_APP_ID=your_algolia_app_id >> .env
       - echo ALGOLIA_SEARCH_ONLY_API_KEY=your_algolia_search_only_api_key >> .env
       - echo ALGOLIA_ADMIN_API_KEY=your_algolia_admin_api_key >> .env
       - echo ALGOLIA_INDEX_NAME=your_algolia_index_name >> .env
       - echo FB_APP_ID= >> .env
       - npm run build
  post_build:
    commands:
      - aws s3 sync "public/" "s3://{対象のバケット名}" --delete --acl "public-read"
artifacts:
   base-directory: public
   files:
      - "**/*"

build の部分では、.env ファイルを作成しています。
API キーなどについては直接書いてますが、本来であれば環境変数として埋め込むのが正解だと思います。

4. Lambda で実行結果を通知

最後に、結果を通知します。
今回はとりあえず終わったことがわかればいいので内容は適当ですが、下記のような形で Slack に通知します。

import boto3
import json
import logging

from urllib.request import Request, urlopen
from urllib.error import URLError, HTTPError

SLACK_CHANNEL = 'channel_name'
HOOK_URL = 'https://hooks.slack.com/services/XXXXX...'

logger = logging.getLogger()
logger.setLevel(logging.INFO)

def lambda_handler(event, context):

    post_to_slack(json.dumps(event, indent=2))

    job_id = event["CodePipeline.job"]["id"]
    put_job_success(job_id)

    return {'status': 'success'}

def put_job_success(job_id):
    pipeline = boto3.client('codepipeline')
    pipeline.put_job_success_result(
        jobId=job_id,
        currentRevision={
            'revision': 'revision_string',
            'changeIdentifier': 'changeIdentifier_string'
        }
    )

def put_job_failer(job_id, message):
    pipeline = boto3.client('codepipeline')
    pipeline.put_job_failure_result(
        jobId=job_id,
        failureDetails={
            'type': 'JobFailed',
            'message': message
        }
    )
    post_to_slack(message)

def post_to_slack(message):
    slack_message = {
        'channel': SLACK_CHANNEL,
        'attachments': [
            {
                'fields': [
                    {
                        'value': "Finished deploying. \n```\n{}\n```".format(message)
                    }
                ]
            }
        ]
    }

    req = Request(HOOK_URL, json.dumps(slack_message).encode('utf-8'))
    try:
        response = urlopen(req)
        response.read()
        logger.info("Message posted to %s", slack_message['channel'])
    except HTTPError as e:
        logger.error("Request failed: %d %s", e.code, e.reason)
    except URLError as e:
        logger.error("Server connection failed: %s", e.reason)

ハマったところ

CodePipeline から Lambda を呼ぶときは、その Lambda 内で必ず put_job_success_result() または put_job_failure_result() を実行する必要があります。
これをしないと、処理が CodePipeline に返ってこずに、次のアクションに移らず、タイムアウトで失敗していまいます。


以上で、自動デプロイのフローが完成しました。
今回はすべて AWS のサービスを使用しましたが、やはり親和性が高いので設定もコンソール上でほとんどできてしまいました。

ひとまずこれで環境が整った感じなので、これから色々と書き進めていきたいと思います。


comments powered by Disqus