lycheejam's tech log

チラ裏のメモ帳 | プログラミングは苦手、インフラが得意なつもり。

GitHub + AWS CodePipeline|Build|Deployで自動ビルド・デプロイ環境の構築

概要

ASP.NET CoreアプリをAWS LightsailでホスティングされているCentOS 7のサーバで公開するにあたり自動ビルド・自動デプロイ環境を構築しました。
所謂、CI環境と言うものですが自動テストは組み込んでいないためタイトルを「自動ビルド・デプロイ環境の構築」としています。

今回のインフラ基盤として使用したAWS LightsailはCodePipelineと統合されていないため、自動ビルド・デプロイ環境にAWS Codeシリーズを使用する旨味はあまりありません。
しかし、過去にAWS CodeBuildを使用してVue.jsの自動ビルド・デプロイ環境を整えた自身の経験があったためAWSを選択しました。

また、今回の構成では下記のような特徴があります。

  • AWS EC2などではなくオンプレミスなサーバ
    • AWS CodeDeployと統合されていない環境
    • CodeDeployのAgentが必要な環境
  • OSがCentOS

1つ目のオンプレミス環境についてはLightsailもAWSの1つのサービスではありますが、CodeDeployとは統合されていません。
そのため、Agentのインストールが必要になると言う点。
2つ目のOSについて、CodeDeployを使用する上でUbuntuRHELについては公式ドキュメント含め多少ナレッジが公開されています。
しかし、CentOSについてはAgentのインストール等々ナレッジが公開されていなかったので手探り状態での構築となった点です。

目次

環境構成(自動ビルド・デプロイの流れ)

構築する環境構成を整理します。自動ビルド・デプロイ環境の雰囲気を掴むことが目的です。
構成の雰囲気を掴んでしまえば構築はAWSを触っているだけでできると思うので。

下記の構成図は実際に構築した構成とは異なりますが説明しやすいように必要な部分を抜粋した図です。
実際の構成図についてはGitHubのリポジトリをご参照ください。

f:id:HM_Atlas:20190401201508p:plain

アプリ基盤

  • AWS Lightsail(アプリのデプロイ先)
    • アプリ:ASP.NET Core MVC + MySQL + nginx(リバースプロキシ)
$ cat /etc/redhat-release
CentOS Linux release 7.6.1810 (Core)

処理の流れ

構成図を元に自動ビルド・デプロイの流れをまとめると下記の3つにまとめることができます。

①②③ローカルからPUSH〜ソースDL

ローカルからGitHubへPushするとCodePipelineに通知されパイプラインが起動する。
そして、GitHubから所定のS3バケットへソースをダウンロードする。

④⑤⑥⑦ビルド実行からアップロード

ソースのDLが完了するとCodePipelineからビルド実行が命令され、CodeBuildが起動する。
その後、S3バケットからビルド実行用コンテナにソースをダウンロード・ビルド実行・ビルド済み資産のアップロードが実行される。

CodeBuildでは内部的にLambdaが使用されています。
CodeBuildで使用可能なコンテナイメージは標準のモノからDockerHubに存在するモノまで各種選択可能なので、今回はMS公式のdotnet dockerイメージを使用します。

⑧⑨⑩⑪⑫デプロイの実行

ビルドの実行・アップロードの完了後、CodePipelineからCodeDeploy(親)へデプロイ実行命令が通知される。
その後、CodeDeployAgent(子)へデプロイ命令が通知されビルド済み資産をS3バケットからLightsail(アプリ稼働環境)へダウンロードされデプロイが実行される。

図の中で⑨と⑩のデプロイ実行命令と検知を分けたのはCodeDeployAgent(子)が5秒間隔でCodeDeploy(親)へ命令が発行されていないかの問い合わせをしているためです。

自動ビルド・デプロイ環境の構築

