🐡

Boto3でAWSリソースを構築してみた 1

2024/12/09に公開

はじめに

こんにちは!
Boto3を使ってAWSリソースを構築してみました。
その際に学んだ内容の備忘録です。

今回構築する構成図はこちらになります。
draw.ioで作りましたが、とても便利なサービスでした。

思ったより内容が長くなりそうだったため、
今回は「VPCの構築」と「IAMロールの作成」の2つについて紹介します。

以下の内容は、また次回にまとめたいと思います。

  • EC2インスタンスを作成する
  • SSMエンドポイントを作成し、EC2インスタンスに安全ににアクセスする

対象読者

AWSの基本を学んだ方。
Pythonの基本文法を学んだ方。

やらないこと

環境構築
ローカルマシンにシークレットアクセスキーなどのクレデンシャルを保存し、エディタはVSCodeを使用しています。
適当にコネコネして環境を作ってしまったため、やり方を覚えていませんでした。
環境構築についても記事にまとめたいと思いますが、今回はスキップします。

目次

1. VPCを構築する

  • VPCを作成する
  • サブネットを作成する
  • インターネットゲートウェイを作成し、VPCにアタッチする
  • パブリックルートテーブルを作成し、サブネットに関連付ける

2. IAMロールを作成する

  • IAMロールを作成する
  • IAMロールにSSMFullAccessポリシーをアタッチする
  • インスタンスプロファイルを作成し、IAMロールをアタッチする

1.VPCを構築する

  • 10.0.0.0/16のVPCを作成する
  • 作成したVPCに「MyVPC」のタグを追加する
create_vpc.py
import boto3
from botocore.exceptions import ClientError

ec2_client = boto3.client('ec2')


# VPCを構築
def create_vpc():
    try:
        response = ec2_client.create_vpc(
            CidrBlock='10.0.0.0/16'
        )
        
        vpc_id = response['Vpc']['VpcId']
        print(f'VPCを作成しました。VPC ID: {vpc_id}')
        
        # VPCにタグを付与する
        ec2_client.create_tags(
            Resources=[vpc_id],
            Tags=[
                {'Key': 'Name', 'Value': 'MyVPC'}
            ]
        )
        print(f'VPCにタグを付与しました。VPC_ID: {vpc_id}')
        
    except ClientError as e:
        print(f'クライアントエラーが発生しました。: {e.response["Error"]["Message"]}')
    except Exception as e:
        print(f'予期しないエラーが発生しました。: {e}')

# メソッド呼び出し
create_vpc()

クライアントオブジェクトの生成

ec2_client = boto3.client('ec2')

この部分では、boto3モジュールのclientメソッドを呼び出し、引数には操作したいAWSサービスを指定します。

引数でリージョンを指定する事もできますが、私はaws configureでデフォルトリージョンをap-northeast-1に設定しているため、引数で指定せずとも自動で東京リージョンにリソースが作成されます。

ec2_clientはクライアントオブジェクトです。
クライアントオブジェクトが、AWSサービスを操作するためのインターフェースになります。

このクライアントオブジェクトには、様々なメソッドが内包されています。
これらのメソッドを使って、AWSリソースの構築・操作などを行なっていきます。

内部的には、AWSのAPIエンドポイントへリクエストを送信しています。

VPCの作成

response = ec2_client.create_vpc(
            CidrBlock='10.0.0.0/16'
        )

クライアントオブジェクトec2_clientに対してcreate_vpcメソッドを呼び出し、オプションとして引数にCidrBlockを指定しています。
CidrBlockは必須の引数です。
他にも様々なオプションを引数としてcreate_vpcメソッドに渡すことができます。

VPCの作成に成功した場合、APIリクエストのレスポンスがresponse変数に格納されます。

レスポンスは通常JSON形式なのですが、Boto3がPythonの辞書形式に変換してくれます。

レスポンス例
{
    'Vpc': {
        'VpcId': 'vpc-12345678',     
        'CidrBlock': '10.0.0.0/16',    
        'State': 'pending',            
        'DhcpOptionsId': 'dopt-12345678', 
        'InstanceTenancy': 'default',  
        'IsDefault': False             
    }
}

