nazolabo

フリーランスのWebエンジニアが近況や思ったことを発信しています。

M1(Apple Silicon) Mac に google-cloud-sdk を無理やりインストールする(2021/01/19時点)

公式からダウンロードしてきた tarball からインストールすると以下のエラーが出ます。

/google-cloud-sdk ❯❯❯ ./install.sh
Welcome to the Google Cloud SDK!
Traceback (most recent call last):
  File "/Users/nazo/google-cloud-sdk/bin/bootstrapping/install.py", line 12, in <module>
    import bootstrapping
  File "/Users/nazo/google-cloud-sdk/bin/bootstrapping/bootstrapping.py", line 32, in <module>
    import setup  # pylint:disable=g-import-not-at-top
  File "/Users/nazo/google-cloud-sdk/bin/bootstrapping/setup.py", line 57, in <module>
    from googlecloudsdk.core.util import platforms
  File "/Users/nazo/google-cloud-sdk/lib/googlecloudsdk/__init__.py", line 23, in <module>
    from googlecloudsdk.core.util import importing
  File "/Users/nazo/google-cloud-sdk/lib/googlecloudsdk/core/util/importing.py", line 23, in <module>
    import imp
  File "/opt/homebrew/Cellar/python@3.9/3.9.1_6/Frameworks/Python.framework/Versions/3.9/lib/python3.9/imp.py", line 23, in <module>
    from importlib import util
  File "/opt/homebrew/Cellar/python@3.9/3.9.1_6/Frameworks/Python.framework/Versions/3.9/lib/python3.9/importlib/util.py", line 2, in <module>
    from . import abc
  File "/opt/homebrew/Cellar/python@3.9/3.9.1_6/Frameworks/Python.framework/Versions/3.9/lib/python3.9/importlib/abc.py", line 17, in <module>
    from typing import Protocol, runtime_checkable
  File "/opt/homebrew/Cellar/python@3.9/3.9.1_6/Frameworks/Python.framework/Versions/3.9/lib/python3.9/typing.py", line 26, in <module>
    import re as stdlib_re  # Avoid confusion with the re we export.
  File "/opt/homebrew/Cellar/python@3.9/3.9.1_6/Frameworks/Python.framework/Versions/3.9/lib/python3.9/re.py", line 124, in <module>
    import enum
  File "/Users/nazo/google-cloud-sdk/lib/third_party/enum/__init__.py", line 26, in <module>
    spec = importlib.util.find_spec('enum')
AttributeError: module 'importlib' has no attribute 'util'

curl でインストールするともう少し進みますが以下のエラーが出ます。

~ ❯❯❯ curl https://sdk.cloud.google.com | bash
(略)
Welcome to the Google Cloud SDK!

To help improve the quality of this product, we collect anonymized usage data
and anonymized stacktraces when crashes are encountered; additional information
is available at <https://cloud.google.com/sdk/usage-statistics>. This data is
handled in accordance with our privacy policy
<https://cloud.google.com/terms/cloud-privacy-notice>. You may choose to opt in this
collection now (by choosing 'Y' at the below prompt), or at any time in the
future by running the following command:

    gcloud config set disable_usage_reporting false

Do you want to help improve the Google Cloud SDK (y/N)?


This will install all the core command line tools necessary for working with
the Google Cloud Platform.

Beginning update. This process may take several minutes.
ERROR: (gcloud.components.update) The following components are unknown [anthoscli, kuberun].

これは anthoscli kuberunコンポーネントがインストールできない( gcloud components install anthoscli kuberun と同等)のが原因です。

回避するには、curl で上記エラーが出た時点で ~/google-cloud-sdk にファイル一式があるはずなので、移動して以下のように実行します。

~/google-cloud-sdk ❯❯❯ ./install.sh --override-components core gcloud-deps bq gcloud gsutil
Welcome to the Google Cloud SDK!

(略)

Your current Cloud SDK version is: 323.0.0
Installing components from version: 323.0.0

