Web系零細企業での出来事

組み込み屋からWeb系エンジニアへ異世界転生を果たした筆者がWeb系技術の話題を中心に書き連ねるブログ

サーバーレスでEvernoteに登録したブックマークをランダムにSlackへ通知するアプリを作る②

 前回はEvernoteAPIを利用して、Bookmarkの一覧を取得するところまでを解説しました。今回は、AWS SAMを使ってサーバーレスなインフラを構築していきます。

参考

www.ketancho.net

AWS CLIの準備

まずはこれがないと始まりません。pipでインストールしましょう。

pip install awscli

AWS CLIの初期設定を行います。 ※予めAWSコンソールからsam-sampleというユーザーを作成し、アクセスキーを取得しておきます。

$ aws configure --profile sam-sample
AWS Access Key ID [None]: A****
AWS Secret Access Key [None]: U****
Default region name [None]: ap-northeast-1
Default output format [None]: json

S3バケットの準備

 SAMでのデプロイまでの流れは以下の通りです。

  1. SAMテンプレートファイルをパッケージ化しS3に保存。
  2. パッケージ化したテンプレートを使用するよう記述したCloudFormationテンプレートファイルを作成。
  3. CloudFormationテンプレートファイルに基づいてデプロイ。

パッケージ化したSAMテンプレートファイルを保存するためのS3バケットをあらかじめ作成しておきます。さきほど作成したsam-sampleユーザーにバケット作成権限を持たせている場合には下記コマンドで作成可能です。バケット名はグローバルでユニークである必要がありますので、被らない名前をつけてください。

aws s3 mb s3://sam-bucket --profile sam-sample

SAMテンプレートの作成

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: Create Lambda function by using AWS SAM.

# 共通設定
Globals:
  Function:
    Runtime: python2.7
    Timeout: 15
    MemorySize: 256

# リソース定義
Resources:
  GetBookmarkLambda:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: 'get-bookmark/lambda_function.zip'
      Handler: handler.handler
      Events:
        Timer:
          Type: Schedule
          Properties:
            Schedule: rate(5 minutes)

AWSTemplateFormatVersionは現在のところ'2010-09-09'しかありませんので、これを指定してください。

TransformではAWS::Serverless-2016-10-31を指定するとyaml形式でテンプレートを記述することができます。元々はjsonで記述するしかなかったのですが、現在ではより可読性の高いyamlで記述するのがおすすめです。

Resourcesで実際に使用するリソースを定義していきます。SAMで使用できるリソースはLambda(AWS::Serverless::Function)、APIGateway(AWS::Serverless::Api)、DynamoDB(AWS::Serverless::SimpleTable)などがあります。今回の構成ではLambdaしか使いません。

Lambda functionの作成

# coding:utf-8
import random
import logging
from HTMLParser import HTMLParser

from evernote.api.client import EvernoteClient
from evernote.edam.notestore.ttypes import NoteFilter, NotesMetadataResultSpec

import settings


class LinkParser(HTMLParser):
    # ノート内のURLを抽出するパーサー
    def __init__(self):
        HTMLParser.__init__(self)
        self.url = ""

    def handle_starttag(self, tag, attrs):
        if tag == "a":  # 開始タグがaであるかどうか判定
            attrs = dict(attrs)  # タプルを辞書に変換する
            if 'href' in attrs:  # キー値(属性名)がhrefであるか判定
                self.url = attrs['href']

    @property
    def link(self):
        return self.url


