M1 MacでビルドしたコンテナイメージをOpenShiftにデプロイするまで

# 

# はじめに
意気揚々とM1 MacbookAirを購入して「もう1年半くらい経つしほぼIntel製と使い勝手変わらんやろ」と思い、手始めにOpenShiftにコンテナデプロイしようとしたところ、いろんな壁にブチあたりましたのでそれらを回避すべくとった対処法を備忘録としてまとめます。

## 前提条件
* OpenShift:v4.9 on AWS
    * 参考手順：https://qiita.com/ydo/items/874c7139119a17a4ecac
* 2020 M1 MacBook Air
    * 以下インストール済
        * homebrew
        * oc
    * OpenShiftにアクセスできること

# やりたかったこと
* ローカルでコンテナイメージをビルド→デプロイ
* ローカルビルドしたイメージをOpenShift Image RegistryにPush
* OpenShift上にPod/Service/Routeをデプロイ→アクセス確認

IntelCPU(x86-64)/Kubernetes/Dockerなら瞬殺できそうなものですが、今回はM1(arm64)/OpenShift/podmanという環境なので色々工夫が必要そう。

# Podmanのインストール
マニュアルによるとOpenShiftデフォルトのImage Registryには、Podmanを使用するような手順になっている。そのためM1 MacにPodmanをインストールします。

以下のサイトを参考にインストール
https://rheb.hatenablog.com/entry/podman-machine#podman-machine%E3%81%AE%E5%AE%9F%E8%A1%8C

```
$ brew install podman
```
コンソールを立ち上げ直して以下を実施。podmanの初期セットアップです。

```
$ podman machine init
Downloading VM image: fedora-coreos-35.20211029.2.0-qemu.aarch64.qcow2.xz: done
Extracting compressed file
```

PodmanのVMを立ち上げます。

```
$ podman machine start
INFO[0000] waiting for clients...
INFO[0000] listening tcp://127.0.0.1:7777
INFO[0000] new connection from  to /var/folders/pn/gs04f4fd7yj7xtkvw1m_csbm0000gn/T/podman/qemu_podman-machine-default.sock
Waiting for VM ...
Machine "podman-machine-default" started successfully
```

以下のコマンドで実際に立ち上がっているかわかります。

```
$ podman machine list
NAME                     VM TYPE     CREATED         LAST UP            CPUS        MEMORY      DISK SIZE
podman-machine-default*  qemu        31 seconds ago  Currently running  1           2.147GB     10.74GB
``` 

これでPodmanが使える状態になったつもりです。

# コンテナイメージをビルドしてみる
今回はPythonでHello worldを表示する簡単なイメージを作成しました。
まずはapp.pyを作成

```
$ cat << 'EOF' > app.py
from flask import Flask
  
app = Flask(__name__)
  
@app.route('/')
def index():
    return 'Hello world'
  
if __name__ == '__main__':
    app.run(debug=False, host='0.0.0.0', port=80)
EOF
```

続いてDockerfileの作成

```
$ cat << 'EOF' > Dockerfile
FROM python:3.9.6-slim
  
RUN pip install flask \
    && mkdir /app
  
COPY app.py /app/
WORKDIR /app
  
CMD ["python", "app.py"]
EOF
```

これでイメージをビルドする材料は揃いました。

```
$ ls
Dockerfile   app.py
```

実際にビルドします。

```
$ podman build -t my-app:latest .
STEP 1/5: FROM python:3.9.6-slim
Resolved "python" as an alias (/etc/containers/registries.conf.d/000-shortnames.conf)
...omit...
Successfully tagged localhost/my-app:latest
6dc1643647facb115369f9988fb11fb4a3c1c6e49d0c5b89b867f85d4ffc53f0
```

無事にビルドできました。余裕ですね。

# OpenShift Image RegistryにPushする。。。準備をする
次にOpenShiftのImage Registryにこのイメージを持っていきたいのですが、OpenShiftはBuildConfigによるクラスタ内でのビルドが一般的ですので、IPIインストールの状態のままでは外部からのイメージPushに対応できません。ここらへんがDokcerHubやECR/GCRとちょっと色が違うところですね。
ですので、このImage Registryに外部からログインできるよう設定します。

