Github Actions でブランチの操作を行う

こちらのブログは個人ブログと同じ内容です

www.ritolab.com

少し前までは CI/CD を回そうと思ったら Circle CI や Travis CI を Github と連携させたりしていましたが、GitHub Actions が使えるようになってからは Github だけで Ci/CD も完結できるようになりました。(正式版は 2019年11月13日に提供開始)

今回は GitHub Actions を使って色々な操作を試してみたいと思います。

GitHub Actions

GitHub Actions は、Github 上でワークフローに沿ったそのリポジトリについての処理を実行させることができるサービスです。ワークフローは yml で記述します。
https://github.co.jp/features/actions

CI/CD はもちろん、そのリポジトリに関しての操作など結構色々な事が行えます。

また、よく行われているような操作に関しては、GitHub Marketplace で Action がたくさん提供されています。 自分でワークフローの全てをゴリゴリ記述しなくても、アクションを入れてしまえば済むものも多く、結構簡単に使う事ができます。

GitHub Marketplace
https://github.com/marketplace?type=actions

無料・有料それぞれで使用できる範囲

使ってみようとなると、無料だとどこまで使えるのかっていうのは気になるところです。

パブリックリポジトリに関しては、特段「課金」というものはなく無料で使えるようです。

プライベートリポジトリに関しては、無料プランを含む料金プランごとにそれぞれ「同時実行ジョブ数」「ストレージ」「実行時間」が割り当てられていて、制限を超えた場合は、課金しないとその月に関してはそれ以上使えなくなる感じになっています。

予め課金のリミットを設定しておいて、その設定金額分までは超過しても後で請求とかにもできるみたい。(リミット無制限なんて事もできる)

無料ユーザの場合、月 2000 分まで実行が可能(Linux)なので、個人で使うなら十分ですね。

ただし、消費時間の計算が、どの OS で動作させるかで異なるので、Linux 以外を使う場合は注意が必要です。 (ドキュメントに書いてある時間は Linux での動作の場合で、Windows の場合は2倍の消費、macOS の場合は 10 倍の時間消費になる。なので、無料ユーザの場合、Linux なら 2000 分、Windows なら 1000 分、macOS なら 200 分が毎月使える時間になるのか。)

どれくらい GitHub Actions を使用したかを知りたい時は、ユーザーの Settings > Billing から確認する事ができます。

f:id:ro9rito:20200608080146p:plain

ワークフローを作成する

とりあえず始めて GitHub Actions を動かしてみます。まずはワークフローを定義するためにファイルを作成します。 リポジトリに「Action」タブがあるのでこれを押下します。

f:id:ro9rito:20200608080327p:plain

ページ遷移すると、既に公開されているありがたい Workflows が沢山表示されますが、今回は使わないので set up a workflow yourself のリンクを押下します。

f:id:ro9rito:20200608080425p:plain

すると、ワークフローエディターが開くのでここから アクションを定義していく事ができます。

f:id:ro9rito:20200608080457p:plain

これをそのまま保存します。

f:id:ro9rito:20200608080526p:plain

再度 Actions タブに遷移すると、ワークフローの一覧が表示されていて、詳細を確認する事ができます。

f:id:ro9rito:20200608080600p:plain

無事に GitHub Actions を動作させる事ができました。

f:id:ro9rito:20200608080631p:plain

ちなみに今回は手っ取り早く Github 上からファイルを作成しましたが、以下のパスで yml ファイルを作成してリモートリポジトリに push するでも OK です。
.github/workflows/FILE_NAME.yml

Github Actions でブランチの操作を行う

ワークフローを定義できるようになったので、色々操作してみたいと思います。

Github Actions は公式ドキュメントが充実しているので、基本操作はそちらを読むと良いと思います。

Github Actions リファレンス
https://help.github.com/ja/actions/reference

ブランチを作成する

自動でブランチを切りたい時とかってありますよね。

dev ブランチに push したら release ブランチを作成するようにしてみます。

name: create new branch

on:
push:
branches: [ dev ]

jobs:
build:
runs-on: ubuntu-latest

steps:
- name: checkout dev
uses: actions/checkout@v2
with:
ref: dev

- name: Create new Branch
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
git checkout -b release
git push origin release

step 配下に 2つのアクションを定義しました。

  • dev ブランチをチェックアウトする
  • 新しいブランチを作成し、リモートリポジトリへプッシュする

これだけで勝手にブランチを切ってくれるので簡単です。 ポイントは Github の token を env に登録しておく事くらいです。

今回は run を定義しましたが、Marketplace にもブランチを切るアクションが公開されているので、やりたい事がそれで実現できるならそちらを使っても良いと思います。

タイムゾーンを設定する

なんだかこのままでは味気ないので、自動で作成されるブランチに日付をつけるようにしてみます。

- name: Create new Branch
env:
TZ: 'Asia/Tokyo'
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
DATE=`date +"%Y%m%d"`
BRANCH_NAME="release_$DATE"
git checkout -b "$BRANCH_NAME"
git push origin "$BRANCH_NAME"

タイムゾーンUTC なので、日本時間に合わせるために環境変数タイムゾーンを設定しています。

プルリクを作成する

ブランチ切ったらプルリク出したい欲が湧いてきたので、やってみます。

name: create branch & pull request

on:
push:
branches: [ dev ]

jobs:
build:
runs-on: ubuntu-latest

steps:
# hub コマンドをインストールする
- name: install hub
uses: geertvdc/setup-hub@v1.0.0

# dev ブランチをチェックアウトする
- name: checkout dev
uses: actions/checkout@v2
with:
ref: dev

# ブランチを作成する
- name: Create Branch
id: create_branch
env:
TZ: 'Asia/Tokyo'
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
DATE=`date +"%Y%m%d"`
BRANCH_NAME="release_$DATE"
git checkout -b "$BRANCH_NAME"
git push origin "$BRANCH_NAME"
echo ::set-output name=branch_name::$BRANCH_NAME

