AWS にサーバーを1台立てて、自分のパソコンで書いた Python をそこで動かしました。
前の記事で S3 にファイルを置けるようにしたので、今回はその続きです。S3 に置いたプログラムを、立てたばかりのサーバーへ渡して動かします。
やってみて一番驚いたのは、立てようとしたサーバーが立てられなかったことでした。前もって「2コア・8 GiB の t3.large にしよう」と決めていて、こう打ちました。
aws ec2 run-instances --image-id ami-053ea429a1c73a5b7 --instance-type t3.large ...
返ってきたのはエラーです。
An error occurred (InvalidParameterCombination) when calling the RunInstances operation:
The specified instance type is not eligible for Free Tier.
「指定されたインスタンスタイプは無料プランの対象ではありません」という意味です。いまの AWS の無料プランでは、選べるサーバーの大きさが6種類だけに絞られています。入門記事をいくつも読んでいましたが、この話はどこにも出てきませんでした。
この記事では、その6種類の調べ方から、サーバーを立てて ssh で入り、S3 のプログラムを動かして、止めるまでを11ステップで書きます。途中でつまずいた4か所も、エラーの原文ごと残します。
この記事でできること
AWS のアカウントと aws コマンドがある状態から、サーバーを1台立てて、S3 に置いたプログラムをそこで動かすまでを作ります。
- 対象読者:EC2 をまだ自分で立てたことがない方
- かかるお金:約12円(サーバーを37分動かしました)。止めたあともディスク代が1日14円かかります
- 試した日:2026年9月14日
- 試した環境:iMac(2017・Intel)/macOS Ventura 13.7.8/awscli 2.36.44/東京リージョン(ap-northeast-1)
前の2本の続きです。アカウントの作り方は1本目、aws コマンドを入れるところは2本目に書きました。
「無料プラン」なのに12円かかるのはなぜか
いまの無料プランでは、アカウントを作ると最大200ドルぶんのクレジットが配られます。サーバーを動かすと、その金額からクレジットが減ります。クレジットカードから引き落とされるわけではありません(自分で有料プランに切り替えないかぎり請求は立ちません)。この記事の12円も、クレジットから引かれたぶんです。詳しくは1本目の「先にお金の話をします」に書きました。
この記事は2025年7月15日より後に作ったアカウントの話です
無料プランの中身は2025年7月に変わりました。それより前に作ったアカウントは「12か月無料枠」という別の条件で動いていて、選べるサーバーの種類も違います。私のアカウントは2026年9月12日に作ったものです。
何と何をつなぐのか
登場するのは4つです。ollama だけは、立てたサーバーの中に入れます。
| 名前 | 何をするものか |
|---|---|
| EC2 | AWS のサーバー。1台借りて、好きなソフトを入れて使う |
| IAM ロール | サーバーに持たせる身分証。これが無いと、サーバーの中から S3 を読めない |
| S3 バケット | ファイルの置き場。前の記事で作ったもの |
| ollama | 言語モデルを動かすソフト。サーバーの中に入れて、サーバーの中からだけ呼べるようにする |
全体は11ステップです。最初に並べておきます。
| # | 何をするか | どこで打つか |
|---|---|---|
| 1 | 鍵ペアを作る | iMac |
| 2 | 22番ポートを自分の回線にだけ開ける | iMac |
| 3 | サーバーを1台起動する | iMac |
| 4 | ssh で入る | iMac |
| 5 | IAM ロールを作る | iMac |
| 6 | IAM ロールをサーバーに付ける | iMac |
| 7 | S3 からプログラムを取る | サーバー |
| 8 | ollama を入れる | サーバー |
| 9 | モデルを落とす | サーバー |
| 10 | プログラムを動かす | サーバー |
| 11 | サーバーを止める | iMac |
5と6は iMac で打ちます。サーバーの中で打ちたくなりますが、その時点のサーバーには身分証が無いので通りません。
1章 サーバーを立てる(1〜3)
まず、選べる大きさを調べる
冒頭のエラー文には続きがあって、次に打つコマンドまで書かれていました。
For a list of Free Tier instance types,
run 'describe-instance-types' with the filter 'free-tier-eligible=true'.
そのとおりに打ちます。
aws ec2 describe-instance-types --filters Name=free-tier-eligible,Values=true \
--query 'InstanceTypes[].[InstanceType,VCpuInfo.DefaultVCpus,MemoryInfo.SizeInMiB]' \
--output text
--query は、返ってきた JSON から欲しい値だけを抜き出す指定です。InstanceTypes[] が配列の全要素、[A,B,C] がその中から3つ取り出して並べるという意味になります。--output text を付けると、見出しの無いタブ区切りで返ります。
私の画面にはこう出ました。
m7i-flex.large 2 8192
c7i-flex.large 2 4096
t3.micro 2 1024
t4g.small 2 2048
t4g.micro 2 1024
t3.small 2 2048
6行しかありません。3列目はメガバイトではなく MiB(メビバイト)です。1024 で割ると、AWS の一覧表に出ている「8 GiB」のような数字になります。
| タイプ | CPU | メモリ(MiB) | メモリ(GiB) |
|---|---|---|---|
| m7i-flex.large | 2 | 8192 | 8 |
| c7i-flex.large | 2 | 4096 | 4 |
| t3.small | 2 | 2048 | 2 |
| t4g.small | 2 | 2048 | 2 |
| t3.micro | 2 | 1024 | 1 |
| t4g.micro | 2 | 1024 | 1 |
この6つ以外は、無料プランのアカウントでは起動できません。CPU はどれも2つです。差はメモリで、一番大きい m7i-flex.large が 8 GiB でした。
私は llama3.2:3b という言語モデルを動かすつもりでした。「3b」はパラメータが30億あるという意味で、ファイルサイズは2.0GB あります。動かすとメモリに読み込まれるので、8 GiB あるものを選びました。
この6種類は2026年9月14日に私のアカウントで調べた結果です。上のコマンドを自分で打って確かめてください。
リージョンの指定について
この記事のコマンドには --region が出てきません。2本目で aws configure を実行したときに ap-northeast-1(東京)を設定してあるので、何も書かなければ東京に作られます。
いま何が設定されているかはこれで見えます。
$ aws configure get region
ap-northeast-1
1. 鍵ペアを作る(iMac で)
サーバーに入るための鍵を作ります。
aws ec2 create-key-pair --key-name aws-study-key \
--query 'KeyMaterial' --output text > ~/.ssh/aws-study-key.pem
chmod 400 ~/.ssh/aws-study-key.pem
ここは一度きりです。--query 'KeyMaterial' で取り出しているのが秘密鍵の本体で、AWS 側はこれを保存していません。持っているのは公開鍵だけです。
つまり、aws-study-key.pem を消したり上書きしたりすると、そのサーバーには二度と入れません。サーバーを作り直すしかなくなります。
chmod 400 は「本人だけが読める」という印です。これを付けないと ssh が鍵を受け付けません。
$ ls -l ~/.ssh/aws-study-key.pem
-r--------@ 1 user staff 1675 9 14 05:14 /Users/user/.ssh/aws-study-key.pem
2. 22番ポートを自分の回線にだけ開ける(iMac で)
サーバーに ssh で入るには、22番ポートを開ける必要があります。ここを世界中に開けないでください。
AWS で「どのポートを誰に開けるか」を決めるのがセキュリティグループです。サーバーの前に立つ受付のような設定で、ここに書いていない通信は届きません。
作る前に2つ調べます。1つ目は自分がどの IP アドレスからインターネットに出ているかです。AWS がそれだけを返すページを用意しています。
$ curl -s https://checkip.amazonaws.com
203.0.113.45
2つ目は VPC の ID です。VPC は AWS の中に用意される自分専用のネットワークで、サーバーはこの中に置かれます。アカウントを作ると1つ最初から用意されているので、自分で作る必要はありません。その ID を調べます。
$ aws ec2 describe-vpcs --filters Name=isDefault,Values=true \
--query 'Vpcs[].[VpcId,CidrBlock]' --output text
vpc-0f76c0325c7d15214 172.31.0.0/16
2つそろったので作ります。
# 自分の IP と VPC ID を変数に入れる
MYIP=$(curl -s https://checkip.amazonaws.com)
VPCID=$(aws ec2 describe-vpcs --filters Name=isDefault,Values=true \
--query 'Vpcs[0].VpcId' --output text)
# セキュリティグループを作る
SG=$(aws ec2 create-security-group \
--group-name aws-study-sg \
--description "SSH from home" \
--vpc-id "$VPCID" \
--query 'GroupId' --output text)
echo "$SG"
# 22番を自分の IP だけに開ける
aws ec2 authorize-security-group-ingress \
--group-id "$SG" --protocol tcp --port 22 --cidr "$MYIP/32"
/32 は「この IP ちょうど1つ」という意味です。0.0.0.0/0 と書くと世界中から入れるようになります。
できあがりを確認します。
$ aws ec2 describe-security-groups --group-ids "$SG" \
--query 'SecurityGroups[].IpPermissions[].[IpProtocol,FromPort,IpRanges[0].CidrIp]' \
--output text
tcp 22 203.0.113.45/32
SG は変数なので、ターミナルを閉じると消えます。日をまたいで続きをやるときは、名前から引き直してください。
SG=$(aws ec2 describe-security-groups --group-names aws-study-sg \
--query 'SecurityGroups[0].GroupId' --output text)
自宅の回線は、ルーターを再起動すると IP アドレスが変わることがあります。変わると入れなくなるので、新しい IP を足します。
NEWIP=$(curl -s https://checkip.amazonaws.com)
aws ec2 authorize-security-group-ingress \
--group-id "$SG" --protocol tcp --port 22 --cidr "$NEWIP/32"
足すだけだと古い穴が開いたままになります。使わない IP は消しておきます。
aws ec2 revoke-security-group-ingress \
--group-id "$SG" --protocol tcp --port 22 --cidr "203.0.113.45/32"
3. サーバーを1台起動する(iMac で)
OS は Amazon Linux 2023 にしました。最新のイメージ ID は AWS が公開しているので、自分で番号を覚えておく必要はありません。変数に入れて、そのまま次のコマンドで使います。
AMI=$(aws ssm get-parameters \
--names /aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64 \
--query 'Parameters[0].Value' --output text)
echo "$AMI"
私のときは ami-053ea429a1c73a5b7 でした。この番号は更新されるので、記事の値を写さずに自分で引いてください。
起動します。インスタンス ID も変数に受けます。
IID=$(aws ec2 run-instances \
--image-id "$AMI" \
--instance-type m7i-flex.large \
--key-name aws-study-key \
--security-group-ids "$SG" \
--block-device-mappings '[{"DeviceName":"/dev/xvda","Ebs":{"VolumeSize":30,"VolumeType":"gp3","DeleteOnTermination":true}}]' \
--tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=aws-study}]' \
--query 'Instances[0].InstanceId' --output text)
echo "$IID"
echo "$IID" で i-0dbef9a972814797b のような文字列が出ます。これがサーバー1台の名前で、以降のコマンドで何度も使います。
長いので、1行ずつ何をしているか書きます。
| 行 | 意味 |
|---|---|
--image-id |
OS。上で調べた Amazon Linux 2023 |
--instance-type |
大きさ。6種類から選んだもの |
--key-name |
1で作った鍵の名前 |
--security-group-ids |
2で作った受付の設定 |
--block-device-mappings |
ディスク30GB。DeleteOnTermination はサーバーを消したらディスクも消すという指定 |
--tag-specifications |
名前を付ける。あとで見分けるため |
起動には少し時間がかかります。待つコマンドが用意されています。
aws ec2 wait instance-running --instance-ids "$IID"
これが返ってきたら、つなぎ先の IP アドレスを調べます。
IP=$(aws ec2 describe-instances --instance-ids "$IID" \
--query 'Reservations[0].Instances[0].PublicIpAddress' --output text)
echo "$IP"
IID と IP もターミナルを閉じると消えます。名前から引き直せます。
IID=$(aws ec2 describe-instances --filters Name=tag:Name,Values=aws-study \
Name=instance-state-name,Values=running,stopped \
--query 'Reservations[0].Instances[0].InstanceId' --output text)
2章 ssh で入る(4)
4. ログイン名は OS で決まっている
ssh -i ~/.ssh/aws-study-key.pem ec2-user@"$IP"
ここで詰まる方がいるかもしれません。ログイン名は自分で決めるものではなく、OS ごとに決まっています。Amazon Linux 2023 は ec2-user です。Ubuntu なら ubuntu になります。
初めてつなぐサーバーには「本当にこのサーバーで合っていますか」という確認が出て、yes と打つまで止まります。スクリプトから回すときはこれを付けておくと止まりません。
ssh -i ~/.ssh/aws-study-key.pem \
-o StrictHostKeyChecking=accept-new \
ec2-user@"$IP"
入れたら中を見てみます。
$ cat /etc/os-release | head -2
NAME="Amazon Linux"
VERSION="2023"
CPU の型番とコア数は lscpu で分かります。全部で30行以上出るので、欲しい2行だけ抜き出します。
$ lscpu | grep -E "Model name|^CPU\(s\)"
CPU(s): 2
Model name: Intel(R) Xeon(R) Platinum 8488C
grep -E は「どちらかに当てはまる行」を探す書き方で、| が「または」です。^CPU\(s\) の ^ は行の先頭、\( はかっこそのものを探すという意味になります(かっこは grep -E では別の意味を持つので、\ を付けて打ち消します)。
メモリとディスクも見ておきます。
$ free -h | head -2
total used free shared buff/cache available
Mem: 7.6Gi 439Mi 6.9Gi 0.0Ki 246Mi 7.0Gi
$ df -h / | tail -1
/dev/nvme0n1p1 30G 1.8G 29G 6% /
8 GiB のはずが 7.6Gi と出ます。表示のとおり、全部が使えるわけではありません。
起動のときには /dev/xvda と書いたのに、中で見ると /dev/nvme0n1p1 になっています。同じディスクで、AWS へ指定する名前とサーバーの中で見える名前が違うだけです。
Python も最初から入っていました。
$ python3 --version
Python 3.9.25
3章 S3 からプログラムを取る(5〜7)
ここが今回で一番手数の多いところです。サーバーの中から S3 を読むには、身分証が要ります。
つまずき:aws コマンドはあるのに、何もできない
Amazon Linux 2023 には aws コマンドが最初から入っていました。
$ aws --version
aws-cli/2.33.15 Python/3.9.25 Linux/6.18.44-99.149.amzn2023.x86_64
iMac では brew install awscli に45分かかったので(2本目の記事です)、拍子抜けしました。ところが打ってみると、何もできません。
$ aws sts get-caller-identity
Unable to locate credentials. You can configure credentials by running "aws login".
$ aws s3 cp s3://mitsuhashi-study-2026/benchmark.py .
fatal error: Unable to locate credentials
「あなたが誰なのか分かりません」と言われています。コマンドは入っていても、身分が空っぽでした。
エラー文は aws login を勧めてきますが、この記事では使いません。次に書く IAM ロールを付ければ、サーバー側で何も打たずに身分が入ります。
⛔ アクセスキーをサーバーに置かない
iMac では aws configure でアクセスキーを入れました。同じことをサーバーでもやりたくなりますが、やらないでください。
アクセスキーは、持っている人が誰でも自分になりすませる文字列です。サーバーの中にファイルとして置くと、そのサーバーに入られたときに一緒に持っていかれます。
代わりに使うのが IAM ロールです。キーを置かずに、AWS のほうから一時的な認証情報を受け取る形になります。サーバーの中の aws コマンドは、設定ファイルが無いときに自分でその認証情報を取りに行きます。だから aws configure を打つ必要がありません。受け取る認証情報は自動で切り替わるので、盗まれても長くは使えません。
5. IAM ロールを作る(iMac で)
ここから2ステップは iMac のターミナルで打ちます。サーバーの中で打っても、身分が無いので通りません。
まず JSON を2つ書きます。iMac の好きな場所に、この名前で保存してください。
trust.json ——「このロールを誰が使えるか」を書きます。
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": {"Service": "ec2.amazonaws.com"},
"Action": "sts:AssumeRole"
}]
}
-
Versionの2012-10-17は日付ではなく、書式のバージョン番号です。今日の日付を入れるものではありません -
Principalが「誰が」です。ec2.amazonaws.comと書いてあるので、EC2 のサーバーだけがこのロールを使えます -
sts:AssumeRoleは「このロールを借りる」という操作の名前です
s3read.json ——「何をしていいか」を書きます。
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Action": ["s3:GetObject", "s3:ListBucket"],
"Resource": [
"arn:aws:s3:::mitsuhashi-study-2026",
"arn:aws:s3:::mitsuhashi-study-2026/*"
]
}]
}
arn: で始まる長い文字列は ARN といって、AWS の中の物を1つに特定する住所です。コロンで区切られていて、S3 の場合はこう並びます。
arn:aws:s3:::mitsuhashi-study-2026
↑↑
││
│└─ アカウント ID の欄(S3 では空)
└── リージョンの欄(S3 では空)
2つの欄が空なので、s3 のうしろにコロンが3つ続きます。
2行書いているのは、指すものが違うからです。/* の無いほうがバケットそのもの(s3:ListBucket の相手)、/* の付いたほうがバケットの中のファイル1つ1つ(s3:GetObject の相手)になります。
AWS には AmazonS3ReadOnlyAccess という出来合いの権限があって、これを付ければ1行で済みます。それは使いませんでした。あれを付けると、アカウントの中の S3 バケットを全部読めてしまいます。ここでは読みたいバケットが1つだけなので、その1つだけを書きました。
作ります。
# ロールを作る
aws iam create-role --role-name aws-study-ec2-s3read \
--assume-role-policy-document file://trust.json
# 権限を付ける
aws iam put-role-policy --role-name aws-study-ec2-s3read \
--policy-name read-mitsuhashi-study-2026 \
--policy-document file://s3read.json
file:// は「このファイルの中身を読んで送る」という指定です。カレントディレクトリに2つの JSON を置いた状態で打ってください。
1本目で作った IAM ユーザーに AdministratorAccess を付けていれば、この2つのコマンドは通ります。権限を絞ったユーザーで打つと AccessDenied になるので、そのときは iam:CreateRole と iam:PutRolePolicy を足してください。
6. ロールをサーバーに付ける(iMac で)
ここでロールとは別のものをもう1つ作ります。インスタンスプロファイルです。
EC2 に付けられるのはロールそのものではなく、このインスタンスプロファイルのほうです。AWS の決まりなので、そういうものだと思って作ります。名前はロールと同じにしておくと混乱しません。
aws iam create-instance-profile --instance-profile-name aws-study-ec2-s3read
aws iam add-role-to-instance-profile \
--instance-profile-name aws-study-ec2-s3read \
--role-name aws-study-ec2-s3read
サーバーに付けます。
aws ec2 associate-iam-instance-profile \
--instance-id "$IID" \
--iam-instance-profile Name=aws-study-ec2-s3read
つまずき:作った直後は、まだ見えない
そのまま打ったら、こう返ってきました。
An error occurred (InvalidParameterValue) when calling the AssociateIamInstanceProfile operation:
Value (aws-study-ec2-s3read) for parameter iamInstanceProfile.name is invalid.
Invalid IAM Instance Profile name
「名前が正しくありません」と言われています。名前は合っていました。作ったばかりで、EC2 のほうからまだ見えていなかっただけです。
エラー文からはそう読み取れません。私は名前を打ち間違えたかと思って、get-instance-profile で存在を確かめ直しました。ちゃんとありました。
数十秒おいて打ち直したら、1回で通りました。私の場合、最初のエラーから打ち直しまでの間が1分ほどです。待っても通らないときのために、5秒ずつ最大10回試す形にしておくと止まりません。
for i in $(seq 10); do
aws ec2 associate-iam-instance-profile \
--instance-id "$IID" \
--iam-instance-profile Name=aws-study-ec2-s3read 2>/dev/null && break
echo "まだ反映されていません($i 回目)"
sleep 5
done
付いたか確認します。
$ aws ec2 describe-iam-instance-profile-associations \
--filters Name=instance-id,Values="$IID" \
--query 'IamInstanceProfileAssociations[].[State,IamInstanceProfile.Arn]' --output text
associated arn:aws:iam::************:instance-profile/aws-study-ec2-s3read
associated になれば完了です。同じエラーに当たったら、名前を疑う前に数十秒おいてもう一度打ってみてください。
7. 取れた(サーバーで)
ここからまたサーバーの中に戻ります。もう一度打ちます。
$ aws sts get-caller-identity --query "Arn" --output text
arn:aws:sts::************:assumed-role/aws-study-ec2-s3read/i-0dbef9a972814797b
さっきは「誰か分かりません」だったところに、名前が出ました。assumed-role は「ロールを借りている状態」という意味です。
$ aws s3 cp s3://mitsuhashi-study-2026/benchmark.py .
download: s3://mitsuhashi-study-2026/benchmark.py to ./benchmark.py
取れました。アクセスキーは1文字も置いていません。
私が取ってきた benchmark.py は、自分で書いて GitHub に置いてあるものです。同じものを試したい方はここから取れます。
wakana19811111-source/local-llm-on-intel-imac
権限が本当に絞れているかを確かめる
「1つのバケットだけ」と書いたポリシーが効いているかは、わざと範囲の外を打つと分かります。
$ aws s3 ls
An error occurred (AccessDenied) when calling the ListBuckets operation:
User: arn:aws:sts::************:assumed-role/aws-study-ec2-s3read/i-0dbef9a972814797b
is not authorized to perform: s3:ListAllMyBuckets
aws s3 ls(バケットの一覧)は断られました。指定したバケットからファイルを取ることはできて、一覧を見ることはできません。狙いどおりです。
AmazonS3ReadOnlyAccess を付けていたら、どちらも通ってしまって、絞れているかどうかが分かりませんでした。
4章 ollama を入れる(8〜9)
ここからはサーバーの中での作業です。
8. ollama を入れる
言語モデルを動かすために ollama を入れます。公式が用意しているスクリプトを実行します。
curl -fsSL https://ollama.com/install.sh | sh
curl ... | sh は、落としてきたスクリプトをそのまま実行する書き方です。中身を見ずに動かすことになるので、信用できる配布元でだけ使ってください。気になる方は、いったんファイルに落として読んでから実行できます。
curl -fsSL https://ollama.com/install.sh -o install.sh
less install.sh
sh install.sh
私が試したときは sudo のパスワードを聞かれませんでした。
10秒で終わりました。
>>> Installing ollama to /usr/local
>>> Downloading ollama-linux-amd64.tar.zst
>>> Creating ollama systemd service...
>>> Enabling and starting ollama service...
>>> The Ollama API is now available at 127.0.0.1:11434.
WARNING: No NVIDIA/AMD GPU detected. Ollama will run in CPU-only mode.
10秒で済んだのは、Linux 向けには組み立て済みのファイル(ollama-linux-amd64.tar.zst)が配布されていて、落として展開するだけで終わるからです。2本目に書いた45分は、Intel の Mac 向けにそういうファイルの配布が2026年8月に止まっていて、自分のパソコンの中で1から組み立てることになったためでした。別のソフトの話ですが、時間の差はソフトの重さではなく、組み立て済みのファイルがあるかどうかで決まります。
最後の行の警告は「GPU が見つからないので CPU だけで動かします」という意味です。今回選べた6種類のサーバーに GPU は付いていないので、これで正常です。
動いているかを2つの方法で確かめる
>>> Enabling and starting ollama service... の行にあるとおり、ollama は systemd に登録されました。systemd は Linux で常駐するソフトを管理する部分で、ここに登録しておくとサーバーを再起動したときに自動で立ち上がります。
$ systemctl is-active ollama
active
$ systemctl is-enabled ollama
enabled
active が「いま動いている」、enabled が「再起動しても自動で立ち上がる」です。
もう1つ、どのポートで待っているかも見ておきます。
$ ss -tlnp | grep 11434
LISTEN 0 4096 127.0.0.1:11434 0.0.0.0:*
ss は開いているポートを一覧するコマンドで、-t が TCP、-l が待ち受け中のものだけ、-n が名前に変換せず数字のまま表示、-p がそのポートを使っているプログラム名です。⚠ -p を付けてもプログラム名の欄が空でした。ollama は専用の利用者を作って動くので、そのためだと思いますが確かめてはいません。
大事なのは 127.0.0.1:11434 の部分です。127.0.0.1 は「自分自身」を指すアドレスなので、このサーバーの中からしかつながりません。外に開いていないので、セキュリティグループに穴を足す必要はありません。
入れた直後に ollama --version を打つと could not connect to a running Ollama instance という警告が出ることがあります。ss で 11434 が出ていれば動いているので、待てば消えます。
9. モデルを落とす
$ T0=$(date +%s); ollama pull llama3.2:3b; T1=$(date +%s); echo $((T1-T0)) 秒
...
10 秒
10秒でした。2.0GB あります。
$ ollama list
NAME ID SIZE MODIFIED
llama3.2:3b a80c4f17acd5 2.0 GB Less than a second ago
$ df -h / | tail -1
/dev/nvme0n1p1 30G 5.8G 25G 20% /
ディスクは最初 1.8GB でしたから、ollama 本体とモデルで 4GB ほど増えました。30GB にしておいて正解でした。
試しに1つ聞いてみます。かかった時間は date を前後で挟んで測りました。
T0=$(date +%s%N)
echo "Reply with exactly one word: hello" | ollama run llama3.2:3b
T1=$(date +%s%N)
echo $(( (T1-T0)/1000000 )) ミリ秒
hello
4905 ミリ秒
⚠ 4行をまとめて1回で流してください。1行ずつ打つと、自分がキーを叩いている時間まで数えてしまいます。
同じことをもう一度やると 516ミリ秒でした。差の4.4秒は、モデルをメモリに読み込む時間です。
読み込まれている間は ollama ps で見えます。
$ ollama ps
NAME ID SIZE PROCESSOR CONTEXT UNTIL
llama3.2:3b a80c4f17acd5 2.9 GB 30%/70% CPU/GPU 4096 4 minutes from now
UNTIL が4分後になっています。4分使わないとメモリから降りて、次はまた4.9秒かかります。
PROCESSOR の欄に 30%/70% CPU/GPU と出ますが、このサーバーに GPU は付いていません(入れたときに No NVIDIA/AMD GPU detected と出ています)。なぜこう表示されるかは調べていません。
また SIZE が 2.9 GB で、ollama list の 2.0 GB と違います。こちらも理由は調べていません。
5章 プログラムを動かす(10)
つまずき:1本だけでは動かない
S3 から取ってきた benchmark.py を打ちました。
$ python3 benchmark.py llama3.2:3b
Traceback (most recent call last):
File "/home/ec2-user/benchmark.py", line 16, in <module>
from ollama_client import DEFAULT_MODEL, classify
ModuleNotFoundError: No module named 'ollama_client'
benchmark.py は、実際に推論を呼ぶ部分を別のファイル(ollama_client.py)に分けていました。S3 には benchmark.py しか置いていませんでした。
前の記事で S3 にファイルを置いたとき、1本で足りると思い込んでいました。動かして初めて分かります。
ollama_client.py も同じ道で運びます。iMac から S3 へ上げて、サーバーから取ります。
# iMac で
aws s3 cp ollama_client.py s3://mitsuhashi-study-2026/
# サーバーで
aws s3 cp s3://mitsuhashi-study-2026/ollama_client.py .
運んだファイルが途中で壊れていないかは、両方で同じ値を出して比べられます。
# iMac(macOS)
$ shasum -a 256 ollama_client.py
963e8e721e0734ae923e2fd3fc0c45f211aaf3df5acb5a10ae1afa180066629c
# サーバー(Linux)
$ sha256sum ollama_client.py
963e8e721e0734ae923e2fd3fc0c45f211aaf3df5acb5a10ae1afa180066629c
コマンド名が macOS と Linux で違います。macOS は shasum -a 256、Linux は sha256sum です。
もう1つ、requests というライブラリが要ります。ollama_client.py が requests.post() で http://localhost:11434/api/generate に問い合わせる作りなので、これが無いと動きません。
$ python3 -c "import requests; print(requests.__version__)"
2.25.1
Amazon Linux 2023 には最初から入っていました。私の iMac の python3.9 には入っていないので、あちらで動かすなら別に入れる必要があります。
10. 動かす
$ python3 benchmark.py llama3.2:3b
model: llama3.2:3b
warm-up (excluded): 11.2s
== labelled words (scored) ==
ok たいよう: nature (expected nature) / a sunny day / 4.6s
ok うさぎ: animal (expected animal) / a cute rabbit / 4.6s
ok おうち: object (expected object) / a house / 4.3s
ok クレヨン: object (expected object) / a colored pencil / 4.5s
ok うみ: nature (expected nature) / a body of water / 4.7s
NG ちょうちょ: object (expected animal) / a red lantern / 4.5s
accuracy: 5/6
== unlabelled words (manual review) ==
- うちゅうじん: a Japanese instrument / object / 4.6s
- かいじゅう: a type of Japanese sword / object / 5.1s
- おばけ: a ghost / object / 4.3s
- にんじゃ: a tiger / animal / 4.3s
== latency ==
mean 4.6s / min 4.3s / max 5.1s (target 10s)
動きました。iMac で書いた Python を、1文字も直さずに Amazon Linux の上で動かせました。
出力の読み方
このプログラムは、日本語の単語を 11件投げます。内訳はこうです。
| 区分 | 件数 | 中身 | 何を見るか |
|---|---|---|---|
| warm-up | 1 | ねこ |
⛔ 数えない。出力の2行目 warm-up (excluded): 11.2s がこれ |
| labelled | 6 | たいよう・うさぎ・おうち・クレヨン・うみ・ちょうちょ | 正解を決めてあるので、当たったかどうかを数える |
| unlabelled | 4 | うちゅうじん・かいじゅう・おばけ・にんじゃ | 正解を決めていない。人が読んで判断する |
ねこ の行が出力に見当たらないのは、warm-up (excluded) の行がそれだからです。単語名は印字されません。最初の1件を捨てているのは、さっきの「1回目だけ遅い」を平均に混ぜないためです。
最後の mean 4.6s / min 4.3s / max 5.1s は、warm-up を除いた10件(labelled 6件+unlabelled 4件)の集計です。max 5.1s は unlabelled の かいじゅう の行にあります。labelled の6行だけを見ると最大は 4.7秒なので、上の6行だけ眺めていると 5.1 がどこから来たのか分かりません。
全体は56秒かかりました。画面に出ている秒数は小数第1位までの丸めた値なので、足し上げても全体とはぴたりと合いません。
当たったのは6件中5件
外したのは ちょうちょ で、英語のほうが a red lantern(赤い提灯)になっています。「ちょうちょ」と「ちょうちん」を取り違えたようです。
正解を決めていない4件も、当たっているのは おばけ → a ghost だけでした。かいじゅう が「日本刀の一種」、にんじゃ が「虎」になっています。私が試した1回では、このくらい外しました。
終了コードで合否が決まる
最後に、10件のうち一番長かった応答が10秒以内かどうかで終了コードが決まります。
$ echo $?
0
0 は合格です。一番長かったのが 5.1秒だったので通りました。10秒を超えていたら 1 が返り、シェルスクリプトから呼んだときにそこで止められます。
6章 止める(11)
11. 止める(iMac で)
動かしっぱなしにしないでください。ssh を切っても止まりません。サーバーが running である限り、秒単位でお金がかかり続けます。
aws ec2 stop-instances --instance-ids "$IID"
aws ec2 wait instance-stopped --instance-ids "$IID"
いま何が動いているかは、これで一覧できます。作業の終わりに必ず打ってください。
$ aws ec2 describe-instances \
--query "Reservations[].Instances[].[InstanceId,State.Name,InstanceType]" \
--output table
止め方は2種類ある
aws ec2 stop-instances # 止める(中身は残る)
aws ec2 terminate-instances # 消す(中身も消える)
stop ではディスク代が止まりません。ここは分かりにくいので、単価と一緒に並べます。
| 何に | 単価 | いつ止まるか |
|---|---|---|
| サーバー本体(m7i-flex.large) | 0.12369 ドル/時(約18.5円) |
stop でも terminate でも止まる |
| ディスク(EBS gp3 30GB) | 0.096 ドル/GB・月 → 30GB で 2.88 ドル/月(約430円・1日14円) | terminate のときだけ止まる |
使い分けはこう考えました。
-
続きを明日やる →
stop。中身がそのまま残るので、startして ssh すればすぐ再開できる。1日14円は払う -
しばらく触らない →
terminate。入れた ollama もモデルも消えるので、次は1章からやり直し。そのかわり0円になる
私は続きをやるつもりだったので stop にしました。
単価はコマンドで引ける
上の単価は、AWS が公開している価格表から取りました。ブラウザで料金ページを見に行かなくても引けます。
aws pricing get-products --service-code AmazonEC2 --region us-east-1 \
--filters "Type=TERM_MATCH,Field=instanceType,Value=m7i-flex.large" \
"Type=TERM_MATCH,Field=location,Value=Asia Pacific (Tokyo)" \
"Type=TERM_MATCH,Field=operatingSystem,Value=Linux" \
"Type=TERM_MATCH,Field=tenancy,Value=Shared" \
"Type=TERM_MATCH,Field=preInstalledSw,Value=NA" \
"Type=TERM_MATCH,Field=capacitystatus,Value=Used" \
--max-results 1
返ってくるのは、JSON の文字列がそのまま1つ入った形です。--query では中まで掘れないので、Python に渡して読みます。
... | python3 -c "
import json,sys
d = json.load(sys.stdin)
for p in d['PriceList']:
o = json.loads(p)
for term in o['terms']['OnDemand'].values():
for dim in term['priceDimensions'].values():
print(dim['description'], '→', dim['pricePerUnit']['USD'])
"
$0.12369 per On Demand Linux m7i-flex.large Instance Hour → 0.1236900000
--region us-east-1 は打ち間違いではありません。価格を答える窓口はバージニア北部にしかないので、東京の値段を聞くときも接続先は us-east-1 です。知りたいリージョンは location のほうで指定します。
pricing も次の ce(Cost Explorer)も、1本目で作った AdministratorAccess の IAM ユーザーなら打てます。権限を絞っている方は pricing:GetProducts と ce:GetCostAndUsage を足してください。
今日いくら使ったかは、今日は分からない
作業を終えたあと、いくらかかったか見ようとしました。
$ aws ce get-cost-and-usage --region us-east-1 \
--time-period Start=2026-09-01,End=2026-09-15 \
--granularity MONTHLY --metrics UnblendedCost \
--query 'ResultsByTime[].Total.UnblendedCost'
[
{
"Amount": "0",
"Unit": "USD"
}
]
0 と出ますが、0円という意味ではありません。この数字は反映まで24時間ほどかかります。当日ぶんを知りたいときは、上の単価に動かした時間を掛けて自分で出すほうが早いです。
私は2回に分けて合計37分動かしたので、0.12369 × 37 ÷ 60 = 約0.077 ドル、日本円で約12円でした。
サーバーを止めたあとに、また入るとき
stop したサーバーは start でまた使えます。中身はそのまま残っています。ただし IP アドレスが変わります。
aws ec2 start-instances --instance-ids "$IID"
aws ec2 wait instance-running --instance-ids "$IID"
IP=$(aws ec2 describe-instances --instance-ids "$IID" \
--query 'Reservations[0].Instances[0].PublicIpAddress' --output text)
echo "$IP"
私の場合、止める前は 13.x.x.x でしたが、起動し直したら 52.x.x.x に変わっていました。頭の数字から違うので、前の IP を控えていても使えません。起動するたびに調べ直してください。
固定したいなら Elastic IP を使えますが、月550円ほどかかります。使うたびに調べるほうが安く済みます。
つまずいた4か所
この記事で出てきたつまずきをまとめます。エラー文で検索して来た方は、ここだけ読んでも足ります。
| # | 出たもの | 何が起きていたか |
|---|---|---|
| 1 |
InvalidParameterCombination / not eligible for Free Tier
|
無料プランでは6種類しか起動できない。describe-instance-types --filters Name=free-tier-eligible,Values=true で調べる |
| 2 |
InvalidParameterValue / Invalid IAM Instance Profile name
|
名前は合っている。作った直後で EC2 から見えていないだけ。数十秒おいて打ち直す |
| 3 |
Unable to locate credentials(サーバーの中で) |
aws コマンドは入っているが身分が無い。IAM ロールを付ける。⛔ アクセスキーを置かない |
| 4 | ModuleNotFoundError: No module named 'ollama_client' |
import している相方のファイルも S3 に上げる必要がある |
まとめ
11ステップと、かかった時間です。
| # | 何をしたか | どこで | かかった時間 |
|---|---|---|---|
| 1 | 鍵ペアを作る | iMac | 数秒 |
| 2 | 22番を自分の IP だけに開ける | iMac | 数秒 |
| 3 | サーバーを起動する | iMac | 30秒ほど |
| 4 | ssh で入る | iMac | 1回目で通った |
| 5 | IAM ロールを作る | iMac | 1分 |
| 6 | ロールをサーバーに付ける | iMac | 1回目は反映待ちで失敗 |
| 7 | S3 からプログラムを取る | サーバー | 数秒 |
| 8 | ollama を入れる | サーバー | 10秒 |
| 9 | モデルを落とす(2.0GB) | サーバー | 10秒 |
| 10 | プログラムを動かす | サーバー | 56秒 |
| 11 | 止める | iMac | 30秒ほど |
一番時間を取られたのは、手を動かす部分ではなく調べ物でした。選べるサーバーの大きさが6種類に絞られていることも、インスタンスプロファイルがロールとは別物であることも、打ってみるまで知りませんでした。
3本を通して、AWS のアカウントを作るところからサーバーで自分のプログラムを動かすところまで来ました。クレジットから引かれたのは、サーバーを動かした12円です(前の2本はほぼ0円でした)。⚠ このほかに、残したディスクのぶんが1日14円ずつ増えていきます。
次は、ここまで画面とコマンドで作ってきた構成を Terraform で書き直して、terraform destroy の1行で後片付けできるようにするつもりです。消し忘れが一番お金のかかる失敗なので、そこを1行にしておきたいと考えています。
