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 に負荷がかかるので注意は必要です。

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

Vue 3 + vue-router-nextを動かす

Vue 3のbeta版がリリースされて、あわせて周辺ツールがalphaからbetaへ作業中とのことだったのでvue-router動くかなと思ってやってみた。

github.com

github.com

以下素振りりぽじとり

github.com

プロジェクトのセットアップ

必要なものをyarn addする。

yarn add vue@next vue-router@next

あと開発用にいつもの。lint周りはお好みなので省略

yarn add -D webpack webpack-cli webpack-dev-server ts-loader vue-loader clean-webpack-plugin html-webpack-plugin typescript

webpackの設定書く

webpack.config.js

/* eslint-disable @typescript-eslint/no-var-requires */
const { resolve } = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const { VueLoaderPlugin } = require('vue-loader')
const webpack = require('webpack')

const outputPath = resolve(__dirname, 'dist')

/** @type {import('webpack').ConfigurationFactory} */
const config = (env = {}) => ({
  mode: env.prod ? 'production' : 'development',
  devtool: env.prod ? 'source-map' : 'inline-source-map',
  devServer: {
    contentBase: outputPath,
    historyApiFallback: true,
    hot: true,
    stats: 'minimal',
  },
  output: {
    path: outputPath,
    publicPath: '/',
    filename: 'bundle.js',
  },
  entry: [resolve(__dirname, 'src/main.ts')],
  module: {
    rules: [
      {
        test: /\.ts$/,
        use: 'ts-loader',
      },
      {
        test: /\.vue$/,
        use: 'vue-loader',
      },
    ],
  },
  resolve: {
    alias: {
      vue: '@vue/runtime-dom',
      '~': resolve('src'),
    },
    extensions: ['.ts', 'd.ts', '.tsx', '.js', '.vue'],
  },
  plugins: [
    new VueLoaderPlugin(),
    new HtmlWebpackPlugin({
      template: resolve(__dirname, 'src/index.html'),
    }),
    new CleanWebpackPlugin(),
  ],
})

module.exports = config

適当にエイリアスの設定とかもしておく。

package.jsonに開発鯖起動用のスクリプト書く。

"scripts": {
    "dev": "webpack-dev-server --mode=development",
}

これでsrc/main.tsをエントリポイントとしてサーバーが立ち上がるようになるはず。

composition api + vue-router

viewsにindex.htmlを適当に用意。

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Poketto</title>
</head>
<body>
  <div id="root"></div>
</body>
</html>

エントリポイントを定義する。従来とは若干apiが変わっているため注意。

createAppに<router-view />が定義されているメインのコンポーネントを渡し、rootにマウントする。やっていることは今までのVueと同じ。

main.ts

import { createApp } from 'vue'
import App from '~/App.vue'
import { route } from '~/router'

const app = createApp(App)
app.use(route)
app.mount('#root')

現時点でApp.vueもrouterもないので定義していく。

App.vue

<script>
export default {
  name: 'App',
}
</script>

<template>
  <div>
    <router-view />
  </div>
</template>

ページコンポーネントを定義する。

今回はカレントパスとなるindex.vueとサブページsub.vueを作る。なんか表示したかったので適当にcomputedを利用したreadonlyなデータを吐く関数も用意した。(useAppConfig)

vue-routerは既存のthis.$routeからのアクセスではなくなり、useRouterというnamed exportされている関数を用いることでjavascript側からhistoryの操作をすることができる。 router-linkは今まで通りに使えるが、特に型が効いたりはしない。

views/useAppConfig

import { computed } from 'vue'

export const useAppConfig =
  computed(() => {
    return {
      name: 'poketto',
      version: '0.0.1',
      mode: process.env.NODE_ENV,
    }
  })

views/index.vue

<script>
import { useAppConfig } from '~/views/useAppConfig'
import { useRouter } from 'vue-router'

export default {
  name: 'Index',
  setup() {
    const router = useRouter()
    const toSub = () => router.push({ name: 'sub' })
    return {
      useAppConfig,
      toSub
    }
  },
}
</script>