手順ですが、OpenShiftのマニュアルに記載があります。
https://access.redhat.com/documentation/ja-jp/openshift_container_platform/4.9/html/registry/registry-exposing-secure-registry-manually_securing-exposing-registry

手順に則り作業を進めます。
まずはImage Regisry用のRouteとしてDefaultRouteを使用するよう設定します。

```
$ oc patch configs.imageregistry.operator.openshift.io/cluster --patch '{"spec":{"defaultRoute":true}}' --type=merge
```

するとProject:openshift-image-registryにdefault-routeがデプロイされます。

```
$ oc get route -n openshift-image-registry
NAME            HOST/PORT                                                                             PATH   SERVICES         PORT    TERMINATION   WILDCARD
default-route   default-route-openshift-image-registry.apps.democluster.XXX.XXX.XXX.com          image-registry   <all>   reencrypt     None
```

次にPodmanでImage Registryにログインします。

```
$ HOST=$(oc get route default-route -n openshift-image-registry --template='{{ .spec.host }}')
$ podman login -u kubeadmin -p $(oc whoami -t) --tls-verify=false $HOST
Login Succeeded!
```

これでM1 Mac上からOpenShift Image RegistryにイメージをPushする準備が整いました。

## OpenShift Image RegistryにPushする！
一通り準備できたので実際にPushします。
まずは先ほどのイメージのtagを付け替えます。
対象のイメージ名を確認

```
$ podman images 
REPOSITORY                TAG         IMAGE ID      CREATED         SIZE
localhost/my-app          latest      6dc1643647fa  16 minutes ago  132 MB
docker.io/library/python  3.9.6-slim  65db2d7dbed0  4 months ago    121 MB
```
tagの命名規則ですが、`<default-routeのHOST>/<Openshift上のProject名>/<iamge名>:latest`のように付与します。以下のコマンドでは`ocp-demo`Project内のイメージとしてpushしています。

```
$ podman tag localhost/my-app:latest default-route-openshift-image-registry.apps.democluster.XXX.XXX.XXX.com/ocp-demo/my-app:latest
```

そしてPush！
※今回は証明書を設定してないので、オプションに`--tls-verify=false`を設定

```
$ podman push default-route-openshift-image-registry.apps.democluster.XXX.XXX.XXX.com/ocp-demo/my-app:latest --tls-verify=false

```
なんのコンソールログも出ませんが、これでPushは成功です。
ためしにOpenShift上でイメージを確認しましょう。

```
$ oc get imagestream | grep my-app
my-app        default-route-openshift-image-registry.apps.democluster.XXXX.XXXX.XXXX.com/ocp-demo/my-app        latest   About an hour ago
```

# OpenShift上にデプロイする
これでイメージのBuild→Pushができたので、あとはDeployです。
今回は簡単にPodとService、そしてMacのブラウザからアクセスできるようRouteを作成します。
まずはPodのyamlを作成

app-pod.yaml

```
apiVersion: v1
kind: Pod
metadata:
  name: flask
  labels:
    app: flask
spec:
  containers:
  - name: flask
    image: image-registry.openshift-image-registry.svc:5000/ocp-demo/my-app:latest
    ports:
    - containerPort: 80
```

imageですが、OpenShift上からImage Registryにアクセスする場合は内部アクセスになるため、`image-registry.openshift-image-registry.svc:5000/(パス/)イメージ名:バージョン`でイメージpullできます。

次にserviceのyamlを作成

```
apiVersion: v1
kind: Service
metadata:
  name: flask
spec:
  type: ClusterIP
  selector:
    app: flask
  ports:
    - protocol: TCP
      port: 80
      targetPort: 80
      name: http
```

デプロイしてみます。

```
$ oc new-project ocp-demo
$ oc apply -f .
pod/flask created
service/flask created
```