def handler(event, context):
    # Evernoteクライアントを初期化する(1)
    client = EvernoteClient(token=settings.AUTH_TOKEN, sandbox=False)
    note_store = client.get_note_store()

    # ノートブック一覧の取得(2)
    notebooks = note_store.listNotebooks()

    logger = logging.getLogger()
    logger.setLevel(logging.INFO)

    for notebook in notebooks:
        # bookmarksという名前のノートブックにブックマークを保存している。
        if notebook.name == 'bookmarks':
            # 検索条件設定:指定ノートブックにひも付くノート(3)
            filter = NoteFilter()
            filter.notebookGuid = notebook.guid

            # 検索結果設定:ノートタイトルのみ取得(4)
            resultSpec = NotesMetadataResultSpec()
            resultSpec.includeTitle = True

            # ノートメタデータの検索(5)
            metalist = note_store.findNotesMetadata(filter, 0, 10000, resultSpec)

            parser = LinkParser()

            # ノートGUIDを検索条件にノートデータの取得(6)
            # ランダムに5件のbookmarkを抽出
            for n in random.sample(metalist.notes, 5):
                content = note_store.getNoteContent(n.guid)
                parser.feed(content)
                logger.info('Note title: {}\tLink: {}'.format(n.title, parser.link))
    
    return 'OK!'

前回書いたコードをLambda用に少し書き換えました。NOTEのタイトルとブックマークURLをログに出力するようにしています。

デプロイ

今回はLambdaでPython外部モジュールのevernoteを使うので、Lambda functionのコードとモジュールそのものを一緒にzipで固めてデプロイする必要があります。 まずは外部モジュールをカレントディレクトリにインストールします。

pip install evernote -t ./

次にzip圧縮

zip -r lambda_function.zip ./

テンプレートをパッケージ化させてさきほど作成したS3バケットにアップロードします。

aws cloudfront package \
       --template-file template.yaml \
       --s3-bucket sam-backet \
       --output-template-file packaged-template.yaml \ 
       --profile sam-sample

このコマンドでtemplate.yamlの内容をパッケージ化し、それをインポートしたpackaged-template.yamlという名称のCloudFormation用テンプレートファイルができあがります。後はこのテンプレートを基にCloudFormationでデプロイします。

aws cloudformation deploy \
       --template-file packaged-template.yaml \
       --stack-name get-bookmark-stack \
       --capabilities CAPABILITY_IAM \
       --profile sam-sample

CloudFormationでは、テンプレートで定義されたひと塊のリソース群をスタックと呼びます。今回は、get-bookmark-stackというスタック名をつけてデプロイしました。あとは自動的にリソースが作製されてデプロイ完了です。5分ごとにLambdaが実行されているログが確認出来れば成功です。

まとめ

今回はLambdaを用いたサーバーレスなインフラ構成をAWS SAMを使ってデプロイするところまで紹介しました。次回はslackへの通知機能を実装していきます。

サーバーレスでEvernoteに登録したブックマークをランダムにSlackへ通知するアプリを作る①

 みなさん、「これ後でしっかり見返そう」と思ってブックマークまではするけど、それで満足して全然見返さないなんてことありませんか?私はかなりあります。全然見ないブックマークばかりEvernoteにたまっております。

 昔に登録したブックマークを定期的にお知らせしてくれれば、まったく目を通していなかったブックマークを見るのではないかとふと思い立ちました。EvernoteAPIを使えば簡単に実装できそうです。

要件

  • Evernoteに登録されたブックマークを1日1回ランダムに5件取得し、slackの個人チャンネルに投稿する。
  • AWS Lambdaを使いサーバーレス構成で実現する。
  • インフラ構成はSAMを使用してコード管理する。

将来的に実装したいこと

  • 同じブックマークを連日投稿しないように制御する。
  • EvernoteだけでなくPocketのブックマークも取得して通知する。

インフラ構成

f:id:yuki86042239:20180612204906p:plain

 CloudWatchのTimeBasedEventで24時間おきにLambdaを起動し、EvernoteAPIからブックマーク情報を取得し、Slackに通知するというシンプルな構成です。

Evernote APIの調査

 まずはEvernote APIを使ってブックマークを取得できるか試してみましょう。

公式ドキュメント

Documentation - Evernote Developers

codezine.jp

こちらの記事を参考に進めます。

