dip Engineer Blog

Engineer Blog
ディップ株式会社のエンジニアによる技術ブログです。
弊社はバイトル・はたらこねっとなど様々なサービスを運営しています。

バイトルアプリ開発にジョインしました!@林田守加

ご挨拶٩( ᐛ )و

みなさま、初めまして!
林田守加 @hayashidamoka と申します!
9月1日よりディップ株式会社に入社いたしました!
好きな仕事を”選んで”楽しく働ける人を増やし、
その人の人生も周りの人も全世界すらも楽しく変えちゃうようなそんなきっかけを作るため、
Androidエンジニアとしてバイトルアプリの開発をがんがんやって参ります!
どうぞ、よろしくお願い致します!

ディップの好きなところ

まだ入社から約1ヶ月しか経っていないのですが、好きポイントをたくさん見つけたのでお話しさせてください!

1. 先輩たちがとても優しい!

流行りの新型コロナウイルスの対策で、出社は週1日。
週4日はリモートでお仕事をしています。
入社早々のリモートワークだったので、1人でやっていけるかな...と不安もあったのですが、杞憂でした!
先輩方が常に気にかけてくださっていて、私が困る前にSlackで声をかけてくださいます。文章だけでなく、ビデオ通話で顔を見ながら優しく教えてくださることも!歓迎会もリモートで開催していただきました✨

2. 急速に成長できる環境

またまた先輩たちのお話になってしまうのですが、
私が将来どうしたいかをじっくりと聞いてくださって、そのためには何をしていくべきなのかを一緒になって本気で考えてくださいます。
どんな環境でも自分次第で成長することはできますが、周りの方々の支えもあると何倍にも早いスピードで進化していくことができます。
うまく言葉にするのは難しいのですが、面接中から大きな期待をひしひし感じていて、こんな環境で揉まれたい!と志望度がさらに跳ね上がりました。
プレッシャーはありますが、こんなに成長できる環境はありません!!期待を超えられるようにがんばります!!!
そして近い将来、後輩の人生に寄り添って育てていく側になりたいです💪

3.素敵な出会い

ディップは拠点が38箇所あり、2000名以上の社員が在籍しています!(契約社員、アルバイト、パートナーさんを含めるともっと多いです!)

TUNAGという社内SNSが活発に使われていて、社員全員のプロフィールを見て好きな物やどんな人かを知ったり、気軽に記事の投稿をして社員全員に共有することができます!
入社して半月経たないうちにびっくりするぐらい趣味が会うお友達と出会うことができました!部署もオフィスも全然違う方なので超奇跡的!!🥰
他にも、同好会があったり、共通の趣味を持った人たちが集まってざっくばらんなお話をする会(現在はリモートで開催)があったりするそうなので参加を企んでおります😎
会社全体で交流する文化があるのって素敵だなぁって思いました❣️

4. オフィスからの景色

私が通勤しているディップ本社は六本木グランドタワーの31Fにあります!
なのですっごく高いです。それに大きな窓がいっぱいあります。全面ガラス張りってやつだと思います。
なんと東京タワーが目の前!スカイツリーだって見えちゃうのです。
東京を一望できるこのビル...朝もお昼も夜も前も後ろも右も左も絶景ですが、今回はお昼のスカイツリー側の写真を載せます!

f:id:hayashidamoka:20201007110403j:plain
✨ザ・東京!って感じ✨

語り出すと長くなってしまうので割愛しますが
エンジニアになれるなんて夢にも思っていなかった上京したての頃に、同じビルのとある会社からこの景色を見させていただく機会がありました。
いつかこんなオフィスで働けるエンジニアになれたら...と憧れを抱いてこの景色を写真に収めたのを覚えています。
私にとってはただ綺麗な景色というだけでなく、初心を思い出させてくれる景色でもあるのです。
あの時憧れたかっこいいエンジニアを目指して精進していきます!

まとめ

エンジニアが欲しい制度が沢山あるところとか、社員に優しすぎるところとか 、社員総会が超豪華で楽しみとか、好きなところはまだまだありますが書ききれないのでそろそろ締めます!
気になったそこのあなたはぜひ入社してきてください!
一緒に働けるのを楽しみにしています!!🤗

余談

テックブログを書かせていただくにあたって、はてなブログのアカウントを作りました。メアドとかパスワードとか入力して利用規約とか読んで、順調にアカウント開設に向かっていたところ...
...!!!!!

こんな質問をされました。

f:id:hayashidamoka:20201007110637p:plain
「いぬどしであるか、」

....????
い ぬ ど し . . . ? ฅU•ﻌ•Uฅワン
なんで戌年???私丑年だし押せないなぁ...いやでもそもそもなんでこんなこと聞くんだろ???ってちょっと悩んで

コピペして調べてよく読んだら気がついたんですけど
「せいねんであるか、」
って問われてただけでした...

成年と戌年、似てますよね...????笑💧

著者

林田守加
f:id:hayashidamoka:20201007110900p:plain
(インタビュー記事はまだないです...お楽しみにっ!)

TerraformでFargateを構築する

はじめに

