kakakakakku blog

Weekly Tech Blog: Keep on Learning!

Trivy の Misconfiguration Scanning で Dockerfile の設定ミスを検出しよう

Trivy の「Misconfiguration Scanning」を使うと Dockerfile の設定ミス(セキュリティ課題やベストプラクティス乖離など)を検出できる❗️今回は Trivy を活用した Dockerfile のスキャンを試した作業ログをまとめる📝

aquasecurity.github.io

ちなみに Trivy の Misconfiguration Scanning は Terraform, AWS CloudFormation など複数のスキャンをサポートしている.Trivy x Terraform を試した記事は前に公開してあって,Trivy の設定や GitHub Actions ワークフローなどはほぼ同じで OK👌

kakakakakku.hatenablog.com

どんなルールがあるのか

以下の Vulnerability Database で確認できる.現時点では「26種類」がサポートされている👏

  • Least Privilege User
  • No Dist Upgrade
  • No Duplicate Alias
  • No Healthcheck
  • No Maintainer
  • No Orphan Package Update
  • No Self Referencing Copy From
  • No Ssh Port
  • No Sudo Run
  • Only One Cmd
  • Only One Entrypoint
  • Only One Healthcheck
  • Port Out Of Range
  • Purge Apk Package Cache
  • Purge Dnf Package Cache
  • Purge Microdnf Package Cache
  • Purge Yum Package Cache
  • Purge Zipper Cache
  • Standardise Remote Get
  • Use Apt Auto Confirm
  • Use Apt No Install Recommends
  • Use Copy Over Add
  • Use Slash For Copy Args
  • Use Specific Tags
  • Use Workdir Over Cd
  • User Absolute Workdir

avd.aquasec.com

Dockerfile ベストプラクティスに載っている Least Privilege UserUse Copy Over Add などを検出できたり,また Use Apt No Install Recommends などイメージサイズを削減するためのよく知られた Tips なども検出できる💡

docs.docker.com

docs.docker.com

docs.docker.com

さっそく試す

過去に個人的に開発をしたことがあるいくつかの Dockerfile に対して Trivy を実行してみて,検出された警告を紹介する.ちなみに今回は Trivy 0.45.0 を使う.

$ trivy --version
Version: 0.45.0

Severity(重大度): HIGH

🔗 Least Privilege User

root ユーザーを避けるべし!という警告🛑

HIGH: Last USER command in Dockerfile should not be 'root'

Running containers with 'root' user can lead to a container escape situation. It is a best practice to run containers as non-root users, which can be done by adding a 'USER' statement to the Dockerfile.

See https://avd.aquasec.com/misconfig/ds002

avd.aquasec.com

🔗 Use Apt No Install Recommends

apt-get コマンドを使っている場合はイメージサイズを削減するために –no-install-recommends を使おう!という警告🛑

HIGH: '--no-install-recommends' flag is missed: xxx

'apt-get' install should use '--no-install-recommends' to minimize image size.

See https://avd.aquasec.com/misconfig/ds029

avd.aquasec.com

Severity(重大度): MEDIUM

🔗 Use Specific Tags

latest タグを避けるべし!という警告🛑

MEDIUM: Specify a tag in the 'FROM' statement for image 'ubuntu'

When using a 'FROM' statement you should use a specific tag to avoid uncontrolled behavior when the image is updated.

See https://avd.aquasec.com/misconfig/ds001

avd.aquasec.com

🔗 Use Workdir Over Cd

RUN cd xxx のように書くなら WORKDIR を使おう!という警告🛑

MEDIUM: RUN should not be used to change directory: xxx

Use WORKDIR instead of proliferating instructions like 'RUN cd … && do-something', which are hard to read, troubleshoot, and maintain.

See https://avd.aquasec.com/misconfig/ds013

avd.aquasec.com

Severity(重大度): LOW

🔗 Use Copy Over Add

ADD ではなく COPY を使うべし!という警告🛑

LOW: Consider using 'COPY ./ /usr/local/xxx' command instead of 'ADD ./ /usr/local/xxx'

You should use COPY instead of ADD unless you want to extract a tar file. Note that an ADD command will extract a tar file, which adds the risk of Zip-based vulnerabilities. Accordingly, it is advised to use a COPY command, which does not extract tar files.

See https://avd.aquasec.com/misconfig/ds005

avd.aquasec.com

🔗 No Healthcheck

HEALTHCHECK を追加しよう!という警告🛑

LOW: Add HEALTHCHECK instruction in your Dockerfile

You should add HEALTHCHECK instruction in your docker container images to perform the health check on running containers.

See https://avd.aquasec.com/misconfig/ds026

avd.aquasec.com

ちなみに Dockerfile のヘルスチェック機能 (HEALTHCHECK) に関しては前に検証してブログにまとめてある💡

kakakakakku.hatenablog.com

不要な警告を減らす

Trivy の設定ファイル trivy.yaml を活用すると細かく挙動を制御できる.例えば Trivy で警告する Severity(重大度)の閾値を設定する場合は以下のように書くことで LOW / MEDIUM を無視できて警告を減らせる👏

severity:
  - HIGH
  - CRITICAL

もし特定の警告を無視する場合はまた別の設定ファイル .trivyignore に ID を設定すれば OK👌

AVD-DS-0002

個別に警告を無視する

Dockerfile にインラインコメントとして #trivy:ignore:... と書けば個別に警告を無視できる.以下は例として Least Privilege User の警告を例外的に無視するために #trivy:ignore:AVD-DS-0002 と書いた.

USER root #trivy:ignore:AVD-DS-0002

