アプリのデプロイ作業を自動化したお話

みなさんこんにちは、那須です。
今日から技術的な記事を note に書いていくことにしました。これまで続けてきたことを場所を変えて note でやるだけなんですが、気持ちを新たにして書いていきますね。初回はアプリの API や Web のデプロイ自動化の話です。

お伝えしたいことはこんな内容です。

・自動化するとオペミスする可能性を減らせるし時間の余裕もできる
・GitHub Actions だけでやりたいことができた
・onedog を使ってみて欲しい


どんなアプリをデプロイしてるの?

私が所属する dotD では、onedog という愛犬の健康管理ができたり犬が大好きな人たちが集まるコミュニティがあるサービスを提供しています。詳しくは↓こちらのページをのぞいてみてください。


自動化する前の状態

デプロイ自動化の仕組みを作る前は、大まかには以下のような手順で開発担当者がデプロイ作業をしていました。

1. GitHub にコードを push、プルリクエスト(以降、PR)をマージ
2. (本番デプロイのみ)リリースする commit にタグをつける
3. デプロイ用サーバにデプロイする資源を clone もしくは fetch する
4. デプロイする

このように見ると大した手順ではない作業のように見えますが、そもそも人間は間違いを起こしたりミスしたりする生き物です。作業中のオペミスはつきものですよね?
特に 4 のデプロイする部分は、一言でデプロイと言っても諸事情によりたくさんのコマンドを実行する必要があります。そしてだいたいのデプロイ作業時刻は夜間になりがちです。そこでオペミスでサービスに影響を与えたりすると、他のメンバーは寝ている可能性大なのでリカバリも一人でしないといけません。想像するだけでとてもつらい状況ですね…

オペミスを防ぎ、かつ作業も素早く行いたい。そういう思いが開発チーム全員にありました。普段はデプロイ作業をしない私ですが、決まったタスクを自動化することには最近慣れていたのでデプロイ自動化の仕組みを作ってみることにしました。


検討したこと

検討すると言っても、デプロイに縁のない私が簡単に作れるわけもありません。右も左もわからんとはこのことですね。実際に API をデプロイしている遠藤からいくつかサービスを教えてもらいました。

Circle CI、Travis CI、Jenkins、GitHub Actions などの名前を聞いたので、まずはそれぞれのサービスがどういうもので、どんな料金体系で、私たちが必要としていることができるかどうか、を中心に調べていきました。
結論としては GitHub Actions でやってみようということになったのですが、どれでもやりたいことはできるしどのクラウドサービスでも使えるので、決め手は料金になりました。安ければ安いほど心理的に気軽に試せるので、ほぼ無料でできそうな GitHub Actions に期待を寄せることになりましたね。料金が高ければ高いほど、このままだとダメだとわかってても今までの分を無理に取り戻そうとする気がするので(私だけ?)。


試してみた

GitHub Actions についてはググってもいろんな情報が出てきますが、昔のバージョンと今のバージョンで色々動作が異なるといった情報をいくつか見かけたので、公式ドキュメントをガッツリ読んでいくことにしました。幸いにもやりたいことに対してどうすればいいかがすぐに見つかるくらいにいいドキュメントでした。

実際に運用で使っているワークフローの例(抜粋)をご紹介します。AWS へのデプロイで、VPC 外からはアクセスできない環境へデプロイする場合は以下のような感じのワークフローを使っています。デプロイ用サーバに GitHub Actions のセルフホストランナーを入れて GitHub から制御できるようにしています。

# .github/workflows/example1.yml

on:
  workflow_dispatch:
    inputs:
      tag:
        description: 'Tag to be checked out (option)'
        required: false

jobs:
  define_deploy_env:
    name: Define stage
    runs-on: ubuntu-latest
    outputs:
      deploy_env: ${{ steps.define_deploy_env.outputs.DEPLOY_ENV }}
    steps:
      - id: define_deploy_env
        run: |
          echo ${{ github.ref }}
          case "${{ github.ref }}" in
            "refs/heads/develop" )
              echo "::set-output name=DEPLOY_ENV::develop"
            "refs/heads/staging" )
              echo "::set-output name=DEPLOY_ENV::staging"
            "refs/heads/master" )
              echo "::set-output name=DEPLOY_ENV::production"
            * ) exit 1 ;;
          esac