そのため、response['Vpc']['VpcId']とすることで、VPC IDを取り出すことができます。

タグ作成

ec2_client.create_tags(
            Resources=[vpc_id],
            Tags=[
                {'Key': 'Name', 'Value': 'MyVPC'}
            ]
        )

create_tagsでは、Resources引数でタグを付与するリソースを指定します。

タグはリスト形式で指定します。
今回はタグを1つしか設定していませんが、下記のように複数のタグを指定する事ができます。

タグ複数指定の例
Tags=[
                {'Key': 'Name', 'Value': 'MyVPC'},
                {'Environment': 'Dev'},
                {'Owner': 'Tanaka Taro'}
            ]

例外処理

    except ClientError as e:
        print(f'クライアントエラーが発生しました。: {e.response["Error"]["Message"]}')
    except Exception as e:
        print(f'予期しないエラーが発生しました。: {e}')

exceptでは、指定した型と一致する例外をキャッチします。

except ClientError as e:

AWS APIのリクエスト時に、「アクセス権限の不足」「リクエストのパラメータ誤り」など、リクエストに問題があり処理に失敗すると、クライアントエラーが発生します。

from botocore.exceptions import ClientError

こちらをインポートすることで、except ClientError as e:でクライアントエラーの例外をキャッチできます。

この記述が無いと、下記のように冗長なコードを書いてクライアントエラーの例外をキャッチする必要があります。

from botocore.exceptions import ClientError なし
except ec2_client.exceptions.ClientError as e:
from botocore.exceptions import ClientError あり
except ClientError as e:

botocoreとは、boto3の基盤となるライブラリです。
exceptionsとはbotocoreライブラリの中にある、例外クラスが集まったモジュールです。

exceptionモジュールには、ClientErrorなど様々な例外クラスがあります。

例外インスタンスにはresponse属性があり、そこに詳細なエラー内容が格納されています。

{
  "Error": {
    "Code": "InvalidParameterValue",
    "Message": "CIDR block is malformed"
  },
  "ResponseMetadata": {
    "RequestId": "12345-abcdef",
    "HTTPStatusCode": 400,
    "RetryAttempts": 0
  }
}

そのため、e.response['Error']['Message']とすることで、エラーメッセージを取り出すことができます。

except Exception as e:

Exceptionは例外の基底クラスです。
これにより、明示的に指定しなくても全ての標準例外をキャッチできます。

最初はAWS ClientErrorなどAWS固有の例外をキャッチし、その後に汎用的な例外をキャッチしています。

サブネット作成

  • 10.0.1.0/24のサブネットを作成
  • サブネットに「Public Subnet-a」のタグを追加
create_vpc.py
# サブネットの作成
def create_subnet(vpc_id, cidr_block, availability_zone):
    try:    
        response = ec2_client.create_subnet(
            VpcId=vpc_id,
            CidrBlock=cidr_block,
            AvailabilityZone=availability_zone
        )
        subnet_id = response['Subnet']['SubnetId']
        print(f'サブネットを作成しました。サブネットID: {subnet_id}')
        
        ec2_client.create_tags(
            Resources=[subnet_id],
            Tags=[
                {'Key': 'Name', 'Value': 'Public Subnet-a'}
            ]
        )
    
    except ClientError as e:
        print(f'クライアントエラーが発生しました。: {e.response["Error"]["Message"]}')
    except Exception as e:
        print(f'予期しないエラーが発生しました。: {e}')

# メソッド呼び出し
vpc_id = 'vpc-0a6b4a6df4b4fff9a'
cidr_block = '10.0.1.0/24'
availability_zone = 'ap-northeast-1a'
create_subnet(vpc_id, cidr_block, availability_zone)

サブネット作成も、VPC作成と基本的に変わりません。

create_subnetの引数で必須なのは、vpc_idcidr_blockのみです。
上記のコードではavailability_zoneも引数として渡していますが、これは必須ではありません。
サブネットを作成する際は、AZを指定しなくてもAWS側で自動で設定してくれます。