┌────────────────────────────────────────────────────────────────────────────┐
│                    These components will be installed.                     │
├─────────────────────────────────────────────────────┬────────────┬─────────┤
│                         Name                        │  Version   │   Size  │
├─────────────────────────────────────────────────────┼────────────┼─────────┤
│ BigQuery Command Line Tool                          │     2.0.64< 1 MiB │
│ BigQuery Command Line Tool (Platform Specific)2.0.58< 1 MiB │
│ Cloud SDK Core Libraries (Platform Specific)2020.07.10< 1 MiB │
│ Cloud Storage Command Line Tool                     │       4.573.5 MiB │
│ Cloud Storage Command Line Tool (Platform Specific)4.51< 1 MiB │
└─────────────────────────────────────────────────────┴────────────┴─────────┘

For the latest full release notes, please visit:
  https://cloud.google.com/sdk/release_notes

(略)

For more information on how to get started, please visit:
  https://cloud.google.com/sdk/docs/quickstarts

インストールに成功しました。

override-components で指定した引数は、以下で確認したものからエラーが出るものを抜いたものになります。

~/google-cloud-sdk ❯❯❯ cat bin/bootstrapping/.default_components
["core", "gcloud-deps", "bq", "gcloud", "gsutil", "anthoscli", "kuberun"]%

anthoscli も kuberun も、おそらく無くて困る人は多くないのではないかと思いますので、抜いてインストールしてしまっていいと思います。

2020年の仕事と技術の振り返り

今年は合計2社にお世話になりました。ところでフリーランスで関わった会社名とか公表するものなのでしょうか?

関わり方

1社最大週2で、なるべく事業ドメインから遠いところをお手伝いする、というスタンスで働きました。事業ドメインに直接関わるものだと大抵はスピード感(主に緊急対応という意味で)が必要になるため、なるべくそこから遠いけど役に立つ内容で関われるものでお手伝いしております。

1社週2にしているのは、複数社の案件を同時に受けたいというのがありましたが、実際には私の知名度が足りなくそこまで仕事が回ってくることはありませんでした。数ヶ月だけ週2 x 2社の状態はありました。週5は疲れてしまうので当面はしない予定です。

原則として準委任契約による月額払いでの契約をお願いしております。これは私は一人でアプリを全部仕上げたりデザインを納品するような作業は得意としておらず、テクニカルな困りごとを臨機応変に対応するという内容を中心に活動しているためです。また原則フルリモートでお願いしております。

Rails によるアプリケーション開発 1社目

Rails のバージョンを 4 -> 5 に上げる仕事、同時に Webpacker のバージョンも同様に上げる仕事を中心に行いました。

Rails 側は元の書き方がそこそこ行儀が良い( Rails そのものを魔改造していない)ために問題になることは少なかったですが、Webpacker 側は動かないものが大量発生して大変でした。アップグレード作業は定形で引っかかる箇所もありますが、どちらかというと固有の事情を解決することが多そうなので、どれだけ全体を把握できるかが勝負になると思います。

Elixir ( Phoenix ) + Vue ( Nuxt ) + AWS EKS によるアプリケーション開発

Elixir 全く書いたことなかったのに誘っていただいて貴重な経験ができました。今年はほぼこの案件を中心に動いていました。実稼働している k8s を触れたのも良い経験になりました。

Elixir はパターンマッチが強力なため、今までとはちょっと考え方を変えて書かないといけないところもあり、頭の体操的な面白さがありました。一方で Erlang/OTP 側がわからないと詰まるところもたまにあり、そのあたりの調査力はまだまだ不足していると感じました。あとドキュメントが少ないので大体ソースコードを直接追うことが多いのも大変かなと思います。利用者がもっと増えてほしいところです。

アプリケーション側以外で大きくやったこととしては、以下のようなものがあります。

  • 既存インフラの Terraform 化(Terraform Cloud)
  • ExUnit でのテスト整備
  • Lamnda@Edge での OIDC 認証
  • Headless CMS の採用

特に Terraform 化では、既存のインフラとの整合性を保ちつつ IaC しやすい構造を作るというところで多少矛盾が生じるところもあり、落とし所を見つけるのが大変でした。「ここはインポートしたけど新しく追加しないで!」というような感じのコメントを残したりすることでなるべく今後も秩序を維持できるように残しておきました。

前職でアーキテクチャ設計やCI/CD設計などを全て自分がやるしかなかった経験が生かされ、ある程度構成を見ただけで実用的なTerraform分割の道筋がすぐ見えるようになったと思います。またCI/CDワークフローも整備し、誰でも安全にデプロイできるようにしておきました。

