強欲で謙虚なツボツボ

趣味の読書の書の方

【Rails】form_withで非同期通信して表示を動的に変更(data-remote="true")

概要

リストを表示するときに追加読み込み、並び替え、絞り込みなどで表示を動的に変更したいことがしばしばある。
そんな時に、:remote => true.js.erbなどを利用して対応する。
また、そのままだと:remote=>trueをしてもJSリクエストとして処理されず、.js.erbではなく.html.erbが描画されてしまったのでrails-ujsを使うようにする。

 

環境

ruby 3.1.2p20 (2022-04-12 revision 4491bb740a)
Rails 7.0.3.1
Bundler version 2.3.7

 

完成物

github

https://github.com/di-kotobuki/hatena/tree/ajax

・動作の様子

 

data-remote="true"

フォームやリンクのhtml要素にdata-remote="true"の属性が付くと通信がJSリクエストとなって非同期通信になる。
これの何がいいかと言うと、画面がリロードされずに画面の一部の要素を再描画できたりすることだと思う。
VueやReactを使うなら不要になる。

 

やり方

link_toであればremote: trueを、form_withであればlocal: falseを設定すればいい。

Rails で JavaScript を利用する - Railsガイド

route

適当なgetとpostのルートを作成。

Rails.application.routes.draw do
  get "/articles", to: "articles#index"
  post "/articles", to: "articles#create"
end

controller

上記で参照しているarticlesコントローラーとそのメソッドを作成。
コマンド一つでcontrollerやviewを作ってくれるのでそれを編集していく。

$ rails generate controller Articles index --skip-routes
class ArticlesController < ApplicationController
  def index
    @articles = ["aaa", "bbb", "ccc"]
  end

  def create
    @article = params[:title]
    respond_to do |format|
      format.html { redirect_to articles_path }
      format.js
    end
  end
end

view

indexでレンダリングされるindex.html.erbは先のコマンド実行で作成されているので適当に加筆。

<Articles#index>
<Find me in app/views/articles/index.html.erb>
<div>
  <%= form_with url: articles_path, local: false do |f| %>
    <%= f.text_field :title %>
    <%= f.submit %>
  <% end %>
  <%= render "list", articles: @articles %>
</div>

リスト部分をテンプレートに分割する(_list.html.erb)。

<ul id="articles">
  <% articles.each do |article| %>
    <li><%= article %></li>
  <% end %>
</ul>

createでレンダリングされるのはcontroller側で特に指定していなければcreate.js.erbレンダリングされる。これは自分でファイルを作成する。
内容は、formで入力された値でhtml要素を追加するというもの。

(() => {
  document.getElementById("title").value = "";
  const target = document.getElementById("articles");
  const el = document.createElement("li");
  el.textContent = "<%= @article %>";
  target.appendChild(el);
})();

 

rails-ujs

ここまでの状態でrails -sで起動して確認してみると、submitしても何もページが再読み込みされるだけで何も起こらない。
本来formt.jscreate.js.erbが読み込まれるはずが、format.htmlindexへのリダイレクトになってしまっている。
この理由は、submit時のリクエストのContent-TypeでMIMEタイプがtext/htmlになっているからで、これはrails-ujsを適用すれば解決するらしい。
この辺りの通信に関する部分はRailsのバージョンで異なっていたりするらしいので、調べるときはRailsのバージョンをちゃんと見た方がいい。
下記を参考にする。

javascript - Rails UJS not firing with Rails 7 - Stack Overflow

プロジェクトルートで実行。

$ ./bin/importmap pin @rails/ujs

app/javascript/application.jsに追記。

import Rails from '@rails/ujs';
Rails.start();

 

おしまい

これで完成物の出来上がり。

【Rails】Macでの環境構築(エラー対応も)

概要

macOSRailsの開発作業ができるようにするまでの手順。
macには元からrubyとbundlerが入っているが、バージョン管理をしたいのでrbenvでrubyをインストールしてそちらを適用させる。
あと、途中のrbenv installの時にエラーが出たのでその対応。

 

環境

macOS Big Sur 11.4
MacBook Air(Retina, 13-inch, 2019)
プロセッサ Intel Core i5
メモリ 8GB

 

手順

