SourcegraphをGitHub Actions+CloudFormationで構築・管理する

こちらは、[GitHub Actions Advent Calendar 2019](https://qiita.com/advent-calendar/2019/github-actions) 11日目の記事になります！

# はじめに

[ハンズラボ株式会社](https://www.hands-lab.com/)の[yktakaha4](https://twitter.com/yktakaha4)です🍲
普段は、[7日目に投稿しているhomines22さん](https://qiita.com/homines22/items/7c309cdb093eee3240af)らと、某小売のポイントシステムや、ECサイトの開発・運用に携わっています
当初[弊社のアドベントカレンダー](https://qiita.com/advent-calendar/2019/handslab)に2枠投稿するつもりで記事を書いていたのですが、参加者が増えて枠を譲ることとしたため、こちらに引っ越しました

-----

今年ももうすぐ終わりですね...🎅
色々ありましたが、業務で大変だったのはやはり消費増税対応（もとい軽減税率対応）でした

機能変更がないため普段見ないフリ🙈をしているあのレガシーコードから、注文や商品といった業務の根幹に関わる大事なこのテーブルまで、あちらこちらに`軽減税率区分`や`税率別合計金額`といった項目を追加する...という対応で、
修正自体は軽微なのに対して業務影響度合いが大きく、小売に関わるシステムをご担当の諸氏にも頭を抱えた方がいらっしゃったものと思います

私個人としても、外部ECサイトとのデータ連携や、注文締め、出荷、お客様へのメール送信などといった様々な箇所に対して改修を行いましたが、
複数システム・機能間のフロント、API、バッチからデータベースのカラムまで横断的に影響調査を行うのは、中々骨の折れる作業でした

最初はVSCodeやGitHubの検索機能、grepなどで状況に応じて適当に調べていましたが、
作業の途中からローカル環境に[Livegrep](https://github.com/livegrep/livegrep)を導入したところ、調査の効率がだいぶ上がり、特に想定外のリポジトリやコードに影響が及んでいた...というケースに事前に気づくことができるようになりました

Livegrepはとてもいいツールでしたが、検索対象のリポジトリやインデクシングを自分で管理する必要があったり、アカウントによるアクセス制御などの機能がついておらず複数人で利用するには不便な部分があったので、
税対応が完了して最近やっと人心地ついたところで、改めてチームで全文検索の仕組みを手軽に共用できるよう整備することとしました

# どんなものを作るか

今回は、アカウント管理やリポジトリの自動クローリングといった機能がついた全文検索システムである[Sourcegraph](https://github.com/sourcegraph/sourcegraph)のOSS版を使うことにしました
[こちら](https://knowledge.sakura.ad.jp/21833/)の記事に非常によくまとまっていますが、runするだけで即座に利用できる便利な[Dockerイメージが公開されている](https://hub.docker.com/r/sourcegraph/server/)ので、こちらをAWSで運用します🐳

社内向けの用途でスケーリングも必要なく、かつインスタンス停止時にデータを残したかったので、
DockerがインストールされたEC2をCloudFormationで作成しようと思います
また、弊社の標準的な勤務時間は10時から19時なので、それ以外の時間帯はインスタンスを停止させ、なるべく低コストに運用できると良さそうです

ということで、アーキテクチャは以下のような形になりました🍠

![アーキテクチャ.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/106259/ad71ad34-b59f-e4b3-d773-b84a0617e222.png)

CloudFormationのデプロイとEC2インスタンスの起動および停止はGitHub Actionsを用いて行います
本記事の執筆時点で、指定時間にインスタンスを立ち上げるにはCloudWatch EventsからLambdaを実行する必要があるようですが、
GitHub Actionsには[所定のワークフローを定期実行する機能](https://help.github.com/ja/actions/automating-your-workflow-with-github-actions/workflow-syntax-for-github-actions#onschedule)があるので、こいつでAWS CLIを呼び出すようにすれば手軽にインスタンスの起動・停止が実現できそうです

# できたもの

会社で実際に使っているものは社内ネットワークでの利用を前提とした構成になっているので、
インターネットアクセスが可能な形に修正したものを以下リポジトリに公開しました:octocat:

https://github.com/yktakaha4/sourcegraph-ec2

## 構築手順

まず、リポジトリをフォークします🍴

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/106259/c5a36180-a5a9-1c98-cf15-2c7dad281f66.png)

フォーク直後だとGitHub Actionsが有効にならないようなので、`Actions`タブから有効化しておきます（スクショ撮り忘れた...）

`Settings -> Secrets -> Add a new secret` で、以下の値を設定します

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/106259/200820b7-2497-3538-4422-e36ad21988f9.png)

|キー|説明|
|---|---|
|AWS_ACCESS_KEY_ID|AWSのアクセスキーID|
|AWS_SECRET_ACCESS_KEY|AWSのアクセスキー|
|AWS_DEFAULT_REGION|デプロイ先のリージョン<br>`ap-northeast-1`など|
|RESOURCE_NAME|AWS上でのリソース名<br>`sourcegraph-ec2`など|
|AVAILABILITY_ZONE|デプロイ先のアベイラビリティゾーン<br>`ap-northeast-1c`など|
|SUBNET_ID|デプロイ先のサブネットID<br>`AVAILABILITY_ZONE`配下のサブネットを指定すること|
|SECURITY_GROUP_ID_LIST|セキュリティグループID（複数指定時はカンマ区切り）<br>TCP443と2633ポートを開放したものを指定|

シークレットが指定できたら、なんでもよいので`master`ブランチにプッシュします
とりあえずなのでREADME.mdをいじります

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/106259/783e6f29-4157-1f3d-e487-a06f9cf6063c.png)

適当に文面を編集の上、コミットします

この記事を書いていて思い至ったのですが、これだと横展開がしづらいので、タグ打ったらデプロイ...みたいな感じにした方がよいですね
あるいはワークフローの手動実行ができるとよいのですが...

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/106259/0a8c38ab-600a-423c-7205-633d0e781f30.png)

`master`ブランチが更新されると、CloudFormation上に`RESOURCE_NAME` で指定したスタックが生成されます

```yaml:deploy.yml
on:
  push:
    branches:
      - master

name: Deploy

jobs:
  deploy:
    name: Deploy to AWS
    runs-on: ubuntu-18.04
    steps:
      - uses: actions/checkout@v1

      - name: Setup Python 3.7
        uses: actions/setup-python@v1
        with:
          python-version: "3.7"
          architecture: "x64"

      - name: Install awscli
        run: pip install --upgrade pip awscli

      - name: Deploy CloudFormation Stack
        run: ./scripts/deploy.bash
        env:
          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          AWS_DEFAULT_REGION: ${{ secrets.AWS_DEFAULT_REGION }}
          RESOURCE_NAME: ${{ secrets.RESOURCE_NAME }}
          AVAILABILITY_ZONE: ${{ secrets.AVAILABILITY_ZONE }}
          SUBNET_ID: ${{ secrets.SUBNET_ID }}
          SECURITY_GROUP_ID_LIST: ${{ secrets.SECURITY_GROUP_ID_LIST }}
```

```bash:deploy.bash
#!/bin/bash -eu

base_dir="$(cd "$(dirname $0)/.."; pwd)"

aws cloudformation deploy \
    --stack-name "$RESOURCE_NAME" \
    --template-file "$base_dir/aws/cfn.yml" \
    --capabilities CAPABILITY_NAMED_IAM \
    --no-fail-on-empty-changeset \
    --parameter-overrides \
        ResourceName="$RESOURCE_NAME" \
        AvailabilityZoneName="$AVAILABILITY_ZONE" \
        SubnetId="$SUBNET_ID" \
        SecurityGroupIdList="$SECURITY_GROUP_ID_LIST"
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/106259/64df1485-1e2c-d214-6072-cd69b081cb99.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/106259/867e2271-01a0-dce0-3131-61dc8f7ddf3d.png)

インスタンスができたら、パブリックIPを確認し、接続してみます

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/106259/e8523503-f629-5d96-e5e6-8fb1b92608e2.png)

Sourcegraphはソースコード検索用途のため、httpでは運用したくありません
幸いなことに、デフォルトで管理画面向けに自己署名のSSL証明書を発行する仕様となっているため、こいつを使ってhttps通信をおこなうこととします

`https://インスタンスのパブリックIP/`に接続すると、以下メッセージが表示されます
接続できない場合は、セキュリティグループでTCP443ポートと2633ポートが空いていることを確認してください

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/106259/4871d213-21cf-bf3a-3a98-ce301ba6b3ee.png)

https通信にあたり、証明書が不審のため接続を渋られている状況です
Google Chromeであれば`保護されていない通信 -> 証明書（無効） 
 -> 詳細な情報`と開いていくと、`指紋`の項にフィンガープリントが表示されています

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/106259/70d66e8d-54db-9d91-f57c-0d6d064b4e0b.png)

要はこの証明書が信頼できるものか判断できればいい...ということですが、
本リポジトリでは、インスタンス構築時に生成した証明書のフィンガープリントをCloudwatchLogsに吐き出すようにしています

`Cloudwatch -> ロググループ -> /var/log/messages -> インスタンスID`を開いたのち、`cert`でログをフィルタすると表示されますので、
値が一致することを確認し、問題なければ証明書の信頼設定を行います

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/106259/e4444413-c949-30dd-0e22-15cb0f664773.png)

Google Chrome上で表示されている証明書アイコンをデスクトップにドロップしてダウンロードの後、
ダブルクリックするとキーチェーンアクセスが表示されます

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/106259/da26ab86-2201-657b-673c-0985769fe7ad.png)