その他プライベートでやったようなこと

ECSのタスクスケジューラをどうにかしたかった

AWS ECSのバッチ処理をもう少し使いやすくできないかなーと試行錯誤し、Goでシンプルなスケジューラーでも書こうと思ったのですが、すぐにやる気が消えてしまったのでやめました。ECSの異常終了の検知が思ったより簡単だったのでそれでいいかなという感じもします。( AWS ECSのタスクが異常終了したらログURL付きでSlackに通知する - nazolabo

現在は MWAA( Amazon Managed Workflows for Apache Airflow (MWAA) のご紹介 | Amazon Web Services ブログ )も存在し、Airflow側でもawslogsから実行結果を取得する機能が入っていますので(Airflowのタスク実行環境を分離する|Dentsu Digital Tech Blog)、ECSのタスクスケジューリングでは機能不足な場合でも困ることはないのではないかと思います。

Vue 3.0 (というか composition API

使うかどうかわからないのですが、Vue/Nuxtの最新を追うためにいくつかコードを書いていました。 https://github.com/nazo/binsen/pull/7

現在のVue/NuxtはReact/Nextの後追いになっている感じが強く、もともとNuxtのほうが実用性としては先行していたはずだったのにというところから、もっとReactとは違う良さを出していってほしいなという感じがします。とはいえ現時点でのフロントエンドはNextかNuxt以外は選択肢にはないと思います。

WebAssembly

nazo.hatenablog.com

感想と今後

週2で開発業務は翌週に記憶が切れていることが多く、単純にもう1日やったほうがいいなと思うこと多かったので、事業ドメインから外れていてもなかなか厳しいと思いました。よほどピンポイントに専門的な内容を行う場合(技術顧問的な?)でない限り、週3はあったほうがいいのではないかと思います。

声をかけていただいた会社さんの中には良いポジションを提示してくれたところもあったのですが、週5・出勤あり・正社員でと言われるとお断りするしかないため、申し訳なく思うところもありました。とはいえ今の働き方が現時点では一番良いと思っているので、当面このままで続けていこうと思います。(将来的には変えるとは思います)

こちらから積極的に声をかけていないということもあり、残念ながら現在仕事が途切れてしまっている状態なので、現在積極的に仕事を募集しております。気軽にご連絡をお待ちしております。詳しくは ポートフォリオ · GitHub をご覧下さい。

それでは来年も皆様宜しくお願い致します。

rust-nesをwasm化した

github.com

相変わらずOAM描画もキー入力も実装されていないのにNESエミュレーターってレベルじゃないだろという感じですが、ついでに元のrust-nesよりいくつか修正してwasmで動くようにしました。ちなみに元のバージョンはSDLです。

GitHub - rustwasm/wasm-pack: 📦✨ your favorite rust -> wasm workflow tool! を使うと、手軽にRustでWebアプリケーションを書くことができます。コアになるライブラリは GitHub - rustwasm/wasm-bindgen: Facilitating high-level interactions between Wasm modules and JavaScript なのですが、wasm-packはこれにWebpackなどを追加し、ホットリロード的なものとかを最初から提供してくれるものです。

Examples - The `wasm-bindgen` Guide が充実しており、今回必要だったcanvasやfetch、requestAnimationFrameなどはもちろん、WebGLやWebRTCのサンプルもあり、すぐに試すことが可能です。Rustが書ける方であればすぐに使えると思います。

WebAssemblyの現実的な用途としてはWeb的なものよりバイナリ操作的なものが中心になるのではないかと思うのですが、突然使うことになるかもしれないので今のうちに触っておいて損はしないのかなと思っています。

久々に触ってて面白かったので、もう少し機能を実装したいところです。

nuxt/http で axios-module の credentials: true を再現する

https://github.com/nuxt/http は実質 https://github.com/nuxt-community/axios-module の後継に当たるもので、fetch APIをベースにした https://github.com/sindresorhus/ky を利用しています。

qiita.com

Migration Guides - Http Module を見てわかる通り、axios-module と比較しても名前が変わっただけのような作りになっており、置換だけでほぼ完全に移行することができますが、nuxt/http では credentials: true オプションが削除されています。今回はこれをどうしたらいいのかという話になります。

そもそも axios-module の credentials: true とは?

axios-module の credentials: true は、 XMLHttpRequest に対し withCredentials = true をデフォルトで設定するためのオプションです。クロスドメインCookie等の資格情報をXHR経由で渡す時に設定するものです。

yamory.io

withCredentials オプションは XHR 固有のもののため、fetch API には存在しません。代わりに credentials = 'omit' | 'same-origin' | 'include' というオプションが存在します。

  • omit : 同一ドメイン上でも資格情報が送られない
  • same-origin : 同一ドメイン上でのみ資格情報が送られる
  • include : 全てのドメインで資格情報が送られる

XHR での withCredentials = false が 'same-origin' で、 withCredentials = true が 'include' に該当します。(ただし polyfill を使うと omit に該当する XHR のオプションが存在しないために同一ドメイン上では資格情報の送信が行われます) なおサーバー側の Access-Control-Allow-Credentials 等は状況に応じて別途必要です。

nuxt/http で再現する

該当のオプションが存在しない nuxt/http で再現するには、デフォルト値を無理やり書き換える必要があります。プラグインで対応します。参考:Globally set options.Credentials · Issue #92 · nuxt/http · GitHub

export default ( { $http } ) => {
    $http._defaults.credentials = 'include'; 
}

TypeScriptではプロパティにアクセスができないので、型を破壊して強引にプロパティを変更します。

import { defineNuxtPlugin } from '@nuxtjs/composition-api';
import { Context } from '@nuxt/types/app';

export default defineNuxtPlugin(({ $http }: Context) => {
  ($http as any)._defaults.credentials = 'include';
});

まとめ

セキュリティ上の懸念点があるためにこのような対応が行われていると思いますので、強引に変えるのは良くないのではないかと思います。

nuxt/http では setToken() というメソッドが用意されており、 Authorization ヘッダをこちらでグローバルに指定することが可能です。こちらで資格情報を管理するのが良いのではないかと思いますが、axios-module と互換性があると言うのであれば credentials 指定があっても良かったのではないかと思います。

Windows の VSCode で Microsoft C++ ツールセットを使用する

GitHub - nazo/rust-nes: Rust NES Emulator (Work In Progress)WindowsVSCodeデバッグしようとしたのですが、そのままではビルドが通りませんでした。

基本情報として、Windows 上で Microsoft C++ツールセット( Visual Studio のツールセット)を Visual Studio 以外から使うには、vcvarsall.bat を呼び出して環境変数を設定する必要があります。rust-nesSDL を使っていますので、当然 Windows API が呼び出せなければいけません。

VSCode のショートカットのプロパティを開き、「リンク先」を以下のように変更します。

"C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\Common7\Tools\VsDevCmd.bat" -arch=amd64 & "C:\Program Files\Microsoft VS Code\Code.exe"

パスは Visual Studio のバージョンなどでも変わるので、適宜自分の環境に合わせて書き換えてください。

launch.json 側でどうにかする方法もあるとは思うのですが、多分これが一番早いと思います。

Windows 環境にはあまり詳しくないので、用語などに多少の誤りがある可能性があります。

参考

docs.microsoft.com

github.com

github.com

AWS ECSのタスクが異常終了したらログURL付きでSlackに通知する

概要

AWS ECSのタスク停止はCloudWatch Eventで検知できますが、このイベントパターンはChatBotに対応していないので、Lambdaから自前でSlackに投げる必要があります。

ECSの停止状態からログを見たい時、一覧からどのタスクが何かを発掘して…とか、CloudWatch Logsから発掘して…とかは大変です。

上記の両方を満たすため、LambdaからSlack通知を行い、かつ詳細のURLを添付するようにします。

Lambda

以下の関数を作ります。IAMで ecs:DescribeTaskDefinition を allow しておく必要があります。

const AWS = require('aws-sdk');
const https = require('https');
const qs = require('querystring');

function postToSlack(message) {
  return new Promise ((resolve, reject) => {
    const postData = qs.stringify(message);
    const req = https.request({
      hostname: 'slack.com',
      path: '/api/chat.postMessage',
      method: "POST",
      headers: {
        'Content-Type': 'application/x-www-form-urlencoded',
        'Content-Length': Buffer.byteLength(postData)
      }
    });
    req.on('response', res => {
      resolve(res);
    });

    req.on('error', err => {
      reject(err);
    });
    req.write(postData);
    req.end();
  });
}

exports.handler = async function(event) {
  if (event.detail.containers.find((containerEvent) => containerEvent.exitCode != 0) === undefined) return;

  const region = event.region;
  AWS.config.update({region});
  const startedAt = event.detail.startedAt;
  const stoppedAt = event.detail.stoppedAt;
  const taskDefinitionArn = event.detail.taskDefinitionArn;
  const ecs = new AWS.ECS();
  const taskDefinition = await ecs.describeTaskDefinition({taskDefinition: taskDefinitionArn}).promise();
  const ecsCluster = event.detail.clusterArn.split("/")[1];
  const ecsTaskID = event.detail.taskArn.split("/")[1];
  const detailURL = `https://${region}.console.aws.amazon.com/ecs/home?region=${region}#/clusters/${ecsCluster}/tasks/${ecsTaskID}/details`;
  let text = `Task is STOPPED : ${taskDefinitionArn}\nRunning at: ${startedAt} - ${stoppedAt}\nDetail: ${detailURL}\n`;

  taskDefinition.taskDefinition.containerDefinitions.forEach((containerDefintion) => {
    const name = containerDefintion.name;
    const conatinerEvent = event.detail.containers.find((containerEvent) => containerEvent.name == name);
    const logDriver = containerDefintion.logConfiguration.logDriver;
    text = text + `\`\`\`\nContainer: ${name}\nExitCode: ${conatinerEvent.exitCode}\n`;
    if (logDriver == "awslogs") {
      const logGroup = containerDefintion.logConfiguration.options["awslogs-group"];
      const prefix = containerDefintion.logConfiguration.options["awslogs-stream-prefix"];
      const logStream = `${prefix}/${name}/${ecsTaskID}`;
      const LogURL = `https://${region}.console.aws.amazon.com/cloudwatch/home?region=${region}#logsV2:log-groups/log-group/${encodeURIComponent(logGroup)}/log-events/${encodeURIComponent(logStream)}`;
      text = text + `LogURL: ${LogURL}\n`;
    }
    text = text + "```\n";
  });

  const message = {
    token: process.env.SLACK_ACCESS_TOKEN,
    channel: process.env.SLACK_CHANNEL,
    text: text
  };

  await postToSlack(message);
};

環境変数 SLACK_ACCESS_TOKEN SLACK_CHANNEL の設定が必要です。

CloudWatch Event

停止だけ取ります。起動も取りたい場合は STOPPED の設定を消してください。

{
  "detail-type": [
    "ECS Task State Change"
  ],
  "source": [
    "aws.ecs"
  ],
  "detail": {
    "lastStatus": [
      "STOPPED"
    ]
  }
}

ターゲットに Lambda 関数で上で作った関数を指定します。

結果

f:id:nazone:20200621113237p:plain

このような通知が飛んでくるようになります。リンクをクリックするとタスク詳細画面やCloudWatch Logsの画面にすぐ飛ぶことができ、原因の把握に役立ちます。

タスクの変化が少ない環境なら全ての停止を通知してもいいと思いますし、より少ないなら起動と停止の両方を通知してもいいと思います。用途によってカスタマイズしてください。

今回のテストで使った CDK のソースは https://github.com/nazo/cdk-ecs-stop-task-slack になります。

クラウドインフラにおけるResourceとDeployment

何の話

クラウドインフラ(AWSGCPなど、その中で取り扱うもの)で、ResourceとDeploymentという境界を分けるとうまく整理できるのではないかという話です。

※この話は私が勝手に言っている内容です。用語定義や概念など一般的なものでないので、あくまで参考として読んでいただければと思います。

前提

現代のアプリケーションは12Factor Appに基づき、Buildフェーズで作られたビルドに環境変数で設定を当てることによってデプロイが完了します。RAILS_ENVのような設定は使いません。

またCIでビルドされたイメージには一意のタグが付き(いわゆるlatest運用ではない)、それに環境変数を当てることで一意のリリースが作成されます。12Factor Appの「V. ビルド、リリース、実行」も参考にしてください。

Resource

一般的なクラウドリソースをResourceと呼びます(ダジャレを言いたいわけではないです)。

Resourceは一度作ると「そのResourceが持っている機能で」操作を行うことがありますが、Resourceそのものを作成した時と同じ方法で操作することはほぼありません。

Terraformは現代においてベストなIaCツールの1つですが、その理由に「そうなるべきである状態を定義し、インフラ側をその状態になるようにする」という構造で作られているからです。

「あるべき状態を定義してそれを実行する」という店ではAnsibleもそうでしたが、Ansibleは書き方が手続き型で、かつ何でも書けてしまうという難点がありました。しかしTerraformは書き方を大幅に制限することで、「インフラのあるべき状態」以外のことはほぼ書けないようになっています。

Deployment

ビルドしたリソースを配置する作業に付随するものはDeploymentです。

Resourceは一度作ると「その作業プロトコルで」それ以上の操作をすることは基本的にありませんが、Deploymentは「同一の作業プロトコルで」初期配置やビルドの更新などを行います。

例えばk8sのコントロールプレーン(AWSならEKS)はResourceですが、その上で行うk8sの操作は全てDeploymentと分類できます。(k8s自体のDeploymentとは別の話です。DaemonSetだろうとLoadBalancerだろうとここでは全てDeploymentです)

ちょっと話が逸れますが、AWS RDSそのものはResourceで、その中に入るDBのデータはDeployment、というようなイメージを、インフラ全体とアプリケーションに対して定義しているような感じだと思っています。

なぜ分けるのか

例えばAWS ECSのタスク定義には通常最新のイメージのタグを記述しますが(これはk8sでも同様ですが)、最新のイメージのタグは通常CIで決定されますので、アプリケーションのビルドCIと同居している必要があります。タスク定義のライフサイクルがCIと違う場所に存在する、例えばクラウドリソースと同じライフサイクルで存在すると、CIに入れてしまうとアプリケーションの更新とクラウドリソースの更新が混ざる可能性がありますし、CIに入れないと最新のタグを取得できないので最新のイメージで適切に更新する方法がありません。(latestタグ運用はダメ絶対)

つまり、Resourceというのは「変化しないもの」であり(厳密には自動メンテナンスで勝手にバージョンを上げられて…みたいなこともありますが)、Deploymentは「変化するもの」ということです。

私はECSのサービスやタスク定義をTerraformで他のResourceと同一ライフサイクルで管理したことがありましたが、上記問題により辛い結果になっていました(その時はlatest的なタグをCIから同時にpushすることで、Terraformから更新する時だけは仕方なくそちらを参照するという苦肉の策で回避していました)。TerraformでECSタスク定義やk8sを管理すること自体は否定しませんが(それが良いかどうかは別として)、Resourceと同一のライフサイクルにするのは無理があるのではないかと考えています。

k8sYAMLは一見「あるべき状態を書く」ようになっていますが、それは通信プロトコル的な部分の話で、現実にはCIで毎回違うイメージのタグに書き換えてデプロイすることになります。このYAMLの部分は下記参考記事のInfrastructure as Dataであり(この呼び方は議論の余地があると思いますが、概念の分類としては良いと思います)、それを実運用するには通常その上にCodeが必要になります。

deeeet.com

個人的には、Deploymentというのはインフラというより「アプリケーションという名のデータ」というほうが近く、そもそもこれはInfrastructureなのか?と思うところがありますが、これがk8sになるとALB Ingress Controllerやらexternal-dnsやらでクラウドリソースそのものも制御できてしまうので、このあたりがk8sをより複雑にさせているのではないかと思うところです。

まとめ

用語は正直何でもいいし英語圏の人たちにちゃんと決めてほしいのですが、ともかく「デプロイするコンテナ」というのは通常のクラウドリソースとは別物なのではないか、ということが言いたかったことです。

Deploymentは「あるべき状態」が日々変化するため、そういう目的で管理するのが難しいですが、しかし統一的な「あるべき状態」を維持できるツールにしておかないと管理できていないサーバー(「構成ドリフト」とか「スノーフレークサーバー」とかそういうやつ)が生まれます。Resourceとはライフサイクルを分けつつ、コードと実態が乖離しない、そもそも乖離できないようにする構成を考えましょう。