インフラエンジニアとしてTerraform運用を行っているのですが、 TerraformやFargateもだいぶ浸透してきて、導入している企業も増えてきているように感じます。 そのようなケースのサンプルとして公開したいと思います。

ファイル構成

ファイル構成は以下としています。

├── logs
│   ├── backend.tf
│   ├── main.tf
│   ├── outputs.tf
│   ├── provider.tf
│   └── variables.tf
├── buckets
│   ├── backend.tf
│   ├── main.tf
│   ├── outputs.tf
│   ├── provider.tf
│   └── variables.tf
├── ecr
│   ├── backend.tf
│   ├── main.tf
│   ├── outputs.tf
│   ├── provider.tf
│   └── variables.tf
└── fargate
    ├── backend.tf
    ├── files
    │   └── container_definition.json
    ├── main.tf
    ├── outputs.tf
    ├── provider.tf
    └── variables.tf

ポイントとしては、logとFargateのtfファイルの階層を分けている事です。 分ける理由としては、同階層にしてしまうとTerraform destoryした時にログまで消えてしまって後悔。。。というケースが起こるかなと思い階層を分けています。 同様の理由としてS3bucket、ECRも階層を分けています。

また、files以下にコンテナ定義のjsonファイル等を入れています。

Fargate

まずはクラスターから

resource "aws_ecs_cluster" "default" {
  name = local.service_name
}

タスク定義は以下のようになっています。 定義の内容をjsonファイルに記載しておいて、ECRのARNなどはdataソースを使って記入しています。

data "template_file" "default" {
  template = file("files/container_definition.json")
  vars = {
    ECR_ARN      = data.terraform_remote_state.ecr.outputs.default["repository_url"]
    SERVICE_NAME = local.service_name
  }
}

resource "aws_ecs_task_definition" "default" {
  family                   = local.service_name
  container_definitions    = data.template_file.default.rendered
  task_role_arn            = aws_iam_role.default.arn
  network_mode             = "awsvpc"
  execution_role_arn       = aws_iam_role.default.arn
  cpu                      = 512
  memory                   = 1024
  requires_compatibilities = ["FARGATE"]
}

タスク定義のjsonファイルは以下のようになっています。

[
  {
    "name": "${SERVICE_NAME}",
    "image": "${ECR_ARN}",
    "essential": true,
    "portMappings": [
      {
        "containerPort": 80,
        "hostPort": 80
      }
    ],
    "logConfiguration": {
      "logDriver": "awslogs",
      "options": {
        "awslogs-region": "ap-northeast-1",
        "awslogs-group": "/ecs/${SERVICE_NAME}",
        "awslogs-stream-prefix": "${SERVICE_NAME}"
      }
    }
  }
]

Ecsのサービスは以下のようになっています。

resource "aws_ecs_service" "default" {
  name            = local.service_name
  cluster         = aws_ecs_cluster.default.id
  task_definition = aws_ecs_task_definition.default.arn
  desired_count   = 2
  launch_type     = "FARGATE"

  load_balancer {
    target_group_arn = aws_lb_target_group.default.arn
    container_name   = local.service_name
    container_port   = 80
  }

  network_configuration {
    subnets = [
      data.terraform_remote_state.subnet.outputs.publib_a["id"],
      data.terraform_remote_state.subnet.outputs.publib_c["id"],
    ]
    security_groups = [
      aws_security_group.default.id
    ]
    assign_public_ip = true
  }
}

LB

resource "aws_lb" "default" {
  name = local.service_name
  internal           = false
  load_balancer_type = "application"
  subnets = [
    data.terraform_remote_state.subnet.outputs.publib_a["id"],
    data.terraform_remote_state.subnet.outputs.publib_c["id"],
  ]
  security_groups = [
    aws_security_group.default.id,
  ]

  enable_deletion_protection = true

  access_logs {
    bucket  = data.terraform_remote_state.buckets.outputs.default["id"]
    enabled = true
  }
}

resource "aws_lb_target_group" "default" {
  name        = local.service_name
  port        = 80
  protocol    = "HTTP"
  vpc_id      = data.terraform_remote_state.subnet.outputs.default["id"],
  target_type = "ip"
}

VPCとサブネットはバックエンドから参照しています。 また、security groupは別で作成しておいてください。 bucketについては、前述の通り階層を分けているためバックエンドから参照してください。 ターゲットグループのtarget_typeはFargateと連携するためにipに指定しておいてください。

LBへSSL証明書の適用 Certificate Managerに登録してあるSSL証明書を参照しています。

resource "aws_lb_listener" "default" {
  load_balancer_arn = aws_lb.default.arn
  protocol          = "HTTPS"
  port              = "443"
  ssl_policy        = "ELB_SecurityPolicy-TLS-1-2-2017-01"
  certificate_arn   = data.terraform_remote_state.certificate.outputs.default["arn"]
  default_action {
    type             = "forward"
    target_group_arn = aws_lb_target_group.default.arn
  }
}

80ポートへのアクセスはリダイレクトするようにします。