今回はサブネットが作成されるAZをap-northeast-1aとして指定しました。

インターネットゲートウェイの作成とVPCへのアタッチ

・インターネットゲートウェイの作成
・インターネットゲートウェイにタグ「MyInternetGateway」を追加
・インターネットゲートウェイをVPCへアタッチ

create_vpc.py
# インターネットゲートウェイの作成とVPCへのアタッチ
def create_and_attach_internet_gateway(vpc_id):
    try:
        # インターネットゲートウェイの作成
        response = ec2_client.create_internet_gateway()
        igw_id = response['InternetGateway']['InternetGatewayId']
        print(f'インターネットゲートウェイを作成しました。ID: {igw_id}')
        
        # 作成したインターネットゲートウェイをVPCにアタッチ
        ec2_client.attach_internet_gateway(
            InternetGatewayId=igw_id,
            VpcId=vpc_id
        )
        print(f'インターネットゲートウェイ {igw_id}を VPC {vpc_id} にアタッチしました。')
        
        # インターネットゲートウェイにタグを付与
        ec2_client.create_tags(
            Resources=[igw_id],
            Tags=[
                {'Key': 'Name', 'Value': 'MyInternetGateway'}
            ]
        )
    
    except ClientError as e:
        print(f'クライアントエラーが発生しました。{e.response["Error"]["Message"]}')
    except Exception as e:
        print(f'予期しないエラーが発生しました。{e}')


# メソッド呼び出し
vpc_id = 'vpc-0a6b4a6df4b4fff9a'
create_and_attach_internet_gateway(vpc_id)

パブリックルートテーブル作成

  • ルートテーブルの作成
  • ルートテーブルに0.0.0.0/0のルートを追加
  • サブネットにルートテーブルを関連付ける
  • ルートテーブルに「Public Route Table」タグを追加
create_vpc.py
# パブリックルートテーブルの作成と、Public Subnet-aへの関連付け
def create_and_associate_public_roue_table(vpc_id, igw_id, subnet_id):
    try:
        # ルートテーブルの作成
        response = ec2_client.create_route_table(VpcId=vpc_id)
        route_table_id = response['RouteTable']['RouteTableId']
        print(f'ルートテーブルを作成しました。ID: {route_table_id}')
        
        # インターネットゲートウェイをデフォルトルートに設定
        ec2_client.create_route(
            RouteTableId=route_table_id,
            DestinationCidrBlock='0.0.0.0/0',
            GatewayId=igw_id
        )
        print(f'インターネットゲートウェイ {igw_id} をルートテーブル {route_table_id} に関連づけました。')
        
        # サブネットにルートテーブルを関連付ける
        ec2_client.associate_route_table(
            RouteTableId=route_table_id,
            SubnetId=subnet_id
        )        
        print(f'ルートテーブル {route_table_id} をサブネット {subnet_id} に関連づけました。')
        
        # ルートテーブルにタグを付与
        ec2_client.create_tags(
            Resources=[route_table_id],
            Tags=[
                {'Key': 'Name', 'Value': 'Public Route Table'}
            ]
        )
        
    except ClientError as e:
        print(f'クライアントエラーが発生しました。: {e.response["Error"]["Message"]}')
    except Exception as e:
        print(f'予期しないエラーが発生しました。: {e}')


# メソッド呼び出し
vpc_id = 'vpc-0a6b4a6df4b4fff9a'
igw_id = 'igw-093f3facfc76adf1b'
subnet_id = 'subnet-0b9946d0fa68185dd'
create_and_associate_public_roue_table(vpc_id, igw_id, subnet_id)

2. IAMロールを作成する

  • IAMロールの信頼ポリシーを定義
  • IAMロールを作成
  • IAMロールにSSMFullAccessポリシーをアタッチ
  • インスタンスプロファイルの作成
  • インスタンスプロファイルにIAMロールをアタッチ
create_role.py
import boto3
import json
from botocore.exceptions import ClientError


