Scrapyのキャッシュを定期的に削除する

ZOZOテクノロジーズ #1 Advent Calendar 2020 23日目です。 ScrapyのCacheにおいて定期的にCacheを削除するような機能を作成したので紹介します。

ScrapyのCacheについて

ScrapyのCacheはHttpCacheMiddlewareで実装されています。
Cacheを有効にするとScrapyからリクエストを送った後に返ってきたレスポンスをCacheし、再度同じリクエストを送った際にCacheのデータを利用することができます。
Cacheを利用することによってクローリング対象のサーバへのリクエストを減らすことができます。 またCacheを保存する場所においてはデフォルトでlocalに保存されます。保存場所は変更可能で、DBや他のミドルウェアに設定可能です。
今回はデフォルトのlocalに保存される場合の話になります。

HTTPCACHE_EXPIRATION_SECSについて

Scrapy側でCacheを削除できる機能が存在するのか調査を行いました。 Scrapyが提供しているCache関連で設定できる環境変数を調べるとHTTPCACHE_EXPIRATION_SECSが設定できます。
HTTPCACHE_EXPIRATION_SECSはCacheに有効期限を設定できる設定になります。自分は最初HTTPCACHE_EXPIRATION_SECSを設定すればCacheを削除されると思ってました。しかし実装を追ってみると、特に削除されないことがわかりました。

実際にどのように実装されているのか中身を見てみようと思います。 HttpCacheMiddlewareの実装場所はhttpcache.pyになります。 storageの選択に関してはinit部分のstorageの部分で設定されます。デフォルトではscrapy.extensions.httpcache.FilesystemCacheStorageが設定されています。

self.storage = load_object(settings['HTTPCACHE_STORAGE'])(settings)

よってextensionsに配置されているFilesystemCacheStorageクラスがhttpcache.pyで利用されるようになります。

Cacheを利用する際はretrieve_responseメソッドが呼び出されます。メソッド内の_read_meta部分で 有効期限の条件分岐が実装されています。

実装箇所を確認すると下記のように実装されています。

       if 0 < self.expiration_secs < time() - mtime:
            return  # expired

実装より、特にexpireしたCacheの削除は行われないことがわかります。 周辺の処理を確認しても特に削除する実装は存在しませんでした。

Scrapyのmiddlewareは自分で実装すことが可能なので新しくCacheを削除するmiddlewareを追加しました。

実装

下記のようにmiddlewareとして実装を行いました。

from scrapy.settings import Setting
from scrapy.utils.misc import load_object
import shutil
import glob
from pathlib import Path

class CacheCleanMiddleware(object):
    def __init__(self, settings: Settings, crawler):
        self.crawler = crawler
        self.storage = load_object(settings['HTTPCACHE_STORAGE'])(settings)
        self.expiration_secs = settings.getint('HTTPCACHE_EXPIRATION_SECS')

    @classmethod
    def from_crawler(cls, crawler):
        o = cls(crawler.settings, crawler)
        return o


    def process_response(self, request, response, spider):
        rpath = self.storage._get_request_path(spider, request)
        cache_root_directory = Path(os.path.join(self.storage.cachedir, spider.name))
        cache_directory = sorted(cache_root_directory.glob('*/*/'), key=lambda f: os.stat(f).st_mtime)
        for f in cache_directory:
            metapath = os.path.join(f, 'pickled_meta')
            if os.path.exists(metapath):
                mtime = os.stat(metapath).st_mtime
                if 0 < self.expiration_secs < time() - mtime:
                    shutil.rmtree(Path(f).parent)
        cache_size = sum((os.path.getsize(f) for f in cache_root_directory.glob('**/*') if os.path.isfile(f)))
        if cache_size / 1024**2 >= 300:
            for i in range(cache_directory) / 3:
                metapath = os.path.join(f, 'pickled_meta')
                shutil.rmtree(Path(f).parent)

        return response

