GitHub Enterprise Server と AWS Code Pipeline を連携させた コンテンツのデプロイ

はじめに

システム開発部の木田です。AWS関連を中心に仕事をしています。

「静的コンテンツのみで構成されるアプリケーションを Amazon S3+CloudFront で公開したい。手作業を減らしたいので良い方法は無いか?」

と、相談を受けました。単純にファイルをS3にアップロードするだけならば方法は色々ありますが、良い方法を検討してみましょう。

要件の整理

今回は以下の要件で考えます。

  • アプリケーションのソースファイルは GitHub Enterprise Server 上のリポジトリで管理されている
  • 開発/テストプロセスは考慮しない。 master ブランチにマージされたものをリリース対象にする
  • 静的コンテンツはソースファイルから ビルド する
  • 社内のリリースプロセスに合わせるため、S3へファイルをデプロイする前に手動承認を行なう
  • S3 へのファイルデプロイ後、Amazon CloudFront で "ファイルの無効化(Invalidation)"*1 を行なう

先日、AWS Code Pipeline が GitHub Enterprise Server をサポートすると発表がありました。今回は検証を兼ねて AWS Code Pipeline を利用します。

AWS CodePipeline が GitHub Enterprise Server のサポートを開始

以下、社内の備忘用に少し丁寧にまとめます。

公開環境の準備

まずは、手作業で静的コンテンツの生成、配置、公開が出来るように確認(準備)をします。

GitHub Enterprise Server リポジトリ

auカブコム証券ではシステム開発のメインリポジトリとして (GitHub.com ではなく) GitHub Enterprise Server を利用しています。

GitHub Enterprise Server 上にテスト用のリポジトリを作成します。 今回サンプルで作成するアプリケーションは valhalla-app という名前*2にします。 同名のリポジトリを用意しローカルにクローンします。ブランチは masterdevelop の2つだけ使い、master ブランチをリリース対象とします。

アプリケーション

今回は AWS CodeBuild でビルドが出来れば何でも良いので、サンプル用に React を利用します。

$ create-react-app valhalla-app
$ cd valhalla-app
$ yarn start

サンプルアプリケーションが http://localhost:3000/ で動作します。 動作が確認できたら ビルドします。

$ yarn build

静的コンテンツが build/ フォルダに作成されます。

