こんにちは。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アプリケーション開発を始めてみてはいかがでしょうか。
本記事が皆様のお役にたてば幸いです。
ではサウナラ~🔥