ECS タスク起動と SSM 登録を自動連携してみる

記事タイトルとURLをコピーする

雨の日が気持ち良い季節ですね。
プロセスエンジニアリング課の礒です。こんにちは。

今回は ECS Fargateタスクに SSM からログインしたい方向けのTipsをご紹介します。

概要

まず、Amazon EC2 (以下 "EC2" )インスタンスや Fargate コンテナに AWS Systems Manager (以下 "SSM" )からログインするには、以下の作業が必要です。
(各機能の詳細については本記事では割愛させていただきます。)

  • [インスタンス側] SSM エージェントのインストール
  • [ SSM 側] アクティベーションの作成
  • [ SSM 側] マネージドインスタンスの登録

この記事では、 ECS タスクと SSM を連携させて上記登録作業とその解除作業を自動でおこなうための設定をご説明します。
AWS CLIを利用しますので、お手元に実行環境をご用意ください。

実行環境

OS: Amazon Linux 2
AWS CLI: v2.2.27

SSM セッションマネージャーのインスタンス枠の変更

SSM セッションマネージャーから Fargate タスクに接続するには、「インスタンス枠」の設定をデフォルト値から変更する必要があります。
( EC2 インスタンスを登録する場合はこの設定は不要です。)

設定の反映に時間がかかるので、先にやっておきましょう。

AWSコンソールで SSM の画面を開き、「フリートマネージャー」をクリックします。
f:id:swx-iso:20210810220543j:plain

設定>アカウント設定の変更 をクリックします。
f:id:swx-iso:20210810220629j:plain

チェックを入れて「設定の変更」 をクリックします。 f:id:swx-iso:20210818102022j:plain

Docker イメージの管理リポジトリを作成

これから作成するイメージを push するための ECR リポジトリ「test-repo」を作成します。
コマンドを実行して返ってきた repositoryUri の値は後で使うので控えておきましょう。

$aws ecr create-repository --repository-name test-repo
{
    "repository": {
        "repositoryArn": "arn:aws:ecr:ap-northeast-1:000000000000:repository/test-repo",
        "registryId": "000000000000",
        "repositoryName": "test-repo",
        "repositoryUri": "000000000000.dkr.ecr.ap-northeast-1.amazonaws.com/test-repo",
        "createdAt": "2021-08-10T20:05:09+09:00",
        "imageTagMutability": "MUTABLE",
        "imageScanningConfiguration": {
            "scanOnPush": false
        },
        "encryptionConfiguration": {
            "encryptionType": "AES256"
        }
    }
}

Dockerイメージの作成

プロジェクト用ディレクトリを作成します。

sample_project/
├── Dockerfile
└── ssm-entrypoint.sh

・Dockerfile

# 任意のイメージ
FROM tomcat:9.0.50-jdk11-openjdk

COPY ssm-entrypoint.sh /usr/local/bin/ssm-entrypoint.sh

# ssm-user アカウントの sudo アクセス許可設定
RUN adduser ssm-user && \
    echo "ssm-user ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/ssm-agent-users

# awscliのインストール
RUN curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip" \
  && unzip awscliv2.zip \
  && sudo ./aws/install

# SSM エージェントをインストールし、CloudWatch にログを送信するように設定
RUN mkdir /tmp/ssm \
  && cd /tmp/ssm \
  && wget https://s3.amazonaws.com/ec2-downloads-windows/SSMAgent/latest/debian_amd64/amazon-ssm-agent.deb -O /tmp/ssm/amazon-ssm-agent.deb \
  && sudo dpkg -i /tmp/ssm/amazon-ssm-agent.deb \
  && cp /etc/amazon/ssm/seelog.xml.template /etc/amazon/ssm/seelog.xml

# 片付け
RUN rm -f awscliv2.zip \
  && rm -f /tmp/ssm/amazon-ssm-agent.deb

# コンテナ開始時にssm-entrypoint.sh
ENTRYPOINT ["sudo", "-E", "/bin/bash", "-c", "/usr/local/bin/ssm-entrypoint.sh"]

参考:
docs.aws.amazon.com

docs.aws.amazon.com

・ssm-entrypoint.sh

#!/bin/bash
set -e

# 自身のマネージドインスタンス ID が記載されているファイルの指定 (↓のパス固定で OK )
export REGISTRATION_FILE="/var/lib/amazon/ssm/registration"
cleanup() {
    # コンテナ終了時、マネージドインスタンス登録を解除
    echo "--- deregister managed instance"
    aws ssm deregister-managed-instance --instance-id "$(cat "${REGISTRATION_FILE}" | jq -r .ManagedInstanceID)" || true
    exit 0
}
trap "cleanup" EXIT  # コンテナ停止をトリガーに cleanup 関数を実行

