takaya030の備忘録

PHP、Laravel、Docker などの話がメインです

gcloud app deploy : This deployment has too many files

Laravel のプロジェクトを GAE にデプロイしたときに以下のエラーが発生したときの対処手順メモ

ERROR: (gcloud.app.deploy) Error Response: [400] This deployment has too many files. New versions are limited to 10000 files for this app.

エラー原因

Laravel のプロジェクトに Google APIs Client Library for PHP (google/apiclient) を追加したことでプロジェクトのファイル数が 10,000 を超えたため
(GAE は 1 バージョンあたり 10,000 ファイルまでしかアップロードできない)

# before installing google/apiclient
$ cd laravel
$ ls -FR | grep -v / | grep -v "^$" | wc -l
5688

# after installing google/apiclient
$ cd laravel
$ composer require google/apiclient:v2.1.3
$ ls -FR | grep -v / | grep -v "^$" | wc -l
10868

google/apiclient をインストールしただけで 5180 ファイルも増えている

対処方法

調べたところ増加したファイルの大半は google/apiclient-services のファイルだった
これらは Google の各種サービスにアクセスするためのクラス群であるが、ソースを見たところ各サービスは独立していて互いに参照していないように見受けられた
今回は Gmail API のみ使えればよいので、それ以外のファイルは削除した

$ cd laravel
$ mv vendor/google/apiclient-services/src/Google/Service/Gmail /tmp
$ mv vendor/google/apiclient-services/src/Google/Service/Gmail.php /tmp
$ rm -rf vendor/google/apiclient-services/src/Google/Service/*
$ mv /tmp/Gmail vendor/google/apiclient-services/src/Google/Service/
$ mv /tmp/Gmail.php vendor/google/apiclient-services/src/Google/Service/

$ ls -FR | grep -v / | grep -v "^$" | wc -l
5832

再度 "gcloud app deploy" を実行したところデプロイが正常に完了し、GAE で Gmail API が動作していることを確認した

Lumen でお手軽に Google API を使う

以前、Laravel から Google API を使う記事を書きましたが、今回は Lumen からより簡単に使ってみます

検証環境

$ php --version
PHP 5.5.34 (cli) (built: Jun 24 2017 00:40:52)
Copyright (c) 1997-2015 The PHP Group
Zend Engine v2.5.0, Copyright (c) 1998-2015 Zend Technologies

$ php artisan --version
Laravel Framework version Lumen (5.1.7) (Laravel Components 5.1.*)

OAuth クライアント ID、Refresh Token の取得

以下の記事を参考に Google API で使用する Client ID、Access Token を取得する
takaya030.hatenablog.com

Google APIs Client Library for PHP のインストー

前回 oauth2 ライブラリは oriceon/oauth-5-laravel を使用しましたが、今回は google/apiclient を使います

$ composer require google/apiclient:v2.1.3

サンプルコード

.env

.env に以下の内容を追加

# oauth
GOOGLE_CLIENT_ID=Your-Clinet-Id
GOOGLE_CLIENT_SECRET=Your-Clinet-Secret
GOOGLE_REFRESH_TOKEN=Your-Refresh-Token

app/Http/Controllers/GmailController.php

<?php

namespace App\Http\Controllers;

class GmailController extends Controller
{
    // show labels
    public function showLabels()
    {
        $client = new \Google_Client();
        $client->setClientId( env('GOOGLE_CLIENT_ID') );
        $client->setClientSecret( env('GOOGLE_CLIENT_SECRET') );
        $client->setScopes([ "https://mail.google.com/" ]);
        $client->refreshToken( env('GOOGLE_REFRESH_TOKEN') );

        $service = new \Google_Service_Gmail($client);

        // Print the labels in the user's account.
        $user = 'me';
        $results = $service->users_labels->listUsersLabels($user);

        $labels = [];
        if (count($results->getLabels()) == 0) {
            $labels[] = "No labels found.";
        } else {
            $labels[] = "Labels:";
            foreach ($results->getLabels() as $label) {
                $labels[] = sprintf("- %s", $label->getName());
            }
        }

        return view( 'gmail.labels', ['labels' => $labels] );
    }
}

app/Http/routes.php

<?php

/*
|--------------------------------------------------------------------------
| Application Routes
|--------------------------------------------------------------------------
|
| Here is where you can register all of the routes for an application.
| It is a breeze. Simply tell Lumen the URIs it should respond to
| and give it the Closure to call when that URI is requested.
|
*/