特に凝った実装は行っていませんがCacheの保存場所にあるCacheデータのmtmeを確認し、HTTPCACHE_EXPIRATION_SECSで指定された有効期間を過ぎていたらCacheを削除する仕組みになっています。また上限を300MBとし、300MB以上Cacheデータが増えた場合は古いCacheをある程度削除するような仕組みになっています。

まとめ

Scrapyのキャッシュを削除できるような処理を追加してみました。PC内で動かす際には特に問題ないのですが、Docker等リソースが限られている場合にScrapyを動かすとストレージが上限に達する場合があるので定期的にCacheを削除する必要が出てくるかなと思います。

Vaultを触ってみる

ZOZOテクノロジーズ #1 Advent Calendar 2020 21日目です。
今回はHashiCorpがリリースしているVaultについて触ってみました。

Vaultについて

Vaultはパスワードやアクセストークン等、機密情報を管理するソフトウェアです。 役割としてはgoogleのSecret MagegerやawsのSecrets Managerに近いです。
またVaultの機能として一時的にサーバーへアクセスできるTokenを発行するOne-Time Tokenの機能や各機密情報に関してユーザーの権限を設定することができるPolicyの機能があります。 Vaultのアーキテクチャについては下記の図になります。

https://www.vaultproject.io/img/layers.png Architecture | Vault by HashiCorp

アーキテクチャについては下記の特徴があります。

  • Vault間で通信するデータは全てbarrierを通して暗号化され、通信される。

  • ストレージに保存される機密情報は暗号化され保存されるようになる。

Vaultの構築

今回はDocker上に構築を行います。 DockerのVersionに関しては下記になります。

  • Docker:20.10.0

構築手順に関してはGetting Startedの部分を参考に構築します。

Vaultの起動の前にConfigファイルを設定します。Configファイルを設定しなくてもVaultを起動することができますが。デフォルトではUIが有効になっておらずAPIサーバのみ起動します。今回はUIを有効にして利用したいのでConfigファイルを有効にします。
VaultのConfigファイルの記述はHashiCorpが提供しているHCLになります。 今回は下記のような設定で動かします

storage "file" {
  path    = "./vault/data"
}

listener "tcp" {
  address     = "0.0.0.0:8201"
  tls_disable = 1
}

api_addr = "http://0.0.0.0:8200"
ui = true

vaultのWebUIはデフォルトでは有効になっていないため設定で有効にする必要があります。 最終的に下記のDockerfileでまとまりました

FROM ubuntu:latest
RUN apt update
RUN apt install -y curl gnupg gnupg2 lsb-release
RUN curl -fsSL https://apt.releases.hashicorp.com/gpg |  apt-key add - && \
    apt-add-repository "deb [arch=amd64] https://apt.releases.hashicorp.com $(lsb_release -cs) main" && \
    apt-get update &&  apt-get install vault
COPY ./config.hcl ./config.hcl

CMD vault server -dev -config=config.hcl

作成したDockerfileでコンテナを作成し、localhost:8001/uiにアクセスすると下記のUIが表示されます。 f:id:jon20:20201221050948p:plain 今回はdevモードで起動しているのでコンテナを起動した際のConsoleに表示されているTokenを用いることでVaultにログインできます。 f:id:jon20:20201221051212p:plain 無事ログインできたことが確認できました。

Secrets Enginesの設定

次にSecrets Enginesを用いてKey-Value Store(KVS)を作成してみます。Secrets Enginesの機能はPath形式でVaultにおいて機密情報をどこに保存するか設定できる機能になります。保存する場所はConsulやRabbitMQ等、様々なミドルウェアと連携できます。
今回はkv/hogeのPath名で、sample:sampleのKey-Value(KV)を保存しました。 f:id:jon20:20201221081930p:plain

www.vaultproject.io

policyの設定

次にpolicyの設定を行ってみます。 policyの設定はPoliciesタブで設定することができます。 policyの記述に関してもHCLで記述することができます。 許可されたユーザがkv/のPathを閲覧できるようにしたいので下記の設定を作成します。 f:id:jon20:20201221082521p:plain

