0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【図解】AWS EC2とは。SSHで入れない、S3が読めない — 権限は3方向で考える

0
Last updated at Posted at 2026-08-16

対象読者: AWSをこれから触る人。EC2という名前しか知らなくて構いません。 AWS勉強シリーズの5本目です。全体の地図は索引記事にあります。前半でEC2そのものを説明して、後半は権限に絞ります。サーバーを1台立てるとき、誰が入れて、そのサーバー自身は何を触れて、誰が起動ボタンを押せるのか。EC2で初心者が最初に踏むのは、だいたいこの2つです。

  • サーバーを作ったのに、SSHで入れない
  • EC2の上のプログラムから、S3のファイルが読めない

見た目はどちらも「繋がらない」ですが、原因は別物です。前者は入口の設定、後者はサーバー自身の権限。ポートを開けても後者は直りません。この記事を読み終わると、この2つを切り分けられて、セキュリティグループに何を書けばいいかも決められるようになります。

EC2の権限は借りた部屋に例えると分かる。警備員=セキュリティグループ、鍵=キーペア、社員証=IAMロール、ビル管理人=IAMポリシー

EC2とは何か

Amazon Elastic Compute Cloud(EC2)は、AWSの中にコンピューターを1台借りて、自分のプログラムを動かし続けるサービスです。借りた1台をインスタンスと呼びます。

自分のノートPCとの違いは、実物が手元に無いことだけです。OS(LinuxやWindows)が入っていて、SSHでログインでき、好きなソフトを入れて、プログラムを常時動かせます。違うのは、数分で作れて、要らなくなったら消せて、動かした時間だけお金を払う点です。

「Elastic(伸縮する)」の名前どおり、性能はあとから変えられます。最初は一番小さいサイズで作り、足りなければ止めて大きいサイズに変えて起動し直せます。物理サーバーを買うのと違って、選択を間違えても後で直せます。

何に使うか

  • Webサイト・Webアプリを動かす: 一番多い用途です。Apache/Nginxを入れてブラウザからアクセスさせる
  • バッチ処理・定期実行: 毎晩CSVを集計する、データを変換するといった、常駐や長時間の処理
  • 開発用・検証用のサーバー: 「自分のPCでは動くのに本番で動かない」を確かめる場所
  • 他のサービスの操作元: EC2の上のプログラムからS3にファイルを置く、SQSからメッセージを取る

裏を返すと、短時間で終わる処理ならLambda、コンテナ前提ならECSやFargateの方が向きます。EC2を選ぶ基準は「OSまで自分で触りたい」「プロセスを常駐させたい」の2つです。

EC2とS3・SQSの違い

このシリーズで触ってきたサービスと比べると、EC2だけ役割が違います。

何をするものか あなたが書いたコードは
S3 ファイルの置き場 動かない(置くだけ)
SQS / SNS メッセージの受け渡し 動かない(渡すだけ)
EC2 コンピューターそのもの ここで動く

S3もSQSも「あなたのコードが外から呼ぶ相手」でした。EC2はあなたのコードが住む場所で、そこから他のサービスを呼ぶ側になります。だから権限の話が急に増えます。「誰がこのサーバーに入れるか」「このサーバーは何を触っていいか」という、置き場サービスには無かった問いが出てくるからです。

今AWSの地図のどこにいるか

このシリーズで触ってきたものと、今回の位置です。

AWSを学ぶ順序の地図。IAM・S3・SQS/SNSが完了、EC2が今ここ、VPCやRDSはまだ知らなくていい

IAM(誰が何をしていいか)を1本目にやったのは、この回のためでもあります。EC2の権限は、そのIAMの知識を3方向に使い分ける話になります。

今はこれだけ分かればOK

EC2の記事を読むとVPC、サブネット、AMI、EBS、ENI、ELB…と用語が雪崩れてきます。権限の話をするなら、覚えるのは3つだけです。

