RubyのアプリケーションをPR単位でテストできるようにしてみた

はじめに

RubyのアプリケーションをPR単位でテストできるようにしてみた記事です。
仕事でRubyを使っていて「PR単位で気軽にテスト環境を作りたいなぁ」という人向けの内容です。

背景など

元々仕事で

  • 複数のプロジェクトが同時並行で進んでいる
  • 単一のステージング環境を使い、テストをしている
  • そのためプロジェクトがリリースされるまで、別のプロジェクトの機能がステージング環境でテストできない

という状況が発生しており、新機能リリース時のリードタイムが発生しやすいという課題を抱えていました。
また僕自身も直近ではリリースマネージャ的な動きをしていたこともあり、この状況をどうにかしたいという思いがありました。

そんな時、メルカリから公開されたKubetempuraの「PR単位でテスト環境を構築する」という考えを知り、「この方法で何とかできないだろうか?」と対応方法を考え始めた感じですね

engineering.mercari.com

ただ、

  • 僕のk8sなどへのまだ理解が浅いこと
  • 現状の環境に追加するにはKubetempuraは規模が大きすぎるかもしれない
  • 実際に運用が回るのかスモールスタートで検証したい

というところもあり、Kubetempura導入までは至っていませんでした。

そんな中、個人的な興味から試していたAppRunnerがテスト環境として使えるのではないかと思い始め

gamelinks007.hatenablog.com

色々と試行錯誤してみたところ、以下のようにPR単位でテスト環境を構築できるものができました。

github.com

作ったもの

作ったものとしては以下です。

github.com

シンプルなSinatraのアプリケーションをTerraform/GitHub Actionsを使うことでAWS上にテスト環境を自動で構築できるようにしています。
まだREADMEなどはまだ整備できていませんが、ForkしてAWS周りの認証キーなどをセットすれば動かせると思います。

やったこと

先日公開した以下の記事で作成したTerraformをもとに最低限(AppRunner/IAM/ECRまわり)の環境を構築できるコードを書き、

github.com

以下のようなシンプルなSinatraのWebアプリケーションを作成しています。
(RubySinatraを使っているのは一番使い慣れた言語であることと、テストのための環境構築が楽だったためです)

require 'sinatra'

configure do
  set :bind, '0.0.0.0'
end

get '/' do
  'Hello world!'
end

get '/halo_infinite' do
  'HALO Infinite'
end

get '/halo_2' do
  'HALO 2'
end

それを以下のようにPRが作成されたタイミングでテスト環境を構築するようにしました。

name: 'Create staging with pull request'

on:
  pull_request:
    types: [opened, synchronize]

jobs:
  terraform:
    name: 'Terraform'
    runs-on: ubuntu-latest

    defaults:
      run:
        shell: bash

    steps:
      - name: Checkout
        uses: actions/checkout@v3

      - 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: ${{ secrets.AWS_REGION }}

      - name: Setup Terraform
        uses: hashicorp/setup-terraform@v1
        with:
          terraform_version: 1.1.9

      - name: Init
        run: terraform init
        working-directory: infra
      
      - name: Get
        run: terraform get
        working-directory: infra

      - name: Apply
        id: apply
        run: terraform apply -auto-approve -var 'pr_number=${{ github.event.number }}' -var 'commit_hash=${{ github.sha }}' -var 'region=${{ secrets.AWS_REGION }}'
        working-directory: infra
        continue-on-error: true

      - name: Login to Amazon ECR
        if: ${{ steps.apply.outcome == 'failure' }}
        uses: aws-actions/amazon-ecr-login@v1

      - name: Push Container(Only synchronize pull request)
        if: ${{ steps.apply.outcome == 'failure' }}
        run: |
          docker build -t ${{ secrets.AWS_ID }}.dkr.ecr.${{ secrets.AWS_REGION }}.amazonaws.com/pullrequest-${{ github.event.number }}:latest .
          docker push ${{ secrets.AWS_ID }}.dkr.ecr.${{ secrets.AWS_REGION }}.amazonaws.com/pullrequest-${{ github.event.number }}:latest

初回のみterraform applyが成功し、二回目以降はすでに同じ名前のリソースが生成されているので失敗するようになっています。
またPRの情報(PRやIssueの番号)などを使い、PRごとに環境を作れるようにしています。