valhalla-app
  |--build/
  |--node_modules/
  |--public/
  |--src/
  |--.gitignore
  |--package.json
  `--README.md

以下 2点を整理しておきましょう

  • ビルドコマンド: yarn build
  • ビルド 生成物: build/ サブディレクトリ以下の全ファイル

S3 と CloudFront

コンテンツ配置用の S3 バケットを作成し、CloudFront でコンテンツを公開出来るようにします。

  • コンテンツ配置用 S3バケットを作成し、CloudFront 以外からアクセス出来ないようにする
  • CloudFront で S3バケットを Origin (コンテンツソース) として設定する
  • ビルドされた静的ファイルを S3バケットにアップロードする
  • CloudFront で公開された コンテンツを確認する
  • 更新された静的コンテンツを S3バケットにアップロードしても、(すぐには)CloudFront に反映されないことを確認する
  • CloudFront で ファイルの無効化(Invalidation) を行い、コンテンツが更新されることを確認する

上記手順は割愛します。

以下 4点を整理しておきます。

  • コンテンツ配置用 S3 バケット名:<YOUR_BUCKET_NAME>
  • CloudFrontがオリジンとして使うフォルダパス: /contents
  • CloudFront ドメイン名:<YOUR_DOMAIN>.cloudfront.net
  • CloudFront ディストリビューションID: <YOUR_DISTRIBUTION_ID>

ここまでで手動でビルドした静的コンテンツを S3+CloudFront で公開出来るようになりました。

f:id:dividebyneko:20201023091756p:plain

次は CodePipeline を使ってこの処理を自動化していきます。

AWS Code Pipeline

AWS Code Pipeline には ソース、ビルド、テスト、デプロイ、承認、呼び出し の 6 種類のアクションが用意されています。 今回はテスト以外の 5 つのアクションを以下の順で組み合わせます。

  1. ソース: GitHub Enterprise Server からソースコードを取得する
  2. ビルド: ソースコードから静的コンテンツを生成する
  3. 承認: 手動の承認アクションを実行
  4. デプロイ: 静的コンテンツをS3 バケットに配置する
  5. 呼び出し:APIを呼び出して Cloud Front の ファイルの無効化(Invalidation) を行なう

f:id:dividebyneko:20201023092102p:plain

パイプラインで処理するソースコードやビルド生成物などは アーティファクト と呼ばれ、パイプライン用の S3 バケットに保存されます。

Code Pipeline の各アクションには、それぞれ異なるタイプの アクションプロバイダ が提供されています。 適切なアクションプロバイダを選択して利用します。

パイプラインを作成する前に、各アクションで何を行なうか、何を準備しておけばよいか確認していきます。

事前準備

ソースの準備

CodePipeline が GitHub Enterprise Server にアクセス出来るように準備します。

GitHub Enterprise Server がパブリックにアクセス可能ならば、特にやることはありません。

パブリックアクセスが出来ない場合、指定したVPC 経由でGitHub Enterprise Server と通信させることが出来ます。 VPC経由で接続する場合、 GitHub Enterprise Server がどのように展開されているかによって、対応は変わります。

例えば

  • GitHub Enterprise Server がインターネットに公開されているが、接続元IPアドレスなどで制限している
    • VPC 経由でグローバルに出て行く際のIPアドレスを固定し、このIPアドレスをGitHub Enterprise Server で許可します。
  • GitHub Enterprise Server はインターネットに公開されておらず、他のAWSアカウント/VPC で提供されている
    • VPC 間通信 でGitHub Enterprise Server に接続します。
  • GitHub Enterprise Server がオンプレミスで提供されている
    • Direct Connect や VPN接続を利用してオンプレミス環境に接続します。

等々、GitHub Enterprise Server の環境に合わせて接続出来るように準備する必要があります。

今回は 1番目のパターンで検証します。

GitHub Enterprise Server 接続用のVPCを用意し、NAT Gateway or NAT インスタンス に EIP で固定IPアドレスを割り当てます。 また、CodePipeline 用のセキュリティグループを用意します。今回は任意の宛先に対する HTTPS アウトバウンドアクセスを許可するセキュリティグループを作成しておきます。 (手順は割愛)

f:id:dividebyneko:20201022164133p:plain

以下を確認します

  • Code Pipeline が GitHub Enterprise Server との接続に使う VPC の VPC ID
  • 同 サブネット ID
  • 同 セキュリティグループ ID

接続構成に合わせて GitHub Enterprise Server 側のアクセス制限等を適宜設定してください。

GitHub Enterprise Server への接続

事前に確認したいので、 パイプライン作成前に GitHub Enterprise Server へ接続しておきましょう。

GitHub Enterprise Server connections - AWS CodePipeline

AWS Code Pipeline サイドメニューの 設定→接続 でGitHub Enterprise Server を登録します。

f:id:dividebyneko:20201021175922p:plain

「ホスト」に 接続先のGitHub Enterprise Server を登録し、登録したホストに対してユーザーアカウント/リポジトリを指定して「接続」します。1つの「ホスト」登録を複数の「接続」が利用します。GitHub Enterprise Server 側にはCode Pipeline 連携用のアカウントを用意する*3のが良いでしょう。

ホストは各プロバイダータイプ(BitBucket, GitHub, GitHub Enterprise Server)に 1 つのみ登録できるようです。

まずは「ホスト」の作成です。

f:id:dividebyneko:20201021180748p:plain

ホスト名(名前)、プロバイダー(GitHub Enterprise Server)、エンドポイント(GitHub Enterprise Server の URL) を指定します。今回はVPCを利用して接続するので、VPC ID, サブネット ID, セキュリティグループ IDを準備したものに合わせて設定します。

f:id:dividebyneko:20201021180801p:plain f:id:dividebyneko:20201021180812p:plain

ホストを作成するとステータスが「VPC設定を初期化しています」になるので「保留中」になるまで待ち、「ホストをセットアップ」します。ホストのセットアップでは GitHub Enterprise Server へのログインが求められます*4

f:id:dividebyneko:20201023084030p:plain

ログインできたら Code Pipeline 連携用の GitHub App を作成します。

f:id:dividebyneko:20201021181223p:plain

ホストが利用可能になったら「接続」を作成します。 f:id:dividebyneko:20201021181353p:plain

プロバイダ(GitHub Enterprise Server) を選択し、接続名を適当に付けます*5 先ほど作成したホストをリストから選び、「GitHub Enterprise Server に接続」します。

f:id:dividebyneko:20201021181455p:plain

GitHub Enterprise Server 側で認可を行います。

f:id:dividebyneko:20201021181615p:plain

次に、GitHub Enterprise Server アプリをインストールします。 「新しいアプリをインストールする」を選び、personal account または Organization と リポジトリを選択します。 リポジトリは All repositories か、もしくは指定したリポジトリのみかを選べます。

f:id:dividebyneko:20201023084109p:plain

f:id:dividebyneko:20201021181820p:plain

リストで作成した GitHub Enterprise Server アプリ を選び(名前が数字なので分りにくい)、「接続」します。 作成した「接続」が利用可能になれば完了です。

f:id:dividebyneko:20201023084125p:plain

少し分りにくいですが

  • AWS アカウントに GitHub Enterprise Server ホストを登録する(全接続で共有)
  • 登録したホストを利用して、CodePipeline と GitHub Enterprise Server のアカウントを接続する
  • 接続したアカウント(または所属する Organization) が所有するリポジトリを選択する

という流れになっています。複数の(所有者が異なる)リポジトリを利用する場合は、複数の接続を作成します。 適切に権限を設定した連携用アカウントを用意するのが良いでしょう。

接続とホストが利用可能になっていることを確認します。

f:id:dividebyneko:20201022183902p:plain f:id:dividebyneko:20201022183915p:plain

ビルドの準備

アプリケーションのビルドには AWS CodeBuild を使います。

ビルドするためには 3 つのものが必要です。

  • ソースファイル
  • buildspec ファイル (buildspec.yml)
  • 生成物(アーティファクト)の出力先

ソースファイルは CodePipeline を介して入力アーティファクトとして渡されます。*6

buildspec.yml はアプリケーションのビルド方法を定義したファイルです。

今回は React にあわせてシンプルに書いた 以下のものを使います。

buildspec.yml

version: 0.2

phases:
  install:
    commands:
      - npm install -g yarn
      - yarn --version
  pre_build:
    commands:
      - echo install NPM dependencies
      - yarn install
  build:
    commands:
      - echo Build started on `date`
      - yarn build
  post_build:
    commands:
      - echo Build completed on `date`
artifacts:
  files:
    - '**/*'
  base-directory: build

肝になる部分だけ抜粋すると以下です。 yarn build コマンドでビルドし、build ディレクトリにある全てのファイルとサブディレクトリを出力アーティファクト(生成物)として定義しています。

phases:
  build:
    commands:
      - yarn build
artifacts:
  files:
    - '**/*'
  base-directory: build

buildspec.yml は (AWS上でなく) Git リポジトリのトップレベルに作成し、開発者がビルド方法を定義します。 アプリケーションごとに使用言語やビルド方法が異なっていても「buildspec.yml に記載の方法でビルドする」と抽象化する事ができます。

以下を確認します

  • Git リポジトリのルートに buildspec.yml を作成する
valhalla-app
  |--build/
  |--node_modules/
  |--public/
  |--src/
  |--.gitignore
  |--buildspec.yml
  |--package.json
  `--README.md