用語 意味 ひとことで
セキュリティグループ(SG) インスタンスに付ける通信の許可リスト 外から誰が入れるか
キーペア SSHでログインするための鍵。公開鍵がサーバーに入る 入るときの鍵
IAMロール(インスタンスプロファイル) インスタンス自身に持たせる権限 サーバーが何を触れるか

後回しでいい語: VPC・サブネット(SGの外側のネットワーク設定)、EBS(ディスク)、AMI(OSの型)、ENI(仮想LANカード)。最初の1台を立てるだけなら既定のままで動きます。

権限は3方向ある

EC2の権限が分かりにくいのは、種類の違う3つが同時に出てくるからです。冒頭の絵がその3つで、オフィスの部屋に例えるとこうなります。

  • 部屋の前に立つ警備員が、誰を通すかのリストを持っている = セキュリティグループ
  • 部屋のを持っている人だけが中に入れる = キーペア
  • 部屋の中の人が首から下げる社員証があると、資料室に入れる = IAMロール
  • そもそも部屋を借りていいかを決めるのがビルの管理人 = IAMポリシー

この4つを、方向で整理し直します。

  1. 外 → サーバー: 誰がこのサーバーに接続できるか。担当はセキュリティグループキーペア
  2. サーバー → AWS: このサーバーの上のプログラムがS3やSQSを触れるか。担当はIAMロール
  3. 人 → AWS: そもそも誰がこのインスタンスを起動・停止・削除できるか。担当はIAMポリシー(前回のIAM記事の話)

初心者がハマるのはたいてい1と2の取り違えです。「EC2からS3が読めない」を1(SG)の問題だと思って全ポートを開ける、という事故が起きます。実際は2(ロール)の問題です。

使い方。3つを付けてサーバーを立てる

やることは4つです。部屋を借りて、警備員を立てて、鍵を作って、社員証を渡す。この順番で組み立てます。

部屋を借りてから使えるようになるまでの4コマ。①部屋を借りる(誰も入れない)→②警備員を立てる(80番と22番)→③鍵を作る(入れた)→④社員証を渡す(資料を取れた)

上の4コマがそのままコードの順番になります。Python(boto3)で書くと次のとおりです。motoでAWSアカウント無しに実行しています。

import json
import os

import boto3

ec2 = boto3.client("ec2", region_name="ap-northeast-1")
iam = boto3.client("iam")
# 既定VPCを名指しで取る(先頭を取ると別のVPCを掴むことがある)
vpc_id = ec2.describe_vpcs(Filters=[{"Name": "isDefault", "Values": ["true"]}]
                           )["Vpcs"][0]["VpcId"]
ami_id = ec2.describe_images(Owners=["amazon"])["Images"][0]["ImageId"]   # OSの型

# ① 外→サーバー: セキュリティグループ
sg = ec2.create_security_group(GroupName="web-sg", Description="web server",
                               VpcId=vpc_id)["GroupId"]
ec2.authorize_security_group_ingress(GroupId=sg, IpPermissions=[
    # HTTP(80番)は全世界に開ける
    {"IpProtocol": "tcp", "FromPort": 80, "ToPort": 80,
     "IpRanges": [{"CidrIp": "0.0.0.0/0"}]},
    # SSH(22番)は自分のIPだけ。/32 は「このIP1つだけ」の意味
    {"IpProtocol": "tcp", "FromPort": 22, "ToPort": 22,
     "IpRanges": [{"CidrIp": "203.0.113.10/32"}]},
])

# ② 入るときの鍵: キーペア。秘密鍵はこの1回しか受け取れない
kp = ec2.create_key_pair(KeyName="my-key")
with open("my-key.pem", "w") as f:
    f.write(kp["KeyMaterial"])
os.chmod("my-key.pem", 0o400)      # 権限が緩いとsshが鍵を拒否する

サーバー自身の権限は、IAMロールを作ってインスタンスプロファイルという入れ物に入れてから渡します。EC2にロールを直接は渡せず、この入れ物が要る、というのがEC2特有の一手間です。

