もがき系プログラマの日常

もがき系エンジニアの勉強したこと、日常のこと、気になっている技術、備忘録などを紹介するブログです。

株式会社メディアドゥに潜入して、Go言語 初心者向けハンズオンを「少しだけ」受けてきた

はじめに

こんばんは。

Goの勉強しはじめてます。

基本的なところとかを少しづつやってるんですが、今回 Go言語 初心者向けハンズオン #2 に参加させていただきました。

このハンズオン自体 #2で、自分は今回初参加かつ、仕事が忙しくて #1の資料を見れていなかった(言い訳)ので、ちょっと転校生気分で参加しました。

しかもそんな状態なのに、あろうことか仕事の影響で遅れて参加するという更に転校生気分に拍車をかけつつの参加でした。

タイトルに「少しだけ」とつけたのは↑が理由です。

すでに結構進んでいて、ゴルーチンとかテストの説明を少し聞けたくらいでした。

ただ、最後の課題には間に合ったので、試行錯誤の結果を載せておきます。

課題の内容は以下。

  1. じゃんけんゲームを作ろう
  2. 会話ゲームを作ろう

ハンズオンで使用されたスライドに仕様も記載されています。

やってみた

自分の実力では制限時間が短かったので、時間内に出来たのは1だけでした。。

そのかわりテストとかも書いたりして、1は完全に終わらそうと頑張りました。

package main

import (
    "fmt"
    "math/rand"
    "strconv"
    "time"
)

const (
    // GU じゃんけんグー
    GU = iota
    // CHOKI じゃんけんチョキ
    CHOKI
    // PA じゃんけんパー
    PA
)

const (
    // DRAW 引き分け
    DRAW = iota
    // LOSE 負け
    LOSE
    // WIN 勝ち
    WIN
)

// Player ユーザーの勝敗などを持つ構造体
type Player struct {
    winCount  int
    loseCount int
    drawCount int
}

// executeCount 実行回数
const executeCount = 100

func main() {
    player := Player{
        winCount:  0,
        loseCount: 0,
        drawCount: 0,
    }

    for i := 1; i <= executeCount; i++ {
        battle(&player)
    }

    drawResult(player)
}

// battle じゃんけん開始
func battle(player *Player) {
    playerHand := selectHand()
    enemyHand := selectHand()
    switch judge(playerHand, enemyHand) {
    case DRAW:
        player.drawCount++
    case LOSE:
        player.loseCount++
    case WIN:
        player.winCount++
    }
}

// selectHand じゃんけんの手を決定する
func selectHand() int {
    rand.Seed(time.Now().UnixNano())
    return rand.Intn(2)
}

// judge 判定
// @see https://qiita.com/mpyw/items/3ffaac0f1b4a7713c869
func judge(playerHand int, enemyHand int) int {
    return (playerHand - enemyHand + 3) % 3
}

// drawResult 勝敗結果表示
func drawResult(player Player) {
    fmt.Println("勝利=" + strconv.Itoa(player.winCount) + "回")
    fmt.Println("引分=" + strconv.Itoa(player.drawCount) + "回")
    fmt.Println("敗北=" + strconv.Itoa(player.loseCount) + "回")
    fmt.Println("-------------------------------")
    fmt.Println("合計=" + strconv.Itoa(executeCount) + "回")
}

=> $ go run main.go
勝利=28回
引分=44回
敗北=28回
-------------------------------
合計=100

時間制限があったので焦ってしまい、当初 judge() のロジックは完全にif文で泥臭くやってました(笑)

その後テストを書き始める前に、参考サイトを確認し編集しました。

(あー頭が良くなりたい)

以下はテストです。

package main

import (
    "strconv"
    "testing"
)

func TestSelectHand_ありえない値(t *testing.T) {
    hand := selectHand()
    if hand != PA && hand != GU && hand != CHOKI {
        t.Error("selectHand()でとれるはずのない値が取れている => " + strconv.Itoa(hand))
    }
}

func TestJudge_引き分けなのに引き分けではない(t *testing.T) {
    result := judge(0, 0)
    if result != DRAW {
        t.Error("引き分けのはずなのに引き分けではない => " + strconv.Itoa(result))
    }
}

func TestJudge_負けなのに負けではない(t *testing.T) {
    result := judge(1, 0)
    if result != LOSE {
        t.Error("負けのはずなのに負けではない => " + strconv.Itoa(result))
    }
}

func TestJudge_勝ちなのに勝ちではない(t *testing.T) {
    result := judge(2, 0)
    if result != WIN {
        t.Error("勝ちのはずなのに勝ちではない => " + strconv.Itoa(result))
    }
}

