「Amazon Web Services 基礎からのネットワーク&サーバー構築 改訂版」をBoto3とAnsibleで写経してみた

社内ではAWSが普通に使われているため、常々基礎からきちんと学びたいと考えていました。

そんな中、書籍「Amazon Web Services 基礎からのネットワーク&サーバー構築 改訂版」の社内勉強会が開催されることになりました。

Amazon Web Services 基礎からのネットワーク&サーバー構築 改訂版

Amazon Web Services 基礎からのネットワーク&サーバー構築 改訂版

 
これはちょうどよい機会だと思い、書籍をひと通り読んでみました。

書籍では、オンプレでやってたことをAWSで実現するにはどうすればよいかなど、AWSのインフラまわりの基礎的なところが書かれており、とてもためになりました。

ただ、手動でAWS環境を構築したため、このまま何もしないと忘れそうでした。

何か良い方法がないかを探したところ、AWS SDK for Python(Boto3)がありました。これを使ってPythonスクリプトとして残しておけば忘れないだろうと考えました。

 
なお、Boto3ではなくAnsibleでも構築できそうでしたが、今回はBoto3の使い方に慣れようと思い、

として、写経してみることにしました。

 
目次

 

環境

現在では、Boto3・AnsibleともにPython3で動くようです。

 

IAMユーザーの作成と設定

書籍では、AWSアカウントについては特に触れられていませんでした。

そこで、今年の3/11に開かれたJAWS DAYS 2017のセッション「不安で夜眠れないAWSアカウント管理者に送る処方箋という名のハンズオン」に従い、IAMユーザーを作成・使用することにしました。

 
主な設定内容は

としました。

 

Boto3の準備

今回はawscliを使わず、Boto3だけで環境を準備します。

READMEのQuick Startに従って、環境ファイルを用意します。
boto/boto3: AWS SDK for Python

~/.aws/credentials

こちらに aws_access_key_idaws_secret_access_keyを設定します。
【鍵管理】~/.aws/credentials を唯一のAPIキー管理場所とすべし【大指針】 | Developers.IO

今回は試しにデフォルト以外のprofile(my-profile)を使うことにしてみました。
boto3 で デフォルトprofile以外を使う - Qiita

[my-profile]
aws_access_key_id = YOUR_KEY
aws_secret_access_key = YOUR_SECRET

 

~/.aws/config

こちらにはデフォルトリージョンを設定します。

READMEにはus-east-1と記載されています。

東京リージョンの場合は何を指定すればよいかを探したところ、 ap-northeast-1 を使うのが良さそうでした。
AWS のリージョンとエンドポイント - アマゾン ウェブ サービス

[default]
region=ap-northeast-1

ただ、自分の書き方が悪いせいか、上記のように書いてもデフォルトリージョンとして設定されなかったため、Boto3を使うところでリージョンを指定しています。

 
あとは、

# ~/.aws/credentialsのmy-profileにあるキーを使う
session = boto3.Session(profile_name='my-profile')

# Clientを使う場合
client = session.client('ec2', region_name='ap-northeast-1')

# Resourceを使う場合
resource = session.resource('ec2', region_name='ap-northeast-1')

のようにして、アクセスキーなどを持ったclientとresourceを取得し、それを使って操作します。

なお、clientやresourceの第一引数にはAWS サービスの名前空間を使えば良さそうでした。
AWS サービスの名前空間 | Amazon リソースネーム (ARN) と AWS サービスの名前空間 - アマゾン ウェブ サービス

今回使用するAmazon EC2Amazon VPC名前空間は、ともに ec2 のようでした。

 

Ansibleまわり

途中でEC2の設定をするため、Ansibleまわりの準備もしておきます。
ansible.cnfでssh_configを設定する | Developers.IO

 

ssh_config

HostNameのxxx.xxx.xxx.xxxは、EC2を立てた時にパブリックIPアドレスへと差し替えます。

Host webserver
  User ec2-user
  HostName xxx.xxx.xxx.xxx
  # IdentityFileはカレントディレクトリに置いておけば良い
  IdentityFile syakyo_aws_network_server2.pem
  StrictHostKeyChecking no
  UserKnownHostsFile /dev/null

 

ansible.cnf