# ③ サーバー→AWS: EC2が引き受けられるロールを作る
trust = {"Version": "2012-10-17", "Statement": [{
    "Effect": "Allow",
    "Principal": {"Service": "ec2.amazonaws.com"},   # 引き受けていいのはEC2
    "Action": "sts:AssumeRole"}]}
iam.create_role(RoleName="ec2-s3-reader",
                AssumeRolePolicyDocument=json.dumps(trust))

# 対象は絞る。ListBucketはバケット、GetObjectはその中身と、指定先が違う
pol = {"Version": "2012-10-17", "Statement": [
    {"Effect": "Allow", "Action": "s3:ListBucket",
     "Resource": "arn:aws:s3:::my-bucket"},
    {"Effect": "Allow", "Action": "s3:GetObject",
     "Resource": "arn:aws:s3:::my-bucket/*"}]}
arn = iam.create_policy(PolicyName="S3ReadOnly",
                        PolicyDocument=json.dumps(pol))["Policy"]["Arn"]
iam.attach_role_policy(RoleName="ec2-s3-reader", PolicyArn=arn)

# ロールをインスタンスプロファイルに入れる(EC2はこの形でしか受け取れない)
iam.create_instance_profile(InstanceProfileName="ec2-s3-reader")
iam.add_role_to_instance_profile(InstanceProfileName="ec2-s3-reader",
                                 RoleName="ec2-s3-reader")

# ④ 3つを渡して起動
r = ec2.run_instances(
    ImageId=ami_id, InstanceType="t3.micro", MinCount=1, MaxCount=1,
    KeyName="my-key",                                  # 鍵
    SecurityGroupIds=[sg],                             # 入口
    IamInstanceProfile={"Name": "ec2-s3-reader"},      # サーバーの権限
    MetadataOptions={"HttpTokens": "required"},        # IMDSv2を必須にする(後述)
)

実行結果です。

SG rules: [(80, '0.0.0.0/0'), (22, '203.0.113.10/32')]
keypair: my-key 秘密鍵の先頭: -----BEGIN RSA PRIVATE KEY-
instance: i-1801bd63d60fa4836 running t3.micro
  key: my-key | sg: ['web-sg'] | profile: ec2-s3-reader | IMDSv2: required

今あなたが作ったもの

インスタンスが running になりました。手元のPCと比べると、こういう状態です。

自分のPC 今作ったEC2
電源 自分で入れる run_instances で入った。止めるまで課金が続く
ログイン パスワード my-key.pem(鍵ファイル)。無くすとSSHでは入れない(SSM Session Manager等の別経路はある)
外から入れるか 基本入れない SGで許可した80番と22番だけ入れる
他のサービスを触る権限 自分のアカウント インスタンスプロファイル経由。アクセスキーは置いていない

アクセスキーをサーバーに置かないのが今回いちばん大事な点です。ロールを付けたインスタンスの中では、AWS SDKが自動で一時的な認証情報を取ってきます。コードに鍵を書く必要も、~/.aws/credentials を置く必要もありません。

その一時認証情報の取得口が**インスタンスメタデータ(IMDS)**です。ここで MetadataOptions={"HttpTokens": "required"} を指定しました。これはIMDSv2必須という設定です。v1が使える状態だと、アプリの脆弱性を踏んだときに外部から認証情報を抜かれる経路が残ります。新しいAMIやアカウント設定で既定がv2必須になっていることもありますが、起動時に明示しておくのが確実です。

1か所だけ変えてみる

SSHの許可範囲を 203.0.113.10/32(自分のIPだけ)から 0.0.0.0/0(全世界)に変えると、設定はこう見えます。

ec2.authorize_security_group_ingress(GroupId=sg, IpPermissions=[
    {"IpProtocol": "tcp", "FromPort": 22, "ToPort": 22,
     "IpRanges": [{"CidrIp": "0.0.0.0/0"}]}])
port 80 from 0.0.0.0/0
port 22 from 203.0.113.10/32
port 22 from 0.0.0.0/0   <- 危険

セキュリティグループは許可の足し算。22番を自分のIPに絞っていても0.0.0.0/0を足すと両方残り、広い方が勝つ

