AWS S3 に静的ウェブサイトをホスティングして独自ドメインで公開するまで

この記事は個人ブログと同じ内容です

www.ritolab.com


静的ウェブサイトを Amazon S3ホスティングし、独自ドメインで公開するまでの環境構築を terraform を使って行っていきます。

実行環境

今回の実行環境は以下になります

  • Terraform v0.15.1

コードはモジュール化を行わずに一つの tf ファイルに書いていきます。

今回使う AWS のサービスは、S3 と Route53 です。

環境変数について

terraform で構成を定義していくにあたり、環境変数を tfvars に記述しておきますが、以下のようになっています。

terraform.tfvars

aws_id           = 123456789
aws_access_key   = "XXXXXXXXXXXXXXXXXXXX"
aws_secret_key   = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
aws_region       = "ap-northeast-1"
domain_name      = "example.com"
domain_host_name = "www"
  • AWS のアカウント情報と、デフォルトのリージョンとして東京リージョンを指定しています。
  • ネイキッドドメインとホスト名をそれぞれ分けて定義しています。

変数の設定

これから使用する値について変数とプロバイダを設定しておきます。

main.tf

# variables
variable "aws_id" {}
variable "aws_access_key" {}
variable "aws_secret_key" {}
variable "aws_region" {}
variable "domain_name" {}
variable "domain_host_name" {}

provider "aws" {
  access_key = var.aws_access_key
  secret_key = var.aws_secret_key
  region     = var.aws_region
}

locals {
  fqdn = {
    name = "${var.domain_host_name}.${var.domain_name}"
  }
  bucket = {
    name = local.fqdn.name
  }
}
  • AWS プロバイダを定義しています。
  • Route53 や S3 で使用するための FQDNBucket 名の変数を作成しています。

アクセスログ用のバケット作成

S3 にホスティングする場合でも、アクセスログを取る事ができます。

docs.aws.amazon.com

そのためのバケットを S3 に作成します。(これが無くてもサイト公開はできるので、必要なら作成してください)

main.tf

## AWS アカウントのユーザー
data "aws_canonical_user_id" "current_user" {}

## S3 for app logs
resource "aws_s3_bucket" "app_logs" {
  bucket = "${local.bucket.name}-logs"

  grant {
    id          = data.aws_canonical_user_id.current_user.id
    permissions = ["FULL_CONTROL"]
    type        = "CanonicalUser"
  }
  grant {
    permissions = ["READ_ACP", "WRITE"]
    type        = "Group"
    uri         = "http://acs.amazonaws.com/groups/s3/LogDelivery"
  }
}
  • 権限は ACL ポリシーで設定しています。
    • 前者(1 つめの grant)は、コンソール画面から操作できるように、AWS アカウントの正規ユーザーの権限を設定しています。
    • 後者(2 つめの grant)は、ログを配信するための権限です。

アプリケーション用のバケット作成

静的なウェブサイトのソースコードを配置する S3 バケットを作成します。

main.tf

## S3 for Static Website Hosting
resource "aws_s3_bucket" "app" {
  bucket = local.bucket.name
  acl    = "public-read"
  policy = templatefile("bucket-policy.json", {
    "bucket_name" = local.bucket.name
  })

  website {
    index_document = "index.html"
    error_document = "error.html"
  }

  logging {
    target_bucket = aws_s3_bucket.app_logs.id
    target_prefix = "log/"
  }
}

バケット名は FQDN と同じにします。

templatefile として挿入している json は以下のようになっています。

bucket-policy.json

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "PublicReadGetObject",
            "Effect": "Allow",
            "Principal": "*",
            "Action": [
                "s3:GetObject"
            ],
            "Resource": [
                "arn:aws:s3:::${bucket_name}/*"
            ]
        }
    ]
}

logging の部分は、前項でアクセスログ用のバケットを作成した場合に指定するので、作っていなければ指定不要です。

バケットを作成したら index.html を配置してアクセスしてみると表示されます。

f:id:ro9rito:20210507113705p:plain

独自ドメインの設定

WEB サイトの公開はできましたが、ここまでだと URL が amazonaws.com なので、独自ドメインを設定します。

ドメインとホストゾーンの作成について

ドメインですが、前提として、Route53 に登録済みのドメインを使用します。

また、ネイキッドドメインのホストゾーン登録は済ませてある前提で進めるので、例えば www.example.com で S3 にホスティングしたサイトを公開するなら、そのネイキッドドメインである example.com のホストゾーンは作成しておいてくだださい。

docs.aws.amazon.com

これだけです。

ちなみに、ドメインのネームサーバーと、作成したホストゾーンの NS レコードに設定されているネームサーバーが同じ事を確認して、もし違うようならどちらかに揃える必要があります。

レコードの設定

terraform で Route53 にレコードを作成します。

main.tf

# Route 53
## ネイキッドドメインのホストゾーン情報
data aws_route53_zone "naked" {
  name = var.domain_name
}

## A レコード作成(to S3 Bucket)
resource "aws_route53_record" "main_a" {
  zone_id = data.aws_route53_zone.naked.zone_id
  name    = local.fqdn.name
  type    = "A"
  alias {
    evaluate_target_health = true
    name = "s3-website-${var.aws_region}.amazonaws.com"
    zone_id = aws_s3_bucket.app.hosted_zone_id
  }
}
  • A レコードを作成しています。
  • ホストゾーンは、作成済みのネイキッドドメインのホストゾーン情報を取得して zone_id に指定しています。
  • エイリアスの設定として、S3 のウェブサイトエンドポイントを設定します。

docs.aws.amazon.com

動作確認

レコードを作成したら、あとは DNS の浸透を待ってアクセスすると表示されます。

f:id:ro9rito:20210507113920p:plain

S3 にホスティングしたウェブサイトを独自ドメインで公開できました。

OASとTypescript ベネフィット編

4月からRoxxに合流したkotamatsuiです(※kotamatさんとは別人です)。よろしくおねがいします。 こちらの記事はZennからの転載になります zenn.dev


はじめに

スキーマ駆動開発という手法が数年前からたびたび話題にあがっています。 スキーマ駆動開発の紹介は既に多数の先人が記事を書いておられるのでそちらをご覧いただくこととして、この記事ではOAS、特にOpenAPI Genertorを利用したスキーマ駆動開発を導入することによる、フロントエンド目線で見たときに実作業者が得られるベネフィットをまとめてみたいと思います。