$app->get('/gmail/labels', 'GmailController@showLabels');

resources/views/gmail/labels.blade.php

<!doctype html>
<html>
    <head>
        <title>gmail labels</title>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    </head>
    <body>
    @foreach($labels as $label)
        {{ $label }}<br />
    @endforeach
    </body>
</html>

動作確認

テストサーバー起動

$ php artisan serve

WEBブラウザで http://localhost:8000/gmail/labels を開いて下の画像のように表示されれば成功です
f:id:takaya030:20170715001557p:plain

まとめ

oriceon/oauth-5-laravel のときはエンドポイントの url を文字列で直接指定したり、レスポンスの json を自分でパースしなければならなかったりといろいろ手間がかかりましたが、google/apiclient ではそれらをライブラリ側で吸収してくれているので以前より簡潔なコードになっています

Go言語(golang)で Gmail の受信トレイにメールを追加してみる

Gmail API を使うとメール送信無しに Gmail の受信トレイに直接メールを追加することができます。その検証をしたときのメモ。

検証環境

Windows10 Home Edition

C:\>go version
go version go1.8 windows/amd64

Access Token の取得

以下の記事を参考に Gmail API で使用する Access Token を取得する
takaya030.hatenablog.com

oauth2 関連パッケージの取得

C:\>go get golang.org/x/oauth2
C:\>go get golang.org/x/oauth2/google
C:\>go get google.golang.org/api/gmail/v1

ソースコード

insertmail.go

package main

import (
  "fmt"
  "time"
  "log"
  "encoding/base64"

  "golang.org/x/oauth2"
  "golang.org/x/oauth2/google"
  gmail "google.golang.org/api/gmail/v1"
)

func main() {

  config := oauth2.Config{
    ClientID:     "Your Client ID",
    ClientSecret: "Your Client Secret",
    Endpoint:     google.Endpoint,
    RedirectURL:  "urn:ietf:wg:oauth:2.0:oob",  //今回はリダイレクトしないためこれ
    Scopes:       []string{"https://mail.google.com/"}, //必要なスコープを追加
  }

  expiry,_  := time.Parse("2006-01-02", "2017-07-11")
  token := oauth2.Token{
	AccessToken:  "Your Access Token",
	TokenType:    "Bearer",
	RefreshToken: "Your Refresh Token",
	Expiry:       expiry,
  }

  client := config.Client(oauth2.NoContext, &token)

  srv, err := gmail.New(client)
  if err != nil {
    log.Fatalf("Unable to retrieve gmail Client %v", err)
  }

  // email date
  const rfc2822 = "Mon Jan 02 15:04:05 -0700 2006"
  email_date  := time.Now().Format(rfc2822)

  // an email data
  message := gmail.Message{
    Raw: base64.URLEncoding.EncodeToString([]byte("Date: " + email_date + "\r\n" +
         "From: hoge@example.com\r\n" +
         "To: fuga@gmail.com\r\n" +
         "Subject: test mail\r\n" +
         "Content-Type: text/html; charset=UTF-8\r\n" +
         "\r\n" +
         "<html><body>This is a test mail.</body></html>")),
    LabelIds:       []string{"INBOX"},
  }

  // insert message
  m, err := srv.Users.Messages.Insert("me", &message).Do()
  if err != nil {
    log.Fatalf("Unable to insert messages. %v", err)
  }
  fmt.Printf("Finish inserting message. id:%s", m.Id)
}

ビルド

C:\go_src>go build insertmail.go

動作確認

C:\go_src>insertmail.exe
Finish inserting message. id:15d37199b2d9xxxx

Gmail の受信トレイ(INBOX)に以下のメールが追加されていれば成功です
f:id:takaya030:20170712232850p:plain

Go言語(golang)で Gmail API を使う

検証環境

Windows10 Home Edition

C:\>go version
go version go1.8 windows/amd64

Access Token の取得

以下の記事を参考に Gmail API で使用する Access Token を取得する
takaya030.hatenablog.com

oauth2 関連パッケージの取得

