Skip to content

Instantly share code, notes, and snippets.

@tohutohu
Last active November 21, 2023 11:10
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save tohutohu/22c782cafd4088048cda51b1252a3c6f to your computer and use it in GitHub Desktop.
Save tohutohu/22c782cafd4088048cda51b1252a3c6f to your computer and use it in GitHub Desktop.
ISUCON9 予選素振り用 CloudFormation 簡易ポータル付き
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
@tohutohu
Copy link
Author

出力のところに各種IPアドレスが表示されます。
インスタンスには最初に指定したGitHub IDに登録されている公開鍵を利用して ssh isucon@<IP> でログインできます。
image

@tohutohu
Copy link
Author

簡易ポータル

  • サーバーを指定してベンチを開始できる
  • ベンチ時にコメントを追加できる
  • 実行履歴とそのログが見られる

スコアの遷移とかは見られません。
技術的には1つLambda Functionがあって、それがSSMのRun Commandを開始したり結果を表示したりしています。
image
image
image

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment