🈂️

ai16zのCrypto AI Agent開発フレームワーク「Eliza」の本番環境構築

2025/01/04に公開

こんにちは!Web3特化の開発会社 Komlock lab CTOの山口なつきです。
前回の記事では、Crypto AIエージェントの開発フレームワークであるElizaでETH送金を行うまでの実装を解説しました。
https://zenn.dev/komlock_lab/articles/aecde5d1a41709

今回は、開発したAI Agentを本番環境のサーバーにデプロイしていくところを解説していきます。バックエンドサーバーのインフラ構築なので地味な作業です。

はじめに(定期)

最近「Crypto x AI Agent」が話題ですが、僕はブロックチェーン上での決済機能を持つAIエージェントに、DeFiに匹敵する可能性を感じています。今後はこの分野での情報発信を強化し、社内のメンバーとも共有していく予定です。興味のある方はぜひフォローしてください。

https://x.com/komlocklab

Crypto AI Agent 初めて聞いた!という方は、miinさんの記事を読むことをお勧めします。2024年12月時点のCrypto X AI Agentのトレンドやユースケースを理解することができます。
https://note.com/miin_nft/n/nf8cb760c3563

概要

今回はAWS EC2+SupabaseでElizaの簡易的な本番環境を構築していきます。
普段AWSは使い慣れているのと、Supabaseはデータベースをコスパよく運用できそうだったので採用しました。CICDは実装せずローカルからデプロイするところまでの記事です。

今回利用したツール

  • AWS Elastic Beanstalk: Webアプリケーションの実行環境を自動的に構築するためのサービス
  • Supabase: PostgreSQL をベースにした、Backend as a Service。Firebaseとよく比較されます。
  • terraform: インフラ構築の自動化(今回はAWS部分のみ)
  • eraser.io: 構成図の作成
  • cursor composer agent: terraformのテンプレートを自動生成するのに利用

インフラ構成図

subnetなど細かい部分は省略しています。

eraser.ioについて

図形を作るためのツールです。drawioを普段使っていたのですが、UXが苦手だったのとAIとの相性があまり良くな買ったので、今回はeraser.ioを採用しました。使い易く成果物もスタイリッシュなので気に入っています。
https://app.eraser.io/

terraformのスクリプトをAIと共有するだけで、綺麗な図式を出力してくれます。
思っていたものと違う場合も、チャットすることで修正可能です。(ただし細かい部分は伝わりにくい)

構築開始

Supabaseでアカウント登録

3分で終わります。
https://supabase.com/

作成が完了したら、ダッシュボードのトップ画面からproject api keyとurlを取得します。
あとで利用するので、メモ。

Supabase adapterの導入

SupabaseのDBをElizaOSで利用するために、adapterの導入が必要です。
こちらのドキュメントを参考に導入。
発展途上ということもあって、正直あまり安定していないです。
https://elizaos.github.io/eliza/docs/packages/adapters/

terraformでAWS環境の構築

terraformを利用してAWS環境を自動構築します。
EC2, ECS, Lambdaなど色々選択肢がありましたが、Elizaはpm2で動作することが想定された仕様なのとstarter templateはdocker化されていなかったので、AWS Elastic Beanstalkを利用してEC2でデプロイすることにしました。

参考テンプレート

main.tfのみここでは共有します。環境変数は適当に置き換えると機能します。
少し長いのと検証用のコードなのでセキュリティ面のレビューは必須です。(cidr_blocks = ["0.0.0.0/0"]の部分など)

################################################################################
# Terraform Settings
################################################################################
terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }

  # Example: S3 backend for state
  backend "s3" {
    bucket         = "eliza-application-terraform-state"
    key            = "terraform.tfstate"
    region         = "ap-northeast-1"
    encrypt        = true
    dynamodb_table = "eliza-application-terraform-state-lock"
  }
}

################################################################################
# AWS Provider
################################################################################
provider "aws" {
  region = var.aws_region
}