バージョン管理用のrbenvをインストール
rbenvで好きなバージョンのrubyをインストール
gemで好きなバージョンのrailsをインストール

rbenvをインストール

brew install rbenv ruby-build

brew installできたら.bashrcや.zshrcに下記を追記

PATH="$HOME/.rbenv/bin:$PATH"
eval "$(rbenv init -)"

.bashrcや.zshrcを再読み込みして(source ~/.bashrc)、rbenv -vでrbenvのバージョンを確認できたらOK

rubyをインストール

rbenvでインストールできるrubyのバージョンを確認する

rbenv install --list
2.6.10
2.7.6
3.0.4
3.1.2
jruby-9.3.6.0
mruby-3.1.0
picoruby-3.0.0
rbx-5.0
truffleruby-22.1.0
truffleruby+graalvm-22.1.0

Only latest stable releases for each Ruby implementation are shown.
Use 'rbenv install --list-all / -L' to show all local versions.

今回は一番新そうな3.1.2をインストールする(そこそこ時間かかります)

rbenv install 3.1.2

私はここでエラーが出ました。

これだけ見てもわからないのでログ全文を確認する(/var/folders/....../.log)。
下のスクショはログの前半部分、残りはエラー解決に関係なさそう。

デカデカと表示されているOpenSSL関連に問題がありそう。
インストールされているOpenSSLを確認していく。

$ which openssl
/usr/bin/openssl

$ openssl version -a
LibreSSL 2.8.3
built on: date not available
platform: information not available
options:  bn(64,64) rc4(16x,int) des(idx,cisc,16,int) blowfish(idx)
compiler: information not available
OPENSSLDIR: "/private/etc/ssl"

LibreSSLとでているが、これはmacOSに標準搭載されているものらしい。
以前、homebrewでインストールした気がするのでそちらの状態も確認する。

$ brew list
==> Formulae
autoconf ca-certificates gettext git m4 openssl@1.1	pcre2 pkg-config pyenv rbenv readline ruby-build xz

$ brew info openssl@1.1
openssl@1.1: stable 1.1.1q (bottled) [keg-only]
Cryptography and SSL/TLS Toolkit
https://openssl.org/
/usr/local/Cellar/openssl@1.1/1.1.1m (8,081 files, 18.5MB)
  Poured from bottle on 2021-12-15 at 23:34:25
/usr/local/Cellar/openssl@1.1/1.1.1q (8,097 files, 18.5MB)
  Poured from bottle on 2022-07-13 at 12:40:34
From: https://github.com/Homebrew/homebrew-core/blob/HEAD/Formula/openssl@1.1.rb
License: OpenSSL
==> Dependencies
Required: ca-certificates ✔
==> Caveats
A CA file has been bootstrapped using certificates from the system
keychain. To add additional certificates, place .pem files in
  /usr/local/etc/openssl@1.1/certs

and run
  /usr/local/opt/openssl@1.1/bin/c_rehash

openssl@1.1 is keg-only, which means it was not symlinked into /usr/local,
because macOS provides LibreSSL.

If you need to have openssl@1.1 first in your PATH, run:
  echo 'export PATH="/usr/local/opt/openssl@1.1/bin:$PATH"' >> ~/.zshrc

For compilers to find openssl@1.1 you may need to set:
  export LDFLAGS="-L/usr/local/opt/openssl@1.1/lib"
  export CPPFLAGS="-I/usr/local/opt/openssl@1.1/include"

For pkg-config to find openssl@1.1 you may need to set:
  export PKG_CONFIG_PATH="/usr/local/opt/openssl@1.1/lib/pkgconfig"

==> Analytics
install: 1,225,966 (30 days), 3,060,362 (90 days), 11,394,164 (365 days)
install-on-request: 51,798 (30 days), 122,474 (90 days), 473,105 (365 days)
build-error: 1,766 (30 days)

openssl@1.1を使うには色々と環境変数を設定しなければいけないようだ。
まとめて.zshrcに追記してしまう。

export PATH="/usr/local/opt/openssl@1.1/bin:$PATH"
export LDFLAGS="-L/usr/local/opt/openssl@1.1/lib"
export CPPFLAGS="-I/usr/local/opt/openssl@1.1/include"
export PKG_CONFIG_PATH="/usr/local/opt/openssl@1.1/lib/pkgconfig"