Fオプションのファイル(ssh_config)は、プルパスでなくても問題ありませんでした。

[ssh_connection]
ssh_args = -F ssh_config

 

Inventoryファイル(hosts)

ここに記述するホスト名は、ssh_configのHostの値と一致させます。今回はwebserverとなります。

[web]
webserver

 
あとは、ターミナルから

$ ansible-playbook -i hosts ch4_apache.yml

のようにして、EC2インスタンスに対して実行します。

 

写経

今回写経したコードを載せておきます。

なお、だいたいのものはClientとResourceServiceのどちらでも操作できました。

ただ、戻り値が

  • Clientは、dict
  • ResourceServiceは、各オブジェクト

だったので、ResourceServiceの方が扱いやすいのかなと感じました。

 

Chapter2
VPCの作成

https://boto3.readthedocs.io/en/latest/reference/services/ec2.html#EC2.Client.create_vpc

response = ec2_client.create_vpc(
    CidrBlock='192.168.0.0/16',
    AmazonProvidedIpv6CidrBlock=False,
)
vpc_id = response['Vpc']['VpcId']

 

VPCに名前をつける

https://boto3.readthedocs.io/en/latest/reference/services/ec2.html#EC2.Vpc.create_tags

vpc = ec2_resource.Vpc(vpc_id)
tag = vpc.create_tags(
    Tags=[
        {
            'Key': 'Name',
            'Value': 'VPC領域2'
        },
    ]
)

 

VPCの一覧を確認

https://boto3.readthedocs.io/en/latest/reference/services/ec2.html#EC2.Client.describe_vpcs

response = ec2_client.describe_vpcs(
    Filters=[
        {
            'Name': 'tag:Name',
            'Values': [
                'VPC領域',
            ]
        }
    ]
)

 

アベイラビリティゾーンの確認

https://boto3.readthedocs.io/en/latest/reference/services/ec2.html#EC2.Client.describe_availability_zones

response = ec2_client.describe_availability_zones(
    Filters=[{
        'Name': 'state',
        'Values': ['available'],
    }]
)

 

VPCにサブネットを作成

https://boto3.readthedocs.io/en/latest/reference/services/ec2.html#EC2.Vpc.create_subnet

vpc = ec2_resource.Vpc(vpc_id)
response = vpc.create_subnet(
    AvailabilityZone=availability_zone,
    CidrBlock=cidr_block,
)

 

サブネットに名前をつける

https://boto3.readthedocs.io/en/latest/reference/services/ec2.html#EC2.Subnet.create_tags

tag = ec2_subnet.create_tags(
    Tags=[{
        'Key': 'Name',
        'Value': subnet_name,
    }]
)

 

インターネットゲートウェイを作成

https://boto3.readthedocs.io/en/latest/reference/services/ec2.html#EC2.Client.create_internet_gateway

response = ec2_client.create_internet_gateway()

 

インターネットゲートウェイに名前をつける

https://boto3.readthedocs.io/en/latest/reference/services/ec2.html#EC2.InternetGateway.create_tags

internet_gateway = ec2_resource.InternetGateway(internet_gateway_id)
tags = internet_gateway.create_tags(
    Tags=[{
        'Key': 'Name',
        'Value': 'インターネットゲートウェイ2',
    }]
)

 

インターネットゲートウェイVPCに紐づける

https://boto3.readthedocs.io/en/latest/reference/services/ec2.html#EC2.InternetGateway.attach_to_vpc

internet_gateway = ec2_resource.InternetGateway(internet_gateway_id)
response = internet_gateway.attach_to_vpc(
    VpcId=vpc_id,
)

 

ルートテーブルを作成する

https://boto3.readthedocs.io/en/latest/reference/services/ec2.html#EC2.Client.create_route_table

response = ec2_client.create_route_table(VpcId=vpc_id)

 

ルートテーブルに名前をつける
route_table = ec2_resource.RouteTable(route_table_id)
tag = route_table.create_tags(
    Tags=[{
        'Key': 'Name',
        'Value': 'パブリックルートテーブル2',
    }]
)

 

ルートテーブルをサブネットに割り当てる

https://boto3.readthedocs.io/en/latest/reference/services/ec2.html#EC2.RouteTable.associate_with_subnet

