dip Engineer Blog

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

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 (写真右)

ディップは新卒一年生でも活躍できる爆速成長環境だった

はじめに

エンジニア1年の振り返りとして 新卒研修とそれぞれの配属されてからのお話しをしたいと思います。

新卒研修

研修内容について

開発研修ではディップでの開発業務の基礎を学び、実践しました。 新卒3人で2ヶ月間にわたり、1つのシステムをリリースしました。

先輩方から実際の業務レベルの指導を受けながら 19新卒主導で要求整理からリリースまで行いました!

具体的には、

  1. リクエスタからの要望を整理(要求整理)
  2. 必要な要件の定義(機能要件定義・非機能要件定義)
  3. システム設計・DB設計
  4. 開発
  5. リクエスタとの受け入れなどの調整
  6. リリース
  7. ユーザーマニュアルの作成

を行いました。

研修を通して、私たちは開発業務の基本を学ぶことができました。 特に今でも学べてよかったと思う内容として、

  • 社内のウォーターフォール開発の進め方
  • チーム開発でのgithubの運用
  • 「自分が作りたいものをつくる」個人開発との違い
  • プロジェクト全体のタスク管理の大切さ
  • 先輩/上長への進捗の報連相
  • 基本的な資料作成

...など、配属前に大切さを実感できてよかったと思います。

作り上げたもの

「社内用語がいい感じにわかる」物が欲しいという要求に対して、 私たちはSlackで知らない単語を簡単に検索できるシステム「スラケン」を リリースしました。

f:id:satoshi-baba:20200408144908p:plain

Slackでask "知りたい単語"を送信すると、登録されている言葉を検索し、 送信したチャンネルに検索結果を返してくれます。 検索結果には正式名称や読み方、その他の呼び方と意味が表示されます。

f:id:satoshi-baba:20200408144927p:plain

発音や綴りがわからない言葉も検索できるように、あいまい検索を実装しています。 あいまい検索の解説はこちら

f:id:satoshi-baba:20200408144941p:plain

新卒研修でリリースして数ヶ月経ったスラケンですが、今も部署全体で使われており、他の部署でも使いたい!と要望が出ているサービスです。 自分たちで苦労して作ったサービスが使われて、先輩たちが盛り上がっているのを見ると、サービスの開発って楽しい!もっと使われるようにしたい! と開発がもっと好きになりました。

個別の振り返り

@ogihara_xxx

初めましてこんにちは。19新卒でモバイルアプリ開発を担当している@ogihara_xxxです。
今回はディップに入社して1年目の様子について紹介したいと思います。

1年目、どんな事した?

私は、「バイトル」のモバイルアプリを開発するチームで業務を行なっています。 掲載されているお仕事の内容を見やすくしたり、応募しやすくなるようなアプリの改修や、 ストアにアプリのアップデートを反映させるリリース業務などを行なってきました。
1年目でも重要な機能の開発や、リリース業務に携わることが出来た事で、iOSだけでなく Androidも含めアプリ全体として考えられる視点を身に付ける事が出来たのは大きな学びの1つです。

そんな中で、技術的に印象に残っている事を紹介します。

A/Bテストの実装で...

アプリを改善していく中で、どちらの施策の方が効果的に改善できるかA/Bテストで判断することは多いかと思います。
A/BテストだとAパターンとBパターンの両方実装する必要があり、大変ですね。
抜け漏れも起こりやすくなってしまうと思います。

そんな時、例えばA/Bテストのパラメータなどを定義する場合
実装の仕方を工夫することを学びました。
swiftで値定義する際を例に示します。
2つの同数のパラメータを定義する場合

enum ABTestName: String {
      case testA = "normal"
      case testB = "changed"
}
enum ABTestId: String {
      case testA = "1234"
      case testB = "5678"
}

という書き方も出来ますが、ABTestIdとABTestNameが同数ならば
以下のような書き方も出来ます。

enum ABTestName: String {
    case testA = "normal"
    case testB = "changed"

    func ABTestId() -> String {
        switch self {
        case .testA:
            return "normal"
        case .testB:
        return "changed"
        }
    }
}

このように書いた場合、testBパターンのABTestIdを書き忘れてしまったとしても
switch文のお陰でコンパイルエラーとなり、書き忘れに気付くことが出来ます。
コードを書いていく段階でバグの原因を減らしていく事が
効率的にプロダクトを運用していく上で大切であることに気がつきました。
これはどんなプロダクトや言語にも通じる事だと思います。

1年目を終えて

入社して比較的すぐにメインプロダクトの開発に携わってきた中で
大規模なプロダクトを作っていく中での緊張感や大変なこともありますが、
改善していったアプリがユーザーの方々に使ってもらえて、良いレビューを頂けた時は
とても嬉しく思います。 まだまだ、技術者としては駆け出しですが、
より深く技術力をつけてアプリをグロース出来るように頑張ります!

@r-tsuzuki