.zshrcを読み込んでOpenSSLがどうなったか確認する。

$ which openssl
/usr/local/opt/openssl@1.1/bin/openssl

$ openssl version -a
OpenSSL 1.1.1q  5 Jul 2022
built on: Tue Jul  5 09:08:33 2022 UTC
platform: darwin64-x86_64-cc
options:  bn(64,64) rc4(16x,int) des(int) idea(int) blowfish(ptr)
compiler: clang -fPIC -arch x86_64 -O3 -Wall -DL_ENDIAN -DOPENSSL_PIC -DOPENSSL_CPUID_OBJ -DOPENSSL_IA32_SSE2 -DOPENSSL_BN_ASM_MONT -DOPENSSL_BN_ASM_MONT5 -DOPENSSL_BN_ASM_GF2m -DSHA1_ASM -DSHA256_ASM -DSHA512_ASM -DKECCAK1600_ASM -DRC4_ASM -DMD5_ASM -DAESNI_ASM -DVPAES_ASM -DGHASH_ASM -DECP_NISTZ256_ASM -DX25519_ASM -DPOLY1305_ASM -D_REENTRANT -DNDEBUG
OPENSSLDIR: "/usr/local/etc/openssl@1.1"
ENGINESDIR: "/usr/local/Cellar/openssl@1.1/1.1.1q/lib/engines-1.1"
Seeding source: os-specific

これでrbenv install 3.1.2をしてもエラーが変わりませんでした。
調べてみると--with-openssl-dirというオプションでrubyをインストールするときのopensslを指定できるらしい。

# ディレクトリは$ which opensslで確認したopenssl@1.1までのパス
export RUBY_CONFIGURE_OPTS="--with-openssl-dir=/usr/local/opt/openssl@1.1"

やっとインストールできました。

$ rbenv install 3.1.2
Downloading ruby-3.1.2.tar.gz...
-> https://cache.ruby-lang.org/pub/ruby/3.1/ruby-3.1.2.tar.gz
Installing ruby-3.1.2...
ruby-build: using readline from homebrew
Installed ruby-3.1.2 to /Users/otam/.rbenv/versions/3.1.2

$ rbenv versions
* system
  3.1.2

作業ディレクトリ内で3.1.2を適用する。

$ rbenv local 3.1.2
$ rbenv rehash

$ rbenv versions
  system
* 3.1.2

$ ruby -v
ruby 3.1.2

railsをインストール

$ gem install rails
$ rails -v
Rails 7.0.3.1

アプリケーションを作成する。

$ rails new hatena

アプリケーション起動

$ cd hatena
$ rails s
=> Booting Puma
=> Rails 7.0.3.1 application starting in development
=> Run `bin/rails server --help` for more startup options
Puma starting in single mode...
* Puma version: 5.6.4 (ruby 3.1.2-p20) ("Birdie's Version")
*  Min threads: 5
*  Max threads: 5
*  Environment: development
*          PID: 15596
* Listening on http://127.0.0.1:3000
* Listening on http://[::1]:3000
Use Ctrl-C to stop

 

エラーの対応で結構時間がかかった。
もう深夜3時だ、明日は平日だから問題あるね。
でもエラー解決できたのは嬉しかったからよし。

【AWS】 Amazon MemoryDB for Redis vs ElasticCache

概要

チャット機能を実装する際によく利用されているRedisだが、AWSを利用する場合はどのサービスを選択すべきかがよくわかっていなかったので調べた。

 

選択肢

redisで検索すると以下の2つがヒットする。

  • Amazon MemoryDB for Redis
  • ElasticCache

これらのどちらを利用すべきか。
例えばチャット機能を実装したい場合、「AWS チャット」みたいに検索するとElasticCacheを利用するモデルが検索結果には多い。
ただ、MemoryDBの方が後発サービスらしく、こちらを使った方が良さそうにも思える。

間違ってreidsと検索しているが結果は変わらない

 

比較検討

しばらく調べてみて一番参考になりそうだったのが↓

https://cloudwellserved.com/amazon-elasticache-for-redis-vs-amazon-memorydb-for-redis/

ElasticCache

