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のマイグレーションを自動化する

Mastodonの鯖缶を4年ほどやってみての雑感

はじめに

この記事はFediverse Advent Calendar 2021 の7日目の記事です。

adventar.org

 

2017年からMastodonサーバーを運営してみての雑感を書きたいと思い、参加しました。

 

軽く自己紹介

S.H.という名前で、Creatodonという創作系のMastodonサーバーの管理人をしています。

gamelinks007.net

 

また、Mastodonのバックエンドで使用されているプログラミング言語Rubyに趣味でパッチなどを投げてたりします。

 

その辺の話は以下の記事にまとまっていますので、そちらを読んでいただければと思います。

gamelinks007.hatenablog.com

 

4年間の振り返りなど

運営など

2017年4月のMastodonブーム以来、色んなサーバーが出来ては消えていきました。

DBが爆破しちゃったり、アップデートが上手くいかなかったり、運営に疲れたり、その理由は様々でした。

 

うちのサーバーは時々トラブルが起きたものの、幸いDBの爆破などもなく平穏でした。

またユーザーも各々が好きなことをしている感じなので大きなトラブルとかもなく(まあ、僕の観測範囲内に無いだけかもですが)緩い感じで運営できていました。

 

そのおかげか鯖缶として運営に疲れるとかそういうこともなかったですね。

 

ほかのサーバーもそうなのかはわかりませんが、あんまり気張って運営していないくらいが気楽で運営しやすいのかもしれません。

僕の観測範囲ですが、古くからあるサーバーはガチガチの運営でもなかったりするように思いますし

 

そのほかの分散SNSなど

この四年間でPleromaやMisskey、今は亡きHivewayなど様々な分散SNSが登場し、ずいぶんとFediverseは賑やかになってきたなぁと思います(2017年のころだとGNU ScoialとかMastodonとかくらい?だったと思うので)

 

その分サーバーの数も増え、人の交流も増えたと思います。

 

ただ、同時に何かしらのトラブルを見かけることも増えたように思います。

 

そのトラブルも分散SNSの仕組みのおかげか関連する投稿が流れてくることも少ないですし、ドメインブロックなどで元から断つこともできたりするので大丈夫かなと思ってたり。

 

まあ、一人のユーザーとしても、SNSを運営する管理人としても複数の選択肢が出てきたのはうれしいですね。

 

個人としての分散SNS

一個人として分散SNSを使う分にはかなり満足していますね。

特に自分の好きなように呟けるサーバーを自分で運営しているというのは大きいかなと。

 

それといくつものサーバーがあり、サーバーそれぞれのカラーも違うので自分に合っているところを探してもいいですし、または自分で建ててもよいところも好きですね。

 

いい意味で好きにできる土壌はあるなと思ってます。

 

ただ、その分自分と価値観の違う人と出会うことも多いので、衝撃を受けることもあったりします。

それがいい刺激の時もあれば、良くないときもあります。

 

そういう時にスッと距離を置きやすいのは分散SNSのいいところかなと思いますね。

 

おわりに

 

Mastodonサーバーを建ててからもう4年も経っているのかと思うと感慨深いですね……。

今後も緩く運営していきたいと思います。