サーバレスで実現する!ゲノムブラウザ JBrowse 2 の実行環境
ゲノムブラウザ JBrowse2 のサーバレス構成について、研究現場からのフィードバックを反映した改善版を紹介します。本記事では、CloudFront + S3 による基本構成に加え、以下の改善を実施しました。
- CloudFront Origin Access Control (OAC) の採用
- CloudFront 標準ログ(v2)の採用
- 複数プロジェクトでの CloudFront のログ集約管理
ゲノムブラウザをサーバレスな構成にすることのメリット
サーバレスなので Linux などのサーバの管理が不要になります。利用費はデータ保存料(サイズが大きなファイルは主にゲノムデータ)と、データ転送料のみでとても安価に運用できます。CDN(Contents Delivery Network)を介して配信するため、アクセスが集中しても問題なく、世界中からレスポンス良くアクセスできます。
デメリット
AWS の学習コストが発生することです。ですが、今の時代オンプレミス環境で Web サーバを起動して配信環境の構築、日々の運用負担を考えると学習価値はあると思います。実際問題、運用フェーズですと一番高いのは人件費ですからね。サーバのお守りに費やす時間を削減しましょう。
構成概要
一般的な CloundFront + S3 構成です。また、前回の構成から以下の AWS のアップデートを反映させた構成としました。
- [NEW] CloudFrontからS3への新たなアクセス制御方法としてOrigin Access Control (OAC)が発表されました! | DevelopersIO
- CloudFront 標準ログがアップデート!新機能のパーティションやJSON出力を試してみた | DevelopersIO
運用面の改善
ドメインの管理を省略するために独自のドメインを使用せずに CloundFront デフォルトの URL を使用して https アクセスする方針とします。
また、CloudFront のアクセスログは複数の CloundFront ディストリビューションから単一の S3 バケットに保存する方針とします。
CloudFront + S3 環境構築面の改善
運用面の改善に伴い、CloudFormation テンプレートを 2 つに分けました。初回に 1 度作成が必要な S3 バケット(青色)と、配信環境用の CloundFront + S3 セット(ピンク色)に分けています。
これにより、研究プロジェクト毎に CloudFront + S3 セットを簡単に量産できます。
CloudFormation テンプレート
テンプレートの分割理由
- 共有リソース(ログバケット)の一元管理
- プロジェクト固有リソースの独立したライフサイクル管理
- 環境構築の再利用性向上
CloudFront 用のログ保存 S3 バケット作成
- S3 バケットの基本設定
- バケット名は AWS アカウント ID をサフィックスとして使用
- バケット名はハードコードしているため、必要に応じて修正してください
multiple-cloudfront-logs-bucket-[AWSアカウントID]
- バージョニングを有効化
- セキュリティ設定
- CloudFront からのアクセスのみを許可(OAC を採用)
- ライフサイクルルール
- 保存したログについては削除しない限りは永続保存
- 7 日以上経過した古いバージョン(削除したログ) を自動削除
- 未完了のマルチパートアップロードを 7 日後に削除
バケットポリシーは制限を厳しくしかったのですが、今後追加されていく複数の CloudFront ディストリビューションに備えてガチガチな制限はしていません。CloudFront 作成後に都度バケットポリシーを変更する運用が可能でしたら、CloudFrontn のディストリビューション名で制限入れた方がベターです。
テンプレート 1
AWSTemplateFormatVersion: "2010-09-09"
Description: CloudFront Logs S3 Bucket for Access Log v2 (Shared by multiple CloudFront distributions)
Parameters:
NoncurrentVersionRetentionDays:
Description: Number of days to retain noncurrent versions
Type: Number
Default: 7
MinValue: 1
MaxValue: 365
Metadata:
AWS::CloudFormation::Interface:
ParameterGroups:
- Label:
default: "Required input parameters"
Parameters:
- NoncurrentVersionRetentionDays
Resources:
S3BucketLogs:
Type: AWS::S3::Bucket
DeletionPolicy: Retain
UpdateReplacePolicy: Retain
Properties:
BucketName: !Sub multiple-cloudfront-logs-bucket-${AWS::AccountId}
OwnershipControls:
Rules:
- ObjectOwnership: BucketOwnerEnforced
PublicAccessBlockConfiguration:
BlockPublicAcls: True
BlockPublicPolicy: True
IgnorePublicAcls: True
RestrictPublicBuckets: True
VersioningConfiguration:
Status: Enabled
BucketEncryption:
ServerSideEncryptionConfiguration:
- ServerSideEncryptionByDefault:
SSEAlgorithm: "AES256"
BucketKeyEnabled: true
LifecycleConfiguration:
Rules:
- Id: AbortIncompleteMultipartUpload
AbortIncompleteMultipartUpload:
DaysAfterInitiation: 7
Status: "Enabled"
- Id: DeleteOldVersions
NoncurrentVersionExpiration:
NoncurrentDays: !Ref NoncurrentVersionRetentionDays
Status: Enabled
S3BucketLogsPolicy:
Type: AWS::S3::BucketPolicy
Properties:
Bucket: !Ref S3BucketLogs
PolicyDocument:
Version: '2012-10-17'
Statement:
- Sid: AllowCloudFrontServicePrincipalWrite
Effect: Allow
Principal:
Service: delivery.logs.amazonaws.com
Action:
- s3:PutObject
Resource: !Sub ${S3BucketLogs.Arn}/*
Condition:
StringEquals:
aws:SourceAccount: !Ref 'AWS::AccountId'
Outputs:
LogsBucketName:
Description: Name of the CloudFront logs bucket
Value: !Ref S3BucketLogs
Export:
Name: !Sub ${AWS::StackName}-LogsBucketName
LogsBucketArn:
Description: ARN of the CloudFront logs bucket
Value: !GetAtt S3BucketLogs.Arn
Export:
Name: !Sub ${AWS::StackName}-LogsBucketArn
LogsBucketDomainName:
Description: Domain name of the CloudFront logs bucket
Value: !GetAtt S3BucketLogs.DomainName
Export:
Name: !Sub ${AWS::StackName}-LogsBucketDomainName
CloudFront + S3 配信構成セット
- S3 バケットの設定
- 入力したプロジェクト名とアカウント ID によるユニークなバケット名を作成
- 30 日後に STANDARD_IA ストレージクラスへ自動移行
- 削除、または上書きしたファイルは最新から 3 バージョンのみ保持
- CloudFront の設定
- OAC によるセキュアなアクセス制御
- 1 秒キャッシュポリシーの適用
- HTTP/2 および IPv6 の有効化
- ログ配信の設定
- 標準ログ(v2)を採用
- JSON 形式でのログ出力
テンプレート 2
AWSTemplateFormatVersion: "2010-09-09"
Description: Static contents distribution using S3 and CloudFront with OAC
Parameters:
ProjectName:
Description: Project Name
Type: String
Description:
Description: Description for CloudFront Distribution
Type: String
Metadata:
AWS::CloudFormation::Interface:
ParameterGroups:
- Label:
default: "Required input parameters"
Parameters:
- ProjectName
- Description
Resources:
S3Bucket:
Type: AWS::S3::Bucket
DeletionPolicy: Retain
Properties:
BucketName: !Sub ${ProjectName}-bucket-${AWS::AccountId}
OwnershipControls:
Rules:
- ObjectOwnership: "BucketOwnerEnforced"
PublicAccessBlockConfiguration:
BlockPublicAcls: True
BlockPublicPolicy: True
IgnorePublicAcls: True
RestrictPublicBuckets: True
VersioningConfiguration:
Status: Enabled
BucketEncryption:
ServerSideEncryptionConfiguration:
- ServerSideEncryptionByDefault:
SSEAlgorithm: "AES256"
BucketKeyEnabled: true
LifecycleConfiguration:
Rules:
- Id: AbortIncompleteMultipartUpload
AbortIncompleteMultipartUpload:
DaysAfterInitiation: 7
Status: "Enabled"
- Id: TransitionToIA
Transitions:
- StorageClass: STANDARD_IA
TransitionInDays: 30
Status: "Enabled"
- Id: NoncurrentVersionExpiration
NoncurrentVersionExpiration:
NewerNoncurrentVersions: 3
NoncurrentDays: 7
Status: Enabled
BucketPolicy:
Type: AWS::S3::BucketPolicy
Properties:
Bucket: !Ref S3Bucket
PolicyDocument:
Version: '2012-10-17'
Statement:
- Sid: AllowCloudFrontServicePrincipal
Effect: Allow
Principal:
Service: cloudfront.amazonaws.com
Action: s3:GetObject
Resource: !Sub ${S3Bucket.Arn}/*
Condition:
StringEquals:
AWS:SourceArn: !Sub arn:aws:cloudfront::${AWS::AccountId}:distribution/${CloudFrontDistribution}
CloudFrontOriginAccessControl:
Type: AWS::CloudFront::OriginAccessControl
Properties:
OriginAccessControlConfig:
Name: !Sub ${ProjectName}-OAC
Description: Origin Access Control for S3
SigningBehavior: always
SigningProtocol: sigv4
OriginAccessControlOriginType: s3
OneSecondCachePolicy:
Type: AWS::CloudFront::CachePolicy
Properties:
CachePolicyConfig:
Name: !Sub ${ProjectName}-cache-policy
Comment: "Cache for 1 second"
DefaultTTL: 1
MaxTTL: 1
MinTTL: 1
ParametersInCacheKeyAndForwardedToOrigin:
CookiesConfig:
CookieBehavior: none
HeadersConfig:
HeaderBehavior: none
QueryStringsConfig:
QueryStringBehavior: none
EnableAcceptEncodingBrotli: true
EnableAcceptEncodingGzip: true
CloudFrontLogsDeliverySource:
Type: AWS::Logs::DeliverySource
Properties:
Name: !Sub ${ProjectName}-cloudfront-delivery-source
ResourceArn: !Sub arn:aws:cloudfront::${AWS::AccountId}:distribution/${CloudFrontDistribution}
LogType: ACCESS_LOGS
CloudFrontLogsDeliveryDestination:
Type: AWS::Logs::DeliveryDestination
Properties:
Name: !Sub ${ProjectName}-cloudfront-delivery-destination
DestinationResourceArn: !Sub arn:aws:s3:::multiple-cloudfront-logs-bucket-${AWS::AccountId}
OutputFormat: json
CloudFrontLogsDelivery:
Type: AWS::Logs::Delivery
DependsOn: CloudFrontLogsDeliverySource
Properties:
DeliverySourceName: !Sub ${ProjectName}-cloudfront-delivery-source
DeliveryDestinationArn: !GetAtt CloudFrontLogsDeliveryDestination.Arn
RecordFields:
- timestamp
- DistributionId
- date
- time
- x-edge-location
- sc-bytes
- c-ip
- cs-method
- cs(Host)
- cs-uri-stem
- sc-status
- cs(Referer)
- cs(User-Agent)
- cs-uri-query
- cs(Cookie)
- x-edge-result-type
- x-edge-request-id
- x-host-header
- cs-protocol
- cs-bytes
- time-taken
- x-forwarded-for
- ssl-protocol
- ssl-cipher
- x-edge-response-result-type
- cs-protocol-version
- fle-status
- fle-encrypted-fields
- c-port
- time-to-first-byte
- x-edge-detailed-result-type
- sc-content-type
- sc-content-len
- sc-range-start
- sc-range-end
- timestamp(ms)
- origin-fbl
- origin-lbl
- asn
- c-country
- cache-behavior-path-pattern
S3SuffixPath: !Sub ${ProjectName}/{yyyy}/{MM}/{dd}/{HH}
S3EnableHiveCompatiblePath: false
CloudFrontDistribution:
Type: AWS::CloudFront::Distribution
Properties:
DistributionConfig:
Comment: !Ref Description
PriceClass: PriceClass_200
Origins:
- DomainName: !GetAtt S3Bucket.RegionalDomainName
Id: S3Origin
OriginAccessControlId: !Ref CloudFrontOriginAccessControl
S3OriginConfig:
OriginAccessIdentity: ''
DefaultRootObject: index.html
DefaultCacheBehavior:
TargetOriginId: S3Origin
Compress: true
ViewerProtocolPolicy: redirect-to-https
CachePolicyId: !Ref OneSecondCachePolicy
OriginRequestPolicyId: 88a5eaf4-2fd4-4709-b370-b4c650ea3fcf # CORS-S3Origin
AllowedMethods:
- GET
- HEAD
CachedMethods:
- GET
- HEAD
HttpVersion: http2
IPV6Enabled: true
Enabled: true
Outputs:
CloudFrontDomainName:
Description: CloudFront Distribution Domain Name
Value: !GetAtt CloudFrontDistribution.DomainName
S3BucketName:
Description: S3 Bucket Name
Value: !Ref S3Bucket
S3BucketArn:
Description: S3 Bucket ARN
Value: !GetAtt S3Bucket.Arn
CloudFrontLogsDeliveryArn:
Description: CloudFront Logs Delivery ARN
Value: !GetAtt CloudFrontLogsDelivery.Arn
CloudFront の URL にアクセスした結果
CloudFront のデフォルトの URL にアクセスしてみます。問題なく JBrowse の画面を操作できます。
コンテンツのアップロード方法は前回の記事を参考にしてください。
ゲノムブラウザ JBrowse2 を CloudFront + S3 で Web 公開してみた | DevelopersIO
ログ確認
CloudFront のアクセスログは、プロジェクト名ごとに以下のような階層構造で保存されます。
以下は CloudFront + S3 のセットを 2 組作成しているため、プロジェクト名が 2 つ(abashiri と kitami)があります。
multiple-cloudfront-logs-bucket-257284627556
└── AWSLogs
└── 257284627556
└── CloudFront
├── abashiri # <- CloudFormation テンプレートで入力したプロジェクト名
│ └── 2025
│ └── 01
│ └── 31
│ ├── 06
│ │ └── E2K8036MS1GDS5.2025-01-31-06.a57adb5e.gz
│ └── 07
│ ├── E2K8036MS1GDS5.2025-01-31-07.24dea1d9.gz
│ └── E2K8036MS1GDS5.2025-01-31-07.7610839b.gz
└── kitami # <- 同じくプロジェクト名
└── 2025
└── 01
└── 31
└── 07
└── E2F5T7ZC07SF6T.2025-01-31-07.d42c4df1.gz
Athena でログ検索
CloudFront の標準ログ(v2)で JSON 形式で出力するようにしました。Athena で検索が可能か試してみます。
プロジェクト名毎にプレフィックスが異なるため、abashiri
プロジェクトのログを検索するためのテーブルを作成しました。
CREATE EXTERNAL TABLE `cloudfront_logs_abashiri`(
`timestamp` string COMMENT 'from deserializer',
`DistributionId` string COMMENT 'from deserializer',
`date` string COMMENT 'from deserializer',
`time` string COMMENT 'from deserializer',
`x-edge-location` string COMMENT 'from deserializer',
`sc-bytes` string COMMENT 'from deserializer',
`c-ip` string COMMENT 'from deserializer',
`cs-method` string COMMENT 'from deserializer',
`cs(Host)` string COMMENT 'from deserializer',
`cs-uri-stem` string COMMENT 'from deserializer',
`sc-status` string COMMENT 'from deserializer',
`cs(Referer)` string COMMENT 'from deserializer',
`cs(User-Agent)` string COMMENT 'from deserializer',
`cs-uri-query` string COMMENT 'from deserializer',
`cs(Cookie)` string COMMENT 'from deserializer',
`x-edge-result-type` string COMMENT 'from deserializer',
`x-edge-request-id` string COMMENT 'from deserializer',
`x-host-header` string COMMENT 'from deserializer',
`cs-protocol` string COMMENT 'from deserializer',
`cs-bytes` string COMMENT 'from deserializer',
`time-taken` string COMMENT 'from deserializer',
`x-forwarded-for` string COMMENT 'from deserializer',
`ssl-protocol` string COMMENT 'from deserializer',
`ssl-cipher` string COMMENT 'from deserializer',
`x-edge-response-result-type` string COMMENT 'from deserializer',
`cs-protocol-version` string COMMENT 'from deserializer',
`fle-status` string COMMENT 'from deserializer',
`fle-encrypted-fields` string COMMENT 'from deserializer',
`c-port` string COMMENT 'from deserializer',
`time-to-first-byte` string COMMENT 'from deserializer',
`x-edge-detailed-result-type` string COMMENT 'from deserializer',
`sc-content-type` string COMMENT 'from deserializer',
`sc-content-len` string COMMENT 'from deserializer',
`sc-range-start` string COMMENT 'from deserializer',
`sc-range-end` string COMMENT 'from deserializer',
`timestamp(ms)` string COMMENT 'from deserializer',
`origin-fbl` string COMMENT 'from deserializer',
`origin-lbl` string COMMENT 'from deserializer',
`asn` string COMMENT 'from deserializer',
`c-country` string COMMENT 'from deserializer',
`cache-behavior-path-pattern` string COMMENT 'from deserializer'
)
PARTITIONED BY (
year STRING,
month STRING,
day STRING,
hour STRING
)
ROW FORMAT SERDE
'org.openx.data.jsonserde.JsonSerDe'
STORED AS INPUTFORMAT
'org.apache.hadoop.mapred.TextInputFormat'
OUTPUTFORMAT
'org.apache.hadoop.hive.ql.io.HiveIgnoreKeyTextOutputFormat'
LOCATION
's3://multiple-cloudfront-logs-bucket-257284627556/AWSLogs/257284627556/CloudFront/abashiri/'
TBLPROPERTIES (
'compressionType'='gzip',
'has_encrypted_data'='false',
'ignore.malformed.json'='true',
'projection.enabled'='true',
'projection.year.type'='integer',
'projection.year.range'='2024,2025',
'projection.month.type'='integer',
'projection.month.range'='1,12',
'projection.month.digits'='2',
'projection.day.type'='integer',
'projection.day.range'='1,31',
'projection.day.digits'='2',
'projection.hour.type'='integer',
'projection.hour.range'='0,23',
'projection.hour.digits'='2',
'storage.location.template'='s3://multiple-cloudfront-logs-bucket-257284627556/AWSLogs/257284627556/CloudFront/abashiri/${year}/${month}/${day}/${hour}'
);
ログの検索もできました。
まとめ
- CloudFront + S3 のサーバレス構成による運用負荷の削減
- OAC 採用とログ集約による、セキュリティと運用性の向上
- CloudFormation テンプレートの分割による、環境構築の再利用性向上
おわりに
CloudFront の標準ログ(v2)設定に苦労しました。もう少しバケットポリシーの制限を頑張れないか考えてみます。