実際筆者は最近、APIの仕様書を渡されて2つのエンドポイントからなるSPA(サーバー側はもう動いている状態)を0ベースで開発する機会がありましたが、渡されたAPI仕様書をOAS定義に翻訳し、各種ジェネレーターを駆使してAJAX部分を実装することで、Nuxt typescriptのプロジェクト作成からアプリケーション完成まで4~5時間程度で終わらせることができました。

とにかく使いこなせれば、病みつきになるレベルの爆速開発が可能になります

スタブサーバーが用意できる

OpenAPI generatorには各種スタブサーバー用のジェネレーターが用意されています。 これによりサーバー側の実装が進んでいない状態でも、OASの定義にexampleを書いておけばそのレスポンスを使ってフロント側の実装を進めたり、デモ画面を作成したりすることが可能になります。 フロントエンドエンジニアはexampleだけでもいじれるようになっておけば、様々なデザインパターンを想定したデータを作って開発を進めることができます。

スタブサーバーの実装方法にこだわりが無ければAPI Sproutを利用するのが良いでしょう。 dockerですぐ動く状態のイメージが用意されていますので、docker-composeに追加してOASの定義ファイルをマウントするだけでお手軽にスタブサーバーが用意できます。更新検知もしてくれて、OASの定義を変更すればすぐさまレスポンスも追従してくれます。

API Sproutをローカルの開発サーバーと別のポートで建てておくと、フロント側で開発サーバー見に行くかスタブ見に行くか切り替えるなど出来ていろいろと使い勝手が良いです。

なお、exampleを書かなかった場合でも、例えば文字列なら"String"のような何かしらスキーマに合わせた値を返してくれます。

IDEの強力な支援が受けられる

pathsからAPIクライアントを生成

上記該当箇所のコード

OpenAPI generatorには各種APIクライアント用のジェネレーターも用意されています。

上の図はtypescript-axios用のジェネレーターの例です。ジェネレーターはOASのpathsの内容に応じて上記のようなメンバメソッドを持ったクラスを生成してくれますので、BaseURLや認証情報などのConfigをクラスのコンストラクタに渡してしまえば、あとはメソッドを叩くだけでリクエストが可能です。

youtu.be

上記の例ではリクエストにパラメーターが不要だったので引数がありませんが、パラメーターが必要な場合は引数に何が必要かIDEが表示してくれます。また、Promiseが解決された場合の戻り値の型をAxiosReasponseのGenericsに埋め込んでくれるため、IDEはレスポンスの型を静的に推論することができます。

components/schemas(など)からIntefaceを生成

ジェネレーターは上記APIクライアントと同時にcomponents/schemasの内容やpathsのレスポンスボディなどのスキーマから、TSのInterfaceを生成してくれます。

これによりAPIのレスポンスが返ってくる前の初期値を作ったり、

import { DefaultApi, SomeModel } from "path/to/api";
class Hoge {
  private response: null | SomeModel = null

  async request (): Promise<void> {
    this.response = await new DefaultApi().someRequestGet().data
  }
}

のような形でレスポンスを格納する変数を先にnullで初期化しておくなどの実装も簡単に行うことができます。

この生成されたInterfaceはOASのdescriptionがTSDocの書式で埋め込まれていますので、型をきちんと保ち続けていればマウスホバーするだけでIDEがdescriptionの内容を表示してくれるようになります。このため実装作業者はTSのコードとAPI仕様書やAPIの実際のレスポンスの画面とを行ったり来たりする頻度を大幅に減らすことができます。

また、typescrit-axios用のジェネレーターはenumにも対応しており、x-enum-varnamesというプロパティを使うことでstring enumを生成することも可能です。

~~
components:
  schemas:
    Genre:
      type: string
      enum:
        - rock
        - jazz
        - progress
      x-enum-varnames:
        - rock
        - jazz
        - progress
~~

↓生成物

~~
/**
 * 
 * @export
 * @enum {string}
 */
export enum Genre {
    rock = 'rock',
    jazz = 'jazz',
    progress = 'progress'
}
~~

スキーマ変更したときに影響範囲を一網打尽にできる(実装側の型の健全性が保たれていれば)

これは前項の内容とほぼ被りますが、TSの型検査とスキーマ定義が結合できるということは、スキーマを変更した場合にその影響を受ける部分が型チェックであぶりだせるということになります。

上記の例は、IDはintegerで来ると思って実装していたが実はstringが来ることが分かったのでスキーマを変更したケースです。定義ファイルの該当箇所を変更してジェネレーターでAPIクライアントを生成しなおしてから再度型検査を行ったことで、型が不一致になった個所が画面下のターミナルに羅列されています。

(補)Vueで型健全性をちょっと良くする小技

Vueのtemplate内はただの文字列なので、TSの型検査時には型検査が行われません1

型検査を抜けてしまうということは実際にコンポーネントをマウントして表示させてみるまで壊れていることに気づけないということになり、デバッグの負担が非常に増えます。これを避けるために、computedの戻り値の型指定を使う方法があります。

<template>
  <div>
    {{ hoge.some.property }}
  </div>
</template>
<script lang="ts">
import { Vue, Component, Prop } from 'nuxt-property-decorator';

interface Hoge {
  some: {
    property: string;
  };
}
@Component
export default class HogeComponent extends Vue {
  @Prop({ type: Object, required: true }) readonly hoge!: Hoge;
}
</script>

例えば上記のような場合、Hogeインターフェースの構造が変わった際にhoge.some.propertyに対する型検査は行われないため実行時エラーになります。 これを下記のように変更すると、トランスパイル時にエラーに気づくことができ、修正の負担を軽減することができます。

<template>
  <div>
    {{ property }}
  </div>
</template>
<script lang="ts">
import { Vue, Component, Prop } from 'nuxt-property-decorator';

interface Hoge {
  some: {
    property: string;
  };
}
@Component
export default class HogeComponent extends Vue {
  @Prop({ type: Object, required: true }) readonly hoge!: Hoge;
  get property(): string {
    return this.hoge.some.property
  }
}
</script>

template内が簡素になり可読性も上がるため、個人的にはかなり好んで多用しています。 もちろんこの方法はあくまで修正の負担を軽減してくれるだけで、完全に安全であることを保障してくれるわけではありません。テストは別途しっかり用意する必要があります。

実行時に実装とスキーマ定義の不整合を検査できる

スキーマ定義をしっかり決めて開発を進めても、実際に統合テストしてみたらスキーマとずれが生じることは(特に型が怪しい言語を使っている場合)よく起こります。

