Vue.js $emit 使わないで props で method 渡したほうが良くない?

これは、個人ブログ からの転用です ROXXに入って学んだことの1つです。

概要

Vue.js で 親コンポーネントの method 実行させたい場合、$emit 使ってイベントを発火させるより、
props に method をコールバックとして登録しておいて実行させたほうが以下のメリット上げられるので「こっちのほうが良くね?」って話です

  • props の成約をつけられる(requird, etc)
  • $emit の文字列を管理しなくていい
  • IDEで補完が効く

実際のコード

ボタン押したらカウントアップしていくようなやつ f:id:akki_megane:20200708203420p:plain

呼び出し側の親コンポーネント

<template>
    <div>
        <div>{{ count }}</div>
        <child
                :handle-add-number="addCount"
                @addNumber="addCount"
        />
    </div>
</template>

<script>
    import child from "./child";

    export default {
        components: {child},
        data: () => ({
            count: 0
        }),
        methods: {
            addCount(number) {
                return this.count += number
            }
        }
    }
</script>


コンポーネント

<template>
    <button @click="countUpProps()">count up props</button>
    <button @click="countUpEmit()">count up emit</button>
</template>

<script>
    export default {
        props: {
            handleAddNumber: {
                type: Function,
                required: true // required をつけて必須に
            }
        },
        methods: {
            // Prop で渡した function を実行
            countUpProps() {
                this.handleAddNumber(1)
            },
            // emit を使って function を実行
            countUpEmit() {
                // emit の event は 文字列で管理
                this.$emit('addNumber', 1)
            }
        }
    }
</script>


props で定義しておけばこんなふうにIDEで補完が効きます f:id:akki_megane:20200708203603p:plain


こんなかんじで、props を使うと、
requird で縛れて、method の渡し忘れを防げたり、
$emit の文字列管理しなくていいかつ、IDEで補完が効くのでその分typo が防げる という点で開発しやすくなるかなと思います。

TS でやると

Vue も Vue3 から、TypeScript を正式にサポートということで未来を見据えて、
同じ処理を Vue3 + TypeScript で書いてみます


呼び出し側の親コンポーネント

<template>
    <div>
        <p>{{ count }}</p>
        <child
                :handle-add-num="addNumber"
                @addNumber="addNumber"
        />

    </div>
</template>

<script lang="ts">
    import {defineComponent, ref} from 'vue';
    import child from "./child.vue";
    import AddNumberInterface from "../types/AddNumberInterface"; //function の Interface

    export default defineComponent({
        components: {
            child
        },
        setup() {

            const count = ref<number>(0)

            //Interface を指定して関数を定義
            const addNumber: AddNumberInterface = (num: number) => {
                return count.value += num
            }

            return {
                //data
                count,

                //function
                addNumber
            }
        }
    })
</script>


props で渡す関数の Interface 定義

export default interface AddNumberInterface {
    (num: number): number
}


コンポーネント

<template>
    <div>
        <button @click="countUpProps()">count up props</button>
        <button @click="countUpEmit()">count up emit</button>
    </div>

</template>

<script lang="ts">
    import { PropType, defineComponent , SetupContext} from 'vue';
    import AddNumberInterface from "../types/AddNumberInterface";

    type Props = {
        handleAddNum: AddNumberInterface; //Prop の Interface を定義
    }

    export default defineComponent({
        props: {
            handleAddNum: {
                // PropType を使って Prop の type に使いたい関数のInterface を指定
                type: Function as PropType<AddNumberInterface>,
                required: true
            }
        },
        setup(props: Props, context: SetupContext) {
            const countUpProps = () => {
                props.handleAddNum(1)
            }

            const countUpEmit = () => {
                context.emit('addNumber', 1)
            }

            return {
                countUpProps,
                countUpEmit,
            }
        }
    })
</script>


PropType(これは Vue2.6 からあったはず?) と TypeScript を使うことにより、props の Typeを独自のInterfaceに変更することができ、
どんな functionを渡せばいいか、明示することができました!
「Vue ぽくない」とか言われそうですが、 という概念が好きな私にとっては、とても書きやすく感じました。

まとめ

