astamuse Lab

astamuse Labとは、アスタミューゼのエンジニアとデザイナーのブログです。アスタミューゼの事業・サービスを支えている知識と舞台裏の今を発信しています。

Python が Cloud Functionsで使えるようになったので試してみました

f:id:astamuse:20181030211402j:plain

こんにちは、アスタミューゼでデザイナーをしている@YojiShirakiです。最近、細かい自動化したい処理が増えてきて困っておりまして。そんな折に Cloud Functions で Python が使えるようになったと聞いたので早速触ってみようかと。

そこそこちゃんと書いたので、読む時間が無い方はブックマークすることをおススメします。


目次

Cloud Functions とは

昨今のサーバーレスアーキテクチャ思想に呼応する関数をクラウド上で実行できるサービスです。有名どころでは AWS の Lambda、 Microsoft の Azure Functions があります。GCP にも Cloud Functions というサービスがありこれまでは JavaScript のみに対応していました。最近、これの Python 対応がベータリリースされたので練習がてら一つ作ってみます。

今回の練習アプリ

今回は Search Console のデータを Cloud Functions 上で取得して BigQuery に入れる処理を作ります。Search Console は非常に便利ですが、データ保持期間が16カ月であるため年間比較する際にやや不便です。そこで今回、Search Console のデータを引っこ抜いて保存してしまいます。

構成は下図。途中、JSON 鍵の管理などで Cloud KMS、Cron 処理のために GAE + Cloud Pub/Sub が登場するてんこ盛りの内容ですがどうぞお付き合いください。

f:id:astamuse:20181030184822j:plain

前提

今回のコードについては GCP プロジェクトの選択・作成やAPIの有効化は事前に終わっているものとします。また、今回の機能はすべてベータ版なので仕様変更により動作しなくなることもあります。悪しからずご了承ください。また Cloud のサービスの性質上、様々なタスクを走らせた結果、一定の料金が発生します。今回の構成は無茶なことをしなければ大きな金額は発生しませんが、プログラムの実行は自己責任でお願いします。

JSON キー の発行と管理

まずは今回のプロジェクト用に API キー(JSON) を発行します。Cloud Console のAPIとサービスから認証情報を生成しましょう。鍵のタイプはサービスアカウントキーにします。役割は後で編集しますが、一度「BigQueryのデータ編集者」で良いです。取得した JSON は一度ローカルに保存しておいてください。

f:id:astamuse:20181030184851j:plain サービスアカウントキーを選択。

f:id:astamuse:20181030184901j:plain 権限は BigQuery のデータ編集者で(たぶんジョブユーザーでも良い)

権限の追加

鍵を発行したら権限の追加設定を行います。IAMから、今発行した鍵ユーザーの編集画面を開き、役割に「Pub/Subパブリッシャー」を追加してください。これで Pub/Sub に対してメッセージ投げられます。

f:id:astamuse:20181030184940j:plain

Search Console へのアカウント追加

ついでに、データを取得したい Search Console に今回の JSON キー のアカウントを追加しておきます。これによってOAuth 認証を経ずとも Search Console のデータを取得できます。取得した JSON キー の client_mail の値(メールアドレス形式)を Search Console に追加しましょう。権限はフルで良いかと思います。

f:id:astamuse:20181030185154j:plain JSON キー の client_mail の値を確認します。

f:id:astamuse:20181030185223j:plain 先ほどのアドレスを Search Console のユーザーに追加します。これによって OAuth 認証を回避できます。

なお、Search Console の権限ドキュメントはこちらです。

Search Console の権限資料

KMS を利用して JSON 鍵 を暗号化する

さて次は、取得した JSON 鍵 を暗号化します。JSON 鍵 をそのまま git 管理するのは憚られますし、況やコードに埋め込むのも問題です。そこで今回は Cloud KMS を使って暗号化された状態で保持し、必要に応じて復号する流れにします。

KMSの暗号化・復号化については公式の説明がわかりやすいので、一度ご覧ください。

暗号化されたリソースの使用

JSON 鍵 を暗号化するための暗号鍵を作成

まず暗号化の前に、暗号化のための暗号鍵を準備します。この概念についても公式(オブジェクト階層 )に詳しい説明があるのでそちらをご参照ください。

鍵の作成は以下のコマンドでできます。まずkeyring(鍵束)を作成し、その後、key(鍵)を作成します。

# KEY RING の作成(KEYRING-NAME は任意に決めてください)
gcloud kms keyrings create [KEYRING-NAME] --location=global

# 暗号鍵の作成(KEY-NAME は任意に決めてください)
gcloud kms keys create [KEY-NAME] \
  --location=global \
  --keyring=[KEYRING-NAME] \
  --purpose=encryption

これで暗号化に必要な鍵ができました。鍵は KMS 内にあり、私たちは直接触ることはありません。

暗号鍵を用いて JSON 鍵 を暗号化

先ほど作成した暗号鍵(@Cloud KMS)を用いて JSON 鍵 を暗号化します。

gcloud kms encrypt \
  --plaintext-file=secrets.json \
  --ciphertext-file=secrets.json.enc \
  --location=global \
  --keyring=[KEYRING-NAME] \
  --key=[KEY-NAME]