付与した権限の詳細に関しては下記にまとまってます。

www.vaultproject.io

Authentication Methodsの設定

次にAuthentication Methodsを設定します。Authentication Methodsは名前の通り、認証方法を設定し、認証を利用するユーザーを管理することができます。認証方法はJwt認証やGithubの認証を使って認証する方法を設定できます。今回はUsername & Passwordで認証する方法を試してみます。 作成したユーザーは下記になります。sampleuserという名前で、先程作成したtest policyをアタッチしています。なのでこのユーザーはkv/のPathのみ閲覧することができます。追加でBOBというユーザを作成してます。こちらのユーザは特に何にもpolicyをアタッチしてないのでkv/のPathは閲覧できないように設定してます。 f:id:jon20:20201221083232p:plain

試しにログインして確認してみます。
sampleuserの画面 f:id:jon20:20201221083722p:plain BOBの画面 f:id:jon20:20201221084103p:plain

sampleuserでkv/のPath以下に配置されているKVを閲覧することができました。また、別途作成したBOBのアカウントではkv/のPath以下は閲覧できないことがわかりました。

ちなみにBOBの画面に表示されているcubbyholeについてはユーザーアカウント毎にデフォルトで用意されているKVSになります。こちらで設定した値はユーザー毎にスコープされます。

https://www.vaultproject.io/docs/secrets/cubbyhole

まとめ

今回はVaultをdevモードで構築してみました。 awsgcpやlocalで保存しているパスワードなどの情報をvaultで一括管理できるので便利だなと思いました。 しかしawsのsecrets magegerやgcpのsecret magegerと違い、Vaultを自前で運用する必要がでてくるので導入にはそれなりのコストが必要と思いました。
まだVaultの機能であるOne-Time Tokenの機能など、試していない機能もいくつか存在するので時間がある際に試してみようと思います。

mysqlのコンテナに初期データを流し込む

ゆるっと Advent Calendar 2020 13日目です。今回はmysqlのコンテナに初期データを流し込む方法について紹介します。

概要

開発環境を構築するためにDBのテーブルや初期データを配置したい場合があると思います。コンテナ起動後にデータを流し込む方法がありますがなるべく手間が無く構築できる方法がないか探しました。今回はmysqlについて探したところInitializing a fresh instanceの機能がありました。

MysqlのDockerhubのドキュメントに書かれているInitializing a fresh instanceの項目を引用すると

When a container is started for the first time, a new database with the specified name will be created and initialized with the provided configuration variables. Furthermore, it will execute files with extensions .sh, .sql and .sql.gz that are found in /docker-entrypoint-initdb.d. Files will be executed in alphabetical order. You can easily populate your mysql services by mounting a SQL dump into that directory and provide custom images with contributed data. SQL files will be imported by default to the database specified by the MYSQL_DATABASE variable.

つまり/docker-entrypoint-initdb.d ディレクトリに.sh, .sql, .sql.gzの拡張子のファイルを配置すると対象のファイルを読み込み、実行してくれます。実際に試してみます。

使用環境

  • macOS 11.0.1
  • docker 20.10.0
  • docker-compose 1.27.4
  • mysql 8.0

実行例

下記のdocker-compose.ymlを用意します。

docker-compose.yml

version: '3'
services:
  db:
    image: mysql:8.0
    ports:
      - 3306:3306
    environment:
      TZ: Asia/Tokyo
      MYSQL_ROOT_PASSWORD: root
    volumes:
        - ./mysql/:/docker-entrypoint-initdb.d/
    command: mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci

次に初期データを流し込むために下記のsqlファイルを用意します。

sample.sql

CREATE DATABASE IF NOT EXISTS sample;
USE sample;

CREATE TABLE IF NOT EXISTS sample_table
(
  `id`         int(11) NOT NULL AUTO_INCREMENT,
  `sample`     text,
  PRIMARY KEY (`id`)
);

INSERT INTO sample_table (sample) VALUES ("sample1");
INSERT INTO sample_table (sample) VALUES ("sample2");

