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

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

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

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をおとなしくつかったほうがいいかなと思います。

less コマンドの基本的な使い方と知っておくと便利な機能

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

www.ritolab.com

LinuxCLI から操作している時に、ログや CSV などのテキストファイルなどの内容を確認するためのコマンドがいくつかありますが、less コマンドがなかなか使い勝手が良いので紹介します。

less とは

less は Unix 系のコマンドで、テキストファイルの内容を閲覧するためのコマンドです。

www.greenwoodsoftware.com

  • less は more の機能を拡張したコマンド(見た感じは一緒)
  • tail より使い勝手が良い(開いてから検索できる など)
  • エディタではないので、うっかり中身を変更してしまうことがない
  • メモリほとんど消費しない(実行時にファイル内容をすべて読み込まないため)

基本的な使い方

less コマンドはテキストファイルの内容を閲覧するためのコマンドなので「開く」そして「読み進める」これだけです。 とてもシンプルなので簡単です。

ファイルを開く

less <FILE_PATH>

ファイルを開いた後は、以下のキーで操作できます。

  • j 一行進む
  • k 一行戻る
  • d 半ページ進む
  • u 半ページ戻る
  • f 1ページ進む
  • b 1ページ戻る
  • g 先頭行へ飛ぶ
  • G 最終行へ飛ぶ
  • <number>g 入力した行(<number>行目)へ飛ぶ
  • q 終了する

検索

ファイルの中身が大量にある時など、1行ずつ見ていくと埒が明かないので探したい部分を検索にかけることができます。

ファイルを開いた後で、検索をかけると、ヒットした最初の行を先頭としてページが表示されます。

検索ワードには正規表現も記述可能です。

現在のページ以降を検索
/<search_word>
現在のページ以前を検索
?<search_word>

検索後は以下のキーでも操作できるようになります。

  • n 現在地の次にヒットした行へ進む
  • N 現在地の前にヒットした行へ戻る

検索にヒットしたものだけを表示させる

検索にヒットしたものだけを表示させることもできます。 & キーを入力後、検索ワードを入力します。

&<search_word>

表示を元に戻すには再度 & キーを入力し、検索ワードは入力せずに [ENTER] 押下で戻ります。

※ ファイルの大きさによってその分 CPU に負荷がかかるので注意

行番号を表示する

ファイルを開いた後で -N を入力すると、行番号を表示できます。

行番号表示前

行番号表示前

行番号表示後

行番号表示後

行番号を非表示にする時も同じです。もう一度 -N を入力すると非表示になります。

※ ファイルの大きさによってその分 CPU に負荷がかかるので注意

現在地情報を表示する

例えば大量の行数のあるファイルを上からなめている時に、今どの辺にいるのかがわからなくなりますが、 -M を入力すると、以下の情報が表示されるようになり、現在地が気になる場合には役に立ちます。

  • 現在表示されているものが何行目から何行目までか
  • 総行数
  • 現在地が総行数に対してどの辺りにいるか(パーセンテージ)

現在地情報を表示する

行の折り返し操作

1行がコンソール画面の横幅より長い場合は折り返されますが、-S を入力すると「折り返す」「折り返さない」を切り替えられます。

折り返す(デフォルト)

折り返す(デフォルト)

折り返さない

折り返さない

折り返さないと表示がスッキリして良いですね。ただし横スクロールはできないので、1行すべてを見なくて良い場合などで使います。

ビープー音を鳴らさない

先頭行から更に上に戻ろうとした場合や、最終行まで来た時に更に下に進もうとする場合にビープー音がなりますが、微妙にストレスなので音を止めたい時があります。

その時は -q を入力すると ON/OFF ができます。

リアルタイムでログを監視する

tail -f コマンドなどでリアルタイムにログを閲覧できますが、less コマンドでも可能です。

less では F を入力すると監視状態に入ります。

監視状態

ctrl + c で監視状態が解除されるので、そこから検索したりできます。

監視状態を解除した後でも、再度 F を入力すれば監視状態になります。

閲覧自体を終了させたい場合は ctrl + c からの q で終了できます。

オプション

上記の操作は less コマンド実行時にオプションをつけて実行する事もできます。

行数を表示する
less -N <file_name>
指定した行から表示させる
# 書式
less +<line> <file_name>

# 10 行目から表示開始
less +10 access_log
検索する文字列を指定する
# 書式
less +/<search_word> <file_name>

# p オプションも同じ
less -p<search_word> <file_name>
リアルタイムで入力を監視する
less +F <file_name>
tail -f <file_name> | grep -e xxx -e xxx
みたいに grep で監視できないのがちょっと残念

他のコマンドの実行結果を閲覧する

パイプを使って他のコマンドの実行結果を読むこともできます。

<some command> | less

例えば、圧縮したログを読む時などパイプで less へ渡してあげるとスムーズに確認できます。

gzip -dc xxx.gz | less

(ちなみに gz ファイルは パイプで渡さなくても less xxx.gz で読めます)

まとめ

less コマンドは、操作が直感的でわかりやすいのが良い点だと思います。

メモリ消費の部分も、試しに 100万件 1.6GB のログファイルを読んでみたけどメモリ消費はほぼありませんでした。(1画面分だけ読み込むため)

% wc access_log
10000000 190901510 1684049674 access_log

% ls -lhS
1.6G 4 29 17:46 access_log

% ps aux | grep access_log
%CPU %MEM
0.0 0.0 less access_log

ただし less コマンドに限った事ではありませんが、大きすぎるファイルを検索したり採番したりすると CPU に負荷がかかるので注意は必要です。

結構使いやすいので、試してみてください。