その際レスポンスやリクエストを目視してどこが不整合を起こしているか確認するというのはあまり賢い選択肢ではありません。OASを利用することでこの不整合カ所の調査を半自動化することができます。

OASopenapi2schemaというツールを使うことでJSON Schemaに変換することが可能です。JSON Schemaはオブジェクトの構造を検証することができるスキーマで、言語に依存しない中立的な仕様としてそれなりに知名度があり、様々な言語に対応しているバリデーターが存在します。

例えばJavascriptであればajvなどがあります。 これらを用いることでリクエストボディを送信する前にOAS定義との整合性を検査したり、レスポンスを受け取った後にレスポンスボディを検査したりすることができます。

ajvによるバリデーションの例

筆者が試した時点のopenapi2schemaはデフォルトでJSON Schema Draft v.4で出力してくれたので、まずはajvに使用するスキーマのバージョンを伝えます。

import Ajv from 'ajv';
import draft4 from 'ajv/lib/refs/json-schema-draft-04.json';
const ajv = new Ajv({schemaId: 'auto', allErrors: true}).addMetaSchema(draft4);

次に、このajvと生成したスキーマをセットにしてexportしておきます。

import Ajv from 'ajv';
import draft4 from 'ajv/lib/refs/json-schema-draft-04.json';
import schema from 'path/to/scheme.json'; // openapi2schemaで生成したjsonファイル

const ajv = new Ajv({schemaId: 'auto', allErrors: true}).addMetaSchema(draft4);
const validator: {schema: typeof schema; ajv: Ajv.Ajv} = {
  schema,
  ajv
};

export default validator;

この時一つ注意しておくべきポイントとして、tsconfigの"resolveJsonModule"をtrueにしておく必要があります。"resolveJsonModule"を有効にしておくと、jsonをimportした際にtypescriptが中身を見て自動的に型推論してくれるようになります。 もしプロジェクトの事情により"resolveJsonModule"を有効にできない場合、あまりきれいな方法ではありませんがjson-to-ts-cliを使用してshcema.jsonからinterfaceの定義ファイルを生成することが可能です。

あとは実際にリクエストを飛ばす際に

import validator from "path/to/validator";
import { DefaultAPI } from "path/to/api";

export async function fetch() {
  const api = new DefaultAPI();                  // typescript-axiosクラスを初期化
  const res = await api.someRequestGet();        // リクエストを実行
  const path = "/some/request";
  const validate = validator.ajv.compile(
    validator.schema[path].get.responses[200]
  );                                             // テスト対象のスキーマをajvに流し込む
  const valid = validate(res.data);              // バリデーションを実行
  if (!valid) {
    console.warn(`validation error: ${path}`);
    console.warn(validate.errors);               // バリデーションエラーが発生した箇所をコンソールに表示
  }
}

のようにすることでバリデーションが可能です。

ただ、このスキーマ定義用のjsonはそれなりのファイルサイズになります。本番ビルドにこのjsonファイルを含めるのはあまり賢明ではないと思いますので、適宜ビルド環境に応じてバリデーション関連のファイルとロジックを切り離せるような仕組みにしておくとよいでしょう。

実装と連動した「信頼できる」API仕様書が出来上がる

これまで見てきたようにどっぷりOASに依存してクライアントの開発を進めた場合、必然的にOASスキーマ定義は実装と乖離することができない(乖離してしまうとそもそもまともに動かない)状態に持っていくことができます。

そのため、「OpenAPI generatorが動いている」かつ、「ジェネレーターで生成されたAPIクライアントが実装に組み込まれている」という二点さえ確認できれば、「OASの定義ファイルに書かれている内容は概ね最新の状態に保守されている」ということを保障することができます。

全てのAJAXOASベースに置き換えることができてしまえば、もう二度と「微妙に実装とずれてる仕様書やInterface定義」に苦しめられることはありません。

なお、VScodeにはopenapi-previewというプラグインがありますので、APIドキュメントとして利用するにはこのプラグインyamlファイルだけあれば事足ります。専用のプレビュー用サーバーなどを立てるのは、ほとんどの場合お金と時間の無駄になるだけでしょう。

※ただし、descriptionの文言だけはただの文字列なので、そこに嘘が書かれていてもTypescriptは何も指摘してくれません。信頼できるのはあくまでスキーマです。

まとめ

OASとTSの連携は、一度はまると病みつきになるレベルに爆速でコーディングが可能になるポテンシャルを持っていますが、使ったことがないとどうしても定義ファイルを管理するコストが気になって二の足を踏んでしまうケースが多いかと思います。

OASの定義ファイルと生成物自体は既存の実装を全く破壊しないので、部分的にじわじわとOASに置き換えておくことも可能です。 まずはエンドポイント一つでもOAS化してみて実際にどんなものか体験してもらうのが、ベネフィットを感じるには手っ取り早いかと思います。


  1. VScodeであればVeturのvetur.experimental.templateInterpolationServiceを使うことでtemplate内の型検査も行ってくれますが、今のところエディタ上で警告を出してくれるだけなのでトランスパイルは通ってしまうのと、ものすごくVScodeの負荷が大きくなるので動作が重くなるといった問題があります。

Vuexの型強化で学んだ、TypeScript型強化のTIPS

この記事は個人ブログの転載です。

kotamat.com

Vuexは型がゆるいゆるいという話を散々されているのを見てきて、工夫すれば型の強化はできるんじゃないかと挑戦してみたところ、そこそこいい感じのものができたのですが、その過程でさまざまな工夫が必要だったためその知見を共有します。

実際に作ったPR

こちらのPRにてOpen中。

const store = new Vuex.Store({
  state: { value: 0 },
  getters: {
    rootValue: (state) => state.value,
  },
  actions: {
    foo() {},
  },
  mutations: {
    foo() {},
  },
  modules: {
    a: {
      namespaced: true,
      state: { value: 1 },
      actions: {
        test: {
          root: true,
          handler({ dispatch }) {
            dispatch("foo");
          },
        },
        test2: {
          handler({ dispatch }) {
            dispatch("foo");
          },
        },
      },
      modules: {
        b: {
          state: { value: 2 },
        },
        c: {
          namespaced: true,
          state: { value: 3 },
          getters: {
            constant: () => 10,
            count(state, getters, rootState, rootGetters) {
              getters.constant;
              rootGetters.rootValue;
            },
          },
          actions: {
            test({ dispatch, commit, getters, rootGetters }) {
              getters.constant;
              rootGetters.rootValue;

              dispatch("foo");
              dispatch("foo", null, { root: true });

              commit("foo");
              commit("foo", null, { root: true });
            },
            foo() {},
          },
          mutations: {
            foo(state, payload: string) {},
          },
        },
      },
    },
  },
});