AWSのサービス概要にも「In-Memory Cache」とあるように、データをメモリにキャッシュとして保存して取り扱うサービス。
本来データベースにデータを取りに行くところを、キャッシュがあればそちらを取りに行かせることができて、データベースへのアクセスを減らせて処理も軽くなるから速度改善できるよ、というサービスのようだ。

決してデータベースではなく、あくまでデータベースのキャッシュとしての立ち位置であり、AWS上ではRDSと共に運用されるサービスである。
また、読み込みはキャッシュからできるが、書き込みはデータベースに対して行わなければならず、その後にキャッシュの更新も必要。

単体で利用するならばセッションの保持とかできそう。

Amazon MemoryDB for Redis

AWSのサービス概要にも「Fully managed, Redis-compatible, in-memory database service」とあるように、データベースサービスである。
RDS + ElasticCacheで運用していたものをまとめて一つのサービスとしたようなもの。
データの読み書き共にこのサービスに対して実行すればよく、こちらの方がデータベースとしてRedisを使いたいという利用目的に適っている。

料金はどこを見てもElasticCacheの約1.5倍と見積もっているところが多い気がする。

 

結論

セッションの保持などのブラウザ上でいうCookieのような利用がしたいのであれば、ElasticCache単体で良さそう。

チャットなどのデータベースに対する読み書きのような利用がしたいのであれば、ElasticCache + RDSよりも運用しやすそうなAmazon MemoryDB for Redisを使いたい。

備忘録(go-swaggerでAPIサーバを作る、gorm, air, dockerと共に)

GoでREST APIを作ります。

go-swaggerはyamlでWebAPIの定義をすることで、サーバーサイド・クライアントサイドのコードをコマンドの実行で生成してくれる。
今回はサーバーサイドだけ欲しいのでクライアントサイドは生成しない。
ただし、リクエストを受けた時にパラメータに従ってデータベースとやりとりする部分の処理(handler)は自身で実装する必要がある。
Railsとかでいうcontrollerのメソッドが中身空っぽな状態で必要なだけ自動生成され、その中身を自分で実装するみたいな感じ?

データベースはmysql

DockerでAPI用のコンテナとデータベース用のコンテナを起動させる。
また、コード編集でホットリロードさせるためにAPIの起動にはairを使う。

 

 

ディレクトリ構成

最終的にこうなる(ディレクトリと使うファイルのみ表示)
gen以下が自動生成されたコードで、ルートディレクトリからだとgo run api/server/gen/cmd/xxx/main.goで起動できる。
gen以下の生成されたコードは編集不可だが、restapi内のconfigure_xxx.goだけは編集可能。起動時処理やミドルウェアを記述したりするもので、このファイル存在時にgo-swaggerによるコード生成を行っても上書きされないから編集することができる。

root
  ├─ api
  │    ├─ server
  │    │    ├─ gen
  │    │    │    ├─ cmd
  │    │    │    │    └─ xxx
  │    │    │    │         └─ main.go
  │    │    │    ├─ models
  │    │    │    └─ restapi
  │    │    │         ├─ operations
  │    │    │         └─ configure_xxx.go
  │    │    └─ handlers
  │    │         └─ 自分で作成したhandler
  │    ├─ swagger
  │    │    └─ swagger.yaml
  │    ├─ .air.toml
  │    ├─ go.mod
  │    ├─ go.sum
  │    └─ Dockerfile
  ├─ config
  │    └─ config.go
  ├─ db
  │    └─ Dockerfile
  └─ docker-compose.yml

 

Docker

Dockerfileとdocker-compose.ymlを作成

# api/Dockerfile
FROM golang:1.16-alpine

RUN apk update && apk add git

# ホットリロードのためにairを導入
RUN go get github.com/cosmtrek/air

WORKDIR /usr/src/app
COPY api .
RUN go mod download && go mod verify

EXPOSE 8000
# db/Dockerfile
FROM mysql:5.7

COPY db/my.conf /etc/mysql/conf.d

EXPOSE 3306
# db/my.conf
[mysqld]
character-set-server=utf8mb4
collation-server = utf8mb4_bin
general_log=1
general_log_file=/var/log/mysql/mysql.log
log-error=/var/log/mysql/mysql-error.log

[mysqld]
character-set-server=utf8mb4

[client]
character-set-server=utf8mb4
# docker-compose.yml
version: "3"