C:\>go get golang.org/x/oauth2
C:\>go get golang.org/x/oauth2/google
C:\>go get google.golang.org/api/gmail/v1

ソースコード

gmailtest.go

package main

import (
  "fmt"
  "time"
  "log"

  "golang.org/x/oauth2"
  "golang.org/x/oauth2/google"
  gmail "google.golang.org/api/gmail/v1"
)

func main() {

  config := oauth2.Config{
    ClientID:     "Your Client ID",
    ClientSecret: "Your Client Secret",
    Endpoint:     google.Endpoint,
    RedirectURL:  "urn:ietf:wg:oauth:2.0:oob",	//今回はリダイレクトしないためこれ
    Scopes:       []string{"https://mail.google.com/"}, //必要なスコープを追加
  }

  expiry,_  := time.Parse("2006-01-02", "2017-07-11")
  token := oauth2.Token{
	AccessToken:  "Your Access Token",
	TokenType:    "Bearer",
	RefreshToken: "Your Refresh Token",
	Expiry:       expiry,
  }

  client := config.Client(oauth2.NoContext, &token)

  srv, err := gmail.New(client)
  if err != nil {
    log.Fatalf("Unable to retrieve gmail Client %v", err)
  }

  r, err := srv.Users.Labels.List("me").Do()
  if err != nil {
    log.Fatalf("Unable to get labels. %v", err)
  }

  if (len(r.Labels) > 0) {
    fmt.Print("Labels:\n")
    for _, l := range r.Labels {
      fmt.Printf("- %s\n",  l.Name)
    }
  } else {
    fmt.Print("No label found.")
  }
}

ビルド

C:\go_src>go build gmailtest.go

動作確認

C:\go_src>gmailtest.exe
Labels:
- CATEGORY_PERSONAL
- プライベート
- 旅行
- CATEGORY_SOCIAL
- 領収書
- IMPORTANT
- 仕事
- CATEGORY_UPDATES
- CATEGORY_FORUMS
- CHAT
- SENT
- INBOX
- TRASH
- CATEGORY_PROMOTIONS
- DRAFT
- SPAM
- STARRED
- UNREAD

docker-compose を使って Google App Engine for Go の開発環境を作る

検証環境

Windows10 Home Edition
VirtualBox 5.1.22
Docker version 17.05.0-ce, build 89658be
docker-compose version 1.6.2, build 4d72027

ディレクトリ構成

+---gaego
|   |   docker-compose.yml
|   |   
|   +---data
|   |       Dockerfile
|   |       
|   +---sdk
|           Dockerfile
|            
+---logs
|    
+---www
    |       
    +---app
            app.yaml
            hello.go

各種設定ファイル

gaego/docker-compose.yml

version: "2"
services:
  data:
    build: ./data
    volumes:
      - ../:/data
  sdk:
    build: ./sdk
    volumes_from:
      - data
    ports:
      - "8080:8080"
      - "8000:8000"

gaego/data/Dockerfile

FROM busybox
LABEL maintainer "takaya030"

RUN mkdir -p /data
VOLUME ["/data"]
CMD ["true"]

gaego/sdk/Dockerfile

FROM google/cloud-sdk:alpine
LABEL maintainer "takaya030"

# install go1.6
RUN curl -Lso go1.6.4.linux-amd64.tar.gz https://storage.googleapis.com/golang/go1.6.4.linux-amd64.tar.gz \
	&& tar -C /usr/local -xzf go1.6.4.linux-amd64.tar.gz \
	&& rm go1.6.4.linux-amd64.tar.gz

# install GAE for Go SDK
RUN gcloud components install app-engine-go

# install git
RUN apk add --update --no-cache \
		git

# create docker user
RUN adduser -S -u 1000 -g 50 docker \
	&& echo 'docker:tcuser' | chpasswd

RUN mkdir -p /data/www
VOLUME ["/data"]
WORKDIR /data/www
ENV GOPATH /data/www
ENV PATH $PATH:/usr/local/go/bin:/data/www/bin

USER 1000

CMD ["dev_appserver.py","--host","0.0.0.0","--admin_host","0.0.0.0","./app"]

gaego/www/app/app.yaml

runtime: go
api_version: go1

handlers:
- url: /.*
  script: _go_app

gaego/www/app/hello.go

