のぴぴのメモ

自分用のLinuxとかの技術メモ

Red Hat Developer ProgramのサブスクリプションはRed Hat DeveloperからRHELイメージをダウンロードすると自動的に付与される件

Red Hat Developer Programのに参加すると、無料のRed Hat Enterprise Linuxサブスクリプションを取得することができます。RHELのを取得できるのも嬉しいですが、redhat
ナレッジベースを参照できるようになるのが嬉しいです。
ちなみに、この無償プログラムは2016/4から提供されています。
また2020/12には、16 システムまでのプロダクション環境での利用に拡張されています。
www.redhat.com

(1)Red Hat Developer Programに参加してサブスクリプションを取得する

こちらの記事にまとまっていたのでこちらは割愛します。
qiita.com

(2)サブスクリプションの更新

Red Hat Developer Programのサブスクリプション"Red Hat Developer Subscription"は、他のサブスクリプション同様有効期限が1年です。
有効期限切れた場合、Red Hat DeveloperからRHELイメージをダウンロードすると、自動的に新しいRed Hat Developer Subscriptionが付与されました。

更新

  • 2018/9/9 初版
  • 2021/9/19 Red Hat Developerの最新ページに合わせ更新

Lambda(python)で特定のロググループ & ログストリームにログ出力するサンプルコード

Lambdaで特定のロググループ & ログストリームにログ出力するコードのサンプルです。
この例では、eventの内容をLogsに出力しています。

コードは以下の記事の内容をベースに一部最適化してます。

import json
import time
import boto3

logGroupName = "security-alaert"
logStreamName = "development"
  
def lambda_handler(event, context):

    #Get Session
    client = boto3.client('logs')

    #Put Log Event
    put_logs(client, logGroupName, logStreamName, "Received event:{0}".format( json.dumps(event)))

    return {
        'statusCode': 200,
        'body': json.dumps('Hello from Lambda!')
    }


def put_logs(client, group_name, stream_name_prefix, message):
    try:
        #Set Logs Event Data
        log_event = {
            'timestamp': int(time.time()) * 1000,
            'message': message
        }
        
        #Set Flags
        exist_log_stream = True
        sequence_token = None
        while True:
            break_loop = False
            try:
                if exist_log_stream == False:
                    #Create LogGroup
                    try:
                        client.create_log_group(logGroupName=group_name)
                    except client.exceptions.ResourceAlreadyExistsException:
                        pass
                    #Create LogStream
                    client.create_log_stream(
                        logGroupName = group_name,
                        logStreamName = stream_name_prefix)
                    exist_log_stream = True
                    #Write First event log
                    client.put_log_events(
                        logGroupName = group_name,
                        logStreamName = stream_name_prefix,
                        logEvents = [log_event])
                    break_loop = True
                elif sequence_token is None:
                    client.put_log_events(
                        logGroupName = group_name,
                        logStreamName = stream_name_prefix,
                        logEvents = [log_event])
                else:
                    client.put_log_events(
                        logGroupName = group_name,
                        logStreamName = stream_name_prefix,
                        logEvents = [log_event],
                        sequenceToken = sequence_token)
                    break_loop = True
            except client.exceptions.ResourceNotFoundException as e:
                exist_log_stream = False
            except client.exceptions.DataAlreadyAcceptedException as e:
                sequence_token = e.response.get('expectedSequenceToken')
            except client.exceptions.InvalidSequenceTokenException as e:
                sequence_token = e.response.get('expectedSequenceToken')
            except Exception as e:
                print(e)
            
            if break_loop:
                break
    except Exception as e:
        print(e)

macOS zshのプロンプトにgitブランチをカラー表示してみる

やりたいこと

macOSzshのプロンプトにgitのブランチを表示するようにします。
表示はカラー表示させます。

今回の環境は以下の通りです。

  • ProductName: Mac OS X
  • ProductVersion: 10.15.7
  • BuildVersion: 19H1323

gitセットアップに関する他の関連記事はこちら。

セットアップ手順

モジュールのダウンロード

cd
mkdir .zsh
curl https://raw.githubusercontent.com/git/git/master/contrib/completion/git-completion.zsh -o ~/.zsh/git-completion.zsh
curl https://raw.githubusercontent.com/git/git/master/contrib/completion/git-prompt.sh -o ~/.zsh/git-prompt.sh

git-completion.zshgit-prompt.shの2つのファイルが空でない状態であることを確認します。

ls  -l .zsh
total 56
-rw-r--r--  1 XXXXXX  XXXX   7133  9 12 15:48 git-completion.zsh
-rw-r--r--  1 XXXXXX  XXXX  17781  9 12 15:48 git-prompt.sh

ログインシェルへの追加

ログインプロンプトに表示するための設定を~/.zshrcに追加します。

cat >> ~/.zshrc << EOL

# For git
fpath=(~/.zsh $fpath)
if [ -f ${HOME}/.zsh/git-completion.zsh ]; then
       zstyle ':completion:*:*:git:*' script ~/.zsh/git-completion.zsh
fi
if [ -f ${HOME}/.zsh/git-prompt.sh ]; then
       source ${HOME}/.zsh/git-prompt.sh
fi
GIT_PS1_SHOWDIRTYSTATE=true
GIT_PS1_SHOWUNTRACKEDFILES=true
GIT_PS1_SHOWSTASHSTATE=true
GIT_PS1_SHOWUPSTREAM=auto
setopt PROMPT_SUBST ; PS1='%F{46}mac%f: %F{051}%c%F{198}\$(__git_ps1)%f\\$ '
EOL
  • 最後の行の\$(__git_ps1)と、\\$の冒頭のバックスラッシュ(\)は、catで入力するヒアドキュメントがシェルとして評価されないようにするためのエスケープで追加したも

テスト

別のterminalを立ち上げ、gitのリポジトリディレクトリでbranchが表示されることを確認します。

AWS Organizationsのメンバーアカウントで予算設定するためには、先に管理アカウントでAWS Cost Explorerを有効化しないといけない件

困ったこと

AWS Organizationsのメンバーアカウントで予算設定をしようとしたところ、先にpayer account(AWS Organizationsの管理アカウント)側でbudgetsを有効化するようにというメッセージが出てエラーになりました。

BUDGET_CONFIG_JSON='
{
   "BudgetName": "Example Budget",
   "BudgetType": "COST",
    "BudgetLimit": {
        "Amount": "100",
        "Unit": "USD"
    },
    "TimeUnit": "MONTHLY"
}'

aws budgets create-budget \
    --account-id 999999999999 \
    --budget "${BUDGET_CONFIG_JSON}"    

発生したエラー内容

An error occurred (AccessDeniedException) when calling the CreateBudget operation: Account 999999999999 is a linked account. To enable budgets for your account, ask the payer account to enable budgets first.

解決方法

エラーメッセージには、ask the payer account to enable budgets first.とありましたが、実機で確認したところ正確には管理アカウントでAWS Cost Explorerを有効化する必要がありました。

管理アカウントのAWS Cost Explorerの有効化方法

AWS Organizationsの管理アカウントにアクセスします。
AWS Cost Explorerは、マネコンでAWS Cost Explorerを起動することで有効化されます。CLI(API)によるAWS Cost Explorer有効化はできません。*1

  • AWS Organizationsの管理アカウントの管理者権限のあるユーザで、マネージメントコンソールにサインインして
  • Billing and Cost Management コンソールを開く https://console.aws.amazon.com/billing/
  • 左のナビゲーションペインから[Cost Explorer]を選択し、
  • Cost Explorer を起動
  • 有効化されるまで24時間待つ
  • AWS Organizationsの管理アカウントでCost Explorer が利用可能になったことを確認します

メンバーアカウントでのBudget作成

  • Budgetを作成したメンバーアカウントにアクセスできるようにします(マネコン/CLIどちらでも可)
  • Budgetを作成する
BUDGET_CONFIG_JSON='
{
   "BudgetName": "Example Budget",
   "BudgetType": "COST",
    "BudgetLimit": {
        "Amount": "100",
        "Unit": "USD"
    },
    "TimeUnit": "MONTHLY"
}'

aws budgets create-budget \
    --account-id 999999999999 \
    --budget "${BUDGET_CONFIG_JSON}"    
  • 作成したBudgetを確認する
aws budgets describe-budgets --account-id 999999999999
{
    "Budgets": [
        {
            "BudgetName": "Example Budget",
            "BudgetLimit": {
                "Amount": "100.0",
                "Unit": "USD"
            },
            "CostTypes": {
                "IncludeTax": true,
                "IncludeSubscription": true,
                "UseBlended": false,
                "IncludeRefund": true,
                "IncludeCredit": true,
                "IncludeUpfront": true,
                "IncludeRecurring": true,
                "IncludeOtherSubscription": true,
                "IncludeSupport": true,
                "IncludeDiscount": true,
                "UseAmortized": false
            },
            "TimeUnit": "MONTHLY",
            "TimePeriod": {
                "Start": "2021-09-01T09:00:00+09:00",
                "End": "2087-06-15T09:00:00+09:00"
            },
            "CalculatedSpend": {
                "ActualSpend": {
                    "Amount": "0.048",
                    "Unit": "USD"
                }
            },
            "BudgetType": "COST",
            "LastUpdatedTime": "2021-09-05T15:37:34.817000+09:00"
        }
    ]
}

gitのpull reqやpushで更新があったディレクトリを抽出しJOSNの配列にするシェル芸

やりたいこと

  • system毎にディレクトリが分かれているterraformがあり(下記イメージ)、パイプラインで更新があったプロジェクトのディレクトリを判別してterraformを実行したい。
  • 具体的には、git diffを使って差分があるディレクトリを抽出し、それをJOSNの配列にしたい。
  • 差分は、mainブランチとの差分で、1コミット分の差分抽出でOK
  • シェルで実現したい
.
├── README.md
├── modules
│   ├── network
│   ├── resource
│   └── security
└── projects
    ├── system-a
    │   ├── backend.tf
    │   ├── data.tf
    │   ├── local.tf
    │   ├── main.tf
    │   ├── provider.tf
    │   └── terraform.tf
    ├── system-b
    └── system-c

やりかた

mainへのpull requestの場合のmainコミットとの差分抽出

TARGET_BRANCH=main
git fetch --depth 1 origin ${TARGET_BRANCH}  > /dev/null 2>&1
git diff  ${TARGET_BRANCH} HEAD --name-only -- 'projects/' | \
  sed 's:\(^projects/[^/]*\)*.*$:\1:' | \
  sort | uniq | \
  jq -scR 'split("\n") | .[:-1]';

push時の一つ前のコミットとの差分抽出

TARGET_BRANCH=main
git fetch --depth 2 origin ${TARGET_BRANCH}  > /dev/null 2>&1
git diff HEAD^ HEAD --name-only -- 'projects/' | \
  sed 's:\(^projects/[^/]*\)*.*$:\1:' | \
  sort | uniq | \
  jq -scR 'split("\n") | .[:-1]';

実行例

TARGET_BRANCH=main
git fetch --depth 1 origin ${TARGET_BRANCH}  > /dev/null 2>&1
git diff  ${TARGET_BRANCH} HEAD --name-only -- 'projects/' | \
  sed 's:\(^projects/[^/]*\)*.*$:\1:' | \
  sort | uniq | \
  jq -scR 'split("\n") | .[:-1]';
["projects/system-a","projects/system-b","projects/system-c"]

解説

  • git fetchgit diff
TARGET_BRANCH=main
git fetch --depth 1 origin ${TARGET_BRANCH}  > /dev/null 2>&1
git diff  ${TARGET_BRANCH} HEAD --name-only -- 'projects/' 

projects/system-a/backend.tf
projects/system-a/data.tf
projects/system-a/local.tf
projects/system-a/main.tf
projects/system-a/provider.tf
projects/system-b/Makefile
projects/system-b/backend.tf
projects/system-c/dummy
  • sed 's:\(^projects/[^/]*\)*.*$:\1:' |
    • sedでprojects直下のフォルダまでのパスまでだけ抽出しています。
    • sedの書き方は、こちらを参照ください
    • /projects/xxxxxのパスまで絞り込めますが、この段階では同じディレクトリが重複した状態になります。
projects/system-a
projects/system-a
projects/system-a
projects/system-a
projects/system-a
projects/system-b
projects/system-b
projects/system-c
  • /sort | uniq/
    • uniqコマンドで重複している行を削除します。uniqの前提としてソートされている必要があるため、あらかじめsortコマンドで行をソートします。
    • ここまでの実行結果