公式のリファレンスや、色々な書籍でもVueで親のmethod を実行した場合は、$emit を使う、
というのは当たり前のように記載されていますが、実際書き比べてみたり、実際のコードを運用してみた観点からしても、
props で method を渡したほが、明示的かつケアレスミスを減らすことができて、とても良いと感じています。
$emit を使う意義を感じなくなってきているので、$emit を使う理由や、もっとよい方法があれば教えてもらえるとありがたいです。
未来のことはわかりませんが、Vue3 からは TypeScript サポートされより型を意識した開発をするように今後なっていくのだとしたら、 上記のような書き方はさらに恩恵を受けそうだなと思っています。

ISMSの内部監査に向けて行ったこと

CTO室情報システム担当の吉澤です!

前回ISMSとPマークの違いについて 書かせていただきました。 今回も引き続き紹介させていただきます。

 

先日、ISMSの内部監査を行いました。ROXXでは2回目、私のキャリアとしては初の監査となりました。

今回の記事ではこの内部監査にお話します。

監査に必要なタスクの洗い出し


監査に備え、必要なタスクを洗い出しました。

マインドマップを利用したので、イメージをつかんでいただければと思います。

 

f:id:wakanayoshizawa:20200618101425p:plain

このマインドマップで全体のタスク量を把握しつつ順に準備を進めていきました。

 

各部署へのヒアリング内容

具体的な業務は各部署ごとにヒアリングをしながら項目の確認をすすめていきます。

ヒアリング内容は主に3点を担当しました。

3点とはいえ業務としては多岐にわたっていたので、作業として最も時間を要したリスク管理について書いてみたいと思います。

リスク管理について リスクアセスメントでは、生じる恐れがあるすべてのリスクを想定します。

次に、リスクごとに発生頻度、業務・会社への影響度を設定していきます。

そして、発生頻度と影響度が高いリスクについて、対策を年内実施する計画としてタスクに落とし込みます。

今回は内部監査の日程が控えていたため、発生頻度と影響度が高いと想定されたもののうち、内部監査までに対応できそうなものを中心にすすめてきました。

承認フローの見直しなど内部監査に間に合わない、長期的に実施するべきもの については年次の計画の中で対応してく予定です。

 

内部監査を実施した印象

 

私の実務としてはリスク管理に関してオペレーションに最も時間がかかったのですが、内部監査では、情報区分、教育、手順やプライバシーポリシー及び情報セキュリティ方針の運用状況について指摘される事項が多かったです。

事前の自分のイメージと現実のずれも感じとても勉強になりました。

最後に ROXXではAgent Bank、Back Checkとサービス及び組織が急拡大しています。

このような状況の中でISMSの内部監査を体験できたことは、自分にとって大きな経験となりました。

ISMSについては引き続き本番の審査、年次計画もあるので、継続的な運用に努めていきます。

LaravelをECS上で運用するTips

こちらの記事は下記ブログと同じものになります。 kotamat.com

今まではEC2上でLaravelを動かしてきたが、CVEの対応など、定期的にミドルウェアをアップデートする仕組みとして、VMレベルでのプロビジョニングをするのが大変になってきたので、Dockerコンテナ上で動く仕組みを考える必要が出てきた。

Dockerコンテナ上で動かす仕組みとしてPreview環境ではk8sを採用しているものの、メンテナンス性において社内にECSを実際に運用したことのあるメンバーがいるという観点から、安全をとってECSの採用を検討した。

ECSをLaravelで採用する上で、特に運用面にていくつか考慮しなければならない点があったので、本記事でまとめる。

TL; DR

  • ログ出力のため、標準出力・標準エラーへの反映を行う
  • Terraform上で.envを作成、S3にあげてからGithubActionsで取り回す
  • コンテナのデプロイ後を、CloudWatch Event + Lambdaで検知する
  • Batchのコンテナサービス化か、Scheduled Task + On-demand migrationか
  • キューワーカーはServiceのサイドカーで起動

以下上記を解説していく

ログ

Laravel側

標準の stack ログチャンネルにおいては、エラーログをmonologを用いた stderr へのストリーミング送信、通常ログを laravel.log へのファイル出力としている。 一方ECSでは、docker log で出力されるログをログとして収集しているため、stderrでの出力は検知できるものの、通常のログは検知する事ができない。

下記のようにファイル出力をstdoutにリダイレクトしようと思っても、PHPがファイルを見つけられずにエラーになってしまう

FROM php:fpm-alpine

# ... 諸々のプロビジョニング

COPY . /var/www/html
RUN ln -sf /dev/stdout /var/www/html/storage/log/laravel.log