func TestBattle(t *testing.T) {
    player := Player{
        winCount:  0,
        loseCount: 0,
        drawCount: 0,
    }
    battle(&player)
    if player.drawCount == 0 && player.winCount == 0 && player.loseCount == 0 {
        t.Error("勝敗結果が登録されていない")
    }
}

=> $ go test -v
=== RUN   TestSelectHand_ありえない値
--- PASS: TestSelectHand_ありえない値 (0.00s)
=== RUN   TestJudge_引き分けなのに引き分けではない
--- PASS: TestJudge_引き分けなのに引き分けではない (0.00s)
=== RUN   TestJudge_負けなのに負けではない
--- PASS: TestJudge_負けなのに負けではない (0.00s)
=== RUN   TestJudge_勝ちなのに勝ちではない
--- PASS: TestJudge_勝ちなのに勝ちではない (0.00s)
=== RUN   TestBattle
--- PASS: TestBattle (0.00s)
PASS
ok      _/path/to/study_01     0.057s

終わりに

スタッフで参加されていた@yukpizさんがQiitaに解答を載せてくれています。

【解答】Go言語初心者向けハンズオン #2

正直2番の方は解答見てもちょっとわからなかったので、もうちょいゴルーチン・チャネルについて勉強しないとなと思いました。

また、懇親会で @yukpizさんとお話させてもらい、エンジニアの登壇を応援する会を教えてもらいました。

slackチームにも参加させていただきました。

自分が運営している 週1でブログ書くslackチームに入ってくれている人たちも何人かいらして、面白かったですw

せっかくslackチームにも入ったので、来年こそは登壇を経験したいと思います。

f:id:kojirooooocks:20181128134525p:plain

ではでは。

アスクルに潜入して、レガシー感謝の日のお話を聞いてきた

はじめに

こんばんは。今回はこちらのイベントに参加してきました。

askul.connpass.com

次の日が勤労感謝の日ということで、日頃レガシーと戦っているお話をいろいろと聞いてきました。

各スライド

レガシーとは何者か

@dskst9さんの発表

レガシーフロントエンド

@Kotanin0さんの発表

レガシーコア 不変の価値

@k_h_sisspさんの発表

レガシーシステム・レガシーソフトと向き合って

@warumonogakariさんの発表

www.slideshare.net

人のレガシーを笑うな

@m_noriiさんの発表

www.slideshare.net

本当にあった最速でコードをレガシー化させる怖い話

@shigeshibu44さんの発表

レガシーインフラと向き合った三年

@k-nishigakiさんの発表

感想

レガシー感謝の日というイベント名だけあって、大体のお話が、レガシーが完全悪じゃないよっていう感じでした。

今までそのプロダクトを支えてくれていたのは、実際動いてお金を生み出してくれているレガシーなコードなので、やはりそこには、敬意と感謝と「愛」を持って接するべきだなと思いました。

そういう意味で、考え方が変わるというか、考え方を変えるキッカケになるようなイベントでした。

レガシーなコードをみて、誰にも当たることが出来ない憤りを抱くよりも、そのコードと向き合ってより良くしてあげようという気持ちが大事だなと。

ただ、@k_h_sissp さんが言っていた、2000くらすのcase文や、30000行のクラスファイルとかと、実際に対面すると、そこまで穏やかな気持ちになれるかは、、、微妙なところですが。。。

あと、今回のイベントはとにかく名言が多いなと思いましたw

メモの殴り書きなので、間違ってるかもしれないですが、一部書いてみます。

  1. レガシーの定義って相対的なもので、未来から見ると、自分が作った瞬間からレガシーになる。
  2. レガシー(遺産)は決して、マイナスだけではなく、プラスもあるしマイナスもある。
  3. 完璧なベストプラクティスが無いのと同じく、完璧なアンチパターンはない。
  4. 現在は環境変化に対応できることが大事。対応するためには進化する必要がある。
  5. レガシーが存在していることで、自分が進化しているということが理解できる。
  6. レガシーシステム・ソフトだけを解決するのではなく、チームを良くすることも大事。
  7. 暗いと不平を言うよりも、進んで灯りをつけましょう。

終わりに

実際にはとても愛のあるイベントでした。

自分的に一番面白かったのは、@m_noriiさんの発表で、特にNULLGWDOCOMOの話は、めちゃめちゃ懐かしくて吹きましたw

あ、あと、アスクルすごいオフィスでした。

今度はEC-Meetupとかもあるみたいなので、また参加したいなと思いました。