というのがあったときに、

store.dispatch("a/c/foo")
store.dispatch("a/c/test")
store.dispatch("a/test")
store.dispatch("a/test2")
store.dispatch("foo")

store.commit("a/c/foo","gg")
store.commit("foo")

store.state.a.b.value
store.state.a.c.value
store.state.a.value
store.state.value

store.getters["a/c/constant"]
store.getters["a/c/count"]
store.getters.rootValue

この辺がすべて型安全に書ける感じになっています。 また、dispatchやcommitの引数は補完が効くようになっており、第2引数の型は第1引数で指定した文字列によって決まるようになっています。

TypeScriptの柔軟性高い型表現

単なるジェネリクスだけではなく、 Mapped TypesConditional TypesTemplate Literal Types などにより、型を非常に柔軟に書けるのがTypeScriptの強みだと思っています。 Vuexは同一オブジェクトのなかで循環する形で参照する挙動(actions→commit()でmutationを呼ぶなど)があるので、こればっかりはTSで表現できないですが、少なくともVuex外とのインターフェースにおいては型安全に書けるのではないかと思いチャレンジしてみました。

今回使ったTIPSを難易度低い順に紹介しようと思います。(公式ドキュメントに記載されている内容は前提知識として割愛)

1. Index Types + Conditional Typesによる型表現の抽出

Vuexはmoduleという機能によりネストしたステート管理を実現しています。これがVuexの型表現を難しくしているものであり、いままで型を厳密に表現できてこれなかったものなのかなと思っています。

moduleは他のstateやactionsと同階層のとこに記述されるため、ネスト表現を取得するにはindexがmodulesのものを抽出し、かつmodulesの下にあるオブジェクトそれぞれに対して再帰的に型表現を呼び出していく必要があります。

store.stateの型表現を例に説明します。

type YieldState<S, T extends ModuleTree<S> | undefined> = (S extends () => any
  ? ReturnType<S>
  : S) &
  (T extends ModuleTree<S>
    ? {
        [K in keyof T]: T extends ModuleTree<S> // -1
          ? YieldState<
              T[K]["state"],
              T[K] extends { modules: object } ? T[K]["modules"] : {} // -2
            >
          : never;
      }
    : {});
...
export declare class Store<...> {
  readonly state: YieldState<S, SO["modules"]>;
}

ModuleTreeというのはmodules下のオブジェクトツリーを表している型なのですが、 1 で各moduleのオブジェクトを抽出した上で、 2 で更に下層にmodulesがあればそれを、なければ空を指定して再帰的に呼び出すことによってstateのツリー向上を実現しています。

2. 不活性仮型引数

今までのVuexの型は型引数にstateの型だけを渡してました。当然それだけではstate以外の型を解釈する事はできないため、型表現に限界があったわけです。 今回はStoreOptions(Vuex.extends()の引数で渡すオブジェクト全体)を型として指定できるようにすることによって、呼び出し時点での型で全体の方を表現できるようにしました。 一方後方互換性を考えたときに、第一型引数をstateからStoreOptionsに変えてしまうことは避けるべきです。ここで発生するのは

  1. stateの型だけを指定したVuexでも正常に動く
  2. StoreOptionsの型を指定した場合は従来のVuexも動くし、今回の型強化も使える

ということを実現する必要が出てきました。

今回こちらを解決する方法として第一型引数を不活性にし、第三型引数にて、stateの型をStoreOptionsの型を考慮した上で決定するという形にしました。

export declare class Store<
  _,
  SO extends StoreOptions<any> = StoreOptions<_>,
  S = SO["state"] extends _ ? SO["state"] : _