認証

 Evernote Clientの認証を通る方法として、APIキーもしくはデベロッパトークンの2種類があります。APIキーはOAuthでの実装が必要です。今回は、1年間の期限付きではありますが、手軽なデベロッパトークンで進めていきます。

Developer Tokens - Evernote Developers

検証用環境のサンドボックスと本番環境のプロダクショントークンがそれぞれ入手できます。※プロダクショントークンは2018.6.19現在Webページから取得することはできません。サポートに問い合わせ取得してください。

SDKのインストール

 SDKはPython2にのみ対応している(Python3対応はbeta版)ということなので、Python2で進めていきます。インストールはpipで行います。

pip install evernote

Note一覧を取得し、ブックマークしたページのURLを抽出する

 下記は今回実装したコードです。

# coding:utf-8
import random
from HTMLParser import HTMLParser

from evernote.api.client import EvernoteClient
from evernote.edam.notestore.ttypes import NoteFilter, NotesMetadataResultSpec

import settings


class LinkParser(HTMLParser):
    # ノート内のURLを抽出するパーサー
    def __init__(self):
        HTMLParser.__init__(self)
        self.url = ""

    def handle_starttag(self, tag, attrs):
        if tag == "a":  # 開始タグがaであるかどうか判定
            attrs = dict(attrs)  # タプルを辞書に変換する
            if 'href' in attrs:  # キー値(属性名)がhrefであるか判定
                self.url = attrs['href']

    @property
    def link(self):
        return self.url


if __name__ == '__main__':
    # Evernoteクライアントを初期化する(1)
    client = EvernoteClient(token=settings.AUTH_TOKEN, sandbox=False)
    note_store = client.get_note_store()

    # ノートブック一覧の取得(2)
    notebooks = note_store.listNotebooks()

    for notebook in notebooks:
        # bookmarksという名前のノートブックにブックマークを保存している。
        if notebook.name == 'bookmarks':
            # 検索条件設定:指定ノートブックにひも付くノート(3)
            filter = NoteFilter()
            filter.notebookGuid = notebook.guid

            # 検索結果設定:ノートタイトルのみ取得(4)
            resultSpec = NotesMetadataResultSpec()
            resultSpec.includeTitle = True

            # ノートメタデータの検索(5)
            metalist = note_store.findNotesMetadata(filter, 0, 10000, resultSpec)

            parser = LinkParser()

            # ノートGUIDを検索条件にノートデータの取得(6)
            # ランダムに5件のbookmarkを抽出
            for n in random.sample(metalist.notes, 5):
                content = note_store.getNoteContent(n.guid)
                print('\tNote title: ' + n.title)
                parser.feed(content)
                print('\tLink: ' + parser.link)

 EvernoteAPIでのNote一覧の取得は以下のような手順で行います。

  1. 認証トークンを与えてEvernoteClientインスタンスを初期化。そのインスタンスからnote_storeを取得。
  2. note_storeからnotebook一覧を取得。
  3. NoteFilterインスタンスを生成。所望の検索条件を作成する。今回は、bookmarksという名前のnotebookにブックマークを保存していますので、そのnotebook内のNoteをすべて取得することとします。
  4. NotesMetadataResultSpecインスタンスを生成。
  5. NoteFilterインスタンスを与えて、ノートのメタデータリストを取得。
  6. メタデータリストから個別にノートのコンテンツを取得する。

 5.でノートのいったんノートのメタデータを取得するのは、通信量を節約するためです。ノートのコンテンツには添付ファイルなどの大容量データが含まれる場合があるので、それを含めてリストを取得してしまうと莫大な量を通信してしまう可能性があります。コンテンツを含めてリスト取得するメソッドもあるようですが、それは非推奨となっています。

 NoteのコンテンツはHTMLで記述されていますので、HTMLParserを使用してブックマークページのURLを抽出しています。

 上記のコードでEvernote APIを利用してbookmarkを一括で取得し、その中からランダムに5件抽出してタイトルとリンクURLを表示させることができました。