デプロイの準備

今回のデプロイは単純です。

ビルド結果のアーティファクト(生成物) を CloudFront がオリジンとするS3 バケットにコピーするだけです。

簡単に 2 点確認しておきましょう

  • デプロイ先となる コンテンツ配置用 S3 バケット
  • CodeFront がオリジンとする パス(フォルダ)

承認

Code Pipeline には 手動承認(Manual Approval) アクションが既定で用意されています。 通知先の SNS topic や表示するメッセージなどの設定が出来ます。 運用上は

  • 誰が承認者か
  • 承認依頼をどのように通知するか

など、ワークフローとしての設計が必要になりますが、今回はシンプルに AWS コンソール上で パイプラインの状態を確認し、承認することにします。

特に準備や設定はありません。

CloudFront の Invalidation

コンテンツ配置用 S3 バケットにコンテンツを保存しても、すぐには CloudFront に反映されません。 キャッシュのタイムアウトを自然に待っても良いですが、今回は Code Pipeline の一連の処理の中で キャッシュをクリアします。

クラスメソッドさんの記事を参考に Lamdaを作成します。

CodePipelineからAWS Lambdaを呼び出してCloudFrontのキャッシュを削除(Invalidation)してみた | Developers.IO

Distribution ID と Invalidation を行なうパス を Code Pipeline から受け取り、ジョブの成功をCode Pipeline に伝えるシンプルな lambda です。 説明用にエラー処理等は省いています。

lambda

import json
import boto3
import time

def lambda_handler(event, context):
    job_data = event['CodePipeline.job']['data']
    user_parameters = json.loads(
        job_data['actionConfiguration']['configuration']['UserParameters']
    )
    distribution_id = user_parameters['distributionId']
    path = user_parameters['path']
    
    cf = boto3.client('cloudfront')
    invalidation = cf.create_invalidation(
        DistributionId=distribution_id,
        InvalidationBatch={
            'Paths': {
                'Quantity': 1,
                'Items':[path]
            },
            'CallerReference': str(time.time())
        })
    
    cp = boto3.client('codepipeline')
    cp.put_job_success_result(jobId=event['CodePipeline.job']['id'])

