おうち Nomad & Consul クラスタの構築 - Consul 編
はじめに
前回は物理構築までやりました。今回は OS の基本セットアップと Consul のセットアップをしていきます。
OS のインストール
以下を参考に Raspberry Pi に Ubuntu Server をインストールします。今回は Ubuntu LTS 20.04 (Pi 3/4) を選択します。
基本セットアップ
キーボード
JIS 配列のキーボードを使っているので、まずはキーボードの変更をします。
$ sudo dpkg-reconfigure keyboard-configuration
Generic 105-key (Intl) PC を選んで Japanese を選びます。それ以外の選択肢はデフォルトで OK です。
Capslock を Ctrl に変換
Capslock を ctrl に変更するために以下のファイルを変更します。
$ cat /etc/default/keyboard ... XKBOPTIONS="ctrl:nocaps" ...
変更後再起動します。
時間周り
timezone を設定します。
$ sudo ln -sf /usr/share/zoneinfo/Asia/Tokyo /etc/localtime $ sudo dpkg-reconfigure --frontend noninteractive tzdata
次に、systemd-timesyncd を使って、ntp サーバの設定をします。設定ファイル /etc/systemd/timesyncd.conf を以下のように変更します。
$ cat /etc/systemd/timesyncd.conf ... [Time] NTP=ntp.nict.jp FallbackNTP=0.jp.pool.ntp.org ...
変更後に systemd-timesyncd を再起動します。
$ sudo systemctl restart systemd-timesyncd
Docker
Docker をインストールします。基本的には以下と同じ手順を実行します。
# リポジトリのセットアップ $ sudo apt-get update $ sudo apt-get install \ apt-transport-https \ ca-certificates \ curl \ gnupg \ lsb-release $ curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg $ echo \ "deb [arch=arm64 signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu \ $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null # docker のインストール $ sudo apt-get update $ sudo apt-get install docker-ce docker-ce-cli containerd.io
上記を実行したあとユーザーを docker グループに追加します。
$ sudo usermod -a -G docker <username>
IP アドレスの固定
Raspberry Pi の IP アドレスがころころ変わると困るので、それを固定します。今回は netplan を使います。
まずは以下のファイルを作成します。固定するアドレスは 192.168.1.203/24 とします。
$ cat /etc/netplan/99-config.yaml network: version: 2 renderer: networkd ethernets: eth0: dhcp4: false dhcp6: false addresses: [192.168.1.203/24] gateway4: 192.168.1.1
以下のコマンドで設定を反映します。
$ sudo netplan apply
Consul のセットアップ
そもそも Consul とは
いよいよ Consul です。Consul はクラスタ内でどのサービスがどのノード上で動作しているかを特定する、いわゆるサービスディスカバリの機能を備えた Hashicorp のプロダクトです。
consul の実行時プロセスは agent と呼ばれ、server と client の 2 種類に分類され、server と client は共に複数ノードあってよいということになっています。server は consul に登録されているサービスの情報である catalog というものを管理しており、catalog のマスターデータは server の中でも leader という役割のノードが持っています。また、leader は Raft という分散合意アルゴリズムにしたがって選出され、catalog の更新は leader を通すことになっています。
セットアップ
Raft の性質上、server は最低でも 3 台はないと耐障害性を持たないです(3 台なら 1 台までは壊れてもなんとかなる)。前回の記事では 1 台のみ server を用意すると書きましたが、どうせならとやはり 3 台用意することにしました。
なので、構成図は以下のようになります。
consul をちゃんとセットアップするには以下を参考にすればいいですが、いかんせん長いのでここでは必要最小限な手順だけをピックアップして紹介します。
共通
# インストール $ export CONSUL_VERSION="1.8.0" $ export CONSUL_URL="https://releases.hashicorp.com/consul" $ curl --silent --remote-name \ ${CONSUL_URL}/${CONSUL_VERSION}/consul_${CONSUL_VERSION}_linux_arm64.zip $ unzip consul_${CONSUL_VERSION}_linux_arm64.zip $ sudo chown root:root consul $ sudo mv consul /usr/bin/ # ユーザとデータ用/設定用ディレクトリを作成 $ sudo useradd --system --home /etc/consul.d --shell /bin/false consul $ sudo mkdir --parents /opt/consul $ sudo chown --recursive consul:consul /opt/consul $ sudo mkdir --parents /etc/consul.d $ sudo chown --recursive consul:consul /etc/consul.d # 設定ファイルの生成 ## サーバの場合 $ sudo touch /etc/consul.d/server.hcl $ sudo chown --recursive consul:consul /etc/consul.d $ sudo chmod 640 /etc/consul.d/server.hcl ## クライアントの場合 $ sudo touch /etc/consul.d/client.hcl $ sudo chown --recursive consul:consul /etc/consul.d $ sudo chmod 640 /etc/consul.d/client.hcl # サービスの定義、起動 $ sudo touch /usr/lib/systemd/system/consul.service $ sudo systemctl enable consul $ sudo systemctl start consul
なお、consul.service
の中身はこんな感じ
[Unit] Description="HashiCorp Consul - A service mesh solution" Documentation=https://www.consul.io/ Requires=network-online.target After=network-online.target ConditionFileNotEmpty=/etc/consul.d/consul.hcl [Service] Type=notify User=consul Group=consul ExecStart=/usr/bin/consul agent -config-dir=/etc/consul.d/ ExecReload=/bin/kill --signal HUP $MAINPID KillMode=process KillSignal=SIGTERM Restart=on-failure LimitNOFILE=65536 [Install] WantedBy=multi-user.target
サーバ の設定ファイルはこんな感じで
datacenter = "home" server = true bootstrap_expect = 3 data_dir = "/opt/consul" bind_addr = "{{ GetInterfaceIP \"eth0\" }}" retry_join = ["192.168.1.200", "192.168.1.201", "192.168.1.202"] client_addr = "0.0.0.0" ui_config { enabled = true }
クライアントはこんな感じです。
datacenter = "home" data_dir = "/opt/consul" bind_addr = "{{ GetInterfaceIP \"eth0\" }}" retry_join = ["192.168.1.200", "192.168.1.201", "192.168.1.202"]
retry_join の中身はご自身の Raspberry Pi のアドレスに置き換えてください。
上記を終わらせると、http://<ui設定がenabledなIP アドレス>:8500
で以下のような consul の Web UI にアクセスすることができます。
これにて Consul のセットアップ終了です!
おわりに
今回は Consul のセットアップについて書きました。(やる気になれば)最後に Nomad のセットアップや Consul Connect についても書きたいと思います。
参考
ブートローダーの修復
はじめに
先日、ゼロからのOS自作入門をやってたらブートローダーを消し飛ばしてしまいました。具体的に言うと /boot/efi
配下を書き換えちゃったってことです。
これによりしばらく、プライベート用の PC が動かなくなっていたのですが、無事修復できたので、後世のために記録を残しておきます。
準備
まず、以下を用意します。
- 別の PC(今回は MacBook Pro)
- USB メモリ(OS が入るくらいのサイズ)
テキトーな Linux ディストロを USB にインストールします。
macOS だと BalenaEtcher が便利です。
手順
USB ブートで Linux を立ち上げて以下を実行していきます。
調査
まずはディスクの状況を確認します。
$ sudo fdisk -l Disk /dev/loop0: 1.2 GiB, 1277792256 bytes, 2495688 sectors Units: sectors of 1 * 512 = 512 bytes Sector size (logical/physical): 512 bytes / 512 bytes I/O size (minimum/optimal): 512 bytes / 512 bytes Disk /dev/sda: 931.5 GiB, 1000204886016 bytes, 1953525168 sectors Units: sectors of 1 * 512 = 512 bytes Sector size (logical/physical): 512 bytes / 512 bytes I/O size (minimum/optimal): 512 bytes / 512 bytes Disklabel type: gpt Disk identifier: C4D9398D-765B-4BCE-9CA3-8084194B8DB8 Device Start End Sectors Size Type /dev/sda1 2048 1023999 1021952 499M Windows recovery environment /dev/sda2 1024000 1226751 202752 99M EFI System /dev/sda3 1226752 1259519 32768 16M Microsoft reserved /dev/sda4 1259520 840779272 839519753 400.3G Microsoft basic data /dev/sda5 840779776 842031103 1251328 611M Windows recovery environment /dev/sda6 842033152 1945744089 1103710938 526.3G Linux filesystem /dev/sda7 1945745408 1953523711 7778304 3.7G Linux swap Disk /dev/sdb: 14.8 GiB, 15871246336 bytes, 30998528 sectors Units: sectors of 1 * 512 = 512 bytes Sector size (logical/physical): 512 bytes / 512 bytes I/O size (minimum/optimal): 512 bytes / 512 bytes Disklabel type: dos Disk identifier: 0xad92e0dc Device Boot Start End Sectors Size Id Type /dev/sdb1 * 64 2904959 2904896 1.4G 0 Empty /dev/sdb2 3532 9035 5504 2.7M ef EFI (FAT-12/16/32)
/dev/sda2
がブートローダー用のパーティションで、/dev/sda6
ルートパーティションです。
環境の再現
/dev/sda2
や /dev/sda6
、その他諸々のデバイスをマウントして環境を再現し chroot
します。
$ sudo mount /dev/sda6 /mnt/ $ sudo mount /dev/sda2 /mnt/boot/efi $ sudo for i in /sys /proc /run /dev; do sudo mount --bind "$i" "/mnt$i"; done $ sudo chroot /mnt
ブートローダーの再インストール
念願のブートローダーインストールです。
$ grub-install /dev/sda $ upgrade-grub
あとは、blkid
コマンドで調べた UUID と /etc/fstab
にある EFI パーティションの UUID が異なっていたら、/etc/fstab
を書き換えます。
$ blkid | grep EFI
あとは再起動して終わり
おわりに
これでいくらブートローダーをふっ飛ばしても怖くない
参考
macOS の launchd を使ってサービスを自動起動する
はじめに
Linux には systemd という起動処理やシステム管理を行うプログラムがあります。systemd は /etc/systemd/system 以下に xxxx.service を作り systemctl enable xxxx
と実行すれば、起動時処理を追加できます。
これを macOS でもやりたいわけですが、これは launchd を使えば実現できます。
plist ファイル
launchd を使って起動時になにかのプログラムを動作させるためには plist ファイルを作る必要があります。/Library/LaunchDaemons 以下に plist ファイルを作り launchctl コマンドでロードしてやれば、plist ファイルの内容に従ってプログラムが起動します。
$ sudo launchctl load /Library/LaunchDaemons/com.consul.plist
ちなみに停止するには unload してやればいいです
$ sudo launchctl unload /Library/LaunchDaemons/com.consul.plist
参考までに、以下は僕が登録している plist ファイルの一例です。
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0"> <dict> <key>Label</key> <string>com.consul</string> <key>ProgramArguments</key> <array> <string>/Users/USER_NAME/Root/local/bin/consul</string> <string>agent</string> <string>-config-dir=/Users/USER_NAME/Root/etc/consul.d</string> <string>-node=macOS</string> </array> <key>RunAtLoad</key> <true/> <key>KeepAlive</key> <true/> <key>NetworkState</key> <true/> <key>StandardOutPath</key> <string>/tmp/consul-stdout.log</string> <key>StandardErrorPath</key> <string>/tmp/consul-stderr.log</string> <key>UserName</key> <string>USER_NAME</string> </dict> </plist>
ポイントとしては
- RunAtLoad で起動時に実行されるようにする
- KeepAlive で自動で再起動を試みるようにする
- NetworkState でネットワークデバイスが有効になり、IP アドレスが割り当てられているときに限りプログラムを動作させるようにする
- StandardOutPath, StandardErrorPath で標準出力、エラー出力をファイルに残すようにする
ってところくらいですかねー。以上!
参考
LINE CTF 2021 writeup
LINE CTF 2021 にチーム Tomatosalad で参加しました。結果は 200 ポイント、 73 位でした。 僕は crypto カテゴリの問題を 2 問解きました。
babycrypto1
#!/usr/bin/env python from base64 import b64decode from base64 import b64encode import socket import multiprocessing from Crypto.Cipher import AES from Crypto.Random import get_random_bytes from Crypto.Util.Padding import pad, unpad import hashlib import sys class AESCipher: def __init__(self, key): self.key = key def encrypt(self, data): iv = get_random_bytes(AES.block_size) self.cipher = AES.new(self.key, AES.MODE_CBC, iv) return b64encode(iv + self.cipher.encrypt(pad(data, AES.block_size))) def encrypt_iv(self, data, iv): self.cipher = AES.new(self.key, AES.MODE_CBC, iv) return b64encode(iv + self.cipher.encrypt(pad(data, AES.block_size))) def decrypt(self, data): raw = b64decode(data) self.cipher = AES.new(self.key, AES.MODE_CBC, raw[:AES.block_size]) return unpad(self.cipher.decrypt(raw[AES.block_size:]), AES.block_size) flag = open("flag", "rb").read().strip() COMMAND = [b'test',b'show'] def run_server(client, aes_key, token): client.send(b'test Command: ' + AESCipher(aes_key).encrypt(token+COMMAND[0]) + b'\n') client.send(b'**Cipher oracle**\n') client.send(b'IV...: ') iv = b64decode(client.recv(1024).decode().strip()) client.send(b'Message...: ') msg = b64decode(client.recv(1024).decode().strip()) client.send(b'Ciphertext:' + AESCipher(aes_key).encrypt_iv(msg,iv) + b'\n\n') while(True): client.send(b'Enter your command: ') tt = client.recv(1024).strip() tt2 = AESCipher(aes_key).decrypt(tt) client.send(tt2 + b'\n') if tt2 == token+COMMAND[1]: client.send(b'The flag is: ' + flag) client.close() break if __name__ == '__main__': server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) server.bind(('0.0.0.0', 16001)) server.listen(1) while True: client, address = server.accept() aes_key = get_random_bytes(AES.block_size) token = b64encode(get_random_bytes(AES.block_size*10))[:AES.block_size*10] process = multiprocessing.Process(target=run_server, args=(client, aes_key, token)) process.daemon = True process.start()
攻撃者ができることは
- iv + AES.enc(token + "test", iv) を手に入れられる
- 一度だけ攻撃者が指定した iv', msg を使って iv' + AWS.enc(msg, iv') を手に入れられる
- テキトーな暗号文を復号してもらえる
目標は iv + AES.enc(token + "show", iv) をサーバに渡して復号してもらうことです。iv はレスポンスの先頭に引っ付いてるので既知です。問題はどのようにして AES.enc(token + "test", iv) から AES.enc(token + "show", iv) を求めるかです。
ここでポイントとなるのは、token はちょうど 10 ブロック分でというところです。CBC モードの暗号化処理を見てみましょう。(Wikipedia より引用)
token がちょうど 10 ブロック分ということは、暗号化した後の token の最後の 1 ブロックを iv 、msg を "show" に設定してサーバに暗号化して貰えれば、 "show" の暗号ブロックのみを手に入れられます。
これを暗号化した token と結合すれば AES.enc(token + "show", iv) の出来上がりです。
ソルバは以下の通り
from pwn import * from base64 import b64decode from base64 import b64encode AES_BLOCK_SIZE = 16 # bytes p = remote("35.200.115.41", 16001) def get_test_command(p) -> bytes: p.recvuntil("test Command: ") return b64decode(p.recvline(keepends=False)) def enc(p, iv: bytes, msg: bytes) -> bytes: p.recvuntil("IV...: ") p.sendline(b64encode(iv)) p.recvuntil("Message...: ") p.sendline(b64encode(msg)) p.recvuntil("Ciphertext:") return b64decode(p.recvline(keepends=False)) def send_command(p, msg: bytes) -> bytes: p.recvuntil("Enter your command: ") p.sendline(b64encode(msg)) return b64decode(p.recvline(keepends=False)) iv_token_test = get_test_command(p) iv_token = iv_token_test[:AES_BLOCK_SIZE*11] next_iv = iv_token[-AES_BLOCK_SIZE:] # feed to oracle as iv next_iv_show = enc(p, next_iv, b"show") show = next_iv_show[AES_BLOCK_SIZE:] iv_token_show = iv_token + show result = p.recvline(keepends=False) send_command(p, iv_token_show) print(p.recv()) p.close()
babycrypto2
ぶっちゃけ babycrypto1 より簡単です。ソースコードは babycrypto1 とほぼ同じで、差分だけ取り出してみると
AES_KEY = get_random_bytes(AES.block_size) TOKEN = b64encode(get_random_bytes(AES.block_size*10-1)) COMMAND = [b'test',b'show'] PREFIX = b'Command: ' def run_server(client): client.send(b'test Command: ' + AESCipher(AES_KEY).encrypt(PREFIX+COMMAND[0]+TOKEN) + b'\n') while(True): client.send(b'Enter your command: ') tt = client.recv(1024).strip() tt2 = AESCipher(AES_KEY).decrypt(tt) client.send(tt2 + b'\n') if tt2 == PREFIX+COMMAND[1]+TOKEN: client.send(b'The flag is: ' + flag) client.close() break
ポイントは
- 最初に手に入れられる暗号が iv + AES.enc(token + "test", iv) から iv + AES.enc("Command: " + "test" + token, iv) に変わった
- サーバに暗号化してもらえなくなった(できるのは暗号文の復号のみ)
このような状態で AES.enc("Command: " + "show" + token, iv) を求める必要があります。この問題で使えるのが iv です。
CBC モードの復号処理を見てみましょう。(Wikipedia より引用)
最初のブロックが平文に戻る直前に iv と xor を取っていますよね。xor の性質(同じ値で xor を計算すると 0 になる)を使って iv を改ざんすることで、復号したあとの平文を test ではなく show にすることができます。
ソルバは以下の通り
from pwn import * from base64 import b64decode from base64 import b64encode from Crypto.Util.number import bytes_to_long as b2l from Crypto.Util.number import long_to_bytes as l2b AES_BLOCK_SIZE = 16 # bytes p = remote("35.200.39.68", 16002) def get_test_command(p) -> bytes: p.recvuntil("test Command: ") return b64decode(p.recvline(keepends=False)) def send_command(p, msg: bytes) -> bytes: p.recvuntil("Enter your command: ") p.sendline(b64encode(msg)) return p.recvline(keepends=False) iv_command_test_token = get_test_command(p) iv = iv_command_test_token[:AES_BLOCK_SIZE] partial_iv = b2l(iv[9:9+4]) ^ b2l(b"test") ^ b2l(b"show") partial_iv = l2b(partial_iv) iv = iv[:9] + partial_iv + iv[9+4:] enc = iv + iv_command_test_token[AES_BLOCK_SIZE:] send_command(p, enc) print(p.recv()) p.close()
おわりに
死ぬほどやる気がでなかった
参考
Eloquent Model 内の処理の委譲
はじめに
Eloquent Model の create メソッ の実装を見てやろうと Laravel Framework のコードを潜ってみたところ、なかなか面白い処理フローになっていたのでメモ
TL;DR
Model::create()
を実行すると処理がBuilder::create()
に委譲される- 処理の委譲は
__call()
メソッドとForwardCalls
トレイトを使って実現される
__call()
https://laravel.com/api/8.x/Illuminate/Database/Eloquent/Model.html を見たらわかると思うが Model クラスには create()
メソッドが存在しない。
また、Model クラスは他のクラスを継承していないし、use しているトレイトにもそれらしいメソッドはない。では、Model::create
を実行したときに一体なにが呼び出されるのか。
実は Model クラスは __call()
メソッドを実装していて、定義されていないメソッドを呼び出すと __call()
メソッドに処理が移る
abstract class Model implements Arrayable, ArrayAccess, Jsonable, JsonSerializable, QueueableEntity, UrlRoutable { use Concerns\HasAttributes, Concerns\HasEvents, Concerns\HasGlobalScopes, Concerns\HasRelationships, Concerns\HasTimestamps, Concerns\HidesAttributes, Concerns\GuardsAttributes, ForwardsCalls; // forwardCallTo() が実装されている ...(snip)... /** * Handle dynamic method calls into the model. * * @param string $method * @param array $parameters * @return mixed */ public function __call($method, $parameters) { if (in_array($method, ['increment', 'decrement'])) { return $this->$method(...$parameters); } // リレーション周りの処理はこの分岐に入るっぽい? if ($resolver = (static::$relationResolvers[get_class($this)][$method] ?? null)) { return $resolver($this); } // Model::create() を呼び出すと通る処理フローはこっち return $this->forwardCallTo($this->newQuery(), $method, $parameters); }
コードを見る限りリレーション以外の処理は最後の行で ForwardsCalls@forwardCallTo()
に処理が移る (要調査)
forwardCallTo()
forwardCallTo()
メソッドの実装は以下のようになっている
/** * Forward a method call to the given object. * * @param mixed $object * @param string $method * @param array $parameters * @return mixed * * @throws \BadMethodCallException */ protected function forwardCallTo($object, $method, $parameters) { try { return $object->{$method}(...$parameters); } catch (Error | BadMethodCallException $e) { ...(snip)... } }
実装の通り、forwardCallTo()
は第 1 引数のオブジェクトに処理を委譲している
newQuery() の戻り値
結局 Model はどこに処理を委譲しているのか。これは newQuery()
の戻り地を見るとわかる
処理フローはコードを見るとわかるが、いかんせん長いので結果だけをいうと最終的には newEloquentBuilder()
メソッド内で
Illuminate\Database\Eloquent\Builder
クラスのインスタンスが生成され、これが返される
Illuminate\Database\Eloquent\Model
クラス内
/** * Get a new query builder for the model's table. * * @return \Illuminate\Database\Eloquent\Builder */ public function newQuery() { return $this->registerGlobalScopes($this->newQueryWithoutScopes()); } /** * Register the global scopes for this builder instance. * * @param \Illuminate\Database\Eloquent\Builder $builder * @return \Illuminate\Database\Eloquent\Builder */ public function registerGlobalScopes($builder) { foreach ($this->getGlobalScopes() as $identifier => $scope) { $builder->withGlobalScope($identifier, $scope); } return $builder; } /** * Get a new query builder that doesn't have any global scopes. * * @return \Illuminate\Database\Eloquent\Builder|static */ public function newQueryWithoutScopes() { return $this->newModelQuery() ->with($this->with) ->withCount($this->withCount); } /** * Get a new query builder that doesn't have any global scopes or eager loading. * * @return \Illuminate\Database\Eloquent\Builder|static */ public function newModelQuery() { return $this->newEloquentBuilder( $this->newBaseQueryBuilder() )->setModel($this); } /** * Create a new Eloquent query builder for the model. * * @param \Illuminate\Database\Query\Builder $query * @return \Illuminate\Database\Eloquent\Builder|static */ public function newEloquentBuilder($query) { return new Builder($query); }
Illuminate\Database\Eloquent\Builder
クラス内
/** * Set a model instance for the model being queried. * * @param \Illuminate\Database\Eloquent\Model $model * @return $this */ public function setModel(Model $model) { ...(snip)... return $this; }
おわりに
Eloquent Model は自身で実装していない処理は ForwardCalls トレイトを使って、Builder クラスに処理を委譲していることがわかった
Docker Compose で Firebase のテスト環境を作る
はじめに
仕事で Firebase の Readltime Database を使う機会があり、Firebase Local Emulator Suite を使ったテスト環境を構築したのでメモ
設定ファイル
Dockerfile
Firebase CLI をインストールして、Realtime Database のエミュレータのみを起動している。 なお、Realtime Database のエミュレータは 9000 ポートにバインドされるので、ホストからアクセスしたい場合は EXPOSE でポートを公開しておく
FROM openjdk:11-jre-slim EXPOSE 9000 RUN apt-get update && apt-get install -y wget RUN wget -O firebase https://firebase.tools/bin/linux/latest \ && chmod u+x ./firebase \ && mv firebase /usr/local/bin ENTRYPOINT ["firebase", "emulators:start", "--only", "database"]
docker-compose.yaml
これに関しては特に言うことはない
version: "3.7" services: ...(snip)... firebase: build: context: . dockerfile: ./docker/firebase/Dockerfile volumes: - ./docker/firebase/firebase.json:/firebase.json
firebase.json
ポイントは Realtime Databse が bind するホストを 0.0.0.0 にするところ。 デフォルトでは localhost でリクエストを待ち受けているが、これでは firebase コンテナ外部からアクセスできないので、0.0.0.0 に変えておく必要がある
{ "emulators": { "database": { "host": "0.0.0.0", "port": 9000 } } }
参考
Hugo で tailwindcss を使う
はじめに
Hugo でテーマを作ってみようと思い、そのテーマに tailwindcss を使おうと思ったので色々調べたのでここにまとめておく
準備
$ hugo new site hugo-tailwindcss $ cd hugo-tailwindcss
必要なライブラリをインストールする
$ npm init -y $ npm install -D tailwindcss@latest \ @tailwindcss/typography \ postcss-cli@latest \ autoprefixer@latest \ @fullhuman/postcss-purgecss
設定ファイルを作る
PostCSS の設定ファイルを作成する
$ cat <<EOT > postcss.config.js const purgecss = require('@fullhuman/postcss-purgecss')({ content: ['./hugo_stats.json'], defaultExtractor: (content) => { let els = JSON.parse(content).htmlElements; return els.tags.concat(els.classes, els.ids); } }); module.exports = { plugins: [ require('tailwindcss'), require('autoprefixer'), ...(process.env.HUGO_ENVIRONMENT === 'production' ? [purgecss] : []) ] }; EOT
tailwindcss の設定ファイルを作成する
$ cat <<EOT > tailwindcss.config.js module.exports = { plugins: [ require('@tailwindcss/typography') ] } EOT
PurgeCSS のための設定ファイルを作成する。この設定により、ビルド時に hugo_stats.json
が作成される
$ mkdir -p config/production $ cat <<EOT > config/production/config.toml [build] writeStats = true EOT
hugo_stats.json
には以下のように、そのとき使われている HTML タグや CSS の class, id の情報が記載されている。PurgeCSS はこの情報を使って、使用されていない CSS のクラスを削除して、最終的に生成される CSS ファイルのサイズを削減できる
$ cat hugo_stats.json { "htmlElements": { "tags": [ "!doctype", "body", "div", "head", "html", "link", "meta", "title" ], "classes": [ "bg-gray-200", "bg-white", "flex", "font-extrabold", "h-screen", "items-center", "justify-center", "p-10", "rounded-xl", "shadow-xl", "text-6xl", "text-blue-500" ], "ids": null }
CSSやHTMLファイルを作る
$ mkdir assets $ cat <<EOT > assets/style.css @tailwind base; @tailwind components; @tailwind utilities; EOT
動作確認用に layouts/index.html
を作成して、上記で作成した CSS ファイルを読み込む
$ cat <<EOT > layouts/index.html <!DOCTYPE html> <html lang="{{ .Site.Language.Lang }}"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> {{ \$css := resources.Get "style.css" | resources.PostCSS }} {{ if hugo.IsProduction }} {{ \$css = \$css | minify | fingerprint | resources.PostProcess }} {{ end }} <link rel="stylesheet" href="{{ \$css.Permalink }}"> <title>{{ .Site.Title }}</title> </head> <body class="h-screen flex justify-center items-center bg-gray-200"> <div class="text-6xl text-blue-500 font-extrabold rounded-xl shadow-xl p-10 bg-white">Hello World</div> </body> </html> EOT
動作確認
最後に動作確認をする
$ hugo server -D --gc ... Running in Fast Render Mode. For full rebuilds on change: hugo server --disableFastRender Web Server is available at http://localhost:1313/ (bind address 127.0.0.1) Press Ctrl+C to stop ...
http://localhost:1313/
にアクセスして、下記の画面が表示できれば OK
今日の成果物