deploy:
  name: Deploy
  runs-on: 
    - self-hosted
    - ${{ needs.define_deploy_env.outputs.deploy_env }}
  needs: define_deploy_env
  steps:
  - name: Check out branch
    uses: actions/checkout@v2
    if: github.event.inputs.tag == ''
    with:
      ref: ${{ github.ref }}
  - name: Check out tag
    uses: actions/checkout@v2
    if: github.event.inputs.tag != ''
    with:
      ref: ${{ github.event.inputs.tag }}
  - name: Prerequisites
    run: |
      pwd
      git branch
      /usr/local/bin/docker-compose build --no-cache
  - name: Deploy
    shell: bash
    run: >
      /usr/local/bin/docker-compose run
      -e DEPLOY_ENV=${{ needs.define_deploy_env.outputs.deploy_env }}
      deploy

Web などのデプロイについては、GitHub ホストランナーを使って以下のようなワークフローでデプロイしています。先ほどの例でもそうでしたが、実際のデプロイ処理は GitHub Actions のワークフローとは別のシェルスクリプトや Docker コンテナで実行しています。こうすることで、GitHub Actions ではデプロイの一連の流れだけを書いて実際のデプロイ処理はそれ専用の仕組みで実行するので、それぞれのファイルの中身はシンプルになりました。

# .github/workflows/example2.yml

on:
  workflow_dispatch:
    inputs:
      tag:
        description: 'Tag to be checked out (option)'
        required: false

jobs:
  deploy:
    runs-on: ubuntu-latest

    steps:
    - name: Check out branch
      uses: actions/checkout@v2
      if: github.event.inputs.tag == ''
      with:
        ref: ${{ github.ref }}
    - name: Check out tag
      uses: actions/checkout@v2
      if: github.event.inputs.tag != ''
      with:
        ref: ${{ github.event.inputs.tag }}
    - name: Specify outputs
      id: set_output
      shell: bash
      run: |
        echo ${{ github.ref }}
        case "${{ github.ref }}" in
          "refs/heads/develop" ) echo "::set-output name=DEPLOY_ENV::develop" ;;
          "refs/heads/staging" ) echo "::set-output name=DEPLOY_ENV::staging" ;;
          "refs/heads/master" ) echo "::set-output name=DEPLOY_ENV::production" ;;
          * ) echo "::set-output name=DEPLOY_ENV::none" ;;
        esac
    - name: Set up Python
      uses: actions/setup-python@v2
      with:
        python-version: '3.x'
    - name: Install dependencies
      run: |
        pip install awscli
        # その他いくつか必要なものをインストールしている
    - name: Configure AWS Credentials
      uses: aws-actions/configure-aws-credentials@v1
      with:
        aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
        aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
        aws-region: ap-northeast-1
        role-to-assume: ${{ secrets.AWS_ROLE }}
        role-external-id: ${{ secrets.AWS_ROLE_EXTERNAL_ID }}
        role-session-name: github-actions-user
    - name: deploy
      shell: bash
      run: bash deploy.sh ${{ steps.set_output.outputs.DEPLOY_ENV }}

上の例だと on の部分は workflow_dispatch のみで手動トリガーでのデプロイとなっていますが、実際には別のワークフローファイルで PR がマージされたタイミングでデプロイする場面もあります。


自動化した結果

先ほどご紹介したワークフローで実際にデプロイする運用にしていますが、本当に楽です! 例えば20分くらいかけてオペミスしないように気をつけて手を動かしてデプロイ作業していたのが、GitHub でボタンを押してお茶でも飲みながら数分待つだけになりました。文字にするとインパクトに欠けますが、当事者としては大きな違いです。もう少し早くこういうことができるのを知りたかったですね。まだ他にも手作業で運用している部分はあるので、可能な限り自動化してチームの仕事の速度を上げていけるようにしていきたいと思います。
わからないなりに調べながら、責任を持って決めたり進めたりさせてもらったチームのみんなには感謝しています!


さいごに

今回はデプロイ自動化についての取り組みをご紹介しました。他にもいろんなネタで記事を書いていこうと思っていますが、「こういう時どうしてる?」「今やってることをもう少し知りたい」などのリクエストがあればできるだけそれにお答えしていこうとも考えていますので、気軽にお問い合わせいただければ嬉しいです。

まだまだ小さい会社ですがめちゃくちゃ優秀なメンバーが揃っているので困ったら助けてもらえますし、やりたいことがあればとりあえずやってみる精神でいろんなことに取り組める環境です。自社事業と共創事業の両方に関われる会社ってそう多くないと思ってます。ちょっとでも気になったそこのアナタ、一緒に面白い世界を作っていきませんか?


この記事が気に入ったらサポートをしてみませんか?