resource "aws_lb_listener" "redirect_https" {
  load_balancer_arn = aws_lb.default.arn
  port              = "80"
  protocol          = "HTTP"

  default_action  {
    type = "redirect"

    redirect {
      port        = "443"
      protocol    = "HTTPS"
      status_code = "HTTP_301"
    }
  }
}

variables

最後に、variablesは以下のようになっています。

variable "environment" {
  default = "development"
}

locals {
  service_name = "${var.environment}-service-name"
}

ここでlocalsを使っているのは、環境名などを変数に埋め込むためです。 特に必要ないかもしれないです。

最後に

これからTerraformを導入していきたい、Fargateを使ってみたいという方に少しでも参考になれば。。。

参考

筆者

dippeople.dip-net.jp (写真左)

BitriseのiOS Auto Provisionが超絶使いやすくなった件

はじめに

こんにちは。iOSエンジニアの@satoshi-babaです。 BitriseではiOS Auto Provisionの新しいステップが6月にリリースされました。 これは神対応だと思ったので早速使ってみました。

何が神対応なのか

BitriseはProvisioning ProfileをBitriseに登録する必要があり、Provisioning Profileが更新された場合に毎回アップロードし直してました。 ですが2年ほど前にリリースされたiOS Auto Provisionのステップを使用すると、BitriseにProvisioning Profileの登録が不要になり、自動で管理してくれるようになりました。

しかしこれは2FAを使用していたため、1ヶ月に1度の更新が必要だったのです。 それが2020年の6月にリリースされた、iOS Auto Provision with App Store Connect APIを使用すると解決するのです!

僕は弊社のBitriseの管理をしているのですが、毎月1度はBuildがうまくいかない問題の対応をしていました。(忘れる方が悪いのですが...) これは僕にとって救世主であって、即座に入れた方がいいと判断して導入しました!

導入の仕方

長い前置きでしたがここから導入していきます。 導入は簡単で認証情報の登録→ステップの差し替え→認証情報の設定をするだけです。

1. 認証情報の登録

まずはApp Store Connectにアクセスし、ユーザとアクセスのキーを開きます。 f:id:satoshi-baba:20200803101643p:plain

+ボタンを押すと登録するためのモーダルが表示されるので、適当な名称を入力します。 権限はDeveloper権限以上があれば動作します。 f:id:satoshi-baba:20200803101649p:plain

登録が完了すると一覧に表示されますので、APIキーのダウンロードをします。 また、キーIDとIssuer IDがこの後に必要になるのでコピーしておきましょう。 f:id:satoshi-baba:20200803101653p:plain

2. ステップの差し替え

次はWorkflowの修正をします。 ios-auto-provisionをios-auto-provision-appstoreconnectに変更するだけです。

- ios-auto-provision:
    inputs:
    - team_id: xxx
    - distribution_type: ad-hoc

- ios-auto-provision-appstoreconnect@0:
    inputs:
    - distribution_type: ad-hoc

3. 認証情報の設定

最後にBitriseに1で登録した認証情報を設定します。 Account Settings → Apple Developer Account → Add an Accountから登録します。 f:id:satoshi-baba:20200803101804p:plain

1で登録した際にメモした各種情報を入力します。 APIキーも忘れずにアップロードしましょう。 f:id:satoshi-baba:20200803101809p:plain

Workflowを変更したプロジェクトのTeamタブのApple Developer Portal APIに使用する認証情報を設定します。 f:id:satoshi-baba:20200803101813p:plain

以上で設定が完了しました。 Buildも通るようになっているはずです。

感想

これで月1の2FAの作業から解放されました...! このような神対応がされるのがBitriseのいいところですね! 皆さんもぜひ試してみてください!

著者

dippeople.dip-net.jp

GitHub Actionsでリリースノート作成を自動化してみた

はじめに

こんにちは、PHPで求人系サービスの開発や社内向けツールの開発を行なっている @taku-0728 です。
今回はGitHub Actionsについて記事を書きました。
この記事を書くまで全然知らなかったので実際に触ってみて、成果物としてリリースノート作成を自動化するワークフローを作ります。
同じように「GitHub Actions便利そうだし触ってみたいけどよくわからない...」という方が実際に触ってみるきっかけになればいいと思います。
よろしくお願いいたします。

GitHub Actionsとは

GitHub Actionsは、コードを保存するのと同じ場所でソフトウェア開発のワークフローを自動化し、プルリクエストやIssueで協力することを支援します。 個々のタスクを書き、アクションを呼び出し、それらを組み合わせてカスタムのワークフローを作成できます。ワークフローとは、GitHubで任意のコードプロジェクトをビルド、テスト、パッケージ、リリース、またはデプロイするためにリポジトリで設定できる、カスタムの自動プロセスです。

GitHub Actionsについてより

詳しくは上記サイトに載っていますが、要はPull RequestやIssueやPushなどのイベントをトリガーとして、ビルド、テスト、パッケージ、リリースを自動で行ってくれるものです。

背景

どのチームでもそうかもしれませんが、私のチームでは本番リリース作業終了時にGithubにリリースノートを作成しています。この作業ですがリリースの度に発生するにもかかわらず、毎回手動で作成しているので何かルールや仕組みで自動化できないかと思っていました。
リリースノートに書かれた説明とリリース用のプルリクエストの説明の文言が似ていたので、これは1つに統合したいと考えGitHub Actionsを使って自動化に挑戦しました。