route_table = ec2_resource.RouteTable(route_table_id)
route_table_association = route_table.associate_with_subnet(SubnetId=subnet_id)

 

デフォルトゲートウェイをインターネットゲートウェイに割り当てる

https://boto3.readthedocs.io/en/latest/reference/services/ec2.html#EC2.RouteTable.create_route

route_table = ec2_resource.RouteTable(route_table_id)
route = route_table.create_route(
    DestinationCidrBlock='0.0.0.0/0',
    GatewayId=internet_gateway_id,
)

 

ルートテーブルを確認する

https://boto3.readthedocs.io/en/latest/reference/services/ec2.html#EC2.Client.describe_route_tables

response = ec2_client.describe_route_tables()

 

Chapter3
キーペアを作成してローカルに保存する

https://boto3.readthedocs.io/en/latest/reference/services/ec2.html#EC2.Client.create_key_pair

# キーペアを作成していない場合はキーペアを作成する
if not os.path.exists(KEY_PAIR_FILE):
    response = ec2_client.create_key_pair(KeyName=KEY_PAIR_NAME)
    print(inspect.getframeinfo(inspect.currentframe())[2], response['KeyName'])
    with open(KEY_PAIR_FILE, mode='w') as f:
        f.write(response['KeyMaterial'])

 

ローカルに保存したキーのパーミッションを変更する
# modeは8進数表記がわかりやすい:Python3からはprefixが`0o`となった
os.chmod(KEY_PAIR_FILE, mode=0o400)

 

セキュリティグループを作成する

https://boto3.readthedocs.io/en/latest/reference/services/ec2.html#EC2.Client.create_security_group

response = ec2_client.create_security_group(
    Description=name,
    GroupName=name,
    VpcId=vpc_id,
)

 

セキュリティグループでSSHのポートを開ける
security_group = ec2_resource.SecurityGroup(security_group_id)
response = security_group.authorize_ingress(
    CidrIp='0.0.0.0/0',
    IpProtocol='tcp',
    # ポートは22だけ許可したいので、From/Toともに22のみとする
    FromPort=22,
    ToPort=22,
)

 

EC2インスタンスを立てる
response = ec2_resource.create_instances(
    ImageId=IMAGE_ID,
    # 無料枠はt2.micro
    InstanceType='t2.micro',
    # 事前に作ったキー名を指定
    KeyName=key_pair_name,
    # インスタンス数は、最大・最小とも1にする
    MaxCount=1,
    MinCount=1,
    # モニタリングはデフォルト = Cloud Watchは使わないはず
    # Monitoring={'Enabled': False},
    # サブネットにavailability zone が結びついてるので、明示的なセットはいらないかも
    # Placement={'AvailabilityZone': availability_zone},
    # セキュリティグループIDやサブネットIDはNetworkInterfacesでセット(詳細は以下)
    # SecurityGroupIds=[security_group_id],
    # SubnetId=subnet_id,
    NetworkInterfaces=[{
        # 自動割り当てパブリックIP
        'AssociatePublicIpAddress': is_associate_public_ip,
        # デバイスインタフェースは1つだけなので、最初のものを使う
        'DeviceIndex': 0,
        # セキュリティグループIDは、NetworkInterfacesの方で割り当てる
        # インスタンスの方で割り当てると以下のエラー:
        # Network interfaces and an instance-level security groups may not be specified on the same request
        'Groups': [security_group_id],
        # プライベートIPアドレス
        'PrivateIpAddress': private_ip,
        # サブネットIDも、NetworkInterfacesの方で割り当てる
        # インスタンスの方で割り当てると以下のエラー:
        # Network interfaces and an instance-level subnet ID may not be specified on the same request
        'SubnetId': subnet_id,
    }],
    TagSpecifications=[{
        'ResourceType': 'instance',
        'Tags': [{
            'Key': 'Name',
            'Value': instance_name,
        }]
    }],
)

 

EC2インスタンスがrunningになるまで待つ

https://boto3.readthedocs.io/en/latest/reference/services/ec2.html#EC2.Instance.wait_until_running

ec2_instance.wait_until_running()

 

Chapter4
GUIの「DNSホスト名の編集」を実行する

https://boto3.readthedocs.io/en/latest/reference/services/ec2.html#EC2.Client.modify_vpc_attribute