最終的に下記のディレクトリ構成でdocker-compose upを行いコンテナを生成、実行してみます。

./
├── mysql
│   └── sample.sql
├── docker-compose.yml

コンテナ起動後にdatabaseを確認すると無事テーブルと初期データが作成されていることがわかります。

mysql> select * from sample_table;
+----+---------+
| id | sample  |
+----+---------+
|  1 | sample1 |
|  2 | sample2 |
+----+---------+
2 rows in set (0.01 sec)

ではどのような仕組みで実行されているのかDockerfileの中身をみてみようと思います。

仕組み

対象のリポジトリは下記になります。

github.com

対象のDockerfileを確認するとentrypointがdocker-entrypoint.shに設定されています。docker-entrypoint.shの中身を見てみると _main関数が初めに実行されることがわかります。関数内でdocker_temp_server_startが呼ばれてmysqlサーバが起動されているので以降の処理を追ってみます。docker_process_init_files関数で/docker-entrypoint-initdb.d/配下のファイルが渡されているので、この処理でsqlファイルの実行が行われていそうな気がします。

対象の関数内の中身をみると各拡張子ごとに処理を分けている部分があります。その中でdocker_process_sql関数が実行されmysqlコマンドが実行されているのでこの関数内でSQLが実行されていることがわかります。

まとめ

コンテナ作成時に初期データを流し込めるのは便利だと思いました。実装に関しても特に難しい処理は行われていないので他のDBにも流用できると思いました。

PythonでGCPの認証を行う

GCPの認証ってたまにどうやるんだっけ?って忘れてしまうので備忘録として残しておこうと思います。 今回はCloud Pub/Subを例に認証を行います。

今回使用する言語

予備知識

サービスアカウントとユーザーアカウントの認証の仕組みに関しては下記の記事が参考になります。

medium.com

Cloud Pub/Subのlibraryの使い方に関しては下記になります。

googleapis.dev

サービスアカウントを用いて認証

環境変数を用いる

export GOOGLE_APPLICATION_CREDENTIALS="[PATH]"

上記のようにサービスアカウントが含まれるパスを指定してあげるとclient libraryが使えるようになります こちらの方法だとCloud Pub/SubにPublishする際は下記のように扱うことができます。

from google.cloud import pubsub_v1
import os

project_id = os.getenv("PROJECT_ID", "")
topic_id = os.getenv("TOPIC_ID", "")
publisher = pubsub_v1.PublisherClient()
topic_path = publisher.topic_path(project_id, topic_id)
publisher.publish(topic_path, b"Hello World")

ファイルパスを指定して認証

google.oauth2.service_account moduleを使って認証するやり方です。SERVICE_ACCOUNT_FILE_PATH環境変数にファイルのパスを指定している感じです。

from google.cloud import pubsub_v1
from google.oauth2 import service_account
import os
project_id = os.getenv("PROJECT_ID", "")
topic_id = os.getenv("TOPIC_ID", "")
service_account_file_path = os.getenv("SERVICE_ACCOUNT_FILE_PATH", "")
credentials = service_account.Credentials.from_service_account_file(service_account_file_path)
publisher = pubsub_v1.PublisherClient(credentials=credentials)
topic_path = publisher.topic_path(project_id, topic_id)

publisher.publish(topic_path, b"Hello World")

データを直接読み込んで認証

同じように先ほどのgoogle.oauth2.service_account moduleを使います。 Credentialsクラスで定義されているfrom_service_account_infoメソッドを使うことでファイルパスを指定することなくサービスアカウントの情報を直接渡して認証できます。 特にコンテナなど、jsonファイルを中に置きたくない場合に有効なのかなと思います。SERVICE_ACCOUNT_FILE環境変数にサービスアカウントの情報が入ってます。