自動ビルド・デプロイ環境を構築します。
本来であればCodeBuildで使用するビルド指示ファイルのbuildspec.ymlとCodeDeployで使用するデプロイ指示ファイルのappspec.ymlを作成してからパイプライン等々を作成すればよいのですが、全体の流れが分かりづらくなるので下記の順番で構築します。

  1. 事前準備
  2. サーバにCodeDeploy Agentをインストール
  3. CodeDeployアプリケーションとデプロイグループの作成
  4. CodePipelineとCodeBuildプロジェクトの作成
  5. CodeBuildのbuildspec.ymlを作成
  6. CodeDeployのappspec.ymlとデプロイスクリプトを作成
  7. 動作確認

事前準備

以下の準備を事前に行っておきます。

通信用ポートに穴をあける(事前準備)

CodeDeploy AgentはSSL通信でやり取りするので443ポートの通信を許可します。
Lightsailの場合はサーバ側でファイアウォールの設定ができないためブラウザから設定します。

f:id:HM_Atlas:20190401201804p:plain

GitHubリポジトリの作成

CodePipelineプロジェクト作成時にソースとしてリポジトリを指定する必要があるため予めGitHub上にリポジトリを作成(アップ)しておきます。

S3バケットの作成

GitHubからDLしたソースやCodeBuildでビルドされた成果物の置き場所となるS3バケットを予め作成しておきます。
CodePipelineが自動で作成する方法も選択できますがバケットを固定したかったのでバケットを予め作成しておきパイプラインを作成する際にバケットを指定します。

サーバにCodeDeploy Agentをインストール

まず、最初にデプロイ先のサーバへCodeDeploy Agentをインストールします。
デプロイ先サーバの環境構築は完了しているものとします。

AWS CLIのインストール

CodeDeployでインスタンス(サーバ)を登録するためにAWS CLIが必要なのでインストールします。
インストール操作については下記の公式ドキュメント通りで問題なく行えました。

# pipインストール
$ curl -O https://bootstrap.pypa.io/get-pip.py
$ python get-pip.py --user
# インストール実行
$ pip install awscli --upgrade --user
# バージョン確認
$ aws --version
aws-cli/1.16.121 Python/2.7.5 Linux/3.10.0-957.5.1.el7.x86_64 botocore/1.12.111
# パス確認
$ which aws
~/.local/bin/aws

ユーザのアクセスキー設定

CodeDeployへインスタンスを登録するにあたり初回登録のためにIAMユーザのアクセスキーを設定する必要があります。
事前にインスタンス用のIAMユーザを作成する方法が正攻法なのでしょうが、インスタンス登録時に自動で作成される方法を今回は実施するため権限を持った別ユーザを登録に使用します。
ここで設定するIAMユーザのアクセス権限にはIAMユーザの作成・削除などの権限が必要になります。 詳細はこちらの公式ドキュメント下部に記載があります。

この部分はあとからいくらでも変更できるので問題ないかと思います。

$ aws configure
AWS Access Key ID :XXXXXXXXXXXXXXXXXXXX
AWS Secret Access Key :XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
Default region name :ap-northeast-1
Default output format :json

CodeDeployのインスタンス登録

現在、作業しているサーバをオンプレミスのインスタンスとしてCodeDeployに登録します。

オンプレミスインスタンスの登録方法はAWS公式ドキュメントによると下記の3つがあります。

  1. aws deploy registerコマンドを使う方法
  2. aws deploy register-on-premises-instanceコマンドでiam-user-arnを使う方法
  3. aws deploy register-on-premises-instanceコマンドでiam-session-arnを使う方法

今回は1つ目のregisterコマンドを使用します。オンプレミスインスタンスを登録する際に自動でIAMユーザを作成してくれるからです。
AWS公式ドキュメントのコマンド例ではオプション(引数)で--iam-user-arn(IAMユーザ)を指定していますが
下記コマンドの様に--iam-user-arnを指定せずにコマンドを発行することで適切なポリシーがアタッチされたIAMユーザが作成されます。
IAMユーザ名はインスタンス名(--instance-name)が適用されます。

