tag:crieit.net,2005:https://crieit.net/users/nikaera/feed nikaeraの投稿 - Crieit Crieitでユーザーnikaeraによる最近の投稿 2021-12-19T01:01:44+09:00 https://crieit.net/users/nikaera/feed tag:crieit.net,2005:PublicArticle/17872 2021-12-19T01:01:44+09:00 2021-12-19T01:01:44+09:00 https://crieit.net/posts/aws-ecs-fargate-amp-grafana 📔 ECS Fargate のメトリクスを Prometheus Agent 使って AMP に送って Grafana で監視する <h1 id="はじめに"><a href="#%E3%81%AF%E3%81%98%E3%82%81%E3%81%AB">はじめに</a></h1> <p><strong>この記事は <a target="_blank" rel="nofollow noopener" href="https://qiita.com/advent-calendar/2021/aws">AWS Advent Calendar 2021</a> の 5 日目の記事です。</strong></p> <p><a target="_blank" rel="nofollow noopener" href="https://aws.amazon.com/jp/fargate/">Fargate</a> で Node.js アプリのメトリクスを <a target="_blank" rel="nofollow noopener" href="https://prometheus.io/blog/2021/11/16/agent/">Prometheus Agent</a> をサイドカーコンテナとして動かして、<a target="_blank" rel="nofollow noopener" href="https://aws.amazon.com/jp/prometheus/">Amazon Managed Service for Prometheus (AMP)</a> に送信して <a target="_blank" rel="nofollow noopener" href="https://grafana.com/">Grafana</a> で見られるようにしてみました。</p> <p>ちなみに Promethus Agent はまだ <a target="_blank" rel="nofollow noopener" href="https://prometheus.io/blog/2021/11/16/agent/#prometheus-agent-mode">実験的な機能</a> なため、<strong>実務での利用は推奨しません。</strong></p> <p>本記事の環境構築には <a target="_blank" rel="nofollow noopener" href="https://aws.amazon.com/jp/cdk/">AWS CDK</a> を利用しています。</p> <h1 id="動作環境"><a href="#%E5%8B%95%E4%BD%9C%E7%92%B0%E5%A2%83">動作環境</a></h1> <ul> <li>Node.js v16.13.0</li> <li>AWS CDK 2.0.0 (build 4b6ce31)</li> <li>Prometheus 2.32.1</li> </ul> <h1 id="環境構築"><a href="#%E7%92%B0%E5%A2%83%E6%A7%8B%E7%AF%89">環境構築</a></h1> <h2 id="AWS CDK で環境構築する"><a href="#AWS+CDK+%E3%81%A7%E7%92%B0%E5%A2%83%E6%A7%8B%E7%AF%89%E3%81%99%E3%82%8B">AWS CDK で環境構築する</a></h2> <p>CDK で構築作業を進めます。まずは下記コマンドで CDK プロジェクトを作成します。使用言語は <code>TypeScript</code> を選択します。</p> <pre><code class="bash">mkdir prometheus-agent-test && cd prometheus-agent-test cdk init --language typescript </code></pre> <p>まず CDK でインフラ構築を進めていく前に、メトリクス収集テスト用の Node.js アプリを準備します。</p> <h3 id="ECS Fargate で動かす Node.js アプリを準備する"><a href="#ECS+Fargate+%E3%81%A7%E5%8B%95%E3%81%8B%E3%81%99+Node.js+%E3%82%A2%E3%83%97%E3%83%AA%E3%82%92%E6%BA%96%E5%82%99%E3%81%99%E3%82%8B">ECS Fargate で動かす Node.js アプリを準備する</a></h3> <p><a target="_blank" rel="nofollow noopener" href="https://github.com/siimon/prom-client">prom-client</a> を利用して、Node.js のメトリクスが取得できるだけの Node.js アプリを準備します。<code>prometheus-agent-test</code> フォルダで下記コマンドを実行します。</p> <pre><code class="bash">mkdir metrics-app && cd metrics-app npm init -y npm install --save prom-client </code></pre> <p>次に <code>metrics-app</code> フォルダ内に <code>index.js</code> を作成して下記を編集します。</p> <pre><code class="javascript">// metrics-app/index.js "use strict"; const http = require("http"); const server = http.createServer(); const client = require("prom-client"); const register = new client.Registry(); // 5秒間隔でメトリクスを取得する client.collectDefaultMetrics({ register, timeout: 5 * 1000 }); server.on("request", async function (req, res) { // /metrics にアクセスしたら、Prometheus のレポートを返す if (req.url === "/metrics") { res.setHeader("Content-Type", register.contentType); const metrics = await register.metrics(); return res.end(metrics); } else { return res.writeHead(404, { "Content-Type": "text/plain" }); } }); server.listen(8080); </code></pre> <p><code>node index.js</code> コマンドを実行して <code>http://localhost:8080/metrics</code> にアクセスしてみます。下記のように各種メトリクスが出力されている様子が確認できれば OK です。</p> <p><img src="https://i.gyazo.com/a5feae6dba9a9f4eaecae0055dd9be9e.png" alt="Prometheus のレポートが正常に出力されている様子" /><br /> <strong>Prometheus のレポートが正常に出力されている様子</strong></p> <p>今回は ECS 上で Node.js アプリを動作させるため、<code>Dockerfile</code> も作成します。</p> <pre><code class="docker"># metrics-app/Dockerfile FROM public.ecr.aws/docker/library/node:16-alpine3.12 AS builder EXPOSE 8080 WORKDIR /usr/src/app COPY package*.json ./ RUN npm install --max-old-space-size=4096 COPY . . CMD [ "node", "index.js" ] </code></pre> <p>上記 <code>Dockerfile</code> 作成後、再び動作検証のため下記コマンドを実行してから、<code>http://localhost:8080/metrics</code> にアクセスしてみます。</p> <pre><code class="bash">docker build -t prometheus-agent-test/metrics-app . docker run -p 8080:8080 prometheus-agent-test/metrics-app:latest </code></pre> <p>先ほどと同様に <code>http://localhost:8080/metrics</code> アクセス時に各種メトリクスが出力されている様子を確認できれば OK です。</p> <h3 id="Node.js アプリを監視する Prometheus Agent を準備する"><a href="#Node.js+%E3%82%A2%E3%83%97%E3%83%AA%E3%82%92%E7%9B%A3%E8%A6%96%E3%81%99%E3%82%8B+Prometheus+Agent+%E3%82%92%E6%BA%96%E5%82%99%E3%81%99%E3%82%8B">Node.js アプリを監視する Prometheus Agent を準備する</a></h3> <p>まずは Prometheus 関連ファイルを配置するためのフォルダを作成します。<code>prometheus-agent-test</code> フォルダ内で下記コマンドを実行します。</p> <pre><code class="bash">mkdir prometheus-agent && cd prometheus-agent </code></pre> <p>次に Prometheus の設定テンプレートファイルを作成します。テンプレートファイルは <a target="_blank" rel="nofollow noopener" href="https://atmarkit.itmedia.co.jp/ait/articles/1610/18/news008.html"><code>sed</code></a> を利用して中身の <code>__TASK_ID__</code> および <code>__REMOTE_WRITE_URL__</code> を書き換えて利用します。</p> <pre><code class="yaml"># prometheus-agent/prometheus.tmpl.yml global: scrape_interval: 5s external_labels: monitor: "prometheus" scrape_configs: - job_name: "prometheus-agent-test" static_configs: - targets: ["localhost:8080"] labels: # デフォルトの localhost:8080 がインスタンスとして利用されると、 # メトリクスの判別がしづらくなるため ECS Task の ID を利用する instance: "__TASK_ID__" remote_write: # AMP ワークスペース作成時に控えておいた、 # `エンドポイント - リモート書き込み URL` を設定する箇所 - url: "__REMOTE_WRITE_URL__" sigv4: region: ap-northeast-1 queue_config: max_samples_per_send: 1000 max_shards: 200 capacity: 2500 </code></pre> <p>設定ファイルの作成が完了したら、テンプレートファイルを利用して Prometheus の設定ファイルを作成し、Prometheus Agent を起動させるためのシェルスクリプトを作成します。</p> <pre><code class="sh"># prometheus-agent/docker-entrypoint.sh #!/bin/sh while [ -z "$taskId" ] do # ECS Fargate で起動したタスク ID を取得する taskId=$(curl --silent ${ECS_CONTAINER_METADATA_URI}/task | jq -r '.TaskARN | split("/") | .[-1]') echo "waiting..." sleep 1 done echo "taskId: ${taskId}" echo "remoteWriteUrl: ${REMOTE_WRITE_URL}" # タスク ID `taskId` および、環境変数 `REMOTE_WRITE_URL` で、 # Prometheus のテンプレートファイル `prometheus.tmpl.yml` の内容を書き換え、 # その結果を `/etc/prometheus/prometheus.yml` に出力する cat /etc/prometheus/prometheus.tmpl.yml | \ sed "s/__TASK_ID__/${taskId}/g" | \ sed "s>__REMOTE_WRITE_URL__>${REMOTE_WRITE_URL}>g" > /etc/prometheus/prometheus.yml # --enable-feature=agent で Prometheus を Agent モードで起動する # Prometheus のコンフィグファイルには上記で出力した `/etc/prometheus/prometheus.yml` を利用する /usr/local/bin/prometheus \ --enable-feature=agent \ --config.file=/etc/prometheus/prometheus.yml \ --web.console.libraries=/etc/prometheus/console_libraries \ --web.console.templates=/etc/prometheus/consoles </code></pre> <p>これで Prometheus Agent 起動のための準備は整ったため、最後に <code>Dockerfile</code> を準備します。ちなみに Prometheus Agent は <code>v2.32.0</code> 以降で利用可能です。<strong>本記事では <code>v2.32.1</code> を利用します。</strong></p> <pre><code class="docker"># prometheus-agent/Dockerfile FROM --platform=arm64 alpine:3.15 ADD prometheus.tmpl.yml /etc/prometheus/ RUN apk add --update --no-cache jq sed curl # ARM64 で動作する Prometheus v2.32.1 を curl でダウンロード展開する RUN curl -sL -O https://github.com/prometheus/prometheus/releases/download/v2.32.1/prometheus-2.32.1.linux-arm64.tar.gz RUN tar -zxvf prometheus-2.32.1.linux-arm64.tar.gz && rm prometheus-2.32.1.linux-arm64.tar.gz # `prometheus` コマンドを `/usr/local/bin/prometheus` に移動する RUN mv prometheus-2.32.1.linux-arm64/prometheus /usr/local/bin/prometheus COPY ./docker-entrypoint.sh / RUN chmod +x /docker-entrypoint.sh CMD ["/docker-entrypoint.sh"] </code></pre> <p>ここまでで CDK でインフラ整備を進めていくための下準備は完了です。</p> <h3 id="ECS Fargate 上で Node.js アプリおよび Prometheus Agent を動作させる"><a href="#ECS+Fargate+%E4%B8%8A%E3%81%A7+Node.js+%E3%82%A2%E3%83%97%E3%83%AA%E3%81%8A%E3%82%88%E3%81%B3+Prometheus+Agent+%E3%82%92%E5%8B%95%E4%BD%9C%E3%81%95%E3%81%9B%E3%82%8B">ECS Fargate 上で Node.js アプリおよび Prometheus Agent を動作させる</a></h3> <p>あとは CDK で ECS Fargate 上で Node.js アプリおよび Prometheus Agent、Grafana を動作させるための環境を整備していきます。</p> <p><code>lib/prometheus-agent-test-stack.ts</code> の内容を書き換えます。</p> <pre><code class="typescript">// lib/prometheus-agent-test-stack.ts import { Construct } from "constructs"; import { Stack, StackProps, aws_ecs as ecs, aws_logs as logs, aws_aps as aps, aws_ecs_patterns as ecs_patterns, aws_iam as iam, aws_elasticloadbalancingv2 as elbv2, Duration, CfnOutput, } from "aws-cdk-lib"; export class PrometheusAgentTestStack extends Stack { constructor(scope: Construct, id: string, props?: StackProps) { super(scope, id, props); // Node.js アプリに ecs_patterns.ApplicationLoadBalancedFargateService を利用して ALB 経由でアクセス可能にする const projectName = "prometheus-agent-test"; const fargateService = new ecs_patterns.ApplicationLoadBalancedFargateService( this, `${projectName}-fargate-service`, { serviceName: `${projectName}-fargate-service`, cpu: 256, desiredCount: 3, listenerPort: 80, taskImageOptions: { family: `${projectName}-taskdef`, image: ecs.ContainerImage.fromAsset("metrics-app"), containerPort: 8080, logDriver: ecs.LogDrivers.awsLogs({ streamPrefix: `/${projectName}/metrics-app`, logRetention: logs.RetentionDays.ONE_DAY, }), }, cluster: new ecs.Cluster(this, `${projectName}-cluster`, { clusterName: `${projectName}-cluster`, }), memoryLimitMiB: 512, } ); fargateService.targetGroup.configureHealthCheck({ path: "/metrics", timeout: Duration.seconds(8), interval: Duration.seconds(10), healthyThresholdCount: 2, unhealthyThresholdCount: 4, healthyHttpCodes: "200", }); // 本質ではないが、Gravition2 で動作させたいために RuntimePlatform のプロパティを上書きしている const fargateServiceTaskdef = fargateService.taskDefinition.node .defaultChild as ecs.CfnTaskDefinition; fargateServiceTaskdef.addPropertyOverride("RuntimePlatform", { CpuArchitecture: "ARM64", OperatingSystemFamily: "LINUX", }); // AMP への書き込み権限を付与する fargateService.taskDefinition.taskRole.addManagedPolicy( iam.ManagedPolicy.fromAwsManagedPolicyName( "AmazonPrometheusRemoteWriteAccess" ) ); // (2021/12/18) Amazon Managed Service for Prometheus のワークスペースを作成して、Prometheus の remote-write URL を取得する const apsWorkspace = new aps.CfnWorkspace( this, `${projectName}-prom-workspace`, { alias: `${projectName}-prom-workspace`, } ); const apsWorkspaceRemoteUrl = `${apsWorkspace.attrPrometheusEndpoint}api/v1/remote_write`; // (2021/12/18) 本記事で頻出する "エンドポイント - リモート書き込み URL" をコンソールに出力する new CfnOutput(this, "prom-remote-write-url", { value: apsWorkspaceRemoteUrl, description: "Prometheus Workspace の remote-write URL", exportName: "PromRemoteWriteURL", }); // AMP へメトリクス情報を送信するための Prometheus Agent コンテナを追加する const containerName = `${projectName}-prometheus-agent`; fargateService.taskDefinition.addContainer(containerName, { containerName, image: ecs.ContainerImage.fromAsset("prometheus-agent"), memoryReservationMiB: 128, environment: { // (2021/12/18) CDK 経由で作成した Prometheus の remote-write URL を設定する REMOTE_WRITE_URL: apsWorkspaceRemoteUrl, }, logging: new ecs.AwsLogDriver({ streamPrefix: `/${projectName}/prometheus-agent`, logRetention: logs.RetentionDays.ONE_DAY, }), }); // Grafana のタスク定義を作成する const grafanaDashboardTaskDefinition = new ecs.FargateTaskDefinition( this, `${projectName}-grafana-taskdef`, { family: `${projectName}-grafana-taskdef`, } ); // Grafana のタスクが Prometheus Query を叩けるように権限付与する grafanaDashboardTaskDefinition.taskRole.addManagedPolicy( iam.ManagedPolicy.fromAwsManagedPolicyName("AmazonPrometheusQueryAccess") ); // Grafana のコンテナを追加する。パスプレフィクスには dashboard を設定する const grafanaDashboardContainerName = `${projectName}-grafana-dashboard`; grafanaDashboardTaskDefinition.addContainer(grafanaDashboardContainerName, { containerName: grafanaDashboardContainerName, image: ecs.ContainerImage.fromRegistry("public.ecr.aws/ubuntu/grafana"), environment: { AWS_SDK_LOAD_CONFIG: "true", GF_AUTH_SIGV4_AUTH_ENABLED: "true", GF_SERVER_SERVE_FROM_SUB_PATH: "true", GF_SERVER_ROOT_URL: "%(protocol)s://%(domain)s/dashboard", }, portMappings: [{ containerPort: 3000 }], memoryLimitMiB: 512, logging: new ecs.AwsLogDriver({ streamPrefix: `/${projectName}/grafana-dashboard`, logRetention: logs.RetentionDays.ONE_DAY, }), }); const grafanaDashboardServiceName = `${projectName}-grafana-dashboard-service`; const grafanaDashboardService = new ecs.FargateService( this, grafanaDashboardServiceName, { serviceName: grafanaDashboardServiceName, cluster: fargateService.cluster, taskDefinition: grafanaDashboardTaskDefinition, desiredCount: 1, } ); // Grafana のタスクを ALB のターゲットグループに紐づける fargateService.listener.addTargets( `${projectName}-grafana-dashboard-target`, { priority: 1, conditions: [elbv2.ListenerCondition.pathPatterns(["/dashboard/*"])], healthCheck: { path: "/dashboard/login", interval: Duration.seconds(10), timeout: Duration.seconds(8), healthyThresholdCount: 2, unhealthyThresholdCount: 3, healthyHttpCodes: "200", }, port: 3000, protocol: elbv2.ApplicationProtocol.HTTP, targets: [grafanaDashboardService], } ); } } </code></pre> <p>その後、<code>cdk deploy</code> でインフラを構築します。</p> <p><img src="https://i.gyazo.com/c7da0f6c6b5a57edee47ae20a8026f8f.png" alt="CDK によるインフラ構築が正常に実行された時の様子" /><br /> <strong>CDK によるインフラ構築が正常に実行された時の様子</strong></p> <p>デプロイが正常に完了したのを確認したら、<code>Outputs</code> に出力されている <strong><code>PrometheusAgentTestStack.prometheusagenttestfargateserviceServiceURL<識別子></code> の URL 末尾に <code>/metrics</code> を付与してアクセスしてみます。</strong> 出力されている URL のフォーマットは <code>http://<識別子>.ap-northeast-1.elb.amazonaws.com</code> になります。</p> <p>つまり、<strong><code>http://<識別子>.ap-northeast-1.elb.amazonaws.com/metrics</code> にアクセスします。</strong></p> <p><img src="https://i.gyazo.com/c13a2b0efc3bb96e79b4f1f5a2886a8a.png" alt="ALB 経由で Node.js アプリにアクセス可能なことを確認する" /><br /> <strong>ALB 経由で Node.js アプリにアクセス可能なことを確認する</strong></p> <p>また、<code>Outputs</code> に出力されている <strong><code>PrometheusAgentTestStack.promremotewriteurl</code> は後に利用する <code>エンドポイント - リモート書き込み URL</code> で使用するので控えておきます。</strong></p> <p>ここまでで AWS CDK でのインフラ構築作業は完了しました。最後に Grafana で AMP のメトリクスを可視化するための作業を進めていきます。</p> <h1 id="Grafana で Prometheus (AMP) のメトリクスを可視化する"><a href="#Grafana+%E3%81%A7+Prometheus+%28AMP%29+%E3%81%AE%E3%83%A1%E3%83%88%E3%83%AA%E3%82%AF%E3%82%B9%E3%82%92%E5%8F%AF%E8%A6%96%E5%8C%96%E3%81%99%E3%82%8B">Grafana で Prometheus (AMP) のメトリクスを可視化する</a></h1> <p>先ほどの <code>/metrics</code> パスへのアクセス同様、<code>Outputs</code> に出力されている URL の末尾に <code>/dashboard/login</code> を付与してアクセスします。<strong>Grafana の初期ユーザおよびパスワードは <code>admin</code> となります。</strong></p> <p>つまり、<code>http://<識別子>.ap-northeast-1.elb.amazonaws.com/dashboard/login</code> にアクセスしてみます。</p> <p><img src="https://i.gyazo.com/fe4ea6515f54dec23234e10a93023a36.png" alt="ログインを行う" /></p> <p>ログイン情報が正しければ、新しいパスワードを設定する画面に遷移するので新たなパスワードを入力してログインを終えます。ログイン後は、Prometheus (AMP) をデータソースとして追加するために下記の操作を行います。</p> <p><img src="https://i.gyazo.com/599d49e4167ccc35c5d44d5df7678036.png" alt="1. 歯車アイコンをクリックして <code>Data sources</code> をクリックする" /><br /> <strong>1. 歯車アイコンをクリックして <code>Data sources</code> をクリックする</strong></p> <p><img src="https://i.gyazo.com/e88bfc58a35deaa16a16920a5e551837.png" alt="2. <code>Add data source</code> ボタンをクリックする" /><br /> <strong>2. <code>Add data source</code> ボタンをクリックする</strong></p> <p><img src="https://i.gyazo.com/1c8031a3182f03e20194bf0b76e66e5a.png" alt="3. データソースとして Prometheus を選択する" /><br /> <strong>3. データソースとして Prometheus を選択する</strong></p> <p><img src="https://i.gyazo.com/1684c1fca9decb29e60bd654400fd06b.png" alt="4. Prometheus をデータソースとして追加する" /></p> <p><img src="https://i.gyazo.com/2119361eac350044e783ed0c9280580a.png" alt="4. Prometheus をデータソースとして追加する" /></p> <p><img src="https://i.gyazo.com/00cc7edbfc45dbd43ad3b0cccea72c7d.png" alt="4. Prometheus をデータソースとして追加する" /><br /> <strong>4. Prometheus をデータソースとして追加する</strong></p> <p>Prometheus (AMP) に送信したメトリクスを Grafana で可視化するための準備が整ったので、実際に Grafana のダッシュボードでメトリクスを可視化してみます。手っ取り早くメトリクスを可視化するため、ダッシュボードには <a target="_blank" rel="nofollow noopener" href="https://grafana.com/grafana/dashboards/11159">NodeJS Application Dashboard</a> を利用します。</p> <p><img src="https://i.gyazo.com/b36c0281e273e21fc9474666786281c3.png" alt="1. + アイコンをクリックして、<code>Import</code> をクリックする" /><br /> <strong>1. + アイコンをクリックして、<code>Import</code> をクリックする</strong></p> <p><img src="https://i.gyazo.com/130718dff8b86758acb415764bd37338.png" alt="2. <code>NodeJS Application Dashboard</code> の ID を入力して <code>Load</code> ボタンをクリックする" /><br /> <strong>2. <code>NodeJS Application Dashboard</code> の ID を入力して <code>Load</code> ボタンをクリックする</strong></p> <p><img src="https://i.gyazo.com/2aa8903f3f64d8021699cd5d4bfa9757.png" alt="3. 必要な情報を入力して <code>NodeJS Application Dashboard</code> のインポートを完了する" /><br /> <strong>3. 必要な情報を入力して <code>NodeJS Application Dashboard</code> のインポートを完了する</strong></p> <p><img src="https://i.gyazo.com/1378b09c281da28653917ee40494dee8.png" alt="4. ダッシュボードから Prometheus のメトリクスが確認できる" /><br /> <strong>4. ダッシュボードから Node.js アプリのメトリクスが確認できる</strong></p> <p>ここまでの手順でメトリクスの可視化は完了しましたが、負荷に応じて実際にメトリクスが変化する様子も確認してみます。<a target="_blank" rel="nofollow noopener" href="https://github.com/tsenart/vegeta">Vegeta</a> を利用して、実際に負荷をかけてみます。下記コマンドを実行します。</p> <pre><code class="bash">echo 'GET http://<識別子>.ap-northeast-1.elb.amazonaws.com/metrics' | vegeta attack -duration=5s | vegeta report </code></pre> <p>その後、再び Grafana のダッシュボードを見にいきます。負荷をかけた時間帯のみグラフに変化があることを確認できるはずです。</p> <p><img src="https://i.gyazo.com/d019d33d3e4bd321ae4d1f4bbecc6ef8.png" alt="ダッシュボードの CPU 使用率のグラフに変化があったことを確認できる" /><br /> <strong>ダッシュボードの CPU 使用率のグラフに変化があったことを確認できる</strong></p> <h1 id="おわりに"><a href="#%E3%81%8A%E3%82%8F%E3%82%8A%E3%81%AB">おわりに</a></h1> <p>今回は ECS Fargate のメトリクスを Prometheus Agent で Amazon Managed Service for Prometheus (AMP) に送信し、それを Grafana で可視化する方法について紹介しました。</p> <p>ECS のサービスでタスクを実行する場合は <a target="_blank" rel="nofollow noopener" href="https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/developerguide/service-discovery.html">サービスディスカバリ</a> の利用が可能なため、Prometheus の <a target="_blank" rel="nofollow noopener" href="https://prometheus.io/docs/prometheus/latest/configuration/configuration/#dns_sd_config">サービスディスカバリの設定</a> を行うことで、単一の Prometheus で全てのコンテナのメトリクスを扱うことも可能です。</p> <p>また Node.js アプリを作成する際に利用した <code>prom-client</code> で <a target="_blank" rel="nofollow noopener" href="https://github.com/siimon/prom-client#custom-metrics">カスタムメトリクス</a> を作成することで、監視したい項目を自由に増やすことも可能です。</p> <p>本記事が ECS Fargate を監視する際の検討材料の 1 つとなれたら幸いです。</p> <h1 id="参考リンク"><a href="#%E5%8F%82%E8%80%83%E3%83%AA%E3%83%B3%E3%82%AF">参考リンク</a></h1> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://aws.amazon.com/jp/fargate/">AWS Fargate(サーバーやクラスターの管理が不要なコンテナの使用)| AWS</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://prometheus.io/blog/2021/11/16/agent/">Introducing Prometheus Agent Mode, an Efficient and Cloud-Native Way for Metric Forwarding | Prometheus</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://aws.amazon.com/jp/prometheus/">Amazon Managed Service for Prometheus | フルマネージド Prometheus | Amazon Web Services</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://grafana.com/">Grafana: The open observability platform | Grafana Labs</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://aws.amazon.com/jp/cdk/">AWS クラウド開発キット – アマゾン ウェブ サービス</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://github.com/siimon/prom-client">siimon/prom-client: Prometheus client for node.js</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://github.com/tsenart/vegeta">tsenart/vegeta: HTTP load testing tool and library. It's over 9000!</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://grafana.com/grafana/dashboards/11159">NodeJS Application Dashboard dashboard for Grafana | Grafana Labs</a></li> </ul> nikaera tag:crieit.net,2005:PublicArticle/17602 2021-08-16T02:31:05+09:00 2021-08-22T07:44:40+09:00 https://crieit.net/posts/unity-ios-android-secret-manager 📔Unity で iOS/Android アプリの設定値をセキュアに扱う方法 <h1 id="はじめに"><a href="#%E3%81%AF%E3%81%98%E3%82%81%E3%81%AB">はじめに</a></h1> <p>iOS/Android でユーザーの情報をセキュアに扱う必要があったので、調査したところ Android には <a target="_blank" rel="nofollow noopener" href="https://developer.android.com/reference/androidx/security/crypto/EncryptedSharedPreferences">EncryptedSharedPreferences</a> が存在することを知りました。iOS には <a target="_blank" rel="nofollow noopener" href="https://developer.apple.com/documentation/security/keychain_services">Keychain Services</a> が存在します。</p> <p>今回は Unity の iOS/Android プラットフォーム上で設定値を保存するための実装を行う必要があったので、Unity から扱えるようネイティブプラグインを作成しました。今後もこういった要望はありそうでしたので、記事として手順や内容を書き記しておくことにしました。</p> <p>本記事内で紹介しているコードは下記にアップ済みです。</p> <p><a target="_blank" rel="nofollow noopener" href="https://github.com/nikaera/Unity-iOS-Android-SecretManager-Sample">https://github.com/nikaera/Unity-iOS-Android-SecretManager-Sample</a></p> <h1 id="動作環境"><a href="#%E5%8B%95%E4%BD%9C%E7%92%B0%E5%A2%83">動作環境</a></h1> <ul> <li>MacBook Air (M1, 2020)</li> <li>Unity 2020.3.15f2</li> <li>Android 6.0 以上 <ul> <li><a target="_blank" rel="nofollow noopener" href="https://developer.android.com/reference/androidx/security/crypto/EncryptedSharedPreferences">EncryptedSharedPreferences</a> が使用可能なバージョン</li> </ul></li> </ul> <h1 id="Android のネイティブプラグインを作成する"><a href="#Android+%E3%81%AE%E3%83%8D%E3%82%A4%E3%83%86%E3%82%A3%E3%83%96%E3%83%97%E3%83%A9%E3%82%B0%E3%82%A4%E3%83%B3%E3%82%92%E4%BD%9C%E6%88%90%E3%81%99%E3%82%8B">Android のネイティブプラグインを作成する</a></h1> <p>Android 環境ではまず <a target="_blank" rel="nofollow noopener" href="https://github.com/googlesamples/unity-jar-resolver">External Dependency Manager for Unity</a> を利用して、Unity の Android ネイティブプラグインで <code>EncryptedSharedPreferences</code> 利用可能にします。</p> <h2 id="(追記) Gradle を利用したライブラリのインストール方法"><a href="#%28%E8%BF%BD%E8%A8%98%29+Gradle+%E3%82%92%E5%88%A9%E7%94%A8%E3%81%97%E3%81%9F%E3%83%A9%E3%82%A4%E3%83%96%E3%83%A9%E3%83%AA%E3%81%AE%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%88%E3%83%BC%E3%83%AB%E6%96%B9%E6%B3%95">(追記) Gradle を利用したライブラリのインストール方法</a></h2> <p><a target="_blank" rel="nofollow noopener" href="https://twitter.com/shiena">shiena</a> さんにご教授いただいたのですが、<a target="_blank" rel="nofollow noopener" href="https://zenn.dev/shiena/articles/unity-sqlcipher#gradle%E3%82%92%E5%88%A9%E7%94%A8">こちらの記事</a>のように Gradle を利用することでも簡易にライブラリの取り込みが可能なようでした。</p> <p>手順は上記の記事をご参照いただくとして、Gradle を利用する方法で外部ライブラリを取り込む際の <code>Assets/Plugins/Android/mainTemplate.gradle</code> および <code>Assets/Plugins/Android/gradleTemplate.properties</code> は下記になります。</p> <p>```diff gradle:Plugins/Android/mainTemplate.gradle<br /> dependencies {<br /> implementation fileTree(dir: 'libs', include: ['*.jar'])<br /> + implementation 'androidx.security:security-crypto:1.1.0-alpha03'<br /> <strong>DEPS</strong>}</p> <p>android {</p> <pre><code><br />```diff properties:Assets/Plugins/Android/gradleTemplate.properties org.gradle.jvmargs=-Xmx**JVM_HEAP_SIZE**M org.gradle.parallel=true android.enableR8=**MINIFY_WITH_R_EIGHT** + android.useAndroidX=true unityStreamingAssets=.unity3d**STREAMING_ASSETS** **ADDITIONAL_PROPERTIES** </code></pre> <p><strong>Gradle を利用した方法でライブラリを利用される際は、次の <code>External Dependency Manager for Unity で必要なパッケージをインストールする</code> の手順はスキップ可能です。<code>EncryptedSharedPreferences を利用するためのネイティブコードを追加する</code> のステップから進めてください。</strong></p> <p><code>External Dependency Manager for Unity</code> を利用する方法だと、取り込み先プロジェクト内でライブラリの競合が発生する恐れがあります。Gradle を利用する方法であれば回避が可能です。<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup></p> <h2 id="External Dependency Manager for Unity で必要なパッケージをインストールする"><a href="#External+Dependency+Manager+for+Unity+%E3%81%A7%E5%BF%85%E8%A6%81%E3%81%AA%E3%83%91%E3%83%83%E3%82%B1%E3%83%BC%E3%82%B8%E3%82%92%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%88%E3%83%BC%E3%83%AB%E3%81%99%E3%82%8B">External Dependency Manager for Unity で必要なパッケージをインストールする</a></h2> <p><code>External Dependency Manager for Unity</code> をインポートするため <a target="_blank" rel="nofollow noopener" href="https://github.com/googlesamples/unity-jar-resolver/blob/master/external-dependency-manager-latest.unitypackage">unitypackage</a> をダウンロードして、<strong><code>EncryptedSharedPreferences</code> を導入したい Unity プロジェクトを開いてから <code>unitypackage</code> をクリックすることで、<code>External Dependency Manager for Unity</code> を Unity プロジェクトにインポートします。</strong></p> <p><img src="https://i.gyazo.com/1af7cdf4d7d5749e59e151eef1ca5493.png" alt="ダウンロードした <code>unitypackage</code> をクリックして Unity プロジェクトに External Dependency Manager for Unity をインポートする" /></p> <p>Unity プロジェクトの <code>Build Settings</code> からプラットフォームは Android に切り替えておきます。<code>Enable Android Auto-resolution?</code> というダイアログの選択肢はどちらを選んでも構いません。<sup id="fnref:2"><a href="#fn:2" class="footnote-ref" role="doc-noteref">2</a></sup></p> <p>External Dependency Manager for Unity で各種パッケージを管理する方法は <a target="_blank" rel="nofollow noopener" href="https://github.com/googlesamples/unity-jar-resolver#android-resolver-usage">README</a> に記載がある通り、<strong><code>*Dependencies.xml</code> というファイルを <code>Editor</code> フォルダに配置することで可能になります。</strong></p> <p>今回は <code>EncryptedSharedPreferences</code> を導入するため、下記の xml ファイルを <code>Editor</code> フォルダ内に配置します。</p> <pre><code class="xml"><!-- Assets/Editor/AndroidPluginDependencies.xml --> <?xml version="1.0" encoding="utf-8"?> <dependencies> <androidPackages> <!-- 本記事ではバージョン 1.1.0-alpha03 を利用している --> <androidPackage spec="androidx.security:security-crypto:1.1.0-alpha03"> <androidSdkPackageIds> <!-- Google の Maven リポジトリからインストールするため、 extra-google-m2repository を指定する --> <androidSdkPackageId>extra-google-m2repository</androidSdkPackageId> </androidSdkPackageIds> </androidPackage> </androidPackages> </dependencies> </code></pre> <p>その後、<strong>Unity メニューから <code>Assets -> External Dependency Manager -> Android Resolver -> Force Resolve</code> を選択して、<code>Assets/Editor/AndroidPluginDependencies.xml</code> の内容を元に <code>EncryptedSharedPreferences</code> を利用するのに必要なパッケージを自動で <code>Assets/Plugins/Android</code> フォルダにダウンロードします。</strong></p> <p><img src="https://i.gyazo.com/df394e15149e54dae3e9a81848512ee9.png" alt="1. Unity メニューから <code>Assets -> External Dependency Manager -> Android Resolver -> Force Resolve</code> を選択する" /><br /> <strong>1. Unity メニューから <code>Assets -> External Dependency Manager -> Android Resolver -> Force Resolve</code> を選択する</strong></p> <p><img src="https://i.gyazo.com/f6d2ec95ef9c2afdc857fecef2b165e5.png" alt="2. 実行に成功すると EncryptedSharedPreferences を利用するのに必要なライブラリ群が <code>Assets/Plugins/Android</code> フォルダに配置される" /><br /> <strong>2. 実行に成功すると EncryptedSharedPreferences を利用するのに必要なライブラリ群が <code>Assets/Plugins/Android</code> フォルダに配置される</strong></p> <p>ここまで来ればあとは Android ネイティブコードを <code>Assets/Plugins/Android</code> フォルダ内に配置して Unity 側から叩けるようにするだけです。</p> <h2 id="EncryptedSharedPreferences を利用するためのネイティブコードを追加する"><a href="#EncryptedSharedPreferences+%E3%82%92%E5%88%A9%E7%94%A8%E3%81%99%E3%82%8B%E3%81%9F%E3%82%81%E3%81%AE%E3%83%8D%E3%82%A4%E3%83%86%E3%82%A3%E3%83%96%E3%82%B3%E3%83%BC%E3%83%89%E3%82%92%E8%BF%BD%E5%8A%A0%E3%81%99%E3%82%8B">EncryptedSharedPreferences を利用するためのネイティブコードを追加する</a></h2> <p>早速下記の Android ネイティブコードを <code>Assets/Plugins/Android~~<del>/SecretManager.java</code> に配置します。</p> <pre><code class="java">// Assets/Plugins/Android/SecretManager.java package com.nikaera; import com.unity3d.player.UnityPlayerActivity; import java.lang.Exception; // External Dependency Manager for Unity によって、 // 必要な jar が含まれているため EncryptedSharedPreferences の利用が可能になる import androidx.security.crypto.EncryptedSharedPreferences; import androidx.security.crypto.MasterKey; import android.content.Context; import android.content.SharedPreferences; import android.content.SharedPreferences.Editor; import android.os.Bundle; import android.util.Log; public class SecretManager { private SharedPreferences sharedPreferences; public SecretManager(Context context) { try { // EncryptedSharedPreferences で設定値を保存する際に用いる、 // 暗号鍵を扱うためのラッパークラスをデフォルト設定で作成する MasterKey masterKey = new MasterKey.Builder(context) .setKeyScheme(MasterKey.KeyScheme.AES256_GCM) .build(); // EncryptedSharedPreferences のインスタンスを生成する // コンストラクタで作成した masterKey を指定している this.sharedPreferences = EncryptedSharedPreferences.create( context, context.getPackageName(), masterKey, EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM ); } catch (Exception e) { e.printStackTrace(); } } /** * 指定したキーで値を保存する関数 * @param key 値を保存する際に用いるキー * @param value 保存したい値 * @return boolean 値の保存に成功したかどうか */ public boolean put(String key, String value) { SharedPreferences.Editor editor = sharedPreferences.edit(); editor.putString(key, value); return editor.commit(); } /** * 指定したキーで保存した値を取得する関数 * `put` 関数で保存した値を取得するのに利用する * @param key 取得したい値のキー * @return string キーに紐づく値、存在しなければ空文字が返却される */ public String get(String key) { return sharedPreferences.getString(key, null); } /** * 指定したキーで値を削除する関数 * @param key 削除したい値のキー * @return boolean 値の削除に成功したかどうか */ public boolean delete(String key) { SharedPreferences.Editor editor = sharedPreferences.edit(); editor.remove(key); return editor.commit(); } } </code></pre> <p>その後、上記を Unity スクリプトから実行可能にするための C# クラスを作成します。本記事ではファイルを <code>Assets/Scripts/EncryptedSharedPreferences.cs</code> に配置します。</p> <pre><code class="csharp">// Assets/Scripts/EncryptedSharedPreferences.cs</del><del> using UnityEngine; /// <summary> /// 利用するネイティブコードは <c>Assets/Plugins/Android/SecretManager.java</c> に記載 /// </summary> /// <remarks> /// <a href="https://developer.android.com/reference/androidx/security/crypto/EncryptedSharedPreferences">EncryptedSharedPreferences</a> /// </remarks> class EncryptedSharedPreferences { private readonly AndroidJavaObject _secretManager; public EncryptedSharedPreferences() { // コンストラクタで com.nikaera.SecretManager のインスタンス生成を行う var activity = new AndroidJavaClass("com.unity3d.player.UnityPlayer") .GetStatic<AndroidJavaObject>("currentActivity"); var context = activity.Call<AndroidJavaObject>("getApplicationContext"); _secretManager = new AndroidJavaObject("com.nikaera.SecretManager", context); } public bool Put(string key, string value) { return _secretManager.Call<bool>("put", key, value); } public string Get(string key) { return _secretManager.Call<string>("get", key); } public bool Delete(string key) { return _secretManager.Call<bool>("delete", key); } } </code></pre> <p>あとは用途に応じて下記のようなコードで設定値の保存や取得などを行えます。</p> <pre><code class="csharp">// ... var _sharedPreferences = new EncryptedSharedPreferences(); // name をキーとして値を nikaera で保存する _sharedPreferences.Put("name", "nikaera"); // name をキーとして値を取得する var name = _sharedPreferences.Get("name"); // "nikaera" が出力される Debug.Log(name); // name をキーとして値を削除する _sharedPreferences.Delete("name"); // ... </code></pre> <h1 id="iOS のネイティブプラグインを作成する"><a href="#iOS+%E3%81%AE%E3%83%8D%E3%82%A4%E3%83%86%E3%82%A3%E3%83%96%E3%83%97%E3%83%A9%E3%82%B0%E3%82%A4%E3%83%B3%E3%82%92%E4%BD%9C%E6%88%90%E3%81%99%E3%82%8B">iOS のネイティブプラグインを作成する</a></h1> <p>iOS の場合は外部ライブラリを利用しないため、<code>External Dependency Manager for Unity</code> は利用しません。<strong>本来であれば Swift で信頼できる外部フレームワークを取り込み利用できると良さそうですが、今回は Objective-C でネイティブプラグインを書いていきます。</strong><sup id="fnref:3"><a href="#fn:3" class="footnote-ref" role="doc-noteref">3</a></sup></p> <h2 id="Keychain Services を利用するためのネイティブコードを追加する"><a href="#Keychain+Services+%E3%82%92%E5%88%A9%E7%94%A8%E3%81%99%E3%82%8B%E3%81%9F%E3%82%81%E3%81%AE%E3%83%8D%E3%82%A4%E3%83%86%E3%82%A3%E3%83%96%E3%82%B3%E3%83%BC%E3%83%89%E3%82%92%E8%BF%BD%E5%8A%A0%E3%81%99%E3%82%8B">Keychain Services を利用するためのネイティブコードを追加する</a></h2> <p>早速下記の iOS ネイティブコードを <code>Assets/Plugins/iOS/KeychainService.mm</code> に配置します。</p> <pre><code class="objc">// Assets/Plugins/iOS/KeychainService.mm</del>~~ // Keychain Services を利用するために Security フレームワークを利用する #import <Security/Security.h> extern "C" { // 指定したキーで値を保存する関数 // - param // - dataType: 値を保存する際に用いるキー // - value: 保存したい値 // - return // - 保存時のステータスコードを返却する (0 以外は失敗) int addItem(const char *dataType, const char *value) { NSMutableDictionary* attributes = nil; NSMutableDictionary* query = [NSMutableDictionary dictionary]; NSData* sata = [[NSString stringWithCString:value encoding:NSUTF8StringEncoding] dataUsingEncoding:NSUTF8StringEncoding]; [query setObject:(id)kSecClassGenericPassword forKey:(id)kSecClass]; [query setObject:(id)[NSString stringWithCString:dataType encoding:NSUTF8StringEncoding] forKey:(id)kSecAttrAccount]; OSStatus err = SecItemCopyMatching((CFDictionaryRef)query, NULL); if (err == noErr) { attributes = [NSMutableDictionary dictionary]; [attributes setObject:sata forKey:(id)kSecValueData]; [attributes setObject:[NSDate date] forKey:(id)kSecAttrModificationDate]; err = SecItemUpdate((CFDictionaryRef)query, (CFDictionaryRef)attributes); return (int)err; } else if (err == errSecItemNotFound) { attributes = [NSMutableDictionary dictionary]; [attributes setObject:(id)kSecClassGenericPassword forKey:(id)kSecClass]; [attributes setObject:(id)[NSString stringWithCString:dataType encoding:NSUTF8StringEncoding] forKey:(id)kSecAttrAccount]; [attributes setObject:sata forKey:(id)kSecValueData]; [attributes setObject:[NSDate date] forKey:(id)kSecAttrCreationDate]; [attributes setObject:[NSDate date] forKey:(id)kSecAttrModificationDate]; err = SecItemAdd((CFDictionaryRef)attributes, NULL); return (int)err; } else { return (int)err; } } // 指定したキーで値を取得する関数 // - param // - dataType: 値を取得する際に用いるキー // - return // - キーに紐づく値、存在しなければ空文字が返却される char* getItem(const char *dataType) { NSMutableDictionary* query = [NSMutableDictionary dictionary]; [query setObject:(id)kSecClassGenericPassword forKey:(id)kSecClass]; [query setObject:(id)[NSString stringWithCString:dataType encoding:NSUTF8StringEncoding] forKey:(id)kSecAttrAccount]; [query setObject:(id)kCFBooleanTrue forKey:(id)kSecReturnData]; CFDataRef cfresult = NULL; OSStatus err = SecItemCopyMatching((CFDictionaryRef)query, (CFTypeRef*)&cfresult); if (err == noErr) { NSData* passwordData = (__bridge_transfer NSData *)cfresult; const char* value = [[[NSString alloc] initWithData:passwordData encoding:NSUTF8StringEncoding] UTF8String]; char *str = strdup(value); return str; } else { return NULL; } } // 指定したキーで値を削除する関数 // - param // - dataType: 値を削除する際に用いるキー // - return // - 保存時のステータスコードを返却する (0 以外は失敗) int deleteItem(const char *dataType) { NSMutableDictionary* query = [NSMutableDictionary dictionary]; [query setObject:(id)kSecClassGenericPassword forKey:(id)kSecClass]; [query setObject:(id)[NSString stringWithCString:dataType encoding:NSUTF8StringEncoding] forKey:(id)kSecAttrAccount]; OSStatus err = SecItemDelete((CFDictionaryRef)query); if (err == noErr) { return 0; } else { return (int)err; } } } </code></pre> <p><code>Keychain Services</code> は <code>Security</code> フレームワークを利用するため、<strong><code>KeychainService.mm</code> に対して <code>Security</code> フレームワークの依存関係を設定する必要があります。</strong></p> <p><img src="https://i.gyazo.com/ba82aaced24b83b37bf8c63e1ee7142f.png" alt="<code>KeychainService.mm</code> で <code>Security</code> フレームワークの利用を可能にする" /><br /> <strong><code>KeychainService.mm</code> で <code>Security</code> フレームワークの利用を可能にする</strong></p> <p>その後、上記を Unity スクリプトから実行可能にするための C# クラスを作成します。本記事ではファイルを <code>Assets/Scripts/KeychainService.cs</code> に配置します。</p> <pre><code class="csharp">// Assets/Scripts/KeychainService.cs using System.Runtime.InteropServices; /// <summary> /// 実装は <c>Assets/Plugins/iOS/KeychainService.mm</c> に記載 /// </summary> /// <remarks> /// <a href="https://developer.apple.com/documentation/security/keychain_services">Keychain Services</a> /// </remarks> class KeychainService { #if UNITY_IOS [DllImport("__Internal")] private static extern int addItem(string dataType, string value); [DllImport("__Internal")] private static extern string getItem(string dataType); [DllImport("__Internal")] private static extern int deleteItem(string dataType); #endif public bool Put(string key, string value) { #if UNITY_IOS // 返却されるステータスが 0 なら成功 return addItem(key, value) == 0; #endif } public string Get(string key) { #if UNITY_IOS return getItem(key); #else return null; #endif } public bool Delete(string key) { #if UNITY_IOS // 返却されるステータスが 0 なら成功 return deleteItem(key) == 0; #endif } } </code></pre> <p>あとは用途に応じて下記のようなコードで設定値の保存や取得などを行えます。</p> <pre><code class="csharp">// ... var _keychainService = new KeychainService(); // name をキーとして値を nikaera で保存する _keychainService.Put("name", "nikaera"); // name をキーとして値を取得する var name = _keychainService.Get("name"); // "nikaera" が出力される Debug.Log(name); // name をキーとして値を削除する _keychainService.Delete("name"); // ... </code></pre> <h1 id="(余談) インターフェースで iOS/Android のふるまいを共通化する"><a href="#%28%E4%BD%99%E8%AB%87%29+%E3%82%A4%E3%83%B3%E3%82%BF%E3%83%BC%E3%83%95%E3%82%A7%E3%83%BC%E3%82%B9%E3%81%A7+iOS%2FAndroid+%E3%81%AE%E3%81%B5%E3%82%8B%E3%81%BE%E3%81%84%E3%82%92%E5%85%B1%E9%80%9A%E5%8C%96%E3%81%99%E3%82%8B">(余談) インターフェースで iOS/Android のふるまいを共通化する</a></h1> <p>このままだとプラットフォームを切り替える毎にコードを書き直さないとならないので、インターフェースを利用して共通化を行います。</p> <pre><code class="csharp">public interface ISecretManager { /// <summary> /// 指定したキーで値を保存する関数 /// </summary> /// <param name="key">キー</param> /// <param name="value">値</param> /// <returns>保存に成功したかどうか</returns> bool Put(string key, string value); /// <summary> /// 指定したキーの値を取得する関数 /// </summary> /// <param name="key">キー</param> /// <returns>指定したキーで設定された値、無ければ null</returns> string Get(string key); /// <summary> /// 指定したキーの値を削除する関数 /// </summary> /// <param name="key">キー</param> /// <returns>削除に成功したかどうか</returns> bool Delete(string key); } </code></pre> <p>その後、<code>Assets/Scripts/EncryptedSharedPreferences.cs</code> および <code>Assets/Scripts/KeychainService.cs</code> を下記の通り <code>ISecretManager</code> の実装に紐付けます。</p> <pre><code class="csharp">// Assets/Scripts/EncryptedSharedPreferences.cs using UnityEngine; /// <summary> /// 利用するネイティブコードは <c>Assets/Plugins/Android/SecretManager.java</c> に記載 /// </summary> /// <remarks> /// <a href="https://developer.android.com/reference/androidx/security/crypto/EncryptedSharedPreferences">EncryptedSharedPreferences</a> /// </remarks> class EncryptedSharedPreferences: ISecretManager { private readonly AndroidJavaObject _secretManager; public EncryptedSharedPreferences() { var activity = new AndroidJavaClass("com.unity3d.player.UnityPlayer") .GetStatic<AndroidJavaObject>("currentActivity"); var context = activity.Call<AndroidJavaObject>("getApplicationContext"); _secretManager = new AndroidJavaObject("com.nikaera.SecretManager", context); } #region ISecretManager public bool Put(string key, string value) { return _secretManager.Call<bool>("put", key, value); } public string Get(string key) { return _secretManager.Call<string>("get", key); } public bool Delete(string key) { return _secretManager.Call<bool>("delete", key); } #endregion } </code></pre> <pre><code class="csharp">// Assets/Scripts/KeychainService.cs using System.Runtime.InteropServices; /// <summary> /// 実装は <c>Assets/Plugins/iOS/KeychainService.mm</c> に記載 /// </summary> /// <remarks> /// <a href="https://developer.apple.com/documentation/security/keychain_services">Keychain Services</a> /// </remarks> class KeychainService: ISecretManager { #if UNITY_IOS [DllImport("__Internal")] private static extern int addItem(string dataType, string value); [DllImport("__Internal")] private static extern string getItem(string dataType); [DllImport("__Internal")] private static extern int deleteItem(string dataType); #endif // KeychainService.mm に定義した関数を呼び出す #region ISecretManager public bool Put(string key, string value) { #if UNITY_IOS return addItem(key, value) == 0; #else return false; #endif } public string Get(string key) { #if UNITY_IOS return getItem(key); #else return null; #endif } public bool Delete(string key) { #if UNITY_IOS return deleteItem(key) == 0; #else return false; #endif } #endregion } </code></pre> <p>あとは上記をよしなに利用可能な <code>SecretManager</code> クラスを作成します。</p> <pre><code class="csharp">// Assets/Scripts/SecretManager.cs using UnityEngine; /// <summary> /// <em>Editor 利用時のみ PlayerPrefs を利用する</em> /// </summary> /// <remarks><see cref="KeychainService" />, <see cref="EncryptedSharedPreferences" /></remarks> public static class SecretManager { #if UNITY_EDITOR #elif UNITY_ANDROID private static ISecretManager _instance = new EncryptedSharedPreferences(); #elif UNITY_IOS private static ISecretManager _instance = new KeychainService(); #endif public static bool Put(string key, string value) { #if UNITY_EDITOR PlayerPrefs.SetString(key, value); PlayerPrefs.Save(); return true; #elif UNITY_IOS || UNITY_ANDROID return _instance.Put(key, value); #else Debug.Log("Not Implemented."); return false; #endif } public static string Get(string key) { #if UNITY_EDITOR return PlayerPrefs.GetString(key); #elif UNITY_IOS || UNITY_ANDROID return _instance.Get(key); #else Debug.Log("Not Implemented."); return null; #endif } public static bool Delete(string key) { #if UNITY_EDITOR PlayerPrefs.DeleteKey(key); PlayerPrefs.Save(); return true; #elif UNITY_IOS || UNITY_ANDROID return _instance.Delete(key); #else Debug.Log("Not Implemented."); return false; #endif } } </code></pre> <p>これでプラットフォーム間の実装差異を気にすることなく、下記のような記述で設定値の保存や取得などを行えます。<strong>iOS/Android 以外のプラットフォームで追加実装したい場合は<a target="_blank" rel="nofollow noopener" href="https://docs.unity3d.com/ja/2021.1/Manual/PlatformDependentCompilation.html">プラットフォーム依存コンパイル</a>と <code>ISecretManager</code> の実装クラスを新たに作成することで簡単に追加できます。</strong></p> <pre><code class="csharp">// ... // name をキーとして値を nikaera で保存する SecretManager.Put("name", "nikaera"); // name をキーとして値を取得する var name = SecretManager.Get("name"); // "nikaera" が出力される Debug.Log(name); // name をキーとして値を削除する SecretManager.Delete("name"); // ... </code></pre> <h1 id="おわりに"><a href="#%E3%81%8A%E3%82%8F%E3%82%8A%E3%81%AB">おわりに</a></h1> <p>今回は iOS/Android で設定値をセキュアに扱うための方法についてまとめてみました。実際は <code>Keychain Services</code> 周りは実装が大変なので、<code>External Dependency Manager for Unity</code> とか使って <a target="_blank" rel="nofollow noopener" href="https://github.com/kishikawakatsumi/KeychainAccess">KeychainAccess</a> のような外部ライブラリを利用する構成のほうが良いと思われます。</p> <p>本記事の内容に誤りがあったり、実際にはセキュアな実装ができていない等々あれば是非コメントでご指摘いただけますと幸いです。</p> <h1 id="参考リンク"><a href="#%E5%8F%82%E8%80%83%E3%83%AA%E3%83%B3%E3%82%AF">参考リンク</a></h1> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://developer.android.com/topic/security/data?hl=ja">Android デベロッパー  |  Android Developers</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://developer.android.com/reference/androidx/security/crypto/EncryptedSharedPreferences?hl=ja">EncryptedSharedPreferences  |  Android デベロッパー  |  Android Developers</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://developer.apple.com/documentation/security/keychain_services">Keychain Services | Apple Developer Documentation</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/masaki_shoji/items/6c512c7ebb30a13cda1d">SharedPreferences を自前で難読化するのはもう古い?これからは EncryptedSharedPrefenreces を使おう - Qiita</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/sachiko-kame/items/261d42c57207e4b7002a">iOS のキーチェーンについて - Qiita</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/nyhk-oi/items/189236d0627d43e7d658">Unity で IOS にセキュアに値を保存するには KeyChain を使おう - Qiita</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://github.com/googlesamples/unity-jar-resolver">googlesamples/unity-jar-resolver: Unity plugin which resolves Android & iOS dependencies and performs version management</a></li> </ul> <div class="footnotes" role="doc-endnotes"> <hr /> <ol> <li id="fn:1" role="doc-endnote"> <p>逆に <code>External Dependency Manager for Unity</code> を利用する方法のメリットは、UnityPackage などでライブラリとして配布する際に、ライブラリを動作させるのに必要な外部パッケージも同梱した状態で配布が可能になるなどがあります。(当然ライセンスには気を付ける必要がありますが...) <a href="#fnref:1" class="footnote-backref" role="doc-backlink">↩︎</a></p> </li> <li id="fn:2" role="doc-endnote"> <p>パッケージの依存関係を自動で解決するかどうかという選択肢になります。本記事では明示的に Resolve を実行するため <code>Disable</code> でも <code>Enable</code> でも進行上の問題はありません。 <a href="#fnref:2" class="footnote-backref" role="doc-backlink">↩︎</a></p> </li> <li id="fn:3" role="doc-endnote"> <p><a target="_blank" rel="nofollow noopener" href="https://cocoapods.org/">CocoaPods</a> もサポートされているようなので、iOS でも Android 同様、外部ライブラリを取り込むのは簡単にできそうでした。例えば <a target="_blank" rel="nofollow noopener" href="https://github.com/kishikawakatsumi/KeychainAccess">KeychainAccess</a> とか使いたい。 <a href="#fnref:3" class="footnote-backref" role="doc-backlink">↩︎</a></p> </li> </ol> </div> nikaera tag:crieit.net,2005:PublicArticle/17487 2021-07-06T00:47:48+09:00 2021-07-06T00:51:27+09:00 https://crieit.net/posts/azure-cosmos-db-sort 📝 Azure Cosmos DB でソートしようとするとフリーズする <p><a target="_blank" rel="nofollow noopener" href="https://docs.microsoft.com/ja-jp/azure/cosmos-db/mongodb-introduction">MongoDB 用 API</a> で Azure CosmosDB 向けの開発を行っていたのですが、<code>sort</code> 実行時にエラーが発生してしまいリソースが取得できなくなる問題が発生してしまいました。</p> <p>結論から言ってしまうと、この <a target="_blank" rel="nofollow noopener" href="https://stackoverflow.com/a/60809868">Stack Overflow の回答</a> 通り対処すれば解決可能なのですが、簡易的に日本語でも解決策を記しておきます。</p> <p>また、本記事内容の問題に遭遇したときに見つけたのですが、事前に <a target="_blank" rel="nofollow noopener" href="https://hub.docker.com/r/microsoft/azure-cosmosdb-emulator/">Azure CosmosDB の Docker イメージ</a> を利用しておけば、CosmosDB 特有の挙動に気づけるようになるかもしれません。私は <a target="_blank" rel="nofollow noopener" href="https://hub.docker.com/_/mongo">MongoDB の Docker イメージ</a> を利用して開発や動作検証を行っておりました。</p> <hr /> <p>公式サイトでの <a target="_blank" rel="nofollow noopener" href="https://docs.microsoft.com/ja-jp/azure/cosmos-db/mongodb-indexing">MongoDB 用 API でのインデックス管理</a> についての記事を見ていくと下記の文言が出てきます。</p> <blockquote> <p>クエリに並べ替えを適用するには、並べ替え操作で使用されるフィールドに対してインデックスを作成する必要があります。</p> </blockquote> <p>そのため、例えば MongoDB の ORM である <a target="_blank" rel="nofollow noopener" href="https://mongoosejs.com/">Mongoose</a> の利用例でいうと、スキーマを定義する際に下記のようにソートに利用したいキーに対してインデックスを指定する必要があります。</p> <pre><code class="typescript">@Schema({ timestamps: { createdAt: "created_at", updatedAt: "updated_at" } }) export class TestSchema1 extends Document { _id: string; // ソートしたいキーには index を付ける @Prop({ index: true }) sort!: number; } const schema = SchemaFactory.createForClass(TestSchema1); export const TestSchema1 = schema; </code></pre> <p>また、範囲検索を行いたい場合は各キーにインデックスを作成することで可能になります。</p> <pre><code class="typescript">@Schema({ timestamps: { createdAt: "created_at", updatedAt: "updated_at" } }) export class TestSchema2 extends Document { _id: string; @Prop() start_at: Date; @Prop() end_at: Date; } // start_at と end_at で範囲検索を行いたいため、 // それぞれに unique インデックスを作成している const schema = SchemaFactory.createForClass(TestSchema2) .index({ start_at: 1 }, { unique: true }) .index({ end_at: 1 }, { unique: true }); export const TestSchema2 = schema; </code></pre> <p>地味な Tips のような記事ですが、割とハマりました。。この記事が同じ轍を踏んでしまっている方の参考になれれば幸いです 😇</p> nikaera tag:crieit.net,2005:PublicArticle/17447 2021-06-26T20:44:05+09:00 2021-06-26T20:44:54+09:00 https://crieit.net/posts/improve-skills-without-stress 📔 マイペースに技術研鑽を継続する方法 <p><img src="https://nikaera.com/archives/improve-skills-without-stress/cover.jpg" alt="本記事のイメージ画像" /><br /> <strong>Image by <a target="_blank" rel="nofollow noopener" href="https://pixabay.com/users/hans-2/?utm_source=link-attribution&utm_medium=referral&utm_campaign=image&utm_content=8810">Hans Braxmeier</a> from <a target="_blank" rel="nofollow noopener" href="https://pixabay.com/?utm_source=link-attribution&utm_medium=referral&utm_campaign=image&utm_content=8810">Pixabay</a></strong></p> <h1 id="はじめに"><a href="#%E3%81%AF%E3%81%98%E3%82%81%E3%81%AB">はじめに</a></h1> <p>技術研鑽のための行動が習慣化して確立してきたので、また困り始めた時に参照するための備忘録的な感じで、習慣化に至るまでの流れを記事化しておくことにしました。自分の中では努力していると一切感じず自然に技術研鑽及び技術者としてのプレゼンス向上のために行動できるサイクルができた印象です。</p> <p>昔から色々なやり方で上記の習慣化にはトライしていたものの、全てが長続きしなかった自分でも継続できるやり方なので、ある程度の再現性はあるかもしれません。(が、あくまでも自分のやり方にはなります。。🙃</p> <p>ちなみに本記事の内容を推敲していたところ、最近読んだ<a target="_blank" rel="nofollow noopener" href="https://www.amazon.co.jp/dp/4820728245">心理的安全性のつくりかた</a>に出てきた <strong>「きっかけ・行動・みかえり」</strong> のパターンになっていることに気づきました。</p> <h1 id="技術研鑽のサイクル"><a href="#%E6%8A%80%E8%A1%93%E7%A0%94%E9%91%BD%E3%81%AE%E3%82%B5%E3%82%A4%E3%82%AF%E3%83%AB">技術研鑽のサイクル</a></h1> <p>自分で自分のモチベを保ちつつ、自然な技術研鑽サイクルとして定着したフローを説明していきます。</p> <h2 id="「きっかけ」:実現したい開発アイデアが浮かぶ"><a href="#%E3%80%8C%E3%81%8D%E3%81%A3%E3%81%8B%E3%81%91%E3%80%8D%EF%BC%9A%E5%AE%9F%E7%8F%BE%E3%81%97%E3%81%9F%E3%81%84%E9%96%8B%E7%99%BA%E3%82%A2%E3%82%A4%E3%83%87%E3%82%A2%E3%81%8C%E6%B5%AE%E3%81%8B%E3%81%B6">「きっかけ」:実現したい開発アイデアが浮かぶ</a></h2> <p><img src="https://nikaera.com/archives/improve-skills-without-stress/1.jpg" alt="きっかけを見つけた感じのイメージ" /><br /> <strong>Image by <a target="_blank" rel="nofollow noopener" href="https://pixabay.com/users/hans-2/?utm_source=link-attribution&utm_medium=referral&utm_campaign=image&utm_content=8810">Hans Braxmeier</a> from <a target="_blank" rel="nofollow noopener" href="https://pixabay.com/?utm_source=link-attribution&utm_medium=referral&utm_campaign=image&utm_content=8810">Pixabay</a></strong></p> <p>個人的に突発的にコレ作れたら面白そうとか、自分はコレがあったら便利だなっていうアイデアが浮かぶことがあるのですが、<strong>大体そのアイデアを本気で実現したいと本当に思えるピークは体感最長でも 3-4 日くらいです。</strong> そのため、アイデアを具体化して開発に着手するまでの期間としては 3-4 日以内を目安に考えています。</p> <p><strong>アイデアが思い浮かんでから長く期間が空いてしまい、具体化する気が無くなってしまった場合はメモアプリにアイデアをストックしておきます。</strong> それらを見返すとより有益なアイデアが浮かんだりするので今後のきっかけ作りに有効活用できます。</p> <p>また、 <strong>開発のアイデアについてですが、プライベートだけでなく仕事で開発しているシステムの新機能/改善案等もアイデアとして扱うことが可能です。</strong> 自身の仕事の質向上にもつながりますし、行動を起こすためのきっかけ作りとしても利用できて、技術検証や実装等は業務時間で行えるため割とオススメの方法です。</p> <p><em>当たり前ですが NDA に触れるような内容を自身の成果として公開してしまうのは絶対にダメです。あくまでも一般的な技術知識のみを自身の得た知見として公開するというスタンスです。</em></p> <h2 id="「行動」:開発アイデアを検証可能な形で実装する"><a href="#%E3%80%8C%E8%A1%8C%E5%8B%95%E3%80%8D%EF%BC%9A%E9%96%8B%E7%99%BA%E3%82%A2%E3%82%A4%E3%83%87%E3%82%A2%E3%82%92%E6%A4%9C%E8%A8%BC%E5%8F%AF%E8%83%BD%E3%81%AA%E5%BD%A2%E3%81%A7%E5%AE%9F%E8%A3%85%E3%81%99%E3%82%8B">「行動」:開発アイデアを検証可能な形で実装する</a></h2> <p><img src="https://nikaera.com/archives/improve-skills-without-stress/2.jpg" alt="行動してる感のあるイメージ" /><br /> <strong>Image by <a target="_blank" rel="nofollow noopener" href="https://pixabay.com/users/sasint-3639875/?utm_source=link-attribution&utm_medium=referral&utm_campaign=image&utm_content=1822566">Sasin Tipchai</a> from <a target="_blank" rel="nofollow noopener" href="https://pixabay.com/?utm_source=link-attribution&utm_medium=referral&utm_campaign=image&utm_content=1822566">Pixabay</a></strong></p> <p><strong>開発では、とにかく動作確認が可能な形で実装を行うことに注力します。細かい設計などは置いておいて仮説検証を優先していくイメージです。</strong> そのため、技術選定は何基準でも問題ありませんが、アイデアの内容によってそこら辺の技術基準は変えていくのが良いと考えています。</p> <p>例えば、<strong>プラグインやライブラリの開発については使い慣れた言語や業務で利用している技術を選定すると、知識や知見を深めることに繋がりやすかったです。サービス開発については初期は触れる技術領域が広く浅くになりがちなので、新しい技術など使い慣れていない言語などを採用して知識や知見を広げることを意識すると最後まで楽しく開発できました。</strong></p> <p>技術研鑽という名目で取り組む開発なので、完成を目指す必要はありません。しかし、成果物として完成させることを目指したいという場合は、<strong>モチベーションを絶やさないようベースとなる機能の実装を短期間で行うことを意識します。ベースとなる機能の実装というのは、一通りそのシステムの動作検証を他者が行える状態を示します。</strong> システムの動作検証を他者が行える状態まで持っていければ、正しく成果物が評価できる状態になっているはずだからです。</p> <p>まとめると、<strong>モチベを保てるやり方を意識して技術選定を行い、具体的な成果物を意識してベースとなる機能の実装を短期間の細かいサイクルで進められると、満足感のある形で開発を進めることができました。</strong></p> <h2 id="「みかえり」:過程で得たもの全てをアウトプットする"><a href="#%E3%80%8C%E3%81%BF%E3%81%8B%E3%81%88%E3%82%8A%E3%80%8D%EF%BC%9A%E9%81%8E%E7%A8%8B%E3%81%A7%E5%BE%97%E3%81%9F%E3%82%82%E3%81%AE%E5%85%A8%E3%81%A6%E3%82%92%E3%82%A2%E3%82%A6%E3%83%88%E3%83%97%E3%83%83%E3%83%88%E3%81%99%E3%82%8B">「みかえり」:過程で得たもの全てをアウトプットする</a></h2> <p><img src="https://nikaera.com/archives/improve-skills-without-stress/3.jpg" alt="みかえりしてる感のあるイメージ" /><br /> <strong>Photo by <a target="_blank" rel="nofollow noopener" href="https://unsplash.com/@grakozy?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Greg Rakozy</a> on <a target="_blank" rel="nofollow noopener" href="https://unsplash.com/?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Unsplash</a></strong></p> <p><strong>成果物の公開だけでなく、その開発を通して得た知見や知識及びソースコードなど全てアウトプットします。行動の途中だとしてもモチベが下がってきたら、みかえりのフェーズに移行します。</strong> 例えば、成果物で言うと、ストア公開するだけでなくソースコードも GitHub の公開リポジトリにアップします。知識や知見などは技術ブログで記事化してまとめて公開します。 <strong>他者の目に触れる場所へ公開することを意識すると、アウトプットの質を高めるモチベに繋がります。</strong> 承認欲求を満たすためではなく、あくまで自分のアウトプットの質を高める施策の一貫として考えます。</p> <p>上記を意識するとリファクタリングやベスプラに沿った開発ができているか等のチェックに繋がり、知識や知見をより深めることに繋がります。記事化も同様で、文章として知見や知識を残す過程で正しい内容なのか、本当に正しく動作するソースコードが書けているかに意識が向くため、誤った知識の修正や復習に繋がります。</p> <p>成果物だけでなく身につけた技術も含めて全てをアウトプットすることで、余すこと無く行動した結果を有効活用できます。<strong>このサイクルを何回か行い習慣化してくると、インプットした内容を全てアウトプットしきるための行動が自然と起こせるようになっていきました。</strong> アウトプットする過程できっかけが生まれて更に行動することにつながっていくという流れも生まれました。</p> <p>具体例としては、<a target="_blank" rel="nofollow noopener" href="https://github.com/nikaera/Teemo">シンプルなプラグイン</a>を作っていく過程で、ライブラリ化やモジュール化した方が良い機能がでてきたので、別リポジトリに切り出して<a target="_blank" rel="nofollow noopener" href="https://www.npmjs.com/package/@nikaera/react-emoji-textarea">パッケージマネージャーからインストール可能にしました</a>。また、業務で特殊な事情で取り組んだ開発内容を新たな知見として<a target="_blank" rel="nofollow noopener" href="https://zenn.dev/nikaera/articles/cloudfront-for-mediapackage">記事化</a>してアウトプットを増やすことができるようになりました。</p> <hr /> <p>ちなみに私はアウトプットする手段としての記事化には重い腰が中々上がらない人間だったのですが、<a target="_blank" rel="nofollow noopener" href="https://www.youtube.com/watch?v=j7ifT8S5pN0">堤 修一さんの Youtube 動画</a> と同様の考え方に自然に至りました。</p> <p><strong>全てが自分のやった成果として目に見える形で残っていくので、後から見返したときに自分の実績として実感が湧きやすく、作りっぱなしで終わっていた頃と比べると相当な達成感を味わうこともできます。</strong> ゲームでいう様々な実績を解除してくような感覚に近いかもしれません。🎮</p> <h1 id="おわりに"><a href="#%E3%81%8A%E3%82%8F%E3%82%8A%E3%81%AB">おわりに</a></h1> <p>現状個人的には上記のサイクルが上手く機能していて、最近習慣化の軌道に乗った感じが自分の中にあったので知見として記事化しておくことにしました。今後、上記サイクルについては改善を繰り返しながらアップデートしていきますが、一旦現状の内容を後から見返せるようにしました。</p> <p>また今回始めて記事内でイメージ画像をふんだんに使ってみましたが、画像探すの楽しいし記事のクオリティが上がったと錯覚できるので、今後もポエム記事については積極的に画像を利用していこうと思いました (違</p> <p>この記事内容がどなたかの行動を起こすきっかけとなれれば幸いです!🙏</p> nikaera tag:crieit.net,2005:PublicArticle/17446 2021-06-26T20:15:08+09:00 2021-06-26T20:15:08+09:00 https://crieit.net/posts/pytest-sqlalchemy-alembic 📝 pytest で alembic のマイグレーションを行う方法 <h1 id="はじめに"><a href="#%E3%81%AF%E3%81%98%E3%82%81%E3%81%AB">はじめに</a></h1> <p><a target="_blank" rel="nofollow noopener" href="https://fastapi.tiangolo.com/ja/">FastAPI</a> と <a target="_blank" rel="nofollow noopener" href="https://github.com/sqlalchemy/sqlalchemy">SQLAlchemy</a> を利用して Web API 開発を行っていた際、SQLAlchemy のマイグレーションツールである <a target="_blank" rel="nofollow noopener" href="https://github.com/sqlalchemy/alembic">alembic</a> を利用していました。</p> <p>ただ E2E テストを書こうとした際に、pytest 実行中に alembic でデータベースマイグレーションを行う方法が分からず模索していました。結果的にマイグレーションのやり方は分かったものの一応今後も利用するかもしれないため、その内容を記事として残しておくことにしました。</p> <p><strong>本記事内で利用しているソースコードを含む FastAPI プロジェクトを <a target="_blank" rel="nofollow noopener" href="https://github.com/nikaera/fastapi-sqlalchemy-alembic-pytest-sample">GitHub リポジトリ</a>上にアップしておいたので、詳細を確認されたい方がいればご参照くださいませ。</strong></p> <h1 id="alembic でマイグレーションを行う"><a href="#alembic+%E3%81%A7%E3%83%9E%E3%82%A4%E3%82%B0%E3%83%AC%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3%E3%82%92%E8%A1%8C%E3%81%86">alembic でマイグレーションを行う</a></h1> <p><code>conftest.py</code> にグローバルで利用するマイグレーション用の <code>fixture</code> を定義すれば OK です。</p> <pre><code class="python"># conftest.py import os import alembic.config import pytest from sqlalchemy import create_engine from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import sessionmaker from sqlalchemy_utils import database_exists, create_database, drop_database # テスト用の初期データを定義した module を import する (必要があれば) # from .seed import users, contents # 指定したパラメータを用いて alembic によるデータベースマイグレーションを行う # 引数のデフォルト設定では全てのマイグレーションを実行するようになっている def migrate(migrations_path, alembic_ini_path='alembic.ini', connection=None, revision="head"): config = alembic.config.Config(alembic_ini_path) config.set_main_option('script_location', migrations_path) if connection is not None: config.attributes['connection'] = connection alembic.command.upgrade(config, revision) # テスト実行用にセットアップされたデータベースのセッション情報を扱う関数 # scope に session を指定することでテスト全体で一回だけ実行されるようにする @pytest.fixture(scope="session", autouse=True) def SessionLocal(): test_sqlalchemy_database_url = os.environ['DATABASE_URL'] engine = create_engine(test_sqlalchemy_database_url) # 既にテスト用データベースが存在していたら破棄する if database_exists(test_sqlalchemy_database_url): drop_database(test_sqlalchemy_database_url) # テスト用データベースを作成する create_database(test_sqlalchemy_database_url) # 環境変数 DATABASE_URL で指定したデータベースに対して、 # マイグレーションを行いテスト実行に必要なテーブルを一括作成する # 第一引数に指定している alembic は `alembic init <環境名>` 実行時に指定した環境名を入力 with engine.begin() as connection: migrate("alembic", 'alembic.ini', connection) Base = declarative_base() Base.metadata.create_all(engine) SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) # テスト用の各種データを追加する (必要があれば) # db_session = SessionLocal() # for user in users: # db_session.add(user) # db_session.commit() # for content in contents: # db_session.add(content) # db_session.commit() # db_session.close() # テスト用データ追加後のセットアップ済みの状態で # テスト用に利用する SessionLocal を返却する yield SessionLocal # テストが全て終わったら、テスト用データベースを破棄して、 # SQLAlchemy のセッションも切断する drop_database(test_sqlalchemy_database_url) engine.dispose() </code></pre> <h1 id="FastAPI の pytest への適用例"><a href="#FastAPI+%E3%81%AE+pytest+%E3%81%B8%E3%81%AE%E9%81%A9%E7%94%A8%E4%BE%8B">FastAPI の pytest への適用例</a></h1> <p>上記を関数を利用する方法は各自のテスト環境によって異なると思いますが、一応私が FastAPI のテストコードを書く際に利用したソースコードを元に参考例を載せておきます。</p> <p><code>conftest.py</code> と同じディレクトリに <code>client.py</code> を作成します。</p> <pre><code class="python"># client.py from fastapi import Header, HTTPException, status from fastapi.testclient import TestClient from app.dependencies import get_database from app.main import app # conftest で定義した fixture の SessionLocal を元に、 # データベースセッションを作成するための override_get_db 関数を定義して、 # get_database の代わりに override_get_db を実行するよう差し替える def temp_db(f): def func(SessionLocal, *args, **kwargs): def override_get_db(): db = SessionLocal() try: yield db finally: db.close() app.dependency_overrides[get_database] = override_get_db f(*args, **kwargs) app.dependency_overrides[get_database] = get_database return func client = TestClient(app) </code></pre> <p>あとは <code>pytest</code> のコード内で下記のような記述を行えば、FastAPI の内部でテスト用データベースを利用してくれるようになります。</p> <pre><code class="python"># test_main.py from fastapi import status from .client import client, temp_db # temp_db fixture を定義しておくことで、 # 関数の実行中は FastAPI の内部でテスト用データベースを利用する @temp_db def test_read_me_token_valid(): response = client.get("/users/me", headers={"Authorization": "Bearer 1234567890"}) assert response.status_code == status.HTTP_200_OK #... </code></pre> <h2 id="参考リンク"><a href="#%E5%8F%82%E8%80%83%E3%83%AA%E3%83%B3%E3%82%AF">参考リンク</a></h2> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/bee2/items/ff9c86d8d345dbcab497#2-%E3%83%86%E3%82%B9%E3%83%88%E3%82%B1%E3%83%BC%E3%82%B9%E6%AF%8E%E3%81%ABdb%E3%82%92%E4%BD%9C%E6%88%90%E3%81%97%E4%BB%96%E3%81%AE%E3%83%86%E3%82%B9%E3%83%88%E3%82%B1%E3%83%BC%E3%82%B9%E3%82%84%E6%9C%AC%E7%95%AA%E7%94%A8%E3%81%AEdb%E3%81%AB%E5%BD%B1%E9%9F%BF%E3%82%92%E4%B8%8E%E3%81%88%E3%81%9A%E3%81%ABapi%E3%81%AEunittest%E3%82%92%E8%A1%8C%E3%81%86">FastAPI でテスト用のクリーンな DB を作成して pytest で API の Unittest を行う - Qiita</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/_akiyama_/items/9ead227227d669b0564e">pytest:フィクスチャ(fixture)の使い方 - Qiita</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://fastapi.tiangolo.com/tutorial/dependencies/">Dependencies - First Steps - FastAPI</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://docs.pytest.org/en/6.2.x/">pytest: helps you write better programs — pytest documentation</a></li> </ul> nikaera tag:crieit.net,2005:PublicArticle/17425 2021-06-21T22:22:57+09:00 2021-06-21T22:22:57+09:00 https://crieit.net/posts/Node-js-GitHub-Actions 📝 Node.js パッケージを公開するための GitHub Actions を構築する <p><a target="_blank" rel="nofollow noopener" href="https://github.com/nikaera/react-emoji-textarea">react-emoji-textarea</a> の開発を行った際、<a target="_blank" rel="nofollow noopener" href="https://docs.github.com/ja/github/administering-a-repository/releasing-projects-on-github/managing-releases-in-a-repository">リリース</a>を作成したら自動的に Node.js パッケージにライブラリが公開される仕組みがほしいと考え、GitHub Actions でそれを実現することにしました。</p> <p>その際、公式サイトに <a target="_blank" rel="nofollow noopener" href="https://docs.github.com/en/actions/guides/publishing-nodejs-packages#publishing-packages-using-yarn">公開されている内容</a> を参考に GitHub Actions を作成したのですが、そのまま利用すると私の環境では下記のエラーが発生してしまいました。</p> <pre><code class="bash">error Couldn't publish package: "https://registry.yarnpkg.com/@nikaera/react-emoji-textarea: You do not have permission to publish \"react-emoji-textarea\". Are you logged in as the correct user?" </code></pre> <p>上記のエラーについて調査しながら改修したところ、最終的に下記の GitHub Actions で Node.js パッケージを公開できるようになりました。<code>secrets.NPM_TOKEN</code> には <a target="_blank" rel="nofollow noopener" href="https://docs.npmjs.com/creating-and-viewing-access-tokens">NPM Token</a> を登録します。</p> <pre><code class="yml"># package.yml name: Node.js Package on: # workflow_dispatch を追加して手動でも実行できるよう改修 workflow_dispatch: release: types: [created] jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - uses: actions/setup-node@v2 with: node-version: "14.x" registry-url: "https://registry.npmjs.org" # registry.npmjs.org へアクセスする際は必ず認証を試みるオプションを追加 always-auth: true # scope には自分のユーザ名を指定 scope: "@nikaera" # .npmrc に https://registry.npmjs.org アクセス時に利用する認証情報を記載する - run: echo "//registry.npmjs.org/:_authToken=$<span>{</span><span>{</span> secrets.NPM_TOKEN <span>}</span><span>}</span>" > ~/.npmrc - name: Build react-emoji-textarea 😆💖 run: | yarn install --frozen-lockfile yarn format yarn build - run: yarn publish --access public env: NODE_AUTH_TOKEN: $<span>{</span><span>{</span> secrets.NPM_TOKEN <span>}</span><span>}</span> </code></pre> nikaera tag:crieit.net,2005:PublicArticle/17424 2021-06-21T22:21:42+09:00 2021-06-21T22:23:21+09:00 https://crieit.net/posts/chrome-extension-only-one-window 📝 Chrome 拡張で 1つのウインドウを使い回す方法 <p><a target="_blank" rel="nofollow noopener" href="https://chrome.google.com/webstore/detail/teemo-%F0%9F%92%95/alhdkgcgpmdfbidaapdlnmbhoanoijka">Teemo</a> という Chrome 拡張を開発したのですが、その際 1 つのウインドウを使い回す構成にしたいなと考えていました。</p> <p>例えば何も考えずに <code>chrome.windows.create</code> を Chrome 拡張を開くたびに呼び出すと、呼び出すたびにウインドウが新規作成されてしまいます。そうすると、都度画面に不要なウインドウが出てきて邪魔になるだけでなく、手動で不要なウインドウを消す作業をユーザーに強いることとなってしまいます。。🙃</p> <p>上記のような挙動が望まれるケースもあると思いますが、Teemo ではウインドウ間を頻繁に行き来するため、ショートカットを利用して拡張機能を呼び出すことを見込んでいました。そのため、ショートカットを利用して拡張機能を呼び出すたびにウインドウが新規作成され続ける挙動は望んでいませんでした。</p> <p>1 つのウインドウを使い回すためには、<code>chrome.windows.create</code> 時に作成される <code>window</code> の <code>id</code> を保持しておきます。その後、Chrome 拡張が呼び出されるたびに <code>window</code> が既に存在するかどうかを保持していた <code>id</code> を元にチェックします。既に <code>window</code> が存在していた場合はそれを使いまわします。存在していなかった場合は、<code>chrome.windows.create</code> で <code>window</code> を新規作成します。</p> <pre><code class="javascript">// background.js // window の id を保持しておくための変数 let vid = -1; chrome.browserAction.onClicked.addListener(function () { // vid の値を元に Chrome 拡張で開いた window の取得を試みる chrome.windows.get(vid, function (chromeWindow) { // エラーが無く、既に window が存在している場合は、 // そのステータスを { focused: true } にすることで最前面に呼び出す if (!chrome.runtime.lastError && chromeWindow) { chrome.windows.update(vid, { focused: true }); return; } // 上記以外のパターンでは window を新規作成する chrome.windows.create( { url: "index.html", type: "popup", }, function (window) { // 新規作成した window を使い回せるようにするため、 // vid 変数に window の id を保持しておく vid = window.id; } ); }); }); </code></pre> <p>上記コードで 1 つのウインドウを使い回すことが出来るようになるはずです。✅</p> nikaera tag:crieit.net,2005:PublicArticle/17395 2021-06-13T20:10:47+09:00 2021-06-13T20:10:47+09:00 https://crieit.net/posts/teemo-first-release 📔 チャットの短文作成に便利な Chrome 拡張機能を開発してみた <h1 id="はじめに 📝"><a href="#%E3%81%AF%E3%81%98%E3%82%81%E3%81%AB+%F0%9F%93%9D">はじめに 📝</a></h1> <p>最近とある事情により Twitter の DM を利用しているのですが、Slack などのように絵文字をショートカット入力できないことにフラストレーションが溜まってきていました。そのため、絵文字をショトカで入力可能にしてくれる Chrome 拡張機能を探したのですが見つけられませんでした。</p> <p>そこで、無いなら作ろうということで <a target="_blank" rel="nofollow noopener" href="https://chrome.google.com/webstore/detail/teemo-%F0%9F%92%95/alhdkgcgpmdfbidaapdlnmbhoanoijka?hl=ja&">Teemo</a> を開発しました。</p> <p>ソースコードは <a target="_blank" rel="nofollow noopener" href="https://github.com/nikaera/Teemo">GitHub 上で公開しています。</a> 何かご要望等ございましたら PR や Issue 作成頂けますと喜びます。Teemo の実際の挙動については下記の動画で確認できます 🎥</p> <p><a target="_blank" rel="nofollow noopener" href="https://www.youtube.com/watch?v=bJTHbzw1Ee4">Teemo 💕 - Quickly generate chat message with embedded emoji! 💖😆</a></p> <h1 id="考えていたこと 💭"><a href="#%E8%80%83%E3%81%88%E3%81%A6%E3%81%84%E3%81%9F%E3%81%93%E3%81%A8+%F0%9F%92%AD">考えていたこと 💭</a></h1> <p>今回 Teemo の開発を行うに当たり、考えていた点は下記になります。</p> <ul> <li>よくある <code>:</code> 入力からの絵文字ショートカットを導入する <ul> <li>Slack や GitHub、JIRA などではおなじみの入力方法 ⌨️</li> </ul></li> <li>パレットから選択する際は半角英数字で検索できるようにしたい <ul> <li>Twitter では日本語で検索しないと絵文字が探せない 🔍</li> <li>普段英数字で絵文字検索をするので目的の絵文字が見つけづらい 🕵️</li> </ul></li> <li>拡張機能を利用することで文章入力の煩わしさが増加することは避けたい <ul> <li>コピペや文章クリアの機能等にもショトカ利用できるようにしたい 💨</li> </ul></li> </ul> <p>プロトタイピングしながら友人に進捗をシェアしながら開発は進めていきました。本当は個人で利用する想定で進めていたのですが、割と評判が良かったため Chrome ウェブストアに公開するのを目標に動いていました。そして、Chrome ウェブストアで公開できるクオリティを目指して動いたことで満足のいく拡張機能が作れました。</p> <h1 id="使い方 ⚒️"><a href="#%E4%BD%BF%E3%81%84%E6%96%B9+%E2%9A%92%EF%B8%8F">使い方 ⚒️</a></h1> <p>Teemo の使い方について紹介いたします。</p> <h2 id="Teemo を Chrome 拡張機能として追加する 🛍️"><a href="#Teemo+%E3%82%92+Chrome+%E6%8B%A1%E5%BC%B5%E6%A9%9F%E8%83%BD%E3%81%A8%E3%81%97%E3%81%A6%E8%BF%BD%E5%8A%A0%E3%81%99%E3%82%8B+%F0%9F%9B%8D%EF%B8%8F">Teemo を Chrome 拡張機能として追加する 🛍️</a></h2> <p>まずは <a target="_blank" rel="nofollow noopener" href="https://chrome.google.com/webstore/detail/teemo-%F0%9F%92%95/alhdkgcgpmdfbidaapdlnmbhoanoijka?hl=ja&authuser=0">Chrome ウェブストア</a> にアクセスして、Teemo を Chrome に追加する必要があります。</p> <p><img src="https://i.gyazo.com/3649738203bc1e06da63a48d3c8140c0.png" alt="スクリーンショット 2021-06-13 18.24.31.png" /><br /> <strong>1. <a target="_blank" rel="nofollow noopener" href="https://chrome.google.com/webstore/detail/teemo-%F0%9F%92%95/alhdkgcgpmdfbidaapdlnmbhoanoijka?hl=ja&authuser=0">Chrome ウェブストア</a> にアクセスして右上の <code>Chrome に追加</code> ボタンをクリックする</strong></p> <p><img src="https://i.gyazo.com/ea19a358e038d73af1b73e6413e80420.png" alt="スクリーンショット 2021-06-13 18.27.27.png" /><br /> <strong>2. 拡張機能として Teemo の追加が完了したら、ツールバーのアイコンをクリックするか <code>Ctrl or Cmd + Shift + O</code> を入力して Teemo のエディタが開くことを確認する</strong></p> <p>上記確認できれば Teemo が正常に Chrome 拡張機能として追加できています。</p> <h2>Teemo で絵文字を <code>:</code> でショートカット入力する 💨</h2> <p><strong>Teemo のエディタでは <code>:</code> を利用することで文章入力を止めることなく、シームレスに絵文字入力が行えます。<code>:</code> を入力後、関連ワードを半角英数字で入力することで絵文字候補がウィンドウ下部に表示されます。入力したい絵文字をマウスかキーボードの矢印キーで選択して入力できます。</strong></p> <p><img src="https://i.gyazo.com/5b18b25505d443a14befff21e06ecbe8.png" alt="スクリーンショット 2021-06-13 18.51.19.png" /><br /> <strong><code>:</code> を入力後、関連ワードを半角英数字で入力することで絵文字候補がウィンドウ下部に表示される 💨</strong></p> <h2 id="Teemo で絵文字をパレットから選択して入力する 🎨"><a href="#Teemo+%E3%81%A7%E7%B5%B5%E6%96%87%E5%AD%97%E3%82%92%E3%83%91%E3%83%AC%E3%83%83%E3%83%88%E3%81%8B%E3%82%89%E9%81%B8%E6%8A%9E%E3%81%97%E3%81%A6%E5%85%A5%E5%8A%9B%E3%81%99%E3%82%8B+%F0%9F%8E%A8">Teemo で絵文字をパレットから選択して入力する 🎨</a></h2> <p>絵文字をパレットから選択して入力するには <code>Ctrl or Cmd + E</code> キーを入力するか、ウィンドウ下部の真ん中にある <code>🎨</code> ボタンをクリックしてパレットを表示する必要があります。ちなみに絵文字選択用のパレットには OSS の <a target="_blank" rel="nofollow noopener" href="https://github.com/missive/emoji-mart">Emoji Mart</a> を利用しています。</p> <p><img src="https://i.gyazo.com/2fd8384fa90adc797b2cf643cb1f8652.png" alt="スクリーンショット 2021-06-13 18.48.36.png" /><br /> <strong>パレットで関連ワードを入力することで絵文字検索ができる。絵文字をクリックすることで入力できる 🐱</strong></p> <h2 id="Teemo で入力した文章をクリップボードにコピーする 📋"><a href="#Teemo+%E3%81%A7%E5%85%A5%E5%8A%9B%E3%81%97%E3%81%9F%E6%96%87%E7%AB%A0%E3%82%92%E3%82%AF%E3%83%AA%E3%83%83%E3%83%97%E3%83%9C%E3%83%BC%E3%83%89%E3%81%AB%E3%82%B3%E3%83%94%E3%83%BC%E3%81%99%E3%82%8B+%F0%9F%93%8B">Teemo で入力した文章をクリップボードにコピーする 📋</a></h2> <p><strong>入力した文章は <code>Ctrl or Cmd + C</code> キーを入力するか、ウィンドウ下部の右にある <code>📋</code> ボタンをクリックすることで全文コピーが可能です。わざわざ範囲選択を行わずとも文章を全文コピーすることができます。</strong> コピーが正常にできるとウィンドウ下部の真ん中にある <code>📋</code> ボタンが <code>📋✔️</code> ボタンになります。</p> <p><img src="https://i.gyazo.com/17955efbabe4b849cc095a4db118793a.png" alt="スクリーンショット 2021-06-13 18.57.30.png" /><br /> <strong>Teemo で文章入力後 <code>Ctrl + C</code> で全文コピー後、ツイート編集画面にペーストした時の様子 🐦</strong></p> <h2 id="Teemo で入力した文章をクリアする 🧼"><a href="#Teemo+%E3%81%A7%E5%85%A5%E5%8A%9B%E3%81%97%E3%81%9F%E6%96%87%E7%AB%A0%E3%82%92%E3%82%AF%E3%83%AA%E3%82%A2%E3%81%99%E3%82%8B+%F0%9F%A7%BC">Teemo で入力した文章をクリアする 🧼</a></h2> <p><strong>Teemo では単一のウィンドウを使い回すため、改めて文章を作成する際に一度入力した文章をクリアする必要があります。</strong> そのため、<code>Ctrl or Cmd + R</code> キーを入力するか、ウィンドウ下部の左にある <code>🆕</code> ボタンをクリックすることで文章をクリアできるようにしています。</p> <p><img src="https://i.gyazo.com/5096d26a87ff99ec76f5f9b6c12eaa82.png" alt="スクリーンショット 2021-06-13 19.01.42.png" /><br /> <strong>Teemo の入力欄を <code>🆕</code> ボタンをクリックしてクリアした時の様子 🧼</strong></p> <h1 id="おわりに 🔚"><a href="#%E3%81%8A%E3%82%8F%E3%82%8A%E3%81%AB+%F0%9F%94%9A">おわりに 🔚</a></h1> <p>個人的に Teemo のおかげで、Twitter での絵文字を利用した文章作成についてストレス無く出来るようになったので良かったです。副次的な効果として、どんなサイトでも絵文字を利用した文章作成が簡単に出来るようになったため、他に絵文字をショートカット入力できないサイトがあったとしても同様の困りごとは発生しなくなりました。☺️</p> <p>SNS 上などで文章を作成する際に絵文字を頻繁に利用する方には割とご満足いただける拡張機能になっているはずです多分 🙋</p> nikaera tag:crieit.net,2005:PublicArticle/17310 2021-05-30T20:49:14+09:00 2021-05-30T20:49:14+09:00 https://crieit.net/posts/rails-database-url 📝 Rails で config/database.yml よりも ENV['DATABASE_URL'] の設定が優先される話 <p>MySQL を利用する Rails プロジェクトを起動しようとしたところ、下記のエラーが発生しました。</p> <pre><code class="bash">bin/rails s # データベースアダプターには mysql2 を選択している状態なのに postgresql で接続しようとしている Error loading the 'postgresql' Active Record adapter. Missing a gem it depends on? pg is not part of the bundle. Add it to your Gemfile. (LoadError) </code></pre> <pre><code class="yml"># config/database.yml ファイルの中身一部抜粋 # mysql2 をデータベースアダプターとして利用しているため、 # PostgreSQL 接続のための pg ライブラリの追加を求めるエラーが発生しているのは何かおかしい。。 default: &default adapter: mysql2 #... </code></pre> <p>何でや、、と思い Rails のドキュメントを読んでいた所、公式サイトに <a target="_blank" rel="nofollow noopener" href="https://edgeguides.rubyonrails.org/configuring.html#connection-preference"><code>Connection Preference</code> に関する記述</a>を見つけました。</p> <blockquote> <p>Since pool is not in the ENV['DATABASE_URL'] provided connection information its information is merged in. Since adapter is duplicate, the ENV['DATABASE_URL'] connection information wins.</p> </blockquote> <p>どうやら <code>config/database.yml</code> と <code>ENV['DATABASE_URL']</code> の両方が設定されている場合、設定値に重複がある項目については <code>ENV['DATABASE_URL']</code> の値が優先されるようでした。</p> <p>今回は <code>DATABASE_URL</code> に下記の PostgreSQL URL が設定されてしまっていたため、<code>config/database.yml</code> では MySQL を利用していたのにも関わらず、データベースアダプターに PostgreSQL が使われてしまっていたようです。</p> <pre><code class="bash">env | grep DATABASE_URL # 環境変数 DATABASE_URL に PostgreSQL の URL が設定されている DATABASE_URL=postgresql://user:password@localhost:5432/something_development </code></pre> <p>そのため、<code>DATABASE_URL</code> の中身を空にすることで、本来の意図通りに <code>config/database.yml</code> の設定を反映させることができました。</p> <pre><code class="bash"># 筆者は fish を利用しているので set -e で環境変数を空にした set -e DATABASE_URL # 何も出力されないことを確認し DATABASE_URL が空であることを確認する env | grep DATABASE_URL # Rails サーバーが正常に起動するか再度確認する bin/rails s #... # 無事アプリケーションサーバーが起動すること確認できれば OK => Booting Puma => Rails 6.0.3.6 application starting in development => Run `rails server --help` for more startup options Puma starting in single mode... * Version 4.3.7 (ruby 2.6.1-p33), codename: Mysterious Traveller * Min threads: 5, max threads: 5 * Environment: development * Listening on tcp://127.0.0.1:3000 * Listening on tcp://[::1]:3000 Use Ctrl-C to stop </code></pre> <p>今回はたまたま別プロジェクトでゴニョゴニョ作業をしていた名残で環境変数 <code>DATABASE_URL</code> が設定されてしまっていて、かつそのままの流れで Rails プロジェクトで作業していたため遭遇してしまいました。。</p> <p>ローカル環境で変数を設定する際は <a target="_blank" rel="nofollow noopener" href="https://github.com/direnv/direnv">direnv</a> や <a target="_blank" rel="nofollow noopener" href="https://www.dotenv.org/">dotenv</a> 等を利用して極力手動では環境変数をいじらないようにしようと思いました (所感)</p> <h2 id="参考リンク"><a href="#%E5%8F%82%E8%80%83%E3%83%AA%E3%83%B3%E3%82%AF">参考リンク</a></h2> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://edgeguides.rubyonrails.org/configuring.html#connection-preference">Configuring Rails Applications — Ruby on Rails Guides</a></li> </ul> nikaera tag:crieit.net,2005:PublicArticle/17282 2021-05-26T22:30:30+09:00 2021-05-26T22:30:30+09:00 https://crieit.net/posts/unity-gameci-github-actions GameCI で Unity の CI 環境を GitHub Actions で構築する <h1 id="はじめに"><a href="#%E3%81%AF%E3%81%98%E3%82%81%E3%81%AB">はじめに</a></h1> <p>先日同僚が Unity の CI 環境を構築するためのライブラリである <a target="_blank" rel="nofollow noopener" href="https://game.ci/">GameCI</a> について教えてくれました。早速 GameCI の GitHub Actions を利用して、サンプルプロジェクトで色々動作検証してみたところ、Unity の CI 環境を楽に構築できることが分かりました。</p> <p>もちろん、<a target="_blank" rel="nofollow noopener" href="https://unity3d.com/jp/unity/features/cloud-build">Unity Cloud Build</a> を利用すれば CI 環境の構築は以前から楽にできました。しかし、選択肢の 1 つとして GameCI を持っておくことで、<strong>サクッと GitHub Actions に統合する形で Unity の CI 環境を導入できるのは他には無いメリットを感じました。</strong></p> <p>本記事で紹介しているソースコード、及び検証時に利用したプロジェクトは GitHub にアップ済みですので、手っ取り早く内容を把握されたい方は下記をご参照くださいませ。</p> <p><a target="_blank" rel="nofollow noopener" href="https://github.com/nikaera/Unity-GameCI-Sample">https://github.com/nikaera/Unity-GameCI-Sample</a></p> <p>業務でも利用できそうなので、GameCI を利用して CI 環境を構築する手順を記事でまとめました。</p> <h1 id="GameCI に備わっている機能紹介"><a href="#GameCI+%E3%81%AB%E5%82%99%E3%82%8F%E3%81%A3%E3%81%A6%E3%81%84%E3%82%8B%E6%A9%9F%E8%83%BD%E7%B4%B9%E4%BB%8B">GameCI に備わっている機能紹介</a></h1> <p>GameCI には現状下記の GitHub Actions が用意されているようです。</p> <div class="table-responsive"><table> <thead> <tr> <th>機能</th> <th>概要</th> </tr> </thead> <tbody> <tr> <td><a target="_blank" rel="nofollow noopener" href="https://game.ci/docs/github/activation">Activation</a></td> <td>Unity ライセンスを任意の Unity バージョンで発行する</td> </tr> <tr> <td><a target="_blank" rel="nofollow noopener" href="https://game.ci/docs/github/test-runner">Test runner</a></td> <td>Unity の PlayMode 及び EditMode のテストを実行する (テスト結果の出力にも対応)</td> </tr> <tr> <td><a target="_blank" rel="nofollow noopener" href="https://game.ci/docs/github/builder">Builder</a></td> <td>任意の Platform ビルドを実行する (<a target="_blank" rel="nofollow noopener" href="https://docs.github.com/ja/actions/guides/storing-workflow-data-as-artifacts">アーティファクト</a> 利用でダウンロードも可能)</td> </tr> <tr> <td><a target="_blank" rel="nofollow noopener" href="https://game.ci/docs/github/returning-a-license">Returning a license</a></td> <td>Unity ライセンスの返却ができる (Professional License のみ対応)</td> </tr> <tr> <td><a target="_blank" rel="nofollow noopener" href="https://game.ci/docs/github/remote-builder">Remote builder</a></td> <td>GitHub Actions のスペックでは満足のいくビルドができない際に AWS 環境でハイスペックなマシンを用意してビルドできる。ビルドのためのインフラ構築には <a target="_blank" rel="nofollow noopener" href="https://aws.amazon.com/jp/cloudformation/">AWS CloudFormation</a> を使用している (現在は AWS のみ対応。今後 GCP, Azure にも対応予定とのこと)</td> </tr> <tr> <td><a target="_blank" rel="nofollow noopener" href="https://game.ci/docs/github/deployment/android">Deployment</a></td> <td>Unity ビルドを各種 Platform 向けにデプロイする (iOS 及び Android のみ記載あり。厳密に言うと <code>Builder</code> でビルド出力した内容を <a target="_blank" rel="nofollow noopener" href="https://fastlane.tools/"><code>fastlane</code></a> を用いてデプロイするためのワークフロー紹介になっている)</td> </tr> </tbody> </table></div> <p>上記を見ると既に <strong>GameCI には開発者として Unity CI に欲しい機能は最低限揃っているように見受けられました。</strong> また本記事では、今後機会があれば試してみたいと考えていますが <strong>Remote builder 及び Deployment</strong> については言及していません。</p> <p>今回は実例を交えながら <strong>Activation 及び Test runner、Builder、Returning a license</strong> の使用方法について紹介していきます。</p> <h2 id="Activation: GameCI で必要となる Unity License のアクティベーションを行う"><a href="#Activation%3A+GameCI+%E3%81%A7%E5%BF%85%E8%A6%81%E3%81%A8%E3%81%AA%E3%82%8B+Unity+License+%E3%81%AE%E3%82%A2%E3%82%AF%E3%83%86%E3%82%A3%E3%83%99%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3%E3%82%92%E8%A1%8C%E3%81%86">Activation: GameCI で必要となる Unity License のアクティベーションを行う</a></h2> <p>GameCI で Unity ライセンスをアクティベートするには <a target="_blank" rel="nofollow noopener" href="https://game.ci/docs/github/activation">Activation</a> を利用します。早速ドキュメントの手順に沿って作業を進めていきます。</p> <p>まず CI を導入したい GitHub 上の Unity プロジェクトの <code>.github/workflows</code> 内に Unity ライセンスアクティベート用のワークフローファイルを作成します。</p> <pre><code class="yml"># .github/workflows/activation.yml name: Acquire activation file on: workflow_dispatch: {} jobs: activation: name: Request manual activation file 🔑 runs-on: ubuntu-latest steps: # GameCI の Activation を利用して alf ファイルを発行する - name: Request manual activation file id: getManualLicenseFile uses: game-ci/unity-request-activation-file@v2 with: # Unity プロジェクトのバージョンを指定する # ProjectSettings/ProjectVersion.txt に記載されているバージョンを入力すれば OK unityVersion: 2020.3.5f1 # Upload artifact (Unity_v20XX.X.XXXX.alf) - name: Expose as artifact uses: actions/upload-artifact@v2 with: name: $<span>{</span><span>{</span> steps.getManualLicenseFile.outputs.filePath <span>}</span><span>}</span> path: $<span>{</span><span>{</span> steps.getManualLicenseFile.outputs.filePath <span>}</span><span>}</span> </code></pre> <p>その後、<a target="_blank" rel="nofollow noopener" href="https://docs.github.com/ja/github/setting-up-and-managing-your-github-user-account/managing-user-account-settings/managing-the-default-branch-name-for-your-repositories#about-management-of-the-default-branch-name">デフォルトブランチ</a> にプッシュして GitHub Actions で実行可能にしたら、下記手順に従い Unity ライセンスファイルのアクティベート及びダウンロードを行います。</p> <p><img src="https://i.gyazo.com/bd276ca6dcf6a2c12ce9ff9569e08ce3.png" alt="1. ブラウザから GitHub リポジトリにアクセスして、Unity ライセンスアクティベート用のワークフローを実行して <code>alf</code> ファイルを生成する" /><br /> <strong>1. ブラウザから GitHub リポジトリにアクセスして、Unity ライセンスアクティベート用のワークフローを実行して <code>alf</code> ファイルを生成する</strong></p> <p><img src="https://i.gyazo.com/2271b3cb35efc7f1c9d51702662cdac9.png" alt="2. ワークフローの実行に成功したら、該当項目をクリックして詳細画面に遷移する" /><br /> <strong>2. ワークフローの実行に成功したら、該当項目をクリックして詳細画面に遷移する</strong></p> <p><img src="https://i.gyazo.com/71b9dff8266c9bc990e1b709a5191535.png" alt="3. <code>Artifacts</code> の項目から <code>alf</code> ファイルをダウンロードする" /><br /> <strong>3. <code>Artifacts</code> の項目から <code>alf</code> ファイルをダウンロードする</strong></p> <p><img src="https://i.gyazo.com/cfec48fc58f2a17560ea2e7d0f71cc41.png" alt="4. [Unity license manual activation webpage](https://license.unity3d.com) からログインして <code>alf</code> ファイルをアップロードする" /><br /> <strong>4. <a target="_blank" rel="nofollow noopener" href="https://license.unity3d.com">Unity license manual activation webpage</a> からログインして <code>alf</code> ファイルをアップロードする</strong></p> <p><img src="https://i.gyazo.com/72239a40ef5b2474f34c814a68f8c61d.png" alt="5. Unity ライセンスの用途に応じて適切な選択肢を入力する (本記事では Personal ライセンスを選択)" /><br /> <strong>5. Unity ライセンスの利用用途に応じて適切な選択肢を入力する (本記事では Personal ライセンスを選択)</strong></p> <p><img src="https://i.gyazo.com/9ddc63dc321a68986bfedfb8a97c8f00.png" alt="6. <code>Download license</code> ボタンをクリックして <code>ulf</code> ファイルをダウンロードする" /><br /> <strong>6. <code>Download license</code> ボタンをクリックして <code>ulf</code> ファイルをダウンロードする</strong></p> <p>これで Unity ライセンスファイルのアクティベートは完了です。<strong>次にアクティベートしたライセンスファイルを GitHub リポジトリの <a target="_blank" rel="nofollow noopener" href="https://docs.github.com/ja/actions/reference/encrypted-secrets">Secrets</a> に登録して、GameCI で PlayMode 及び EditMode のテストが実行できるようにしていきます。</strong></p> <p><em><code>alf</code> 拡張子のファイルがライセンスリクエストファイルを指していて、Unity ライセンスの発行に必要となるファイルです。<code>ulf</code> 拡張子のファイルが Unity ライセンスのファイルです。<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup></em></p> <h2 id="Test runner: PlayMode 及び EditMode テストを実行して結果を参照する"><a href="#Test+runner%3A+PlayMode+%E5%8F%8A%E3%81%B3+EditMode+%E3%83%86%E3%82%B9%E3%83%88%E3%82%92%E5%AE%9F%E8%A1%8C%E3%81%97%E3%81%A6%E7%B5%90%E6%9E%9C%E3%82%92%E5%8F%82%E7%85%A7%E3%81%99%E3%82%8B">Test runner: PlayMode 及び EditMode テストを実行して結果を参照する</a></h2> <p>GitHub Actions 上でテストを実行するために、<strong>先ほどアクティベートした Unity ライセンスの情報を ワークフロー上で扱えるようにする必要があります。そのため、まずは Secrets に <code>ulf</code> ファイルの内容を登録することから始めます。</strong></p> <p><img src="https://i.gyazo.com/e126ae5e2fe9339d56047b8497808100.png" alt="1. Unity ライセンスの情報登録のため、Github リポジトリの <code>Secrets</code> 登録画面に遷移する" /><br /> <strong>1. Unity ライセンスの情報登録のため、Github リポジトリの <code>Secrets</code> 登録画面に遷移する</strong></p> <p><img src="https://i.gyazo.com/f52356a229caa4e31e3ae8268d53a4e6.png" alt="2. GameCI はライセンス情報参照のため <code>Secrets</code> の <code>UNITY_LICENSE</code> を参照する。そのため、<code>Name</code> を <code>UNITY_LICENSE</code> で <code>Value</code> に <code>ulf</code> ファイルの中身をコピー & ペーストしておく" /><br /> <strong>2. GameCI はライセンス情報参照のため、デフォルト設定では <code>Secrets</code> の <code>UNITY_LICENSE</code> を参照する。そのため、<code>Name</code> が <code>UNITY_LICENSE</code>、<code>Value</code> には <code>ulf</code> ファイルの中身を登録する<sup id="fnref:2"><a href="#fn:2" class="footnote-ref" role="doc-noteref">2</a></sup></strong></p> <p>上記作業で GameCI でテストやビルド実行を行える環境が整ったので、動作検証のためテスト実行用のワークフローファイルを作成します。</p> <pre><code class="yml"># .github/workflows/test.yml name: Run EditMode and PlayMode Test on: workflow_dispatch: {} jobs: test: name: Run EditMode and PlayMode Test runs-on: ubuntu-latest steps: # actions/checkout@v2 を利用して作業ディレクトリに # Unity プロジェクトの中身をダウンロードしてくる - name: Check out my unity project. uses: actions/checkout@v2 # GameCI の Test runner を利用して # EditMode 及び PlayMode のテストを実行する - name: Run EditMode and PlayMode Test uses: game-ci/unity-test-runner@v2 env: # 2. の手順で Secrets に登録した Unity ライセンスの情報を指定する UNITY_LICENSE: $<span>{</span><span>{</span> secrets.UNITY_LICENSE <span>}</span><span>}</span> # もし Professional license を使いたい場合は、 # メールアドレス、パスワード、シリアルナンバーを入力する必要がある # ref: https://game.ci/docs/github/test-runner#professional-license # UNITY_EMAIL: $<span>{</span><span>{</span> secrets.UNITY_EMAIL <span>}</span><span>}</span> # UNITY_PASSWORD: $<span>{</span><span>{</span> secrets.UNITY_PASSWORD <span>}</span><span>}</span> # UNITY_SERIAL: $<span>{</span><span>{</span> secrets.UNITY_SERIAL <span>}</span><span>}</span> with: projectPath: . # テストの実行結果もみたい場合は githubToken を指定する # secrets.GITHUB_TOKEN は Secrets 未登録でも利用可能 githubToken: $<span>{</span><span>{</span> secrets.GITHUB_TOKEN <span>}</span><span>}</span> # Unity プロジェクトのバージョンを指定する # ProjectSettings/ProjectVersion.txt に記載されているバージョンを入力すれば OK unityVersion: 2020.3.5f1 # 実行したいテストの種類を指定できる # 指定可能な値は All, PlayMode, EditMode # testMode: All # テスト実行時に利用したい Docker イメージを明示的に指定できる # customImage: 'unityci/editor:2020.1.14f1-base-0' # テストの実行結果をアーティファクトにアップロードしてダウンロードして参照できるようにする - uses: actions/upload-artifact@v2 if: always() with: name: Test results path: artifacts </code></pre> <p>上記のワークフローファイルを GitHub Actions 上で動作検証する際の手順は下記になります。</p> <p><img src="https://i.gyazo.com/9bace70734f1e99955f5b9aa068b31de.png" alt="1. Unity のテストを実行するためのワークフローを選択して実行する" /><br /> <strong>1. Unity のテストを実行するためのワークフローを選択して実行する</strong></p> <p><img src="https://i.gyazo.com/ffc31b5919431fedb516f1932a13ce62.png" alt="2. ワークフローの実行が成功したら、詳細画面に遷移した後、<code>Test Results</code> の項目からテストの実行結果を確認する" /><br /> <strong>2. ワークフローの実行が成功したら、詳細画面に遷移した後、<code>Test Results</code> の項目からテストの実行結果を確認する</strong></p> <p>テスト実行用のワークフローファイルでは <a target="_blank" rel="nofollow noopener" href="https://docs.github.com/ja/actions/reference/events-that-trigger-workflows#manual-events"><code>workflow_dispatch</code></a> で実行可能にしていますが、<code>pull_request</code> を利用すればプルリク時にテストを実行させることが可能になります。</p> <h2 id="Builder: プロジェクトのビルドを実行して出力結果を確認する"><a href="#Builder%3A+%E3%83%97%E3%83%AD%E3%82%B8%E3%82%A7%E3%82%AF%E3%83%88%E3%81%AE%E3%83%93%E3%83%AB%E3%83%89%E3%82%92%E5%AE%9F%E8%A1%8C%E3%81%97%E3%81%A6%E5%87%BA%E5%8A%9B%E7%B5%90%E6%9E%9C%E3%82%92%E7%A2%BA%E8%AA%8D%E3%81%99%E3%82%8B">Builder: プロジェクトのビルドを実行して出力結果を確認する</a></h2> <p>GameCI にはプロジェクトのビルドを行うための GitHub Actions も用意されています。<strong>実際に GameCI で WebGL ビルドを行いその内容を GitHub Pages で確認できるようにして動作検証していきます。</strong></p> <p>早速 WebGL ビルドを行うためのワークフローファイルを作成していきます。</p> <pre><code class="yml"># .github/workflows/webgl_build.yml name: Run the WebGL build on: workflow_dispatch: {} jobs: build: name: Run the WebGL build runs-on: ubuntu-latest steps: # actions/checkout@v2 を利用して作業ディレクトリに # Unity プロジェクトの中身をダウンロードしてくる - name: Check out my unity project. uses: actions/checkout@v2 # GameCI の Builder を利用して、 # Unity プロジェクトのビルドを実行する - name: Run the WebGL build uses: game-ci/unity-builder@v2 env: UNITY_LICENSE: $<span>{</span><span>{</span> secrets.UNITY_LICENSE <span>}</span><span>}</span> with: # 今回は WebGL ビルドを行いたいため WebGL を指定する # WebGL 以外の指定可能な値は下記に記載の値が利用可能 # ref: https://docs.unity3d.com/ScriptReference/BuildTarget.html targetPlatform: WebGL unityVersion: 2020.3.5f1 # Builder で出力した WebGL ビルドを GitHub Pages にデプロイする - name: Deploy to GitHub Pages uses: JamesIves/[email protected] with: # GitHub Pages デプロイ用の Orphan ブランチ名を指定する branch: gh-pages # デプロイ用ビルドフォルダパスを指定する # GameCI の Builder はデフォルトでは build フォルダにビルド内容を出力する folder: build # Builder で出力した WebGL ビルドをアーティファクトでダウンロード可能にする - name: Upload the WebGL Build uses: actions/upload-artifact@v2 with: name: Build path: build </code></pre> <p>上記のワークフローファイルを GitHub Actions 上で動作検証する際の手順は下記になります。</p> <p><img src="https://i.gyazo.com/b8f43baef2e2edbf8d5510ce8f58172d.png" alt="1. Unity の WebGL ビルドを実行するためのワークフローを実行する" /><br /> <strong>1. Unity の WebGL ビルドを実行するためのワークフローを実行する</strong></p> <p><img src="https://i.gyazo.com/a102d1f5cb4d0581746770d1d9cad965.png" alt="2. ワークフローの実行が成功したら、詳細画面に遷移した後、ビルド内容が正常か確認する" /><br /> <strong>2. ワークフローの実行が成功したら、詳細画面に遷移した後、ビルド内容が正常そうか確認する</strong></p> <p><img src="https://i.gyazo.com/7d39c035b52ba3862d5dad18213a2041.png" alt="3. ビルド内容を確認するための GitHub Pages の設定を Settings から行う" /><br /> <strong>3. ビルド内容を確認するための GitHub Pages の設定を Settings から行う</strong></p> <p><img src="https://i.gyazo.com/b2e1bd86ca9d3b3f207a5ddbb1de3ba7.png" alt="4. GitHub Pages でブラウザから WebGL ビルドの動作確認をする" /><br /> <strong>4. GitHub Pages でブラウザから WebGL ビルドの動作確認をする</strong></p> <p><strong>上記のように Builder を利用することで WebGL ビルドの成否及び、最新のビルド内容を常に GitHub Pages で見られるようにできます。</strong> すると WebGL ビルドが正常かどうかの確認が常に GitHub Pages を見れば把握できるようになるため、<a target="_blank" rel="nofollow noopener" href="https://unityroom.com/unity1weeks">Unity1 週間ゲームジャム</a> などに参加する際で便利に活用できそうです。</p> <p><em>WebGL ビルドを行う際、Unity バージョンやアセットの対応状況によっては正しくブラウザ上で動作しないビルドが出力されます。<strong>ただし、ブラウザで発生するエラー内容によっては WebGL のビルド設定を見直すだけで解決できる場合があります。</strong> 例えば <code>unityframework is not defined</code> というエラーが発生した際は、<a target="_blank" rel="nofollow noopener" href="https://qiita.com/aguroshou0413/items/1451a6779a92acb96b78">この記事</a> のように WebGL の <code>Build Settings</code> を見直すことで解決できる場合があります。</em></p> <p><em>私の環境では <a target="_blank" rel="nofollow noopener" href="https://github.com/JamesIves/github-pages-deploy-action"><code>JamesIves/github-pages-deploy-action</code></a> で GitHub Pages へのデプロイを行った際、<strong>デフォルトでは <code>/WebGL/WebGL</code> フォルダにビルド内容が出力されました。そのため、ブラウザから WebGL ビルドにアクセスする際、<code><GitHub Pages の URL>/WebGL/WebGL</code> のような URL にアクセスする必要がありました。</strong></em></p> <h2 id="Returning a license: GameCI で利用している Unity License を返却する"><a href="#Returning+a+license%3A+GameCI+%E3%81%A7%E5%88%A9%E7%94%A8%E3%81%97%E3%81%A6%E3%81%84%E3%82%8B+Unity+License+%E3%82%92%E8%BF%94%E5%8D%B4%E3%81%99%E3%82%8B">Returning a license: GameCI で利用している Unity License を返却する</a></h2> <p><strong>通常利用することは無いと<a target="_blank" rel="nofollow noopener" href="https://game.ci/docs/github/returning-a-license">公式サイト</a>にも書かれていますが、Professional License の返却も GameCI で行うことが可能です。</strong> 今回は Personal License を利用したため使用しませんでしたが、下記をワークフローのステップに組み込むことで Professional License を返却できるようです。</p> <pre><code class="yml"># ... # どこかのタイミングでライセンスのアクティベートを行う - name: Activate Unity uses: game-ci/unity-activate@v1 env: UNITY_LICENSE: $<span>{</span><span>{</span> secrets.UNITY_LICENSE <span>}</span><span>}</span> #... # ステップの最後などに game-ci/unity-return-license@v1 を呼び出して # アクティベート済みのライセンスを返却する - name: Return license uses: game-ci/unity-return-license@v1 if: always() #... </code></pre> <h1 id="おわりに"><a href="#%E3%81%8A%E3%82%8F%E3%82%8A%E3%81%AB">おわりに</a></h1> <p>以前 Unity コマンドを駆使して自分で CI 環境を構築した経験があるのですが、<br /> GameCI を利用した方が全然楽に Unity CI 環境構築を GitHub Actions 上で行えました。</p> <p>ちなみに <a target="_blank" rel="nofollow noopener" href="https://game.ci/docs/docker/docker-images">GameCI で利用されている Docker イメージ</a> は以前からよく使われていた <a target="_blank" rel="nofollow noopener" href="https://hub.docker.com/r/gableroux/unity3d/">gableroux/unity3d</a> が元になっているようでした。ってか <a target="_blank" rel="nofollow noopener" href="https://gableroux.com/about/">GabLeRoux さんのホームページ</a> を見たら、GameCI の開発を始めた方のようでした。すごい。</p> <p>本記事が GitHub Actions で Unity CI 環境構築を始めようとしている方の助けになれれば幸いです。</p> <h1 id="参考リンク"><a href="#%E5%8F%82%E8%80%83%E3%83%AA%E3%83%B3%E3%82%AF">参考リンク</a></h1> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://game.ci/">GameCI - The fastest and easiest way to automatically test and build your game projects</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://unity3d.com/jp/unity/features/cloud-build">Services - Cloud Build - Unity</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://aws.amazon.com/jp/cloudformation/">AWS CloudFormation(テンプレートを使ったリソースのモデル化と管理)| AWS</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://fastlane.tools/">fastlane - App automation done right</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://docs.github.com/ja/github/setting-up-and-managing-your-github-user-account/managing-user-account-settings/managing-the-default-branch-name-for-your-repositories#about-management-of-the-default-branch-name">リポジトリのデフォルトブランチ名を管理する - GitHub Docs</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://blog.yucchiy.com/2019/01/08/how-to-get-unity-free-license/">Unity でパーソナルライセンスのシリアルナンバーを発行する | Yucchiy's Note</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://license.unity3d.com">Unity license manual activation webpage</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://docs.github.com/ja/actions/reference/encrypted-secrets">暗号化されたシークレット - GitHub Docs</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://docs.unity3d.com/ScriptReference/BuildTarget.html">Unity - Scripting API: BuildTarget</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://unityroom.com/unity1weeks">Unity 1 週間ゲームジャム | フリーゲーム投稿サイト unityroom</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/aguroshou0413/items/1451a6779a92acb96b78">Unity2020 WebGL 9 割まで読み込めるがアプリが起動しない不具合の解決方法 - Qiita</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://github.com/marketplace/actions/deploy-to-github-pages">Deploy to GitHub Pages · Actions · GitHub Marketplace</a></li> </ul> <div class="footnotes" role="doc-endnotes"> <hr /> <ol> <li id="fn:1" role="doc-endnote"> <p><code>alf</code> ファイル及び <code>ulf</code> ファイルの実態は XML ファイルです。 <a href="#fnref:1" class="footnote-backref" role="doc-backlink">↩︎</a></p> </li> <li id="fn:2" role="doc-endnote"> <p>適当なテキストエディタで <code>ulf</code> ファイルを開き全文をコピー & ペーストします。 <a href="#fnref:2" class="footnote-backref" role="doc-backlink">↩︎</a></p> </li> </ol> </div> nikaera tag:crieit.net,2005:PublicArticle/17235 2021-05-23T18:18:06+09:00 2021-05-23T19:26:23+09:00 https://crieit.net/posts/pillow-vertical-writing 📝 Pillow を使って画像に縦書きテキストを埋め込む <h2 id="はじめに"><a href="#%E3%81%AF%E3%81%98%E3%82%81%E3%81%AB">はじめに</a></h2> <p>縦書きテキストを画像に埋め込みたいと頼まれたので、<br /> Python 製の画像処理ライブラリ <a target="_blank" rel="nofollow noopener" href="https://pillow.readthedocs.io/en/stable/">Pillow</a> を使ってサクッと実装してみました。</p> <p>一応ソースコードは Gist にもアップ済みです ✍️<br /> <a target="_blank" rel="nofollow noopener" href="https://gist.github.com/nikaera/c1049708ff548b06cab0ae377adc4ac7">https://gist.github.com/nikaera/c1049708ff548b06cab0ae377adc4ac7</a></p> <h2 id="動作環境"><a href="#%E5%8B%95%E4%BD%9C%E7%92%B0%E5%A2%83">動作環境</a></h2> <ul> <li>Python 3.9.5</li> <li>Pillow 8.2.0</li> </ul> <h2 id="画像に縦書きテキストを埋め込む"><a href="#%E7%94%BB%E5%83%8F%E3%81%AB%E7%B8%A6%E6%9B%B8%E3%81%8D%E3%83%86%E3%82%AD%E3%82%B9%E3%83%88%E3%82%92%E5%9F%8B%E3%82%81%E8%BE%BC%E3%82%80">画像に縦書きテキストを埋め込む</a></h2> <p>まずは今回利用する <a target="_blank" rel="nofollow noopener" href="https://pillow.readthedocs.io/en/stable/">Pillow</a> を予めインストールしておきます。</p> <pre><code class="bash">pip install Pillow </code></pre> <p>その後 <code>main.py</code> を作成して下記を入力します。<br /> テキストを埋め込みたい画像を <code>main.py</code> と同じフォルダに <code>sample.jpeg</code> という名前で配置しておきます。</p> <pre><code class="python"># main.py # Pillow の利用するモジュールのみをインポートする from PIL import Image, ImageDraw, ImageFont # 読み込みたいフォント情報を入力する font_name = "/System/Library/Fonts/ヒラギノ角ゴシック W0.ttc" font_size = 48 font = ImageFont.truetype(font_name, font_size) # テキストを埋め込みたい画像 sample.jpeg を読み込む im = Image.open('sample.jpeg') d = ImageDraw.Draw(im) # 画像に埋め込みたいテキスト情報を入力する # (後述するが、改行コードには未対応) text = "bifdLcFCKXtFJZmPZhzdefjhhYTtuJPAYsR" # 文章を改行するまでの文字数を入力する split_number = 11 # split_number で指定した文字数ごとに分割され配列に格納される # ref: https://qiita.com/yasunori/items/551a7c20ef9b81474e2a splits = [text[i: i+split_number] for i in range(0, len(text), split_number)] # 画像の width を読み込み、画像の右端の座標を取得する # top_right_margin には余白を設定する (描画領域の端が画像の端と被ってしまうため) w, _ = im.size top_right_margin = 13 right_edge = w - top_right_margin # テキストの入力領域に端が赤い四角形を描画する d.rectangle((right_edge, top_right_margin, right_edge - font_size * len(splits), font_size * split_number + top_right_margin), fill=(255, 255, 255), outline=(255, 0, 0)) # 分割した文章を上記四角形内に左にずらしながら縦書き入力する for index, item in enumerate(splits): d.text((right_edge - (font_size / 2) - font_size * index, top_right_margin), item, fill="black", anchor="mt", font=font, direction="ttb") # 縦書きテキストを埋め込んだ画像を test.png として出力する im.save("test.png") </code></pre> <p>上記ソースコード内で特筆すべき事項として <code>d.text</code> があります。<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup><br /> まず <a target="_blank" rel="nofollow noopener" href="https://pillow.readthedocs.io/en/stable/handbook/text-anchors.html"><code>anchor</code> オプション</a> で文字を横中央に寄せて、縦を上端に寄せるよう設定しています。</p> <p>更に <a target="_blank" rel="nofollow noopener" href="https://pillow.readthedocs.io/en/stable/reference/ImageDraw.html#PIL.ImageDraw.ImageDraw.text"><code>direction</code> オプション</a> を利用することで、文字列を縦に入力しています。縦に入力するためのオプションとして <code>ttb</code> を入力しています。</p> <p>実際に <code>main.py</code> を実行した際に生成される画像は下記のとおりです。</p> <p><img src="https://i.gyazo.com/a51851cded2f1d4bf2da6ff1e98df912.jpg" alt="テキストを埋め込む前の画像" /><br /> <strong>テキストを埋め込む前の画像</strong></p> <p><img src="https://i.gyazo.com/7908dd812db36afeee315ae5bd287406.png" alt="<code>main.py</code> を実行してテキストを埋め込んだ画像" /><br /> <strong><code>main.py</code> を実行してテキストを埋め込んだ画像</strong></p> <h2 id="おわりに"><a href="#%E3%81%8A%E3%82%8F%E3%82%8A%E3%81%AB">おわりに</a></h2> <p>今回は Pillow を用いて縦書きテキストの画像埋め込みを実装しましたが、<br /> ブラウザベースで埋め込みをやりたい場合は <a target="_blank" rel="nofollow noopener" href="https://html2canvas.hertzen.com/">html2canvas</a> からの png 出力ダウンロードで実装できそうでした。(ただその場合は各種ブラウザ対応とかモバイル対応が大変そう。。 👀)</p> <h2 id="参考リンク"><a href="#%E5%8F%82%E8%80%83%E3%83%AA%E3%83%B3%E3%82%AF">参考リンク</a></h2> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://pillow.readthedocs.io/en/stable/">Pillow — Pillow (PIL Fork) 8.2.0 documentation</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://pillow.readthedocs.io/en/stable/handbook/text-anchors.html">Text anchors — Pillow (PIL Fork) 8.2.0 documentation</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://pillow.readthedocs.io/en/stable/reference/ImageDraw.html#PIL.ImageDraw.ImageDraw.text">ImageDraw Module — Pillow (PIL Fork) 8.2.0 documentation</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/masushin/items/fbeb9018c9b17fe4d244">Pillow を日本語縦書きに対応させる - Qiita</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://gist.github.com/nikaera/c1049708ff548b06cab0ae377adc4ac7">Embedding Japanese vertical writing characters into an image using Pillow.</a></li> </ul> <div class="footnotes" role="doc-endnotes"> <hr /> <ol> <li id="fn:1" role="doc-endnote"> <p>当初は <a target="_blank" rel="nofollow noopener" href="https://pillow.readthedocs.io/en/stable/reference/ImageDraw.html#PIL.ImageDraw.ImageDraw.multiline_text"><code>multiline_text</code> 関数</a> を用いて改行にも対応した形でテキスト埋め込みを実装する予定でした。しかし、<code>ValueError: ttb direction is unsupported for multiline text</code> というエラーが発生してしまい <code>multiline_text</code> 関数の利用は断念しました。。😭 <a href="#fnref:1" class="footnote-backref" role="doc-backlink">↩︎</a></p> </li> </ol> </div> nikaera tag:crieit.net,2005:PublicArticle/16763 2021-03-22T13:21:27+09:00 2021-03-22T13:21:27+09:00 https://crieit.net/posts/sync-zenn-with-dev-action Zenn の記事を DEV に自動的に同期させる GitHub Action 作ってみた <h1 id="はじめに"><a href="#%E3%81%AF%E3%81%98%E3%82%81%E3%81%AB">はじめに</a></h1> <p>去年 <a target="_blank" rel="nofollow noopener" href="https://dev.to/">DEV</a> のアカウントを作成したものの、今まで全く有効活用出来ていませんでした。</p> <p>DEV には <a target="_blank" rel="nofollow noopener" href="https://dev.to/michaelburrows/comment/125j0">カノニカル URL</a> を設定出来るので、常々 Zenn の記事を投稿する際にクロスポストしたいなと考えておりました。そこで、<strong>Zenn に記事を投稿したら、自動的に DEV にも記事を投稿 & 同期する GitHub Action を作ってみました。</strong><br /> <a target="_blank" rel="nofollow noopener" href="https://github.com/nikaera/sync-zenn-with-dev-action">sync-zenn-with-dev-action</a></p> <p>今回初めて GitHub Action を自作したのですが、その中で得た知見を残す形で記事を書くことにしました。また、GitHub Action は TypeScript で作成しました。</p> <h1 id="開発した GitHub Action の概要"><a href="#%E9%96%8B%E7%99%BA%E3%81%97%E3%81%9F+GitHub+Action+%E3%81%AE%E6%A6%82%E8%A6%81">開発した GitHub Action の概要</a></h1> <p>まずはザッとどのような GitHub Action を作成したのか、概要について説明いたします。</p> <p><strong>GitHub リポジトリで管理している Zenn の記事を DEV に同期して投稿する GitHub Action を作成しました。</strong> その際に DEV へ投稿する記事には Zenn の該当記事へのカノニカル URL も自動で設定できます。これにより DEV と Zenn へ記事をシームレスにクロスポストすることが可能となります。</p> <p>今回作成した GitHub Action を利用するワークフローファイルの一例は下記となります。</p> <pre><code class="yml">name: "Sync all Zenn articles to DEV" on: push: branches: - main jobs: build: runs-on: ubuntu-latest steps: - name: checkout my project uses: actions/checkout@v2 - name: dev.to action step uses: nikaera/sync-zenn-with-dev-action@v1 # id を設定することで、後のジョブで Output で指定した値が参照可能になる id: dev-to with: # DEV の API キーを指定する api_key: $<span>{</span><span>{</span> secrets.api_key <span>}</span><span>}</span> # (オプション) DEV に記事を投稿した際に Zenn のカノニカル URL を設定したい場合に指定する # username: nikaera # (オプション) 改行区切りで指定した articles フォルダ内のファイルパスを記載した txt ファイルを指定することで、記載された記事のみを同期するようになる。 # 他プラグインと組み合わせることで差分のみを txt ファイルに載せることが可能。詳細については後述の Outputs の項目に記載。 # added_modified_filepath: ./added_modified.txt # (オプション) Zenn の articles 以下全ての記事を常に DEV に同期するか指定する # update_all が true のときは added_modified_filepath は無視される。 # update_all: false # 上記アクション実行時に DEV に新規で同期する記事に関しては Zenn のマークダウンヘッダに # 該当する DEV の記事の ID が dev_article_id として記載されるようになる。 # 今後はその ID を元に同期するようになるため、該当する Zenn の記事をコミットする。 # 新規で同期する記事が無ければ、このジョブは実行しない。 - name: write article id of DEV to articles of Zenn. run: | git config user.name github-actions git config user.email [email protected] git add $<span>{</span><span>{</span> steps.dev-to.outputs.newly-sync-articles <span>}</span><span>}</span> git commit -m "sync: Zenn with DEV [skip ci]" git push if: steps.dev-to.outputs.newly-sync-articles # Outputs には DEV の記事情報 (title, url) が含まれるようになるため、 # 最後に出力して実行結果の内容を確認することもできる - name: Get the output articles. # dev-to という id が紐付いたジョブの Outputs を取得して echo で内容を出力する run: echo "$<span>{</span><span>{</span> steps.dev-to.outputs.articles <span>}</span><span>}</span>" </code></pre> <p>簡単に <code>nikaera/sync-zenn-with-dev-action@v1</code> というジョブの内部処理について説明いたします。</p> <ol> <li>Inputs の <code>update_all</code> 及び <code>added_modified_filepath</code> で受け取った情報を元に、<br /> どの記事を DEV に同期させるか判定する</li> <li>DEV に同期する記事のファイルパス一覧を取得して、<br /> それぞれの記事のヘッダに <code>dev_article_id</code> の記載があるか判定する</li> <li>Inputs の <code>api_key</code> を利用して、Zenn の記事に <code>dev_article_id</code> が含まれていれば、<br /> 該当する DEV の記事を更新する。含まれていなければ DEV に新規で記事を作成する</li> <li>Inputs の <code>username</code> を利用して、<br /> DEV の記事に該当する Zenn 記事のカノニカル URL を設定する</li> <li>DEV に新規で記事を作成した場合は、<br /> Zenn の該当記事のヘッダに <code>dev_article_id</code> を書き込む</li> <li>DEV に記事を投稿する際、Zenn は対応しているが、<br /> DEV では対応していない記述は削除する (<code>:::</code> 記法や一部のコード記法)</li> <li>記事の公開ステータス及びタグなどについても DEV の記事に反映する<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup></li> <li>新規で DEV に記事を同期した Zenn 記事のファイルパスを、<br /> Outputs の <code>newly-sync-articles</code> に設定する<br /> (後のジョブで <code>dev_article_id</code> の含まれた記事をコミットしたいため)</li> <li>ワークフローで同期された記事情報は Outouts の <code>articles</code> に設定する</li> </ol> <p>Inputs と Outputs の内容一覧については下記になります。</p> <p><strong>Inputs</strong></p> <div class="table-responsive"><table> <thead> <tr> <th align="left">キー</th> <th align="left">説明</th> <th align="center">必須</th> </tr> </thead> <tbody> <tr> <td align="left">api_key</td> <td align="left">DEV の <a target="_blank" rel="nofollow noopener" href="https://docs.forem.com/api/#section/Authentication">API Key</a> を設定する</td> <td align="center">o</td> </tr> <tr> <td align="left">username</td> <td align="left">Zenn の <strong>自分のアカウント名</strong> を設定する (DEV に同期する記事に Zenn のカノニカル URL を設定したい場合のみ)</td> <td align="center">x</td> </tr> <tr> <td align="left">added_modified_filepath</td> <td align="left">改行区切りで指定した articles フォルダ内のファイルパスを記載した txt ファイルを指定することで、記載された記事のみを同期するようになる。<strong>PR やコミット差分のファイルのみを取得するための GitHub Action <a target="_blank" rel="nofollow noopener" href="https://github.com/jitterbit/get-changed-files">jitterbit/get-changed-files@v1</a> と組み合わせることで、更新差分のあった記事のみを随時同期することも可能。<sup id="fnref:2"><a href="#fn:2" class="footnote-ref" role="doc-noteref">2</a></sup></strong> 更新差分のあった記事のみを随時同期するための<a target="_blank" rel="nofollow noopener" href="https://github.com/nikaera/zenn.dev/blob/main/.github/workflows/sync-zenn-with-dev.yml">実際のワークフローファイルはこちら</a></td> <td align="center">x</td> </tr> <tr> <td align="left">update_all</td> <td align="left">Zenn の全ての記事をどうきするかどうかを設定する。GitHub Action 初回実行時のみ true にする使い方を想定している。デフォルトは true。<strong><code>added_modified_filepath</code> よりも <code>update_all</code> が優先されるため <code>added_modified_filepath</code> を設定する場合は false を設定する必要あり`</strong></td> <td align="center">x</td> </tr> </tbody> </table></div> <p><strong>Outputs</strong></p> <div class="table-responsive"><table> <thead> <tr> <th align="left">キー</th> <th align="left">説明</th> </tr> </thead> <tbody> <tr> <td align="left">articles</td> <td align="left">同期された DEV の記事のタイトル及び URL が格納された配列</td> </tr> <tr> <td align="left">newly-sync-articles</td> <td align="left">DEV で新たに新規作成された Zenn 記事のファイルパスが格納された配列。<strong><a target="_blank" rel="nofollow noopener" href="https://github.com/nikaera/sync-zenn-with-dev-action/blob/main/.github/workflows/test.yml#L31-L38">実際のワークフローファイルの該当する記述</a>のように、必ずコミットに含めるようにする必要がある (理由は後述)</strong></td> </tr> </tbody> </table></div> <p>Inputs 及び Outputs については<a target="_blank" rel="nofollow noopener" href="https://docs.github.com/ja/actions/creating-actions/metadata-syntax-for-github-actions#">公式サイトの説明</a>をご参照ください。</p> <h2 id="Zenn の記事を DEV に同期するための仕組み"><a href="#Zenn+%E3%81%AE%E8%A8%98%E4%BA%8B%E3%82%92+DEV+%E3%81%AB%E5%90%8C%E6%9C%9F%E3%81%99%E3%82%8B%E3%81%9F%E3%82%81%E3%81%AE%E4%BB%95%E7%B5%84%E3%81%BF">Zenn の記事を DEV に同期するための仕組み</a></h2> <p>Zenn の記事を新規で DEV に同期する際は、DEV に記事を新規作成する必要があります。<strong>その際に Zenn の記事と DEV の記事を紐付けるための何らかの仕組みが必要となります。そうしないと、今後 Zenn の記事内容を更新した際に、DEV のどの記事に内容を同期させればよいかが不明なためです。</strong></p> <p>そこで、記事を同期するための仕組みとして、<strong><code>dev_article_id</code> というフィールドを Zenn のマークダウンヘッダに追記することで DEV の同期すべき記事との紐付けを行うことにしました。</strong><code>dev_article_id</code> には <a target="_blank" rel="nofollow noopener" href="https://docs.forem.com/api/#operation/createArticle">DEV の記事作成 API</a> 実行時の返り値である <code>id</code> を設定します。</p> <p>一度 <code>id</code> を <code>dev_article_id</code> として Zenn の記事に紐付けてしまえば、次回以降に記事の同期を行う際は <a target="_blank" rel="nofollow noopener" href="https://docs.forem.com/api/#operation/updateArticle">DEV の記事更新 API</a> を利用できます。</p> <p>また、<strong>Outputs の <code>newly-sync-articles</code> には新規で作成された DEV 記事の <code>id</code> である <code>dev_article_id</code> が追記された Zenn 記事のファイルパスが格納されています。そのため、<code>nikaera/sync-zenn-with-dev-action@v1</code> 実行後は、下記のように <code>steps.dev-to.outputs.newly-sync-articles</code> 内に格納されたファイル群をコミットに反映させる必要があります。</strong></p> <pre><code class="yml"># `nikaera/sync-zenn-with-dev-action@v1` 実行後に必ず定義すべきジョブ # DEV に新規に作成した記事がなければ実行しない (if: steps.dev-to.outputs.newly-sync-articles) - name: write article id of DEV to articles of Zenn. run: | git config user.name github-actions git config user.email [email protected] git add $<span>{</span><span>{</span> steps.dev-to.outputs.newly-sync-articles <span>}</span><span>}</span> git commit -m "sync: Zenn with DEV [skip ci]" git push if: steps.dev-to.outputs.newly-sync-articles </code></pre> <p>上記のジョブで <code>newly-sync-articles</code> に格納された <code>dev_article_id</code> が追記された Zenn 記事は随時コミットに反映しないと、<strong>Zenn の全ての記事が同期毎 DEV に新規作成され続けるという不具合を引き起こしてしまうので、ご注意ください</strong></p> <h1 id="GitHub Action を開発する手順"><a href="#GitHub+Action+%E3%82%92%E9%96%8B%E7%99%BA%E3%81%99%E3%82%8B%E6%89%8B%E9%A0%86">GitHub Action を開発する手順</a></h1> <p>サクッと開発に取り組みたかったため、<a target="_blank" rel="nofollow noopener" href="https://docs.github.com/ja/actions/creating-actions/creating-a-docker-container-action">Docker コンテナを利用する方法</a> ではなく、<a target="_blank" rel="nofollow noopener" href="https://docs.github.com/ja/actions/creating-actions/creating-a-javascript-action">JavaScript を利用する方法</a> で開発を進めていくことにしました。</p> <h2 id="TypeScript で GitHub Action を作る"><a href="#TypeScript+%E3%81%A7+GitHub+Action+%E3%82%92%E4%BD%9C%E3%82%8B">TypeScript で GitHub Action を作る</a></h2> <p>GitHub 公式が TypeScript で GitHub Action を作るための <a target="_blank" rel="nofollow noopener" href="https://github.com/actions/typescript-action">テンプレートプロジェクト</a> を用意してくれています。今回はこのテンプレートプロジェクトを利用する形でプロジェクトを作成しました。</p> <p><em>(余談) GitHub Action では <a target="_blank" rel="nofollow noopener" href="https://docs.github.com/ja/actions/creating-actions/creating-a-docker-container-action">Docker コンテナ</a> を用いてワークフローを実行可能です。<strong>そのため、実行環境は自由に設定出来ます。(Go, Python, Ruby, etc.)</strong></em></p> <p>早速 TypeScript のテンプレートプロジェクトを元に自分の GitHub Action プロジェクトを作成します。</p> <p><img src="https://i.gyazo.com/359f6f795bab9807c9f480c0f922973a.png" alt="スクリーンショット 2021-03-21 13.25.54.png" /><br /> <strong>1. テンプレートプロジェクトを元に GitHub Action の TypeScript プロジェクトを作成する</strong></p> <p><img src="https://i.gyazo.com/76aecd3d63db95d34c086774ded122d4.png" alt="スクリーンショット 2021-03-21 13.29.43.png" /><br /> <strong>2. プロジェクトの作成後 <code>git clone</code> してきて開発する準備を整える</strong></p> <h2 id="GitHub Action プロジェクトの開発を進めるための準備を行う"><a href="#GitHub+Action+%E3%83%97%E3%83%AD%E3%82%B8%E3%82%A7%E3%82%AF%E3%83%88%E3%81%AE%E9%96%8B%E7%99%BA%E3%82%92%E9%80%B2%E3%82%81%E3%82%8B%E3%81%9F%E3%82%81%E3%81%AE%E6%BA%96%E5%82%99%E3%82%92%E8%A1%8C%E3%81%86">GitHub Action プロジェクトの開発を進めるための準備を行う</a></h2> <p>テンプレートプロジェクトを <code>git clone</code> したら、まずは <code>action.yml</code> の内容を変更します。<br /> 今回作成した GitHub Action の <code>action.yml</code> は下記となっております。</p> <pre><code class="yml"># action.yml # GitHub Action のプロジェクト名 name: 'Sync Zenn articles to DEV' # GitHub Action のプロジェクト説明文 description: 'Just sync Zenn articles to DEV.' # GitHub Action の作者 author: 'nikaera' # GitHub Action に渡せる引数の値定義 inputs: api_key: # フィールドの指定が必須であれば true、必須でなければ false を設定する # DEV の API キーは同期を行う際に必須なため、true を設定している required: true # フィールドの説明文 description: 'The api_key required to use the DEV API (https://docs.forem.com/api/#section/Authentication)' username: required: false description: "Zenn user's account name. (Fields to be filled in if canonical url is set.)" articles: required: false description: "The directory where Zenn articles are stored." # フィールドにはデフォルト値を指定することも可能 # Zenn の記事がデフォで格納されているフォルダ名を指定している default: articles update_all: require: false description: "Whether to synchronize all articles." default: true added_modified_filepath: required: false description: | Synchronize only the articles in the file path divided by line breaks. You can use jitterbit/get-changed-files@v1 to get only the file paths of articles that have changed in the correct format. (https://github.com/jitterbit/get-changed-files) # GitHub Action 実行後に参照可能になる値定義 outputs: articles: description: 'A list of URLs of dev.to articles that have been created or updated' newly-sync-articles: description: 'File path list of newly synchronized articles.' # GitHub Action の実行環境 runs: using: 'node12' # テンプレートプロジェクトでは コンパイル先が dist になるため `dist/index.js` を指定している main: 'dist/index.js' </code></pre> <p>TypeScript のテンプレートプロジェクトでは、バンドルツールとして <a target="_blank" rel="nofollow noopener" href="https://github.com/vercel/ncc"><code>ncc</code></a> が採用されています。<strong>GitHub Action 実行時に使用されるのは ncc によりコンパイルされた単一の JavaScript ファイル (<code>dist/index.js</code>) になります。</strong></p> <p>あとは <code>src</code> フォルダ内でプログラムを書いて、<code>npm run all && node dist/index.js</code> のようにコマンド実行しながら開発を進めていくだけです。</p> <p><em>(余談) GitHub Action の開発ツールとして Docker を利用した <a target="_blank" rel="nofollow noopener" href="https://github.com/nektos/act"><code>act</code></a> というものが存在するようです。ローカル環境で検証する際は <a target="_blank" rel="nofollow noopener" href="https://github.com/nektos/act#known-issues">既知の問題</a> に対応する必要がありそうですが、GitHub Action の開発で非常に有効活用できそうで気になっております。</em></p> <p>今回の開発では利用しなかったのですが、今後開発を進めていく中で利用する機会も出てきそうなので、その際は本記事内容を更新する形で知見を追記したいと考えております。</p> <p>:::</p> <h2 id="GitHub Action を実装する際に利用した機能"><a href="#GitHub+Action+%E3%82%92%E5%AE%9F%E8%A3%85%E3%81%99%E3%82%8B%E9%9A%9B%E3%81%AB%E5%88%A9%E7%94%A8%E3%81%97%E3%81%9F%E6%A9%9F%E8%83%BD">GitHub Action を実装する際に利用した機能</a></h2> <p>GitHub Action を実装する際に利用した機能を、実際のコード内容を抜粋して簡単に説明していきます。下記で紹介する内容は <a target="_blank" rel="nofollow noopener" href="https://github.com/actions/toolkit">GitHub Actions Toolkit</a> の機能です。</p> <pre><code class="typescript">/** 下記の yml の with で指定した値は core.getInput で受け取ることが可能。 - name: dev.to action step uses: nikaera/sync-zenn-with-dev-action@v1 id: dev-to with: # DEV の API キーを指定する api_key: $<span>{</span><span>{</span> secrets.api_key <span>}</span><span>}</span> */ core.getInput("api_key", { required: true }); core.getInput("update_all", { required: false }); /** 下記の yml の steps.<ジョブで指定した id>.outputs で参照可能な値をセットすることが可能。 セットする内容は文字列である必要がある。 - name: dev.to action step uses: nikaera/sync-zenn-with-dev-action@v1 id: dev-to - name: Get the output articles. run: echo "$<span>{</span><span>{</span> steps.dev-to.outputs.articles <span>}</span><span>}</span>" */ core.setOutput("articles", JSON.stringify(devtoArticles, undefined, 2)); core.setOutput("newly-sync-articles", newlySyncedArticles.join(" ")); /** GitHub Action 実行時に出力されるログをレベルごとに出力することが可能 core.debug はローカル実行時のみに出力内容を確認することができる */ core.debug("debug"); core.info(`update_all: ${updateAll}`); core.error(JSON.stringify(error)); </code></pre> <p>上記だけ把握してれば GitHub Action の開発は問題なく行うことができました。</p> <h1 id="作成した GitHub Action を実際に GitHub 上で実行可能にする"><a href="#%E4%BD%9C%E6%88%90%E3%81%97%E3%81%9F+GitHub+Action+%E3%82%92%E5%AE%9F%E9%9A%9B%E3%81%AB+GitHub+%E4%B8%8A%E3%81%A7%E5%AE%9F%E8%A1%8C%E5%8F%AF%E8%83%BD%E3%81%AB%E3%81%99%E3%82%8B">作成した GitHub Action を実際に GitHub 上で実行可能にする</a></h1> <p>ローカル環境で一通り開発が完了したら、GitHub リポジトリに push した後タグ付けを行います。<strong>GitHub Action はタグを設定しないと実行できないため必要な作業となります。</strong> 今回タグ付けは GitHub 上で行いました。</p> <p><img src="https://i.gyazo.com/252514cca20470154475db75b133e9b3.png" alt="スクリーンショット 2021-03-22 8.11.58.png" /><br /> <strong>1. タグの項目をクリックする</strong></p> <p><img src="https://i.gyazo.com/a8c145f8d6ae7b1f6253eb4866bcbb51.png" alt="スクリーンショット 2021-03-22 8.14.47.png" /><br /> <strong>2. <code>Create a new release</code> ボタンをクリックしてタグ作成画面に遷移する</strong></p> <p><img src="https://i.gyazo.com/25e019e42227ad7e0b0b634e68bb1873.png" alt="スクリーンショット 2021-03-22 8.20.00.png" /><br /> <strong>3. <code>Publish release</code> ボタンをクリックしてタグの作成を完了する</strong></p> <p><strong>上記の例では <code>v1</code> というタグを作成したので <code>nikaera/sync-zenn-with-dev-action@v1</code> のような記述で GitHub Action を利用可能になりました。</strong> 私は Zenn の記事を <a target="_blank" rel="nofollow noopener" href="https://github.com/nikaera/zenn.dev/"><code>zenn.dev</code></a> というリポジトリで管理しているため、早速このリポジトリに GitHub Action を導入してみます。</p> <h2 id="Zenn の全ての記事を DEV に同期するためのワークフロー"><a href="#Zenn+%E3%81%AE%E5%85%A8%E3%81%A6%E3%81%AE%E8%A8%98%E4%BA%8B%E3%82%92+DEV+%E3%81%AB%E5%90%8C%E6%9C%9F%E3%81%99%E3%82%8B%E3%81%9F%E3%82%81%E3%81%AE%E3%83%AF%E3%83%BC%E3%82%AF%E3%83%95%E3%83%AD%E3%83%BC">Zenn の全ての記事を DEV に同期するためのワークフロー</a></h2> <p>本記事の GitHub Action では DEV の API キーを使用するため、<strong>事前にシークレットへ <code>API_KEY</code> という名称で値を登録しておきます。</strong><a target="_blank" rel="nofollow noopener" href="https://docs.github.com/ja/actions/reference/encrypted-secrets#creating-encrypted-secrets-for-a-repository">公式サイトの手順</a> に従い シークレットの登録が完了したら、該当リポジトリに <code>.github/workflows/sync-zenn-with-dev-all.yml</code> というワークフローファイルを作成します。</p> <pre><code class="yml"># .github/workflows/sync-zenn-with-dev-all.yml name: "Sync-All Zenn with DEV" on: workflow_dispatch: jobs: build: runs-on: ubuntu-latest if: contains(github.event.head_commit.message, '[skip ci]') == false steps: - name: setup node project uses: actions/checkout@v2 - name: dev.to action step uses: nikaera/sync-zenn-with-dev-action@v1 id: dev-to with: api_key: $<span>{</span><span>{</span> secrets.api_key <span>}</span><span>}</span> # Zenn の自分のアカウント名を指定すると # DEV 記事のカノニカル URL に Zenn 記事の URL を指定できる # username: nikaera update_all: true - name: write article id of DEV to articles of Zenn. run: | git config user.name github-actions git config user.email [email protected] git add $<span>{</span><span>{</span> steps.dev-to.outputs.newly-sync-articles <span>}</span><span>}</span> git commit -m "sync: Zenn with DEV [skip ci]" git push if: steps.dev-to.outputs.newly-sync-articles - name: Get the output articles. run: echo "$<span>{</span><span>{</span> steps.dev-to.outputs.articles <span>}</span><span>}</span>" </code></pre> <p>上記は手動実行が可能な Zenn の記事を全て DEV に同期するためのワークフローファイルになります。作成が完了したらワークフローファイルを実行して、記事が正常に同期できているか確認してみます。</p> <p><img src="https://i.gyazo.com/e0740fe369752db67514e0ab50e88772.png" alt="スクリーンショット 2021-03-22 8.35.38.png" /><br /> <strong>1. <code>Actions</code> タブをクリックする</strong></p> <p><img src="https://i.gyazo.com/2aeadddccf08c0dddc54dddab464edee.png" alt="スクリーンショット 2021-03-22 8.37.24.png" /><br /> <strong>2. 作成した <code>Sync-All Zenn with DEV</code> ワークフローファイルを選択する</strong></p> <p><img src="https://i.gyazo.com/d9e138de1f6e0baac8b0c3bc6742f4f2.png" alt="スクリーンショット 2021-03-22 8.40.11.png" /><br /> <strong>3. <code>Run workflow</code> ボタンを実行して、ワークフローを実行する</strong></p> <p><img src="https://i.gyazo.com/e67df0f6e33d1f57a46000d20ef2c824.png" alt="スクリーンショット 2021-03-22 8.45.23.png" /><br /> <strong>4. ワークフローの実行が正常に完了していれば、ログに DEV の記事情報が出力される</strong></p> <p><img src="https://i.gyazo.com/4acc0b0de3acd6731918a9347e7baea8.png" alt="スクリーンショット 2021-03-22 8.50.37.png" /><br /> <strong>5. <code>dev.to</code> にアクセスして Zenn の記事情報が正常に同期されていることを確認する</strong></p> <p>正常に Zenn の全ての記事が DEV に同期されていることが確認できたら、次は Zenn の記事が新規作成されたり、更新されたときのみに DEV に同期するためのワークフローファイルを作成します。</p> <h2 id="更新差分のあった Zenn 記事のみを DEV に同期するためのワークフロー"><a href="#%E6%9B%B4%E6%96%B0%E5%B7%AE%E5%88%86%E3%81%AE%E3%81%82%E3%81%A3%E3%81%9F+Zenn+%E8%A8%98%E4%BA%8B%E3%81%AE%E3%81%BF%E3%82%92+DEV+%E3%81%AB%E5%90%8C%E6%9C%9F%E3%81%99%E3%82%8B%E3%81%9F%E3%82%81%E3%81%AE%E3%83%AF%E3%83%BC%E3%82%AF%E3%83%95%E3%83%AD%E3%83%BC">更新差分のあった Zenn 記事のみを DEV に同期するためのワークフロー</a></h2> <p><code>main</code> ブランチが更新されたときのみ更新された記事のみを DEV に同期するためのワークフローファイルを作成します。該当リポジトリに <code>.github/workflows/sync-zenn-with-dev.yml</code> というワークフローファイルを作成します。</p> <pre><code class="yml"># .github/workflows/sync-zenn-with-dev.yml name: "Sync Zenn with DEV" on: push: branches: - main jobs: build: runs-on: ubuntu-latest if: contains(github.event.head_commit.message, '[skip ci]') == false steps: - name: setup node project uses: actions/checkout@v2 - name: get modified files id: files uses: jitterbit/get-changed-files@v1 - name: output modified files to text run: | for changed_file in $<span>{</span><span>{</span> steps.files.outputs.added_modified <span>}</span><span>}</span>; do echo "${changed_file}" >> added_modified.txt done - name: dev.to action step uses: nikaera/sync-zenn-with-dev-action@v1 id: dev-to with: api_key: $<span>{</span><span>{</span> secrets.api_key <span>}</span><span>}</span> # Zenn の自分のアカウント名を指定すると # DEV 記事のカノニカル URL に Zenn 記事の URL を指定できる # username: nikaera added_modified_filepath: ./added_modified.txt update_all: false - name: write article id of DEV to articles of Zenn. run: | git config user.name github-actions git config user.email [email protected] git add $<span>{</span><span>{</span> steps.dev-to.outputs.newly-sync-articles <span>}</span><span>}</span> git commit -m "sync: Zenn with DEV [skip ci]" git push if: steps.dev-to.outputs.newly-sync-articles - name: Get the output articles. run: echo "$<span>{</span><span>{</span> steps.dev-to.outputs.articles <span>}</span><span>}</span>" </code></pre> <p>上記ワークフローファイルの作成が完了したら、早速動作確認のために、<strong>まさに今執筆中の本記事内容をリポジトリに push してみます。</strong></p> <p><img src="https://i.gyazo.com/36512e7874b49edc1e48f0ef88af5d89.png" alt="スクリーンショット 2021-03-22 9.07.55.png" /><br /> <strong>1. <code>git push</code> 後に該当するワークフローの実行結果を確認する</strong></p> <p><img src="https://i.gyazo.com/cbb0a1cf7b94605680e39630dded0096.png" alt="Image from Gyazo" /><br /> <strong>2. <code>dev.to</code> に執筆途中の内容で記事が同期されていることを確認する</strong></p> <p>これまで説明してきた 2 つのワークフローを利用することで、大抵のユースケースはカバーできるはずです。</p> <h1 id="おわりに"><a href="#%E3%81%8A%E3%82%8F%E3%82%8A%E3%81%AB">おわりに</a></h1> <p>GitHub Action の勉強のために取り組んだプロジェクトですが、思いの外楽しくて他にも色々な機能のアイデアがあるので随時実装していきたいと考えています。(英訳, タイトルフォーマット変更, etc.)</p> <p>DEV に Zenn の記事をクロスポストする GitHub Action を公開することで、いつもお世話になっている Zenn というプラットフォームを海外の方に認知していただける機会を創出できたのかもと考えたらテンションが上がってきました。</p> <p>それはさておき、Zenn の記事を他でも有効活用するための GitHub Action を開発する際には、恐らく本記事で紹介した GitHub Action のコードが参考になるはずです。</p> <p>また、<a target="_blank" rel="nofollow noopener" href="https://docs.github.com/ja/actions/creating-actions/publishing-actions-in-github-marketplace">GitHub Action の Marketplace</a> というものが用意されているようなので、開発がある程度完了次第、こちらに申請するのも試してみたいと考えております。</p> <p>ところで、<em>Crieit さんにも記事投稿 API ができたらクロスポストできるようにしたいな...という個人的願望がありますw</em></p> <h1 id="参考リンク"><a href="#%E5%8F%82%E8%80%83%E3%83%AA%E3%83%B3%E3%82%AF">参考リンク</a></h1> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://dev.to/">DEV Community 👩‍💻👨‍💻</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://dev.to/michaelburrows/comment/125j0">dev.to supports canonical URLs so you can share content without impacting SEO... - DEV Community</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://github.com/nikaera/sync-zenn-with-dev-action">nikaera/sync-zenn-with-dev-action: Just sync Zenn articles to DEV.</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://docs.forem.com/api/#section/Authentication">DEV API (beta)</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://github.com/jitterbit/get-changed-files">jitterbit/get-changed-files: Get all of the files changed/modified in a pull request or push's commits.</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://docs.github.com/ja/actions/creating-actions/creating-a-docker-container-action">Docker コンテナのアクションを作成する - GitHub Docs</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://docs.github.com/ja/actions/creating-actions/creating-a-javascript-action">JavaScript アクションを作成する - GitHub Docs</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://github.com/actions/typescript-action">actions/typescript-action: Create a TypeScript Action with tests, linting, workflow, publishing, and versioning</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://github.com/vercel/ncc">vercel/ncc: Compile a Node.js project into a single file. Supports TypeScript, binary addons, dynamic requires.</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://github.com/nektos/act">nektos/act: Run your GitHub Actions locally 🚀</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://github.com/actions/toolkit">actions/toolkit: The GitHub ToolKit for developing GitHub Actions.</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://docs.github.com/ja/actions/reference/encrypted-secrets#creating-encrypted-secrets-for-a-repository">暗号化されたシークレット - GitHub Docs</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://www.deepl.com/ja/pro#developer">DeepL Pro:テキストの他、Word などの文書をセキュアに翻訳</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/">Qiita</a></li> <li><a href="https://crieit.net/advent-calendars/2020/crieit">Crieit - プログラマー、クリエイターが何でも気軽に書けるコミュニティ</a></li> </ul> <div class="footnotes" role="doc-endnotes"> <hr /> <ol> <li id="fn:1" role="doc-endnote"> <p>DEV (Forem) の仕様上、<a target="_blank" rel="nofollow noopener" href="https://dev.to/p/editor_guide#front-matter">タグは最大でも 4 つまで</a>しか設定できないため、Zenn で設定したタグの先頭 4 つまで DEV の記事には設定しています <a href="#fnref:1" class="footnote-backref" role="doc-backlink">↩︎</a></p> </li> <li id="fn:2" role="doc-endnote"> <p>当然といえば当然ですが <a target="_blank" rel="nofollow noopener" href="https://github.com/jitterbit/get-changed-files">jitterbit/get-changed-files@v1</a> は <code>workflow_dispatch</code> でワークフローを手動実行した際や、<code>force push</code> 等でファイル差分を正確に特定できない操作には対応しておりませんので、その場合はエラーが発生します <a href="#fnref:2" class="footnote-backref" role="doc-backlink">↩︎</a></p> </li> </ol> </div> nikaera tag:crieit.net,2005:PublicArticle/16750 2021-03-16T01:03:30+09:00 2021-03-16T01:03:30+09:00 https://crieit.net/posts/playfab-api-call-limitation 📔 PlayFab の API 制限に引っかかった <h1 id="はじめに"><a href="#%E3%81%AF%E3%81%98%E3%82%81%E3%81%AB">はじめに</a></h1> <p><a target="_blank" rel="nofollow noopener" href="https://docs.microsoft.com/ja-jp/gaming/playfab/features/automation/cloudscript-af/">PlayFab で CloudFunction</a> を利用しているときに API 制限に引っかかってしまいました。負荷テストをした際に初めて気づいたのですが、公式ページにも言及が無かったため発覚が遅れてしまいました。そのため、PlayFab に依存していた機能を部分的に外す必要が出てきてしまい苦労しました。</p> <p>本記事では、上記のような事態に陥る方を減らすため、API 制限に気づくまでの軌跡を辿りながら、PlayFab の CloudFunction を利用する際の注意点について、記事として残しておきたいと思います。</p> <h1 id="PlayFab の API 制限に引っかかった要因"><a href="#PlayFab+%E3%81%AE+API+%E5%88%B6%E9%99%90%E3%81%AB%E5%BC%95%E3%81%A3%E3%81%8B%E3%81%8B%E3%81%A3%E3%81%9F%E8%A6%81%E5%9B%A0">PlayFab の API 制限に引っかかった要因</a></h1> <p>PlayFab の CloudFunction を利用すると、<strong>PlayFab 経由で独自 Web API を実行することが可能になります。また、CloudFunction 経由で独自 Web API を実行すると、PlayFab ユーザ情報が含まれたパラメタが含まれた状態でリクエストが飛んでくるため、その情報を利用することでサーバーサイドで PlayFab の操作を行うことが出来るようになり大変便利です。</strong></p> <p>そのため、あるプロジェクトでは PlayFab CloudFunction を Azure Function や AWS Lambda のような FaaS を使っている感じで利用しておりました。<em>そして、その利用の仕方は誤りであったことに後々気づきます...</em></p> <h2 id="負荷テストを実装するフェーズで CloudFunction を大量に叩いてみる"><a href="#%E8%B2%A0%E8%8D%B7%E3%83%86%E3%82%B9%E3%83%88%E3%82%92%E5%AE%9F%E8%A3%85%E3%81%99%E3%82%8B%E3%83%95%E3%82%A7%E3%83%BC%E3%82%BA%E3%81%A7+CloudFunction+%E3%82%92%E5%A4%A7%E9%87%8F%E3%81%AB%E5%8F%A9%E3%81%84%E3%81%A6%E3%81%BF%E3%82%8B">負荷テストを実装するフェーズで CloudFunction を大量に叩いてみる</a></h2> <p>PlayFab の CloudFunction を実行するにあたり利用した PlayFab の API は <a target="_blank" rel="nofollow noopener" href="https://docs.microsoft.com/en-us/rest/api/playfab/cloudscript/server-side-cloud-script/executefunction?view=playfab-rest"><code>Server-Side Cloud Script - Execute Function</code></a> というものになります。</p> <p>同接 2000 人想定で負荷テストのシナリオを実装することが求められていたため、その通りシンプルに 2000 件同時に <code>Server-Side Cloud Script - Execute Function</code> を実行するシナリオを Gatling で組んでみました。すると、<strong>何回やっても数十件以上は必ずエラーが発生していることが分かりました。</strong></p> <pre><code class="bash"># Gatling で負荷テストを実行した際に 50件失敗している様子 ================================================================================ ---- Global Information -------------------------------------------------------- > request count 2000 (OK=1950 KO=50 ) > min response time 320 (OK=320 KO=354 ) > max response time 14459 (OK=9723 KO=14459 ) > mean response time 998 (OK=934 KO=3485 ) > std deviation 1510 (OK=1304 KO=4310 ) > response time 50th percentile 545 (OK=543 KO=656 ) > response time 75th percentile 1085 (OK=1077 KO=7209 ) > response time 95th percentile 2243 (OK=2029 KO=10210 ) > response time 99th percentile 7947 (OK=7775 KO=14353 ) > mean requests/sec 100 (OK=97.5 KO=2.5 ) ---- Response Time Distribution ------------------------------------------------ > t < 800 ms 1393 ( 70%) > 800 ms < t < 1200 ms 367 ( 18%) > t > 1200 ms 190 ( 10%) > failed 50 ( 3%) ---- Errors -------------------------------------------------------------------- > status.find.is(200), but actually found 400 50 (100.0%) ================================================================================ </code></pre> <p>正直 2000件程度の API アクセスであれば、何の問題もなく負荷テストが通ると考えていたので、この結果には驚きました。<strong>原因は何なのか調べたところ、Azure Function で PlayFab ユーザ認証を行うために利用していた <a target="_blank" rel="nofollow noopener" href="https://docs.microsoft.com/en-us/rest/api/playfab/authentication/authentication/validateentitytoken?view=playfab-rest"><code>Authentication - Validate Entity Token</code></a> で 503 エラーが発生していることが分かりました。</strong></p> <p>少ない API 実行件数で負荷テストを実行する場合は問題ないのですが、<strong>件数が一定数超えたタイミングで 503 エラーが返却されるようになってしまいます。しかし、たまに同じ件数を実行しているはずなのにスムーズに全件 API 実行に成功することもありました。これは何らかのレートリミット等に引っかかっているのかも知れないということで調査したところ、次の事実が判明しました。</strong></p> <h2 id="CloudFunction は FaaS の用途には適さない"><a href="#CloudFunction+%E3%81%AF+FaaS+%E3%81%AE%E7%94%A8%E9%80%94%E3%81%AB%E3%81%AF%E9%81%A9%E3%81%95%E3%81%AA%E3%81%84">CloudFunction は FaaS の用途には適さない</a></h2> <p>どうやら PlayFab 公式フォーラムの <a target="_blank" rel="nofollow noopener" href="https://community.playfab.com/questions/30828/server-api-limitations.html">ある投稿</a> によると、<strong>PlayFab の Server API を呼び出す際は 10秒間に 1000回という制限があるようでした。</strong> そして、この制限を突破するには商用のための契約をした後にインスタンス割当に関する交渉をすることで可能になるかも知れないとのことでした。</p> <blockquote> <p>Servers are rate limited to 1,000 calls per 10 seconds. What jital is highlighting that the per-player rate should be no more than a few times a minute. A server can call at a higher rate, as it is calling for a lot of users, potentially. If you need a higher limit than 1,000 per 10 seconds, you'll want to talk to our sales team about getting on an Enterprise contract so that we can work with you on custom limits. There's an option on the Contact Us form on the main site to message them, if you want to go that route.</p> </blockquote> <p>つまり、<strong>普通に PlayFab を利用している限りはプランをアップグレードしようと制限に引っかかるということが分かりました。</strong> また、<a target="_blank" rel="nofollow noopener" href="https://community.playfab.com/comments/48674/view.html">今年 1月に投稿された内容</a> を見るに 10秒以内に 5000以上のユーザーがログイン/登録できたとあり、もう API の 10秒間に 1000回呼び出し制限は撤廃されたのかを聞いているユーザがいたのですが、まだ撤廃されていないと返信されていたので偶然だったようでした。</p> <p>ちなみに私も上記が気になったので、<strong>セッションごとにレートリミットのかかり方が変わるのか検証するために異なるユーザ情報を用いてリクエスト 2000件を並列に実行してみましたが、503 エラーは変わらず返却され続けていたので、少なくとも私の手元の環境では効果はなさそうでした。</strong></p> <blockquote> <p>No, the rate limits on the Client and Server API calls has not changed. However, the rate limits are currently enforced on a per-server basis. And since the service runs a great many servers for load balancing, it is possible to exceed those limits from time to time.</p> </blockquote> <p>原因の調査中 PlayFab は EC2 の us-west-2 リージョンでリクエストを受けていそうなことが分かったのですが、そのアクセス先のインスタンスがロードバランサによって分散されているため、レートリミットの制限がインスタンス先により、時と場合によって制限されるかどうかが決まってくるのかもしれないとのことでした。</p> <p>以上のことから、<strong>PlayFab の CloudFunction については Azure Function や AWS Lambda のような FaaS のような用途には使わず、あくまでもアプリケーションで補助的に利用するための独自スクリプトを動かす程度に留めて利用するのが正解なように感じました。</strong></p> <p><strong>PlayFab のユーザ認証情報である SessionTicket や EntityToken を利用することで、認証周りの実装部分を省くことが出来るかも知れないと思い期待していたのですが、それは別の BaaS を使うか IaaS で自前で作るのが良さそうでした。</strong></p> <h1 id="おわりに"><a href="#%E3%81%8A%E3%82%8F%E3%82%8A%E3%81%AB">おわりに</a></h1> <p>API の 10秒間に 1000回呼び出し制限については明示的にドキュメントに記載があるわけでもなかったため、気づくことが出来ずプロジェクト終盤で気づくという事故が起きてしまったのですが、私と似たような境遇に陥る人が少しでも減るようにと記事を書いてみました。</p> <p>とはいえ、少し調べれば出てくるような制限だったので純粋に調査不足だったなあと反省しました。。CloudFunction はとても便利ですが、利用する際は API の呼び出し制限等用法には十分お気をつけてご利用くださいませ。</p> <p>PlayFab が便利な BaaS であることに疑いの余地は無いので今後も利用すると思いますが、知見を貯めつつ効果的に使えるよう勉強していきたいと考えております。また何か知見を得たら随時ブログ記事に書き溜めていきたいと思います。</p> <h1 id="参考リンク"><a href="#%E5%8F%82%E8%80%83%E3%83%AA%E3%83%B3%E3%82%AF">参考リンク</a></h1> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://docs.microsoft.com/ja-jp/gaming/playfab/features/automation/cloudscript-af/">Azure 関数を使用した PlayFab CloudScript - PlayFab | Microsoft Docs</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://docs.microsoft.com/en-us/rest/api/playfab/cloudscript/server-side-cloud-script/executefunction?view=playfab-rest">Server-Side Cloud Script - Execute Function (PlayFab CloudScript) | Microsoft Docs</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://docs.microsoft.com/en-us/rest/api/playfab/authentication/authentication/validateentitytoken?view=playfab-rest">Authentication - Validate Entity Token (PlayFab Authentication) | Microsoft Docs</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://community.playfab.com/questions/30828/server-api-limitations.html">Server API limitations - Playfab Community</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://community.playfab.com/questions/30828/server-api-limitations.html?childToView=48674#comment-48674">Server API limitations - Playfab Community</a></li> </ul> nikaera tag:crieit.net,2005:PublicArticle/16749 2021-03-15T12:45:11+09:00 2021-03-15T12:45:11+09:00 https://crieit.net/posts/gatling-multiuser Gatling で複数ユーザ認証した情報を元に負荷テストする <h1 id="はじめに"><a href="#%E3%81%AF%E3%81%98%E3%82%81%E3%81%AB">はじめに</a></h1> <p>今までは <a target="_blank" rel="nofollow noopener" href="https://jmeter.apache.org/">JMeter</a> でしか負荷テストを行ったことなかったのですが、最近 PlayFab で CloudFunction の負荷テストを行う際に <a target="_blank" rel="nofollow noopener" href="https://gatling.io/">Gatling</a> を初めて利用しました。</p> <p>今回の負荷テストでは、各ユーザ毎のレートリミットの制限等も考慮した実利用時を想定した形で行うことが要求されたため、単一ユーザの認証情報を使い回すことは望ましくないと考えました。そこで、複数の認証済みユーザの情報を元に <a target="_blank" rel="nofollow noopener" href="https://docs.microsoft.com/ja-jp/gaming/playfab/features/automation/cloudscript-af/">PlayFab の CloudFunction</a> の負荷テストを実施したのですが、若干実装に苦戦したため手順について記事として残しておくことにしました。</p> <p>また、本記事では Gatling のセットアップから記載していますが、該当コードやその説明を早く見たいという方は <a href="#複数ユーザ認証を行うテストシナリオを実装する"><code>複数ユーザ認証を行うテストシナリオを実装する</code></a> 項目をご参照ください。</p> <h1 id="動作環境"><a href="#%E5%8B%95%E4%BD%9C%E7%92%B0%E5%A2%83">動作環境</a></h1> <ul> <li>macOS Big Sur</li> <li>Java OpenJDK 12.0.1 <ul> <li>未インストールの方は事前に <a target="_blank" rel="nofollow noopener" href="https://jdk.java.net/">公式サイトから</a> OpenJDK をインストールしてください</li> </ul></li> </ul> <h1 id="Gatling の環境を整える"><a href="#Gatling+%E3%81%AE%E7%92%B0%E5%A2%83%E3%82%92%E6%95%B4%E3%81%88%E3%82%8B">Gatling の環境を整える</a></h1> <p><strong>Gatling には 2 種類のセットアップ方法が用意されています。</strong> スタンドアローンなツールを直接公式サイトからダウンロードするか、<a target="_blank" rel="nofollow noopener" href="https://maven.apache.org/">Maven</a> や <a target="_blank" rel="nofollow noopener" href="https://www.scala-sbt.org/">sbt</a> といったツール経由でダウンロードするか選択できます。</p> <p>どちらの方法でセットアップするかについてですが、<strong>新規でテストケースを Gatling で書いていく用途だと前者になり、既存のプロジェクトに Gatling を取り込む用途だと後者になるかと存じます。</strong></p> <p>本記事では、前者のスタンドアローンなツールを直接公式サイトからダウンロードする方法で Gatling の環境をセットアップします。</p> <h2 id="公式サイトから Gatling をダウンロードする"><a href="#%E5%85%AC%E5%BC%8F%E3%82%B5%E3%82%A4%E3%83%88%E3%81%8B%E3%82%89+Gatling+%E3%82%92%E3%83%80%E3%82%A6%E3%83%B3%E3%83%AD%E3%83%BC%E3%83%89%E3%81%99%E3%82%8B">公式サイトから Gatling をダウンロードする</a></h2> <p><a target="_blank" rel="nofollow noopener" href="https://gatling.io/open-source/">Gatling のトップページ</a> に遷移して、ページを <code>2 Ways to use Gatling</code> の項目までスクロールした後、ダウンロードボタンをクリックします。</p> <p><img src="https://i.gyazo.com/0bbfb36fa7ea0704d4ebc50682f836ef.png" alt="スクリーンショット 2021-03-14 21.57.35.png" /><br /> <strong><code>DOWNLOAD GATLING'S BUNDLE</code> にある <code>DOWNLOAD NOW</code> ボタンをクリックする</strong></p> <p>ファイルダウンロード後はダウンロードした zip ファイルを適当なフォルダに展開して配置します。<br /> 早速ターミナルで展開したフォルダ内にある <code>./bin/gatling.sh</code> を実行して、正常にコマンドが実行できるか確認してみます。</p> <p><em>OS が Windows の場合は <code>./bin/gatling.bat</code> を実行します。</em></p> <pre><code class="bash">⊨ ./bin/gatling.sh ~/D/gatling-charts-highcharts-bundle-3.5.1 GATLING_HOME is set to /Users/nika/Desktop/gatling-charts-highcharts-bundle-3.5.1 Choose a simulation number: [0] computerdatabase.BasicSimulation [1] computerdatabase.advanced.AdvancedSimulationStep01 [2] computerdatabase.advanced.AdvancedSimulationStep02 [3] computerdatabase.advanced.AdvancedSimulationStep03 [4] computerdatabase.advanced.AdvancedSimulationStep04 [5] computerdatabase.advanced.AdvancedSimulationStep05 0 # 実行したいテストの番号を入力する、今回は適当に 0 を指定 Select run description (optional) # 実行するテストに関する説明文を入力する。何も入力する内容が無い or # 説明文の入力が完了したら Enter を入力して、実際にテストを実行する # 選択したテストの実行が開始する # (0 を入力したので computerdatabase.BasicSimulation が実行される) Simulation computerdatabase.BasicSimulation started... #... Simulation computerdatabase.BasicSimulation completed in 26 seconds Parsing log file(s)... Parsing log file(s) done Generating reports... # テストの実行が無事完了すると、結果が表示されレポートファイルが生成される。 # レポート生成先のファイルパスは実行結果の末尾に表示される。 # レポートファイルは HTML で生成されるため、適当なブラウザで開くことで内容を確認することが出来る。 ================================================================================ ---- Global Information -------------------------------------------------------- > request count 13 (OK=13 KO=0 ) > min response time 230 (OK=230 KO=- ) > max response time 483 (OK=483 KO=- ) > mean response time 324 (OK=324 KO=- ) > std deviation 98 (OK=98 KO=- ) > response time 50th percentile 259 (OK=259 KO=- ) > response time 75th percentile 415 (OK=415 KO=- ) > response time 95th percentile 476 (OK=476 KO=- ) > response time 99th percentile 482 (OK=482 KO=- ) > mean requests/sec 0.481 (OK=0.481 KO=- ) ---- Response Time Distribution ------------------------------------------------ > t < 800 ms 13 (100%) > 800 ms < t < 1200 ms 0 ( 0%) > t > 1200 ms 0 ( 0%) > failed 0 ( 0%) ================================================================================ Reports generated in 0s. Please open the following file: /Users/nika/Desktop/gatling-charts-highcharts-bundle-3.5.1/results/basicsimulation-20210314133324259/index.html </code></pre> <p><code>./bin/gatling.sh</code> を実行した後、上記のような出力が確認できれば、問題なくスタンドアローン版の Gatling 環境のセットアップが完了しています。</p> <p>また、<strong>負荷テストのレポートを確認したい場合は、出力結果にある <code>Please open the following file: <レポートのファイルパス></code> に記載されているファイルをブラウザで開きます。</strong><br /> html 拡張子を開くアプリのデフォルトが何らかのブラウザになっているのであれば、ターミナルから <code>open <レポートのファイルパス></code> を実行するのでも構いません。</p> <h1 id="Gatling でテストシナリオを実装する"><a href="#Gatling+%E3%81%A7%E3%83%86%E3%82%B9%E3%83%88%E3%82%B7%E3%83%8A%E3%83%AA%E3%82%AA%E3%82%92%E5%AE%9F%E8%A3%85%E3%81%99%E3%82%8B">Gatling でテストシナリオを実装する</a></h1> <p><strong>Gatling のテストシナリオを書く場所は <code>./user-files/simulations</code> フォルダ内になります。</strong> テストシナリオを Scala で書いた後、ファイルを <code>./user-files/simulations</code> フォルダに配置します。すると、<code>./bin/gatling.sh</code> を実行した際の実行するテストシナリオのリストに出てくるようになります。</p> <h2 id="簡単なテストシナリオを試しに書いてみる"><a href="#%E7%B0%A1%E5%8D%98%E3%81%AA%E3%83%86%E3%82%B9%E3%83%88%E3%82%B7%E3%83%8A%E3%83%AA%E3%82%AA%E3%82%92%E8%A9%A6%E3%81%97%E3%81%AB%E6%9B%B8%E3%81%84%E3%81%A6%E3%81%BF%E3%82%8B">簡単なテストシナリオを試しに書いてみる</a></h2> <p>まずは試しに私のブログに対してのアクセス負荷を計測するためのテストを実装していきます。<code>./user-files/simulations</code> フォルダ内に <code>nikaera.com</code> フォルダを作成し、<code>AccessSimulation.scala</code> という負荷テストのシナリオファイルを作成します。</p> <pre><code class="scala">// ./user-files/simulations/nikaera.com/AccessSimulation.scala package com.nikaera import scala.concurrent.duration._ import io.gatling.core.Predef._ import io.gatling.http.Predef._ import scala.collection.mutable.ListBuffer import io.gatling.core.structure.PopulationBuilder // 1. Simulation クラスを継承してテストシナリオを実行するクラスを定義する class AccessSimulation extends Simulation { // 2. HTTP アクセスする際の設定値を入力する。 // 今回はアクセス先のベース URL を定義するための baseUrl のみ指定 val httpConf = http .baseUrl("https://nikaera.com") // 3. Scala の ListBuffer を用いて複数シナリオを格納する scenarios 変数を用意する val scenarios = new ListBuffer[PopulationBuilder]() // 4. httpConf で設定した情報を元に / (https://nikaera.com) 及び // /tech/ (https://nikaera.com/tech/) へ同時 10 アクセスするのを、 // 5秒毎に 3回実行するシナリオを作成して、scenarios 変数に格納する val pollingApiScn = scenario("Polling Simulation") .exec( http("Top Page") .get("/") .check(status.is(200)) ) .exec( http("Tech Page") .get("/tech/") .check(status.is(200)) ) scenarios += pollingApiScn .inject( atOnceUsers(10), nothingFor(5 seconds), atOnceUsers(10), nothingFor(5 seconds), atOnceUsers(10) ) .protocols(httpConf); // 5. 4. で定義したシナリオを実行して https://nikaera.com のアクセス負荷を計測する setUp( scenarios(0) ) } </code></pre> <p>再度 <code>./bin/gatling.sh</code> を実行して、実際に負荷テストを行ってみます。</p> <pre><code class="bash">⊨ ./bin/gatling.sh ~/D/gatling-charts-highcharts-bundle-3.5.1 GATLING_HOME is set to /Users/nika/Desktop/gatling-charts-highcharts-bundle-3.5.1 Choose a simulation number: [0] com.nikaera.AccessSimulation [1] computerdatabase.BasicSimulation [2] computerdatabase.advanced.AdvancedSimulationStep01 [3] computerdatabase.advanced.AdvancedSimulationStep02 [4] computerdatabase.advanced.AdvancedSimulationStep03 [5] computerdatabase.advanced.AdvancedSimulationStep04 [6] computerdatabase.advanced.AdvancedSimulationStep05 0 # 作成したテストシナリオである com.nikaera.AccessSimulation を選択して実行します。 Select run description (optional) Simulation com.nikaera.AccessSimulation started... #... Simulation com.nikaera.AccessSimulation completed in 10 seconds Parsing log file(s)... Parsing log file(s) done Generating reports... ================================================================================ ---- Global Information -------------------------------------------------------- > request count 60 (OK=60 KO=0 ) > min response time 11 (OK=11 KO=- ) > max response time 372 (OK=372 KO=- ) > mean response time 104 (OK=104 KO=- ) > std deviation 99 (OK=99 KO=- ) > response time 50th percentile 53 (OK=53 KO=- ) > response time 75th percentile 212 (OK=212 KO=- ) > response time 95th percentile 259 (OK=259 KO=- ) > response time 99th percentile 307 (OK=307 KO=- ) > mean requests/sec 5.455 (OK=5.455 KO=- ) ---- Response Time Distribution ------------------------------------------------ > t < 800 ms 60 (100%) > 800 ms < t < 1200 ms 0 ( 0%) > t > 1200 ms 0 ( 0%) > failed 0 ( 0%) ================================================================================ Reports generated in 0s. Please open the following file: /Users/nika/Desktop/gatling-charts-highcharts-bundle-3.5.1/results/accesssimulation-20210314144205502/index.html </code></pre> <p>テストシナリオの実行に成功していることが確認できたら、複数ユーザ認証した情報を元に行うテストシナリオを書いていきます。</p> <h2 id="複数ユーザ認証を行うテストシナリオを実装する"><a href="#%E8%A4%87%E6%95%B0%E3%83%A6%E3%83%BC%E3%82%B6%E8%AA%8D%E8%A8%BC%E3%82%92%E8%A1%8C%E3%81%86%E3%83%86%E3%82%B9%E3%83%88%E3%82%B7%E3%83%8A%E3%83%AA%E3%82%AA%E3%82%92%E5%AE%9F%E8%A3%85%E3%81%99%E3%82%8B">複数ユーザ認証を行うテストシナリオを実装する</a></h2> <p>複数ユーザ認証するテストシナリオを実装していきます。PlayFab で複数ユーザの認証情報を元に CloudFunction の負荷テストを行うことを想定しています。テストシナリオを作成するにあたり、<code>./user-files/simulations/</code> フォルダに新たに <code>playfab.com</code> フォルダを作成して、その中に <code>TestCloudFunctionSimulation.scala</code> ファイルを生成します。</p> <p><em>下記コードの <code>setUp</code> でシナリオを複数指定する箇所についてですが、より良いやり方があれば是非ご教授いただけますと幸いです...</em></p> <pre><code class="scala">// ./user-files/simulations/playfab.com/TestCloudFunctionSimulation.scala package com.playfab import java.io._ import java.net.{HttpURLConnection, URL} import io.gatling.core.Predef._ import io.gatling.http.Predef._ import scala.concurrent.duration._ import scala.util.Random import scala.util.parsing.json.JSON import scala.collection.mutable.ListBuffer import io.gatling.core.structure.PopulationBuilder class TestCloudFunctionSimulation extends Simulation { // PlayFab に登録されたユーザの ID 群 val playfabUsers = Array[String]( "user1", "user2", "user3", "user4", "user5", "user6", "user7", "user8", "user9", "user10" ) // PlayFab の TitleId 及び Secret を変数に保持しておく val playfabId = "XXXXX" val playfabSecret = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" val playfabApiUrl = s"https://${playfabId}.playfabapi.com"  // PlayFab の Login With Server Custom Id を利用して、 // ユーザの認証情報を取得するために用いる関数 def getPlayfabAuth(serverCustomId: String): Option[Any] = { val url = new URL( s"${playfabApiUrl}/Server/LoginWithServerCustomId" ) val con = url.openConnection().asInstanceOf[HttpURLConnection] HttpURLConnection.setFollowRedirects(false) con.setRequestMethod("POST") con.setRequestProperty("Content-Type", "application/json") con.setRequestProperty("X-SecretKey", playfabSecret) con.setDoOutput(true) val out = new OutputStreamWriter(con.getOutputStream(), "UTF-8") out.write(s"""{ "CreateAccount": false, "ServerCustomId": "${serverCustomId}" }""") out.flush() out.close() con.connect() val in = con.getInputStream val br = new BufferedReader(new InputStreamReader(in, "UTF-8")); val json = br.readLine() in.close() con.disconnect() return JSON.parseFull(json) } val httpConf = http .baseUrl(playfabApiUrl) .header("Content-Type", "application/json") // CloudFunction の Request Body を作成するために利用する関数 def cloudScriptDto(funcName: String, funcArgs: String): String = { return s"""{ "FunctionName": "${funcName}", "FunctionParameter": ${funcArgs} }""" } val scenarios: ListBuffer[PopulationBuilder] = new ListBuffer[PopulationBuilder]() playfabUsers.foreach(userId => { // playfabUsers で指定したユーザ ID 情報を元に、 // PlayFab の認証情報を取得利用することで、 val playfab = this.getPlayfabAuth(userId).get val map: Map[String, Option[Any]] = playfab.asInstanceOf[Map[String, Option[Any]]]; // 愚直に JSON オブジェクトのパースを行い、必要な情報を変数に取り出す。 val data = map.get("data").get.asInstanceOf[Map[String, Option[Any]]]; val entityTokenInfo = data.get("EntityToken").get.asInstanceOf[Map[String, Option[Any]]]; val entityToken = entityTokenInfo.get("EntityToken").get.asInstanceOf[String]; val entity = entityTokenInfo.get("Entity").get.asInstanceOf[Map[String, Option[Any]]]; val entityId = entity.get("Id").get.asInstanceOf[String]; // Test2 API については CloudFunction 実行時のパラメタを、 // ランダム指定したいため、Random を用いてパラメタを散らすようにする val rand = new Random val values = Array( "value1", "value2", "value3", "value4", "value5" ) val values_length = values.length // アクセス負荷を調査したい CloudFunction API 群を実行する。 val pollingApiScn = scenario(s"PollingSimulation: ${entityId}") .exec( http("Test1 Api") .post("/CloudScript/ExecuteFunction") .header("X-EntityToken", entityToken) .body(StringBody(cloudScriptDto("Test1", null))) .check(status.is(200)) ) .exec( http("Test2 Api") .post("/CloudScript/ExecuteFunction") .header("X-EntityToken", entityToken) .body( StringBody( cloudScriptDto( "Test2", s"""{"value": "${values(rand.nextInt(values_length))}"}""" ) ) ) .check(status.is(200)) ); // 1 ユーザあたり 3秒毎に 100回ずつ API 群を実行した際の // 負荷テストのシナリオを scenarios 変数に格納する scenarios += pollingApiScn .inject( atOnceUsers(100), nothingFor(10 seconds), atOnceUsers(100), nothingFor(10 seconds), atOnceUsers(100) ) .protocols(httpConf); }); // scenarios 変数に格納されたテストシナリオを並列実行する setUp( scenarios(0), scenarios(1), scenarios(2), scenarios(3), scenarios(4), scenarios(5), scenarios(6), scenarios(7), scenarios(8), scenarios(9) ) } </code></pre> <p>ザッとインラインコメントで説明を書きましたが、重要な点についてのみ補足します。<br /> <code>def getPlayfabAuth</code> は PlayFab 認証するための関数となっていますが、<strong>適宜認証に用いるサービス毎で関数の内容を変更することで、他サービスで認証するための関数として利用可能です。</strong><br /> また <code>playfabUsers.foreach</code> 内で各ユーザが実行するテストシナリオを生成しつつ、それらを <code>scenarios</code> 変数に保持しています。そうすることで、最後に <code>setUp</code> 関数でシナリオをまとめてセットできるようになります。</p> <p><em><code>playfabUsers.foreach</code> で値を指定するのではなく <a target="_blank" rel="nofollow noopener" href="http://www.ajisaba.net/develop/gatling/test_case_csv_feeder.html">CSV でテストに与えるフィードデータを定義する</a> する方法もあります。認証部分も含めてテストシナリオを書きたい場合にも便利に利用できます。またアカウント情報を CSV ファイルに定義しておくと、テストユーザの情報を新規追加したい場合で管理が楽になります。</em></p> <p>上記テストシナリオのソースコードを応用することで、様々なサービスで複数ユーザ認証した情報を元に負荷テストを行うためのシナリオを作成することが可能となります。</p> <h1 id="おわりに"><a href="#%E3%81%8A%E3%82%8F%E3%82%8A%E3%81%AB">おわりに</a></h1> <p>今回は Gatling で複数ユーザ認証した情報を元に負荷テストする手順について書きました。</p> <p><strong>Gatling で生成されるレポートは見やすく、改善点を洗い出してコードの改善作業をするのにとても有用でした。</strong> また、JMeter と比べて動作が軽いため気軽に実行しやすく、テストシナリオが全てコードで管理されているためシナリオの改変も素早く行うことが出来ました。</p> <p>個人的にはテストシナリオを全てコードで記述できる Gatling が気に入ったので今後も有効活用していきたいと感じました。ただ、<strong>Gatling 以外にも Python で書ける <a target="_blank" rel="nofollow noopener" href="https://github.com/locustio/locust">locust</a> や JavaScript で書ける <a target="_blank" rel="nofollow noopener" href="https://github.com/loadimpact/k6">k6</a> など、他にも気になるツールがいくつかあったので今後試してみたいなと考えています。</strong></p> <p>勝手に負荷テストは JMeter 一択だと思っていたのですが、負荷テストツールには色々あるようなのでプロジェクトや自分にあったものを選定していけるよう随時キャッチアップしていきたいと感じました。</p> <h1 id="参考リンク"><a href="#%E5%8F%82%E8%80%83%E3%83%AA%E3%83%B3%E3%82%AF">参考リンク</a></h1> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://jmeter.apache.org/">Apache JMeter - Apache JMeter™</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://gatling.io/">Gatling Open-Source Load Testing – For DevOps and CI/CD</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://docs.microsoft.com/ja-jp/gaming/playfab/features/automation/cloudscript-af/">Azure 関数を使用した PlayFab CloudScript - PlayFab | Microsoft Docs</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://jdk.java.net/">JDK Builds from Oracle</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://maven.apache.org/">Maven – Welcome to Apache Maven</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://www.scala-sbt.org/">sbt - The interactive build tool</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://gatling.io/open-source/">Open Source – Gatling Open-Source Load Testing</a></li> <li><a target="_blank" rel="nofollow noopener" href="http://www.ajisaba.net/develop/gatling/test_case_csv_feeder.html">負荷試験ツール・Gatling・CSV ファイルと Feeder を使ったテストケース</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://locust.io/">Locust - A modern load testing framework</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://k6.io/">Load testing for engineering teams | k6</a></li> </ul> nikaera tag:crieit.net,2005:PublicArticle/16723 2021-03-08T01:51:05+09:00 2021-03-08T01:51:05+09:00 https://crieit.net/posts/jest-private-readonly-mock 📝 Jest で private readonly な値をモックする方法 <h1 id="はじめに"><a href="#%E3%81%AF%E3%81%98%E3%82%81%E3%81%AB">はじめに</a></h1> <p>Jest でクラスの <code>private readonly</code> な変数を差し替えたい時に若干引っかかったのでメモっておきます。タイトルでは Jest とありますが、本記事の内容は JavaScript でモックする際の有効な手法の 1 つとして利用することが可能です。</p> <h1 id="Object.defineProperty を利用して値を差し替える"><a href="#Object.defineProperty+%E3%82%92%E5%88%A9%E7%94%A8%E3%81%97%E3%81%A6%E5%80%A4%E3%82%92%E5%B7%AE%E3%81%97%E6%9B%BF%E3%81%88%E3%82%8B">Object.defineProperty を利用して値を差し替える</a></h1> <p>結論から言うと変数を差し替えたい場合は下記のような記述になります。</p> <pre><code class="javascript">const mockValue = ""; Object.defineProperty(service, "privateReadOnlyValue", { value: mockValue, }); </code></pre> <p>ちなみに関数を差し替えたい場合は下記のような記述になります。</p> <pre><code class="javascript">Object.defineProperty(service, "privateSumFunction", { value: jest.fn((a, b) => a + b), }); </code></pre> <p>各種テストケースで使いまわしているインスタンスの <code>private readonly</code> な変数をモックした場合、値をリストアしたいケースも出てきました。その場合の記述としては、下記が有効でした。</p> <pre><code class="javascript">// tmpService 変数に service インスタンスを clone して利用する const tmpService = Object.create(service); Object.defineProperty(tmpService, "privateReadOnlyValue", { value: "", }); </code></pre> <h1 id="おわりに"><a href="#%E3%81%8A%E3%82%8F%E3%82%8A%E3%81%AB">おわりに</a></h1> <p><code>Object.defineProperty</code> と <code>Object.create</code> を駆使すれば大体のケースでは事足りそうです。</p> <h1 id="参考リンク"><a href="#%E5%8F%82%E8%80%83%E3%83%AA%E3%83%B3%E3%82%AF">参考リンク</a></h1> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Object/create">Object.create() - JavaScript | MDN</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://github.com/facebook/jest/issues/2227#issuecomment-265005782">Mocking read-only properties for a class · Issue #2227 · facebook/jest</a></li> </ul> nikaera tag:crieit.net,2005:PublicArticle/16645 2021-01-23T21:12:04+09:00 2021-01-23T21:12:04+09:00 https://crieit.net/posts/aws-lightsail-containers-rust-actix-web AWS Lightsail Containers に Actix web をデプロイする <h1 id="はじめに"><a href="#%E3%81%AF%E3%81%98%E3%82%81%E3%81%AB">はじめに</a></h1> <p><a target="_blank" rel="nofollow noopener" href="https://github.com/actix/actix-web">Actix web</a> で Web アプリケーションを作ったのですが、技術勉強も兼ねていたので、デプロイ先も今まで試したことがないものを試そうとしていました。そこで、日頃業務でも AWS を利用しているということもあり、去年末に発表された <a target="_blank" rel="nofollow noopener" href="https://aws.amazon.com/jp/about-aws/whats-new/2020/11/announcing-amazon-lightsail-containers/">AWS Lightsail Containers</a> をデプロイ先に採用しました。</p> <p>AWS Lightsail Containers へのデプロイ自体は非常に簡単でした。また、デプロイにあたり Rust の Docker イメージ作成のやり方も学べました。今回はそのあたりの手順をまとめる形で記事として書き残しておくことにしました。</p> <h1 id="Actix web の Docker イメージを作成する"><a href="#Actix+web+%E3%81%AE+Docker+%E3%82%A4%E3%83%A1%E3%83%BC%E3%82%B8%E3%82%92%E4%BD%9C%E6%88%90%E3%81%99%E3%82%8B">Actix web の Docker イメージを作成する</a></h1> <p>開発したアプリケーションでは React でフロントエンド開発をしていて、ビルドしたものを Actix web の public フォルダに配置する形で公開しています。そのため、下記の Dockerfile ではマルチステージビルドを利用しておりますが、本質的には <code>FROM rust:1.49</code> 以降の記述が Actix web に関するものとなります。</p> <pre><code class="Dockerfile"># React ビルド用のイメージ FROM node:14.15.4-alpine3.10 as client_builder ARG REACT_APP_API_URL ARG REACT_APP_GYAZO_AUTH_URL ARG REACT_APP_GA_UNIVERSAL_ID WORKDIR /client COPY ./client/package*.json . RUN yarn install ADD ./client . RUN yarn build # Actix web ビルド用のイメージ FROM rust:1.49 # Actix web にアクセスするためのポートを公開する EXPOSE 8080 # Actix web プロジェクトのフォルダをイメージに追加する WORKDIR /server ADD ./server . # プロジェクトフォルダ内で `cargo install` してビルドを生成する RUN cargo install --path . # 不要になったファイル群を削除する RUN ls | grep -v -E 'templates' | xargs rm -r # React ビルド用のイメージでビルドした内容を Actix web ビルド用イメージに追加する COPY --from=client_builder /client/build ./build RUN mkdir tmp # `cargo install` コマンドで生成したビルドを実行して Actix web を起動する # 下記のコマンド名称は Cargo.toml 内の [package.name] に準ずる CMD ["bloggimg-server"] </code></pre> <p>また、<strong>Docker ビルド時のオプション管理を楽にするため、Docker Compose を利用しました。単一の Docker イメージをビルドする際にも利用しておくことで、後々コンテナを追加して連携させたいときにも即座に対応できたりでオススメです。</strong></p> <pre><code class="yml"># docker-compose.yml # context に Actix web プロジェクトのパスを指定する # args に Docker ビルド時に利用したい ARGS の値を環境変数で設定する # image に Docker の &lt;イメージ名:タグ名&gt; を指定する (今回は Docker Hub にデプロイする想定) # env_file に開発/動作検証時に利用したい dotenv ファイルを指定する # ports にポートマッピングの設定を書く version: '3.8' services: app: build: context: ./ args: - REACT_APP_API_URL=${REACT_APP_API_URL} - REACT_APP_GYAZO_AUTH_URL=${REACT_APP_GYAZO_AUTH_URL} - REACT_APP_GA_UNIVERSAL_ID=${REACT_APP_GA_UNIVERSAL_ID} image: n1kaera/bloggimg:v1.0.0 env_file: - ./server/.env ports: - 8080:8080 </code></pre> <p>上記を自分の Actix web プロジェクトに応じて改変し <code>docker-compose up</code> して動作検証します。動作検証ができ次第、<code>docker-compose build</code> を実行して Docker イメージをビルドします。ビルドに成功したら次は Docker Hub にイメージを push します。</p> <h1 id="Docker Hub にビルドしたイメージを push する"><a href="#Docker+Hub+%E3%81%AB%E3%83%93%E3%83%AB%E3%83%89%E3%81%97%E3%81%9F%E3%82%A4%E3%83%A1%E3%83%BC%E3%82%B8%E3%82%92+push+%E3%81%99%E3%82%8B">Docker Hub にビルドしたイメージを push する</a></h1> <p>今回は AWS Lightsail Containers で使用するイメージの管理に <a target="_blank" rel="nofollow noopener" href="https://hub.docker.com/">Docker Hub</a> を利用します。Docker Hub へ push する前に <code>docker login --username=<Docker Hub のユーザ名></code> コマンドで Docker Hub へのログインを済ませておきます。</p> <p>その後 <code>docker-compose push</code> コマンドで Docker イメージを Docker Hub に push します。</p> <p><img src="https://i.gyazo.com/06ce4ca43a26c73c227b9eb768f65685.png" alt="スクリーンショット 2021-01-23 20.37.39.png" /><br /> <strong>Docker Hub のページから、正常に Docker イメージが push できていそうか確認する</strong></p> <p>Docker イメージの push が成功していることを確認できたら、残りは AWS Console 上での作業になります。</p> <h1 id="AWS Console から Lightsail Containers Service を作成する"><a href="#AWS+Console+%E3%81%8B%E3%82%89+Lightsail+Containers+Service+%E3%82%92%E4%BD%9C%E6%88%90%E3%81%99%E3%82%8B">AWS Console から Lightsail Containers Service を作成する</a></h1> <p>AWS Console にログイン後、<a target="_blank" rel="nofollow noopener" href="https://lightsail.aws.amazon.com/ls/webapp/home/containers">Lightsail サービス</a> を選択して Lightsail サービスのトップページへ遷移します。遷移したら Containers タブを選択し、<code>Create container services</code> ボタンから Container Service を作成します。</p> <p><img src="https://i.gyazo.com/42a8e6fa6224a6a52212cdb7461a8515.png" alt="スクリーンショット 2021-01-23 18.55.16.png" /><br /> <strong>AWS Console へログイン後 Lightsail のページに遷移して、Containers タブを選択する</strong></p> <p><img src="https://i.gyazo.com/25c1b55863c4d77249d3105ba7a97afd.png" alt="スクリーンショット 2021-01-23 18.57.06.png" /><br /> <strong>Containers タブを選択すると出てくる、<code>Create container services</code> ボタンをクリックする</strong></p> <p><code>Create container services</code> ボタンをクリックした遷移先の画面で、リージョンやキャパシティ (Micro であれば 3ヶ月間のみ無料で利用可能) 等を選択して、名称を入力します。今回は最初にデプロイのための準備をすでに済ませているので、Container Service を作成するついでにデプロイ設定も行います。</p> <p>デプロイ設定は <code>Set up your first deployment</code> の項目から行うことが可能です。</p> <p><img src="https://i.gyazo.com/e8e4ab4e3b29afaf6298f7eb75355c34.png" alt="スクリーンショット 2021-01-23 19.07.53.png" /><br /> <strong><code>Set up deployment</code> の部分をクリックして、デプロイの設定項目を表示する</strong></p> <p><img src="https://i.gyazo.com/da0fd3ce440f441082a367a0dfe350b6.png" alt="スクリーンショット 2021-01-23 19.12.14.png" /><br /> <strong>Docker Hub イメージを利用してデプロイする際に必要な設定項目を入力する</strong></p> <p><img src="https://i.gyazo.com/eefec162ec66adab937d5139f70763cc.png" alt="スクリーンショット 2021-01-23 19.16.16.png" /><br /> <strong>コンテナのヘルスチェックのための情報を入力する</strong></p> <p><img src="https://i.gyazo.com/0c532eb818754d554a61028f6a177dde.png" alt="スクリーンショット 2021-01-23 19.19.17.png" /><br /> <strong>すべての情報入力が完了したら <code>Create container services</code> ボタンをクリックする</strong></p> <p><img src="https://i.gyazo.com/102978a025c8babbba40886e1b551ae6.png" alt="スクリーンショット 2021-01-23 19.22.48.png" /><br /> <strong>遷移後の画面下部の <code>Deployment versions</code> からデプロイ状況の確認が行える</strong></p> <p><img src="https://i.gyazo.com/eaaf0b1f1d2b2d37807d49764cafa681.png" alt="スクリーンショット 2021-01-23 19.39.55.png" /><br /> <strong>正常にデプロイできていれば <code>Deployment versions</code> の項目が Active になる</strong></p> <p>デプロイが完了したら <code>Public domain</code> が発行されているはずなので、正常にアクセスして Web アプリケーションが利用できそうか確認します。<code>Public domain</code> は該当する Container Service のトップページから確認できます。</p> <p><img src="https://i.gyazo.com/9ee36e7f1018c47a9c12e51ebe6df6d0.png" alt="スクリーンショット 2021-01-23 19.48.23.png" /><br /> <strong>AWS Lightsail Containers のトップページにある <code>Public domain</code> から動作検証する</strong></p> <p><img src="https://i.gyazo.com/a381f52745c002ce47addf7721b7ec0b.png" alt="スクリーンショット 2021-01-23 19.51.27.png" /><br /> <strong>一通りの動作検証を行い、正常にデプロイできていそうか確認する</strong></p> <p>これで作業は完了です。新しい Docker イメージでデプロイし直したい場合は、<code>Deployments</code> タブの <code>Modify your deployment</code> リンクをクリックすれば可能です。</p> <h1 id="(おまけ) 独自ドメインで Container Service へアクセス可能にする"><a href="#%28%E3%81%8A%E3%81%BE%E3%81%91%29+%E7%8B%AC%E8%87%AA%E3%83%89%E3%83%A1%E3%82%A4%E3%83%B3%E3%81%A7+Container+Service+%E3%81%B8%E3%82%A2%E3%82%AF%E3%82%BB%E3%82%B9%E5%8F%AF%E8%83%BD%E3%81%AB%E3%81%99%E3%82%8B">(おまけ) 独自ドメインで Container Service へアクセス可能にする</a></h1> <p>AWS Lightsail Containers では独自ドメインの紐付け及び、HTTPS 化も簡単に設定できます。<code>Custom domains</code> タブを選択した後、画面下部にある <code>Create certificate</code> リンクをクリックすることで設定画面を表示します。</p> <p><img src="https://i.gyazo.com/93019714418209872be82c396931beee.png" alt="スクリーンショット 2021-01-23 20.07.22.png" /><br /> <strong><code>Custom domain</code> タブをクリックしてから、<code>Create certificate</code> リンクをクリックする</strong></p> <p><img src="https://i.gyazo.com/b6cc81261f5d3bec4b236e04c0409372.png" alt="スクリーンショット 2021-01-23 20.10.22.png" /><br /> <strong>各種設定項目の入力が完了したら <code>Create</code> ボタンをクリックする</strong></p> <p><img src="https://i.gyazo.com/d8075a0e1eedeb1e518e7f0ae22554a1.png" alt="スクリーンショット 2021-01-23 20.16.02.png" /><br /> <strong>ドメイン検証のために、CNAME レコードの設定を求められるので各自で設定作業を行う</strong></p> <p><img src="https://i.gyazo.com/3aa4608c22ab83193d75b3e968d47b77.png" alt="スクリーンショット 2021-01-23 20.20.26.png" /><br /> <strong>正常に CNAME レコードを設定した後、しばらく経つと Status が Valid になる</strong></p> <p>上記まで確認したら、<code>Create certificate</code> で設定したドメインの CNAME レコードに <code>Public domain</code> の値を設定しておきます。設定内容が反映され次第、独自ドメインへアクセスすることで HTTPS 経由で Container Service へアクセスできるようになります。</p> <p><img src="https://i.gyazo.com/21d69cbc2370b588e01cfae43afa50ca.png" alt="スクリーンショット 2021-01-23 20.27.26.png" /><br /> <strong><code>Custom domains</code> で Container Service で起動しているサービスにアクセスできることを確認する</strong></p> <h1 id="おわりに"><a href="#%E3%81%8A%E3%82%8F%E3%82%8A%E3%81%AB">おわりに</a></h1> <p>AWS Lightsail Containers を利用して Actix web プロジェクトをデプロイする手順について簡単にまとめてみました。便利ではあるものの、個人開発で利用する分には価格面及び性能面で Lightsail Instance のほうが良いなと現時点では感じてしまいました。</p> <p>しかし、日本リージョンが用意されていたりロードバランサーを備えていたり、簡単にスケールさせやすくかつ定額で利用可能なサービスであるというメリットを活かせる場面があれば有効活用できそうだなと感じました。</p> <h1 id="参考リンク"><a href="#%E5%8F%82%E8%80%83%E3%83%AA%E3%83%B3%E3%82%AF">参考リンク</a></h1> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://aws.amazon.com/jp/blogs/news/lightsail-containers-an-easy-way-to-run-your-containers-in-the-cloud/">Lightsail コンテナ: クラウドでコンテナを実行する簡単な方法 | Amazon Web Services ブログ</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://hub.docker.com/_/rust">rust - Docker Hub</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://lightsail.aws.amazon.com/ls/docs/en_us/articles/amazon-lightsail-enabling-distribution-custom-domains">Enabling custom domains for your Amazon Lightsail distributions | Lightsail Documentation</a></li> </ul> nikaera tag:crieit.net,2005:PublicArticle/16644 2021-01-23T14:33:35+09:00 2021-01-23T14:33:35+09:00 https://crieit.net/posts/cookie-rust-actix-web Actix web で HttpOnly な Cookie を設定する <h1 id="はじめに"><a href="#%E3%81%AF%E3%81%98%E3%82%81%E3%81%AB">はじめに</a></h1> <p>最近 Rust を勉強するため、<a target="_blank" rel="nofollow noopener" href="https://github.com/actix/actix-web">Actix web</a> で <a target="_blank" rel="nofollow noopener" href="https://github.com/nikaera/bloggimg">Bloggimg</a> という Web アプリケーションを作りました。その際、セッション管理のために Cookie を利用したのですが、その際の手順及び設定方法についてまとめておきます。</p> <p>本記事では Rust や Actix web のインストール方法については説明しません。Mac であれば <code>brew install rustup</code> して <code>rustup-init</code> した後、<code>PATH</code> に <code>$HOME/.cargo/bin</code> を追加するだけで大丈夫なはずです。詳細なインストール手順については <a target="_blank" rel="nofollow noopener" href="https://www.rust-lang.org/tools/install">公式サイト</a> をご参照ください。</p> <p>開発環境については <a target="_blank" rel="nofollow noopener" href="https://marketplace.visualstudio.com/items?itemName=rust-lang.rust">VSCode の Rust Plugin</a> がオススメです。Rustup で Rust をインストールしている場合、設定から Rustup の PATH を <code>$HOME/.cargo/bin/rustup</code> にするだけで利用可能です。設定手順の詳細は<a target="_blank" rel="nofollow noopener" href="https://takoyaking.hatenablog.com/entry/2020/01/05/180000">こちら</a>をご参照ください。</p> <h1 id="動作環境"><a href="#%E5%8B%95%E4%BD%9C%E7%92%B0%E5%A2%83">動作環境</a></h1> <ul> <li>Mac mini (M1, 2020) <ul> <li>Rust 1.49</li> <li>Actix web 3</li> </ul></li> </ul> <h1 id="Actix web で Cookie をセットする"><a href="#Actix+web+%E3%81%A7+Cookie+%E3%82%92%E3%82%BB%E3%83%83%E3%83%88%E3%81%99%E3%82%8B">Actix web で Cookie をセットする</a></h1> <p>サーバー側で Cookie を設定するため、HTTP レスポンスヘッダーに <a target="_blank" rel="nofollow noopener" href="https://developer.mozilla.org/ja/docs/Web/HTTP/Headers/Set-Cookie">Set-Cookie</a> を含める形でセッション情報をクライアントへ渡します。その際、最低でも Cookie の属性に <code>HttpOnly</code> と <code>Secure</code>、<code>SameSite=Strict</code> は設定します。実際の Cookie を設定するための Actix web でのサンプルコードは下記になります。</p> <pre><code class="rust">use std::env; use actix_web::{App, HttpServer}; use actix_web::cookie::{Cookie, SameSite}; use actix_web::{get, web, Error, HttpRequest, HttpResponse}; use serde::{Deserialize}; /// Cookie に設定するキー /// 今回は cookie_test をキーとして使用する /// const KEY: &str = "cookie_test"; /// 存在していれば、HTTP Request ヘッダーから Cookie 文字列を取得する関数 /// /// # Arguments /// * `req` - actix_web::HttpRequest /// /// # Return value /// * Option<String> - key=value; key1=value1;~ のような Cookie の文字列 /// fn get_cookie_string_from_header(req: HttpRequest) -> Option<String> { let cookie_header = req.headers().get("cookie"); if let Some(v) = cookie_header { let cookie_string = v.to_str().unwrap(); return Some(String::from(cookie_string)); } return None; } /// 存在していれば、特定のキーで Cookie に設定された値を取得するための関数 /// /// # Arguments /// * `key` - Cookie から取り出したい値のキー /// * `cookie_string` - get_cookie_string_from_header 関数で取得した Cookie の文字列 /// /// # Return value /// * Option<String> - Cookie に設定されている値を取得する /// fn get_cookie_value(key: &str, cookie_string: String) -> Option<String> { // 取得した Cookie 文字列を ; で分割してループで回す let kv: Vec<&str> = cookie_string.split(';').collect(); for c in kv { // Cookie 文字列をパースして key で指定した値とマッチしたキーが存在するかチェックする match Cookie::parse(c) { Ok(kv) => { if key == kv.name() { // key で指定した値とマッチしたキーが存在していたら、その値を取得する return Some(String::from(kv.value())); } } Err(e) => { println!("cookie parse error. -> {}", e); } } } return None; } /// 特定のキーで環境変数から値を取得するための関数 /// /// # Arguments /// * `key` - 環境変数から取り出したい値のキー /// /// # Return value /// * String - 環境変数の値を文字列として取得する /// fn get_env(key: &str) -> String { match env::var(key) { Ok(value) => return value, Err(e) => println!("ENV: ERR {:?}", e), } return String::new(); } /// 環境変数に設定された HTTPS の値が 1 か判定する /// Cookie の属性に Secure を付与するか判定するのに使用する /// /// # Return value /// * bool - Secure 属性を付与するか判定するための真偽値 /// fn is_https() -> bool { return get_env("HTTPS") == "1"; } /// Cookie に設定する値を扱う HTTP Query の定義 #[derive(Deserialize)] pub struct CookieQuery { pub value: String, } /// Cookie を設定するために用意したルート /// /// # Example /// /// 例えば GET /cookie?value=test にアクセスした場合、 /// Cookie に cookie_test=test が設定されるようになる /// #[get("/cookie")] async fn set_cookie(query: web::Query<CookieQuery>) -> Result<HttpResponse, Error> { // 設定したい Cookie を作成する // その際に Secure, HttpOnly, SameSite=Strict 属性を付与する let cookie = Cookie::build(KEY, &query.value) .secure(is_https()) .http_only(true) .same_site(SameSite::Strict) .finish(); // 作成した Cookie を HTTP Response の Set-Cookie ヘッダーに含めることで、 // HTTP Response を受け取ったクライアントに Cookie をセットさせる return Ok(HttpResponse::Ok() .header("Set-Cookie", cookie.to_string()) .body("")); } /// KEY で指定した Cookie が存在すれば、その値を返却する /// KEY で指定した Cookie が存在しなければ、空の文字列を返却する #[get("/")] async fn index(req: HttpRequest) -> Result<HttpResponse, Error> { let cookie_string = get_cookie_string_from_header(req); if let Some(s) = cookie_string { if let Some(v) = get_cookie_value(KEY, s) { return Ok(HttpResponse::Ok().body(v)); } } return Ok(HttpResponse::Ok().body("")); } #[actix_web::main] async fn main() -> std::io::Result<()> { HttpServer::new(|| { App::new() .service(set_cookie) .service(index) }) .bind("0.0.0.0:8080")? .run() .await } </code></pre> <p>ザッとインラインコメントで説明していますが、<br /> 最も重要な <code>set_cookie</code> 関数について簡単に説明します。</p> <p>Actix web には <a target="_blank" rel="nofollow noopener" href="https://docs.rs/actix-web/3.3.2/actix_web/http/struct.Cookie.html"><code>Cookie</code> クラス</a>が存在します。この <code>Cookie</code> クラスは Cookie 文字列を生成したり、パースしたりするのに役立ちます。<code>set_cookie</code> 関数では、Cookie を生成するための関数 <a target="_blank" rel="nofollow noopener" href="https://docs.rs/actix-web/3.3.2/actix_web/http/struct.Cookie.html#method.build"><code>Cookie::build</code></a> を利用しています。</p> <p><code>Cookie::build</code> 関数を利用することで、メソッドチェインで Cookie の値や属性を設定できます。<strong>作成した Cookie は <code>to_string</code> 関数を使用することで文字列として出力できます。出力した Cookie 文字列を HTTP レスポンスヘッダーに <code>Set-Cookie</code> として設定すれば Cookie を設定できます。</strong></p> <h1 id="動作検証"><a href="#%E5%8B%95%E4%BD%9C%E6%A4%9C%E8%A8%BC">動作検証</a></h1> <p>今回用意した Actix web のサンプルコードには 2つのエンドポイントを用意しました。</p> <div class="table-responsive"><table> <thead> <tr> <th>URI</th> <th>説明</th> </tr> </thead> <tbody> <tr> <td><code>GET /cookie</code></td> <td><code>value</code> クエリで HttpOnly な Cookie を設定する</td> </tr> <tr> <td><code>GET /</code></td> <td><code>GET /cookie</code> で設定した Cookie を確認する</td> </tr> </tbody> </table></div> <p><code>cargo run</code> で Actix web のサンプルを起動した後に、ブラウザで <code>http://localhost:8080/cookie?value=sample</code> にアクセスしてみます。またその際に HTTP レスポンスヘッダーを確認したいため、<a target="_blank" rel="nofollow noopener" href="https://developer.mozilla.org/ja/docs/Learn/Common_questions/What_are_browser_developer_tools">開発者ツール</a>を開いておきます。</p> <p><img src="https://i.gyazo.com/9a1cb0cf73aa001b9ad3fdf7d8ae9966.png" alt="スクリーンショット 2021-01-23 13.12.27.png" /><br /> <strong>HTTP レスポンスヘッダーに Set-Cookie が含まれていることを確認する</strong></p> <p><code>Set-Cookie</code> が含まれていることが確認できたら正常に Cookie が設定されているか確認します。</p> <p><img src="https://i.gyazo.com/a6963e1d7ec82c57b3ae8d4978e1c116.png" alt="スクリーンショット 2021-01-23 13.26.52.png" /><br /> <strong>HTTP リクエストヘッダーの Cookie に <code>cookie_test=sample</code> が存在していることを確認する</strong></p> <p><img src="https://i.gyazo.com/282473c4c235d92233929f95c25ca899.png" alt="スクリーンショット 2021-01-23 13.32.00.png" /><br /> <strong>実際にブラウザーにも Cookie が正しく設定されているか、開発者ツールで確認する</strong></p> <p>正常に Cookie がセットされていることが確認できれば作業完了です。Cookie の属性に <code>Secure</code> を設定した場合の動作検証は、環境変数に <code>HTTPS=1</code> をセットして <code>cargo run</code> で可能です。</p> <h1 id="おわりに"><a href="#%E3%81%8A%E3%82%8F%E3%82%8A%E3%81%AB">おわりに</a></h1> <p>Actix web で割と汎用的に使えそうな知識として Cookie の設定方法について、メモ的な記事を書いてみました。引き続き、Rust への理解を深めるために <a target="_blank" rel="nofollow noopener" href="https://github.com/nikaera/bloggimg">Bloggimg</a> の開発を進めながら学習を進めていきます 🧑‍🎓</p> <p><em>本記事の内容がセキュリティの観点から適切でない場合等はコメントでご指摘いただけますと幸いです。</em></p> <h1 id="参考リンク"><a href="#%E5%8F%82%E8%80%83%E3%83%AA%E3%83%B3%E3%82%AF">参考リンク</a></h1> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://www.rust-lang.org/tools/install">Install Rust - Rust Programming Language</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://marketplace.visualstudio.com/items?itemName=rust-lang.rust">Rust - Visual Studio Marketplace</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://takoyaking.hatenablog.com/entry/2020/01/05/180000">VSCodeでRustインストールしたのに「Rustup not available」が出るとき (備忘録) - TAKOYAKING’s blog</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://developer.mozilla.org/ja/docs/Web/HTTP/Headers/Set-Cookie">Set-Cookie - HTTP | MDN</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://github.com/actix/actix-web">actix/actix-web: Actix Web is a powerful, pragmatic, and extremely fast web framework for Rust.</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://docs.rs/actix-web/3.3.2/actix_web/http/struct.Cookie.html">actix_web::http::Cookie - Rust</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://developer.mozilla.org/ja/docs/Learn/Common_questions/What_are_browser_developer_tools">ブラウザー開発者ツールとは? - ウェブ開発を学ぶ | MDN</a></li> </ul> nikaera tag:crieit.net,2005:PublicArticle/16612 2021-01-18T02:08:16+09:00 2021-01-18T02:08:16+09:00 https://crieit.net/posts/bloggimg-first-release 📔 ブログを書く用途に特化した Gyazo のツールを開発してみた <p><img src="https://i.gyazo.com/263820f97c341755faf69d9269471bf8.png" alt="Gyazo を技術記事を書く用途で使っているので専用の便利ツールを作ってみた" /><br /> <strong>Gyazo を技術記事を書く用途で使っているので専用の便利ツールを作ってみた</strong></p> <h1 id="はじめに"><a href="#%E3%81%AF%E3%81%98%E3%82%81%E3%81%AB">はじめに</a></h1> <p>いつもブログ記事に載せるキャプチャ画像の編集 & アップロード先として <a target="_blank" rel="nofollow noopener" href="https://gyazo.com/">Gyazo</a> を利用させていただいているのですが、日々使っている中で不満に感じる点もちょくちょく出てくるようになってきました。</p> <p>そのため、3連休を用いて <a target="_blank" rel="nofollow noopener" href="https://www.rust-lang.org/ja">Rust</a> の勉強がてら <a target="_blank" rel="nofollow noopener" href="https://www.bloggimg.net/">Bloggimg</a> というウェブアプリケーションを作ってみました。ソースコードは MIT ライセンスで <a target="_blank" rel="nofollow noopener" href="https://github.com/nikaera/bloggimg">GitHub のリポジトリ</a>にアップしております。<em>ちなみに最初は <code>Gyazo for Blog</code> という名称で開発をしていたため、本記事内のスクショには <code>Gyazo for Blog</code> という文字列が出てきますが、現在は <code>Bloggimg</code> という名称になっております。。</em></p> <p><strong><code>Bloggimg</code> を開発したのは、ブログ記事を書く際に利用する画像のアップロードから加工、マークダウンとして利用するまでのフローを最適化したかったからです。</strong> ブログ記事を書く際に、記事内で用いるスクショ画像の加工や、そのアップロードにすごく時間を取られてしまうなーと日頃から感じていたのでそれを解決したかったのです。✅</p> <p>開発中に得た知見等については別途技術記事として書いて残す予定です。</p> <h1 id="考えていたこと"><a href="#%E8%80%83%E3%81%88%E3%81%A6%E3%81%84%E3%81%9F%E3%81%93%E3%81%A8">考えていたこと</a></h1> <p>今回 Bloggimg の開発を行うに当たり、考えていた点は下記になります。</p> <ul> <li>画像の編集ツールは引き続き Gyazo に用意されているものを使う <ul> <li>既に最高に使いやすい 👑</li> </ul></li> <li>キャプチャ画像をアップロードする際に、<strong>自動的に特定のコレクションに紐付けるようにする</strong> <ul> <li>技術記事毎にコレクションを分けて管理しているため、技術記事を書いている最中にアップするキャプチャ画像は全て特定のコレクションにまとまっていて欲しい</li> </ul></li> <li>ワークスペースのようなツールを目指し、ブログを書く時だけに使える機能を開発する <ul> <li>例えば、ワンクリックで画像マークダウンの記述がコピーできたり、画像のアップロードをし直しやすくするため画像削除がお手軽に出来るよう削除ボタンに即アクセス出来るようにしたり...</li> </ul></li> </ul> <p>特にアップした画像を <strong>自動的に特定のコレクションに紐付けるようにする</strong> については本記事で紹介しているウェブアプリケーションを作成するキッカケとなった点なので外せない点でした。</p> <h1 id="使い方"><a href="#%E4%BD%BF%E3%81%84%E6%96%B9">使い方</a></h1> <p>Bloggimg の使い方についてご紹介いたします。</p> <h2 id="ログインする"><a href="#%E3%83%AD%E3%82%B0%E3%82%A4%E3%83%B3%E3%81%99%E3%82%8B">ログインする</a></h2> <p><a target="_blank" rel="nofollow noopener" href="https://www.bloggimg.net/">Bloggimg</a> を利用するためには、まず Gyazo アカウントでログインして頂く必要がございます。トップページの右上にあるログインボタンから Gyazo アカウントでログインします。</p> <p><img src="https://i.gyazo.com/b0e863464696ce778ca853d7fac56ab9.png" alt="スクリーンショット 2021-01-11 15.31.27.png" /><br /> <strong>1. トップページ右上に配置されたログインボタンから Gyazo アカウント認証を行う</strong></p> <p><img src="https://i.gyazo.com/1bd6c3212c8fa95196ec0eaaef94d4d7.png" alt="スクリーンショット 2021-01-11 15.35.59.png" /><br /> <strong>2. Gyazo アカウント認証が正常に完了したら、再度トップページを開く</strong></p> <p><img src="https://i.gyazo.com/110fe178f5f158153640ab25271d90a2.png" alt="スクリーンショット 2021-01-11 15.42.21.png" /><br /> <strong>3. トップページを開いた時に Gyazo にアップした直近の画像が確認できるはずです</strong></p> <h2 id="ログアウトする"><a href="#%E3%83%AD%E3%82%B0%E3%82%A2%E3%82%A6%E3%83%88%E3%81%99%E3%82%8B">ログアウトする</a></h2> <p>ウェブアプリケーションからログアウトするには、ログイン後にトップページ右上に表示される <code>ログアウト</code> ボタンをクリックすることでログアウトできます。</p> <p><img src="https://i.gyazo.com/e7391a84be6e2e22845e975383dd78b0.png" alt="スクリーンショット 2021-01-11 16.00.02.png" /><br /> <strong>ログイン後にトップページ右上に表示される <code>ログアウト</code> ボタンをクリックしてログアウトする</strong></p> <h2 id="画像ファイルをアップロードする"><a href="#%E7%94%BB%E5%83%8F%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E3%82%92%E3%82%A2%E3%83%83%E3%83%97%E3%83%AD%E3%83%BC%E3%83%89%E3%81%99%E3%82%8B">画像ファイルをアップロードする</a></h2> <p>画像は一枚でも複数枚でもアップロードすることが可能です。画像アップロードの方法としてドラッグ & ドロップとファイル選択ダイアログを用意しております。</p> <p><img src="https://i.gyazo.com/4e559abbc492b82b58349cd511f7987c.png" alt="スクリーンショット 2021-01-11 16.04.31.png" /><br /> <strong>画面中央の点線枠内に画像ファイルをドラッグ & ドロップするか、点線枠内をクリックしてファイル選択ダイアログから選択することで画像をアップロードできる</strong></p> <h2 id="画像ファイルをアップロードする際に自動でコレクションを紐付ける"><a href="#%E7%94%BB%E5%83%8F%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E3%82%92%E3%82%A2%E3%83%83%E3%83%97%E3%83%AD%E3%83%BC%E3%83%89%E3%81%99%E3%82%8B%E9%9A%9B%E3%81%AB%E8%87%AA%E5%8B%95%E3%81%A7%E3%82%B3%E3%83%AC%E3%82%AF%E3%82%B7%E3%83%A7%E3%83%B3%E3%82%92%E7%B4%90%E4%BB%98%E3%81%91%E3%82%8B">画像ファイルをアップロードする際に自動でコレクションを紐付ける</a></h2> <p>Gyazo トップページ左端にコレクションリストが表示されているので、画像を紐づけたいコレクションを選択します。新たにコレクションを作成する場合はコレクションリスト上部にある <code>コレクションを作成</code> ボタンをクリックします。</p> <p><img src="https://i.gyazo.com/b06be51cd5cde0e09733325e696c655a.png" alt="スクリーンショット 2021-01-11 16.37.30.png" /><br /> <strong>1. コレクションリストの中から画像を紐づけたいコレクションを選択する</strong></p> <p><img src="https://i.gyazo.com/7d3dec131a20b1a7f6d24d10bbe8c39e.png" alt="スクリーンショット 2021-01-11 16.42.16.png" /><br /> <strong>2. コレクションを選択後に遷移した先の URL 末尾のコレクション ID をコピーする</strong></p> <p><img src="https://i.gyazo.com/eb052b62aec6500c030e390c85172de8.png" alt="スクリーンショット 2021-01-11 16.45.46.png" /><br /> <strong>3. トップページの最上部に 2. で控えていたコレクション ID をペーストする</strong></p> <p>上記までのステップが完了し、正しくコレクション ID が入力できていれば、次回以降のファイルアップロード時に自動で指定したコレクションに画像が紐づくようになります。</p> <h2 id="アップロードした画像ファイルを編集する"><a href="#%E3%82%A2%E3%83%83%E3%83%97%E3%83%AD%E3%83%BC%E3%83%89%E3%81%97%E3%81%9F%E7%94%BB%E5%83%8F%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E3%82%92%E7%B7%A8%E9%9B%86%E3%81%99%E3%82%8B">アップロードした画像ファイルを編集する</a></h2> <p>画像ファイルのアップロード時や <code>画像の再読み込み</code> ボタンをクリックすることで、最新 20件の画像リストを画面最下部にロードされます。画像リストの各項目ではプレビュー、編集、削除、マークダウンのコピーを行うことが可能です。</p> <p><img src="https://i.gyazo.com/1226a831420cd25992d8de1f4446c5ed.png" alt="スクリーンショット 2021-01-11 16.58.33.png" /><br /> <strong>画像リストの各項目の機能概要図</strong></p> <h3 id="プレビュー"><a href="#%E3%83%97%E3%83%AC%E3%83%93%E3%83%A5%E3%83%BC">プレビュー</a></h3> <p>サムネ画像をクリックすることで、Gyazo にアップした元画像をプレビューすることが可能です。サムネ画像では画像の判別がしにくい場合に詳細を確認するための機能となります。</p> <p><img src="https://i.gyazo.com/e8d3c1b2e3c4247fdcde156dfa18e343.png" alt="スクリーンショット 2021-01-11 17.04.28.png" /><br /> <strong>アップした画像の詳細を確認するためにプレビュー機能を利用する</strong></p> <h3 id="編集"><a href="#%E7%B7%A8%E9%9B%86">編集</a></h3> <p>編集は該当画像の Gyazo ページにて行えるように、タイトルをクリックすることで Gyazo ページを別タブで開きます。</p> <p><img src="https://i.gyazo.com/83ab2ec4c4c4e1251302aea840377870.png" alt="スクリーンショット 2021-01-11 17.07.37.png" /><br /> <strong>別タブで開いた Gyazo ページから画像の編集作業を行う</strong></p> <h3 id="削除"><a href="#%E5%89%8A%E9%99%A4">削除</a></h3> <p>画像の削除は <code>画像の削除</code> ボタンをクリックすることで、削除を行うための画面に遷移します。削除しようとしている画像で間違いないか確認後、削除を行うという手順になっております。</p> <p><img src="https://i.gyazo.com/a3e219b5efb8494103432b369ee99534.png" alt="スクリーンショット 2021-01-11 17.10.11.png" /><br /> <strong>Gyazo から選択した画像を削除する</strong></p> <h3 id="マークダウンのコピー"><a href="#%E3%83%9E%E3%83%BC%E3%82%AF%E3%83%80%E3%82%A6%E3%83%B3%E3%81%AE%E3%82%B3%E3%83%94%E3%83%BC">マークダウンのコピー</a></h3> <p><code>マークダウンをコピー</code> ボタンをクリックすることで、クリップボードにマークダウン形式で該当画像を表示するための記述をコピーすることができます。具体的には下記のような記述がコピーされます。</p> <p>ブログを書く先がマークダウン形式での記述に対応していれば、そのままペーストするだけで画像を表示することが可能です。</p> <pre><code class="md">![スクリーンショット 2021-01-11 17.10.11.png](https://i.gyazo.com/a3e219b5efb8494103432b369ee99534.png) </code></pre> <h1 id="おわりに"><a href="#%E3%81%8A%E3%82%8F%E3%82%8A%E3%81%AB">おわりに</a></h1> <p>この記事を書くのにも実際に Bloggimg を用いましたが、個人的に今までよりも Gyazo でブログ記事内で利用する画像に関する作業効率は上がったように感じました。ブログを書くという用途に Gyazo を利用されている方のお役に立てれば幸いです。</p> <p>また、今後は下記の機能実装を進めていく予定です。</p> <ul> <li>画像アップ時に自動でアスペクト比を維持した状態で画像のリサイズを自動で行う機能</li> <li>画像アップ時のタイトルの接頭辞が指定できるようにする機能</li> <li>編集した画像が自動的にコレクションに紐づく機能 <ul> <li>心残りな点として編集した画像をコレクションに紐付ける機能は API でできなかったため、現在手動で行う必要があります。。Gyazo の API がコレクションの紐づけにも対応したら対応したいと考えています ✅</li> </ul></li> </ul> nikaera tag:crieit.net,2005:PublicArticle/16540 2021-01-06T01:15:51+09:00 2021-01-11T02:06:56+09:00 https://crieit.net/posts/aws-lambda-cron 📝 AWS Lambda で cron みたいに定期実行する <p>コンテナをホットスタンバイさせるために EC2 でインスタンス起動して cron で ping 飛ばしていたのですが、コスト的に勿体ないなーと思っていました。しかし、「AWS Lambda 使えばいいじゃん」という指摘を受け、確かにってなったので cron で定期実行していた ping 処理を AWS Lambda + EventBridge で置き換えました。</p> <p>実は <a target="_blank" rel="nofollow noopener" href="https://devcenter.heroku.com/articles/scheduler">Heroku Scheduler</a> とか使って同様のことをしていた時期もあったのですが、10分毎しか実行できない制約があったりして使い勝手が悪かったので、後々も使っていけそうな知見な気がしたのでメモがてら記事で残しておくことにしました。</p> <p>まず、AWS Console から Lambda サービスを選択して関数を新たに作成します。</p> <p><img src="https://i.gyazo.com/b4b591876af8519f9b22cfa35131327c.png" alt="AWS Lambda のトップ画面から関数を新たに作成する" /><br /> <strong>1. AWS Lambda のトップ画面から関数作成のための画面に遷移する</strong></p> <p><img src="https://i.gyazo.com/00a4e4415827a6e745f47e2ca5d39e1d.png" alt="必要な情報を入力して AWS Lambda の関数を作成する" /><br /> <strong>2. 必要な情報を入力して Lambda の関数を作成する</strong></p> <p>関数が作成でき次第、ping 処理を書いていきます。http リクエストを行うためのライブラリとして Node.js の標準モジュール(https) を利用します。</p> <p>Lambda 関数作成直後の <code>index.js</code> は下記のような記述になっていると思います。</p> <pre><code class="javascript">// index.js exports.handler = async (event) => { // TODO implement const response = { statusCode: 200, body: JSON.stringify('Hello from Lambda!'), }; return response; }; </code></pre> <p>こちらを Node.js の標準モジュール(https) を利用する形で下記のように書き換えます。</p> <pre><code class="javascript">// index.js // "https://www.google.com/" に HTTP リクエストを実行する (ping) const https = require('https'); const httpRequest = async (url) => { return new Promise((resolve, reject) => { const req = https.request(url, (res) => { let body = ''; res.on('data', (chunk) => { body += chunk; }); res.on('end', () => { console.log(`response: ${body}`); resolve(body); }); }) req.on('error', (e) => { console.error(e); reject(e); }); req.end(); }); } exports.handler = async (event) => { const body = await httpRequest("https://www.google.com/"); return { statusCode: 200, body }; }; </code></pre> <p>その後、右上にある <code>Deploy</code> ボタンをクリックして関数にソースコードを反映します。実際に関数が意図したとおりに動作するか、<code>Test</code> ボタンをクリックして動作検証してみます。</p> <p><img src="https://i.gyazo.com/ebbf0c5d9188faea7c49f04da63067fa.png" alt="1. <code>Test</code> ボタンをクリックします" /><br /> <strong>1. <code>Test</code> ボタンをクリックします</strong></p> <p><img src="https://i.gyazo.com/cc4099af0014d6f5ea0701c0e630af63.png" alt="2. 動作検証時のパラメーターを入力してテスト環境を作成する" /><br /> <strong>2. 動作検証時のパラメーターを入力してテスト環境を作成する</strong></p> <p><img src="https://i.gyazo.com/8cee96e8065baa486c966c6596b2f36c.png" alt="3. 2. のテスト環境で関数の動作検証を行い正常に実行できていることを確認する" /><br /> <strong>3. 2. のテスト環境で関数の動作検証を行い正常に実行できていることを確認する</strong></p> <p>正常に関数が実行できていること確認できれば、後は定期実行可能にすれば作業完了です。定期実行するためのスケジューラには EventBridge を利用します。<code>Add trigger</code> ボタンから EventBridge を追加します。</p> <p><img src="https://i.gyazo.com/f2b87343ebe5a32901b60721c8ef98e8.png" alt="1. <code>Add trigger</code> ボタンからトリガー追加画面に遷移する" /><br /> <strong>1. <code>Add trigger</code> ボタンからトリガー追加画面に遷移する</strong></p> <p><img src="https://i.gyazo.com/de52cedffde5acbf38bec0ad55f7a98c.png" alt="2. EventBridge トリガーを追加して定期実行の設定を行う" /><br /> <strong>2. EventBridge トリガーを追加して定期実行の設定を行う</strong></p> <p><img src="https://i.gyazo.com/c519d210f18b58ff4f9aeded662ee995.png" alt="3. EventBridge トリガーの追加が無事に完了したことを確認する" /><br /> <strong>3. EventBridge トリガーの追加が無事に完了したことを確認する</strong></p> <p>また <code>2.</code> では 1分毎に実行するスケジュールを設定しましたが、<a target="_blank" rel="nofollow noopener" href="https://docs.aws.amazon.com/ja_jp/eventbridge/latest/userguide/scheduled-events.html">EventBridge の書式</a> を用いてより複雑なスケジュール設定を行うことも可能です。</p> <p>最後に本当に定期実行されていて、関数の実行も正常に行われていそう確認します。<code>Monitoring</code> タブをクリックして、関数の実行状況を確認していきます。</p> <p><img src="https://i.gyazo.com/54672fc613642b52471db2f51d006b37.png" alt="1. <code>Monitoring</code> タブをクリックする" /><br /> <strong>1. <code>Monitoring</code> タブをクリックする</strong></p> <p><img src="https://i.gyazo.com/b76a3b7bc3de06c47756b7eed62559d1.png" alt="2. Lambda 関数が定期実行されていることを確認する" /><br /> <strong>2. Lambda 関数が定期実行されていることを確認する</strong></p> <p><img src="https://i.gyazo.com/836da1ed016618aa791397bcd609e9a3.png" alt="3. Lambda 関数の実行結果が正しいことも確認する" /><br /> <strong>3. Lambda 関数の実行結果が正しいことも確認する</strong></p> <p>これで作業完了です。お疲れさまでした。</p> nikaera tag:crieit.net,2005:PublicArticle/16452 2020-12-31T17:34:56+09:00 2021-01-03T02:22:37+09:00 https://crieit.net/posts/summarize-2020 📔 2020年の振り返り <h1 id="はじめに"><a href="#%E3%81%AF%E3%81%98%E3%82%81%E3%81%AB">はじめに</a></h1> <p>今年は結果的にプライベートと仕事の両面で充実した年にできました。来年の自分が今を振り返れるように、今年始めからの記憶を引っ張り出しながら総括しました。</p> <p>今年問わず作ったものは <a target="_blank" rel="nofollow noopener" href="/tech/">Tech</a> ページに、技術記事については <a target="_blank" rel="nofollow noopener" href="/rss_feeds/">RSS Feeds</a> にまとめてあります。</p> <h1 id="出来事"><a href="#%E5%87%BA%E6%9D%A5%E4%BA%8B">出来事</a></h1> <h2 id="1月"><a href="#1%E6%9C%88">1月</a></h2> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://note.com/nikaera/n/n859dac693e73">Death Stranding のプラチナ獲得</a></li> <li>弊社に面接にいらっしゃったベテラン開発者の方に何でうちに応募してくださったのか聞いたら、僕の Twitter や Qiita アカウントを見てくださり技術力がありそうと判断してくれたからと聞いて爆嬉しかった</li> <li><a target="_blank" rel="nofollow noopener" href="https://twitter.com/n1kaera/status/1227047114166820870?s=20">ambr オフ会参加 (オフィシャルなオフ会に初参加)</a></li> <li>Quest 用アプリケーションの初リリース体験実績解除 <ul> <li>申請時の知見の一部については <a target="_blank" rel="nofollow noopener" href="https://qiita.com/nikaera/items/4adaa276e6115c7aa95b">Qiita 記事</a> として投下</li> </ul></li> <li>Android で AR アプリケーションの開発及び、マルチプレイを可能にするバックエンド開発を担当した</li> </ul> <h2 id="2月"><a href="#2%E6%9C%88">2月</a></h2> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/nikaera/items/30e1c7981aecbdc07e9e">Docker で各種モバイル VR 向けの Unity ビルドが出来るようにした</a></li> <li>フルリモートでアジャイルな開発チームにジョインする (WebView/ReactNative/iOS/Android) <ul> <li>主は ReactNative の iOS/Android のネイティブプラグイン開発</li> <li>コア機能の実装にのみ注力しパフォーマンスチューニング等々を行っていたためポジション的にはひたすら地味だった</li> </ul></li> </ul> <h2 id="3月"><a href="#3%E6%9C%88">3月</a></h2> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://note.com/nikaera">note</a> デビューした <ul> <li>初投稿は <a target="_blank" rel="nofollow noopener" href="https://note.com/nikaera/n/na290d203aa5a">精神衛生を保つため Chrome で Twitter を閲覧している時にフォロワー数を非表示にする</a> っていうやつ</li> </ul></li> <li>お題が「Home」の <a href="https://crieit.net/boards/web1week-202003">web1week</a> に参加した <ul> <li>参加した時に投稿した記事は<a href="https://crieit.net/boards/web1week-202003/661fc53a1809d108713c0ebcb2e0ab17">こちら</a></li> </ul></li> </ul> <h2 id="4月"><a href="#4%E6%9C%88">4月</a></h2> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://medium.com/kadinche-engineering">会社の Medium ブログ開設</a> したのと、いくつか記事を寄稿した <ul> <li><a target="_blank" rel="nofollow noopener" href="https://medium.com/kadinche-engineering/azure-kinect-dk-%E3%81%AE%E9%96%8B%E7%99%BA%E7%92%B0%E5%A2%83%E6%A7%8B%E7%AF%89%E3%81%8B%E3%82%89-kinectfusion-%E3%81%AE%E3%82%B5%E3%83%B3%E3%83%97%E3%83%AB%E3%82%92%E5%8B%95%E3%81%8B%E3%81%99%E3%81%BE%E3%81%A7-cd7d550994fe">Azure Kinect DK の開発環境構築から KinectFusion のサンプルを動かすまで</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://medium.com/kadinche-engineering/%E6%9C%80%E7%9F%AD%E3%81%A7-magic-leap-1-%E3%81%AE%E9%96%8B%E7%99%BA%E7%92%B0%E5%A2%83%E3%82%92%E6%A7%8B%E7%AF%89%E3%81%99%E3%82%8B-f1cf41864dde?source=collection_home---6------6-----------------------">最短で Magic Leap 1 の開発環境を構築する</a></li> </ul></li> <li>色々工夫して iPhone TrueDepth を WebRTC でブラウザに転送して、Three.js で表示する仕組みを実現した <ul> <li><a target="_blank" rel="nofollow noopener" href="https://twitter.com/n1kaera/status/1248933812530118656?s=20">Twitter でシェア</a> してみたら、予想だにしないことに一方的に尊敬していたエンジニアの方々からいいねを貰えてモチベが爆上がりした</li> </ul></li> <li>CloudFormation と和解。IaC の利便性を完全に理解し始める</li> <li>今更 SEKIRO にハマりまくる & <a target="_blank" rel="nofollow noopener" href="https://twitter.com/n1kaera/status/1255211139371134976?s=20">プラチナトロフィー獲得</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://note.com/nikaera/n/n859dac693e73">デス・ストランディングから学んだこと</a> が現在の自分の考え方の基礎として根付き始める <ul> <li><strong>全ての考え方を 0 or 1 ではなく、グラデーションにハメ込むことが出来るようになった</strong></li> </ul></li> </ul> <h2 id="5月"><a href="#5%E6%9C%88">5月</a></h2> <ul> <li>再びお題が「Like」の <a href="https://crieit.net/boards/web1week-202005">web1week</a> に参加した <ul> <li>参加した時に投稿した記事は<a href="https://crieit.net/boards/web1week-202003/30DaysSongChallenge-30">こちら</a> (あとから確認したら投稿先を間違えていた...)</li> </ul></li> <li>Medium に目次が無いことに不満をいただき <a target="_blank" rel="nofollow noopener" href="https://chrome.google.com/webstore/detail/toc-of-medium/jcpcpidaogolhkcapgdiblaeglijmhaa?hl=ja">Chrome プラグイン</a>を作成する <ul> <li><a target="_blank" rel="nofollow noopener" href="https://medium.com/@nikaera/chrome-extension-for-creating-a-toc-for-medium-8c8d344313fe">プラグインの紹介記事</a> まで書いてたけど全くインストール数伸びなかった、、けど今みたら 12人ほど使ってくれている人いるぽくて嬉しい</li> </ul></li> <li>お題が「密」の unity1week に <a target="_blank" rel="nofollow noopener" href="https://unityroom.com/games/seimitsu">参戦した</a></li> </ul> <h2 id="6月"><a href="#6%E6%9C%88">6月</a></h2> <ul> <li>S3 + EventBridge + CloudWatch + CloudFront + MediaLive + MediaPackage + AppSync + Amplify + DynamoDB + Cognito + Lambda + API Gateway + SSM という AWS ガッツリなインフラ構築から、バックエンド開発及び iOS アプリ開発までをおもむろに始めた <ul> <li>まず <a target="_blank" rel="nofollow noopener" href="https://qiita.com/nikaera/items/c4a4c2f5eb74b5732294">DynamoDB の仕様にハマる</a> (自分のリサーチ & 勉強不足によるせい...)</li> <li>つぎに <a target="_blank" rel="nofollow noopener" href="https://zenn.dev/nikaera/articles/cloudfront-for-mediapackage">MediaPackage + CloudFront の構築</a> に苦戦する</li> <li>そしてデバッグが辛くなり <a target="_blank" rel="nofollow noopener" href="https://qiita.com/nikaera/items/27c1ba5f8fd2e61425c4">Serverless Framework でエラーを検知して Webhook で Slack に通知を飛ばす方法</a> を実践し始めたりしていた</li> </ul></li> <li><a target="_blank" rel="nofollow noopener" href="https://third.shenmue.com/">シェンムー3</a> のプレイを開始。<a target="_blank" rel="nofollow noopener" href="https://twitter.com/n1kaera/status/1298642734912163842?s=20">ワクワクするし美しすぎる町並み</a>に興奮し、しばらくの間深夜までプレイする日々が続く</li> <li>映画の <a target="_blank" rel="nofollow noopener" href="https://hello-world-movie.com/">HELLO WORLD</a> を見て、劇中 3回号泣する <ul> <li>元々は <a target="_blank" rel="nofollow noopener" href="https://jp.gamesindustry.biz/article/1909/19093002/">Unity で作られたシーンがあるという記事</a> を見て興味を持ち見ようと思い立った感じだった</li> </ul></li> </ul> <h2 id="7月"><a href="#7%E6%9C%88">7月</a></h2> <ul> <li>ひたすらトラブルバスターしてた (一番忙しかった気がする)</li> </ul> <h2 id="8月"><a href="#8%E6%9C%88">8月</a></h2> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://www.playstation.com/ja-jp/games/ghost-of-tsushima/">Ghost of Tsushima</a> にハマりまくる & <a target="_blank" rel="nofollow noopener" href="https://twitter.com/n1kaera/status/1290268995912794117?s=20">プラチナトロフィー獲得</a></li> <li>DDD 開発の際、DI コンテナ入れたいよねっていう話から TypeScript 環境で利用可能なライブラリを調査して <a target="_blank" rel="nofollow noopener" href="https://github.com/inversify/InversifyJS">InversifyJS</a> と <a target="_blank" rel="nofollow noopener" href="https://github.com/microsoft/tsyringe">tsyringe</a> を見つける <ul> <li>最初 InversifyJS を発見してそれで開発をしていたものの、microsoft 製の tsyringe を発見し、「メンテナが大手だしコンストラクタインジェクションだけしか使わないし、こっちのが良くね?」という話になり InversifyJS から tsyringe へのリライト作業を行う...w</li> </ul></li> <li><a target="_blank" rel="nofollow noopener" href="https://docs.aws.amazon.com/sdk-for-go/api/service/s3/#example_S3_CopyObject_shared00">AWS SDK for Go</a> で関数の引数と返り値を Type で定義するっていう考え方は非常に参考になった <ul> <li>引数が <code>*Input</code> という定義で、返り値が <code>*Output</code> という定義で分かりやすく読みやすい</li> </ul></li> <li>自作 iOS ライブラリの CocoaPods 対応について C++ 周りの linker error に対する解決のためのアドバイスを急ぎ求められたので、<strong>共有された情報から自分がハマった経験に照らし合わせてソレっぽい対策案を共有したらガチッとハマって解決でき、経験が生きた感がめっちゃあって、めちゃくちゃ嬉しかった</strong></li> </ul> <h2 id="9月"><a href="#9%E6%9C%88">9月</a></h2> <ul> <li>フライパンでコーヒーの焙煎を始める <ul> <li>チャフの飛散に苦しめられるが、風呂場で作業することで諸々ストレスフリーになる</li> <li>このときはまだ、後ほど焙煎機を購入することになるとは夢にも思わなかったのである...</li> </ul></li> <li>Azure を用いた開発に本格的に携わり始める <ul> <li>その際得た知見は Zenn で <a target="_blank" rel="nofollow noopener" href="https://zenn.dev/nikaera/books/nestjs-azure-dev">本としてまとめた</a>。本来は記事として書くつもりだったが、分量が増えすぎたため記事内容を分割して、本としてまとめた</li> <li>PlayFab CloudFunctions のための関数実装のために Azure でシステム構築していたため、PlayFab にもそれなりに詳しくなる</li> </ul></li> <li>とある案件から別案件に移る際に、 <strong>「えー、〇〇 に行っちゃうんですか。nikaera さんは今後も 〇〇 を一緒にやっていって欲しいのに」</strong> って結構強めに言われたことが未だにめちゃくちゃ嬉しい</li> <li><strong>シンガポール現地のフリーランスの方と仕事を共同で進めることになる。技術に関する事柄やプライベートに関する事柄のやり取り等々、全て英語でコミュニケーションを行わざるを得なくなり、そのおかげで英語でコミュニケーションを取ることに一切抵抗が無くなる</strong></li> </ul> <h2 id="10月"><a href="#10%E6%9C%88">10月</a></h2> <ul> <li><strong>約 10 年ぶりに私用携帯を HTC EVO から iPhone 12 mini に機種変する</strong> <ul> <li>開発用途でなく普段生活で使うことのみを考慮するということであれば iPhone 12 mini は最強にオススメできるスマホです</li> <li>会社支給のスマホで 7年近くを賄っていたため、特に不便がなかったため...</li> </ul></li> <li>AWS Amplify への <a target="_blank" rel="nofollow noopener" href="https://github.com/aws-amplify/amplify-cli/pull/5608">PR</a> がマージされる <ul> <li>もとは <a target="_blank" rel="nofollow noopener" href="(https://github.com/bboure/serverless-appsync-simulator/issues/51#issuecomment-709808350)">serverless-amplify-simulator の Issue</a> で議論していたのだが、改修すべき内容は <code>amplify-cli</code> にあったのでそちらで PR を提出した</li> <li><strong>細かくづまづいた点を進捗共有兼ねて Issue で一人投稿しまくっておくと、他の開発者の役にも立てるし自然とその問題に詳しくなっていくし、OSS 活動への取っ掛かりとしては最高なんじゃないかと勝手に思い始める</strong></li> </ul></li> <li>NPM に初自作ライブラリを公開する <ul> <li><a target="_blank" rel="nofollow noopener" href="https://www.npmjs.com/package/serverless-amplify-auth">serverless-amplify-auth</a> という Amplify 開発を行う際は必ず行うであろう IAM Policy の制限を Serverless で行うことが出来るようにするやつ</li> </ul></li> </ul> <h2 id="11月"><a href="#11%E6%9C%88">11月</a></h2> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://gohugo.io/">Hugo</a> で自分のブログ(<a target="_blank" rel="nofollow noopener" href="https://nikaera.com/">nikaera.com</a>)を <a target="_blank" rel="nofollow noopener" href="https://docs.github.com/ja/free-pro-team@latest/github/working-with-github-pages/about-github-pages">GitHub Pages</a> 上に構築する</li> <li>カジュアル面談した人に <a target="_blank" rel="nofollow noopener" href="https://qiita.com/nikaera/items/508b88a060c8a7b68ec2">Qiita のネタ記事</a> 見ましたって言われて嬉しいよりも恥ずかしいが上回った</li> <li><a target="_blank" rel="nofollow noopener" href="https://www.etsy.com/">Etsy</a> でアクセサリ販売している方に日本のフリマ事情を詳細にお伝えしたらおまけのプレゼントを送付してくださった <ul> <li>ちなみに購入物は Death Stranding の <a target="_blank" rel="nofollow noopener" href="https://www.etsy.com/listing/834901460/inspired-by-death-stranding-sam-bridges?ref=yr_purchases">ドリームキャッチャー</a></li> </ul></li> <li>Moonlander が自宅に届きテンション上がって<a target="_blank" rel="nofollow noopener" href="https://nikaera.com/archives/introduction-to-moonlander/">紹介記事を書く</a></li> </ul> <h2 id="12月"><a href="#12%E6%9C%88">12月</a></h2> <ul> <li>今年学んだ重要なことを <a target="_blank" rel="nofollow noopener" href="https://nikaera.com/archives/birthday-2020/">記事</a> として残しといた (これもある意味総括な気がする)</li> <li>GitHub Profile を充実させる <ul> <li>こんな感じ -> <a target="_blank" rel="nofollow noopener" href="https://github.com/nikaera">https://github.com/nikaera</a></li> <li>更に GitHub Profile を充実させるために <a target="_blank" rel="nofollow noopener" href="https://zenn-badge.vercel.app/">Zenn のバッジを作成するサービス</a> を作った</li> </ul></li> <li><a target="_blank" rel="nofollow noopener" href="https://lapras.com/public/JPCBXDB">Lapras</a> の技術力スコアが <strong>3.36</strong> になってた <ul> <li>基準とか良く分からないけど純粋に上位 13% に入ったと言われてるのは嬉しかった</li> </ul></li> <li>いくつか空いてたアドベントカレンダーに参戦した <ul> <li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/nikaera/items/b866fd6adca12a88f0ed">MediaPackage 用の CloudFront ディストリビューションを AWS SDK で作成する</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/nikaera/items/825165be91ad5cdb6b56">Serverless のプラグインを TypeScript で作成する方法</a></li> </ul></li> <li><strong>AWS Lambda を用いた他社製品との連携システムが好評で、去年から今年末まで特に目立った不具合等も起きずに運用できたため、次期開発に繋がりそうとの連絡があり開発者として爆喜ぶ</strong></li> </ul> <h1 id="おわりに"><a href="#%E3%81%8A%E3%82%8F%E3%82%8A%E3%81%AB">おわりに</a></h1> <p>今年は後半からすごい勢いでギアが入ってきた感があり、諸々活動するための足がかりを作れた気がします。身も心も進化したなと思えて成長できたなという充足感は割と高めな 1年だったので、この勢いのまま 2021 年もマイペースに色んなことにチャレンジしていければなーと思っております。</p> <p>この記事を書いている人物のプロフィールは <a target="_blank" rel="nofollow noopener" href="/profile/">Profile</a> からご確認いただけます。何かございましたら <a target="_blank" rel="nofollow noopener" href="/contact/">Contact</a> からお気軽にご連絡くださいませ。</p> <p>それではみなさま良いお年を!!😆</p> nikaera