from google.cloud import pubsub_v1
from google.oauth2 import service_account
import json
import os
project_id = os.getenv("PROJECT_ID", "")
topic_id = os.getenv("TOPIC_ID", "")
service_account_file = os.getenv("SERVICE_ACCOUNT_FILE", "")
credentials = service_account.Credentials.from_service_account_info(json.loads(service_account_file))
publisher = pubsub_v1.PublisherClient(credentials=credentials)
topic_path = publisher.topic_path(project_id, topic_id)

publisher.publish(topic_path, b"Hello World")

JWTを使って認証

google.auth.jwt.Credentialsを使って認証する方法です。
先ほどのgoogle.oauth2.service_account moduleの認証でもJWTを内部で扱っているのですが、アクセストークンの扱い方がそれぞれ違うみたいです。簡単にまとめると

使用ライブラリ 認証方法
google.oauth2.service_account module JWTを用いてoauth2.0のアクセストークンを獲得して、GCPの認証はoauth2.0のアクセストークンをBearerトークンとして行う
google.auth.jwt.Credentials JWTで生成したBearerトークンを使ってGCPの認証を行う

のような違いがあるみたいです。

from google.cloud import pubsub_v1
from google.auth import jwt
import json
import os
project_id = os.getenv("PROJECT_ID", "")
topic_id = os.getenv("TOPIC_ID", "")
service_account_file = os.getenv("GOOGLE_APPLICATION_CREDENTIALS", "")
publisher_audience = "https://pubsub.googleapis.com/google.pubsub.v1.Publisher"
credentials =  jwt.Credentials.from_service_account_info(json.loads(service_account_file), audience=publisher_audience)
publisher = pubsub_v1.PublisherClient(credentials=credentials)
topic_path = publisher.topic_path(project_id, topic_id)

publisher.publish(topic_path, b"Hello World")

ユーザーアカウントを用いて認証

環境変数を用いて認証

サービスアカウントを用いた方法と変わらないです。GOOGLE_APPLICATION_CREDENTIALSに使いたいユーザーアカウントのファイルパスを指定します。

ファイルパスを指定して認証

google.oauth2.credentials moduleを使って認証します。

from google.cloud import pubsub_v1
from google.oauth2 import credentials 
import os
project_id = os.getenv("PROJECT_ID", "")
topic_id = os.getenv("TOPIC_ID", "")
user_account_path = os.getenv("GOOGLE_APPLICATION_CREDENTIALS", "")
credentials = credentials.Credentials.from_authorized_user_file(user_account_path)
publisher = pubsub_v1.PublisherClient(credentials=credentials)
topic_path = publisher.topic_path(project_id, topic_id)

publisher.publish(topic_path, b"Hello World")

データを直接読み込んで認証

こちらも同じようにgoogle.oauth2.credentials moduleを使います。

from google.cloud import pubsub_v1
from google.oauth2 import credentials 
import json
import os

project_id = os.getenv("PROJECT_ID", "")
topic_id = os.getenv("TOPIC_ID", "")
user_account_file = os.getenv("GOOGLE_APPLICATION_CREDENTIALS", "")
credentials = credentials.Credentials.from_authorized_user_info(json.loads(user_account_file))
publisher = pubsub_v1.PublisherClient(credentials=credentials)
topic_path = publisher.topic_path(project_id, topic_id)

publisher.publish(topic_path, b"Hello World")

まとめ

google-authを眺めていると、まだ挙げていない認証方法もあるのかなと思います。最近は色々なクラウド環境を使う機会が増えてきたので、どのような認証方法が提供されているのか把握できてるとスムーズに開発が行えると思います。
とりあえずGCPの認証に関してはある程度理解できたので今後困ることはなさそうです。

Buildkitを使ってDockerfile以外からビルドする

ZOZOテクノロジーズ #4 Advent Calendar 2019 15日目の記事です。 今回はBuildkitを使ってDockerfile以外のsyntaxからImageをBuildしてみたいと思います。

Buildkitとは

BuildkitはMobyが開発しているOSSで、docker buildをより高速にセキュアにbuildできるツールらしいです。Docker18.09に正式統合され、DOCKER_BUILDKIT=1を指定すればBuildkitが有効になります。