また、--tagsオプションで指定しているkeyvalueについてはインスタンスを特定するためのタグになります。
複数インスタンスを運用する際にインスタンスを区別するために使用されます。
これはあとからCodeDeployのダッシュボードから変更可能です。

$ aws deploy register --instance-name hogehoge-CentOS-1GB-Tokyo-1 --tags Key=Name,Value=CodeDeployhogehoge --region ap-northeast-1
Creating the IAM user... DONE
IamUserArn: arn:aws:iam::123456789123:user/AWS/CodeDeploy/hoge-CentOS-1GB-Tokyo-1
Creating the IAM user access key... DONE
AccessKeyId: XXXXXXXXXXXXXXXXXXXX
SecretAccessKey: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
Creating the IAM user policy... DONE
PolicyName: codedeploy-agent
PolicyDocument: {
    "Version": "2012-10-17",
    "Statement": [ {
        "Action": [ "s3:Get*", "s3:List*" ],
        "Effect": "Allow",
        "Resource": "*"
    } ]
}
Creating the on-premises instance configuration file named codedeploy.onpremises.yml...DONE
Registering the on-premises instance... DONE
Adding tags to the on-premises instance... DONE
Copy the on-premises configuration file named codedeploy.onpremises.yml to the on-premises instance, and run the following command on the on-premises instance to install and configure the AWS CodeDeploy Agent:
aws deploy install --config-file codedeploy.onpremises.yml

これでオンプレミスインスタンス(CodeDeployAgent)用のユーザが作成されました。
IAMユーザのポリシーについては最初からS3の読み取り権限が付与されています。
これはCodeDeployでデプロイを実行する際にアプリなどの配置する資産をS3から取得するためです。

公式ドキュメントを見る限りではインスタンスごとにユーザが必要とのことですが本当なんですかね?

CodeDeploy Agentのインストール

上記のregisterコマンドを発行するとIAMユーザの作成に成功しましたと言った風なログが出力されます。
最下行に次は「aws deploy install --config-file codedeploy.onpremises.ymlを実行してください」とログが出ています。

これでAgentのインストールをするわけですがコマンドを発行すると下記の様にエラーになります。

$ aws deploy install --config-file codedeploy.onpremises.yml
Only Ubuntu Server, Red Hat Enterprise Linux Server and Windows Server operating systems are supported.

Oh... CentOSだとダメっぽいですね...
調べて見ると下記の方法でインストールが可能でした。

# インストールファイルの取得に必要なwgetとAgentの実行に必要なrubyのインストール
$ sudo yum install -y ruby wget
# インストールスクリプトのダウンロード
$ wget https://aws-codedeploy-ap-northeast-1.s3.amazonaws.com/latest/install
# 実行権限付与
$ chmod +x ./install
# インストールスクリプトの実行
$ sudo ./install auto
インストール:
  codedeploy-agent.noarch 0:1.0-1.1597
完了しました!

下記コマンドでAgentの実行状態を確認可能です。

# 実行状態確認
$ sudo service codedeploy-agent status
The AWS CodeDeploy agent is running as PID 9149
# 使用可能コマンドの確認
$ sudo service codedeploy-agent
Usage: /etc/init.d/codedeploy-agent {start|stop|status|restart}

ただ、これだけではAgentがうまくCodeDeployの親と通信してくれないのでAgentの設定ファイルを編集します。

codedeployagent.ymlの編集

CodeDeploy Agentの設定ファイルであるcodedeployagent.ymlを編集します。 このcodedeployagent.ymlを編集するに至ったうらみツラミは下記の記事にまとめました。

下記のパスに設定ファイルが存在するので編集します。

$ sudo vi /etc/codedeploy-agent/conf/codedeployagent.yml
  • /etc/codedeploy-agent/conf/codedeployagent.yml
# 省略
# 最下行に下記を追加
:on_premises_config_file: '/home/centos/codedeploy.onpremises.yml'

ここで指定しているcodedeploy.onpremises.ymlはAgentインストールの実行ディレクトリに作成されているのでそれを指定します。
ファイル自体の配置場所は任意の場所に移動させても構わないと思います。