> {...

こうすることでStoreOptionsがあればそのstateを、なければ今までのstateの型で表現されるようになります。

3. Vuex特有のネスト構造の文字列結合によるフラット化

dispatch(), commit()は第一引数にネストしたモジュールを / つなぎで指定し、第2引数にその対象関数のpayloadを指定するという仕様になっています。 getterも関数ではないものの、同じようなネスト構造の表現をしています。

つまり

const s = {
  modules: {
    foo: {
      modules: {
        hoge: {
          actions: {
            bar: (n: number) => "aaa",
          },
        },
      },
    },
    bar: {
      actions: {
        bal: (x: string) => "aaa",
      },
    },
  },
};

{
  "bar/bal": (x: string) => string,
  "foo/hoge/bar": (n: number) => string
}

のような形に変換する必要があるわけです。今回はこれを下記のような形で実現しました

type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (
  k: infer I
) => void
  ? I
  : never;

type ExtractObject<
  T,
  S extends string = "actions",
  P extends string = ""
> = (T extends {
  [_ in S]: infer O;
}
  ? { [K in keyof O as `${P}${string & K}`]: O[K] }
  : {}) &
  (T extends {
    modules: infer O;
  }
    ? UnionToIntersection<
        {
          [K in keyof O]: ExtractObject<O[K], S, `${P}${string & K}/`>;
        }[keyof O]
      >
    : {});

内包されているTIPSは3つです。順番に説明していきます

UnionからIntersectionに変換する

まずひとつ目のUnionToIntersectionですが、こちらを参考にさせていただきました。 記載にある通りUnionからIntersectionに変換する型です。これは何が嬉しいかというと、Mapped Types を使うと、返却される型がUnion型になるのですが、今回導出したい型はあくまで一つのオブジェクトの型になるためIntersectionの型にする必要があり、ここで使用しています。

KeyRemappingを使った、コードジャンプの実現と/区切りのkey

KeyRemappingはTS4.1から導入された機能で、Mapped Types内で as を使うと新しいkeyを使うことができるものになります

type MappedTypeWithNewProperties<Type> = {
    [Properties in keyof Type as NewKeyType]: Type[Properties]
}

ExtractObject型の第3引数は今までの再起表現で渡されてきたprefixの文字列(moduleの / つなぎされた文字)になるのですが、そのprefixとkeyを結合することによってVuexの引数で使われている文字列にできます。 また、もともとのKeyをRemapしているだけなので、そのKeyをベースにコードジャンプができるようになり、

このような挙動を可能にしています。

Mapped Types と オブジェクトのあとの[keyof K]

今回型を実装するに当たり、他のTSプロジェクトの型実装なども参考にしていたのですが、よく出てくる表現で下記のようなものがありました

{
  [K in keyof T]: Hoge<T[K]>
}[keyof T]

これは何をしているかというと T 型の各keyに対する要素をHogeの型引数に指定し、それをすべてUnion型で結合したものとして返却します。 この表現によりオブジェクトをフラットに展開することができ、上記で説明したUnionToIntersectにより一つのオブジェクト型に戻すことができます。 Hogeに当たるところを今回は再起表現にしたため、ネストしている全ての抽出対象の関数をフラットに展開することができました。

まとめ

今回Vuexの型表現を強化するためにさまざまなTIPSを駆使して実現してきました。型パズルの欲求を満たすには丁度いいお題だったかなと思います。 まだこのPRではmapXxx()系やnamespacedがfalseのとき、root指定のmoduleのときなどが考慮されていないため、型表現としては不十分なところはあるのですが、ここは時間あるときに必要あればやっていこうかなと思います。

デイリースクラムの発表順をランダムで決める bot を作る [discord.js + TypeScript]

新型コロナの影響で、ご多分に漏れず弊社の開発メンバーもフルリモートでお仕事をしています。

コアタイム開始の11:00になると Discord に集まりデイリースクラムを行っているのですが、毎回発表の順番を決めるのが面倒なので、参加メンバーをシャッフルしてリストで返す bot を作ってみました。

パッケージ類

Discord コミュニティ謹製のパッケージ discord.js を使いました。

外部パッケージ使う際は型情報が分かっておくと便利ですね。discord.js はそれ自体で型情報を持っているので @types/◯◯ みたいなパッケージを入れる必要はないです。

{
  "name": "discord_sort_gacha",
  "version": "1.0.0",
  "description": "Sort random and list up the online member.",
  "main": "index.js",
  "scripts": {
    "start": "node ./build/index.js",
    "debug": "ts-node ./src/index.ts",
    "compile": "tsc -p .",
    "compile:test": "tsc -p . --noEmit"
  },
  "author": "Shohei-Japan",
  "license": "MIT",
  "dependencies": {
    "discord.js": "^12.5.3",
    "dotenv": "^8.2.0"
  },
  "devDependencies": {
    "@types/node": "^14.14.41",
    "ts-node": "^9.1.1",
    "typescript": "^4.2.4"
  }
}

Bot のアカウント作成

こちらのページに全部書いてあります。

Botアカウント作成- discord.py

このページの通りに行って、Bot アカウント作成、トークン取得、Discord サーバーに招待、までできたら OK です。

テスト送信

さっそくテスト送信してみます。

Discord テキストチャンネルで「おはよう」と送信すると、「やあやあ」と返してくれる処理を書きます。

import { Client, Message } from "discord.js"
const client = new Client()

client.on('message', (message: Message) => {
  if (message.content === 'おはよう') {
    message.channel.send('やあやあ')
  }
})

const token = xxxxxx // 取得した bot のトークン
client.login(token)

結果

f:id:show-hei:20210423045835p:plain うまく通ったので、続きをやっていきましょう。

Discord 用語のおさらい

Discord ではメンバーが集まるためのサーバーを作りますが、discord.js ではこれをギルド (guild) と呼んでます。

ギルドにはチャンネルがあり、テキストチャンネル、ボイスチャンネル、DM チャンネルなどが存在します。

今回の目的としては、デイリースクラムのために「ボイスチャンネルに集まったメンバー」を、「bot にコマンドを送信したテキストチャンネル」にリスト化して返すことですので、テキストチャンネルとボイスチャンネルを活用します。

チャンネルに参加しているメンバー情報は各チャンネル内に含まれているのでそれも使っていきましょう。

実装

これからの処理はこの client.on() の中に記述していきます。 Message 型を参照しているので、message.xxx のデータは補完が効くので楽です。

client.on('message', (message: Message) => {
// bot コメントを受け取ったあとの処理
})

bot コメントしたメンバーのいるボイスチャンネルとメンバー一覧を取得

変数 message 内には、送信されたギルド情報が入っており、ギルド情報にはそのギルド内のチャンネル一覧情報が含まれています。 名前からは連想しづらいのですが、チャンネル情報はチャンネルの cache 内に含まれているようです。ここから bot コメントを送信したメンバーの ID が含まれているボイスチャンネルと、取得したボイスチャンネルに存在するメンバー一覧を取得していきます。

bot コメントは !gacha というテキストとしました。

// コメントしたメンバーを取得
const author = message.author

// コメントを取得
if (message.content === '!gacha') {
  // ギルドに含まれるチャンネルを取得
  const channels = message.guild.channels
  // channels 内の各チャンネルは type を持ち、'text', 'voice' などの値を持つ
  const voiceCH = channels.cache.find((ch: GuildChannel) => {
    if (ch.type !== 'voice') {
      return false
    }
    // ボイスチャンネルのうち、コメントしたメンバーがいるチャンネルを取得
    return !!ch.members.filter((member: GuildMember) => member.user.id === author.id)
  }) as VoiceChannel
    
  if (!voiceCH) {
    console.error('チャンネルがみっかんないよ')
    return
  }
    
  // voice チャンネルに参加しているメンバー一覧を取得
  const voiceCHMemberNames: string[] = voiceCH.members.map((member: GuildMember) => member.user.username)

メンバーをシャッフルする関数

メンバーをシャッフルするための関数を別途用意しました。これを通してあげるといいですね。

const shuffleArray = (array: string[]) => {
  for(let i = (array.length - 1); 0 < i; i--){
    var r = Math.floor(Math.random() * (i + 1))

    var tmp = array[i]
    array[i] = array[r]
    array[r] = tmp
  }
  return array
}

シャッフルしたメンバーをリストにしてチャンネルに返す

あとはシャッフル関数を通して、体裁を整えてチャンネルに返しましょう。

const shuffledMembers = shuffleArray(voiceCHMemberNames)

// リストにしてテキストチャンネルに送信する
const joinedMembers = shuffledMembers
  .map((member: string, index: number) => `${index + 1}. ${member}`)
  .join('\n')

message.channel.send(joinedMembers)

完成

完成したので試してみましょう。

f:id:show-hei:20210423063842p:plain

f:id:show-hei:20210423063904p:plain

ランダムでリスト化することができました!

f:id:show-hei:20210423063944p:plain

channels.cache を参照しているので、その名称から古いキャッシュを参照しちゃう?と不安でしたが、ちゃんとリアルタイムでのメンバーを取ってきてくれました。

index.ts全コード

クリックすると展開されます

// デプロイを想定してるので dotenv 使ってます。
require('dotenv').config()
import {
  Client,
  GuildChannel,
  GuildMember,
  Message,
  User,
  VoiceChannel
} from "discord.js"
const client = new Client()

// アプリケーション名を確認のために表示
client.on('ready', () => {
  if (!client.user) {
    console.error('認証できないよ')
    return
  }
  console.log(`Logged in as ${client.user.tag}.`)
})


// チャット欄のコメントを受け取る処理
client.on('message', (message: Message) => {
  if (!message.guild) {
    console.error('ギルドがないよ')
    return
  }

  // コメントしたメンバーを取得
  const author = message.author

  if (message.content === '!gacha') {
    const channels = message.guild.channels
    const voiceCH = channels.cache.find((ch: GuildChannel) => {
      if (ch.type !== 'voice') {
        return false
      }
      // ボイスチャンネルのうち、コメントしたメンバーがいるチャンネルを取得
      return !!ch.members.filter((member: GuildMember) => member.user.id === author.id)
    }) as VoiceChannel
    
    if (!voiceCH) {
      console.error('チャンネルがみっかんないよ')
      return
    }

    // voice チャンネルに参加しているメンバー一覧を取得
    const voiceCHMemberNames: string[] = voiceCH.members
      .map(member => member.user.username)
    const shuffledMembers = shuffleArray(voiceCHMemberNames)

    // リストにしてテキストチャンネルに送信する
    const joinedMembers = shuffledMembers
      .map((member: string, index: number) => `${index + 1}. ${member}`)
      .join('\n')

    const sendMessage = joinedMembers || 'メンバー取得できなかったよ'

    message.channel.send(sendMessage)
  }
})

/**
 * 与えられた配列をシャッフルして返す
 * @param array 
 * @returns 
 */
const shuffleArray = (array: string[]) => {
  for(let i = (array.length - 1); 0 < i; i--){
    var r = Math.floor(Math.random() * (i + 1))

    var tmp = array[i]
    array[i] = array[r]
    array[r] = tmp
  }
  return array
}

const token = process.env.DISCORD_TOKEN
client.login(token)

adobeのスーパー解像度で刑事ドラマのやつの検証

転載元zenn.dev


 
刑事ドラマでよくある、なんとか犯人らしき人物を監視カメラで捉えたんだけど、どうしようもないボケボケの解像度で、サイバー対策室的なのに所属しているハッカーポジションの若い人材が「お待ちください。ッターーン」のワンクリで解像度上げて犯人の顔割り出すやつ。

現実的に詳細な画像=存在しない情報なので無理だろっていつも思ってたんですけど、adobeが現実のものにしたようです。

その名も"スーパー解像度"
わかりやすくていいですね。

blog.adobe.com

実際に使ってみて、ほんとに刑事ドラマみたいなのできるのか検証してみました。

やっていくぞ!

まずはraw画像を用意します。
(この時点で違和感覚えた方は目を瞑って未来に期待してください)

f:id:ezm_u:20210422122218j:plain
コロナ禍における閑散とした雑多な駅前商店街。趣があります

ベランダから撮りました。
住んでるとこバレそうですけど男の一人暮らしなので問題ありません。

写真が準備できたら最新版のPhotoshopで開きます。
raw画像は自動でCamera Rawが立ち上がるので、画像を右クリックして"強化"を選択します。
ダイアログが開くので、完了時間など確認しつつ"強化"しましょう。

f:id:ezm_u:20210422122340p:plain

f:id:ezm_u:20210422122357p:plain
のアクションボタンは普通のデザイナーなら"実行"とかにしそうですが、"強化"ってすごい。ゲームとかじゃなくて、現実世界でボタンひとつで何かを強化したことある人いますか?

強化完了!スーパー解像度!

強化した画像がこちらです。
このサイズだと全然わからないですね。
f:id:ezm_u:20210422122514j:plain
車のナンバープレートにフォーカスしてみましょう。
刑事ドラマ感がでてきました。

まずは元の解像度の画像から。
左上の漢字っぽいの不明瞭で読めないですね。

f:id:ezm_u:20210422122546j:plain
強化前の画像です。全然知らない人の車なのでナンバーの一部を隠しています

これをスーパー解像度で強化すると...

.

.

.

.

.

.

.

.

.

.

.

.

板橋!

なんとか読める状態に。
普通にすごい。どういう仕組みか全然わかりません。

f:id:ezm_u:20210422122651j:plain
強化後の画像です。全然知らない人の車なのでナンバーの一部を隠しています

Gifアニも用意したのでスーパー解像度による強化をお楽しみください。
f:id:ezm_u:20210422122722g:plain
以上です。

Supervisor を実際に操作してデーモン化に触れてみる

この記事は個人ブログと同じ内容です

www.ritolab.com


プロセス制御システム である Supervisor を実際に動かしてみて、彼のことを理解しようとしてみました。

Supervisor(スーパーバイザー)

Supervisor

supervisord.org

スーパーバイザーは、ユーザーがUNIXライクなオペレーティングシステム上の多数のプロセスを監視および制御できるようにするクライアント/サーバーシステムです。

プロセスを監視して、落ちたら起動し直してくれたりするツール。

Supervisor を起動すると、設定している管理対象も一緒に起動してくれる。

実行環境

linux 環境があれば良いので Docker で適当に作成

Dockerfile

FROM amazonlinux:latest

# EPEL インストール
RUN amazon-linux-extras install -y epel

# supervisord インストール
RUN yum install -y supervisor

# 設定ファイル設置
COPY nginx.ini /etc/supervisord.d/nginx.ini

イメージをビルドして

# イメージをビルド
docker build -t amazon_linux_20210417 .

コンテナを起動する

# コンテナ起動
docker run -d --privileged amazon_linux_20210417 /sbin/init

# コンテナ ID 出力
-> e17xxxxxxxxxxxxxxxxxxxxxxxx

出力されたコンテナ ID を指定してコンテナの中に入る

# コンテナの中に入る
docker exec -it e17 /bin/bash

Supervisor の起動完了

# バージョン確認
$ supervisorctl version
3.4.0

管理対象を設定する

nginx を管理対象にして操作していく。

nginx.ini

[program:nginx]
command=/usr/sbin/nginx -g "daemon off;"
autostart=true
autorestart=true

.ini じゃなくて .conf が良ければ、supervisord.conf の最下部に include があるのでそこを変更する。

/etc/supervisord.conf

[include]
files = supervisord.d/*.ini

supervisorctl コマンド

スーパーバイザーを操作するには supervisorctl コマンドを使っていく。

Running supervisorctl - Supervisor 公式

http://supervisord.org/running.html#supervisorctl-command-line-options

管理対象のプロセスの状態を確認する

supervisorctl status ... で管理対象のプロセスの状態を確認できる

status <name>      単一プロセスのステータスを取得する
status <gname>:*   グループ内のすべてのプロセスのステータスを取得する
status <name> <name>   複数の名前付きプロセスのステータスを取得する
status       すべてのプロセスステータス情報を取得する

nginx の状態を確認してみます。

$ supervisorctl status
nginx                            RUNNING   pid 136, uptime 0:34:04

RUNNING しっかり動いてる。

管理対象を再起動する

supervisorctl restart ... でプロセスの再起動ができる

restart <name>     プロセスを再起動します
restart <gname>:*  グループ内のすべてのプロセスを再起動します
restart <name> <name>  複数のプロセスまたはグループを再起動します

nginx を再起動してみます。

# 再起動を実行
$ supervisorctl restart nginx
nginx: stopped
nginx: started

# ログ
2021-04-17 07:12:27,752 INFO waiting for nginx to stop
2021-04-17 07:12:27,767 INFO stopped: nginx (exit status 0)
2021-04-17 07:12:27,771 INFO spawned: 'nginx' with pid 152
2021-04-17 07:12:37,814 INFO success: nginx entered RUNNING state, process has stayed up for > than 10 seconds (startsecs)

再起動されました。

プロセスを落としてみる

プロセスを何度か落としてみましたが、何度でも這い上がってくる感じがさすがデーモン化といったところ。

f:id:ro9rito:20210419201222p:plain

これがスーパーバイザーの力か。恐るべしデーモン閣下

設定を変更して反映する

nginx の管理設定項目を変更してそれを適用させてみます。

設定ファイルを変更しただけでは適応されないので反映してあげる必要がある。

/etc/supervisord.d/nginx.ini

[program:nginx]
command=/usr/sbin/nginx -g "daemon off;"
autostart=true
autorestart=true

# 起動後設定値(秒)より早くプロセスが終了した場合は起動失敗とする
startsecs=10

# 起動失敗時のリトライ回数。この回数を超えると FATAL 状態となり起動施行をストップする
startretries=2

下の 2 を新たに設定値に加えました。

startsecs 起動後設定値(秒)より早くプロセスが終了した場合は起動失敗とする startretries 起動失敗時のリトライ回数。この回数を超えるとステータスが FATAL になって起動をストップする Section Settings http://supervisord.org/configuration.html#program-x-section-settings

変更を反映するには update を使う。プロセスが再起動されて新しい設定値も読み込まれる。

$ supervisorctl update nginx
nginx: stopped
nginx: updated process group

一度 remove してから再度 add する

nginx を Supervisor の管理対象から外したのち、再度管理対象にしてみます。

stop でプロセスを止めます。

$ supervisorctl stop nginx
nginx: stopped

remove で管理対象から外します。

$ supervisorctl remove nginx
nginx: removed process group

この状態から start でプロセスを開始させようとすると、管理対象から外れているのでエラーになります。

$ supervisorctl start nginx
nginx: ERROR (no such process)

再度管理対象とするには add を使います。

$ supervisorctl add nginx
nginx: added process group

再度 nginx が起動します。

ちなみに、remove していようが Supervisor 自体を再起動したら起動します。

ステータスが FATAL になったものを再度起動させる

起動に失敗して FATAL になった管理対象ってどうやって復帰させるんだろう。という事で確認。

現在、FATAL の状態

$ supervisorctl status
nginx                            FATAL     Exited too quickly (process log may have details)

手動で起動させるには start で行う

start <name>       プロセスを開始します
start <gname>:*       グループ内のすべてのプロセスを開始します
start <name> <name>    複数のプロセスまたはグループを開始する
start all         すべてのプロセスを開始します

nginx を起動してみます。

$ supervisorctl start nginx
nginx: started

FATAL ステータスの nginx を起動できました。

まとめ

Supervisor 実際に触ってみたら操作はとてもシンプルでした。

supervisorctl コマンドは add/start とか、reload/reread/update みたいな、シンプルゆえに雰囲気で触ってると意外とどれがどういう動きをするのかわからないなと思ったので、実際に触ってみて確認して理解が深まったことは収穫でした。

みんながきっと1万回は聞いている、VS Code Remoteでコンテナ開発をやる方法

この記事はzenn.devで書いたやつなんで、そっちの方も見てやってくださいな。

zenn.dev


こんにちは皆さん。

いやね、皆さんこう言いますよ。 「それもう何番煎じやねん!前も聞いたわ!」 ってね。

でも、こうも思うじゃないですか。 「たくさん記事があるってことは、ひょっとしてめっちゃくちゃ大事なことなのでは?」 とね。

とういうわけで、毎回まるで前提のごとく使っていたVS Code Remote Containerによるコンテナ開発について、自分が使っているやり方について簡潔に書いてみます。 マニュアルなぞっても面白くないですしね。

VS Code Remote

VSCではリモート環境に入ってエディタを起動することができます。 イメージ的にはこんな感じで、リモート環境上で立ち上げたエディタを、外部から遠隔操作するようなイメージですね。 VSC Remote VS Code Remote自体は随分前からあったのですが、WindowsのDocker開発の環境が整うまでに結構かかったのですよね。 私のプライベートマシンがWindowsであることもあって、完全にRemote Containersができるようになったときは、無駄に感動したものです。

このエクステンションは、このリモート環境をどこに用意するかで、3つに分かれています。 普通の外部サーバで実行しSSHでアクセスするもの、WindowsのWSL上で実行するもの、そして、立ち上げたコンテナ上で実行するものです。 今回解説するのは、コンテナにアクセスするものです。

VS Code Remote Containers

VSCをコンテナ上で実行できるパッケージですが、その実行方法がいくつかあります。各々について解説していきます。

パッケージインストール

何をやるにもVSCの Remote Containerが必要です。さっさと入れちゃいましょう。 https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers 入れるのは一瞬ですが、前提として、docker for mac もしくは docker for windowsが必要です。 私の環境ではdocker for windows の WSL2ホスティングを使っています。

attatch container

まずは手っ取り早くコンテナ上でVSCを実行してみましょう。 例として私の環境でPHP8の開発をしたいと考えたとき、まぁ、PHP8をそもそも入れていないわけですよ。Windowsですし。

> php
php : 用語 'php' は、コマンドレット、関数、スクリプト ファイル、または操作可能なプログラムの名前として認識されません。名前
が正しく記述されていることを確認し、パスが含まれている場合はそのパスが正しいことを確認してから、再試行してください。
発生場所 行:1 文字:1
+ php
+ ~~~
    + CategoryInfo          : ObjectNotFound: (php:String) [], CommandNotFoundException
    + FullyQualifiedErrorId : CommandNotFoundException

ただ、dockerはあるので、そいつでphpを立ち上げてみます。

docker run -it --rm --name php-testing php:8 bash

終わったら、後でexitして落とします。

さて、コンテナが立ち上がってい状況で、VSCで以下のような手順を踏みます。 1. 左下のアイコンを押す 2. Remote-containers: Attatch to runnning container を選択する 3. 立ち上がっているコンテナを選択する

これだけで、コンテナ上で実行するVSCが立ち上がります。 実際に見てみたのはこんな感じです。 (PHP古いな。。。更新しよう) これでPHP8の入った環境中でのVSCを使った開発が始められるようになります。 もちろん、PHP intelephense などのエクステンションをインストールすることもできます。 しかし、注意すべきは、エクステンションのインストールは、環境ごとになるという点です。普段ホストで開発していると気にしないですが、コンテナ開発をする場合、基本的にコンテナは実行終了とともに破棄することが多いので、その場合、いちいちエクステンションをインストールしなおさなきゃいけない点に留意しましょう。 面倒な場合は、コンテナを破棄せず、使いまわすようにしましょう。

devcontainer

複数人での開発を考えると、構築する環境定義をプロダクトコードの中に入れておくのが楽です。 VSCのRemote Containerでは、動かしたいコンテナ環境を、docker-composeと少々に設定で定義することができます。 その方法を解説していきましょう。

.devcontainer

まず、プロダクトリポジトリのルート配下に以下のようにディレクトリとファイルを作ります。

.devcontainer
  ┗ devcontainer.json
  ┗ docker-compose.yml

超単純には、docker-compose.ymlがVSCで開発するうえで必要な環境を用意する部分で、devcontainer.jsonが実際にVSCをどこでどのように起動するかを定義する部分です。 docker-composeを使わないで、dockerのイメージを直接指定するやり方もありますが、今回はdocker-composeを使った方法の解説だけにします。

コンテナの定義

私がよくLaravelの記事を書くときに使っているテンプレを例にしてみましょう。 大体の場合、PHPを動かす環境とデータベースがあれば事足りますので、docker-compose.ymlは以下のようになる場合が多いです。

version: "3"

services:
    workspace:
        build: workspace
        command: sleep infinity
        volumes:
            - ../:/var/www/
        ports: 
            - 8000:8000

    db:
        image: mysql
        environment:
            - MYSQL_ROOT_PASSWORD=secret
            - MYSQL_USER=niisan
            - MYSQL_DATABASE=niisan
            - MYSQL_PASSWORD=secret

ここで、workspaceという未定義のコンテナが出てくるので、いつものdocker-composeで環境を立ち上げるのと同じやり方で、workspaceのコンテナを定義しておきます。

.devcontainer
  ┗ devcontainer.json
  ┗ docker-compose.yml
  ┗ workspace
    ┗ Dockerfile

仕事じゃないので、たいして派手な内容ではないですが、以下のようなDockerfileを使っています。

FROM php:8

RUN apt-get update && apt-get install -y git unzip libonig-dev libzip-dev && \
docker-php-ext-install mbstring pdo pdo_mysql zip && \
pecl install xdebug && docker-php-ext-enable xdebug

COPY --from=composer /usr/bin/composer /usr/bin/composer

とりあえず、composerとテストできる環境があればいいや的なやつですね。

devcontainer.json

最後に、devcontainer.jsonを定義しましょう。

{
    "name": "oauth-laravel",
    "dockerComposeFile": "docker-compose.yml",
    "service": "workspace",
    "workspaceFolder": "/var/www",
    "settings": {
        "editor.tabSize": 4
    },
    "shutdownAction": "stopCompose"
}

このなかで、dockerComposeFileserviceは必須で、どのcomposeファイルを使って、どのサービス上でVSCを動作させればいいかを判断します。 ここまでできれば、あとはVSCを再起動させるだけです。 左下のアイコンを押して、Remote Containers: Reopen in Container を選択します。これで、開発環境に入った状態で、VSCが再起動します。 ちょっとわかりにくいかもですが、コンテナ環境上でエディタが起動しています。

おまけ

エクステンションの指定

devcontainerで起動させる場合、エクステンションについては少しありがたい機能があります。上に挙げたdevcontainer.jsonでextensionsという項目があるので、ここにあらかじめ入れておきたいエクステンションを記録しておけばよいです。jsonに簡単に追加する機能もあります。 そうすると、こんな感じになります。

{
    "name": "oauth-laravel",
    "dockerComposeFile": "docker-compose.yml",
    "service": "workspace",
    "workspaceFolder": "/var/www",
    "settings": {
        "editor.tabSize": 4
    },
    "shutdownAction": "stopCompose",
    "extensions": [
        "felixfbecker.php-intellisense"
    ]
}

とりあえず、PHPで必要そうなエクステンションはあらかじめ入れるようにしておきましょう。

デバッガ

デバッガもリモート環境中で動かせます。 ...動かせているはず。 試しにPHPxdebug動かしてみましょう。 とりあえず、以下のiniファイルを/usr/local/etc/php/conf.d/docker-php-ext-xdebug.iniに突っ込みます。

zend_extension=xdebug
xdebug.mode = debug
xdebug.start_with_request = yes
xdebug.client_port = 9000

自動でやるんならDockerfileで入れてあげればいいかなって思いますが、今回は普通に編集したのを突っ込みました。 普通に動きますね。

まとめ

というわけで、VS Code Remote Containersの自分流のやり方を書いてみました。 もし、もっと深く知りたいとかであれば、マニュアルサイトを見に行ってくれればと思います。

今回は今すぐに始めるとしたら、どういう感じで始められるかな?という観点でやってみました。 開発環境をリモートの隔離された環境に作り、ホストをきれいに保つというのは、私にとっては割と重要事項ですので、同じ考えを持つ方々にとって良い記事となっていれば幸いです。

今回はこんなところです。

参考

マニュアル
devcontainer.jsonの仕様