secret.jsonというのが先ほど取得した JSON 鍵 だとお考え下さい。これを KMS の暗号鍵を使って暗号化するという寸法です。そして、このコマンドで作成されたsecret.json.encというバイナリファイルが暗号化された JSON 鍵です。基本はこれをgit管理し必要に応じて復号して利用します。

バイナリファイルを文字に変換する

さて、出来上がったsecret.json.encについてはこのまま利用してもよいのですが、Cloud Function の性質上バイナリでやり取りするのが面倒なので文字列変換しておきます。ターミナルから Python でデコードします。

$ python
> import io
> import base64
> with io.open('secret.json.enc', 'rb') as f:
>     base64.b64encode(f.read()).decode('ascii')

結果は

CiQA6mnLKL032sEfR3rnCEocT8dbZxHAS03RsiAN6uxVHSA......3a5ckdgNQ==

といったようなよく見る文字列が返ってきます。これを今後使っていきます。

Cloud Functions のコード

鍵周りの下準備が整ったので次に Cloud Functions に移ります。

構成

Cloud Functions 側を作るためにまずは3つのファイルを用意します。

project_dir(任意にお作りください)
├ main.py
├ requirements.txt
└ env.yaml

main.py は実際にコードを書くファイルです。requirements.txt にはインストール必要なモジュールを記載します。env.yaml には実行時に与えたい環境変数を定義します。後者2ファイルは以下のようなものです。

google-api-python-client
google-cloud-bigquery

requirements.txt

CIPHERTEXT : CiQA6mnLKL032sEfR3rnCEocT8dbZxHAS03RsiAN6uxVHSA......3a5ckdgNQ==
PROJECT_ID : [YOUR_PROJECT_ID]
LOCATION : [KMS_LOCATION]
KEY_RING : [KMS_KEYRING_NAME}
KEY_NAME : [KMS_KEY_NAME]
DATASET : [BIGQUERY_DATASET_NAME]
TABLE : [BIGQUERY_TABLE_NAME]
SITE_URL : [YOUR_SITE_URL] # 取得したサイトのURL。プロトコルから記載する(例:https://astamuse.co.jp/

env.yaml

CIPHERTEXTには、先ほど KMS で 暗号済の JSON 鍵 を入れます。 Cloud Functions は外部ファイルを持てないのでこういった形にせざるを得ません。

その他PROJECT_IDなどはご自身のプロジェクトのIDなど適宜記入してください。

main.py

main.py はやや長いですが、以下のようなコードです。

def import_gsc(event, context):
    import os
    import json
    import datetime
    import base64
    import logging
    import tempfile

    from google.oauth2 import service_account
    from googleapiclient.discovery import build
    from google.cloud import bigquery

    ## 固定変数
    PROJECT_ID = os.environ['PROJECT_ID']
    LOCATION   = os.environ['LOCATION']
    KEY_RING   = os.environ['KEY_RING']
    KEY_NAME   = os.environ['KEY_NAME']
    DATASET    = os.environ['DATASET']
    TABLE      = os.environ['TABLE']
    SITE_URL   = os.environ['SITE_URL']

    date_format = "%Y-%m-%d"

    ## 日付のジェネレータ(後で使います)
    def date_range(start_date, end_date, delta=datetime.timedelta(days=1)):
        current_date = start_date
        while current_date <= end_date:
            yield current_date
            current_date += delta

    ## 引数取得(実際は引数チェックなど入れます)
    messages   = json.loads(base64.b64decode(event['data']).decode('ascii'))
    start_date = datetime.datetime.strptime(messages['start_date'], date_format)
    end_date   = datetime.datetime.strptime(messages['end_date'], date_format)


    ## KMSからJSONキーの復元
    logging.info("Fetch encrypted key form kms and decrypt it.")
    kms_client   = build('cloudkms', 'v1')
    key_path     = 'projects/{}/locations/{}/keyRings/{}/cryptoKeys/{}'.format(
                        PROJECT_ID, LOCATION, KEY_RING, KEY_NAME)
    crypto_keys  = kms_client.projects().locations().keyRings().cryptoKeys()
    kms_request  = crypto_keys.decrypt(
                          name = key_path
                        , body = {'ciphertext': os.environ['CIPHERTEXT']})
    kms_response = kms_request.execute()
    json_key     = json.loads(base64.b64decode(kms_response['plaintext'].encode('ascii')))


    ## GSCからデータ取得, GBQへインサート
    logging.info("Fetch data from gsc.")
    scope        = ['https://www.googleapis.com/auth/webmasters.readonly']
    credentials  = service_account.Credentials.from_service_account_info(json_key, scopes=scope)
    gsc_client   = build('webmasters', 'v3', credentials=credentials)

    for target_date in date_range(start_date, end_date): # 先ほどのジェネレーターを利用して日付ごとにデータ取得
        target_date_str = target_date.strftime("%Y-%m-%d")
        gsc_request = {
                      'startDate'  : target_date_str
                    , 'endDate'    : target_date_str
                    , 'dimensions' : ["page","query","device"]
                    , 'rowLimit'   : 1000
                }

        gsc_response = gsc_client.searchanalytics().query(
                                siteUrl   = SITE_URL
                                , body    = gsc_request).execute()
        print("Fetched data of {}".format(target_date_str))

        ## GBQへインサート
        output_rows = []

        if 'rows' in gsc_response:
            for row in gsc_response['rows']:
                output_rows.append([
                    row['keys'][0]
                    , row['keys'][1]
                    , row['keys'][2]
                    , row['clicks']
                    , row['impressions']
                    , row['ctr']
                    , row['position']
                    , target_date_str
                    , SITE_URL
                ])

            if len(output_rows) > 0:
                # JSON KEY を一時的にファイルで利用したいのでtempfileで書き出します
                with tempfile.NamedTemporaryFile(mode='w+t', encoding='utf-8') as tf:
                    json.dump(json_key, tf)
                    tf.seek(0)

                    bq_client   = bigquery.Client.from_service_account_json(tf.name, project=PROJECT_ID)
                    dataset_ref = bq_client.dataset(DATASET)
                    table_ref   = dataset_ref.table(TABLE)
                    table_obj   = bq_client.get_table(table_ref)
                    error       = bq_client.insert_rows(table_obj, output_rows) 

                    # TODO : error 内容に応じた処理