codedeploy.onpremises.ymlにはオンレミスインスタンスを登録した際に作成されたIAMユーザのアクセスキーなどの情報が記載されています。
これをAgentで読み込むように編集しました。

$ cat codedeploy.onpremises.yml
---
region: ap-northeast-1
iam_user_arn: arn:aws:iam::123456789123:user/AWS/CodeDeploy/hogehoge-CentOS-512MB-Tokyo-1
aws_access_key_id: XXXXXXXXXXXXXXXXXXXX
aws_secret_access_key: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

以上でインスタンスの登録・エージェントにインストール作業は完了です。

ブラウザからCodeDeployのオンプレミスインスタンスの画面で登録したインスタンスが確認可能です。

f:id:HM_Atlas:20190401201838p:plain

CodeDeployアプリケーションとデプロイグループの作成

CodeBuildプロジェクトはCodePipelineのパイプライン作成過程で作成可能ですが、CodeDeployのアプリケーションとデプロイグループのみ先に作成が必要なので作成します。
※事前に作成しておくとCodePipelineのプロジェクト作成時にデプロイプロバイダとして選択可能となるため設定が容易となります。

  • CodeDeploy画面よりアプリケーションを作成
  • 作成完了後、作成したアプリケーション画面より「デプロイグループ」を作成

タグのkeyvalueインスタンス一覧の画面から確認可能です。
また、あとから編集可能です。

上記以外の設定は適宜埋めて頂いてあとはデフォルトで構いません。
デプロイタイプやデプロイ設定の詳細は下記の公式ドキュメントをご参照ください。

下記のスクリーンショットは設定時の参考画像です。

Application作成 DeployGroup作成 作成後
f:id:HM_Atlas:20190401201925p:plain f:id:HM_Atlas:20190401201949p:plain f:id:HM_Atlas:20190401202015p:plain
f:id:HM_Atlas:20190401202038p:plain f:id:HM_Atlas:20190401202101p:plain

CodePipelineとCodeBuildプロジェクトの作成

CodePipelineでパイプラインを作成します。 パイプラインを作成する過程で一緒にCodeBuildのビルドプロジェクトを作成します。
※CodeBuildプロジェクトを先に作るとBuild SourceにCodePiplineを指定できなかったため。

CodePipelineのダッシュボードから「パイプラインの作成」を押下して作成を開始します。
ほぼデフォルト通りで、入力しなければいけないところを適宜変更して対応します。

Step1 パイプラインの設定の選択

CodePipelineに任せると自動的にバケットが作成されてしまうので今回は指定します。

f:id:HM_Atlas:20190401202201p:plain

Step2 ソースステージの追加

今回はGitHubリポジトリをソースとしてmasterブランチにPUSHされたタイミングで自動ビルド・デプロイを走らせたいのでWebHookを使います。

  • GitHubを選択し任意のリポジトリを指定
  • 変更検出オプション:「GitHubのウェブフック(推奨)」を選択

GitHubに接続するとリポジトリが選択可能になります。

f:id:HM_Atlas:20190401202221p:plain

Step3 ビルドステージの追加(CodeBuildプロジェクトの作成)

ビルドプロバイダには任意のものを選択します。
今回はCodeBuildを使用するので「CodeBuild」を選択し、「Create Project」よりCodeBuildプロジェクトの作成を開始します。

CodePipeline作成途中に作成するのでビルドソースや出力アーティファクトの設定が自動でされます。
入力ソースを増やしたり編集したりも作成完了後にCodePipelineのブラウザ画面から操作可能です。

  • ビルドプロバイダ:「AWS CodeBuild」を選択
  • 「Create project」からCodeBuildプロジェクトを作成する。
    • Environment image:「Custom image」を選択
    • Environment type:「Linux」を選択
    • Image registry:「Other registry」を選択
    • External registry URL:MS公式のDockerイメージを使用 →「mcr.microsoft.com/dotnet/core/sdk