# プルリクを作成する
- name: Create Pull Request
env:
REVIEWERS: "rito328"
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
BRANCH_NAME: ${{ steps.create_branch.outputs.branch_name }}
run: |
hub pull-request -m "Github Actions からプルリクを出す" -b master -h $BRANCH_NAME -l "PR:draft" -r "$REVIEWERS"

プルリクをコマンドで投げるために hub コマンドをインストールしてそれでやっています。

ブランチ名をプルリクの step へ渡すために以下の事を行っています。

  • ブランチ作成 step に id を設定
  • アクションの出力パラメータを設定

出力パラメータの設定は、以下の部分です。

echo ::set-output name=branch_name::$BRANCH_NAME

これで、別の step の出力を別の step から参照する事ができるので、プルリクSTEP の env 設定時にこの出力を取り出して設定しています。

この辺りは以下のリファレンスが参考になります。

GitHub Actionsのワークフローコマンド

これを実行すると、プルリクが作成されます。

f:id:ro9rito:20200608081805p:plain

ちなみに pull request の作成も、Marketplace にアクションが多数公開されているので、気に入るものを探してみると良いと思います。

Slack 通知を行う

プルリクを出したら、それを伝えたい欲が湧いてきました。slack でこの事を通知しようと思います。

name: create branch & pull request

on:
push:
branches: [ dev ]

jobs:
build:
runs-on: ubuntu-latest

steps:
# hub コマンドをインストールする
- name: install hub
uses: geertvdc/setup-hub@v1.0.0

# dev ブランチをチェックアウトする
- name: checkout dev
uses: actions/checkout@v2
with:
ref: dev

# ブランチを作成する
- name: Create Branch
id: create_branch
env:
TZ: 'Asia/Tokyo'
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
DATE=`date +"%Y%m%d"`
BRANCH_NAME="release_$DATE"
git checkout -b "$BRANCH_NAME"
git push origin "$BRANCH_NAME"
echo ::set-output name=branch_name::$BRANCH_NAME

# プルリクを作成する
- name: Create Pull Request
id: create_pull_request
env:
REVIEWERS: "rito328"
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
BRANCH_NAME: ${{ steps.create_branch.outputs.branch_name }}
run: |
RESULT=`hub pull-request -m "Github Actions からプルリクを出す" -b master -h $BRANCH_NAME -l "PR:draft" -r "$REVIEWERS"`
echo ::set-output name=url::$RESULT

# メッセージを作成する
- name: create message
id: create_message
env:
BRANCH_NAME: ${{ steps.create_branch.outputs.branch_name }}
URL: ${{ steps.create_pull_request.outputs.url }}
run: |
MESSAGE="$BRANCH_NAME ブランチを作成してプルリクを出しました $URL"
echo ::set-output name=message::$MESSAGE

# slack で通知する
- name: Post to slack
uses: novoda/github-slack-action@master
with:
color: good
text: ${{ steps.create_message.outputs.message }}
webhook: ${{ secrets.SLACK_WEBHOOK }}

slack 通知に関しては、Marketplace にアクションが沢山公開されているので、その中から気に入ったやつを選んで使うと手っ取り早いですね。

(ただ、どのアクションもあれこれ詳細な通知を出したがるので、なかなかシンプルなやつがなかった)

GitHub Marketplaceからのアクションの利用

これもさっきのプルリクの時とさほどかわりません。 hub コマンドでプルリクを出すコマンドを実行すると、プルリクの URLが返ってくるので、それを出力してメッセージに含ませます。

slack 通知は Marketplace で公開されているアクションを使ったので、一度メッセージ作成の STEP を設けてメッセージを組み立てて、slack 通知 STEP で引数としてそれを渡しています。

これで slack 通知が行われるようになりました。

f:id:ro9rito:20200608082019p:plain

スケジュールで動作させる

これまでは、dev ブランチへの push をトリガーとしてワークフローが動作するようにしていましたが、決まった日時や曜日でこれを動作するようにしてみます。

on:
schedule:
- cron: '*/15 * * * *'

push:
branches: [ dev ]

これで、プッシュのトリガーとは別に、スケジュールでもこのワークフローが稼働するようになりました。cron と同じ書式っていうのが、見慣れているのでなかなか良いですね。

トリガー(ワークフローを実行するきっかけ)に関しては、以下に解説があります。

ワークフローをトリガーするイベント

まとめ

Github Actions が登場する前までは、別の CI サービスと連携しなければならなかったので、連携コストだったり、CI の方のサービス自体が調子悪いとかで開発に影響が出たりとか、なかなかツラいポイントもありましたが、Github Actions は簡単な上にコード管理と一気通貫で CI/CD を管理できるのでとても良いサービスだと思います。

テスト回したりデプロイしたりもまた次回やってみましょう。

GitHub Actionsのドキュメント
https://help.github.com/ja/actions

ルートによってグローバルスコープを適用する

(この記事は個人ブログの転載です。)

ルートによってグローバルスコープを適用する - あしたからがんばる

Backcheck事業部の前田です。

グローバルスコープまわりでハマっていて、PHPユーザーズSlackの皆さんに色々と助けていただきました。ありがとうございます。

多くの知見を得たので、ここにまとめておきます。

例題

Laravelでブログを作成します。 ここでの重要な要件は以下です。

  • 記事は「公開ステータス」を持ちます
  • 公開ステータスが非公開の記事は閲覧者に見えてはいけません
  • 公開ステータスが非公開の記事は管理画面では見えないといけません

グローバルスコープ

さて、「公開ステータスが非公開の記事は閲覧者に見えてはいけません」という要件を満たす簡単な方法は以下のような方法です。

<?php

$posts = Post::where('publish_status', 'published')->get();

簡単に思いつき、簡単に実装できます。
ただし、この方法の場合、記事を取得する箇所はすべてこのwhere句を書かないといけません。面倒ですね。
また、今後の機能改修を考えると、where句を入れ忘れる事故が発生しそうです。怖いですね。

そこで、グローバルスコープを適用させます。
グローバルスコープを適用することで、非公開の記事を存在しないものとして扱えます。