この lambda は Code Pipeline から次のようなパラメーターを渡されることを期待しています。

{
    "distributionId": "YOUR_DISTRIBUTION_ID",
    "path": "/*"
}

Code Pipeline から呼び出す lambda は、ジョブの成功/失敗 を Code Pipeline に通知するように作成する必要があります。 そうでなければ、Code Pipeline から呼び出した lambda が 延々と実行中ステータスのままになってしまいます。

    cp = boto3.client('codepipeline')
    cp.put_job_success_result(jobId=event['CodePipeline.job']['id'])

また、lamda には適切な権限を与えてください。 CloudFront の Invalidation と Code Pipeline への ジョブ結果通知 をする権限が必要です。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "cloudfront:CreateInvalidation"
            ],
            "Resource": "*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "codepipeline:PutJobFailureResult",
                "codepipeline:PutJobSuccessResult"
            ],
            "Resource": "*"
        }
    ]
}

パイプラインの作成

準備が出来たので、パイプラインを作成します。

f:id:dividebyneko:20201022144737p:plain

適当な名前を付けてパイプラインを作成します。今回はサービスロールは新規に作成します。

f:id:dividebyneko:20201022144748p:plain

ソースステージとして GitHub Enterprise Server を選択し、事前に作成済みの「接続」、リポジトリ、ブランチを適切に設定します。

f:id:dividebyneko:20201022145353p:plain

出力アーティファクトは Code Pipeline のデフォルトにします。Code Pipeline 用に自動作成されるアーティファクト用 S3 バケットに zip 形式で保存されます。

f:id:dividebyneko:20201022150751p:plain

ビルドステージでは AWS CodeBuild のプロジェクトを作成します。

f:id:dividebyneko:20201022150832p:plain

今回は特にビルド環境に気を使う必要が無いので、マネージドイメージの Amazon Linux 2 を適当に選びます。

f:id:dividebyneko:20201022150914p:plainf:id:dividebyneko:20201022150923p:plain

buildspec はリポジトリのルートに buildspec.yml を作成してあるのでデフォルトのままで良いです。

作成したビルドプロジェクトを選択して、ビルドステージの設定は完了です。

f:id:dividebyneko:20201022151237p:plain

デプロイステージは デプロイプロバイダーとして Amazon S3 を選び、配置先のバケットとデプロイパス*7を設定します。 事前に準備した コンテンツ配置用 S3 バケットにデプロイします。

Code Pipelineのデフォルトでは ビルド結果の出力アーティファクトとして zip 形式で保存するので「デプロイする前にファイルを抽出する」にチェックを入れます。

f:id:dividebyneko:20201022151925p:plain

レビューして、パイプラインを作成します。

f:id:dividebyneko:20201022152251p:plain

アクションの追加

パイプライン作成ウィザードでは ソース→ビルド→デプロイ の一連の流れを持ったパイプラインを作成できます。追加のアクションが必要な場合はパイプラインを編集します。

Build ステージと Deploy ステージの間に Approve ステージを追加し、手動での承認アクションを待つようにしましょう。

f:id:dividebyneko:20201022152543p:plain

アクションプロバイダーとして Manual Approve を選択します。

f:id:dividebyneko:20201022152752p:plainf:id:dividebyneko:20201022152802p:plain

ここで通知先となる SNSトピックやレビュー用URLなどを設定できますが、今回は省略します。AWS コンソール上でパイプラインの状態を確認して承認することにします。

CloudFront の Invalidation を行なう lambda アクションを追加します。

パイプラインの中身は ステージ、アクショングループ、アクション という階層化されています。 今回は S3 へのファイル デプロイと CloudFront の Invalidation は一体の作業と考え、同じアクショングループ内に「アクションの追加」をします。*8

f:id:dividebyneko:20201022153251p:plain

f:id:dividebyneko:20201022153608p:plain

アクションプロバイダとして lambda を選択し、作成済みの関数を設定します。 作成した lambda は、ユーザーパラメーターとして以下のような JSON を渡されることを期待しているので distributionId を対象の CloudFront のものに設定した JSONを(改行を抜いて 1行で) ユーザーパラメーターにセットします。

{
    "distributionId": "YOUR_DISTRIBUTION_ID",
    "path": "/*"
}

