Ponz Dev Log

ゆるくてマイペースな開発日記

TerraformとMotoでAWSリソースの作成をローカルで試そう

この記事は terraform Advent Calendar 2024 の8日目の記事です。 昨日は @mitomito さんによる [terraform]S3とTransferFamilyでsftpサーバを作るでした。

Table of Contents

  • はじめに
  • Motoとは?
  • いざ実践
    • 使用するソフトウェア
    • Motoを起動
    • Motoの動作確認
    • Terraformの設定
    • Terraformリソース定義の記述
    • terraformコマンドを実行

はじめに

クラウドサービスのプロビジョニングにTerraformを使いたいものの、実環境に繋がずできるだけローカルで完結させたい場合ってありませんか? 例えば以下のようなケースです。

  • クラウドのアカウントの調達リードタイム、コスト調整の関係でアカウント利用にハードルが高い。
  • セキュリティポリシーの制約でインターネットの限られたサイトしか接続できない。
  • 実際の環境を複数人で操作しているためテスト用途でも作業のバッティングが頻繁に発生する。
  • Terraform初心者向けに手元で試してもらいたい。

今回はAWSにリソースを作成したいケースを例に、AWSのモックに使える Moto を使用して可能な限りローカルでTerraformの操作を完結させます。

Motoとは?

github.com

AWSサービスのモック機能を提供するPythonライブラリです。 Pythonを使わずともServer Modeで任意の環境で使用可能です。 コンテナイメージも提供されているのでこの記事ではコンテナでMotoを使用します。

MotoでサポートされたAWSリソースと操作は以下のサイトにまとめられています。 EC2、DynamoDB、SQSなど代表的なリソースはサポートされていますね。 Bedrockも操作は限定的ながらサポート対象に入っていることは驚きです。

https://docs.getmoto.org/en/stable/docs/services/index.html

いざ実践

使用するソフトウェア

ソフトウェア バージョン
AWS CLI 2.22.12
Terraform 1.10.1
Moto 5.0.22
Podman 5.3.1

Motoを起動

コンテナイメージを手元にpullして podman コマンドでMotoを起動します。 Docker Desktopを使っている場合は podmandocker と読み替えてください。

$ podman pull ghcr.io/getmoto/motoserver:5.0.22

$ podman run --rm -d --name moto \
    -p 3000:3000 \
    -e MOTO_PORT=3000 \
    -e MOTO_IAM_LOAD_MANAGED_POLICIES=true \
    ghcr.io/getmoto/motoserver:5.0.22

Motoの公開ポートはデフォルトで5000ですが環境変数 MOTO_PORT で任意のポートに変更可能です。 また、IAMのAWS管理ポリシーを使用したい場合は環境変数 MOTO_IAM_LOAD_MANAGED_POLICIES をセットしないとMotoでは使用できません。とりあえずセットしておくとよいでしょう。

dev.classmethod.jp

Motoの動作確認

簡易な動作確認でAWS CLIでMotoを呼び出します。 事前に環境変数 AWS_ENDPOINT をセットして aws コマンドを実行します。 TerraformでAWSサービスの呼び出しをMotoのエンドポイントに向けるためにも使用します。

$ export AWS_ENDPOINT_URL=http://localhost:3000

$ aws sts get-caller-identity --no-cli-pager
{
    "UserId": "AKIAIOSFODNN7EXAMPLE",
    "Account": "123456789012",
    "Arn": "arn:aws:sts::123456789012:user/moto"
}

Terraformの設定

ローカルで動かす場合と実環境で実行する場合で設定を明示的に分けておくと便利です。 Terraformでは *_override.tf というファイルを作ると設定をマージしてくれます。

developer.hashicorp.com

Terraformステートバックエンドの設定 (backend.tf) とTerraformプロバイダーの設定 (provider.tf) が既にある状態を仮定して、ローカルではMotoを使用するケースでそれぞれの設定を上書きするファイルを記述します。

バックエンド設定 デフォルト (backend.tf)

terraform {
  backend "s3" {}
}

バックエンド設定 ローカル用の上書き (backend_override.tf)

terraform {
  backend "local" {
    path = "terraform.tfstate"
  }
}

プロバイダー設定 デフォルト (provider.tf)

terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "5.80.0"
    }
  }
}

provider "aws" {
  region = var.region
}

プロバイダー設定 ローカル用の上書き (provider_override.tf)