github.com

実装の方針

 Buildkitを使ってDockerfile以外からbuildするのですが、今回はyamlファイルからbuildできるようにしたいと思います。buildkitはbuildする際にLLBというDAG構造の中間言語に変換するのでdockerfile以外の記述方法に差し替えることが可能になっています。
 実装方法としてはbuildkitを使うことで有効になるsyntaxフォーマットを利用することで実装できます。syntaxフォーマットを利用するとdocker buildする際に対応したdocker imageをDocker Hubからもってきて、grpc経由でbuildkitdとやり取りしてBuildするらしいです。なのでDocker Hubのアカウントが必要です。
では、これからDockerfileの代替としてSamplefileというものを作っていきます。

実装

コードの実装

 Buildkitリポジトリに書かれているREADMEのExploring LLBの項目でBuildpacksGockerfileを実装している人が紹介されていたので先人の人たちのコードを参考にしてかなり雑に実装したコードが下記のリポジトリになります。cmd/main.goがgrpc関連でbuild.goYamlファイルをparseしてLLBに変換しているコードです。

github.com

buildしてDocker Hubにimageをpushする

 下記のようにDocker HubにpushするためのDockerfileを作成します。

FROM golang:1.12.0 AS builder

WORKDIR /build
COPY . ./
RUN CGO_ENABLED=0 go build -o /sample ./cmd/

FROM scratch
COPY --from=builder /sample /bin/sample
ENTRYPOINT ["/bin/sample"]

次にbuildなのですが、気をつけることとしては、syntaxフォーマットで指定した名前と同じになるようにbuild tagをつけます。 私のDocker Hubのアカウント名はjon20なので下記のコマンドでbuildします。

docker build . -t  jon20/samplefile

buildできたらDocker Hubにpushします。

docker push jon20/samplefile

確認

 下記のようにSamplefile.yamlを適当なディレクトリに作成します。

#syntax=jon20/samplefile
image: hello-world:latest

そしてdockerのbuildコマンドを実行します。

docker build -f Samplefile.yaml -t jon20/sample .

成功すれば下記のようにImageが作成されたことが確認できました。 f:id:jon20:20191214074017p:plain

まとめ

Dockerfile以外から無事buildすることができました。 正直な所、部分部分しか理解できておらず、説明不足な所が多いですがBuildkitに興味を持っていただければ嬉しいです。

Vue.jsでgoogle mapを利用する

ゆるっと Advent Calendar 2019 2日目です。
今回はVue.jsでgoogle mapを使う際によく使いそうなところを紹介できたらと思います。

下準備

今回使用するライブラリは以下になります。

www.npmjs.com

yarn add vue2-google-maps

で追加できます。

index.jsを以下のように書き換えます。google mapのapiキーの取得に関しては割愛させていただきます。

import Vue from 'vue'
import App from './App.vue'
import * as VueGoogleMaps from 'vue2-google-maps'

Vue.use(VueGoogleMaps, {
  load: {
    key: process.env.VUE_APP_GOOGLE_MAP_API,
    libraries: 'places',
    region: 'JP',
    language: 'ja'
  }
})
Vue.config.productionTip = false

new Vue({
  render: h => h(App),
}).$mount('#app')

google mapの表示

Quickstartに書いてるまんまのコードです。GmapMapコンポーネントを利用します。

<template>
  <div>
    <GmapMap
      :center="{lat:10, lng:10}"
      :zoom="7"
      map-type-id="terrain"
      style="width: 500px; height: 300px"
    >
      <GmapMarker
        :key="index"
        v-for="(m, index) in markers"
        :position="m.position"
        :clickable="true"
        :draggable="true"
        @click="center=m.position"
      />
    </GmapMap>
  </div>
</template>

f:id:jon20:20191201222304p:plain

google mapの設定

optionsプロパティにMapOptionsを渡せます。 例としてStreetViewの表示を消します。