この lambda は アーティファクトを使わず、また出力もしないので 入力/出力 アーティファクトは空欄にします。

パイプライン実行 サービス Role の権限

パイプライン実行 サービス Role で コンテンツ配置用 S3 バケット に オブジェクトの書き込みが出来るようにしておいてください。

パイプラインの実行

手動で 「変更をリリースする」 もしくは GitHub Enterprise Server 側で変更を検知すると、パイプラインが実行されます。 GitHub Enterprise Server の変更検知の設定等は特に必要ありません。

ソースを修正して master ブランチにマージすると、自動的に パイプラインが実行されます。

f:id:dividebyneko:20201022170504p:plainf:id:dividebyneko:20201022170513p:plain

今回はパイプライン中に 承認アクションを入れているので、パイプラインの実行が保留されます。 [レビュー] ボタンから承認を行ないます。

f:id:dividebyneko:20201022170918p:plainf:id:dividebyneko:20201022170927p:plain

コメントもレビュー用URLも設定していないので見た目が寂しいですが、承認します。

f:id:dividebyneko:20201022171618p:plain

問題が無ければ パイプラインの実行が完了します。

少し待ってから CloudFront を確認し、コンテンツが更新されていることを確認します。

f:id:dividebyneko:20201022172244p:plain

結果

AWS Code Pipeline を利用して GitHub Enterprise Server から Amazon S3 に静的コンテンツを配置出来ました。

f:id:dividebyneko:20201023092102p:plain

auカブコム証券では今までもリリース作業に Jenkins, AWS CodeBuild, あるいは手製のバッチ等 を利用していましたが AWS Code Pipeline を使うことで以下のようなメリットがあります。

  • リリースまでの処理を複数のアクションの組み合わせとして管理できる
    • CodeBuild やバッチから AWS CLIをガリガリと呼び出せば何でも出来ますが、理解しにくくなる
  • マネージドサービスなので、ビルド/リリースのための環境構築や維持管理が不要になる
  • ビルド/リリースに関わるユーザーや権限の管理を、AWS環境のユーザー管理と統合できる
  • 手動の承認プロセスを組み込める

今回は静的コンテンツをS3へ配置するシンプルな例でしたが、その他のデプロイパターンにも対応できます。 諸々の(社内的な)制約をクリア出来れば、リリースプロセス改善の有力な候補となりそうです。

今回やらなかったこと

GitHub Enterprise Server をソースにビルドしてデプロイするまで一通りの流れを検証しましたが、 実際は以下のような点も考慮が必要でしょう。

  • 開発環境へのデプロイやテストを含めた一連の流れ
  • 承認依頼の通知方法、承認者や権限の整理
    • 運用を考えると外部ワークフローとの連携などをした方が良いかも
  • 不要になったコンテンツのクリーンアップ
    • 同名のファイルは上書きされますが、利用しなくなったコンテンツは S3 バケットに残ります
  • Invalidation 用 lambda のエラー処理やロギング
  • Invalidation の終了確認
    • lambda で Invalifation を実行した時点で パイプラインのジョブは完了となるが CloudFront 側で Invalidation が完了するまでコンテンツは更新されない。

おわりに

auカブコム証券では一緒に働く仲間を募集しています

私が所属している システム開発部 アプリ基盤グループのミッションを一言で言うとシステムのモダン化です。 アプリケーション アーキテクチャの検討や、開発プロセスの標準化/自動化、新技術の導入などが担当領域です。セキュリティやコスト管理も重要です。

レガシーなシステムが多数あり、業務規程や各種ポリシー、監査対応などの制約に縛られることも多いですが、折り合いをつけながらシステム全体の生産性、品質を高めるための取り組みを行なっています。

採用についてはこちら auカブコム証券株式会社の会社情報 - Wantedly

*1:いわゆるキャッシュ削除

*2:○○-test ○○-poc, ○○-validation みたいな名前が乱立すると混乱するので、適当な名前を付ける

*3:今回は手早く個人用アカウントで検証します

*4:当社の環境ではここで GitHub Enrerprise Server が 500 エラーを返しました。GitHub Enerprise Server からログアウトし、再度ログインし直してからホストのセットアップを行なうと上手く行きました。セッションやMFAが関連しているかもしれません

*5:文字数制限があるのでリポジトリのURLそのままでは入りませんでした

*6:GitHub Enterprise Server から取得したソース がアーティファクトとして保存されている

*7:パスの先頭に / を付けると上手くデプロイ出来ません、ハマりました

*8:本来、ステージ/アクショングループ はしっかり設計しなければいけないと思う