# IAMとEC2クライアントを作成
iam_client = boto3.client('iam')
ec2_client = boto3.client('ec2')


# ロール名とインスタンスプロファイルを指定
role_name = 'SSMFullAccessRole'
instance_profile_name = 'SSMInstanceProfile'


# IAMロールの信頼ポリシーを定義
trust_policy = {
    'Version': '2012-10-17',
    'Statement': [
        {
            'Effect': 'Allow',
            'Principal': {'Service': 'ec2.amazonaws.com'},
            'Action': 'sts:AssumeRole'
        }
    ]
}


# IAMロールを作成
try:
    response = iam_client.create_role(
        RoleName=role_name,
        AssumeRolePolicyDocument=json.dumps(trust_policy),
        Description='IAM Role for EC2 to access SSM'
    )
    print(f'IAMロールを作成しました。ロール名: {role_name}')
except ClientError as e:
    print(f'クライアントエラーが発生しました。:{e.response["Error"]["Message"]}')
except Exception as e:
    print(f'予期しないエラーが発生しました。: {e}')



# IAMロールにSSMFullAccessポリシーをアタッチ
try:
    iam_client.attach_role_policy(
        RoleName=role_name,
        PolicyArn='arn:aws:iam::aws:policy/AmazonSSMFullAccess'
    )
    print(f'IAMロール {role_name} に AmazonSSMFullAccessポリシーをアタッチしました。')
    
except ClientError as e:
    print(f'クライアントエラーが発生しました。:{e.response["Error"]["Message"]}')
except Exception as e:
    print(f'予期しないエラーが発生しました。:{e}')
    

# インスタンスプロファイルの作成
try:
    iam_client.create_instance_profile(
        InstanceProfileName=instance_profile_name
    )
    print(f'インスタンスプロファイル {instance_profile_name} を作成しました。')
except ClientError as e:
    print(f'クライアントエラーが発生しました。:{e.response["Error"]["Message"]}')
except Exception as e:
    print(f'予期しないエラーが発生しました。:{e}')


# インスタンスプロファイルにIAMロールをアタッチ
try:
    iam_client.add_role_to_instance_profile(
        InstanceProfileName=instance_profile_name,
        RoleName=role_name
    )
    print(f'インスタンスプロファイル {instance_profile_name} にロール{role_name}を追加しました。')
except ClientError as e:
    print(f'クライアントエラーが発生しました。{e.response["Error"]["Message"]}')
except Exception as e:
    print(f'予期しないエラーが発生しました')

IAMとEC2クライアントを作成

iam_client = boto3.client('iam')
ec2_client = boto3.client('ec2')

今回はIAMのリソースを操作します。
そのためboto3.client('iam')でIAMリソースを操作するためのクライアントオブジェクトを生成し、iam_clientへ格納します。

ここで、ちょっとAWSについて曖昧な部分があったのでメモします。

インスタンスプロファイルとは

EC2インスタンスに、直接IAMロールをアタッチすることはできません。
そのため、インスタンスプロファイルと呼ばれる入れ物を用意します。

この入れ物には、1つだけIAMロールを格納することができます。
IAMロールを格納したら、そのインスタンスプロファイルをEC2インスタンスにアタッチします。

このようにして、間接的にEC2インスタンスにIAMロールをアタッチします。
構図としては、EC2インスタンス > インスタンスプロファイル > IAMロール > ポリシー となります。

IAMロールの信頼ポリシーとは

IAMロールには、信頼ポリシーと管理ポリシーの2つを設定する必要があります。

信頼ポリシーとは、「誰が」このロールを引き受ける事ができるのかを定義します。
「誰」とは認証主体のことで、エンティティと呼ばれたりもします。
「誰」と書きましたが、AWSアカウントだけでなく、EC2やLambdaなどのAWSサービスも認証主体となり、ロールを引き受けることができます。

管理ポリシーとは、「何が」できるのかを定義します。
その認証主体がどんな操作ができるのか、その権限を決めます。

IAMロールの信頼ポリシーを定義

