terraformコマンドメモ

f:id:hige_dev:20210411174310p:plain

目次

確認したversion

$ terraform -v
Terraform v0.14.9
+ provider registry.terraform.io/hashicorp/aws v3.34.0

$ aws --version
aws-cli/2.1.32 Python/3.8.8 Linux/4.15.0-140-generic exe/x86_64.ubuntu.18 prompt/off

概要

AWSにある既存リソースをTerraformで管理する。 新規リソース作成時もTerraformを使うことで、状態の把握が用意になったり、複数人でのリソース管理で設定漏れが起きにくくなったりする。

事前手順

以下の方法などを利用して、MFA認証してaws cliを実行できるようにしておく

hige-dev.hatenablog.com

運用手順

基本的に使うコマンドは以下の5つ。

# 最初に実行するコマンド。main.tfを変更時にも必要になる 
$ terraform init

# 既存のリソースをterraformで管理するためのコマンド
# リソースデータを取得して、各.tfファイルに記載することで長くなるので割愛
# terraformingという便利ツールも併用するといくらか楽
$ terraform import

# 追加、変更した*.tfファイルと、現状との差分を表示
# diffが出た場合、そのように変更されてしまうため、【必ず】確認すること
# (意図しない変更が起きる可能性がある)
$ terraform plan

# 変更した*.tfファイルが反映される
# また、それに合わせてS3で管理してるterraform.tfstateも更新される
$ terraform apply

# tfファイルを「=」の位置で揃えるコマンド。(必須ではないが、やっておくと整形コミットが作られにくそう)
$ terraform fmt

管理するリソースは、terraform state listにあるもののみ。

新規リソースを追加する場合

.tfファイルに追記しplan => apply

既存リソースを編集する場合

terraformで管理してるリソースの場合

.tfファイルを編集してplan => apply

terraformで管理していないリソースの場合

importして.tfファイルを編集し、plan => apply

既存リソースを削除する場合

terraformで管理してるリソースの場合

該当の.tfファイルから削除しplan => apply

※ terraformで管理しているリソースを、コンソールなどから手動で削除した場合、apply実行時に再度作られてしまうため、上記の対応が必要

terraformで管理していないリソースの場合

手動で削除

terraform管理対象から外す場合

terraform state rm XXXでリソース名を指定(リソース名はterraform state listのもの)

ファイルについて

ファイル名 概要
terraform.tfstate リソースの状態を管理するファイル。main.tf内に記載のS3バケットで管理
*.tf リソースファイル。EC2、LBなど種類ごとに分けると管理しやすい

オンプレからAWSへの移行作業を終えて

f:id:hige_dev:20210313092900p:plain 先日、自社サービスをオンプレからAWSへの移行を完了した。費用・工数などコストの見積もりから、実作業まで全てに関わることができた。インフラ歴半年なのに、とても良い経験をさせてくれた弊社に感謝(人手不足、というのもある)。

万全で望んだつもりだったけど、当日は色々と準備不足が露呈してしまったため、反省しきり。またそんな中でも土壇場でよしなにやってくれた先輩方に頭が上がらない。問題解決力がすごいなあ。問題を切り分けて、一つずつ解決する、を意識する。

やったこと