################################################################################
# (1) Grant iam:PassRole permission to the Terraform deployer role
################################################################################
data "aws_caller_identity" "current" {}

# Policy allowing Terraform to pass the EB service role
resource "aws_iam_policy" "terraform_deployer_passrole_policy" {
  name        = "${var.application_name}-terraform-deployer-passrole"
  description = "Allow the Terraform role to pass the EB service role"
  policy      = jsonencode({
    Version = "2012-10-17",
    Statement = [
      {
        Effect   = "Allow",
        Action   = "iam:PassRole",
        Resource = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:role/${var.application_name}-eb-service-role"
      }
    ]
  })
}

# Attach that policy to the IAM user that runs Terraform
# TODO: Replace with the role attachment
resource "aws_iam_user_policy_attachment" "attach_deployer_passrole_policy" {
  user       = "terraformを実行する際に利用しているIAMユーザー名"
  policy_arn = aws_iam_policy.terraform_deployer_passrole_policy.arn
}

resource "aws_iam_role_policy_attachment" "attach_eb_ec2_profile_passrole_policy" {
  role       = aws_iam_role.eb_ec2_role.name
  policy_arn = aws_iam_policy.terraform_deployer_passrole_policy.arn
}


################################################################################
# Data source for dynamic AZ retrieval
################################################################################
data "aws_availability_zones" "available" {
  state = "available"
}

################################################################################
# SSM Parameter (for Supabase key)
################################################################################
resource "aws_ssm_parameter" "supabase_anon_key" {
  name        = "/${var.application_name}/supabase-anon-key"
  description = "Supabase anonymous key"
  type        = "SecureString"
  value       = var.supabase_anon_key
}

################################################################################
# S3 Bucket for EB Deployments
################################################################################
resource "aws_s3_bucket" "eb_bucket" {
  bucket = "${var.application_name}-eb-deployments"
}

resource "aws_s3_bucket_versioning" "eb_bucket_versioning" {
  bucket = aws_s3_bucket.eb_bucket.id
  versioning_configuration {
    status = "Enabled"
  }
}

################################################################################
# Elastic Beanstalk Application
################################################################################
resource "aws_elastic_beanstalk_application" "eb_app" {
  name        = var.application_name
  description = "Eliza application"
}

################################################################################
# IAM Role + Policies for EC2 (Beanstalk Instances)
################################################################################
resource "aws_iam_role" "eb_ec2_role" {
  name = "${var.application_name}-eb-ec2-role"

  assume_role_policy = jsonencode({
    Version = "2012-10-17",
    Statement = [
      {
        Action    = "sts:AssumeRole",
        Effect    = "Allow",
        Principal = {
          Service = "ec2.amazonaws.com"
        }
      }
    ]
  })
}

# Attach AWS managed policies needed by Beanstalk EC2
resource "aws_iam_role_policy_attachment" "eb_ec2_web_tier" {
  role       = aws_iam_role.eb_ec2_role.name
  policy_arn = "arn:aws:iam::aws:policy/AWSElasticBeanstalkWebTier"
}

resource "aws_iam_role_policy_attachment" "eb_ec2_worker_tier" {
  role       = aws_iam_role.eb_ec2_role.name
  policy_arn = "arn:aws:iam::aws:policy/AWSElasticBeanstalkWorkerTier"
}

resource "aws_iam_role_policy_attachment" "eb_ec2_container" {
  role       = aws_iam_role.eb_ec2_role.name
  policy_arn = "arn:aws:iam::aws:policy/AWSElasticBeanstalkMulticontainerDocker"
}