package hello

import (
    "fmt"
    "net/http"
)

func init() {
    http.HandleFunc("/", handler)
}

func handler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprint(w, "Hello, world!")
}

イメージのビルド

gaego ディレクトリに移動後、以下のコマンドでイメージをビルドします。

$ docker-compose build

動作確認

以下のコマンドで開発用サーバーが起動します。

$ docker-compose up -d

web ブラウザで http://192.168.99.100:8080 にアクセスして "Hello, world!" の文字が表示されれば成功です。

http://192.168.99.100:8000 にアクセスすると管理ページが表示されます。
f:id:takaya030:20170708180943p:plain

Rails5.1 サーバーのソースコード変更後の自動リロードの設定

Ruby on Rails 5.1 の development モードでソースコード変更後、自動でサーバーに反映させる設定について

検証環境

Ruby on Rails 5.1.1

設定内容

config/environments/development.rb を以下のように変更後、rails server を再起動することでソースコードの変更が直ちにサーバーに反映されるようになる

--- config/environments/development.rb.orig     Tue Jul 04 19:45:02 2017
+++ config/environments/development.rb  Thu Jul 06 23:13:35 2017
@@ -50,5 +50,6 @@

   # Use an evented file watcher to asynchronously detect changes in source code,
   # routes, locales, etc. This feature depends on the listen gem.
-  config.file_watcher = ActiveSupport::EventedFileUpdateChecker
+  #config.file_watcher = ActiveSupport::EventedFileUpdateChecker
+  config.file_watcher = ActiveSupport::FileUpdateChecker
 end

Docker と Lumen を使って Google App Engine for PHP の開発環境を作る

はじめに

今まで GAE は Twitter Bot をいくつか動かす程度にしか使っていませんでしたが、こちらの記事を読んで感化され、本格的に使うために開発環境を構築してみました。
cloud-ja.googleblog.com

以前にも Docker で GAE for PHP の環境は作ったことはありましたが、改めて見るとイケてなかったので良い機会なので作り直しました。
takaya030.hatenablog.com


ポイントは以下の二点です。

  • GAE for PHP が対応している PHP の最新バージョンは 5.5.34 (2017年6月現在)
  • PHP の Memcache や Memcached 拡張がインストールされていると SDK のアプリがローカルで動作しないため、サーバーの PHP はソースビルドでインストールする

検証環境

Windows10 Home Edition
VirtualBox 5.1.22
Docker version 17.05.0-ce, build 89658be
docker-compose version 1.6.2, build 4d72027

ディレクトリ構成

+---gae
|   |   docker-compose.yml
|   |   
|   +---composer
|   |       Dockerfile
|   |       
|   +---config
|   |       
|   +---data
|   |       Dockerfile
|   |       
|   +---phpcli
|   |       Dockerfile
|   |       
|   +---sdk
|           Dockerfile
|            
+---logs
|    
+---www

各種設定ファイル

gae/docker-compose.yml

version: "2"
services:
  data:
    build: ./data
    volumes:
      - ../:/data
  composer:
    build: ./composer
    volumes_from:
      - data
  phpcli:
    build: ./phpcli
    volumes_from:
      - data
  sdk:
    build: ./sdk
    volumes:
      - ./config/:/home/docker/.config
    volumes_from:
      - data
    ports:
      - "8080:8080"
      - "8000:8000"

gae/composer/Dockerfile

FROM php:5.5.34
LABEL maintainer "takaya030"

WORKDIR /tmp

RUN apt-get update -y && \
	apt-get install -y git unzip sudo && \
	apt-get clean

# create docker user
RUN useradd -d /home/docker -m -s /bin/bash -u 1000 -g 50 docker

# install composer
RUN curl -sS https://getcomposer.org/installer | php && \
	mv composer.phar /usr/local/bin/composer && \
	composer self-update

ENV COMPOSER_HOME /home/docker/.composer
RUN sudo -u docker composer global require hirak/prestissimo

RUN mkdir -p /data/www
VOLUME ["/data"]
WORKDIR /data/www

USER 1000

ENTRYPOINT ["composer"]
CMD ["--help"]

gae/data/Dockerfile

FROM busybox
LABEL maintainer "takaya030"

RUN mkdir -p /data
VOLUME ["/data"]
CMD ["true"]