対応内容 難易度
見積もり(費用・工数
移行後の構成図作成
工数表作成
進捗管理
サーバー構築
移行手順書作成
varnishの動き把握
internalALB作成
Route53で向き先変更

12月に、他の作業しながら見積もりや構成図作成など下準備をして承認を得た。1月から先日の2.5ヶ月ぐらいでサーバー構築や向き先変更などの修正。

見積もり(費用・工数

費用見積もりは結構楽しかった。今使ってるサーバーのcpu, memoryを調べて、それをEC2とかマネージドサービスに置き換える作業。オンプレ側は余裕を持ったサイズ感で、今回は割とそれに沿ったサイズのEC2を選択してるので、のちのちサイズダウンで費用削減して、目に見える成果として評価に繋げられそう。

AWSの方に話を聞くと、経験則でサーバーなどの費用と転送料金の割合は5:1(だったかな?)ぐらい、とのことだったのが学び。今後の見積もりにも使えそう。(その割に「だったかな?」とか書いてるけど)

工数見積もりは、本当に難しい。割と余裕を持ったスケジュール感で提出したけど、結局1週間後ろ倒しして、さらに直前にも延期しそうな事態になり、ユーザーにも告知していたのでとてもアワアワした。(なんとか2回目の延期は避けられたけど)

工数見積もりの精度を高めるには、経験を積むことが大事だけど、30超えてからのスタートである自分はそんな余裕はないので、今の作業にどれぐらい時間がかかってるのか、を常に意識しなくては、という気持ちが強まった。昔Study Plusとかで勉強時間を記録してたけど、あーゆーのが必要かなあ。togglとかやってみるか。

進捗管理

移行作業に参加してくれたのは自分の他に3人で、うち二人は10年以上経験のある人たちで、恐縮しっぱなしの中、進捗管理をした。理解が曖昧な点はバッチリ突っ込まれるし、理解してるつもりだったところも他の解決法を提案してくれたり、すごくプレッシャーだったけど勉強になった。

特に、インフラ専門でやってきた方には、「そんな細かい事必要?」と思うようなこともたくさん突っ込まれた。が、振り返ってみると、そのぐらいの粒度を意識としては持ってた方が良いな、ということは理解できた。スピード感との兼ね合いもあるので、全てをじっくり丁寧に、というわけにはいかないのだけれども(スピードが遅い自分なんかは特に)

varnishの動き把握

これは、プロダクトの肝みたいなところがあって、とても難しかった。varnishはC言語で書かれてる、とのことだけど、コードを雰囲気で読むこと自体はできたし、処理の流れをドキュメントにまとめたりもした。が、ロードバランサー的な働きをしたり、キャッシュを管理してたり、メンテナンスページを表示させたり、いろんな働きをさせてたので、影響が出ないように移行するのが大変だった。

また、varnishの前にnginxがいて、nginxについての理解が追いついてないまま移行してしまったので、ちゃんと勉強する。バーチャルホストとか、ssl terminationとか、アップストリームとか、socketとかheaderとか。昔読んだときはいまいちイメージできなかったけど、今回の経験で理解できそうな気がする。

サーバー構築

stagingとproduction合わせて35台ぐらいサーバーがあったけど、サーバー構築はchefで実行したため、そこまで大変でもなかった。chefは、recipeとかcookbookとかknifeとか、名前がシャレててとても良い。ツールとしては今更感のあるツールなのかもだけれども。

DBサーバーは、RDSに移行したかったけど、MySQLが5.6なので、いったんEC2にそのまま移行して、5.7に上げてからRDSに切り替えることに。DBについては全然わからん。。(ここは先輩に任せた)

internalALBを作成

元々各サーバーには内部にhaproxyがあり、そちらを利用して転送先を振り分けてたけど、それをinternalALBに置き換えよう、ということで並行して進めた。internalALBのxxxxポートに送るとメールサーバーに、yyyyポートに送るとvarnishサーバーに、のような感じ。

ただ、元々haproxyとhostsで運用してたのをinternalALBに置き換えたことで不具合が生じた。haproxyに戻すことも検討したが、internalNLBに変更することで事なきを得た。

原因は、internalALBを固定IPだと誤認してたこと。/etc/hostsで特定のホスト名はinternalALBのIPに行くようにしてたけど、ALBはIPが不定のため、通じなくなった。その点、NLBはElasticIPが割り当てられるためIPが固定で、弊社の運用方法でも問題なかった。(が、hosts運用はあまりスマートではなさそうなので、他の方法も検討したい)

これは、色々勉強になった。

ALBを使用してのメンテナンス画面切替

AWS側のメンテナンス画面は、外からのアクセスを受けるexternalALBを作って、リスナールールを使って切り替えた。

  1. メンテ作業者用のルール
  2. メンテ画面表示用のルール
  3. 一般ユーザー用のルール

を用意して、「メンテ画面表示用のルール」を2番目に置く事で、メンテ作業者はサイトを表示できて、一般ユーザーはメンテ画面が表示される、的な。(「メンテ画面表示用のルール」を一番下に持ってくると、一般ユーザーもサイトが表示される) オンプレ側でやってた、メンテナンス用vcl(varnishの設定ファイル)をデプロイして〜みたいなのより分かりやすくてスマート。

Route53での向き先変更

元々Route53を使ってたから、向き先を替えるだけで良かったので、全体的にはそこまで難しくもなかった。が、DNSは、理解がヨワヨワなこともあって、レコードの種類など当日に慌てる結果となってしまった。あと、Route53からS3に向けるレコードで、SSL化が必要、とかでうまく切り替わらず、先輩方に対応していただいたが、同時に別の対応をしていたため理解できてない。ので、来週教えてもらおう。

反省点

移行手順書の作成

当日慌てないように実際に叩くコマンドも記載したため、1万文字ぐらいになった。前週にはメンテ参加者で読み合わせをしてたのだけど、結局慌てた。。記述順番のミス(DB移行前にrails consoleでレコードを操作する手順を記載)は、よく考えれば自分のレベルでもわかるはずなのに、経験不足を言い訳にできないミス。作業直前で気づけたけど、こういうのはなくさなきゃダメだな。

あと、読み合わせ後にも何度か手順書を見直して添削してたのだけど、メンテ前に作業できる部分を対応して削除したら、その記述のタイミングでDB作業に入ろうとしていた方に迷惑をかけてしまった。DBに全く関係ない記述だったから大丈夫かと思ったけど、変更点は共有すべきだった。が、変更のたびに共有してもノイズになってしまうので、頻度が難しい。(そもそも、読み合わせ前には添削不要なレベルまで完成させておくべきだった)

メンテ切り替え

メンテナンスの切り替えは、盛大に失敗した。

  • オンプレ側はvarnishのメンテナンス用設定ファイルをデプロイしてメンテナンス画面に切り替え
  • AWS側はメンテナンスサーバーを用意しておき、ALBのルールで順番を移動してメンテナンス画面に切り替え
  • 移行準備が出来次第、Route53で切り替える

という流れだったのだけれども、オンプレ側で用意していたメンテナンス用設定ファイルを勘違いしていて、うまく表示できなかった。無茶苦茶焦った。事前にAWS側ではメンテナンス画面が表示されることを確認してたけど、オンプレ側はデプロイ手順が若干複雑で、上司からも「そこまでしなくても大丈夫でしょ」的な雰囲気だったので確認してなかった。

いったんメンテナンス解除したりでアワアワしたけど、先にRoute53でAWS切り替えたことでなんとかメンテナンス表示された。多分2~3分だけど、出だしだったので焦った。。手間でもきちんと確認しなきゃいかん。

感想

調査・コスト試算に半月ぐらい、移行は2.5ヶ月。全当初の見積もりよりも1週間ぐらい後ろ倒しでの移行完了。体的に失敗は粒度が荒いことに起因するので、全体を俯瞰しつつも細かい点にも気を配る意識を持とう、と思った。

アプリケーション側の理解を深めたくてインフラの勉強を始めたけど、インフラも楽しいなあ。アプリケーション側の理解も深まってる(はず)。もう少しどっちつかずの状態で両方学びたい。

AWS SessionManagerで、踏み台なしのssh環境を構築する

f:id:hige_dev:20210306174802p:plain AWS SessionManagerを使うことで、踏み台サーバーが不要になるので便利!というお話。

導入手順の大きな流れとしては、

  1. EC2にSSM agentをインストール(Amazon Linuxubuntuは基本的にインストール済み)
  2. EC2にSSM用のSecurityGroupを作ってアタッチ
  3. SSM用にVPCエンドポイントを作成する
  4. AWS CLIのSession Manager プラグインをローカルにインストール

sshするのはAWSコンソール上からのみ、という場合は4は不要だけど、普通はローカルからsshしたいと思うのでそちらも記載。

概要

EC2をPublicSubnetに置くと、インターネットからアクセスされる危険性が高まるため、 f:id:hige_dev:20210306173524p:plain

基本的にEC2はPrivateSubnetに配置するのが望ましい。 が、そのままではEC2にログインもできなくなってしまうため、 f:id:hige_dev:20210306173539p:plain

PublicSubnetに踏み台サーバーを置くのが定石。 f:id:hige_dev:20210306173618p:plain

こうすることで、Privateに置いたEC2へは、踏み台以外のアクセスを拒否できるため、比較的安全な構成を構築できる。

が、踏み台サーバーそのもののセキュリティが甘ければ意味はなく、運用・保守などサーバーの維持コストもかかってしまう。

AWS Systems ManagerのSessionManagerを使うと(ややこしい)、踏み台サーバーなしでPrivateSubnetにsshできるようになるため、

  1. 踏み台サーバーが不要のため運用・保守コストがかからない
  2. (踏み台サーバーを作って放置するよりは)セキュリティ的にも安心

というメリットがある。

ややこしい名前について整理すると、

name description
AWS Systems Manager マネージドサービス
SessionManager 機能

ということになるのかな、、?

SSMを導入する手順

を書こうと思ったのだけど、セキュリティ的な問題は一次情報を見るべきだし、自分の下手な説明でセキュリティホールを作ってしまうのはやばいので、公式ドキュメントを見た方が安全。

docs.aws.amazon.com

ただ、それだけだとこの記事に意味がなくなってしまうので、セキュリティを強化するtipsを2つほど紹介しておく。

  1. SSMでsshできるIP制限
  2. ssm-userのsudo権限を削除

1. SSMでsshできるIP制限

EC2へのsshするIPを制限する場合は、SecurityGroupで特定のIPにport22を許可するが、SSMを使ったsshでは、SecurityGroupでIP制限ができない。この場合は、EC2にアタッチするロールにポリシーを追加でアタッチする。

名前はお任せだけど、つけるとしたら AllowIPListForSSMPolicy とか?

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "ssm:StartSession",
                "ssmmessages:CreateDataChannel"
            ],
            "Resource": "*",
            "Condition": {
                "ForAnyValue:IpAddress": {
                    "aws:SourceIp": [
                        "xxx.xxx.xxx.xxx/32",
                        "xxx.xxx.xxx.xxx/24"
                    ]
                }
            }
        }
    ]
}