ちなみに以下のように直前に独立したコメントを書いたらうまく動かなかった💨

#trivy:ignore:AVD-DS-0002
USER root

Trivy を GitHub Actions で動かす

Trivy を GitHub Actions で動かす場合は「trivy-action」を使えば簡単に導入できる❗️

github.com

1点注意点としては Trivy は警告があってもデフォルトでは exit code 0 を返す.よって GitHub Actions をエラーにするために exit code 1 を返すように設定ファイル trivy.yamlexit-code: 1 と書いておく💡

exit-code: 1

severity:
  - HIGH
  - CRITICAL

単純な GitHub Actions ワークフローとしてはこんな感じかとー👀

name: Trivy

on:
  push:
    branches:
      - master
  pull_request:
    branches:
      - master

jobs:
  trivy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Run Trivy
        uses: aquasecurity/trivy-action@master
        with:
          scan-type: config
          trivy-config: trivy.yaml

実は以下の記事で Trivy の Misconfiguration Scanning を GitHub Actions で動かしたワークフローとまったく同じ👌

kakakakakku.hatenablog.com

hadolint (Haskell Dockerfile Linter)

ちなみに今までは Dockerfile の Linter として hadolint (Haskell Dockerfile Linter) を使っていた.GitHub Actions のワークフローに組み込んで自動化したり,新しく現場に参画したときに Dockerfile を読みながら hadolint を実行して改善の提案をしたり.

github.com

2020年頃にブログ記事を書いてるけど,hadolint は Bash の Linter として ShellCheck も同梱されてて,RUN の警告を検出しやすいという強みはあると思う💬

kakakakakku.hatenablog.com

まとめ

Trivy の Misconfiguration Scanning を使って Dockerfile の設定ミス(セキュリティ課題やベストプラクティス乖離など)を検出してみた❗️便利〜 \( 'ω')/

Dockerfile の Linter なら hadolint もあるけど,Trivy は Misconfiguration Scanning 以外にもたくさん機能があるため「開発組織で使うツールセットを Trivy に統一できる」というメリットがあると思う💬 個人的には Trivy を使えば使うほど機能の幅広さに驚かされていて,今後は Trivy を積極的に導入したいと思っていたりもする❗️どんどん試すぞ〜

新しく公開された Distributed Load Testing on AWS (DLT) ワークショップを試した

負荷テストを実行したいけど,ラップトップや Amazon EC2 インスタンス1台から実行すると負荷テストを実行する側がボトルネックになってしまって,期待した負荷テストにならないという悩みはよくあると思う🔥

そこで負荷テスト専用の SaaS などを活用して負荷テストを実現する案もあるけど,AWS では「AWS ソリューションライブラリ(サービスではない)」として負荷テストを実行・管理する「Distributed Load Testing on AWS (DLT)」が提供されている❗️

aws.amazon.com

僕自身は2年ほど前に Distributed Load Testing on AWS (DLT) を検証したことがあるけど,最近 Distributed Load Testing on AWS (DLT) を体験するワークショップ(日本語🇯🇵)が公開されたらしく,復習も兼ねてさっそく試してみた❗️

  1. Distributed Load Testing on AWS とは
  2. ワークショップの準備
  3. DLT での負荷テスト - 単一 URL
  4. DLT での負荷テスト - JMeter シナリオ
  5. リソースの削除

catalog.us-east-1.prod.workshops.aws

試した感想

まず,Distributed Load Testing on AWS (DLT) がどのようにデプロイされるかを把握できるようになるなーと感じた💡Distributed Load Testing on AWS (DLT) はソリューションとして AWS CloudFormation テンプレートが公開されていてワンクリックでデプロイできるとは言え,多くのサービスを組み合わせて実現するため,全体像は理解しにくいと思う.実際に Distributed Load Testing on AWS (DLT) を試したくても複雑だから諦めたという話も聞いたことがある💨そういう場合はこのワークショップを活用して Distributed Load Testing on AWS (DLT) の素振りをしておけば,仕事で使うときに実戦投入しやすくなる❗️

DLT のアーキテクチャ図を AWS での分散負荷テスト | AWS ソリューション より引用

そして「解説が充実している」のも素晴らしいポイントだと感じた💡JMeter とは?Taurus とは?という基本的な説明もあるし,負荷テストの結果から「どういう傾向が読み取れるか」という考察も丁寧にまとまっていた.ワークショップを進めることに集中しすぎると「結果が出てやったー」で次に進んでしまうこともあるけど,ちゃんと理解を深めながら進める工夫がされていて良かった❗️

さらに僕自身はワークショップ手順で試す "1回目" と "2回目" に加えて Task Count / Concurrency / Hold For の値を変えてみて追加の実行をしてみたり,Distributed Load Testing on AWS (DLT) がどういう仕組みで動いているのかを調べたりという「追加課題」も考えて試してみた.時間があったらやってみるとさらに理解が深まると思う.例えば AWS Step Functions ステートマシンはよくできてて参考になる.

Distributed Load Testing on AWS (DLT) をもっと理解するには

日本語はなく英語になってしまうけど Distributed Load Testing on AWS (DLT) のドキュメント「Implementation Guide」に仕組みなどが詳しくまとまっていて,1度読んでおくと良いと思う.例えば,コスト的には1ヶ月で $30.90 程度だよ〜という情報や AWS サポートに問い合わせもできるよ〜という情報も載っている👀

docs.aws.amazon.com

実際のコードなどは GitHub に公開されている👀

github.com