gae/phpcli/Dockerfile

主に artisan コマンドを実行するためのイメージです

FROM php:5.5.34
LABEL maintainer "takaya030"

RUN apt-get update && \
  apt-get install -y libfreetype6-dev libjpeg62-turbo-dev libpng12-dev libmcrypt-dev && \
  docker-php-ext-install pdo_mysql mysqli mbstring gd iconv mcrypt && \
  apt-get clean

RUN mkdir -p /data/www
VOLUME ["/data"]
WORKDIR /data/www

ENTRYPOINT ["php"]
CMD ["--version"]

gae/sdk/Dockerfile

FROM google/cloud-sdk:alpine
LABEL maintainer "takaya030"

# build PHP 5.5.34
RUN apk add --update --no-cache --virtual .persistent-deps \
		ca-certificates \
		curl \
		tar \
		libpng \
		libxml2 \
		sudo \
		xz
RUN apk add --no-cache --virtual .build-deps \
	autoconf \
	dpkg-dev dpkg \
	file \
	g++ \
	gcc \
	libc-dev \
	make \
	pcre-dev \
	pkgconf \
	re2c \
	coreutils \
	curl-dev \
	libedit-dev \
	libxml2-dev \
	libpng-dev \
	sqlite-dev \
	libressl \
	libressl-dev \
	zlib-dev \
  && curl -Lso php-5.5.34.tar.gz http://jp2.php.net/get/php-5.5.34.tar.gz/from/this/mirror && \
	tar xvzf php-5.5.34.tar.gz && \
	cd php-5.5.34 && \
	./configure --prefix=/usr/local/php-5.5.34/ \
    --enable-bcmath \
    --enable-calendar \
    --enable-ftp \
    --enable-mbstring \
    --enable-opcache \
    --enable-phar \
    --enable-soap \
    --enable-sockets \
    --enable-zip \
    --disable-fileinfo \
    --disable-flatfile \
    --disable-posix \
    --with-curl \
    --with-gd \
    --with-openssl \
    --without-sqlite3 \
    --without-pdo-sqlite \
    --without-imap \
    --without-kerberos \
    --without-imap-ssl \
    --without-interbase \
    --without-ldap \
    --without-mssql \
    --without-oci8 \
    --without-pgsql \
    --without-pear \
    --with-pdo-mysql=mysqlnd \
    --with-mysqli=mysqlnd \
    --with-mysql=mysqlnd \
	--with-config-file-path=/etc && \
	make && make install && \
	cp ./php.ini-production /etc/php.ini && \
	cd .. && \
	rm -r php-5.5.34 php-5.5.34.tar.gz \
  && apk del .build-deps

# modify /etc/php.ini
RUN sed -i -e "s/;date.timezone *=.*$/date.timezone = Asia\/Tokyo/" /etc/php.ini

# symbolic links
RUN ln -s /usr/local/php-5.5.34/bin/php /usr/local/bin/php && \
	ln -s /usr/local/php-5.5.34/bin/php-cgi /usr/local/bin/php-cgi && \
	ln -s /usr/local/php-5.5.34/bin/php-config /usr/local/bin/php-config && \
	ln -s /usr/local/php-5.5.34/bin/phpize /usr/local/bin/phpize

# install GAE for PHP SDK
RUN gcloud components install app-engine-php

# create docker user
RUN adduser -S -u 1000 -g 50 docker \
	&& echo 'docker:tcuser' | chpasswd

# for saving gcloud config
RUN sudo -u docker gcloud config set core/disable_usage_reporting true && \
    sudo -u docker gcloud config set component_manager/disable_update_check true && \
    sudo -u docker gcloud config set metrics/environment github_docker_image

RUN mkdir -p /data/www
VOLUME ["/data","/home/docker/.config"]
WORKDIR /data/www

USER 1000

CMD ["dev_appserver.py","--php_executable_path=/usr/local/bin/php-cgi","--host","0.0.0.0","--admin_host","0.0.0.0","./"]

イメージのビルド

gae ディレクトリに移動後、以下のコマンドでイメージをビルドします。

$ docker-compose build

Lumen のインストー