CIDRを指定することで、そのIPだけを許可するのか、そのネットワークに属するIP全てを許可するのかが指定できる。

2. ssm-userのsudo権限を削除

SSMを使用すると、デフォルトでssm-userが作られる。このユーザーは、パスワードなしでrootになれるクセモノで、EC2サーバー内でユーザーの権限管理をしていても、AWSコンソールからssm-userでログインできてしまう。

すると、sudo権限がないユーザーが、AWSコンソールからssm-userでsudoを振りかざし、 sudo rm -rf /のような地獄のコマンドを打つことすらできてしまう

そうならないためにも、このssm-userは基本的に削除するべき。

対応すべき内容は、

  • ユーザーの削除
  • sudoersファイルの削除

あたりかな。

ユーザーの削除は、userdel コマンドで /home/ssm-userディレクトリも削除できる。

また、ユーザーを削除すれば問題はなさそうだけど、不要なファイルは削除するに越したことはないので、ssm-user用に作られるsudoersファイルも削除する。

$ sudo userdel ssm-user
$ sudo rm /etc/suroers.d/ssm-user

まだやるべきことあるかな、、あったら教えてください。

サーバーを作るたびにssm-userを削除するのは面倒だし、対応漏れの可能性も大いにあるので、ssm-userを削除した上でAMIを作っておいて、そこからEC2を起動する運用にするといいかも。