ビルドプロジェクト作成完了後、下記画像の様に「Successful」と表示されてCodeBuildプロジェクトが選択可能となっています。

f:id:HM_Atlas:20190401202248p:plain

コンテナに使用するイメージを今回はMS公式のDockerイメージを指定しました。
Dockerイメージを指定した理由としてはAWS側に.NET Coreの実行環境が2.1までしか用意されていないので2.2のプロジェクトをビルドしたら普通にコケたためです(そりゃそうだ)

f:id:HM_Atlas:20190401202315p:plain

Step4 デプロイステージの追加

先程作成したデプロイアプリケーションとデプロイグループを選択します。

f:id:HM_Atlas:20190401202346p:plain

Step5 確認

入力値に間違いが無いかを確認し、作成を完了する。

f:id:HM_Atlas:20190401202421p:plain

作成完了するとPipelineが自動で走りますがbuildspec.ymlを置いてないので普通にコケます。

f:id:HM_Atlas:20190401202449p:plain

CodeBuildのbuildspec.ymlを作成

CodeBuildでビルドする際の指示ファイルを作成します。
リポジトリのrootに追加する形でbuildspec.ymlを作成し追加します。

各フェーズやartifactsセクションについてはAWSの公式ドキュメントが整備されているので参考にしてください。
※Artifactsとは聞き慣れないワードで最初戸惑いましたがビルドされた成果物(アウトプット)と言う考え方で問題ないと思います。

  • buildspec.yml
version: 0.2 # AWS指定のバージョン

phases:
  install:
    commands:
      - dotnet --version
  pre_build:
    commands:
      - dotnet clean -c Release  # プロジェクトのクリーンアップ
  build:
    commands:
      - dotnet build -c Release  # プロジェクトのビルド
  post_build:
    commands:
      - dotnet publish -c Release  # プロジェクトの発行
      # CodeDeployで使用するスクリプトをコピー
      - cp -r deployscripts <hoge>/bin/Release/netcoreapp2.2/publish/
      # CodeDeployで使用するappspec.ymlをコピー
      - cp appspec.yml <hoge>/bin/Release/netcoreapp2.2/publish/
artifacts:
  files:
    - '**/*'  # ベースディレクトリで指定されたディレクトリ配下を全て出力する
  base-directory: '<hoge>/bin/Release/netcoreapp2.2/publish'

内容は単純でdotnet cliのコマンドでビルドしてアプリケーションを発行しています。
npmなどではdotnet関連のコマンドがnpm installnpm buildに変わるだけです。
今回は起動したDockerコンテナにPrivileged権限(特権)を与えていないのでコマンドに制約がありますが、普通にビルドするだけだったら不要じゃないかなと思ってます。
また、cpコマンドでコピーしているファイル/ディレクトリはCodeDeployで使用するファイルになります。
環境によっては不要になるコマンドです。

ビルド実行時のカレントディレクトリと環境変数について

ビルド実行時のカレンドディレクトリはリポジトリのrootです(リポジトリ直下)
今回は単純な操作であるため相対パスで指定していますが、CodeBuild標準の環境変数を使用することで絶対パスとして指定できます。
使用可能な環境変数については下記の公式ドキュメントをご参照ください。

appspec.ymldeployscriptsについて

appspec.ymldeployscriptsリポジトリのrootに配置しています。
上記のファイルとディレクトリについては後述するCodeDeployでデプロイする際に使用するため、ビルド成果物の中に含める必要があります。
そのため、dotnet publish -c Releaseで発行されたアプリケーションディレクトリ(<hoge>/bin/Release/netcoreapp2.2/publish)にファイルをコピーします。
appspec.ymldeployscriptsの内容や役割については後述します。

CodeDeployのappspec.ymlとデプロイスクリプトを作成

リポジトリのroot(リポジトリ直下)に追加する形でappspec.ymldeployscriptsディレクトリ(とスクリプト)を作成し追加します。