まとめ

 今回は、Evernote APIを利用してNoteの一覧を取得する方法について紹介しました。次回は、このコードをAWS SAMでデプロイしてみたいと思います。

大手電機メーカー組み込みエンジニアから零細Web系企業エンジニアへの転職時にやったこと

 30歳を目前にして大手電機メーカーから零細Web系企業に転職したわけですが、転職後に前職と現職の畑違いっぷりに愕然としました。ある程度覚悟はしていたのですがそれをはるかに凌駕していました。

 今回はその当時の心境とやっていたことなんかをまとめてみました。

入社前に思っていたこと&勉強したこと

同じプログラミングだし大丈夫でしょ。

 組み込み系とWeb系といえどやっぱり同じプログラミングだし、少なからず共通項もあるだろうと思っていました。その期待は入社後に悉く打ち砕かれます。

Python

 弊社ではサーバーサイド言語としてPythonを採用しているので、まず初めにPythonを勉強します。勉強は「みんパイ」こと「みんなのPython」で行いました。

https://www.amazon.co.jp/dp/B01NCOIC2P/ref=dp-kindle-redirect?_encoding=UTF8&btkr=1

 非常にわかりやすく丁寧に書いてあるので、入門書には最適です。ほとんど戸惑うことなく進めることができました。ただ、私は前職でC言語を使っていましたので、オブジェクト指向&動的型付けの言語になじむのは少し時間がかかりました。

Django

 PythonのWebアプリケーションフレームワークとしてDjangoを使っています。みんパイでの学習が終わったらこちらの学習を行います。

さぁ始めましょう | Django documentation | Django

Django公式ドキュメントのチュートリアルです。正直Web系の知識がほとんどない状態でやるのはおすすめしません。一般的なMVCフレームワークを理解していないと、身になりません。

入社後に感じたこと&やったこと

~入社2週間 まず言っていることがわからない

 まず、同僚が話すことがほとんど理解できません。聞いたことないor意味の分からない用語がポンポン出てきます。LXC?SSHvm?アルファベット3文字多すぎやろ・・・。とにかく毎日毎日わからない単語が出てきた場合は、evernoteに書き溜めて次の日までに調べるべきワードとしてストックしていました。これを1か月も続けると大体わかるようになってきます。知識の点と点が線でつながっていく快感はこの時期にしか味わえませんね。

~入社3週間 どこから手を付けていいかわからない。

 わからないことしかないので、どういった順番で知識を習得していけばいいかわからない。とにかく気になったことを片っ端から調べていきます。その時は深く理解できなくても、ほかのことを調べていくうちに知識が補間されていきます。最初のうちは知識の幅を広げることを重視していました。

~入社5週間 実際にWebシステムを作ってみる。

 やっぱり手を動かさないと見つきませんね。しかもDB設計からきっちりやる。流石に先輩に教えられながらじゃないとできませんが、これでWebシステムの全体像をつかむことができました。ログイン機能付きのTodo管理アプリなんかから初めてみるのがおすすめですね。

入社2か月~ プロジェクトを任される

 入社して2か月目から実際のプロジェクト運営を任されました。数百万円規模の比較的小さな案件ではありましたが、要件定義・モデル設計・UI設計・実装などなど全てを担当することができました。全てのことが初めて尽くしでしたが、わからないなりにもやってみれば案外できるものです。といっても先輩からフォローは多分にして頂きました。この最初のプロジェクトをやり切ることで仕事の全体像をつかむことができたので、やはり実践に勝る学習はないと感じた時期でもありました。ただ、やはり初めてのプロジェクトとということもあり、今思い返してみると設計的にまずい部分がたくさんあるのでできれば再設計して作り直したいところではあります。

まとめ

 転職時に感じたことと取り組んだことなんかについて書き連ねました。人によっては耐え難いほどの環境変化にさらされたわけですが、こういう状況が案外性に合っていたのでつらさは全然感じませんでした。むしろ楽しかったですね。自分の性格に感謝です。