MotoではサポートされていないAWS固有のチェック処理を外す設定 (skip_*) を記載します。 またAmazon S3へのアクセスは仮想ホスト形式が標準ですがローカルだと名前解決できないのでパス形式を強制する設定 (s3_use_path_style) も追加しておきます。Motoガイドでは同等設定の s3_force_path_style を指定するように記載がありますが、この設定はTerraform AWS Provider v5で削除されているので使用できません。

provider "aws" {
  skip_credentials_validation = true
  skip_metadata_api_check     = true
  skip_requesting_account_id  = true
  s3_use_path_style           = true
}

Terraformリソース定義の記述

Moto固有の設定を完了したら残りは通常のTerraformリソース定義の記述でOKです。 Amazon VPCを作成します。CIDRブロックをvariablesで受け取り、作成したVPCのIDをoutputsで出力もしてみましょう。

variables.tf

variable "region" {
  description = "AWS Region"
  type        = string
  default     = "ap-northeast-1"
}

variable "cidr_block" {
  description = "VPC CIDR Block"
  type        = string
  default     = "10.23.0.0/16"
}

main.tf

resource "aws_vpc" "qiita" {
  cidr_block           = var.cidr_block
  instance_tenancy     = "default"
  enable_dns_hostnames = true
  enable_dns_support   = true

  tags = {
    "Name" = "qiita-2024-vpc"
  }
}

outputs.tf

output "vpc_id" {
  description = "VPC ID"
  value       = aws_vpc.qiita.id
}

terraformコマンドを実行

ローカルで terraform コマンドを実行してAmazon VPCを作成してみましょう。 Amazon VPCはMotoで標準的にサポートされているリソースなので問題なく実行完了します。

$ terraform init
Initializing the backend...
Successfully configured the backend "local"! Terraform will automatically
...
Terraform has been successfully initialized!


$ terraform validate
Success! The configuration is valid.


$ terraform plan

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the
following symbols:
  + create

Terraform will perform the following actions:

  # aws_vpc.qiita will be created
  + resource "aws_vpc" "qiita" {
      + arn                                  = (known after apply)
      + cidr_block                           = "10.23.0.0/16"
      + default_network_acl_id               = (known after apply)
      + default_route_table_id               = (known after apply)
      + default_security_group_id            = (known after apply)
      + dhcp_options_id                      = (known after apply)
      + enable_dns_hostnames                 = true
      + enable_dns_support                   = true
      + enable_network_address_usage_metrics = (known after apply)
      + id                                   = (known after apply)
      + instance_tenancy                     = "default"
      + ipv6_association_id                  = (known after apply)
      + ipv6_cidr_block                      = (known after apply)
      + ipv6_cidr_block_network_border_group = (known after apply)
      + main_route_table_id                  = (known after apply)
      + owner_id                             = (known after apply)
      + tags                                 = {
          + "Name" = "qiita-2024-vpc"
        }
      + tags_all                             = {
          + "Name" = "qiita-2024-vpc"
        }
    }

Plan: 1 to add, 0 to change, 0 to destroy.

Changes to Outputs:
  + vpc_id = (known after apply)


$ terraform apply -auto-approve
...
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.

Outputs:

vpc_id = "vpc-14e084e497b5b9d80"

本当にMotoにもVPCが作成されているかAWS CLIでチェックしてみます。 Outputsに表示された vpc_id でクエリしてみると確かに指定したプロパティとタグ名が入っています。

$ aws ec2 describe-vpcs --vpc-ids vpc-14e084e497b5b9d80
{
    "Vpcs": [
        {
            "OwnerId": "123456789012",
            "InstanceTenancy": "default",
            ...(omit)...
            "IsDefault": false,
            "Tags": [
                {
                    "Key": "Name",
                    "Value": "qiita-2024-vpc"
                }
            ],
            "VpcId": "vpc-14e084e497b5b9d80",
            "State": "available",
            "CidrBlock": "10.23.0.0/16",
            "DhcpOptionsId": "default"
        }
    ]
}

Motoの実行ログをのぞいてみると全てのAPIコールで「HTTP/1.1 200」が返却されていますね。 もし途中でエラーが発生した場合は未サポートのリソースや設定が含まれる場合があります。

$ podman logs moto
podman logs moto
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
 * Running on all addresses (0.0.0.0)
 * Running on http://127.0.0.1:3000
 * Running on http://10.88.0.2:3000
Press CTRL+C to quit
10.88.0.2 - - [07/Dec/2024 05:18:31] "POST / HTTP/1.1" 200 -
10.88.0.2 - - [07/Dec/2024 05:36:29] "POST / HTTP/1.1" 200 -
10.88.0.2 - - [07/Dec/2024 05:36:29] "POST / HTTP/1.1" 200 -
...

以上。