# Custom policy to allow access to SSM parameter and S3 bucket
resource "aws_iam_role_policy" "eb_ec2_policy" {
  name = "${var.application_name}-eb-ec2-policy"
  role = aws_iam_role.eb_ec2_role.id

  policy = jsonencode({
    Version = "2012-10-17",
    Statement = [
      {
        Effect = "Allow",
        Action = [
          "ssm:GetParameter",
          "ssm:GetParameters",
          "ssm:GetParametersByPath"
        ],
        Resource = [
          aws_ssm_parameter.supabase_anon_key.arn
        ]
      },
      {
        Effect = "Allow",
        Action = [
          "s3:Get*",
          "s3:List*",
          "s3:PutObject"
        ],
        Resource = [
          "${aws_s3_bucket.eb_bucket.arn}/*",
          aws_s3_bucket.eb_bucket.arn
        ]
      }
    ]
  })
}

# EC2 Instance Profile
resource "aws_iam_instance_profile" "eb_ec2_profile" {
  name = "${var.application_name}-eb-ec2-profile"
  role = aws_iam_role.eb_ec2_role.name
}

################################################################################
# IAM Role (for Elastic Beanstalk Service)
################################################################################
resource "aws_iam_role" "eb_service_role" {
  name = "${var.application_name}-eb-service-role"

  assume_role_policy = jsonencode({
    Version = "2012-10-17",
    Statement = [
      {
        Action    = "sts:AssumeRole",
        Effect    = "Allow",
        Principal = {
          Service = "elasticbeanstalk.amazonaws.com"
        }
      }
    ]
  })
}

# Attach required AWS managed policies for Beanstalk service
resource "aws_iam_role_policy_attachment" "eb_service_role_policy" {
  role       = aws_iam_role.eb_service_role.name
  policy_arn = "arn:aws:iam::aws:policy/AWSElasticBeanstalkManagedUpdatesCustomerRolePolicy"
}

resource "aws_iam_role_policy_attachment" "eb_service_role_enhanced_health" {
  role       = aws_iam_role.eb_service_role.name
  policy_arn = "arn:aws:iam::aws:policy/service-role/AWSElasticBeanstalkEnhancedHealth"
}

resource "aws_iam_role_policy_attachment" "eb_service_role_service" {
  role       = aws_iam_role.eb_service_role.name
  policy_arn = "arn:aws:iam::aws:policy/service-role/AWSElasticBeanstalkService"
}

################################################################################
# Create a new VPC
################################################################################
resource "aws_vpc" "eb_vpc" {
  cidr_block = "10.0.0.0/16"

  tags = {
    Name = "${var.application_name}-vpc"
  }
}

################################################################################
# Internet Gateway & Public Route Table
################################################################################
resource "aws_internet_gateway" "eb_igw" {
  vpc_id = aws_vpc.eb_vpc.id

  tags = {
    Name = "${var.application_name}-igw"
  }
}

resource "aws_route_table" "eb_public_rt" {
  vpc_id = aws_vpc.eb_vpc.id

  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.eb_igw.id
  }

  tags = {
    Name = "${var.application_name}-public-rt"
  }
}

################################################################################
# Create two Public Subnets using the first two available AZs
################################################################################

resource "aws_subnet" "eb_public_subnet_1" {
  vpc_id            = aws_vpc.eb_vpc.id
  cidr_block        = "10.0.1.0/24"
  availability_zone = data.aws_availability_zones.available.names[0]

  tags = {
    Name = "${var.application_name}-public-subnet-1"
  }
}

resource "aws_subnet" "eb_public_subnet_2" {
  vpc_id            = aws_vpc.eb_vpc.id
  cidr_block        = "10.0.2.0/24"
  availability_zone = data.aws_availability_zones.available.names[1]

  tags = {
    Name = "${var.application_name}-public-subnet-2"
  }
}

################################################################################
# Associate the public subnets with the public route table
################################################################################
resource "aws_route_table_association" "eb_public_rt_assoc_1" {
  subnet_id      = aws_subnet.eb_public_subnet_1.id
  route_table_id = aws_route_table.eb_public_rt.id
}

resource "aws_route_table_association" "eb_public_rt_assoc_2" {
  subnet_id      = aws_subnet.eb_public_subnet_2.id
  route_table_id = aws_route_table.eb_public_rt.id
}