この関数は、想定として Pub/Sub から送られてくるメッセージの中にstart_dateend_date`%Y-%m-%dのフォーマットで渡ってくる仕様にしています。その日付に基づいてrange_dateで日付をジェネレートして一日ごとに Search Console からデータ取得し BigQuery にインサートするという流れです。

関数の引数となっているeventcontextは Pub/Sub から送られてくるデータを受ける引数です。この辺りは公式仕様を確認しておくとよいです。

引数チェックや、同一レコードチェック、例外処理などは割愛しているので適宜追加してください。

BigQuery のテーブルスキーマ

BigQuery のテーブルスキーマーも掲載しておきます。先ほどの関数をデプロイする前にテーブルは作っておきましょう。

[
      {
        "name": "page",
        "type": "STRING"
      },
      {
        "name": "words",
        "type": "STRING"
      },
      {
        "name" : "device",
        "type" : "STRING"
      },
      {
        "name": "clicks",
        "type": "FLOAT"
      },
      {
        "name": "impressions",
        "type": "FLOAT"
      },
      {
        "name": "ctr",
        "type": "FLOAT"
      },
      {
        "name": "position",
        "type": "FLOAT"
      },
      {
        "name": "data_date",
        "type": "DATE"
      },
      {
        "name": "site_url",
        "type": "STRING"
      }
]

このスキーマ―については、Search Console から取得するデータのディメンジョンによってはカラムを追加する必要があるかもしれません(countryとか?・・使わないと思うけど)。

function のデプロイ

さて一通り準備できたので関数をデプロイします。デプロイはコマンドから実行します。今回は Pub/Sub からトリガーを引くことができるように下記のコマンドでデプロイします。

gcloud beta functions deploy [YOUR_FUNCTION_NAME] --trigger-resource [YOUR_TOPIC_NAME] --trigger-event google.pubsub.topic.publish --runtime=python37 --env-vars-file env.yaml 

上記コマンドを実行すると、自動的に、実行ディレクトリのmain.py内にある対象関数をデプロイするようになっています。ですので YOUR_FUNCTION_NAMEmain.pyに記述しているデプロイしたい関数名を入れてください(今回の場合はimport_gsc)。YOUR_TOPIC_NAMEは今回利用する Pub/Sub 内のトピック名を指定します。おそらく既存のトピックを使うことにはならないと思うので、このタイミングで任意に決めて問題ありません(例:import_gsc_topic など)。デプロイすると自動で Cloud Pub/Sub にトピックが作成されています(下記のリンク先で確認できます)。

Cloud Pub/Sub トピック一覧

デプロイした関数の実行

先ほどデプロイした関数を実行します。gcloudコマンドには Pub/Sub 経由でトリガーを引くコマンドがあるのでデバッグとしてこちらを利用します。

gcloud beta pubsub topics publish [YOUR_TOPIC_NAME] --message '{"start_date" : "2018-10-01", "end_date" : "2018-10-02"}'

実行ログの確認

実行ログについては Stackdriver から確認できます。利用している module の関係で WARNING が出ていますが気にしないでください。

GAE と Cloud Pub/Sub でスケジューリングする

Cloud Functions で Search Console のデータを BigQuery に格納できるようになったので、これを定期実行します。が、Cloud Function には定期実行する仕組みありません。そこで Google App Engine の Cron 機能を利用します。Pub/Sub にメッセージを送るだけなのでそこまで複雑ではありませんが、面倒だと思う場合は Cloud Function を HTTP トリガーできるようにデプロイして、その HTTPエンドポイントを 外部サービスから叩くという手もありだと思います(誰でも叩けてしまいますが)。

構成がやや複雑に見えますが、Google 中の人も同じ方法を謳ってるので仕方ありません。

Cloud Functions for Firebase でジョブをスケジューリング(cron)する

構成

GAE 側は4つのファイルを用意します。

project_dir(任意)
├ app.yaml
├ cron.yaml
├ main.py
└ requirements.txt

app.yaml

これは GAE に乗せるアプリケーションの設定と環境変数を記載しておきます。KMS の鍵情報や Pub/Sub のトピック情報。また JSON 鍵 の暗号化文字列を持たせています。

runtime: python37
env_variables:
  PROJECT_ID: [YOUR_PROJECT_ID]
  LOCATION: [KMS_LOCATION]
  KEY_RING: [KMS_KEY_RING]
  KEY_NAME: [KMS_KEY_NAME]
  CIPHERTEXT: XXXXXXXXXXXX(JSON 鍵の暗号化済文字列)XXXXXXXXXXXXXXXXXXX...
  PUBSUB_TOPIC: [YOUR_TOPIC_NAME]

cron.yaml

次に cron の設定を記載します。ここで定期的に GAE 上のURLを叩く仕組みにします。

cron:
- description: "daily summary job"
  url: /import_gsc
  schedule: every monday 09:00
  timezone: Asia/Tokyo

timezone 設定は忘れずにね。

requirements.txt

requirement.txt は Cloud Function と同様に必要な外部モジュールを記載します。今回はアプリケーションプレームワークに Bottle を利用しました。Flask でも問題ありません。お好みでどうぞ。

bottle
google-cloud-pubsub
google-api-python-client

main.py

main.py では実行するトリガーのためのURLの処理を記述します。

from google.oauth2 import service_account
from googleapiclient.discovery import build
from google.cloud import pubsub
from bottle import Bottle, debug, route, run, default_app, request
from pprint import pprint
import datetime
import os
import json
import base64
import logging

logging.getLogger().setLevel( logging.DEBUG )

@route('/')
def index():
    return "Hi, Georgie! Aren't you gonna say hello?"


@route('/import_gsc')
def import_gsc():

    ## アクセス制御 
    if request.headers.get('X-Appengine-Cron') != 'true':
        logging.error('Invalid access.')
        return 'Error'
    
    ## KMSからキーの復元
    logging.info("Fetch encrypted key form kms. And decrypt.")
    kms_client   = build('cloudkms', 'v1')
    key_path     = 'projects/{}/locations/{}/keyRings/{}/cryptoKeys/{}'.format(
                          os.environ['PROJECT_ID']
                        , os.environ['LOCATION']
                        , os.environ['KEY_RING']
                        , os.environ['KEY_NAME']
                    )
    crypto_keys  = kms_client.projects().locations().keyRings().cryptoKeys()
    kms_request  = crypto_keys.decrypt(
                        name = key_path,
                        body = {'ciphertext': os.environ['CIPHERTEXT']})
    response     = kms_request.execute()
    json_key     = json.loads(base64.b64decode(response['plaintext'].encode('ascii')))

    ## PUB/SUBにメッセージを投げる
    logging.info("Send message to pub/sub.")
    
    credentials = service_account.Credentials.from_service_account_info(json_key)
    pub_client  = pubsub.PublisherClient(credentials=credentials)
    topic_path  = 'projects/{}/topics/{}'.format(os.environ['PROJECT_ID'], os.environ['PUBSUB_TOPIC'])

    target_date = datetime.datetime.today() - datetime.timedelta(2)
    target_date = target_date.strftime("%Y-%m-%d")

    logging.info("Target date is {}".format(target_date))
    message     = {"start_date" : target_date, "end_date" : target_date }
    message     = json.dumps(message)
    
    response    = pub_client.publish(topic_path, message.encode('utf-8'))
    pprint(response)

    return 'Success'

app = default_app()

流れとしては、まずアクセス制御。ヘッダーにX-Appengine-Cron:Trueが無いものは弾きます。このヘッダーは GAE の Cron アクセスに付与されるものです。外部からこのヘッダーを持ったアクセスが来た場合が Google 側がそのヘッダーを削除する仕組みになっています(参考:cron.yaml リファレンス )。また Google App Engine の Cron は IP アドレス 0.1.0.1 からのアクセスするらしいのでそれで制御しても良さそうです(未検証)。

アクセス制御が終わったら KMS でJSON 鍵を復号します。その鍵を利用して Pub/Sub にメッセージを送り処理は終了します。データの取得対象日は2日前を指定します。前日だとAPI経由でデータが取得できないことがあるためです。

GAE にデプロイ

コードができたら GAE にデプロイします。cron.yamlもデプロイします。

gcloud app deploy app.yaml cron.yaml

cron を強制実行してテスト

デプロイが完了したら cron を強制実行してテストします。先ほどのデプロイで GAE の Cron Job にジョブが登録されているので、そこから強制実行します。

Google App Engin Cron Job

実行した際のログについては Stackdriver から確認できます。利用している module の関係で WARNING が出ていますが気にしないでください(二回目)。

BigQuery のデータを確認

特にエラーがなければ BigQuery にデータがインサートされていると思います。確認してみましょう。

Big Query (コマンドで確認してもいいです)

特に問題なければ、Pub/Sub 経由で過去分のデータを取得するのもいいかもしれません。先に載せたコマンドを再掲しますので適宜ご利用ください。

gcloud beta pubsub topics publish [YOUR_TOPIC_NAME] --message '{"start_date" : "2018-10-01", "end_date" : "2018-10-02"}'

Data Studio で表示してみる

さて最後に余興として BigQuery のデータを Data Studio で取得してダッシュボードにしてみます。ダッシュボードはいろいろ載せすぎると却ってよく分からなくなるので2,3グラフを載せるのに留めます。 下記のリンクから Data Studio を開いてください。

Google Data Studio

開いたら新しいレポートの開始をします。開いたら右側のデータソースの追加のところから、一番下にある「新しいデータソースを追加」をクリックします。遷移先で BigQuery を選択しウィザードを進んでください。途中、BigQuery のデータセットを選ぶ場面があると思うので、そこで今回作成したデータセットを選択すれば後は自由にデータを抽出できます。

私は自分のダッシュボードとして以下のようなものを作りました。

f:id:astamuse:20181030210745j:plain

全体の検索データを俯瞰しつつ、特に注力したいキーワードを含む検索クエリの数値をグラフ化してます。もう少し本気で作りこめばまぁまぁ使えそうです。 なお、当然ながら BigQuery のデータの取得には料金が発生しますので、誤ってクラウド破産などしないように十分にお気を付けください(自己責任ですお願いします)。

BigQueryで150万円溶かした人の顔

まとめ

ちょっと興味がのってしまいややゴツイ構成になってしまいましたが、サーバーレスアーキテクチャというのは往々にしてこうなるものなのかと思います。ネット上でそこまで情報がなく、手探りだったこともあり土日を潰しましたが非常にいい勉強になりました。もし本稿について何かご質問があれば@YojiShirakiにDMお願いします。

では、本日も最後までお読みいただきありがとうございました。

………あ、そうだ。

例によって当社では一緒にサービス開発してくれるエンジニア・デザイナー・ディレクターを超絶賛募集しております。

カジュアル面談も随時行っておりますので、「ちょっと話聞きたい」という方は、このブログのサイドバー下にあるアドレスか@YojiShirakiにDMいただければと思います。いつでもウェルカムです。採用サイトもありますので下の水色のバナーから是非どうぞ!ではまた:- )

@YojiShirakiの過去記事)

参考

google cloud functions

Cloud KMS

Cloud Pub/Sub

GAE Cron Job

Data Studio

初めてのPXEブート

どうもお久しぶりです、元バンドマンの新米エンジニアgucciです。
最初のブログ投稿からはや4ヶ月も経ってしまいました。
さらに振り返ると、ちょうど1年ほど前にエンジニアになるべくプログラミングスクールに通い始め、
そこからもう1年が経ったのだなぁとしみじみ思います。

さて、そんな私が今回ご紹介するのは「PXEブートサーバを構築しよう!」です。
これは入社してすぐに研修でやったものの一つで、とても勉強になったのでご紹介させていただきます。
私のように文系出身でサーバなどの基礎知識がなかったり、
Linuxのコンソールにあまり触れたことがなかったりするけど、
それでも「エンジニアになろうかしら!」と思っているそんなあなたにうってつけです。
ぜひ、今回ご紹介するPXEブートサーバの構築を試してみてください。
また、途中で登場する用語やコマンドなどの意味を調べながらやっていくと、より自分の身につくと思うのでおすすめです。

PXEブートサーバとは

さて、ここまでおすすめしておいてそもそもPXEとはなんなのかを説明していませんでした。
PXEとは、

Preboot eXecution Environment(PXE、ピー・エックス・イー、ピクシー)は、コンピュータのブート環境のひとつ。インテルの策定したネットワークブートの規格である。ネットワークブートを利用することにより、ストレージをもたないクライアントコンピュータや、ストレージに別のOSが導入されているクライアントコンピュータがサーバ上のOSイメージを使用して起動できる。
(wikipedia先生より)

とのこと。
これを利用することで一体なにができるかと簡単に言うと、CDやUSBからOSをインストールするのではなく、
PCとPCをLANケーブルで接続してネットワーク経由でOSのインストールができる!ということです。
たとえば、CDドライブが壊れてしまった古いパソコンへ新しいOSをインストールしたり、
一度に大量のPCへ接続してOSをインストールしたりすることができるようになります。
PXEを利用してPCのプログラムを起動することができるサーバを「PXEブートサーバ」と言います。

今回はVirtual Boxという仮想マシンを用いてPXEブートサーバを構築しましょう。
HostマシンとTargetマシンはどちらもVirtual Boxで作れるので、わざわざPCを複数用意する必要はありません。
無料ですべて行えるのでご安心を。

一番簡単にPXEを構築できる方法をご紹介するので、色々と足りない設定や説明などあるかもしれませんがご容赦ください。
ではさっそく挑戦してみましょう!

Host機をVirtual Boxで作成

Virtual BoxというのはOracleが開発している仮想化ソフトのことで、使用しているPC上に仮想的なPCを作成し、別のOSをインストール・実行することができます。
こちらは何かと便利なのでPCに入れておいて損はないかと思われます。
今回使用するOSはUbuntuというフリーのLinuxディストリビューションの1つです。
こちらのUbuntu18.04.1LTSを用いてやっていきましょう。

VIrutlal Boxをインストールして新規マシンを作成し、Ubuntuをインストールする方法はたくさんネットに載っているのでそちらをご参考ください。
必要に応じて設定は変更すべきですが、PXEブートサーバをお試しで作ることが目的なので基本的にはデフォルトの設定のまま突き進んで問題ありません。

Ubuntuのインストーラはこちらから、ubuntu-18.04.1-live-server-amd64.isoをダウンロードしてください。

インストールが完了して「Reboot Now」を叩けばVMが再起動してUbuntuが起動してくれます。
それでは、本題のPXEブートサーバを構築していきましょう!

PXEブートサーバとはどういう仕組み?

そもそもPCは起動時にBIOSなどの起動デバイスで設定された順番にデバイスを読み込んでいきます。
基本設定ではHDDが先に読み込まれますが、このBIOSの設定を変更することでCDなどの光学ドライブやUSBから起動させることができるようになります。
BIOSの設定によりPXEから起動することで、ネットワーク起動が実現できるわけです。

しかし、マシンの中にインストーラを用意して接続しただけだと、
どこからそのマシンに接続して、
どうやってファイルを転送をして、
どこにインストーラがあるのか、
マシンにはわかりません。それを設定してあげるのが今回のゴールです。

PXEブートサーバは以下の流れにより実現できます。

f:id:astamuse:20181024084353j:plain

簡単に例えると、

  1. DHCPサーバがどこに接続するのか住所を指定

  2. TFTPサーバがファイルを転送してくれる配達員

  3. HTTPサーバが実際にインストーラが置いてある倉庫

といったイメージがわかりやすいかと思います。
これらのサーバをそれぞれ設定することで、PXEブートサーバが構築できるのです。

PXEブートサーバの構築

それでは、VMにインストールしたUbuntuを使って実際に各サーバを設定してきましょう!

Ubuntuを起動後、まずはパッケージのupdateから。

$ sudo apt update
$ sudo apt upgrade

まず、DHCPサーバをインストールします。

$ sudo apt install isc-dhcp-server

次に、 TFTPサーバをインストールします。

$ sudo apt install tftpd-hpa

そして、HTTPサーバとして使用するapache2をインストールします。

$ sudo apt install apache2

最後に、Ubuntuのインストーラのisoイメージをダウンロードしましょう。
今回は手軽にPXEブートを体験するために、liveCD用installerではなく通常のCD用installerを使います。

$ sudo wget http://cdimage.ubuntu.com/releases/18.04.1/release/ubuntu-18.04.1-server-amd64.iso

これで、必要な材料は揃いました。
ここからは、諸々の設定と適切な配置を行なっていきます。

DHCPの設定を編集していきましょう。

$ sudo vi /etc/dhcp/dhcpd.conf

エディターが開かれると思うので、

option domain-name "example.org";
option domain-name-servers ns1.example.org, ns2.example.org;

をコメントアウトして、 一番下に下記を足します。

subnet 10.0.2.0 netmask 255.255.255.0 {
              range 10.0.2.91  10.0.2.99;
              filename "pxelinux.0";
}

次に、ダウンロードしたisoイメージを適切な場所に配置します。
isoイメージを展開してあげる必要があるので、そのためのディレクトリを作って、

$ sudo mkdir /mnt/ubuntu

展開します。

$ sudo mount -o loop,ro ubuntu-18.04.1-server-amd64.iso /mnt/ubuntu/

この展開したisoイメージの中に、
TFTPサーバに配置するnetbootのファイルと、HTTPサーバに配置するinstallのファイルが入っているので、それらを配置してあげましょう。

TFTPサーバにnetbootのファイルをセット。

$ sudo cp -r /mnt/ubuntu/install/netboot/* /var/lib/tftpboot/

HTTPサーバにinstallのファイルをセット。

$ sudo ln -s /mnt/ubunu /var/www/html/

上はコピーをしていて、下はリンクを貼っています。
こういったコマンドの意味も、ぜひ調べながらやってみてくださいね。

最後に、
PXEブートサーバに静的なIPアドレスを設定します。

$ ifconfig と叩くと、enp0s3に現在設定されているIPアドレスが表示されるかと思います。
このIPアドレスをこちらで指定する静的なIPアドレスに設定してあげます。
どこの住所なのかを予めしっかりと決めてあげるということですね。

$ sudo vi /etc/netplan/50-cloud-init.yaml

エディターが開かれるので、下記のように編集してください。

network:
     ethernets:
             enp0s3:
                        addresses: [10.0.2.7/24]
                        gateway4: 10.0.2.1
                        dhcp4: false
                        nameservers:
                                 addresses: [10.0.2.1]
           version: 2

この設定を反映させるために、

$ sudo netplan apply

と叩いてあげましょう。 もう一度ifconfigを叩くと、10.0.2.7に変わっていることがわかると思います。

3つのサーバを立ち上げましょう!

$ sudo systemctl start tftpd-hpa
$ sudo systemctl start isc-dhcp-server 
$ sudo systemctl start apache2

これで…PXEブートサーバの準備はバッチリです!

TargetマシンとBIOSの設定

PXEブートさせるTargetマシンをVirtual Boxで新規に作成しましょう。
作成したVMのBIOS設定を変更します。
設定ボタンを押し「システム」へ行くと、「起動順序」という項目があると思います。
ここの項目で、ネットワークにチェックを入れて一番上に移動させてください。
この設定により、起動時にネットワークからブートさせることができます。

ネットワークアダプタの変更

作成した2つのVM機の両マシン間で通信を行うために再び設定ボタンを押して「ネットワーク」へ行きましょう。
ここの割り当てを内部ネットワークにどちらのマシンも切り替えます。
名前や高度な設定はデフォルトのままでOKです。

これで…全ての準備が整いました!

Let's PXEブート

それでは、TargetマシンのVMを立ち上げてみましょう。
おねがい!動いて!

f:id:astamuse:20181024103741p:plain

やりました!
インストールを叩いてどんどん進んで行きましょう!
「Configure the network」画面まできたら、設定したnameserverのアドレスを入れます。

f:id:astamuse:20181024104201p:plain

次に、hostnameを求められるので、PXEブートサーバを構築したUbuntuのマシンの名前を入力します。
そして、「Choose a mirror of the Ubuntu archive」の画面では、一番上のenter information manuallyを選択します。
ここで、apache2で立てたHTTPサーバの場所を教えてあげます。
今回は静的なIPアドレスに10.0.2.7を設定したので、

f:id:astamuse:20181024104615p:plain

次の画面では、

f:id:astamuse:20181024104905p:plain

/var/www/html/配下にリンクを貼った/ubuntu/を入れてあげます。
isoイメージをマウントしてあるubuntuというディレクトリのリンクが貼ってあるので、これによりisoイメージを実行できるというわけです。
この次の画面は空っぽのままでEnterを押してください。
次の画面にインストーラが遷移すれば、もう安心です!
インストーラに従ってインストールを進めていけば無事にTargetマシンにUbuntu18.04.1がインストールされます!

最後までインストールが終わりenterを押すと再起動が始まってまたPXEブートが始まってしまうので、
一度マシンをシャットダウンしてBIOSの設定を直しましょう。HDDから読みこむように設定を直して、起動すると…!

イエス!PXEブート成功!おめでとうございます!
お疲れ様でした!

ついでにあと少し

ここまでで今回のゴールには到達しました。 なので、ここからはちょっとした付け足しみたいなものです。

その1

〜PXEブートでUbuntuをインストールしたTargetのマシンをそのまま使いたい場合〜

現在の設定だと、
パッケージをインストールしようとした際、インストール時に指定したPXEブートサーバのhttp://10.0.2.7/ubuntu/を見に行ってしまいます。
$ sudo apt updateをしても失敗してしまうはずです。
そこで、パッケージの参照先を正しく指定し直しましょう。

$ sudo vi /etc/apt/source.list

この中を以下に書き換えてあげましょう。

deb http://jp.archive.ubuntu.com/ubuntu/ bionic main restricted
deb http://jp.archive.ubuntu.com/ubuntu/ bionic-updates main restricted
deb http://jp.archive.ubuntu.com/ubuntu/ bionic universe
deb http://jp.archive.ubuntu.com/ubuntu/ bionic-updates universe
deb http://jp.archive.ubuntu.com/ubuntu/ bionic multiverse
deb http://jp.archive.ubuntu.com/ubuntu/ bionic-updates multiverse
deb http://jp.archive.ubuntu.com/ubuntu/ bionic-backports main restricted universe multiverse

これで、再度$sudo apt updateを行うと無事にパッケージがインストールされるはずです。

その2

〜仮想マシンではなく、実際のマシンに対してPXEブートを行いたい場合〜

最近のノートPCはNIC(LANケーブルを挿す穴)が搭載されていないことが多いですが、
デスクトップであったりNICが搭載されているノートPCと、 同様にTargetのマシンがあれば実マシンでのPXEブートが可能です。
その際、TargetマシンのBIOSの設定とPXEブートサーバを構築したVMマシンの設定を変更する必要があります。
TargetマシンのBIOS画面の起動方法は調べる必要がありますが、一般的にはPC起動時にF2キーを連打するとBIOS画面が起動できます。
BIOS画面が起動できたら、PXE(もしくはNetwork)を一番最初に起動するようにしてあげましょう。
PXEブートサーバを構築したVMマシンの設定は、「ネットワーク」をブリッジアダプターに変更します。
これで、実マシンに対するPXEブートができるはずです。

最後に

いかがだったでしょうか?
PXEブートサーバの構築方法だけでも色々とありますので、他のやり方もネットを参考にしながら試してみると面白いと思います。
また、Ubuntuのversionも変わって行くと各パッケージの仕様も色々と変わって行くので、
トライ&エラーを繰り返しながら動かしてみてください。 長々とお付き合いいただきありがとうございました。 それでは、良きPXEブートライフを!

アスタミューゼでは、エンジニア・デザイナーを募集しています。 ご興味のある方は是非、こちらのバナーから気軽にご応募ください。 お待ちしております!

Vue.jsでAccordionを作ってみるく

こんにちわ。 今日は前回の続き(「絶対フォント感」を身につけようとすると新たな扉が開く話 - その1 - http://lab.astamuse.co.jp/entry/2018/06/06/131745)を書こうと思ってたのですが、 アクセス数がかなり少かった(;;)ので一旦お休みしてフロントエンドっぽい事書いていこうと思います。

概要

vueでやるとAPIやdataリストにタイトルや本文を入れたくなりますが、今回は以下の要望を満たすものを作っていこうと思います。

  • アコーディオンの中身(タイトルや本文)はhtml側で制御したい

それでは早速Vue.jsでAccordionを作ってみるく♡

step.1 下準備(html、css)

今回デザインはこんな感じで作っていきます。

See the Pen vue-accordion - html,css by 35n139e (@35n139e) on CodePen.

step.2 開閉機能を実装する

ひとまずアコーディオンの最低限の要素を実装していきます。

  • ボタンを押す
  • ターゲットコンテンツの開閉(toggle)



<div id="app">
  <js-accordion></js-accordion>
</div>



Vue.component('js-accordion', {
  template: `
  <div class="js-accordion" v-cloak>
    <button class="js-accordion--trigger" type="button" :class="{ '_state-open': isOpened }" @click="accordionToggle()">
      アコーディオン
    </button>
    <div class="js-accordion--target" :class="{ '_state-open': isOpened }" v-if="isOpened">
  <div class="js-accordion--body">
      アコーディオンの中身
  </div>
    </div>
  </div>
  `,
  data() {
    return {
      isOpened: false
    };
  },
  methods: {
    accordionToggle: function(){
      this.isOpened = !this.isOpened;
    },
  }
});

new Vue({
  el: '#app'
});


See the Pen vue-accordion - step2 by 35n139e (@35n139e) on CodePen.

step.3 タイトル・本文をhtml側で制御出来るようにする

冒頭でも書いたように今回のアコーディオンの要望は「アコーディオンの中身(タイトルや本文)はhtml側で制御したい」でしたので、'slot'や'props'を用いてそれを実現していきます。

slotが複数あるので、slotにnameを付けて紐づけしていきます。

<div id="app">
<js-accordion>
  <div slot="title">アコーディオン1</div>
  <div class="js-accordion--body" slot="body">
    <p>アコーディオン1の中身</p>
    <p>アコーディオン1の中身</p>
    <p>アコーディオン1の中身</p>
  </div>
</js-accordion>
Vue.component('js-accordion', {
  template: `
  <div class="js-accordion" v-cloak>
    <button class="js-accordion--trigger" type="button" :class="{ '_state-open': isOpened }" @click="accordionToggle()">
      <slot name="title"></slot>
    </button>
    <div class="js-accordion--target" :class="{ '_state-open': isOpened }" v-if="isOpened">
      <slot name="body"></slot>
    </div>
  </div>
  `
note
「アコーディオンの中身(見た目含め)は色々変わるけど、タイトルの部分は一緒なのでもう少しシンプルにマークアップできないかな?」 といった要望があった場合には、slotではなくpropsを用いて以下のように表現することも可能です。
<div id="app">
<js-accordion title="アコーディオン1">
  <div class="js-accordion--body">
    <p>アコーディオン1の中身</p>
    <p>アコーディオン1の中身</p>
    <p>アコーディオン1の中身</p>
  </div>
</js-accordion>


Vue.component('js-accordion', {
  template: `
  <div class="js-accordion" v-cloak>
    <button class="js-accordion--trigger" type="button" :class="{ '_state-open': isOpened }" @click="accordionToggle()">
      {{this.title}}
    </button>
    <div class="js-accordion--target" :class="{ '_state-open': isOpened }" v-if="isOpened">
      <slot></slot>
    </div>
  </div>
  `
  props: {
    title: { required: true },
  },
ほんの少しですがマークアップダイエットが出来ました。

See the Pen vue-accordion - step3 by 35n139e (@35n139e) on CodePen.


fin アニメーションをつけて完成

最後に開閉時のアニメーションを付けていきましょう

vueでanimationをつけるときは<transition>を使っていきます。

高さの伸縮は、cssだけでも表現可能なのですが制約があるので、今回はvueで開閉後の高さを取得してcssのtransitionでアニメーションさせて、 FadeInOutはCSS アニメーションで表現していきます。

まず、<transition>にイベントタイミングを設定していきます。 (詳しくはこちらhttps://jp.vuejs.org/v2/guide/transitions.html#JavaScript-%E3%83%95%E3%83%83%E3%82%AF

// 閉じた状態の高さは
el.style.height = '0'
// 開いた状態の高さは
el.style.height = el.scrollHeight + 'px';


jqueryのslideDown()ように高速でdomをいじるのではなく、 0px →開いた状態の高さpxをtransition: height 0.4s ease-in-outでアニメーションさせてあげれば表現出来ます。 (勿論jqueryでもこの方法は可能)

.js-accordion{
  &--target{
    transition: height 0.4s ease-in-out;
  }
  // (略)
  &-enter-active{
    animation-duration: 1s;
    animation-fill-mode: both;
    animation-name: js-accordion--anime__opend;
  }
  &-leave-active{
    animation-duration: 1s;
    animation-fill-mode: both;
    animation-name: js-accordion--anime__closed;
  }
}

@keyframes js-accordion--anime__opend {
  0% {
    opacity: 0;
  }
  100% {
    opacity: 1;
   }
}
@keyframes js-accordion--anime__closed {
  0% {
   opacity: 1;
  }

  100% {
    opacity: 0;
  }
}


// (略)
 template: `
(略)
<transition name="js-accordion" @before-enter="beforeEnter" @enter="enter" @before-leave="beforeLeave" @leave="leave">
  <div class="js-accordion--target" :class="{ '_state-open': isOpened }" v-if="isOpened">
    <slot name="body"></slot>
  </div>
</transition>
`,
// (略)
method:{
  beforeEnter: function(el) {
    el.style.height = '0';
  },
  enter: function(el) {
    el.style.height = el.scrollHeight + 'px';
  },
  beforeLeave: function(el) {
    el.style.height = el.scrollHeight + 'px';
  },
  leave: function(el) {
    el.style.height = '0';
}

Vue.jsでAccordion、完成です!

See the Pen vue-accordion - fin by 35n139e (@35n139e) on CodePen.



駆け足でしたが、以上で終わります。

今年はvue周りの書籍が複数出版されて更にVue.jsの普及が進みそうでうれしいですね。

Copyright © astamuse company, ltd. all rights reserved.