Post

<?php

class Post extends Model
{
    protected static function boot(): void
    {
        parent::boot();
        
        static::addGlobalScope('status', function (Builder $query): void {
            $query->where('publish_status', 'published');
        });
    }
}

PostController

<?php

$posts = Post::all();

都度where句を書かなくて良くなりました。
where句を書き忘れる心配もありませんね。

ルートによってグローバルスコープを適用させる

もうひとつの要件、「公開ステータスが非公開の記事は管理画面では見えないといけません」を考えると、現状のままでは難しいです。
グローバルスコープにより、管理画面でも非公開の記事が閲覧できなくなるからです。

<?php

$posts = Post::withoutGlobalScope('status')->get();

のようにすれば取れなくもないのですが、管理画面で毎回グローバルスコープを外す処理を書くのは面倒です。

よって、「管理画面以外ではグローバルスコープを適用させる」のようにできると良さそうです。

そこで、以下のようにしてみました。

Post

<?php

class Post extends Model
{
    protected static function boot(): void
    {
        parent::boot();
        
        if (app(PublishStatusScopeManager::class)->isActive()) {
            static::addGlobalScope('status', function (Builder $query): void {
                $query->where('publish_status', 'published');
            });
        }
    }
}

PublishStatusScopeManager をsingletonでサービスコンテナに登録しておき、管理画面以外の場合はグローバルスコープ適用フラグを設定しておく作戦です。

ちなみに、最初はこの設定はミドルウェアで行っていました。 以下のようなイメージです。

f:id:chiroruxx:20200604212300p:plain

モデル結合ルートに対応する

ですが、ミドルウェアではモデル結合ルートでグローバルスコープが効きませんでした。
以下のような流れになっていたようです。(用語は適当です)

f:id:chiroruxx:20200604212336p:plain

ここでは、以下のような流れになっています。

  1. モデル結合ルートの場合、ルートで指定されたモデルが存在するかを先にチェックします。(モデルが存在しない場合はミドルウェアを適用せずに404を返したいためだと思います)
  2. モデルの存在チェックをする際にモデルをbootします。
    • このタイミングではまだミドルウェアには到達していないので、 グローバルスコープの適用フラグは設定されていません。
    • なので、グローバルスコープが適用されません。(checkActiveする段階でsetActiveできていない)
  3. ミドルウェアでフラグを設定するも、既にbootされているので手遅れ・・・
  4. (ちなみに、コントローラからモデルを呼び出す際も、既にモデルはboot済みなのでboot処理は呼ばれません。)

ここで僕は「ぐぬぬぬ・・・」と悩んだのですが、最初に紹介したslackで「RouteMatchedイベントの後に設定すれば良いのではないか?」と教えてもらい、解決しました!(本当にありがとうございます!)

コード的にはこんな感じです。

EventServiceProvider

<?php

class EventServiceProvider extends ServiceProvider
{
    // ...省略

    public function boot()
    {
        parent::boot();

        Event::listen(RouteMatched::class, function (RouteMatched $event) {
            if (!Str::startsWith($event->route->uri, 'admin/')) {
                app(PublishedStatusScopeManager::class)->setActive();
            }
        });
    }
}

ちょっと「管理画面かどうか」のチェックは雑ですが、、、

ちなみに、イメージ的にはこんな感じの流れです。

f:id:chiroruxx:20200604212417p:plain

(UMLスキルが低くて下手っぴですが・・・)
ここでは、EventListenerはクロージャで表現しているので、クラスは作ってないです。

まとめ

「管理画面とその他の画面で取得条件を分ける」などは、よくあるケースだと思います。
今回は、以下の方法で対応しました。

  • グローバルスコープを使う
  • ルートをもとに、グローバルスコープを出し分ける
  • 出し分けのフラグはRouteMatchedイベントが発火されたタイミングで行う

オンライン/オフライン勉強会を運営してみて思うこと

こちらの記事は個人ブログの転記です オンライン/オフライン勉強会を運営してみて思うこと - 白メガネの日記

オンライン勉強会の運営してます

昨今の事情もあり、エンジニアの勉強会もオンラインで開催されることが増え、
自分自身もいくつかのオンラインイベントの運営に携わっています。

nyamucoro.connpass.com

study-in-virtual.connpass.com

オフラインとオンラインではやっぱり勝手が違うので、
運絵する上でのメリット、デメリットも違ってきます。
そんなことを運営の目線で語ってみようと思います。

ちなみにcluster ってVR空間で主にイベントしています。

cluster.mu

オフライン勉強会のメリット/デメリット

メリット

  • 通話アプリがあれば運営がオンラインでも普通に運営できる
    • 以外といける
    • discord 使ってます
  • 会場を確保しなくていい
    • オンラインでいいので実際の場所確保する必要がないのは運営としてはかなり楽
    • 会社等のコネがなければこの時点で積むことも
  • 懇親会用の料理や、お酒の手配をしなくていい
    • こちら手配も面倒ですが、参加費をとっている場合当日キャンセルは運営が負担する必要があったりする
  • 参加場所を選ばない
    • 会場までいかなくていい、場所を選ばない
    • 諸事情で、家ではなく出先で運営作業をする必要がありましが、PCがあれば問題なくできました
  • 当日キャンセルが少ない
    • 移動の手間がないため?か少ない
    • (わかる当日行きたくなくなることあるよね)
  • 気軽に参加してもらえる(やっぱり参加人数多い)
    • Live配信等もあるので、参加ハードルはオンラインより圧倒的に低いと思われる
    • 東京以外の方の多く参加してくれていてとても嬉しいです
  • 運営としてはやるたびに発見があっておもしろい
    • 問題も色々あるけどね

デメリット

  • 懇親会がない
    • 懇親会の準備は面倒だけど、こっちも本編と同じくらい大事だと思っています
  • YoutubeLive等の配信にはそこそこスペックのPCが必要
  • 参加や、登壇者環境はそれぞれ違う
    • クライアント起因によるマイクトラブルや、映像トラブルはそこそこある
  • 遭遇したことがないトラブルが起こる
    • ナレッジ不足はいなめない
    • 都度対応することがあるので勘弁して
    • 例:) 確認不足で用意したオフラインの会場が突如強制終了する