# アクティベーションの作成
echo "--- create SSM activation"
ACTIVATION_PARAMETERS=$(aws ssm create-activation \
    --default-instance-name "ecs-task" \
    --description "ecs-task" \
    --iam-role "IAMRoleForSSM" \
    --region "ap-northeast-1")

# アクティベーションの作成結果からアクティベーションコード/ ID を抽出
export ACTIVATION_CODE=$(echo $ACTIVATION_PARAMETERS | jq -r .ActivationCode)
export ACTIVATION_ID=$(echo $ACTIVATION_PARAMETERS | jq -r .ActivationId)

# アクティベーションコード/ ID を利用してマネージドインスタンスを登録
echo "--- register SSM instance"
sudo amazon-ssm-agent -register -code "${ACTIVATION_CODE}" -id "${ACTIVATION_ID}" -region "ap-northeast-1" -y

# ハイブリッドアクティベーションのコードはもう必要ないため削除
echo "--- delete SSM activation"
aws ssm delete-activation --activation-id "${ACTIVATION_ID}"

# SSMエージェントの登録
echo "--- start SSM agent"
nohup sudo amazon-ssm-agent

参考: docs.docker.jp

docs.aws.amazon.com

Dockerfileとssm-entrypoint.shが作成できたら、ビルドしてECRリポジトリにpushしましょう。

# build
docker build -t test-repo .

# push用タグづけ
docker tag test-repo:latest 000000000000.dkr.ecr.ap-northeast-1.amazonaws.com/test-repo:latest

# Docker レジストリ・サーバにログイン
aws ecr get-login-password --region ap-northeast-1 | docker login --username AWS --password-stdin 000000000000.dkr.ecr.ap-northeast-1.amazonaws.com

# push
docker push 000000000000.dkr.ecr.ap-northeast-1.amazonaws.com/test-repo:latest

ECS クラスター・ ECS タスクの作成

CloudFormationでテンプレートをアップロードします。
f:id:swx-iso:20210810212441j:plain

・cloudformation.yml

# 前提条件: ECS配置用のPublic Subnetが構築済みであること
AWSTemplateFormatVersion: "2010-09-09"
Description: "ECS test"
Parameters:
  GroupId:
    Description: "(Required) Enter group id for tag."
    MinLength: 1
    Type: "String"
  VpcId:
    Description: "(Required) Select vpc id."
    MinLength: 1
    Type: "AWS::EC2::VPC::Id"
  EcsPublicSubnetIdAz1:
    Description: "(Required) Select public subnet id for ECS." # CloudWatchLogsにログ出力するので、PublicSubnetを指定する。
    MinLength: 1
    Type : "AWS::EC2::Subnet::Id"
  EcsPublicSubnetIdAz2:
    Description: "(Required) Select public subnet id for ECS." # CloudWatchLogsにログ出力するので、PublicSubnetを指定する。
    MinLength: 1
    Type : "AWS::EC2::Subnet::Id"
  EcrRepositoryArn:
    Description: "(Required) Enter ECR repository URI."
    MinLength: 1
    Type: "String"
Metadata:
  AWS::CloudFormation::Interface:
    ParameterGroups:
      - Label:
          default: "Group Configuration"
        Parameters:
          - "GroupId"
          - "VpcId"
      - Label:
          default: "ECS Configuration"
        Parameters:
          - "EcsPublicSubnetIdAz1"
          - "EcsPublicSubnetIdAz2"
          - "EcrRepositoryArn"
