はじめに
今年もMWS Cupが無事終了しました。今年のplatform担当はNFLabs.の市岡、北村、渡邉、貝塚の計4名です。去年のスコアサーバの課題を洗い出し、改良を加えました。インフラ構築手順や構築時に使用できるテンプレートを紹介いたしますので、CTFの運営をされる方はぜひ読んでいってください。MWS Cupの紹介は前回のブログ記事で行いましたので割愛させていただきます。
スコアサーバの要件と課題
今年もCTFdを使ってMWS Cupのスコアサーバを構築することにしました。スコアサーバの構築にあたって、要件と昨年までの構成の問題点を洗い出しました。
スコアサーバに求められる要件
- 競技時間中、CTFdを使用した競技継続が不能とならないこと(高信頼性)
- 障害が起こった場合、できる限りダウンタイムを短くし、稼働率を高めること(高可用性)
- 障害が起こった場合でも、データ損失の可能性が低い構成であること
- MWS Cup 2023参加者以外に問題が漏洩しないこと
昨年までの構成の問題点
信頼性・可用性が低い
- 昨年までの構成では、AWS上に構築した1つのEC2インスタンスでCTFdを実行していました。インスタンスに障害が発生した場合、競技の中断という事態に直結します。また、リクエスト数の増大を緩和する措置が存在しないため、単一のインスタンスの処理量が過多となると、応答速度の低下は免れません。
- LoadBalancerを導入し、実際の処理を行うインスタンスは冗長構成とする必要があります。
データ損失のリスクが高い
- データの保存領域(RDS)は冗長化されていないため、単一障害点となります。
- EC2インスタンスと同様に冗長化する必要があります。
- 競技中の問題修正が不便
- 昨年度までの構成では問題データ(問題文以外の添付ファイルなど)はEBSに書き込まれるため、事前にバックアップを取る場合はEBSのスナップショットを取る必要があります。競技中かつ問題修正後に障害が発生した場合、事前に取ったバックアップから復旧し、その後に再度、問題の修正を適用する必要があるためダウンタイムが長くなります。(これは可用性の面でも解決する必要がある問題です。)
- EFSをマウントする構成を導入し、問題データ(問題文以外の添付ファイルなど)の書き込みをEFSに行うことで、インスタンス側の障害と問題データの保存箇所を分離する必要があります。この変更により、インスタンスの復旧のみで最新の問題データは保持されたまま参照できるのでダウンタイムが短くなります。
競技インフラの構築
上記の要件や問題点を踏まえ、インフラ構成の改良を行いました。 本年度のMWS Cup競技で使用した最終的なインフラの構成は以下の通りです。
構築手順
1. CloudFormationによるインフラ構築
上記のリソースは全てAWS上に構築しています。 CloudFormationのtemplateにしました。
CloudFormation-template.yaml
AWSTemplateFormatVersion: 2010-09-09 Parameters: ProjectName: Description: unique in this aws account Type: String Default: "cf-platform-2023" resourceNamePrefix: Description: unique in this aws account Type: String Default: "cf-platform" DeployRegion: Description: region to deploy Type: String Default: "ap-northeast-1" FQDN: Description: fqdn of ALB. Type: String Default: "score.example.com" hostedZoneId: Description: route53 zoneid of example.com Type: AWS::Route53::HostedZone::Id keyPair: Description: keypair for ec2 instance Type: AWS::EC2::KeyPair::KeyName AMI: Description: AMI id to use ec2 instance Type: AWS::EC2::Image::Id Default: "ami-09a81b370b76de6a2" EngineVersion: Type: String AllowedValues: - '8.0.28' Default: '8.0.28' DBUsername: Type: String # 文字数制限 MinLength: '1' MaxLength: '16' DBPassword: NoEcho: 'true' Type: String MinLength: '8' MaxLength: '41' DeletionProtection: Type: String AllowedValues: - 'true' - 'false' Default: 'false' Resources: # VPC subnetの設定(public subnet2つとprivate subnet2つ) VPC: Type: AWS::EC2::VPC Properties: CidrBlock: "10.0.0.0/16" EnableDnsHostnames: true EnableDnsSupport: true InstanceTenancy: "default" Tags: - Key: "Name" Value: !Sub "${resourceNamePrefix}-vpc" - Key: "Project" Value: !Ref ProjectName PrivateSubnet1: Type: AWS::EC2::Subnet Properties: CidrBlock: "10.0.128.0/20" AvailabilityZone: !Sub "${DeployRegion}a" Tags: - Key: "Name" Value: !Sub "${resourceNamePrefix}-private1" - Key: "Project" Value: !Ref ProjectName VpcId: !Ref VPC PrivateSubnet2: Type: AWS::EC2::Subnet Properties: CidrBlock: "10.0.144.0/20" AvailabilityZone: !Sub "${DeployRegion}c" Tags: - Key: "Name" Value: !Sub "${resourceNamePrefix}-private2" - Key: "Project" Value: !Ref ProjectName VpcId: !Ref VPC PublicSubnet1: Type: AWS::EC2::Subnet Properties: CidrBlock: "10.0.0.0/20" AvailabilityZone: !Sub "${DeployRegion}a" Tags: - Key: "Name" Value: !Sub "${resourceNamePrefix}-public1" - Key: "Project" Value: !Ref ProjectName VpcId: !Ref VPC PublicSubnet2: Type: AWS::EC2::Subnet Properties: CidrBlock: "10.0.16.0/20" AvailabilityZone: !Sub "${DeployRegion}c" Tags: - Key: "Name" Value: !Sub "${resourceNamePrefix}-public2" - Key: "Project" Value: !Ref ProjectName VpcId: !Ref VPC internetGateway: Type: AWS::EC2::InternetGateway Properties: Tags: - Key: "Name" Value: !Sub "${resourceNamePrefix}-igw" - Key: "Project" Value: !Ref ProjectName internetGatewayAttachment: Type: AWS::EC2::VPCGatewayAttachment Properties: InternetGatewayId: !GetAtt internetGateway.InternetGatewayId VpcId: !Ref VPC EIPForNat: Type: AWS::EC2::EIP Properties: Domain: vpc Tags: - Key: "Name" Value: !Sub "${resourceNamePrefix}-eip" - Key: "Project" Value: !Ref ProjectName NatGateway: Type: AWS::EC2::NatGateway Properties: AllocationId: !GetAtt EIPForNat.AllocationId SubnetId: !Ref PublicSubnet1 Tags: - Key: "Name" Value: !Sub "${resourceNamePrefix}-nat" - Key: "Project" Value: !Ref ProjectName routeTablePublic: Type: AWS::EC2::RouteTable Properties: Tags: - Key: "Name" Value: !Sub "${resourceNamePrefix}-rt-public" - Key: "Project" Value: !Ref ProjectName VpcId: !Ref VPC routeTablePrivate: Type: AWS::EC2::RouteTable Properties: Tags: - Key: "Name" Value: !Sub "${resourceNamePrefix}-rt-private" - Key: "Project" Value: !Ref ProjectName VpcId: !Ref VPC routepublic: Type: AWS::EC2::Route DependsOn: internetGateway Properties: RouteTableId: !Ref routeTablePublic DestinationCidrBlock: 0.0.0.0/0 GatewayId: !GetAtt internetGateway.InternetGatewayId routeTableAssociationPublic1: Type: AWS::EC2::SubnetRouteTableAssociation Properties: RouteTableId: !Ref routeTablePublic SubnetId: !Ref PublicSubnet1 routeTableAssociationPublic2: Type: AWS::EC2::SubnetRouteTableAssociation Properties: RouteTableId: !Ref routeTablePublic SubnetId: !Ref PublicSubnet2 RouteNATGateway: DependsOn: NatGateway Type: AWS::EC2::Route Properties: RouteTableId: !Ref routeTablePrivate DestinationCidrBlock: '0.0.0.0/0' NatGatewayId: !Ref NatGateway RouteTableAssociationPrivate1: Type: AWS::EC2::SubnetRouteTableAssociation Properties: RouteTableId: !Ref routeTablePrivate SubnetId: !Ref PrivateSubnet1 RouteTableAssociationPrivate2: Type: AWS::EC2::SubnetRouteTableAssociation Properties: RouteTableId: !Ref routeTablePrivate SubnetId: !Ref PrivateSubnet2 # データストア EFS: Type: AWS::EFS::FileSystem Properties: BackupPolicy: Status: "DISABLED" Encrypted: true FileSystemTags: - Key: "Name" Value: !Sub "${resourceNamePrefix}-efs" - Key: "Project" Value: !Ref ProjectName LifecyclePolicies: - TransitionToIA: AFTER_30_DAYS - TransitionToPrimaryStorageClass: AFTER_1_ACCESS PerformanceMode: "generalPurpose" ThroughputMode: "bursting" ReplicationConfiguration: Destinations: - AvailabilityZoneName: !Sub "${DeployRegion}c" Region: !Ref DeployRegion EFSMountTarget1: Type: AWS::EFS::MountTarget Properties: FileSystemId: !Ref EFS SubnetId: !Ref PrivateSubnet1 SecurityGroups: - !GetAtt SGForEFS.GroupId EFSMountTarget2: Type: AWS::EFS::MountTarget Properties: FileSystemId: !Ref EFS SubnetId: !Ref PrivateSubnet2 SecurityGroups: - !GetAtt SGForEFS.GroupId SGForEFS: Type: AWS::EC2::SecurityGroup Properties: GroupDescription: 'for efs' GroupName: !Sub "${resourceNamePrefix}-sg-efs" SecurityGroupIngress: - IpProtocol: -1 FromPort: -1 ToPort: -1 SourceSecurityGroupId: !GetAtt SGForEC2.GroupId Tags: - Key: "Name" Value: !Sub "${resourceNamePrefix}-sg-efs" - Key: "Project" Value: !Ref ProjectName VpcId: !Ref VPC RDS: Type: 'AWS::RDS::DBInstance' DeletionPolicy: Delete Properties: DBInstanceIdentifier: !Sub "${resourceNamePrefix}-rds" DBInstanceClass: "db.t3.small" AllocatedStorage: 20 Engine: "mysql" EngineVersion: !Ref EngineVersion MasterUsername: !Ref DBUsername MasterUserPassword: !Ref DBPassword PubliclyAccessible: false StorageType: "gp3" BackupRetentionPeriod: 0 MultiAZ: true StorageEncrypted: true DeletionProtection: !Ref DeletionProtection CopyTagsToSnapshot: true DBSubnetGroupName: !Ref RDSSubnetGroup VPCSecurityGroups: - !GetAtt SGForRDS.GroupId Tags: - Key: "Name" Value: !Sub "${resourceNamePrefix}-rds" - Key: "Project" Value: !Ref ProjectName RDSSubnetGroup: Type: "AWS::RDS::DBSubnetGroup" Properties: DBSubnetGroupDescription: !Sub "for ${resourceNamePrefix}-rds" DBSubnetGroupName: !Sub "${resourceNamePrefix}-rds-subnet-group" SubnetIds: - !Ref PrivateSubnet1 - !Ref PrivateSubnet2 Tags: - Key: "Name" Value: !Sub "${resourceNamePrefix}-rds-subnet-group" - Key: "Project" Value: !Ref ProjectName SGForRDS: Type: AWS::EC2::SecurityGroup Properties: GroupDescription: 'for rds' GroupName: !Sub "${resourceNamePrefix}-sg-rds" SecurityGroupIngress: - IpProtocol: 'tcp' FromPort: 3306 ToPort: 3306 SourceSecurityGroupId: !GetAtt SGForEC2.GroupId Tags: - Key: "Name" Value: !Sub "${resourceNamePrefix}-sg-rds" - Key: "Project" Value: !Ref ProjectName VpcId: !Ref VPC RedisSubnetGroup: Type: AWS::ElastiCache::SubnetGroup Properties: CacheSubnetGroupName: !Sub "${resourceNamePrefix}-redis-subnetgroup" Description: !Sub "${resourceNamePrefix}-redis-SubnetGroup" SubnetIds: - !Ref PrivateSubnet1 - !Ref PrivateSubnet2 Redis: Type: AWS::ElastiCache::ReplicationGroup Properties: AutoMinorVersionUpgrade: true CacheNodeType: "cache.t4g.micro" CacheParameterGroupName: "default.redis7" CacheSubnetGroupName: !Ref RedisSubnetGroup Engine: "redis" EngineVersion: 7.0 MultiAZEnabled: true NodeGroupConfiguration: - ReplicaCount: 1 ReplicationGroupDescription: !Sub "${resourceNamePrefix}-redis" SecurityGroupIds: - !Ref SGForRedis SGForRedis: Type: AWS::EC2::SecurityGroup Properties: GroupDescription: 'for redis' GroupName: !Sub "${resourceNamePrefix}-sg-redis" SecurityGroupIngress: - IpProtocol: "tcp" FromPort: 6379 ToPort: 6379 SourceSecurityGroupId: !GetAtt SGForEC2.GroupId Tags: - Key: "Name" Value: !Sub "${resourceNamePrefix}-sg-redis" - Key: "Project" Value: !Ref ProjectName VpcId: !Ref VPC SGForEC2: Type: AWS::EC2::SecurityGroup Properties: GroupDescription: 'for ec2' GroupName: !Sub "${resourceNamePrefix}-sg-ec2" SecurityGroupEgress: - IpProtocol: -1 FromPort: -1 ToPort: -1 CidrIp: 0.0.0.0/0 SecurityGroupIngress: - IpProtocol: tcp FromPort: 80 ToPort: 80 SourceSecurityGroupId: !GetAtt SGForALB.GroupId Tags: - Key: "Name" Value: !Sub "${resourceNamePrefix}-sg-ec2" - Key: "Project" Value: !Ref ProjectName VpcId: !Ref VPC EC2Role: Type: AWS::IAM::Role Properties: Path: / RoleName: !Sub ${resourceNamePrefix}-EC2Role AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - ec2.amazonaws.com Action: - sts:AssumeRole MaxSessionDuration: 3600 ManagedPolicyArns: - "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore" InstanceProfile: Type: AWS::IAM::InstanceProfile Properties: Path: / Roles: - !Ref EC2Role # EC2インスタンス2つ立てる EC2one: Type: AWS::EC2::Instance Properties: BlockDeviceMappings: - DeviceName: /dev/sda1 Ebs: VolumeSize: 8 CreditSpecification: CPUCredits: "standard" IamInstanceProfile: !Ref InstanceProfile ImageId: !Ref AMI InstanceType: "t3.micro" KeyName: !Ref keyPair Monitoring: false PropagateTagsToVolumeOnCreation: true Tags: - Key: "Name" Value: !Sub "${resourceNamePrefix}-ec2-1" - Key: "Project" Value: !Ref ProjectName NetworkInterfaces: - AssociatePublicIpAddress: "false" DeviceIndex: "0" GroupSet: - !GetAtt SGForEC2.GroupId SubnetId: !Ref PrivateSubnet1 EC2two: Type: AWS::EC2::Instance Properties: BlockDeviceMappings: - DeviceName: /dev/sda1 Ebs: VolumeSize: 8 CreditSpecification: CPUCredits: "standard" IamInstanceProfile: !Ref InstanceProfile ImageId: !Ref AMI InstanceType: "t3.micro" Monitoring: false PropagateTagsToVolumeOnCreation: true Tags: - Key: "Name" Value: !Sub "${resourceNamePrefix}-ec2-2" - Key: "Project" Value: !Ref ProjectName NetworkInterfaces: - AssociatePublicIpAddress: "false" DeviceIndex: "0" GroupSet: - !GetAtt SGForEC2.GroupId SubnetId: !Ref PrivateSubnet2 ALBTargetGroup: Type: AWS::ElasticLoadBalancingV2::TargetGroup Properties: Name: !Sub "${resourceNamePrefix}-tg" Tags: - Key: "Name" Value: !Sub "${resourceNamePrefix}-tg" - Key: "Project" Value: !Ref ProjectName Port: 80 Protocol: HTTP Matcher: HttpCode: '200' VpcId: !Ref VPC TargetType: instance Targets: - Id: !Ref EC2one - Id: !Ref EC2two ALB: Type: AWS::ElasticLoadBalancingV2::LoadBalancer Properties: Type: "application" Scheme: "internet-facing" Name: !Sub ${resourceNamePrefix}-alb Tags: - Key: Name Value: !Sub ${resourceNamePrefix}-alb - Key: "Project" Value: !Ref ProjectName Subnets: - !Ref PublicSubnet1 - !Ref PublicSubnet2 SecurityGroups: - !Ref SGForALB SGForALB: Type: AWS::EC2::SecurityGroup Properties: GroupDescription: 'for alb' GroupName: !Sub "${resourceNamePrefix}-sg-alb" SecurityGroupIngress: - IpProtocol: 'tcp' FromPort: 443 ToPort: 443 CidrIp: 0.0.0.0/0 - IpProtocol: 'tcp' FromPort: 80 ToPort: 80 CidrIp: 0.0.0.0/0 Tags: - Key: "Name" Value: !Sub "${resourceNamePrefix}-sg-alb" - Key: "Project" Value: !Ref ProjectName VpcId: !Ref VPC ListenerHTTP: Type: AWS::ElasticLoadBalancingV2::Listener Properties: DefaultActions: - Type: redirect RedirectConfig: Protocol: HTTPS Port: 443 Host: '#{host}' Path: '/#{path}' Query: '#{query}' StatusCode: 'HTTP_301' LoadBalancerArn: !Ref ALB Port: 80 Protocol: HTTP ListenerHTTPS: Type: AWS::ElasticLoadBalancingV2::Listener Properties: Certificates: - CertificateArn: !Ref Cert DefaultActions: - Type: authenticate-cognito Order: 1 AuthenticateCognitoConfig: UserPoolArn: !GetAtt UserPool.Arn UserPoolClientId: !Ref UserPoolClient UserPoolDomain: !Ref UserPoolDomain - Type: forward TargetGroupArn: !Ref ALBTargetGroup Order: 2 LoadBalancerArn: !Ref ALB Port: 443 Protocol: HTTPS UserPool: Type: AWS::Cognito::UserPool Properties: UserPoolName: !Sub "${resourceNamePrefix}-userpool" UsernameConfiguration: CaseSensitive: true Schema: - Name: name AttributeDataType: String Required: true Policies: PasswordPolicy: MinimumLength: 9 RequireLowercase: false RequireNumbers: false RequireSymbols: false RequireUppercase: false TemporaryPasswordValidityDays: 90 AdminCreateUserConfig: AllowAdminCreateUserOnly: true AccountRecoverySetting: RecoveryMechanisms: - Name: admin_only Priority: 1 UserPoolClient: Type: "AWS::Cognito::UserPoolClient" Properties: ClientName: !Sub "${resourceNamePrefix}-client" GenerateSecret: true UserPoolId: !Ref UserPool CallbackURLs: - !Sub "https://${FQDN}/oauth2/idpresponse" AllowedOAuthFlows: - "code" AllowedOAuthScopes: - "email" - "openid" - "phone" AllowedOAuthFlowsUserPoolClient: true ExplicitAuthFlows: - "ALLOW_USER_SRP_AUTH" - "ALLOW_REFRESH_TOKEN_AUTH" - "ALLOW_USER_PASSWORD_AUTH" SupportedIdentityProviders: - "COGNITO" UserPoolDomain: Type: "AWS::Cognito::UserPoolDomain" Properties: Domain: !Sub "${resourceNamePrefix}-domain" UserPoolId: !Ref UserPool Cert: Type: AWS::CertificateManager::Certificate Properties: DomainName: !Ref FQDN Tags: - Key: Name Value: !Sub ${resourceNamePrefix}-cert - Key: "Project" Value: !Ref ProjectName ValidationMethod: DNS DomainValidationOptions: - DomainName: !Ref FQDN HostedZoneId: !Ref hostedZoneId ALBARecord: Type: AWS::Route53::RecordSet Properties: HostedZoneId: !Ref hostedZoneId Name: !Ref FQDN Type: A AliasTarget: DNSName: !GetAtt ALB.DNSName HostedZoneId: !GetAtt ALB.CanonicalHostedZoneID EvaluateTargetHealth: false Outputs: EFSFileSystemId: Description: efs Value: !Ref EFS
このテンプレートを実行することによって、今回の環境で使用するインフラについては全て構築されます。
テンプレートに必要なパラメータは以下の通りです。
パラメータ名 | 設定内容 | 設定例 |
---|---|---|
AMI | 最新のubuntuのAMI | ami-09a81b370b76de6a2 |
DBPassword | 新たに設定するDBのパスワード | LYimv6sXnj72jGRH |
DBUsername | 新たに設定するDBのユーザ名 | dbuser |
DeletionProtection | DBの削除保護を有効化するか否か | false |
DeployRegion | デプロイ先のリージョン | ap-northeast-1 |
EngineVersion | デプロイするMySQLのバージョン | 8.0.28 |
FQDN | CTFdにアクセスするときのFQDN | test.example.com |
hostedZoneId | 上記のFQDNを名前解決するためのRoute 53 ホストゾーン | Z00000000D00M0FZRZ0BI |
keyPair | EC2インスタンス用のキーペアの名前 | MWS-Score-Server-Keypair |
ProjectName | Project タグの値 作成するすべてのリソースに可能な限りProject タグを付けます。 |
cf-platform-2023 |
resourceNamePrefix | リソースの名前の先頭につける名前 | cf-platform-test1 |
2. EC2インスタンスでの作業
EC2インスタンスへのアクセスにはAWS Systems Manager Session Manager(以下、SSM)を使用します。SSMを使用することで、プライベートサブネットに存在するインスタンスにBastionなしでアクセスすることができ、セキュリティグループでSSH接続を許可する必要もありません。(メリット多いのでぜひ使いましょう) さて、本題。アクセス後、EC2インスタンス内で必要な作業を以下に示します。
※ これらの作業は構築したインスタンスすべて(テンプレート通りなら2つ)に対して行います。全てのインスタンスに対して作業を行わない場合、一つのインスタンスで作業を行った後に、当該インスタンスをもとにAMIを作成して、作成したAMIをもとに他インスタンスを起動します。(テンプレートでAMI指定部分だけ変更した後に、CloudFormationの変更セット機能を使用すると楽です。)
2.1 必要なサービスのインストール
以下ではSSMでシェルにアクセスし、cd
コマンドで/home/ssm-user
に移動後に作業することをお勧めします。
- Dockerや
docker compose
をインストール- 以下のWebサイトを参考に、
docker compose
を使えるようにします。 https://docs.docker.com/engine/install/ubuntu/
- 以下のWebサイトを参考に、
- CTFdをダウンロード
- 以下のURLから新しいバージョンを見つけて、ダウンロードします。 https://github.com/CTFd/CTFd/releases
sudo apt install unzip wget https://github.com/CTFd/CTFd/archive/refs/tags/3.6.0.zip unzip 3.6.0.zip
2.2. EFSのマウント
EFSをマウントします。NFSは起動のたびにマウントする必要があるため、systemdを使います。
NFSに必要なパッケージをインストールします。
sudo apt install nfs-common
次に、/etc/systemd/system/auto-mount.service
という名前でファイルを生成し、マウントコマンドを書き込みます。
Description = Auto EFS Mount. After=local-fs.target ConditionPathExists=/home/ssm-user/CTFd-3.6.0/.data [Service] ExecStart=/home/ssm-user/AutoMount.sh Restart=no Type=simple [Install] WantedBy=multi-user.target
/home/ssm-user/AutoMount.sh
に以下を書き込みます。
#!/usr/bin/bash mkdir -p "/home/ssm-user/CTFd-3.6.0/.data" mount -t nfs4 -o nfsvers=4.1,rsize=1048576,wsize=1048576,hard,timeo=600,retrans=2,noresvport <ファイルシステムID>.efs.<リージョン名>.amazonaws.com:/ "/home/ssm-user/CTFd-3.6.0/.data"
<ファイルシステムID>
はCloudFormationの出力EFSFileSystemId
の値に置き換えてください。また<リージョン名>
はCloudFormationのパラメータで指定した値に置き換えてください。
一般的にホームディレクトリにrootが実行するコマンドを置くことはよくないかもしれませんが、ユーザubuntuはパスワードなしでsudoを実行できる設定のため、新たな脅威にはならないと判断しました。気になる方はsudo chown root:root /home/ssm-user/AutoMount.sh
とchmod 755 /home/ssm-user/AutoMount.sh
を実行してください。
2.3. docker-compose.ymlの修正
データの保持はAWSのマネージドサービス(RDS、ElastiCache、EFS)に移動させたため、CTFd公式のdocker-compose.yml
を以下のように修正しました。
version: '2' services: ctfd: build: . user: root restart: always ports: - "8000:8000" environment: - UPLOAD_FOLDER=/var/uploads - DATABASE_URL=mysql+pymysql://<データベースのユーザ名>:<データベースのパスワード>@<データベースのエンドポイントのURL>/ctfd - REDIS_URL=redis://<ElastiCache for Redisの設定エンドポイントのURL>:6379 - WORKERS=2 - SECRET_KEY=aaaaaa - LOG_FOLDER=/var/log/CTFd - ACCESS_LOG=- - ERROR_LOG=- - REVERSE_PROXY=true volumes: - .data/CTFd/logs1:/var/log/CTFd - .data/CTFd/uploads:/var/uploads - .:/opt/CTFd:ro networks: default: internal: nginx: image: nginx:stable restart: always volumes: - ./conf/nginx/http.conf:/etc/nginx/nginx.conf ports: - 80:80 depends_on: - ctfd networks: default: internal: internal: true
<データベースのユーザ名>
と<データベースのパスワード>
はCloudFormationのパラメータで指定した値に置き換えてください。
<データベースのエンドポイントのURL>
と<ElastiCache for Redisの設定エンドポイントのURL>
はAWS Management Consoleで確認して置き換えてください。
パスワードやSECRET_KEY
について、パスワード生成器などを使って十分な強度を確保してください。
いよいよ、起動します。
sudo docker compose up -d
3. AWS Cognitoの設定
CloudFormationテンプレートで作成したCognito UserPoolに競技関係者用のユーザーとその資格情報を作成します。
CTFdではパスワードポリシを設定できないことから、MWS Cup参加者以外にログインされるリスクが高いと判断しました。このため、CTFdにアクセスする際に、参加者共通のパスワードを要求するためにAWS Cognitoを使用しています。
工夫した点について
- CTFdが保存する情報をすべてマネージドサービスに移動(信頼性・可用性バク上がり!!!)
- CTFdが保存する情報をすべて以下のマネージドサービスに移動させることで、2つのアベイラビリティゾーンにまたがって保存されるようにしました。
- RDB(データベース)
- ElastiCache for Redis(キャッシュ)
- EFS(ファイルシステム)
- CTFdが保存する情報をすべて以下のマネージドサービスに移動させることで、2つのアベイラビリティゾーンにまたがって保存されるようにしました。
- ALBを導入することでEC2インスタンスの負荷分散
- EC2インスタンスのサイズをt3.mediumにし、2台構成としました。昨年度はm5.xlarge1台で運用していました。結果として今年度の構成はインスタンスの数は増えていますが、より低コストで、冗長化された構成となりました。
- Basic認証からAWS Cognitoに変更
- Dockerで動いていたNginxの設定を修正する必要が無くなりました。(ちょっとだけ手間が省けた!)
実際のアクセス状況
今年度のMWS Cupの競技者人数は88名、スタッフを含めた関係者を合わせると、この環境にアクセスする人数はおよそ120名程度となりました。 MWS Cupでは、開始時刻直後と、終了時刻直後にアクセス数のピークが来ます。 LoadBalancerのダッシュボード上で確認したところ、レスポンスタイムは開始直後で、最大1.8秒かかりました。
リクエスト数はピーク時の開始直後に約2700リクエスト、終了直後に約2000リクエストほどが来ていました。
競技後、競技者の何人かにスコアサーバの応答速度についての不満をインタビューしてみましたが、特に不満を感じない応答速度だったようです。(やったね)
昨年度はEC2インスタンスがそのまま外部からのアクセス先になっていたため、通信状況をAWSコンソール上で可視化することはできませんでした。(CTFdの機能としてローカルファイルに通信ログを保存はしていましたが、視認性が悪かったです。) 今回はALBを通すことでALBのメトリクスとして通信状況が可視化でき、簡単に確認が出来るようになりました!
おわりに
MWS Cupのスコアサーバで実際に使用したAWSの設定やCTFdの設定を紹介しました。 CTFdを使って参加人数が少し大きい大会を開く方々の参考になればうれしいです。