API GatewayをTerraformで構築するのはあまり向かないかも

AWS Lambda + API Gatewayでサーバーレスのメール送信関数を用意することにした。

そして、そのインフラ構築にTerraformを利用してみたのだが、結論、API GatewayとTerraformはどうにも相性がよくないように思われる。

なにが困るのか

  • 開発時、ガンガンデプロイしてトライ&エラーを繰り返したい開発の作業と、どうしても慎重な操作を必要とするTerraformはあんまり合ってない。
    • しかもAPI GatewayまわりはTerraformのコードも複雑で、AWSに慣れていても読み解くのに苦労する。
  • 本番は本番で、まるまる全て更新しようとするTerraformと、変更点は必要最小限に抑えたい本番デプロイという作業では方向性がずれている気がしてならない。うっかりAPI GatewayのデプロイIDを更新してしまうとデグレードを起こしてしまうという問題が実際に起きた。個人の開発環境なのでそこまで大きな問題ではなかったが、これを業務で起こすリスクを背負い続けるのは相当嫌なものがある。
  • Terraformで作るとなるとインフラエンジニアの得意分野になってくるわけだが、APIの構築や設計はアプリケーションエンジニアが自由にしたい部分。いちいちインフラエンジニアにお伺いを立てないとAPIの設計もままならない開発体制というのはいかがなものか?

ではどうするか

コンソールからの手作業は再現性が怪しくなるので、それはそれで嬉しくない。

そこで、API GatewayはTerraformを使わないか、使うにしても初期構築だけに留める。そしてCLIのようにコマンド化されつつもTerraformよりは柔軟に対応できるツールで運用するほうがよいのではないかと思われる。使ったことはないが、CDKももしかしたら便利かもしれない。

なんにせよ、根本的に、アプリケーションエンジニアがTerraformを使わねばならない開発体制は負担が大きくなりがちである。アプリケーションエンジニアが自由に作業できるべき業務については、Terraformのコードをゴリゴリ触らないと作業ができないよう設計するよりも、インフラエンジニア側が歩み寄って(つまりTerraformに一元化できなくなる点は妥協して)、アプリケーションエンジニアが作業しやすいよう開発体制を設計したほうがよいことが多いのでは、と個人的に思う。

Error: Image Optimization using Next.js' default loader is not compatible with `next export`.

課題

Next.jsで作成したアプリケーションを静的ウェブサイトとしてExportしたい。そこでpackage.jsonのbuildの箇所を書き換えた上で

"scripts": {
  "dev": "next dev",
  "build": "next build && next export", <----ここ
  "start": "next start",
  "lint": "next lint"
},

npm run buildしたところ、

npm run build

以下のようなエラーがでてExportに失敗する。

Error: Image Optimization using Next.js' default loader is not compatible with `next export`.
  Possible solutions:
    - Use `next start` to run a server, which includes the Image Optimization API.
    - Configure `images.unoptimized = true` in `next.config.js` to disable the Image Optimization API.
  Read more: https://nextjs.org/docs/messages/export-image-api
    at C:\MyFile\GitHub\personal-website\src\node_modules\next\dist\export\index.js:149:23
    at async Span.traceAsyncFn (C:\MyFile\GitHub\personal-website\src\node_modules\next\dist\trace\trace.js:79:20)

解決

next.config.jsで画像を最適化させないよう設定する。下記のimagesの項目の部分がそれ。

const nextConfig = {
    reactStrictMode: true,
    swcMinify: true,
    images: {
      unoptimized: true,
    },
  }

このようなことをする必要があるのは、Next.jsの仕様による

  This is because Next.js optimizes images on-demand, as users request them (not at build time).

(意訳: (デフォルトでは)Next.jsは画像をユーザーのリクエストがあった際にオンデマンドで最適化する。最適化が行われるのはビルド時じゃない)

はてなブログで画像を利用する

はてなブログに画像を投稿したいが、記事をすべてgit管理しているためブログ投稿画面から画像を投稿する通常の手順が使いづらい。

画像を大量にGitに保存するのもあまり好ましいことではない。というわけで少しばかりひねった解決策が必要となった。

やりたいこと

解決策

結論、まずはてなフォトライフに画像をアップロードし、それをはてなブログから読ませることにした。

考え方

方向性としては、クラウドストレージに画像を保存し、そこに直リンしてブログで画像を表示させるというもの。

S3に保存してもよかったのだが、いくらS3が安いとはいえ大量に長く使うとコストが馬鹿にならない。塵も積もれば山となる、である。宣伝を特にしない個人サイトでそこまで大量のPVが集まるとは思えないから、転送料も微々たるものだとは思うが、こういうところでいい加減にするクセを付けるのはよろしくない。

そこで廉価なサービスを探していたのだが、なんのことはない、はてなブログの付随サービス的なものとしてはてなフォトライフというサービスがあったため、これ幸いと採用した次第である。

