Last active
November 25, 2024 02:11
-
-
Save tohutohu/22c782cafd4088048cda51b1252a3c6f to your computer and use it in GitHub Desktop.
ISUCON9 予選素振り用 CloudFormation 簡易ポータル付き
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
AWSTemplateFormatVersion: '2010-09-09' | |
Description: 'ISUCON9 Qualifier template' | |
Parameters: | |
KeyPairName: | |
Description: "Amazon EC2 Key Pair" | |
Type: AWS::EC2::KeyPair::KeyName | |
GitHubUsername: | |
Description: "GitHub Username for SSH public key" | |
Type: String | |
Resources: | |
MyVPC: | |
Type: 'AWS::EC2::VPC' | |
Properties: | |
CidrBlock: '192.168.0.0/16' | |
EnableDnsSupport: true | |
EnableDnsHostnames: true | |
MyInternetGateway: | |
Type: 'AWS::EC2::InternetGateway' | |
GatewayAttachment: | |
Type: 'AWS::EC2::VPCGatewayAttachment' | |
Properties: | |
VpcId: !Ref MyVPC | |
InternetGatewayId: !Ref MyInternetGateway | |
MySubnet: | |
Type: 'AWS::EC2::Subnet' | |
Properties: | |
VpcId: !Ref MyVPC | |
CidrBlock: '192.168.1.0/24' | |
AvailabilityZone: ap-northeast-1a | |
MyRouteTable: | |
Type: 'AWS::EC2::RouteTable' | |
Properties: | |
VpcId: !Ref MyVPC | |
MyRoute: | |
Type: 'AWS::EC2::Route' | |
DependsOn: GatewayAttachment | |
Properties: | |
RouteTableId: !Ref MyRouteTable | |
DestinationCidrBlock: '0.0.0.0/0' | |
GatewayId: !Ref MyInternetGateway | |
SubnetRouteTableAssociation: | |
Type: 'AWS::EC2::SubnetRouteTableAssociation' | |
Properties: | |
SubnetId: !Ref MySubnet | |
RouteTableId: !Ref MyRouteTable | |
MySecurityGroup: | |
Type: 'AWS::EC2::SecurityGroup' | |
Properties: | |
GroupDescription: 'Enable SSH access' | |
VpcId: !Ref MyVPC | |
SecurityGroupIngress: | |
- IpProtocol: tcp | |
FromPort: 22 | |
ToPort: 22 | |
CidrIp: 0.0.0.0/0 | |
- IpProtocol: tcp | |
FromPort: 80 | |
ToPort: 80 | |
CidrIp: 0.0.0.0/0 | |
- IpProtocol: tcp | |
FromPort: 443 | |
ToPort: 443 | |
CidrIp: 0.0.0.0/0 | |
- IpProtocol: -1 | |
CidrIp: 192.168.0.0/16 | |
Server1: | |
Type: 'AWS::EC2::Instance' | |
Properties: | |
ImageId: 'ami-03b1b78bb1da5122f' | |
InstanceType: c6i.large | |
SubnetId: !Ref MySubnet | |
SecurityGroupIds: | |
- !Ref MySecurityGroup | |
KeyName: !Ref KeyPairName | |
PrivateIpAddress: '192.168.1.10' | |
UserData: | |
Fn::Base64: !Sub | | |
#!/bin/bash | |
GITHUB_USER=${GitHubUsername} | |
mkdir -p /home/isucon/.ssh | |
curl -s https://github.com/$GITHUB_USER.keys >> /home/isucon/.ssh/authorized_keys | |
chown -R isucon:isucon /home/isucon/.ssh | |
chmod 600 /home/isucon/.ssh/authorized_keys | |
cd /home/isucon/isucari | |
find . ! -path "./webapp*" -exec rm -rf {} + | |
wget https://github.com/KOBA789/t.isucon.pw/releases/latest/download/fullchain.pem -O /etc/nginx/ssl/fullchain.pem | |
wget https://github.com/KOBA789/t.isucon.pw/releases/latest/download/key.pem -O /etc/nginx/ssl/privkey.pem | |
sed -i 's/isucon9.catatsuy.org/isucon9.t.isucon.pw/g' /etc/nginx/sites-enabled/isucari.conf | |
sudo systemctl reload nginx | |
Server2: | |
Type: 'AWS::EC2::Instance' | |
Properties: | |
ImageId: 'ami-03b1b78bb1da5122f' | |
InstanceType: c6i.large | |
SubnetId: !Ref MySubnet | |
SecurityGroupIds: | |
- !Ref MySecurityGroup | |
KeyName: !Ref KeyPairName | |
PrivateIpAddress: '192.168.1.20' | |
UserData: | |
Fn::Base64: !Sub | | |
#!/bin/bash | |
GITHUB_USER=${GitHubUsername} | |
mkdir -p /home/isucon/.ssh | |
curl -s https://github.com/$GITHUB_USER.keys >> /home/isucon/.ssh/authorized_keys | |
chown -R isucon:isucon /home/isucon/.ssh | |
chmod 600 /home/isucon/.ssh/authorized_keys | |
cd /home/isucon/isucari | |
find . ! -path "./webapp*" -exec rm -rf {} + | |
wget https://github.com/KOBA789/t.isucon.pw/releases/latest/download/fullchain.pem -O /etc/nginx/ssl/fullchain.pem | |
wget https://github.com/KOBA789/t.isucon.pw/releases/latest/download/key.pem -O /etc/nginx/ssl/privkey.pem | |
sed -i 's/isucon9.catatsuy.org/isucon9.t.isucon.pw/g' /etc/nginx/sites-enabled/isucari.conf | |
sudo systemctl reload nginx | |
Server3: | |
Type: 'AWS::EC2::Instance' | |
Properties: | |
ImageId: 'ami-03b1b78bb1da5122f' | |
InstanceType: c6i.large | |
SubnetId: !Ref MySubnet | |
SecurityGroupIds: | |
- !Ref MySecurityGroup | |
KeyName: !Ref KeyPairName | |
PrivateIpAddress: '192.168.1.30' | |
UserData: | |
Fn::Base64: !Sub | | |
#!/bin/bash | |
GITHUB_USER=${GitHubUsername} | |
mkdir -p /home/isucon/.ssh | |
curl -s https://github.com/$GITHUB_USER.keys >> /home/isucon/.ssh/authorized_keys | |
chown -R isucon:isucon /home/isucon/.ssh | |
chmod 600 /home/isucon/.ssh/authorized_keys | |
cd /home/isucon/isucari | |
find . ! -path "./webapp*" -exec rm -rf {} + | |
wget https://github.com/KOBA789/t.isucon.pw/releases/latest/download/fullchain.pem -O /etc/nginx/ssl/fullchain.pem | |
wget https://github.com/KOBA789/t.isucon.pw/releases/latest/download/key.pem -O /etc/nginx/ssl/privkey.pem | |
sed -i 's/isucon9.catatsuy.org/isucon9.t.isucon.pw/g' /etc/nginx/sites-enabled/isucari.conf | |
sudo systemctl reload nginx | |
Benchmarker: | |
Type: 'AWS::EC2::Instance' | |
Properties: | |
ImageId: 'ami-03b1b78bb1da5122f' | |
InstanceType: c6i.2xlarge | |
SubnetId: !Ref MySubnet | |
IamInstanceProfile: !Ref SSMInstanceProfileForBenchmarker | |
SecurityGroupIds: | |
- !Ref MySecurityGroup | |
KeyName: !Ref KeyPairName | |
PrivateIpAddress: '192.168.1.100' | |
UserData: | |
Fn::Base64: !Sub | | |
#!/bin/bash | |
GITHUB_USER=${GitHubUsername} | |
mkdir -p /home/isucon/.ssh | |
curl -s https://github.com/$GITHUB_USER.keys >> /home/isucon/.ssh/authorized_keys | |
chown -R isucon:isucon /home/isucon/.ssh | |
chmod 600 /home/isucon/.ssh/authorized_keys | |
echo "fs.file-max=1048576" >> /etc/sysctl.conf | |
echo "net.core.somaxconn=65535" >> /etc/sysctl.conf | |
echo "net.core.rmem_max=16777216" >> /etc/sysctl.conf | |
echo "net.core.wmem_max=16777216" >> /etc/sysctl.conf | |
echo "net.ipv4.tcp_fin_timeout=10" >> /etc/sysctl.conf | |
echo "net.ipv4.tcp_tw_reuse=1" >> /etc/sysctl.conf | |
echo "net.ipv4.tcp_rmem=4096 87380 16777216" >> /etc/sysctl.conf | |
echo "net.ipv4.tcp_wmem=4096 65536 16777216" >> /etc/sysctl.conf | |
# 反映 | |
sysctl -p | |
SSMRoleForBenchmarker: | |
Type: 'AWS::IAM::Role' | |
Properties: | |
AssumeRolePolicyDocument: | |
Version: '2012-10-17' | |
Statement: | |
- Effect: Allow | |
Principal: | |
Service: ec2.amazonaws.com | |
Action: sts:AssumeRole | |
Path: "/" | |
ManagedPolicyArns: | |
- arn:aws:iam::aws:policy/service-role/AmazonEC2RoleforSSM | |
SSMInstanceProfileForBenchmarker: | |
Type: 'AWS::IAM::InstanceProfile' | |
Properties: | |
Roles: | |
- !Ref SSMRoleForBenchmarker | |
Server1IP: | |
Type: 'AWS::EC2::EIP' | |
Properties: | |
Domain: vpc | |
Server1IPAssociation: | |
Type: 'AWS::EC2::EIPAssociation' | |
Properties: | |
InstanceId: !Ref Server1 | |
AllocationId: !GetAtt Server1IP.AllocationId | |
Server2IP: | |
Type: 'AWS::EC2::EIP' | |
Properties: | |
Domain: vpc | |
Server2IPAssociation: | |
Type: 'AWS::EC2::EIPAssociation' | |
Properties: | |
InstanceId: !Ref Server2 | |
AllocationId: !GetAtt Server2IP.AllocationId | |
Server3IP: | |
Type: 'AWS::EC2::EIP' | |
Properties: | |
Domain: vpc | |
Server3IPAssociation: | |
Type: 'AWS::EC2::EIPAssociation' | |
Properties: | |
InstanceId: !Ref Server3 | |
AllocationId: !GetAtt Server3IP.AllocationId | |
BenchmarkerIP: | |
Type: 'AWS::EC2::EIP' | |
Properties: | |
Domain: vpc | |
BenchmarkerIPAssociation: | |
Type: 'AWS::EC2::EIPAssociation' | |
Properties: | |
InstanceId: !Ref Benchmarker | |
AllocationId: !GetAtt BenchmarkerIP.AllocationId | |
PortalLambdaExecutionRole: | |
Type: 'AWS::IAM::Role' | |
Properties: | |
AssumeRolePolicyDocument: | |
Version: '2012-10-17' | |
Statement: | |
- Effect: Allow | |
Principal: | |
Service: lambda.amazonaws.com | |
Action: sts:AssumeRole | |
Path: "/" | |
ManagedPolicyArns: | |
- arn:aws:iam::aws:policy/AmazonEC2ReadOnlyAccess | |
- arn:aws:iam::aws:policy/service-role/AmazonSSMAutomationRole | |
- arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole | |
PortalLambdaFunction: | |
Type: 'AWS::Lambda::Function' | |
Properties: | |
FunctionName: 'ISUCON9Portal' | |
Handler: index.handler | |
Role: !GetAtt PortalLambdaExecutionRole.Arn | |
Runtime: nodejs20.x | |
Timeout: 180 | |
Environment: | |
Variables: | |
BENCHMARKER_INSTANCE_ID: !Ref Benchmarker | |
Code: | |
ZipFile: | | |
const {ListCommandsCommand, SendCommandCommand, GetCommandInvocationCommand, SSMClient} = require('@aws-sdk/client-ssm'); | |
const querystring = require('querystring'); | |
const ssm = new SSMClient({region: 'ap-northeast-1'}); | |
const instanceId = process.env.BENCHMARKER_INSTANCE_ID; | |
const commandGenerator = (target) => { | |
const targets = { | |
'Server1': '192.168.1.10', | |
'Server2': '192.168.1.20', | |
'Server3': '192.168.1.30', | |
} | |
const targetIp = targets[target]; | |
return `./bin/benchmarker -allowed-ips 192.168.1.10,192.168.1.20,192.168.1.30 -target-url https://${targetIp} -target-host https://isucon9.t.isucon.pw -payment-url http://192.168.1.100:5555 -payment-port 5555 -shipment-url http://192.168.1.100:7000 -shipment-port 7000`; | |
} | |
exports.handler = awslambda.streamifyResponse(async (event, responseStream, context) => { | |
if (event.rawPath === '/favicon.ico') { | |
// faviconは無視 | |
responseStream = awslambda.HttpResponseStream.from(responseStream, {statusCode: 404}); | |
responseStream.end(); | |
return; | |
} | |
console.log(event) | |
responseStream = awslambda.HttpResponseStream.from(responseStream, { | |
statusCode: 200, | |
headers: { | |
'Content-Type': 'text/html; charset=utf-8', | |
'Content-Security-Policy': `script-src 'none'`, | |
} | |
}); | |
if (event.requestContext.http.method === 'GET') { | |
if (event && event.queryStringParameters && event.queryStringParameters.commandId) { | |
return await showResult(event, responseStream); | |
} | |
return showIndex(responseStream); | |
} else if (event.requestContext.http.method === 'POST') { | |
return await executeCommand(event, responseStream); | |
} | |
responseStream.end(); | |
}); | |
async function showIndex(responseStream) { | |
responseStream.write(`<html><body> | |
<h1>ISUCON9予選 ベンチマーカー起動ページ</h1> | |
<h2>ベンチマーカー実行</h2> | |
<form action="" method="post"> | |
<div> | |
<div><label for="Server1">Server1 (192.168.1.10):<input type="radio" id="Server1" name="target" value="Server1" checked /></label></div> | |
<div><label for="Server2">Server2 (192.168.1.20):<input type="radio" id="Server2" name="target" value="Server2" /></label></div> | |
<div><label for="Server3">Server3 (192.168.1.30):<input type="radio" id="Server3" name="target" value="Server3" /></label></div> | |
</div> | |
<label for="comment">コメント: <input type="text" id="comment" name="comment" /></label> | |
<input type="submit" value="実行"/> | |
</form>`) | |
const results = await ssm.send(new ListCommandsCommand({maxResults: 1000, InstanceId: instanceId})) | |
responseStream.write(`<h2>実行履歴</h2>`); | |
responseStream.write(`<ul>`); | |
results.Commands.forEach(command => { | |
responseStream.write(`<li><a href="?commandId=${command.CommandId}">${command.RequestedDateTime.toISOString()} / ${command.Comment} / ${command.Status}</a></li>`); | |
}); | |
while (results.NextToken) { | |
results = await ssm.send(new ListCommandsCommand({maxResults: 1000, InstanceId: instanceId, NextToken: results.NextToken})); | |
results.Commands.forEach(command => { | |
responseStream.write(`<li><a href="?commandId=${command.CommandId}">${command.RequestedDateTime.toISOString()} / ${command.Comment} / ${command.Status}</a></li>`); | |
}); | |
} | |
responseStream.write(`</ul>`); | |
responseStream.write(`</body></html>`); | |
responseStream.end(); | |
return; | |
} | |
async function showResult(event, responseStream) { | |
const commandId = event.queryStringParameters.commandId; | |
responseStream.write(`<html><body><h1>ISUCON9予選 ベンチマーカーログ: ${commandId}</h1>`); | |
responseStream.write(`<div><a href="/">戻る</a></div>`); | |
let results = await ssm.send(new GetCommandInvocationCommand({CommandId: commandId, InstanceId: instanceId})); | |
if (results.Status === 'InProgress') { | |
responseStream.write(`<p>実行中`); | |
while (results.Status === 'InProgress') { | |
results = await ssm.send(new GetCommandInvocationCommand({ | |
CommandId: commandId, | |
InstanceId: instanceId | |
})); | |
responseStream.write(`.`); | |
await new Promise(resolve => setTimeout(resolve, 1000)); | |
responseStream.write(`.`); | |
await new Promise(resolve => setTimeout(resolve, 1000)); | |
responseStream.write(`.`); | |
await new Promise(resolve => setTimeout(resolve, 1000)); | |
responseStream.write(`.`); | |
await new Promise(resolve => setTimeout(resolve, 1000)); | |
responseStream.write(`.`); | |
await new Promise(resolve => setTimeout(resolve, 1000)); | |
} | |
responseStream.write(`完了</p>`); | |
} | |
if (results.Comment) { | |
responseStream.write(`<h2>コメント</h2>`); | |
responseStream.write(`<p>${results.Comment}</p>`); | |
} | |
responseStream.write(`<h2>結果</h2>`); | |
try { | |
const json = JSON.parse(results.StandardOutputContent); | |
responseStream.write(`<pre>${JSON.stringify(json, null, 2)}</pre>`); | |
} catch { | |
responseStream.write(`<pre>${results.StandardOutputContent}</pre>`); | |
} | |
responseStream.write(`<h2>ログ</h2>`); | |
responseStream.write(`<pre>${results.StandardErrorContent}</pre>`); | |
responseStream.write(`</body></html>`); | |
responseStream.end(); | |
} | |
async function executeCommand(event, responseStream) { | |
// POSTリクエストの場合、EC2でコマンドを実行 | |
const workingDirectory = '/home/isucon/isucari' | |
responseStream.write(`<html><body><h1>ISUCON9予選 ベンチマーカー起動ページ</h1>`); | |
responseStream.write(`<div>ベンチマーカー開始中</div>`); | |
const body = decodeBase64ToFormUrlencoded(event.body); | |
const command = commandGenerator(body.target ?? "Server1"); | |
const comment = body.comment ?? ""; | |
const id = await executeCommandOnEC2(instanceId, command, workingDirectory, comment, responseStream); | |
responseStream.write(` | |
<div>ベンチマーカー開始完了</div> | |
<div>コマンドID: ${id}</div> | |
<div><a href="?commandId=${id}">結果を見る</a></div> | |
<div><a href="/">戻る</a></div> | |
</body></html>`); | |
responseStream.end(); | |
return | |
} | |
async function executeCommandOnEC2(instanceId, command, workingDirectory, comment, responseStream) { | |
const commandParams = { | |
InstanceIds: [instanceId], | |
DocumentName: 'AWS-RunShellScript', | |
Parameters: {commands: [command], workingDirectory: [workingDirectory]}, | |
Comment: comment, | |
}; | |
let runCommandId; | |
await ssm.send(new SendCommandCommand(commandParams)) | |
.then(data => { | |
// RunCommandのIDを取得します。 | |
runCommandId = data.Command.CommandId; | |
}) | |
.catch(data => responseStream.write(data)); | |
return runCommandId; | |
} | |
function decodeBase64ToFormUrlencoded(base64String) { | |
if (!base64String) { | |
return {}; | |
} | |
// Base64エンコードされた文字列をデコード | |
const decodedString = Buffer.from(base64String, 'base64').toString('utf-8'); | |
// デコードされた文字列をapplication/x-www-form-urlencodedとして解析 | |
const parsedObject = querystring.parse(decodedString); | |
return parsedObject; | |
} | |
PortalLambdaFunctionUrl: | |
Type: AWS::Lambda::Url | |
Properties: | |
AuthType: NONE | |
TargetFunctionArn: !Ref PortalLambdaFunction | |
InvokeMode: RESPONSE_STREAM | |
PermissionForURLInvoke: | |
Type: AWS::Lambda::Permission | |
Properties: | |
Action: lambda:InvokeFunctionUrl | |
FunctionUrlAuthType: NONE | |
FunctionName: !Ref PortalLambdaFunction | |
Principal: '*' | |
Outputs: | |
Server1IP: | |
Description: "Server1 IP" | |
Value: !Ref Server1IP | |
Server2IP: | |
Description: "Server2 IP" | |
Value: !Ref Server2IP | |
Server3IP: | |
Description: "Server3 IP" | |
Value: !Ref Server3IP | |
BenchmarkerIP: | |
Description: "Benchmarker IP" | |
Value: !Ref BenchmarkerIP | |
PortalURL: | |
Description: "Portal URL" | |
Value: !GetAtt PortalLambdaFunctionUrl.FunctionUrl | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
出力のところに各種IPアドレスが表示されます。

インスタンスには最初に指定したGitHub IDに登録されている公開鍵を利用して
ssh isucon@<IP>
でログインできます。