ばいばいペッパーくん。

f:id:kojirooooocks:20181123003014j:plain

nginxのログをfluentdでS3へ送る

簡単な話ですが、めっちゃめちゃ久しぶりに触って完全に忘却の彼方だったので、備忘録のため残しておきます。

やってみた

1. td-agentインストール

今回はec2に入れたので、公式にあったコマンドを実行しました。

$ sudo curl -L https://toolbelt.treasuredata.com/sh/install-amazon2-td-agent3.sh | sh

.
..
...
....
.....

インストール:
  td-agent.x86_64 0:3.2.0-0.el2                                                                                                                                                                                                                                                                                               

依存性関連をインストールしました:
  avahi-libs.x86_64 0:0.6.31-19.amzn2    cups-client.x86_64 1:1.6.3-35.amzn2          cups-libs.x86_64 1:1.6.3-35.amzn2                       m4.x86_64 0:1.4.16-10.amzn2.0.2                mailx.x86_64 0:12.5-19.amzn2    ncurses-compat-libs.x86_64 0:6.0-8.20170212.amzn2.1.2    patch.x86_64 0:2.7.1-10.amzn2.0.2   
  spax.x86_64 0:1.5.2-13.amzn2.0.1       system-lsb-core.x86_64 0:4.1-27.amzn2.3.5    system-lsb-submod-security.x86_64 0:4.1-27.amzn2.3.5    util-linux-user.x86_64 0:2.30.2-2.amzn2.0.4   

完了しました!

Installation completed. Happy Logging!

2. 確認

$ td-agent --version
td-agent 1.2.2

一応自動起動も行ってます。

$ sudo systemctl enable td-agent.service
$ sudo systemctl start td-agent.service

3. pluginのインストール

$ sudo /opt/td-agent/embedded/bin/fluent-gem install fluent-plugin-s3
Fetching: fluent-plugin-s3-1.1.7.gem (100%)
Successfully installed fluent-plugin-s3-1.1.7
Parsing documentation for fluent-plugin-s3-1.1.7
Installing ri documentation for fluent-plugin-s3-1.1.7
Done installing documentation for fluent-plugin-s3 after 0 seconds
1 gem installed

4. 設定ファイル記載

公式の設定をほぼほぼ使ってます。

# nginx(access.log)
<source>
  @type    tail
  path     /var/log/nginx/access.log
  format   nginx
  tag      nginx.access
  pos_file /var/log/nginx/access.log.pos
</source>

# nginx(error.log)
<source>
  @type                    tail
  path                     /var/log/nginx/error.log
  tag                      nginx.error
  pos_file                 /var/log/nginx/error.log.pos
  format                   multiline
  format_firstline         /^\d{4}/\d{2}/\d{2} \d{2}:\d{2}:\d{2} \[\w+\] (?<pid>\d+).(?<tid>\d+): /
  format1                  /^(?<time>\d{4}/\d{2}/\d{2} \d{2}:\d{2}:\d{2}) \[(?<log_level>\w+)\] (?<pid>\d+).(?<tid>\d+): (?<message>.*)/
  multiline_flush_interval 3s
</source>

# upload(S3)
<match nginx.**>
  @type                s3
  aws_key_id           xxxxxxxxxxxxx
  aws_sec_key          zzzzzzzzzzzzzzzzzzz
  s3_bucket            nginx-logs
  s3_region            ap-northeast-1
  path                 ${tag}/
  s3_object_key_format %{path}%{time_slice}_%{index}.%{file_extension}

  <buffer tag,time>
    @type            file
    path             /var/log/td-agent/s3
    timekey          3600
    timekey_wait     10m
    timekey 60
    timekey_wait 60
    chunk_limit_size 256m
  </buffer>
</match>

5. 再起動

$ sudo systemctl restart td-agent

これで完成です。

f:id:kojirooooocks:20181122005337p:plain

f:id:kojirooooocks:20181122005345p:plain

こんな感じでアクセスログ上がってます。

終わりに

めちゃめちゃ久しぶりに触ったので、完全に忘れてました。

S3にあげたログをredshiftで拾ってそれをmetabaseで可視化するのが目標なので、とりあえずはここまでにしておきます。

別々のdocker-composeで作成したコンテナに接続する

はじめに

こんばんは。

ちょっとした小ネタといういうか、調べた事があったのでブログに書き残しておきます。

現在携わっている案件では、docker-composeを使用して、ローカル開発環境(LAMP + KVS)コンテナ群を立ち上げて開発をしています。

そして、それとは別に、別のタスク(FW完全リプレイス案件)も降ってきています。