直接Laravelからstdoutに出力する方法は下記のようになる。

return [
    'default' => env('LOG_CHANNEL', 'stack'),
    'channels' => [
        'stack' => [
            'driver' => 'stack',
            'channels' => ['stderr', 'stdout'],
            'ignore_exceptions' => false,
        ],
        'stdout' => [
            'driver' => 'monolog',
            'handler' => StreamHandler::class,
            'formatter' => env('LOG_STDERR_FORMATTER'),
            'with' => [
                'stream' => 'php://stdout',
            ],
        ],

        'stderr' => [
            'driver' => 'monolog',
            'level' => 'error',
            'handler' => StreamHandler::class,
            'formatter' => env('LOG_STDERR_FORMATTER'),
            'with' => [
                'stream' => 'php://stderr',
            ],
        ],
        // ...
];

こちらにすることによって、docker logに出力されるようになるのだが、php7.2以前を使っている場合、下記のように先頭にプロセスが表示されてしまう。

php_1    | [14-Jun-2020 14:14:18] WARNING: [pool www] child 8 said into stdout: "[2020-06-14 14:14:18] local.INFO: this is info log  "
php_1    | [14-Jun-2020 14:14:18] WARNING: [pool www] child 8 said into stderr: "[2020-06-14 14:14:18] local.ERROR: this is exception {"exception":"[object] (Exception(code: 0): this is exception at /var/www/html/routes/web.php:18)"
php_1    | [14-Jun-2020 14:14:18] WARNING: [pool www] child 8 said into stderr: "[stacktrace]"

PHP7.3以降ではPHP-FPMの設定にdecorate_workers_outputというディレクティブが生えているので、こちらをnoにすることによって表示されないようになる。

decorate_workers_output = no

ECS側

ECS側では標準のままだとログが一行ごとに出力されてしまい、スタックトレースなど複数行に渡るエラーが見えづらい。 そこで、TaskDefinitionのログ定義にて、datetime formatを指定することで、datetime formatベースでログをまとめてくれるようになる

[
  {
    // ... 諸々のコンテナの設定
    "logConfiguration": {
      "logDriver": "awslogs",
      "options": {
        "awslogs-group": "${awslog_group}",
        "awslogs-region": "${region}",
        "awslogs-datetime-format": "%Y-%m-%d %H:%M:%S",
        "awslogs-stream-prefix": "ecs"
      }
   }
  },
]

デプロイ

基本

GitHub ActionsをCDで用いている場合、GitHub Actions側では下記のようにすることで、TaskDefinitionの変更とデプロイができる

jobs:
  job:
    runs-on: ubuntu-latest
    steps:
    # ...
      - name: Change Task Definition
        id: render-td
        uses: aws-actions/amazon-ecs-render-task-definition@v1
        with:
          task-definition: ${{ steps.fetch-td.outputs.task-definition }}
          container-name: my-container-name
          image: ${{ steps.built-image.outputs.image }}

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

Task Definitionに環境変数をどのように注入するのか

ここで問題になるのはTaskDefinitionの環境変数をどうするかである。 ECSを使う場合、AWSの他のサービスを使うことになると思うが、 Task Definitionで記述される情報の中には当然これらの情報に対するアクセス情報を含める必要がある。

開発環境ではLaravelと同リポジトリ.env.local のようなものを用意しておき、Dockerfile内にて

COPY .env.local .env

のようにすれば.envを適応できるが、本番環境の機微情報をバージョン管理システムにコミットするわけには行かないため、本番運用ではこの方法は使えない。

TaskDefinitionには environmentFiles という設定項目があり、S3に存在する.envファイルを環境変数として使用することができる

{
  "environmentFiles": {
    "value": "<S3のobjectのarn>",
    "type": "s3" // s3のみサポート
  }
}

Terraformで環境を構築しているのであれば、上記ファイルを動的に作成し、Taskロールに付与すればいい

resource "aws_s3_bucket_object" "env_file" {
  bucket  = var.env_file_bucket
  key     = ".env"
  content = <<ENV
DB_CONNECTION=mysql
DB_HOST=${var.db_host}
DB_PORT=3306
DB_DATABASE=${var.db_name}
DB_USERNAME=${var.db_username}
DB_PASSWORD=${var.db_password}
ENV
  acl     = "private"
}

resource "aws_iam_policy" "download_env_file" {
  policy = <<POLICY
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "s3:GetObject",
        "s3:ListBucket"
      ],
      "Resource": "arn:aws:s3:::${var.env_file_bucket}/.env"
    }
  ]
}
POLICY
}

