Simple minds think alike

より多くの可能性を

RailsプロジェクトでVisual Studio Code Remote Container使ってみた

ローカル環境でDockerを使ってRailsプロジェクトを開発する際に

  • Railsのmigrationのようなコマンドが実行しにくい
  • アプリケーションログが見づらい

といった課題があり、Dockerを使わない場合と比べて少し手間がかかっていました。

コンテナ内のファイルシステムを直接触れるVisual Studio Code Remote Containerを使い始めたのですが、手間を減らしつつも便利開発できていそうな感じがしています。 シンプルなRuby on railsアプリケーションを例に取って導入方法を紹介したいと思います。

イメージとしては、下の図のような感じでVSCodeというインタフェースを介して、Dockerコンテナ内であらゆるタスクが実行できます。

環境

RailsアプリをDocker-composeで起動できるように

まずは、 docker-composeRailsアプリケーションサーバのコンテナを起動できるようにします。

Railsプロジェクト準備

構成はできるだけシンプルな方が分かりやすいので

  • データベースはデフォルトの SQLite3
  • Webpackerなし

になるように、rails new して新規アプリケーションを作ってみます。

$ rails new vscode-remote-container-prj --skip-webpack-install

あとでデバッグ実行を試すので Gemfileに

group :development do
  gem 'debase'
  gem 'ruby-debug-ide'
end

を追加しておきます。

Dockerfile, docker-compose.yml

シンプルなものを作っておきます。

動作確認

Webブラウザから http://localhost:3000 にアクセスして、動くことを確認しておきます。

$ docker-compose up

Visual Studio Code Remote Containerでコンテナのファイルシステムにアクセス

ここからはVS code側の設定です。コンテナは停止状態でOK。

VSCode拡張インストール

Remote - Containersをインストールします。

Visual Studio Codeで開くディレクトリを指定

左下の緑色の箇所を選択し、先程作ったローカルのRailsプロジェクトのディレクトリを指定します

開発コンテナ設定ファイルの作成

from docker-compose.yml、サービス名 web 、と順番に選択すると自動的に設定ファイルができます。Dockerfileも選択できますが docker-compose.yml 使った方が設定の柔軟性が高いです。

開発コンテナ設定ファイルについて

.devcontainerディレクトリに設定ファイルが2つできます。

devcontainer.json

  • docker-compose.ymlのパス
  • VSコードで使うdocker-compose.yml内のサービス名
  • ワークスペースのフォルダ
  • コンテナ作成時に自動インストールする

などが設定できます。

docker-compose.ymlのパスはデフォルトで、["../docker-compose.yml", "docker-compose.yml"]のように複数指定されていて、次のVSCode Remote Container用の docker-compose.ymlで指定される項目は上書きされます。

詳しくは devcontainer.jsonのリファレンスをご参考ください。

docker-compose.yml

シンプルな以下の設定

version: '3'
services:
  web:
    volumes:
      - .:/workspace:cached
    command: /bin/sh -c "while sleep 1000; do :; done"

で上書きするようになっているので、プロジェクトのdocker-compose.ymlを変更せずにVSCode側で柔軟にデバッグ実行などができます。

docker-compose.yml を使った設定方法に関して、詳しくはこちらのドキュメントに記載があります。

VS Code Remote Containerで開いた直後の状態

コンテナに接続された状態になります。

しかし、rails serverは起動しなくなっているので、http://localhost:3000にアクセスしてもレスポンスは返ってきません。

Visual Studio Code Remote Containerでデバッグできるように

ここからはRemote Containerとは関係ない部分にはなりますが、作ったRailsプロジェクトをデバッグ実行できるようにしてみます。

開発コンテナ設定ファイルの編集

先程生成された設定ファイル .devcontainer/devcontainer.json のExtensionsを以下のように変更し、Rubyのコードをデバッグできるようにします。

"extensions": [
    "rebornix.ruby"
]

launch.jsonを作成

以下のような .vscode/launch.jsonを作り、VS Code上からRails serverを実行できる状態にしておきます。

