3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

SlackからRailsアプリケーションをAWS ECS Fargateへデプロイする手順

Last updated at Posted at 2023-12-12

はじめに

本記事では、Ruby on Railsで構築したアプリケーションをSlack経由でデプロイする方法について紹介します。

従来の方法では、AWSマネジメントコンソールから直接CodeDeployを実行したり、インスタンスにSSHで接続してコマンドを実行する必要がありました。
これは、新しく参加するメンバーやQAエンジニアにとっては複雑で手間のかかる手順でした。

そこで、より簡単かつ効率的にデプロイを行えるよう、Slackを利用したデプロイ方法に構成を変更しました。

はじめに、完成した構成の概要を以下に図示します。

image.png

左側から順に、以下のフローが取られます。

  1. Slackからチャット形式でデプロイコマンドを投稿
  2. AWS Lambdaに適用したSlack boltがデプロイコマンドを処理
  3. Fargateのタスクを置換

以降の項目では、各レイヤーの構成について説明します。

Railsアプリケーションの構成について

赤枠で示された部分の、Railsアプリケーションの構成について説明します。
image.png

このセクションでは、RailsアプリケーションがAWS Fargateにデプロイされる構成について説明します。
Fargateのタスク定義ではAmazon ECRを参照しており、Railsアプリケーションのイメージをpushしています。
Fargateではローリングアップデートを採用し、タスク定義を更新することでデプロイを実現しています。

Railsの基本的な使い方については割愛します。

Slack boltの設定と開発

次に、Slackの設定について説明します。
image.png

Slack Appの設定は以下のリンクを参照して行ってください。

Lambda, Boltのデプロイ処理

image.png

初回設定

下記を参考にLambdaにBoltを構築します。

Lambdaの前面にはApi Gatewayを設置し、Slack Appとの疎通を確認してください。

デプロイコマンドの処理

Slackから受け取ったデプロイコマンドを処理します。
次のようなテキストを受け取る想定です。

コマンド形式:

  • @release [command] [project] [target] [image-tag]

コマンド例:

  • @release deploy hogehoge stage hogehoge-1234-branch-name
  • @release deploy piyopiyo production v1.0

コマンド形式の補足です。
@ release ... Slackアプリ名
deploy ... コマンド種別
project ... 複数のプロジェクトが存在するため、プロジェクト名を明示的に指定
target ... 検証環境、本番環境を指定
image-tag ... Amazon ECRにpushしたイメージのタグ名を指定

続いて、コード例を説明します。

Slack BoltとAWS SDKの初期設定

const { App, AwsLambdaReceiver } = require('@slack/bolt');
const AWS = require('aws-sdk');

const awsLambdaReceiver = new AwsLambdaReceiver({
  signingSecret: process.env.SLACK_SIGNING_SECRET
});
const app = new App({
  token: process.env.SLACK_BOT_TOKEN,
  receiver: awsLambdaReceiver
});

Slackのリトライ処理の無視

Slackんpリトライ処理を無視しています。
Slack apiは3秒以内にレスポンスを返さないとリトライする仕様になっていますが
Lambdaの場合は3秒以内にレスポンスを返すこと保証できないため、リトライ処理を明示的に無視しています。

app.use(async ({ context, next, ack }) => {
  if (
    context.retryNum &&
    context.headers['X-Slack-Retry-Reason'] === 'http_timeout'
  ) {
    await ack?.();
    return;
  }

  await next();
});

メンションイベントの処理

Slackからアプリがメンションされた際に動作する処理を記載します。

app.event('app_mention', async ({ event, say }) => {
  try {
    const { channel, event_ts, text } = event;
    const message = await handleCommand(event, say);

    await say({
      channel,
      thread_ts: event_ts,
      text: "say :wave: \n> command: " + text + "\n> messages: " + message
    });
  } catch (error) {
    console.error(error);
  }
});

AWS Lambda ハンドラ

AWS Lambdaのエントリポイント部分です。