イメージビルド後、以下のコマンドで Lumen をインストールします。(www フォルダにインストールされます)
後述する GaeSupportLumen を使用する場合 Lumen 5.1 が必要なため下記の通り "v5.1.4" を指定します。

$ docker-compose run --rm composer create-project laravel/lumen . v5.1.4

GaeSupportLumen のインストー

Lumen インストール後、以下のコマンドで GaeSupportLumen をインストールします。

$ docker-compose run --rm composer require shpasser/gae-support-lumen:~1.0
$ docker-compose run --rm composer require illuminate/mail:~5.0

bootstrap/app.php を下記の通りに変更します。

--- bootstrap\app.php.orig   Mon Aug 03 21:56:04 2015
+++ bootstrap\app.php        Sat Jun 24 10:43:33 2017
@@ -19,7 +19,7 @@
     realpath(__DIR__.'/../')
 );

-// $app->withFacades();
+$app->withFacades();

 // $app->withEloquent();

@@ -80,6 +80,7 @@

 // $app->register(App\Providers\AppServiceProvider::class);
 // $app->register(App\Providers\EventServiceProvider::class);
+$app->register(Shpasser\GaeSupportLumen\GaeSupportServiceProvider::class);

 /*
 |--------------------------------------------------------------------------

以下のコマンドで GAE の各種設定を行います。

$ docker-compose run --rm phpcli artisan gae:setup --config my-gae-project

gcloud の初期設定

以下のコマンドで gcloud の初期設定を行います。この操作は一回実行するだけで OK です

$ docker-compose run --rm sdk gcloud config set core/disable_usage_reporting true
$ docker-compose run --rm sdk gcloud config set component_manager/disable_update_check tru
e
$ docker-compose run --rm sdk gcloud config set metrics/environment github_docker_image

gcloud info で設定内容が確認できます

$ docker-compose run --rm sdk gcloud info
Starting gae_data_1
Google Cloud SDK [162.0.0]

Platform: [Linux, x86_64] ('Linux', 'f857ebb6463e', '4.4.66-boot2docker', '#1 SMP Fri May 5 20:44:25 UTC 2017', 'x86_64', '')
Python Version: [2.7.13 (default, Dec 22 2016, 09:22:15)  [GCC 6.2.1 20160822]]
Python Location: [/usr/bin/python2]
Site Packages: [Disabled]

Installation Root: [/google-cloud-sdk]
Installed Components:
  gsutil: [4.27]
  core: [2017.07.07]
  app-engine-python: [1.9.57]
  gcloud: []
  bq: [2.0.24]
System PATH: [/google-cloud-sdk/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin]
Python PATH: [/google-cloud-sdk/lib/third_party:/google-cloud-sdk/lib:/usr/lib/python27.zip:/usr/lib/python2.7:/usr/lib/python2.7/plat-linux2:/usr/lib/python2.7/lib-tk:/usr/lib/python2.7/lib-old:/usr/lib/python2.7/lib-dynload]
Cloud SDK on PATH: [True]
Kubectl on PATH: [False]

Installation Properties: [/google-cloud-sdk/properties]
User Config Directory: [/home/docker/.config/gcloud]
Active Configuration Name: [default]
Active Configuration Path: [/home/docker/.config/gcloud/configurations/config_default]

Account: [None]
Project: [None]

Current Properties:
  [metrics]
    environment: [github_docker_image]
  [core]
    disable_usage_reporting: [true]
  [component_manager]
    disable_update_check: [true]

Logs Directory: [/home/docker/.config/gcloud/logs]
Last Log File: [/home/docker/.config/gcloud/logs/2017.07.19/13.19.29.094936.log]

git: [git version 2.11.2]
ssh: [OpenSSH_7.4p1, LibreSSL 2.4.4]

動作確認

以下のコマンドで開発用サーバーが起動します。

$ docker-compose up -d

web ブラウザで http://192.168.99.100:8080 にアクセスして以下の画面が表示されれば成功です。
f:id:takaya030:20170624114005p:plain

http://192.168.99.100:8000 にアクセスすると管理ページが表示されます。
f:id:takaya030:20170624114032p:plain

更新履歴

  • (2017/07/19) gclod config のフォルダを volume 指定するように変更。gcloud の初期設定を追加
  • (2017/06/27) composer の hirak/prestissimo プラグインが機能していなかったのを修正