前提条件

後出しで前提条件を語りますが、
前提として運営の連携がオンラインでも取れるような信頼があること
ということが大事だと思っています。
現在参加している勉強会の運営メンバーは、基本的にオフラインのイベントで一緒に運営として参加したことがあるメンバーが多めです。
オンラインのコミュニケーションは基本、ある程度の信頼があるから成り立つものだと思っているので、初対面とかでいきなりオンラインで運営とかちょっとむずかしいのか?と思っています。

日々改善しています

ナレッジ不足はいかんせんありますが、運営でノウハウをためて
いろんな人に使ってもらおうと公開していますので、ぜひ見てもらえると嬉しいです github.com

まとめ

オンラインイベントは、オフラインイベント比べると運営負担は少ないし、
開催ハードルも若干低いのかなと感じます。 もっといろんなかたちでオフラインの勉強会が増えてほしいとも思います。

けど、それぞれいいとこもあるのでやっぱりオフラインのイベントもしたいなー、と思う今日このごろです。

Alpine.js で ToDo アプリ作ってみた

こんにちは、ROXX の匠平 (@show60) です。

フロントエンド技術の栄枯盛衰の流れは激しいですね。

今回は Alpine.js というフレームワークで簡単な ToDo アプリを作ってみました。

Alpine という名前はすごく好き。山という意味だが Nuxt.js が山っぽいアイコンなのと関係あるのは分からない。

Alpine.js とは

2019年11月から開発され、12月にバージョン1.0がリリースされています。5/24現在でバージョン2.3.5というまだ生まれたてのフレームワークです。

Alpine.js は、Vue や React などの大きなフレームワークのリアクティブで宣言的な性質をはるかに低いコストで提供します。

DOM を保持し、適切な動作を施すことができます。

Tailwind の JavaScript 版のようなものです。

github.com

下記リンクを見ると、すでに多くの記事で紹介されています。

GitHub - alpinejs/awesome-alpine: 🚀A curated list of awesome resources related to Alpine.

記事のタイトルに、tiny、minimal、lightweight という言葉が目立つのと、また Vue.js や React と比較している記事も多いです。

実装

早速ですが、出来上がったものがこちら。

公式が Tailwind の JavaScript 版と言っているので CSS を TailwindCSS で当ててみました。script は html 内に収めてます。

See the Pen yLYwRjx by Shohei-Japan (@shohei-japan) on CodePen.

特徴

Alpine.js の中核となるのはディレクティブです。記述方法は Vue.js とかなり近いため、親しみのあるものだと感じました。

v-forx-forv-onx-on のように v と x が置き換わっただけとも見れますね。

以下、今回使用したディレクティブのみ抜粋して説明します。

x-data

x-data は新しいコンポーネントスコープを宣言します。フレームワークに、データオブジェクトを使用して新しいコンポーネントを初期化するよう指示します。

Vue コンポーネントの data プロパティのように考えてください。

script 内で関数やプロパティを定義し、1つの関数としてまとめたら、それを x-data で呼び出してあげれば使うことができます。

alpine/README.ja.md at master · alpinejs/alpine · GitHub

x-text

Vue.js ではタグの文字列にマスタッシュ構文を配置することで割と直感的にスクリプトからの値を表示できますが、Alpine.js では x-text に渡してあげることで表示できます。

<!-- Vue.js -->
<span>{{ data }}</span>
<!-- Alpine.js -->
<span x-text="data"></span>

alpine/README.ja.md at master · alpinejs/alpine · GitHub

x-if

Vue.js の v-if と同じ使い方が可能です。

気をつけることとしては必ず template タグに使用することくらいです。

x-for

こちらも Vue.js の v-for と同じ使い方です。同様に template タグにのみ使用します。

template タグ内の表記

x-if と x-for は template タグで表記するので、DocumentFragment として扱われます。

JS で描画処理を行わせるために template タグで表記することが必須となっているのでしょう。

DevTools で確認すると丸見えになってますね。x-for の表記なんかもそのまま見えるので嫌な人はいるかもしれません。

x-if タグについても、CSSdisplay: none の処理を当てているのではなく、JS のほうでその追加・削除を行っています。

f:id:show-hei:20200524232315p:plain

さいごに

Vue.js や React といった他のフレームワークを置き換えるほどの強力な機能が備わっているようではなさそうだなという感想です。

ただ HTML を離れることなく実装できることは最も特徴的かつ強みではでしょうか。つまり Script タグに実装を行わずに完結できることです。

公式の実装サンプルを見ると いずれも HTML のみで完結していることが分かります。これが Tailwind の JavaScript 版という所以ですね。

2年目エンジニアの私個人としては、フレームワークが生まれて広がっていく過程を追うことができる良い機会だなと思っています。

毎週金曜日に NewsLetter も発行しているようですよ!

ユーティリティークラスベースのcss設計に抵抗感があった俺を、それを使いたい俺が説得する

Tailwind CSSいいなあ熱が自分の中で高まっているので、Tailwind CSSの根幹でもあるユーティリティクラスベースのcss設計について書いてみます。 (ユーティリティクラスベースじゃなくて、Tailwind CSSではユーティリティファーストっていっているけど、まあいいか)

f:id:skmtko:20200525132325p:plain
ユーティリティークラス 結構いいやつなんやなと感じてきた...

cssに関してreact、vueとかのコンポーネントベースのフロント実装をしっかり始める前までは、スクラッチでフロント開発を行う際には、FLOCSSやら、SMACCSやらのCSS設計思想に基づいて、ガッチガチのCSS設計をして、CSSを根絶する!と息巻いていました。

というのも、3年とか前の話です。

その後reactを書き始めると、css_modules, styledComponents などの恩恵で、そこまでガチガチな、css設計思想が不要になって来ます。