projects/system-a
projects/system-b
projects/system-c
  • jq -scR 'split("\n") | .[:-1]'
    • jqコマンドでJSONの配列に変換しています。

GitHubでSAML SSO認証が必要なレポジトリ用にgitコマンドでのアクセスが必要になった時のメモ

とある経緯で新しい案件のレポジトリをgit coneしようとして認証エラーになった時の対処メモです。
発生したgitのエラーはこちらです。

git clone https://github.com/XXXXXX/XXXXXX.git
Cloning into 'XXXXXX'...
remote: The `XXXXXX' organization has enabled or enforced SAML SSO. To access
remote: this repository, you must re-authorize the OAuth Application `XXXXXX`.
fatal: unable to access 'https://github.com/XXXXXX/XXXXXX.git/': The requested URL returned error: 403

対処方法

  • 端末の既存の認証データを削除する(MACならキーチェーンWindowsなら資格情報マネージャー)
  • GitHubPersonal access tokensを発行する
    • ブラウザでGitHubを開き、Settings -> Developer settingsへ移動し、Personal access tokensを選択
    • Generate New Tokenで新しいトークンを発行する
    • トークンを控えた後に、Enable SSOトークンと関連付させるSSOを選び認証させる
  • 改めてgit cloneを実行し、パスワードに発行したトークンを入力して認証する

Windows ServerでDNSサーバをPowerShellでセットアップする手順

手順

DNSサーバのインストール

  • DNSサーバと管理ツールのインストール
Install-WindowsFeature DNS -IncludeManagementTools 
  • 有効化するためにWindowsを再起動
Restart-Computer -Force

DNSサーバの管理ツールの実行

GUIDNSサーバを管理する

GUIは、DNS Manager(dnsmgmt.msc)を利用します。

  • CLIからDNS Managerを起動する場合
dnsmgmt.msc
  • メニューから起動する場合
    • [Start]->[Windows Administrative Tools] -> [DNS]
CLIDNSサーバを管理する

CLIの場合は、dnscmdコマンドを利用します。dnscmdコマンドの説明はこちらです。

CLIでのDNSサーバ管理

条件付きフォワーダー設定
  • 条件付きフォワーダーを追加する(ADなし)
dnscmd /zoneadd 対象ドメイン /forwarder フォワード先IP [フォワード先IP] ・・・

#実行例
dnscmd /zoneadd test.local /forwarder 8.8.8.8 8.8.4.4
  • 条件付きフォワーダーを追加する(ADあり)
dnscmd /zoneadd 対象ドメイン /dsforwarder フォワード先IP [フォワード先IP] ・・・ [/dp] [/domain | /forest | /regacy]
  1. /dp ・・条件付きフォワーダー設定をレプリケーションする場合のオプション。
  2. [/domain | /forest | /regacy] ・・/dpオプションでレプリケーションをする範囲の指定。通常はforest単位/ドメイン単位のどちらか指定
  • 設定した条件付きフォワーダ設定の確認
dnscmd /zoneinfo 対象ドメイン

#実行例
dnscmd /zoneinfo test.local

AWS BackupのOrganizations連携やクロスアカウントの機能をCLIで設定する方法

はじめに

AWS Backupの権限分掌の設計を行うにあたって、それぞれの機能の設定をどのロールの人が実施できるのか明確化するために確認したものです。
確認結果としては、「バックアップポリシー」と「クロスアカウントモニタリング」はAWS Organizationsの機能、「クロスアカウントでバックアップ」はAWS Backupの機能でした。(マネコンだとAWS Backupの画面にOrganizationsの機能が統合されていて、利用者からすると便利ですが、権限設計する側からするとわかりずらくてつらい。)

設定方法

バックアップポリシーの有効/無効

バックアップポリシーは、Organizationの中でバックアップを一元管理するための機能です。
こちらの機能は、AWS Organizationsの機能のため、AWS OrganizationsのAPIで操作します。(AWS Backupの機能ではない)

#有効化
RootId=$(aws --output text organizations list-roots --query 'Roots[].Id' )
aws organizations enable-policy-type  --root-id ${RootId}  --policy-type "BACKUP_POLICY"

#無効化
RootId=$(aws --output text organizations list-roots --query 'Roots[].Id' )
aws organizations disable-policy-type  --root-id ${RootId}  --policy-type "BACKUP_POLICY"

#状態確認
aws organizations list-roots 

クロスアカウントモニタリングの有効/無効

クロスアカウントモニタリングは、Organizationsの管理アカウントから、組織内のすべてのアカウントのバックアップアクティビティをモニタリングする機能です。こちらもAWS OrganizationsのAPIで操作します。(AWS Backupの機能ではない)

#有効化
aws organizations enable-aws-service-access --service-principal backup.amazonaws.com

#無効化
aws organizations disable-aws-service-access --service-principal backup.amazonaws.com

#状態確認(リストにbackup.amazonaws.comがあればEnable、なければDisable)
aws organizations  list-aws-service-access-for-organization   

クロスアカウントバックアップ有効/無効

クロスアカウントでバックアップ(リカバリーポイント)をコピーする機能です。こちらはAWS Backupの機能ですので、AWS BackupのAPIで操作します。
グロスアカウントの具体的設定方法については、こちらを参照下しさい。

#有効化
aws backup update-global-settings --global-settings '{"isCrossAccountBackupEnabled":"true"}'

#無効化
aws backup update-global-settings --global-settings '{"isCrossAccountBackupEnabled":"false"}'

#状態参照
aws backup describe-global-settings

なお本機能はバックアップを他のアカウントにコピーするだけです。他のアカウントのバックアップデータを利用してリストアすることはできません。

AWS Backup does not support recovering resources from one AWS account to another. However, you can copy a backup from one account to a different account and then restore it in that account. For example, you can't restore a backup from account A to account B, but you can copy a backup from account A to account B, and then restore it in account B.

https://docs.aws.amazon.com/aws-backup/latest/devguide/recov-point-create-a-copy.html#restore-cabより引用

Amazon ECS タスク定義の"タスクサイズのCPU"と”コンテナのCPUユニット”の違いを調べてみた

こちらはAWS Containers Advent Calendar 2020の13日目の記事です。
本記事は個人の意見であり、所属する組織の見解とは関係ありません。

とてもこまか〜い話ですが、Amazon ECSのタスク定義設定ではCPUのリソース設定に関して以下の2つのパラメータがあります。この2つのパラメータがどんな挙動をするのか気になりましたのでドキュメントと実機での挙動確認から調べてみました。最後にOSレベルですが素潜りもしてみました。(時間があればcgroupのコードも確認したかったのですが時間がなく、残念)

  • タスクサイズのCPU: タスクに使用される CPU量。
  • コンテナ定義レベルでのCPUユニット: コンテナ用に予約した cpu ユニットの数

f:id:nopipi:20201212213809p:plain:w500

長い記事になってしまったので調査結果の結論を先に行ってしまうと、以下の通りになりました。

  • コンテナのCPUユニットは、
    • 各コンテナに対するCPU制限で、各コンテナ合計値に対する設定値の割合で利用可能なCPUリソースが決まる
    • ECSはdockerのCPU Sharesを利用し実現しており、dockerはLinuxの場合はcgroupのcpu.sharesで機能を実現している
    • cgroupのcpu.shares仕様で、CPUの競合が発生していない時はコンテナのCPUユニットによる制限は発動しない
  • タスクサイズのCPUは、
    • 単一のタスクが対象となり、設定値はハードリミットになる
    • 実機確認の限りでは、Linuxではcgroupの cpu.cfs_period_uscpu.cfs_quota_usで機能を実現しているように見える

なお本記事に記載している内容は、下記前提で記載していますのでご了承ください。

  • ECS + EC2(Linux)構成で確認しています。Fargate構成やEC2(Windows)構成の場合は本記事の内容と異なる可能性があります。
  • 執筆時点(2020年12月13日)での実機環境での挙動確認の結果に基づいて記事を作成しています。

はじめにAmazon ECSとCPU設定について

Amazon ECSとは

Amazon Elastic Container Service (Amazon ECS) は、フルマネージド型のコンテナオーケストレーションサービスで、多数の仮想マシンを束ねてコンテナを一元管理運用するためのサービスになります。Amazon ECSは一元管理する機能ですので、実際にコンテナを実行する環境は別に必要となります。現時点でコンテナの実行環境としては、EC2インスタンスか、マネージドサービスのAWS Fargateのどちらかが選択できます。(今開催されているAWS re:Invent2020でAmazon ECS Anywhereが発表され、2021年にはどこにでもデプロイすることが可能となりますね)

Amazon ECSについて詳しく知りたいという方は、こちらのBlackBeltが参考になるかと思いますのでご参照ください。
aws.amazon.com

Amazon ECSのタスクとコンテナの関係

おさらいをかねて、Amazon ECSのタスクとコンテナの関係を振り返ります。

  • ECSタスク : Amazon ECSで管理する最小単位。Amazon ECSはタスク単位でコンテナの立ち上げ/立ち下げを行います。タスクには一つ以上のコンテナが含まれています。また単一のタスクは、同一のホストOS上で稼働させます。一つのタスクを2台以上のホストOSにまたいで実行することはできません。ECSタスクの構成は、タスク定義(Task definition)で設定されます。
  • コンテナ: dockerのコンテナです。ECSタスクの中に包含されています。単一のコンテナを起動するには、タスクの定義に1 つのコンテナのみ定義することで実現します。

f:id:nopipi:20201213135457p:plain:w600

まずはドキュメントで確認

まずはAmazon ECSの開発者ガイドでそれぞれのパラメータの仕様を確認してみます。ドキュメントでは、Amazon ECSタスク定義に説明があり、それぞれ下記に設定の記載があります。

上記のドキュメントの内容を表にまとめると以下のようになります。

表.タスクサイズのCPUとコンテナのCPUユニットの違い

設定 適用範囲 制限 空きCPUリソース
がある場合の挙動
備考
タスクサイズのCPU タスク ハード制限 上限を超えて
割り当てられない
コンテナのCPUユニット タスク内のコンテナ 各コンテナ合計値
に対する
割合で評価
設定値を超えて
CPUをコンテナに割り当て
dockerの
--cpu-sharesを利用

また上記以外に下記仕様もあります。

  • 双方の設定とも、1vCPU = 1024として設定を行う
  • タスク内の各コンテナに設定したCPUユニットの合計値が、タスクサイズのCPU以内であること

これらの内容を図にまとめると以下のようになります。

f:id:nopipi:20201213142511p:plain:w300

実機で挙動確認

それではドキュメントの内容を実機で確認して行きたいと思います。
なお今回は下記環境で挙動確認をしています。

  • バージョン
    • ECS Agentバージョン: 1.48.1
    • dockerバージョン: 19.03.13-ce
  • ワーカー構成
    • 起動タイプ: EC2インスタンス
    • AMI: Amazon ECS-optimized Amazon Linux 2 AMI(amzn2-ami-ecs-hvm-2.0.20201209-x86_64-ebs)
    • OS: Amazon Linux2(kernel version = 4.14.209-160.335.amzn2.x86_64)
    • EC2インスタンス構成
      • m5.large or m5.xlargeを利用
      • CPUオプションで1core=1threadに設定(CPUのHyperThreadの影響を排除するため)。従って今回の構成でのOS上のCPU数は以下の通りとなります。
        • m5.large -> OSが認識するCPUは、1個
        • m5.xlarge -> OSが認識するCPUは、2個

コンテナのcpuユニットの挙動を見てみた

では最初に、コンテナのcpuユニットのみ設定した状態での挙動を見てみます。

(1)ホストOSが1CPUの場合
  • ホストOS構成: m5.large(1スレッド/core設定) x 1ノード
  • OSが認識するCPU数: 1個
  • タスク構成
    • container1: CPU Unit = 768
    • container2: CPU Unit = 256

タスク内に2つのコンテナがあり、CPU配分を3/4(75%)と1/4(25%)に設定します。このタスクを1タスクのみ起動します。絵にすると以下の通です。

f:id:nopipi:20201213165350p:plain:w500

この状態で、ホストOSにsshでログインして、下記コマンドでそれぞれのコンテにCPU負荷を与えます。

docker exec -it <コンテナID> /bin/bash -c 'while true;do :; done;'

2つのコンテナにCPU負荷を欠けた状態で、ホストOS上でdocker container statsでコンテナのリソース利用状況を確認します。すると、container1がCPU利用率約 75%container2がCPU利用率約 25%でCPUユニットの設定通りの制限がかかっていることが確認できました。
f:id:nopipi:20201213170025p:plain

次に、この状態からContainer2の負荷をゼロにします(docker exec・・・のコマンドを、CTRL+Cで強制終了します)。すると、container1のCPU利用率が約100%になりました。
f:id:nopipi:20201213170443p:plain
ここまでで、CPUユニット設定の割合でCPU配分が決まることと、CPUに空きがある場合はタスクはCPUユニットの設定以上のCPUリソースを利用可能なことが確認できました。

(1)ホストOSが2CPUの場合

では次に、m5.xlarge(1スレッド/core設定) x 1ノード構成でホストOSが2CPUの場合どのような挙動になるか確認します。タスク定義は、先ほどの物と同じです。

f:id:nopipi:20201213172105p:plain:w500

この状態でまず、container1にのみ負荷をかけてみます。CPUの利用状況は、先ほどと同様container1のみCPU 100%の状態になりました。
f:id:nopipi:20201213172524p:plain

次にcontainer2にも負荷をかけます。この場合普通に想像すると、container1とcontainer2のCPU利用率の割合は3:1になることを予想すると思います。しかし実態は、なぜか2つのコンテナともCPU利用率が約100%になりました。
f:id:nopipi:20201213172816p:plain

この事象を理解するには、まずそれぞれのコンテナ上のプロセスがホストOSのどのCPUで実行されているかを確認する必要があります。

プロセスがどのCPUで実行されているかは、topコマンドで確認することができます。ただしデフォルトではこの情報は表示されないため、設定を変更する必要があります。具体的にはtopコマンドを実行し、fを押したあと、カーソルでP = Last Used Cpu (SMPに移動してdで表示するよう設定変更し、qで終了します。そうすると、右端にPという列が現れます。これはそのプロセスが最後に実行されたCPUのプロセッサ番号を表しています。

先ほどのcontainer1とcontainer2に負荷を掛けた状態で上記手順で、各プロセスが実行されているCPUを確認すると、0番と1番とありそれぞれのコンテナ上のプロセスが異なるCPUで実行されていることが確認できます。
f:id:nopipi:20201213175131p:plain

ではなぜ実行CPUが異なることでCPUユニットの制限が適用されないのでしょうか。結論を言うとLinuxカーネルのcgroupの仕様ということになります。

Amazon ECSのコンテナのCPUユニットは、dockerのCPU Shares機能を利用してコンテナのCPUリソースの制限を実現しています。そしてdockerのCPU Shares機能は、Linuxの場合はlinuxカーネルcgroupcpu.sharesを利用してCPUリソースの制限を実現しています。このcgroupのcpu.shareは、複数のプロセスが CPU リソースを競い合う場合のみに有効な機能となります。*1

ここまでの内容を絵にまとめるとこんな感じです。

f:id:nopipi:20201213181630p:plain:w500


ではCPU競合状態になったら本当にCPUユニットの設定が発動するのか確認してみます。先ほどのタスク数を1個から2個に増やして4つのコンテナ全てにCPU負荷をかけてCPUの競合状態を発生させます。

f:id:nopipi:20201213182610p:plain:w500

すると予想通りCPUユニット設定が効果を発揮しそれぞれのタスクで、container1のCPU利用率は約75%、container2のCPU利用率は約25%になりました。
f:id:nopipi:20201213182741p:plain

タスクサイズのCPU

次に、タスクサイズのCPUの挙動を実機で確認します。先ほどのCPUユニットの検証に利用したタスク定義に、タスクサイズのCPU設定を追加して以下の構成にします。

  • タスク構成
    • タスクサイズのcpu: 1024 (1CPUを割り当て)
    • コンテナのCPUユニット
      • container1: CPU Unit = 768
      • container2: CPU Unit = 256

この構成で、CPUユニット検証で利用したm5.xlargeのEC2インスタンスでタスクを1つだけ起動します。構成を絵にすると以下の通りです。

f:id:nopipi:20201213184457p:plain:w500

この状態で、2つのコンテナに対してCPU負荷を与えます。すると、先ほどのCPUユニット設定のみの場合はそれぞれのコンテナがCPUを100%利用しタスク全体では200%利用していましたが、今回はタスクサイズのcpu: 1024(=1CPUで制限)が入ったことで、それぞれのコンテナのCPU利用率は約50%にキャップされ、タスク全体で100%となりました。
f:id:nopipi:20201213184957p:plain

この状態についてですが、まずCPUユニットは先どのCPUユニット設定のみの場合と同様、ホストOSが認識するCPU数2個に対してCPUを利用するプロセスも2個でCPUの競合が発生していない状態のため、CPUユニット設定によるCPUリソース制限は発動していない状態となります。その上でタスクサイズのCPU制限はハード制限でタスク全体で1CPU分のCPUリソースしか利用できない設定(cpu = 1024)のため、結果としてそれぞれのプロセスのCPU利用率が50%に制限されてしまった、という物になります。

ではCPU競合状態が発生した場合はどうなるのか次に確認します。cgroupの制御がかかっていないホストOS上でwhile true;do :; doneを実行してCPU競合状態を発生させます。状況を絵にすると以下の通りになります。

f:id:nopipi:20201213190240p:plain:w500

すると、予想通りですがCPU競合が発生しCPUユニットの設定が有効化されてcontainer1とcontainer2でCPU利用率が3:1の状態になりました。
f:id:nopipi:20201213190507p:plain

タスクサイズのCPU設定について素潜りしてみる

すでにここまでで満腹感はありますが、もう一つ調べてみます。
コンテナのCPUユニットではdockerのCPU Shareを利用していることはAmazon ECSドキュメントに記載されていますが、タスクサイズのCPU設定については具体的な記載はありません。そこで実機のホストOSの中見てどんな風にタスクサイズの制限を実現しているか覗いてみます。

cgroupの構造を確認する

Linuxで動作するdockerでは、リソース制限をkernelのcgroupで実現しています。コンテナのCPUユニットは、話は端おりますが、dockerのCPU sharesの機能を利用しこの機能はcgroupcpu.sharesで実現しています。ということからタスクサイズのCPUもcgroupの機能で実現しているだろうなというあたりが付きます。そこでまずタスクを起動したときのcgroupの構造を確認してみます。

ということで、タスクサイズのCPUの実機確認をした下記構成を使ってホストOS上でcgroup構成を確認します。

f:id:nopipi:20201213184457p:plain:w500

まずはdocker container inspectコマンドで、コンテナID、内部IDやcgroupの親ディレクトリなどを確認してみます。
ホストOS上でdockerコマンドで起動しているコンテナを確認します。

$ docker ps
CONTAINER ID        IMAGE                                                                        COMMAND                  CREATED              STATUS                        PORTS                   NAMES
d2c1740cfafa        290054392706.dkr.ecr.ap-northeast-1.amazonaws.com/simple-httpserver:latest   "docker-php-entrypoi…"   About a minute ago   Up About a minute (healthy)   0.0.0.0:32783->80/tcp   ecs-cpuunit-and-tasksizecpu-1-container2-8afc9a86cdd6bac92500
cc28ca289411        290054392706.dkr.ecr.ap-northeast-1.amazonaws.com/simple-httpserver:latest   "docker-php-entrypoi…"   About a minute ago   Up About a minute (healthy)   0.0.0.0:32782->80/tcp   ecs-cpuunit-and-tasksizecpu-1-container1-d08d9fb3a4d6dbde8301
8851d081efe9        amazon/amazon-ecs-agent:latest                                               "/agent"                 3 hours ago          Up 3 hours (healthy)                                  ecs-agent

docker container inspect ><コンテナID>で、各コンテナの内部IDやcgroupのディレクトリを確認します。

$ sudo yum -y install jq

$ CONTAINER_ID=cc28ca289411
$ docker container inspect ${CONTAINER_ID} | jq -r '.[] |{ Id:.Id, Name:.Name, CgroupParent:.HostConfig.CgroupParent, CpuShares:.HostConfig.CpuShares}'
{
  "Id": "cc28ca2894114178ee023b549bfd29800f9ea311858d9c19b9bed38b308a1fbd",
  "Name": "/ecs-cpuunit-and-tasksizecpu-1-container1-d08d9fb3a4d6dbde8301",
  "CgroupParent": "/ecs/a411e266bd2649a88deec27182fce603",
  "CpuShares": 768
}
$ CONTAINER_ID=d2c1740cfafa
$ docker container inspect ${CONTAINER_ID} | jq -r '.[] |{ Id:.Id, Name:.Name, CgroupParent:.HostConfig.CgroupParent, CpuShares:.HostConfig.CpuShares}'
{
  "Id": "d2c1740cfafa72bdc7b505dec468ce0bac02210222c3e977cce08a8f12394a3f",
  "Name": "/ecs-cpuunit-and-tasksizecpu-1-container2-8afc9a86cdd6bac92500",
  "CgroupParent": "/ecs/a411e266bd2649a88deec27182fce603",
  "CpuShares": 256
}

内容を表にまとめると以下の通りです。

Container CONTAINER ID ID CgroupParent CpuShares
Container1 cc28ca289411 cc28ca2894114178ee023b549bfd29800f9ea311858d9c19b9bed38b308a1fbd /ecs/a411e266bd2649a88deec27182fce603 768
Container2 d2c1740cfafa d2c1740cfafa72bdc7b505dec468ce0bac02210222c3e977cce08a8f12394a3f /ecs/a411e266bd2649a88deec27182fce603 256

この内容を踏まえてcgroupのCPUに関するリソース制限設定を確認してみます。cgroupのCPU制限設定は/sys/fs/cgroup/cpuディレクトリ配下で確認できます。またコンテナのCgroupParentから各コンテナの設定は、/sys/fs/cgroup/cpu/ecs/a411e266bd2649a88deec27182fce603配下であることがわかります。

$ cd /sys/fs/cgroup/cpu/ecs/a411e266bd2649a88deec27182fce603
$ ls -l
合計 0
drwxr-xr-x 2 root root 0 1213 11:05 cc28ca2894114178ee023b549bfd29800f9ea311858d9c19b9bed38b308a1fbd
-rw-r--r-- 1 root root 0 1213 11:31 cgroup.clone_children
-rw-r--r-- 1 root root 0 1213 11:31 cgroup.procs
-rw-r--r-- 1 root root 0 1213 11:05 cpu.cfs_period_us
-rw-r--r-- 1 root root 0 1213 11:05 cpu.cfs_quota_us
-rw-r--r-- 1 root root 0 1213 11:31 cpu.rt_period_us
-rw-r--r-- 1 root root 0 1213 11:31 cpu.rt_runtime_us
-rw-r--r-- 1 root root 0 1213 11:31 cpu.shares
-r--r--r-- 1 root root 0 1213 11:31 cpu.stat
-r--r--r-- 1 root root 0 1213 11:31 cpuacct.stat
-rw-r--r-- 1 root root 0 1213 11:31 cpuacct.usage
-r--r--r-- 1 root root 0 1213 11:31 cpuacct.usage_all
-r--r--r-- 1 root root 0 1213 11:31 cpuacct.usage_percpu
-r--r--r-- 1 root root 0 1213 11:31 cpuacct.usage_percpu_sys
-r--r--r-- 1 root root 0 1213 11:31 cpuacct.usage_percpu_user
-r--r--r-- 1 root root 0 1213 11:31 cpuacct.usage_sys
-r--r--r-- 1 root root 0 1213 11:31 cpuacct.usage_user
drwxr-xr-x 2 root root 0 1213 11:05 d2c1740cfafa72bdc7b505dec468ce0bac02210222c3e977cce08a8f12394a3f
-rw-r--r-- 1 root root 0 1213 11:31 notify_on_release
-rw-r--r-- 1 root root 0 1213 11:31 tasks

ディレクトリのファイル一覧を見ると、container1/2のコンテナのID名のディレクトリがあることが確認できます。
では各コンテナのCPUユニット設定があるか確認してみます。

$ cat ./cc28ca2894114178ee023b549bfd29800f9ea311858d9c19b9bed38b308a1fbd/cpu.shares 
768
$ cat ./d2c1740cfafa72bdc7b505dec468ce0bac02210222c3e977cce08a8f12394a3f/cpu.shares 
256

Container1/2のCPUユニット設定が入っていることが確認できました。ここまでの内容を絵にまとめます。

f:id:nopipi:20201213205618p:plain

タスクサイズのCPUはどのように制限を実現しているか

とここまで見ると、ecs/a411e266bd2649a88deec27182fce603にタスクサイズのCPU設定があるっぽいことが見えてきます。ということでこのディレクトリのパラメータ(ファイル)を眺めていくと、間の話は飛ばしますが、結果としてタスクサイズのCPUのリソース制限はcpu.cfs_period_uscpu.cfs_quota_usで実現しているっぽいことがわかりました。

これらの設定は何かというと、以下の通りとなります。要は指定した期間で利用可能なCPU時間の制限で、CPUのハード制限の設定になります。詳しくはこちらのカーネルのドキュメントを参照下さい。

  • cpu.cfs_period_us : CPUリソース制限の期間の長さ(マイクロ秒単位)
  • cpu.cfs_quota_us : 期間内の利用可能な合計実行時間(マイクロ秒単位)

今回の環境のcgroupのCPU関連の設定を表にまとめると以下の通りになりました。

項目 cpu.cfs_period_us cpu.cfs_quota_us cpu.shares
タスク 100000 100000 1024
Container1 100000 -1 768
Container2 100000 -1 256

Container1/2は、cpu.cfs_quota_us = -1とありますが、これはContainer1/2単位ではcpu.cfs_quota_usによるCPUリソースのハード制限は無効化されているということになります。
一方でタスクは、 cpu.cfs_period_us = 100000(1秒)cpu.cfs_quota_us = 100000(1秒)とあり、これはタスクは1秒間にCPUリソースを1秒分利用できる = CPU1個分のリソースが割り当てられているということになります。

ここまでの実機の挙動確認から、タスクサイズのCPU設定によるCPUリソース制限は、cgroupのcpu.cfs_period_us cpu.cfs_quota_usで実現していそうだということまでが整理できました。

タスクサイズのCPUのcgroup設定は誰が行っているのか

これが最後です。ではタスクサイズのCPU設定のcpu.cfs_period_us cpu.cfs_quota_usは誰が設定しているのでしょうか?
ここまで来ればおそらくAmazon ECS Agentが設定しているだろうということはあたりがつきます。そこで、Amazon ECS Agentのプロセスのステムコールトレースを取得して確認してみます。

sudo yum -y install strace

ps -ef|grep -v grep |grep '/sbin/docker-init -- /agent'
root      4261  4232  0 08:12 ?        00:00:00 /sbin/docker-init -- /agent

sudo strace -f -tt -p 4261 -o ecs_agent_strace.log

で、ログを見ると cpu.cfs_period_uscpu.cfs_quota_usのファイルへの書き込みをしていることが見て取れます。

<前略>
#ディレクトリの作成
23434 11:05:13.722263 mkdirat(AT_FDCWD, "/sys/fs/cgroup/systemd/ecs/a411e266bd2649a88deec27182fce603", 0755) = 0
<中略>
#cpu.cfs_period_usへの書き込み
23434 11:05:13.724464 openat(AT_FDCWD, "/sys/fs/cgroup/cpu/ecs/a411e266bd2649a88deec27182fce603/cpu.cfs_period_us", O_WRONLY|O_CREAT|O_TRUNC|O_CLOEXEC, 000) = 27
23434 11:05:13.724487 epoll_ctl(4, EPOLL_CTL_ADD, 27, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=3911208272, u64=140054204796240}}) = 0
23434 11:05:13.724505 fcntl(27, F_GETFL) = 0x8001 (flags O_WRONLY|O_LARGEFILE)
23434 11:05:13.724519 fcntl(27, F_SETFL, O_WRONLY|O_NONBLOCK|O_LARGEFILE) = 0
23434 11:05:13.724535 write(27, "100000", 6) = 6
23434 11:05:13.724562 epoll_ctl(4, EPOLL_CTL_DEL, 27, 0xc00050d7a4) = 0
23434 11:05:13.724577 close(27)     
<中略>
#cpu.cfs_quota_usへの書き込み
23434 11:05:13.724594 openat(AT_FDCWD, "/sys/fs/cgroup/cpu/ecs/a411e266bd2649a88deec27182fce603/cpu.cfs_quota_us", O_WRONLY|O_CREAT|O_TRUNC|O_CLOEXEC, 000) = 27
23434 11:05:13.724616 epoll_ctl(4, EPOLL_CTL_ADD, 27, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=3911208272, u64=140054204796240}}) = 0
23434 11:05:13.724633 fcntl(27, F_GETFL) = 0x8001 (flags O_WRONLY|O_LARGEFILE)
23434 11:05:13.724647 fcntl(27, F_SETFL, O_WRONLY|O_NONBLOCK|O_LARGEFILE) = 0
23434 11:05:13.724662 write(27, "100000", 6) = 6
23434 11:05:13.724683 epoll_ctl(4, EPOLL_CTL_DEL, 27, 0xc00050d7a4) = 0
23434 11:05:13.724698 close(27)   

まとめ

本記事では、Amazon ECS タスク定義の"タスクサイズのCPU"と”コンテナのCPUユニット”の違いをドキュメントと実機の挙動確認から調べてみました。
調査結果としては、以下の通りになります。

  • コンテナのCPUユニットは、
    • 各コンテナに対するCPU制限で、各コンテナ合計値に対する設定値の割合で利用可能なCPUリソースが決まる
    • ECSはdockerのCPU Sharesを利用し実現しており、dockerはLinuxの場合はcgroupのcpu.sharesで機能を実現している
    • cgroupのcpu.shares仕様で、CPUの競合が発生していない時はコンテナのCPUユニットによる制限は発動しない
  • タスクサイズのCPUは、
    • 単一のタスクが対象となり、設定値はハードリミットになる
    • 実機確認の限りでは、Linuxではcgroupの cpu.cfs_period_uscpu.cfs_quota_usで機能を実現しているように見える

最後に

私は普段は、AWS Professional ServicesとしてAWS クラウドを使用して期待するビジネス上の成果を実現するようお客様を支援しています。
Professional Servicesには、私みたいな人以外に、ML、Bigdata、セキュリティ、アジャイル、UI/UXのデザインなど各分野のスペシャリストが揃っているエキサイティングなチームです。
興味がある方はこちらの採用のページをご確認ください。
aws.amazon.com

AWS環境のRHEL7/8にDNSキャッシュ(dnsmasq)を設定してみた

はじめに

AWS環境にRHEL8とRHEL7にdnsmasqを利用したDNSキャッシュを設定した時の手順メモです。ネットワーク設定にDHCPを利用している環境の場合、DHCPとの整合性を合わせる部分が鍵となります。

dnsmasqとは

Dnsmasqは軽量で比較的容易に設定できるDNSサーバのフォワーダとDHCPサーバをもつソフトウェアです(wikipediaより)。
これは私見ですが、個々のサーバでのDNSキャッシュを実現する場合、BIND9では高機能すぎるのでdnsmasqが利用されるケースが多いのではないかと思います。
詳しくは以下を参照してください。

作成する構成

RHEL8/7とも、この記事の手順を実行するとDNS解決は以下の図のような構成になります。 dnsmasqは、127.0.0.1の53番ポートでListenしており、アプリケーションがDNSゾルバ参照するときは、この127.0.0.1にアクセスするようになります。dnsmasqは、この後のRHEL7の手順でdnsmasqを構築した時のファイル名になります。RHEL8ではNetworkManagerからdbus経由で設定がdnsmasqに流し込まれるため、設定ファイルはありません。*1

f:id:nopipi:20201117015314p:plain
dnsmasqによるDNSキャッシュサーバ構成概要

RHEL8とRHEL7の違い

RHEL8とRHEL7の違いですが、RHEL8はNetworkManagerがよろしくやってくれるのでNetworkManagerの設定を一部変更するだけでdnsmasqの設定を行う必要はありませんが、RHEL7ではdnsmasqやdhcp設定を個別に設定する必要がある点が異なる点です。

RHEL8にdnsmasqを設定する

実行環境

今回は以下の環境で手順を検証しました。

  • AWSのRHEL8のインスタンスを利用
  • AMI
    • AMI ID: ami-0dc185deadd3ac449
    • AMI Name: RHEL-8.3.0_HVM-20201031-x86_64-0-Hourly2-GP2
    • Region: Tokyo(ap-northeast-1)

設定手順

dnsmasqインストールと実行ユーザ設定

dnsmasqパッケージをインストールします。

sudo yum -y install dnsmasq
NetworkManagerの設定変更

NetworkManagerの設定ファイル/etc/NetworkManager/NetworkManager.conf[main]セッションに、dns=dnsmasqを追加します。

sudo vi /etc/NetworkManager/NetworkManager.conf
  • /etc/NetworkManager/NetworkManager.conf
中略
[main]
plugins = ifcfg-rh,
#plugins=ifcfg-rh

dns=dnsmasq  #この行を追加

[logging]
以下略
再起動
sudo reboot
動作確認

digを利用してDNS参照して動作確認を行います。

(a) digコマンドのインストール
RHEL8にdigコマンドがインストールされていない場合は、bind-utilsパッケージをインストールします。

sudo yum -y install bind-utils

(b) 動作テスト
ANSWER SECTION:で問い合わせたFQDN名のDNSが解決できていること、および末尾のSERVER:で参照先のDNSゾルバが127.0.0.1であることを確認します。

dig aws.amazon.com

; <<>> DiG 9.11.20-RedHat-9.11.20-5.el8 <<>> aws.amazon.com
<中略>
;; ANSWER SECTION:
aws.amazon.com.		48	IN	CNAME	tp.8e49140c2-frontier.amazon.com.
tp.8e49140c2-frontier.amazon.com. 48 IN	CNAME	dr49lng3n1n2s.cloudfront.net.
dr49lng3n1n2s.cloudfront.net. 14 IN	A	143.204.75.74

;; Query time: 2 msec
;; SERVER: 127.0.0.1#53(127.0.0.1)
;; WHEN: Sun Nov 22 10:42:32 UTC 2020
;; MSG SIZE  rcvd: 137

RHEL7にdnsmasqを設定する

基本的には下記のAWSの記事を元にしていますが、一部手順とdnsmasq設定の見直しをしています。

実行環境

  • AWSのRHEL7のインスタンスを利用
  • AMI
    • AMI ID: ami-0e876007231767016
    • AMI Name: RHEL-7.8_HVM-20200803-x86_64-0-Hourly2-GP2
    • Region: Tokyo(ap-northeast-1)

設定手順

dnsmasqインストールと実行ユーザ設定

(a)dnsmasqのパッケージインストール
dnsmasqパッケージをインストールします。

sudo yum -y install dnsmasq

(b)dnsmasq実行ユーザ/グループ作成
この後のdnsmasq.confuser=group=で指定するdnsmasqの実行ユーザ&グループを作成します。

sudo groupadd -r dnsmasq
sudo useradd -r -g dnsmasq dnsmasq
dnsmasq設定ファイルの変更

(a) 設定ファイル(/etc/dnsmasq.conf)バックアップ

sudo mv /etc/dnsmasq.conf /tmp/dnsmasq.conf.orig

(b)設定ファイル(/etc/dnsmasq.conf)編集
テキストエディタ(viなど)で、設定ファイルを編集します。

sudo vi /etc/dnsmasq.conf

設定ファイルには以下の設定を行います。

# Server Configuration
listen-address=127.0.0.1   #local loop backでListenする
port=53
bind-interfaces    #listen-addressで指定したIPにbindを制限する
user=dnsmasq     #先ほど作成したdnsmasqの実行の専用ユーザを指定
group=dnsmasq  #先ほど作成したdnsmasq専用グループを指定
pid-file=/var/run/dnsmasq.pid

# Name resolution options
resolv-file=/etc/resolv.dnsmasq  #dnsmasqのDNSクエリー先のDNSサーバ指定
cache-size=500
domain-needed
bogus-priv

neg-ttl=60
#max-ttl=
#max-cache-ttl
#min-cache-ttl
  • dnsmasq設定のポイント
    • Listen先: listen-address=portで指定。かつbind-interfacesで指定したIPにbindを限定
    • 実行ユーザ/グループ: user=groupで指定
    • DNSクエリー先指定: resolv-file=DNSゾルバーを記載したファイルを指定。未設定の場合は/etc/resolv.confを参照。
    • キャッシュ保有期間/TTLに関する設定
      • 否定応答のキャッシュ時間指定: neg-ttl=で秒で指定。
      • 肯定応答のキャッシュ時間: 保持可能な最大時間をmax-cache-ttl=で秒で指定。(後述)
dnsmasqのDNS参照先設定(/etc/resolv.dnsmasq)

dnsmasqからのDNS参照先として、/etc/dnsmasq.confresolv-file=ファイル名で指定したファイルにdnsmasqが利用するDNSゾルバーを指定します。

DNSIP="$(awk -e '/^nameserver/{ print $2}' /etc/resolv.conf)"
echo $DNSIP

sudo bash -c "echo \"nameserver $DNSIP\" > /etc/resolv.dnsmasq"
dnsmasqサービスの有効化

(a)dnsmasqサービスの起動とOS起動時の自動起動設定

sudo systemctl restart dnsmasq.service
sudo systemctl enable dnsmasq.service

(b)dnsmasqの起動確認
プロセスが起動していること、ログにエラーがないことを確認します。

sudo systemctl status dnsmasq

● dnsmasq.service - DNS caching server.
   Loaded: loaded (/usr/lib/systemd/system/dnsmasq.service; enabled; vendor preset: disabled)
   Active: active (running) since Mon 2020-11-16 16:02:09 UTC; 8s ago
 Main PID: 13057 (dnsmasq)
    Tasks: 1 (limit: 100988)
   Memory: 792.0K
   CGroup: /system.slice/dnsmasq.service
           └─13057 /usr/sbin/dnsmasq -k

Nov 16 16:02:09 ip-10-1-64-32.ap-northeast-1.compute.internal systemd[1]: Started DNS caching server..
Nov 16 16:02:09 ip-10-1-64-32.ap-northeast-1.compute.internal dnsmasq[13057]: listening on lo(#1): 127.0.0.1
Nov 16 16:02:09 ip-10-1-64-32.ap-northeast-1.compute.internal dnsmasq[13057]: started, version 2.79 cachesize 500
Nov 16 16:02:09 ip-10-1-64-32.ap-northeast-1.compute.internal dnsmasq[13057]: compile time options: IPv6 GNU-getopt DBus no-i18n IDN2 DHCP DHCPv6 no-Lua TFTP no-conntrack ipset auth DNS>
Nov 16 16:02:09 ip-10-1-64-32.ap-northeast-1.compute.internal dnsmasq[13057]: reading /etc/resolv.dnsmasq
Nov 16 16:02:09 ip-10-1-64-32.ap-northeast-1.compute.internal dnsmasq[13057]: using nameserver 10.1.0.2#53
Nov 16 16:02:09 ip-10-1-64-32.ap-northeast-1.compute.internal dnsmasq[13057]: read /etc/hosts - 2 addresses
DHCPDNSゾルバー設定変更と反映

(a) DHCP設定ファイル更新(/etc/dhcp/dhclient.conf)
DHCPで変更するときに、dnsmasq DNS キャッシュをデフォルトの DNSゾルバーとするようして設定するよう、/etc/dhcp/dhclient.confを編集します。

sudo bash -c "echo 'supersede domain-name-servers 127.0.0.1, $DNSIP;' >> /etc/dhcp/dhclient.conf"

上記を実行すると、最初に127.0.0.1DHCPで取得し設定されていた元々のDNSゾルバーがエントリーされているはずです。

sudo cat /etc/dhcp/dhclient.conf
supersede domain-name-servers 127.0.0.1, 172.31.0.2;  <==2つのIPは環境によって変わります

(b)設定反映
dhclient コマンドを実行し、設定を反映します。(dhclientではなくrebootでも可能)

sudo dhclient
動作確認

digを利用してDNS参照して動作確認を行います。
(a) digコマンドのインストール
RHEL8にdigコマンドがインストールされていない場合は、bind-utilsパッケージをインストールします。

sudo yum -y install bind-utils

(b) 動作テスト
ANSWER SECTION:で問い合わせたFQDN名のDNSが解決できていること、および末尾のSERVER:で参照先のDNSゾルバが127.0.0.1であることを確認します。

dig aws.amazon.com

; <<>> DiG 9.11.20-RedHat-9.11.20-5.el8 <<>> aws.amazon.com
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 2564
;; flags: qr rd ra; QUERY: 1, ANSWER: 3, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 4096
;; QUESTION SECTION:
;aws.amazon.com.			IN	A

;; ANSWER SECTION:
aws.amazon.com.		35	IN	CNAME	tp.8e49140c2-frontier.amazon.com.
tp.8e49140c2-frontier.amazon.com. 35 IN	CNAME	dr49lng3n1n2s.cloudfront.net.
dr49lng3n1n2s.cloudfront.net. 21 IN	A	143.204.75.74

;; Query time: 3 msec
;; SERVER: 127.0.0.1#53(127.0.0.1)
;; WHEN: Mon Nov 16 16:15:02 UTC 2020
;; MSG SIZE  rcvd: 137

dnsmasqのキャッシュ保持期間

dnsmasqのキャッシュ保持期間は、dnsmasqの設定と各DNSレコードのTTL時間を評価して決定します。

  • dnsmasq設定のmax-cache-ttlとレコードのTTLいづれか小さい方の値がキャッシュ保持期間となる
  • ただし非推奨設定であるが、dnsmasq設定のmin-cache-ttlで0より大きい値を設定している場合、min-cache-ttlの設定値が最短の保持期間となる。(例えばmax-cache-ttlとレコードのTTLの評価でキャッシュ保持期間が5秒とされても、min-cache-ttl10秒であればキャシュ保持期間は10秒となる
  • max-ttl=はクライアントにレコードを渡す際にレコードに設定するTTLの最大値の設定。max-cache-ttlと同じ値を指定する。

*1: NetworkManagerから起動したdnsmasqの引数にenable-dbus=org.freedesktop.NetworkManager.dnsmasqというのがあり、これがdbus経由でdnsmasq設定が投入するためのオプションになります

アカウントに対してS3 block public accessをCLIで設定してみた

AWS CLIを利用してS3のパブリックアクセス ブロックを設定しようとしたら、設定対象によってCLIのコマンドが異なるという面倒な仕様だったのでメモを書いておきます。またAccess Pointは、Access Pointの作成時しかパブリックアクセス ブロックの設定ができない点は留意が必要です。

  • パブリックアクセス ブロックのCLI設定
    • アカウントに対して設定: aws s3control put-public-access-blockで設定
    • バケットに対して設定: aws s3api put-public-access-blockで設定
    • Access Pointに対して設定 aws s3control create-access-pointAccess Point作成時に設定(作成後の変更不可!!)

アカウントに対してパブリックアクセス ブロックを設定するCLI手順

PROFILE=<環境に合わせて設定。デフォルトの場合は"default"を指定>

ACCOUNTID="$(aws --profile ${PROFILE} --output text sts get-caller-identity --query 'Account')"

JSON='{
    "BlockPublicAcls": true,
    "IgnorePublicAcls": true,
    "BlockPublicPolicy": true,
    "RestrictPublicBuckets": true
}'

aws --profile ${PROFILE} s3control put-public-access-block --account-id ${ACCOUNTID} --public-access-block-configuration "${JSON}"

C言語/glibc getaddrinfo()を使ってホスト名からIPアドレスに変換するサンプル

C言語の標準ライブラリで、接続先のホストの名前解決をしてIPアドレス情報を取得するには、従来はgethostbynameの利用が一般的でした。しかし現在はIPv6に対応するため、getaddrinfo()を利用するべきとされています。*1

f:id:nopipi:20201116013457p:plain
getaddrinfo()の処理概要

ここでは、getaddrinfo()による名前解決とIPアドレスに解決した情報の参照方法のサンプルコードを以下に記載します。またおまけで、getaddrinfo()を実行した時の挙動をざっくり説明します。

サンプルコード

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>
#include <arpa/inet.h>

#define  BUFF_SIZE 1024

int main(int argc, char **argv)
{
    char *hostname;
    char *service;
    struct addrinfo hints, *res0, *res;
    int ret;
    char ipandport[BUFF_SIZE], buf[BUFF_SIZE];
    int port;

    /* set hostname and service from arguments */
    if( argc < 2 ){
        fprintf(stderr, "error: illigal arguments.\nsample_getaddrinfo HOSTNAME [SERVICE]\n");
        return(EXIT_FAILURE);
    }else{
        /* set hostname */
        hostname = (char *)malloc(sizeof(char) * BUFF_SIZE);
        strncpy(hostname, *(argv+1), BUFF_SIZE);

        /* set port, if service is specified */
        if( argc > 2){
            service = (char *)malloc(sizeof(char) * BUFF_SIZE);
            strncpy(service, *(argv+2), BUFF_SIZE);
        }else{
            service = NULL;
        }
    }
    printf("hostname=%s, service=%s\n", hostname, service);

    /* main dish!! */
    memset(&hints, 0, sizeof(hints));
    hints.ai_socktype = SOCK_STREAM;
    hints.ai_family = PF_UNSPEC;

    if ((ret = getaddrinfo(hostname, service, &hints, &res0)) != 0) {
        perror("error at getaddrinfo():");
        return(EXIT_FAILURE);
    }

    /* convert ai_addr from binary to string */
    switch(res0->ai_family){
        case AF_INET:
            inet_ntop(res0->ai_family, &((struct sockaddr_in *)res0->ai_addr)->sin_addr, buf, BUFF_SIZE);
            port = ntohs( ((struct sockaddr_in *)res0->ai_addr)->sin_port );
            break;
        case AF_INET6:
            inet_ntop(res0->ai_family, &((struct sockaddr_in6 *)res0->ai_addr)->sin6_addr, buf, BUFF_SIZE);
            port = ntohs( ((struct sockaddr_in6 *)res0->ai_addr)->sin6_port );
            break;
        default:
            fprintf(stderr, "ai_family is neither AF_INET nor AF_INET6\n");
            break;
    }
    sprintf(ipandport, "ip=%s, port=%d", buf, port);

    /* print addrinfo */
    printf("addrinfo {\n");
    printf("    int              ai_flags    = %2d \n", res0->ai_flags);
    printf("    int              ai_family   = %2d (AF_INET=2, AF_INET6=10, AF_UNSPEC=0)\n", res0->ai_family);
    printf("    int              ai_socktype = %2d (SOCK_STREAM=1, SOCK_DGRAM=2)\n",         res0->ai_socktype);
    printf("    int              ai_protocol = %2d\n", res0->ai_protocol);
    printf("    socklen_t        ai_addrlen  = %2d\n", res0->ai_addrlen);
    printf("    struct sockaddr *ai_addr     = %s\n",  ipandport );
    printf("    char            *ai_canonname= %s\n",  res0->ai_canonname);
    printf("    struct addrinfo *ai_next     = %p\n",  res0->ai_next);
    printf("}\n");

    return(EXIT_SUCCESS);
}

使い方

ビルド

上記サンプルコードのファイル名を、sample_getaddrinfo.cとした場合の実行手順です。

gcc -o sample_getaddrinfo sample_getaddrinfo.c

実行例

./sample_getaddrinfo google.com
hostname=google.com, service=(null)
addrinfo {
    int              ai_flags    =  0 
    int              ai_family   =  2 (AF_INET=2, AF_INET6=10, AF_UNSPEC=0)
    int              ai_socktype =  1 (SOCK_STREAM=1, SOCK_DGRAM=2)
    int              ai_protocol =  6
    socklen_t        ai_addrlen  = 16
    struct sockaddr *ai_addr     = ip=172.217.25.78, port=0
    char            *ai_canonname= (null)
    struct addrinfo *ai_next     = 0x1b3faf0
}

補足

getaddrinfo()の挙動

大まかな挙動ですが、アプリケーションからcライブラリのgetaddrinfo()を呼び出すと、/etc/nsswitch.confに設定された順序にしたがってhostsファイルやDNSクエリー発行でホスト名のIPアドレス変換をします。この時DNSクエリーの発行先は、/etc/resolve.confの設定に従います。

f:id:nopipi:20201116013457p:plain
getaddrinfo()の処理概要

処理概要

  1. アプリーケーションから、ホスト名の名前解決でCライブラリのgetaddrinfo()を呼び出します
  2. getaddrinfo()は、/etc/nsswitch.confを参照し解決順序を取得します
  3. nsswitch.confは一般にhostsファイルから参照する設定のため、最初に/etc/hostsの内容を参照し、該当するホスト名がないか確認します
  4. hotsファイルにない場合はDNSクエリーを発行してホスト名の解決を図ります
    1. DNSクエリーの発行先のDNSサーバ情報を/etc/resolve.confから取得します
    2. DNSでUPD処理をするためsocketを準備し、クエリーを発行し応答を受信します

getaddrinfo()実行時のstrace

getaddrinfo()挙動の参考としてstraceによるサンプルコード実行時のシステムコールのトレースを貼り付けます。

$ strace -F -tt ./sample_getaddrinfo google.com
strace: option -F is deprecated, please use -f instead
15:08:38.895790 execve("./sample_getaddrinfo", ["./sample_getaddrinfo", "google.com"], 0x7fff89a8f6b8 /* 22 vars */) = 0
<中略>
<-----------ここからgetaddrinfo()を呼び出した中の処理になります
<中略>
15:08:38.901343 open("/etc/nsswitch.conf", O_RDONLY|O_CLOEXEC) = 3<<<nsswitch.confで解決優先順位を確認
15:08:38.901539 fstat(3, {st_mode=S_IFREG|0644, st_size=1713, ...}) = 0
15:08:38.901680 read(3, "#\n# /etc/nsswitch.conf\n#\n# An ex"..., 4096) = 1713
<中略><<< /etc/hostsファイルを読み込み確認
15:08:38.902267 open("/etc/host.conf", O_RDONLY|O_CLOEXEC) = 3 
15:08:38.906723 fstat(3, {st_mode=S_IFREG|0644, st_size=126, ...}) = 0
15:08:38.907003 read(3, "127.0.0.1   localhost localhost."..., 4096) = 126
<中略><<<resolv.confを読み込みでDNSクエリー発行先を取得
15:08:38.902946 open("/etc/resolv.conf", O_RDONLY|O_CLOEXEC) = 3
15:08:38.903064 fstat(3, {st_mode=S_IFREG|0644, st_size=129, ...}) = 0
15:08:38.903266 read(3, "options timeout:2 attempts:5\n; g"..., 4096) = 129
<中略><<< UDPによるDNSサーバへの問い合わせ(問い合わせ先 = 10.1.0.2)
15:08:38.911102 socket(AF_INET, SOCK_DGRAM|SOCK_CLOEXEC|SOCK_NONBLOCK, IPPROTO_IP) = 3  <== ここから、DNS(UDP 53port)でDNSにクエリーを発行し応答を受信する処理になります
15:08:38.911335 connect(3, {sa_family=AF_INET, sin_port=htons(53), sin_addr=inet_addr("10.1.0.2")}, 16) = 0
15:08:38.911535 poll([{fd=3, events=POLLOUT}], 1, 0) = 1 ([{fd=3, revents=POLLOUT}])
15:08:38.911694 sendmmsg(3, [{msg_hdr={msg_name=NULL, msg_namelen=0, msg_iov=[{iov_base="8\276\1\0\0\1\0\0\0\0\0\0\6google\3com\0\0\1\0\1", iov_len=28}], msg_iovlen=1, msg_controllen=0, msg_flags=0}, msg_len=28}, {msg_hdr={msg_name=NULL, msg_namelen=0, msg_iov=[{iov_base="\276\316\1\0\0\1\0\0\0\0\0\0\6google\3com\0\0\34\0\1", iov_len=28}], msg_iovlen=1, msg_controllen=0, msg_flags=0}, msg_len=28}], 2, MSG_NOSIGNAL) = 2
15:08:38.912113 poll([{fd=3, events=POLLIN}], 1, 2000) = 1 ([{fd=3, revents=POLLIN}])
15:08:38.912375 ioctl(3, FIONREAD, [44]) = 0
15:08:38.912558 recvfrom(3, "8\276\201\200\0\1\0\1\0\0\0\0\6google\3com\0\0\1\0\1\300\f\0\1"..., 2048, 0, {sa_family=AF_INET, sin_port=htons(53), sin_addr=inet_addr("10.1.0.2")}, [28->16]) = 44
15:08:38.912858 poll([{fd=3, events=POLLIN}], 1, 1998) = 1 ([{fd=3, revents=POLLIN}])
15:08:38.913024 ioctl(3, FIONREAD, [56]) = 0
15:08:38.916333 recvfrom(3, "\276\316\201\200\0\1\0\1\0\0\0\0\6google\3com\0\0\34\0\1\300\f\0\34"..., 65536, 0, {sa_family=AF_INET, sin_port=htons(53), sin_addr=inet_addr("10.1.0.2")}, [28->16]) = 56
15:08:38.916608 close(3)                = 0
<中略>
<-----------ここまでgetaddrinfo()の処理
<中略>
15:08:38.924689 write(1, "addrinfo {\n", 11addrinfo {    <== 結果出力
) = 11
15:08:38.924772 write(1, "    int              ai_flags   "..., 39    int              ai_flags    =  0 
) = 39
15:

AWSで動くRHEL 7のレポジトリ名称がいつの間にか変わっていた件について(RHUI2->RHUI3)

RHEL7にdockerをインストールしようとして、二年前に確認した手順でセットアップしようとしたらうまくいかず、2時間ぐらい唸っていたら、いつの間にかRHUIがバージョンアップしてレポジトリ名称がごっそり変わっていたらしい。
access.redhat.com

もう無駄なところでハマるから、メジャーバージョンの中でこう言う変更するのやめてほしい。

BINDのResolverを UserDataとAWS CLIの"ec2 run-instances"で作成する

概要

作成する構成

検証などでVPCの中にDNSゾルバーを個別に立てる場合に、bindのインストールや設定をUserDataに埋め込んでしまうことで、OSへのログインをせずに簡単にbindによるDNSゾルバーを作ってしまう手順です。かつAWS CLIで作業しています。
前後作業がいろりろありますが、手っ取り早く該当部分を見たい方は"(4)DNSゾルバーの作成"を参照ください。
f:id:nopipi:20200623015246p:plain:w400

前提

  • bash環境での作業を前提としています
  • AWS CLI v2の環境で動作確認をしています(AWS CLI v1でも多分OKと思います)

設定手順

(1)準備

(1)-(a)作業環境の準備

AWS CLIで利用するプロファイルなどを設定します。

export PROFILE=default  #利用するプロファイルに変更してください
export AWS_PAGER=""
(1)-(a)作成に必要な情報設定

DNSゾルバーの作成先のVPCやメンテナンス用のSSHの接続情報を指定します。

#作成先VPC情報
VPCID="vpc-xxxxxxxxxxxxxxxxx"
VPC_CIDR="10.1.0.0/16"
DNS_RESOLVER_SUBNET="subnet-xxxxxxxxxxxxxxxxx"

#SSHメンテナンス接続情報
SSH_SRC_CIDR="0.0.0.0/0"   #メンテナンスでのSSHログイン元の CIDRを指定してください。
KEYNAME="CHANGE_KEY_PAIR_NAME"  #環境に合わせてキーペア名を設定してください。

(2)DNSゾルバ用セキュリティーグループの作成

VPC内からのDNSクエリーをセキュリティーグループで許可します。

# DNS用セキュリティーグループ作成
DNS_SG_ID=$(aws --profile ${PROFILE} --output text \
    ec2 create-security-group \
        --group-name DnsSG \
        --description "Allow Dns" \
        --vpc-id ${VPCID}) ;

aws --profile ${PROFILE} \
    ec2 create-tags \
        --resources ${DNS_SG_ID} \
        --tags "Key=Name,Value=DnsSG" ;

# セキュリティーグループにDNSのinboundアクセス許可を追加
aws --profile ${PROFILE} \
    ec2 authorize-security-group-ingress \
        --group-id ${DNS_SG_ID} \
        --protocol tcp \
        --port 53 \
        --cidr ${VPC_CIDR} ;

aws --profile ${PROFILE} \
    ec2 authorize-security-group-ingress \
        --group-id ${DNS_SG_ID} \
        --protocol udp \
        --port 53 \
        --cidr ${VPC_CIDR} ;

#メンテナンス用にSSHのinboundアクセス許可を追加
aws --profile ${PROFILE} \
    ec2 authorize-security-group-ingress \
        --group-id ${DNS_SG_ID} \
        --protocol tcp \
        --port 22 \
        --cidr ${SSH_SRC_CIDR};

(3)Amazon Linux2 AMI ID取得

#最新のAmazon Linux2のAMI IDを取得します。
AL2_AMIID=$(aws --profile ${PROFILE} --output text \
    ec2 describe-images \
        --owners amazon \
        --filters 'Name=name,Values=amzn2-ami-hvm-2.0.????????.?-x86_64-gp2' \
                  'Name=state,Values=available' \
        --query 'reverse(sort_by(Images, &CreationDate))[:1].ImageId' ) ;
echo  "AL2_AMIID = ${AL2_AMIID}"

(4)DNSゾルバーの作成

DNSゾルバー用のEC2インスタンスを作成します。
bindの設定は、EC2インスタンスのUserDataに定義して、起動時にスクリプト実行によりセットアップさせます。BINDのforwarding先のDNSサーバを変更する場合はforwarders { 8.8.8.8; 8.8.4.4; };の部分を修正して下さい。

TAGJSON='
[
    {
        "ResourceType": "instance",
        "Tags": [
            {
                "Key": "Name",
                "Value": "Dns"
            }
        ]
    }
]'

USER_DATA='#!/bin/bash -xe
                
yum -y update
yum -y install bind bind-utils
hostnamectl set-hostname dns

LOCAL_IP=$(curl http://169.254.169.254/latest/meta-data/local-ipv4)

cat > /etc/named.conf << EOL
# アクセス制御。trustというグループに属するIPアドレスを定義する。
acl "trust" {
        '"${VPC_CIDR}"';
        127.0.0.1;
};

options {
        # UDP53でDNSクエリを受け付ける自分自身のIPアドレス
        listen-on port 53 {
                127.0.0.1;
                ${LOCAL_IP};
        };
        # IPv6は使わないのでnone
        listen-on-v6 port 53 { none; };
        directory       "/var/named";
        dump-file       "/var/named/data/cache_dump.db";
        statistics-file "/var/named/data/named_stats.txt";
        memstatistics-file "/var/named/data/named_mem_stats.txt";

        # DNSクエリはaclで設定した送信元のみ許可
        allow-query     { trust; };
        allow-query-cache { trust; };

        # 再帰問い合わせもaclで問い合わせした送信元のみ許可
        recursion yes;
        allow-recursion { trust; };

        # DNS問い合わせの転送先
        # Google Public DNSを利用します。
        forwarders { 8.8.8.8; 8.8.4.4; };
        # 問い合わせの転送に失敗した場合は自分自身で名前解決を行う
        # 問い合わせ転送に失敗した際名前解決をあきらめる場合はonlyを設定する
        forward only;

        dnssec-enable yes;
        dnssec-validation yes;

        bindkeys-file "/etc/named.iscdlv.key";

        managed-keys-directory "/var/named/dynamic";

        pid-file "/run/named/named.pid";
        session-keyfile "/run/named/session.key";
};

logging {
        channel default_debug {
                file "data/named.run";
                severity dynamic;
        };
        # DNSクエリログ用の出力設定
        channel query-log {
                # 以下すべてのチャネルで3世代10Mごとにログローテーションを行う
                file "/var/log/named/query.log" versions 3 size 10M;
                severity  info;
                print-category yes;
                print-severity yes;
                print-time yes;
        };
        # ゾーン転送ログ用の出力設定
        channel xfer-log {
                file "/var/log/named/xfer.log" versions 3 size 10M;
                severity  info;
                print-category yes;
                print-severity yes;
                print-time yes;
        };
        # 上記以外の種類のエラーログ用の出力設定
        channel error-log {
                file "/var/log/named/error.log" versions 3 size 10M;
                severity  error;
                print-category yes;
                print-severity yes;
                print-time yes;
        };
 
        # ログ種別ごとの出力先設定指定
        category queries { query-log; };
        category xfer-in { xfer-log; };
        category xfer-out { xfer-log; };
        category default { error-log; };
};

zone "." IN {
        type hint;
        file "named.ca";
};

include "/etc/named.rfc1912.zones";
include "/etc/named.root.key";
EOL

#ログ用フォルダの作成
mkdir /var/log/named
chown -R named:named /var/log/named/

# Bindの起動
systemctl enable named
systemctl start named
'

# DNSサーバの起動
aws --profile ${PROFILE} \
    ec2 run-instances \
        --image-id ${AL2_AMIID} \
        --instance-type t2.micro \
        --key-name ${KEYNAME} \
        --subnet-id ${DNS_RESOLVER_SUBNET} \
        --security-group-ids ${DNS_SG_ID}\
        --associate-public-ip-address \
        --tag-specifications "${TAGJSON}" \
        --user-data "${USER_DATA}" ;

(5)VPC DHCPオプションセットの変更

作成したDNSゾルバーにDNSのクエリーが飛ぶように、新しいDHCPオプションセットを作成しVPCにアタッチします。新しいDHCPオプションでは、下記を設定しています。

  • DNS参照先設定に、作成したDNSゾルバーを指定
  • NTPの同期先にAmazon Time Sync Service(169.254.169.123)を指定
#DNSサーバのローカルIP取得
DnsLocalIP=$(aws --profile ${PROFILE} --output text \
    ec2 describe-instances \
        --filter "Name=tag:Name,Values=Dns" "Name=instance-state-name,Values=running"  \
    --query 'Reservations[].Instances[].PrivateIpAddress' \
)
echo ${DnsLocalIP}

#VPC: DHCPオプションセット作成
DHCPSET_ID=$(aws --profile ${PROFILE} --output text \
    ec2 create-dhcp-options \
        --dhcp-configurations \
            "Key=domain-name,Values=onprem.internal" \
            "Key=domain-name-servers,Values=${DnsLocalIP}" \
            "Key=ntp-servers,Values=169.254.169.123" \
    --query 'DhcpOptions.DhcpOptionsId'; )

#VPC: DHCPオプションセット関連付け
aws --profile ${PROFILE} \
    ec2 associate-dhcp-options \
      --vpc-id ${VPCID} \
      --dhcp-options-id ${DHCPSET_ID} ;

(6)Client作成

(6)-(a)用セキュリティグループ作成
# DNS用セキュリティーグループ作成
CLIENT_SG_ID=$(aws --profile ${PROFILE} --output text \
    ec2 create-security-group \
        --group-name ClientSg \
        --description "Allow ssh" \
        --vpc-id ${VPCID}) ;

aws --profile ${PROFILE} \
    ec2 create-tags \
        --resources ${CLIENT_SG_ID} \
        --tags "Key=Name,Value=ClientSg" ;

#メンテナンス用にSSHのinboundアクセス許可を追加
aws --profile ${PROFILE} \
    ec2 authorize-security-group-ingress \
        --group-id ${CLIENT_SG_ID} \
        --protocol tcp \
        --port 22 \
        --cidr ${SSH_SRC_CIDR};
(6)-(b) Clientインスタンス作成
#クライアントインスタンス作成
TAGJSON='
[
    {
        "ResourceType": "instance",
        "Tags": [
            {
                "Key": "Name",
                "Value": "Client"
            }
        ]
    }
]'

#ユーザデータ設定
USER_DATA='#!/bin/bash -xe
yum -y update
hostnamectl set-hostname client
'

#インスタンス起動
aws --profile ${PROFILE} \
    ec2 run-instances \
        --image-id ${AL2_AMIID} \
        --instance-type t2.micro \
        --key-name ${KEYNAME} \
        --subnet-id ${DNS_RESOLVER_SUBNET} \
        --security-group-ids ${CLIENT_SG_ID}\
        --associate-public-ip-address \
        --tag-specifications "${TAGJSON}" \
        --user-data "${USER_DATA}"; 

(7)動作テスト

(7)-(a) ClientへのSSHログイン
ssh ec2-user@<clientのIP>
(7)-(b) Client確認
#DNS参照先確認
# /etc/resolv.confの設定確認
cat /etc/resolv.conf 
; generated by /usr/sbin/dhclient-script
search onprem.internal
options timeout:2 attempts:5
nameserver 10.1.x.x  <== DNSリゾルバーインスタンスの Private IPが設定されていることを確認

# digによるDNS参照
dig amazon.co.jp

; <<>> DiG 9.11.4-P2-RedHat-9.11.4-9.P2.amzn2.0.3 <<>> amazon.co.jp
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 51709
;; flags: qr rd ra; QUERY: 1, ANSWER: 3, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 4096
;; QUESTION SECTION:
;amazon.co.jp.			IN	A

;; ANSWER SECTION:  <<==DNS参照の応答が正常にあることを確認
amazon.co.jp.		46	IN	A	52.119.168.48
amazon.co.jp.		46	IN	A	52.119.164.121
amazon.co.jp.		46	IN	A	52.119.161.5

;; Query time: 7 msec
;; SERVER: 10.1.0.14#53(10.1.0.14)   <==DNSリゾルバーインスタンスの Private IPであることを確認
;; WHEN: 月  6月 22 17:33:12 UTC 2020
;; MSG SIZE  rcvd: 89

Amazon S3のバケットのバージョニング設定とオブジェクトのバージョンの遷移を確認してみた

はじめに

バケットのバージョニング設定により、オブジェクトのバージョンがどのように遷移するのかがいまいちわかっていなかったので、実際に動かして整理してみました。

まとめ

  • バケットのバージョニング未設定の場合、オブジェクトのVersionIdはnullになる
  • バージョニングを有効化した後は
    • 既存オブジェクトのVersionIdはnullのまま
    • バージョニング有効化後にPUTしたオブジェクトには、VersionIdに一意のIDが付与
  • バージョニングをサスペンドした後は
    • サスペンド後にPUTしたオブジェクトのVersionIdは、nullになる
    • 既存でVersionIdがnullのオブジェクトが存在する場合、そのオブジェクトが更新される
    • 既存にVersionIdがnullのオブジェクトが存在しない場合は、新規にVersionIdがnullのオブジェクトが作成される

バージョニング有効化前から存在するオブジェクトのバージョン遷移

f:id:nopipi:20200601164859p:plain:w600

バージョニング有効化後に新規にPUTしたオブジェクトのバージョン遷移

f:id:nopipi:20200601165631p:plain:w600

確認手順と結果

事前準備

#プロファイル指定
PROFILE=default #他のプロファイルを利用する場合はここを変更

#検証用バケットの設定用パラメータ
BUCKET_NAME="versioning-test-bucket-$( od -vAn -to1 </dev/urandom  | tr -d " " | fold -w 10 | head -n 1)"
REGION=$(aws --profile ${PROFILE} configure get region)

#テストデータ生成
dd if=/dev/urandom of=test-data-1.dat bs=4096 count=1
dd if=/dev/urandom of=test-data-2.dat bs=4096 count=2
dd if=/dev/urandom of=test-data-3.dat bs=4096 count=3

ls -l test*

バケットの作成

#バケット作成
aws --profile ${PROFILE} \
    s3api create-bucket \
        --bucket ${BUCKET_NAME} \
        --create-bucket-configuration LocationConstraint=${REGION};

#バージョン設定の確認
#未設定なので何もレスポンスが無いはずです。
aws --profile ${PROFILE} s3api get-bucket-versioning --bucket "${BUCKET_NAME}"

#データが空であることを確認
aws --profile ${PROFILE} s3 ls "s3://${BUCKET_NAME}"

バージョニング未設定時のオブジェクト確認

オブジェクトをPUTする
#バージョニングを設定していない状態でS3にデータを確認
aws --profile ${PROFILE} s3 cp test-data-1.dat s3://${BUCKET_NAME}/userdata01.dat
aws --profile ${PROFILE} s3 cp test-data-1.dat s3://${BUCKET_NAME}/userdata02.dat
状態を確認する

(1) オブジェクトの一覧表示
PUTした2つのオブジェクトが表示されます。

aws --profile ${PROFILE} s3 ls "s3://${BUCKET_NAME}"
2020-05-31 18:49:51       4096 userdata01.dat
2020-05-31 18:49:52       4096 userdata02.dat

(2) オブジェクトのバージョン確認
バケットに対してバージョニングが未設定の状態でオブジェクトをPUTしているため、userdata01.dat、userdata02.datともVersionIdは"null"になります。

Object 投入タイミング 更新時間 VersionID IsLatest
userdata01.dat Versioning設定前 09:49:51 null true
userdata02.dat Versioning設定前 09:49:52 null true
aws --profile ${PROFILE} s3api list-object-versions --bucket ${BUCKET_NAME}
{
    "Versions": [
        {
            "ETag": "\"dadc49ab6df3592cca0ce5edd9e03886\"",
            "Size": 4096,
            "StorageClass": "STANDARD",
            "Key": "userdata01.dat",
            "VersionId": "null",
            "IsLatest": true,
            "LastModified": "2020-05-31T09:49:51.000Z",
            "Owner": {
                "DisplayName": "nobuyuf",
                "ID": "135708a85354d2fa2c74a4f52b3a2256c9ce912005f4677f36fd73af25be2793"
            }
        },
        {
            "ETag": "\"dadc49ab6df3592cca0ce5edd9e03886\"",
            "Size": 4096,
            "StorageClass": "STANDARD",
            "Key": "userdata02.dat",
            "VersionId": "null",
            "IsLatest": true,
            "LastModified": "2020-05-31T09:49:52.000Z",
            "Owner": {
                "DisplayName": "nobuyuf",
                "ID": "135708a85354d2fa2c74a4f52b3a2256c9ce912005f4677f36fd73af25be2793"
            }
        }
    ]
}

バージョニング有効化

バケットに対してバージョニングを有効にします。

バージョニング有効化設定
aws --profile ${PROFILE} s3api \
    put-bucket-versioning \
        --bucket "${BUCKET_NAME}" --versioning-configuration Status=Enabled;
設定の確認
aws --profile ${PROFILE} s3api get-bucket-versioning --bucket "${BUCKET_NAME}"
{
    "Status": "Enabled"
}

バージョニング有効後にPUTしたオブジェクト確認

  • 既存のuserdata02.datに新たなファイルをPUTします
  • 新規にuserdata03.datをPUTします
データの投入
aws --profile ${PROFILE} s3 cp test-data-2.dat s3://${BUCKET_NAME}/userdata02.dat
aws --profile ${PROFILE} s3 cp test-data-2.dat s3://${BUCKET_NAME}/userdata03.dat
状態を確認する

(1) オブジェクトの一覧表示
userdata02.datが更新され、新規にuserdata03.datが追加され、合計3つのオブジェクトが表示されます。

aws --profile ${PROFILE} s3 ls "s3://${BUCKET_NAME}"
2020-05-31 18:49:51       4096 userdata01.dat
2020-06-01 14:52:14       8192 userdata02.dat
2020-06-01 14:52:16       8192 userdata03.dat

(2) オブジェクトのバージョン確認

  • バージョニング設定前のオブジェクトは、VersionIDがnullのまま
  • userdata02.datは、既存のオブジェクトIsLatestがfalseに変更
Object 投入タイミング 更新時間 VersionID IsLatest メモ
userdata01.dat Versioning設定前 09:49:51 null true 更新なし
userdata02.dat Versioning設定後(1) 05:52:14 0qDa.... true 新規追加
userdata02.dat Versioning設定前 09:49:52 null false LsLatestがfalseに変更
userdata03.dat Versioning設定後(1) 05:52:16 TX.M.... true 新規追加
aws --profile ${PROFILE} s3api list-object-versions --bucket ${BUCKET_NAME}
{
    "Versions": [
        {
            "ETag": "\"dadc49ab6df3592cca0ce5edd9e03886\"",
            "Size": 4096,
            "StorageClass": "STANDARD",
            "Key": "userdata01.dat",
            "VersionId": "null",
            "IsLatest": true,
            "LastModified": "2020-05-31T09:49:51.000Z",
            "Owner": {
                "DisplayName": "nobuyuf",
                "ID": "135708a85354d2fa2c74a4f52b3a2256c9ce912005f4677f36fd73af25be2793"
            }
        },
        {
            "ETag": "\"a41d74143fff00d45a6b9997e400a32a\"",
            "Size": 8192,
            "StorageClass": "STANDARD",
            "Key": "userdata02.dat",
            "VersionId": "0qDa2pi27gEqqGOdQhK5UVJVcBj_drRr",
            "IsLatest": true,
            "LastModified": "2020-06-01T05:52:14.000Z",
            "Owner": {
                "DisplayName": "nobuyuf",
                "ID": "135708a85354d2fa2c74a4f52b3a2256c9ce912005f4677f36fd73af25be2793"
            }
        },
        {
            "ETag": "\"dadc49ab6df3592cca0ce5edd9e03886\"",
            "Size": 4096,
            "StorageClass": "STANDARD",
            "Key": "userdata02.dat",
            "VersionId": "null",
            "IsLatest": false,
            "LastModified": "2020-05-31T09:49:52.000Z",
            "Owner": {
                "DisplayName": "nobuyuf",
                "ID": "135708a85354d2fa2c74a4f52b3a2256c9ce912005f4677f36fd73af25be2793"
            }
        },
        {
            "ETag": "\"a41d74143fff00d45a6b9997e400a32a\"",
            "Size": 8192,
            "StorageClass": "STANDARD",
            "Key": "userdata03.dat",
            "VersionId": "TX.MGfR96qMJ4mrIFBT61d8.Bs5McjzB",
            "IsLatest": true,
            "LastModified": "2020-06-01T05:52:16.000Z",
            "Owner": {
                "DisplayName": "nobuyuf",
                "ID": "135708a85354d2fa2c74a4f52b3a2256c9ce912005f4677f36fd73af25be2793"
            }
        }
    ]
}

もう一度オブジェクトをPUTし確認

データを投入
aws --profile ${PROFILE} s3 cp test-data-3.dat s3://${BUCKET_NAME}/userdata02.dat
aws --profile ${PROFILE} s3 cp test-data-3.dat s3://${BUCKET_NAME}/userdata03.dat
状態を確認する

(1) オブジェクトの一覧表示
userdata02.datとuserdata03.datが更新され、同じく3つのオブジェクトが表示されます。

aws --profile ${PROFILE} s3 ls "s3://${BUCKET_NAME}"
2020-05-31 18:49:51       4096 userdata01.dat
2020-06-01 15:27:39      12288 userdata02.dat
2020-06-01 15:27:41      12288 userdata03.dat

(2) オブジェクトのバージョン確認

  • userdata02.datとuserdata03.datで、IsLatestがtrueのオブジェクトが追加され、既存オブジェクトはLsLatestがfalseに変更
Object 投入タイミング 更新時間 VersionID IsLatest メモ
userdata01.dat Versioning設定前 09:49:51 null true 更新なし
userdata02.dat Versioning設定後(2) 06:27:39 Um.k.... true 新規追加
userdata02.dat Versioning設定後(1) 05:52:14 0qDa.... false LsLatestがfalseに変更
userdata02.dat Versioning設定前 09:49:52 null false 更新なし
userdata03.dat Versioning設定後(2) 06:27:41 WZ8d.... true 新規追加
userdata03.dat Versioning設定後(1) 05:52:16 TX.M.... false LsLatestがfalseに変更
aws --profile ${PROFILE} s3api list-object-versions --bucket ${BUCKET_NAME}
{
    "Versions": [
        {
            "ETag": "\"dadc49ab6df3592cca0ce5edd9e03886\"",
            "Size": 4096,
            "StorageClass": "STANDARD",
            "Key": "userdata01.dat",
            "VersionId": "null",
            "IsLatest": true,
            "LastModified": "2020-05-31T09:49:51.000Z",
            "Owner": {
                "DisplayName": "nobuyuf",
                "ID": "135708a85354d2fa2c74a4f52b3a2256c9ce912005f4677f36fd73af25be2793"
            }
        },
        {
            "ETag": "\"c3cfd43243650a7c2095f744d1ad796e\"",
            "Size": 12288,
            "StorageClass": "STANDARD",
            "Key": "userdata02.dat",
            "VersionId": "Um.kAFeAkaayk3botsDRJmyvLQp6dUZr",
            "IsLatest": true,
            "LastModified": "2020-06-01T06:27:39.000Z",
            "Owner": {
                "DisplayName": "nobuyuf",
                "ID": "135708a85354d2fa2c74a4f52b3a2256c9ce912005f4677f36fd73af25be2793"
            }
        },
        {
            "ETag": "\"a41d74143fff00d45a6b9997e400a32a\"",
            "Size": 8192,
            "StorageClass": "STANDARD",
            "Key": "userdata02.dat",
            "VersionId": "0qDa2pi27gEqqGOdQhK5UVJVcBj_drRr",
            "IsLatest": false,
            "LastModified": "2020-06-01T05:52:14.000Z",
            "Owner": {
                "DisplayName": "nobuyuf",
                "ID": "135708a85354d2fa2c74a4f52b3a2256c9ce912005f4677f36fd73af25be2793"
            }
        },
        {
            "ETag": "\"dadc49ab6df3592cca0ce5edd9e03886\"",
            "Size": 4096,
            "StorageClass": "STANDARD",
            "Key": "userdata02.dat",
            "VersionId": "null",
            "IsLatest": false,
            "LastModified": "2020-05-31T09:49:52.000Z",
            "Owner": {
                "DisplayName": "nobuyuf",
                "ID": "135708a85354d2fa2c74a4f52b3a2256c9ce912005f4677f36fd73af25be2793"
            }
        },
        {
            "ETag": "\"c3cfd43243650a7c2095f744d1ad796e\"",
            "Size": 12288,
            "StorageClass": "STANDARD",
            "Key": "userdata03.dat",
            "VersionId": "WZ8dIE4sY1OzI1h6lCzESy_.W33I4xtt",
            "IsLatest": true,
            "LastModified": "2020-06-01T06:27:41.000Z",
            "Owner": {
                "DisplayName": "nobuyuf",
                "ID": "135708a85354d2fa2c74a4f52b3a2256c9ce912005f4677f36fd73af25be2793"
            }
        },
        {
            "ETag": "\"a41d74143fff00d45a6b9997e400a32a\"",
            "Size": 8192,
            "StorageClass": "STANDARD",
            "Key": "userdata03.dat",
            "VersionId": "TX.MGfR96qMJ4mrIFBT61d8.Bs5McjzB",
            "IsLatest": false,
            "LastModified": "2020-06-01T05:52:16.000Z",
            "Owner": {
                "DisplayName": "nobuyuf",
                "ID": "135708a85354d2fa2c74a4f52b3a2256c9ce912005f4677f36fd73af25be2793"
            }
        }
    ]
}

バージョニング をsuspend

バケットのバージョニングをsuspendします。

バージョニングのsuspend設定
aws --profile ${PROFILE} s3api \
    put-bucket-versioning \
        --bucket "${BUCKET_NAME}" --versioning-configuration Status=Suspended;
設定の確認
aws --profile ${PROFILE} s3api get-bucket-versioning --bucket "${BUCKET_NAME}"
{
    "Status": "Suspended"
}

Suspended状態でオブジェクトをPUTし確認

データを投入
aws --profile ${PROFILE} s3 cp test-data-3.dat s3://${BUCKET_NAME}/userdata02.dat
aws --profile ${PROFILE} s3 cp test-data-3.dat s3://${BUCKET_NAME}/userdata03.dat
状態を確認する

(1) オブジェクトの一覧表示
userdata02.datとuserdata03.datが更新され、同じく3つのオブジェクトが表示されます。

aws --profile ${PROFILE} s3 ls "s3://${BUCKET_NAME}"
2020-05-31 18:49:51       4096 userdata01.dat
2020-06-01 15:46:08      12288 userdata02.dat
2020-06-01 15:46:10      12288 userdata03.dat

(2) オブジェクトのバージョン確認

Object 投入タイミング 更新時間 VersionID IsLatest メモ
userdata01.dat Versioning設定前 09:49:51 null true 更新なし
userdata02.dat Suspended状態 06:46:08 null true 既存のnullバージョンのものを更新
userdata02.dat Versioning設定後(2) 06:27:39 Um.k.... false LsLatestがfalseに変更
userdata02.dat Versioning設定後(1) 05:52:14 0qDa.... false 更新なし
userdata02.dat Versioning設定前 - - - 上書き更新され消滅
userdata03.dat Suspended状態 T06:46:10 null true 新規追加
userdata03.dat Versioning設定後(2) 06:27:41 WZ8d.... false LsLatestがfalseに変更
userdata03.dat Versioning設定後(1) 05:52:16 TX.M.... false 更新なし
aws --profile ${PROFILE} s3api list-object-versions --bucket ${BUCKET_NAME}
{
    "Versions": [
        {
            "ETag": "\"dadc49ab6df3592cca0ce5edd9e03886\"",
            "Size": 4096,
            "StorageClass": "STANDARD",
            "Key": "userdata01.dat",
            "VersionId": "null",
            "IsLatest": true,
            "LastModified": "2020-05-31T09:49:51.000Z",
            "Owner": {
                "DisplayName": "nobuyuf",
                "ID": "135708a85354d2fa2c74a4f52b3a2256c9ce912005f4677f36fd73af25be2793"
            }
        },
        {
            "ETag": "\"c3cfd43243650a7c2095f744d1ad796e\"",
            "Size": 12288,
            "StorageClass": "STANDARD",
            "Key": "userdata02.dat",
            "VersionId": "null",
            "IsLatest": true,
            "LastModified": "2020-06-01T06:46:08.000Z",
            "Owner": {
                "DisplayName": "nobuyuf",
                "ID": "135708a85354d2fa2c74a4f52b3a2256c9ce912005f4677f36fd73af25be2793"
            }
        },
        {
            "ETag": "\"c3cfd43243650a7c2095f744d1ad796e\"",
            "Size": 12288,
            "StorageClass": "STANDARD",
            "Key": "userdata02.dat",
            "VersionId": "Um.kAFeAkaayk3botsDRJmyvLQp6dUZr",
            "IsLatest": false,
            "LastModified": "2020-06-01T06:27:39.000Z",
            "Owner": {
                "DisplayName": "nobuyuf",
                "ID": "135708a85354d2fa2c74a4f52b3a2256c9ce912005f4677f36fd73af25be2793"
            }
        },
        {
            "ETag": "\"a41d74143fff00d45a6b9997e400a32a\"",
            "Size": 8192,
            "StorageClass": "STANDARD",
            "Key": "userdata02.dat",
            "VersionId": "0qDa2pi27gEqqGOdQhK5UVJVcBj_drRr",
            "IsLatest": false,
            "LastModified": "2020-06-01T05:52:14.000Z",
            "Owner": {
                "DisplayName": "nobuyuf",
                "ID": "135708a85354d2fa2c74a4f52b3a2256c9ce912005f4677f36fd73af25be2793"
            }
        },
        {
            "ETag": "\"c3cfd43243650a7c2095f744d1ad796e\"",
            "Size": 12288,
            "StorageClass": "STANDARD",
            "Key": "userdata03.dat",
            "VersionId": "null",
            "IsLatest": true,
            "LastModified": "2020-06-01T06:46:10.000Z",
            "Owner": {
                "DisplayName": "nobuyuf",
                "ID": "135708a85354d2fa2c74a4f52b3a2256c9ce912005f4677f36fd73af25be2793"
            }
        },
        {
            "ETag": "\"c3cfd43243650a7c2095f744d1ad796e\"",
            "Size": 12288,
            "StorageClass": "STANDARD",
            "Key": "userdata03.dat",
            "VersionId": "WZ8dIE4sY1OzI1h6lCzESy_.W33I4xtt",
            "IsLatest": false,
            "LastModified": "2020-06-01T06:27:41.000Z",
            "Owner": {
                "DisplayName": "nobuyuf",
                "ID": "135708a85354d2fa2c74a4f52b3a2256c9ce912005f4677f36fd73af25be2793"
            }
        },
        {
            "ETag": "\"a41d74143fff00d45a6b9997e400a32a\"",
            "Size": 8192,
            "StorageClass": "STANDARD",
            "Key": "userdata03.dat",
            "VersionId": "TX.MGfR96qMJ4mrIFBT61d8.Bs5McjzB",
            "IsLatest": false,
            "LastModified": "2020-06-01T05:52:16.000Z",
            "Owner": {
                "DisplayName": "nobuyuf",
                "ID": "135708a85354d2fa2c74a4f52b3a2256c9ce912005f4677f36fd73af25be2793"
            }
        }
    ]
}

検証データのクリーンナップ

オブジェクトの削除
# 削除マーカーがついているオブジェクトの削除
aws --profile ${PROFILE} --output text \
      s3api list-object-versions \
        --bucket ${BUCKET_NAME} \
    --query 'DeleteMarkers[].{Key:Key,VersionId:VersionId}' | while read key versionid
do
    aws --profile ${PROFILE} \
        s3api delete-object \
            --bucket ${BUCKET_NAME} \
            --key ${key} \
            --version-id ${versionid}
done 

# それ以外のオブジェクトの削除
aws --profile ${PROFILE} --output text \
      s3api list-object-versions \
        --bucket ${BUCKET_NAME} \
    --query 'Versions[].{Key:Key,VersionId:VersionId}' | while read key versionid
do
    aws --profile ${PROFILE} \
        s3api delete-object \
            --bucket ${BUCKET_NAME} \
            --key ${key} \
            --version-id ${versionid}
done 
バケットの削除
aws --profile ${PROFILE} \
    s3api delete-bucket \
        --bucket "${BUCKET_NAME}"