古い許可は消えず、両方が残ります。 セキュリティグループは「許可の足し算」なので、広い方が実質の設定になります。範囲を狭めたいときは追加ではなく revoke_security_group_ingress で古い行を消してください。22番を全世界に開けると総当たりログインが来ます。気になるならサーバーの /var/log/secureFailed password の行を数えてみてください。

作った直後のグループの中身も見ておきます。

作った直後のingress(内向き): []
作った直後のegress(外向き): [('-1', ['0.0.0.0/0'])]

内向きは空(誰も入れない)、外向きは全許可です。ここでいう「作ったばかり」は自分で create_security_group したSGの話で、VPCに最初からある default という名前のSGは別物です。あちらは「同じdefault SGを付けたリソース同士は通す」という内向きルールを最初から1本持っています。「作ったばかりのサーバーに繋がらない」は、たいていSGの内向きが空なだけです。逆に外向きは既定で開いているので、サーバーからのapt/yumやAPI呼び出しは最初から通ります(既定VPCのようにインターネットへの出口がある場合の話です)。

踏み台サーバー経由にしたいときは、IPではなくグループを指定できます。

ec2.authorize_security_group_ingress(GroupId=sg, IpPermissions=[
    {"IpProtocol": "tcp", "FromPort": 22, "ToPort": 22,
     "UserIdGroupPairs": [{"GroupId": bastion_sg}]}])   # 踏み台SGからだけ許可

踏み台のIPが変わっても直す必要がなくなります。

セキュリティグループには結局、何を書くのか

ここが一番迷います。作った直後のセキュリティグループが何を持っているか、実際に見てみます。

sg = ec2.create_security_group(GroupName="web-sg", Description="web", VpcId=vpc)["GroupId"]
d = ec2.describe_security_groups(GroupIds=[sg])["SecurityGroups"][0]
print("入る方向:", d["IpPermissions"])
print("出る方向:", d["IpPermissionsEgress"])
入る方向: []
出る方向: [{'IpProtocol': '-1', 'IpRanges': [{'CidrIp': '0.0.0.0/0'}], ...}]

入る方向は空っぽ、出る方向は最初から全開放です。つまり作りたてのサーバーは「誰も入れないが、中からはどこへでも出られる」状態から始まります。

だから普段書くのは入る方向だけです。出る方向は既定のまま触りません(サーバーがOSの更新やAWSのAPIを呼ぶのに使うので、閉じると逆に困ります)。

戻りの通信の設定は要らない

初心者が最初に混乱するのがここです。「80番を開けたけど、サーバーが返事を送り返す分の許可も要るのでは?」と考えてしまいます。

要りません。セキュリティグループは行きを許可すれば、その戻りは自動で通ります(ステートフル、という性質です)。80番を1行書けば、閲覧者からのリクエストも、サーバーからの応答も両方通ります。

何番を開けるか

用途ごとに決まっています。自分がやりたいことの行だけ開ければ十分です。

やりたいこと ポート ソース(誰から)
Webサイトを公開する 80 0.0.0.0/0(全世界)
WebサイトをHTTPSで公開する 443 0.0.0.0/0(全世界)
SSHでログインして作業する 22 自分のIPだけ
WindowsにRDPで入る 3389 自分のIPだけ
別のサーバーからMySQLを使う 3306 相手のセキュリティグループ
別のサーバーからPostgreSQLを使う 5432 相手のセキュリティグループ

太字の行を 0.0.0.0/0 にしないでください。22番を全世界に開けると、数分で世界中からログイン試行が来ます。

ソースの3つの書き方

「誰から」の欄には3種類の書き方があります。

書き方 意味 使う場面
0.0.0.0/0 全世界のどこからでも 公開するWebサイトの80番・443番
203.0.113.10/32 このIPアドレス1つだけ 自分の家や会社からのSSH
sg-0abc123... このセキュリティグループが付いた機器から Webサーバー → DBサーバー

3つ目が実務で一番使います。相手のIPアドレスを調べる必要がなく、サーバーを増やしても設定を直さなくて済むからです。

