Boto3でAWSリソースを構築してみた 1
はじめに
こんにちは!
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」のタグを追加する
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:
でクライアントエラーの例外をキャッチできます。
この記述が無いと、下記のように冗長なコードを書いてクライアントエラーの例外をキャッチする必要があります。
except ec2_client.exceptions.ClientError as e:
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」のタグを追加
# サブネットの作成
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_id
と cidr_block
のみです。
上記のコードではavailability_zone
も引数として渡していますが、これは必須ではありません。
サブネットを作成する際は、AZを指定しなくてもAWS側で自動で設定してくれます。
今回はサブネットが作成されるAZをap-northeast-1a
として指定しました。
インターネットゲートウェイの作成とVPCへのアタッチ
・インターネットゲートウェイの作成
・インターネットゲートウェイにタグ「MyInternetGateway」を追加
・インターネットゲートウェイをVPCへアタッチ
# インターネットゲートウェイの作成と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」タグを追加
# パブリックルートテーブルの作成と、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ロールをアタッチ
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ロールの信頼ポリシーを定義
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'
)
RoleName
とAssumeRolePolicyDocument
は必須の引数です。
RoleNameは一意である必要があります。
Description
は無くても大丈夫です。
信頼ポリシーはjson形式で渡す必要があります。
そのためjsonモジュールのdumpsメソッドで、Pythonの辞書型で定義した信頼ポリシーをjson形式に変換しています。
IAMロールにSSMFullAccessポリシーをアタッチ
iam_client.attach_role_policy(
RoleName=role_name,
PolicyArn='arn:aws:iam::aws:policy/AmazonSSMFullAccess'
)
RoleName
とPolicyArn
は必須の引数です。
RoleNameは一意であるため、これで特定のロールにポリシーをアタッチする事ができます。
ARNとは
arnとは、Amazon Resource Nameの略です。
AWS上で構築するほぼ全てのリソースは一意の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