FW自体がガラッと変わるタスクなので、そもそもレポジトリ自体を分けています。

今までは、検証環境用のテストDB(RDB)にVPN経由で接続して、ビルトインサーバーを立ち上げてリプレイス案件は対応していました。

ただ、もうそれめんどくさいなと思って、ローカル環境作ることにしたのですが、DatabaseとKVS(Redis)は、既存の旧docker開発環境のものを共有して使いたいと思いました。

表側のFWの構成のみ変わるので、インフラ層は変わらないからです。

なんかそんなのないかなーと探していると、

このあたりのやつを使えば行けそうな気がしたので、やってみました。

やってみた

現状立ち上がっている旧docker開発環境のAPサーバと同じネットワークにする事が必要なので、現状立ち上がっているdockerコンテナのネットワーク設定を調べられるものがないかを探してみました。

http://docs.docker.jp/engine/reference/commandline/network_ls.html#network-ls

なんかそれっぽいのがあったので実行してみます。

$ docker network ls
NETWORK ID          NAME                      DRIVER              SCOPE
aaaaaaaaaaaa        bridge                    bridge              local
bbbbbbbbbbbb        host                      host                local
cccccccccccc        none                      null                local
dddddddddddd        xxxxxxx_default           bridge              local

出てきました。

一覧の一番下に、共有したい対象の旧docker開発環境のAPサーバの名前がありました。 _defaultっていうsuffixが付いてるのは、なんでだろうと調べてると、なんかそれっぽい一文を見つけました。

https://docs.docker.com/compose/networking/

version: "3"
services:
  web:
    build: .
    ports:
      - "8000:8000"
  db:
    image: postgres
    ports:
      - "8001:5432"

When you run docker-compose up, the following happens:

  1. A network called myapp_default is created.
  2. A container is created using web’s configuration. It joins the network myapp_default under the name web.
  3. A container is created using db’s configuration. It joins the network myapp_default under the name db.

Let's Google翻訳

docker-composeを実行すると、次のようになります。

myapp_defaultというネットワークが作成されます。 コンテナは、Webの構成を使用して作成されます。これは、webという名前でmyapp_defaultというネットワークに参加します。 コンテナは、dbの構成を使用して作成されます。 myapp_defaultという名前のネットワークにdbという名前で参加します。

特に指定しないと、_defaultのsuffixがつくみたいです。たぶん。

というわけで作ってみました。

version: '3'

services:
  replace_app:
    tty: true
    container_name: replace_container
    build:
      context: ./docker_files/
      dockerfile: ./Dockerfile
      args:
        - ENVIRONMENT=local
    ports:
      - 5260:80
    volumes:
      - ./:/var/www/project:cached
    networks:
      - xxxxxxx_default

networks:
  xxxxxxx_default:
    external: true

networksで、対象のコンテナのネットワーク名を指定して、トップレベルのnetworksキーで対象のネットワークの定義をしています。

externalをtrueにすることで、外へのアクセスが可能になり、dockerが別でネットワークの作成をすることがなくなります。

これで、実際に新コンテナから、旧コンテナ群のmysqlに接続することが出来ました。

終わりに

ネットワーク系は全く知識ないんですが、ドキュメント見ればなんとかなりますね。

ローカルで使用するだけのコンテナだから、おかしかったら捨てりゃいいので、心理的ストレスもなくよかったです。

すごいなーと思ってたら、全く同じことをしていた人がいて、世の中狭いなとなりました。

おやすみなさい。

APIGatewayとDynamoDBとSlash CommandsでブログSlackのブログ管理

はじめに

こんにちは。

連続でGo言語頑張っています。

今回は、APIGateway、DynamoDBとSlackのSlashCommandを駆使して、ブログのSlackチームの手動管理をなくす対応を行いました。

やってみた

APIGateway

ブログ登録用のAPIを作成します。

API作成

特に説明すること無いんですが、事前に用意しているLambdaと連携してます。

f:id:kojirooooocks:20181102001533p:plain

ステージは、1こだけです。

f:id:kojirooooocks:20181102001546p:plain

使用量プラン

使用量プランをつくります。

そんなにガンっとユーザーが増えるわけはないと思うんですが、念の為です。

先程作ったステージと紐づけます。

f:id:kojirooooocks:20181102001558p:plain

とりあえず、これでアクセス出来ます。

f:id:kojirooooocks:20181102001611p:plain

次は、ブログを登録するようのパラメータのマッピングテンプレートを登録します。

APIGateWayはこれで終わり。

DynamoDB