appspec.ymlにはデプロイする際にデプロイ先のサーバで実行したいシェルスクリプトを記述するためデプロイ時に必要なシェルも作成します。 前述していたdeployscriptsはこのシェルスクリプトを格納するディレクトリ名です。
今回はなんとなく長い名前を付けましたが任意の名前で問題ありません。

シェルの作成

今回はサービスとして登録しているものを起動・停止するシェルの2種類を用意します。

シェル用ディレクトリの作成

シェルスクリプト格納用にdeployscriptsと言うディレクトリをリポジトリのrootに追加します。
ディレクトリ名はappspec.ymlで指定するので任意のディレクトリ名で構いません。

サービス起動シェル

先程、作成したdeployscriptsディレクトリ内にシェルを作成します。
内容はサービスを起動するだけの簡単なシェルです。

  • dotnetstart.sh
#!/bin/sh

service="hoge"

echo "systemctl start $service"
sudo systemctl start $service
サービス停止シェル

起動シェルと同様にdeployscripts内にシェルを作成します。

  • dotnetstop.sh
#!/bin/sh

service="hoge"

echo "systemctl stop $service"
sudo systemctl stop $service

appspec.ymlの作成

デプロイ時にデプロイ先のサーバで実行させたいことを記述します。
各セクションの詳細については公式ドキュメントで確認してください。

version: 0.0
os: linux

files:
  - source: /   # ビルド済み資産のどの階層からを配置対象とするかを指定
    destination: /var/mvcapp  # デプロイ先のアプリ資産配置パス
hooks:  # hookごとに実行したいシェルを指定
  ApplicationStop:
    - location: deployscripts/dotnetstop.sh
      runas: centos  # 実行ユーザの指定
  ApplicationStart:
    - location: deployscripts/dotnetstart.sh
      runas: centos
filesセクションについて
files:
  - source: /
    destination: /var/mvcapp

filesセクションではsourceでリビジョンから取得するファイルを指定しています。
/でリビジョンに配置されているファイル全てと言うことになります。
複数指定などの方法については公式ドキュメントで詳しく解説されています。下記をご参照ください。

※リビジョンって言葉は聞き慣れない言葉ですがCodeDeploy的に言うとappspec.ymlを含んだデプロイ対象のファイル群を指しているっぽいです。
今回はCodeBuildでビルドされた成果物と考えればいいです。

更に、destinationでリビジョンの展開先を指定します。
今回は/var/mvcappをアプリケーションのワーキングディレクトリとしているため指定しています。

hooksセクションについて

デプロイする一連の流れの中でイベントフックが設定されており、イベントフックのどの時点でスクリプトを実行するか選択可能です。

フックは13種類あり、内4種類はCodeDeploy側で使用するため実質9種類のタイミングがあります。
下記の公式ドキュメントに画像付きで細かく解説されています。

今回はアプリ資産の配置前にアプリを停止し、アプリ資産の配置後にアプリを起動したいので下記のフックを選択しました。

hooks:
  ApplicationStop:
    - location: deployscripts/dotnetstop.sh
      runas: centos
  ApplicationStart:
    - location: deployscripts/dotnetstart.sh
      runas: centos

シェルについては相対パスで指定しています。カレントディレクトリがビルドで出力されたファイル群のrootになります。 また、実行ユーザについても指定可能です。

動作確認

上記で全ての設定は完了です。
CodePipelineから手動で「変更のリリース」を実行してパイプラインを起動させるか指定したGitHubリポジトリにPUSHして自動ビルド・デプロイが完了することを確認してください。

構築手順の中で公式ドキュメントばかりに頼っていますが、構築の大体の雰囲気を掴んで公式ドキュメントを参照していくのが良いと思います。

雑感

この記事長すぎね。書くの時間かかりすぎね。
書き始めてから2週間経過してやっと投稿に至ったんですが...
正直書いてる途中で力尽きてるよね。
appspec.ymlbuildspec.ymlは別記事にもっとわかりやすくまとめたほうがいいなって書きながら思ってました。
正味書きたかったことはCodeDeployAgentの設定編集だけなんだけどね。