余談: 転送料は意外に馬鹿にならない

大したアクセス数のないサービスならあまり問題にならない転送料だが、それなりの規模になると意外にいいお値段になる。CloudFrontだと2022/10/29現在1GBあたり17円弱くらい10TBまでかかるのだが、仮に毎日1万PVあり、1PVあたり1MB(0.001GB)の転送量が発生したとすると、一月あたりのコストは約5100円となる。

10,000pv * 0.001 gb * 30日 * 約17円/GB ≒ 5100円

ちなみにこういう場合、クラスメソッドのように割引を受けられるサービスを使うなどして少しでも安く上がるよう工夫する手がある。もちろん、とても大きなサービスを提供していて、自社で十分なボリュームディスカウントを得られるなら、素直に直接契約すれば済む。

Next.jsをインストールして動かしてみる。(Windows)

やったこと

手順はこちらにある。

  • https://nodejs.org/en/ からNode.jsをDownloadしてインストール
  • npx create-next-app@latest --typescriptと打ち、対話式でプロジェクト名を聞かれるため入力。今回はpersonal-website
    • typescriptを使いたいので、--typescriptをつける。
    • ディレクトリ名がpersonal-websiteで作られたが、srcにリネーム。
  • cd srcしてからnpm run dev

これだけで初期画面を表示させられた。意外に簡単。

20221029211513

AWS利用時のセキュリティについて考えた際のメモ(2022/10)

AWSのセキュリティ関連知識の復習のため、AWS認定 セキュリティ-専門知識を読んだときのメモ。

セキュリティ意識の高まり

数年前は影が薄かったセキュリティ用のサービスも、今や使って当然のものになりつつある。昔はCloudTrailとConfigさえも恐る恐るそっと提案せねばならなかったりしたものだが、今ではそれらはもちろん、GuardDutyやWAFなどもむしろ使った経験がないのはいかがなものかくらいの重要な扱いを受けている印象がある。いいことである。

CloudTrail, Config, GuardDuryなど、とりあえず有効化しておけばいい類のサービスは、時間を見てひとまとめにし、Terraformで一式作れるようにしておいてもいいかもしれない。

相変わらず敷居は高い

一方でセキュリティ向けのサービスはいずれも敷居が高い。その前提となっている考え方をわかっていないと、なにをどこまですればいいのか検討さえつかない。NIST サイバーセキュリティ フレームワーク (CSF) AWS クラウドにおける NIST CSF への準拠として公開されているが――ちなみにIPAFramework for Improving Critical Infrastructure Cybersecurity 重要インフラのサイバーセキュリティを 改善するためのフレームワークとして対訳を公開してくれている――どちらかというとマイナーな資料なのではないか。実際の開発現場は、動くものを作るのに精一杯で、セキュリティまで手が回らない(手だけでなく、頭もお金も)ことがしばしばだから、無理もないことだが、このあたりを上手くまとめつつ関係者各位に説明できるスキルが重要だろう。自分がちゃんと説明できるくらい理解しておくのは大前提として。

高まる管理コストにどう対応するか?

またセキュリティ系のサービスは管理コストもなかなかのものがある。セキュリティ専門のエンジニアがつける大規模プロジェクトや、または金融系のようにセキュリティリスクの高さが段違いのプロジェクトは、おそらくセキュリティのためのコストにも理解があると思われるからまだいいかもしれない。だが、そうでない開発プロジェクトだと、セキュリティのために乏しい予算と人的リソースを身を切る思いで(しぶしぶ)出しているのが実情ということもあろう。

が、そうはいっても、そのようなプロジェクトにおいてもセキュリティをないがしろにするわけにもいかない。

そんな場合は、なるべくそもそも守るものを減らすアーキテクチャや運用方式を選択するのが良い工夫であるように思われる。たとえばマネージドなサービスを活かしてEC2を自前で運用するのを極力避ければ、それだけセキュリティ監査のための工数は大きく減るだろう。また直接の手オペを減らしてなるべくツール化または完全自動化し、メンバーに渡す権限を限定したほうが、大きな権限を渡したメンバーの管理に気を遣うよりも安く上がるのではないか。守るものが少なければ、必要な気遣いも少なくて済む。

参考

.tfvarsをDRYに保つには?

課題

同じ設定であれば、設定ファイル(*.tfvars)をコピーして各ディレクトリに置くのではなく、共通の設定ファイルとして読み込むようにしたい。

だがTerraformに外部ファイルの読み込みらしき機能が見当たらない。どうすればよいか。

解決

実行時に-var-fileオプションを使う。

terraform apply -var-file="../shared.tfvars"

公式によると、オーバライドも可能であるらしい。

If the same variable is assigned multiple values, Terraform uses the last value it finds, overriding any previous values.