resource "aws_iam_user_policy_attachment" "env_file" {
  policy_arn = aws_iam_policy.download_env_file.arn
  user       = var.iam_user_name
}

デプロイ後の通知

上記GitHub Actionsの例を用いると、 aws-actions/amazon-ecs-deploy-task-definition@v1 ではwait-for-service-stabilityをtrueにすることによって、デプロイ完了が正常に終わったかどうかを確認することができる。 ただ、GitHubActionsは実行時間の従量課金型であるため、デプロイ方法によっては必要以上の課金が発生してしまう。

CloudWatch Event + Lambdaのログを用いることによってデプロイ状況を通知することが可能なので、そちらの紹介を行う。

Terraformにて下記のようなmoduleを作成し、

tfファイル(長いので省略)

locals {
  base_name = "${var.base_name}-ecs-notification"
}
resource "aws_cloudwatch_event_rule" "main" {
  name = local.base_name
  event_pattern = jsonencode({
    "source" : [
      "aws.ecs"
    ],
    "detail-type" : [
      "ECS Task State Change"
    ],
    "detail" : {
      "clusterArn" : [
        var.cluster_arn
      ]
    }
  })
}

resource "aws_cloudwatch_log_group" "lambda_log" {
  name = "/aws/lambda/${local.base_name}-lambda"
}

resource "aws_iam_role" "lambda" {
  name = local.base_name
  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement : [
      {
        Action = "sts:AssumeRole"
        Principal = {
          Service = "lambda.amazonaws.com"
        }
        Effect = "Allow"
      }
    ]
  })
}

resource "aws_iam_policy" "lambda" {
  policy = jsonencode(
    {
      Version = "2012-10-17",
      Statement = [
        {
          Effect = "Allow",
          Action = [
            "logs:CreateLogStream",
            "logs:PutLogEvents"
          ],
          Resource = [
            aws_cloudwatch_log_group.lambda_log.arn
          ]
        }
      ]
    }
  )
}
resource "aws_iam_role_policy_attachment" "lambda" {
  policy_arn = aws_iam_policy.lambda.arn
  role       = aws_iam_role.lambda.name
}

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

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

resource "aws_lambda_function" "main" {
  function_name    = local.base_name
  handler          = "index.handler"
  role             = aws_iam_role.lambda.arn
  runtime          = "nodejs12.x"
  filename         = data.archive_file.lambda.output_path
  source_code_hash = data.archive_file.lambda.output_base64sha256
  environment {
    variables = {
      SLACK_CHANNEL  = var.slack_channel
      SLACK_ICON     = var.slack_icon
      SLACK_APP_NAME = local.base_name
      SLACK_HOOK_URL = var.slack_hook_url
    }
  }
}
resource "aws_cloudwatch_event_target" "main" {
  arn  = aws_lambda_function.main.arn
  rule = aws_cloudwatch_event_rule.main.name
}

以下のようなLambda関数を用意すればいい

Slack通知の関数

'use strict';

const https = require('https');
const url = require('url');

// envs
const SLACK_CHANNEL = process.env.SLACK_CHANNEL
const SLACK_ICON = process.env.SLACK_ICON
const SLACK_APP_NAME = process.env.SLACK_APP_NAME
const SLACK_HOOK_URL = process.env.SLACK_HOOK_URL

exports.handler = async (event, context, callback) => {
    const eventDetail = event.detail
    if (!eventDetail) {
        console.log("The event is not expected", event)
    }

    const message = genNotifyMessage(eventDetail)
    if (!message) {
        return
    }

    const result = await sendSlack(message)
    console.log(result)
}

function genNotifyMessage(eventDetail) {
    const targetStatuses = [
        {
            from: "PENDING",
            to: "RUNNING",
            message: `${eventDetail.group} deploy started`
        },
        {
            from: "RUNNING",
            to: "RUNNING",
            message: `${eventDetail.group} deploy completed`
        }
    ]

    const target = targetStatuses
        .find(({from, to}) => from === eventDetail.lastStatus && to === eventDetail.desiredStatus)

    return target && target.message
}