大手電機メーカー組み込みエンジニアから零細Web系企業エンジニアへの転身がどの程度劇的か比較してみる

 30歳目前にして大手電機メーカーから零細Web系企業へ転職した私が様々な観点から前職と現職を比較してみます。

企業規模

電機メーカー

  • 資本金 数百億円
  • 売上高 数兆円
  • 従業員数 数万人
  • 創業年数  約100年

Web系企業(入社当時)

  • 資本金 100万円
  • 売上高 1500万円
  • 従業員数 1人(私が従業員第一号です)
  • 創業年数 0年

福利厚生

電機メーカー

ここは流石の大企業。年収換算すると一体いくらになるんでしょう?

Web系企業

必要最低限って感じですね。

年収

電機メーカー

500万円

Web系企業

600万円

額面は上がったけど、福利厚生考慮するとどうなんでしょう? でも創業0年でこれだけ出してくれるのはかなりありがたいです。普通あり得ないと思います。

残業時間

電機メーカー

0~20時間

Web系企業

0~60時間

ここはあまり変わりませんね。

使用言語

電機メーカー

オブジェクト指向?聞いたことあるけどよくわからん。

Web系企業

え?これ全部使えないとお話しにならないの? これだけじゃ役に立たない?フレームワーク

必要となる技術的知識

電機メーカー

ソフトエンジニアだけどハードもかなり触ります。

Web系企業

  • DB設計
  • インフラ設計
  • Docker
  • Django
  • Angular
  • ネットワーク
  • HTTP
  • SSL
  • Nginx
  • CI
  • AWS
  • etc.

粒度がめちゃくちゃですが、思いつくままに書きました。実質1人でほとんどすべてのことをやらないといけないので、覚えるべきことはまだまだ書ききれないほど無数にあります。しかも覚えたことがどんどん陳腐化します。

統合開発環境

電機メーカー

+エディタとしてMIFESを使っていました。

Web系企業

  • Vidual Studio Code

Vimキーバインドで使ってます。 流石の人気IDEめちゃ快適です。

バージョン管理

電機メーカー

他人が編集中のファイルは触ることができませんので、チェックアウトするときにはチームメンバーに一声かけます。

Web系企業

  • git

バージョン管理といえばgitですよね。

プロジェクト管理

電機メーカー

Web系企業

  • gitlab

自社サーバーにインストールして使ってます。

デバッグ

電機メーカー

仕様書を上からなぞってデバッグしていく。退屈だけどとても大事な作業なので嫌々やっていました。

Web系企業

  • CI による自動テスト

テストコード書くことこそがテストなのだということを知りました。まあ、フロントエンドは手動テストだったりするんですが。

仕事内容

電機メーカー

  • 性能系技術者と打ち合わせしソフト仕様の策定
  • 実装
  • デバッグ
  • デザインレビュー資料の作成
  • 設計変更時には関係者の合意を得るために設計変更書を持ち歩いて一人ひとりに変更内容を説明して押印してもらう。通称スタンプラリーを行う。10名ほど回らないといけないので、これで1日の業務が終わってしまうこともしばしば。

Web系企業

  • 受託開発のプロジェクトマネジメント
  • Webシステムの実装以外のフェーズ(要件定義・設計・デプロイ・運用・保守)
  • 実装は学生アルバイト(10名ほど)に任せているので、学生アルバイトのマネジメント
  • 学生アルバイトの教育
  • インフラ構築

ローンチ後にバグ発生した場合

電機メーカー

  • 全国のサービスマンがお宅訪問し、ソフトウェアの書き換え対応を行う。数十億円規模の損失となる。

Web系企業

まとめ

 今回は自己紹介的なものも兼ねて、私の前職と現職を比較してみました。次回は、転職直後に感じたことや勉強したことなどを紹介します。