今回作るもの

今回はすでに公開されているcreate-releaseを書き変えて、「masterブランチをベースにしたプルリクエストが作成されたとき、そのプルリクエストのタイトルをタグ名とし、そのプルリクエストの本文をリリースの説明としてドラフトリリースを作成する」ワークフローを作ります。

実際に作ってみる

ワークフローを設定する

  1. リポジトリ/.github/配下にworkflowsディレクトリを作ります。
    $ mkdir {リポジトリ名}/.github/workflows

  2. 1で作成したworkflowsディレクトリの配下にtest.ymlファイルを作成し、create-releaseを参考に下記のように記載します。

on:
  push:
    tags:
      - 'v*'

name: Create Release

jobs:
  build:
    name: Create Release
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@master
      - name: Create Release
        id: create_release
        uses: actions/create-release@v1
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        with:
          tag_name: ${{ github.ref }}
          release_name: Release ${{ github.ref }}
          body: |
            Changes in this Release
            - First Change
            - Second Change
          draft: false
          prerelease: false

詳細はGitHub Actionsのワークフロー構文にリファレンスがありますがざっくり説明すると

on:
  push:
    tags:
      - 'v*'

まず1行目のonでどのイベントをトリガーとして動作するかを指定します。2行目にpushを記載されているので、ここではpushイベントがトリガーとして動作します。3行目と4行目にそれぞれtagsv*とあるので今回は「先頭がvのタグがpushされると動作する」という意味になります。

name: Create Release

jobs:
  build:
    name: Create Release
    runs-on: ubuntu-latest

ここの先頭のnameでこのワークフローに名前をつけ、jobsから実際のワークフロー処理に入っていきます。buildはこのジョブのIDですので、任意の文字列で大丈夫です。次のnameはこのジョブの名前です。その次のruns-onではこのジョブを実行するマシンの種類を設定します。macOSやWindowsも指定できますが、ここではUbuntuの最新版を指定します。

steps:
  - name: Checkout code
    uses: actions/checkout@master
  - name: Create Release
    id: create_release
    uses: actions/create-release@v1
    env:
      GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
    with:
      tag_name: ${{ github.ref }}
      release_name: Release ${{ github.ref }}
      body: |
        Changes in this Release
        - First Change
        - Second Change
      draft: false
      prerelease: false

実際に処理を行っている部分です。 ここで行っていることは大きく分けて2つあります。

- name: Checkout code
  uses: actions/checkout@v2

上のnameでこの処理に名前をつけ、uses:で既に公開されているアクションを使用できます。今回はブランチをチェックアウトしてくれる既存アクションのactions/checkoutを使用します。

- name: Create Release
  id: create_release
  uses: actions/create-release@master
  env:
    GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
  with:
    tag_name: ${{ github.ref }}
    release_name: Release ${{ github.ref }}
    body: |
      Changes in this Release
      - First Change
      - Second Change
    draft: false
    prerelease: false

今回の本題となる実際にリリースを作成している部分です。
envでは環境変数を指定できます。今回はGitHub Actionsを使うときに機密情報を保持するためのsecrets配下のGITHUB_TOKENを指定していますが、Action側で生成されるため自身で用意する必要はありません。
withではこのアクションに対する入力値を指定します。${{ github.ref }}はワークフローの実行をトリガーしたブランチまたはタグ名が入ります。詳しくはこちら
bodyにはこのリリースの説明が入ります。
draftはドラフトリリースにするかどうかを示し、prereleaseはプレリリースにするかどうかを示します。

以上がcreate-releaseのテンプレートです。
今回はトリガーにタグを指定しているため、実際にタグをpushしてみます。

$ git tag -a v1.0 -m 'auto release test'
$ git push origin v1.0

Github上で確認してみます。
f:id:taku-0728:20200710135226p:plain

期待通りのリリースが登録されているはずです。
自動化はできましたが、このままではリリースの内容が固定値しか入れられないので今回は

・トリガー:masterブランチをベースにしたプルリクエストが作成されたとき
・タグ名:そのプルリクエストのタイトル
・リリース内容:そのプルリクエストの本文

となるようにこのワークフローを書き換えていきます。

ワークフローを編集する

まずトリガーをタグのpushからプルリクエストの作成に書き変えます。 先ほどのコードの

on:
  push:
    tags:
      - 'v*'

ここを

on:
  pull_request:
    types: [opened]
    branches:
      - master

このように書き換えます。これでmasterブランチをベースとしたプルリクエストが作成された時にワークフローが動作するようになります。 そして

with:
  tag_name: ${{ github.ref }}
  release_name: Release ${{ github.ref }}
  body: |
    Changes in this Release
    - First Change
    - Second Change
  draft: false
  prerelease: false

ここの部分を

with:
  tag_name: ${{ github.event.pull_request.title }}
  release_name: ${{ github.event.pull_request.title }}
  body: ${{ github.event.pull_request.body }}
  draft: true
  prerelease: false