exports.lambda_handler = async (event, context, callback) => {
  const handler = await awsLambdaReceiver.start();
  return handler(event, context, callback);
};

コマンド処理

メンションを受け取ったあとのコマンドを処理します。
ここでは、コマンドの解析、認証、ECSとECRへの通信、タスク定義の更新などの処理を行います。
エラーハンドリング等の処理は省略しています。

async function handleCommand(event, say) {
  console.log(event);
  const { channel, event_ts, text, user } = event;
  // @ release [command] [project] [target] [image-tag]
  // 例) @ release deploy projectA stage hogehoge-image
  const textWithoutMention = text.replace(/^<@(.+?)>/, '').trim();
  const [command, project, target, imageTag] = textWithoutMention.split(' ');

  let message = '';

  // チャンネルごとに実行できるユーザーとコマンドを指定したホワイトリスト
  const allowUsersAndCommands = {
    'AAAAAAAAAA': { // #projectA-stage
      users: [
        'XXXXXXXXX',   // @ userA
        'YYYYYYYYY',   // @ userB
        'ZZZZZZZZZ',   // @ userC
      ],
      commands: {
        'deploy': ['stage', 'stage2'],
      },
    },
    'BBBBBBBBBB': { // #projectA-production
      users: [
        'XXXXXXXXX',   // @ userA
      ],
      commands: {
        'deploy': ['production'],
      },
    }
  };

  const ecs = new AWS.ECS();
  const ecr = new AWS.ECR();

  switch (command) {
    case 'deploy':
      await say({
        channel,
        thread_ts: event_ts,
        text: 'デプロイを開始します🚀',
      });
      switch (project) {
        case 'projectA':
          switch (target) {
            case 'stage':
            case 'stage2':
              const taskDefinitionName = `projectA-${target}-ecs-task-def`;
              const clusterName = `projectA-${target}-ecs-cluster`;
              const serviceName = `projectA-${target}-ecs-service`;

              // ECSタスク定義を更新する
              const taskDefinition = await ecs.describeTaskDefinition({
                taskDefinition: taskDefinitionName,
              }).promise();
              
              const newTaskDefinition = taskDefinition.taskDefinition;
              const describeImages = await ecr.describeImages({
                repositoryName: 'aaaaaaaaaaaaaaa',
                registryId: 'zzzzzzzzzzzzzz',
              }).promise();
              const imageTags = describeImages.imageDetails.map(image => image.imageTags).flat();

              newTaskDefinition.containerDefinitions[0].image = `zzzzzzzzzzzzzz.dkr.ecr.ap-northeast-1.amazonaws.com/aaaaaaaaaaaaaaa:${imageTag}`;

              // newTaskDefinitionのうち、不要項目はバリデーションに引っかかるため削除する
              delete newTaskDefinition.taskDefinitionArn;
              delete newTaskDefinition.status;
              delete newTaskDefinition.revision;
              delete newTaskDefinition.requiresAttributes;
              delete newTaskDefinition.compatibilities;
              delete newTaskDefinition.registeredAt;
              delete newTaskDefinition.registeredBy;

              // タスク定義を登録する
              await ecs.registerTaskDefinition(newTaskDefinition).promise();

              // ECSサービスを更新する
              await ecs.updateService({
                cluster: clusterName,
                service: serviceName,
                taskDefinition: `arn:aws:ecs:ap-northeast-1:zzzzzzzzzzzzzz:task-definition/${taskDefinitionName}`
              }).promise();

              // ECSサービスが完全に更新されるまで待機する
              await ecs.waitFor('servicesStable', {
                cluster: clusterName,
                services: [serviceName],
              }).promise()
              message = 'デプロイが完了しました🎉';
              break;

~~~~省略~~~~~~

まとめ

Slack boltを使ってチャット形式でデプロイする構成ができました。
新規で参加していただくメンバーやQAエンジニアの方でも手軽にデプロイできる環境になり、CD面での作業が高速化しました。
今後はdeployコマンド以外の拡張や、各プロジェクトに適用、展開を考えています。

3
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?