nazolabo

Webエンジニアのnazoさんのブログです。お仕事募集中です。 https://nazo.dev/

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

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

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

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

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

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

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

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

参考

docs.microsoft.com

github.com

github.com

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

概要

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

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

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

Lambda

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

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

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

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

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

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

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

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

  await postToSlack(message);
};

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

CloudWatch Event

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

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

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

結果

f:id:nazone:20200621113237p:plain

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

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

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

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

何の話

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

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

前提

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

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

Resource

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

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

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

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

Deployment

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

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

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

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

なぜ分けるのか

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

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

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

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

deeeet.com

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

まとめ

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

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

アタッチされたサービスとTerraformのModuleの分け方

何の話

TerraformのModuleの分け方はマイクロサービス的な単位で分けると良いという話です。TerraformだけではなくてIaC全般に言える話だと思います。

アタッチされたサービス

Twelve-Factor AppのIV. バックエンドサービスでは、バックエンドサービス(DBやキャッシュなど)はアタッチされたサービスとして扱う、としております。

f:id:nazone:20200502144124p:plain

設定は環境変数で接続先を具体的に記入し、アプリケーションコード(ビルド)の差し替えなしにバックエンドサービスを切り替えれるようにすべきとしております。

これにより様々なメリットが生まれますが、ここでは特に責任分界点がこの接続先によって明確になるということがあります。

f:id:nazone:20200502144709p:plain

マイクロサービスの設計では独立性が重要になりますので、責任分界点が明確になっているということは独立性が高いものを組み合わせて安全に利用できている状態と言えると思います。

TerraformによるModule化の境界

Terraformに限らずIaCではインフラの単位をコードとして再利用しようとすると失敗しやすく、例えば「フロントエンド用のnginx」と「画像変換サーバー用nginx」を「同じnginxだから」と言って「同じmodule化」すると確実に失敗します。

今の例は見ただけで失敗だとわかると思いますが、ようは複雑性の意味が全然違うものを共通にしようとすると、その複雑性を隠蔽するためにより複雑な汎用化をしないといけなくて本末転倒になるという話です。

TerraformにおけるModule化は、「共通のものを1つにまとめる」という用途より、「名前空間の分離」を目的として設計するほうが上手くいくと考えております。

例えばTerraformでAWSリソースをModule化する場合、以下のようにするのが良いです。

f:id:nazone:20200502150041p:plain

上記の「アタッチされたサービス」の図とほぼ同じなのがわかるかと思います。責任分界点が事前に明確になっているので、その単位でModule化することによって、Module内でお互いのことを意識することがほとんどなくなります。Module間で必要な情報(エンドポイントなど)はvariablesとoutputで明確に書く必要がありますので、どの情報が外で使われるかも明確になります。

IAM(Role)やSG(セキュリティグループ)もModule毎に個別で作ることで、変に再利用されることがなく、最小の権限を個別に与えることができます。別環境の同リソース(staging/productionなど)や似たような権限のModuleでも必ず個別に作りましょう。

Moduleは汎用的に使えないの?

もちろんそんなことはありません。責任分界点がしっかりしているModuleは汎用的になります。一般的なWebサービスの構成はそんなにどれも違いがないのでModule単位で別プロジェクトに持ち出しなども簡単にできるようになると思います。以前紹介した terraform-aws-modules などの設計も参考にしてください。

参考

memo.yuuk.io

Unity で gRPC を試す( HelloworldUnity )

概要

Unity で gRPC を試すには、以前はいろいろ準備する必要があったようですが、現在は https://github.com/grpc/grpc/tree/master/examples/csharp/HelloworldUnity にサンプルが丸ごと入っていますので、これを動かすところまで試します。

手順

上記を UnityEditor で開きます。

README.md に書いてある通り、別途 grpc_csharp_unity.zip が必要です。指定場所からダウンロードして展開して出てきた Plugins ディレクトリをプロジェクトに突っ込みます。

エラーが出る