<template>
  <div>
    <p>{{ useAppConfig.name }}</p>
    <p>{{ useAppConfig.version }}</p>
    <p>{{ useAppConfig.mode }}</p>
    <router-link :to="{ name: 'sub' }">
      to sub link
    </router-link>
    <div>
      <button @click="toSub">
        to sub button
      </button>
    </div>
  </div>
</template>

views/sub.vue

<script>
import { useRouter } from 'vue-router'

export default {
  name: 'Index',
  setup() {
    const router = useRouter()
    const toHome = () => router.push({ path: '/' })
    return {
      toHome,
    }
  },
}
</script>

<template>
  <div>
    <p>Sub Page</p>
    <router-link :to="{ path: '/' }">
      home
    </router-link>
    <div>
      <button @click="toHome">
        to sub button
      </button>
    </div>
  </div>
</template>

routerの定義。

useRouterと同様に、新しくrouter作成用の関数などがnamed exportされるようになっているため、これらを使う。

router.ts

import { createRouter, createWebHistory } from 'vue-router'
import Index from '~/views/index.vue'
import Sub from '~/views/sub.vue'

export const routerHistory = createWebHistory()

export const route = createRouter({
  history: routerHistory,
  routes: [
    {
      path: '/home',
      redirect: '/',
    },
    {
      path: '/',
      name: 'index',
      component: Index,
    },
    {
      path: '/sub',
      name: 'sub',
      component: Sub,
    },
  ],
})

ここまでできたらyarn devで動作確認。

f:id:apple19940820:20200428191849g:plain

うごく

おわり

vue-routerはまだalphaなので大きくapiが変わる可能性もあるが、現時点ではちゃんと動作する。

別でフルtsxで書いてみたけどvue-routerはなんか動かなかった。あまり追えてない。

github.com

ちゃんと改善を回すためのDX Criteriaの活用

f:id:kotamat:20200429160545p:plain

DX Criteriaの第一弾がリリースされ、だいぶ時間が立ってから導入を検討することになったので、今更感は結構ありますが、ROXXなりの導入方法をもとに知見を共有することによって、今後導入される企業様並びにCTOの方々の何かしらの参考になればと思い、ブログを書くことにしました。

tl; dr

  • とりあえずやるよりは目的を持ってやったほうがいい
  • アセスメントは事業部ごとに行った
  • アセスメントめっちゃ多いので、2段階の絞り込みで対象範囲を絞った
  • 運用のためにシートをいじってみた
  • 一人でやるよりは、現場メンバーの声を反映したほうが解像度上がる

DX Criteriaとは

日本CTO協会が作成した、DXの基準となるものです。日本CTO協会はDXを2つの意味(Digital Transformation, Developer eXperience)で捉えており、DX Criteriaでは5つのテーマ、8つのカテゴリ、8つの項目で全320個のチェックにより数値化し、自己診断することができます。 詳しくは公式ページにて確認いただけます。 今回はこれをどのように現場で生かしていくのかを考え、改善サイクルに組み込んでいったROXX社の事例の紹介をさせていただきます。

DX Criteriaの導入背景と目的

今まではトップラインの向上を会社全体でも各事業部でも追ってきました。当然それは事業としての成長には不可欠ではありつつ、評価軸が事業成長に結びつくかどうかによりがちでもありました。 プロダクト開発においては、当然施策のリリース量も大事ではありつつ、それを追いすぎることによって将来の改善スピードの低下につながるような因子(いわゆる技術的負債など)がフォーカスに当たらなくなるというのが発生します。

「事業成長」と「事業部サイドからは見えないプロダクトに開発に必要な投資」のバランスを取るのは非常に大変であり、双方別々の責任者が立っていることが多い(求められるスキルが全く異なる)ため、優先順位をつけるためのコミュニケーションも非常に取りづらい状況になりがちです。

これを客観的評価軸に基づいて実施することによって、それが「ただのエゴ」ではなく「中長期を見据えた正しい意思決定」として認知されるようにする必要があり、それを行うために第三者機関が設定した評価軸を欲しておりました。

まさしくDXCriteriaはそれに該当するため、今回導入することにしたという背景があります。

当然導入するということは、何かしらの改善に活かすまでがゴールであります。すべての改善項目を同時に改善することは不可能であるため、改善効果の高いものからしっかりと改善に至るまでを評価軸ごとに目標設定し、実施していくことを導入時に決定しました。