Podの状態を見てみましょう

```
$ oc get pods 
NAME    READY   STATUS             RESTARTS      AGE
flask   0/1     CrashLoopBackOff   2 (17s ago)   36s
```
**うまくいってない。。。**

とりあえず原因を探ります。

```
$ oc describe pod flask
...omit...
Events:
  Type     Reason          Age                From               Message
  ----     ------          ----               ----               -------
  Normal   Scheduled       83s                default-scheduler  Successfully assigned ocp-demo/flask to ip-10-0-136-247.ap-northeast-1.compute.internal
  Normal   AddedInterface  81s                multus             Add eth0 [10.129.0.27/23] from openshift-sdn
  Normal   Pulled          80s                kubelet            Successfully pulled image "image-registry.openshift-image-registry.svc:5000/ocp-demo/my-app-test:latest" in 532.909698ms
  Normal   Pulled          80s                kubelet            Successfully pulled image "image-registry.openshift-image-registry.svc:5000/ocp-demo/my-app-test:latest" in 71.807742ms
  Normal   Pulled          65s                kubelet            Successfully pulled image "image-registry.openshift-image-registry.svc:5000/ocp-demo/my-app-test:latest" in 142.873945ms
  Normal   Pulling         34s (x4 over 81s)  kubelet            Pulling image "image-registry.openshift-image-registry.svc:5000/ocp-demo/my-app-test:latest"
  Normal   Pulled          34s                kubelet            Successfully pulled image "image-registry.openshift-image-registry.svc:5000/ocp-demo/my-app-test:latest" in 130.168857ms
  Normal   Created         33s (x4 over 80s)  kubelet            Created container flask
  Normal   Started         33s (x4 over 80s)  kubelet            Started container flask
  Warning  BackOff         6s (x7 over 79s)   kubelet            Back-off restarting failed container
  
$ oc logs flask
standard_init_linux.go:228: exec user process caused: exec format error
```

コンテナ内でエラーを吐いているようです。
「なんだこのエラー？」と思ってググるとこんな記事がありました。
https://qiita.com/keita_ogawa/items/e115c46f1c8caf6fd34d

どうやらイメージビルドした環境とコンテナ実行環境のCPUアーキテクチャが異なるために発生しているみたいです。

解決策はDockerの際のものだったので、Podmanのコマンドを調べます。
こちらに`podman build`のオプションが記載されています。
https://docs.podman.io/en/latest/markdown/podman-build.1.html

Dockerと同様`--platform`をつければいけそうですが、`<OS>/<Arch>` のような書き方が必要で、その組み合わせはgoの環境変数を使うとのこと。以下のURLで調べます。
https://golang.org/doc/install/source#environment
今回は`linux/amd64`が該当します。


では改めて、`--platform`オプションをつけてビルドしてみます。

```
$ podman build --platform linux/amd64 -t my-app:v1.1 .
STEP 1/5: FROM python:3.9.6-slim
Resolved "python" as an alias (/etc/containers/registries.conf.d/000-shortnames.conf)
Trying to pull docker.io/library/python:3.9.6-slim...
Getting image source signatures
Copying blob sha256:1374279231a56d30ab37ec576d0d9f339d627fdaa5e5b99a1a1c6216234e6731
Copying blob sha256:99046ad9247f8a1cbd1048d9099d026191ad9cda63c08aadeb704b7000a51717
Copying blob sha256:dae61f727682a7d9dbb8974480f2674f80187673a550423d383930a4e80cf237
Copying blob sha256:466485ee627754b8f2ee4a302965fdea56ac3b2ed509b4f54e9796ab4162e837
Copying blob sha256:de739b0566731810358ff60001048daf2672b9c3a9125a3bb2fa65165f26a8eb
Copying blob sha256:466485ee627754b8f2ee4a302965fdea56ac3b2ed509b4f54e9796ab4162e837
Copying blob sha256:99046ad9247f8a1cbd1048d9099d026191ad9cda63c08aadeb704b7000a51717
Copying blob sha256:1374279231a56d30ab37ec576d0d9f339d627fdaa5e5b99a1a1c6216234e6731
Copying blob sha256:dae61f727682a7d9dbb8974480f2674f80187673a550423d383930a4e80cf237
Copying blob sha256:de739b0566731810358ff60001048daf2672b9c3a9125a3bb2fa65165f26a8eb
Copying config sha256:c6e50ab8d4256a0d25bb6246d77a61d5f7407b898278f97c89cf98ef91fd2292
Writing manifest to image destination
Storing signatures
STEP 2/5: RUN pip install flask     && mkdir /app
exec container process `/bin/sh`: Exec format error
Error: error building at STEP "RUN pip install flask     && mkdir /app": error while running runtime: exit status 1
```