結局運用任せなのでミス・漏れの可能性はあるのだけれど。。(ある程度の規模ならCloudFormationとかTerraformを使うべきだけど)

おまけ: スマートな ~/.ssh/config 運用

SSMはとても便利だけど、サーバーが増えるたびに ~/.ssh/config に追加してかなきゃいけない点はちょっとスマートじゃないな、と思ってた。

そんな事思ってたら、先輩がサラッとワンライナーにしてくれた。

# ~/.ssh/config

Host i-*
  User ...
  IdentityFile ~/.ssh/id_rsa
  ProxyCommand $(PROFILE={aws configureで設定したprofile}; EC2HOST=$(echo %h | cut -d. -f2); INSTANCEID=$(aws ec2 describe-instances --profile ${PROFILE} --filters "Name=tag:Name,Values=${EC2HOST}" --query "Reservations[*].Instances[*].[InstanceId]" --output text);echo aws ssm start-session --profile ${PROFILE} --target ${INSTANCEID} --document-name AWS-StartSSHSession --parameters 'portNumber=%p')

これを設定しておくことで、 サーバーが増えても~/.ssh/configに追加せずに $ ssh {サーバー名} だけでログインできるようになる。

注意点は、EC2のタグにサーバー名を設定しておく事と、$ aws configure --profile ○○ でprofileを設定しておく事。(defaultしか使わないなら不要だけど)

