GitHub Actionsを開発して公開するまで(Docker編)

## はじめに
Github Actions便利ですよね．毎月2000分までタダで使えるので，個人プロジェクトはほぼこれでまかなえてしまいます．Docker，もしくはJavaScriptで開発できるので，バックエンドの方もフロントの方も触りやすそうです．

すでに公式日本語ドキュメントがあるので，最新情報は[こちら](https://help.github.com/ja/actions/building-actions/creating-a-docker-container-action)を確認してください．
個人的に手順全体の概要があったほうがわかりやすいと思い，この記事を書いています．
`action.yml`の役割とAction自体を検証するCIの作成方法は役立つと思います．
その他の詳細部分も書きましたが，個別のAction実装作業には役立たないので読まなくても大丈夫だと思います．

## この記事の前提
- Dockerを利用して開発する前提です．JavaScriptでの開発は扱いません．
- Dockerにある程度詳しいことを前提としています．

## 開発に至った背景
- CI上でElasticSearchを起動したかったが，公式のActionがまだ利用したいバージョンに対応していなかった
- 利用しているプラグインをインストールした状態でテストしたかったが，こちらも公式が未対応だった
- なので，上記2点を改善したアクションを自分で開発することにした．

## 手順の概要
1. 新しいrepositoryを作る
1. `action.yml`を作る（これから作るActionがどんな引数を受け取るのか定義する）
1. `Dockerfile`を作る
1. コンテナ起動時に実行されるスクリプトを作る
1. 今回開発したAction自体をテストするCIワークフローを定義する
1. マーケットプレイスに公開する

## 詳細
### 新しいrepositoryを作る
- 新しいrepositoryを作ります（対象読者には説明不要と思われるので省略）
- READMEを追加せよと公式にはありますが，なくても動きます．もちろん公開するわけなので，あったほうがベター．英語だとよりベターでしょう．
- 下記は一例です

```README.md
# elasticsearch-github-actions

setup elasticsearch in your github actions' workflow

## Usage
steps:
- name: Configure sysctl limits
  run: |
    sudo swapoff -a
    sudo sysctl -w vm.swappiness=1
    sudo sysctl -w fs.file-max=262144
    sudo sysctl -w vm.max_map_count=262144

- uses: miyataka/elasticsearch-github-actions@1
  with:
    stack-version: '7.6.2'
    plugins: 'analysis-kuromoji analysis-icu'
```

### `action.yml`を作る（これから作るActionがどんな引数を受け取るのか定義する）
- 今回開発したものを引用します

```action.yml
name: 'Run Elasticsearch with Plugins'         # actionの名前，マケプレで表示される．
description: 'Elasticsearch with Plugins'      # actionの説明，マケプレで表示される．
author: 'miyataka'                             # 自分のGitHub IDなど

branding:                                      # マケプレのアイコンを設定する
  icon: 'database'
  color: 'red'

inputs:                                        # Actionを実際にCIで使うときに，渡してほしいパラメータを定義する
  stack-version:
    description: 'The version of Elasticsearch'
    required: true
  plugins:
    description: 'Elasticsearch plugin strings'
    required: false
    default: ''

runs:
  using: 'docker'
  image: 'Dockerfile'
  env:
    STACK_VERSION: ${{ inputs.stack-version }} # inputでもらったパラメータを環境変数に設定してコンテナにわたす
    PLUGINS: ${{ inputs.plugins }}
```

### `Dockerfile`を作る
- やっていることはシンプルです．`entrypoint.sh`をイメージに追加しているだけ．
- 今回諸事情でdocker in dockerをやるので，ベースイメージは内部で`docker`コマンドを使える`docker:stable`です

```Dockerfile
FROM docker:stable
RUN apk add --update bash
COPY entrypoint.sh /entrypoint.sh

ENTRYPOINT ["/entrypoint.sh"]
```

### コンテナ起動時に実行されるスクリプトを作る
- さきほど登場したentrypoint.shを実装します
- hello worldでもなんでもいいのですが，説明の都合上今回開発したものを引用します

```shell
#!/bin/bash

set -euxo pipefail

# 必須にした引数が存在するかチェック
if [[ -z $STACK_VERSION ]]; then
  echo -e "\033[31;1mERROR:\033[0m Required environment variable [STACK_VERSION] not set\033[0m"
  exit 1
fi

docker network create elastic

PLUGINS_STR=`echo ${PLUGINS} | sed -e 's/\n/ /g'` # 引数に含まれている改行文字を置換
MAJOR_VERSION=`echo ${STACK_VERSION} | cut -c 1`  # メジャーバージョンを取得

PLUGIN_INSTALL_CMD=""

# pluginsを複数インストールするケースがあるのでfor文でコマンドを組み立てる
if [ "x${PLUGINS_STR}" != "x" ]; then
    ARRAY=(${PLUGINS_STR})
    for i in "${ARRAY[@]}"
    do
        PLUGIN_INSTALL_CMD+="elasticsearch-plugin install --batch ${i} && "
    done
fi

# single node only
if [ "x${MAJOR_VERSION}" == 'x6' ]; then # メジャーバージョンによって起動オプションを分岐する．
  docker run \
    --rm \
    --env "node.name=es1" \
    --env "cluster.name=docker-elasticsearch" \
    --env "cluster.routing.allocation.disk.threshold_enabled=false" \
    --env "bootstrap.memory_lock=true" \
    --env "ES_JAVA_OPTS=-Xms1g -Xmx1g" \
    --env "xpack.security.enabled=false" \
    --env "xpack.license.self_generated.type=basic" \
    --ulimit nofile=65536:65536 \
    --ulimit memlock=-1:-1 \
    --publish "9200:9200" \
    --detach \
    --network=elastic \
    --name="es1" \
    --entrypoint="" \
    docker.elastic.co/elasticsearch/elasticsearch:${STACK_VERSION} \
    /bin/sh -vc "${PLUGIN_INSTALL_CMD} /usr/local/bin/docker-entrypoint.sh"
elif [ "x${MAJOR_VERSION}" == 'x7' ]; then
  docker run \
    --rm \
    --env "node.name=es1" \
    --env "cluster.name=docker-elasticsearch" \
    --env "cluster.initial_master_nodes=es1" \
    --env "discovery.seed_hosts=es1" \
    --env "cluster.routing.allocation.disk.threshold_enabled=false" \
    --env "bootstrap.memory_lock=true" \
    --env "ES_JAVA_OPTS=-Xms1g -Xmx1g" \
    --env "xpack.security.enabled=false" \
    --env "xpack.license.self_generated.type=basic" \
    --ulimit nofile=65536:65536 \
    --ulimit memlock=-1:-1 \
    --publish "9200:9200" \
    --detach \
    --network=elastic \
    --name="es1" \
    --entrypoint="" \
    docker.elastic.co/elasticsearch/elasticsearch:${STACK_VERSION} \
    /bin/sh -vc "${PLUGIN_INSTALL_CMD} /usr/local/bin/docker-entrypoint.sh"
fi

# 上記で起動したコンテナに対して，curlで死活確認するコンテナを立ち上げる
docker run \
  --network elastic \
  --rm \
  appropriate/curl \
  --max-time 120 \
  --retry 120 \
  --retry-delay 1 \
  --retry-connrefused \
  --show-error \
  --silent \
  http://es1:9200

sleep 10

echo "Elasticsearch up and running"
```

### 今回開発したAction自体をテストするCIワークフローを定義する
- すでにActionを使った事がある場合にはご存知でしょうが，`.github/workflows/*.yml`を配置するとCIを実行してくれますね．
- 今回開発したAction自体も思ったとおりに動くか検証するためのCIを作りました

```yaml
name: Elasticsearch GitHub Action

on: [push]

jobs:
  run-action:
    name: Start Elasticsearch
    runs-on: ubuntu-latest
    strategy:
      matrix:
        elasticsearch: ["6.4.3", "7.6.2"]
    steps:
      - name: Checkout
        uses: actions/checkout@v1

      - name: Configure sysctl limits
        run: |
          sudo swapoff -a
          sudo sysctl -w vm.swappiness=1
          sudo sysctl -w fs.file-max=262144
          sudo sysctl -w vm.max_map_count=262144

      - name: Start Elasticsearch                     # 重要なのはこの部分
        uses: ./
        with:
          stack-version: ${{ matrix.elasticsearch }}
          plugins: |
            analysis-kuromoji
            analysis-icu

      - name: Elasticsearch is reachable
        run: |
          curl --verbose --show-error http://localhost:9200
```

### マーケットプレイスに公開する
- 動作確認もできたら，あとは公開するだけです．
- ここまでの手順を踏んでいれば，以下のような表示がされているはずです．
<img width="994" alt="publish-github-action-to-markeplace-button.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/165342/a8e39643-bb35-f148-2c11-1be593ec36e9.png">
- このボタンを押して，指示に従い公開しましょう．
- ここは[公式doc](https://help.github.com/ja/actions/building-actions/publishing-actions-in-github-marketplace)をみたほうがいいですね

## 参考と補足
- 引用したrepositoryはこちらです
    - https://github.com/miyataka/elasticsearch-github-actions
- 複数バージョンに対応するためのPRを公式に対して出しています．マージされるといいなぁ．
    - https://github.com/elastic/elastic-github-actions/pull/8


## 最後に
すでにたくさんActionsが公開されているのですが，まだまだかゆいところに手が届かない感じのものも多いです．
OSS開発するにはまだまだブルーオーシャンです．皆で開発＆公開してよりよい開発効率を目指しましょう！