このように書き換えます。 これでタグ名とリリースの名前がプルリクエストのタイトルに、 リリース内容がプルリクエストの説明になり、ドラフトリリースが適用されます。
最終的なymlファイルの内容は下記になります。

name: リリースの自動作成
on:
  pull_request:
    types: [opened]
    branches:
      - master
jobs:
  auto_release:
    name: auto_release
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@master
      - name: Create Release
        id: create_release
        uses: actions/create-release@v1
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        with:
          tag_name: ${{ github.event.pull_request.title }}
          release_name: ${{ github.event.pull_request.title }}
          body: ${{ github.event.pull_request.body }}
          draft: true
          prerelease: false

実際にプルリクエストを作ってみると、タイトルと本文を基にしたドラフトリリースが作成されているはずです。 f:id:taku-0728:20200710135220p:plain

まとめ

今回はリリース作業の中で自動化できそうな作業をみつけたので、GitHub Actionsを使って自動化に挑戦しました。 GitHub Actionsについてこの記事を書くまで全然知らなかったのですが、いざ触ってみると色々便利なことに気づきました。同じように触ってみたいけどよくわからない方の参考になれば幸いです。
(本当はプルリクエストがマージされたらドラフトリリースをリリースするところまでやりたかったのですが、思ったより詰まったのと長くなりそうなので別の記事でやります。)

参考

筆者

dippeople.dip-net.jp (写真右)

Railsはデフォルトでライブラリライセンスを消してしまうという話

はじめに

こんにちは。ウェブアプリエンジニアの @r-kirishima です。
業務では主にRuby on Rails(以下RoR)で開発をしています。
RoRは細かい内部処理を意識せずとも簡単にアプリが作れてしまう素敵フレームワークです。
しかし、時には内部でどのような処理が走っているのかを理解しなければならないこともあります。

今回はコード内にライセンスを明示しておく必要のあるJavascriptのライブラリをRoRアプリで利用しようとした際に、

  • ライセンス表示がRoRに消されてしまう可能性があること
  • またその問題解決のためにgemの実装について調べたこと

についてお話しします。

ライセンス表示守れていますか?

jQueryを例に

ユーザの多いjQueryのライセンスを例にお話しします。
jQuery foundation - Licenseに以下のような文言があります。

You are free to use the Project in any other project (even commercial projects) as long as the copyright header is left intact.

ざっくり訳すと、以下のようになります。
「jQueryのプロジェクトは商用であれ、他のどんなプロジェクトで使ってくれて構わないよ、ただしコピーライト表示はそのまま残しておいてくれよ!

ここでいうコピーライト表示とはjQuery 3.5.1の冒頭にある下記のようなコメントのことです。

/*!
 * jQuery JavaScript Library v3.5.1
 * https://jquery.com/
 *
 * Includes Sizzle.js
 * https://sizzlejs.com/
 *
 * Copyright JS Foundation and other contributors
 * Released under the MIT license
 * https://jquery.org/license
 *
 * Date: 2020-05-04T22:49Z
 */

ライセンスをあまり気にせずにRoR x jQueryの組み合わせを使っている方は多いと思います。
そういう方は本番環境( RAILS_ENV=production )のアプリのアセットをブラウザから確認してみてください。
もしかしたらライセンス表示が消えてしまっている可能性が高いですよ。
そしてそれは無自覚でもライセンス違反です......

ライセンスはなぜ消えたのか

どうしてライセンス表示が消えてしまったのか?

コメント、長い変数名、インデント、改行などは人間の可読性を上げるためのものです。
一方で実行する機械にとっては基本的に不要なものです。
RoRにおいてはAsset Pipelineが機械にとっての「ゴミ」を取り除いてくれるのですが、
その際に人間にとっては必要なライセンスもまた「ゴミ」として削除されてしまうのです。

Asset PipelineとUglifier

上で少しでてきたAsset Pipelineについて軽く説明します。
Asset PipelineとはJavascript、CSS、画像などのアセットを入力すると、設定に従った加工してくれる仕組みのことです。
詳細はRailsガイドを参照してください。
本記事ではJavascriptから機械にとって不要なものを取り除いてくれるもの、ぐらいに考えてもらえば結構です。

Rails5においてはAsset Pipelineにsprockets-railsというgemを用いています。
また、特にSprocketsはJavascriptの圧縮作業(Compress)にUglifierという別のgemを用いています。

Uglifierのデフォルト設定とSprocketsのデフォルト設定

