動機
企業ネットワークで
- 普段遣いのPCからはサーバーシステム環境に直接接続できない
- システム毎に管理用端末は分離すべし、というルールがある
- AWSコンソールに接続する端末は複数システムで共通化可能
みたいな制約がある場合に、インフラ管理用端末をシステム毎に用意していくのは手間がかかって管理も煩雑なので、インフラ管理用端末をCloud9で構築しようと考えた。
前提条件
- 対象業務はインフラ構築・運営
- 管理対象インフラはすべてAWS上にある
- 管理対象インフラのAWSリソースは基本的にCloudFormationで管理する
- 管理対象環境にはEC2で立てたLinuxサーバーがあり、設定はAnsibleで管理する
- LinuxのディストリビューションはRHEL
- 環境は東京リージョンに構築する
- 管理用端末は管理対象環境となるべく分離する
構成
PoC用の最小構成(AZなどの表現は省略)
構成の意図としては
- 管理用端末は管理対象環境とVPCを分離してネットワーク的に独立させる
- 管理端末からはAWSの任意のAPIを叩くために、管理端末はパブリックネットワークに配置する
- Linuxサーバーを管理するため、管理端末のAnsibleからSSM経由のSSHで接続する
- プライベートネットワークのLinuxサーバーにSSMでセッションを張るために、SSMのVPCエンドポイントを作成する
- Linuxサーバーのセットアップ等に必要なファイルをS3から取得できるようにするため、S3のVPCエンドポイントを作成する
やってみて分かったこと
AnsibleからSession ManagerでLinuxサーバーに接続できる状態を実現するまでいくつかはまるところがあった。解決しなければならなかったポイントとしては
- AnsibleでLinuxサーバーを設定するのにセッションマネージャーで接続するためには、Ansibleで接続する前に(すなわちAnsibleを使わないで)SSM Agentをセットアップしないといけない → Amazon Linuxなどと違ってデフォルトでSSM Agentが入っていない
- Cloud9環境でデフォルトで設定されるAWSマネージド一時認証情報では、セッションマネージャー接続ができないという制約がある
- Ansibleからセッションマネージャー経由でSSHする場合には、SSH公開鍵認証の秘密鍵が必要 → 通常 aws ssm start-session でSSH接続する際に秘密鍵は必要ない
といったところ。Ansibleからのセッションマネージャー接続に秘密鍵が必要というのは何か回避策がありそうな気がするけど、直でSSH接続する場合には用意しているものなので、今回は素直に秘密鍵を用意することにした。
構築手順
AWSコンソールは、東京リージョンになっていることを確認して作業を進める。
- 管理用のVPCを作成する → デフォルトのVPCを流用する場合には省略可
- 管理端末をCloud9で構築する
- 管理対象のVPC、サーバーを構築する
- 作成した環境の動作確認 (このとき期待する動作をしなかったところがあるため追加作業を実施した)
1. 管理用のVPCを作成する
CloudFormationで、以下のテンプレートでmgmt-vpcという名前のスタックを作成する。
---
AWSTemplateFormatVersion: 2010-09-09
Resources:
# Network
Vpc:
Type: AWS::EC2::VPC
Properties:
CidrBlock: 192.168.0.0/23
EnableDnsSupport: true
EnableDnsHostnames: true
Tags:
- Key: Name
Value: !Sub ${AWS::StackName}-vpc
InternetGateway:
Type: AWS::EC2::InternetGateway
Properties:
Tags:
- Key: Name
Value: !Sub ${AWS::StackName}-igw
InternetGatewayAttachment:
Type: AWS::EC2::VPCGatewayAttachment
Properties:
InternetGatewayId: !Ref InternetGateway
VpcId: !Ref Vpc
PublicSubnet:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref Vpc
CidrBlock: 192.168.0.0/25
MapPublicIpOnLaunch: true
Tags:
- Key: Name
Value: !Sub ${AWS::StackName}-public-subnet
PublicRouteTableAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref PublicSubnet
RouteTableId: !Ref PublicRouteTable
# Route Table
PublicRouteTable:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref Vpc
Tags:
- Key: Name
Value: !Sub ${AWS::StackName}-public-rtb
# Routing
PublicDefaultRoute:
Type: AWS::EC2::Route
Properties:
RouteTableId: !Ref PublicRouteTable
DestinationCidrBlock: '0.0.0.0/0'
GatewayId: !Ref InternetGateway
このファイルをCloudShellの[アクション] > [ファイルのアップロード]でアップロードし、CloudShellからaws cloudformation deploy
コマンドを実行してをスタック作成する。
aws cloudformation deploy --stack-name mgmt-vpc --template-file MgmtVpc.yml --capabilities CAPABILITY_NAMED_IAM
2. 管理端末をCloud9で構築する
AWSコンソールからインフラ管理用のCloud9環境を作成する。
- Cloud9の画面に遷移し、[環境を作成]をクリック
- パラメータを指定する(指定内容は例)
- 名前:mgmt-c9-MyCloud9
- 環境タイプ:新しいEC2インスタンス
- インスタンスタイプ:t2.micro
- プラットフォーム:Amazon Linux 2
- タイムアウト:30分
- 接続:AWS Systems Manager (SSM)
- VPC設定を展開
- Amazon 仮想プライベートクラウド (VPC):さっき作成したVPC(名前:mgmt-vpc-vpc)
- サブネット:さっき作成したサブネット(名前:mgmt-vpc-public-subnet)
- [作成]をクリック
これで環境が作成されるとともに、初回実行時はIAMリソースとして以下が自動的に作成される。
- AWSServiceRoleForAWSCloud9
- AWSCloud9SSMAccessRole
- AWSCloud9SSMInstanceProfile
環境の作成はCloudShellからも実行できるが、そのアカウントで1度もCloud9環境を作成していないとこれらのIAMリソースがないために失敗する。初回作成以降は、以下のようなコマンドで同様の環境が作成可能。
subnet_id=$(aws ec2 describe-subnets --filters Name=tag:Name,Values=mgmt-vpc-public-subnet --query 'Subnets[0].SubnetId' --output text)
aws cloud9 create-environment-ec2 \
--name mgmt-c9-MyCloud9 \
--instance-type t2.micro \
--image-id amazonlinux-2-x86_64 \
--automatic-stop-time-minutes 30 \
--connection-type CONNECT_SSM \
--subnet-id "${subnet_id}"
Cloud9環境が作成できたら、AWSコンソールのCloud9の画面でその環境を選択し[Cloud9を開く]でCloud9 IDEの画面を開く
IDEのBashターミナルから必要なツールをインストール・アップデートする
sudo yum -y install jq git
pip install git-remote-codecommit ansible cfn-lint
curl "https://s3.amazonaws.com/session-manager-downloads/plugin/latest/linux_64bit/session-manager-plugin.rpm" -o "/tmp/session-manager-plugin.rpm"
sudo yum install -y /tmp/session-manager-plugin.rpm
インフラコード管理用のリポジトリをCodeCommitに作成する。
aws codecommit create-repository --repository-name AnsibleCfnDemoRepo
git clone codecommit://AnsibleCfnDemoRepo
3. 管理対象環境を構築する
管理対象環境を構築するためのAnsibleコードを用意する。
CodeCommitで管理するために、git cloneで作成した場所に、ローカル(PC)からアップロードする。
AnsibleCfnDemoRepo/
├── cfn_templates.yml # CloudFormation テンプレート生成などのためのPlaybook
├── group_vars
│ ├── env_dev.yml # 環境がprod, dev, ...などとあると想定し、環境依存する変数を定義するファイル
│ ├── env_prod.yml
│ └── system_demo.yml # 各環境共通の変数を定義するファイル
├── inventory
│ ├── dev # 環境ごとにインベントリをディレクトリで分ける
│ │ ├── hosts_aws_ec2.yml # EC2用の動的インベントリ
│ │ └── hosts_static.yml # 静的インベントリ
│ └── prod
│ ├── hosts_aws_ec2.yml
│ └── hosts_static.yml
└── roles
└── cfn_templates
├── tasks
│ └── main.yml
└── templates
├── CfnS3.yml # CloudFormationテンプレートをアップロードする用のS3を作成する用のテンプレート
└── SystemResource.yml # 管理対象環境のリソースを定義したテンプレート
今回はDemoシステムのdev環境を構築するという体で、それに必要なファイルを準備する。
all:
children:
aws_ec2: # 動的インベントリのホストで使用する変数をここで定義
vars:
ansible_ssh_common_args: -o StrictHostKeyChecking=no -o ProxyCommand="sh -c \"aws ssm start-session --target %h --document-name AWS-StartSSHSession --parameters 'portNumber=%p'\""
ansible_user: ec2-user
ansible_ssh_private_key_file: '~/.ssh/demo-{{ env_stage }}.key'
ansible_become: true
system_demo:
children:
env_dev:
children:
ansible_server: # このインベントリ使用時はenv_devのgroup_varsを参照する
hosts:
localhost:
ansible_connection: local
vars:
ansible_become: false
plugin: aws_ec2
regions:
- ap-northeast-1
filters:
# 起動しているEC2インスタンスのみを対象とする
instance-state-name: running
# 対象のシステム、環境を特定する
tag:environment:
- 'dev'
tag:system-code:
- 'demo'
exclude_filters:
- tag:Name:
- 'aws-cloud9-*' # Cloud9のインスタンスは除外する
keyed_groups:
- key: tags.ServerType
separator: ""
parent_group: "env_dev"
hostnames:
- tag:Name
compose:
# ansible_host: private_ip_address
ansible_host: instance_id # SSM Session Managerでホストに接続するためEC2のインスタンスIDをansible_hostに
system_code: demo
cfn_templates_path: "~/environment/cfn" # envirmentディレクトリはCloud9 IDEに合わせた設定
stack_name_dict:
cfn_s3: "{{ system_code }}-{{ env_stage }}-cfn-s3"
system_resource: "{{ system_code }}-{{ env_stage }}-system-resource"
env_stage: dev
main_vpc:
cidr: 10.0.0.0/23
subnet_dict:
PrivateMain:
- { az_num: 1, cidr: 10.0.0.0/25 }
- { az_num: 2, cidr: 10.0.0.128/25 }
interface_endpoint_list: # Systems Managerに必要なエンドポイント
- ec2messages
- ssm
- ssmmessages
demo_sv:
hostname: DEMOSV01
type: demo_servers
subnet: PrivateMainAz1Subnet
instance_type: t2.micro
image_id: ami-04fdeff70b7359022 # 東京リージョンのRHEL-9.2.0_HVM-20230905-x86_64-38-Hourly2-GP2
---
- name: CloudFormationテンプレート置き場作成
file:
state: directory
path: "{{ cfn_templates_path }}"
- name: CloudFormation用S3 CFnテンプレート生成
template:
src: CfnS3.yml
dest: "{{ cfn_templates_path }}/{{ stack_name_dict.cfn_s3 }}.yml"
validate: '/usr/local/bin/aws cloudformation validate-template --template-body=file://%s'
- name: 管理対象環境CFnテンプレート生成
template:
src: SystemResource.yml
dest: "{{ cfn_templates_path }}/{{ stack_name_dict.system_resource }}.yml"
#jinja2: lstrip_blocks: "True", trim_blocks: "True"
---
AWSTemplateFormatVersion: 2010-09-09
Resources:
S3Bucket:
Type: AWS::S3::Bucket
Properties:
BucketName: !Sub '${AWS::StackName}-${AWS::Region}'
AccessControl: Private
PublicAccessBlockConfiguration:
BlockPublicAcls: true
BlockPublicPolicy: true
IgnorePublicAcls: true
RestrictPublicBuckets: true
VersioningConfiguration:
Status: Suspended
BucketEncryption:
ServerSideEncryptionConfiguration:
- ServerSideEncryptionByDefault:
SSEAlgorithm: AES256
LifecycleConfiguration:
Rules:
- Id: CFn-Templates-Expiration-Rule
Status: Enabled
ExpirationInDays: 30
- Id: Delete-NonCurrentVersion-Rule
Status: Enabled
NoncurrentVersionExpiration:
NoncurrentDays: 7
ExpiredObjectDeleteMarker: true
- Id: AbortIncompleteMultipartUpload
Status: Enabled
AbortIncompleteMultipartUpload:
DaysAfterInitiation: 7
#jinja2: lstrip_blocks: "True", trim_blocks: "True"
---
AWSTemplateFormatVersion: 2010-09-09
Resources:
# VPC
Vpc:
Type: AWS::EC2::VPC
Properties:
CidrBlock: {{ main_vpc.cidr }}
EnableDnsSupport: true
EnableDnsHostnames: true
Tags:
- Key: Name
Value: !Sub ${AWS::StackName}-vpc
# Subnets
{% for subnet_list in main_vpc.subnet_dict | dict2items | sort(attribute='key') %}
{% for subnet in subnet_list.value %}
{{ subnet_list.key }}Az{{ subnet.az_num }}Subnet:
Type: AWS::EC2::Subnet
Properties:
AvailabilityZoneId: !FindInMap
- RegionAzIdMap
- !Ref 'AWS::Region'
- az{{ subnet.az_num }}
VpcId: !Ref Vpc
CidrBlock: {{ subnet.cidr }}
MapPublicIpOnLaunch: {% if subnet_list.key.startswith('Public') %}true{% else %}false{% endif +%}
Tags:
- Key: Name
Value: !Sub ${AWS::StackName}-{{ subnet_list.key | lower }}-az{{ subnet.az_num }}-subnet
{{ subnet_list.key }}RouteTableAssociationAz{{ subnet.az_num }}Subnet:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref {{ subnet_list.key }}Az{{ subnet.az_num }}Subnet
RouteTableId: !Ref {{ subnet_list.key }}RouteTable
{% endfor %}
{% endfor %}
# Route Table
{% for subnet_name in main_vpc.subnet_dict.keys() | sort %}
{{ subnet_name }}RouteTable:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref Vpc
Tags:
- Key: Name
Value: !Sub ${AWS::StackName}-{{ subnet_name | lower }}-rtb
{% endfor %}
# IAM
LinuxSvIamRole:
Type: AWS::IAM::Role
Properties:
RoleName: !Sub '${AWS::StackName}-LinuxSvRole'
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
-
Effect: "Allow"
Principal:
Service: "ec2.amazonaws.com"
Action:
- "sts:AssumeRole"
Path: "/"
ManagedPolicyArns:
- arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore
LinuxSvIamInstanceProfile:
Type: AWS::IAM::InstanceProfile
Properties:
InstanceProfileName: !Sub '${AWS::StackName}-LinuxSvInstanceProfile'
Path: "/"
Roles:
- !Ref LinuxSvIamRole
# Security Group
LinuxSvSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: Web Server Security Group
GroupName: !Sub ${AWS::StackName}-web-sv-sg
VpcId: !Ref Vpc
SecurityGroupIngress:
- IpProtocol: icmp
FromPort: -1
ToPort: -1
CidrIp: 0.0.0.0/0
Tags:
- Key: Name
Value: !Sub ${AWS::StackName}-linux-sv-sg
AwsPrivateLinkSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: AWS service private link security group
GroupName: !Sub ${AWS::StackName}-private-link-sg
VpcId: !Ref Vpc
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 443
ToPort: 443
CidrIp: {{ main_vpc.cidr }}
Description: allow use private link
Tags:
- Key: Name
Value: !Sub ${AWS::StackName}-aws-private-link-sg
# KeyPair
KeyPair:
Type: AWS::EC2::KeyPair
Properties:
KeyName: !Sub ${AWS::StackName}-keypair
# EC2 Instance
DemoSvInstance:
DependsOn: S3Endpoint # SSMエージェントダウンロード時にS3にアクセスするため
Type: AWS::EC2::Instance
Properties:
InstanceType: {{ demo_sv.instance_type }}
ImageId: {{ demo_sv.image_id }}
SubnetId: !Ref {{ demo_sv.subnet }}
SecurityGroupIds:
- !Ref LinuxSvSecurityGroup
IamInstanceProfile: !Ref LinuxSvIamInstanceProfile
KeyName: !Ref KeyPair
UserData: # RHELのAMIにはSSMエージェントが入っていないので、インスタンス作成時にインストールする
Fn::Base64: !Sub |
#!/bin/bash
dnf --disablerepo=* -y install https://s3.${AWS::Region}.amazonaws.com/amazon-ssm-${AWS::Region}/latest/linux_amd64/amazon-ssm-agent.rpm
Tags:
- Key: Name
Value: {{ demo_sv.hostname }}
- Key: ServerType
Value: {{ demo_sv.type }}
# VPC Endpoint
S3Endpoint:
Type: AWS::EC2::VPCEndpoint
Properties:
PolicyDocument:
Version: "2012-10-17"
Statement:
-
Effect: "Allow"
Principal: '*'
Action: 's3:ListAllMyBuckets'
Resource: 'arn:aws:s3:::*'
-
Effect: "Allow"
Principal: '*'
Action: '*'
Resource:
- !Sub "arn:aws:s3:::amazon-ssm-${AWS::Region}/*" # for SSM Agent RPM Download
RouteTableIds:
{% for subnet_name in main_vpc.subnet_dict.keys() | sort %}
- !Ref {{ subnet_name }}RouteTable
{% endfor %}
ServiceName: !Sub 'com.amazonaws.${AWS::Region}.s3'
VpcId: !Ref Vpc
{% for name in interface_endpoint_list %}
{{ name | capitalize }}PrivateLink:
Type: AWS::EC2::VPCEndpoint
Properties:
PrivateDnsEnabled: true
ServiceName: !Sub 'com.amazonaws.${AWS::Region}.{{ name }}'
VpcEndpointType: Interface
VpcId: !Ref Vpc
SubnetIds:
- !Ref PrivateMainAz1Subnet # デモのためシングルAZ
SecurityGroupIds:
- !Ref AwsPrivateLinkSecurityGroup
{% endfor %}
# --------------------------------------------------------------
Mappings:
RegionAzIdMap:
ap-northeast-1:
az1: apne1-az4
az2: apne1-az2
az3: apne1-az1
# --------------------------------------------------------------
- hosts: [ansible_server]
roles:
- cfn_templates
ファイルをこれだけそろえて、cfn_templates.ymlをプレイブックとして実行すると、CloudFormationテンプレートアップロード用のS3、管理対象環境を作成するためのCloudFormationテンプレートが作成される。
cd ~/environment/AnsibleCfnDemoRepo
ansible-playbook cfn_templates.yml -vD -i inventory/dev/
作成されたテンプレートをCloudFormationスタックとして作成すると、管理対象環境が構築される。
aws cloudformation deploy --stack-name demo-dev-cfn-s3 \
--template-file ~/environment/cfn/demo-dev-cfn-s3.yml
aws cloudformation deploy --stack-name demo-dev-system-resource \
--template-file ~/environment/cfn/demo-dev-system-resource.yml \
--s3-bucket demo-dev-cfn-s3-ap-northeast-1 \
--s3-prefix cfn-templates \
--tags system-code=demo environment=dev \
--capabilities CAPABILITY_NAMED_IAM
4. 構築した環境の動作確認
この環境はプライベートネットワークに素のRHEL9のサーバーを建てただけなのでサービスとしては何も動いていないが、AnsibleでLinuxサーバーを管理できる状態であることが期待値なので、AnsibleでLinuxサーバーに何かタスクを実行させられることを確認する。
まず、動的インベントリの確認をするため、Linuxサーバーが起動した状態でansible-inventoryコマンドを実行して結果を確認する。
$ ansible-inventory -i inventory/dev/ --graph
@all:
|--@aws_ec2:
| |--DEMOSV01
|--@system_demo:
| |--@env_dev:
| | |--@ansible_server:
| | | |--localhost
| | |--@demo_servers:
| | | |--DEMOSV01
|--@ungrouped:
インベントリは期待通りの状態だったので、次は動作確認用のプレイブックを用意して実行する。
- hosts: [ansible_server]
tasks:
- name: EC2接続用キーファイル存在確認
stat:
path: ~/.ssh/{{ system_code }}-{{ env_stage }}.key
register: keyfile
- name: EC2接続用キー取得および保存
when: not keyfile.stat.exists
shell: aws ssm get-parameter --name /ec2/keypair/$(aws ec2 describe-key-pairs --filters Name=key-name,Values={{ stack_name_dict.system_resource }}-keypair --query KeyPairs[*].KeyPairId --output text) --with-decryption --query Parameter.Value --output text > ~/.ssh/{{ system_code }}-{{ env_stage }}.key
check_mode: no
- name: EC2接続用キーファイルパーミッション変更
file:
path: ~/.ssh/{{ system_code }}-{{ env_stage }}.key
mode: "0400"
- hosts: [demo_servers]
tasks:
- name: hellow world
debug:
msg: "Hello World!"
「EC2接続用キー」は、EC2インスタンス作成時に指定したキーペアの秘密鍵のこと。キーペアはSystemResource.ymlのスタックで作成しており、CloudFormationで作成したキーペアの情報はパラメータストアに保存されるので、パラメータストアから取得してCloud9上に保存し、AnsibleからのSSH接続時に使用する。
動作確認のためプレイブックを実行する。
$ ansible-playbook demo_servers.yml -vD -i inventory/dev/
No config file found; using defaults
(中略)
PLAY [demo_servers] *******************************************************************************************************************************************************************************************************************
TASK [Gathering Facts] ****************************************************************************************************************************************************************************************************************
fatal: [DEMOSV01]: UNREACHABLE! => {"changed": false, "msg": "Failed to connect to the host via ssh: \nAn error occurred (403) when calling the StartSession operation: Server authentication failed: <UnauthorizedRequest><message>Forbidden.</message></UnauthorizedRequest>\n\nssh_exchange_identification: Connection closed by remote host", "unreachable": true}
PLAY RECAP ****************************************************************************************************************************************************************************************************************************
DEMOSV01 : ok=0 changed=0 unreachable=1 failed=0 skipped=0 rescued=0 ignored=0
localhost : ok=4 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
…うまくいかない。
セッションマネージャー経由のSSHが失敗しているようなので切り分けをすると、以下の通りだった。
- AWSコンソールのEC2画面から[接続] > [セッションマネージャー]での接続は成功
- AWSコンソールのCloudShellから
aws ssm start-session --target <LinuxサーバーのインスタンスID>
を実行すると、接続は成功 - Cloud9のbashターミナルから
aws ssm start-session --target <LinuxサーバーのインスタンスID>
を実行すると失敗
$ aws ssm start-session --target i-xxxxxxxxxxxxx
An error occurred (403) when calling the StartSession operation: Server authentication failed: <UnauthorizedRequest><message>Forbidden.</message></UnauthorizedRequest>
原因がよくわからなかったので結局サポートに問い合わせた結果、以下のことが分かった。
- Cloud9環境でAWSマネージド一時認証情報を使ってAWS CLIを実行する(デフォルトの動作)場合、セッションマネージャーは動作しないという制約がある
- アクセスキーを使用した認証情報をaws configureで設定して使用するか、インスタンスプロファイルの権限を使用するかでこの制約を回避できる
- アクセスキーやインスタンスプロファイルを使うためにはCloud9のAWS Settings設定でAWSマネージド一時認証情報を無効化する必要がある
今回はインスタンスプロファイルを使う方式を採用することにした。
(アクセスキーを作成しようとするとAWSコンソールが頑張って代替案を推してくるので…)
インスタンスプロファイルを作成するCloudFormationテンプレートを準備する。
---
AWSTemplateFormatVersion: 2010-09-09
Resources:
InfraCloud9IamRole:
Type: AWS::IAM::Role
Properties:
RoleName: !Sub '${AWS::StackName}-InfraCloud9Role'
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
-
Effect: "Allow"
Principal:
Service:
- "ec2.amazonaws.com"
- "cloud9.amazonaws.com"
Action:
- "sts:AssumeRole"
Path: "/"
ManagedPolicyArns:
- arn:aws:iam::aws:policy/AWSCloud9SSMInstanceProfile
- !Ref InfraAnsibleOperationIamPolicy
InfraAnsibleOperationIamPolicy:
Type: AWS::IAM::ManagedPolicy
Properties:
ManagedPolicyName: !Sub '${AWS::StackName}-InfraAnsibleOperation'
PolicyDocument:
Version: "2012-10-17"
Statement:
- # デモなのでAdministratorAccess相当の権限つけているが、実際にはAnsibleでのインフラ管理に必要な権限を定義するべし
Effect: Allow
Action: "*"
Resource: "*"
InfraCloud9InstanceProfile:
Type: AWS::IAM::InstanceProfile
Properties:
InstanceProfileName: !Sub '${AWS::StackName}-InfraCloud9InstanceProfile'
Path: "/"
Roles:
- !Ref InfraCloud9IamRole
このテンプレートでCloudFormationスタックを作成する。
aws cloudformation deploy --stack-name demo-dev-infra-c9-iam \
--template-file demo-dev-infra-c9-iam.yml \
--s3-bucket demo-dev-cfn-s3-ap-northeast-1 \
--s3-prefix cfn-templates \
--tags system-code=demo environment=dev \
--capabilities CAPABILITY_NAMED_IAM
作成に成功したら、インスタンスプロファイルをCloud9環境にアタッチする。
my_cloud9_instance_id=$(aws ec2 describe-instances --filters Name=instance-state-name,Values=running --query 'Reservations[*].Instances[?contains(to_string(Tags),`aws-cloud9-mgmt-c9-MyCloud9-`)].InstanceId' --output text)
instance_profile_assoc_id=$(aws ec2 describe-iam-instance-profile-associations --filters Name=instance-id,Values=${my_cloud9_instance_id} --query 'IamInstanceProfileAssociations[*].AssociationId' --output text)
aws ec2 replace-iam-instance-profile-association --association-id ${instance_profile_assoc_id} --iam-instance-profile Name=demo-dev-infra-c9-iam-InfraCloud9InstanceProfile
インスタンスプロファイルを使うようにするため、Cloud9のAWSマネージド一時認証情報を無効化する。
GUIからの操作で、Cloud9 IDEの右上の歯車を押してPreferencesタブを開いて、AWS Settings > Credentials > AWS managed temporary credentials: をオフにする。
Cloud9のAWSマネージド一時認証情報を無効化するとAWS CLIのデフォルトリージョンが未設定になるので、東京リージョンに設定する。
aws configure set region ap-northeast-1
AWS CLIがインスタンスプロファイルの権限で実行されるようになっていることを確認する。
$ aws sts get-caller-identity
{
"UserId": "<ランダムな文字列>:<EC2インスタンスID>",
"Account": "<AWSアカウントID>",
"Arn": "arn:aws:sts::<AWSアカウントID>:assumed-role/demo-dev-infra-c9-iam-InfraCloud9Role/<EC2インスタンスID>"
}
Arnがassume-role/<アタッチし直したインスタンスプロファイルのロール>になっているのでOK。
再度、動作確認用のプレイブックを実行する。
$ ansible-playbook demo_servers.yml -vD -i inventory/dev/
No config file found; using defaults
Using inventory plugin 'ansible_collections.amazon.aws.plugins.inventory.aws_ec2' to process inventory source '/home/ec2-user/environment/AnsibleCfnDemoRepo/inventory/dev/hosts_aws_ec2.yml'
(中略)
PLAY [demo_servers] *******************************************************************************************************************************************************************************************************************
TASK [Gathering Facts] ****************************************************************************************************************************************************************************************************************
ok: [DEMOSV01]
TASK [hellow world] *******************************************************************************************************************************************************************************************************************
ok: [DEMOSV01] => {
"msg": "Hello World!"
}
PLAY RECAP ****************************************************************************************************************************************************************************************************************************
DEMOSV01 : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
localhost : ok=3 changed=0 unreachable=0 failed=0 skipped=1 rescued=0 ignored=0
うまくいった。