こんにちは。SCSKのふくちーぬです。
私が所属する技術戦略本部では、『2030 共創ITカンパニー』の実現に向けて、全社技術戦略の策定や先進技術を開拓し、新たな価値創出・社会実装に向けた様々な取り組みを進めております。
その中でも技術戦略本部では、「Dify」を触れる環境を全社員に提供して、技術検証に加えて高度デジタル人材の育成を促進する取り組みをしています。
本記事では、OSSのLLMアプリケーション開発プラットフォーム「Dify」をAWS上でホスティングし全社展開している知見やRAGアプリケーションを紹介します。
Difyとは
各種LLM(大規模言語)モデルを活用して、GUIベースで簡単に生成Aiアプリケーションを開発できるツールです。
Dify クラウドサービス
クラウドサービスとして提供される完全マネージ型のSaaSです。AWSアカウントと同様にテナント(アカウント)を分けたり、多様な認証方法が用意されISO 27001:2022認証(ISMS)を始めとしたコンプライアンスも順守されているため、エンタープライズの大規模利用に向いています。
Dify コミュニティ版
オープンソースとして提供されるセルフホスティング型のソリューションです。クラウドサービスとは異なり、Dify本体のサービス利用料はかかりませんが、ホストティングしている基盤の利用料や運用管理は発生します。またテナントを分割することはできず、認証方法が限定(メールアドレス+パスワード)されています。その他ライセンスの利用条項が記載しているので、自社利用する際はご参照の上構築ください。
アーキテクチャ
ユーザーは、HTTPS通信でDify Webアプリケーションにアクセスします。
- ALB及びACMを利用したHTTPS通信
- ALBのセキュリティグループでは、IPアドレスでのアクセス制御
- Difyアプリケーションは、EC2内のDocker-Composeコマンドで稼働
構築手順
弊社で全社展開しているDify環境のノウハウを活かして、今回は個人でも扱いやすいDifyセルフホスティング環境を構築する手順をご紹介します。
Amazon Route 53 のドメイン取得
Route 53 でドメインを取得していきます。
今回は、Route 53 で提供されるTLDの中で最安の「.click」を購入しています。3$/年の格安料金です。
連絡先情報を入力します。
10分程待つと、AWS側でリクエストが承認されて、ステータスが「成功」になります。
これで、ドメインは取得は完了です。
証明書の取得
次にドメインに紐づいた証明書を取得します。
ACMのコンソールに切り替えます。「リクエスト」を押下します。
「パブリック証明書をリクエスト」を選択して、「次へ」を押下します。
Route 53 で取得したドメイン名を入力します。検証方法、キーアルゴリズムはデフォルトの設定のままで構いません。
「リクエスト」を押下します。
ACM証明書の作成は完了しました。Route 53 とのDNS検証をするために、CNAMEレコードを登録します。「Route 53 でレコードを作成」を押下します。
「レコードを作成」を押下します。
Route 53 とのDNS検証が完了するまで、10分程待ちます。
Route 53 の検証が成功すれば、ステータスが「発行済み」に変更されます。
これで、証明書の取得は完了です。
AWS CloudFormtion テンプレート
テンプレートで指定する各種パラメータの値を控えておいてください。
- DifyVersion
下記サイトをご確認し、指定のバージョンを選んでください。
- DockerComposeVersion
- ACMCertificateArn
発行したACMのARNを指定してください。
- HostedZoneId
Route 53 パブリックホストゾーンのIDを指定してください。
- DomainName
Route 53 で登録したドメイン名を指定してください。
その後、以下のCloudFormtionテンプレートをデプロイしてください。
AWSTemplateFormatVersion: '2010-09-09'
Description: Dify CloudFormation Template with ALB and EC2 (HTTPS)
Parameters:
ProjectName:
Type: String
Default: dify
Description: Project name used as prefix for resources
VpcCIDR:
Type: String
Default: 10.0.0.0/16
PubSubnet1CIDR:
Type: String
Default: 10.0.0.0/24
PubSubnet2CIDR:
Type: String
Default: 10.0.1.0/24
PriSubnet1CIDR:
Type: String
Default: 10.0.10.0/24
PriSubnet2CIDR:
Type: String
Default: 10.0.11.0/24
AllowedCIDR:
Type: String
Default: 0.0.0.0/0
AmazonLinuxAMI:
Type: AWS::SSM::Parameter::Value
Default: /aws/service/ami-amazon-linux-latest/al2023-ami-kernel-6.1-x86_64
DifyVersion:
Type: String
Default: 1.1.0
DockerComposeVersion:
Type: String
Default: v2.35.1
ACMCertificateArn:
Type: String
Description: ACM Certificate ARN for HTTPS
HostedZoneId:
Type: String
Description: Route 53 Hosted Zone ID
DomainName:
Type: String
Description: Domain name
Resources:
VPC:
Type: AWS::EC2::VPC
Properties:
CidrBlock: !Ref VpcCIDR
EnableDnsHostnames: true
EnableDnsSupport: true
Tags:
- Key: Name
Value: !Sub "${ProjectName}-vpc"
PubSubnet1:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref VPC
AvailabilityZone: !Select [0, !GetAZs '']
CidrBlock: !Ref PubSubnet1CIDR
MapPublicIpOnLaunch: true
Tags:
- Key: Name
Value: !Sub "${ProjectName}-pub-subnet-1"
PubSubnet2:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref VPC
AvailabilityZone: !Select [1, !GetAZs '']
CidrBlock: !Ref PubSubnet2CIDR
MapPublicIpOnLaunch: true
Tags:
- Key: Name
Value: !Sub "${ProjectName}-pub-subnet-2"
PriSubnet1:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref VPC
AvailabilityZone: !Select [0, !GetAZs '']
CidrBlock: !Ref PriSubnet1CIDR
Tags:
- Key: Name
Value: !Sub "${ProjectName}-Pri-subnet-1"
PriSubnet2:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref VPC
AvailabilityZone: !Select [1, !GetAZs '']
CidrBlock: !Ref PriSubnet2CIDR
Tags:
- Key: Name
Value: !Sub "${ProjectName}-Pri-subnet-2"
InternetGateway:
Type: AWS::EC2::InternetGateway
Properties:
Tags:
- Key: Name
Value: !Sub "${ProjectName}-igw"
VPCGatewayAttachment:
Type: AWS::EC2::VPCGatewayAttachment
Properties:
VpcId: !Ref VPC
InternetGatewayId: !Ref InternetGateway
PubRouteTable:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref VPC
Tags:
- Key: Name
Value: !Sub "${ProjectName}-pub-rt"
PubRoute:
Type: AWS::EC2::Route
DependsOn: VPCGatewayAttachment
Properties:
RouteTableId: !Ref PubRouteTable
DestinationCidrBlock: 0.0.0.0/0
GatewayId: !Ref InternetGateway
PubSubnet1RouteTableAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref PubSubnet1
RouteTableId: !Ref PubRouteTable
PubSubnet2RouteTableAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref PubSubnet2
RouteTableId: !Ref PubRouteTable
EIP:
Type: AWS::EC2::EIP
Properties:
Domain: vpc
Tags:
- Key: Name
Value: !Sub "${ProjectName}-eip"
NATGateway:
Type: AWS::EC2::NatGateway
Properties:
AllocationId: !GetAtt EIP.AllocationId
SubnetId: !Ref PubSubnet1
Tags:
- Key: Name
Value: !Sub "${ProjectName}-natgw"
PriRouteTable:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref VPC
Tags:
- Key: Name
Value: !Sub "${ProjectName}-Pri-rt"
PriRoute:
Type: AWS::EC2::Route
Properties:
RouteTableId: !Ref PriRouteTable
DestinationCidrBlock: 0.0.0.0/0
NatGatewayId: !Ref NATGateway
PriSubnet1RouteTableAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref PriSubnet1
RouteTableId: !Ref PriRouteTable
PriSubnet2RouteTableAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref PriSubnet2
RouteTableId: !Ref PriRouteTable
ALBSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: Allow HTTPS from AllowedCIDR
VpcId: !Ref VPC
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 443
ToPort: 443
CidrIp: !Ref AllowedCIDR
Tags:
- Key: Name
Value: !Sub "${ProjectName}-alb-sg"
EC2SecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: Allow traffic from ALB only
VpcId: !Ref VPC
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 80
ToPort: 80
SourceSecurityGroupId: !Ref ALBSecurityGroup
SecurityGroupEgress:
- IpProtocol: -1
CidrIp: 0.0.0.0/0
Tags:
- Key: Name
Value: !Sub "${ProjectName}-ec2-sg"
ALB:
Type: AWS::ElasticLoadBalancingV2::LoadBalancer
Properties:
Name: !Sub "${ProjectName}-alb"
Subnets:
- !Ref PubSubnet1
- !Ref PubSubnet2
SecurityGroups:
- !Ref ALBSecurityGroup
Scheme: internet-facing
LoadBalancerAttributes:
- Key: idle_timeout.timeout_seconds
Value: '60'
Type: application
Tags:
- Key: Name
Value: !Sub "${ProjectName}-alb"
ALBTargetGroup:
Type: AWS::ElasticLoadBalancingV2::TargetGroup
Properties:
Name: !Sub "${ProjectName}-tg"
VpcId: !Ref VPC
Protocol: HTTP
Port: 80
TargetType: instance
Targets:
- Id: !Ref Instance
HealthCheckProtocol: HTTP
HealthCheckPath: /signin
Tags:
- Key: Name
Value: !Sub "${ProjectName}-tg"
ALBListener:
Type: AWS::ElasticLoadBalancingV2::Listener
Properties:
Certificates:
- CertificateArn: !Ref ACMCertificateArn
DefaultActions:
- Type: forward
TargetGroupArn: !Ref ALBTargetGroup
LoadBalancerArn: !Ref ALB
Port: 443
Protocol: HTTPS
InstanceRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service: [ec2.amazonaws.com]
Action: ['sts:AssumeRole']
ManagedPolicyArns:
- arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore
- arn:aws:iam::aws:policy/AmazonBedrockFullAccess
InstanceProfile:
Type: AWS::IAM::InstanceProfile
Properties:
Roles: [!Ref InstanceRole]
Instance:
Type: AWS::EC2::Instance
Properties:
ImageId: !Ref AmazonLinuxAMI
InstanceType: t3.medium
IamInstanceProfile: !Ref InstanceProfile
SubnetId: !Ref PriSubnet1
SecurityGroupIds: [!Ref EC2SecurityGroup]
Tags:
- Key: Name
Value: !Sub "${ProjectName}-ec2"
UserData:
Fn::Base64: !Sub |
#!/bin/bash
sudo dnf install -y git docker
sudo systemctl enable --now docker
sudo usermod -aG docker ec2-user
sudo curl -L "https://github.com/docker/compose/releases/download/${DockerComposeVersion}/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose
sudo ln -s /usr/local/bin/docker-compose /usr/bin/docker-compose
cd /opt
sudo git clone https://github.com/langgenius/dify.git
cd /opt/dify
sudo git checkout ${DifyVersion}
cd /opt/dify/docker
sudo cp .env.example .env
docker-compose up -d
Route53Record:
Type: AWS::Route53::RecordSet
Properties:
HostedZoneId: !Ref HostedZoneId
Name: !Ref DomainName
Type: A
AliasTarget:
DNSName: !GetAtt ALB.DNSName
HostedZoneId: !GetAtt ALB.CanonicalHostedZoneID
Outputs:
InitialAccessURL:
Description: Initial access URL for Dify
Value: !Sub "https://${DomainName}/install"
LoginURL:
Description: Login URL for Dify
Value: !Sub "https://${DomainName}/signin"
CloudFormationでステータスが「CREATE_COMPLETE」になっていれば、環境構築は完了です。
Amazon Bedrock でのモデル有効化
今回は、以下のモデルを有効しております。
初めてBedrock内のモデルを利用する方は、下記手順に沿って実施ください。
Difyの設定
管理者アカウントの作成
CloudFormationテンプレート内に記載してある、以下のURLから初期ログインしていきます。
初回ログイン時のみ、以下のURLにて管理者アカウントを作成します。
└https://<ドメイン名>/install
「セットアップ」を押下したら、ログインに進みます。
ログイン
これ以降のアクセスは、以下のURLを利用します。
└https://<ドメイン名>/signin
先程のログイン情報でログインをしてください。
モデルの有効化
今回は、Amazon Bedrock経由でモデルを利用します。ユーザーアイコンを押下し、「設定」を押下します。
その後、「モデルプロバイダー」を押下します。
「セットアップ」を押下します。
CloudFormationテンプレートをデプロイした”リージョン”と、今回利用する”モデルID”を入力します。
以下のように、AWSアカウント内で利用可能なモデルが表示されればアプリケーション作成の準備が整いました。
DifyでRAGアプリケーションを構築
今回は、RAG ON/OFFができるチャットアプリケーションを開発しました。
RAG(Retrieval-Augmented Generation、検索拡張生成)とは
今では、生成AI・LLM(Large Language Model、大規模言語モデル)がエンタープライズでも多く利用されています。LLMは、事前にある時点での学習データを大量に読みこませています。その中での課題の一つに、LLMが社内情報や最新情報にもとづいた回答を生成できないことです。この問題を解決するための技術が、RAG(Retrieval-Augmented Generation、検索拡張生成)です。
RAGを利用することで、欲しい情報を検索して抽出し、その内容をもとに生成AIに回答させることができます。これを応用すると、生成AIは学習済の情報だけではなく、未学習の情報からも回答を生成することができます。
ナレッジの作成
RAGの例として、弊社のプレスリリースを参照させた汎用的なチャットアプリケーションを作成してみます。
RAGで参照元となるPDFファイルをナレッジに登録します。
Difyのトップページ上部にある、「ナレッジ」を押下します。
「ナレッジを作成」を押下します。
「テキストファイルからインポート」を選択し、該当のファイルをドラッグ&ドロップでアップロードします。
今回は、弊社のプレスリリース(メジャーリーグベースボール(MLB™)とオフィシャルパートナー契約を締結)をPDF資料として利用しました。
アップロードが完了したら、「次へ」を押下します。
下記の設定を選択後、「保存して処理」を押下します。
- チャンク設定:自動
⇒チャンクとは、小さな文章のかたまりのことです。AIが理解しやすいサイズに、文章を分割します。
- インデックスモード:
⇒AIが情報を素早くみつけさせるための、索引を作成します。
- 検索設定:
⇒単語そのものを探す「全文検索」と、意味や内容が類似しているものを探す「ベクトル検索」を組み合わせています。
ナレッジが作成だれたら、「ドキュメントに移動」を押下します。
以下のように、チャンクごとに文章を確認することができます。ナレッジの作成は完了です。
ワークフローの作成
スタジオ内ではアプリケーションを一から作成することもできます。また、あらかじめ用意されたテンプレートから簡単に作成することもできます。
Difyでは、GUIで以下のようなフローをノーコードで作成することが可能です。
本アプリケーションでは、2か所で生成AIモデルを利用しています。
- 検索:ドキュメントを数値ベクトルに変換して、意味的に関連する情報を検索するために埋め込みモデルを使用しています。
⇒amazon.titan-embed-text-v1(Amazon社がBedrockのみで提供。)
- テキスト生成:検索結果を基に、日本語で自然で正確な回答を作成するためにテキストモデルを使用しています。
⇒anthropic.claude-3-5-sonnet-20240620-v1:0(Anthropic社が提供しているClaude 3シリーズの中規模モデル。)
作成したアプリケーションは、YAML形式でエクスポートできます。そのため、他のユーザーでも簡単にアプリケーションを動かすことが可能です。
app:
description: ''
icon: 🤖
icon_background: '#FFEAD5'
mode: advanced-chat
name: チャットアプリ
use_icon_as_answer_icon: false
kind: app
version: 0.1.2
workflow:
conversation_variables: []
environment_variables: []
features:
file_upload:
image:
enabled: false
number_limits: 3
transfer_methods:
- local_file
- remote_url
opening_statement: ''
retriever_resource:
enabled: false
sensitive_word_avoidance:
enabled: false
speech_to_text:
enabled: false
suggested_questions: []
suggested_questions_after_answer:
enabled: false
text_to_speech:
enabled: false
language: ''
voice: ''
graph:
edges:
- data:
sourceType: llm
targetType: answer
id: llm-answer
source: llm
sourceHandle: source
target: answer
targetHandle: target
type: custom
- data:
isInIteration: false
sourceType: start
targetType: if-else
id: 1744250148284-source-1744251195990-target
selected: false
source: '1744250148284'
sourceHandle: source
target: '1744251195990'
targetHandle: target
type: custom
zIndex: 0
- data:
isInIteration: false
sourceType: if-else
targetType: knowledge-retrieval
id: 1744251195990-true-1744251325785-target
source: '1744251195990'
sourceHandle: 'true'
target: '1744251325785'
targetHandle: target
type: custom
zIndex: 0
- data:
isInIteration: false
sourceType: if-else
targetType: llm
id: 1744251195990-false-llm-target
selected: false
source: '1744251195990'
sourceHandle: 'false'
target: llm
targetHandle: target
type: custom
zIndex: 0
- data:
isInIteration: false
sourceType: knowledge-retrieval
targetType: llm
id: 1744251325785-source-llm-target
source: '1744251325785'
sourceHandle: source
target: llm
targetHandle: target
type: custom
zIndex: 0
nodes:
- data:
desc: ''
selected: false
title: 開始
type: start
variables:
- label: RAGオン/オフ
max_length: 48
options:
- 'ON'
- 'OFF'
required: true
type: select
variable: RAG
height: 89
id: '1744250148284'
position:
x: -78.33688053024684
y: 158.40698792460137
positionAbsolute:
x: -78.33688053024684
y: 158.40698792460137
selected: false
sourcePosition: right
targetPosition: left
type: custom
width: 244
- data:
context:
enabled: true
variable_selector:
- '1744251325785'
- result
desc: ''
memory:
role_prefix:
assistant: ''
user: ''
window:
enabled: false
size: 10
model:
completion_params:
temperature: 0.7
mode: chat
name: anthropic.claude-3-5-sonnet-20240620-v1:0
provider: bedrock
prompt_template:
- id: 75ef1bf4-b63b-4112-ac19-863de25d714f
role: system
text: '{{#context#}}'
selected: false
title: LLM
type: llm
variables: []
vision:
configs:
detail: high
enabled: true
height: 97
id: llm
position:
x: 1033.7579811930557
y: 240.61385193672766
positionAbsolute:
x: 1033.7579811930557
y: 240.61385193672766
selected: false
sourcePosition: right
targetPosition: left
type: custom
width: 244
- data:
answer: '{{#llm.text#}}'
desc: ''
selected: false
title: 回答
type: answer
variables: []
height: 106
id: answer
position:
x: 1417.5860697804653
y: 240.61385193672766
positionAbsolute:
x: 1417.5860697804653
y: 240.61385193672766
selected: false
sourcePosition: right
targetPosition: left
type: custom
width: 244
- data:
cases:
- case_id: 'true'
conditions:
- comparison_operator: contains
id: f456ee9b-076f-42d3-b923-d5e6bd671e45
value: 'ON'
varType: string
variable_selector:
- '1744250148284'
- RAG
id: 'true'
logical_operator: and
desc: ''
selected: false
title: IF/ELSE
type: if-else
height: 125
id: '1744251195990'
position:
x: 236.2764581017642
y: 158.40698792460137
positionAbsolute:
x: 236.2764581017642
y: 158.40698792460137
selected: false
sourcePosition: right
targetPosition: left
type: custom
width: 244
- data:
dataset_ids:
- cf3656ef-4763-4a7f-be8d-d26c5e5e58ca
desc: ''
multiple_retrieval_config:
reranking_enable: true
reranking_mode: weighted_score
top_k: 4
weights:
keyword_setting:
keyword_weight: 0.3
vector_setting:
embedding_model_name: amazon.titan-embed-text-v1
embedding_provider_name: bedrock
vector_weight: 0.7
query_variable_selector:
- '1744250148284'
- sys.query
retrieval_mode: multiple
selected: true
title: 知識取得
type: knowledge-retrieval
height: 91
id: '1744251325785'
position:
x: 601.5046934164834
y: 127.42604915556694
positionAbsolute:
x: 601.5046934164834
y: 127.42604915556694
selected: true
sourcePosition: right
targetPosition: left
type: custom
width: 244
viewport:
x: 97.03150392645694
y: 67.62661575788061
zoom: 0.5987393523094646
完成したチャットアプリケーション
完成したアプリケーションは、以下のような形です。
指定したIPアドレス範囲であれば、URLを公開することで、誰でもアクセスできるようになります。
RAG OFFでの挙動
まずは、RAG OFFの場合の挙動を確認してみます、
「SCSKとMLBはどんな関係がありますか?」と入力してみます。
本LLMでは、SCSKとMLBの契約に関する情報は未学習のため”直接的な関係はありません”という回答が生成されています。
RAG ONでの挙動
次に、RAGをONにしてみます。
先程と同様の質問をしてみます。
きちんと、事前に読み込ませたプレスリリースを基に”SCSKとMLBが2年間のパートナーを締結した”旨が回答されましたね。
Difyプラットフォームの社内導入結果
事業部門からコーポレート部門まで幅広い従業員が生成AIアプリケーションを自らの手で構築できるようになり、部署内での業務効率化やナレッジ活用が向上しました。新入社員でも、AIに関する情報収集アプリケーションや商材検索アプリケーション等短期間で業務に役立つAIツールを開発できるようになったことが特筆すべき点です。
今後もAIを活用した業務改革や人材育成を組織レベルで推進していきます。
InfoWeaveについて
弊社では、AWSアカウント上でエンタープライズのセキュリティ要件を満たしたRAGソリューション(InfoWeave)を展開しております。CloudFormationテンプレートで簡単にデプロイでき、エンタープライズシステムの連携も可能です。ご興味がある方はいつでもご相談くださいませ(メールアドレス:cbdc-all@scsk.jp)。
お問い合わせ|企業のDX戦略を加速するハイブリッドクラウドソリューション
最後に
いかがだったでしょうか。
本記事では、DifyをAWSでホスティングする方法とRAGアプリケーションの開発手法を解説しました。
これを機に、皆さんの社内や個人でも、AWS上でDifyをセルフホスティングして、生成AIアプリケーション開発を始めてみてはいかがでしょうか。
本記事が皆様のお役にたてば幸いです。
ではサウナラ~🔥