超スマートで感動した。これぞプログラミング

まとめ

踏み台サーバーではなくSession Manager使うと、すごくシンプルでセキュアにsshできて、かっこいい。

AWS EC2(ubuntu18.04)のファイルシステム拡張方法

f:id:hige_dev:20210306175511p:plain

何度かやってるけど忘れてしまうので備忘録。

ファイルシステム拡張の流れ

  1. EC2ダッシュボードから、EBSのvolumeを増やす
  2. EC2にsshして、lsblkなどでvolumeの確認
  3. growpartでパーティションを拡張
  4. resize2fsで、ファイルシステムを拡張

1. EC2コンソールで、EBSのvolumeを増やす

ここはすぐわかるので簡潔に。 ①EC2コンソールのElastic Block Store(EBS)のボリュームから、 ②対象のボリュームを選択して「アクション => ボリュームの変更」 して、任意のサイズに変更。 f:id:hige_dev:20210304063455p:plain 基本的にサイズアップのみ。サイズダウンする場合は、手間がかかるので割愛。(新しくインスタンスを立ち上げて、減らしたいEBSをそちらにアタッチして、新EC2に元からアタッチされてたEBSに内容をコピーする、的な流れ)

2. EC2にsshして、lsblkなどでvolumeの確認

1で8 => 16GB増やしてから、EC2にsshでログインして lsblk コマンドを叩いてみると、EBSがマウントされている /(ルート)が拡張されてないことがわかる

$ df -h
Filesystem      Size  Used Avail Use% Mounted on
...
/dev/nvme0n1p1  7.7G  2.6G  5.2G  34% /
...

$ lsblk
NAME        MAJ:MIN RM  SIZE RO TYPE MOUNTPOINT
...
nvme0n1     259:0    0   16G  0 disk
└─nvme0n1p1 259:1    0    8G  0 part /

1で増やしたdiskサイズに合わせて、part(パーティション)を拡張してあげる必要がある。

3. growpartでパーティションを拡張

growpartコマンドで、パーティションを拡張する。この時の引数は、 growpart [ディスク] [パーティション番号]なので、以下のように指定する。

$ $ sudo growpart /dev/nvme0n1 1
CHANGED: partition=1 start=2048 old: size=16775135 end=16777183 new: size=33552351,end=33554399

nvme0n1ディスクの、1番目のパーティション。nvme0n1p1の、最後のp1パーティション1という意味なので、二つ目の引数には1だけを指定する。sudo growpart /dev/nvme0n1 p1ではない。

4. resize2fsで、ファイルシステムを拡張

lsblkではパーティションが拡張されたが、この時点ではまだファイルシステムには認識されていない。

$ lsblk
NAME        MAJ:MIN RM  SIZE RO TYPE MOUNTPOINT
...
nvme0n1     259:0    0   16G  0 disk
└─nvme0n1p1 259:1    0   16G  0 part /

$ df -h
Filesystem      Size  Used Avail Use% Mounted on
...
/dev/nvme0n1p1  7.7G  2.6G  5.2G  34% /
...

そのため、拡張したパーティションを、ファイルシステムに認識させる必要がある。 そのためには、resize2fsコマンドを打つだけ。

$ sudo resize2fs /dev/nvme0n1p1
resize2fs 1.44.1 (24-Mar-2018)
Filesystem at /dev/nvme0n1p1 is mounted on /; on-line resizing required
old_desc_blocks = 1, new_desc_blocks = 2
The filesystem on /dev/nvme0n1p1 is now 4194043 (4k) blocks long.

なんか英語が出てびびるけど、dfしてみると8 =>16に拡張されていることがわかる。

$ df -h
Filesystem      Size  Used Avail Use% Mounted on
...
/dev/nvme0n1p1   16G  2.6G   13G  17% /
...

まとめ

同じような情報は腐る程あるけど、書いてみると理解が進むのでよし。

AWSのMFAで認証する自作shell関数を紐解く

概要