create_role.py
trust_policy = {
    'Version': '2012-10-17',
    'Statement': [
        {
            'Effect': 'Allow',
            'Principal': {'Service': 'ec2.amazonaws.com'},
            'Action': 'sts:AssumeRole'
        }
    ]
}

'Principal': {'Service': 'ec2.amazonaws.com'},で、認証主体となるサービスをec2に設定しています。
つまり、ec2インスタンスがこのロールを引き受ける事ができます。

正確には、このロールをLambdaなどの他サービスにアタッチすることは可能です。
しかし認証主体をec2に限定しているため、ec2以外のサービスにアタッチしても機能しません。

'Action': 'sts:AssumeRole'
この部分ですが、STSとはセキュア・アクセス・トークンの略で、一時的な認証情報を発行するAWSサービスです。

sts:AssumeRoleとは、AWS STSに対して「このロールを引き受けるための認証情報をください」とリクエストをするアクションです。
このアクションが、('Effect': 'Allow')で許可されます。

まとめると、このロールを引き受けたのがec2サービスならば、STSへ認証情報のリクエストを行い、このロールを引き受ける事ができる。そしてロールにアタッチされている管理ポリシーで許可されている操作が可能になる。
となります。

IAMロールの作成

response = iam_client.create_role(
        RoleName=role_name,
        AssumeRolePolicyDocument=json.dumps(trust_policy),
        Description='IAM Role for EC2 to access SSM'
    )

RoleNameAssumeRolePolicyDocumentは必須の引数です。
RoleNameは一意である必要があります。
Descriptionは無くても大丈夫です。

信頼ポリシーはjson形式で渡す必要があります。
そのためjsonモジュールのdumpsメソッドで、Pythonの辞書型で定義した信頼ポリシーをjson形式に変換しています。

IAMロールにSSMFullAccessポリシーをアタッチ

iam_client.attach_role_policy(
        RoleName=role_name,
        PolicyArn='arn:aws:iam::aws:policy/AmazonSSMFullAccess'
    )

RoleNamePolicyArnは必須の引数です。
RoleNameは一意であるため、これで特定のロールにポリシーをアタッチする事ができます。

ARNとは

arnとは、Amazon Resource Nameの略です。
AWS上で構築するほぼ全てのリソースは一意のarnを持ちます。
arnはリソースを特定するための識別子です。

こちらを例に、arnについて見てみましょう。

arn例
arn:aws:ec2:us-east-1:123456789012:instance/i-0abcd1234efgh5678
部分 説明
arn arn 固定値で、先頭部分はこの識別子で始まります。
パーティション aws これも実質固定値です。他には中国リージョン用のaws-cnや米国政府用のaws-us-govなどがありますが、使う機会は無いと思います。
サービス ec2 サービス名です。
リージョン us-east-1 リソースが存在するリージョンです。IAMのようなグローバルサービスにはリージョンが存在しないため、省略されます。
アカウントID 123456789012 AWSアカウントIDです。AWSが持つリソースの場合はawsになります。
リソース instance/i-0abcd1234efgh5678 リソースタイプ/リソース名で、ec2インスタンスを一意に識別します。
今回の場合
arn:aws:iam::aws:policy/AmazonSSMFullAccess

arn...固定値
aws...(ほぼ)固定値
iam...サービス名
::...リージョン名(グローバルサービスのため省略)
aws...aws管理ポリシーのため、リソースの所有者はAWSである事を示す。
policy/AmazonSSMFullAccess...リソース(ポリシー)を一意に特定する部分。

インスタンスプロファイルを作成する

iam_client.create_instance_profile(
        InstanceProfileName=instance_profile_name
    )

必須の引数はインスタンスプロファイル名のみです。
これは一意である必要があります。

終わりに

なんとかここまでBoto3で構築できました。
次回はEC2インスタンスを作成し、apacheをインストールしてwebページにアクセスできるようにします。
さらにSSMエンドポイントを作成し、EC2と安全に通信できるように設定します。
余力があればリソースの削除もBoto3で実装してみたいです。

Discussion