<GmapMap
      :center="{lat:10, lng:10}"
      :zoom="7"
      :options="{streetViewControl: false}"
      map-type-id="terrain"
      style="width: 500px; height: 300px"
    >
      <GmapMarker
        :key="index"
        v-for="(m, index) in markers"
        :position="m.position"
        :clickable="true"
        :draggable="true"
        @click="center=m.position"
      />
    </GmapMap>

StreetViewのアイコンが消えているのが確認できました。 f:id:jon20:20191201222344p:plain

イベントの発火

全部試していませんが、公式に記載されているイベントトリガを使えます。 試しにマップのドラッグが終了した際にメソッドを実行してみます。

<template>
  <div>
    <GmapMap
      :center="{lat:10, lng:10}"
      :zoom="7"
      :options="{streetViewControl: false}"
      map-type-id="terrain"
      style="width: 500px; height: 300px"
      @dragend="onDragEnd"
    >
      <GmapMarker
        :key="index"
        v-for="(m, index) in markers"
        :position="m.position"
        :clickable="true"
        :draggable="true"
        @click="center=m.position"
      />
    </GmapMap>
  </div>
</template>

<script>
export default {
  name: "HelloWorld",
  methods: {
    onDragEnd() {
      console.log("hoge");
    }
  }
};
</script>

表示している領域の座標取得

getBoundsを使います。LatLngBoundsクラスが返されるのでgetCentergetSouthWestで中心座標や南西の座標が取得できます。

マーカの設置

すでにQuickstartでGmapMakerが定義されていますのでこれを流用してpositionを設定すればマーカーが設置できます。dataプロパティにmarkersを定義します。

export default {
  name: "HelloWorld",
  data() {
    return {
      markers: [{ position: { lat: 10, lng: 10 } }]
    };
  },
  methods: {
    onDragEnd() {
      console.log("hoge");
    }
  }
};

マーカーを設置できました。 f:id:jon20:20191202015342p:plain

マーカの上にWindowを表示

GmapInfoWindowコンポーネントを利用します。マーカーをクリックしたらWindowを表示します。 optionsプロパティでInfoWindowOptionsを設定します。今回はpixelOffsetだけ設定します。

<template>
  <div>
    <GmapMap
      :center="{lat:10, lng:10}"
      :zoom="7"
      :options="{streetViewControl: false}"
      map-type-id="terrain"
      style="width: 500px; height: 300px"
      @dragend="onDragEnd"
    >
      <GmapInfoWindow
        :options="infoOptions"
        :position="infoWindowPos"
        :opened="infoWinOpen"
        @closeclick="infoWinOpen=false"
      >
        hoge</GmapInfoWindow>
      <GmapMarker
        :key="index"
        v-for="(m, index) in markers"
        :position="m.position"
        :clickable="true"
        :draggable="true"
        @click="toggleInfoWindow(m)"
      />
    </GmapMap>
  </div>
</template>

<script>
export default {
  name: "HelloWorld",
  data() {
    return {
      markers: [
        { position: { lat: 10, lng: 10 } },
        { position: { lat: 11, lng: 11 } }
      ],
      infoOptions: {
        pixelOffset: {
          width: 0,
          height: -35
        }
      },
      infoWindowPos: null,
      infoWinOpen: false
    };
  },
  methods: {
    onDragEnd() {
      console.log("hoge");
    },
    toggleInfoWindow(marker) {
      this.infoWindowPos = marker.position;
      this.infoWinOpen = true;
    }
  }
};
</script>

hogeと書かれたWindowが表示されました。 f:id:jon20:20191202030713p:plain

まとめ

google mapを使用する際に使いそうな所を紹介しました。
結構細かく設定できるのでドキュメントを色々覗いてみるのも面白いと思います。

メルカリさんのスカラシップを利用してGo Conference 2019 Springに参加してきました

今回、運良くメルカリさんのスカラシップを利用してGo Conference 2019 Springに参加することができました!!

Go Conference 2019 Springでおもしろかったセッションを中心にブログに書き残せたらなと思います。
また、Go Conference 2019 Springの前日にも社内見学やランチなどがあり、その辺りも簡単に触れられればと思います。