Terraform loads variables in the following order, with later sources taking precedence over earlier ones:

  • Environment variables
  • The terraform.tfvars file, if present.
  • The terraform.tfvars.json file, if present.
  • Any .auto.tfvars or .auto.tfvars.json files, processed in lexical order of their filenames.
  • Any -var and -var-file options on the command line, in the order they are provided. (This includes variables set by a Terraform Cloud workspace.)

意訳: 同じ変数が割り当てられていたら後から読んだ設定でオーバーライドする。環境変数 -> terraform.tfvars -> The terraform.tfvars.json -> *.auto.tfvars or *.auto.tfvars.json -> -var-var-fileオプションでの指定 の順で読むそうなので、-var-fileで指定するとそれが最優先となる。

つまり、-var-fileで共通の設定を読んで各ディレクトリに個別に設置したterraform.tfvarsでオーバーライドする、といった使い方は無理である。

どうしてもオーバーライドする処理が必要なら、Environment variablesのほうに共通の設定を書いておくしかないかもしれない。となると、インストール手順の違いで処理結果が変わるという恐ろしいことが起こるので、Terraform専用のインスタンスを作ったり、あるいはTerraform実行用のコンテナを使えるようDockerfileを用意したり、といった工夫も必要になるので、少々運用でのカバーが必要そうだ。

GitHub ActionsにAWS リソースへのアクセス権を渡すには?

課題

GitHub ActionsでAWSのリソースを操作したい場合、当然に権限を持つ必要がある。

どうすればいいか。

解決

その1 アクセスキーを払い出す

まず思いつくのは専用のIAMユーザーとAWSアクセスキーを作成し、GitHub Actionsに使わせること。

もちろんアクセスキーをハードコーディングするのは避けるべきで、利用する際はGithubのsecretsとして先に保存し、そこから読みだしたアクセスキーを設定する。

    deploy:
        runs-on: ubuntu-22.04
        strategy:
        matrix:
            python-version: ["3.9"]
        steps:
        - uses: actions/checkout@v3
        - name: aws-credentials
            uses: aws-actions/configure-aws-credentials@v1
            with:
            # この部分。
            aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
            aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
            aws-region: ${{ env.AWS_REGION }}

しかしこれだとアクセスキーの払い出しが必要となる。またアクセスキーの管理もずっとしなければならない。もちろん可能だが、避けられるなら避けたい。

その2  OpenID Connect + Role

素晴らしくもOpenID Connect方式でRoleを割り振ることができる。

AWS側でID プロバイダの設定を済ませた後、

    # Terraform用コード。尚変数を外から読み込んでいる関係で、これだけだと動かない。
    locals {
    deploy_name = "${var.environment.name}-github-actions-deploy"
    }

    # https://zenn.dev/yukin01/articles/github-actions-oidc-provider-terraform 参照
    data "http" "github_actions_openid_configuration" {
    url = "https://token.actions.githubusercontent.com/.well-known/openid-configuration"
    }

    data "tls_certificate" "github_actions" {
    url = jsondecode(data.http.github_actions_openid_configuration.body).jwks_uri
    }

    resource "aws_iam_openid_connect_provider" "github_actions_deploy" {
    url = "https://token.actions.githubusercontent.com"

    client_id_list = ["sts.amazonaws.com"]
    # https://docs.aws.amazon.com/ja_jp/IAM/latest/UserGuide/id_roles_providers_create_oidc_verify-thumbprint.html
    thumbprint_list = data.tls_certificate.github_actions.certificates[*].sha1_fingerprint
    }

    resource "aws_iam_role" "github_actions_deploy" {
    name                 = local.deploy_name
    path                 = "/"
    description          = "For deploy"
    assume_role_policy   = templatefile("${path.module}/templates/iam_role_github_actions_deploy_assume_role_policy.json", { repository_key = var.common.repository_key, aws_account_id = var.common.aws_account_id })
    managed_policy_arns  = concat([var.iam_policy_github_actions_deploy.arn], var.github_actions.additional_policys)
    max_session_duration = "3600"
    }

下記のようなGitHub Actions用設定で動かせる。

    env:
    AWS_REGION: "ap-northeast-1"
    AWS_ROLE_ARN: "${{ secrets.AWS_ROLE_ARN }}"

    # AWS OpenID Connect用
    permissions:
    id-token: write
    contents: read

    jobs:
    test-and-deploy:
        runs-on: ubuntu-22.04
        strategy:
        matrix:
            python-version: ["3.9"]
        steps:
        - uses: actions/checkout@v3
        # ~途中省略~
        - uses: aws-actions/configure-aws-credentials@v1
            with:
            role-to-assume: ${{ env.AWS_ROLE_ARN }}
            aws-region:  ${{ env.AWS_REGION }}
        - run: aws sts get-caller-identity

参考