services:
  api:
    build:
      context: .
      dockerfile: api/Dockerfile
    container_name: api
    environment:
      - DB_DRIVER=mysql
      - DB_DATABASE=dev
      - DB_USER=user
      - DB_PASSWORD=password
      - DB_HOST=db
      - DB_PORT=3306
    volumes:
      - api:/usr/src/app
    ports:
      - 8000:8000
    tty: true
    command: /bin/sh -c "air -c .air.toml"
    depends_on:
      - db

  db:
    build:
      context: .
      dockerfile: db/Dockerfile
    container_name: db
    environment:
      - MYSQL_DATABASE=dev
      - MYSQL_USER=user
      - MYSQL_PASSWORD=password
      - MYSQL_ROOT_PASSWORD=password
      - TZ=Asia/Tokyo
    ports:
      - 3306:3306

 

go-swagger導入

cd api
go mod init github.com/{githubのユーザー名}/{githubリポジトリ名}
go get -u github.com/go-swagger/go-swagger/cmd/swagger

 

swagger.yamlを作成

go-swaggerはOpenAPI2.0対応で、3.0は対応していないので注意

VScodeを使っているならば、Swagger Viewerというプラグインが便利

# api/swagger/swagger.yaml
---
swagger: "2.0"
info:
  version: 1.0.0
  title: Test
basePath: /api/v1
shemes:
  - http
paths:
  /user:
    get:
      tags:
        - user
      produces:
        - application/json
      responses:
        200:
          desciption: List of users
          schema:
            type: array
            items:
              $ref: "#/definitions/User"
        default:
          description: Unexpected error
          schema:
            $ref: "#/definitions/Error"
definitions:
  User:
    type: object
    properties:
      id:
        type: integer
        example: 1
      name:
        type: string
        example: テスト名前
      email:
        type: string
        example: test@gmail.com
      created_at:
        type: string
      updated_at:
        type: string
      deleted_at:
        type: string
  Error:
    type: object
    properties:
      code:
        type: integer
        example: 400
      message:
        type: string
        example: bad request

 

swagger.ymlからコードを生成

これによってgen以下にコードが生成される

cd api
mkdir -p server/gen
swagger generate server --strict-additional-properties -t ./server/gen -f ./swagger/swagger.yaml

 

.air.tomlの編集

起動するのに生成されたmain.goを参照するように変更し、hostとportを指定する
hostはデフォルトが127.0.0.0でDockerコンテナ外からアクセスするのに不便なので0.0.0.0を指定
portは起動するたびにランダムでホットリロードされるたびにポートが変化して面倒なので8000を指定

# api/.air.toml
cmd = "go build -o ./tmp/main ./server/gen/cmd/xxx"
full_bin = "APP_ENV=dev APP_USER=air ./tmp/main --host 0.0.0.0 --port 8000"

 

ハンドラを作成

この時点でdocker-compose up -dで起動できるが、localhost:8000/api/v1/userにアクセスすると「operation user.GetUser has not yet been implemented」と返ってくる
自動生成されたコードを見るとapi/server/gen/restapi/configure_xxx.goでapi.UserGetUserHandlerが定義されていないことがわかる。
なのでハンドラを作成して設定する。

# api/server/handers/user.go
type UserHandler struct {}

func NewUserHandler() UserHandler {
    return UserHandler{}
}

func (uh *UserHandler) GetUser(params user.GetUserParams) middleware.Responder {
    ver us []*models.User
    return user.NewGetUserOK().WithPayload(us)
}
# api/server/gen/restapi/configure_xxx.go
# configureAPI内に下記を追加
uh := handlers.NewUserHandler()
api.UserGetUserHandler = user.GetUserHandlerFunc(uh.GetUser)

これでlocalhost:8000/api/v1/userにアクセスするとhanderのWithPayloadで設定した空の配列が返ってくる

 

環境変数設定

docker-compose.ymlのenvironmentに記述した環境変数を読み込んでおく

# api/config/config.go
package config

type Config struct {
    DbDriver   string
    DbDatabase string
    DbUser     string
    DbPassword string
    DbHost     string
    DbPort     string
}