**うまくいかないじゃん。**

エラーを見るとなんだかさっきと似たような事象っぽいです。
内容を頼りに色々調べると、こんなissueが。
https://github.com/containers/podman/issues/12144

PodmanのVMにqemu-user-staticをインストールして複数のアーキテクチャに対応できる環境を作ろうってことですね。

```
$ podman machine ssh sudo rpm-ostree install qemu-user-static
Warning: Permanently added '[localhost]:53022' (ED25519) to the list of known hosts.
Checking out tree 68a745f...done

...omit...

Changes queued for next boot. Run "systemctl reboot" to start a reboot
```

そして再起動

```
$ podman machine ssh sudo systemctl reboot
```

この状態で先ほどのビルドコマンドを実行してみます。

```
$ podman build --platform linux/amd64 -t my-app:v1.1 .
STEP 1/5: FROM python:3.9.6-slim
Resolved "python" as an alias 

...omit...

Successfully tagged localhost/my-app:v1.1
f7a36870966d7a0a1dba7348b6ca03c0855eaf32e7376259221ca4619481f028
```
**ビルド通った！**

早速OpneShift上で動かしてみましょう。
タグ付け→Push

```
$ podman tag localhost/my-app:v1.1 default-route-openshift-image-registry.apps.democluster.XXXX.XXXX.XXXX.com/ocp-demo/my-app:v1.1
$ podman push default-route-openshift-image-registry.apps.democluster.XXXX.XXXX.XXXX.com/ocp-demo/my-app:v1.1 --tls-verify=false
```

v1.1をつけたので、app-pod.yamlのimageを書き換えます。

```
apiVersion: v1
kind: Pod
metadata:
  name: flask
  labels:
    app: flask
spec:
  containers:
  - name: flask
    image: image-registry.openshift-image-registry.svc:5000/ocp-demo/my-app:v1.1
    ports:
    - containerPort: 80
```

エラーを繰り返しているPodを削除して、再作成！

```
$ oc delete pod flask
pod "flask" deleted
$ oc apply -f app-pod.yaml
pod/flask created
```

確認してみます

```
$ oc get pods
NAME    READY   STATUS    RESTARTS   AGE
flask   1/1     Running   0          24s
```
**動いたーーー！！**

ですがまだコンテナが動いただけなので、正常にアクセスできるかわかりません。
なので、routeを作成してMacのブラウザからアクセスできるか確認します。
以下のコマンドでrouteを作成

```
$ oc expose service flask
```

しばらくするとrouteが作成され、HOSTが表示されます。

```
$ oc get route
NAME    HOST/PORT                                                     PATH   SERVICES   PORT   TERMINATION   WILDCARD
flask   flask-ocp-demo.apps.democluster.XXXX.XXXX.XXXX.com                   flask      http                 None
```

このHOSTに`http://`をつけてブラウザでアクセスすると
![](https://i.imgur.com/KOzHXD6.png)

ということで、やっとデプロイすることができました。。。！

# おわりに
これまでCPUのアーキテクチャを気にしてビルド・デプロイを行なってこなかったので、いい勉強になりました。
これからもまだまだ苦労はありそうですが、なんとかM1 Mac上でOpenShiftの開発環境を整備できるよう対応していきます。

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