ユーティリティクラスとは何

クラス名がそのまま、cssのプロパティとその値をそのまま表現しているもの。 例えば極端な話、margin-top-10px とかをdiv要素に当てると、そのままmargin-top: 10px を 指定することができるというもの(本当はそんなそんな直球な命名じゃなくて、mt-4 みたいな感じだけど)

FLOCSS(https://github.com/hiloki/flocss)では以下の様に説明されています。

ComponentとProjectレイヤーのObjectのモディファイアで解決することが難しい・適切では無い、わずかなスタイルの調整のための便利クラスなどを定義します。 Utilityは、Component、ProjectレイヤーのObjectを無尽蔵に増やしてしまうことを防いだり、またこれらのObject自体が持つべきではないmarginの代わりに.mbs { margin-bottom: 10px; }のようなUtility Objectを用いて、隣接するモジュールとの間隔をつくるといった役割を担います。 またclearfixテクニックのためのルールセットが定義されているヘルパークラスも、このレイヤーに含めます。

つまり、ユーティリティークラスは、あくまで補助としての役割である。

これだけみると、「ユーティリティークラスベースでcss書いていくとかどういうことやねん」です。

話は変わってきた

「ユーティリティークラスベースでcss書いていくとかどういうことやねん」だったわけだったんですが、話しは変わってきました。

まずそもそも、なぜユーティリティークラスを補助として使わなかったのか、それはなるべく要素に対して、少ないクラス名をつけることでスタイリングをしたかったから。 例えば、list とつければリストの見た目になって欲しかったし、リストの子要素はlist__item とつければちゃんとした見た目になって欲しかったから。

同じ部品を作るために、同じクラスをつける必要があったから。

でも、vueやreactならば、部品(コンポーネント)を呼び出せば、特に毎回classを当てなくても(component内部でclassを当てたりはしてるはずだけど)、listはリストの見た目をしてくれるし、list__itemはリストの子要素の見た目をしてくれる。 さらにcssModulesや scoped style や styledComponents を使えば、componentの中に影響範囲を絞ることもできる幸せ

コンポーネントにスタイルが閉じ込められているのではれば、たとえばコンポーネント内で

<div class=“display-block background-color-blue border-rardius-50 border-1px border-style-solid border-color-bloack box-sizing-border-box margin-none padding-10px”>

みたいなえげつないクラスをつけていても、同じような部品を使いたいときは、その部品をきっと呼び出すだろうし、 コンポーネントの呼び出し先ではきっと、コンポーネント命名が正しければそれで良くて、きっと中身のクラスに関してはクラスの命名なんて全く気にしないはず。

みたいな感じで、コンポーネントベースでの実装においては、CSS命名規則はそこまで厳格にガッチガチにする必要はないのでは?(コンポーネントが適当に分けられていて、かつちゃんと命名されていれば)

駄目押し

割とここまでで、個人的に結構ユーティリティークラスベースいいやんとなってきましたが、駄目押しに Tailwind CSS のドキュメントで紹介されいる利点をあげます。

" You aren't wasting energy inventing class names. "

クラス名に悩む必要がない! 明確に main-content とか list-card とかがあるんだったら良いけれど、ドキュメント内で例であげられている様な、sidebar-inner-wrapper の様な「おそらくレイアウトのために生まれたんだろうなあ...」な要素に対してのクラス名に悩ませる必要がなくなります!幸せ。

" Your CSS stops growing. "

CSSが成長し続けることがない!
そりゃあそうですね、普通に画面やパーツが増えるごとにcssを追加していけば、cssの量はどんどん増え続けます。基本的にユーティリティークラスを再利用してスタイリグを進めていくので、cssの増加はほとんどないはずです。

" Making changes feels safer. "

変更が怖くない!
cssはグローバルに影響を及ぼします。そのために、命名記法を厳格に設計し名前空間を区切るなどをして、なんとかそれを回避するぞするんですが、ユーティリティークラスベースならば、基本的に要素へのクラスの追加、付け替えしか起きないはずなのでグローバルのstyleに影響を及ぼさない。つまり変更により他のクラスを壊してしまう心配がありません。

まとめ

ざっくりとこんな感じの理由で、ユーティリティークラスベースのcss設計やりたくなりました。 多分他にも旨みや、これだから「ユーティリティークラスベース」でも心配ないとか多分きっとあると思いますが、結局はフロント開発を進める上でcssとHTMLとで構造を二重で管理するのが辛かんったんだなあとしみじみ感じました。

参考

GitHub - hiloki/flocss: CSS organization methodology. Utility-First - Tailwind CSS CSS Utility Classes and "Separation of Concerns" 翻訳 - Qiita

ISMSとPマークの違い

3月から情シスで入社した吉澤です!

ROXXではセンシティブな個人情報を扱うため、ISMSプライバシーマークを取得し運用しています。 よく見かけるこのISMSプライバシーマークなのですが、私はISMSプライバシーマークで何が違うのがイメージできませんでした。 両方取得となると色々な配慮が必要となるこのISMSプライバシーマーク、この二つの違いについてまとめてみたのでご覧いただければと思います。

  ISMS Pマーク
規格 国際標準規格 ISO/IEC 27001:2013 日本工業規格 JISQ15001:2017
日本工業規格 JIS Q 27001:2014
対象 全ての情報資産 企業内の個人情報
事業所・部門・事業単位 企業全体
要求 情報の機密性・完全性・可用性の維持 適切な個人情報の取り扱い
弊社での取り扱い 情報セキュリティ方針 個人情報等の取扱いについて
更新間隔 3年毎+毎年継続審査 2年毎

ISMSもPマークも情報管理の仕組みと体制を「PDCA」サイクルで毎年改善していくことが目的となります。 情報を洗い出して作成する書類は統合して管理することが可能で、規格の要求事項を網羅したチェックリストを作成することで、内部監査を同時に実施することも可能です。

ISMS、Pマークを導入し継続することで、社内全体のセキュリティ水準が上がり、取り扱う情報の整理やリスク対策もできるので、まだ導入していない企業の方は検討いかがでしょうか!

CodePipelineとGitHubを連携する方法を追求したら Github Actionsでやるべきという結論に至った話

こちらは下記のブログの転載です。

kotamat.com

会社でGitHubソースコードの管理として、AWSをインフラ基盤としてつかっているのですが、今回ECSを用いて環境を構築する事になり、以前試験的に運用していたサービスで構築していたCodePipelineをつかったデプロイフローを参考に構築していっておりました。 ただ、あるタイミングで、「これってGithub Actionつかったほうがいいよね」って思うタイミングがあり、全面的に構築を変えたので、その経緯と意思決定の理由を記事にします。

CodePipelineにしてた理由:AWS ECSと親和性が高かった。

御存知の通り、会社ではTerraformを用いてIaCをしています。 ECSのアプリケーションを構築する際、デプロイのたびにTaskDefinitionというjsonファイルに記述したimageのタグを最新にしてデプロイする必要があります。 通常であれば

  1. TaskDefinitionを新たに作り直して、新しいバージョンで保存
  2. ECSのサービスの中のTaskDefinitionのバージョンを変更

というプロセスが必要となるのですが、CodePipelineでは、imagedefinitions.jsonという、コンテナ名とimageのタグだけが記述されたjsonファイルを入力とし、下記のように設定を書くだけで、デプロイ環境を構築することができます。

resource "aws_codepipeline" "main" {
  stage {
    ... // imagedefinitions.jsonを出力するなにか。大抵はCodeBuildで出力する
  }
  stage {
    name = "Deploy"
    action {
      category = "Deploy"
      configuration = {
        ClusterName = aws_ecs_cluster.main.name
        ServiceName = aws_ecs_service.main.name
      }
      input_artifacts = [
        "BuildArtifact" // imagedefinitions.jsonが入っている想定
      ]
      name             = "Deploy"
      output_artifacts = []
      owner            = "AWS"
      provider         = "ECS"
      run_order        = 1
      version          = "1"
    }
  }
}

当然ほかのリソースもTerraformで記述できるため、単一のTerraformのソースコード配下だけで全てが完結してかけるようになります。

CodePipelineの壁:GitHubと接続する3つの方法

ただ、CodePipeline単体だけでは意味がありません。GitHubを使用してソースコードを管理しているので、GitHubとはうまく連携させたいですね。 そうなった場合に、CodePipelineとは3つの方法で連携する必要があります

1. GitHubからソースを持ってくる

当然ソースをGitHubから持ってこないとビルドもできません。

下記のようにCodePipelineにステージを追加し、category Sourceとして、GitHubを指定する必要があります。

resource "aws_codepipeline" "main" {
  stage {
    name = "Source"
    action {
      category = "Source"
      configuration = {
        Branch               = "master"
        Owner                = "kotamat"
        PollForSourceChanges = false
        Repo                 = var.repo_name
        OAuthToken           = var.github_token
      }
      name  = "Source"
      owner = "ThirdParty"
      output_artifacts = [
        "SourceArtifact"
      ]
      provider = "GitHub"
      version  = "1"
    }
  }
  ...
}

OAuthTokenには、GitHubからリソースをとってこれる権限を持ったPrivate Access Tokenを発行し付与する必要があります。 こちらではvarで指定していますが、必要に応じてSSMのParameterに入れてもいいかもしれません。

2. GitHubからソースをとってくるタイミングをトリガーする

1だけでは、手動でCodePipelineを起動しない限りソースをとってくることはできません PollForSourceChangesをtrueにすればポーリングすることはできますが、パッシブにトリガーすることはできません。

GitHubの方に存在するWebhookの機能をつかって、Githubで発火した任意のタイミングを補足し、CodePipelineがそれを扱えるようにします

resource "aws_codepipeline_webhook" "main" {
  authentication  = "GITHUB_HMAC"
  name            = local.base_name
  target_action   = "Source"
  target_pipeline = aws_codepipeline.main.name
  authentication_configuration {
    secret_token = local.secret
  }
  filter {
    json_path    = "$.ref"
    match_equals = "refs/heads/{Branch}"
  }
}

resource "github_repository_webhook" "main" {
  configuration {
    url          = aws_codepipeline_webhook.main.url
    content_type = "json"
    insecure_ssl = true
    secret       = local.secret
  }
  events     = ["push"]
  repository = var.repo_name
}

この2つを追加するだけでWebhookの処理を作ることができるので、Terraformで扱うのは簡単ですね。 GitHubのwebhookを使うためにはgithubのproviderを設定する必要があります。

3. CodePipelineの結果をCommit Statusに反映する

一応上記でデプロイはできるようになるのですが、デプロイが正常に完了したかどうかをGitHubで確認したくなると思います。 こちらは、CodePipelineの完了を、CloudWatchのEvent Targetで補足し、その結果をLambdaに流してLambdaからGithubのCommitStatusを更新するということをする必要があります。

機能として完全に独立しているのでmodule化して実装するのがいいかと思います。(以下はmodule化している前提)

data "archive_file" "lambda" {
  source_file = "${path.module}/lambda/${var.function_name}.js"
  output_path = "${path.module}/lambda/${var.function_name}.zip"
  type        = "zip"
}

resource "aws_cloudwatch_event_target" "main" {
  arn  = aws_lambda_function.main.arn
  rule = aws_cloudwatch_event_rule.main.name
}

resource "aws_lambda_permission" "main" {
  action        = "lambda:InvokeFunction"
  function_name = aws_lambda_function.main.function_name
  principal     = "events.amazonaws.com"
}

data "template_file" "event_pattern" {
  template = file("${path.module}/rule/event_pattern.json")

  vars = {
    codepipeline_arn = var.codepipeline_arn
  }
}

resource "aws_cloudwatch_event_rule" "main" {
  event_pattern = data.template_file.event_pattern.rendered
}

resource "aws_iam_role" "main" {
  assume_role_policy = file("${path.module}/policies/assume_lambda.json")
}

resource "aws_iam_role_policy_attachment" "codepipeline_readonly" {
  policy_arn = "arn:aws:iam::aws:policy/AWSCodePipelineReadOnlyAccess"
  role       = aws_iam_role.main.name
}

resource "aws_lambda_function" "main" {
  function_name    = var.function_name
  handler          = "${var.function_name}.handler"
  role             = aws_iam_role.main.arn
  runtime          = "nodejs12.x"
  filename         = data.archive_file.lambda.output_path
  source_code_hash = base64sha256(filesha256(data.archive_file.lambda.output_path))
  timeout          = 300
  environment {
    variables = {
      ACCESS_TOKEN = var.github_token
    }
  }
}

上記のlambdaのfilepathに、下記のファイルを設置しておきます。

const aws = require('aws-sdk');
const axios = require('axios');

const BaseURL = 'https://api.github.com/repos';

const codepipeline = new aws.CodePipeline();

const Password = process.env.ACCESS_TOKEN;

exports.handler = async event => {
    console.log(event);
    const { region } = event;
    const pipelineName = event.detail.pipeline;
    const executionId = event.detail['execution-id'];
    const state = transformState(event.detail.state);

    if (state === null) {
        return null;
    }

    const result = await this.getPipelineExecution(pipelineName, executionId);
    const payload = createPayload(pipelineName, region, state);
    try {
        return await this.postStatusToGitHub(result.owner, result.repository, result.sha, payload);
    } catch (error) {
        console.log(error);
        return error;
    }
};

function transformState(state) {
    if (state === 'STARTED') {
        return 'pending';
    }
    if (state === 'SUCCEEDED') {
        return 'success';
    }
    if (state === 'FAILED') {
        return 'failure';
    }

    return null;
}

function createPayload(pipelineName, region, status) {
    console.log('status', status);
    let description;
    if (status === 'started') {
        description = 'Build started';
    } else if (status === 'success') {
        description = 'Build succeeded';
    } else if (status === 'failure') {
        description = 'Build failed!';
    }

    return {
        state: status,
        target_url: buildCodePipelineUrl(pipelineName, region),
        description,
        context: 'continuous-integration/codepipeline',
    };
}

function buildCodePipelineUrl(pipelineName, region) {
    return `https://${region}.console.aws.amazon.com/codepipeline/home?region=${region}#/view/${pipelineName}`;
}

exports.getPipelineExecution = async (pipelineName, executionId) => {
    const params = {
        pipelineName,
        pipelineExecutionId: executionId,
    };

    const result = await codepipeline.getPipelineExecution(params).promise();
    const artifactRevision = result.pipelineExecution.artifactRevisions[0];

    const revisionURL = artifactRevision.revisionUrl;
    const sha = artifactRevision.revisionId;

    const pattern = /github.com\/(.+)\/(.+)\/commit\//;
    const matches = pattern.exec(revisionURL);

    return {
        owner: matches[1],
        repository: matches[2],
        sha,
    };
};

exports.postStatusToGitHub = async (owner, repository, sha, payload) => {
    const url = `/${owner}/${repository}/statuses/${sha}`;
    const config = {
        baseURL: BaseURL,
        headers: {
            'Content-Type': 'application/json',
        },
        auth: {
            password: Password,
        },
    };

    try {
        const res = await axios.post(url, payload, config);
        console.log(res);
        return {
            statusCode: 200,
            body: JSON.stringify(res),
        };
    } catch (e) {
        console.log(e);
        return {
            statusCode: 400,
            body: JSON.stringify(e),
        };
    }
};

あとはポリシーとか諸々をいい感じに設定しておきます。 はい、以上です。

結論、めんどい

はい、めんどいです。(特に最後のCommit Statusへの反映のあたりとか特に) 更に、CodePipelineはTerraformで記述していくのですが、stepの中で条件を達成したら発動するものみたいなのがあった場合にそれをいい感じに表現することはできないです。 また、CodePipelineだけではビルドはできないので、ほかのサービスと連携することが求められます。

そこでGitHub Actions

GitHub Actionsとは、GitHubが提供している、CI/CDの仕組みです。ソースコード.github/workflows/**.yml に適切なymlを書くだけで、簡単にCI/CDを実現することができます。 GitHub Actionsを使うことで、下記のような恩恵を受けることができます。

  1. Stepごとにマーケットプレイスで提供されているものを使えるため、よく使うテンプレートのようなものは、単にそれを呼び出すだけで使える
  2. if構文が使えるので、細かい条件をもとに実行する/しないをstepごとやjobごとに設定できる
  3. ソースの提供、トリガーのタイミング、Commit Statusの反映がほぼ何もせずとも実装できる←でかい

ほかにも色々ありますが、細かい仕様は 公式ドキュメント を参考にしてください。

AWS、Terraformとの連携方法

当然Github Action側ではAWSのIAM Roleを直接アタッチしたりはできません。Github Action用に適度な権限を付与されたユーザを作成し、それのアクセスキーをつかって諸々のリソースをいじることになります。 Terraformではそのアクセスキーを生成するところまでを責務とするのが良さそうです。

また、ECSで用いるTaskDefinitionは、Terraform側でベースを作成したほうが何かと都合がいいので、S3にベースとなるTaskDefinitionを設置するまでを行い、Github Actionではそれをつかって更新するようにします。

ECRに対しての更新処理の権限

resource "aws_iam_user_policy_attachment" "can_handle_ecr" {
  policy_arn = aws_iam_policy.actions_for_ecr.arn
  user       = var.iam_user_name
}

resource "aws_iam_policy" "actions_for_ecr" {
  name   = "${var.base_name}-github-actions-ecr"
  policy = data.template_file.actions_for_ecr.rendered
}

data template_file "actions_for_ecr" {
  template = file("${path.module}/policies/actions-for-ecr.json")

  vars = {
    ecr_arn = var.ecr_arn
  }
}
{
  "Version":"2012-10-17",
  "Statement":[
    {
      "Sid":"GetAuthorizationToken",
      "Effect":"Allow",
      "Action":[
        "ecr:GetAuthorizationToken"
      ],
      "Resource":"*"
    },
    {
      "Sid":"AllowPull",
      "Effect":"Allow",
      "Action":[
        "ecr:GetDownloadUrlForLayer",
        "ecr:BatchGetImage",
        "ecr:BatchCheckLayerAvailability"
      ],
      "Resource":"${ecr_arn}"
    },
    {
      "Sid":"AllowPush",
      "Effect":"Allow",
      "Action":[
        "ecr:GetDownloadUrlForLayer",
        "ecr:BatchGetImage",
        "ecr:BatchCheckLayerAvailability",
        "ecr:PutImage",
        "ecr:InitiateLayerUpload",
        "ecr:UploadLayerPart",
        "ecr:CompleteLayerUpload"
      ],
      "Resource":"${ecr_arn}"
    }
  ]
}

ECRからimageをとってきてTaskDefinitionを更新する

resource "aws_iam_user_policy_attachment" "deploy_task_definition" {
  policy_arn = aws_iam_policy.actions_for_deploy_task_definition.arn
  user       = var.iam_user_name
}

resource "aws_iam_policy" "actions_for_deploy_task_definition" {
  policy = data.template_file.actions_for_ecs_deploy_task_definition.rendered
  name   = "${var.base_name}-github-actions-task-definition"
}

data template_file "actions_for_ecs_deploy_task_definition" {
  template = file("${path.module}/policies/ecs-deploy-task-definition.json")

  vars = {
    ecs_service_arn         = var.ecs_service_arn
    task_execution_role_arn = var.task_execution_role_arn
    task_role_arn           = var.task_role_arn
  }
}
{
  "Version":"2012-10-17",
  "Statement":[
    {
      "Sid":"RegisterTaskDefinition",
      "Effect":"Allow",
      "Action":[
        "ecs:RegisterTaskDefinition"
      ],
      "Resource":"*"
    },
    {
      "Sid":"PassRolesInTaskDefinition",
      "Effect":"Allow",
      "Action":[
        "iam:PassRole"
      ],
      "Resource":[
        "${task_role_arn}",
        "${task_execution_role_arn}"
      ]
    },
    {
      "Sid":"DeployService",
      "Effect":"Allow",
      "Action":[
        "ecs:UpdateService",
        "ecs:DescribeServices"
      ],
      "Resource":[
        "${ecs_service_arn}"
      ]
    }
  ]
}

TaskDefinitionのjsonファイルの生成

s3 bucket objectに指定しているcontentがだいぶカオスな感じになっていますが、 これは aws_ecs_task_definitionjson出力をサポートしていないため、container_definitionsから無理やり生成している感じになっています。

resource "aws_s3_bucket_object" "task_definition" {
  bucket  = var.task_definition_bucket
  key     = var.task_definition_filename
  content = jsonencode({
    containerDefinitions = jsondecode(aws_ecs_task_definition.main.container_definitions)
    networkMode : aws_ecs_task_definition.main.network_mode
    requiresCompatibilities : aws_ecs_task_definition.main.requires_compatibilities
    taskRoleArn : aws_ecs_task_definition.main.task_role_arn
    executionRoleArn : aws_ecs_task_definition.main.execution_role_arn
    family : aws_ecs_task_definition.main.family
    cpu : aws_ecs_task_definition.main.cpu
    memory : aws_ecs_task_definition.main.memory
  })
  acl     = "private"
}

data "template_file" "download_task_definition_policy" {
  template = file("${path.module}/policies/download-task-definition.json")
  vars = {
    bucket = var.task_definition_bucket
  }
}

resource "aws_iam_policy" "download_task_definition" {
  policy = data.template_file.download_task_definition_policy.rendered
}

resource "aws_iam_user_policy_attachment" "task_definition" {
  policy_arn = aws_iam_policy.download_task_definition.arn
  user       = var.iam_user_name
}
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "s3:GetObject",
        "s3:ListBucket"
      ],
      "Resource": "arn:aws:s3:::${bucket}/*"
    }
  ]
}

GitHub Actions側の設定

あとは下記のようなymlを .github/actions/ に追加するだけです。 上記で生成されたACCESS_KEY_IDとSECRETをgithubリポジトリの設定に追加してmasterブランチでpushすれば自動でデプロイされます。

name: Deploy to ECS
on:
  push:
    branches:
      - master
jobs:
  stg:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v1

      - 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

      - name: Login to Amazon ECR
        id: login-ecr
        uses: aws-actions/amazon-ecr-login@v1

# build
      - name: Build, tag, and push image to Amazon ECR
        id: build
        env:
          ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
          ECR_REPOSITORY: my-repository
          IMAGE_TAG: ${{ github.sha }}
        run: |
          docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG .
          docker tag $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG $ECR_REGISTRY/$ECR_REPOSITORY:latest
          docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
          docker push $ECR_REGISTRY/$ECR_REPOSITORY:latest
          echo "::set-output name=image::$ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG"
# deploy
      - name: Download base of task definition file
        run: |
          aws s3 cp s3://bucket-of-task-definition/task-definition.json task-definition.json

      - name: Render Amazon ECS task definition
        id: render-container
        uses: aws-actions/amazon-ecs-render-task-definition@v1
        with:
          task-definition: task-definition.json
          container-name: my-container
          image: ${{ steps.build.outputs.image }}

      - name: Deploy to Amazon ECS service
        uses: aws-actions/amazon-ecs-deploy-task-definition@v1
        with:
          task-definition: ${{ steps.render-container.outputs.task-definition }}
          service: ecs-service
          cluster: ecs-cluster

      - name: Logout of Amazon ECR
        if: always()
        run: docker logout ${{ steps.login-ecr.outputs.registry }}

まとめ

なかなか長くなってしまいましたが、GitHubをつかっているのであれば、CI/CDはGitHub Actionsをおとなしくつかったほうがいいかなと思います。