えんじにあメモ

試してみた技術とか、たまに家電ネタ

Github Actionsで複数のアーキテクチャ向けのDockerイメージを作る

🆙 2021/03/21 docker公式アクションを使ってイメージを作る記事を書きました

sminamot-dev.hatenablog.com

🆙 2020/06/20 crazy-max/ghaction-docker-buildx の利用をv1 -> v3に更新

あらすじ

前回おうちk8sの環境がめでたく整いました 🎉 sminamot-dev.hatenablog.com

さて何か動かすためにDockerイメージを作ろ、と思ってまずは適当に以下のような(なんの意味もない)イメージを作りました

FROM alpine:3.11

ENTRYPOINT ["echo", "hogehoge"]

Macでビルド後DockerHubに上げて、いざラズパイ上で動かしてみると…

$ sudo docker run --rm sminamot/multi-arch-sample
standard_init_linux.go:211: exec user process caused "exec format error"

おちた

原因は、ラズパイはarmアーキテクチャで動いているが、上記DockerfileをビルドしたMac環境ではx86_64アーキテクチャで動いていて、 ビルド時のalpineはx86_64向けのイメージが使われているため 😰

対策としてはDockerfileで明示的にarm向けのalpineを指定してあげれば良い

-FROM alpine:3.11
+FROM arm32v7/alpine:3.11

ENTRYPOINT ["echo", "hogehoge"]

しかし、これだと今度はx86_64アーキテクチャの環境では動かせない
(Docker Desktopではarm64armアーキテクチャがサポートされているのでMac上だと動かすことは可能です)

Dockerfileを複数用意してタグで環境を管理するのもちょっとめんどい

buildx

そこで buildx を使います

docs.docker.com

buildxを使うことで様々なアーキテクチャ用のイメージを作成することができます

今回はGitHubActions上でのbuildxを使ったビルドをやってみます

GitHub Actionsの設定