こんにちは、@r-tsuzukiです。 19新卒で唯一の文系web開発未経験エンジニアとして入りました。 入社当初はGitHubの使い方やwebサービスの開発経験がなく、新卒研修で初めて本格的な開発をしました。

1年目の業務

開発演習を終えた後、求人媒体の一括管理サービスである「バイトルマスター」の開発チームに配属されました。 開発チームでは、企業の採用効率が良くするための機能や、一目で内容がわかるような画面を開発しています。

今回は、レスポンシブデザインの開発で学んだ話を共有します。

画面サイズが違っても、わかりやすくしたい

Webサービスは、ユーザーが自分にあった使い方ができると思います。 じっくり吟味したいときはパソコンの大画面で見比べたり、 簡単に確認したいときはスマートフォンで片手でさっと見たり...

画面サイズが変わったとしても、違和感なく、内容がわかるようにすることが大切だと思います。

例えば、パソコンでは違和感なく読める長い項目名が スマートフォンでは折り返したり、隣り合った項目を画面外に配置してしまったりしたら 見るためにスクロールしたり、画面を横向きにする必要が出てきてしまいます。

調べた結果、閲覧する画面サイズにあわせて表示を変える方法があることを知りました。

CSSの@media screen and (min-width: 画面の最小の幅)を使うと 同じ長い項目の表示でも、

パソコンの大画面で見た時→全部表示する スマートフォンで見た時→最初の数文字だけ表示する

という具合に分けて画面表示を変えられました。

それまで、端末毎に表示を変えられることを知らなかったので、 開発で使っている画面のみでの表示確認を行なっていましたが、 ブラウザ/画面毎での表示を確認するようになりました。

1年目を終えて

他にも、連携している求人媒体の取り込み処理や、深夜に動くバッチの改修も担当させていただきました。 当初開発経験がなく、何をしたらいいかわからなかった状態から フロントエンドからバックエンドまで幅広く経験を積むことで、 自分からどういう実装・仕様がいいかの意見を持てるようになりました。

まだまだエンジニアとして技術の理解や視野が狭い部分もあるので、 これからより使いやすいwebサービスを開発できるように頑張っていきたいと思います。

@r-kirishima

19新卒としてディップに入社した@r-kirishimaです。
大学は物理化学系で、情報系の知識はおおむね独学です。

開発研修後の本配属では「ナースではたらこ」のユーザサイトを中心に改修するチーム(以下ナースチーム)に所属しています。
ナースチームの特徴として様々な言語や技術を用いたシステムの保守・改修を上流工程からリリースまでがっつり関わることができる、というものがあります。
私自身、様々なプロジェクトへのチャレンジによって技術だけでなく、上流工程でのコミュ力や提案力も大きく伸ばすことができました。

具体的なお仕事の例

速度改善

ある時、企画部門から下記の要求を受けました。

速度改善ツールであるPageSpeed Insightsで[画像の遅延読み込み]をするべきという指摘をされているので対応して欲しい

しかし、対象のページはほとんど画像のないページであるため違和感を覚えました。
そこでフロントエンドを中心に改修を行っているチームと協力して調査を行ったところ、重たいのは確かに画像ではあったのですが、ページの下の方で用いられているGoogleMapの地図データでした。
地図データの受信メカニズムをChromeの開発者ツールNetworkを用いて調べたところ、通常の[画像の遅延読み込み]系の処理を組み込むだけでは読み込み時の地図データ受信を遅らせることはできないことがわかりました。 そこでGoogleMapを遅延読み込み化させられるライブラリlazyLoadGoogleMapsを今回は利用することにしました。

調査結果を元に改修方針を企画部門に相談し、無事実装を終えることができました。
その結果として、同サイトの他ページと比べてPageSpeed Insightsのスコアが低かったページを他ページと同等の水準まで引き上げることができました。
また、副次的な効果として画面内にGoogleMapの描画エリアが入るまでAPIを叩かない設定にしたのでAPIを叩く回数が減少し、サービスのコストダウンにもつなげることができました!

本プロジェクトを通しての個人の成長としては下記について学べました。

  • ブラウザのレンダリングの仕組み
  • 速度改善がサービスに及ぼす影響
  • 速度改善の具体的な手法
  • 遅延読み込み関連の実装法やテスト法

1年目を終えて

大きな裁量を持たせていただいた上、様々なチャレンジをさせてもらえたので、

  • プロジェクトを進めていく力
  • フロントからバックエンド、バッチ、DBなどの幅広い知識

などを身につけることができました。
とはいえ、まだまだ駆け出しで一つ一つの知識も浅いので、より深化させられるよう頑張っていきたいと思っています!

まとめ

GitHubや資料作成などの基本的なことから、実務で使用する様々な技術など学ぶことが多い1年間でした。 現状に満足することなく、今後も圧倒的成長をしていきます! (先輩方!引き続きご指導お願いします!)

最後まで読んでいただき、ありがとうございました。 少しでもディップに興味を持っていただけましたら、ぜひディップに遊びにきてください!

著者

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