AWSでMFA認証をする時に役立つツールがあった。

github.com

が、credentialな情報だし、あまりよそのツールを使いたくないし、そこまですることないよね、ということで、shell関数にした(ヤツを職場のすごい人が綺麗にしてくれた)。ので、awkの勉強も兼ねて、分解してみる。

 

MFA有効化するshell関数

以下を実行することで、hoge で登録したAWSのアカウントでMFA認証できる。(cli操作ができるようになる)

$ aws_mfa hoge

bashの場合(~/.bash_profile)

function aws_mfa() {
  if [ $# != 1 ]; then
    echo "profileを指定してください"
    return
  fi
  unset AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY AWS_SESSION_TOKEN
  read -p "MFAトークン: " token
  eval `aws sts get-session-token --profile $1 \
          --serial-number $(aws sts get-caller-identity --profile $1 --query Arn --output text | sed 's/:user/:mfa/') \
          --token-code ${token} \
          --duration-seconds 129600 \
        | awk ' $1 == "\"AccessKeyId\":" { gsub(/\"/,""); gsub(/,/,""); print "export AWS_ACCESS_KEY_ID="$2 } $1 == "\"SecretAccessKey\":" { gsub(/\"/,""); gsub(/,/,""); print "export AWS_SECRET_ACCESS_KEY="$2} $1 == "\"SessionToken\":" { gsub(/\"/,""); gsub(/,/,""); print "export AWS_SESSION_TOKEN="$2 } '`
}

zshの場合(~/.zprofile)

function aws_mfa() {
  if [ $# != 1 ]; then
    echo "profileを指定してください"
    return
  fi
  unset AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY AWS_SESSION_TOKEN
  printf '%s ' 'MFAトークン: '
  read token
  eval `aws sts get-session-token --profile $1 \
          --serial-number $(aws sts get-caller-identity --profile $1 --query Arn --output text | sed 's/:user/:mfa/') \
          --token-code ${token} \
          --duration-seconds 129600 \
        | awk ' $1 == "\"AccessKeyId\":" { gsub(/\"/,""); gsub(/,/,""); print "export AWS_ACCESS_KEY_ID="$2 } $1 == "\"SecretAccessKey\":" { gsub(/\"/,""); gsub(/,/,""); print "export AWS_SECRET_ACCESS_KEY="$2} $1 == "\"SessionToken\":" { gsub(/\"/,""); gsub(/,/,""); print "export AWS_SESSION_TOKEN="$2 } '`
}

※上記を設定したのち $ source ~/.bash_profile する。

~/.aws/credentials で設定したprofileを引数にして実行するとMFAが認証される。

$ aws_mfa hoge

説明(bash

自分は最近zshを使いはじめたけど、bashの方が多そうなのでbashで説明。 大きく分けると、以下に分類できる。のでそれぞれ見ていく。

  1. if節
  2. unset
  3. read
  4. eval

1. if節

  if [ $# != 1 ]; then
    echo "profileを指定してください"
    return
  fi

これは、実行時に引数付けてないとエラーが出るようにしたいので書いてる。 defaultだけ使ってる(--profileを使用してない)場合はなくても良いかも。でも、複数のAWSアカウントやIAMユーザーを使い分ける場合は間違いを防ぐためにあった方がいい。と思う。

2. unset

unset AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY AWS_SESSION_TOKEN

4のeval実行時に環境変数がセットされてる状態だとエラーになるので、一度unsetする。

3. read

read -p "MFAトークン: " token

MFAで発行された一時トークンを標準入力で受け取り、変数tokenに格納。 4のstsコマンドのオプションに使用する。 (zshの場合は、ここがちょっと書き方が異なる)

4. eval

eval `aws sts get-session-token --profile $1 \
          --serial-number $(aws sts get-caller-identity --profile $1 --query Arn --output text | sed 's/:user/:mfa/') \
          --token-code ${token} \
          --duration-seconds 129600 \
        | awk ' $1 == "\"AccessKeyId\":" { gsub(/\"/,""); gsub(/,/,""); print "export AWS_ACCESS_KEY_ID="$2 } $1 == "\"SecretAccessKey\":" { gsub(/\"/,""); gsub(/,/,""); print "export AWS_SECRET_ACCESS_KEY="$2} $1 == "\"SessionToken\":" { gsub(/\"/,""); gsub(/,/,""); print "export AWS_SESSION_TOKEN="$2 } '`
}

地獄のようなワンライナーaws sts get-session-tokenで出力された値を、awkによる力技で環境変数にセットしている。

$1(列の1つめ)が AccessKeyIdなら、(gsubで"を整形してから)環境変数AWS_ACCESS_KEY_IDにセット、というのを繰り返してる。

感想

分解してみると「そんなことか」となるけど、まだ思いつけるレベルではないので、血肉にして使いこなしたい。大きいフワッとした問題点を、細かく分けて一個ずつ解決するスキルを身に付けたい in 2021。

AWS SessionManagerでIP制限をする

f:id:hige_dev:20210103120240p:plain

目次

概要

AWSのSessionManagerを利用することで、踏み台を経由せずサーバーにsshできる。 その際、SecurityGroupを使ったIP制限ができないため、IP制限するIAMポリシーをIAMユーザーにアタッチする必要がある。

また、普段AWS consoleにログインするIAMユーザーではなく、SessionManager専用のIAMユーザーを作ることで、credentialが漏洩してしまった際の被害を最小限に抑えられるため、そちらも対応する。

設定方法

1. IP制限するIAMポリシーを作成

IAMポリシーから、新しいポリシー(ex. MinimumPermissionForSSM )を作成し、以下をJSONに貼り付ける(ipは置換で)。おそらくSSM経由でsshするための最小権限。Condition以下がキモ。

aws:SourceIp に複数IPを登録したり、CIDRブロックを指定して同一ネットワークからのIPを許可、などができる。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "ssm:StartSession",
                "ssmmessages:CreateDataChannel"
            ],
            "Resource": "*",
            "Condition": {
                "IpAddress": {
                    "aws:SourceIp": [
                        "xxx.xxx.xxx.xxx/32",
                        "yyy.yyy.yyy.000/24"
                    ]
                }
            }
        }
    ]
}