Tips

正しく導入背景や目的を言語化し、「なんでこれやったんだっけ」みたいな状況にならないようにしたほうがいいと思います。

アセスメントやってみて

弊社には、2つのプロダクトがあり(agent bank, back check)、それぞれ技術スタックとしては同じなのですが、事業フェーズも組織構造も何もかもが違うため、それぞれでちゃんとアセスメントしないと薄まるなと初見で気づきました。

Tips

新規でやる方は、アセスメントの内容を確認の上、どの範囲でアセスメントをするのがいいのかを事前に確認したほうがいいかなと思います。

2段階の絞り込み

当然アセスメントはすべての項目に対してやったほうがいいわけですが、320項目もあり、一つ一つがかなり濃度の高い質問項目であるため、全部やるのは難しいケースもあると思います。

利用上の注意にもある通り、「すべての項目を満たせばいいというものではない」と認識しており、改善プロセスに回しアセスメントの内容を体感できて初めて活用の効果があるものかと思います。

ROXXではまず、開発組織全体として何が問題なのかというのを、5つのテーマから絞ることにしました。

事業と組織の状況を鑑み、改善されるイメージを最も持てた「システム」と「チーム」にフォーカスすることとしました。

次に、その項目に対してアセスメントをそれぞれの事業部の開発責任者主導でチェックをしていきました。

アセスメントをしていく中で、それぞれの評価項目に置いても重要視すべき項目と、現時点では重要視する必要がないものが浮き彫りになってきました。そのためシートを改修し、重要視すべき項目に対して、理想とされる評価を記入したものを別途用意しました。

f:id:kotamat:20200429155105p:plain

既存のアセスメントを「現実」、重要視すべき項目が記入されたものを「理想」とし、「現実」に記入されている項目のうち、「理想」に記入されているものと乖離があるものは黄色になるようにしました。

その結果が下記画像となります。 f:id:kotamat:20200429155121p:plain

f:id:kotamat:20200429155135p:plain

画像の通り、黄色になっているところをフォーカスし、改善していけば効果的に課題解決に向かうことがひと目で理解できるようになりました。

理想と現実を分割したシートはテンプレートとして下記URLで公開しております。もしお使いになる場合は、本家同様シートをコピーした上でお使いいただければと思います。

https://docs.google.com/spreadsheets/d/1NyEmOQ0wqnq_pNHtQlaDLbInGc6w6cCAViLF4sfwqx0/edit?usp=sharing

Tips

2段階の絞り込みにより、記入の負担を最小限にした上で今何を重要視すべきかの洗い出しをしやすくなりました。
DXCriteriaの導入目的を達成するために必要であればシートをカスタマイズするとよさそうです。

メンバーを巻き込んだ優先度付け

絞り込みを行いましたが、それでも対象項目は多いままです。このままでは何から改善を行えばいいかはわからないままです。

そこで、「改善項目」というシートに、対象項目を洗い出し、優先度付を行っていくことにしました。

まずは、当該評価を行った事業部ごとの開発責任者に「なぜその評価を行ったかの理由」と「本人が思う優先順位」をつけてもらうようにしました。

ただ、それだけではDXの当事者である「各開発者」の視点が抜けてしまいます。そのため、各メンバーと1on1を行い、上記評価理由をどう思うかと、その本人が思う優先順位を聞き、メンバーと開発責任者間にどのような現状認識の乖離があるかを把握することとしました。

その結果をもとに、総合的にチームのボトルネックを改善するためにどの項目を優先すべきかを並び替え、優先度の高いものに対してはどのような改善施策をいつまでに行っていくのかを決定しました。

f:id:kotamat:20200429155156p:plain f:id:kotamat:20200429155212p:plain

Tips

評価者一人で優先順位をつけると、現場との認識の乖離によって納得度の低い改善項目となりうるため、メンバーを巻き込んで優先度をつけました。

まとめ

まだこちらの改善プロセスは回し始めたばかりなので、これがどれだけ効果を発揮するのかは今後の改善次第ではありますが、改善プロセスを回し始めてすぐメンバーからは改善が体感できたという声が上がってきたので、改善項目の選定プロセスは一定の効果をもたらしたと感じられております。