{
    "version": "0.2.0",
    "configurations": [
        {
            "name": "Rails App",
            "type": "Ruby",
            "request": "launch",
            "cwd": "${workspaceRoot}", 
            "program": "bin/rails",
            "args": ["s", "-b", "0.0.0.0", "-p", "3000"], 
        }
    ]
}

一度、左下の緑色の箇所から、リモート接続を終了し、接続し直します。

設定ファイルは変更するとコンテナを再度ビルドし直す必要があり、以下のようなダイアログが表示されるので、Rebuildを選択します。

デバッグの実行

デバッグ実行します。

scaffold(↑は rails g scaffold user name:string した)を作ったりして、breakpointに止まることを確認してみてください。

今回作ったプロジェクト

コードはリポジトリにあげておくので、よかった試してみてください。

github.com

参考資料

【Golangスクレイピング】 Colly と goqueryを一緒に使うには

Golangスクレイピングする時はとりあえずgoquery使っておけば良さそうーと思って、あまり別のライブラリを使おうとしてこなかった。けれど、高機能なスクレイピング用ライブラリでCollyというのがあって、スクレイピング処理の中でgoqueryのHTMLElementを取得できるらしく、便利そうだと思って使い始めてみました。

ドキュメントにも↓の e.DOMgoquery.Selectionって書いてますね。こんな感じ書けるらしい。

// コレクターの初期化
c := colly.NewCollector()

// itemNameクラスの付いたAタグを走査
c.OnHTML("a.itemName", func(e *colly.HTMLElement) {
    selectionNode:= e.DOM // ← goquery.Selection
      // 兄弟要素(Siblings)の内容を出力
    selectionNode.SiblingsFiltered(".itemDetail").Each(func(i int, s *goquery.Selection) {
        fmt.Println(s.Html())
    })
})

// サイトにアクセス
c.Visit("https://www.sample-shop.com")

高機能なcollyの個人的に嬉しいところは

goqueryはjqueryのようにDOMに対して豊富な操作が可能で、親子要素(Closest)/兄弟要素(SiblingsMatcher)を取得でき、取り回しが良いのが便利です。

Colly+goqueryは楽に複雑なクローリングを実現できそうなので、サンプルを参考にしながら今後色々なケースで試してみたいです。

参考

Scraping the Web in Golang with Colly and Goquery | Ben Congdon

Herokuで動いているGolangプロジェクトをDockerコンテナで動くように

前回の記事で、Herokuで動くようになったGolangプロジェクト(フレームワークはEcho)を今回Dockerコンテナで動くようにしてみました。 simple-minds-think-alike.hatenablog.com

前提

  • Herokuアカウント作成済
  • ローカル環境にHeroku CLIインストール済
  • ローカル環境にDockerインストール済

Dockerfileの作成

こんな感じで動くDockerfileを作ってみます。

以下のようなDockerfileをプロジェクトに追加しました。一つづつ解説していきます。

# Herokuで実行するGoのバイナリを作る
FROM golang:latest as builder

ENV CGO_ENABLED=0
ENV GOOS=linux
ENV GOARCH=amd64
WORKDIR /app
COPY . .
RUN go build main.go

# 作ったGoのバイナリを実行する
FROM alpine:latest  
COPY --from=builder /app /app

CMD /app/main $PORT

実行バイナリファイルを作るイメージ

FROM golang:latest as builder

実行バイナリを作るためにgo build を使いたいので、golang:latestイメージを使っています。

変数ENV_CGO_ENABLED

ENV CGO_ENABLED=0

netパッケージを使う場合には、CGO_ENABLEDを無効にしないと複数のバイナリができてしまう(Dynamic link)ので無効にしておきます。

参考にしたStackoverflowのリンクを貼っておきます。 stackoverflow.com

Golangのドキュメントにも書いてありました。

Go 1.2 Release Notes - The Go Programming Language

The net package requires cgo by default because the host operating system must in general mediate network call setup. On some systems, though, it is possible to use the network without cgo, and useful to do so, for instance to avoid dynamic linking. The new build tag netgo (off by default) allows the construction of a net package in pure Go on those systems where it is possible.

変数GOOS, GOARCH

ENV GOOS=linux
ENV GOARCH=amd64

以下のHerokuのドキュメントに

Container Registry & Runtime (Docker Deploys) | Heroku Dev Center