テーブルだけ登録しました。

idはstringでslackのUserIDを保存するようにしておきます。

f:id:kojirooooocks:20181102001628p:plain

Lambda

現状実装しているものに加えて、今回作成した、ブログ登録用のlambdaを作成しました。

  1. 水・金・日のリマインダー
  2. ブログの登録

1. 水・金・日のリマインダー

こちらは、すでに作っていたので、処理の流れは基本的には変えてないのですが、tomlファイルで管理していた各ユーザーとRSSフィードURLのマッピングをDynamoDBから取得するように変更しました。

package database

import (
    "github.com/aws/aws-sdk-go/aws"
    "github.com/aws/aws-sdk-go/aws/credentials"
    "github.com/aws/aws-sdk-go/aws/session"
    "github.com/guregu/dynamo"
    config "../config"
)

type WriteBlogEveryWeek struct {
    UserID       string `dynamo:"user_id"`
    UserName     string `dynamo:"user_name"`
    FeedURL      string `dynamo:"feed_url"`
    RequireCount int    `dynamo:"require_count"`
}

/**
 * DynamoDBからデータを全取得する
 */
func FindAll(configData config.ConfigData) []WriteBlogEveryWeek {
    var writeBlogEveryWeek []WriteBlogEveryWeek
    table := getTableObject(configData)
    err := table.Scan().All(&writeBlogEveryWeek)
    if err != nil {
        panic("データの読み込みエラー => " + err.Error())
    }

    return writeBlogEveryWeek
}


/**
 * DynamoDBのテーブルオブジェクトを取得する
 */
func getTableObject(configData config.ConfigData) dynamo.Table {
    credential := credentials.NewStaticCredentials(configData.AWS.AccessKey, configData.AWS.SecretKey, "")
    db := dynamo.New(session.New(), &aws.Config{
        Credentials: credential,
        Region:      aws.String(configData.AWS.Region),
    })

    table := db.Table(configData.AWS.DataBase)

    return table
}

参考サイトがゴロゴロ転がっていたので、そこまで迷うことはありませんでした!

ユーザーデータはDynamoDBからとるようになったので、tomlファイルは、AWSとかSlackの設定だけを管理するようになり、スッキリしました。

2. ブログの登録

前述したとおり、今までは tomlファイルで各メンバーのブログを管理していました。

なので、メンバーが増えるごとに自分がtomlファイルを修正していました。

現在メンバーが20人超えしていて、流石に辛くなったんで、この管理方法をやめるために、各ユーザーで勝手に追加してもらうように対応しました。

使ったのは、 Slash Commands です。

Slash Command -> API Gateway -> Lambda -> DynamoDB登録 という流れです。

こちらの記事をほぼほぼ使わせていただきました。

ありがたい・・・

slack.go

package slack

import (
    "errors"
    "net/url"
    "strings"
    config "../config"
)

/**
 * Slackからのパラメータ格納構造体
 */
type SlackParams struct {
    Token    string
    UserID   string
    UserName string
    Text     string
}

/**
 * Slackから送られたパラメータをパースする
 */
func ParseSlackParams(rawParams interface{}) (result *SlackParams, err error) {
    result = &SlackParams{}

    tmp := rawParams.(map[string]interface{})
    if _, ok := tmp["body"]; !ok {
        err = errors.New("params body does not exists")
        return
    }
    rawQueryString := tmp["body"].(string)
    parsed, err := url.QueryUnescape(rawQueryString)
    if err != nil {
        err = errors.New("params body unescape failed. body: " + rawQueryString)
        return
    }
    params, err := url.ParseQuery(parsed)
    if err != nil {
        err = errors.New("params body parse failed. body: " + rawQueryString)
        return
    }

    result.Token = params["token"][0]
    result.UserID = params["user_id"][0]
    result.UserName = params["user_name"][0]
    result.Text = params["text"][0]

    // Slash CommandでURL形式を送ると <URL>という形式になるので、先頭と末尾をtrimする
    result.Text = strings.TrimLeft(result.Text, "<")
    result.Text = strings.TrimRight(result.Text, ">")
 
    return
}

database.go

package database

import (
    "github.com/aws/aws-sdk-go/aws"
    "github.com/aws/aws-sdk-go/aws/credentials"
    "github.com/aws/aws-sdk-go/aws/session"
    "github.com/guregu/dynamo"

    config "../config"
    slack "../slack"
)

type WriteBlogEveryWeek struct {
    UserID       string `dynamo:"user_id"`
    UserName     string `dynamo:"user_name"`
    FeedURL      string `dynamo:"feed_url"`
    RequireCount int    `dynamo:"require_count"`
}