上述のようにSprocketsはUglifierを利用してJavascriptの圧縮を行っています。
このUglifierのコメントに関するデフォルトの設定はlautis / uglifierを参照すると、
下記のように Copyright というワードの入っているコメントブロックを保持する設定になっています。

  DEFAULTS = {
  # 中略
    :output => {
  # 中略
      :comments => :copyright, # Preserve comments (:all, :jsdoc, :copyright, :none)
  # 中略

  def comment_options
    case comment_setting
    when :all, true
      true
    when :jsdoc
      "jsdoc"
    when :copyright
      encode_regexp(/(^!)|Copyright/i)
    when Regexp
      encode_regexp(comment_setting)
    else
      false
    end
  end
  # 中略

一方でSprocketsでは下記のようにコメントを全て削除する設定でinitializeしてしまい、こちらの設定が実際に用いられます

  # 中略
    def initialize(options = {})
      # Feature detect Uglifier 2.0 option support
      if Autoload::Uglifier::DEFAULTS[:copyright]
        # Uglifier < 2.x
        options[:copyright] ||= false
      else
        # Uglifier >= 2.x
        options[:comments] ||= :none
      end
  # 中略

参考:ぼっち勉強会 - rails + sprockets + uglifierでCopyrightを残したままcompressする

ライセンスを消させない

参考として挙げたブログ内ではRailsの設定ファイルを下記のように書き換えることで解消できる、とされています。

- config.assets.js_compressor = :uglifier
+ config.assets.js_compressor = Uglifier.new(:comments => :copyright)
# より今時の書き方をすると comments: :copyright
# さらに厳密な書き方をすると output: {comments: :copyright}

しかし、commentsオプションをcopyrightに設定して保護されるのは Copyright と入っているコメントのみです。
上でふれているjQuery-3.5.1のライセンスなどには Copyright と入っていないため保護されません。

Copyrightと入っていなくてもライセンスを消させない

Uglifierのコードを見ると、実はSymbolを用いた指定以外にも正規表現を用いた指定が可能であることがわかります。
以下に抜粋して再掲します。

  def comment_options
    case comment_setting
    # 中略
    when Regexp
      encode_regexp(comment_setting)
    # 中略

そこで、今回は以下のように jquery.org/license Available for use under the MIT License Licensed MIT
のワードが入っているコメントを残せるようにしてみました!

- config.assets.js_compressor = :uglifier
+ config.assets.js_compressor = Uglifier.new(output: {comments: /jquery.org\/license|Available for use under the MIT License|Licensed MIT/})
# Rubyにおいてスラッシュ囲みは正規表現リテラルを表します。

増えるたびに正規表現を書くのは大変なので、反応するワードを配列で切り出してみました。

- config.assets.js_compressor = :uglifier
+ trigger_comments = ["jquery.org/license", "Available for use under the MIT License", "Licensed MIT"]
+ config.assets.js_compressor = Uglifier.new(output: {comments: /#{trigger_comments.join('|')}/})

いい感じですね!

おわりに

RoR x jQueryという組み合わせで利用している方も非常に多いと思いますが、
デフォルトでライセンス表示義務違反を犯してしまう罠があるというのはとても驚きです。

ただ闇雲にレールに乗るだけではなく、正しくRoRを理解して使っていきたいですね!

筆者

dippeople.dip-net.jp
dippeople.dip-net.jp

テックブログに自動投稿システム導入してみた

はじめに

2020年1月にテックブログをリニューアルして以来、メンバーに記事を投稿してもらう度に記事のレビューや記事投稿を手作業で行ってきましたが、せっかくなので記事を自動投稿できるようにしたいと思います。

やること

  • 記事のアップロード(下書き状態)
  • 記事内の画像アップロード(フォトライフ)

をGitHubへのプルリクの作成時に起動するようにしたいと思います。

記事のアップロード(下書き状態)

はてなブログAPIのOAuth認証で記事をアップロードしたいと思います。 まずOAuth認証のアクセストークンを発行するためにConsumer KeyとConsumer Secretを取得します。 Consumer KeyとConsumer Secretを利用してAccess Tokenを取得するコードは以下となります。

require 'oauth'
require 'mechanize'

class HatenaAPI
  def initialize
    @consumer = OAuth::Consumer.new(
      ENV['CONSUMER_KEY'],
      ENV['CONSUMER_SECRET'],
      site: 'https://www.hatena.com',
      request_token_url: '/oauth/initiate?scope=read_public%2Cread_private%2Cwrite_public%2Cwrite_private',
      access_token_url: '/oauth/token',
      oauth_callback: 'oob',
      timeout: 300
    )
    @agent = Mechanize.new
  end

  def oauth_authorize
    request_token =  @consumer.get_request_token
    oauth_verifier = ''
    page = @agent.get(request_token.authorize_url)
    form = page.forms[0]
    form.field_with(name: 'name').value = ENV['USER_NAME']
    form.field_with(name: 'password').value = ENV['PASSWORD']
    @agent.submit(form)
    page = @agent.get(request_token.authorize_url)
    form = page.forms[1]
    page = @agent.submit(form)
    oauth_verifier = page.css('div.verifier').text
    @consumer.options.delete(:oauth_callback)
    request_token.get_access_token(oauth_verifier: oauth_verifier)
  end
end

hatena = HatenaAPI.new
access_token = hatena.oauth_authorize
puts "AccessToken: #{access_token[:oauth_token]}"
puts "AccessTokenSecret: #{access_token[:oauth_token_secret]}"

Consumer KeyとConsumer Secret、ユーザ名とパスワードを環境変数として定義しておくとAccess Tokenを取得することができます。

続いて、Access Tokenを利用して記事の投稿を行います。

class HatenaAPI
  attr_reader :header, :hatena_blog, :photolife
  class << self
    def generate_access_token(site)
      consumer = OAuth::Consumer.new(
        ENV['CONSUMER_KEY'],
        ENV['CONSUMER_SECRET'],
        site: site,
        timeout: 300
      )

      OAuth::AccessToken.new(
        consumer,
        ENV['ACCESS_TOKEN'],
        ENV['ACCESS_TOKEN_SECRET']
      )
    end
  end

  def initialize
    @hatena_blog = HatenaAPI.generate_access_token('http://blog.hatena.ne.jp')
    @photolife = HatenaAPI.generate_access_token('http://f.hatena.ne.jp')

    @header = { 'Accept' => 'application/xml', "Content-Type" => "application/xml" }
  end

  def upload_article(file_path)
    body = File.read(file_path)
    @hatena_blog.request(:post, "https://blog.hatena.ne.jp/#{ENV['USER_NAME']}/#{ENV['BLOG_ID']}/atom/entry", Oga.parse_xml(body).to_xml, @header)
  end
end

hatena = HatenaAPI.new
hatena.upload_article('article.xml')

また、アップロードする記事はxmlである必要があるため、以下のテンプレートに記事内容やタイトルを置き換えてアップロードします。

<?xml version="1.0" encoding="utf-8"?>
<entry xmlns="http://www.w3.org/2005/Atom"
       xmlns:app="http://www.w3.org/2007/app">
  <title>{{title}}</title>
  <author><name>{{author}}</name></author>
  <content type="text/x-markdown">
{{content}}
  </content>
  <updated>{{time}}</updated>
  <app:control>
    <app:draft>yes</app:draft>
  </app:control>
</entry>
  • title: 記事のタイトル
  • author: 執筆者のはてなID
  • content: 記事内容
  • time: 投稿時刻 となっています。 また、 <app:draft>yes</app:draft> とすることで下書きとしてアップロードしてくれます。

記事内の画像アップロード(フォトライフ)

最後に、画像のアップロードですがこちらも記事とほぼ同様の手順でアップロードすることができます。

@photolife.request(:post, '/atom/post', body, @header).body)

また、画像アップロードようのxmlのテンプレートは以下となります。

<entry xmlns="http://purl.org/atom/ns#">
  <title>{{title}}</title>
  <content mode="base64" type="{{mime_type}}">{{content}}</content>
  <dc:subject>{{dirname}}</dc:subject>
</entry>
  • title: 画像のタイトル
  • mime_type: 画像のmimeタイプ
  • content: 画像をBase64エンコードしたもの
  • dirname: フォトライフのアップロードするディレクトリ(Hatena Blog等)

最後に

はてなブログAPIを利用した記事の自動投稿を実装しました。 これらをGitHubのイベントと連携することでより快適なテックブログ投稿ができるようになりました。

参考

筆者

dippeople.dip-net.jp (写真左)

【MySQL】インデックスの仕組みについて

はじめに

こんにちは、PHPで求人系サービスの開発や社内向けツールの開発を行なっている @taku-0728 です。
今回は遅いクエリのパフォーマンス改善によく使われるインデックスについて、具体的にどういう仕組みで早くなるのかを自分なりにまとめますので、これからインデックスを貼ろうと思っている方は参考にしていただければと思います。
本記事はあくまで自分の考えの整理も兼ねてまとめている部分があり、誤った解釈をしている可能性もありますのでその際は遠慮なくご指摘いただければ幸いです。
私が業務で使っているRDBがMySQLであるためこの記事ではMySQLを例に説明します。

この記事でわかること

  • MySQLで使われるインデックスの仕組み(B-treeインデックスについて)
  • なぜインデックスを貼ると検索速度が向上する(場合がある)のか
  • インデックスを貼ると検索速度が向上するカラム、向上しにくいカラム
  • インデックスを貼るデメリット

この記事の注意点

  • インデックスとは何か、といった初歩的な内容は割愛します

MySQLで使われるインデックスの仕組み

一口にインデックスと言ってもRDBなどによって種類はいくつかわかれますが、本記事ではMySQLで使われるB-treeインデックスについて紹介します。

B-treeインデックスとは

探索系のアルゴリズム二分探索木とAVL木を応用した B-tree を変形した B+ tree を用いた検索方法のことです。
B+ tree について説明するために、二分探索木から順番に説明していきます。

二分探索木

二分探索木とは「検索したい値が中央値より小さい場合は左に進み、大きい場合は右に進みながら検索していくアルゴリズム」のことです。下記出典元の画像がわかりやすいと思います。

f:id:taku-0728:20200512105047p:plain
二分探索木
出典:Javaで二分探索木(木構造)を実装する

二分探索木は検索に強いアルゴリズムですが、データの挿入や削除によって木の高さが 変わった場合、検索したい値によって検索回数や速度が変わってくる可能性があります。
その問題に対応したのがAVL木です。

AVL木

AVL木とは木の高さに変更があった場合、二分探索木に回転という操作を行うことで木の高さを一定にするデータ構造のことです。回転とは二分探索木の各ノードが持つ値の大小関係の条件を満たしたまま木を変形する操作を意味します。
回転には右回転、左回転があり、右回転の場合は

  • 左にある部分木は、位置が1段高くなる
  • 真ん中にある部分木は、親が変わり、縦の位置は変わらない
  • 右にある部分木は、位置が1段低くなる

という特徴があります。左回転の場合はその逆です。
こちらも下記出典元の画像がわかりやすいと思います。

f:id:taku-0728:20200512104719p:plain
AVL木
出典:B-treeインデックス入門 - Qiita

このAVL木を用いたのが B-tree(B木) です。

B-tree

B-tree とは AVL木と同様なバランス木の一種です。AVL木では各節点にキーがあり、そのキーに対する大小で左右への振り分けを行っていくというルールでした。B木では、節点に複数のキーを格納できます。新たな要素は、それぞれのキーに対して大きいのか、あるいは小さいのかを比較して子の節点へ振り分けられていきます。より詳細に知りたい方はこちらのサイトが参考になると思います。

B+ tree

B+ tree はMySQLで採用されているアルゴリズムです。
ほとんど B tree と同じですが、2点ほど違いがあります。

  • リーフノードとリーフノードを結ぶポインタがある
  • データはリーフノードのみに保持する

言葉で話すと難しいかもしれませんが、こちらのサイトで画像つきで解説されてるのでこちらを参照されるとよりわかりやすいかと思います。

f:id:taku-0728:20200512105138p:plain
B-treeとB+treeの違い
出典:What are the differences between B trees and B+ trees?

データはリーフノードに持つので、途中の子ノードとリーフノードで同じキーがあることが分かります。また、末端のリーフノードたちはポインタで結ばれています。
B-treeと B+ tree のメリットを下記にまとめます。

  • B-tree
    • 子ノードもデータを持つので、探索途中でヒットすればレスポンスが早い。つまり等価条件の探索が向いている。
  • B+ tree
    • リーフノードがポインタでつながっているので、範囲検索に強い。

MySQLではこの「B+ tree」が採用されています。 以上がMySQLで使われるインデックスの仕組みです。

なぜインデックスを貼ると検索速度が向上する(場合がある)のか

上記でインデックスの仕組みを説明しましたが、ここまでこればなぜインデックスを貼ると検索速度が向上する(場合がある)のかはもうわかると思います。
もしインデックスを貼らなければ先頭から順番に値を探していく必要がありますが、インデックスを貼ることによって B+ tree を使ってより効率的に検索していくため検索速度が向上する(場合がある)ということです。

インデックスを貼ると検索速度が向上するカラム、向上しにくいカラム

上記でインデックスを貼ることで検索速度が向上する(場合がある)と説明しましたが、必ず検索速度が向上するわけではなく誤った貼り方をすれば検索速度は向上どころか劣化する可能性もあります。

インデックスを貼ると検索速度が向上するカラム

  • 検索条件で等価条件が使われるカラム
  • 前方一致でlike検索しているカラム

like検索はインデックスが効かないと思われがちですが、前方一致かつ、検索条件がある程度設定されていればちゃんと効きます。

where name like 'hoge%'

インデックスを貼っても検索速度が向上しにくいカラム

  • 検索条件で範囲検索が使われるカラム
  • 中間一致や後方一致でlike検索しているカラム
  • データの種類が少ないカラム

範囲検索でインデックスが効かない理由はこちらのサイトに詳しくかいてあるので、詳しく知りたい方はリンク先の記事を読まれるといいと思います。
また、like検索でも中間一致や後方一致の場合はインデックスは効きません。
データの種類が少ないカラムは前から順番に検索してもインデックスを貼ってもあまり検索速度に違いはないと思います。具体的に何件以上あれば効果があるのかというのは検索条件にもよって変わるので一概にいうのは難しいですが、検索速度の劣化が見受けられたらインデックスを貼ることを検討すればいいかと思います。

インデックスを貼るデメリット

ここまでインデックスの仕組みと検索速度の向上について説明してきました。「インデックスを貼れば速度が向上する可能性があるんだからとりあえず全カラムに貼っておけばいいじゃん」と思う方ももしかしたらいらっしゃるかもしれませんが、インデックスを貼ることはデメリットがないわけではありません。場合によってはデメリットが生じる可能性もあります。
「インデックスの仕組み」で紹介した通りインデックスとは全データをみて探索木を作成するアルゴリズムですが、insertやupdateなどでデータ構造が変わった場合、探索木を作り直す必要があります。そのため不必要にインデックスを貼ることはinsertやupdateなどデータ構造の変更する際の速度低下につながるので注意が必要です。

まとめ

ここまでインデックスの仕組みからインデックスを貼るデメリットまで説明させていただきました。「インデックスを貼りたい時は必要最低限のカラムにのみ貼る」ことを意識すればいいかと思います。
最初にも申し上げましたが、本記事はあくまで自分の考えの整理も兼ねてまとめている部分があり、誤った解釈をしている可能性もありますのでその際は遠慮なくご指摘いただければ幸いです。
最後までお付き合いいただきありがとうございました。

参考

筆者

dippeople.dip-net.jp (写真右)