今回はテストとして以下のルールでDockerHubへイメージをpublishしてみます

  • masterブランチにpushされたら実行
  • 作成するイメージのアーキテクチャamd64/arm32v7
  • 作成したイメージをDockerHubへpublish
    • イメージ名はgithuborg/repositoryとする(今回のテストではsminamot/multi-arch-sample
    • イメージタグはlatest

buildxではpushオプションがあり、ビルドしたイメージをそのままpublishすることが可能です

.github/workflows/docker.yml を作ります

name: Publish Docker image
on:
  push:
    branches:
      - master
jobs:

  build:
    name: build and publish
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@master

      - name: Set up Docker Buildx
        id: buildx
        uses: crazy-max/ghaction-docker-buildx@v3
        with:
          buildx-version: latest
          qemu-version: latest

      - name: Login DockerHub
        run: |
          echo ${{ secrets.DOCKER_PASSWORD }} | docker login -u ${{ secrets.DOCKER_USERNAME }} --password-stdin

      - name: Build and Publish
        run: |
          docker buildx build \
            --push \
            --platform linux/arm/v7,linux/amd64 \
            -t ${{ github.repository }}:latest .

--platform オプションに,区切りで対象のプラットフォームを指定します
上記設定でのGitHub Actions上では以下のプラットフォームが指定可能でした(Set up Docker Buildxの出力で確認)

buildx自体のセットアップには↓を利用させてもらいました
GitHub - crazy-max/ghaction-docker-buildx: GitHub Action to set up Docker Buildx

事前に対象のGitHubレポジトリにDockerHubのIDとパスワードを設定しておきます

GitHubのSettings -> Secrets で作成

Name Value
DOCKER_USERNAME <ID>
DOCKER_PASSWORD <パスワード>

f:id:shosfs:20200201183357p:plain

両方とも設定できて以下のようになっていればOK

f:id:shosfs:20200201183446p:plain

Dockerfileと上記のyamlGitHubへpushすればjobが開始されます

f:id:shosfs:20200201185011p:plain

DockerHubも見てみると… f:id:shosfs:20200201185419p:plain 同じlatestタグでamd64arm/v7のイメージができてる 🎉🎉🎉

これでラズパイ環境用だけでなく、EC2などでも使えるイメージになりました

おわりに

今回作ったリポジトリとDockerイメージはこちらです

参考

Raspberry Pi 4 でおうちk8sクラスタを構築する

概要

だいたいこの手の構築手順はいろいろな方がまとめられてますが、自分がやった作業の備忘録も兼ねてまとめておきます

構成はmaster×1、worker×2として、raspberry piを3台使って作ります

物理構築

物理的な構築は以下の記事とほぼ同じ手順でやりました qiita.com

また、ルータの設定については以下記事と同じ手順で行っています qiita.com この記事自体がだいたい↑これと同じ手順となるのでコレだけ読めば良い説

完成した様子がこちら(良い良い) f:id:shosfs:20200125124454j:plain

Raspberry Pi 4で」というタイトルになってますが、もともとRaspberry Pi 3(Model 3+)を持っていたのでMasterNodeには3を使おうと思います
(下から3番目のラズパイが3)

今回のクラスタ構成のために買ったものを以下にまとめておきます

もの 製品
ラズパイ Raspberry Pi 4 Model B 2
ケース 積層式ケース for Raspberry Pi 4 / Pi 3 Model B+ 1
SDカード KEXIN MicroSDカード UHS-I Class10 SDHC 16GB(3個セット) 3枚
USD充電器 AUKEY USB充電器 50W/10A ACアダプター 5ポート 1
LANケーブル(15cm) エレコム LANケーブル 0.15m×2本 3本(1本は余ってる)
LANケーブル(30cm) TWS-63BK [スリムLANケーブル CAT.6 0.3m 黒] 1
USBケーブル(type-c) USB C ケーブル 30cm SUNGUY 【2本セット0.3m】 2本
スイッチングハブ TP-Link 5ポート スイッチングハブ 1
無線ルータ BUFFALO 無線LAN親機 WMR-433W2-BK 1
  • スイッチングハブは他記事などではよくこちらの商品が使われていて、自分も最初使いましたが、ネットワークがよく途切れるので買い替えました
    • TP-Linkだと電源が必要になりサイズも大きくなりましたが、サイズ的にはケース下のネジが挟まれるようになってちょうど良いサイズだった
  • LANケーブルはすべて15cmでもつなげられるが、ケース最上段に置く無線ルータへはギリギリでケーブルに負荷がかかるため30cmのほうがおすすめ

論理構築

全台共通

https://www.raspberrypi.org/downloads/raspbian/ からOSイメージをダウンロード

今回はk8sクラスタ用でdesktopは不要なのでRaspbian Buster Liteを選びました f:id:shosfs:20200125122401p:plain

他の記事などではddコマンドなどを利用してSDへイメージを書き込んでいますが、自分は balenaEtcher というアプリケーションを利用して書き込みました(弱い)

Select imageに解凍したimgファイル、Select targetに対象のSDを選択して"Flash!"を実行します f:id:shosfs:20200125122910p:plain

書き込みには少し時間かかるのでお茶でも飲みながら待ちましょう

書き込みが完了したらsshdとcgroupsの設定のために以下を実行します(SDカードに対して)

$ cd /Volumes/boot
$ touch ssh

cmdline.txtの末尾にcgroup_enable=cpuset cgroup_enable=memory cgroup_memory=1を追記

$ cd /Volumes/boot
$ cat cmdline.txt
console=serial0,115200 console=tty1 root=PARTUUID=6c586e13-02 rootfstype=ext4 elevator=deadline fsck.repair=yes rootwait quiet init=/usr/lib/raspi-config/init_resize.sh cgroup_enable=cpuset cgroup_enable=memory cgroup_memory=1

SDをラズパイにセットして電源を入れます

arp -aをラズパイと同じLANに繋がれたPC(自分はmac使ってる)から実行してラズパイのIPを調べます

$ arp -a
? (192.168.13.1) at 18:c2:bf:ea:d5:9a on en1 ifscope [ethernet]
? (192.168.13.2) at dc:a6:32:6e:d6:92 on en1 ifscope [ethernet]
...

192.168.13.1はルータのIPなので、192.168.13.2の方っぽいです

sshで入ってみます

$ ssh pi@192.168.13.2

piユーザの初期パスワードはraspberryです

pi@raspberrypi:~ $

無事ログインできました
piユーザ/rootユーザのパスワードを変えておきましょう

$ passwd
$ sudo passwd

作業用のユーザを作成します(パスワード設定も)

$ sudo useradd --groups adm,dialout,cdrom,sudo,audio,video,plugdev,games,users,input,netdev,spi,i2c,gpio -m sminamot
$ sudo passwd sminamot

最後にpiユーザを無効化しておきます

$ sudo passwd --lock pi

コレ以降は作ったユーザで作業を進めます

$ ssh sminamot@192.168.13.2

初期設定

$ sudo raspi-config
  • 4 Localisation Options -> I1 Change Locale
    • en_GB.UTF-8 UTF-8のチェックを外す
    • en_US.UTF-8 UTF-8にチェックを入れる
    • OKを押したあとに聞かれるDefault localeをen_US.UTF-8にする
  • 4 Localisation Options -> I2 Change Timezone
    • Asia -> Tokyo
  • 8 Update

ラズパイ自体のアップデートも行います

$ sudo apt update
$ sudo apt upgrade -y
$ sudo apt dist-upgrade
$ sudo rpi-update

設定書き換えなどのためにvimを入れておきます

$ sudo apt-get install vim -y

次にIPの固定化を行います
設定内容は以下のとおり(hostnameの設定は後ほど)

Node IP hostname
master 192.168.13.101 raspberrypi-master
node1 192.168.13.102 raspberrypi-node01
node2 192.168.13.103 raspberrypi-node02

/etc/dhcpcd.confに以下を追記します(それぞれのNodeに合わせて設定値を変える)

interface eth0
static ip_address=192.168.13.101/24
static routers=192.168.13.1
static domain_name_servers=192.168.13.1 8.8.8.8

反映します

$ sudo service dhcpcd reload

固定したIPでログインができることを確認しておきましょう

$ ssh sminamot@192.168.13.101

SSHの設定をしてパスワードログインはできないようにしておきます

$ sudo vim /etc/ssh/sshd_config
- #PermitRootLogin prohibit-password
+ PermitRootLogin no
- #PubkeyAuthentication yes
+ PubkeyAuthentication yes
- #PasswordAuthentication yes
- #PermitEmptyPasswords no
+ PasswordAuthentication no
+ PermitEmptyPasswords no
- UsePAM yes
+ UsePAM no

手元のmacから公開鍵を書き込んでおきます(ここでは公開鍵は事前に作成してあるものを利用する)

$ ssh-copy-id -i ~/.ssh/id_rsa_raspberrypi sminamot@192.168.13.101

公開鍵の登録が完了したらsshの再起動を行います

$ sudo systemctl restart ssh

※ 手元のmacから秘密鍵を利用したログインができることを確認しておきましょう

iptablesのバージョン更新
コレをやっておかないとk8s構築した際にPodから他Podや外部への通信がうまくできません

$ sudo update-alternatives --set iptables /usr/sbin/iptables-legacy

swapの無効化もしておきます(しておかないとk8sインストール時に失敗します)

$ sudo dphys-swapfile swapoff
$ sudo systemctl stop dphys-swapfile
$ sudo systemctl disable dphys-swapfile

続いてhost名の設定 /etc/hostnameを更新します(それぞれのNodeに合わせて設定値を変える)

$ cat /etc/hostname
raspberrypi-master

/etc/hostsも修正します(それぞれのNodeに合わせて設定値を変える)

127.0.0.1       localhost
::1             localhost ip6-localhost ip6-loopback
ff02::1         ip6-allnodes
ff02::2         ip6-allrouters

-127.0.1.1       raspberrypi
+127.0.1.1       raspberrypi-master

ここまでの作業ができたらラズパイを再起動しましょう

$ sudo reboot

パッケージインストール

ここからk8sクラスタ構築に必要なパッケージをインストールしていきます

まずはDocker

$ sudo apt-get install apt-transport-https ca-certificates curl software-properties-common -y
$ curl -fsSL https://download.docker.com/linux/$(. /etc/os-release; echo "$ID")/gpg | sudo apt-key add -
$ echo "deb [arch=armhf] https://download.docker.com/linux/$(. /etc/os-release; echo "$ID") \
     $(lsb_release -cs) stable" | \
    sudo tee /etc/apt/sources.list.d/docker.list
$ sudo apt-get update
$ sudo apt-get install docker-ce -y
Errors were encountered while processing:
 aufs-dkms
E: Sub-process /usr/bin/dpkg returned an error code (1)

インストール中に上記のエラーで失敗した場合、以下を実行してから再度インストールを行います

$ sudo rm /var/lib/dpkg/info/aufs-dkms.postinst
$ sudo rm /var/lib/dpkg/info/aufs-dkms.prerm
$ sudo dpkg --configure aufs-dkms

# 再度インストール
$ sudo apt-get install docker-ce -y

インストールされたことを確認します

 $ sudo docker version
Client: Docker Engine - Community
 Version:           19.03.5
 API version:       1.40
 Go version:        go1.12.12
 Git commit:        633a0ea
 Built:             Wed Nov 13 07:37:22 2019
 OS/Arch:           linux/arm
 Experimental:      false

Server: Docker Engine - Community
...

続いてkubernetes

$ curl -fsSL https://packages.cloud.google.com/apt/doc/apt-key.gpg|sudo apt-key add -
$ echo "deb http://apt.kubernetes.io/ kubernetes-xenial main" | sudo tee /etc/apt/sources.list.d/kube.list
$ sudo apt-get update
$ sudo apt-get install kubelet kubeadm kubectl -y

インストールされたことを確認します

$ kubectl version
Client Version: version.Info{Major:"1", Minor:"17", GitVersion:"v1.17.2", GitCommit:"59603c6e503c87169aea6106f57b9f242f64df89", GitTreeState:"clean", BuildDate:"2020-01-18T23:30:10Z", GoVersion:"go1.13.5", Compiler:"gc", Platform:"linux/arm"}
The connection to the server localhost:8080 was refused - did you specify the right host or port?

ここまではラズパイ3台ともに共通(一部Nodeに合わせて設定を分ける部分もある)で行っておきます

ここからはMaster/Workerノードに分けた設定を行っていきます

Master

$ sudo kubeadm init --pod-network-cidr=10.244.0.0/16

コンソールに出力されたコマンドを実行します

$ mkdir -p $HOME/.kube
$ sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
$ sudo chown $(id -u):$(id -g) $HOME/.kube/config

また、同じくコンソールに出力されたkubeadm join 192.168.13.101:6443 --token XXXはWorkerのセットアップ時に実行するためメモっておく

続いてネットワークプラグインをインストールします

Raspberry Piはarmプロセッサのため、armにも対応したFlannelを利用します
Creating a single control-plane cluster with kubeadm - Kubernetes

Flannelのタブに記載されているコマンドを実行します

$ kubectl apply -f https://raw.githubusercontent.com/coreos/flannel/2140ac876ef134e0ed5af15c65e414cf26827915/Documentation/kube-flannel.yml

Podのが正しく稼働していることを確認しておきましょう

$ kubectl get po -n kube-system -o wide
NAME                                         READY   STATUS    RESTARTS   AGE     IP               NODE                 NOMINATED NODE   READINESS GATES
coredns-6955765f44-qrq2s                     1/1     Running   0          13m     10.244.0.3       raspberrypi-master   <none>           <none>
coredns-6955765f44-xhqzt                     1/1     Running   0          13m     10.244.0.2       raspberrypi-master   <none>           <none>
etcd-raspberrypi-master                      1/1     Running   0          13m     192.168.13.101   raspberrypi-master   <none>           <none>
kube-apiserver-raspberrypi-master            1/1     Running   0          13m     192.168.13.101   raspberrypi-master   <none>           <none>
kube-controller-manager-raspberrypi-master   1/1     Running   1          13m     192.168.13.101   raspberrypi-master   <none>           <none>
kube-flannel-ds-arm-gwcv9                    1/1     Running   0          2m39s   192.168.13.101   raspberrypi-master   <none>           <none>
kube-proxy-lg4qn                             1/1     Running   0          13m     192.168.13.101   raspberrypi-master   <none>           <none>
kube-scheduler-raspberrypi-master            1/1     Running   1          13m     192.168.13.101   raspberrypi-master   <none>           <none>

Worker

先ほどメモった、Masterのkubeadm init実行の際に出力されたコマンドを実行します

$ sudo kubeadm join 192.168.13.101:6443 --token XXX \
>     --discovery-token-ca-cert-hash sha256:XXX

確認

workerが追加されたことを確認します

$ kubectl get nodes
NAME                 STATUS   ROLES    AGE     VERSION
raspberrypi-master   Ready    master   21m     v1.17.2
raspberrypi-node01   Ready    <none>   3m40s   v1.17.2
raspberrypi-node02   Ready    <none>   3m38s   v1.17.2

worker nodeにラベルを付けておきます

$ kubectl label node raspberrypi-node01 node-role.kubernetes.io/worker=worker
$ kubectl label node raspberrypi-node02 node-role.kubernetes.io/worker=worker

# 確認
$ kubectl get nodes
NAME                 STATUS   ROLES    AGE     VERSION
raspberrypi-master   Ready    master   25m     v1.17.2
raspberrypi-node01   Ready    worker   7m40s   v1.17.2
raspberrypi-node02   Ready    worker   7m38s   v1.17.2

OK!!

Macのkubectlを利用する

k8sの操作をMac上から行うための設定もしておきます(事前にMac上でkubectlを利用できるようにしておく)

master node環境に入り、~/.kube/configの設定から以下の項目をMac側の~/.kube/configをコピペすればOK

  • clusters
  • contexts
  • users

nameは必要に応じて変更しても問題ありません(自分はraspberrypi-k8sに変更しました)

まとめ

これでおうちk8sライフが過ごせそうです

何やろうかな…

参考

賃貸でもテレビを壁掛けしたい

  • テレビまわりの配線がゴチャゴチャ
  • テレビ裏の掃除や機器の設定が狭くてやりづらい
  • オシャレ感を漂わせたい

などなどテレビまわりの不満がありますよね

そんなときにはテレビを壁掛けにすることで、オシャレ感も出しつつ全体をスッキリさせることができるのです

ただ賃貸だと壁に穴開けられないし、スタンド型のもなんか違う…🤔

賃貸でも壁掛けしたい

↓コレなら大丈夫

ホッチキスで壁掛けテレビ TVセッター壁美人Mサイズ / 壁美人.net ホッチキスで壁収納を実現する『壁美人』専門店

テレビの大きさで2種類あってリンクのやつは37~47インチに対応するMサイズ

ちなみに自分が使っているテレビは東芝55J8っていう55インチで、適合はしてないけど、
壁美人Mサイズの耐荷重は20kgということで、16.5kgの55J8は大丈夫でした(自己責任でお願いします)

⚠注意

  • 壁美人が使えるのは石膏ボードの壁のみ
    • 賃貸でも石膏ボードじゃなかったり、壁によってコンクリートにクロス貼られてる箇所もあったり
    • 実際に自分のとこも壁掛けした反対側の壁はダメでした

やってみた

まずはプレートを壁掛けしたい位置にフィルムを使ってホチキス打っていく f:id:shosfs:20200111105302j:plain 写真は一番上の列だけフィルムつけたとこだけど、これだけでもすでにしっかり止まってる

ちなみにこの作業一人でやったけど、けっこうきついので二人以上でやるのがオススメ
(特に最初の1枚目のフィルムを打ち付けるのがプレート支えながらなのでツライ)

全部のフィルムを打ったあと↓ f:id:shosfs:20200111110943j:plain

次にテレビをかけるためのブラケットをつける(サイズ的にギリギリ) f:id:shosfs:20200111111110j:plain

アンテナなどのケーブルをつけてからテレビをかけて完成!

テレビ台やテレビ周りの危機をすべてつなげた状態が以下 f:id:shosfs:20200111111359j:plain めっちゃスッキリ!

壁掛けでテレビ置くスペースが空いた分、各種機器やセンタースピーカーをテレビ台におけるのは嬉しい🥰

GoでGoogleスプレッドシートを操作する

SheetsAPI を利用してGoからスプレッドシートの操作(参照・更新)を行う

CLIで動かすためサービスアカウントを利用して操作を行う

実行環境はMac、Goの利用バージョンは1.13

プロジェクトの作成

Google Cloud Platform からプロジェクトを作成しておく
(すでに作成済みのプロジェクトを利用する場合はこの手順は不要)

今回はテスト用に「MyTestProject」という名前で作成しておいた

SheetsAPIの有効化

作成したプロジェクトからSheetsAPIを利用できるように有効化を行う

「+APIとサービスを有効化」をクリック

f:id:shosfs:20191205182248p:plain
APIとサービスを有効化

検索フォームに「sheets」などと入力すればGoogle Sheets APIが出てくるので選択する

f:id:shosfs:20191205182422p:plain
Google Sheets API を選択

「有効にする」を選択

f:id:shosfs:20191205182609p:plain
Google Sheets API を有効化

サービスアカウントの作成

サイドメニューの「認証情報」を選択

f:id:shosfs:20191205183117p:plain
認証情報

「認証情報を作成」から「サービスアカウントキー」を選択

f:id:shosfs:20191205183205p:plain
サービスアカウントキーを選択

サービスアカウントは「新しいサービスアカウント」を選択し、サービスアカウント名を入力する
今回はテスト用に「sheets-test-account」とした

f:id:shosfs:20191205183805p:plain
サービスアカウントキーの作成

作成後、秘密鍵を含む認証情報のJSONファイルがDLされるので保管しておく(ロジック部分で使うよ)

スプレッドシートの設定

操作を行う対象のスプレッドシートを用意する

作成後、サービスアカウントからの操作を受け付けるための設定を行う

まず、サービスアカウントのメールアドレスをコピっておく
「認証情報」から「サービスアカウントの管理」をクリック

f:id:shosfs:20191205184943p:plain
サービスアカウントの管理

作成したサービスアカウントのメールアドレスをコピー

f:id:shosfs:20191205185834p:plain
サービスアカウントのメールアドレスをコピー

対象のスプレッドシートを開き、右上の「共有」からコピったメールアドレスを入力し、「送信」を選択

f:id:shosfs:20191205190220p:plain
作成したサービスアカウントと共有

これでSheetsAPI経由で操作する事前準備が整ったので、Goでの操作プログラムを書いていく(長い)

Goでのスプレッドシート操作

今回テスト用に使うスプレッドシートは以下のようなやつ

f:id:shosfs:20191205200121p:plain
テスト用スプレッドシート
B列に更新日時を入れていて、今回のロジックではここの日時の更新を行う

作ったロジックは↓

github.com

取得した情報の出力と、B列の時刻を現在時刻に更新する

実行方法

実行する際にはサービスアカウントを作成した際にDLした認証情報のJSONファイルをsecret.jsonという名前でmain.goと同じディレクトリに置いておく
(gitなどにpushしないこと)

実行時にスプレッドシートIDを環境変数として渡すようにしていて、スプレッドシートのURLの以下の部分を指定する

https://docs.google.com/spreadsheets/d/<スプレッドシートID>/

実行

$ SHEET_ID=<スプレッドシートID> go run main.go

ロジック詳細

参照には

srv.Spreadsheets.Values.Get(spreadsheetID, readRange).Do()

更新には

srv.Spreadsheets.Values.Update(spreadsheetID, updateRange, &vr).ValueInputOption("RAW").Do()

を利用している

それぞれ第2引数に指定するreadRangeupdateRangeは文字列で、
GoDoc や、公式のサンプル が参考になる

!の前にはシート名を指定しているが、日本版?のスプレッドシートで作成すると「シート1」のようになるため、
指定する際は日本語でシート1!A:Bのようにしてあげる必要がある(今回は事前に「Sheet1」に変えておいた)

参照時の戻り値および、更新の際に指定する値(&vr)は、ValueRange型で、
ValueRange.Values[][]interface{}となっている

A列も一緒に合わせて更新する場合は以下のようになる(A列の後ろに"-test"を追加)

        var vr sheets.ValueRange
        now := time.Now().Format("2006/01/02 15:04:05")
        for i := 0; i < len(resp.Values); i++ {
-               vr.Values = append(vr.Values, []interface{}{now})
+               vr.Values = append(vr.Values, []interface{}{fmt.Sprintf("%s-test", resp.Values[i][0]), now})
        }
-       updateRange := "Sheet1!B1:B"
+       updateRange := "Sheet1!A1:B"
        if _, err = srv.Spreadsheets.Values.Update(spreadsheetID, updateRange, &vr).ValueInputOption("RAW").Do(); err != nil {
                log.Fatal(err)
        }

まとめ

事前準備がちょっとめんどくさいけど、ロジックは割とシンプル

参考