二回目以降(つまり追加でPRにコミットがpushされた場合は以下のように失敗した結果をもとにECRへ新しいコンテナをビルドしてプッシュするようにしています。

      - name: Apply
        id: apply
        run: terraform apply -auto-approve -var 'pr_number=${{ github.event.number }}' -var 'commit_hash=${{ github.sha }}' -var 'region=${{ secrets.AWS_REGION }}'
        working-directory: infra
        continue-on-error: true

      - name: Login to Amazon ECR
        if: ${{ steps.apply.outcome == 'failure' }}
        uses: aws-actions/amazon-ecr-login@v1

      - name: Push Container(Only synchronize pull request)
        if: ${{ steps.apply.outcome == 'failure' }}
        run: |
          docker build -t ${{ secrets.AWS_ID }}.dkr.ecr.${{ secrets.AWS_REGION }}.amazonaws.com/pullrequest-${{ github.event.number }}:latest .
          docker push ${{ secrets.AWS_ID }}.dkr.ecr.${{ secrets.AWS_REGION }}.amazonaws.com/pullrequest-${{ github.event.number }}:latest

正直、この辺はもう少しやりようがあるとは思いますね……。
とりあえず、動くものを作りたかったので一旦はこれでOKとしてます(何かいい知見ご存じの方いればご教授いただければ幸いです)

またPRがマージされた(またはクローズされた)場合は以下のように作成されたリソースをAWS CLIで削除するようにしています。

name: 'Delete staging with pull request'

on:
  pull_request:
    types: [closed]

jobs:
  terraform:
    name: 'Terraform'
    runs-on: ubuntu-latest

    defaults:
      run:
        shell: bash

    steps:
      - name: Checkout
        uses: actions/checkout@v3

      - 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: ${{ secrets.AWS_REGION }}

      - name: Get AppRunner Service ARN
        id: apprunner-service-arn
        run: |
          service_arn=$(aws apprunner list-services --query "ServiceSummaryList[?ServiceName=='pullrequest-${{ github.event.number }}'].ServiceArn | [0]")
          echo "::set-output name=stdout::$service_arn"
      - name: Delete AppRunner Service
        run: aws apprunner delete-service --service-arn ${{ steps.apprunner-service-arn.outputs.stdout }}

      - name: Delete ECR repository
        run: aws ecr delete-repository --repository-name pullrequest-${{ github.event.number }} --force

      - name: Get Role attachment policy
        id: role-attachment-policy
        run: |
          role_attachment_policy=$(aws iam list-attached-role-policies --role-name pullrequest-4-service-role --query "AttachedPolicies[0].PolicyArn")
          echo "::set-output name=stdout::$role_attachment_policy"
      - name: Dettach policy
        run: aws iam detach-role-policy --role-name pullrequest-${{ github.event.number }}-service-role --policy-arn ${{ steps.role-attachment-policy.outputs.stdout }}

      - name: Delete role
        run: aws iam delete-role --role-name pullrequest-${{ github.event.number }}-service-role

これのおかげでテスト用に作成された環境を削除し忘れることが回避できています。
この辺ももう少しきれいにしたい感じはありますね……。

今後

今回検証用としてPR単位でのテスト環境自動構築をやってみましたが、大分いい感じではありますね。
なので、一旦仕事のほうでつかえないか導入してみて検証を進めていきたいと思います。

今後課題になりそうなのは実用的なWebアプリケーションなどではDBとの接続(RDSやVPCの設定)が必要になりそうなので、そのあたりをどうするかですね。
また、PRにコミットが積まれるたびにtarraform applyされているのは非常に微妙なのでそのあたりもどうにかしたいですね。

さらに、フロントエンドとバックエンドが分かれているような場合ではAPI側をどう呼び出すのかなどが課題になりそうです。

RailsをAppRuner/RDS/ECRな環境にデプロイできるようにしてみた

Railsで作成されたアプリケーションを、AWS上のAppRunner/RDS/ECRを使った環境にデプロイできるようにしてみた時のメモです。

ついでに、Terraformを使ってインフラ周りもコードで管理できるようにしてみた感じです。

 

作ったもの

github.com

 

ほとんど素のRailsアプリをAppRunnerにデプロイできるようにしてみたものです。

現状ではGitHubActionsを使ったECRへのPushなどはできてませんが、そのうち対応したい……。

 

基本的に頑張ったところはTerraform周りで、以下のようにRDS上にDBが作成されたタイミングでDBへのマイグレーションを走らせたり

github.com

 

ECRの環境が作られたタイミングでコンテナをビルドしてpushしたり

github.com

 

その辺の最初にインフラ周りを構築する際の設定を頑張った感じですね。

 

経緯や背景など

  • 住んでる街(浜田市)向けのサービスとかを作る際にできるだけ使い慣れたRailsを使って開発したかった。
  • インフラとかを細かくメンテナンスしたくなかったので、AWS使って楽したい
  • AppRunnerが最近、VPCに一部対応した
  • AppRunnerでのコンテナ運用が依然試して楽だったので使いたい

などのモチベーションがあってこの構成にした感じですね。

実際にやってみて勉強になった感じ(主にTerraform周り)

 

今後

今回作ったものは実際に運用していく感じではないですが、これをもとにほかの言語(例えばコンテナとの相性がいいGoとか、シングルバイナリが作れるRustとか)でサービスを開発して運用していくのは良さそう。

 

あと前からチマチマ作ってる自分が使うようのWebフレームワークとかと合わせてみるのも面白そう

github.com

 

この辺はRubyでのシングルバイナリ生成とか詳細調べて対応したいかな……?

 

Goでicalendarを生成してみた

はじめに

Goでicalendarを生成してみたいけどどうやるんだっけ?って人向けの記事です(多分そんなにいないと思いますが……)

背景

仕事のコードで予約されているデータなどをGoogleカレンダーなどへ連携されるようにしたいという話が出たのが事のきっかけです。
で、同僚の人と「どうしましょうかー」と話してて、「icalendarを使えばよさそう」となり、実際に実装してみた感じです。

作ったもの

github.com

凄くシンプルに実装したサンプルになります。

使っているライブラリとしては以下のものを使っています。

  • gin-gonic/gin(APIサーバー用)

github.com

github.com

やったこと

基本的にシンプルなginのサーバーを書いて、

func main() {
    err := godotenv.Load(".env")
    if err != nil {
        fmt.Printf("can not load .env!")
    }

    r := gin.Default()
    r.GET("/icals", func(c *gin.Context) {
        ical := generateIcal()
        c.String(http.StatusOK, ical)
    })
    r.Run()
}

あとはただgolang-ical使って構造体経由でカレンダーデータを作成した感じですね。

type Calendar struct {
    Title string
    StartAt time.Time
    EndAt time.Time
    ZoomURL string
}

func generateIcal() string {
    cal := ical.NewCalendar()
    cal.SetMethod(ical.MethodRequest)

    var calendars []Calendar

    for i := 0; i < 10; i++ {
        calendar := Calendar{
            Title: fmt.Sprintf("HALO %d", i),
            StartAt: time.Now().Add(time.Duration(i) * time.Hour),
            EndAt: time.Now().Add(time.Duration(i + 1) * time.Hour),
            ZoomURL: os.Getenv("ZOOM_URL"),
        }

        calendars = append(calendars, calendar)
    }

    for _, calendar := range calendars {
        event := cal.AddEvent(calendar.Title)
        event.SetStartAt(calendar.StartAt)
        event.SetEndAt(calendar.EndAt)
        event.SetSummary(calendar.Title)
        event.SetDescription(calendar.Title)
        event.SetDescription(calendar.ZoomURL)
        event.SetURL(calendar.ZoomURL)
        event.SetLocation(calendar.ZoomURL)
    }

    return cal.Serialize()
}

実用性とか考えるとZoomとかのURLも添付できたら面白そうだったのでZoomのURLとかも追加してる感じですね。 意外と癖なくサクッと作れたので簡単なカレンダーAPIとかGoで書くのも面白そうです。

あとは、ngrokを使って、Googleカレンダーとかに追加できるようにして動作確認したりくらいですね。

ngrok http 8080

今後

実際に仕事で使うコードにするとなると色々考えないといけないことも多いですが、すごく簡単にicalendarsを生成できたのでいい感じです。

GoでGoの雛型コードを自動生成できるようにしてみた

はじめに

仕事ではもっぱらGoを書いてて、同じようなコードを結構毎回書いたりしてるのがストレスになってる人向けの記事です。

GoでGoの雛型コードを自動生成できるライブラリ dave/jennifer を使ってコマンド経由で楽に生成できるようにしてみました。

github.com

作ったもの

github.com

使い方としては以下のようにmake経由で作成したいコードのtypeとnameを指定すれば自動的に雛型が生成されます。

make code-gen type=model name=UserProfile

ちなみに↑のコマンドで生成されるのは以下のようなモデルです。

package model

type UserProfile struct{}

作った背景

仕事でGoを使っているんですが、どうしても同じようなコードを毎回手で書く辛さを感じてました。 またRailsとかLaravelではコマンド経由で大まかなコードを自動生成できますが、仕事では特にそういった自動生成できる環境ではなかったので愚直にコードを書いており、微妙な印象もありました。

それに手書きで書いてるとちょっとしたスペルミスとかもあったりしてビルドが通らず、開発体験が良くないなとも感じてました。

そこで、何かコードを自動生成できるライブラリとかないかなと探していたところ dave/jennifer を見つけたので試しに作ってみた感じです。

今後

とりあえず簡単にコードの雛型は生成できるようになったので、仕事でもつかえるようにパスや諸々の修正とかしてみようかと思います。

「ゼロからわかるRuby超入門」を読んだ

以前から読もうと思ってAmazonの欲しいものリストに突っ込んでいた「ゼロからわかるRuby超入門」を送っていただいたので早速読んでみた。

 

www.amazon.co.jp

 

ので、雑に感想など書いてみた。

内容など

かなり初心者向けの内容でわかりやすい感じでした。

個人的に変数とかをキャラクターなどで分かりやすく見せているのは「いいなぁ」と思いました。

 

あとターミナル部分とコード部分の枠の色とか違うので、良くある入門書とかの「ターミナルの実行結果とコードのどっちも枠が同じ色で読みにくい」みたいなのは無い感じなのもうれしいかもしれない。

 

それとRubyでのコードの書き方(メソッドへの切り出しとかキーワード引数とか)からSinatraでの簡単なWebアプリ作成までが通して書かれているのもうれしいですね。

(だいたいはRubyの文法とかで終わっちゃうので「次なにやればいいんだろう」ってなっちゃう)

 

Rubyリファレンスマニュアルへの説明があったり、Ruby 3.0を使っていたりするところも良いですね。

 

今後

妻がノーコードツールとか使ってて「Hashがわからん」とか言っていたので読んでみるように薦めてみようかなと思います。

もしくは今開催できてないCoderDojo浜田とかHamada.rbでの初心者向けイベントとかで使ってみてもいいかもしれない

RustでRubyのNative Extensionを書く

はじめに

先日、Ruby Issue Trackerに以下のチケットが作成されました。

Feature #18481 Porting YJIT to Rust (request for feedback)

内容としては、昨年末にリリースされたRuby 3.1にマージされているYJITという新しいJITコンパイラをより開発しやすくするためにRustを使いたいというものです。

これはRuby(ここでのRubyはCで実装されたCRubyことMRIを意味します)で使用しているCの規格がC99であり、C99ではJITコンパイラを作成するための十分なツールがないことに端を発しています。

つまりC99でJITコンパイラを作成するには開発者自身のスキルセットなどが要求され、開発が難しい部分もあるということのようです。

その点、Rustでは強力なエラーサポートや型、メモリ安全性などがあるため開発がしやすいということかもしれません。 またほかの言語での実装の場合(たとえばGoなど)、GCなどの機能があるためそれ単体でビルドはできても別の問題を踏む可能性があり、採用が難しいところがあるようです。

こういった経緯などからYJIT自体は今後Rustで実装されていく可能性があります。

また以前にはHelixなどのようなRubyのNative ExtensionをRustで実装するというものも出ています。

そこで今回はRubyのNative ExtensionをCではなく、Rustで書くことについて解説したいと思います。

Cで書くNative Extension

CでRubyのNative Extensionを書く場合、以下のようにInit_hello()という関数内でメソッドやクラスなどを定義します。

#include <stdio.h>
#include <ruby.h>

void hello_world()
{
  printf("Hello World!");
}

void Init_hello()
{
  rb_define_global_function("hello_world", hello_world, 0);
}

このコードをビルドし、hello_world.soという共有ライブラリを作成することでNative Extensionを使えるようになります。

使う場合は以下のようにrequireを使い、作成された共有ライブラリを読み込めば自動的にInit_hello()が実行され、そこに定義されたメソッドが使えるようになります。

require 'hello_world'

hello_world()
# => "Hello World!"と出力される

Rustで書く場合

Rustで書く場合も基本的にはCで書く場合と同じです。メソッドやクラスを定義するInit_hello()を定義すればOKです。

#![allow(non_snake_case)]

extern crate libc;
use std::ffi::CString;

extern {
    fn rb_define_global_function(name: *const libc::c_char, func: extern fn(), argc: libc::c_int);
}

extern fn hello_world() {
    println!("Hello, world!");
}

#[no_mangle]
pub extern fn Init_hello() {
    let c_func_name = CString::new("hello_world").unwrap();
    let argc = 0;

    unsafe {
        rb_define_global_function(c_func_name.as_ptr(), hello_world, argc);
    }
}

あとは上記のコードをビルドして共有ライブラリを作成し、以下のようにRuby側から呼び出せばOKです。

require './hello'

hello_world()
# => "Hello World!"と出力される

なお、実装例としては以下のリポジトリを参考にしていただければと思います。

rust_hello_world_ruby_extension

そのほかのRustでのRuby Native Extension作成方法

Helix

Helixは以前RubyKaigiでも紹介されたRustで簡単にRuby Native Extensionを書くことができるライブラリです。

現在ではDeprecatedになっており、使用することは控えたほうがいいですね……。

Helixでは以下のようにメソッドなどを書くことができます。 (なお、以下のコードはHelixの公式より引用)

ruby! {
    class Console {
        def log(string: String) {
            println!("LOG: {}", string);
        }
    }
}

インスタンスメソッドや特異メソッドなどは定義できるようでしたが、メタクラスやグローバルなメソッドなどは定義できなさそうですね……。

ruru

ruruもHelixと同じくRustでのRuby Native Extensionを簡単に実装できるライブラリです。

以下のようにクラスにメソッドを実装できます。 (なお、以下のコードはruruの公式より引用)

#[macro_use]
extern crate ruru;

use ruru::{Boolean, Class, Object, RString};

methods!(
   RString,
   itself,

   fn string_is_blank() -> Boolean {
       Boolean::new(itself.to_string().chars().all(|c| c.is_whitespace()))
   }
);

#[no_mangle]
pub extern fn initialize_string() {
    Class::from_existing("String").define(|itself| {
        itself.def("blank?", string_is_blank);
    });
}

ただ、こちらは長い間メンテナンスされていないようなのであまり推奨できないですね……。

rutie

rutieもHelixなどと同じくRustでRuby Native Extensionを簡単に実装できるライブラリです。

以下のようにマクロを使い、クラスやメソッドを定義することができます。 (なお、以下のコードはrutieの公式より引用)

#[macro_use]
extern crate rutie;

use rutie::{Class, Object, RString, VM};

class!(RutieExample);

methods!(
    RutieExample,
    _rtself,

    fn pub_reverse(input: RString) -> RString {
        let ruby_string = input.
          map_err(|e| VM::raise_ex(e) ).
          unwrap();

        RString::new_utf8(
          &ruby_string.
          to_string().
          chars().
          rev().
          collect::<String>()
        )
    }
);

#[allow(non_snake_case)]
#[no_mangle]
pub extern "C" fn Init_rutie_ruby_example() {
    Class::new("RutieExample", None).define(|klass| {
        klass.def_self("reverse", pub_reverse);
    });
}

rutieは比較的最近もメンテナンスされているようなので、RustでRuby Native Extensionを作成する際には候補に挙げてもいいかもしれません。

FFI

Rubyの標準ライブラリにあるffiを使うことで比較的簡単にRustで一部の実装を作成することもできます。

たとえば以下のようなRustのコードは

#[no_mangle]
pub extern fn hello_world() {
  println!("Hello world!");
}

// ffi_libメソッドで共有ライブラリを読み込む際にInit関数が必要なため
#[no_mangle]
pub extern fn Init_librust_ffi() {
}

以下のようにffiを使い、呼び出すことができます。

require "ffi"

module Hello
    extend FFI::Library

    ffi_lib "./hello.so"
    attach_function :hello_world, [], :void
end

Hello.hello_world()
# => "Hello World!"と出力される

少しだけ面倒くさいのは、以下の二点です。 - module内でextendを使う必要がある - 専用のInit関数を用意する必要がある

この部分が無ければより簡単にRustでのRuby Native Extensionが実装できそうなんですが……。

実際の実装例は以下のリポジトリを参考にしていただければ幸いです。

rust_ffi_ruby_extension

Fiddle

FiddleRubyで簡単に共有ライブラリをimportし、それをもとにクラスやメソッドなどを定義することができるものです。

例えば以下のようなRustのコードを

#[no_mangle]
extern fn hello_world() {
    println!("Hello World");
}

以下のようなRubyのコードで呼び出し、実行することができます。

require 'fiddle'

libm = Fiddle.dlopen('./hello.so')

hello_world = Fiddle::Function.new(
    libm["hello_world"],
    [],
    :void
)

hello_world.call()
# => "Hello World"と出力される

Init_hello関数が不要なので具体的な処理をRustで書いて、それをRubyのclassやmodule内でメソッドとして使う形でいくのがよさそうです。

Fiddleでの実際の実装は、こちらのリポジトリを参考にしていただければと思います。

rust_fiddle_ruby_extension

おわりに

現状、RustでRuby Native Extensionを書く場合は、rutieかFiddleを使うのが楽でよさそうです。 Rust単体で書く場合は、各コードが多くなり辛くなりそうです。とはいえ、Rust単体で書く方があとあと幅広くカスタマイズが出来そうではありますね。

なので、Rust単体で書くか、ruiteまたはFiddleを使うのが今後のスタンダードになりそうです。

参考記事など

RustだけでRuby native extensionを書く Rubyのネイティブ拡張をRustで作成してgemとして公開した RustのstructをRubyのclassとして扱う Ruby/Rust 連携 (2) 手段 Ruby FFIを使ったエクステンションの作り方 Rust でつくるかんたん Ruby Gem

GitHub Actionsで自動的にマイグレーション&DBバックアップが走るようにしてみた

経緯

仕事の関係で、ステージング環境へ自動的にマイグレーションを走らせたい&DBのバックアップを自動化したい状況があり、GitHub Actionsで自動化してみました。 これはその時のアレコレをまとめたものです。

つくったもの(サンプル)

github.com

仕事で書いているのがGoだったのでGoのmigrateを使っています。

やったこと

GitHub ActionsでDBのバックアップ

以下のようにservicesMySQLのコンテナを指定しています。これはDBのバックアップを取るため必要なCLIコマンドを実行できるようにするためですね。

    name: "Auto migration"
    runs-on: "ubuntu-latest"
    services:
      image: mysql:5.7

stepsで先ほど追加したMySQLのコンテナを使い、DBのバックアップを取得しています。

      - name: backup mysql database
        run: |
          mkdir -p backup/staging
          mysqldump --skip-column-statistics --single-transaction --set-gtid-purged=OFF -u ${{ secrets.MYSQL_USERNAME }} -p${{ secrets.MYSQL_PASSWORD }} -h ${{ secrets.MYSQL_DATABASE_HOST }} ${{ secrets.MYSQL_DATABASE_NAME }} > ./backup/staging/backup_`date '+%Y%m%d%H:%M:%S'`.dump

バックアップに必要なパスワードなどはsecrets経由で追加しています。

これでbakup/stagingディレクトリ内にDBのバックアップが保存されます。

S3へとアップロード

次にjakejarvis/s3-sync-actioを使い、S3へとバックアップをアップロードします。

      - name: Push backups to S3 Bucket
        uses: jakejarvis/s3-sync-action@master
        with:
          args: --follow-symlinks
        env:
          AWS_S3_BUCKET: ${{ secrets.AWS_BUCKET_NAME }}
          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_KEY }}
          AWS_REGION: ${{ secrets.AWS_BUCKET_REGION }}
          SOURCE_DIR: 'backup'

同様にAWSへのアクセスキーなどはsecrets経由で渡しています。

これでS3のバケットにDBのバックアップがアップロードされます。

migrateでマイグレーション

最後にmigrateのインストールとマイグレーションを走らせます。

      - name: install migrate
        run: |
          go get -tags 'mysql' github.com/golang-migrate/migrate/v4/cmd/migrate

      - name: running migration
        run: |
          migrate -path ./migrate -database "mysql://${{ secrets.MYSQL_USERNAME }}:${{ secrets.MYSQL_PASSWORD }}@tcp(${{ secrets.MYSQL_DATABASE_HOST }})/${{ secrets.MYSQL_DATABASE_NAME }}" up

go get経由でmigrateを入れ、マイグレーションを走らせます。

これで自動でマイグレーションとDBのバックアップを行うものができました。

参考記事など

ref: golang-migrateでDBマイグレーション ref: appleboy/database-backup-action ref: valerianpereira/backup-action ref: 【Github Actions】CIのstepでmysqlにSQLを実行する ref: jakejarvis/s3-sync-action ref: GitHub ActionsでCloud SQLのマイグレーションを自動化する