func NewConfig() Config {
    return Config{
        DbDriver:   os.Getenv("DB_DRIVER"),
        DbDatabase: os.Getenv("DB_DATABASE"),
        DbUser:     os.Getenv("DB_USER"),
        DbPassword: os.Getenv("DB_PASSWORD"),
        DbHost:     os.Getenv("DB_HOST"),
        DbPort:     os.Getenv("DB_PORT"),
    }
}

 

データベース接続

dockerで起動しているdbへ接続する。
接続情報は先の環境変数
今回はgormを使用する。

go get -u gorm.io/gorm
go get -u gorm.io/driver/mysql
# app/database/database.go
package database

type DatabaseClient struct {
    c *config.Config
}

func NewDatabaseClient(config *config.Config) DatabaseClient {
    return DatabaseClient{
        c: config,
    }
}

func (dc *DatabaseClient) ConnectDatabaseClient() *gorm.DB {
    driver := dc.c.DbDriver
    dsn := fmt.Sprintf(
        "%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local",
        dc.c.DbUser,
        dc.c.DbPassword,
        dc.c.DbHost,
        dc.c.DbPort,
        dc.c.DbDatabase,
    )
    db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
    if err != nil {
        log.Fatal("Failed to connect database: ", err.Error())
    }
    return db
}

func (dc *DatabaseClient) CloseDatabaseClient(db *gorm.DB) {
    sqlDb, _ := db.DB()
    if err := sqlDb.Close(); err != nil {
        log.Fatal("Failed to close connection database: ", err.Error())
    }
}
# api/server/gen/restapi/configure_xxx.go
# configureAPIを編集(下記は編集後の最終形態)
func configureAPI(api *operations.xxxAPI) http.Handler {
    // 下記を追記
    c := config.NewConfig()
    dc := database.NewDatabaseClient(&c)
    db := dc.ConnectDatabaseClient()
    
    // さっきhandler作成の時に追加した記述(引数にdbを持たせる)
    uh := handlers.NewUserHandler(db)
    api.UserGetUserHandler = user.GetUserHandlerFunc(uh.GetUser) 
    
    // 以下は初期のまま
    api.ServerError = errors.ServerError
    api.UseSwaggerUI() 
    api.JSONConsumer = runtime.JSONConsumer()
    api.JSONProducer = runtime.JSONProducer() 
    api.PreServerShutdown = func() {} 
    
    // CloseDatabaseClientを追記 
    api.ServerShutdown = func() { 
        dc.CloseDatabaseClient(db) 
    }
    
    return setupGlobalMiddleware(api.Serve(setupMiddleware)) }
# api/server/handlers/user.go
# データベースから情報を取得するように編集(下記は編集後の最終形態)

# DBを操作できるように変更
type UserHandler struct {
    db *gorm.DB
}

# 引数にdbを受け取るように変更
func NewUserHandler(db *gorm.DB) UserHandler {
    return UserHandler{
        db: db,
    }
}

# DBからデータを取得するように変更
func (uh *UserHandler) GetUser(params user.GetUserParams) middleware.Responder {
    ver us []*models.User
    res := uh.db.Find(&us)
    if err := res.Error; err != nil {
        return user.NewGetUserDefault(500)
    }
    return user.NewGetUserOK().WithPayload(us)
}

 

これでlocalhost:8000/api/v1/userにアクセスすれば、データベースに登録した情報を取得できる。

備忘録(データベース名、ユーザー名の変更)

データベース名とユーザー名の変更の仕方

 

データベース名

MySQL 5.2以上であれば、RENAME DATABASEが使えないので新たに作成して古いものを削除する
テーブルは新しいDBに属するように名前を変更

CREATE DATABASE new_db;
RENAME TABLE old_db.table1 TO new_db.table1, old_db.table2 TO new_db.table2, ... ;
DROP DATABASE db_old;

 

ユーザー名

RENAME USER old_name TO new_name;

備忘録(ECR + ECS で複数コンテナ)

各自のPCではdocker-composeを使ってAPI用のコンテナとフロント用のコンテナを作成して開発しているものを、AWS ECSとECRでデプロイする。

AWS上ではdocker-composeが使われるわけではなく、それぞれのコンテナを別々にdockerコマンドで起動しているような感じになる。
なので、コンテナの数だけECRでリポジトリ・ECSでタスクとサービスを設定する必要がある。
docker-compose.ymlで記述しているような環境変数やポートの設定はECR・ECSの設定としてコンソール上で入力していく。

 

 