Docker images run in dynos the same way that slugs do, and under the same constraints:

と書いてあり、Docker使わない場合とホストOS自体は同じになるようなので、OSはUbuntu 18.04ベース(2020年5月時点で最新のHerokuスタック)ということになります。

実行環境であるalpineはマルチアーキテクチャイメージで、ホストOSと同じlinuxアーキテクチャのイメージになるので、Ubuntu 18.04と同じになるように

  • ENV GOOS=linux
  • ENV GOARCH=amd64

を指定します。

バイナリを実行するイメージ

バイナリを実行するコンテナにシンプルなalpineイメージを使います。

FROM alpine:latest  

ビルドしたバイナリをコピー

COPY --from=builder /app /app

バイナリ実行

CMD /app/main $PORT

前回の記事でも書きましたが、HerokuではWeb worker processに紐付けられるPORT番号は環境変数PORTから取れます。 Herokuの以下のドキュメントに記載があります。

Runtime Principles | Heroku Dev Center

ローカルで動作確認

$ docker build -t golang-sample .
$ docker run -e "PORT=3000" -p 3000:3000 -t golang-sample

以下のURLにアクセスして、動作確認します。

http://localhost:3000/books/1

Heroku container registoryにイメージをpush

Heroku container registoryにログイン

$ heroku container:login

イメージビルドして、Heroku container registoryにイメージをpushします

$ heroku container:push web

moritamorie:~/go/src/github.com/golang-sample$ heroku container:push web
 ›   Warning: heroku update available from 7.39.0 to 7.39.3.
=== Building web (/home/moritamorie/go/src/github.com/golang-sample/Dockerfile)
Sending build context to Docker daemon   11.7MB
Step 1/10 : FROM golang:latest as builder
 ---> 7e5e8028e8ec
〜〜〜
Step 10/10 : CMD /app/main $PORT
 ---> Using cache
 ---> 4f60b0d6aaa9
Successfully built 4f60b0d6aaa9
Successfully tagged registry.heroku.com/golang-sample/web:latest
=== Pushing web (/home/moritamorie/go/src/github.com/golang-sample/Dockerfile)
The push refers to repository [registry.heroku.com/golang-sample/web]
Your image has been successfully pushed. You can now release it with the 'container:release' command

リリース

Heroku container registoryにpushしたイメージをリリースします

$ heroku container:release web

イメージビルド、Heroku container registoryへのpush、リリースの手間を無くす

以下の heroku.yml ファイルを追加すると、buildプロセスをHeroku側に移すことができるので、Dockerを使わないときと同様の手順( git push heroku master )で済みます。

build:
  docker:
    web: Dockerfile

Procfile削除

コンテナ側でバイナリの実行までやってくれるので、Docker対応が済んだらProcfileは削除してしまってOKです。

サンプル

今回のサンプルのコードも置いておくのでよかったら参考にしてみてください! github.com

2020/12/20追記

以下の記事で、Github Actionsを設定することでPRがmasterブランチにマージされた時に自動的にHeroku環境にデプロイ(Container Registryにpush、web dynoに反映)できるようにしてみました。もしよろしければこちらの記事も参考にしてみて下さい! simple-minds-think-alike.hatenablog.com

参考

Golang EchoプロジェクトをHerokuにデプロイしてみた (依存パッケージ管理: go module)

Go modulesが入って、GolangのHerokuへのデプロイがだいぶ簡単になっていたので、備忘録のためプロジェクトの作成から確認までの手順を書いておきます。

環境

  • go 1.13
  • echo v3.3.10

前提

ローカルで動くまで

プロジェクトディレクトリ作成

以下のようなディレクトリを作ります。

mkdir golang-sample
cd golang-sample

簡単なリクエストを受け付けるGolangのコードを作る

簡単なリクエストを受け付ける以下のようなGolangのコード( main.go )を golang-sampleディレクトリ内に作ります。

package main

import (
    "os"
    "net/http"
    "github.com/labstack/echo"
)

type Book struct {
    Title string `json:"title"`
    Author string `json:"author"`
}