そのまま使える3パターン

① 公開するWebサーバー

ec2.authorize_security_group_ingress(GroupId=web_sg, IpPermissions=[
    {"IpProtocol": "tcp", "FromPort": 80,  "ToPort": 80,
     "IpRanges": [{"CidrIp": "0.0.0.0/0"}]},
    {"IpProtocol": "tcp", "FromPort": 443, "ToPort": 443,
     "IpRanges": [{"CidrIp": "0.0.0.0/0"}]},
    {"IpProtocol": "tcp", "FromPort": 22,  "ToPort": 22,
     "IpRanges": [{"CidrIp": "203.0.113.10/32"}]},   # 自分のIPに置き換える
])

② 自分しか使わない検証用サーバー

ec2.authorize_security_group_ingress(GroupId=dev_sg, IpPermissions=[
    {"IpProtocol": "tcp", "FromPort": 22, "ToPort": 22,
     "IpRanges": [{"CidrIp": "203.0.113.10/32"}]},
])

22番を自分のIPだけ。これで足ります。

③ Webサーバーとデータベースの2台構成

データベース側には、IPアドレスではなくWebサーバーのセキュリティグループを書きます。

ec2.authorize_security_group_ingress(GroupId=db_sg, IpPermissions=[
    {"IpProtocol": "tcp", "FromPort": 3306, "ToPort": 3306,
     "UserIdGroupPairs": [{"GroupId": web_sg}]},     # IPではなくSGを指定
])

実行して中身を見ると、IPの欄は空で、参照先のSGだけが入っています。

3306番 ← SG ['sg-ef89609e7367de7c6']  IP指定=[]

こう書いておくと、Webサーバーを2台3台と増やしても、そのサーバーに web_sg を付けるだけでDBに繋がります。DB側の設定は変えません。

セキュリティグループで詰まるところ

実際に叩いて確かめた挙動です。

同じルールをもう一度追加
 → InvalidPermission.Duplicate - The specified rule already exists

「このIPは拒否」と書きたい
 → 書けない。APIに拒否の指定が無く、許可だけを並べる仕組み

IpProtocol="-1" を指定
 → 全プロトコル・全ポートを開ける意味になる。範囲を絞らずに使わない

1台のサーバーに2つのSGを付ける
 → 付いたSG: ['web-sg', 'db-sg']  複数付けられ、許可は合算される

拒否が書けないのが、前の節で見た「許可の足し算」の理由です。狭めたいときは追加ではなく削除(revoke_security_group_ingress)を使います。

なお、ここで扱ったのはインスタンスに付ける通信の許可だけです。サブネット単位で拒否も書ける仕組み(ネットワークACL)や、そもそもネットワークをどう区切るかは、次回のVPCで扱います。

実際に叩くと止まるところ

権限まわりで返るエラーをmotoで再現しました。エラーコードは本物のEC2と同じです。

無いSGを指定して起動        -> InvalidGroup.NotFound
同名のSGをもう一度作る      -> InvalidGroup.Duplicate
同じ許可を二重に追加        -> InvalidPermission.Duplicate
インスタンスを停止          -> stopped
インスタンスを終了          -> terminated

stoppedterminated は別物です。停止は電源を切っただけで、ディスク(EBS)は残り、また起動できます。終了は削除で、戻せません。EBSの料金は停止中もかかり続けるので、「止めたのに請求が来る」はここが理由です。terminateしても、後から追加したボリュームは既定で残ります。

この実測の限界を3つ。1つ目、存在しないキーペア名を指定してもmotoは起動を通しました。本物のEC2は InvalidKeyPair.NotFound で弾きます。2つ目、motoはIAMポリシーの評価をしません(simulate_principal_policy は未実装で例外になりました)。つまり「権限が足りずに起動できない」の再現はローカルではできません。ここは実際のアカウントで確かめるしかない部分です。3つ目として、インスタンスプロファイルを作った直後に起動すると、実アカウントでは反映待ちで Invalid IAM Instance Profile name が返ることがあります。motoでは一瞬で通るので、この待ちもローカルでは体験できません。