################################################################################
# Security Group for Elastic Beanstalk (attached to the new VPC)
################################################################################
resource "aws_security_group" "eb_sg" {
  name_prefix = "${var.application_name}-eb-sg"
  vpc_id      = aws_vpc.eb_vpc.id

  ingress {
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  ingress {
    from_port   = 443
    to_port     = 443
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  ingress {
    from_port   = 3000
    to_port     = 3000
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }

  tags = {
    Name = "${var.application_name}-eb-sg"
  }
}

################################################################################
# Elastic Beanstalk Environment
################################################################################
resource "aws_elastic_beanstalk_environment" "eb_environment" {
  name                = "${var.application_name}-environment"
  application         = aws_elastic_beanstalk_application.eb_app.name
  solution_stack_name = "64bit Amazon Linux 2023 v6.4.1 running Node.js 20"
  tier                = "WebServer"

  # VPC Settings
  setting {
    namespace = "aws:ec2:vpc"
    name      = "VPCId"
    value     = aws_vpc.eb_vpc.id
  }

  setting {
    namespace = "aws:ec2:vpc"
    name      = "Subnets"
    value     = join(",", [
      aws_subnet.eb_public_subnet_1.id,
      aws_subnet.eb_public_subnet_2.id
    ])
  }

  setting {
    namespace = "aws:ec2:vpc"
    name      = "AssociatePublicIpAddress"
    value     = "true"
  }

  # Elastic Beanstalk Service Role
  setting {
    namespace = "aws:elasticbeanstalk:environment"
    name      = "ServiceRole"
    value     = aws_iam_role.eb_service_role.name
  }

  # Launch configuration settings
  setting {
    namespace = "aws:autoscaling:launchconfiguration"
    name      = "DisableIMDSv1"
    value     = "true"
  }

  setting {
    namespace = "aws:ec2:instances"
    name      = "InstanceTypes"
    value     = var.instance_type
  }

  # Auto Scaling size
  setting {
    namespace = "aws:autoscaling:asg"
    name      = "MinSize"
    value     = var.min_instances
  }

  setting {
    namespace = "aws:autoscaling:asg"
    name      = "MaxSize"
    value     = var.max_instances
  }

  # Enable load balancer
  setting {
    namespace = "aws:elasticbeanstalk:environment"
    name      = "EnvironmentType"
    value     = "LoadBalanced"
  }

  # EC2 IAM instance profile
  setting {
    namespace = "aws:autoscaling:launchconfiguration"
    name      = "IamInstanceProfile"
    value     = aws_iam_instance_profile.eb_ec2_profile.name
  }

  # Proxy server settings
  setting {
    namespace = "aws:elasticbeanstalk:environment:proxy"
    name      = "ProxyServer"
    value     = "nginx"
  }

  # Deployment policy
  setting {
    namespace = "aws:elasticbeanstalk:command"
    name      = "DeploymentPolicy"
    value     = "Rolling"
  }

  setting {
    namespace = "aws:elasticbeanstalk:command"
    name      = "BatchSizeType"
    value     = "Fixed"
  }

  setting {
    namespace = "aws:elasticbeanstalk:command"
    name      = "BatchSize"
    value     = "1"
  }

  # Application environment variables
  setting {
    namespace = "aws:elasticbeanstalk:application:environment"
    name      = "NPM_USE_PRODUCTION"
    value     = "false"
  }

  setting {
    namespace = "aws:elasticbeanstalk:application:environment"
    name      = "COMMAND"
    value     = "npm install -g pnpm pm2 && pnpm install && pnpm run build && pnpm run start:service:all"
  }

  setting {
    namespace = "aws:elasticbeanstalk:application:environment"
    name      = "NODE_ENV"
    value     = var.node_env
  }

  setting {
    namespace = "aws:elasticbeanstalk:application:environment"
    name      = "SUPABASE_URL"
    value     = var.supabase_url
  }

  setting {
    namespace = "aws:elasticbeanstalk:application:environment"
    name      = "SUPABASE_ANON_KEY_PARAM"
    value     = aws_ssm_parameter.supabase_anon_key.name
  }

  setting {
    namespace = "aws:elasticbeanstalk:application:environment"
    name      = "DB_POOL_MAX"
    value     = var.db_pool_max
  }

  setting {
    namespace = "aws:elasticbeanstalk:application:environment"
    name      = "PM2_HOME"
    value     = "/var/app/current/.pm2"
  }

  setting {
    namespace = "aws:elasticbeanstalk:application:environment"
    name      = "PORT"
    value     = "3000"
  }

  # Enhanced health reporting
  setting {
    namespace = "aws:elasticbeanstalk:healthreporting:system"
    name      = "SystemType"
    value     = "enhanced"
  }

  # Enable managed updates
  setting {
    namespace = "aws:elasticbeanstalk:managedactions"
    name      = "ManagedActionsEnabled"
    value     = "true"
  }

  setting {
    namespace = "aws:elasticbeanstalk:managedactions"
    name      = "PreferredStartTime"
    value     = "Tue:10:00"
  }

  # Apply up to minor updates
  setting {
    namespace = "aws:elasticbeanstalk:managedactions:platformupdate"
    name      = "UpdateLevel"
    value     = "minor"
  }

  # Make ELB public
  setting {
    namespace = "aws:ec2:vpc"
    name      = "ELBScheme"
    value     = "public"
  }

  # EC2 Security Group
  setting {
    namespace = "aws:autoscaling:launchconfiguration"
    name      = "SecurityGroups"
    value     = aws_security_group.eb_sg.id
  }

  # Health check path
  setting {
    namespace = "aws:elasticbeanstalk:environment:process:default"
    name      = "HealthCheckPath"
    value     = "/"
  }

  setting {
    namespace = "aws:elasticbeanstalk:environment:process:default"
    name      = "Port"
    value     = "3000"
  }

  depends_on = [
    # Ensure EB service role has the managed policies attached
    aws_iam_role_policy_attachment.eb_service_role_policy,
    aws_iam_role_policy_attachment.eb_service_role_enhanced_health,
    aws_iam_role_policy_attachment.eb_service_role_service,

    # Ensure your Terraform role can pass the EB service role
    aws_iam_user_policy_attachment.attach_deployer_passrole_policy
  ]
}

terraform init → terraform plan → terraform applyして完了です。
terraformを導入するためにterraform自体のインストールやステート管理用のS3やdynamodbを作成したりする作業もありますが、今回は省略します。

成功するとこんな感じで、管理画面から確認できます。
実際に動いているかどうかは、SSHでサーバーに接続したり実際にTwitterで機能していることを確認するのが良いと思いました。ログの設定次第ではCloudWatchからも確認できるかと思います。

今回はこれで終了です!
よりコスパの良いVPSを借りたりする選択肢もありましたが、スケーラブルな構成にチャレンジしてみました。将来的にはこういった中央集権的な管理ではなく、分散システムにデプロイできるようになるのが理想だと思います(既にいくつかのプロジェクトが取り組んでいるという話も聞きます)。

Komlock lab もくもく会&LT会

web3開発関連のイベントを定期開催しています!
是非チェックしてください。
https://connpass.com/user/Komlock_lab/open/

Discordでも有益な記事の共有や開発の相談など行っています。
どなたでもウェルカムです🔥
https://discord.gg/Ab5w53Xq8Z

Komlock lab エンジニア募集中

Web3の未来を共創していきたいメンバーを募集しています!!
気軽にDM等でお声がけください。

個人アカウント
https://x.com/0x_natto

Komlock labの企業アカウント
https://x.com/komlocklab

PR記事とCEOの創業ブログ
https://prtimes.jp/main/html/rd/p/000000332.000041264.html
https://note.com/komlock_lab/n/n2e9437a91023

Komlock lab

Discussion