/**
 * Pkを指定して1件取得
 */
func FindByPK(configData config.ConfigData, pk string) WriteBlogEveryWeek {
    var writeBlogEveryWeek WriteBlogEveryWeek
    table := getTableObject(configData)
    table.Get("user_id", pk).One(&writeBlogEveryWeek)
    return writeBlogEveryWeek
}

/**
 * 新しいユーザーデータを作成する
 */
func CreateUser(configData config.ConfigData, slackParams *slack.SlackParams) {
    var writeBlogEveryWeek WriteBlogEveryWeek
    writeBlogEveryWeek.UserID = slackParams.UserID
    writeBlogEveryWeek.UserName = slackParams.UserName
    writeBlogEveryWeek.FeedURL = slackParams.Text
    writeBlogEveryWeek.RequireCount = 1
    table := getTableObject(configData)
    err := table.Put(writeBlogEveryWeek).Run()
    if err != nil {
        panic("登録エラー => " + err.Error())
    }
}

/**
 * DynamoDBのテーブルオブジェクトを取得する
 */
func getTableObject(configData config.ConfigData) dynamo.Table {
    credential := credentials.NewStaticCredentials(configData.AWS.AccessKey, configData.AWS.SecretKey, "")
    db := dynamo.New(session.New(), &aws.Config{
        Credentials: credential,
        Region:      aws.String(configData.AWS.Region),
    })

    table := db.Table(configData.AWS.DataBase)

    return table
}

config.go

package config

import "github.com/BurntSushi/toml"

type ConfigData struct {
    Slack Slack
    AWS   AWS
}

type Slack struct {
    SendAPIURL    string
    ChannelName   string
    RegsiterToken string
}

type AWS struct {
    AccessKey string
    SecretKey string
    Region    string
    DataBase  string
}

/**
 * 設定データを取得する
 */
func GetConfigData() ConfigData {
    var configData ConfigData
    _, err := toml.DecodeFile("config.toml", &configData)
    if err != nil {
        panic("tomlファイルを読み込めません")
    }

    return configData
}

main.go

/**
 * ブログの登録ロジックを実行
 */
func blogRegister(_ context.Context, rawParams interface{}) (interface{}, error) {
    configData := config.GetConfigData()
    envToken := os.Getenv("SLACK_TOKEN")
    params, err := slack.ParseSlackParams(rawParams)
    if err != nil {
        return "スラックのパラメータが取得できませんでした。 error: " + err.Error(), nil
    }
    if envToken != params.Token {
        return "トークンの不一致", nil
    }
    userData := database.FindByPK(configData, params.UserID)
    if userData.UserID != "" {
        return "あなたのブログはすでに登録済みです feedURL: " + userData.FeedURL, nil
    }
    database.CreateUser(configData, params)

    return "ブログを登録しました。これからは妥協は許しませんよ。", nil
}

実際に実行するとこんな感じです。

f:id:kojirooooocks:20181102001648g:plain

特にバリデートしてないのは許してください。

アイコンはこちらから一枚使わせていただいています。。。

これで、メンバー登録を自分の手動運用から離す事ができました。

終わりに

開いてる時間でやってたんですが、結構時間かかりました。

Go言語慣れてきたんですが、ポインターとか使ってなくて、全く使いこなせていません。

もうちょっと本格的に触らないとなー。と思っていたら、知り合いからGoの仕事のお誘いが来ました。

受けられるほどの技術はまだないんですが、とりあえず本当にやるとなった想定で、素振りをブンブンしておきます。

write-blog-every-weekの通知ロジックを再度修正

はじめに

こんばんは。

Go勉強中の僕です。

最近例のブログSlackの通知ロジックをPythonからGoに書き換えたのですが、Slack内にいるGoが得意な方からさらに楽なロジックを教わったので、やってみました。

やってみた

前回までは、SlackのChannels.hisotyAPIから指定の日付の範囲で絞って、発言を取得していました。

その発言からRSSアプリの発言のみを後に抜き出していました。

旧コード

package notification

import (
    "bytes"
    "encoding/json"
    "io/ioutil"
    "net/http"
    "net/url"
    "strconv"
    "time"
)