参考: docs.aws.amazon.com

2. SessionManager専用のIAMユーザーが所属するグループを作成

IAMグループから新しいグループ(ex. ssm)を作成し、1で作成したIAMポリシーをグループにアタッチする。

3. SessionManager専用のIAMユーザーを作成

ユーザー詳細の設定

f:id:hige_dev:20210103110811p:plain

設定 説明
ユーザー名 「ssm_*」のようなprefixをつけておくと、SessionManager専用であることがわかりやすい(任意)
AWS アクセスの種類を選択 「プログラムによるアクセス」を選択
アクセス許可の設定

f:id:hige_dev:20210103115434p:plain 2で作成したグループに所属させる。

ユーザーに直接アタッチもできるが、グループにアタッチしておくことで、複数人への権限管理が容易になる。

CloudTrailでAWSリソースへの意図しない変更に気付けるようにする

f:id:hige_dev:20201231070520p:plain

概要

AWSリソースを意図せず変更してしまった時に気付けるようにする。 具体的には、EventBridgeでCloudTrailのログを検知し、SNS経由でslack通知する。

事前準備

■ CloudTrailの有効化

ドキュメントを参照

SNSを設定

1. SlackアプリでEmailを有効化

f:id:hige_dev:20201231065150p:plain これ。

slackに追加し、適宜入力する。(メールアドレスはSNSで使うのでコピーしておく) f:id:hige_dev:20201231065156p:plain

2. SNSトピックを作成
  1. SNSのトピックから、「トピックの作成」をクリック
  2. Emailを使うのでタイプはスタンダード、あとは適宜入力 f:id:hige_dev:20201231065200p:plain