参考

これらを見ながら作業をしました。

Dockerに慣れてきた人がECSで複数コンテナのデプロイをしてみる - Qiita

Dockerコンテナで作ったアプリをECS+RDSでデプロイする - Qiita

 

手順

Dockerはイメージがあって、それを元にコンテナを作成して、そのコンテナを起動する。
そして、イメージはECR・コンテナの作成と起動はECSで設定するので、ECRでリポジトリを作成してからECSでタスク定義とサービスの設定をする。

 

ECR

Create repository

ECRでDockerイメージを管理するリポジトリを作成する。

API用とフロント用で二つのDockerfileを元としたDockerイメージのために二つのリポジトリを作成する。

  • Visibility setttings:Private
  • Repository name:任意(プロジェクト名-コンテナ名とかが無難)
  • Tag immutability:Disabled
  • KMS encryption: Disabled

Createで作成完了。

 

Push

作成したリポジトリを見るとView push commandsとあるので、それに倣えばいい。
AWS CLIをインストールしておく必要がある。

aws ecr get-login-password --region {リージョン} | docker login --username AWS --password-stdin {AWSアカウントID}.dkr.ecr.{リージョン}/amazonaws.com
# AWS CLIのデフォルトプロファイル以外を使用する場合はプロファイルを指定する
# aws ecr get-login-password --region {リージョン} --pofile {プロファイル名} | docker login --username AWS --password-stdin {AWSアカウントID}.dkr.ecr.{リージョン}/amazonaws.com docker build -t {リポジトリ名} . # Dockerfileがルートディレクトリにない場合はパスを指定する
# dokcer build -t {リポジトリ名} -f {Dockerfileのパス} . dokcer tag {リポジトリ名}:latest {AWSアカウントID}.dkr.ecr.{リージョン}.amazonaws.com/{リポジトリ名}:latest docker push {AWSアカウントID}.dkr.ecr.{リージョン}.amazonaws.com/{リポジトリ名}:latest

 

ECS

クラスター作成→タスク定義→サービス設定

クラスターはdockerインストール済のEC2インスタンスだと思うと楽。
今回は、一つのクラスター内にAPI用のサービスとフロント用のサービスを作成する。

タスクはどのECRリポジトリのイメージを使用してコンテナを作成するかとか環境変数を設定したりする。

サービスはクラスター内でどのタスクを動かすかを決めるようなもの。docker-compose.ymlのservicesだと思うと楽かもしれない。(厳密には全然違うかもしれないけど)
frontendとbackendの二つのサービスになることが多いと思う。

Create Cluster

Step 1: Select cluster template

AWS Fargateでいい場合はNetworking onlyを選択。
Fargateは軽いEC2とでも思えばいい。
EC2みたいにssh接続はできないので異常時に中で確認したりはできない。あと、dockerコンテナのポートをホストで別のポートに結びつけることもできない。(8080:3000みたいな)
開発環境であればFargate、本番環境であればEC2でいいと思う。

Step 2: Configure cluster
  • Cluster name:任意
  • Create VPC:チェック
  • Tags:無記入
  • CloudWatch Container Insights:チェック

Createで作成完了。

Create new Task Definition

API用とフロント用で二つ作成する。
Step2のTask definition nameとTask execution role、Add containerの入力内容以外は同じ。

Step 1: Select launch type compatibility

たぶんクラスターと同じやつを選べばいい。
FARGATEを選択。

Step 2: Configure task and container definitions
  • Task definition name:任意
  • Requires compatibilities:FARGATE(Step 1で選んだやつ)
  • Task role:未選択(IAMロールが必要なら設定)
  • Network mode:awsvpc
  • Operating system family:Linux
  • Task execution role:なければ作成(2回目以降ならば初回で作ったものを選択)
  • Task memory (GB):0.5GB
  • Task CPU (vCPU):0.25 vCPU
  • Add Container:ボタンを押すとコンテナに関する入力欄が表示される
    Container name:任意
    Image:ECRのURIをコピー
    Port mappings:コンテナがListenするポート
    Environment variables:環境変数を設定(docker-compose.ymlのenvironmentやenv_fileの内容)
    その他の項目はそのままにした。
  • Service integration:未チェック
  • Proxy configuration:未チェック
  • Log router integration:未チェック
  • Volumes:未設定
  • Tags:無記入

