ai16zのCrypto AI Agent開発フレームワーク「Eliza」の本番環境構築
こんにちは!Web3特化の開発会社 Komlock lab CTOの山口なつきです。
前回の記事では、Crypto AIエージェントの開発フレームワークであるElizaでETH送金を行うまでの実装を解説しました。
今回は、開発したAI Agentを本番環境のサーバーにデプロイしていくところを解説していきます。バックエンドサーバーのインフラ構築なので地味な作業です。
はじめに(定期)
最近「Crypto x AI Agent」が話題ですが、僕はブロックチェーン上での決済機能を持つAIエージェントに、DeFiに匹敵する可能性を感じています。今後はこの分野での情報発信を強化し、社内のメンバーとも共有していく予定です。興味のある方はぜひフォローしてください。
Crypto AI Agent 初めて聞いた!という方は、miinさんの記事を読むことをお勧めします。2024年12月時点のCrypto X AI Agentのトレンドやユースケースを理解することができます。
概要
今回は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を採用しました。使い易く成果物もスタイリッシュなので気に入っています。
terraformのスクリプトをAIと共有するだけで、綺麗な図式を出力してくれます。
思っていたものと違う場合も、チャットすることで修正可能です。(ただし細かい部分は伝わりにくい)
構築開始
Supabaseでアカウント登録
3分で終わります。
作成が完了したら、ダッシュボードのトップ画面からproject api keyとurlを取得します。
あとで利用するので、メモ。
Supabase adapterの導入
SupabaseのDBをElizaOSで利用するために、adapterの導入が必要です。
こちらのドキュメントを参考に導入。
発展途上ということもあって、正直あまり安定していないです。
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開発関連のイベントを定期開催しています!
是非チェックしてください。
Discordでも有益な記事の共有や開発の相談など行っています。
どなたでもウェルカムです🔥
Komlock lab エンジニア募集中
Web3の未来を共創していきたいメンバーを募集しています!!
気軽にDM等でお声がけください。
個人アカウント
Komlock labの企業アカウント
PR記事とCEOの創業ブログ
Discussion