他には AWS Architecture Blog や YouTube にも Distributed Load Testing on AWS (DLT) 関連情報が公開されている👀

aws.amazon.com

www.youtube.com

ワークショップ実施ログ

今回負荷テストを実行する対象となる「TODO アプリケーション」をデプロイできた👌

AWS CDK でデプロイされてて,アプリケーションは Application Load Balancer (ALB) と Amazon ECS で実装されてる〜

Distributed Load Testing on AWS (DLT) の「管理画面」をデプロイできた👌

テストタイプ Single HTTP Endpoint を選んで /api/alltodo API に対して GET リクエストの負荷テストを実行できた👌

  • 1回目
    • Task Count: 1
    • Concurrency: 1
  • 2回目
    • Task Count: 5
    • Concurrency: 5

テストタイプ JMeter を選んで /api/alltodo API (GET) と /api/createtodo API (POST) と /api/updatetodo/${id} API (POST) に対してシナリオを使った負荷テストを実行できた👌

  • 1回目
    • Task Count: 1
    • Concurrency: 4
  • 2回目
    • Task Count: 5
    • Concurrency: 20

JMeter の統計レポート aggregate.csv を Jupyter Notebook で可視化できた👌

誤植など

ワークショップ手順にフィードバックを送る方法が書かれてなく,試しながら気になった誤植をまとめておく📝

  • 表記揺れ(ToDoTodotodo
  • 所用時間所要時間
  • 表記揺れ(試験対象テスト対象
  • 増設じます。増設します。
  • SucessSuccess
  • するロールスクロール
  • 面左画面左
  • Container InsighsContainer Insights

まとめ

Distributed Load Testing on AWS (DLT) を体験するワークショップを試してみた❗️AWS で負荷テスト環境を構築したくて Distributed Load Testing on AWS (DLT) を導入したいけど,どうやって活用するのかわからないという人に最適な入門コンテンツだった.所要時間としては2,3時間あれば終わると思う🕐

ちなみに今回ワークショップを実施してみて,環境削除は少し面倒だった.デプロイするときは AWS CloudFormation で一発なのに,削除時は DeletionPolicy の設定で残ってしまうリソースも多くポチポチと削除する必要があった.削除用のスクリプトまで用意されてたらもっと嬉しいのになぁ〜💨 あと最終的には ap-northeast-1 リージョンでワークショップを実施したけど,最初 us-east-1 リージョンで構築しようとしたら AWS CloudFormation で DltWorkshopTargetStack スタックの作成に失敗して削除もうまくできず,スタックが DELETE_FAILED 状態で残ってしまったりもして環境構築も少し苦労した🔥

まとめると Distributed Load Testing on AWS (DLT) ワークショップおすすめでーす \( 'ω')/

catalog.us-east-1.prod.workshops.aws

Terraform で AWS Systems Manager Inventory を有効化する

AWS Systems Manager Inventory の有効化(AWS Systems Manager State Manager の関連付け)を Terraform で構築する場合 aws_ssm_association リソースを使って設定できる💡

aws_ssm_associationparameters に関しては Terraform のドキュメントには詳しく載っていないため,マネジメントコンソールの「セットアップインベントリ」画面と AWS Systems Manager DocumentsAWS-GatherSoftwareInventory を参考にしながら設定すると良いと思う❗️

docs.aws.amazon.com

Terraform コード

設定としては以下のように仮置きしている👌

  • 収集対象にする Amazon EC2 インスタンスにはタグ Inventory: true を追加しておく
  • 1時間に1回実行する(早めに動作確認ができるように)
  • Windows は対象外にする
resource "aws_ssm_association" "inventory" {
  name = "AWS-GatherSoftwareInventory"
  association_name = "Inventory"

  targets {
    key    = "tag:Inventory"
    values = ["true"]
  }

  parameters = {
    applications                = "Enabled"
    awsComponents               = "Enabled"
    files                       = ""
    networkConfig               = "Enabled"
    windowsUpdates              = "Disabled"
    instanceDetailedInformation = "Enabled"
    services                    = "Disabled"
    windowsRegistry             = ""
    windowsRoles                = "Disabled"
    customInventory             = "Enabled"
    billingInfo                 = "Enabled"
  }

  schedule_expression = "rate(1 hour)"
}

結果

期待通りに構築できて,AWS Systems Manager Inventory でメタデータも収集できた❗️

参考: AWS Systems Manager Documents

AWS Systems Manager Documents の AWS-GatherSoftwareInventory のコンテンツ(バージョン1)を載せておく.Terraform で指定する EnabledDisabled という値の期待値やデフォルト値などを確認できる💡

{
  "schemaVersion": "2.0",
  "description": "Software Inventory Policy Document.",
  "parameters": {
    "applications": {
      "type": "String",
      "default": "Enabled",
      "description": "(Optional) Collect data for installed applications.",
      "allowedValues": [
        "Enabled",
        "Disabled"
      ]
    },
    "awsComponents": {
      "type": "String",
      "default": "Enabled",
      "description": "(Optional) Collect data for AWS Components like amazon-ssm-agent.",
      "allowedValues": [
        "Enabled",
        "Disabled"
      ]
    },
    "files": {
      "type": "String",
      "default": "",
      "description": "<p>(Optional, requires SSMAgent version 2.2.64.0 and above)<br/><br/>Linux example:<br/><em>[{\"Path\":\"/usr/bin\", \"Pattern\":[\"aws*\", \"*ssm*\"],\"Recursive\":false},{\"Path\":\"/var/log\", \"Pattern\":[\"amazon*.*\"], \"Recursive\":true, \"DirScanLimit\":1000}]<br/></em><br/>Windows example:<br/><em>[{\"Path\":\"%PROGRAMFILES%\", \"Pattern\":[\"*.exe\"],\"Recursive\":true}]</em><br/><br/>Learn More: http://docs.aws.amazon.com/systems-manager/latest/userguide/sysman-inventory-about.html#sysman-inventory-file-and-registry  </p>",
      "displayType": "textarea"
    },
    "networkConfig": {
      "type": "String",
      "default": "Enabled",
      "description": "(Optional) Collect data for Network configurations.",
      "allowedValues": [
        "Enabled",
        "Disabled"
      ]
    },
    "windowsUpdates": {
      "type": "String",
      "default": "Enabled",
      "description": "(Optional, Windows OS only) Collect data for all Windows Updates.",
      "allowedValues": [
        "Enabled",
        "Disabled"
      ]
    },
    "instanceDetailedInformation": {
      "type": "String",
      "default": "Enabled",
      "description": "(Optional) Collect additional information about the instance, including the CPU model, speed, and the number of cores, to name a few.",
      "allowedValues": [
        "Enabled",
        "Disabled"
      ]
    },
    "services": {
      "type": "String",
      "default": "Enabled",
      "description": "(Optional, Windows OS only, requires SSMAgent version 2.2.64.0 and above) Collect data for service configurations.",
      "allowedValues": [
        "Enabled",
        "Disabled"
      ]
    },
    "windowsRegistry": {
      "type": "String",
      "default": "",
      "description": "<p>(Optional, Windows OS only, requires SSMAgent version 2.2.64.0 and above)<br/><br/>Example:<br />[{\"Path\":\"HKEY_CURRENT_CONFIG\\System\",\"Recursive\":true},{\"Path\":\"HKEY_LOCAL_MACHINE\\SOFTWARE\\Amazon\\MachineImage\", \"ValueNames\":[\"AMIName\"]}]<br/><br/>Learn More: http://docs.aws.amazon.com/systems-manager/latest/userguide/sysman-inventory-about.html#sysman-inventory-file-and-registry </p>",
      "displayType": "textarea"
    },
    "windowsRoles": {
      "type": "String",
      "default": "Enabled",
      "description": "(Optional, Windows OS only, requires SSMAgent version 2.2.64.0 and above) Collect data for Microsoft Windows role configurations.",
      "allowedValues": [
        "Enabled",
        "Disabled"
      ]
    },
    "customInventory": {
      "type": "String",
      "default": "Enabled",
      "description": "(Optional) Collect data for custom inventory.",
      "allowedValues": [
        "Enabled",
        "Disabled"
      ]
    },
    "billingInfo": {
      "type": "String",
      "default": "Enabled",
      "description": "(Optional) Collect billing info for license included applications.",
      "allowedValues": [
        "Enabled",
        "Disabled"
      ]
    }
  },
  "mainSteps": [
    {
      "action": "aws:softwareInventory",
      "name": "collectSoftwareInventoryItems",
      "inputs": {
        "applications": "{{ applications }}",
        "awsComponents": "{{ awsComponents }}",
        "networkConfig": "{{ networkConfig }}",
        "files": "{{ files }}",
        "services": "{{ services }}",
        "windowsRoles": "{{ windowsRoles }}",
        "windowsRegistry": "{{ windowsRegistry}}",
        "windowsUpdates": "{{ windowsUpdates }}",
        "instanceDetailedInformation": "{{ instanceDetailedInformation }}",
        "billingInfo": "{{ billingInfo }}",
        "customInventory": "{{ customInventory }}"
      }
    }
  ]
}

Goss: サーバー設定の検証を自動化しよう

歴史的経緯 (?) によって構成ドリフト状態(Infrastructure as Code 本参照)になっている Amazon EC2 インスタンスが複数台あって,まずはサーバーの期待値を宣言して自動テストもしくは自動検証をする仕組みを作りたいなぁーと考えていた💡

今までの経験としては,2014-2016年頃によく使っていて,過去にブログ記事を書いていたりもする Serverspec を思い出しつつ,Ruby に依存せず実行ファイルをポンッと置いたらすぐに使えるようなツールがあったら良いなぁーと思って探していたところ,Go で実装されていて,サーバーの期待値を YAML で宣言できる「Goss」を発見した👀 実際に試してみたらとても便利で最高だったので,検証ログをまとめて公開することにした❗️

github.com

検証環境

今回は別の検証環境として使っていた Amazon EC2 インスタンス (Amazon Linux 2023 AMI) を使う.

セットアップ

README を参考にセットアップする.現状だと v0.3.23 になった.

$ curl -fsSL https://goss.rocks/install | sh

$ goss --version
goss version v0.3.23

しかし README に Using curl | sh is not recommended for production systems と書いてあるため,本番環境などに使う場合はしっかりと VERSION を固定してインストールする👌 現時点で最新は v0.4.2 で,今回はこちらを使う.

$ VERSION=v0.4.2
$ curl -L "https://github.com/goss-org/goss/releases/download/${VERSION}/goss-linux-amd64" -o /usr/local/bin/goss
$ chmod +rx /usr/local/bin/goss

$ goss --version
goss version v0.4.2

Goss でサポートしてるリソース

Goss では「サーバーの期待値を宣言できる」と書いたけど,実際には以下のように多くのリソースがサポートされている❗️

  • addr: google.com:80 などリモートアドレスに疎通できるか検証する
  • command: go version など任意のコマンドを実行した結果(標準出力・標準エラー・終了ステータスなど)を検証する
  • dns: A レコードや CNAME レコードなど名前解決の結果を検証する
  • file: ファイルやディレクトリの存在や owner / group / mode などの設定を検証する
  • group: Linux グループなどグループの設定を検証する
  • http: HTTP リクエストを送信してレスポンス(ステータスコードなど)を検証する
  • interface: eth0 などネットワークインタフェースの設定を検証する
  • kernel-param: sysctl コマンドで設定できるカーネルパラメータを検証する
  • mount: ファイルシステムのマウントポイントを検証する
  • matching: matcher 構文を使って任意の値を検証する
  • package: httpd などパッケージを検証する
  • port: tcp:22 などポートの状態を検証する
  • process: プロセスの状態を検証する
  • service: サービスの状態を検証する
  • user: Linux ユーザーなどユーザーの設定を検証する

リソースの詳細やリソースごとの構文などは以下のドキュメント(マニュアル)に詳しくに載っている📝

github.com

Goss を試す

さっそく Goss を試していく❗️

🌷 サンプル: sshd

まずは README の Quick start に載っている sshd のサンプルを試す❗️

goss autoadd コマンドを使うと環境に最適な Goss ファイル goss.yaml を自動生成してくれる.

$ goss autoadd sshd

自動生成された goss.yaml は以下のようになっていた.自動的に port / service / user / group / process の検証項目が追加されていた👏

port:
  tcp:22:
    listening: true
    ip:
    - 0.0.0.0
  tcp6:22:
    listening: true
    ip:
    - '::'
service:
  sshd:
    enabled: true
    running: true
user:
  sshd:
    exists: true
    uid: 74
    gid: 74
    groups:
    - sshd
    home: /usr/share/empty.sshd
    shell: /sbin/nologin
group:
  sshd:
    exists: true
    gid: 74
process:
  sshd:
    running: true

Goss で検証を実行する場合は goss validate コマンドを実行する.今回は Failed: 0 となって「すべて成功」していた.

$ goss validate
...............

Total Duration: 0.021s
Count: 15, Failed: 0, Skipped: 0

期待通りの結果にならない状態にするため,意図的に sshd を止めて,もう一度 goss validate コマンドを実行すると,今度は Failed: 4「一部失敗」していた.

$ systemctl stop sshd

$ goss validate
FSFSF.........F

Failures/Skipped:

Port: tcp6:22: listening:
Expected
    false
to equal
    true
Port: tcp6:22: ip: skipped

Port: tcp:22: listening:
Expected
    false
to equal
    true
Port: tcp:22: ip: skipped

Process: sshd: running:
Expected
    false
to equal
    true

Service: sshd: running:
Expected
    false
to equal
    true

Total Duration: 0.022s
Count: 15, Failed: 4, Skipped: 2

🌷 サンプル: amazon-ssm-agent

次は goss add コマンドを使って amazon-ssm-agent の Goss ファイルを生成する❗️

goss add コマンドは goss autoadd コマンドと違って packageservice など具体的なリソースを指定して Goss ファイルを生成する.そして goss コマンドに --gossfile オプションを付けるとデフォルトの goss.yaml 以外のファイルを対象にできる.今回は amazon-ssm-agent の検証に限定するため,ファイル名を変えて Goss ファイル goss-ssm.yaml を生成する.

$ goss --gossfile goss-ssm.yaml add package amazon-ssm-agent
$ goss --gossfile goss-ssm.yaml add service amazon-ssm-agent

自動生成された goss-ssm.yaml は以下のようになっていた.

package:
  amazon-ssm-agent:
    installed: true
    versions:
    - 3.2.1377.0-1.amzn2023
service:
  amazon-ssm-agent:
    enabled: true
    running: true

さらに file リソースも試したかったため,amazon-ssm-agent によって出力されるログファイル /var/log/amazon/ssm/amazon-ssm-agent.log も追加する.

$ goss --gossfile goss-ssm.yaml add file /var/log/amazon/ssm/amazon-ssm-agent.log

最終的に goss-ssm.yaml は以下のようになった.

file:
  /var/log/amazon/ssm/amazon-ssm-agent.log:
    exists: true
    mode: "0644"
    owner: root
    group: root
    filetype: file
    contents: []
package:
  amazon-ssm-agent:
    installed: true
    versions:
    - 3.2.1377.0-1.amzn2023
service:
  amazon-ssm-agent:
    enabled: true
    running: true

🌷 サンプル: openssl

今度は command リソースを試してみたく,openssl version コマンドを実行した結果(標準出力)の一致を検証する Goss ファイルを生成する❗️

$ goss --gossfile goss-openssl.yaml add command 'openssl version'

自動生成された goss-openssl.yaml は以下のようになっていた.

command:
  openssl version:
    exit-status: 0
    stdout:
    - 'OpenSSL 3.0.8 7 Feb 2023 (Library: OpenSSL 3.0.8 7 Feb 2023)'
    stderr: ""
    timeout: 10000

🌷 サンプル: gossfile で Goss ファイルをまとめる

最初に生成した Goss ファイル goss.yamlgoss-sshd.yaml にリネームしてから,新しく以下の YAML で goss.yaml を作る.gossfile リソースでは指定した Goss ファイルをまとめることができるため,Goss ファイルを細かく分割しつつ,対象のサーバーに必要な検証項目をまとめて宣言できる.これは便利❗️

gossfile:
  goss-openssl.yaml: {}
  goss-sshd.yaml: {}
  goss-ssm.yaml: {}

現時点で Goss ファイルは4つある.

$ ls -1
goss-openssl.yaml
goss-sshd.yaml
goss-ssm.yaml
goss.yaml

このまま goss validate コマンドを実行すると Count: 27 と表示されている通り,3つの Goss ファイルの検証項目をすべて実行できている \( 'ω')/ 良いじゃん〜

$ goss validate
...........................

Total Duration: 0.086s
Count: 27, Failed: 0, Skipped: 0

Goss エンドポイントを公開する

Goss で特徴的な(おもしろい)機能の一つとして goss serve コマンドがある.以下のように実行すると Goss の検証結果を取得できる API を起動できる❗️そして検証結果のフォーマットとしては jsonjunitprometheus などを指定できるため,Goss の検証結果を他のサービスと連携できるようになっているのは拡張性がありそう \( 'ω')/

rspecish フォーマット API(デフォルト)

$ goss serve

$ curl http://localhost:8080/healthz
...........................

Total Duration: 0.077s
Count: 27, Failed: 0, Skipped: 0

json フォーマット API

$ goss serve --format json

$ curl -s http://localhost:8080/healthz | jq .summary
{
  "failed-count": 0,
  "skipped-count": 0,
  "summary-line": "Count: 27, Failed: 0, Skipped: 0, Duration: 0.080s",
  "test-count": 27,
  "total-duration": 79993971
}

まとめ

サーバーの期待値を YAML で宣言して自動検証できる「Goss」を試してみた❗️最高〜 \( 'ω')/

現時点で Goss はリモート実行には対応してなく,検証するサーバーに Goss をセットアップして実行する必要があるため,AWS Systems Manager Run Command を使って Goss を実行する仕組みを検証しているところ.また別の記事にまとめようと思う📝

正式リリースになった AWS SAM CLI の Terraform サポート機能を試す

2023年9月5日に AWS SAM CLI の Terraform サポート機能が GA (正式リリース)になった👏

Amazon API Gateway や AWS Lambda 関数などサーバーレス関連のコンポーネントは Terraform で統一的に管理しつつも,AWS SAM CLI の開発支援機能(sam local invoke コマンドや sam local start-api コマンドでローカルデバッグ)は使いたい❗️という場面はあって非常に便利な組み合わせだと思う.

aws.amazon.com

実際にどういう開発体験なのかを確認するために AWS ブログに載っていたサンプルを試してみる \( 'ω')/

aws.amazon.com

検証環境

今回は macOS 上で SAM CLI 1.97.0(最新)と Terraform 1.5.7(最新)を使う.

$ sam --version
SAM CLI, version 1.97.0

$ terraform version
Terraform v1.5.7
on darwin_arm64

GitHub リポジトリ確認

サンプルコードは GitHub aws-samples/aws-sam-terraform-examples リポジトリの ga ディレクトリにある❗️

github.com

ga ディレクトリには3種類のサンプルがあって,今回は Amazon API Gateway の REST API 前提の api_gateway_v1 を使う📝

$ tree ga -L 1
ga
├── README.md
├── api_gateway_v1
├── api_gateway_v2
└── api_gateway_v2_tf_cloud

4 directories, 1 file

さらに ga/api_gateway_v1 ディレクトリ配下のファイルをまとめた.tf-resources ディレクトリに main.tf など Terraform コードがあって,src ディレクトリに AWS Lambda 関数コード(今回は Python 実装)があって,events ディレクトリに AWS Lambda 関数 auth の動作確認に使うイベント情報がある👌

$ tree ga/api_gateway_v1
ga/api_gateway_v1
├── events
│   └── auth.json
├── src
│   ├── auth
│   │   ├── app.py
│   │   └── requirements.txt
│   └── responder
│       ├── app.py
│       └── requirements.txt
└── tf-resources
    ├── api.tf
    ├── functions.tf
    ├── main.tf
    ├── samconfig.yaml
    └── variables.tf

6 directories, 10 files

事前準備

サンプルコードを確認したところ,Python 3.9 の AWS Lambda 関数になっていた.せっかくなら最新の Python 3.11 で動作確認をしておきたくて ga/api_gateway_v1/tf-resources/functions.tf を修正した.

--- a/ga/api_gateway_v1/tf-resources/functions.tf
+++ b/ga/api_gateway_v1/tf-resources/functions.tf
@@ -5,7 +5,7 @@ module "lambda_function_responder" {
   source_path   = "../src/responder/"
   function_name = "responder"
   handler       = "app.open_handler"
-  runtime       = "python3.9"
+  runtime       = "python3.11"
   create_sam_metadata = true
   publish       = true
   allowed_triggers = {
@@ -23,6 +23,6 @@ module "lambda_function_auth" {
   source_path   = "../src/auth/"
   function_name = "authorizer"
   handler       = "app.handler"
-  runtime       = "python3.9"
+  runtime       = "python3.11"
   create_sam_metadata = true
 }

しかし AWS Provider 側で Python 3.11 がサポートされてなく,sam build コマンドの実行時に以下のエラーが出てしまう🔥

Error: expected runtime to be one of [nodejs nodejs4.3 nodejs6.10 nodejs8.10 nodejs10.x nodejs12.x nodejs14.x nodejs16.x java8 java8.al2 java11 python2.7 python3.6 python3.7 python3.8 python3.9 dotnetcore1.0 dotnetcore2.0 dotnetcore2.1 dotnetcore3.1 dotnet6 nodejs4.3-edge go1.x ruby2.5 ruby2.7 provided provided.al2 nodejs18.x python3.10 java17], got python3.11

そこで AWS Provider の最新を使えるようにga/api_gateway_v1/tf-resources/main.tf を修正して terraform init -upgrade コマンドを実行しておく.

--- a/ga/api_gateway_v1/tf-resources/main.tf
+++ b/ga/api_gateway_v1/tf-resources/main.tf
@@ -2,7 +2,7 @@ terraform {
   required_providers {
     aws = {
       source  = "hashicorp/aws"
-      version = "~> 4.16"
+      version = "~> 5.17"
     }
   }

準備完了👏

さっそく試す: sam buildsam local invoke

sam build コマンド

Terraform コードや設定など細かいことは後ほど確認するとして,まずは試してみる❗️

ga/api_gateway_v1/tf-resources ディレクトリに移動して,Terraform 変数にリージョンを指定して(今回は東京リージョンを使う),あとは使い慣れた sam build コマンドを実行する.しかし今回は --hook-name オプションで terraform を指定して実行する点に注意💡

実行すると Terraform の local-exec Provisioner の実行ログが大量に出てきて,不安になる(Ctrl+C を押したくなる)けどそのまま待ってると Build Succeeded という表示が出て完了する.

$ cd ga/api_gateway_v1/tf-resources

$ export TF_VAR_aws_region=ap-northeast-1

$ sam build --hook-name terraform --terraform-project-root-path ../
Running Prepare Hook to prepare the current application
Executing prepare hook of hook "terraform"

(中略)

module.lambda_function_auth.null_resource.archive[0] (local-exec):
module.lambda_function_responder.null_resource.archive[0] (local-exec):

(中略)

Apply complete! Resources: 3 added, 0 changed, 0 destroyed.

Build Succeeded

Built Artifacts  : .aws-sam/build
Built Template   : .aws-sam/build/template.yaml

sam local invoke コマンド

そして使い慣れた sam local invoke コマンドに --hook-name terraform オプションを指定して実行すると,public.ecr.aws/lambda/python:3.11-rapid-x86_64 イメージを使って AWS Lambda 関数を実行できる.今回は responder を実行したら Hello TF World って返ってきた \( 'ω')/ うおお〜

$ sam local invoke --hook-name terraform 'module.lambda_function_responder.aws_lambda_function.this[0]'
Skipped prepare hook. Current application is already prepared.
Invoking app.open_handler (python3.11)
Local image was not found.
Removing rapid images for repo public.ecr.aws/sam/emulation-python3.11
Building image.............................................................................................................................................................................................................................................................................................................................................................................................................................
Using local image: public.ecr.aws/lambda/python:3.11-rapid-x86_64.

(中略)

START RequestId: 71b6ab8c-53f9-41ac-8fb0-62bf4ba929b4 Version: $LATEST
END RequestId: 71b6ab8c-53f9-41ac-8fb0-62bf4ba929b4
REPORT RequestId: 71b6ab8c-53f9-41ac-8fb0-62bf4ba929b4  Init Duration: 0.74 ms    Duration: 1551.46 ms  Billed Duration: 1552 ms   Memory Size: 128 MB    Max Memory Used: 128 MB
{"statusCode": 200, "body": "{\"message\": \"Hello TF World\", \"location\": \"xxx.xxx.xxx.xxx\"}"}%

せっかくだから ga/api_gateway_v1/src/responder/app.py を修正して,もう一度 sam build コマンドと sam local invoke コマンドを実行すると,今度は Hello kakakakakku World って返ってきた \( 'ω')/ うおお〜

$ sam build --hook-name terraform --terraform-project-root-path ../

$ sam local invoke --hook-name terraform 'module.lambda_function_responder.aws_lambda_function.this[0]'
Skipped prepare hook. Current application is already prepared.
Invoking app.open_handler (python3.11)
Local image is up-to-date
Using local image: public.ecr.aws/lambda/python:3.11-rapid-x86_64.

(中略)

START RequestId: f8ddd88f-bfd0-45c5-8643-6db922e6d798 Version: $LATEST
END RequestId: f8ddd88f-bfd0-45c5-8643-6db922e6d798
REPORT RequestId: f8ddd88f-bfd0-45c5-8643-6db922e6d798  Init Duration: 0.57 ms    Duration: 1201.38 ms  Billed Duration: 1202 ms   Memory Size: 128 MB    Max Memory Used: 128 MB
{"statusCode": 200, "body": "{\"message\": \"Hello kakakakakku World\", \"location\": \"xxx.xxx.xxx.xxx\"}"}%

そして同じように auth も実行できた \( 'ω')/ うおお〜

$ sam local invoke --hook-name terraform 'module.lambda_function_auth.aws_lambda_function.this[0]' -e ../events/auth.json
Skipped prepare hook. Current application is already prepared.
Invoking app.handler (python3.11)
Local image is up-to-date
Using local image: public.ecr.aws/lambda/python:3.11-rapid-x86_64.

(中略)

START RequestId: c6503ad1-200d-453a-96d9-94c99d0e470a Version: $LATEST
END RequestId: c6503ad1-200d-453a-96d9-94c99d0e470a
REPORT RequestId: c6503ad1-200d-453a-96d9-94c99d0e470a  Init Duration: 0.52 ms    Duration: 269.49 ms   Billed Duration: 270 ms    Memory Size: 128 MB    Max Memory Used: 128 MB
{"principalId": "user|a1b2c3d4", "policyDocument": {"Version": "2012-10-17", "Statement": [{"Action": "execute-api:Invoke", "Effect": "Allow", "Resource": ["arn:aws:execute-api:us-east-1:123456789012:1234567890/prod/*/*"]}]}, "context": {"number": 1, "bool": true, "passed": "from authorizer"}}%

また sam local invoke コマンドの引数に実行する AWS Lambda 関数の Terraform リソース ID を指定する必要があるけど,指定なしで実行すると候補を表示してくれるようになってて親切設計だった👏

$ sam local invoke --hook-name terraform

Skipped prepare hook. Current application is already prepared.
Error: You must provide a function logical ID when there are more than one functions in your template. Possible options in your template: ['module.lambda_function_auth.aws_lambda_function.this[0]', 'module.lambda_function_responder.aws_lambda_function.this[0]']

ちなみに Terraform と AWS SAM CLI の連携に関しては以下のドキュメントに詳しく載っている📝 現時点だと最新情報は日本語に翻訳されてなく,まだ GA 前のドキュメントになっているため,英語を確認する必要がある.

docs.aws.amazon.com

samconfig ファイル

AWS SAM CLI でよく使う samconfig にも対応していて,例えば以下のように samconfig.toml を設定できる.

version = 0.1
[default]
[default.global]
[default.global.parameters]
hook_name = "terraform"
skip_prepare_infra = true
[default.build]
[default.build.parameters]
terraform_project_root_path = "../"

すると sam build コマンドと sam local invoke コマンドのオプションを減らせてシンプルに実行できるようになる👏

$ sam build
$ sam local invoke 'module.lambda_function_responder.aws_lambda_function.this[0]'

ちなみに2023年7月頃から AWS SAM CLI で YAML 形式の samconfig もサポートされててサンプルには samconfig.yaml が含まれた.

version: 0.1
default:
  global:
    parameters:
      hook_name: terraform
      skip_prepare_infra: true
  build:
    parameters:
      terraform_project_root_path: ../

さっそく試す: sam local start-api

今度は sam local start-api コマンドを実行して API の動作確認をする❗️

$ sam local start-api --hook-name terraform
Skipped prepare hook. Current application is already prepared.

AWS SAM CLI does not guarantee 100% fidelity between authorizers locally
and authorizers deployed on AWS. Any application critical behavior should
be validated thoroughly before deploying to production.

Testing application behaviour against authorizers deployed on AWS can be done using the sam sync command.

Mounting responder at http://127.0.0.1:3000/open [GET]
Mounting responder at http://127.0.0.1:3000/secure [GET]

(中略)

2023-09-19 22:00:00 Press CTRL+C to quit

すぐに localhost:3000/open API と /secure API を呼び出せた \( 'ω')/ うおお〜

ちなみに /secure API は Amazon API Gateway の Lambda オーソライザーを使ったアクセス制御になっているため,HTTP ヘッダーに myheader: 123456789 を設定した場合にレスポンスが返るように実装されている.

$ curl http://localhost:3000/open
{"message": "Hello kakakakakku World", "location": "xxx.xxx.xxx.xxx"}

$ curl http://localhost:3000/secure
{"message":"Unauthorized"}

$ curl http://localhost:3000/secure --header 'myheader: 123456789'
{"message": "Hello kakakakakku World", "location": "xxx.xxx.xxx.xxx"}

$ curl http://localhost:3000/secure --header 'myheader: IamInvalid'
{"message":"Unauthorized"}

Terraform コード

うまく実行できたので,次は Terraform コードを確認する❗️

module "lambda_function_responder" {
  source        = "terraform-aws-modules/lambda/aws"
  version       = "~> 6.0"
  timeout       = 300
  source_path   = "../src/responder/"
  function_name = "responder"
  handler       = "app.open_handler"
  runtime       = "python3.11"
  create_sam_metadata = true
  publish       = true
  allowed_triggers = {
    APIGatewayAny = {
      service    = "apigateway"
      source_arn = "${aws_api_gateway_rest_api.api.execution_arn}/*/*"
    }
  }
}

module "lambda_function_auth" {
  source        = "terraform-aws-modules/lambda/aws"
  version       = "~> 6.0"
  timeout       = 300
  source_path   = "../src/auth/"
  function_name = "authorizer"
  handler       = "app.handler"
  runtime       = "python3.11"
  create_sam_metadata = true
}

まず functions.tf を確認すると,AWS Provider の aws_lambda_function ではなく AWS Lambda Terraform module を使っていた.AWS ブログにも書いてある通り,あくまでこれは Terraform コードをシンプルに書くために使われていて,もちろん AWS SAM CLI の Terraform サポートは AWS Provider でも使える👌

github.com

そして AWS Lambda Terraform module を使うことでシンプルに書ける例として create_sam_metadata = true があって,これは Terraform の null_resource を使って sam metadata リソースを自動的に作ってくれる.sam metadata は AWS Lambda 関数のアーティファクトがどこにあるかを伝えるために必要で,さらに AWS Lambda 関数の ZIP ファイルを作るビルド処理にも依存している.sam metadata に関しては以下のドキュメントに載っている❗️

docs.aws.amazon.com

また AWS Lambda Terraform module の main.tf にある null_resource.sam_metadata_aws_lambda_functionpackage.tf にある null_resource.archive を読むと Terraform の local-exec Provisioner を使ってビルドをしてる流れ(sam build コマンドを実行するとログが大量に出てくる部分)など理解できるからおすすめ〜👀

以下のドキュメントには AWS Provider の aws_lambda_function を使ったサンプルも載っていた💡

docs.aws.amazon.com

まとめ

今月 GA になった AWS SAM CLI の Terraform サポート機能をさっそく試してみたけど良かった❗️

さっそく今の現場で実戦投入してみようと思う \( 'ω')/