人側の権限。誰が起動していいか

3方向の3つ目です。EC2を操作する人(またはCI)には、IAMポリシーでこう与えます。

{
  "Version": "2012-10-17",
  "Statement": [
    {"Effect": "Allow", "Action": ["ec2:Describe*"], "Resource": "*"},
    {"Effect": "Allow",
     "Action": ["ec2:RunInstances", "ec2:StartInstances", "ec2:StopInstances"],
     "Resource": "*",
     "Condition": {"StringEquals": {"aws:RequestedRegion": "ap-northeast-1"}}},
    {"Effect": "Allow", "Action": "iam:PassRole", "Resource":
     "arn:aws:iam::123456789012:role/ec2-s3-reader"}
  ]
}

3つ目の iam:PassRole を忘れると「ロール付きのインスタンスだけ起動できない」という分かりにくい詰まり方をします。インスタンスにロールを渡す行為そのものに権限が要るためです。渡していいロールを限定して書くのが定石で、Resource: "*" にすると管理者権限のロールを付けたインスタンスを誰でも作れてしまいます。

TerminateInstances を外してあるのも意図的です。開発者に起動と停止は許し、削除は許さない、という線引きができます。

料金の考え方

  • 稼働時間: インスタンスが running の間だけ課金。Linuxは1分以上なら秒単位、Windowsなどは時間単位。停止中は本体の課金はゼロ
  • EBS(ディスク): 停止中も課金が続く。terminateで自動的に消えるのはルートボリュームだけで、後から追加したボリュームは既定で残ります。describe_volumes で状態が available のまま溜まっていないか見てください
  • 通信: インターネットへの送信に課金。受信は無料
  • パブリックIPv4アドレス: 2024年2月から、使用中かどうかに関係なく1アドレスあたり0.005 USD/時間(月約3.6 USD)。EC2に自動で付くパブリックIPも、Elastic IPも対象です。使わないElastic IPは解放してください

権限とは直接関係ありませんが、「terminateせず止めただけ」のインスタンスと、取り残されたボリュームは、気付かないうちに積み上がります。

よくある注意点

  • 22番を 0.0.0.0/0 に開けない。 自分のIP(/32)に限定するか、Session Manager(SSM)を使って22番を閉じたまま入る方法もあります
  • 秘密鍵は作成時にしか受け取れない。 create_key_pair の戻り値を保存し忘れると、そのインスタンスにSSHでは入れません(SSM Session Managerを有効にしていれば別経路で入れます)
  • アクセスキーをEC2に置かない。 ロールを使えばキーは不要です。置いたキーは漏えいしたら取り消すまで有効なままです
  • IMDSv2を必須にする。 新規作成時に HttpTokens: required を付けます
  • SGの許可は足し算。 狭めたいときは revoke_security_group_ingress で古い行を消します。追加しただけだと両方残ります
  • iam:PassRole を忘れない。 ロール付き起動だけ失敗する原因の筆頭です

まとめ

EC2は「AWSの中にコンピューターを1台借りて、自分のプログラムを動かし続ける」サービスです。借りた1台がインスタンスで、OSまで自分で触れます。

そして権限は方向で3つに分けると迷わなくなります。外から入る権限がセキュリティグループとキーペア、サーバー自身が持つ権限がIAMロール、操作する人の権限がIAMポリシー。

冒頭の2つの症状は、これで切り分けられます。

  • SSHで入れない → 入口の問題。警備員のリスト(SGの22番)か、鍵(キーペア)のどちらか
  • S3が読めない → サーバー自身の問題。社員証(IAMロール)が無い。ポートをいくら開けても直らない

次に手を動かすなら、SGの22番を自分のIPに限定した状態から 0.0.0.0/0 を追加して、describe_security_groups で両方の行が残るのを見てください。許可が足し算だと体で分かれば、EC2の設定で事故る場面はかなり減ります。

次回はVPC(ネットワーク)を予定しています。

参考

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?