`今回追加した証明書を右クリック -> 情報を見る -> 信頼 -> この証明書を使用するとき`で`常に信頼`を選び、
ウィンドウを閉じる際にログインユーザのパスワードを入力すると、証明書が信頼されているものとして指定されます

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/106259/4c8511ea-45a4-acab-1360-cd5809804b2d.png)

再度先ほどのURLにアクセスすると、`詳細設定を表示`を押した後の画面で、接続先へのアクセスが可能になっています

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/106259/ffb084c2-38d4-2815-be9f-408ea526d9f4.png)

いい感じですね！

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/106259/0794d86f-abe9-76cd-c991-f007bc0b5dfe.png)

これ以降の設定は、先ほどの[さくらのナレッジ様の構築手順](https://knowledge.sakura.ad.jp/21833/)の`Sourcegraphの初期設定`の項を参考に行ってください！

```json:管理画面の設定サンプル（ご参考）
{
	// The externally accessible URL for Sourcegraph (i.e., what you type into your browser)
	// This is required to be configured for Sourcegraph to work correctly.
	"externalURL": "https://ec2-52-194-175-117.ap-northeast-1.compute.amazonaws.com",

	"auth.providers": [
		{
			"type": "builtin",
			"allowSignup": false
		}
	]
}
```

## 運用

平日の8:30にインスタンスの自動起動と、毎日19:30にインスタンスの自動停止がおこなわれます🍜

```yaml:start_instance.yml
on:
  schedule:
    # 日本時間で月〜金の毎日9時30分に実行
    - cron: 30 0 * * SUN-THU

name: Start Instance

jobs:
  start:
    name: Start Instance
    runs-on: ubuntu-18.04
    steps:
      - uses: actions/checkout@v1

      - name: Setup Python 3.7
        uses: actions/setup-python@v1
        with:
          python-version: "3.7"
          architecture: "x64"

      - name: Install awscli
        run: pip install --upgrade pip awscli

      - name: Start Instance
        run: ./scripts/instance.bash start
        env:
          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          AWS_DEFAULT_REGION: ${{ secrets.AWS_DEFAULT_REGION }}
          RESOURCE_NAME: ${{ secrets.RESOURCE_NAME }}
```

```bash:instance.bash
#!/bin/bash -eu

cmd="$1"

case "$cmd" in
  "start")
    instance_state="stopped"
    instance_operation="start-instances"
    ;;
  "stop")
    instance_state="running"
    instance_operation="stop-instances"
    ;;
  * )
    echo "invalid command: $cmd"
    exit 1
    ;;
esac

instance_id="$(
  aws ec2 describe-instances --filters "Name=tag:Name,Values=$RESOURCE_NAME" "Name=tag:AutoStart,Values=enabled" |
  jq -r ".Reservations[].Instances[] | [.InstanceId, .State.Name] | select(.[1]==\"$instance_state\")[0]" |
  head -1
)"

if [ -n "$instance_id" ]
then
  aws ec2 $instance_operation --instance-ids "$instance_id" > /dev/null
  echo "succeed: resource-name=$RESOURCE_NAME, command=$cmd"

else
  echo "unfound: resource-name=$RESOURCE_NAME, state=$instance_state"

fi
```

自動起動・停止を止めたい場合は、EC2のコンソールからインスタンスのタグ設定を開き、`AutoStart`キーの値を`disabled`など任意のものに変更してください

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/106259/234f6718-c4fd-c6e4-1e11-4a23ac025972.png)

## 削除

デフォルトだとEC2インスタンスに`t2.medium` を指定しているため、放っておくと課金されてしまいます
Sourcegraphを削除したい場合は、CloudFormationからスタックの削除を行ってください👋

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/106259/dfc2d024-20ba-b6b4-b85b-5227aecc7f2e.png)

# おわりに

個人的にはスケジュールタスクはだいぶ強力な機能に感じます...
Dockerコンテナを任意の時刻に実行できる訳ですから、AWSをはじめとした各種クラウドサービスに対してちょっとしたバッチ処理をやりたい時にはうってつけの機能だなと思いました

ちなみに、GitHub Actionsよりも、[EC2起動時のUserDataの設定](https://github.com/yktakaha4/sourcegraph-ec2/blob/master/aws/cfn.yml#L115-L181)が本記事の中で一番大変だったのですが、本旨から逸れるため割愛します💔