Createで作成完了。

Create Service

API用とフロント用で二つ作成する。
Step1のTask DefinitionでAPI用・フロント用それぞれ対応するタスクを設定。
Step2のSecurity groupはAPI用とフロント用で分けるため別々の設定。

Step 1: Configure service
  • Launch type:FARGATE(クラスターと同じ)
  • Operating system family:Linux
  • Task Definistion:作ったタスクを選択
  • Platform version:LATEST
  • Cluster:作ったクラスタ
  • Service name:任意
  • Service type:REPLICA
  • Number of tasks:1
  • Minimum healty percent:100
  • Maximum percent:200
  • Deployment circuit breaker:Disabled
  • Deployment type:Rolling update
  • Enable ECS managed tags:チェック
  • Propagate tags from:Do not propagate
Step 2: Configure network
  • Cluster VPCクラスター作成時に自動作成されたVPCを選択
  • Subnets:クラスター作成時に自動生成されたサブネットを全て選択
  • Security groups:そのまま(新規作成)
  • Auto-assign public IP:ENABLED(あとで変更できないので注意)
  • Load balancer type:None(本番環境であればApplication Load Balancerを選択した方がいい)
  • Service discovery (optional):API用のみチェック
Service discovery (API用のみ)

各自のPCでdocker-composeを使用するときは、コンテナ内から別コンテナへアクセスのにコンテナ名を使用(http://api:8080みたいに)すればできる。
サービスディスカバリーはそのような設定をすることができ、Namespace nameにlocal・Service discovery nameにapiと設定すれば、local.api:portでフロント用コンテナ内からAPI用コンテナにアクセスできる。

  • Enable service discovery integration:チェック
  • Namespace:create new private namespace
  • Namespace name:任意
  • Configure service discovery service:Create new service discovery service
  • Service discovery name:任意
  • Enable ECS task health propagation:チェック
  • DNS record type:A
  • TTL:10
Step 3: Set Auto Scaling (optional)

本番であればちゃんと設定したほうがいいと思う。

  • Service Auto Scaling:Do not adjust th service's desired count
Step 4: Review

Step 1~3でしてきた設定を確認。

Create Serviceで作成完了。

クラスターでServicesがACITIVE・TasksがRUNNINGになっていることを確認できたらデプロイ完了。

 

Security Groups

これまでの過程でセキュリティグループが自動作成されているが、デフォルトではポートが80番しか空いていないので、コンテナがListenしているポートを開放する。
また、データーベースでRDSを使用する場合はAPI用のコンテナからのアクセスを許可する必要がある。

 

備考

今回作成したECS・ECRをgithubのプッシュを検知して自動デプロイさせる場合は、こちらを参照。(CodeBuild→CodePipeline)

taopo.hatenablog.com

taopo.hatenablog.com

 

備忘録(Go + Air in Docker)

dockerコンテナで go run main.go をした後に、ホスト側のローカルで修正しても go run し直すかdockerコンテナを再起動するしないと反映されない。
Airを使うと変更を随時反映させることができる。(ホットリロード・live reload)
Realizeというのもあるが使ったことはない。

 

Dockerfile

GitHub - cosmtrek/air: ☁️ Live reload for Go apps

FROM golang:1.16-alpine
RUN apk update && apk add git
RUN go get github.com/cosmtrek/air@latest
WORKDIR /usr/src/app
COPY go.mod go.sum .
RUN go mod download && go mod verify
COPY . .
EXPOSE 8080

 

docker-compose.yml

コマンドを go run main.go ではなく air -c .air.toml としてairで起動させる。
.air.tomlは設定ファイルで下記を編集。

air/air_example.toml at master · cosmtrek/air · GitHub

version: "3"

services:
  go:
    build:
      context: .
      dockerfile: Dockerfile
    container_name: go
    volumes:
      - ./:./
    ports:
      - 8080:8080
    tty: true
    command: /bin/sh -c "air -c .air.toml"

あとは、dockerコンテナを起動すれば編集を随時反映してくれる。