func main() {
    // Echo インスタンスをNew
    e := echo.New()

    // ルーティング設定
    e.GET("/books/:id", bookHandler)

    // サーバ起動
    e.Start(":"+os.Getenv("PORT"))
}

// ハンドラ
func bookHandler(c echo.Context) error {
    return c.JSON(http.StatusOK, getBook())
}

func getBook() *Book {
    b := &Book{
        Title: "Golangの本",
        Author: "Golangの本の著者",
    }
    return b
}

HerokuではWeb worker processにポートが紐付けられるのでos.Getenv("PORT")で、PORT番号を取ってきます。 Herokuの以下のドキュメントに記載があります。

Runtime Principles | Heroku Dev Center

echoインストール

echoインストールします。

$ go get github.com/labstack/echo

動作確認

以下を実行し

$ go run main.go

http://localhost:8080/books/1

をブラウザで開きます。

こんな感じの見た目になります。

Heroku上で動くまで

Procfile作成

Procfileを作ります。HerokuのWeb Workerノードが起動したら最初に実行するコマンドを記載します。 Herokuにコードをデプロイした際に、bin配下にバイナリが生成されるので、それを起動するようにします。

web: bin/golang-sample

依存関係ファイルを作る (go modules)

以下のコマンドを実行し、go modulesの依存関係ファイルを作ります。

$ go mod init
$ go build

すると、go.mod, go.sum というファイルができます。

Git初期化する

$ git init
$ git add .
$ git commit -m "initial commit"

 5 files changed
 create mode 100644 .gitignore
 create mode 100644 Procfile
 create mode 100644 go.mod
 create mode 100644 go.sum
 create mode 100644 main.go

Procfile, go.mod, go.sum, main.goの4ファイルがあれば動くなんてお手軽!

herokuプロジェクト作成し、デプロイする

$ heroku create golang-sample-202005
$ git push heroku master

動作確認

URLにアクセスするとローカルで確認した画面と同じものが表示されます。

https://golang-sample-202005.herokuapp.com/books/1

作ったGolangサンプルプロジェクト

github.com

参考

Zendesk widgetをマルチブランド対応せずに複数Webサイトで活用するためのカスタマイズ方法 (ヘルプセンターだけ消したい)

Webサイトを運用しているとユーザの抱えている課題を受け付けるためにも、お問い合わせ機能を作りたいという要望が出てますよね。

そうした時に

  • 低予算で導入できる
  • 導入が容易

といったメリットがあるZendeskを使うことが選択肢として挙がるかと思います。

単一Webサイトのみを運用しているであればこれで良いのですが、複数WebサイトでうまくZendeskを使おうとすると様々な運用方法を考えなければならなくなります。

  • ①複数サイトを同一のZendeskアカウント(Essentialプラン)で運用する場合
    • ユーザ利便性: 低
      • 同じWidgetが表示されてしまう
        • => まったく関係ないWebサイトのヘルプセンター(Q&A)が表示されてしまう
    • コスト: 低
      • Essentialプランは比較的安価。(2020年5月時点で1エージェントあたり$5)
    • 運用性: 高
      • 社内でお問い合わせ対応する人が同じダッシュボードで作業でき、効率的に作業ができる
  • ②複数サイトをそれぞれ別のZendeskアカウント(Essentialプラン)で運用する場合
    • ユーザ利便性: 高
      • Webサイト毎のZendesk Widgetが作れるので、ヘルプセンター(Q&A)・チャット・お問い合わせフォームを分けられる
    • コスト: 低
      • Essentialプランは比較的安価。(2020年5月時点で1エージェントあたり$5)
    • 運用性: 低
      • 社内でお問い合わせ対応する人が異なるダッシュボードで作業する必要があるため作業が大変になる
  • ③複数サイトを同一のZendeskアカウント(Enterpriseプラン)で運用する場合
    • ユーザ利便性: 高
      • Webサイト毎のZendesk Widgetが作れるので、ヘルプセンター(Q&A)・チャット・問い合わせフォームを分けられる
    • コスト: 高
      • 1アカウントで複数のWidgetを作る(マルチブランド対応)ためにはSuport Enterpriseプランの契約が必要になるため、コストが高くなる。2020年5月時点で1エージェントあたり$99)
    • 運用性: 高
      • 社内でお問い合わせ対応する人が同じダッシュボードで作業でき、効率的に作業ができる