Error: Could not load signature of Google.Protobuf.ByteString:get_Span due to: Could not load file or assembly 'System.Memory, Version=4.0.1.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51' or one of its dependencies. assembly:System.Memory, Version=4.0.1.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51 type:<unknown type> member:(null) signature:<none>

こんな感じのエラーが出るのですが、grpc_unity_package.2.26.0-dev までは動作するが、それ以上のバージョンではエラーになるようです。いずれ解決する問題かどうかはよくわかりません。ちなみに grpc_unity_package.2.26.0-dev はここからダウンロードできます

動かす

f:id:nazone:20200505183848p:plain

動かすとこんな画面になります。

ちなみに Unity 側単体でサーバーとクライアントが両方起動しているので、通信はしているのですがよくわからないと思います。

ボタンを押すと表示が変わります。

f:id:nazone:20200505183906p:plain

サーバーを別で動かす

HelloWorldTest.cs に、Server server を new している一帯がありますので、そこを削除なりコメントアウトなりします。

同等のサーバーは https://github.com/grpc/grpc/tree/master/examples/csharp/Helloworld/GreeterServer にありますので、代わりにこちらをビルドして起動してから Unity 側を起動すると、いい感じで通信するようになります。

Webエンジニアとしてフリーランスしています

これまでのあらすじ(&お前誰よ)

UUUM株式会社を2019年10月に退職し、のんびりとした生活をしていましたが、本格的に働くことにしました。

能力や経歴などについてはポートフォリオを作りましたので、そちらをご覧頂ければと思います。

なぜフリーランス

働き方を自分で制御したいというのと、会社員という立場から一度離れていろいろな現場を見てみたい、そして私の力でそれを改善できるのか挑戦してみたい、という点から、しばらくフリーランスをすることを選びました。

特に最近は一人で全て解決することを求められることが多く、他の会社の視点ではどのようになっているのか、どのような他の良い方法があるのかというのをもっと知りたくなり、様々な現場を見てみたいという気持ちが強いです。

前にも少しやっていたのですが、当時の居住地や知名度では正直難しい(私の営業力不足も大きいですが…)状況で断念していました。

全く就職しないの?

そのうちすると思います。フリーランスという形態は自分には向いていないと思っておりますので、よほど軌道に乗って収入に困らない状態が続くとかでなければ就職するのではないかと思います。

その場合にしても、フリーランス中にお試しで仕事をしてマッチしたら入社という形態を取らせていただきたいと考えておりますので、興味がある会社様は先に声をかけていただいておくと良いと思います。

仕事募集してるの?

ありがたいことに今(2020年3月現在)は埋まっているので受けられないのですが、フリーランスなので状況はすぐ変わると思いますので、お仕事の相談をしたい方は先にご連絡頂ければと思います。本記事公開以降も状況が変わっているかもしれませんので、詳しくはお問い合わせ頂ければと思います。

原則としてリモートが可能な現場のみ受け付けております。たまには出社してほしいとかという場合でもお受けできますが、そのあたりは直接ご相談頂ければと思います。また最大でも週4まで、通常週2程度の稼働で受けております。

Terraform AWS modulesでざっくりAWSインフラを作る

https://github.com/terraform-aws-modules には、TerraformですぐにAWSリソースを立ち上げるためのModuleがいくつか存在します。

VPCの作成

terraform-aws-vpcでは、主に以下のリソースを作ってくれます。オプションでNATとかも用意してくれます。

module "vpc" {
  source = "terraform-aws-modules/vpc/aws"

  name = "test"
  cidr = "10.0.0.0/16"

  azs             = ["ap-northeast-1a", "ap-northeast-1c", "ap-northeast-1d"]
  private_subnets = ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"]
  public_subnets  = ["10.0.101.0/24", "10.0.102.0/24", "10.0.103.0/24"]

  enable_nat_gateway = false
  enable_vpn_gateway = false
}

ALBの作成

terraform-aws-albでは、主に以下のリソースを作ってくれます。

Security Group は作ってくれないので、自前で用意しましょう。

resource "aws_security_group" "alb" {
  name        = "alb"
  description = "alb"
  vpc_id      = module.vpc.vpc_id
}

