asciidocで書いた履歴書をGitLabのCIで自動ビルド

履歴書をMarkdownからAsciidocに書き直したので、この機会にビルドも自動化してみました。方法だけ知りたい方は前半を読み飛ばしてください。

Markdownを使っていた理由

これまでMarkdownで書いていた理由は以下です。

  • Markdownはシンプルな書式でエディタだけあれば書ける
  • Wordは特定の環境でしか編集ができない
  • LaTeXは細かく指定ができるが書式が冗長である

Markdownで感じた課題

一方で、Markdownで感じていた問題は以下です。

  • レイアウトを細かく指定できない
  • Markdownの方言で振り回されることがあった
  • 納得できるPDFビルド環境がない(Pandocもイマイチ)

Asciidocで書いてみて

Markdownより書き方は冗長であるものの、Markdownでは手が届かなかった部分が指定できるので満足しています。

ビルドの環境は以下のDockerイメージで構築しています。

htakeuchi/docker-asciidoctor-jp

履歴書/職務経歴書の書き方は以下の記事を参考にしました。

エンジニアが読みたくなる職務経歴書 – dwango on GitHub

GitLabのCIで自動ビルド

gitlabのCIは .gitlab-ci.ymlで設定を記述します。サンプルが豊富にあったのでLaTeXのサンプルをベースに以下の設定を作成しました。

image: htakeuchi/docker-asciidoctor-jp:latest

build-master:
  stage: build
  script:
    - asciidoctor-pdf -r asciidoctor-pdf-cjk-kai_gen_gothic -a pdf-style=KaiGenGothicJP resume.txt
  artifacts:
    paths:
      - "*.pdf"

GitLabにプッシュするとCIが走ります。右側のBrowseからビルドされたファイルを見ることができます。

ブラウザからビルド結果を確認できます。


たまにPushしてもPendingで止まって5分くらい動かない時がありました。気長に待っているとビルドが終わります。

GitHubでも良かったもののCircle CIの設定が面倒だったので1サービスで完結するGitLabで今回はやってみました。自動化しておくとWindows環境でもブラウザだけあればGitLabのWeb IDEから編集してビルドが出来るので便利だと思います。

asciidoc-py3をFedoraにインストール

Markdownで書いていた履歴書を書き直すためにasciidoc環境をFedoraにインストールしました。asciidocはPython 2系で書かれているらしいので3系の実装を利用していきます。OSの環境は以下です。