function sendSlack(message) {
    return new Promise(((resolve, reject) => {

        const formData = {
            channel: SLACK_CHANNEL,
            text: message,
            icon_emoji: SLACK_ICON,
            username: SLACK_APP_NAME,
        };

        const formDataEncripted = JSON.stringify(formData)

        const options = {
            ...url.parse(SLACK_HOOK_URL),
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
                'Content-Length': Buffer.byteLength(formDataEncripted)
            }
        };

        const req = https.request(options, function (res) {
            let chunks = []
            res.on('data', function (chunk) {
                chunks.push(chunk)
            });
            res.on('end', function () {
                console.log(chunks.join(''))
                resolve({
                    body: chunks.join(''),
                    statusCode: res.statusCode,
                    statusMessage: res.statusMessage
                })
            })
        });

        req.write(formDataEncripted);

        req.on('error', function (e) {
            console.log("Sending request error: " + e.message);
            reject(e)
        });

        req.end();
    }))
}

非同期処理系

LaravelにはSchedule taskとJob workerがある。 通常のコンテナではこれらを適切に扱うことができないので、工夫が必要

ScheduleタスクとmigrationのためのBatchサービス

他のサービスを使わない形で手っ取り早く導入するのであればこの形が一番ラク

要件としては下記

  • 起動するコンテナ数は1
  • 起動時のentrypointにてmigrationの実行と、crontabの登録を行う。
  • 通常のserviceと同じタイミングでデプロイ

こうすることによって、バッチコンテナが同時に複数立ち上がることがないため、withoutOverlappingでの制御が可能となり、マイグレーションもデプロイタイミングで実行されるため、他のマネージドサービスを使う必要はない。

やり方も、LaravelのDockerfileで指定しているentrypointに下記を追加するだけ。

# 環境変数で分岐させる場合
if [[ "$BATCH" -eq "1" ]]; then
    apk --no-cache add sudo
    COUNT=0

    while [[ $COUNT -lt 1 ]]
    do
    # DB接続の確認。副作用を起こしたくないので、statusチェックのみ
      php artisan migrate:status --database 'mysql-migrate'
      READY=$?
      if [[ $READY -eq 0 ]]; then
        COUNT=$((COUNT + 1))
      else
        # migrateされていないパターンかも知れないので、installを実行
        php artisan migrate:install --database 'mysql-migrate'
        COUNT=0
      fi
      echo "waiting for finish init process... count: ${COUNT}, ready: ${READY}"
      sleep 1
    done

    echo "migrate"
    php artisan migrate --database 'mysql-migrate' --force
    echo "* * * * * sudo -u www-data php /var/www/html/artisan schedule:run" >> /etc/crontab
fi

CloudWatchを使ったScheduleTaskとGithubActionsでのマイグレーション

よりマネージドで管理する場合は、Scheduleタスクを下記ドキュメントのやり方で毎分実行するようにするとよい。 https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/userguide/scheduled_tasks.html

マイグレーションを自動で行う場合はTaskDefinitionに下記を追加する。

jobs:
  job:
    runs-on: ubuntu-latest
    steps:
    # ...
      - name: Run migration
        run: aws ecs run-task --cluster ecs-cluster --task-definition migration-td #必要なオプションは適宜追加

マイグレーションにてAlter tableを用いたマイグレーションを行っている場合、テーブルロックにより実行が止まらない可能性があるので、自動化には注意したほうがいい。

Job Workerはメインのコンテナのサイドカーで実行

Queueを用いる場合、dequeueするコンテナが必要となる。 メインのコンテナとプロセスが別れていればいいので、キューの実行に厳密性が必要なかったり、FIFOで設定されていれば、サイドカーにしてしまって問題ない。

下記のようにentrypointをartisanでも実行可能な形にしておけば

if [[ "$1" == *artisan* ]]; then
    set -- php "$@"
else
    set -- php-fpm "$@"
fi

exec "$@"

TaskDefinitionでcommandを上書きすることでqueue worker用のコンテナを実行できる

[
  {
    "name": "${container_name}-worker",
    "command": [
      "artisan",
      "queue:work",
      "sqs",
      "--sleep=3",
      "--tries=3"
    ],
    // ... 諸々のコンテナの設定
   }
  },
]

終わりに

ECS上で本番運用する上でのトピックをいくつか紹介させてもらった。 紹介した中でもっといい方法などあればFBいただけると嬉しいです。

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 も発行しているようですよ!