module "alb" {
  source  = "terraform-aws-modules/alb/aws"
  version = "~> 5.0"

  name = "test"

  load_balancer_type = "application"

  vpc_id          = module.vpc.vpc_id
  subnets         = module.vpc.public_subnets
  security_groups = [aws_security_group.alb.id]

  target_groups = [
    {
      name             = "test"
      backend_protocol = "HTTP"
      backend_port     = 80
      target_type      = "ip"
      deregistration_delay = 10
    }
  ]

  http_tcp_listeners = [
    {
      port               = 80
      protocol           = "HTTP"
      target_group_index = 0
    }
  ]
}

ACM

terraform-aws-acmでは、ACM証明書を一発で作ってくれます。これは定型フォーマットなので、そのまま利用して問題ないでしょう。(手元にRoute53管理のドメインがなかったので試してはいません)

module "acm" {
  source  = "terraform-aws-modules/acm/aws"
  version = "~> v2.0"

  domain_name  = "my-domain.com"
  zone_id      = "Z2ES7B9AZ6SHAE"

  subject_alternative_names = [
    "*.my-domain.com",
    "app.sub.my-domain.com",
  ]

  tags = {
    Name = "my-domain.com"
  }
}

SNSのSlack通知

terraform-aws-notify-slackを使うと、SNSトピックをSlackに通知させるやつも簡単にTerraformで管理することが可能です。おそらくこれが一番使うのではないでしょうか。

使い方も簡単で、よくあるCloudWatch AlarmをSlackに通知するのは以下の書き方で実装できます。(手元に通知するものがなかったので試していません)

  
provider "aws" {
  region = "eu-west-1"
}

resource "aws_kms_key" "this" {
  description = "KMS key for notify-slack test"
}

# Encrypt the URL, storing encryption here will show it in logs and in tfstate
# https://www.terraform.io/docs/state/sensitive-data.html
resource "aws_kms_ciphertext" "slack_url" {
  plaintext = "https://hooks.slack.com/services/AAA/BBB/CCC"
  key_id    = aws_kms_key.this.arn
}

module "notify_slack" {
  source = "../../"

  sns_topic_name = "slack-topic"

  slack_webhook_url = aws_kms_ciphertext.slack_url.ciphertext_blob
  slack_channel     = "aws-notification"
  slack_username    = "reporter"

  kms_key_arn = aws_kms_key.this.arn

  lambda_description = "Lambda function which sends notifications to Slack"
  log_events         = true

  tags = {
    Name = "cloudwatch-alerts-to-slack"
  }
}

resource "aws_cloudwatch_metric_alarm" "LambdaDuration" {
  alarm_name          = "NotifySlackDuration"
  comparison_operator = "GreaterThanOrEqualToThreshold"
  evaluation_periods  = "1"
  metric_name         = "Duration"
  namespace           = "AWS/Lambda"
  period              = "60"
  statistic           = "Average"
  threshold           = "5000"
  alarm_description   = "Duration of notifying slack exceeds threshold"

  alarm_actions = [module.notify_slack.this_slack_topic_arn]

  dimensions = {
    FunctionName = module.notify_slack.notify_slack_lambda_function_name
  }
}

https://github.com/terraform-aws-modules/terraform-aws-notify-slack/blob/master/examples/cloudwatch-alerts-to-slack/main.tf

まとめ

公式とはいえ外部にあるmoduleだと、例えば最新の機能を入れたい場合にmoduleの反映を待たないといけないですし、そういう理由以外でもカスタマイズすることが難しく、また、moduleが勝手にアップデートされることが嬉しくないことも多いため、実際に使うのであればローカルにmoduleまるごどcloneして利用するのがいいのではないでしょうか。

SNSのSlack通知やACMに関してはほぼそのまま使っても問題ないと思います。

今はVPC一通りとALBがあればECS on Fargateでクラスタを作ればサービスを上げられるので(DBとかは別にして)、Terraformで管理したいけど書き方が難しいという場合は使ってみるのもありなのではないでしょうか(そんなに難しくないですが…)。