type ChannelHistoriesData struct {
    Ok       bool   `json:"ok"`
    Latest   string `json:"latest"`
    Oldest   string `json:"oldest"`
    Messages []struct {
        Type        string `json:"type"`
        User        string `json:"user,omitempty"`
        Text        string `json:"text"`
        ClientMsgID string `json:"client_msg_id,omitempty"`
        ThreadTs    string `json:"thread_ts,omitempty"`
        Ts          string `json:"ts"`
        Username    string `json:"username,omitempty"`
        Icons       struct {
            Image36 string `json:"image_36"`
            Image48 string `json:"image_48"`
            Image72 string `json:"image_72"`
        } `json:"icons,omitempty"`
    } `json:"messages"`
    HasMore bool `json:"has_more"`
}

/**
 * ChannelHistoryAPIを実行する
 */
func SendChannelHistoryAPI(configData ConfigData, latest time.Time, oldest time.Time) *ChannelHistoriesData {
    // パラメータをURLエンコードしたものを取得
    params := url.Values{}
    params.Set("token", configData.Slack.RequestToken)
    params.Add("channel", configData.Slack.GetChannelID)
    params.Add("latest", strconv.FormatInt(latest.Unix(), 10))
    params.Add("oldest", strconv.FormatInt(oldest.Unix(), 10))
    params.Add("count", "700")
    urlParams := params.Encode()

    // channel history apiを実行する
    response, httpRequestError := http.Get(configData.Slack.GetAPIURL + "?" + urlParams)
    if httpRequestError != nil {
        panic("sendChannelHistoryAPIのリクエストに失敗しました。")
    }
    defer response.Body.Close()

    // responseBodyを最終的に配列へ変換する
    byteData, _ := ioutil.ReadAll(response.Body)
    data := new(ChannelHistoriesData)
    jsonDecodeError := json.Unmarshal(byteData, data)
    if jsonDecodeError != nil {
        panic("JSONのデコードに失敗しました。")
    }

    return data
}

発言をどかっと取得するので、Lambdaから外部へリクエストするのは1回で済んでいたのですが、あくまで「発言を取得する」だけなので、これくらいの件数を取得すれば指定の日付範囲の発言全部取れるな。というどんぶり勘定でした。

今回変更したのは、RSSアプリで登録したFeedURLにリクエストを飛ばして、最新のPostの公開日から今週ブログを書いたかどうかを判断するようにしました。

新コード


package main

import (
    time "time"
    config "./config"
    gofeed "github.com/mmcdole/gofeed"
)

func main() {
    const targetHour int = 15
    const tomlFile string = "config.toml"
    tomlFilePath, _ := filepath.Abs(tomlFile)

    configData := config.GetConfigData(tomlFilePath)
    feedList := FindLatestFeedList(configData)
    targetUserIDList := getTargetUserIDList(feedList, targetHour)
    notification.SendMessage(configData, targetUserIDList)
}


/**
 * 各ユーザーのブログの最新公開日を取得する
 */
func FindLatestFeedList(configData config.ConfigData) map[string]time.Time {
    // 日本時間に合わせる
    locale, _ := time.LoadLocation("Asia/Tokyo")

    results := map[string]time.Time{}
    parser := gofeed.NewParser()
    for i := 0; i < len(configData.MemberData); i++ {
        // 最新フィードの公開日を取得する
        results[configData.MemberData[i].UserID] = getLatestFeedPubDate(configData.MemberData[i].FeedURL, parser, locale)
    }

    return results
}

/**
 * 最新フィードの公開日を取得する
 */
func getLatestFeedPubDate(feedURL string, parser *gofeed.Parser, locale *time.Location) time.Time {
    // フィードを取得
    feed, err := parser.ParseURL(feedURL)
    if err != nil {
        panic("フィードが取得できませんでした。失敗したフィードURL => " + feedURL)
    }

    // 最新日を取得
    published := feed.Items[0].Published
    latest, err := time.ParseInLocation(time.RFC3339, published, locale)
    if err != nil {
        // 取得できない = フォーマットを変えれば取得できる可能性がある
        latest2, err := time.ParseInLocation(time.RFC1123Z, published, locale)
        if err != nil {
            // それでも取得できない場合は、フィードで取得した生データをもらう
            latest = *feed.Items[0].PublishedParsed
        } else {
            latest = latest2
        }
    }

    return latest
}


/**
 * 通知対象のUserIDのリストを取得する
 */
func getTargetUserIDList(feedList map[string]time.Time, targetHour int) []string {
    nowDate := date.GetNowDate(targetHour)
    thisMonday := date.GetThisMonday(nowDate)

    targetUserIDList := []string{}
    for userID, latestPublishDate := range feedList {
        if thisMonday.After(latestPublishDate) {
            // 今週の月曜日がAfterになる = 今週ブログを書いていない
            targetUserIDList = append(targetUserIDList, userID)
        }
    }

    return targetUserIDList
}

