AWS CopilotでECS on Fargate上にコンテナをデプロイしECS Execによるコマンドの実行やSession Managerによるポートフォワーディングを行う
awsリモートマシンで動作中のアプリケーションを調査する際に、非コンテナ環境であればsshdが動いていることが多いのでSSH接続してコマンドを実行することがあるが、 コンテナでは大抵動いていないので中に入れない。どうしても必要であれば動かすという手もあるがポートの開放や鍵の管理の点でなるべく避けたい。 今回はECS on Fargateのコンテナでsshdを動かすことなくコマンドの実行やポートフォワーディングを行う。
動作確認のためのアプリケーションは、App Runner や ECS on Fargate にアプリケーションをデプロイするためのCLI AWS Copilotでデプロイする。
AWS App Runnerの特徴と料金、CloudFormationのResource - sambaiz-net
ECS on EC2 には現状対応していないが、用途が合えばtypeに応じて必要なVPCやALBなどのリソースがまとめて作成されるので簡単に動かすことができて便利。また、CDKなどで作成しておいた既存のリソースを用いることもできる。
CDKでALBとECS(EC2)クラスタを作成し、ecs-cliでDocker Composeの構成をデプロイする - sambaiz-net
デプロイ
copilot initで環境を作成し –deploy で test environment にFargateによる Load Balanced Web Service をデプロイする。
$ brew install aws/tap/copilot-cli
$ copilot -v
copilot version: v1.19.0
$ copilot init --app testapp \
--name app \
--type 'Load Balanced Web Service' \
--dockerfile './Dockerfile' \
--deploy
...
✔ Deployed service app.
Recommended follow-up action:
- You can access your service at http://****.ap-northeast-1.elb.amazonaws.com over the internet.
- Be a part of the Copilot ✨community✨!
Ask or answer a question, submit a feature request...
Visit 👉 https://aws.github.io/copilot-cli/community/get-involved/ to see how!
$ copilot env ls
test
$ curl http://****.ap-northeast-1.elb.amazonaws.com/
ok
生成されるmanifestはこんな感じ。
$ cat copilot/app/manifest.yml
name: app
type: Load Balanced Web Service
http:
path: '/'
image:
build: Dockerfile
port: 8080
cpu: 256
memory: 512
count: 1
exec: true
ECS Execによるコマンドの実行
ECSには kubectl exec のようにコンテナ内でコマンドを実行できるECS Execという機能があってCopilotでは copilot svc exec で実行できる。ローカルマシンにSession Manager plugin for the AWS CLIが入ってない場合はインストールされる。
$ copilot svc exec --app testapp test --env test --name app
Looks like the Session Manager plugin is not installed yet.
Would you like to install the plugin to execute into the container? Yes
$ session-manager-plugin --version
1.2.339.0
コマンドを指定しないか /bin/sh を指定すればシェルを実行できる。
$ copilot svc exec --app testapp test --env test --name app --command /bin/sh
Execute `/bin/sh` in container app in task e136ade28eeb48c5ae69b3681e0e0741.
Starting session with SessionId: ecs-execute-command-0ff9d37f33292cbcd
/ # hostname
ip-10-0-0-147.ap-northeast-1.compute.internal
裏側ではSSMエージェントのバイナリがマウントされ Systems Manager (旧SSM) の Session Manager にセッションが作成される。
/ # yum -y install procps
/ # ps aux
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 0.0 1.1 2689044 47296 ? Ssl 03:31 0:02 java App
root 8 0.0 0.3 1323312 15552 ? Ssl 03:31 0:00 /managed-agents/execute-command/amazon-ssm-agent
root 35 0.0 0.7 1410172 28080 ? Sl 03:31 0:00 /managed-agents/execute-command/ssm-agent-worker
root 46 0.2 0.5 1327588 23196 ? Sl 04:10 0:01 /managed-agents/execute-command/ssm-session-worker ecs-execute-command-0893f9d72f4a53852
root 55 0.0 0.0 13564 3216 pts/0 Ss 04:10 0:00 /bin/sh
root 213 0.0 0.0 51672 3812 pts/0 R+ 04:17 0:00 ps aux
Session Managerによるポートフォワーディング
Session ManagerはSSHトンネルもサポートしておりポートを開放せずにSSH接続することもできるが、 AWS-StartPortForwardingSessionで start-session することで単体でポートフォワーディングできる。
$ aws ssm start-session --target [instance_id] --document-name AWS-StartPortForwardingSession --parameters '{"portNumber":["5005"], "localPortNumber":["5005"]}'
自分でインストールしたSSMエージェントを使う方法
SSMエージェントとAWS CLIをインストールする。
FROM amazoncorretto:18-al2-full AS builder
COPY . .
RUN javac -d ./out src/App.java
RUN yum install -y unzip \
&& curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip" \
&& unzip awscliv2.zip
FROM amazoncorretto:18.0.1-al2
# Install SSM Agent & jq
RUN yum install -y https://s3.amazonaws.com/ec2-downloads-windows/SSMAgent/latest/linux_amd64/amazon-ssm-agent.rpm jq \
&& rm -rf /var/cache/yum/* \
&& yum clean all
# Install AWS CLI
COPY --from=builder ./aws ./aws
RUN ./aws/install
COPY ./entrypoint.sh .
RUN chmod u+x ./entrypoint.sh
COPY --from=builder ./out .
EXPOSE 8080
CMD ["java", "App"]
entrypoint.sh では aws ssm create-activaton で アクティベーションを作成し、 amazon-ssm-agent -register でSystems Managerにインスタンスを登録して 終了時に deregister-managed-instance している。
$ cat entrypoint.sh
#!/bin/bash -e
SERVICE_NAME_FULL="${COPILOT_ENVIRONMENT_NAME}_${COPILOT_APPLICATION_NAME}_${COPILOT_SERVICE_NAME}"
ASSUME_ROLE=$(aws sts assume-role --role-arn "${ACTIVATION_ROLE_ARN}" --role-session-name "${SERVICE_NAME_FULL}")
export AWS_ACCESS_KEY_ID=$(echo $ASSUME_ROLE | jq -r .Credentials.AccessKeyId)
export AWS_SECRET_ACCESS_KEY=$(echo $ASSUME_ROLE | jq -r .Credentials.SecretAccessKey)
export AWS_SESSION_TOKEN=$(echo $ASSUME_ROLE | jq -r .Credentials.SessionToken)
echo "- create SSM activation"
ACTIVATION=$(aws ssm create-activation \
--default-instance-name "${SERVICE_NAME_FULL}" \
--description "${SERVICE_NAME_FULL} service in ECS on Fargate" \
--iam-role ${SSM_ROLE_NAME})
ACTIVATION_CODE=$(echo $ACTIVATION | jq -r .ActivationCode)
ACTIVATION_ID=$(echo $ACTIVATION | jq -r .ActivationId)
echo "- register instance"
amazon-ssm-agent -register -code "${ACTIVATION_CODE}" -id "${ACTIVATION_ID}" -region ap-northeast-1 -y
cleanup() {
echo "- cleanup"
MANAGED_INSTANCE_ID=$(cat /var/lib/amazon/ssm/registration | jq -r .ManagedInstanceID)
if [ -n "${MANAGED_INSTANCE_ID}" ]; then
echo "- deregister instance ${MANAGED_INSTANCE_ID}"
aws ssm deregister-managed-instance --instance-id "${MANAGED_INSTANCE_ID}" || true
fi
if [ -n "${CHILD_PID}" ]; then
echo "- kill the child process ${CHILD_PID}"
kill "${CHILD_PID}"
wait "${CHILD_PID}"
fi
}
trap "cleanup" SIGTERM
nohup amazon-ssm-agent &
# for troubleshooting
# sleep 30
# cat /var/log/amazon/ssm/amazon-ssm-agent.log || true
# cat /var/log/amazon/ssm/errors.log || true
echo "- execute $* as a child process"
sh -c "$*" &
CHILD_PID=$!
wait "${CHILD_PID}"
create-activation で渡すroleは addons/ にCloudFormationのテンプレートを置くことでデプロイ時に作成される。 Parameters には App, Env, Name を含める必要がある。 また、Outputした値は SSM_ROLE_NAME のようなcapital snake caseの環境変数で参照できる。
$ tree copilot/
copilot/
└── app
├── addons
│ └── ssm-role.yaml
└── manifest.yml
$ cat copilot/app/addons/ssm-role.yaml
AWSTemplateFormatVersion: 2010-09-09
Parameters:
App:
Type: String
Description: Your application's name.
Env:
Type: String
Description: The environment name your service, job, or workflow is being deployed to.
Name:
Type: String
Description: The name of the service, job, or workflow being deployed.
Resources:
SSMRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Principal:
Service: [ 'ssm.amazonaws.com' ]
Action: [ 'sts:AssumeRole' ]
Condition:
StringEquals:
aws:SourceAccount: !Ref AWS::AccountId
ArnEquals:
aws:SourceArn: !Sub arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:*
ManagedPolicyArns:
- arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore
- arn:aws:iam::aws:policy/AmazonSSMDirectoryServiceAccess
- arn:aws:iam::aws:policy/CloudWatchAgentServerPolicy
ActivationRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Principal:
AWS: [ !Ref AWS::AccountId ]
Action: [ 'sts:AssumeRole' ]
Policies:
- PolicyName: SSMRolePassPolicy
PolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Action:
- iam:PassRole
Resource: !GetAtt SSMRole.Arn
- Effect: Allow
Action:
- ssm:CreateActivation
- ssm:DeregisterManagedInstance
Resource: '*'
Outputs:
SSMRoleName:
Description: The SSM role name passed to create-activation
Value: !Ref SSMRole
ActivationRoleArn:
Description: The role arn to create activation and pass the SSM role
Value: !GetAtt ActivationRole.Arn
create-activation 時に必要な iam:PassRole 権限は次のような AWS::IAM::ManagedPolicy を作成してOutputすることでTaskRoleに付加することもできるが、 DenyIAMExceptTaggedRoles というPolicyによって iam:* がDenyされてしまうためRoleを作成してassumeしている。 ここで作成したRoleにはアプリケーション名のタグが追加されassumeできるようになっている。
SSMRolePassPolicy:
Type: AWS::IAM::ManagedPolicy
Properties:
PolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Action:
- iam:PassRole
Resource: !GetAtt SSMRole.Arn
ローカル実行時に問題にならないようにENTRYPOINTはmanifestのentrypointで設定している。 またSSMエージェントが衝突するのを回避するためECS Execを無効にしている。
$ cat copilot/app/manifest.yml
...
entrypoint: ["./entrypoint.sh"]
command: ["java", "App"]
exec: false
デプロイ後 PingStatus が Online になっていれば成功。 権限が足りなかったりすると ConnectionLost になるのでその場合はSSMエージェントのログを確認すると良い。
$ aws ssm describe-instance-information
{
"InstanceInformationList": [
{
"InstanceId": "mi-078951953b63f94d0",
"PingStatus": "Online",
"LastPingDateTime": "2022-07-21T20:32:05.949000+09:00",
"AgentVersion": "3.1.1575.0",
"IsLatestVersion": false,
"PlatformType": "Linux",
"PlatformName": "Amazon Linux",
"PlatformVersion": "2",
"ActivationId": "842e7174-729e-4dba-a7ba-7caf293ebf28",
"IamRole": "testapp-test-app-AddonsStack-1JJP0GYRGTT98-SSMRole-MASCURUWZ85I",
"RegistrationDate": "2022-07-21T20:23:30.200000+09:00",
"ResourceType": "ManagedInstance",
"Name": "test_testapp_app",
"IPAddress": "10.0.1.220",
"ComputerName": "ip-10-0-1-220.ap-northeast-1.compute.internal"
}
]
}
このインスタンスはオンプレミスとして扱われるので start-session するには Instance Tier を Advance にする必要があり、 変更するとインスタンスごとに時間あたりの料金が発生するようになる。
$ aws ssm start-session --target mi-078951953b63f94d0 --document-name AWS-StartPortForwardingSession --parameters '{"portNumber":["8080"], "localPortNumber":["5000"]}'
$ curl localhost:5000
ok
ECS ExecのSSMエージェントを使う方法
実はECS Execで作成されるsessionのtargetに直接 start-session することもできる。
$ aws ssm start-session --target ecs:testapp-test-Cluster-VHaYIQCdoUj8_c0add05ab98c49d798ba1cb515c9940d_c0add05ab98c49d798ba1cb515c9940d-527074092 \
--document-name AWS-StartPortForwardingSession --parameters '{"portNumber":["8080"], "localPortNumber":["5000"]}'
$ curl localhost:5000
ok
後片付け
不要なら環境を削除する。
$ copilot app delete testapp
参考
AWS Fargateで動いているコンテナにログインしたくて Systems Manager の Session Manager を使ってみた話 - SMARTCAMP Engineer Blog