3. サブスクリプションの作成
  1. 2で作成したSNSトピックのページから、「サブスクリプションの作成」をクリック
  2. [プロトコルに「Eメール」を選択し、エンドポイントに1でコピーしたEmailをペーストし、その他は適宜入力 f:id:hige_dev:20201231065205p:plain
4. 1で指定したslackチャンネルに確認メールが届くのでconfirmする

こういう感じの。 f:id:hige_dev:20201231065453p:plain

EventBridgeでCloudTrailのログを検知するよう設定

※ほぼCloudWatchEventsと同じだが、一部対応していないためEventBridgeを使用

  1. EventBridgeのルールから「ルールを作成」をクリック
  2. パターンを定義する。事前定義にやりたいことが見つからない場合はドキュメントから、利用したいサービスのイベントを参考にする。 f:id:hige_dev:20201231065231p:plain

[一例] 以下はECSのprod-*クラスターで、タスク実行を除く全ての変更を通知する定義。 正規表現は使えないが、prefixやanything-butなどのパターンマッチでフィルターできる。(おそらくこのパターンマッチがCloudWatch Eventsで使えない)

{
  "source": [
    "aws.ecs"
  ],
  "detail": {
    "eventSource": [
      "ecs.amazonaws.com"
    ],
    "eventName": [
      {
        "anything-but": "RunTask"
      }
    ],
    "requestParameters": {
      "cluster": [
        {
          "prefix": "prod-"
        }
      ]
    }
  }
}

[tips] 特定のユーザーの通知を一時的にオフしたい場合
本来すべきではないが、detailに以下を追加することで、一時的に特定のユーザーだけ通知をオフにできる(CloudTrailにログは記録されている)

"detail": {
  ~
    "userIdentity": {
      "userName": [
        {
          "anything-but": "hoge_user"
        }
      ]
    }
  ~
}

作業終了後は必ず上記を削除すること

3.ターゲットで、事前準備で作成したSNSトピックを選択し、入力の設定で「入力トランスフォーマー」を選択することで必要な情報だけを抽出できる

入力バス : CloudTrailから取得したイベントから、必要なデータを取り出す 入力テンプレート: 入力バスで取り出したデータを、templateに埋め込む

CloudTrailから取得したイベント

{"version":"0","id":"xxx-xxx-xxx-xxx-xxx","detail-type":"AWS API Call via CloudTrail","source":"aws.waf-regional","account":"ACCOUNT_ID","time":"2020-10-22T22:10:58Z","region":"ap-northeast-1","resources":[],"detail":{"eventVersion":"1.05","userIdentity":{"type":"IAMUser","principalId":"xxxxx","arn":"arn:aws:iam::ACCOUNT_ID:user/USER_NAME","accountId":"ACCOUNT_ID","accessKeyId":"xxxxx","userName":"xxxxxx","sessionContext":{"sessionIssuer":{},"webIdFederationData":{},"attributes":{"mfaAuthenticated":"true","creationDate":"2020-10-22T21:31:51Z"}}},"eventTime":"2020-10-22T22:10:58Z","eventSource":"http://waf-regional.amazonaws.com","eventName":"UpdateIPSet","awsRegion":"ap-northeast-1","sourceIPAddress":"xxx.xxx.xxx.xxx","userAgent":"http://console.amazonaws.com","requestParameters":{"updates":[{"iPSetDescriptor":{"type":"IPV4","value":"192.0.0.1/32"},"action":"DELETE"}],"changeToken":"xxx-xxx-xxx-xxx-xxx","iPSetId":"xxx-xxx-xxx-xxx-xxx"},"responseElements":{"changeToken":"xxx-xxx-xxx-xxx-xxx"},"requestID":"xxx-xxx-xxx-xxx-xxx","eventID":"xxx-xxx-xxx-xxx-xxx","eventType":"AwsApiCall","apiVersion":"2015-08-24"}}

入力バス (上記から必要なデータを取り出す。JSON整形ツールなどを使うとやりやすい)

{
  "eventName":"$.detail.eventName",
  "eventSource":"$.detail.eventSource",
  "eventTime":"$.detail.eventTime",
  "requestParameters":"$.detail.requestParameters",
  "source":"$.source","user":"$.detail.userIdentity.userName"
}

入力テンプレート (最初の行に概要があると通知が読みやすい)

"ALBが更新されました。"
"https://ap-northeast-1.console.aws.amazon.com/ec2/v2/home?region=ap-northeast-1#LoadBalancers:sort=loadBalancerName"
"source: <source>"
"user: <user>"
"eventTime: <eventTime>"
"eventSource: <eventSource>"
"eventNane: <eventName>"
"requestParameters: <requestParameters>"

通知 f:id:hige_dev:20201231065236p:plain

4.影響ない範囲で変更してみて、Slackに通知が飛ぶことを確認