response = ec2_client.modify_vpc_attribute(
    EnableDnsHostnames={'Value': True},
    VpcId=vpc_id,
)

 

AnsibleでApacheをインストールし、起動設定にする
# hostsにはhostsファイルの[web]かホスト名(webserver)を指定する
- hosts: webserver
  become: yes
  tasks:
    - name: install Apache
      yum: name=httpd
    - name: Apache running and enabled
      service: name=httpd state=started enabled=yes

 

Chapter6

Chapter6ではプライベートサブネットにEC2インスタンスを立てます。

その方法はパブリックサブネットと同様なため、ここでは差分のみ記載します。

 

パブリックサブネットのAvailability Zoneを取得する

https://boto3.readthedocs.io/en/latest/reference/services/ec2.html#subnet

ec2_subnet = ec2_resource.Subnet(subnet_id)
return ec2_subnet.availability_zone

 

セキュリティグループでICMPのポートを開ける

https://boto3.readthedocs.io/en/latest/reference/services/ec2.html#EC2.SecurityGroup.authorize_ingress

security_group = ec2_resource.SecurityGroup(security_group_id)
response = security_group.authorize_ingress(
    CidrIp='0.0.0.0/0',
    IpProtocol='icmp',
    # ICMPの場合、From/Toともに -1 を設定
    FromPort=-1,
    ToPort=-1,
)

 

Ansibleで、ローカルのSSH鍵をWebサーバへSCPを使って転送する
# hostsにはhostsファイルの[web]かホスト名(webserver)を指定する
- hosts: webserver
  tasks:
    - name: copy private-key to webserver
      copy: src=./syakyo_aws_network_server2.pem dest=~/ owner=ec2-user group=ec2-user mode=0400

 

Chapter7
Elastic IPを取得する

https://boto3.readthedocs.io/en/latest/reference/services/ec2.html#EC2.Client.allocate_address

# DomainのvpcはVPC、standardはEC2-Classic向け
response = ec2_client.allocate_address(Domain='vpc')

 

NATゲートウェイを作成し、Elastic IPを割り当て、パブリックサブネットに置く

https://boto3.readthedocs.io/en/latest/reference/services/ec2.html#EC2.Client.create_nat_gateway

response = client.create_nat_gateway(
    AllocationId=allocation_id,
    SubnetId=subnet_id,
)

 

NATゲートウェイがavailableになるまで待つ

NATゲートウェイを作成した直後はまだavailableになっていないため、availableになるまで待ちます。

手元では約2分ほどかかりました。

https://boto3.readthedocs.io/en/latest/reference/services/ec2.html#EC2.Waiter.NatGatewayAvailable

waiter = ec2_client.get_waiter('nat_gateway_available')
response = waiter.wait(
    Filters=[{
        'Name': 'state',
        'Values': ['available']
    }],
    NatGatewayIds=[nat_gateway_id]
)

 

メインのルートテーブルのIDを取得する

NATゲートウェイのエントリを追加するため、メインのルートテーブルのIDを取得します。

https://boto3.readthedocs.io/en/latest/reference/services/ec2.html#EC2.Client.describe_route_tables

response = ec2_client.describe_route_tables(
    Filters=[
        {
            'Name': 'association.main',
            'Values': ['true'],
        },
        {
            'Name': 'vpc-id',
            'Values': [vpc_id],
        }
    ]
)
main_route_table_id = response['RouteTables'][0]['RouteTableId']

 

メインのルートテーブルにNATゲートウェイのエントリを追加する

https://boto3.readthedocs.io/en/latest/reference/services/ec2.html#EC2.RouteTable.create_route

route_table = ec2_resource.RouteTable(route_table_id)
route = route_table.create_route(
    DestinationCidrBlock='0.0.0.0/0',
    NatGatewayId=nat_gateway_id,
)

 

写経時に作成したものを削除する

ここまででGUIで行った内容をBoto3 & Ansibleで実装しました。

せっかくなので、作成したものをBoto3で削除するPythonスクリプトも作成してみました (boto3_ansible/clear_all.py)。

流れとしては、作成したのとは逆順で削除していく形となります。

 

その他参考

 

ソースコード

GitHubに上げました。
thinkAmi-sandbox/syakyo-aws-network-server-revised-edition-book