Resources:
  IAMRoleForSSM:
    Type: "AWS::IAM::Role"
    DeletionPolicy: "Delete"
    Properties:
      AssumeRolePolicyDocument: 
        Version:  "2012-10-17"
        Statement:
        - Effect: "Allow"
          Principal:
            Service: "ssm.amazonaws.com"
          Action: "sts:AssumeRole"
      Description: "allow ECS task to use SSM agent"
      ManagedPolicyArns: 
        - "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"
      Policies: 
        - PolicyName: "getInformation"
          PolicyDocument:
            "Version": "2012-10-17"
            "Statement":
              - "Effect": "Allow"
                "Action":
                    # マネージドインスタンスを登録解除する際はこのIAMロールの権限が必要
                    - "ssm:DeregisterManagedInstance"
                "Resource": "*"
      RoleName: "IAMRoleForSSM"
      Tags: 
        - Key: "Name"
          Value: "IAMRoleForSSM"
        - Key: "Application"
          Value: !Ref "GroupId"
  ECSCluster:
    Type: "AWS::ECS::Cluster"
    DeletionPolicy: "Delete"
    Properties:
      ClusterName: "ecscluster-test"
      ClusterSettings:
        - Name: "containerInsights"
          Value: "enabled"
      Tags:
        - Key: "Name"
          Value: "ecscluster-test"
        - Key: "Application"
          Value: !Ref "GroupId"
  ECSLogGroup:
    Type: "AWS::Logs::LogGroup"
    DeletionPolicy: "Delete"
    Properties:
      LogGroupName: "/aws/ecs/ecstask"
      RetentionInDays: 7
  ECSTaskExecutionRole:
    Type: "AWS::IAM::Role"
    DeletionPolicy: "Delete"
    Properties:
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: "Allow"
            Principal:
              Service: "ecs-tasks.amazonaws.com"
            Action: "sts:AssumeRole"
      ManagedPolicyArns:
        - "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy"
        - "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"
      Path: "/"
      Policies: 
        - PolicyDocument: {
          "Version": "2012-10-17",
          "Statement": [
              {
                  "Sid": "VisualEditor0",
                  "Effect": "Allow",
                  "Action": [
                      # SSM連携の自動処理に必要な権限
                      "ssm:CreateActivation",
                      "ssm:DeleteActivation",
                      "iam:PassRole"
                  ],
                  "Resource": "*"
              }
          ]
      }
          PolicyName: "AllowSSMOperation"
      RoleName: "ecstask-role"
      Tags:
        - Key: "Name"
          Value: "ecstask-role"
        - Key: "Application"
          Value: !Ref "GroupId"
  ECSTaskDefinition:
    Type: "AWS::ECS::TaskDefinition"
    DeletionPolicy: "Delete"
    Properties:
      ContainerDefinitions:
        - Name: "ecstask"
          Image: !Sub
            - "${RepositoryArn}:latest"
            - { RepositoryArn: !Ref "EcrRepositoryArn" }
          LogConfiguration:
            LogDriver: "awslogs"
            Options:
              awslogs-group: !Ref "ECSLogGroup"
              awslogs-region: !Ref "AWS::Region"
              awslogs-stream-prefix: !Ref "GroupId"
          MemoryReservation: 128
      Cpu: 1024 # 256, 512, 1024, 2048, 4096
      ExecutionRoleArn: !Ref "ECSTaskExecutionRole"
      TaskRoleArn: !Ref "ECSTaskExecutionRole"
      Family: "ecstask"
      Memory: 2048 # 512, 1024, 2048, 4096
      NetworkMode: "awsvpc"
      RequiresCompatibilities:
        - "FARGATE"
      Tags:
        - Key: "Name"
          Value: "ecstask"
        - Key: "Application"
          Value: !Ref "GroupId"
  ECSService:
    Type: "AWS::ECS::Service"
    DeletionPolicy: "Delete"
    Properties:
      Cluster: !Ref "ECSCluster"
      DesiredCount: 1
      LaunchType: "FARGATE"
      NetworkConfiguration:
        AwsvpcConfiguration:
           AssignPublicIp: "ENABLED"  # ECRへの接続のため、コンテナにパブリックIPをアタッチする必要がある
           SecurityGroups:
             - !Ref "ECSSecurityGroup"
           Subnets:
             - !Ref "EcsPublicSubnetIdAz1"
             - !Ref "EcsPublicSubnetIdAz2"
      PlatformVersion: "1.4.0"
      ServiceName: "ecsservice-test"
      Tags:
        - Key: "Name"
          Value: "ecsservice-test"
        - Key: "Application"
          Value: !Ref "GroupId"
      TaskDefinition: !Ref ECSTaskDefinition
  ECSSecurityGroup:
    Type: "AWS::EC2::SecurityGroup"
    DeletionPolicy: "Delete"
    Properties:
      GroupName: "ecsservice-test"
      GroupDescription: "for ecsservice-test"
      Tags:
        - Key: "Name"
          Value: "ecsservice-test"
        - Key: "Application"
          Value: !Ref "GroupId"
      VpcId: !Ref "VpcId"

AWSコンソールでCloudFormationの画面を開き、以下のようにパラメータを入力してスタックを作成します。
f:id:swx-iso:20210818104327j:plain
f:id:swx-iso:20210810211952j:plain

完成

しばらく経つとスタックの作成が完了してECSタスクが起動して、SSMにマネージドインスタンスが登録されていました。 f:id:swx-iso:20210818105848j:plainf:id:swx-iso:20210818105848j:plain (「関連付けのステータス」が失敗になっていますが、問題なく接続できます。)

停止すると登録解除されるようになっています。
ぜひ実際に手を動かしてご確認ください!
★リソースが立ち上げっぱなしだと料金がかかり続けるので、検証が終わったらスタックは削除しましょう★