③のコスト負担が大きく感じたので

  • ①案を採用し、ユーザ利便性を改善する
  • ②案を採用し、運用性を改善する

を実現できるような、カスタマイズがないか調べてみました。

②案の運用性が低さはZendeskの仕様 (1アカウントで複数Widgetを作れない)なのでどうしようもなさそうだったので、①案のユーザ利便性を改善する方法(片方のWebサイトだけヘルプセンターを無効化する)が良さそうだという結論に至り対応しました。

WebサイトのHTMLファイルを編集し、Zendesk Widgetスニペットを読み込む前に以下のオプションをJavaScriptで実行するようにすると、片方のヘルプセンターの表示を無効化できます。 複数のサイトを運用した場合、比較的お問い合わせの少ない方でヘルプセンターを無効にすると良さそうです。

zESettings = {
  webWidget: {
    helpCenter: {
        suppress: true
    }
  }
}

Zendeskのフォームの遷移は以下のようなイメージになります。

参考

Railsプロジェクトでvue.js + typescriptを使うためのts-loaderの設定

Webpacker gemを導入したRailsプロジェクトにtypescript+vue.jsを導入した時に調べたことのメモ記載しておきます。

TypeScript/vueのインストール/設定

既にプロジェクトにはWebpackerは導入済だったので、WebpackerのREADME通り

$ bin/rails webpacker:install:typescript
$ bin/rails webpacker:install:vue

と実行し、typescriptとvueをインストール/設定しました。その際、出力される ts-loader の設定ファイルは、

# /config/webpack/loaders/typescripts.js

const PnpWebpackPlugin = require('pnp-webpack-plugin')

module.exports = {
  test: /\.tsx?(\.erb)?$/,
  use: [
    {
      loader: 'ts-loader',
      options: PnpWebpackPlugin.tsLoaderOptions()
    }
  ]
}

のようになってましたが、このままではVueファイルを以下のようなSFC(単一ファイルコンポーネント)にした場合にうまくTypeScriptとして認識してくれずに、ブラウザで開くとJSエラーが発生していました。

# app/javascript/app.vue

<template>
  <div id="app">
    <p>{{ message }}</p>
  </div>
</template>

<script lang="ts">
export default {
  data: function () {
    return {
      message: "Hello Vue!"
    }
  }
}
</script>

<style scoped>
p {
  font-size: 2em;
  text-align: center;
}
</style>

[エラー内容]

Property or method "message" is not defined on the instance but referenced during render. Make sure that this property is reactive, either in the data option, or for class-based components, by initializing the property

ts-loaderのドキュメントを参照し、オプションを指定するとうまく動くようになってくれました。

# /config/webpack/loaders/typescripts.js

const PnpWebpackPlugin = require('pnp-webpack-plugin')

module.exports = {
  test: /\.tsx?(\.erb)?$/,
  use: [
    {
      loader: 'ts-loader',
      options: PnpWebpackPlugin.tsLoaderOptions({
        appendTsSuffixTo: [/\.vue$/]
      })
    }
  ]
}

Vue-loaderの動き

Vue-loaderのドキュメントを読む

vue-loader はファイルを解析し、それぞれの言語ブロックを必要に応じて他の loader を通し、最終的に module.exports が Vue.js のコンポーネントオプションオブジェクトの CommonJS モジュールに変換します。

と記載されていて、 vue-loader を介して ts-loaderコンパイルしてくれるらしいことが分かります。

PnpWebpackPlugin.tsLoaderOptionsについて

上記の ts-loaderの設定ファイル( /config/webpack/loaders/typescripts.js )で、 PnpWebpackPlugin.tsLoaderOptions()のようにオプション指定されていて、これが何かも気になったので、調べてみました。

Pnpとは?