参加の経緯

自分は普段、Ruby(Rails)を使うことが多いのですが、個人開発ではGoをちょくちょく使っていて、その開発の際にDBのテストはどう書くの?だったりエラー処理ってどうするのが良いの?かなどの疑問がありました。
たまたまGo Conference 2019 Springのタイムテーブルを見てたら先程の疑問が解決しそうなセッションがあり、行きたいな〜と思ってましたが交通費ツライと思ってて参加を見送ろうと思ってました。しかし、運良くTwitterでメルカリさんのスカラシップの情報が流れてきたのでダメ元で応募してみようと思い応募して参加できました。

前日

ランチ

メルカリさんとのランチ会がありました。が、当日にいきなりGoogleのGoチームも参加することに決まりマジか!ってなりました。GoチームにはKeynoteの方や明日発表する方がいて何を話そうか必死に考えてましたが、ランチ会で飛び交う会話がほとんど英語だったので正直理解しようとするのに必死でした(自分が質問して他の人に訳してもらったところくらいしかちゃんと理解できてない)
GoogleOSSの開発の話だったりGoのGenericsの話だったりエラー処理の話だったりを話してた気がします。
日本での発表が初めてだからとても緊張しているよって言っていたのはちゃんと聞き取れました!

社内勉強会

 ありがたいことにメルカリさんの社内勉強会に参加させていただきました。ガチガチの勉強会ではなくGoについての意見交換の場みたいな雰囲気で、気軽に質問ができました。こういった勉強会は気軽に開催できそうですし、他の社員さんと打ち解ける場にもできそうなのでいいなと思いました。

当日のセッション

Case studies of designing developer friendly libraries

speakerdeck.com

より良いライブラリの開発の仕方についてです。
Goの慣習に従うことや、拡張性を保つこと、コード生成を利用するなどが紹介されています。
特に拡張性を保つために、差し替え可能にする手段としてのFunctional Option Patternは使ったことがないので自分の開発に導入したいです。

エラー設計について/Designing Errors

docs.google.com

エラーの基本的なところから、そのエラーをGoでどう処理するのかを聞くことができました。
エラーは関係者によって求められるエラーが違い

  • (クライアント)アプリケーション -> エラーを識別できる
  • エンドユーザー -> 問題を解決するヒントになるようなメッセージが含まれている
  • 運用者 -> 問題の根本的な原因を調査、解決する情報が含まれている

と異なっている部分が紹介されてました。適切な情報を含んだエラーを返すことは運用する上で大事なことだし、Goに限らずアプリケーションの設計として大事だなと思いました。
また、Goでのエラーの処理としてアプリケーション固有のエラーコードを定義する方法があり、コードの見通しがよくなるしエラーの管理が楽になりそうだなと思いました。

Design considerations for container-based Go applications

speakerdeck.com

Goの実装事例を踏まえたコンテナアプリケーションの設計についてです。

  • Configration
  • Logging
  • Monitoring

の3つの設計方針を聞くことができました。
特にloggingの話で、STDOUT/STDERRに書き出し、JSON形式で構造化することでログの抽出が楽になりそうだなと思いました。

Expand observability in Go

docs.google.com

net/http/pprofruntime/traceの使い方について紹介されていました。
すごい丁寧に解説してくれたのでProfilingやTracingをあまり行ったことがない自分でも理解することができました。
実演でどう使うかを見ることができたのでスライド見ながら後で使ってみようと思います。

おまけ

自作キーボードのお店に寄って来ました。
分離式キーボードいいなってなりました。

まとめ

今回Googleの社員さんと話したり、今後の開発に活かせそうなセッションをたくさん聞くことができたので参加できてよかったです。セッションの内容は全部は理解できてないので後でスライドを見直そうと思います。
次にGo Conferenceのスカラシップがある時に応募に悩んでいる方の参考になればと思います。
最後にGo Conference 2019 Springに参加する機会を設けてくれたメルカリさんありがとうございました!