$ cat /etc/*release
Fedora release 29 (Twenty Nine)
NAME=Fedora
VERSION="29 (Workstation Edition)"
ID=fedora
...

必要なパッケージをインストールします。

$ sudo dnf install  xmlto libxml

次にasciidocをインストールします。

$ git clone https://github.com/asciidoc/asciidoc-py3
$ autoconf
$ ./configure
checking for a sed that does not truncate output... /usr/bin/sed
checking whether ln -s works... yes
checking for a BSD-compatible install... /usr/bin/install -c
configure: creating ./config.status
config.status: creating Makefile
$ make
Fixing CONF_DIR in asciidoc.py
Fixing CONF_DIR in a2x.py
python3 a2x.py -f manpage doc/asciidoc.1.txt
python3 a2x.py -f manpage doc/a2x.1.txt
$ sudo make install 
Fixing CONF_DIR in asciidoc.py
Fixing CONF_DIR in a2x.py
/usr/bin/install -c -d //usr/local/bin
/usr/bin/install -c asciidoc.py a2x.py //usr/local/bin/
...
(cd //usr/local/bin; ln -sf asciidoc.py asciidoc)
(cd //usr/local/bin; ln -sf a2x.py a2x)

インストールできたか確認します。

$ which asciidoc
/usr/local/bin/asciidoc

参考サイト:

https://github.com/khenriks/mp3fs/issues/21

Pythonのプログラムをマルチプロセスで動かした

データサイエンスをやっているときに、マシンパワーが発揮できていないことに気がつきました。

1コアしかフルで利用されていない

この原因を調べた所、PythonのGIL(Global Interpreter Lock)が原因であることが分かりました。

ライブラリと拡張 FAQ — Python 3.7.3 ドキュメント
用語集 — Python 3.7.3 ドキュメント

このGILを回避するためにマルチプロセス化をしました。以下のサイトが参考になりました。

Python高速化 【multiprocessing】【並列処理】 – Qiita
multiprocessing — プロセスベースの並列処理 — Python 3.7.3 ドキュメント

具体的にはmultiprocessingパッケージを利用してコードを書き直しました。

変更前のソース

import asyncio
import tqdm
import MeCab
import numpy as np
from multiprocessing import Pool

tab_all = []
# 時間がかかる処理を含む関数
def handle(t):
    strs = mecab.parse(t).split('n')
    table = [s.split() for s in strs]
    table = [row[:4] for row in table if len(row) >= 4]
    if len(table) == 0:
        return
    tab = np.array(table)
    tab_all.append(tab[:,[0,3]].tolist())

if __name__ == '__main__':
    mecab = MeCab.Tagger("-Ochasen")
    f = open('jawiki_small.txt').readlines()
    text = [s.strip() for s in f]
    for t in text:
        handle(t)

変更した箇所

if __name__ == '__main__':
    mecab = MeCab.Tagger("-Ochasen")
    f = open('jawiki_small.txt').readlines()
    text = [s.strip() for s in f]
    with Pool(processes=8) as pool:
        pool.map(handle, text)

実行するとCPUがフルで利用されていることが分かります。(メモリが厳しいので増設したほうが良さそう…)

CPUがフルで利用されている

実行時間を比較したところ 1/3 程度まで削減できたことが分かります。

b-old.pyが変更前、b.pyが変更後のプログラム

Pythonの裏側を理解することの大切さを学びました。

SSH接続をSlackへ通知する

SlackへSSH接続があると通知する仕組みを設定したのでメモしておきます。

OpenSSHへ接続すると /etc/ssh/sshrc が実行されることを利用した。以下のファイルを /etc/ssh/sshrc として配置してパーミッションを設定する。

#!/bin/bash

# [how to use]
# 1. put this file as a /etc/ssh/sshrc
# 2. change file permission with "chmod 755 /etc/ssh/sshrc"

PATH=/usr/bin:/bin:/sbin:/usr/sbin
TIME=`LANG=C date "+%Y/%m/%d %X"`
USER=`whoami`
IP=`who | tac | head -n 1 | cut -d'(' -f2 | cut -d')' -f1`
SERVER=`hostname`

SLACK_MESSAGE="`${USER}` loggined `${SERVER}` at `${TIME}` from `${IP}`"
SLACK_WEBHOOK_URL='https://hooks.slack.com/services/XXXXXXXXXXXX'
SLACK_CHANNEL='#alerts'
SLACK_USERNAME='ssh-notice'
SLACK_ICON_EMOJI='fish'

curl -X POST --data-urlencode 'payload={"channel": "'"$SLACK_CHANNEL"'", "username": "'"$SLACK_USERNAME"'", "text": "'"${SLACK_MESSAGE}"'", "icon_emoji": "'":${SLACK_ICON_EMOJI}:"'"}' ${SLACK_WEBHOOK_URL}

動作の様子

WebサーバのログをS3→Lambda→AWS ElasticSearchで解析

S3にログを上げると自動でAmazon Elasticsearch Serviceへ投入するよう自動化に挑戦してみた。以下のページを読めば普通にできる。

Amazon Elasticsearch Service にストリーミングデータをロードする – Amazon Elasticsearch Service

アーキテクチャ

S3 -> Lambda -> Amazon Elasticsearch Service

いずれのサービスともリージョンが同一になるよう注意する。

S3のバケット作成

tfファイルを残すほどでもないのレベルではあるものの、一貫性を保つために残しておく。

provider "aws" {
  region = "ap-northeast-1"
}

resource "aws_s3_bucket" "b" {
  bucket = "log-store-base"
  acl    = "private"

  tags = {
    Name        = "My bucket"
    Environment = "Staging"
  }
}

Elasticsearch Serviceのドメイン作成

インスタンスは月750時間まで無料枠があるt2.micro.elasticsearchを選んだ。無料枠があるストレージはSSD 10GiBを選んだ。IP制限をしている箇所の 0.0.0.0/32 は適宜

provider "aws" {
  region = "ap-northeast-1"
}

variable "domain" {
  default = "reiwa0407"
}

data "aws_region" "current" {}

data "aws_caller_identity" "current" {}

resource "aws_elasticsearch_domain" "es" {
  domain_name = "${var.domain}"
  elasticsearch_version = "6.4"

  cluster_config {
    instance_type = "t2.small.elasticsearch"
  }

  ebs_options {
    ebs_enabled = true
    volume_size = 10
  }

  snapshot_options {
    automated_snapshot_start_hour = 23
  }

  access_policies = <<POLICY
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Action": "es:*",
      "Principal": "*",
      "Effect": "Allow",
      "Resource": "arn:aws:es:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:domain/${var.domain}/*",
      "Condition": {
        "IpAddress": {"aws:SourceIp": ["0.0.0.0/32"]}
      }
    }
  ]
}
POLICY
}

Lambdaの関数作成

GUI操作かtfで設定

プログラムの作成

nginxのログフォーマット(CentOS 7へパッケージ導入したnginxから抽出)

log_format main '$remote_addr - $remote_user [$time_local] "$request" $status $body_bytes_sent "$http_referer" "$http_user_agent" "$http_x_forwarded_for"';

apacheのログフォーマット(CentOS 7へパッケージ導入したapacheから抽出)

LogFormat "%h %l %u %t "%r" %>s %b "%{Referer}i" "%{User-Agent}i"" combined

Pythonのサンプルコード

Amazon Elasticsearch Service にストリーミングデータをロードする – Amazon Elasticsearch Service

Node.jsのサンプルコード

amazon-elasticsearch-lambda-samples/s3_lambda_es.js at master · aws-samples/amazon-elasticsearch-lambda-samples

そのままだとApacheとNginxの両方に対応していないので修正する。また、エラー時にCloudWatchコンソールへメッセージを出力する処理を追加する。

import re
from datetime import datetime as dt

def parser(line):
    json_body = {}

    '''
    Pickup enclosed "item" in double quotes
    '''
    pattern_quote = re.compile(r'("[^"]+")')
    quote_items = pattern_quote.findall(line)

    # remove blank item in list and double-quote in string
    quote_items = [tmp.replace('"', '') for tmp in quote_items]
    if len(quote_items) < 1:
        raise ValueError

    try:
        request_line = quote_items[0].split()
        json_body['method'] = request_line[0].upper()
        json_body['request_uri'] = request_line[1]
        json_body['http_version'] = request_line[2].upper()
        json_body['referer'] = quote_items[1]
        json_body['user_agent'] = quote_items[2]
    except Exception as e:
        print(e)
        print("t", line)
        return {}

    # remove matched item in list
    line = re.sub(pattern_quote, '', line)

    '''
    Pickup item splited by space
    '''
    request_items = [l for l in line.split() if l]

    try:
        json_body['source_ip'] = request_items[0]
        json_body['remote_user'] = request_items[2]
        date_str = request_items[3].replace('[', '')
        date = dt.strptime(date_str, '%d/%b/%Y:%H:%M:%S')
        json_body['time_stamp'] = date.strftime('%Y-%m-%d %H:%M:%S')
        json_body['time_zone'] = request_items[4].replace(']', '')
        json_body['status_code'] = request_items[5]
        json_body['body_bytes'] = request_items[6]
    except Exception as e:
        print(e)
        print("t", line)
        return {}

    '''
    for p,q in json_body.items():
        print(p,q)
    '''

    return json_body


if __name__ == '__main__':
    '''
    res = parser('192.168.0.182 - - [07/Apr/2019:17:35:45 +0900] "GET / HTTP/1.1" 200 612 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.40 Safari/537.36" "-"')
    print(res)
    '''
    import sys
    with open(sys.argv[1]) as f:
        #print(f.read())
        for l in f.read().splitlines():
            r = parser(l)
            for k,v in r.items():
                # print(k, ":", v)
                pass

終わりに

AWS Kinesis Data Firehoseを使うアプローチのほうがよりリアルタイム性が高くなるらしいので、検証してみたいと思います。

Amazon Kinesis Data Firehose とは – Amazon Kinesis Data Firehose