そもそも、先頭3文字の Pnp というのは何かというと、Yarn 2.0から導入された node_modulesに変わるパッケージの依存関係を解決するAPIであるPlug'n'Playの頭文字を取ったもの。 Plug'n'Play を導入すると、でかい node_modules の変わりに .pnp.jsという単一のファイルで済むようになり70%ほど速度が早くなるようです(参考

ただし、Webpackerのissueを見ると、2020/5/16時点ではまだ使える状態にはなさそうなので、恩恵を受けられる日が来るのを待ちましょう。

PnpWebpackPlugin.tsLoaderOptionsはなぜ要るのか?

Plug'n'Playが導入されると、Typescriptimport 文やRefarenceディレクティブが参照する箇所を node_modules から変えないといけないので、Plug'n'Playが有効になっている場合のみ、ts-loaderのオプションに

  • resolveModuleName
  • resolveTypeReferenceDirective

というオプションを渡して、TypeScriptのデフォルトの実装(import 文やRefarenceディレクティブ)と動きが変わるようにしてあげる必要があるようです。

PnpWebpackPlugin.tsLoaderOptionsの実装は以下のコードを確認しました。 https://github.com/arcanis/pnp-webpack-plugin/blob/659125cee0625f1a543bc1645e3917ad90857052/index.js#L157

参考までに、PnpWebpackPlugin.tsLoaderOptionsの実装の中の

の参照リンクも貼っておきます。

Webpacker Gem/ Loader Packageバージョン

  • Webpacker gem
    • 4.2.2
  • ts-loader
    • 7.0.4
  • vue-loader
    • 15.9.2

その他参考資料

Pull Requestがマージされた時に自動でブランチを削除するようにしてみた

GithubでPull Requestがマージされた後ブランチを毎回削除するのが面倒だったので、自動で削除されるようにしてみました。

また、Git-flow運用している場合、develop,stagingのようなブランチからPRを出す場合があるので、これらのブランチからPul lRequestを出した際は、削除されないようにしておきます。

参考にしたGithubのヘルプのURLも掲載しておきます。

※この設定は2020/5/13時点の情報であり、今後Githubの仕様変更などにより画面・設定方法が変わる可能性があります。

Github上の設定

Pull Requestがマージされた時に自動削除されるように設定する

マージされた時にブランチが削除されるように設定します。プロジェクトの「Settings」=>「Options」を選択し

「Automatically delete head branches」 をチェックします。保存ボタンはないので、チェックを付けた時点で反映されます。

マージすると以下のfeature/branchブランチが自動削除されます。

ただ、多くのプロジェクトではGit-flow運用していて、developやstagingからPull Requestを出すことがあると思います。このままだとこれらのブランチが自動削除されてしまうので、「Branch protection rules」を設定し、ブランチが削除されないようにします。

消えてほしくないブランチを保護する

Githubのドキュメントによると

保護されたブランチは、GitHub Free 及びGitHub FreeのOrganizationではパブリックリポジトリでのみ使用でき、GitHub Pro、GitHub Team、GitHub Enterprise Cloud、GitHub Enterprise Server ではパブリックおよびプライベートリポジトリで使用できます。

という記載があり、事前に

  • アカウント課金する
  • 該当リポジトリをパブリックにする

など対応が必要です。

設定は、プロジェクトの「Settings」 => 「Branches」を選択し、「Add rule」でブランチ毎にルールを追加します。

ブランチのそれぞれで、「Allow deletions」のチェックを外すと、「Automatically delete head branches」を設定してあっても、自動削除されなくなります。

また、デフォルトブランチに設定してある場合も自動削除されなかったので、もしdevelopブランチがデフォルトになっているようであれば、developブランチは「Allow deletions」の設定はなくても大丈夫です。

確認

設定完了後、以下のようにPRを出して、feature/branchブランチのみが削除されることを確認できます。

  • feature/branchブランチ から developブランチにPull Requestを出す
    • => feature/branchブランチが自動削除される
  • developブランチ からstagingブランチにPull Requestを出す
    • => いずれのブランチも自動削除されない
  • stagingブランチ からmasterブランチにPull Requestを出す
    • => いずれのブランチも自動削除されない

参考

「Automatically delete head branches」のhead branchesってどういう意味だろうと思い、Stack overflowで調べてみたところチェックアウトしたブランチのことのようです。

stackoverflow.com

Githubでは、Pull Requestを出した時の元のブランチのことをHead Branchと呼ぶのかー、と勉強になりました。