登録されているFeedURL全てに対してリクエストを送ってるので、前回よりLambdaの実行時間が増えているのですが、とてもシンプルに仕上がりました。

迷ったこと

1. feedのPublishedがXMLによって変換がうまくいかない場合があった

PublishedはPubDateを保持しているようなんですが、JSTに所定のフォーマットで変換しようとすると、特定のフォーマットに限ってうまくいかない場合がありました。

一定のフォーマットで変換してしまうと、変換できないPublishedは 0001-01-01 00:00:00 +0000 UTC とかになってしまってたので、再度変換してあげる必要がありました。

   // 最新日を取得
    published := feed.Items[0].Published
    latest, err := time.ParseInLocation(time.RFC3339, published, locale)
    if err != nil {
        // 取得できない = フォーマットを変えれば取得できる可能性がある
        latest2, err := time.ParseInLocation(time.RFC1123Z, published, locale)
        if err != nil {
            // それでも取得できない場合は、フィードで取得した生データをもらう
            latest = *feed.Items[0].PublishedParsed
        } else {
            latest = latest2
        }
    }

2. lambdaで設定ファイルを読み込ませるには設定ファイルもzipでまとめて上げる必要があった

これは当たり前だよ!

って話なんですが、GOOS=linux GOARCH=amd64 go build -o blog_reminder って感じでバイナリ作って、zipで固めた後lambdaにあげてたんですが、何回やってもTomlファイルロードエラーになって???ってなってました。

ローカルでは問題なかったのになんで?ってなったんですが、そもそもtomlファイルはzipにまとめてないんだから、そりゃ読み込めないよって話でした。

3. map型のループ

keyとvalueが欲しかったので、forでどうやるんだろうと調べてたんですが、range で出来ました。

   for userID, latestPublishDate := range feedList {
        if thisMonday.After(latestPublishDate) {
            // 今週の月曜日がAfterになる = 今週ブログを書いていない
            targetUserIDList = append(targetUserIDList, userID)
        }
    }

終わりに

いや、まじで、すぐに相談できる方がメンバーにいてくれるのってめっちゃ心強いですね。

現状、追加ブログを書いてないユーザーを見分けるロジックが入ってないので、このまま頑張ってGoで実装してみます!!

現場で CircleCIが使えるようになった

何番煎じだという話ですが、ちょっと前に念願かなってCircleCI導入出来たので、ビルド時のキャッシュと自動テストを導入してみました。

今回はメモ程度の投稿ですが残しておきます。

  # キャッシュ作成
  generate_cache:
    machine: true
    steps:
      - checkout
      - restore_cache:
          key: docker--{{ checksum ".circleci/config.yml" }}-{{ checksum "docker-compose.ci.yml" }}-{{ checksum "docker_files/web/Dockerfile" }}
          paths: ~/caches/images.tar
      - run:
          name: Setup Docker
          command: |
            if [ ! -f ~/caches/images.tar ]; then
              docker-compose -f docker-compose.ci.yml build
              mkdir -p ~/caches
              docker save $(docker images | awk 'NR>=2 && ! /^<none>/{print $1}') -o ~/caches/images.tar
            fi
      - save_cache:
          key: docker--{{ checksum ".circleci/config.yml" }}-{{ checksum "docker-compose.ci.yml" }}-{{ checksum "docker_files/web/Dockerfile" }}
          paths: ~/caches/images.tar

  # テスト実行
  test:
    machine: true
    steps:
      - checkout
      - restore_cache:
          key: docker--{{ checksum ".circleci/config.yml" }}-{{ checksum "docker-compose.ci.yml" }}-{{ checksum "docker_files/web/Dockerfile" }}
          paths: ~/caches/images.tar
      - run:
          name: Docker Cache Load
          command: |
            docker load -i ~/caches/images.tar
      - run:
          name: Run Test
          command: |
            docker-compose -f docker-compose.ci.yml run www_web /bin/bash -c '\
              cd /var/www/project && composer update && \
              ENVIRONMENT=ci composer unittest'

workflows:
  version: 2
  build:
    jobs:
      - generate_cache
      - test:
          requires:
            - generate_cache

generate_cacheに関しては、1回目の実行時は4分ほどかかるのですが、それいこうは1分未満で終わるようになりました。

ただ現状、デプロイはJenkinsで自動テストはCircleCIというよくわからない構成になっちゃってるので、CircleCIに合わせていきたいと思います。

この辺も、レガシー感謝の日で色んな人のお話をききたいなー。

メモメモ。